内存隐身的Linux ELF注入技术解析(C/C++代码实现)
在Linux系统安全领域,进程注入技术是渗透测试、恶意代码分析与系统防护的核心研究方向之一。ELF-Injector 作为一款针对x86_64架构的ELF共享对象注入工具,通过精巧的设计实现了“隐蔽注入+独立线程运行”的核心目标,既能在目标进程中加载恶意共享库,又能通过伪装名称规避常规检测。本文将从技术原理、设计思路、核心知识点等维度,全面解析ELF-Injector的工作机制。
一、ELF-Injector的核心目标与应用场景
在深入原理前,首先需明确ELF-Injector的定位:它并非传统意义上的“恶意工具”,而是一款用于研究Linux进程注入技术的实验性工具,其核心目标是将ELF格式的共享对象(.so文件)注入到指定的目标进程中,并让共享对象中的特定函数以独立线程的形式运行,同时通过“伪造名称”降低被发现的概率。
其典型应用场景包括:
- 安全测试:验证目标程序的进程防护能力(如是否能检测到外来共享库注入);
- 调试与监控:在不修改目标程序源码的情况下,为其添加监控逻辑(如日志记录、性能统计);
- 隐蔽持久化:在已获取系统权限的场景下,将功能模块注入到系统常驻进程(如gedit、pulseaudio)中,实现长期运行。
二、ELF-Injector实现的核心技术原理
ELF-Injector的设计围绕“无文件残留+进程内隐蔽执行”展开,整个注入流程可拆解为5个关键步骤,每个步骤都依赖Linux内核提供的底层接口与ELF文件格式特性。
1. 前置准备:目标进程线程全暂停与信息扫描
注入的前提是“控制目标进程”——若目标进程的线程仍在运行,注入过程中可能出现内存竞争(如修改内存时线程正在读写该区域),导致注入失败或目标进程崩溃。ELF-Injector通过以下操作解决此问题:
(1)线程枚举与全暂停
- 线程枚举:通过读取
/proc/<目标PID>/task/目录下的文件(该目录下的文件名即线程TID),获取目标进程的所有线程ID; - 全暂停:调用
ptrace(PTRACE_ATTACH, TID, ...)对每个线程进行“附加”(类似调试器附加进程),附加后线程会暂停执行; - 等待确认:通过
wait()系统调用等待线程暂停信号,确保所有线程(尤其是主线程)都处于可控状态。
(2)目标进程关键信息扫描
在控制目标进程后,ELF-Injector需要获取两个核心信息:
- libc库中的
__libc_dlopen_mode函数地址:dlopen是Linux动态加载共享库的核心函数,而__libc_dlopen_mode是libc内部实现该功能的接口,ELF-Injector通过它在目标进程中加载共享库; - 目标进程的内存映射表:通过读取
/proc/<目标PID>/maps文件,获取目标进程已加载的库(如libc、libpthread)的基地址,为后续函数地址计算提供依据。
2. 共享对象预处理:内存文件(memfd)封装
传统注入工具常将共享库写入磁盘文件,再通过dlopen加载——这种方式会留下文件痕迹,容易被检测。ELF-Injector采用内存文件(memfd) 技术规避此问题:
(1)memfd_create系统调用的作用
memfd_create(系统调用号319)是Linux 3.17+提供的接口,用于创建一个“仅存在于内存中的匿名文件”,其特性包括:
- 无磁盘路径,仅通过文件描述符(fd)访问;
- 进程退出后自动销毁,无残留;
- 支持像普通文件一样读写、映射(mmap)。
(2)共享对象的内存封装流程
- 读取本地ELF共享库文件(用户指定的
-b参数路径); - 调用
memfd_create创建内存文件,指定伪造名称(如默认的“pulseaudio”,用户可通过-n参数自定义); - 将ELF文件内容写入内存文件,形成“内存中的共享库”;
- 通过
/proc/<当前PID>/fd/<memfd_fd>路径,让目标进程可访问该内存文件(Linux下,进程的fd可通过proc文件系统被其他进程访问)。
3. 核心注入:劫持目标进程动态加载共享库
ELF-Injector的核心创新在于“在目标进程内部调用dlopen”——通过修改目标进程的寄存器上下文,“欺骗”目标进程执行dlopen,加载ELF-Injector准备好的内存共享库。
(1)寄存器劫持的原理
Linux下,进程的执行状态由寄存器(如rip指令指针、rdi/rsi参数寄存器、rax系统调用号寄存器)决定。ELF-Injector通过ptrace工具修改目标进程的寄存器,实现“强制调用函数”:
- 保存原始寄存器:通过
ptrace(PTRACE_GETREGS, ...)读取目标进程当前的寄存器状态,后续需恢复,避免影响目标进程正常运行; - 构造函数调用参数:
rdi:指向内存共享库的路径(即/proc/<当前PID>/fd/<memfd_fd>);rsi:dlopen的加载标志(RTLD_LAZY,表示延迟解析符号,加快加载速度);rip:指向__libc_dlopen_mode函数地址(目标进程中libc的基地址 + 函数在libc中的偏移);
- 设置返回地址与断点:
- 在目标进程栈上写入“原始
rip地址”(函数调用完成后需回到原指令继续执行); - 在原始
rip地址处写入断点指令(0xCC,即int3),确保函数调用完成后目标进程会暂停,方便恢复寄存器;
- 在目标进程栈上写入“原始
- 执行函数调用:通过
ptrace(PTRACE_SETREGS, ...)应用修改后的寄存器,再调用ptrace(PTRACE_CONT, ...)让目标进程继续执行——此时目标进程会先执行dlopen,加载内存共享库,再因断点暂停。
(2)共享库加载结果确认
dlopen返回的“共享库句柄”是一个 opaque 指针(难以直接解析),ELF-Injector通过再次读取目标进程的/proc/<目标PID>/maps文件,确认共享库已成功加载(查找伪造名称对应的内存映射项),并获取其在目标进程中的基地址。
4. 独立线程创建:劫持pthread_create
共享库加载后,其内部函数需以“独立线程”形式运行——若直接在目标进程的主线程中执行,会阻塞目标进程的正常逻辑。ELF-Injector通过劫持pthread_create实现线程创建:
(1)pthread_create的作用
pthread_create是POSIX线程库(libpthread)的核心函数,用于创建新线程,其参数包括:
- 线程ID指针(
pthread_t *thread); - 线程属性(
const pthread_attr_t *attr,通常为NULL); - 线程入口函数(
void *(*start_routine)(void *)); - 入口函数参数(
void *arg)。
(2)线程创建的劫持流程
- 检查目标进程是否加载libpthread:若未加载,通过
dlopen动态加载libpthread.so.0(同步骤3的劫持逻辑); - 获取pthread_create函数地址:通过扫描目标进程的
/proc/<目标PID>/maps和libpthread的ELF动态段(PT_DYNAMIC),计算pthread_create的地址; - 构造线程参数:
rdi:指向目标进程中通过mmap分配的线程ID内存地址;rdx:指向共享库中用户指定的入口函数(-f参数指定的函数名,地址=共享库基地址+函数在ELF中的偏移);
- 劫持执行pthread_create:同步骤3的寄存器修改逻辑,让目标进程执行
pthread_create,创建以共享库函数为入口的新线程; - 恢复目标进程:线程创建完成后,恢复目标进程的原始寄存器状态,移除断点,调用
ptrace(PTRACE_DETACH)解除附加,让目标进程正常运行——此时新线程已在后台独立执行。
5. 隐蔽性设计:名称伪造与无文件残留
ELF-Injector的隐蔽性主要依赖两个设计:
- 内存文件名称伪造:
memfd_create指定的伪造名称(如“pulseaudio”)会显示在目标进程的/proc/<目标PID>/maps中,伪装成系统正常库(pulseaudio是Linux常见的音频服务); - 无磁盘残留:共享库始终在内存中,未写入磁盘,传统的文件监控工具(如
find、ls)无法检测; - 线程独立运行:新线程与目标进程的主线程分离,不影响目标进程的正常功能,降低被用户察觉的概率。
三、相关领域核心知识点
理解ELF-Injector需要掌握Linux系统编程、ELF文件格式、动态链接等领域的基础知识点,以下是关键概念解析:
1. ptrace系统调用:进程控制的“瑞士军刀”
ptrace是Linux提供的进程调试接口,也是注入工具的核心依赖,其常用命令包括:
PTRACE_ATTACH:附加到目标进程/线程,使其暂停;PTRACE_GETREGS/PTRACE_SETREGS:读取/修改目标进程的寄存器上下文;PTRACE_PEEKTEXT/PTRACE_POKETEXT:读取/修改目标进程的内存数据;PTRACE_SINGLESTEP:单步执行目标进程指令;PTRACE_CONT:让暂停的进程继续执行。
注意:ptrace存在权限限制——普通用户只能附加到自己的进程,root用户可附加到任意进程。
2. ELF文件格式:动态链接的基础
ELF(Executable and Linkable Format)是Linux下可执行文件、共享库的标准格式,ELF-Injector依赖其以下结构:
- ELF头部(Elf64_Ehdr):存储文件类型(如ET_DYN表示共享库)、程序头表偏移(e_phoff)、节头表偏移(e_shoff)等;
- 程序头表(Elf64_Phdr):描述文件的内存映射信息,其中
PT_DYNAMIC类型的程序头指向动态段; - 动态段(Elf64_Dyn):存储动态链接相关信息,如
DT_STRTAB(动态字符串表地址)、DT_SYMTAB(动态符号表地址)——ELF-Injector通过这两个表查找共享库中的函数偏移; - 符号表(Elf64_Sym):存储函数、变量的符号信息,包括名称偏移(st_name)、地址偏移(st_value)。
3. 动态链接与dlopen系列函数
Linux程序的动态链接分为“加载时链接”(程序启动时加载依赖库)和“运行时链接”(程序运行中通过dlopen加载库),核心函数包括:
dlopen:加载共享库,返回库句柄;dlsym:通过库句柄和符号名,获取函数/变量地址;dlclose:卸载共享库;dlerror:获取动态链接错误信息。
ELF-Injector使用__libc_dlopen_mode而非直接使用dlopen,是因为dlopen通常是libc的导出函数(通过动态符号表可见),而__libc_dlopen_mode是内部实现接口,部分系统中未导出——ELF-Injector通过扫描libc的动态段找到其地址。
4. proc文件系统:进程信息的“窗口”
/proc是Linux的虚拟文件系统,存储当前系统所有进程的实时信息,ELF-Injector核心依赖以下文件:
/proc/<PID>/maps:目标进程的内存映射表,包括每个映射区域的地址范围、权限、对应的文件路径;/proc/<PID>/task/:目标进程的所有线程目录,目录名即TID;/proc/<PID>/fd/<FD>:目标进程的文件描述符对应的路径,ELF-Injector通过它让目标进程访问内存文件。
四、ELF-Injector的设计思路与优缺点
ELF-Injector的设计围绕“隐蔽性”和“稳定性”展开,其思路可总结为“借力系统接口,最小化侵入”——尽量使用Linux原生接口(如memfd、ptrace、dlopen),避免自定义复杂逻辑,降低注入失败概率。
...
enum lib_type {GLIBC, PTHREAD, EVIL};
typedef struct
{
char *func_name;
uint64_t func_addr;
struct user_regs_struct params;
} function_t;
typedef struct
{
char *lib_name;
uint64_t lib_addr;
Elf64_Dyn *dynamic_segmt;
Elf64_Sym *symtab;
char *strtab;
function_t *func;
} library_t;
typedef struct
{
pid_t pid;
int traced;
struct user_regs_struct syscall;
library_t *lib[4];
} process_t;
typedef struct
{
char *path;
char *name;
int memfd;
size_t size;
char *function;
uint8_t *mem;
uint64_t offset;
} binary_t;
extern binary_t * load_payload(const char *, const char *, const char *);
extern library_t * scan_library(const char *, const char *, const pid_t);
extern void __create_thread(process_t *, binary_t *);
extern bool __dl_open(process_t *, const char *, const int, const char *);
extern bool attach_to_process(process_t *);
...
int main (int argc, char ** argv)
{
...
while ((c = getopt(argc, argv, "p:b:f:n:")) != -1)
{
switch(c)
{
case 'p':
victim.pid = atoi(optarg);
break;
case 'b':
str_path = optarg;
break;
case 'f':
str_fname = optarg;
break;
case 'n':
str_bname = optarg;
break;
default:
usage();
}
}
if (str_bname == NULL)
{
str_bname = "pulseaudio";
}
if (victim.pid <= 0 || str_path == NULL || str_fname == NULL)
{
usage();
}
char path[PATH_MAX + 1] = {0};
victim.lib[GLIBC] = scan_library("libc-", "__libc_dlopen_mode", victim.pid);
if (victim.lib[GLIBC] == NULL)
{
aux_exit(NULL, &victim, NULL);
}
//------- 二进制注入 --------
if ((p_bin = load_payload(str_path, str_fname, str_bname)) == NULL)
{
aux_exit("load_payload() Failed", &victim, NULL);
}
if (attach_to_process(&victim) == false)
{
aux_exit("Attach_to_process() Failed", &victim, p_bin);
}
sprintf(path, "/proc/%d/fd/%d", getpid(), p_bin->memfd);
if (__dl_open(&victim, path, EVIL, p_bin->name) == false)
{
fprintf(stderr, "[**] Error: __dl_open(%s) Failed\n", path);
aux_exit(NULL, &victim, p_bin);
}
// ------ 线程注入和劫持 ------
victim.lib[PTHREAD] = scan_library("libpthread", "pthread_create", victim.pid);
if (victim.lib[PTHREAD] == NULL)
{
if (__dl_open(&victim, "libpthread.so.0", PTHREAD, "libpthread") == false)
{
fprintf(stderr, "[**] Error: __dl_open(libpthread.so.0) Failed\n");
aux_exit(NULL, &victim, p_bin);
}
victim.lib[PTHREAD] = scan_library("libpthread", "pthread_create", victim.pid);
if (victim.lib[PTHREAD] == NULL)
{
aux_exit(NULL, &victim, p_bin);
}
}
__create_thread(&victim, p_bin);
aux_exit("\t*** OK ***\n", NULL, NULL);
}
If you need the complete source code, please add the WeChat number (c17865354792)
Usage:
Usage: ./ELF-Injector <option> <value>...
-p Target process id
-b Elf binary path
-f Elf binary function's name
-n File fake name (optional)
Demo:
./ELF-Injector -p $(pidof gedit) -f main -b ./dummy -n fakename1
[*]0x7f32b8e88450 <- __libc_dlopen_mode()
[*]0x000000000630 <- main() offset
[*]Valid ELF binary
[*]Thread 2365 stopped
[*]Thread 2366 stopped
[*]Thread 2367 stopped
[*]Thread 2368 stopped
[*]Thread 2370 stopped
[*]Attached!
[*]0x7f32a8002000 <- String Injected!
[*]0x7f3299e22000 <- 'fakename1' Injected!
[*]0x7f32b5eb49b0 <- pthread_create()
cat /proc/$(pidof gedit)/maps
555e8fb70000-555e8fb72000 r-xp 00000000 fc:01 1704296 /usr/bin/gedit
555e8fd71000-555e8fd72000 r--p 00001000 fc:01 1704296 /usr/bin/gedit
555e8fd72000-555e8fd73000 rw-p 00002000 fc:01 1704296 /usr/bin/gedit
555e90326000-555e9104c000 rw-p 00000000 00:00 0 [heap]
...
...
7f3298ca0000-7f3298ca1000 r-xp 00000000 00:05 65193 /memfd:fakename2 (deleted)
7f3298ca1000-7f3298ea0000 ---p 00001000 00:05 65193 /memfd:fakename2 (deleted)
7f3298ea0000-7f3298ea1000 r--p 00000000 00:05 65193 /memfd:fakename2 (deleted)
7f3298ea1000-7f3298ea2000 rw-p 00001000 00:05 65193 /memfd:fakename2 (deleted)
...
...
...
7f3299e22000-7f3299e23000 r-xp 00000000 00:05 62901 /memfd:fakename1 (deleted)
7f3299e23000-7f329a022000 ---p 00001000 00:05 62901 /memfd:fakename1 (deleted)
7f329a022000-7f329a023000 r--p 00000000 00:05 62901 /memfd:fakename1 (deleted)
7f329a023000-7f329a024000 rw-p 00001000 00:05 62901 /memfd:fakename1 (deleted)
1. 设计亮点
- 无文件残留:通过memfd避免磁盘文件,降低检测风险;
- 线程独立执行:不阻塞目标进程主线程,提高稳定性;
- 名称伪造:伪装成系统常见库,规避常规监控;
- 兼容性处理:自动检测目标进程是否加载libpthread,无则动态加载,提高适配性。
2. 局限性与改进方向
- 依赖ptrace:部分系统可能通过
ptrace_scope(/proc/sys/kernel/yama/ptrace_scope)限制ptrace使用(如仅允许子进程附加),导致注入失败; - 架构依赖:仅支持x86_64架构,无法在ARM等架构上运行;
- 共享库依赖:要求目标进程已加载libc(几乎所有Linux程序都满足),但部分静态编译的程序无法注入;
- 断点可能被检测:
0xCC断点指令可能被目标进程的反调试逻辑检测到。
改进方向可包括:
- 使用
int 3以外的断点方式(如内存页权限修改); - 采用“内联钩子”(Inline Hook)技术,替换目标进程已有函数的指令,而非依赖
ptrace; - 支持更多架构(如ARM64)。
总结
ELF-Injector作为一款ELF注入工具,其核心价值在于将复杂的Linux进程注入技术模块化、可复用——从线程控制、内存文件封装,到寄存器劫持、动态加载,每个步骤都体现了对Linux系统接口的深入理解。
对于安全研究者而言,ELF-Injector不仅是一款工具,更是学习“进程注入原理”的绝佳案例:它涵盖了ptrace调试、ELF动态链接、proc文件系统、memfd等多个核心知识点,且设计思路贴近实战场景(隐蔽性、稳定性)。
需要强调的是,进程注入技术本身无“善恶”之分——它可用于合法的调试、监控,也可被恶意代码利用。在实际使用中,需严格遵守法律法规,仅在授权环境下进行测试,避免侵犯他人系统安全。
Welcome to follow WeChat official account【程序猿编码】
更多推荐


所有评论(0)