【问题标题】:Overflowing variables in c?c语言中的变量溢出?
【发布时间】:2020-07-24 20:51:36
【问题描述】:

鉴于此代码:

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

int main(){
  char *password;
  unsigned int *uid;
  
  printf("enter password: ");
  password = (char *)malloc(0x10);
  uid = (unsigned int *)malloc(0x08);
  *uid = 1;
  fgets(password,0xa0,stdin);
  printf("uid: %d\n",(unsigned long)*uid);
  if (*uid == 0) {
    puts("you are logged in as admin");
  }
  else {
    puts("you are logged in as user");
  }
  return 0;
}

提示是尝试找到一种方法使其将 uid 输出为 0。我发现答案涉及输入长度正好为 31 个字符的任何字符串。我知道这与溢出到 uid 的字符串末尾的空字符有关,但我无法准确理解这是如何发生的。 malloc() 为密码分配 16 个字节,为 uid 分配 8 个字节,而 fgets() 尝试将最多 160 个字符放入密码中。 16 + 8 = 24 个字节,但 31 个字符的字符串将是 31 个字节,第 32 个字节是空字符。我到底错过了什么?

【问题讨论】:

  • 你只为malloc()分配了16个字节。如果您输入超过 15 个字符的 fgets(),将导致未定义的行为。
  • 您从未初始化uid 指向的内存,因此访问*uid 也会导致未定义的行为。
  • 问题不清楚。为什么你认为它会将 uid 输出为 0?密码中的终止 null 只是一个字节,但 int 通常至少是 4 个字节。也不能保证malloc() 会在内存中分配passworduid
  • 在导致未定义行为时试图获得特定结果是徒劳的。
  • 您正在学习或练习利用程序中的缺陷吗?您正在尝试制作溢出缓冲区并导致特定输出的输入?如果是这样,那很大程度上取决于具体情况,包括编译器、特定源代码和编译选项。 uid 对象相对于password 对象的位置可能取决于malloc 实现,但编译器也可能对其进行优化,因为它可以从源代码中看到*uid“应该是”1,所以它可以将printf("uid: %d\n",(unsigned long)*uid); 更改为puts("uid: 1");

标签: c malloc overflow fgets exploit


【解决方案1】:

malloc 不会(通常)分配两个连续的内存块,因为(大多数)malloc 分配的内存块在它们之前和之后都有簿记信息。由于malloc 必须返回“适当对齐”的内存,因此在一个块之后和下一个块之前的 malloc 开销的最小总大小是任何基本对象的最大对齐;大多数 x86 库认为这种对齐方式是 16,即使没有基本对象具有如此严格的对齐方式。在常见的malloc 实现中,此开销是之前的八个字节和之后的八个字节。 (它通常包含一些标志位和分配块的实际大小。)

上面的狡猾的话是因为malloc 的内部实现并没有被C 标准指定,并且在不同的malloc 实现之间确实存在很大差异。我想,在一个理想的世界里,所有的程序都是 100% 可移植的,我们永远不会考虑闯入一个实现来看看它是如何完成工作的。但是漏洞利用是不可移植的,如果你想知道它们是如何工作的——无论是黑帽、白帽还是紫帽活动——你将不得不用实际的实现细节弄脏你的手。我不容忍任何形式的剥削,但了解编译器和标准库的工作方式本身就很有趣,并且可能会磨练您的编程直觉。

所以,回到您的代码。由于它在内存管理系统初始化后很快运行,因此连续的 malloc 调用可能会返回或多或少的相邻内存。由于每个分配的块前后各有 8 个字节,uid 很可能正好是 password 之后的 32 个字节:分配给 password 的 16 个字节、0 个填充字节、8 个尾随开销字节和 8 个前导开销字节。

在您最有可能使用的架构上,整数存储为“小端”,因此uid 中的第一个字节——即地址最低的字节——是低位字节,即包含 1。将 31 个字节 + 一个 NUL 终止符读入 password 将用 0 覆盖 uid 的低位字节。因此您会看到结果。

fgets 也会覆盖malloc 的记账信息,这可能会导致以后出现问题。在这个特定的代码中,损坏的块没有被释放,因此 malloc 簿记块永远不会被检查。但是如果你试图释放这些块,你要么会触发 malloc 一致性检查,要么会导致 malloc 的内存内部视图被破坏;无论哪种方式,迟早都会发生不好的事情。

【讨论】:

  • 我衷心感谢这次故障。最终,我正在从事类似这样的项目,以帮助我设计更强大的代码,并更好地发现未定义行为等错误。
【解决方案2】:

没有定义行为会导致程序将uid打印为0。(实际上,printf 格式对于第二个参数的类型是不正确的,因此程序的行为无论如何都是未定义的。)但是,如果我们假设对练习的预期反应是输入任何正好 31 个字符的密码都会产生预期的结果,那么我们可能会推测这种效果是由以下假设、实现特征和表现形式的组合引起的未定义的行为:

  • 机器以 little-endian 格式表示内存中的整数,因此最低有效字节驻留在最低地址(常见,但绝不是通用的)。

  • 实现的指针提供了内存空间的平面视图(典型,但不保证)。

  • malloc() 将可靠地为uid 分配空间,使其在为password 分配的内存开始后正好32 个字节开始相对于该内存空间(在特定情况下并非完全不可能,但在没有办法保证)。

  • 系统不会检测到password 空间的溢出(典型,但不能保证)。

在这种情况下,如果您输入的密码正好是 31 个字符,确定 3d 的第 32 个字符是换行符,那么fgets 将开始将它们写入内存中@ 指定的地址987654327@。因为它被告知该空间足够大以容纳0xa0 字符(与实际的0x10 字符相反),它可能会继续写入超过分配空间末尾的字节,直到最终写入换行符(fgets()当它认为有可用空间时确实会传输)作为第 32nd 字节,在一个我们可以想象由password + 31 表示的地址。最后,fgets()——我们假设——写入一个空字节以在下一个位置终止字符串,这恰好是分配给uid 的第一个字节。因为存储在 *uid 中的整数值 1 以小端形式表示,所以它会用零字节覆盖其唯一的非零字节,因此此后从内存中读取 *uid 会产生值 0。

我强调,无论是 C 语言还是我所知道的它的任何特定实现都不能保证这样的结果。我进一步相信,在实践中,它会在许多实现中发挥不同的作用。而且我不能保证上面的内容正确地解释了你的 C 实现中实际发生的事情。然而,这是对您观察到的效果产生的一种方式的合理解释。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2015-01-28
    • 1970-01-01
    • 2021-03-26
    • 2022-01-10
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多