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