打印出堆栈给出了奇怪的定位

Printing out stack gives weird positioning of

我目前正在尝试了解 C 中的字符串格式化漏洞,但要做到这一点,我必须了解内存堆栈的一些奇怪的(至少对我而言)行为。

我有一个程序

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

int main(int argc, char *argv[]) {
  char buffer[200];
  char key[] = "secret";

  printf("Location of key: %p\n", key);
  printf("Location of buffer: %p\n", &buffer);

  strcpy(buffer, argv[1]);
  printf(buffer);
  printf("\n");
  return 0;
}

我用

调用
./form AAAA.BBBE.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x.%08x

我希望得到类似

的东西

....41414141.42424245。 ...

但我明白了

....41414141.4242422e.30252e45。 ...(B 和 E 之间有一些字符)。

这里发生了什么?

我禁用了 ASLR 和堆栈保护并使用 -m32 标志编译它。

您正在将 AAAA.BBBE.%08x... 传递给格式说明符 printf。所以 printf 期望每个 %08x 有一个额外的 unsigned integer 参数。但是你不提供任何,行为将是未定义的。

您可以阅读 C Draft Standard (n1256):

If there are insufficient arguments for the format, the behavior is undefined.

您正在从堆栈中的任何地方获取十六进制输出。

我觉得你的输出还不错。 x86 是 little-endian - 数字的最低有效字节在内存中的地址较小,因此 1000 (0x3E8) 存储为 E8 03,而不是 03 E8(那会很大-字节序)。

让我们假设编译器通过堆栈将所有参数传递给 printf,并且预计可变参数从顶部到末端放置在堆栈上(在 x86 上这意味着 "from lower addresses to higher addresses")。

所以,在调用 printf 之前,我们的堆栈会像这样:

<return address><something>AAAA.BBBE.%08x.%<something>
^ - head of the stack

或者,如果我们用十六进制拼写每个字节:

<return address><something>414141412e424242452e253038782e25<something>
^ - head of the stack      A A A A . B B B E . % 0 8 x . %

然后你要求printf从堆栈中取出很多无符号的ints(大概是32位)并以十六进制打印它们,用点分隔。它跳过 <return address> 和堆栈帧的一些其他细节,并从堆栈中 buffer 之前的某个随机点开始(因为 buffer 在父堆栈帧中)。假设在某个时候它将以下块作为 4 字节 int:

<return address><something>414141412e424242452e253038782e25<something>
^ - head of the stack      A A A A . B B B E . % 0 8 x . %
                             ^^^^^^^^

也就是我们的int在内存中是用四个字节表示的。它们的值是,从具有最小地址的字节开始:41 41 41 2e。由于 x86 是小端字节序,2e 是最重要的字节,这意味着该序列被解释为 0x2e414141 并照此打印。

现在,如果我们查看您的输出:

41414141.4242422e.30252e45

我们看到有3个int0x41414141(在内存中存储为41 41 41 41),0x4242422e(在内存中存储为2e 42 42 42因为最低有效字节具有最小地址)和 0x30252e45(在内存中存储为 45 2e 25 30)。也就是说,在这种情况下 printf 读取以下字节:

number one |number two |number three|
41 41 41 41|2e 42 42 42|45 2e 25 30 |
A  A  A  A |.  B  B  B |E  .  %  0  |

这对我来说完全正确 - 正如预期的那样,它是 buffer 的开始。

这基本上就是您使用 %08x 格式输出的内容,并且您在一台小端机器上:

41 41 41 41 2e 42 42 42 45 2e 25 30 38 78 2e 25 30 38 78 2e 25 30 38 78 2e
  • 第一个是所有 41,然后它们被翻转为所有 41
  • 接下来的四个字节是2e424242,变成4242422e
  • 那么,452e2530就变成了30252e45

如果您在调试器中查看内存 window 中的 buffer,就更容易弄清楚这一点。

顺便说一句,你可以像这样打印缓冲区的地址(不带 &):

printf("Location of buffer: %p\n", buffer);