Обработка переездов

В предыдущем посте мы узнали, как анализировать объектный файл, а также импортировать и выполнять из него некоторые функции. Однако функции в нашем игрушечном объектном файле были простыми и самодостаточными: они вычисляли свои выходные данные исключительно на основе своих входных данных и не имели никакого внешнего кода или зависимостей данных. В этом посте мы будем опираться на код из части 1, исследуя дополнительные шаги, необходимые для обработки кода с некоторыми зависимостями.

В качестве примера мы можем заметить, что на самом деле мы можем переписать нашу add10 функцию, используя нашу add5 функцию:

obj.c:

int add5(int num)
{
    return num + 5;
}
 
int add10(int num)
{
    num = add5(num);
    return add5(num);
}

Давайте перекомпилируем объектный файл и попробуем использовать его как библиотеку с нашей loader программой:

$ gcc -c obj.c
$ ./loader
Executing add5...
add5(42) = 47
Executing add10...
add10(42) = 42

Ого! Что-то здесь не так. add5 по-прежнему дает правильный результат, а add10 - нет. В зависимости от вашей среды и состава кода вы можете даже увидеть, как программа loader завершает работу вместо того, чтобы выводить неверные результаты. Чтобы понять, что произошло, давайте исследуем машинный код, сгенерированный компилятором. Мы можем сделать это, попросив инструмент objdump дизассемблировать раздел .text из нашего obj.o:

$ objdump --disassemble --section=.text obj.o
 
obj.o:     file format elf64-x86-64
 
 
Disassembly of section .text:
 
0000000000000000 <add5>:
   0:	55                   	push   %rbp
   1:	48 89 e5             	mov    %rsp,%rbp
   4:	89 7d fc             	mov    %edi,-0x4(%rbp)
   7:	8b 45 fc             	mov    -0x4(%rbp),%eax
   a:	83 c0 05             	add    $0x5,%eax
   d:	5d                   	pop    %rbp
   e:	c3                   	retq
 
000000000000000f <add10>:
   f:	55                   	push   %rbp
  10:	48 89 e5             	mov    %rsp,%rbp
  13:	48 83 ec 08          	sub    $0x8,%rsp
  17:	89 7d fc             	mov    %edi,-0x4(%rbp)
  1a:	8b 45 fc             	mov    -0x4(%rbp),%eax
  1d:	89 c7                	mov    %eax,%edi
  1f:	e8 00 00 00 00       	callq  24 <add10+0x15>
  24:	89 45 fc             	mov    %eax,-0x4(%rbp)
  27:	8b 45 fc             	mov    -0x4(%rbp),%eax
  2a:	89 c7                	mov    %eax,%edi
  2c:	e8 00 00 00 00       	callq  31 <add10+0x22>
  31:	c9                   	leaveq
  32:	c3                   	retq

Вам не обязательно понимать весь вывод, приведенный выше. Здесь всего две релевантные строки: 1f: e8 00 00 00 00 и 2c: e8 00 00 00 00. Они соответствуют двум вызовам add5 функций, которые есть в исходном коде, и objdump даже удобно декодирует для нас инструкцию как callq. Просматривая описания инструкции callq в Интернете (например, эта), мы можем увидеть, что имеем дело с близким, относительным вызовом из-за префикса 0xe8:

Вызов ближайшего, относительного, смещения относительно следующей инструкции.

Согласно описанию, этот вариант инструкции callq состоит из 5 байтов: префикса 0xe8 и 4-байтового (32-битного) аргумента. Отсюда относительный: аргумент должен содержать расстояние между функцией, которую мы хотим вызвать, и текущей позицией - потому что, как работает x86, это расстояние рассчитывается на основе следующей инструкции, а не нашей текущей инструкции callq. Objdump удобно выводит смещение каждой машинной инструкции в выводе выше, поэтому мы можем легко вычислить необходимый аргумент. Например, для первой callq инструкции (1f: e8 00 00 00 00) следующая инструкция находится со смещением 0x24. Мы знаем, что должны вызывать функцию add5, которая начинается со смещения 0x0 (начало нашего раздела .text). Таким образом, относительное смещение равно 0x0 - 0x24 = -0x24. Обратите внимание, что у нас есть отрицательный аргумент, потому что функция add5 находится перед нашей вызывающей инструкцией, поэтому мы бы проинструктировали ЦП перескочить назад из его текущей позиции. Наконец, мы должны помнить, что отрицательные числа - по крайней мере, в системах x86 - представлены их двумя дополнениями, поэтому 4-байтовое (32-битное) представление -0x24 будет 0xffffffdc. Таким же образом мы можем вычислить callq аргумент для второго add5 вызова: 0x0 - 0x31 = -0x31, дополнение до двух - 0xffffffcf:

Кажется, компилятор не генерирует для нас правильные callq аргументы. Мы рассчитали, что ожидаемыми аргументами будут 0xffffffdc и 0xffffffcf, но компилятор только что оставил 0x00000000 в обоих местах. Давайте сначала проверим, верны ли наши ожидания, исправив нашу загруженную .text копию, прежде чем пытаться ее выполнить:

loader.c:

...
 
static void parse_obj(void)
{
...
    /* copy the contents of `.text` section from the ELF file */
    memcpy(text_runtime_base, obj.base + text_hdr->sh_offset, text_hdr->sh_size);
 
    /* the first add5 callq argument is located at offset 0x20 and should be 0xffffffdc:
     * 0x1f is the instruction offset + 1 byte instruction prefix
     */
    *((uint32_t *)(text_runtime_base + 0x1f + 1)) = 0xffffffdc;
 
    /* the second add5 callq argument is located at offset 0x2d and should be 0xffffffcf */
    *((uint32_t *)(text_runtime_base + 0x2c + 1)) = 0xffffffcf;
 
    /* make the `.text` copy readonly and executable */
    if (mprotect(text_runtime_base, page_align(text_hdr->sh_size), PROT_READ | PROT_EXEC)) {
...

А теперь давайте проверим это:

$ gcc -o loader loader.c 
$ ./loader 
Executing add5...
add5(42) = 47
Executing add10...
add10(42) = 52

Ясно, что наша обезьянка-патч помогала: add10 теперь работает нормально и выдает правильный результат. Это означает, что наши ожидаемые callq аргументы, которые мы вычислили, верны. Так почему же компилятор выдал неправильные callq аргументы?

Переезды

Проблема с нашим игрушечным объектным файлом заключается в том, что обе функции объявлены с внешней связью - настройка по умолчанию для всех функций и глобальных переменных в C. И хотя обе функции объявлены в одном файле, компилятор не уверен, где add5 код попадет в целевой двоичный файл. Таким образом, компилятор избегает каких-либо предположений и не вычисляет аргумент относительного смещения инструкций callq. Давайте проверим это, удалив наш патч обезьяны и объявив функцию add5 как static:

loader.c:

...
 
    /* the first add5 callq argument is located at offset 0x20 and should be 0xffffffdc:
     * 0x1f is the instruction offset + 1 byte instruction prefix
     */
    /* *((uint32_t *)(text_runtime_base + 0x1f + 1)) = 0xffffffdc; */
 
    /* the second add5 callq argument is located at offset 0x2d and should be 0xffffffcf */
    /* *((uint32_t *)(text_runtime_base + 0x2c + 1)) = 0xffffffcf; */
 
...

obj.c:

/* int add5(int num) */
static int add5(int num)
...

Перекомпиляция и дизассемблирование obj.o дает нам следующее:

$ gcc -c obj.c
$ objdump --disassemble --section=.text obj.o
 
obj.o:     file format elf64-x86-64
 
 
Disassembly of section .text:
 
0000000000000000 <add5>:
   0:	55                   	push   %rbp
   1:	48 89 e5             	mov    %rsp,%rbp
   4:	89 7d fc             	mov    %edi,-0x4(%rbp)
   7:	8b 45 fc             	mov    -0x4(%rbp),%eax
   a:	83 c0 05             	add    $0x5,%eax
   d:	5d                   	pop    %rbp
   e:	c3                   	retq
 
000000000000000f <add10>:
   f:	55                   	push   %rbp
  10:	48 89 e5             	mov    %rsp,%rbp
  13:	48 83 ec 08          	sub    $0x8,%rsp
  17:	89 7d fc             	mov    %edi,-0x4(%rbp)
  1a:	8b 45 fc             	mov    -0x4(%rbp),%eax
  1d:	89 c7                	mov    %eax,%edi
  1f:	e8 dc ff ff ff       	callq  0 <add5>
  24:	89 45 fc             	mov    %eax,-0x4(%rbp)
  27:	8b 45 fc             	mov    -0x4(%rbp),%eax
  2a:	89 c7                	mov    %eax,%edi
  2c:	e8 cf ff ff ff       	callq  0 <add5>
  31:	c9                   	leaveq
  32:	c3                   	retq

Поскольку мы повторно объявили функцию add5 с внутренней связью, компилятор теперь более уверен и правильно вычисляет callq аргумента (обратите внимание, что системы x86 имеют обратный порядок байтов, поэтому многобайтовые числа, такие как 0xffffffdc, будут представлены младшим байтом первым). Мы можем дважды проверить это, перекомпилировав и запустив наш loader тестовый инструмент:

$ gcc -o loader loader.c
$ ./loader
Executing add5...
add5(42) = 47
Executing add10...
add10(42) = 52

Несмотря на то, что функция add5 объявлена ​​как static, мы все равно можем вызывать ее из инструмента loader, в основном игнорируя тот факт, что теперь это «внутренняя» функция. По этой причине ключевое слово static не следует использовать в качестве средства безопасности для сокрытия API-интерфейсов от потенциальных злонамеренных пользователей.

Но давайте сделаем шаг назад и вернем нашу add5 функцию в obj.c к функции с внешней связью:

obj.c:

int add5(int num)
...
$ gcc -c obj.c
$ ./loader
Executing add5...
add5(42) = 47
Executing add10...
add10(42) = 42

Как мы установили выше, компилятор не вычислил для нас правильные callq аргументы, потому что у него не было достаточно информации. Но более поздние этапы (а именно компоновщик) будут иметь эту информацию, поэтому вместо этого компилятор оставляет некоторые подсказки о том, как исправить эти аргументы. Эти подсказки - или инструкции для более поздних этапов - называются перемещениями. Мы можем проверить их с помощью нашего друга, утилиты readelf. Давайте еще раз рассмотрим obj.o таблицу разделов:

$ readelf --sections obj.o
There are 12 section headers, starting at offset 0x2b0:
 
Section Headers:
  [Nr] Name              Type             Address           Offset
       Size              EntSize          Flags  Link  Info  Align
  [ 0]                   NULL             0000000000000000  00000000
       0000000000000000  0000000000000000           0     0     0
  [ 1] .text             PROGBITS         0000000000000000  00000040
       0000000000000033  0000000000000000  AX       0     0     1
  [ 2] .rela.text        RELA             0000000000000000  000001f0
       0000000000000030  0000000000000018   I       9     1     8
  [ 3] .data             PROGBITS         0000000000000000  00000073
       0000000000000000  0000000000000000  WA       0     0     1
  [ 4] .bss              NOBITS           0000000000000000  00000073
       0000000000000000  0000000000000000  WA       0     0     1
  [ 5] .comment          PROGBITS         0000000000000000  00000073
       000000000000001d  0000000000000001  MS       0     0     1
  [ 6] .note.GNU-stack   PROGBITS         0000000000000000  00000090
       0000000000000000  0000000000000000           0     0     1
  [ 7] .eh_frame         PROGBITS         0000000000000000  00000090
       0000000000000058  0000000000000000   A       0     0     8
  [ 8] .rela.eh_frame    RELA             0000000000000000  00000220
       0000000000000030  0000000000000018   I       9     7     8
  [ 9] .symtab           SYMTAB           0000000000000000  000000e8
       00000000000000f0  0000000000000018          10     8     8
  [10] .strtab           STRTAB           0000000000000000  000001d8
       0000000000000012  0000000000000000           0     0     1
  [11] .shstrtab         STRTAB           0000000000000000  00000250
       0000000000000059  0000000000000000           0     0     1
Key to Flags:
  W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
  L (link order), O (extra OS processing required), G (group), T (TLS),
  C (compressed), x (unknown), o (OS specific), E (exclude),
  l (large), p (processor specific)

Мы видим, что компилятор создал новый раздел с именем .rela.text. По соглашению раздел с перемещениями для раздела с именем .foo будет называться .rela.foo, поэтому мы можем видеть, что компилятор создал раздел с перемещениями для раздела .text. Мы можем изучить перемещения дальше:

$ readelf --relocs obj.o
 
Relocation section '.rela.text' at offset 0x1f0 contains 2 entries:
  Offset          Info           Type           Sym. Value    Sym. Name + Addend
000000000020  000800000004 R_X86_64_PLT32    0000000000000000 add5 - 4
00000000002d  000800000004 R_X86_64_PLT32    0000000000000000 add5 - 4
 
Relocation section '.rela.eh_frame' at offset 0x220 contains 2 entries:
  Offset          Info           Type           Sym. Value    Sym. Name + Addend
000000000020  000200000002 R_X86_64_PC32     0000000000000000 .text + 0
000000000040  000200000002 R_X86_64_PC32     0000000000000000 .text + f

Давайте проигнорируем перемещения из раздела .rela.eh_frame, потому что они выходят за рамки этой публикации. Вместо этого давайте попробуем разобраться в перемещениях из .rela.text:

  • Столбец Offset сообщает нам, где именно в целевом разделе (.text в данном случае) требуется исправление / корректировка. Обратите внимание, что эти смещения точно такие же, как и в нашем само вычисляемом исправлении обезьяны выше.
  • Info - это комбинированное значение: старшие 32 бита - только 16 бит показаны в выходных данных выше - представляют индекс символа в таблице символов, относительно которого выполняется перемещение. В нашем примере это 8, и если мы запустим readelf --symbols obj.o, мы увидим, что он указывает на запись, соответствующую функции add5. Младшие 32 бита (в нашем случае 4) относятся к типу перемещения (см. Type ниже).
  • Type описывает тип переселения. Это псевдостолбец: readelf фактически генерирует его из младших 32 бит поля Info. Различные типы перемещения имеют разные формулы, которые нам нужно применить для выполнения перемещения.
  • Sym. Value может означать разные вещи в зависимости от типа перемещения, но в большинстве случаев это смещение символа, относительно которого мы выполняем перемещение. Смещение рассчитывается от начала раздела этого символа.
  • Addend - это константа, которую нам может потребоваться использовать в формуле перемещения. В зависимости от типа перемещения readelf фактически добавляет имя декодированного символа к выходным данным, поэтому имя столбца - Sym. Name + Addend выше, но фактическое поле хранит только добавление.

Вкратце, эти записи говорят нам, что нам нужно исправить раздел .text на смещениях 0x20 и 0x2d. Чтобы рассчитать, что туда положить, нам нужно применить формулу для типа перемещения R_X86_64_PLT32. Поискав в Интернете, мы можем найти различные спецификации ELF - например, этот - которые расскажут нам, как реализовать R_X86_64_PLT32 перемещение. В спецификации упоминается, что результатом этого перемещения будет word32 - чего мы и ожидаем, потому что callq аргументов в нашем случае 32-битные, а формула, которую нам нужно применить, - это L + A - P, где:

  • L - адрес символа, относительно которого выполняется перемещение (в нашем случае add5)
  • A - постоянное слагаемое (в нашем случае 4)
  • P - это адрес / смещение, где мы сохраняем результат перемещения

Когда формула перемещения ссылается на адреса или смещения некоторых символов, мы должны использовать в расчетах фактические - в нашем случае адреса времени выполнения - адреса. Например, мы будем использовать text_runtime_base + 0x2d как P для второго перемещения, а не только 0x2d. Итак, давайте попробуем реализовать эту логику перемещения в нашем загрузчике объектов:

loader.c:

...
 
/* from https://elixir.bootlin.com/linux/v5.11.6/source/arch/x86/include/asm/elf.h#L51 */
#define R_X86_64_PLT32 4
 
...
 
static uint8_t *section_runtime_base(const Elf64_Shdr *section)
{
    const char *section_name = shstrtab + section->sh_name;
    size_t section_name_len = strlen(section_name);
 
    /* we only mmap .text section so far */
    if (strlen(".text") == section_name_len && !strcmp(".text", section_name))
        return text_runtime_base;
 
    fprintf(stderr, "No runtime base address for section %s\n", section_name);
    exit(ENOENT);
}
 
static void do_text_relocations(void)
{
    /* we actually cheat here - the name .rela.text is a convention, but not a
     * rule: to figure out which section should be patched by these relocations
     * we would need to examine the rela_text_hdr, but we skip it for simplicity
     */
    const Elf64_Shdr *rela_text_hdr = lookup_section(".rela.text");
    if (!rela_text_hdr) {
        fputs("Failed to find .rela.text\n", stderr);
        exit(ENOEXEC);
    }
 
    int num_relocations = rela_text_hdr->sh_size / rela_text_hdr->sh_entsize;
    const Elf64_Rela *relocations = (Elf64_Rela *)(obj.base + rela_text_hdr->sh_offset);
 
    for (int i = 0; i < num_relocations; i++) {
        int symbol_idx = ELF64_R_SYM(relocations[i].r_info);
        int type = ELF64_R_TYPE(relocations[i].r_info);
 
        /* where to patch .text */
        uint8_t *patch_offset = text_runtime_base + relocations[i].r_offset;
        /* symbol, with respect to which the relocation is performed */
        uint8_t *symbol_address = section_runtime_base(&sections[symbols[symbol_idx].st_shndx]) + symbols[symbol_idx].st_value;
 
        switch (type)
        {
        case R_X86_64_PLT32:
            /* L + A - P, 32 bit output */
            *((uint32_t *)patch_offset) = symbol_address + relocations[i].r_addend - patch_offset;
            printf("Calculated relocation: 0x%08x\n", *((uint32_t *)patch_offset));
            break;
        }
    }
}
 
static void parse_obj(void)
{
...
 
    /* copy the contents of `.text` section from the ELF file */
    memcpy(text_runtime_base, obj.base + text_hdr->sh_offset, text_hdr->sh_size);
 
    do_text_relocations();
 
    /* make the `.text` copy readonly and executable */
    if (mprotect(text_runtime_base, page_align(text_hdr->sh_size), PROT_READ | PROT_EXEC)) {
 
...
}
 
...

Теперь мы вызываем функцию do_text_relocations перед тем, как пометить нашу .text копию исполняемого файла. Мы также добавили некоторые отладочные данные, чтобы проверить результат вычислений перемещения. Давайте попробуем:

$ gcc -o loader loader.c 
$ ./loader 
Calculated relocation: 0xffffffdc
Calculated relocation: 0xffffffcf
Executing add5...
add5(42) = 47
Executing add10...
add10(42) = 52

Большой! Наш импортированный код теперь работает так, как ожидалось. Следуя подсказкам по перемещению, оставленным нам компилятором, мы получили те же результаты, что и в наших расчетах по исправлению ошибок в начале этого поста. В наших расчетах перемещения также использовался адрес text_runtime_base, который недоступен во время компиляции. Вот почему компилятор не мог вычислить callq аргументы и должен был вместо этого выдать перемещения.

Обработка постоянных данных и глобальных переменных

До сих пор мы имели дело с объектными файлами, содержащими только исполняемый код без состояния. То есть импортированные функции могут вычислять свои выходные данные исключительно на основе входных данных. Давайте посмотрим, что произойдет, если мы добавим в наш импортированный код зависимости постоянных данных и глобальных переменных. Во-первых, мы добавляем в наш obj.o еще несколько функций:

obj.c:

...
 
const char *get_hello(void)
{
    return "Hello, world!";
}
 
static int var = 5;
 
int get_var(void)
{
    return var;
}
 
void set_var(int num)
{
    var = num;
}

get_hello возвращает постоянную строку, а _115 _ / _ 116_ получает и устанавливает глобальную переменную соответственно. Далее перекомпилируем obj.o и запустим наш загрузчик:

$ gcc -c obj.c
$ ./loader 
Calculated relocation: 0xffffffdc
Calculated relocation: 0xffffffcf
No runtime base address for section .rodata

Похоже, наш загрузчик попытался обработать больше перемещений, но не смог найти адрес времени выполнения для раздела .rodata. Раньше у нас даже не было раздела .rodata, но он был добавлен сейчас, потому что нашему obj.o нужно где-то хранить постоянную строку Hello, world!:

$ readelf --sections obj.o
There are 13 section headers, starting at offset 0x478:
 
Section Headers:
  [Nr] Name              Type             Address           Offset
       Size              EntSize          Flags  Link  Info  Align
  [ 0]                   NULL             0000000000000000  00000000
       0000000000000000  0000000000000000           0     0     0
  [ 1] .text             PROGBITS         0000000000000000  00000040
       000000000000005f  0000000000000000  AX       0     0     1
  [ 2] .rela.text        RELA             0000000000000000  00000320
       0000000000000078  0000000000000018   I      10     1     8
  [ 3] .data             PROGBITS         0000000000000000  000000a0
       0000000000000004  0000000000000000  WA       0     0     4
  [ 4] .bss              NOBITS           0000000000000000  000000a4
       0000000000000000  0000000000000000  WA       0     0     1
  [ 5] .rodata           PROGBITS         0000000000000000  000000a4
       000000000000000d  0000000000000000   A       0     0     1
  [ 6] .comment          PROGBITS         0000000000000000  000000b1
       000000000000001d  0000000000000001  MS       0     0     1
  [ 7] .note.GNU-stack   PROGBITS         0000000000000000  000000ce
       0000000000000000  0000000000000000           0     0     1
  [ 8] .eh_frame         PROGBITS         0000000000000000  000000d0
       00000000000000b8  0000000000000000   A       0     0     8
  [ 9] .rela.eh_frame    RELA             0000000000000000  00000398
       0000000000000078  0000000000000018   I      10     8     8
  [10] .symtab           SYMTAB           0000000000000000  00000188
       0000000000000168  0000000000000018          11    10     8
  [11] .strtab           STRTAB           0000000000000000  000002f0
       000000000000002c  0000000000000000           0     0     1
  [12] .shstrtab         STRTAB           0000000000000000  00000410
       0000000000000061  0000000000000000           0     0     1
Key to Flags:
  W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
  L (link order), O (extra OS processing required), G (group), T (TLS),
  C (compressed), x (unknown), o (OS specific), E (exclude),
  l (large), p (processor specific)

У нас также есть еще .text переездов:

$ readelf --relocs obj.o
 
Relocation section '.rela.text' at offset 0x320 contains 5 entries:
  Offset          Info           Type           Sym. Value    Sym. Name + Addend
000000000020  000a00000004 R_X86_64_PLT32    0000000000000000 add5 - 4
00000000002d  000a00000004 R_X86_64_PLT32    0000000000000000 add5 - 4
00000000003a  000500000002 R_X86_64_PC32     0000000000000000 .rodata - 4
000000000046  000300000002 R_X86_64_PC32     0000000000000000 .data - 4
000000000058  000300000002 R_X86_64_PC32     0000000000000000 .data - 4
...

На этот раз компилятор выполнил еще три R_X86_64_PC32 перемещений. Они ссылаются на символы с индексами 3 и 5, поэтому давайте выясним, что это такое:

$ readelf --symbols obj.o
 
Symbol table '.symtab' contains 15 entries:
   Num:    Value          Size Type    Bind   Vis      Ndx Name
     0: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT  UND
     1: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS obj.c
     2: 0000000000000000     0 SECTION LOCAL  DEFAULT    1
     3: 0000000000000000     0 SECTION LOCAL  DEFAULT    3
     4: 0000000000000000     0 SECTION LOCAL  DEFAULT    4
     5: 0000000000000000     0 SECTION LOCAL  DEFAULT    5
     6: 0000000000000000     4 OBJECT  LOCAL  DEFAULT    3 var
     7: 0000000000000000     0 SECTION LOCAL  DEFAULT    7
     8: 0000000000000000     0 SECTION LOCAL  DEFAULT    8
     9: 0000000000000000     0 SECTION LOCAL  DEFAULT    6
    10: 0000000000000000    15 FUNC    GLOBAL DEFAULT    1 add5
    11: 000000000000000f    36 FUNC    GLOBAL DEFAULT    1 add10
    12: 0000000000000033    13 FUNC    GLOBAL DEFAULT    1 get_hello
    13: 0000000000000040    12 FUNC    GLOBAL DEFAULT    1 get_var
    14: 000000000000004c    19 FUNC    GLOBAL DEFAULT    1 set_var

Записи 3 и 5 не имеют прикрепленных имен, но они ссылаются на что-то в разделах с индексами 3 и 5 соответственно. В выходных данных приведенной выше таблицы разделов мы видим, что раздел с индексом 3 - это .data, а раздел с индексом 5 - .rodata. Чтобы узнать больше о наиболее распространенных разделах в файле ELF, ознакомьтесь с нашим предыдущим постом. Чтобы импортировать наш недавно добавленный код и заставить его работать, нам также необходимо сопоставить разделы .data и .rodata в дополнение к разделу .text и обработать эти R_X86_64_PC32 перемещения.

Однако есть один нюанс. Если мы проверим спецификацию, мы увидим, что R_X86_64_PC32 перемещение производит 32-битный вывод, аналогичный R_X86_64_PLT32 перемещению. Это означает, что расстояние в памяти между исправленной позицией в .text и ссылочным символом должно быть достаточно маленьким, чтобы соответствовать 32-битному значению (1 бит для положительного / отрицательного знака и 31 бит для фактических данных, поэтому менее 2147483647 байт). Наша loader программа использует системный вызов mmap для выделения памяти для копий разделов объектов, но mmap может выделить отображение почти в любом месте адресного пространства процесса. Если мы изменим программу loader так, чтобы она вызывала mmap для каждого раздела отдельно, мы можем в конечном итоге получить .rodata или .data раздел, отображаемый слишком далеко от раздела .text, и не сможем обрабатывать R_X86_64_PC32 перемещений. Другими словами, нам нужно убедиться, что разделы .data и .rodata расположены относительно близко к разделу .text во время выполнения:

Один из способов добиться этого - выделить необходимую память для всех разделов одним вызовом mmap. Затем мы разбиваем его на части и назначаем каждому фрагменту соответствующие права доступа. Давайте изменим нашу loader программу именно для этого:

loader.c:

...
 
/* runtime base address of the imported code */
static uint8_t *text_runtime_base;
/* runtime base of the .data section */
static uint8_t *data_runtime_base;
/* runtime base of the .rodata section */
static uint8_t *rodata_runtime_base;
 
...
 
static void parse_obj(void)
{
...
 
    /* find the `.text` entry in the sections table */
    const Elf64_Shdr *text_hdr = lookup_section(".text");
    if (!text_hdr) {
        fputs("Failed to find .text\n", stderr);
        exit(ENOEXEC);
    }
 
    /* find the `.data` entry in the sections table */
    const Elf64_Shdr *data_hdr = lookup_section(".data");
    if (!data_hdr) {
        fputs("Failed to find .data\n", stderr);
        exit(ENOEXEC);
    }
 
    /* find the `.rodata` entry in the sections table */
    const Elf64_Shdr *rodata_hdr = lookup_section(".rodata");
    if (!rodata_hdr) {
        fputs("Failed to find .rodata\n", stderr);
        exit(ENOEXEC);
    }
 
    /* allocate memory for `.text`, `.data` and `.rodata` copies rounding up each section to whole pages */
    text_runtime_base = mmap(NULL, page_align(text_hdr->sh_size) + page_align(data_hdr->sh_size) + page_align(rodata_hdr->sh_size), PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
    if (text_runtime_base == MAP_FAILED) {
        perror("Failed to allocate memory");
        exit(errno);
    }
 
    /* .data will come right after .text */
    data_runtime_base = text_runtime_base + page_align(text_hdr->sh_size);
    /* .rodata will come after .data */
    rodata_runtime_base = data_runtime_base + page_align(data_hdr->sh_size);
 
    /* copy the contents of `.text` section from the ELF file */
    memcpy(text_runtime_base, obj.base + text_hdr->sh_offset, text_hdr->sh_size);
    /* copy .data */
    memcpy(data_runtime_base, obj.base + data_hdr->sh_offset, data_hdr->sh_size);
    /* copy .rodata */
    memcpy(rodata_runtime_base, obj.base + rodata_hdr->sh_offset, rodata_hdr->sh_size);
 
    do_text_relocations();
 
    /* make the `.text` copy readonly and executable */
    if (mprotect(text_runtime_base, page_align(text_hdr->sh_size), PROT_READ | PROT_EXEC)) {
        perror("Failed to make .text executable");
        exit(errno);
    }
 
    /* we don't need to do anything with .data - it should remain read/write */
 
    /* make the `.rodata` copy readonly */
    if (mprotect(rodata_runtime_base, page_align(rodata_hdr->sh_size), PROT_READ)) {
        perror("Failed to make .rodata readonly");
        exit(errno);
    }
}
 
...

Теперь, когда у нас есть адреса выполнения .data и .rodata, мы можем обновить функцию поиска адреса времени выполнения перемещения:

loader.c:

...
 
static uint8_t *section_runtime_base(const Elf64_Shdr *section)
{
    const char *section_name = shstrtab + section->sh_name;
    size_t section_name_len = strlen(section_name);
 
    if (strlen(".text") == section_name_len && !strcmp(".text", section_name))
        return text_runtime_base;
 
    if (strlen(".data") == section_name_len && !strcmp(".data", section_name))
        return data_runtime_base;
 
    if (strlen(".rodata") == section_name_len && !strcmp(".rodata", section_name))
        return rodata_runtime_base;
 
    fprintf(stderr, "No runtime base address for section %s\n", section_name);
    exit(ENOENT);
}

И, наконец, мы можем импортировать и выполнять наши новые функции:

loader.c:

...
 
static void execute_funcs(void)
{
    /* pointers to imported functions */
    int (*add5)(int);
    int (*add10)(int);
    const char *(*get_hello)(void);
    int (*get_var)(void);
    void (*set_var)(int num);
 
...
 
    printf("add10(%d) = %d\n", 42, add10(42));
 
    get_hello = lookup_function("get_hello");
    if (!get_hello) {
        fputs("Failed to find get_hello function\n", stderr);
        exit(ENOENT);
    }
 
    puts("Executing get_hello...");
    printf("get_hello() = %s\n", get_hello());
 
    get_var = lookup_function("get_var");
    if (!get_var) {
        fputs("Failed to find get_var function\n", stderr);
        exit(ENOENT);
    }
 
    puts("Executing get_var...");
    printf("get_var() = %d\n", get_var());
 
    set_var = lookup_function("set_var");
    if (!set_var) {
        fputs("Failed to find set_var function\n", stderr);
        exit(ENOENT);
    }
 
    puts("Executing set_var(42)...");
    set_var(42);
 
    puts("Executing get_var again...");
    printf("get_var() = %d\n", get_var());
}
...

Давай попробуем:

$ gcc -o loader loader.c 
$ ./loader 
Calculated relocation: 0xffffffdc
Calculated relocation: 0xffffffcf
Executing add5...
add5(42) = 47
Executing add10...
add10(42) = 52
Executing get_hello...
get_hello() = ]�UH��
Executing get_var...
get_var() = 1213580125
Executing set_var(42)...
Segmentation fault

Ой-ой! Мы забыли реализовать новый R_X86_64_PC32 тип перемещения. Формула переселения здесь S + A - P. Мы уже знаем о A и P. По S (цитата из спецификации):

«Значение символа, индекс которого находится в записи о перемещении»

В нашем случае это то же самое, что L для R_X86_64_PLT32. Мы можем просто повторно использовать реализацию и удалить вывод отладки в процессе:

loader.c:

...
 
/* from https://elixir.bootlin.com/linux/v5.11.6/source/arch/x86/include/asm/elf.h#L51 */
#define R_X86_64_PC32 2
#define R_X86_64_PLT32 4
 
...
 
static void do_text_relocations(void)
{
    /* we actually cheat here - the name .rela.text is a convention, but not a
     * rule: to figure out which section should be patched by these relocations
     * we would need to examine the rela_text_hdr, but we skip it for simplicity
     */
    const Elf64_Shdr *rela_text_hdr = lookup_section(".rela.text");
    if (!rela_text_hdr) {
        fputs("Failed to find .rela.text\n", stderr);
        exit(ENOEXEC);
    }
 
    int num_relocations = rela_text_hdr->sh_size / rela_text_hdr->sh_entsize;
    const Elf64_Rela *relocations = (Elf64_Rela *)(obj.base + rela_text_hdr->sh_offset);
 
    for (int i = 0; i < num_relocations; i++) {
        int symbol_idx = ELF64_R_SYM(relocations[i].r_info);
        int type = ELF64_R_TYPE(relocations[i].r_info);
 
        /* where to patch .text */
        uint8_t *patch_offset = text_runtime_base + relocations[i].r_offset;
        /* symbol, with respect to which the relocation is performed */
        uint8_t *symbol_address = section_runtime_base(&sections[symbols[symbol_idx].st_shndx]) + symbols[symbol_idx].st_value;
 
        switch (type)
        {
        case R_X86_64_PC32:
            /* S + A - P, 32 bit output, S == L here */
        case R_X86_64_PLT32:
            /* L + A - P, 32 bit output */
            *((uint32_t *)patch_offset) = symbol_address + relocations[i].r_addend - patch_offset;
            break;
        }
    }
}
 
...

Теперь нам нужно закончить. Еще одна попытка:

$ gcc -o loader loader.c 
$ ./loader 
Executing add5...
add5(42) = 47
Executing add10...
add10(42) = 52
Executing get_hello...
get_hello() = Hello, world!
Executing get_var...
get_var() = 5
Executing set_var(42)...
Executing get_var again...
get_var() = 42

На этот раз мы можем успешно импортировать функции, которые ссылаются на статические постоянные данные и глобальные переменные. Мы даже можем управлять внутренним состоянием объектного файла через определенный интерфейс доступа. Как и прежде, полный исходный код этого поста доступен на GitHub.

В следующем посте мы рассмотрим импорт и выполнение объектного кода со ссылками на внешние библиотеки. Будьте на связи!

Это репост моей публикации из Блога Cloudflare