08 · 对齐数学与调用约定 (x86 vs x64 vs Windows x64)
08 · 对齐数学与调用约定 (x86 vs x64 vs Windows x64)为什么 64 位 ROP 链经常莫名 SIGSEGV? 为什么 lab6 的链要以一个ret开头? 为什么 Windows 的汇编里 call 之前总有sub rsp,0x28?答案全在本文 —— 调用约定与 16 字节栈对齐的数学。0. 三种约定一览 (实测数据)约定平台前 4/6 参数剩余参数对齐要求教学库代表cdecl32 位 Linux/Windows全部压栈 (右→左)全部压栈无lab1 (32位, pad72)System V64 位 Linuxrdi/rsi/rdx/rcx/r8/r9压栈调用点 rsp%160lab2/lab6 (64位)Windows x6464 位 Windowsrcx/rdx/r8/r9压栈同上 32B 影子区Windows lab1. 三种传参方式的结构差异Windows x64 (Windows lab)rcx/rdx/r8/r9 传前4参第5参起压栈调用者预留 32B 影子区 (shadow space)同样要求调用点 rsp%16 0long 是 32 位! (LLP64 坑)64位 System V (lab2/lab6)rdi/rsi/rdx/rcx/r8/r9 传前6参第7参起压栈调用点 rsp%16 0 (必须!)入口 rsp%16 8 (call 压了8)128 字节红区 (不用也合法)32位 cdecl (lab1)参数全部压栈 (右→左)call: 再压返回地址被调函数用 esp8 读参数1返回后调用者 add esp, N 清栈无对齐要求: 栈上全是 4 字节槽图解: 三种约定的本质差别在参数从哪来。32 位 cdecl 一切靠栈 —所以 lab1 的 payload 布局是padding|system|exit|binsh, 参数自然跟在返回地址后面 (cdecl 读取 esp8)。64 位两个约定参数走寄存器 —ROP 链里必须先用 gadget 把参数放进寄存器 (lab6 的pop rdi; ret)。Windows x64 比 SysV 多一个影子区义务 (见第 3 节)。2. 对齐数学推导 (为什么需要插 ret gadget)n 为偶数n 为奇数若 X0 (特殊情况)教学库实例 (X8)lab6 链 [ret][pop_rdi][binsh][system]pop_rdi 弹1 ret 弹1 跳转弹1 3 个n3 奇数 → 链首补 ret → n4lab2 csu 链 G1(0x4007fa)G2(0x4007e0)G1 弹6个寄存器 ret 弹1 call 压1 8n8 偶数 → 天然对齐 无需补链起点: vuln 的 leave;ret此时 rsp%16 8(main→vuln 的 call 保证)每弹出一个 qwordrsp - 8函数入口 rsp%16 (X - 8n) % 16X 8 时:需要 (8 - 8n)%16 8即 n 必须为偶数n 是奇数还是偶数?直接跳转目标函数入口 rsp%168 → 对齐插入一个 ret gadget(纯 ret: 只弹 1 个 qword)n → n1 偶数目标函数入口 rsp%168入口 (0-8n)%16n 偶数 → 0 反而要补 retn 奇数 → 8图解: 推导链 起点rsp%168(SysV 约定, call 压入返回地址造成) →每次pop等价于rsp-8→ 目标函数入口的对齐由链上弹了几次决定。n 偶数 → 入口 8 → 完美; n 奇数 → 入口 0 → libc 内部movaps崩溃,补救 插一个纯 ret gadget(只弹不跳, 翻转奇偶)。实测对照:lab6 用pop rdi; ret(1 弹) → 链[pop_rdi][binsh][system]弹出 3个 qword (n3 奇数) → system 入口 rsp%160 →SIGSEGV;加链首ret后 n4 → 入口 8 →MITIGATIONS-PWNED。lab2 用 csu G1/G2: G1pop rbx,rbp,r12..r15; ret弹 7 个, 再加 call压入的返回地址 → n8 偶数 → 天然对齐,不需要ret gadget。为什么 libc 内部 movaps 要求入口 rsp%168?glibc 的 system/printf 内部大量使用 SSE 指令做 16 字节对齐的整块拷贝(如vfprintf的缓冲搬移),movaps %xmm0, 0(%rsp)这类指令要求目标地址 16 字节对齐, 未对齐 →#GP→ SIGSEGV。库内部假设入口即对齐 (编译期保证), 所以我们的 ROP 链必须把入口对齐, 而不是“大概对齐”。判别法:gdb bt崩溃帧停在 libc 内的movaps→ 就是它。3. Windows x64: shadow space 与对齐的关系影子区作用rcx/rdx/r8/r9 的 4 个保存槽 (32B)被调函数可任意使用(寄存器传参无自动保存)0x8 对齐垫 让 call 之后入口恰好 8被调函数视角入口 rsp%168push rbp; mov rbp,rspsub rsp, K (K%168)帧体 rsp%160自身再 call 别人: 同样先保证 rsp%160 再 sub 0x28调用者视角 (Windows x64)当前 rsp (约定: 已 16 对齐, rsp%160)sub rsp, 0x28 0x20 影子区 (32B, 4×8B) 0x8 对齐垫call 目标函数 (压入 8B 返回地址)被调函数入口 rsp%16 8(和 SysV 完全一致)图解: Windows x64 与 SysV 的对齐规则相同(调用点 rsp%160,入口 rsp%168), 差异只在: 调用者必须额外预留 32 字节影子区(shadow space), 给被调函数保存前 4 个寄存器参数用。编译器常用的sub rsp, 0x28 32B 影子 8B 对齐垫, 一举两得。写汇编/手撸 payload时若漏了影子区, 被调函数会把参数槽写进我们的数据 (栈破坏)。4. 一次 call 的完整栈变化 (时序)被调函数 (vuln)栈 (rsp 变化)调用者 (main)被调函数 (vuln)栈 (rsp 变化)调用者 (main)32位 cdecl 版 (lab1)64位 System V 版 (lab6)push 参数 (右→左, 如 push binsh)call → 压入返回地址 (esp - 4)入口: esp 指向返回地址, 参数在 esp4prologue: push ebp, mov ebp,esp, sub esp,K函数体: buf[64] 溢出 (gets)epilogue: leave → mov esp,ebp, pop ebpret → 弹出返回地址 → RIP 跳回add esp, 8 (调用者清栈 — cdecl)call → 压入返回地址 (rsp - 8)入口 rsp%168, 参数在 rdiprologue: push rbp, mov rbp,rsp, sub rsp,0x208函数体: read(buf, 0x200) → pad0x108leave: rsprbp, pop rbpret → 弹出链首 ret gadget[ret][pop_rdi][binsh][system] 链执行system 入口 rsp%168 → MITIGATIONS-PWNED图解: 无论哪种约定,call的本质 “压返回地址 跳转”,ret的本质 “弹返回地址 跳转”。ROP 就是连续制造 ret—— 每个 ret 把下一个 payload qword 当作返回地址弹出, 让 RIP 沿链爬行。prologue/epilogue (push rbp / leave) 是编译器为调试与栈回溯加的骨架, 也正是lab2 里pad72的来源: buf[64] saved rbp 8B 72, 之后才是返回地址。5. pad 的真实构成 (教学库实测)Labpad构成证据 (反汇编)lab1 (32位)72 0x48buf[64] 4B 对齐 saved ebx 4Blea -0x48(%ebp),%eaxmov -0x4(%ebp),%ebx(PIC 保存 ebx!)lab2 (64位)72 0x48buf[64] saved rbp 8Blea -0x48(%rbp),%raxlab6 (64位, PIEcanary)0x108 264buf[0x100] canary 8B%39$泄漏 canary, pad0x108 命中6. LLP64 的坑 (Windows 独有)Windows x64 遵循LLP64:long仍是 32 位 (4 字节), 只有long long和指针是 64 位。Linux (LP64) 的long是 64 位。后果:结构体布局不同: 含long的 struct 在两边 sizeof 不同, 移植 shellcode的偏移全错。格式化串坑:%ld在 Windows 上读 4 字节、Linux 上读 8 字节 —泄漏 payload 混平台直接错位。长度计算:long len在 Windows 是 32 位, 大分配/大偏移截断。教学库教训: 写 Windows payload 时一律用int/size_t显式声明,禁止依赖long。