1. ELF文件格式概述
ELF(Executable and Linkable Format)是一种用于可执行文件、目标文件和库的标准文件格式,广泛应用于Linux和Unix系统中。ELF文件由多个部分组成,包括文件头(Elf64_Ehdr)、程序头表(Elf64_Phdr)、节区头表(Elf64_Shdr)、符号表(Elf64_Sym)、动态链接表(Elf64_Dyn)等。这些部分共同描述了程序的结构、内存布局和动态链接信息。
2. 动态链接与动态加载
动态链接是现代操作系统中的一种重要技术,允许程序在运行时动态加载和解析所需的库函数。在64位ELF文件中,动态链接的实现主要依赖于以下几个部分:
.plt(Procedure Linkage Table):用于存储动态链接函数的跳转表。每个动态链接函数在.plt中都有一个对应的条目,这些条目在程序运行时会被加载器解析为实际的函数地址。
.got(Global Offset Table):用于存储动态链接函数的地址表。加载器在程序运行时会将.got中的地址解析为实际的函数地址。
.rela.plt:包含与.plt相关的重定位信息,用于加载器在运行时解析动态链接函数的地址。
.dynsym:符号表,存储动态链接函数的名称和地址信息。
.dynstr:字符串表,存储动态链接函数的名称字符串。
3. 补丁程序的实现原理
3.1 读取ELF文件头和节区头
补丁程序首先需要读取ELF文件头(Elf64_Ehdr)和节区头表(Elf64_Shdr),以确定文件的基本信息和各节区的位置。ELF文件头包含了文件的魔数(magic number)、架构类型、入口点地址等信息,而节区头表则描述了每个节区的名称、类型、大小和位置。
3.2 解析动态链接信息
补丁程序通过解析.rela.plt、.dynsym和.dynstr等节区,提取动态链接函数的名称和地址信息。这些信息用于构建一个函数向量(vector_t),其中每个元素包含函数的名称和偏移量。
3.3 构建替换表
补丁程序根据用户输入的旧函数名称和新函数名称,构建一个替换表(repl_t),其中包含旧函数和新函数的偏移量差值。通过遍历函数向量,找到匹配的旧函数和新函数,并计算它们的偏移量差值。
3.4 修改.text节区
补丁程序将ELF文件的.text节区映射到内存中,逐字节扫描并修改旧函数的调用指令。具体步骤如下:
映射.text节区:使用mmap函数将.text节区映射到内存中,以便进行修改。
扫描调用指令:遍历.text节区的每个字节,查找call指令(0xe8)。
计算目标地址:对于每个call指令,计算其目标地址是否为旧函数的地址。如果是,则根据替换表中的差值修改目标地址。
同步修改:将修改后的内存内容同步回文件。
3.5 内存管理
补丁程序使用动态内存分配函数(如malloc、realloc和free)来管理内存。这些函数允许程序在运行时动态分配和释放内存,确保程序的灵活性和效率。
malloc:用于分配指定大小的内存块。
realloc:用于调整已分配内存块的大小。
free:用于释放已分配的内存块。
4. 技术细节
4.1 ELF文件解析
补丁程序通过读取ELF文件头和节区头表,解析文件的基本结构。具体步骤如下:
读取文件头:使用fread函数读取ELF文件头(Elf64_Ehdr),并验证文件的魔数是否正确。
读取节区头表:遍历节区头表,找到.shstrtab、.plt、.rela.plt、.dynsym和.dynstr等节区。
解析动态链接信息:通过读取.rela.plt、.dynsym和.dynstr等节区,提取动态链接函数的名称和地址信息。
4.2 动态内存管理
补丁程序使用动态内存分配函数来管理内存。具体步骤如下:
分配内存:使用malloc函数分配内存块,用于存储函数向量和替换表。
调整内存大小:使用realloc函数调整内存块的大小,以适应动态变化的需求。
释放内存:使用free函数释放已分配的内存块,避免内存泄漏。
4.3 内存映射
补丁程序使用mmap函数将ELF文件的.text节区映射到内存中。具体步骤如下:
计算映射偏移量:根据.text节区的起始地址和页大小,计算映射的偏移量。
映射节区:使用mmap函数将.text节区映射到内存中,以便进行修改。
同步修改:使用msync函数将修改后的内存内容同步回文件。
5. 64位ELF可执行文件的静态补丁程序实现原理(C/C++代码实现
elf_patcher:
...
#define VECTOR_APPEND_TRUE 1
#define VECTOR_APPEND_FALSE 0
#define SUCCESS 0
#define FAIL 1
#define FULL_ERR 2
#define EMPTY_ERR 3
#define OUT_OF_BOUNDS_ERR 4
#define MEM_ERR 5
#define NULL_ERR 6
typedef struct vector vector_t;
struct vector {
char * vector;
size_t data_size;
uint64_t length;
};
int vector_set(vector_t * v, uint64_t pos, char * data);
int vector_add(vector_t * v, uint64_t pos, char * data, uint8_t append);
int vector_rmv(vector_t * v, uint64_t pos);
int vector_get(vector_t * v, uint64_t pos, char * data);
int vector_get_ref(vector_t * v, uint64_t pos, char ** data);
int vector_mov(vector_t * v, uint64_t pos, uint64_t pos_new);
int vector_ini(vector_t * v, size_t data_size);
int vector_end(vector_t * v);
...
/*
* 查看rel.plt或rela.plt,看看.plt和.symtab是如何关联的
*/
typedef struct {
char name[FUNC_NAME_MAX];
uint64_t offset;
} libc_func_t;
typedef struct {
off_t offset_old;
off_t offset_new;
int32_t diff;
} repl_t;
void get_func_vector(Elf64_Ehdr * elf_header, Elf64_Shdr * text_header, FILE * target_elf, vector_t * func_vector);
void patch_elf(vector_t func_vector, repl_t sub_table, Elf64_Ehdr elf_header, Elf64_Shdr text_header, FILE * target_file);
//检查目标是否为有效的ELF
void check_magic_bytes(Elf64_Ehdr * elf_header) {
if ((elf_header->e_ident[EI_MAG0] != ELFMAG0)
|| (elf_header->e_ident[EI_MAG1] != ELFMAG1)
|| (elf_header->e_ident[EI_MAG2] != ELFMAG2)
|| (elf_header->e_ident[EI_MAG3] != ELFMAG3)) {
puts("ELF magic bytes: invalid, is this an ELF file?");
exit(1);
}
}
//将目标标头读入结构体
void read_elf_header(Elf64_Ehdr * elf_header, FILE * target_file) {
size_t ret;
ret = fread(elf_header, sizeof(Elf64_Ehdr), 1, target_file);
if (ferror(target_file)) {
puts("ELF headers: read failed with error.");
exit(1);
}
}
//构建子表
void build_sub_table(int argc, char ** argv, repl_t * sub_table, vector_t func_vector) {
...
for (int i = 0; i < func_vector.length; ++i) {
ret = vector_get(&func_vector, i, (char *) &temp_func);
if (!(strcmp(argv[2], temp_func.name))) {
sub_table->offset_old = temp_func.offset;
++match;
}
if (!(strcmp(argv[3], temp_func.name))) {
sub_table->offset_new = temp_func.offset;
++match;
}
}
if (match != 2) {
puts("Input: unable to find matching old and/or new function symbols");
exit(1);
}
sub_table->diff = sub_table->offset_new - sub_table->offset_old;
}
int main(int argc, char ** argv) {
...
if (argc < 4) {
printf("usage: %s <file> <old_func> <new_func>\n", argv[0]);
return 1;
}
//try to open elf target
target_file = fopen(argv[1], "r+");
if (target_file == NULL) {
perror("target open");
return 1;
}
//init vector
ret = vector_ini(&func_vector, sizeof(libc_func_t));
read_elf_header(&elf_header, target_file);
check_magic_bytes(&elf_header);
get_func_vector(&elf_header, &text_header, target_file, &func_vector);
//构建替换表
build_sub_table(argc, argv, &sub_table, func_vector);
patch_elf(func_vector, sub_table, elf_header, text_header, target_file);
fclose(target_file);
}
测试:
附上了一个示例,将对free()的调用更改为对put()的呼叫。请随意尝试代码并创建自己的可执行文件进行修补。
target:
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
int main() {
char * uwu = "Lain said:";
char * uwu2 = "to the ELFes.";
char * x = malloc(50);
strcpy(x, uwu);
free(x);
puts("\"nothing to see here...\"");
char * y = malloc(50);
strcpy(y, uwu2);
free(y);
return 0;
}
$ ./target
接下来,修补目标:
$ ./elf_patcher target free puts
在补丁之后再次运行目标:
$ ./target
一旦知道了动态函数存根的地址,就可以在程序的.text部分更改调用指令的相对偏移量。例如,如果free()存根位于地址0x20,put()存根在地址0x30,则可以在.text中的调用偏移量中添加0x10,将调用从free()更改为put()
If you need the complete source code, please add the WeChat number (c17865354792)
6. 总结
补丁程序通过解析ELF文件的结构和动态链接信息,构建替换表,并修改.text节区中的函数调用指令,实现了对64位ELF可执行文件的静态补丁功能。该程序利用了ELF文件格式、动态链接技术和动态内存管理等技术,确保了补丁的灵活性和可靠性。
Welcome to follow WeChat official account【程序猿编码】