Appearance
用户态动态库注入(Library Injection)种类与原理
把一段本不属于目标进程的代码,塞进它的地址空间并让它跑起来——这就是"动态库注入"。本文枚举 Linux 用户态常见的几种注入手段,逐层拆解其背后的原理:动态链接器、PLT/GOT、ptrace、进程内存改写。既能用于性能探针、热补丁、沙箱,也可能被恶意软件利用,理解它有助于区分"工具"与"攻击"。
阅读时间:2026-08-05
1. 本篇要回答的问题
- 用户态动态库注入到底有哪几种?各自的前提条件是什么?
LD_PRELOAD为什么能让我的.so抢先加载?它绕过了什么?dlopen运行时加载和注入是什么关系?ptrace注入是怎么在不重启进程的情况下塞进.so的?- 直接写
/proc/pid/mem注入 shellcode 又是怎么回事? - 这些手段对应的"防御/检测"在哪一层?
2. 注入的共性原理:为什么能"塞进去"
无论哪种手法,本质都依赖同一个事实:现代进程的地址空间在运行时是可变的,且加载器允许在运行期引入新的共享库。所有注入都建立在下面三块地基之上:
2.1 动态链接器(ld.so)是可编程的
进程启动时并不是内核直接把程序搬进内存,而是先加载 动态链接器 /lib64/ld-linux-x86-64.so.2,由它根据 .dynamic 段、环境变量、/etc/ld.so.cache 把依赖的 .so 映射进来、做符号重定位。
ld.so 本身暴露了若干"钩子"控制点:
- 环境变量
LD_PRELOAD/LD_LIBRARY_PATH/LD_AUDIT改变加载顺序与路径; - 提供
dlopen()/dlsym()接口让进程自己在运行期加载库; - 提供
LD_AUDIT让外部.so在每个符号绑定/加载事件上插桩。

2.2 PLT/GOT 与懒绑定:符号在运行时才"落地"
调用共享库函数时,编译器生成的是 PLT(过程链接表)+ GOT(全局偏移表) 间接跳转。默认开启 懒绑定(lazy binding):第一次调用某函数时才由 ld.so 解析符号地址写入 GOT。这意味着——
- 运行时换掉 GOT 里的地址,就能重定向函数调用(hook 的底层机制);
- 注入的
.so只要在调用方第一次用到某个函数之前先进场,就能"截胡"。
2.3 进程地址空间可被外部改写
内核通过 ptrace、读写 /proc/pid/mem、/proc/pid/maps 等接口,允许拥有权限的其它进程窥探并修改目标进程的内存。这是 ptrace 注入、/proc/pid/mem 注入的物理基础。
3. 六种注入手段逐一拆解
3.1 LD_PRELOAD —— 最经典、最"光明正大"
bash
# 方法 A:环境变量启动新进程
LD_PRELOAD=/path/to/evil.so ./target_app
# 方法 B:系统级对所有进程生效(需 root,危险)
echo /path/to/evil.so >> /etc/ld.so.preload原理:ld.so 在解析任何依赖之前,先把 LD_PRELOAD 列出的 .so 最先 mmap 进地址空间。由于符号解析遵循"先加载者优先"(全局符号介入,global symbol interposition),你的 .so 里定义的同名函数会覆盖 libc/目标程序里的原始实现。
能做什么:重写 malloc/free(内存统计/检测)、重写 open/read(文件沙箱)、重写 connect(网络重定向)——这正是 asan.md 之外很多性能/调试工具(如 libfaketime、tshark 的 LD_PRELOAD 用法)的机制。
前提:
- 必须是进程启动前设置(子进程会继承,但已运行的进程改不了);
- 受
ld.so安全限制:setuid/setgid 程序会忽略非 root 的LD_PRELOAD(防提权); - 容器里受
ld.so的AT_SECURE标志影响。
防御/检测:/etc/ld.so.preload 是最佳监控点;readelf -d 看 NEEDED、用 LD_DEBUG=libs 可以看到实际加载顺序。
3.2 运行期 dlopen 注入(需程序配合或配合 ptrace)
c
// 目标进程里若主动调用(或你诱导其调用)下面这行,即加载你的 .so
void* h = dlopen("/path/to/hook.so", RTLD_NOW | RTLD_GLOBAL);
void (*init)(void) = dlsym(h, "my_init");
init();原理:dlopen 是 ld.so 暴露给用户态的"运行期加载器"。它内部做的事和启动加载完全一样:映射 .so、解析依赖、执行 .init/构造函数(__attribute__((constructor)))、做符号重定位。注入的 .so 在自身构造函数里即可 hook GOT 或安装信号处理、注册回调。
注入的两种落地方式:
- 程序本身调用 dlopen(如插件系统、热更新框架)——这是合法功能,算不上"注入",是设计如此;
- 借助 ptrace 让目标进程"自己调用 dlopen"(见 3.4)——这才是真正的运行期注入。
3.3 /proc/pid/mem 直接改写(shellcode / GOT 覆写)
原理:/proc/pid/mem 是目标进程整个线性地址空间的"文件视图",有 PTRACE_ATTACH 或等价权限(同 uid 或 root)即可 open 它并用 lseek+write 改写任意虚拟地址的内容。
典型用法:
- GOT 覆写:先读
/proc/pid/maps定位目标模块的 GOT 段,把某个库函数指针改成你的函数地址(地址需已被映射,否则要先把代码写进可执行页); - shellcode 注入:在堆/可写可执行区写入机器码,再用
ptrace或改写返回地址跳过去执行。
前提:需要 ptrace 权限(通常同 uid 或 root,yama 的 ptrace_scope 会限制跨线程附加);写入地址必须可写(或先 mprotect 改页属性——改页属性本身又需要执行代码,形成"先有鸡还是先有蛋",所以纯 /proc/mem 写通常配合 ptrace 改 RIP)。
3.4 ptrace 注入(运行期塞 .so,最主流的"真注入")
ptrace 是调试器(gdb/strace)的核心 syscall,允许一个进程观察/控制另一个进程的执行。用它做注入的标准套路(以 x86-64 为例):

关键技巧:
- 直接把
rip设到dlopen入口,参数(filename字符串、flags)按 System V AMD64 ABI(见 c-abi.md)塞进rdi/rsi,记得先在目标栈上写好字符串; - 更稳妥的做法是
mmap一段内存放一段小型 "loader shellcode",让它依次调用dlopen→dlsym→目标函数,再ptrace跳过去; - 执行完要精确恢复所有寄存器与栈,否则目标进程会崩。
前提:ptrace 权限(同 uid/root,受 kernel.yama.ptrace_scope 限制);目标不能是已经 PTRACE_TRACEME 的(如正在被 gdb 调试)或被 seccomp 禁止。
代表工具:injectso、frida(其早期 Linux 注入也基于 ptrace)、各类热补丁框架。
3.5 LD_AUDIT 审计注入
bash
LD_AUDIT=/path/to/auditor.so ./target原理:ld.so 提供 LD_AUDIT 机制,在每次符号绑定(la_symbind*)、库加载/卸载时回调你 .so 里实现的一组约定函数(la_objsearch / la_activity / la_symbind64 等)。这本质是一种"官方认可的注入点"——你在 la_symbind64 里就能拿到每次符号解析的时机与地址,实现细粒度 hook,且比 LD_PRELOAD 更"低调"(不与目标符号名冲突)。
适用:需要观察"哪些符号被绑定、绑定到哪"的高级场景(如检测 GOT 篡改、做二进制插桩)。
3.6 gdb / print dlopen 注入(调试器即注入器)
gdb
(gdb) attach <pid>
(gdb) call (void*)dlopen("/path/to/hook.so", 2)
(gdb) call (void)my_init()
(gdb) detach原理:gdb 底层就是 ptrace。call dlopen(...) 让 gdb 在目标进程上下文里执行 dlopen,等价于 3.4 的手动版。好处是无需写注入器程序,坏处是要停掉进程、需 ptrace 权限、生产环境慎用。
4. 六法对比
| 手段 | 是否需要目标配合 | 运行期可注入? | 权限要求 | 隐蔽性 | 典型用途 |
|---|---|---|---|---|---|
LD_PRELOAD | 否(启动前) | 否 | 普通用户可对自己进程;setuid 受限 | 低(易查 ld.so.preload) | 性能探针、内存检测、沙箱 |
dlopen(程序主动) | 是 | 是 | 目标自身权限 | 高 | 插件/热更新 |
/proc/pid/mem 改写 | 否 | 是 | 同 uid/root + ptrace | 中 | GOT 覆写、shellcode |
ptrace 注入 | 否 | 是 | 同 uid/root + ptrace_scope | 中 | 热补丁、frida、调试 |
LD_AUDIT | 否(启动前) | 否 | 同 LD_PRELOAD | 中 | 符号级插桩、审计 |
gdb call dlopen | 否 | 是 | ptrace | 低(进程会停) | 临时调试 |
一句话归纳:启动前的注入靠
ld.so的环境变量钩子(LD_PRELOAD/LD_AUDIT);运行期的注入靠"让目标进程自己调dlopen"——要么程序本来就有这个调用,要么用ptrace/gdb//proc/mem强行帮它调。
5. 与本项目其它主题的关系
- 加载原理:所有注入都建立在 compile-link-load.md 的动态链接与 memory-layout.md 的地址空间之上;
- hook 落点:运行时截胡靠 PLT/GOT,细节见 c-abi.md 的调用约定与 elf-format.md 的
.dynamic/重定位; - 观测工具:tools/proc/ 下的
/proc/pid/maps、/proc/pid/mem既是注入手段也是检测手段; - 崩溃排查:注入的
.so若崩了,现场的 core 分析见 core-dump.md;注入器本身用ptrace时与目标的交互错误会表现成各种信号(见 signals.md); - 防御侧:
seccomp、yama.ptrace_scope、ld.so的AT_SECURE共同构成"防注入"的三道防线。
6. 一句话总结
用户态动态库注入的六种手段,本质只有两条路径——启动前借 ld.so 的环境变量钩子(LD_PRELOAD/LD_AUDIT),或运行期让目标进程自己执行 dlopen(程序配合 / ptrace / gdb / 直接改 /proc/pid/mem);它们都依赖动态链接器的可加载性、PLT/GOT 的运行时可重定向、以及进程内存可被外部改写的三个事实。理解这些,既能用注入做性能探针与热补丁,也能看懂攻击者如何用同一套机制做恶意 hook。
关联文档
- compile-link-load.md —— 编译链接加载全过程,注入的地基
- memory-layout.md —— 进程地址空间与
/proc/pid/maps - elf-format.md —— ELF 格式、
.dynamic、重定位 - c-abi.md —— System V AMD64 调用约定,ptrace 注入时参数怎么塞寄存器
- core-dump.md —— 注入 .so 崩了的现场分析
- signals.md —— ptrace 与目标交互产生的信号语义