每个功能的ASLR随机化可以不同吗?

时间:2020-07-14 07:20:24

标签: c assembly x86 aslr

我有以下代码段:

#include <inttypes.h>
#include <stdio.h>

uint64_t
esp_func(void)
{
  __asm__("movl %esp, %eax");
}

int
main()
{
  uint32_t esp = 0;

  __asm__("\t movl %%esp,%0" : "=r"(esp));

  printf("esp: 0x%08x\n", esp);
  printf("esp: 0x%08lx\n", esp_func());
  return 0;
}

在多次执行时会打印以下内容:

❯ clang -g  esp.c && ./a.out
esp: 0xbd3b7670
esp: 0x7f8c1c2c5140

❯ clang -g  esp.c && ./a.out
esp: 0x403c9040
esp: 0x7f9ee8bd8140

❯ clang -g  esp.c && ./a.out
esp: 0xb59b70f0
esp: 0x7fe301f8c140

❯ clang -g  esp.c && ./a.out
esp: 0x6efa4110
esp: 0x7fd95941f140

❯ clang -g  esp.c && ./a.out
esp: 0x144e72b0
esp: 0x7f246d4ef140

esp_func表明ASLR具有28位熵,这在我的现代Linux内核上很有意义。

第一个值没有意义:为什么它大不相同?

我看了看组装,看起来很奇怪...

// From main
0x00001150      55             push rbp
0x00001151      4889e5         mov rbp, rsp
0x00001154      4883ec10       sub rsp, 0x10
0x00001158      c745fc000000.  mov dword [rbp-0x4], 0
0x0000115f      c745f8000000.  mov dword [rbp-0x8], 0
0x00001166      89e0           mov eax, esp            ; Move esp to eax
0x00001168      8945f8         mov dword [rbp-0x8], eax ; Assign eax to my variable `esp`
0x0000116b      8b75f8         mov esi, dword [rbp-0x8]
0x0000116e      488d3d8f0e00.  lea rdi, [0x00002004]
0x00001175      b000           mov al, 0
0x00001177      e8b4feffff     call sym.imp.printf     ; For whatever reason, the value in [rbp-0x8]
                                                       ; is assigned here. Why?


// From esp_func
0x00001140      55             push rbp
0x00001141      4889e5         mov rbp, rsp
0x00001144      89e0           mov eax, esp             ; Move esp to eax (same instruction as above)
0x00001146      488b45f8       mov rax, qword [rbp-0x8] ; This changes everything. What is this?
0x0000114a      5d             pop rbp
0x0000114b      c3             ret
0x0000114c      0f1f4000       nop dword [rax]

所以我的问题是[rbp-0x8]中有什么,它是如何到达那里的,为什么两个值不同?

1 个答案:

答案 0 :(得分:3)

否,堆栈ASLR在程序启动时发生一次。函数之间对RSP的相对调整在编译时是固定的,并且只是为函数的局部var腾出空间的小常数。 (C99可变长度数组和alloca对RSP执行运行时变量调整,但不是随机的。)

您的程序包含未定义的行为,并且实际上并没有打印RSP。而是由先前的printf调用在寄存器中留下的某个堆栈地址(它似乎是堆栈地址,因此其高位确实随ASLR的不同而不同)。它没有告诉您有关函数之间的堆栈指针差异的任何信息,只是告诉您如何不使用GNU C内联汇编。

第一个值是正确打印当前的ESP,但这只是64位RSP的低32位。


将非void函数的结尾降级是不安全的,并且使用返回值是Undefined Behaviour。使用返回值esp_func()的任何调用者都必然会触发UB,因此编译器可以随意将所需的内容留在RAX中。

如果要编写mov %rsp, %rax / ret,请以纯asm格式编写该函数,或者移动到"=r"(tmp)局部变量中。使用GNU C内联asm修改RAX而不告诉编译器它不会改变任何东西。编译器仍然将此视为没有返回值的函数。

MSVC内联汇编程序有所不同:显然支持使用_asm{ mov eax, 123 }之类的东西,然后退出非空函数的末尾,即使内联,MSVC也会将其视为函数返回值。 GNU C内联汇编不需要像这样的愚蠢的技巧:如果您希望您的汇编与C值交互,请像在main中那样使用带有输出约束的扩展汇编。请记住,编译器不会解析GNU C内联asm,只是将模板字符串作为要汇编的编译器asm输出的一部分发出。


我不知道为什么clang从堆栈中重新加载返回值,但这只是clang内部的人工产物,以及禁用优化的代码生成方式。但是由于未定义的行为,允许执行此操作。这是一个非空函数,因此需要具有返回值。最简单的事情是只发出ret,这是某些编译器在启用优化的情况下发生的事情,但即使如此,由于过程间优化也无法解决问题。

使用 未返回一个函数的返回值实际上是C中的未定义行为。这适用于C级别;使用内联asm修改寄存器而不告诉编译器有关寄存器,就编译器而言,不会改变任何内容。因此,您的程序整体上都包含UB,因为它将结果传递给printf。这就是允许编译器以这种方式进行编译的原因:您的代码已经损坏。实际上,它只是从堆栈内存中返回一些垃圾。

TL:DR:这不是发出mov %rsp, %rax / ret作为函数的asm定义的有效方法。

(C ++首先将其增强为UB,以UB结束,但是在C中,只要调用者不使用返回值就是合法的。如果您通过优化编译与C ++相同的源, g ++甚至没有在内联asm模板之后发出ret指令。如果您声明不带返回类型的函数,这可能是为了支持C的default-int返回类型。)


这个UB也是为什么您的注释(修改为固定的printf格式字符串)的修改版本在启用优化的情况下编译(https://godbolt.org/z/sE7e84)会打印出“令人惊讶”的不同“ RSP”值:第二个 isn完全不使用RSP。

#include <inttypes.h>
#include <stdio.h>

uint64_t __attribute__((noinline)) rsp_func(void)
{
  __asm__("movq %rsp, %rax");
}  // UB if return value used

int main()
{
  uint64_t rsp = 0;

  __asm__("\t movq %%rsp,%0" : "=r"(rsp));

  printf("rsp: 0x%08lx\n", rsp);
  printf("rsp: 0x%08lx\n", rsp_func());   // UB here
  return 0;
}

输出示例:

Compiler stderr
<source>:7:1: warning: non-void function does not return a value [-Wreturn-type]
}
^
1 warning generated.
Program returned: 0
Program stdout

rsp: 0x7fff5c472f30
rsp: 0x7f4b811b7170

clang -O3 asm的输出表明,编译器可见的UB是一个问题。即使您使用了noinline,编译器仍然可以看到函数体并尝试进行过程间优化。在这种情况下,UB导致它放弃并且在mov %rsp, %rsicall rsp_func之间不发出call printf,因此它将打印以前的printf恰好在RSI中保留的任何值

# from the Godbolt link
rsp_func:                               # @rsp_func
        mov     rax, rsp
        ret
main:                                   # @main
        push    rax
        mov     rsi, rsp
        mov     edi, offset .L.str
        xor     eax, eax
        call    printf
        call    rsp_func               # return value ignored because of UB.
        mov     edi, offset .L.str
        xor     eax, eax
        call    printf                 # printf("0x%08lx\n", garbage in RSI left from last printf)
        xor     eax, eax
        pop     rcx
        ret
.L.str:
        .asciz  "rsp: 0x%08lx\n"

GNU C基本asm(无约束)对任何事物(__attribute__((naked))函数的主体除外)都没有用。

在编译时看不到UB的情况下,不要以为编译器会按照您的期望进行操作。(当UB在编译时不可见时,编译器必须使代码适用于某些调用者或被调用者,您将获得预期的组合。但是在编译时可见的UB意味着所有选择均已关闭。)