CyberSnakeH/Specter
GitHub: CyberSnakeH/Specter
Specter 是一个基于 ptrace 的 Linux 共享库注入器,用纯安全 Rust 实现,可将任意 `.so` 库注入正在运行的进程。
Stars: 1 | Forks: 0
# Specter
一个基于 ptrace 的共享对象(`.so`)注入器,适用于 **Linux x86-64** —— 这是 Windows DLL 注入在 Linux 上的对应实现。它会强制一个*正在运行*的目标进程去 `dlopen()` 一个任意的库,这是实现诸如将 ImGui 覆盖层注入到另一个进程之类操作的基础。
```
specter [--global] [--timeout SECONDS] [--retries N] [-v|-vv|-vvv]
```
## 工作原理
1. **解析 loader。** 通过读取 `/proc//maps`、定位目标使用的确切 libc 镜像并解析该 ELF 的动态符号表,*在目标进程内部*解析 `dlopen`(或 `__libc_dlopen_mode`)。我们读取的是目标进程*自己的* libc,因此 libc 版本/ABI 的差异永远不会产生影响。
2. **在安全点冻结整个线程组。** 目标进程的每个线程都会被 `PTRACE_SEIZE` + `PTRACE_INTERRUPT` 停止(反复扫描 `/proc//task` 直到没有新线程出现),因此在我们操作时没有任何兄弟线程在运行。只有在*没有*线程于动态 loader 内部执行时我们才会进行冻结 —— 否则某个线程可能会持有我们伪造的 `dlopen` 所需的 loader 锁,因此我们会释放并重试,直到获得一个完全干净的时机。线程组 leader 的寄存器会被快照保存,并且 RAII 守卫机制可以保证即使后续步骤失败,它们也能被恢复,并且每个线程都会被解除挂起(detach)。
3. **暂存路径。** `.so` 路径会被写入目标进程的栈中,安全地位于 ABI red zone 之下。
4. **带有看门狗(watchdog)的调用。** 通过设置寄存器和一个 `NULL` 返回地址,伪造出一个针对 `dlopen(path, flags)` 的 System V AMD64 调用,因此该调用会返回到一个未映射的页面并确定性地陷入陷阱;随后 `RAX` 就会持有该句柄。该调用受 `--timeout` 限制:如果它超时(由于阻塞的 payload 构造函数,或 loader 锁竞争),目标进程会被中断并回滚,而不是一直保持冻结状态。
5. **恢复并解除挂起。** leader 原本的寄存器会被恢复,并且每个线程都会被解除挂起,从而干净地恢复整个进程的运行。
如果 `dlopen` 在*能够映射该库之前*就发生超时,这表明与目标线程存在 loader 锁竞争,而不是 payload 的问题,因此整个注入过程最多会重试 `--retries` 次(在尝试之间释放目标进程可以让锁的持有者完成执行)。如果在库被映射*之后*发生超时,则意味着 payload 的构造函数发生阻塞,这会被立即上报。
该 crate 是 `#![forbid(unsafe_code)]` 的:每一个特权操作都通过安全的包装器进行(ptrace 使用 `nix`,`/proc//mem` 使用 `std` 文件 I/O,ELF 解析使用 `object`)。
## 构建
```
cargo build --release
# 位于 ./target/release/specter 的 binary
```
## 用法
`ptrace` 受到 Yama LSM 的限制。要挂起到一个非注入器子进程的进程,要么以 root 身份运行,要么放宽该策略:
```
echo 0 | sudo tee /proc/sys/kernel/yama/ptrace_scope
```
然后:
```
# 寻找 target
pidof firefox
# Inject
./target/release/specter 12345 /absolute/path/to/payload.so -v
```
### 选项
| 标志 | 默认值 | 含义 |
|------|---------|---------|
| `--global` | off | 使用 `RTLD_GLOBAL` 进行 `dlopen`(payload 符号全局可见)。 |
| `--timeout SECONDS` | `10` | 每次远程调用的看门狗。`0` 表示永久等待。发生超时时目标会被回滚,永远不会冻结。 |
| `--retries N` | `3` | 针对 loader 锁竞争(即在映射库之前停滞的 `dlopen`)的重试次数。阻塞型的构造函数*不会*被重试。 |
| `-v`/`-vv`/`-vvv` | warn | 日志级别(info / debug / trace);日志输出到 stderr。 |
## 端到端尝试
在 `examples/` 目录下提供了一个演示 payload 和几个受害进程:
```
# 1. 构建 demo payload(从 target 内部写入一个标记文件)
cc -shared -fPIC -o examples/payload.so examples/payload.c
# 2. 构建并启动 victim,记录其 PID
cc -O2 -o examples/victim examples/victim.c
./examples/victim &
VICTIM=$!
# 3. Inject
cargo run --release -- "$VICTIM" "$PWD/examples/payload.so" -vv
# 4. 观察从 victim 内部产生的 side effect
cat /tmp/specter_demo.txt # -> "injected into pid "
```
其他受害者演示了强化机制:`victim_mt.c`(多线程冻结)、`victim_dlsym.c`(loader 锁竞争 → 重试)以及 `payload_blocking.c`(一个行为异常的构造函数,它会触发 `--timeout` 看门狗而不会冻结目标)。
## 编写 payload
`dlopen` 会**同步**运行库的构造函数,因此请将你的入口逻辑放在构造函数中并**快速返回** —— 在构造函数返回之前,注入器的远程调用不会结束。对于任何需要长时间运行的任务(渲染 hook、覆盖层事件循环),请在构造函数中生成一个线程:
```
#include
static void *worker(void *arg) { /* hook the render loop, draw ImGui... */ return NULL; }
__attribute__((constructor))
static void on_load(void) {
pthread_t t;
pthread_create(&t, NULL, worker, NULL); // return immediately
}
```
## 适用范围与限制
* **仅限 x86-64。** 远程调用 ABI 为 System V AMD64。aarch64 将需要其自己的寄存器/ABI 处理。
* **面向 glibc。** 符号解析的目标是 glibc/libdl 命名。如果 musl 在映射的 libc 中导出了 `dlopen`(实际上确实如此),它也可以工作,但测试较少。
* **Loader 锁竞争得到了缓解,但并未消除。** 安全注入点检查可以避免在线程的指令指针位于动态 loader 内部时进行冻结,但线程在 libc 中执行时(例如锁自身的互斥锁代码)也可能持有 loader 锁。这种残留情况会进行反应式处理:`dlopen` 超时,目标被释放并重试注入。malloc-arena 锁是一种类似的、较罕见的隐患,也会被看门狗捕获。
* **超时的调用会留下部分副作用。** 回滚会恢复被劫持线程的寄存器,但已中止的 `dlopen`(例如部分映射的库)已经写入的内存不会被撤销。目标进程会继续运行;仅仅是注入失败。
## 许可证
MIT OR Apache-2.0
标签:Rust, SOC Prime, SSH蜜罐, 动态链接库, 可视化界面, 开发工具, 系统编程, 网络流量审计, 进程注入, 通知系统