House of botcake与IOFILE任意读写
利用条件与效果及使用范围
应该是在libc2.31这个手法要的条件就是两个:UAF与申请足够数量的堆块。而效果就是:任意地址写,配合对IOFILE结构体的利用可以实现任意读,结合起来就可以任意读写。主要思路就是绕过tcache的double free检测即tcache key字段来完成攻击。虽然自有tcache机制以来到现在都可以使用,不过因为之前tcache检测很弱直接double free也没有影响,所以主要还是glibc2.29-至今用的比较多。但因为在libc2.32加入了safelinking机制,所以在2.32及之后这个手法就还是回归正常tcache attack了,没有任意读写这么强了,得靠其他漏洞的提前泄露。
攻击思路及流程
主要流程就是先申请7个同一大小的堆块,再申请两个相同大小的堆块,前申请的我们不妨叫堆块a,后申请的叫堆块b,再申请一个堆块防止合并。
释放申请的9个堆块,其中7个堆块会填满tcache对应大小的链表,此时因为tcache已满,我们再释放相同大小的堆块就会进入unsortedbin。
同时,因为unsortedbin的堆块如果物理相邻就会合并,我们需要让堆块a和b合并。合并后我们再申请一个相同大小的堆块,由于系统会首先遍历tcache链表,如果能找到就会在tcache里取堆块。所以会从tcache里取一个堆块出来,此时我们利用UAF漏洞再free一次堆块b,这样堆块b就又在tcache又在unsortedbin了。
为什么能free?实际上free一个指针应该只会检测这个指针+*(这个存放这个指针的地址-8)的地址的p标志位是不是1 |
以及这个指针+*(这个存放这个指针的地址-8)的地址的下一个地址的p标志位是不是1。 |
文字描述太抽象了,比如有UAF,我们直接在链表里找出来那个地址。 |
这个地址一般就是我们写入数据的地址,我们用这个地址减0x10,再用heap 这个地址 |
这个命令看一下堆块的完整性即可。 |
此时如果我们申请比堆块a大小大一点的堆块,由于在tcache里找不到,系统就会在unsortedbin里切割,此时就会切割我们a,b合并后的堆块了。此时我们就完成了overlapping,可以改在tcache里的堆块b的fd指针实现任意地址写了。如果我们不这样申请,而是多申请几次,申请的堆块的总大小等于堆块a的大小,就可以在堆块b的fd指针里留下main_arena相关的地址,而这个地址又和libc相关。只要我们再继续切割unsortedbin不就可以改堆块b的fd指针吗,只要爆破两位就可以构造出指向IOFILE结构体的指针,进而完成任意读写。
IOFILE任意读写
目前我感觉我还是不太有能力结合源码分析,各位如果想看结合源码分析可以看raycp师傅的文章IO FILE 之任意读写,写的非常好。本文大部分也借鉴了其中的内容,不过我是以比较宏观的视角来分析的,可能比较好理解一点。不过源码肯定要看,之后我也会结合源码讲讲。
先回顾一下IOFILE结构体
0x0 _flags |
0x8 _IO_read_ptr |
0x10 _IO_read_end |
0x18 _IO_read_base |
0x20 _IO_write_base |
0x28 _IO_write_ptr |
0x30 _IO_write_end |
0x38 _IO_buf_base |
0x40 _IO_buf_end |
0x48 _IO_save_base |
0x50 _IO_backup_base |
0x58 _IO_save_end |
0x60 _markers |
0x68 _chain |
0x70 _fileno |
0x74 _flags2 |
0x78 _old_offset |
0x80 _cur_column |
0x82 _vtable_offset |
0x83 _shortbuf |
0x88 _lock |
0x90 _offset |
0x98 _codecvt |
0xa0 _wide_data |
0xa8 _freeres_list |
0xb0 _freeres_buf |
0xb8 __pad5 |
0xc0 _mode |
0xc4 _unused2 |
0xd8 vtable |
我们知道,除了进行系统调用的输入/输出read,write这些,其他的输入/输出都会用到IOFILE结构体,即三个输入/输出流中,即stdin(标准输入),stdout(标准输出),stderr(标准错误)。而我们输入/输出一般都是缓冲输入/输出。我们可以这些流中的结构体成员起到任意读写的效果。
比如stdin,我们就是利用了改变输入缓冲区的地址,来把我们本应该输入到输入缓冲区的数据,先输入到了我们要输入的目标地址,即控制stdin的_IO_buf_base与 _IO_buf_end。
Stdin利用流程
当然我们利用肯定是有条件的:
- 条件第一个就是输入缓冲区要没有数据即_IO_read_end —— _ IO_read_ptr >0
- 条件第二个就是flags字段表明这地方可写,即flag里没有不可写的位标志,其中c语言定义了#define _IO_NO_READS 4,也就是我们的flags字段&0x4要为0,即设置flags&~0x4
- 条件第三个我们输入的文件描述符,如果想用键盘输入就要把_fileno设为0
- 条件第四个就是 _IO_buf_base设置成我们写入的地址, _IO_buf_end设置成我们写入结束的地址。且输入缓冲区的大小即 _IO_buf_end- _IO_buf_base要大于我们输入的数据的大小。
这样我们就可以利用stdin进行任意写了。当然其实实际上一般我们很多条件本来就会满足,我们改的时候注意一下这些条件即可一般就改 _IO_buf_base与 _IO_buf_end就可以了。
Stdout利用流程
任意写
由于stdout即有把输出缓冲区的数据输出出来,又有把数据写入输出缓冲区,所以利用stdout可以进行任意读写。但是这个任意写需要我们能控制输出的数据,并不能直接从键盘上读取数据。实际利用的话把_IO_write_ptr(输出缓冲区的开始)改成我们想写的地址的开始,把 _IO_write_end(输出缓冲区的结束)改成我们想写地址的结束即可。
任意读
跟stdin类似,我们攻击的是输出缓冲区,利用输出时如果系统检测到输出缓冲区有数据就会刷新输出缓冲区的机制,把 _IO_write_base改成我们想泄露数据的开始,把 _IO_write_end改成我们想泄露数据的结束即可。
条件就是:
- flags标志不能有_IO_NO_WRITES标志,即flags&0x8必须为0。可以设置flags&~0x8
- flags标志最好有_IO_CURRENTLY_PUTTING标志,因为不然会多一些操作,让流程不可控,可以设置flag | 0x800
- 设置_fileno为1
- 设置_IO_read_end等于 _IO_write_base 或设置 _flag & _IO_IS_APPENDING即 _flag | 0x1000。
- 把 _IO_write_base改成我们想泄露数据的开始,把 _IO_write_ptr改成我们想泄露数据的结束即可。
下面我们看例题
2026ISCC线上挑战赛决赛——note
这个比赛如何暂且不骂了,这个远程靶机也先不骂了。单单看这个题还是出的可以的。题开了pie,开了沙箱,只允许ORW,glibc给的版本是2.31,我们当2.31打吧。远程好像不是这个版本(我***)
逻辑就是普通的堆菜单题有edit功能,但是没有show功能,不能输出数据。有UAF,索引可以申请0-0x15,但索引可以复用。所以我们可以打House of botcake,然后攻击stdout泄露出libc此时就有两种思路了。
第一种是:我们之前在打stdout的时候已经有了攻击stdout的伪造的堆块,所以我们可以用edit功能一直打stdout。这样我们就可以利用stdout读环境变量泄露栈地址打栈上rop。当然没有edit也影响不大,因为索引可以复用,再打一遍House of botcake也一样。
第二种是:因为版本是2.31,我们可以打__free_hook,把freehook改成magic gadget,然后利用setcontext调用实现栈迁移,把栈迁移到freehook来,并且利用setcontext调用read往freehook上写rop链实现ORW。
这两种都可以,这里要泄露堆地址个人感觉不是很方便,所以就没想过写shellcode的。用mprotect把freehook附近的地址改成可执行然后在上面填shellcode感觉应该也可以。
第一种的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('39.96.193.120',10011) |
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)) |
sd = lambda s : p.send(s) |
sdr = lambda s : p.send(str(s).encode()) |
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 add(s,a,d): |
ru(b"Choice: ") |
sdr(1) |
ru(b"Index: ") |
sdr(s) |
ru(b"Size: ") |
sdr(a) |
ru(b"Content: ") |
sd(d) |
def free(s): |
ru(b"Choice: ") |
sdr(2) |
ru(b"Index: ") |
sdr(s) |
def edit(s,a): |
