pwn知识点——内平栈与外平栈
一、核心概念:为何要“栈平衡”
在函数调用过程中,参数和返回地址会被压入栈中。栈平衡指的是在函数调用结束后,必须将栈顶指针(ESP)恢复到调用前的状态,以确保程序能继续正确执行。如果栈失去平衡,后续的指令可能会取到错误的数据,导致程序崩溃或更严重的安全问题。
二 、内平栈与外平栈的区别
特性 | 外平栈 (如 | 内平栈 (如 |
---|---|---|
负责平衡方 | 调用者 (Caller) | 被调用函数 (Callee) |
平衡时机 | 函数调用返回之后 | 函数返回(RET)之时 |
平衡指令 | 在调用后使用 | 使用 |
参数传递 | 参数从右至左压栈 | 参数从右至左压栈 |
主要应用 | C语言默认约定,可变参数函数 | Windows API 等固定参数函数 |
栈布局特征 | 返回地址紧邻局部变量 | 返回地址前可能存在对齐填充 |
技术细节与漏洞利用中的差异
偏移计算:这个区别在栈溢出漏洞利用中至关重要。在外平栈布局中,覆盖返回地址所需的填充长度通常是局部变量的大小(不需要额外加rsp/esp的大小)。而在内平栈布局中,由于可能存在对齐填充,填充长度可能需要额外增加(例如,局部变量大小 + 4/8字节)。在实战中,需要通过动态调试(如GDB)来精确确定偏移量。
利用技巧:在外平栈中,攻击者控制返回地址后,通常还需要平衡栈指针(例如通过一个
pop; ret
的gadget)以使后续的ROP链能正常执行。而在利用内平栈的函数时,由于函数自身会用retn X
平衡栈,攻击者需要计算好这个X
值,确保栈指针能跳转到攻击者希望的位置。
三、常用判断方法
判断特征 | 外平栈 (如 | 内平栈 (如 |
---|---|---|
调用处的指令 | 紧跟 |
|
函数返回指令 | 简单的 |
|
可变参数支持 | 支持(如 | 不支持 |
外平栈:
内平栈:
综合判断与注意事项:
在实际分析中,需要综合观察以下几点来做出准确判断:
交叉验证是关键:不能仅看调用处有无
add esp, X
。你需要同时检查被调用函数的末尾是否是retn X
。如果函数是retn
,而调用后是add esp, X
,这是典型的外平栈;如果函数是retn X
,而调用后没有平衡操作,则是内平栈。留意函数原型:如果函数参数个数可变(如
printf
),它必然使用外平栈(__cdecl
),因为被调用函数在编译时无法确定参数个数。编译器优化的影响:在某些优化模式下,编译器可能会调整平栈方式或指令顺序,因此动态调试验证总是最可靠的方法。
推荐的判断流程:
为了快速且准确地判断,建议你遵循以下流程:
IDA静态分析优先:
首先在函数调用后查看是否有
add esp, X
指令。然后跳转到被调用函数,查看其末尾是
retn
还是retn X
。最后,结合函数名(如Windows API常有
_Function@8
格式)或伪代码提示的可变参数特征进行综合判断。
GDB动态调试验证:
在函数返回指令(
ret
/retn X
)处设置断点。单步执行(
si
),观察ESP
寄存器的变化。若执行retn
后ESP
仅增加4(弹出返回地址),而后由调用函数中的add esp, X
平衡,则为外平栈。若执行retn X
后ESP
一次性增加X+4
,则为内平栈。