在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)共享对象的内存封装流程
  1. 读取本地ELF共享库文件(用户指定的-b参数路径);
  2. 调用memfd_create创建内存文件,指定伪造名称(如默认的“pulseaudio”,用户可通过-n参数自定义);
  3. 将ELF文件内容写入内存文件,形成“内存中的共享库”;
  4. 通过/proc/<当前PID>/fd/<memfd_fd>路径,让目标进程可访问该内存文件(Linux下,进程的fd可通过proc文件系统被其他进程访问)。

3. 核心注入:劫持目标进程动态加载共享库

ELF-Injector的核心创新在于“在目标进程内部调用dlopen”——通过修改目标进程的寄存器上下文,“欺骗”目标进程执行dlopen,加载ELF-Injector准备好的内存共享库。

(1)寄存器劫持的原理

Linux下,进程的执行状态由寄存器(如rip指令指针、rdi/rsi参数寄存器、rax系统调用号寄存器)决定。ELF-Injector通过ptrace工具修改目标进程的寄存器,实现“强制调用函数”:

  1. 保存原始寄存器:通过ptrace(PTRACE_GETREGS, ...)读取目标进程当前的寄存器状态,后续需恢复,避免影响目标进程正常运行;
  2. 构造函数调用参数
    • rdi:指向内存共享库的路径(即/proc/<当前PID>/fd/<memfd_fd>);
    • rsidlopen的加载标志(RTLD_LAZY,表示延迟解析符号,加快加载速度);
    • rip:指向__libc_dlopen_mode函数地址(目标进程中libc的基地址 + 函数在libc中的偏移);
  3. 设置返回地址与断点
    • 在目标进程栈上写入“原始rip地址”(函数调用完成后需回到原指令继续执行);
    • 在原始rip地址处写入断点指令(0xCC,即int3),确保函数调用完成后目标进程会暂停,方便恢复寄存器;
  4. 执行函数调用:通过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)线程创建的劫持流程
  1. 检查目标进程是否加载libpthread:若未加载,通过dlopen动态加载libpthread.so.0(同步骤3的劫持逻辑);
  2. 获取pthread_create函数地址:通过扫描目标进程的/proc/<目标PID>/maps和libpthread的ELF动态段(PT_DYNAMIC),计算pthread_create的地址;
  3. 构造线程参数
    • rdi:指向目标进程中通过mmap分配的线程ID内存地址;
    • rdx:指向共享库中用户指定的入口函数(-f参数指定的函数名,地址=共享库基地址+函数在ELF中的偏移);
  4. 劫持执行pthread_create:同步骤3的寄存器修改逻辑,让目标进程执行pthread_create,创建以共享库函数为入口的新线程;
  5. 恢复目标进程:线程创建完成后,恢复目标进程的原始寄存器状态,移除断点,调用ptrace(PTRACE_DETACH)解除附加,让目标进程正常运行——此时新线程已在后台独立执行。

5. 隐蔽性设计:名称伪造与无文件残留

ELF-Injector的隐蔽性主要依赖两个设计:

  • 内存文件名称伪造memfd_create指定的伪造名称(如“pulseaudio”)会显示在目标进程的/proc/<目标PID>/maps中,伪装成系统正常库(pulseaudio是Linux常见的音频服务);
  • 无磁盘残留:共享库始终在内存中,未写入磁盘,传统的文件监控工具(如findls)无法检测;
  • 线程独立运行:新线程与目标进程的主线程分离,不影响目标进程的正常功能,降低被用户察觉的概率。

三、相关领域核心知识点

理解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【程序猿编码

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐