了解 x86 系统上函数调用中的堆栈帧和堆栈布局

问题描述 投票:0回答:1

我目前正在探索堆栈帧以及它们如何在 C 程序中工作,特别是在未受保护的 32 位 x86 系统上(没有 ASLR、堆栈金丝雀或 DEP)。我主要不是一名计算机科学学生 — 我是一名物理系学生,出于个人好奇心,我还额外修读了 IT 安全课程。由于以下内容是先决主题,因此在我的讲座中并未广泛涉及,而且我手头没有同事可以向我提问,所以我希望在这里获得一些见解!

这是我正在试验的简单 C 程序:

void vulnerable_function(int input) {
  int secret = input;
  char buffer[8];

  //stop execution here looking at stack layout

  gets(buffer);
  if (secret == 0x41424344) {
    printf("Access granted!\n");
  } else {
    printf("Access denied!\n");
  }
}

int main() {
  vulnerable_function(0x23);
  return 0;
}
  1. 在 vurnerable_func 中停止执行时,栈帧是什么样的?具体来说,

    secret
    之前的返回地址、保存的基指针和局部变量(
    buffer
    gets(buffer);
    )在栈上是如何排列的叫?根据我目前的理解,堆栈应该从低内存地址到高内存地址:0x00000000 --> [free]; [缓冲]; [秘密]; [保存的EBP]; [返回]; [输入]; [主堆栈帧] --> 0xFFFFFFFF?

  2. 函数参数通常如何放置在堆栈上?参数(在本例中为

    input
    )是否总是首先放置在堆栈上,然后是返回地址、保存的基指针,然后是局部变量的空间?

  3. gets(buffer);
    的输入如何覆盖
    secret
    变量?什么样的输入会导致程序打印“授予访问权限!”是否可以输入: \x41\x41\x41\x41\x41\x41\x41\x41\x41\x42\x43\x44 或 "AAAAAAAAAABCD" ?因为 0x41 是 A 并且缓冲区在 main 中是 8 个字节,所以可以通过缓冲区溢出覆盖机密来获得所需的结果?

  4. 关于堆栈金丝雀,它们一般放在哪里?它们通常放置在保存的基指针 (EBP) 之后:[缓冲区] [canary] [保存的 EBP] [返回地址]?

我真的很感激任何关于堆栈内存布局、函数调用如何在低级别工作的解释或资源指针!

预先感谢您的帮助!

c x86 buffer buffer-overflow stack-memory
1个回答
0
投票
  1. CPU本身并不需要特定的栈帧结构。堆栈可以看作是可通过 esp 访问的全局数组,寄存器是全局变量(在某些情况下具有特殊含义)。堆栈帧的结构由操作系统指定,因为只有所有程序都遵循约定才能执行程序。这些就是所谓的“调用约定”。这些在不同操作系统之间有所不同(以及无论您处于 32 位还是 64 位模式),因此不可能让您大致了解堆栈帧的外观。这是维基百科链接:https://en.wikipedia.org/wiki/X86_calling_conventions

  2. 如果我们假设 cdecl 约定(这在 32 位模式中很普遍),那么在调用函数之前,函数的变量将以相反的顺序保存在堆栈上。在这种情况下:是的,返回地址、函数参数和局部变量都在堆栈上。但是,其他调用约定不一定是这种情况。

  3. 正如评论中已经指出的:您根本无法保证 C 变量的确切位置。局部变量通常位于堆栈上。但这里是否是这种情况 - 以及顺序是否与 C 代码中的相同 - 没有指定。 假设 C 变量的顺序是在源代码中并且在堆栈上,那么是的:使用“AAAAAAAAAAABCD”,您将覆盖局部变量“secret”。

  4. 堆栈金丝雀所在的位置也没有跨平台标准化。它们通常直接位于返回地址之前:[buffer][secret][canary][return address][ebp]

这是 cdecl 约定中函数调用的一个最小示例。

C 代码:

int func(int var1, int var2)
{
    return var1 + var2;
}

int main(void)
{
    int var = func(5, 10);
    return 0;
}

可能的 x86 汇编代码:

main:
    ; Create stack frame
    push ebp
    mov ebp, esp

    ; Push the variables in opposite order on the stack
    push 10
    push 5

    call func

    ; Because the stack grows from up to down: add 8 to bring the stack pointer on the positon before the variables were pushed onto the stack
    add esp, 8

    ; Destroy stack frame
    ; Restore stack pointer before the stack frame was created
    mov esp, ebp
    pop ebp

    ; Result of the function is in eax
    ; Do something with it ...

    xor eax, eax ; By convention a program gives it exit status in eax. Meaning of 0: no errors
    ret
    
func:
    ; Set eax to 0
    xor eax, eax
    ; Via the esp the function parameters can be addressed
    ; "+ 4" is important, because due to the call func parameter the return address is onto the stack
    ; So jump over the return address and work with the function parameter
    add eax, [esp + 4]
    add eax, [esp + 8]

    ; Using the return address
    ret

请务必记住,创建和销毁堆栈帧有多种可能性。在线工具编译器资源管理器(https://godbolt.org/)是一个很好的工具,可以发现生成的汇编代码。这在很大程度上取决于所使用的编译器、选项和操作系统代码的外观。

© www.soinside.com 2019 - 2024. All rights reserved.