TheRealJoelmatic/Linux-SO-Injector

GitHub: TheRealJoelmatic/Linux-SO-Injector

一款轻量级 Linux .so 动态库注入器,通过 ptrace 和 shellcode 技术将共享库安全地加载到运行中的进程内并执行指定入口函数。

Stars: 0 | Forks: 0

# SO_Injector 轻量级 Linux .so 注入器,带有 ImGui 前端。可附加到运行中的进程,在其内部调用 `dlopen()`,并可选择在已加载的库中启动一个 `entry` 线程。 已在 Arch Linux x86_64 (Linux 7.0.12-arch1-1) 上测试 ![注入](https://static.pigsec.cn/wp-content/uploads/repos/2026/06/9e36d6ce04012912.png) ![主屏幕](https://static.pigsec.cn/wp-content/uploads/repos/2026/06/2588f6e35a012930.png) ## 目录 - [快速开始](#quick-start) - [发布二进制文件](#release-binary) - [环境要求与依赖](#requirements--deps) - [工作原理](#How-it-works) - [概述与安全性](#overview-and-safety) - [高层流程](#high-level-flow) - [关键实现细节与 shellcode](#key-implementation-details-and-shellcodes) - [Path page 与 trampoline](#path-page-and-trampolines) - [为什么 exec region shellcode 和栈空间余量很重要](#why-exec-region-shellcode-and-stack-margin-matter) - [使用说明](#usage-notes) - [故障排除](#troubleshooting) - [致谢 / 参考资料](#credits--references) ## 快速开始 构建并运行 GUI: ``` git clone https://github.com/TheRealJoelmatic/Linux-SO-Injector.git cd Linux-SO-Injector/code/SO_Injector mkdir cmake-build-debug cd cmake-build-debug cmake .. ninja sudo setcap cap_sys_ptrace,cap_dac_read_search=eip ./SO_Injector ./SO_Injector ``` ## 发布二进制文件 如果你从 GitHub 下载了预编译的发布二进制文件,该注入器必须具有 cap_sys_ptrace, cap_dac_read_search=eip 权限。 可以像这样进行设置: ``` sudo setcap cap_sys_ptrace,cap_dac_read_search=eip ./SO_Injector ``` ## 环境要求与依赖 - GLFW 开发头文件(libglfw3-dev 或同等替代品) - OpenGL 开发库(libGL, libglvnd 或 mesa 开发包) - 确保二进制文件包含导出的函数 ``` extern "C" void entry() { notify("entry() called, injection successful!"); } ``` ## 工作原理 ## 概述与安全性 该注入器使用 `ptrace()` 在目标进程中执行进程内的 `dlopen()` 和 `dlsym()` 调用,通过在目标内部写入并执行一小段机器码(shellcode)来实现。这种方法避免了从匿名的 mmap 页面运行复杂的库加载序列(这可能会导致 glibc 崩溃),并试图通过在注入前推进到安全的系统调用退出停止点(syscall-exit stop)来尽量减少对目标的干扰。 ## 高层流程 1. 解析所提供的 `.so` 的绝对路径,并检查它是否已经被加载(扫描 `/proc//maps`)。 2. 通过偏移技巧在目标中解析 `dlopen`、`dlsym` 和 `pthread_create` 的地址:读取本地的 libc 基址和本地符号地址,计算偏移量,并将其添加到目标的 libc 基址中。 3. 使用 `ptrace` 附加到目标进程,并调用 `advanceToSyscall()`,该函数执行两次 `PTRACE_SYSCALL` 停止,以停留在系统调用退出边界处(这是 glibc 不持有内部锁的安全点)。 4. 在目标(`r-xp` 映射)中寻找一个可执行区域(exec region),并备份少量字节(48 字节)以供稍后恢复。 5. 将一个 3 字节的 syscall trampoline(`syscall; int3`)写入 exec region,并使用它在目标中调用 `mmap` 以获取一个可写的 path page。 6. 将 `.so` 路径写入目标的 path page,并将 `k_dlopen_shellcode` 写入 exec region;设置寄存器,使得 shellcode 使用目标自身的栈(带有巨大的额外余量)调用 `dlopen(path, RTLD_LAZY)` 并执行它。 7. 如果 `dlopen` 返回了一个句柄,并且请求了入口函数,则通过 `k_dlsym_shellcode` 调用 `dlsym(handle, name)` 以获取入口符号指针。 8. 如果入口函数存在且可以使用 `pthread_create`,则在目标中创建一个小的 RWX trampoline 页面,写入一个调用入口函数并返回 `NULL` 的简短 stub(以便满足 `void*(*)(void*)` 线程原型),然后从 `k_pthread_shellcode` 调用 `pthread_create`,将该 trampoline 作为启动例程(start routine)传入。 9. Munmap 临时 path page(尽力而为),恢复原始的 exec 字节,恢复寄存器,然后分离(detach)。 ## 关键实现细节与 shellcode 注入器使用了从目标自身的可执行区域执行的几段小型机器码片段。它们的高层目的和寄存器约定如下: - `k_trampoline`(3 字节):`syscall; int3` - 目的:用于从目标执行系统调用(`mmap`、`munmap`)。将其写入 exec 页面并调用是安全的,因为它使用的是内核的系统调用路径。 - `k_dlopen_shellcode`(约 29 字节): - 将原始 `rsp` 保存到 `r15` 中,从 `rsp` 中减去一个巨大的余量(0x20000 = 128 KB)然后进行对齐,将 `rdi` 设置为路径指针(`r12`),将 `rsi` 设置为 `RTLD_LAZY`,并 `call r14`(被设置为目标 `dlopen` 的地址)。调用之后,它恢复 `rsp` 并触发 `int3` 以将控制权交还给注入器。 - 恢复前的寄存器约定: - `r12` = pathPage 地址(指向 .so 路径的指针) - `r14` = 目标 `dlopen` 地址 - `rip` = 写入了 `k_dlopen_shellcode` 的 exec trampoline 地址 - `k_dlsym_shellcode`(约 27 字节):模式类似,但设置 `r12` = libHandle,`r13` = namePtr,`r14` = dlsym 地址;结果在 `rax` 中返回。 - `k_pthread_shellcode`(约 33 字节):为 `pthread_create(pthread_t*, attr, start_routine, arg)` 设置参数——它将 `pthread_t*` 放入 `rdi`(来自 `r12`),`rsi = NULL`,`rdx = start_routine (r14)`,`rcx = NULL`,然后 `call r13`,其中 `r13` 是目标 `pthread_create` 的地址。使用相同的 `rsp` 余量技巧,并以 `int3` 结束。 ## Path page 与 trampoline 注入器在目标内部创建一个 4KB 的 `pathPage`(通过 `mmap`),其布局如下: - 偏移量 0:完整的 `.so` 路径字符串(供 `dlopen` 使用) - 偏移量 512:入口函数名字符串(供 `dlsym` 使用) - 偏移量 768:8 个保留字节,用于 `pthread_t` 存储 因为 `dlopen()` 和 `dlsym()` 必须在 glibc 能找到调用者的 link_map 的上下文中被调用,所以 dlopen/dlsym shellcode 是从目标现有的可执行映射(而不是匿名 `mmap` 页面)执行的。从 exec region 调用可以避免在从匿名页面调用时,glibc 的 RETURN_ADDRESS(0) 查找失败而导致的罕见崩溃。 ## 将 `extern "C" void entry()` 适配为 `pthread_create` `pthread_create` 需要一个签名为 `void*(*)(void*)` 的启动例程。许多用户库提供的是 `extern "C" void entry()`(无参数,void 返回)。为了安全地将此类函数作为线程启动,注入器在目标中创建了一个小的 RWX stub,其机器码等价于: ``` movabs rax, sub rsp, 8 call rax xor eax, eax ; return NULL add rsp, 8 ret ``` 这个 stub 被写入目标中的一个持久化 RWX 页面,该 stub 指针作为 `start_routine` 传递给 `pthread_create`。该 stub 处理栈对齐并返回一个 NULL 指针,以便 `pthread_create` 能看到一个格式正确的 `void*` 返回值。 ## 为什么 exec region shellcode 和栈空间余量很重要 - Exec region:现代 glibc 使用调用者的返回地址和 link_map 来确定 `dlopen` 期间的符号命名空间。从动态加载器未知的匿名 mmap 页面调用 `dlopen` 可能会触及内部代码路径,导致解引用空指针并崩溃。从目标真实的 exec 映射执行 shellcode 可以避免这种情况。 - 栈空间余量:`dlopen` 会触发复杂的 glibc 内部操作和深层调用链。从一个小的私有栈(例如 `mmap` 分配的栈)执行 `dlopen` 可能会违反 glibc 的栈边界检查或基于 TLS 的假设;相反,shellcode 会保存真实的 `rsp`,减去一个巨大的余量(128 KB)以提供额外的腾出空间,进行对齐、调用,然后恢复 `rsp`。 ## 并发与安全性 - 注入器在附加后立即调用 `advanceToSyscall()`,因此目标会停止在系统调用退出边界处。这避免了在 glibc 持有内部锁时进行注入,从而防止可能导致死锁或崩溃的情况。 - 注入器会备份它在 exec region 中覆盖的少量字节,并在完成后恢复它们。 ## 使用说明 - 入口函数必须从共享对象(shared object)中导出(使用 `nm -D libyour.so` 或 `readelf -s` 检查)。 - 使用 syslog 来观察注入库的输出(`test_So` 中包含的 `notify()` 辅助函数示例)。 - 注入器会将最后使用的 `.so` 路径保存在 `~/.config/so_injector/config` 中。 ## 故障排除 - 如果 `dlopen` 失败:运行 `ldd libyour.so` 并确保目标进程可以使用所有依赖项。 - 如果拒绝注入:检查 `/proc/sys/kernel/yama/ptrace_scope` 或按照发布章节中所示设置 capabilities。 - 如果入口未被调用:确认符号名称(区分大小写)以及它已被导出(动态符号表)。 ## 致谢 / 参考资料 https://github.com/gaffe23/linux-inject https://github.com/ParkHanbum/linux_so_injector https://github.com/ilammy/linux-crt
标签:Bash脚本, ImGui, Ptrace, SSH蜜罐, 共享库, 动态注入, 系统编程, 进程注入