Обработка переездов
В предыдущем посте мы узнали, как анализировать объектный файл, а также импортировать и выполнять из него некоторые функции. Однако функции в нашем игрушечном объектном файле были простыми и самодостаточными: они вычисляли свои выходные данные исключительно на основе своих входных данных и не имели никакого внешнего кода или зависимостей данных. В этом посте мы будем опираться на код из части 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(§ions[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(§ions[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