16字节对齐

作者:Vesper Vei
2 分钟阅读

目录

  1. 16字节对齐
    1. 1. 为什么会有这个规定?(硬件的“强迫症”)
    2. 2. 16 字节对齐的“动态变化”
    3. 3. Pwn 选手的噩梦:ROP 链中的对齐坑
    4. 4. 终极补救方案:多加一个 ret
    5. 5. 如何在调试时识别它?
    6. 总结笔记

16字节对齐

在 Pwn 的世界里,16 字节对齐(Stack Alignment) 绝对是新手的“头号杀手”。你可能构造了一个完美的 ROP 链,逻辑无懈可击,但在本地运行或远程攻击时,程序却莫名其妙地在 system 或 printf 内部直接 SIGSEGV(段错误)了。

这种感觉就像你配好了万能钥匙,结果锁芯因为差了 1 毫米而打不开。


1. 为什么会有这个规定?(硬件的“强迫症”)

这个规定源于 x86-64 的 SSE (Streaming SIMD Extensions) 指令集。 为了极致的性能,CPU 处理 128 位(16 字节)的数据(比如浮点数运算)时,会使用类似 movaps (Move Aligned Packed Single-Precision Floating-Point Values) 这样的指令。


2. 16 字节对齐的“动态变化”

这是最容易绕晕的地方。根据 System V ABI 标准:

在执行 call 指令之前,栈顶指针 RSP 必须是 16 字节对齐的。

让我们跟踪一下栈的变化:

  1. 调用前RSP = 0x...00(16 字节对齐,地址以 0 结尾)。

  2. 执行 call:CPU 自动把返回地址(8 字节)压入栈中。

    • 此时 RSP 变成了 0x...F8(不再是 16 的倍数,偏移了 8 字节)。
  3. 进入函数内部:函数的第一条指令通常是 push rbp

    • 此时 RSP 变成了 0x...F0(嘿!又变回 16 的倍数了)。

结论:在函数体内部执行逻辑时,编译器默认 RSP 是对齐的。但如果你是通过特殊的手段(比如 ROP)直接跳到了函数的开头,情况就不一样了。


3. Pwn 选手的噩梦:ROP 链中的对齐坑

当你构造 ROP 链时,你实际上是手动在“拼凑”返回地址。

典型的失败案例:

你找到了 system 的地址,并准备好了 pop rdi; ret 传参。你的 Payload 看起来是这样的:

[padding] + [pop rdi; ret] + [/bin/sh_addr] + [system_addr]

在这个过程中,你跳过了正常的 call 流程,直接通过 ret 弹到了 system 内部。如果此时你的 RSP 刚好停在了一个“不对劲”的位置(比如末尾是 8 而不是 0),system 内部一旦执行到 movaps 就会立刻崩溃。


4. 终极补救方案:多加一个 ret

如果你发现你的 ROP 链在调用 system 时崩溃了,而且调试发现挂在 movaps 上,最简单的黑科技就是:在调用目标函数之前,先塞入一个没用的 ret 指令。

修正后的 Payload:

[padding] + [ret_gadget] + [pop rdi; ret] + [/bin/sh_addr] + [system_addr]


5. 如何在调试时识别它?

在 GDB (如使用插件 pwndbg) 中,当你崩溃时:

  1. 看崩溃的那一条指令。如果是 movaps XMMWORD PTR [rsp+...], xmm... 这种。
  2. 看 RSP 的值。如果地址的最后一位不是 0(比如是 8)。
  3. 石锤了:这就是 16 字节对齐问题。

总结笔记

状态RSP 状态备注
标准 Call 之前必须对齐 (ends in 0)ABI 的强制要求
执行 Call 后不对齐 (ends in 8)因为压入了一个 8 字节的返回地址
ROP 跳转后崩溃通常是 ends in 8导致 system 内部 SIMD 指令报错
对齐策略增加一个 ret 指令强行平移 8 字节,抵消偏移

你现在理解了为什么有时候你的 Payload 逻辑全对,却需要莫名其妙多加一个 ret 才能打通了吧? 你想针对这个对齐问题,实际看一段会导致崩溃的汇编代码,还是说你已经准备好聊聊关于 printf 可变参数和寄存器的那个“终极心机”了?


关系图谱

Loading graph...