一、 实验描述
格式化字符串漏洞是由像 printf(user_input)
这样的代码引起的,其中 user_input 是用户输入的数据,具有 Set-UID root 权限的这类程序在运行的时候,printf 语句将会变得非常危险,因为它可能会导致下面的结果:
- 使得程序崩溃
- 任意一块内存读取数据
- 修改任意一块内存里的数据
最后一种结果是非常危险的,因为它允许用户修改 set-UID root 程序内部变量的值,从而改变这些程序的行为。
本实验将会提供一个具有格式化漏洞的程序,我们将制定一个计划来探索这些漏洞。
二、实验预备知识讲解
2.1 什么是格式化字符串?
printf ("The magic number is: %d", 1911);
上面的这段 C 语言代码运行结果为 The magic number is: 1911
,你会发现字符串 The magic number is: %d
中的格式符 %d
被参数(1911)替换。
其他形式的格式符:
2.2 栈与格式化字符串
格式化函数的行为由格式化字符串控制,printf 函数从栈上取得参数。
2.3 如果参数数量不匹配会发生什么?
如果只有一个不匹配会发生什么?
printf ("a has value %d, b has value %d, c is at address: %08x\n",a, b);
- 在上面的例子中格式字符串需要 3 个参数,但程序只提供了 2 个。
- 该程序能够通过编译么?
printf()
是一个参数长度可变函数。因此,仅仅看参数数量是看不出问题的。- 为了查出不匹配,编译器需要了解
printf()
的运行机制,然而编译器通常不做这类分析。 - 有些时候,格式字符串并不是一个常量字符串,它在程序运行期间生成(比如用户输入),因此,编译器无法发现不匹配。
- 那么
printf()
函数自身能检测到不匹配么?printf()
从栈上取得参数,如果格式字符串需要 3 个参数,它会从栈上取 3 个,除非栈被标记了边界,printf()
并不知道自己是否会用完提供的所有参数。- 既然没有那样的边界标记。
printf()
会持续从栈上抓取数据,在一个参数数量不匹配的例子中,它会抓取到一些不属于该函数调用到的数据。
- 如果有人特意准备数据让 printf 抓取会发生什么呢?
2.4 访问任意位置内存
- 我们需要得到一段数据的内存地址,但我们无法修改代码,供我们使用的只有格式字符串。
- 如果我们调用
printf(%s)
时没有指明内存地址, 那么目标地址就可以通过 printf 函数,在栈上的任意位置获取。printf 函数维护一个初始栈指针,所以能够得到所有参数在栈中的位置 - 观察: 格式字符串位于栈上. 如果我们可以把目标地址编码进格式字符串,那样目标地址也会存在于栈上,在接下来的例子里,格式字符串将保存在栈上的缓冲区中。
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;
}
- 如果我们让 printf 函数得到格式字符串中的目标内存地址 (该地址也存在于栈上), 我们就可以访问该地址。(注:代码中引号内容为 user_input 数组内容的展开)
printf ("\x10\x01\x48\x08 %x %x %x %x %s");
\x10\x01\x48\x08
是目标地址的四个字节, 在 C 语言中,\x10
告诉编译器将一个16
进制数0x10
放于当前位置(占 1 字节)。如果去掉前缀\x10
就相当于两个ascii
字符 1 和 0 了,这就不是我们所期望的结果了。%x
导致栈指针向格式字符串的方向移动(参考 1.2 节)- 下图解释了攻击方式,如果用户输入中包含了以下格式字符串
- 如图所示,我们使用四个
%x
来移动printf
函数的栈指针到我们存储格式字符串的位置,一旦到了目标位置,我们使用%s
来打印,它会打印位于地址0x10014808
的内容,因为是将其作为字符串来处理,所以会一直打印到结束符为止。 user_input
数组到传给printf
函数参数的地址之间的栈空间不是为了printf
函数准备的。但是,因为程序本身存在格式字符串漏洞,所以 printf 会把这段内存当作传入的参数来匹配%x
。- 最大的挑战就是想方设法找出 printf 函数栈指针(函数取参地址)到
user_input
数组的这一段距离是多少,这段距离决定了你需要在%s
之前输入多少个%x
。
2.5 在内存中写一个数字
%n
: 该符号前输入的字符数量会被存储到对应的参数中去
int i;
printf ("12345%n", &i);
- 数字 5(
%n
前的字符数量)将会被写入i
中 - 运用同样的方法在访问任意地址内存的时候,我们可以将一个数字写入指定的内存中。只要将上一小节的
%s
替换成%n
就能够覆盖0x10014808
的内容。 - 利用这个方法,攻击者可以做以下事情:
- 重写程序标识控制访问权限
- 重写栈或者函数等等的返回地址
- 然而,写入的值是由
%n
之前的字符数量决定的。真的有办法能够写入任意数值么?- 用最古老的计数方式, 为了写 1000,就填充 1000 个字符吧。
- 为了防止过长的格式字符串,我们可以使用一个宽度指定的格式指示器。(比如(%0 数字 x)就会左填充预期数量的 0 符号)
三、 实验内容
3.1 实验 1
用户需要输入一段数据,数据保存在 user_input
数组中,程序会使用 printf
函数打印数据内容,并且该程序以 root
权限运行。更加可喜的是,这个程序存在一个格式化漏洞。让我们来看看利用这些漏洞可以搞些什么破坏。
程序说明:
程序内存中存在两个秘密值,我们想要知道这两个值,但发现无法通过读二进制代码的方式来获取它们(实验中为了简单起见,硬编码这些秘密值为 0x44
和 0x55
)。尽管我们不知道它们的值,但要得到它们的内存地址倒不是特别困难,因为对大多数系统而言,每次运行程序,这些内存地址基本上是不变的。实验假设我们已经知道了这些内存地址,为了达到这个目的,程序特意为我们打出了这些地址。
有了这些前提以后我们需要达到以下目标:
- 找出
secret[1]
的值 - 修改
secret[1]
的值 - 修改
secret[1]
为期望值
在/home/shiyanlou/
目录下,新建vul_prog.c
文件,输入以下代码内容:
/* vul_prog.c */
#include <stdlib.h>
#include <stdio.h>
#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;
}
(ps: 编译时可以添加以下参数关掉栈保护。)
3.1.1 找出 secret[1]的值
1.运行 vul_prog 程序去定位 int_input
的位置,这样就确认了 %s
在格式字符串中的位置。
12
和后面的 %016llX
都是自己随意输入的, %016llX
的个数最好在10个以上。
12 的十六进制码就是 0x000C
,可以看到输出中 12 的十六进制在第 8 个位置上,这样我们就能确定格式化字符串的位置了。
2.输入 secret[1]
的地址,记得做进制转换,同时在格式字符串中加入 %s
.
可以看到
secret[1]
的地址是0x602014
,转换成十进制就是6299668
。第八个位置上替换成
%s
就能打印出secret[1]
的值了。
大功告成!U 的 ascii 码就是 55。
大功告成!可以看到已经变成 0x77
了。
3.1.3 修改 secret[1]为期望值
3.2 实验 2
现在让我们把第一个 scanf 语句去掉,并去掉与 int_input 变量相关的所有语句,修改后的 vul_prog.c
程序如下:
/* vul_prog.c */
#include <stdlib.h>
#include <stdio.h>
#define SECRET1 0x44
#define SECRET2 0x55
int main(int argc, char *argv[])
{
char user_input[100];
int *secret;
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 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;
}
同时在 xfce 终端使用 linux 命令设置关闭地址随机化选项:
在 /home/shiyanlou
目录新建一个程序 write_string.c
。以下程序将一个格式化字符串写入了一个叫 mystring
的文件,前 4 个字节由任意你想放入格式化字符串的数字构成,接下来的字节由键盘输入:
/* write_string.c */
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main()
{
char buf[1000];
int fp, size;
unsigned int *address;
/* Putting any number you like at the beginning of the format string */
address = (unsigned int *) buf;
*address = 0x113222580;
/* Getting the rest of the format string */
scanf("%s", buf+4);
size = strlen(buf+4) + 4;
printf("The string length is %d\n", size);
/* Writing buf to "mystring" */
fp = open("mystring", O_RDWR | O_CREAT | O_TRUNC, S_IRUSR | S_IWUSR);
if (fp != -1) {
write(fp, buf, size);
close(fp);
} else {
printf("Open failed!\n");
}
}
修改 vul_prog.c
后编译 vul_prog.c
与 write_string.c
:
然后通过 write_string 程序将内容输入进 mystring 文件中,文件内容包括代码中加入的头四个字节和你之后输入的内容。
先运行 vul_prog
程序,输入 4 个 %016llx
。再运行 write_string
程序,输入 8 个 %016llx
和 1 个 %n
,此操作会生成一个 mystring
文件。
0x8c = 140 = 8*16+8 个逗号+开头 4 个字节
。