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) 上测试


## 目录
- [快速开始](#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蜜罐, 共享库, 动态注入, 系统编程, 进程注入