原文地址:blog.cloudflare.com/how-to-exec…
原文作者:blog.cloudflare.com/author/igna…
发布时间:2021年4月2日
本文由 简悦SimpRead 转码,原文地址 blog.cloudflare.com
继续学习如何从一个对象文件中导入和执行代码。这一次我们将研究E......
处理重定位
在上一篇文章中,我们学习了如何解析一个对象文件并从中导入和执行一些函数。然而,我们的玩具对象文件中的函数是简单和独立的:它们只根据其输入计算输出,没有任何外部代码或数据的依赖。在这篇文章中,我们将在第一部分的代码的基础上,探索处理有一些依赖关系的代码所需的额外步骤。
作为一个例子,我们可能会注意到,我们实际上可以用我们的add5函数重写我们的add10函数。
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工具对我们的obj.o中的.text部分进行反汇编来实现。
$ 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和2c: e8 00 00 00。这些对应于我们在源代码中的两个add5函数调用,objdump甚至为我们方便地将指令解码为callq。通过查看网上对callq指令的描述(如this one),我们可以进一步看到我们正在处理一个 "近距离,相对的调用",因为有0xe8前缀。
近距离,相对的调用,相对于下一条指令的位移。
根据description,这个变体的callq指令由5个字节组成:0xe8前缀和一个4字节(32位)的参数。这就是 "相对 "的由来:参数应该包含我们想调用的函数和当前位置之间的 "距离"--因为x86的工作方式,这个距离是由下一条指令而不是我们当前的callq指令计算的。objdump在上面的输出中方便地输出了每个机器指令的偏移量,所以我们可以很容易地计算出需要的参数。例如,对于第一条callq指令(1f: e8 00 00 00),下一条指令的偏移量是0x24。我们知道我们应该调用add5'函数,它从偏移量0x0'开始(我们的.text部分的开始)。所以相对偏移量是0x0 - 0x24 = -0x24。注意,我们有一个负参数,因为add5函数位于我们的调用指令之前,所以我们将指示CPU从其当前位置向后跳。最后,我们必须记住,负数--至少在x86系统上--是由其二的补充表示的,所以-0x24的4字节(32位)表示为0xffffffdc。用同样的方法,我们可以计算第二个add5调用的callq参数。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函数声明为静态`来验证这一点。
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系统是little-endian,所以像0xffffdc这样的多字节数字将以最小有效字节优先表示)。我们可以通过重新编译和运行我们的loader测试工具来仔细检查。
$ gcc -o loader loader.c
$ ./loader
Executing add5...
add5(42) = 47
Executing add10...
add10(42) = 52
即使add5函数被声明为静态,我们仍然可以从loader工具中调用它,基本上忽略了它现在是一个 "内部 "函数的事实。正因为如此,static关键字不应该被用作安全功能,以隐藏API,避免潜在的恶意用户。
但是,让我们退一步,将我们在obj.c中的add5函数还原为有外部链接的函数。
obj.c。
int add5(int num)
...
$ gcc -c obj.c
$ ./loader
Executing add5...
add5(42) = 47
Executing add10...
add10(42) = 42
正如我们上面所确定的,编译器没有为我们计算适当的callq参数,因为它没有足够的信息。但是后面的阶段(即链接器)会有这些信息,所以相反,编译器留下了一些关于如何修复这些参数的线索。这些线索--或对后期阶段的指示--被称为relocations。我们可以用我们的朋友,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实际上是从Info字段的低32位生成的。不同的重定位类型有不同的公式,我们需要应用这些公式来执行重定位。- `Sym. 值 "可能意味着不同的东西,取决于重定位类型,但大多数情况下,它是我们执行重定位的符号偏移。这个偏移量是从该符号部分的开始计算的。
Addend是我们可能需要在重定位公式中使用的一个常数。根据重定位的类型,readelf实际上是将解码后的符号名称添加到输出中,所以列名是`Sym. Name + Addend",但实际的字段只存储addend。
简而言之,这些条目告诉我们,我们需要在偏移量为0x20和0x2d的.text部分进行修补。为了计算在那里放什么,我们需要应用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)) {
...
}
...
我们现在在标记我们的.text副本可执行之前调用do_text_relocations函数。我们还添加了一些调试输出来检查重定位计算的结果。让我们试试吧。
$ 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返回一个常量字符串,get_var/set_var分别获取和设置一个全局变量。接下来,让我们重新编译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字节)。我们的加载器程序使用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(引用规范的内容)。
"其索引位于重定位条目中的符号的值"
在我们的例子中,它与R_X86_64_PLT32的`L'基本相同。我们可以重新使用这个实现,并在这个过程中删除调试输出。
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上找到。
在下一篇文章中,我们将研究导入和执行引用外部库的对象代码的问题。请继续关注!