Может ли рандомизация ASLR отличаться для каждой функции? - PullRequest
1 голос
/ 14 июля 2020

У меня есть следующий фрагмент кода:

#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 Ответ

3 голосов
/ 14 июля 2020

Нет, ASLR стека происходит один раз при запуске программы. Относительные корректировки RSP между функциями фиксируются во время компиляции и представляют собой лишь небольшие константы, освобождающие место для локальных переменных функции. (Массивы переменной длины C99 и alloca выполняют корректировку переменных времени выполнения для RSP, но не случайным образом.)

Ваша программа содержит неопределенное поведение и фактически не печатает RSP; вместо этого некоторый адрес стека, оставленный в регистре предыдущим вызовом printf (который выглядит как адрес стека, поэтому его старшие биты меняются в зависимости от ASLR). Он ничего не говорит вам о различиях указателей стека между функциями, только о том, как не использовать GNU C inline asm.

Первое значение правильно печатает текущий ESP, но это только младшие 32 бита из 64 -бит RSP.

Падение конца функции, отличной от void, небезопасно , и использование возвращаемого значения - Undefined Behavior. Любой вызывающий, который использует возвращаемое значение esp_func(), обязательно вызовет UB, поэтому компилятор может оставить все, что захочет, в RAX.

Если вы хотите написать mov %rsp, %rax / ret, тогда напишите эту функцию в чистом asm или переместитесь в локальную переменную "=r"(tmp). Использование встроенного asm GNU C для изменения RAX, не сообщая об этом компилятору, ничего не меняет; компилятор по-прежнему видит это как функцию без возвращаемого значения.

MSV C встроенный asm отличается: очевидно, поддерживается использование _asm{ mov eax, 123 } или чего-то еще, а затем выпадение конца непустого функция, и MSV C будет рассматривать это как возвращаемое значение функции даже при встраивании. Встроенный asm GNU C не нуждается в таких глупых хитростях: если вы хотите, чтобы ваш asm взаимодействовал со значениями C, используйте расширенный asm с ограничением вывода, как в main. Помните, что GNU C inline asm - это не , анализируемый компилятором, просто отправьте строку шаблона как часть вывода asm компилятора для сборки.

Я не знаю точно, почему clang перезагружает возвращаемое значение из стека, но это всего лишь артефакт внутренних компонентов clang и как он выполняет генерацию кода с отключенной оптимизацией. Но это разрешено из-за неопределенного поведения. Это непустая функция, поэтому она должна иметь возвращаемое значение. Самым простым было бы просто выдать ret, и это то, что некоторые компиляторы делают при включенной оптимизации, но даже это не решает проблему из-за межпроцедурной оптимизации.

На самом деле это Undefined Behavior от C до , используйте возвращаемое значение функции, которая не вернула единицу . Это применимо на уровне C; использование встроенного asm, который изменяет регистр, не сообщая об этом компилятору, ничего не меняет в том, что касается компилятора. Поэтому ваша программа в целом содержит UB, потому что она передает результат в printf. Вот почему компилятору разрешено компилировать таким образом: ваш код уже сломан. На практике он просто возвращает некоторый мусор из памяти стека.

TL: DR: это недопустимый способ передать mov %rsp, %rax / ret в качестве определения asm для функции.

(C ++ усиливает это тем, что UB в первую очередь падает с конца, но в C это законно, пока вызывающий не использует возвращаемое значение. Если вы компилируете тот же источник, что и C ++, с оптимизацией, g ++ даже не выдает инструкцию ret после вашего встроенного шаблона asm. Вероятно, это сделано для поддержки C типа возврата по умолчанию - int, если вы объявляете функцию без типа возврата.)

Этот UB также является причиной того, почему ваша измененная версия из комментариев (с фиксированными строками формата printf), скомпилированная с включенной оптимизацией (https://godbolt.org/z/sE7e84), печатает «удивительно» разные значения «RSP»: 2-е. не использует 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, %rsi между call 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 Basi c asm (без ограничений) ни для чего не используется (кроме тела функции __attribute__((naked))).

Не предполагайте, что компилятор будет делать то, что вы ожидаете, когда UB виден ему во время компиляции. (Когда UB не отображается во время компиляции, компилятор должен создать код, который будет работать для некоторых вызывающих или вызываемых абонентов, и вы получите ожидаемый asm. Но UB, видимый во время компиляции, означает, что все ставки отключены.)

...