关于printf—可变参数函数的参数寻找
由printf函数的可变参数,很容易得出,printf是一个stdcall的函数,由调用者维护栈帧平衡,那在printf中寻找其参数原理如下:
- printf第一个参数在调用printf时必定位于栈顶;
- printf根据其第一个参数,即字符串指针,对其指向的缓冲区进行扫描,每当扫描到一个%,就根据其后续字符判断是否为格式化控制字符;
- 根据每一个格式化控制字符在字符串缓冲区后进行参数读取;
printf格式化控制字符
- %d 用于读取10进制数值
- %x 用于读取16进制数值
- %p 用于输出16进制数值
- %s 用于读取字符串值
- %n 将对应的栈内容当指针,向其地址写入已打印的字符数目
格式化字符串漏洞原理
由上述printf函数的寻参过程printf无法知道栈上是否放置了正确数量的变量,如果没有足够的变量可供操作,而指针按正常情况下递增,就会产生越界访问。如果在第一个参数,字符串缓冲区中加入大量的控制字符,便可以轻松突破其参数限制,而打印出栈上的数据,以此便可以实现栈上的任意位置读和写。
"%x"-可以十六进制打印出指针后的数值;
"%n"-将前面的所有字符串写入栈的对应位置内容指向的内存
这两个控制字符便是实现任意位置读写的关键。同时,printf还有一个有意思的符号:$,可以用于控制符中直接跳过指定数量的栈区域,如%{n}$x
即会打印出栈上第n个参数的内容;%{n}$n
会直接跳跃到第n个栈区,将其作为地址写入对应内存。
如果字符串为%4$p
,则会打印出0x41414141,而如果字符串为%4$n
,则会将0x41414141当作一个地址,向其中写入已打印的字符数。而此内容位于buff中,可由我们自己构造,也就意味着,如果想向某个地址写入内容,则通过特殊构造,将buff对应位置写入目标地址,即可实现任意地址写。
对于写任意内容的话,由于%n写入的是打印的字符数,也就是说,如果字符串为%1234x%4$n
,则会向第四个栈区对应内存位置写入1234,
至此,得以实现任意地址的读和任意地址写任意值。
常规题目解法
复杂变体
更多的情况参考ctf-wiki fmtstr