无保护32位x86系统栈帧与函数调用栈布局技术咨询
实验用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:vulnerable_function暂停时的栈帧结构
你的地址方向理解完全错误:32位x86的栈是从高地址向低地址生长的,栈指针ESP始终指向栈顶(当前栈的最低地址位置)。当程序在gets(buffer)前暂停时,栈帧从高到低(地址从大到小)的排布为:
- main函数栈帧的栈顶区域(更高地址)
input参数(调用vulnerable_function时由main压入栈)- 返回地址RET(调用函数时自动压入,指向main中调用后的下一条指令)
- 保存的EBP(进入
vulnerable_function后,由push ebp; mov ebp, esp指令保存的main函数栈基址) - 局部变量
secret(4字节,位于EBP下方低地址侧) - 局部数组
buffer(8字节,在secret的更低地址处,靠近栈顶) - 空闲栈空间(最低地址,ESP指向此处)
问题2:函数参数的栈布局规则
在32位x86的C标准调用约定(cdecl)下:
- 调用者会从右到左把函数参数压入栈(多参数场景下,最后一个参数先入栈)
- 调用指令
call自动将返回地址压入栈 - 进入被调用函数后,先执行
push ebp; mov ebp, esp保存调用者的EBP,建立当前函数栈帧 - 最后通过
sub esp, n分配局部变量的栈空间
参数位于当前函数栈帧的上方(高地址侧),即返回地址的更靠上位置;局部变量则在EBP的下方(低地址侧),你之前的顺序描述完全错误。
问题3:覆盖secret的输入方法
gets()不检查输入长度,会持续向buffer写入直到遇到换行/EOF,超出buffer的字节会依次覆盖栈中更低地址的内容。buffer占8字节,后续紧邻的就是4字节的secret变量,因此输入超过8字节后,第9-12字节会覆盖secret的内存。
注意x86是小端字节序:整数0x41424344在内存中存储的字节顺序为\x44\x43\x42\x41(低字节0x44存在低地址,高字节0x41存在高地址)。你给出的AAAAAAAAABCD会将secret设为0x44434241,与目标值不符,无法触发"Access granted!"。
正确输入为:8个A(填满buffer) + DCBA(对应字节\x44\x43\x42\x41),即字符串AAAAAAAADCBA,或字节流\x41\x41\x41\x41\x41\x41\x41\x41\x44\x43\x42\x41,这样secret的内存值就是0x41424344。
问题4:栈金丝雀的位置
你的描述正确:栈金丝雀通常放置在局部变量和保存的EBP之间,栈布局从高到低为:[局部变量(如buffer)] → [栈金丝雀] → [保存的EBP] → [返回地址]
函数返回前会检查金丝雀的值是否被修改(正常情况下只有编译器知道金丝雀的随机值,溢出会覆盖它),若被篡改则直接终止程序,阻止溢出覆盖返回地址。
学习资源建议
- 《深入理解计算机系统》:专门章节讲解栈帧结构、函数调用过程、缓冲区溢出原理,是系统级编程的经典入门书。
- 《汇编语言:基于x86处理器》:详细讲解x86汇编指令、栈操作逻辑,帮助从底层理解函数调用的实现。
- GDB调试实践:用GDB加载32位目标程序,通过
x/20xw $ebp命令查看栈内存内容,结合断点调试直观观察栈帧变化。
内容的提问来源于stack exchange,提问作者Jonas

