【问题标题】:GAS ASM PIE x86-64 access variable with LEA instruction带有 LEA 指令的 GAS ASM PIE x86-64 访问变量
【发布时间】:2019-02-19 23:55:21
【问题描述】:

我正在尝试使用 GAS 语法创建一个汇编程序,该程序可以在 x86-64 架构上以与位置无关的方式从 .data 部分访问它的变量,并强制执行 32bit 架构和 IS (@ 987654326@ 而不是 %rip)。

无论我尝试什么寄存器,我得到的最好结果都是Segmentation fault: 11,即使是用于访问我根本无法访问的 EIP,也就是访问 SF。最好的结果,因为这至少告诉我除了“嗯,它不会做”之外的其他东西。

我在 macOS 10.13.6 mid 2010 Intel Core 2 Duo 上使用gcc 编译文件(这可能是clang 的原因):

$ gcc --version
Configured with: --prefix=/Applications/Xcode.app/Contents/Developer/usr --with-gxx-include-dir=/usr/include/c++/4.2.1
Apple LLVM version 9.1.0 (clang-902.0.39.2)
Target: x86_64-apple-darwin17.7.0
Thread model: posix
InstalledDir: /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin

并将一些选项传递给链接器:

gcc -m32 -Wl,-fatal_warnings,-arch_errors_fatal,-warn_commons,-pie test.s

ld:警告:PIE 已禁用。在代码签名的 PIE 中不允许使用绝对寻址(可能是 -mdynamic-no-pic),但在来自 /whatever.../test-a07cf9.o 的 _main 中使用。要修复此警告,请勿使用 -mdynamic-no-pic 编译或使用 -Wl,-no_pie 链接 ld:致命警告引发的错误(-fatal_warnings) clang:错误:链接器命令失败,退出代码为 1(使用 -v 查看调用) 1


test.s

.text
.global _main

_main:
    xor %eax, %eax
    xor %ebx, %ebx

    # lea var1(%esi/edi/ebp/esp), %ebx  # can't compile, not PIE
    # lea var1(%eip), %ebx  # segfault, obvs

    # lea (%esp), %ebx      # EBX = 17
    # lea (%non-esp), %ebx  # segfault

    # lea 0(%esi), %ebx     # segfault 
    # lea 0(%edi), %ebx     # segfault
    # lea 0(%ebp), %ebx     # EBX = 0
    # lea 0(%esp), %ebx     # EBX = 17
    # lea 0(%eip), %ebx     # segfault, obvs

    movl (%ebx), %eax
    ret

.data
    var1: .long 6

.end

我使用./a.out; echo $? 运行它以检查最后来自ret 的EAX 值。

我查看了各种来源,但主要是英特尔语法或这些问题之一 - 1、2、3。我试图反汇编我能想到的最简单的 C 示例,即全局变量 + return 来自 main() - gcc -S test.c -fPIE -pie -fpie -m32:

int var1 = 6;
int main() { return var1; }

这基本上导致:

    .section    __TEXT,__text,regular,pure_instructions
    .macosx_version_min 10, 13
    .globl  _main                   ## -- Begin function main
    .p2align    4, 0x90
_main:                                  ## @main
    .cfi_startproc
## BB#0:
    pushl   %ebp
Lcfi0:
    .cfi_def_cfa_offset 8
Lcfi1:
    .cfi_offset %ebp, -8
    movl    %esp, %ebp
Lcfi2:
    .cfi_def_cfa_register %ebp
    pushl   %eax
    calll   L0$pb
L0$pb:
    popl    %eax
    movl    $0, -4(%ebp)
    movl    _var1-L0$pb(%eax), %eax
    addl    $4, %esp
    popl    %ebp
    retl
    .cfi_endproc
                                        ## -- End function
    .section    __DATA,__data
    .globl  _var1                   ## @var1
    .p2align    2
_var1:
    .long   6                       ## 0x6


.subsections_via_symbols

这显然使用 MOV 作为 LEA 并且与我的指令几乎相同,除了 -L0$pb 部分应该是 +/- 类似 _var1 的地址 - L0$pb 的地址以进入 .data 部分。

然而,当我尝试使用 var1 和 _main 标签的相同方法时,什么都没有:

.text
.global _main

_main:
    xor %eax, %eax
    xor %ebx, %ebx

    #movl var1-_main(%ebp), %eax  # EAX = 191
    #movl var1-_main(%esp), %eax  # EAX = 204
    #movl var1-_main(%eax), %eax  # segfault
    ret

.data
    var1: .long 6

.end

任何想法我做错了什么?

编辑:

我设法从反汇编的 C 示例中删除了任何不必要的东西,并最终得到了这个:

.text
.global _main

_main:
    pushl %ebp
    pushl %eax
    calll test

test:
    popl %eax

    /* var1, var2, ... */
    movl var1-test(%eax), %eax

    addl $4, %esp
    popl %ebp
    retl

/**
 * how var1(label) - test(label) skips this label
 * if it's about address subtracting?
 */
blobbbb:
    xor %edx, %edx

.data
var1: .long 6
var2: .long 135

这对我来说没有多大意义,因为根据this guide,调用者应该 1)将参数推送到堆栈(无)2)call 标签和 被调用者实际上应该使用 ESP、EBP 和其他寄存器。另外,为什么我什至需要一个中间标签,或者更好地说,没有它有什么办法吗?

【问题讨论】:

  • 您是否尝试过检查 C 编译器的输出并这样做?
  • @fuz 是的,我可以用一个相当小的例子来重现它,push ebp、push eax、调用中间标签、pop eax、在该标签中使用 mov 或 lea、pop ebp、ret。但我认为必须有更好的方法。当我到达 PC 时,我会编辑问题。
  • @fuz 我用一个简单的气体 asm 示例编辑了这个问题,它可以访问 PIE 变量,但在我看来,这很奇怪,而且不符合我在互联网上找到的调用者/被调用者指南。
  • clang 输出中的函数调用是一个假函数调用,用于获取堆栈上指令指针的内容。这会立即在下一条指令中弹出,以将test 的实际地址转换为eax。您会对解释那里实际情况的答案感到满意吗?您能否告诉我编译器生成的实际代码,而不是经过编辑的版本(因为我认为其中缺少某些内容)。
  • @fuz 再次编辑。是的,差不多就行了。另外,如果您有时间,请解释有关上一个示例中遗漏标签的评论或粘贴链接,我会从中进行管理。 :) 我仍然不确定 PIE 和变量是否除了假调用之外没有其他方法,嗯..

标签: assembly x86 gnu-assembler position-independent-code


【解决方案1】:

在 32 位模式中,没有 eip 相对寻址模式,因为在 64 位模式中。因此,像

这样的代码
mov var(%eip), %eax

实际上是不合法的,并且不能在 32 位模式下汇编。 (在 64 位中,它会将地址截断为 32 位)。在传统的非 PIE 32 位二进制文​​件中,您只需这样做

mov var, %eax

它将var 的绝对地址处的值移动到eax,但这在PIE 二进制文件中是不可能的,因为var 的绝对地址在链接时是未知的。

链接器所知道的是二进制文件的布局以及标签之间的距离。因此,要访问全局变量,您可以这样进行:

  1. 找出一些标签的绝对地址并用它加载一些寄存器
  2. 添加从该标签到var 的距离
  3. 访问变量

第 2 步和第 3 步可以使用带位移的寻址模式进行组合。第 1 步很棘手。只有一条有用的指令告诉我们一个我们不知道的地址的地址是什么,那就是call:call 指令将下一条指令的地址压入堆栈,然后跳转到指定的地址地址。如果我们告诉call 只是跳转到下一个地址,我们会将其功能减少到本质上是push %eip:

        call Label                  # like push %eip
Label:  ...

请注意,此用例在 CPU 的返回预测中是特殊情况,实际上不计为函数调用。由于这不是一个真正的函数调用,我们没有建立堆栈框架或类似的东西,我们也没有这个调用的返回。它只是一种获取指令指针值的机制。

因此,我们知道Label的地址。接下来我们可以将其出栈并使用它来查找var的地址:

        call Label
Label:  pop %eax                    # eax = Label
        add $var-Label, %eax        # eax = Label + var - Label = var

然后我们可以取消引用这个来获取var的内容:

        call Label
Label:  pop %eax
        add %eax, $var-Label
        mov (%eax), %eax            # eax = *var

在实际代码中,您会将加法和内存操作数合并以保存指令:

        call Label
Label:  pop %eax
        mov var-Label(%eax), %eax   # eax = *var

如果你想在一个函数中引用多个静态变量,你只需要使用这个技巧一次。只需使用适当的差异:

        call Label
Label:  pop %eax
        mov foo-Label(%eax), %ebx   # ebx = *foo
        mov bar-Label(%eax), %ecx   # ecx = *bar

请注意,gcc 倾向于使用此惯用语的变体来获取指令指针的内容。它创建了一堆这样的函数:

___x86.get_pc_thunk.bx:
        mov (%esp), %ebx
        ret

将返回地址移动到指定的寄存器。这是一个不遵循正常调用约定的特殊函数,每个 eax、ebx、ecx、edx、esi 和 edi 都存在一个,具体取决于 gcc 想要的寄存器使用。代码如下所示:

        call ___x86.get_pc_thunk.bx # ebx = Label
Label:  mov foo-Label(%ebx), %eax   # eax = *foo
        mov bar-Label(%ebx), %ecx   # ecx = *bar

gcc 使用此代码在返回预测不考虑此假调用习惯用法的 CPU 上获得更好的性能。不过我不知道哪些 CPU 实际受到了影响。

最后请注意,没有跳过任何标签。我不太明白你对blobbbb 的意思。哪个控件应该达到这个标签?

最后,您的示例应如下所示:

        .text
        .global _main

_main:  call Label                  # push %eip
Label:  pop %eax                    # eax = Label
        mov var1-Label(%eax), %eax  # eax = *(Label+var1-Label)
        ret


        .data
var1:   .long 6

请注意,永远不需要 .end 指令。以大写 L 开头的标签是不会出现在符号表中的本地标签,这就是 C 编译器喜欢使用它们的原因。

【讨论】:

  • 这说明没有直接引用 32 位 PIE,只有 64 位带有 RIP 寄存器。伟大的!关于跳过的标签,我希望标签在地址空间中一个接一个地排序,因此使用那个虚拟标签我期待var1 - test 返回blobbbb 的地址,即以某种方式无法检索到的值var1.
  • @KeyWeeUsr 是的,地址是线性的。如果您编写 var1-test,为什么您希望减法运算符产生除 var1 和 test 之间的差异之外的任何结果?
  • 我认为它们是有序的,即 _main、test、blobbbb、var1、var2,因此如果我得到上部 (var1) 并在 blobbbb 存在时从中删除下部 (test),我'd 期望 blobbbb 地址产生。也许我仍然没有得到任何东西。
  • @KeyWeeUsr 这不是差异的工作方式。地址只是一个数字。如果你有一个数字foo 并添加到foo 的值bar - foo 你得到foo + bar - foo 这就是bar。就这样。没有技巧,只是简单的加减法。是的,标签是在内存中排序的。但我不太明白你的逻辑。
  • Re: thunks: 人们普遍认为call +0 / pop 会使大多数 CPU 上的返回地址预测器失衡。 IIRC,Agner Fog 的指南甚至提出了这样的要求。但是blog.stuffedcow.net/2018/04/ras-microbenchmarks/#call0 实际测量了,结果证明只有 Via Nano 没有特殊情况的调用/弹出。可能有足够多的现有代码使用了 call/pop 习惯用法,因此值得为 CPU 供应商提供特殊情况。调用 thunk 比内联 call/pop 短 1 个字节,因此在大型库中它可能会节省一些总代码大小。
猜你喜欢
  • 2012-02-27
  • 1970-01-01
  • 2019-10-09
  • 1970-01-01
  • 2020-08-01
  • 1970-01-01
  • 1970-01-01
  • 2014-01-04
  • 2010-12-30
相关资源
最近更新 更多