2021-2022-1 20212805 《Linux内核原理与分析》第十一周作业

格式化字符串漏洞实验

  • 一、实验描述
  • 二、实验前对预备知识的学习
    • 2.1 什么是格式化字符串?
    • 2.2 参数数量不匹配
      • 如果只有一个不匹配会发生什么?
      • 如果有人特意准备数据让 `printf` 抓取会发生什么?
  • 三、实验
    • 3.1 实验1说明
    • 3.2 实验1实操
      • 3.2.1 `vul_prog.c`源码
      • 3.2.2 找出`secret[1]`的值
      • 3.2.3 修改`secret[1]`的值
      • 3.2.4 修改`secret[1]`为期望值
    • 3.3 实验2
      • 3.3.1 修改后的 `vul_prog.c` 程序
      • 3.3.2 新建`write_string.c`
      • 3.3.3 修改`secret[0]`的值

一、实验描述

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

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

最后一种结果是非常危险的,因为它允许用户修改 set-UID root 程序内部变量的值,从而改变这些程序的行为,本次实验的目的就是探索给出程序的格式化漏洞。

二、实验前对预备知识的学习

2.1 什么是格式化字符串?

printf ("The magic number is: %d", 2805);

上面的这段 C 语言代码运行结果为 The magic number is: 2805 ,其中字符串 The magic number is: %d 中的格式符 %d 被参数(2805)替换。
另外,在本实验中还会用到:

%s-字符串
%n-符号以前输入的字符数量
2021-2022-1 20212805 《Linux内核原理与分析》第十一周作业_第1张图片

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

格式化函数的行为由格式化字符串控制,printf 函数从栈上取得参数。

2.2 参数数量不匹配

如果只有一个不匹配会发生什么?

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 抓取会发生什么?

·我们需要得到一段数据的内存地址,但我们无法修改代码,供我们使用的只有格式字符串。
·如果我们调用 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 节)
·下图解释了攻击方式,如果用户输入中包含了以下格式字符串
在这里插入图片描述
2021-2022-1 20212805 《Linux内核原理与分析》第十一周作业_第2张图片
·如图所示,我们使用四个 %x 来移动 printf 函数的栈指针到我们存储格式字符串的位置,一旦到了目标位置,我们使用 %s 来打印,它会打印位于地址 0x10014808 的内容,因为是将其作为字符串来处理,所以会一直打印到结束符为止。
·user_input 数组到传给 printf 函数参数的地址之间的栈空间不是为了 printf 函数准备的。但是,因为程序本身存在格式字符串漏洞,所以 printf 会把这段内存当作传入的参数来匹配 %x
·最大的挑战就是想方设法找出 printf 函数栈指针(函数取参地址)到 user_input 数组的这一段距离是多少,这段距离决定了你需要在 %s 之前输入多少个 %x

%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] 为期望值

3.2 实验1实操

3.2.1 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;
}
$ gcc -z execstack -fno-stack-protector -o vul_prog vul_prog.c 
$ sudo chmod u+s vul_prog
$ ll![请添加图片描述](https://img-blog.csdnimg.cn/4aa8e71f9dab4a73a8eaf7251b6f2fcd.png?x-oss-process=image/watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAMjAyMTI4MDXpgqLmmbrljZo=,size_17,color_FFFFFF,t_70,g_se,x_16)

2021-2022-1 20212805 《Linux内核原理与分析》第十一周作业_第3张图片
2021-2022-1 20212805 《Linux内核原理与分析》第十一周作业_第4张图片

3.2.2 找出secret[1]的值

1.运行 vul_prog 程序去定位 int_input 的位置,这样就确认了 %s 在格式字符串中的位置。2021-2022-1 20212805 《Linux内核原理与分析》第十一周作业_第5张图片
2.输入 secret[1] 的地址,记得做进制转换,同时在格式字符串中加入 %s .
2021-2022-1 20212805 《Linux内核原理与分析》第十一周作业_第6张图片
可以看到 secret[1] 的地址是 0x602014 ,转换成十进制就是 6299668
第八个位置上替换成 %s 就能打印出 secret[1] 的值了。

3.2.3 修改secret[1]的值

![*!](https://img-blog.csdnimg.cn/bb3dc301668c466caf8176ae9c65cf5a.png?x-oss-process=image/watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAMjAyMTI4MDXpgqLmmbrljZo=,size_18,color_FFFFFF,t_70,g_se,x_16)

3.2.4 修改secret[1]为期望值

2021-2022-1 20212805 《Linux内核原理与分析》第十一周作业_第7张图片

3.3 实验2

3.3.1 修改后的 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;
}
$ sudo sysctl -w kernel.randomize_va_space=0

请添加图片描述

3.3.2 新建write_string.c

/* 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");
    }
}

3.3.3 修改secret[0]的值

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

2021-2022-1 20212805 《Linux内核原理与分析》第十一周作业_第8张图片
2021-2022-1 20212805 《Linux内核原理与分析》第十一周作业_第9张图片

$ ./vul_prog 
$ ./mystring
$ ./vul_prog < mystring

2021-2022-1 20212805 《Linux内核原理与分析》第十一周作业_第10张图片

你可能感兴趣的:(linux,运维,服务器)