在Windows下已经有了很多针对Coff文件的加载器,如CoffLoader和CS的BOF功能,但是linux上面相关功能还是欠缺的,因此本本文章介绍一下相关技术,并提供了实现代码
项目地址(求个star):https://github.com/Sndav/coffee
void println(char *buf); void debugln(char *buf); void hello_world(); int test_func_call(unsigned char *buf){ println(buf); return 0; } int main(){ char *buf = "Hello World!"; test_func_call(buf); debugln(buf); hello_world(); return 1; }
期望可以加载上述文件所生成的test.o
文件
我们可以用objdump -d test.o -M intel
看一下这个load函数的汇编
Disassembly of section .text: 0000000000000000 <test_func_call>: 0: f3 0f 1e fa endbr64 4: 55 push rbp 5: 48 89 e5 mov rbp,rsp 8: 48 83 ec 10 sub rsp,0x10 c: 48 89 7d f8 mov QWORD PTR [rbp-0x8],rdi 10: 48 8b 45 f8 mov rax,QWORD PTR [rbp-0x8] 14: 48 89 c7 mov rdi,rax 17: e8 00 00 00 00 call 1c <test_func_call+0x1c> 1c: b8 00 00 00 00 mov eax,0x0 21: c9 leave 22: c3 ret 0000000000000023 <main>: 23: f3 0f 1e fa endbr64 27: 55 push rbp 28: 48 89 e5 mov rbp,rsp 2b: 48 83 ec 10 sub rsp,0x10 2f: 48 8d 05 00 00 00 00 lea rax,[rip+0x0] # 36 <main+0x13> 36: 48 89 45 f8 mov QWORD PTR [rbp-0x8],rax 3a: 48 8b 45 f8 mov rax,QWORD PTR [rbp-0x8] 3e: 48 89 c7 mov rdi,rax 41: e8 [00 00 00 00] call 46 <main+0x23> # 可以看到这里的操作数全是0 46: 48 8b 45 f8 mov rax,QWORD PTR [rbp-0x8] 4a: 48 89 c7 mov rdi,rax 4d: e8 [00 00 00 00] call 52 <main+0x2f> 52: b8 00 00 00 00 mov eax,0x0 57: e8 00 00 00 00 call 5c <main+0x39> 5c: b8 01 00 00 00 mov eax,0x1 61: c9 leave 62: c3 ret
我们可以发现,上述反编译代码的call指令的操作数全部是0,为了能正确找到call的正确位置,所以链接器会修改这个偏移值。怎么修改呢,需要根据rela节中的数据进行修改。rela节的表项结构体如下
typedef struct { Elf32_Addr r_offset; Elf32_Word r_info; Elf32_Sword r_addend; } Elf32_Rela;
我们可以使用readelf -r test.o
获取函数的重定向节,一般来说.text
的rela节的名字是.rela.text
Relocation section '.rela.text' at offset 0x268 contains 5 entries:
Offset Info Type Sym. Value Sym. Name + Addend
000000000018 000500000004 R_X86_64_PLT32 0000000000000000 println - 4
000000000032 000300000002 R_X86_64_PC32 0000000000000000 .rodata - 4
000000000042 000400000004 R_X86_64_PLT32 0000000000000000 test_func_call - 4
00000000004e 000700000004 R_X86_64_PLT32 0000000000000000 debugln - 4
000000000058 000800000004 R_X86_64_PLT32 0000000000000000 hello_world - 4
Relocation section '.rela.eh_frame' at offset 0x2e0 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 + 23
我们可以看到其中的一行,这里面有6个值,但是结构体只有3个,这个原因我们下面会解释
000000000018 000500000004 R_X86_64_PLT32 0000000000000000 println - 4
r_offset: 000000000018
: 代表着这个重定向位置相对section首地址的偏移,这里就是用括号框起来的这个位置,17: e8 [00 00 00 00] call 1c <test_func_call+0x1c>
r_info: 000500000004
:对于32位ELF文件可进一步细分为 24 位符号表索引和 8 位类型字段,64位ELF文件可以32位的的符号表索引和32位的类型字段sym = r_info >> 32 = 5
type = r_info & 0xFFFFFFFF = 4
R_X86_64_PLT32
: 这个不是一个字段,type就是来决定R_X86_64_PLT32
的R_X86_64_PLT32
0000000000000000 println
:这两个是从符号表中关联过来的,后面会详细介绍r_addend: -4
:这是一个很重要的字段,在重定位中有着很重要的作用,在后续的重定位指针章节中会详细介绍typedef struct { Elf64_Word st_name; unsigned char st_info; unsigned char st_other; Elf64_Half st_shndx; Elf64_Addr st_value; Elf64_Xword st_size; } Elf64_Sym;
我们可以通过readelf -s test.o
读取符号节
Symbol table '.symtab' contains 9 entries: Num: Value Size Type Bind Vis Ndx Name 0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND 1: 0000000000000000 0 FILE LOCAL DEFAULT ABS test.c 2: 0000000000000000 0 SECTION LOCAL DEFAULT 1 .text 3: 0000000000000000 0 SECTION LOCAL DEFAULT 5 .rodata 4: 0000000000000000 35 FUNC GLOBAL DEFAULT 1 test_func_call 5: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND println 6: 0000000000000023 64 FUNC GLOBAL DEFAULT 1 main 7: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND debugln 8: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND hello_world
我们可以看到上面关于重定位类型的图,这里的S,A,L,P,G,GOT分别代表
到这里,就需要研究一下这些重定向模式了,但是值得一提的是,在这个加载器中很多类型都是相同的,比如说R_X86_64_PLT32
和R_X86_64_PC32
,因为我们根本没有PLT表
为了重定位,我们需要将重定位的类型分成两种情况,
.o
程序当中某个函数对另一个函数的调用,对程序内字符串的引用等,比如说在这个例子中main
函数中对test_func_call
的调用main
中对hello_world
,debugln
的调用程序内重定向的逻辑其实和标准的链接步骤相同,按照基本的规则进行链接即可。我们这里来看一下本例当中main
函数对test_func_call
的调用。
在重定位表中,这个对应如下表项
000000000042 000400000004 R_X86_64_PLT32 0000000000000000 test_func_call - 4
这里我们可以看到我们修改的偏移是42字节,类型是R_X86_64_PLT32
,代表着修改的长度是32位,4字节,addend = -4
。对应的汇编如下
0000000000000023 <main>: 23: f3 0f 1e fa endbr64 27: 55 push rbp 28: 48 89 e5 mov rbp,rsp 2b: 48 83 ec 10 sub rsp,0x10 2f: 48 8d 05 00 00 00 00 lea rax,[rip+0x0] # 36 <main+0x13> 36: 48 89 45 f8 mov QWORD PTR [rbp-0x8],rax 3a: 48 8b 45 f8 mov rax,QWORD PTR [rbp-0x8] 3e: 48 89 c7 mov rdi,rax >41: e8 [00 00 00 00] call 46 <main+0x23> 46: 48 8b 45 f8 mov rax,QWORD PTR [rbp-0x8] 4a: 48 89 c7 mov rdi,rax 4d: e8 00 00 00 00 call 52 <main+0x2f> 52: b8 00 00 00 00 mov eax,0x0 57: e8 00 00 00 00 call 5c <main+0x39> 5c: b8 01 00 00 00 mov eax,0x1 61: c9 leave 62: c3 ret
根据手册这个类型的计算规则是L + A - P
,但是由于我们这里没有PLT表,但是根据基本原理我们这里要这么计算
sym_real_address + r_addend - patch_real_address
同理,其他的模式也可以通过具体分析去写出来
程序外重定向就更加简单了,根本不需要考虑重定向类型。由于call
指令的操作数记录的是相对rip
的偏移,而真实的rip
相对修改的地址有4/8个字节的偏差(根据操作数长度判定)。所以直接计算这个偏移即可
sym_real_address - patch_real_address - 4/8
到此为止我们介绍完了相关技术,具体代码已经上传至github,因为是rust语言编写可能读起来难度比较大,但是谁让rust好写呢(x,项目地址:https://github.com/Sndav/coffee