Встроенная 64-битная сборка в 32-битной программе GCC C

Я компилирую 32-битный двоичный файл, но хочу встроить в него 64-битную сборку.

void method() {
   asm("...64 bit assembly...");
}

Конечно, когда я компилирую, я получаю ошибки относительно обращения к плохим регистрам, потому что регистры 64 бит.

evil.c:92: Error: bad register name `%rax'

Можно ли добавить некоторые аннотации, поэтому gcc будет обрабатывать разделы asm, используя вместо этого 64-разрядный ассемблер. У меня есть обходное решение, которое компилируется отдельно, карта на странице с PROT_EXEC | PROT_WRITE и копируется в моем коде, но это очень неудобно.

Ответ 1

Нет, это невозможно. Вы не можете запустить 64-битную сборку из 32-битного двоичного кода, так как процессор не будет находиться в длительном режиме во время запуска вашей программы.

Копирование 64-битного кода на исполняемую страницу приведет к неправильному интерпретации кода как 32-битного кода, который будет иметь непредсказуемые и нежелательные результаты.

Ответ 2

Не пытайтесь поместить 64-битный машинный код внутри генерируемой компилятором функции. Это может сработать, поскольку кодирование для функции пролог/эпилог одинаково в 32 и 64-битных, но было бы просто иметь отдельный блок из 64-битного кода.

Проще всего собрать этот блок в отдельный файл, используя GAS .code64 или NASM BITS 64, чтобы получить 64-битный код в объектном файле, который вы можете связать с 32-разрядным исполняемым файлом.

Вы сказали в комментарии, который вы собираетесь использовать для эксплойта ядра против 64-битного ядра из 32-разрядного процесса пользовательского пространства, поэтому вам просто нужны некоторые байты кода в исполняемой части вашей памяти процессов и способ получить указатель на этот блок. Это, безусловно, правдоподобно; если вы можете получить контроль над RIP ядра из 32-битного процесса, это то, что вы хотите, потому что код ядра всегда будет работать в длинном режиме.

Если вы делали что-то с 64-битным кодом в процессе, запущенном в 32-битном режиме, вы могли бы far jmp в блок 64-битного кода ( как предлагает @RossRidge), используя известное значение для дескриптора сегмента ядра __USER_CS 64-битного кода. syscall из 64-битного кода должен возвращаться в 64-битном режиме, но если нет, попробуйте int 0x80 ABI. Он всегда возвращается в режим, в котором вы были, сохраняя/восстанавливая cs и ss вместе с rip и rflags. (Что произойдет, если вы используете 32-битный int 0x80 Linux ABI в 64-битном коде?)


.rodata является частью тестового сегмента вашего исполняемого файла, поэтому просто попросите компилятор поместить байты в массив const. Fun fact: const int main = 195; компилируется в программу, которая выходит без segfault, потому что 195= 0xc3= кодировка x86 для ret (и x86 мало -endian). Для последовательности машинного кода произвольной длины const char funcname[] = { 0x90, 0x90, ..., 0xc3 } будет работать. Требуется const, иначе он будет .data (read/write/noexec) вместо .rodata.

Вы можете использовать const char funcname[] __attribute__((section(".text"))) = { ... }; для управления тем, в каком разделе он входит (например, .text вместе с функциями, генерируемыми компилятором), или даже компоновщик script, чтобы получить больше контроля.


Если вы действительно хотите сделать все это в одном файле .c, вместо использования более простого решения отдельно собранного чистого источника asm:

Чтобы собрать 64-разрядный код вместе с 32-разрядным кодом, созданным компилятором, используйте директиву .code64 GAS в операторе asm * вне любых функций. IDK, если есть какая-либо гарантия того, какой раздел будет активен, когда gcc испускает ваш asm, как gcc будет смешивать этот asm с его asm, но он не будет помещать его в середину функции.

asm(".pushsection .text \n\t"   // AFAIK, there no guarantee how this will mix with compiler asm output
    ".code64            \n\t"
    ".p2align 4         \n\t"
    ".globl my_codebytes  \n\t" // optional
    "my_codebytes:      \n\t"
    "inc %r10d          \n\t"
    "my_codebytes_end:  \n\t"
    //"my_codebytes_len: .long  . - my_codebytes\n\t"  // store the length in memory.  Optional
    ".popsection        \n\t"
#ifdef __i386
    ".code32"      // back to 32-bit interpretation for gcc code
    // "\n\t inc %r10"  // uncomment to check that it *doesn't* assemble
#endif
    );

#ifdef __cplusplus
extern "C" {
#endif
   // put C names on the labels.
   // They are *not* pointers, their addresses are link-time constants
    extern char my_codebytes[], my_codebytes_end[];
    //extern const unsigned my_codebytes_len;
#ifdef __cplusplus
}
#endif
// This expression for the length isn't a compile-time constant, so this isn't legal C
//static const unsigned len = &my_codebytes_end - &my_codebytes;

#include <stddef.h>
#include <unistd.h>

int main(void) {
    size_t len = my_codebytes_end - my_codebytes;
    const char* bytes = my_codebytes;

    // do whatever you want.  Writing it to stdout is one option!
    write(1, bytes, len);
}

Это компилируется и собирается с помощью gcc и clang (проводник компилятора).

Я попробовал на своем рабочем столе дважды проверить:

[email protected]$ gcc -m32 -Wall -O3 /tmp/foo.c
[email protected]$ ./a.out  | hd
00000000  41 ff c2                                          |A..|
00000003

Это правильная кодировка для inc %r10d:)

Программа также работает при компиляции без -m32, потому что я использовал #ifdef, чтобы решить, следует ли использовать .code32 в конце или нет. (Там нет директивы push/pop mode, как для разделов.)

Конечно, дизассемблирование двоичного файла покажет вам:

00000580 <my_codebytes>:
 580:   41                      inc    ecx
 581:   ff c2                   inc    edx

потому что дизассемблер не знает, чтобы переключиться на 64-разрядную разборку для этого блока. (Интересно, есть ли у ELF атрибуты для этого... Я не использовал никаких ассемблерных директив или скриптов компоновщика для генерации таких атрибутов, если такая вещь существует.)

Ответ 3

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

Вот пример кода для 64-разрядного Linux (Ubuntu), который запускается в 32-битном режиме, переключается в режим 64-разрядной версии, запускает функцию, а затем переключается обратно в 32-битный режим. Постройте с помощью gcc -m32.

#include <stdlib.h>
#include <stdio.h>
#include <stdbool.h>

extern bool switch_cs(int cs, bool (*f)());
extern bool check_mode();

int main(int argc, char **argv)
{
    int cs = 0x33;
    if (argc > 1)
        cs = strtoull(argv[1], 0, 16);
    printf("switch to CS=%02x\n", cs);

    bool r = switch_cs(cs, check_mode);

    if (r)
        printf("cs=%02x: 64-bit mode\n", cs);
    else
        printf("cs=%02x: 32-bit mode\n", cs);

    return 0;
}


        .intel_syntax noprefix
        .text

        .code32
        .globl  switch_cs
switch_cs:
        mov     eax, [esp+4]
        mov     edx, [esp+8]
        push    0
        push    edx
        push    eax
        push    offset .L2
        lea     eax, [esp+8]
        lcall   [esp]
        add     esp, 16
        ret

.L2:
        call    [eax]
        lret


        .code64
        .globl check_mode
check_mode:
        xor     eax, eax
        // In 32-bit mode, this instruction is executed as
        // inc eax; test eax, eax
        test    rax, rax
        setz    al
        ret