关于格式化字符串漏洞
很早前就学习了格式化字符串漏洞,今天做题又忘了...记录一下
- 格式化字符串原理
格式化字符串基本格式如下
%[parameter] [flag] [field width] [.precision] [length] type
parameter
n$,他的含义是获得指定位置(也就是n)的参数。比如int a=0x11,b=0x22,c=0x33;printf('%3$p',a,b,c);
field width
输出的最小宽度,常用于补充以达到指定长度
precision
输出的最大长度
length
输出的长度
hh,输出一个字节
h,输出一个双字节
type
%d - 包含正负号的十进制数(负数、0、正数)
%u - 无符号十进制整数
%s - 字符串
%n - 不输出字符,但将%n之前打印出来的字符个数,赋值给一个变量 (通过%n就可以修改内存中的值)
%p - 输出对应变量的值。printf("%p",a) 用地址的格式打印变量 a 的值,printf("%p", &a) 打印变量 a 所在的地址。
附加的格式值。必需放置在 % 和字母之间(例如 %.2f):
- (在数字前面加上 + 或 - 来定义数字的正负性。默认情况下,只有负数才做标记,正数不做标记)
' (规定使用什么作为填充,默认是空格。它必须与宽度指定器一起使用。例如:%'x20s(使用 "x" 作为填充))
- (左调整变量值)
[0-9] (规定变量值的最小宽度) .[0-9] (规定小数位数或最大字符串长度)
printf函数运行大致流程
格式化字符串在进入printf函数之后,函数首先会获得第一个参数,也就是格式化字符串,依次读取格式化字符串中的每一个字符,如果该字符是%,则继续读取下一个非空字符,获取对应的参数解析并输出;如果不为%,则是直接输出。
附一张经典的图,如下

其栈上布局如下:
some value
3.14
123456
addr of "red"
addr of format string : " Color %s, Number %d, Float %4.2f" #参数倒序压入栈中
如果程序写成了 printf("Color %s, Number %d, Float %4.2f"),此时可以发现并没有提供参数,那么程序如何运行呢?程序会照样运行,会将栈上存储格式化字符串地址上面的三个变量分别解析为:
解析其地址对应的字符串
解析其内容对应的整形值
解析其内容对应的浮点值
借用师傅们的图片


stack中我们发现,首先入栈的依旧是格式化字符串,但是上面三个参数不再是之前的那几个了。
- 漏洞利用
一.泄露栈内存- 获取某个变量的值 (%s)
这里使用ctf wiki上面的例子
l
include <stdio.h>
int main() {
char s[100];
int a = 1, b = 0x22222222, c = -1;
scanf("%s", s);
printf("%08x.%08x.%08x.%s\n", a, b, c, s);
printf(s);
return 0;
}
(s输入%08x.%08x.%08x)

可以看出,此时此时已经进入了 printf 函数中,栈中第一个变量为返回地址,第二个变量为格式化字符串的地址,第三个变量为 a 的值,第四个变量为 b 的值,第五个变量为 c 的值,第六个变量为我们输入的格式化字符串应的地址。继续运行程序,按c

将会把上图中0xffffcf44及其后面两个地址包含的内容输出输出:

并不是每次得到的结果都一样 ,栈上的数据会因为每次分配的内存页不同而有所不同
2.获取栈指定变量值
可以使用%n$x获得栈上第n+1个参数,格式化字符串是第一个参数,那么如果想获得printf的第n个参数,就需 要加1.
如,我想获得第三个参数值f7e946bb,那么我就输入%3$x
3.获取对应字符串:%s
4.获取数据:%p - 获取某个变量的值 (%s)
二. 泄露任意地址内存
可以看出,在上面无论是泄露栈上连续的变量,还是说泄露指定的变量值,我们都没能完全控制我们所要泄露的变量的地址。这样的泄露固然有用,可是却不够强力有效。有时候,我们可能会想要泄露某一个 libc 函数的 got 表内容,从而得到其地址,进而获取 libc 版本以及其他函数的地址,这时候,能够完全控制泄露某个指定地址的内存就显得很重要了。那么我们究竟能不能这样做呢?自然也是可以的啦。
1
.#include <stdio.h>
int main() {
char s[100];
int a = 1, b = 0x22222222, c = -1;
scanf("%s", s);
printf("%08x.%08x.%08x.%s\n", a, b, c, s);
printf(s);
return 0;
}
scanf接收入s的值,然后两个printf。这里我们输入%s,如下调试,打印出0xff007325, 就是%s对应的字符串值,所以,输出函数的栈分布,栈上的第一个参数就是格式化字符串的地址。

这就意味着格式化字符串内容可控,同时,还需要注意的是,第一个参数虽然放置的是格式化字符串的地址,但是,输出函数并没有在这里开始调用,你也可以从上图中看到,在0xffffcf50处,又有一个%s,这里才是调用格式化字符串的时候,输出格式化字符串表达的内容时刻。这就意味着,因为格式化字符串我们可以自己控制,那么,如果我格式化字符串里面包含了%s,它会输出%s对应地址(0xff007325)所包含的内容,如果包含scanf@got, 它会输出scanf@got对应地址包含的内容,也就是scanf的真实地址。
总结:1、格式化字符串可以按照自己的意愿输入。2、格式化字符串的地址为栈上的第一个参数,顺序之后的某个位置会调用这个格式化字符串,以格式化字符串的内容输出内容。
所以,我们只要知道,调用这个格式化字符串的位置就可以了。
根据CTF WIKI上的说明方案,我们可以使用下面的字符来确定格式化字符串在哪调用:
[tag]%p%p%p%p%p%p...
一般来说,我们会重复某个字符的机器字长来作为 tag,而后面会跟上若干个 %p 来输出栈上的内容,如果内容与我们前面的 tag 重复了,那么我们就可以有很大把握说明该地址就是格式化字符串的地址,之所以说是有很大把握,这是因为不排除栈上有一些临时变量也是该数值。一般情况下,极其少见,我们也可以更换其他字符进行尝试,进行再次确认。

AAAA 0XFFD2RC30 0XC2 0XF7E596BB 0X41414141 0X702570250
我们调试看一下:
我输入的是AAAA加上8个%p

你会看到,AAAA后面依次输出8个内容,
第一个输出AAAA,这本来就是字符,作为一个标志显示出来罢了。然后往后,%p开始作用,依次是0xffffcfa0(可以看到格式化字符串为第一个参数,%p从格式字符串下一个开始),0xc2, 0xf7e946bb这些都是跟着格式化字符串后面的参数,之后,便打印出来0xffffcfa0地址对应的内容,即字符串。也就是说,其相对printf函数,为第5个参数(第五行)

浙公网安备 33010602011771号