格式字符串利用以段错误结束

Format string exploit ends in segfault

我正在阅读 "Hacking - The Art of Exploitation" 这本书。

这是我利用格式字符串的代码的简化版本。

/* fmt_vuln.c */

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

int main (int argc, char *argv[]){
  char text [1024];

  if (argc < 2){
    printf ("Usage: %s <text to print>\n", argv[0]);
    exit (0);
  }
  strcpy (text, argv[1]);
  printf ("The wrong way to print user-controlled input:\n");
  printf (text);
  printf ("\n");
  return 0;
}

我运行这个命令:

./fmt_vuln AAAA%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x

我得到了:

The wrong way to print user-controlled input:
AAAA59055000.58e347a0.58b68620.ffffffff.00000000.fba56ac8.58a9fc58.41414141

所以,我看到第 8 个格式参数是从格式字符串的开头读取的。

然后,当我 运行 命令时:

./getenv PATH ./fmt_vuln

我得到了地址:

0x7ffe2a673d84

所以我尝试运行:(为了打印PATH变量)

./fmt_vuln $(printf "\x84\x3d\x67\x2a\xfe\x7f")%08x.%08x.%08x.%08x.%08x.%08x.%08x.%s

我得到了:

The wrong way to print user-controlled input:
Segmentation fault

为什么我会出现段错误?从 getenv 程序中我得到了 PATH 的地址,但程序仍然崩溃...

感谢帮助。

出于安全原因,并非计算机中的所有进程都共享相同的 memory space。当我谈论不同的内存空间时,我在说什么?考虑以下 2 个程序:

//program 1
int main(int argc, char** argv){
    printf("%02x", *((uint8_t*)0xf00fba11));
    return 0;
}

//program 2
int main(int argc, char** argv){
    printf("%02x", *((uint8_t*)0xf00fba11));
    return 0;
}

如果这些程序同时 运行(假设它们没有出现段错误(几乎肯定会发生)),它们将打印不同的值。怎么可能??他们都访问内存位置 0xf00fba11!... 还是他们?

为了理解这里发生了什么,我们首先需要理解当cpu从内存中加载一个值时发生了什么。要从内存中加载一个值,cpu 向 RAM 发送一个请求,如下所示:

 cpu
|-------------|                                           |---------|
| read        |-------address out to RAM (0xf00fba11)---->|  RAM    |
|             |                                           |         |
| *0xf00fba11 |<---------data coming back to CPU----------|         |
|-------------|                                           |---------|

在cpu和ram之间有一个特殊的硬件,将地址从"virtual addresses"翻译成"physical addresses",它被称为内存管理单元(MMU简称)。如果程序请求地址 0x1000 处的值,则 MMU 可能 "remap" 0x1000 到 0x8000。如果地址 0x1000 在到达 RAM 进行所有读写之前总是被替换为 0x8000,这可能看起来像是一个毫无意义的操作。该程序仍然以完全相同的方式运行...所以有什么大不了的?

最重要的是现在程序 1 和 2 无法访问彼此的数据。 MMU 可以配置为不存在程序 1 可以从其中读取包含程序 2 的变量之一的地址。这 "mapping" 对于每个进程(大部分)都是唯一的,由操作系统配置。

这是一个 MMU 如何影响我们的玩具 "f00fba11" 示例的示例。

Process 1
 cpu
|-------------|                                           |---------|
| read        |---0xf00fba11---| MMU |--0x1000ba11------->|  RAM    |
|             |                                           |         |
| *0xf00fba11 |<---------data coming back to CPU----------|         |
|-------------|                                           |---------|

    Process 2
 cpu
|-------------|                                           |---------|
| read        |---0xf00fba11---| MMU |--0x7000ba11------->|  RAM    |
|             |                                           |         |
| *0xf00fba11 |<---------data coming back to CPU----------|         |
|-------------|                                           |---------|

进程1和进程2都请求存储在内存地址0xf00fba11的数据,但是他们得到了2个完全不同的RAM单元!这项绝妙的发明被称为"virtual memory"。如果 MMU 将不同地映射它们的内存,我们说 2 个进程有不同的 "address spaces"。操作系统决定这些映射并将 MMU 配置为遵守它们,从而 "insulating" 相互处理。考虑 2 个进程和它们可能想要访问的内存地址。

Process 1
asks for          | gets physical address
------------------------------------
 0x0000 - 0x0fff  | ERROR SEGFAULT
 0x1000 - 0x1fff  | 0x70000 - 0x70fff
 0x2000 - 0x2fff  | 0x30000 - 0x30fff
 0x3000 - 0x3fff  | 0xa7000 - 0xa7fff
      etc....     | etc.....


Process 2
asks for          | gets physical address
------------------------------------
 0x0000 - 0x0fff  | ERROR SEGFAULT
 0x1000 - 0x1fff  | 0xb1000 - 0xb1fff
 0x2000 - 0x2fff  | 0x40000 - 0x40fff
 0x3000 - 0x3fff  | 0x1c000 - 0x1cfff
      etc....     | etc.....

因此,如果在进程 1 中的内存地址 0x7ffe2a673d84 加载了一个环境变量,它可能会转换为物理地址 0x63002a673d84。此外,当进程 2 尝试访问 *0x7ff32a673d84 时,它将被映射到一个完全不同的地址,或者,在您的情况下,进程 2 可能未映射,导致 段错误。

所以坏消息是,我认为您没有任何方法可以 "fix" 使用您的代码解决此问题。做你想做的事情要么给你一个段错误,要么给你随机的、无用的数据。要获取您感兴趣的数据,您需要查看 MMU 配置设置并更改它们,除非您 运行 处于提升的特权级别,否则不允许您这样做。

在我们分开之前,值得注意的是进程之间可能存在一些 共享 地址,以便在两个进程之间来回传递数据或访问共享软件库。也就是说,对于几个不同的进程,0x1000 将转换为 0x5000。

或者我不知道你在说什么。我并没有真正遵循 ./getenv PATH ./fmt_vuln

这条线

你使用的是64位系统,所以你的地址有8个字节。

看看那个地址0x7ffe2a673d84,它只有6个字节。这意味着较高的两个字节为零。但是您不能像 \xff\xaa\xbb\x00 那样简单地提供零字节作为十六进制字符串,因为程序会将零字节解释为字符串的结尾。

您需要使用 32 位系统进行实验,x64 架构不允许您进行该利用。

偶然发现了同样的事情(读同一本书)。

getenv_addr 不是超级可靠。我通过添加

fmt_vuln.c 中显示 PATH 的确切地址来作弊
printf("[*] %s is at %p\n", "PATH", getenv("PATH"));

这将为您提供一个应该有效的地址,并且不应与 getenv_addr 给出的地址相差太远。

此外,确保禁用 ASLR(地址 space 布局随机化 - 这基本上是随机化地址 space 位置):

echo 0 | sudo tee /proc/sys/kernel/randomize_va_space

并确保编译 32 位程序 (gcc -m32)。