将局部变量与 16 字节边界对齐 (x86 asm)

Align local variable to 16-byte boundary (x86 asm)

我在分配 128 位变量时遇到问题,以便它在 16 字节边界上对齐(在堆栈上,而不是在堆上)。当我的函数被调用时,我无法控制堆栈是否对齐,所以我直接假设它不会对齐。

这是我的函数的样子(简化):

; start of stackframe
push ebp
mov ebp, esp

; space for our variable
sub esp, 0x10

; the 128-bit variable would be at [ebp - 0x10]
...

; end of stackframe
mov esp, ebp
pop ebp

现在,我可以通过在 sub esp, 16 之前插入 and esp, 0xFFFF'FFF0 来对齐变量,但之后我将无法再使用 [ebp - 0x10] 引用它,因为 ebp 指的是旧的、未对齐的堆栈指针。

考虑到这一点,我认为我需要在 mov ebp, esp 指令之前对齐堆栈,这样我就可以手动对齐我的变量。所以在这个例子中:

; align esp
and esp, 0xFFFF'FFF0

; start of stackframe
push ebp
mov ebp, esp

; padding (because of the push ebp)
sub esp, 0xC

; space for our variable
sub esp, 0x10

; the 128-bit variable would be at [ebp - 0x10]
...

; end of stackframe
mov esp, ebp
pop ebp

问题是我们无法正确清理堆栈框架末尾的堆栈(不确定)。这是因为我们在对齐堆栈后执行 mov ebp, esp

我真的想不出一个好的方法来做到这一点。由于 sse 对齐要求,我觉得这应该是一个常见问题,但我找不到关于该主题的太多信息。还要记住,在我的函数被调用之前我无法控制堆栈,因为这是 shellcode。

编辑: 我想一个解决方案是将我的堆栈框架包装在另一个堆栈框架中。所以像这样:

push ebp
mov ebp, esp

; align the stack
and esp, 0xFFFF'FFF0

; the "real" stackframe start
push ebp
mov ebp, esp

; padding due to the push ebp prior to this
sub esp, 0xC

; space for our variable
sub esp, 0x10

; our variable is now at [ebp - 0x1C] (i think)
...

; the "real" stackframe end
mov esp, ebp
pop ebp

mov esp, ebp
pop ebp

对齐堆栈后,参考相对于 ESP 的局部变量。 或者如果您不需要很多整数 reg,可能只对齐 EDI 或其他东西而不是 ESP 本身,然后访问相对于那个的记忆。

   push  ebp
   mov   ebp, esp     ; or any register, doesn't really matter

   and  esp, -16      ; round ESP down to a multiple of 16, reserving 0 to 12 bytes
   sub  esp, 32       ; reserve 32 bytes we know are there for sure.

   mov  dword [esp+4], 1234  ; store a local

   xorps  xmm0,xmm0
   movaps [esp+16], xmm0     ; zero 16 bytes of space with an aligned store

   leave            ; mov esp, ebp ; pop ebp
   ret

如果您在函数调用之前推送参数,请记住这会暂时更改 ESP。 您可以通过预先保留足够的 space 作为初始 sub 的一部分来简化并简单地使用 mov 存储 args,就像 GCC 使用 -faccumulate-outgoing-args

一样

如果您需要访问堆栈上的传入函数参数,您仍然可以访问 它们 相对于 EBP。

有很多方法可以解决这个问题,具体取决于您仍然需要访问什么和不需要访问什么。例如对齐堆栈后,您可以将指向预对齐值的指针存储在内存中的某处,从而释放所有其他 7 个寄存器。 (在这种情况下,您可以在 对齐堆栈之前将任何堆栈参数加载到寄存器 中,因此您不需要保留指向堆栈帧顶部的指针。)


在使用 alignas(32) 为本地编译 C 或 C++ 时,查看 clang 输出或 GCC8 及更高版本,例如在 https://godbolt.org/. 上,这些编译器(使用 -O2)执行我的建议并在对齐堆栈后引用相对于 ESP 的局部变量。

标准的 32 位 Linux 调用约定在 call 推送 return 地址之前将 ESP 对齐 16,因此一个简单的 sub 总能到达一个已知的alignas(16) 边界。根据到达 shellcode 的方式,即使利用确实具有该保证的代码,您也可能无法利用它。例如如果这是缓冲区溢出的经典代码注入利用,则易受攻击函数末尾的 ret 将恢复 16 字节堆栈对齐,只需使用直接指向代码的指针覆盖 return 地址.不是用于 ROP 攻击的 return 地址链。

无论如何,如果您想了解编译器如何处理它,那么您应该使用更高的 alignas。 Godbolt 上除 MSVC 之外的编译器安装到目标 Linux。许多其他 32 位 ABI 仅保证 4 字节堆栈对齐。


在 shellcode 中,只使用 movups 加载和存储而不用担心堆栈对齐可能更有意义。即使这意味着您不能使用内存源操作数,除非您使用 AVX 版本。例如如果 ESP 未按 16 对齐,paddd xmm0, [esp+16] 可能会出错,但 movups xmm1, [esp+16] 不会。 vpaddd xmm0, xmm0, [esp+16]

也不行

您必须决定单独的加载指令是否比序言花费更多的负载大小。

此外,[ESP] 寻址模式总是需要一个 SIB 字节,这会增加 1 个字节的代码大小。所以这是一个缺点。为了节省性能,微指令通常是值得的,但对于代码大小,使用 push reg / mov reg, esp.

的 3 字节设置序列可能是值得的

如果您不需要 return,只需 and esp, -16 即可! 例如在您的 shellcode 的最顶部执行此操作,以您想要的任何对齐方式,然后将其用于有效载荷内的任何 calls/rets。你的 exploit 的入口点不会是 ret(对吗?),而且你通常不关心它上面的堆栈上有什么,所以你不需要保留旧值。