格式化字符串利用方式
参考资料:https://www.yuque.com/cyberangel/rg9gdm/vh62v6#4W6x9
格式化字符串漏洞的原理
格式化字符串漏洞本质上是程序没有正确处理格式化字符串参数导致的安全问题。简单来说:
• 编程语言中(如 C/C++)的printf、sprintf等函数支持格式化输出(比如%s输出字符串、%d输出数字)。
• 正常用法是:printf(“%s,%d”, 名字, 年龄)第一个参数是固定的格式化模板,后续是对应的数据参数。
tip):%d(打印一个整型),%s 字符串(打印一个字符串),%c(打印一个字符),%x(以十六进制打印)
为了理解格式化字符串漏洞,我们以 printf 为例进一步理解格式化字符串的调用方法。
正常的 printf 函数的结构是格式化字符串和参数一一对应的,那么在调用 printf 函数的时候参数从右向左依次进栈,进栈之后格式化字符串实在栈顶的位置。
那么在进入 printf 之后,函数会首先获取第一个参数,也就是栈顶的格式化字符串,如果当前字符串不包含%,那么就直接输出,就比如 printf(“hello world!”);,如果包含%就会寻找与之对应的字符。
那如果缺少参数呢?比如 printf(“ Color%s, Number%d, Float%4.2 “);
此时可以发现并没有提供参数,那么程序怎么运行呢?
程序会继续运行,将栈上储存的格式化字符串地址上面的变量分别解析为:其地址对应的字符串,其内容对应的整型值,其内容对应的浮点值。这就会造成格式化字符串漏洞。
解释%n$s,%n$x,%n$p,%n$n
%n$x(读取第 n 个值,以十六进制显示)
直接跳到栈上或寄存器中的第 n 个参数位置,把那个位置上的数据当作一个十六进制整数打印出来
%5$x 就是直接打印栈上的第 5 个参数的十六进制值。
%n$p(读取第 n 个值,以指针形式显示)
直接跳到第 n 个参数位置,把那个数据当作一个内存地址(指针)打印出来。它和 %n$x 类似,但通常会自动加上 0x 前缀,并且会根据系统架构补齐位数,看起来更直观。
%10$p 会直接打印第 10 个参数的地址格式。
%n$s(任意地址读取的核心)
找到栈上的第 n 个参数,把这个参数的值当作一个内存地址,然后去那个地址里读取字符串内容,直到遇到字符串结束符 \0 为止。
如果能控制第 n 个参数的值变成他们想要的地址,再调用 %n$s,就能读取程序内存中任意地址的敏感信息。
格式化字符串漏洞的利用方法
许多程序员有时为了方便就会这样子输出:
1 |
|
不过事实上这是一种十分危险的写法,当第一个参数可被控制时,攻击者将有机会对任意内存地址进行读写操作,这就是格式化字符串漏洞。gcc 编译一下程序,用 gdb 调试看一下栈中的情况。
在 read 函数处输入aaaa%x %x %x %x %x %x %x %x %x %x %x,回撤,看一下栈情况
运行程序和 gdb 中栈情况做个对比,发现程序运行输出后就是栈中的内容,因此我们可以通过叠加%x 或者%p 的方式去获取有限范围内的栈数据。
由此我们可以做到任意地址读:%n$x 可以直接用来获取栈中被视为第 n+1 个参数的值。
任意地址读(泄露栈内容)
依旧以上面的程序为例,我们找到字符串在栈中的相对偏移是第七个
假设我们想要泄露 0x804023 的内容,我们就可以写 p32(0x804023)+”%7$s”
任意地址写
和任意地址读的方法一样,不过是把%s 换成了%n。我们可以通过下面一个例子来了解:%n$n
1 |
|
对于这个来说%n取值的是%n之前的字符个数,但是如果%n重复出现,先前的%n的个数不当做字符串计入。
形如**%123c%n**可以拆分成%123c和%n,%123c会存入123个c(‘a’)对应的字符,对应的c的值也就是123
比如同样偏移是 7,我们如果想要修改 0x804023 的内容 为 50,我们就可以写 p32(0x804023)+b”%50c%7$n”
问题来了:当我们想要把某地址修改为我们想要的地址比如 0x804023==>0x804223 呢,这个我们就可以使用 pwntools 去修改地址,在 pwntools 中我们有专门针对格式化字符串漏洞的工具。
pwntool帮助修改任意地址
1 | fmtstr_payload(num,{x_addr:str_addr}) |
这样子就可以修改地址为我们想要的地址从而获取 shell 了。
_BUUCTF[ jarvisoj_fm ] _
我们通过一道 BUUCTF 基础格式化字符串漏洞来练手,虽然开了 Canary,不过学不学没有影响,直接绕过。
1 | int __cdecl main(int argc, const char **argv, const char **envp) |
通过分析 IDA 我们可以直接直接确定是一个格式化字符串漏洞,核心是要想办法使 x 的值为 4。
我们首先运行程序寻找偏移:发现偏移是 11(正好卡到 )
剩下的就很好说了,找到print出x的函数地址,直接修改其内容为 4 即可通过接下来的 if 语句判断获取 shell。
直接构造 exp,注意这里 p32(addr)原本就是四个字节,相当于是%11$n 前面有四个字符串,直接修改值为 4 了
1 | from pwn import * |
BUUCTF [ wdb_2018_2nd_easyfmt ] (格式化字符串+ret2libc)
趁热打铁再来一道 BUUCTF 的格式化字符串漏洞题目
1 | int __cdecl __noreturn main(int argc, const char **argv, const char **envp) |
思路很清晰,就是格式化字符串漏洞,不过在 IDA 里找了找并没有有关后门的信息。考虑 ret2libc,查看 got 表
由于是一个程序是一个无限循环的函数,我们可以通过利用 printf 的 got 表来寻找 libc 进而构造 exp
更新: 2026-04-10 16:35:23
原文: https://www.yuque.com/idcm/wnemg9/qxgvpx9prk53uc1q