【问题标题】:Align local variable to 16-byte boundary (x86 asm)将局部变量与 16 字节边界对齐 (x86 asm)
【发布时间】:2020-03-26 01:49:43
【问题描述】:

我在分配 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

【问题讨论】:

    标签: assembly x86 memory-alignment callstack


    【解决方案1】:

    对齐堆栈后,引用相对于 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
    

    如果您在函数调用之前推送 args,请记住临时更改 ESP。 您可以通过在前面保留足够的空间作为初始 sub 的一部分来简化操作,并简单地将 args 存储在 mov 中,就像 GCC 对 -faccumulate-outgoing-args 所做的那样


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

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


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

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

    无论如何,如果您想了解编译器如何处理它,那么您应该使用更高的alignas。除了 MSVC,Godbolt 上的编译器都安装在 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 个额外字节的代码大小。所以这是一个缺点。为了节省性能,uops 通常是值得的,但对于代码大小,使用 push reg / mov reg, esp 的 3 字节设置序列可能是值得的。


    如果您不需要返回,只需 and esp, -16 就可以了!在您的 shellcode 的最顶部执行此操作,以您想要的任何对齐方式,然后利用它来处理您有效负载中的任何调用/rets。您的漏洞利用的入口点不是ret(对吗?),而且您通常不关心它上面的堆栈上是什么,因此您不需要保留旧值。

    【讨论】:

    • 感谢您的回复,我希望仍然能够使用 ebp 来引用我的局部变量,因为在推送和弹出之后跟踪 esp 变得相当混乱。我也无法控制是否使用movaps 或movups。基本上情况是我在游戏进程中分配这个shellcode并调用游戏函数之一。该函数使用 sse 并要求我将指针传递给需要 16 字节对齐的结构。此外,这个 shellcode 并不是真正的漏洞利用,我对它有很多控制权(几乎无限大小等......)
    • @jono 啊,这比我想象的更有意义。当您发表评论时,我正在更新;添加了更多建议。您可以使用任何您想要的寄存器作为基指针;如果您可以节省几个 regs,您可以让一个跟踪 ESP 在入口处和另一个作为基础。回复:ESP 移动:也许您错过了 -faccumulate-outgoing-args 代码生成风格,其中 ESP 在 sub 之后不会移动,您只需使用 mov 而不是 push 来存储函数参数。 (另外,使用 __attribute__((noinline)) 的一对简单函数尝试一下;这是一个 GCC 选项。)
    • 哦,这实际上很有趣,从来没有这样想过。所以基本上我只是sub esp, x 一次在顶部,只是将数据移动到[esp + x] 以将参数传递给函数?这样一来,追踪pushes 和pops 就不会让人头疼了。太棒了。
    • @jono:如果这是将代码注入到可执行文件中以修改其行为而不是接管并获取 shell,那么它不是 真的 shellcode。它只是修改游戏可执行文件或进程。 Shellcode 暗示(至少对我来说)它不会返回(所以你可以在前面 and esp, -16 / mov ebp, esp );如果你只是通过你添加的一些代码来转移执行,那不是 shellcode。
    • @jono 如果不是为了漏洞利用,那它不是真正的 shellcode,它只是机器码。
    猜你喜欢
    • 1970-01-01
    • 2015-03-10
    • 2012-04-30
    • 2013-03-05
    • 2017-12-21
    • 2013-05-18
    • 2016-12-19
    • 1970-01-01
    • 2023-03-25
    相关资源
    最近更新 更多