20199324《Linux内核原理与分析》第十二周作业

格式化字符串漏洞实验

一、 实验描述

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

  • 使得程序崩溃
  • 任意一块内存读取数据
  • 修改任意一块内存里的数据(这种结果是非常危险的,因为它允许用户修改 set-UID root 程序内部变量的值,从而改变这些程序的行为)

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

二、 实验内容

2.1 实验一

户需要输入一段数据,数据保存在 user_input 数组中,程序会使用 printf 函数打印数据内容,并且该程序以root权限运行。这个程序存在一个格式化漏洞。让我们来看看利用这些漏洞可以搞些什么破坏。

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

程序说明

程序内存中存在两个秘密值,我们想要知道这两个值,但发现无法通过读二进制代码的方式来获取它们(实验中为了简单起见,硬编码这些秘密值为0x44和0x55)。尽管我们不知道它们的值,但要得到它们的内存地址倒不是特别困难,因为对大多数系统而言,每次运行程序,这些内存地址基本上是不变的。实验假设我们已经知道了这些内存地址,为了达到这个目的,程序特意为我们打出了这些地址。

有了这些前提以后我们需要达到以下目标:

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

注意:因为实验环境是 64 位系统,所以需要使用 %016llx 才能读取整个字。但为了简便起见,对程序进行了修改了,使用 %08x 也能完成实验。

编译运行:

$ gcc -z execstack -fno-stack-protector -o vul_prog vul_prog.c 
$ sudo chmod u+s vul_prog

:这里会报出一些警告 warning,可以忽略。

2.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。

2.1.2 修改 secret[1]的值

只要求修改,不要求改成什么。可以看到已经变成 0x77 了。

2.1.3 修改 secret[1]为期望值

要改成自己期望的值,比如改成十进制1000,手动填充1000太累了!这里可以用格式化字符填充。

从图上得知,secret[1] 的地址是 0x602014,换算成十进制就是6299668。

我们在第7位用%.897u这种设置精度的方式填充字符,897是怎么来的呢?
在 %.897u 前有 6 个 64 位地址,每个地址对应 16 个字符,加上逗号一共就是 6*(16+1)=102 ,在 %.897u 后面的地址上还有一个 , 占一个字符。所以一共是 102+1=103 个字符,要改成 1000 的话,我们就还差 1000-103=897 个字符。

0x3e8 的 10 进制就是 1000 。

2.2 实验二

现在修改vul_prog.c程序,把第一个scanf语句去掉,并去掉与int_input变量相关的所有语句,程序如下:

/* 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 命令设置关闭地址随机化选项:

sudo sysctl -w kernel.randomize_va_space=0

关闭地址随机化后,这样每次运行程序得到的 secret 地址就都一样了。

在 /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 与 write_string.c:

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

然后通过 write_string 程序将内容输入进 mystring 文件中,文件内容包括代码中加入的头四个字节和你之后输入的内容。

先运行 vul_prog 程序,输入 4 个 %016llx 。再运行 write_string 程序,输入 8 个 %016llx 和 1 个 %n ,此操作会生成一个 mystring 文件。然后输入如下命令:

./vul_prog < mystring

修改成功(0x8c = 140 = 8*16+8 个逗号+开头 4 个字节)。

posted @ 2019-12-04 16:55  Ylxxxxx  阅读(234)  评论(0编辑  收藏  举报