我目前正在探索堆栈帧以及它们如何在 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;
}
在 vurnerable_func 中停止执行时,栈帧是什么样的?具体来说,
secret
之前的返回地址、保存的基指针和局部变量(buffer
和gets(buffer);
)在栈上是如何排列的叫?根据我目前的理解,堆栈应该从低内存地址到高内存地址:0x00000000 --> [free]; [缓冲]; [秘密]; [保存的EBP]; [返回]; [输入]; [主堆栈帧] --> 0xFFFFFFFF?
函数参数通常如何放置在堆栈上?参数(在本例中为
input
)是否总是首先放置在堆栈上,然后是返回地址、保存的基指针,然后是局部变量的空间?
gets(buffer);
的输入如何覆盖secret
变量?什么样的输入会导致程序打印“授予访问权限!”是否可以输入: \x41\x41\x41\x41\x41\x41\x41\x41\x41\x42\x43\x44 或 "AAAAAAAAAABCD" ?因为 0x41 是 A 并且缓冲区在 main 中是 8 个字节,所以可以通过缓冲区溢出覆盖机密来获得所需的结果?
关于堆栈金丝雀,它们一般放在哪里?它们通常放置在保存的基指针 (EBP) 之后:[缓冲区] [canary] [保存的 EBP] [返回地址]?
我真的很感激任何关于堆栈内存布局、函数调用如何在低级别工作的解释或资源指针!
预先感谢您的帮助!
CPU本身并不需要特定的栈帧结构。堆栈可以看作是可通过 esp 访问的全局数组,寄存器是全局变量(在某些情况下具有特殊含义)。堆栈帧的结构由操作系统指定,因为只有所有程序都遵循约定才能执行程序。这些就是所谓的“调用约定”。这些在不同操作系统之间有所不同(以及无论您处于 32 位还是 64 位模式),因此不可能让您大致了解堆栈帧的外观。这是维基百科链接:https://en.wikipedia.org/wiki/X86_calling_conventions
如果我们假设 cdecl 约定(这在 32 位模式中很普遍),那么在调用函数之前,函数的变量将以相反的顺序保存在堆栈上。在这种情况下:是的,返回地址、函数参数和局部变量都在堆栈上。但是,其他调用约定不一定是这种情况。
正如评论中已经指出的:您根本无法保证 C 变量的确切位置。局部变量通常位于堆栈上。但这里是否是这种情况 - 以及顺序是否与 C 代码中的相同 - 没有指定。 假设 C 变量的顺序是在源代码中并且在堆栈上,那么是的:使用“AAAAAAAAAAABCD”,您将覆盖局部变量“secret”。
堆栈金丝雀所在的位置也没有跨平台标准化。它们通常直接位于返回地址之前:[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/)是一个很好的工具,可以发现生成的汇编代码。这在很大程度上取决于所使用的编译器、选项和操作系统代码的外观。