一、 实验描述
格式化字符串漏洞是由像printf(user_input)这样的代码引起的,其中user_input是用户输入的数据,具有Set-UID root权限的这类程序在运行的时候,printf语句将会变得非常危险,因为它可能会导致下面的结果:
1.使得程序崩溃
2.任意一块内存读取数据
3.修改任意一块内存里的数据
最后一种结果是非常危险的,因为它允许用户修改set-UID root程序内部变量的值,从而改变这些程序的行为。
本实验将会提供一个具有格式化漏洞的程序,我们将制定一个计划来探索这些漏洞。
二、实验预备知识讲解
2.1 什么是格式化字符串?
printf ("The magic number is: %d", 1911);
试观察运行以上语句,会发现字符串"The magic number is: %d"中的格式符%d被参数(1911)替换,因此输出变成了“The magic number is: 1911”。 格式化字符串大致就是这么一回事啦。
除了表示十进制数的%d,还有不少其他形式的格式符,一起来认识一下吧~
( * %n的使用将在1.5节中做出说明)
2.2 栈与格式化字符串
格式化函数的行为由格式化字符串控制,printf函数从栈上取得参数。
printf ("a has value %d, b has value %d, c is at address: %08x\n",a, b, &c);
2.3 如果参数数量不匹配会发生什么?
如果只有一个不匹配会发生什么?
printf ("a has value %d, b has value %d, c is at address: %08x\n",a, b);
1,在上面的例子中格式字符串需要3个参数,但程序只提供了2个。
2,该程序能够通过编译么?
⑴printf()是一个参数长度可变函数。因此,仅仅看参数数量是看不出问题的。
⑵为了查出不匹配,编译器需要了解printf()的运行机制,然而编译器通常不做这类分析。
⑶有些时候,格式字符串并不是一个常量字符串,它在程序运行期间生成(比如用户输入),因此,编译器无法发现不匹配。
3,那么printf()函数自身能检测到不匹配么?
⑴printf()从栈上取得参数,如果格式字符串需要3个参数,它会从栈上取3个,除非栈被标记了边界,printf()并不知道自己是否会用完提供的所有参数。
⑵既然没有那样的边界标记。printf()会持续从栈上抓取数据,在一个参数数量不匹配的例子中,它会抓取到一些不属于该函数调用到的数据。
4,如果有人特意准备数据让printf抓取会发生什么呢?
2.4 访问任意位置内存
1,我们需要得到一段数据的内存地址,但我们无法修改代码,供我们使用的只有格式字符串。
2,如果我们调用 printf(%s) 时没有指明内存地址, 那么目标地址就可以通过printf函数,在栈上的任意位置获取。printf函数维护一个初始栈指针,所以能够得到所有参数在栈中的位置
3,观察: 格式字符串位于栈上. 如果我们可以把目标地址编码进格式字符串,那样目标地址也会存在于栈上,在接下来的例子里,格式字符串将保存在栈上的缓冲区中。
1
2
3
4
5
6
7
8
|
int
main(
int
argc,
char
*argv[])
{
char
user_input[
100
];
... ...
/* other variable definitions and statements */
scanf(
"%s"
, user_input);
/* getting a string from user */
printf(user_input);
/* Vulnerable place */
return
0
;
}
|
4,如果我们让printf函数得到格式字符串中的目标内存地址 (该地址也存在于栈上), 我们就可以访问该地址.
printf ("\x10\x01\x48\x08 %x %x %x %x %s");
5,\x10\x01\x48\x08 是目标地址的四个字节, 在C语言中, \x10 告诉编译器将一个16进制数0×10放于当前位置(占1字节)。如果去掉前缀\x10就相当于两个ascii字符1和0了,这就不是我们所期望的结果了。
6,%x 导致栈指针向格式字符串的方向移动(参考1.2节)
7,下图解释了攻击方式,如果用户输入中包含了以下格式字符串
8,如图所示,我们使用四个%x来移动printf函数的栈指针到我们存储格式字符串的位置,一旦到了目标位置,我们使用%s来打印,它会打印位于地址0×10014808的内容,因为是将其作为字符串来处理,所以会一直打印到结束符为止。
9,user_input数组到传给printf函数参数的地址之间的栈空间不是为了printf函数准备的。但是,因为程序本身存在格式字符串漏洞,所以printf会把这段内存当作传入的参数来匹配%x。
10,最大的挑战就是想方设法找出printf函数栈指针(函数取参地址)到user_input数组的这一段距离是多少,这段距离决定了你需要在%s之前输入多少个%x。
2.5 在内存中写一个数字
%n: 该符号前输入的字符数量会被存储到对应的参数中去
int i;
printf ("12345%n", &i);
1,数字5(%n前的字符数量)将会被写入i 中
2,运用同样的方法在访问任意地址内存的时候,我们可以将一个数字写入指定的内存中。只要将上一小节(1.4)的%s替换成%n就能够覆盖0×10014808的内容。
3,利用这个方法,攻击者可以做以下事情:
重写程序标识控制访问权限
重写栈或者函数等等的返回地址
4,然而,写入的值是由%n之前的字符数量决定的。真的有办法能够写入任意数值么?
用最古老的计数方式, 为了写1000,就填充1000个字符吧。
为了防止过长的格式字符串,我们可以使用一个宽度指定的格式指示器。(比如(%0数字x)就会左填充预期数量的0符号)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
|
/* vul_prog.c */
#include
#include
#define SECRET1
0x44
#define SECRET2
0x55
int
main(
int
argc,
char
*argv[])
{
char
user_input[
100
];
int
*secret;
long
int_input;
int
a, b, c, d;
/* other variables, not used here.*/
/* The secret value is stored on the heap */
secret = (
int
*) malloc(
2
*sizeof(
int
));
/* getting the secret */
secret[
0
] = SECRET1; secret[
1
] = SECRET2;
printf(
"The variable secret's address is 0x%8x (on stack)\n"
, &secret);
printf(
"The variable secret's value is 0x%8x (on heap)\n"
, secret);
printf(
"secret[0]'s address is 0x%8x (on heap)\n"
, &secret[
0
]);
printf(
"secret[1]'s address is 0x%8x (on heap)\n"
, &secret[
1
]);
printf(
"Please enter a decimal integer\n"
);
scanf(
"%d"
, &int_input);
/* getting an input from user */
printf(
"Please enter a string\n"
);
scanf(
"%s"
, user_input);
/* getting a string from user */
/* Vulnerable place */
printf(user_input);
printf(
"\n"
);
/* Verify whether your attack is successful */
printf(
"The original secrets: 0x%x -- 0x%x\n"
, SECRET1, SECRET2);
printf(
"The new secrets: 0x%x -- 0x%x\n"
, secret[
0
], secret[
1
]);
return
0
;
}
|