Linux下的Object文件加载器
字数 1409 2025-08-24 10:10:13

Linux下的Object文件加载器技术详解

前言

本文详细讲解Linux环境下如何实现一个Object文件(.o)加载器,类似于Windows下的CoffLoader和Cobalt Strike的BOF功能。我们将深入分析ELF文件结构、重定位机制,并提供实现思路。

目标设定

我们以下面的C代码为例,目标是加载其生成的test.o文件:

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;
}

ELF文件结构分析

反汇编分析

使用objdump -d test.o -M intel查看.text节的汇编代码:

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>
  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,需要通过重定位信息来修正。

.rela节分析

.rela节存储重定位信息,结构如下:

typedef struct {
    Elf32_Addr r_offset;
    Elf32_Word r_info;
    Elf32_Sword r_addend;
} Elf32_Rela;

使用readelf -r test.o查看.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

各字段含义:

  • r_offset: 重定位位置相对section首地址的偏移
  • r_info: 64位ELF分为32位符号表索引和32位类型字段
    • sym = r_info >> 32
    • type = r_info & 0xFFFFFFFF
  • Type: 重定位类型,如R_X86_64_PLT32
  • Sym. Name + Addend: 符号名和加数

.symtab节分析

符号表结构:

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

关键字段:

  • st_name: 字符串表中的索引
  • st_shndx: 符号所在节的序号
    • SHN_UNDEF: 符号未在当前文件定义
    • SHN_ABS: 符号是绝对地址
    • SHN_COMMON: 符号用于定义对齐字节
  • st_value: 符号相对于所在节起始地址的偏移

重定位机制详解

重定位类型说明

根据Intel MPX Linux64 ABI文档,重定位类型含义如下:

  • R_X86_64_PLT32 (4): PLT32重定位
  • R_X86_64_PC32 (2): PC相对32位重定位

计算符号说明:

  • A: 被加数
  • B: 共享对象基地址
  • G: 符号在GOT中的偏移
  • L: PLT条目位置
  • P: 重定位存储单元位置
  • S: 符号值
  • Z: 符号大小

程序内重定向

示例:main函数调用test_func_call

重定位表项:

000000000042  000400000004 R_X86_64_PLT32    0000000000000000 test_func_call - 4

对应汇编:

41: e8 00 00 00 00          call   46 <main+0x23>

计算规则:

L + A - P

由于没有PLT表,简化为:

sym_real_address + r_addend - patch_real_address

其中:

  • sym_real_address: 符号的真实内存地址
  • patch_real_address: 需要修改的内存地址
  • r_addend: 加数(这里是-4)

程序外重定向

示例:调用外部函数如hello_world

更简单,直接计算相对偏移:

sym_real_address - patch_real_address - 4/8

其中4/8取决于操作数长度(32位用4,64位用8)

实现总结

  1. 解析ELF文件,获取.text节和.rela.text节
  2. 处理重定位表:
    • 区分程序内和程序外重定向
    • 根据不同类型应用相应计算规则
  3. 修正call指令的操作数
  4. 将修正后的代码加载到内存执行

项目实现已用Rust编写,地址:https://github.com/Sndav/coffee

关键点回顾

  1. ELF文件结构分析,特别是.rela和.symtab节
  2. 重定位机制的理解和计算规则
  3. 区分程序内和程序外重定向的不同处理方式
  4. call指令操作数的修正方法
  5. 实际实现时的内存地址计算

通过以上分析,我们可以实现一个功能完整的Linux Object文件加载器,为安全研究提供更多可能性。

Linux下的Object文件加载器技术详解 前言 本文详细讲解Linux环境下如何实现一个Object文件(.o)加载器,类似于Windows下的CoffLoader和Cobalt Strike的BOF功能。我们将深入分析ELF文件结构、重定位机制,并提供实现思路。 目标设定 我们以下面的C代码为例,目标是加载其生成的test.o文件: ELF文件结构分析 反汇编分析 使用 objdump -d test.o -M intel 查看.text节的汇编代码: 可以看到call指令的操作数全是0,需要通过重定位信息来修正。 .rela节分析 .rela节存储重定位信息,结构如下: 使用 readelf -r test.o 查看.rela.text节: 各字段含义: r_offset : 重定位位置相对section首地址的偏移 r_info : 64位ELF分为32位符号表索引和32位类型字段 sym = r_info >> 32 type = r_info & 0xFFFFFFFF Type : 重定位类型,如R_ X86_ 64_ PLT32 Sym. Name + Addend : 符号名和加数 .symtab节分析 符号表结构: 使用 readelf -s test.o 查看符号表: 关键字段: st_name : 字符串表中的索引 st_shndx : 符号所在节的序号 SHN_UNDEF : 符号未在当前文件定义 SHN_ABS : 符号是绝对地址 SHN_COMMON : 符号用于定义对齐字节 st_value : 符号相对于所在节起始地址的偏移 重定位机制详解 重定位类型说明 根据Intel MPX Linux64 ABI文档,重定位类型含义如下: R_X86_64_PLT32 (4): PLT32重定位 R_X86_64_PC32 (2): PC相对32位重定位 计算符号说明: A : 被加数 B : 共享对象基地址 G : 符号在GOT中的偏移 L : PLT条目位置 P : 重定位存储单元位置 S : 符号值 Z : 符号大小 程序内重定向 示例:main函数调用test_ func_ call 重定位表项: 对应汇编: 计算规则: 由于没有PLT表,简化为: 其中: sym_real_address : 符号的真实内存地址 patch_real_address : 需要修改的内存地址 r_addend : 加数(这里是-4) 程序外重定向 示例:调用外部函数如hello_ world 更简单,直接计算相对偏移: 其中4/8取决于操作数长度(32位用4,64位用8) 实现总结 解析ELF文件,获取.text节和.rela.text节 处理重定位表: 区分程序内和程序外重定向 根据不同类型应用相应计算规则 修正call指令的操作数 将修正后的代码加载到内存执行 项目实现已用Rust编写,地址:https://github.com/Sndav/coffee 关键点回顾 ELF文件结构分析,特别是.rela和.symtab节 重定位机制的理解和计算规则 区分程序内和程序外重定向的不同处理方式 call指令操作数的修正方法 实际实现时的内存地址计算 通过以上分析,我们可以实现一个功能完整的Linux Object文件加载器,为安全研究提供更多可能性。