最近有开发者在群里问:怎么实现一个进程去读写另一个进程的内存?这个问题在游戏外挂、调试工具、系统监控等场景都很常见。今天就给大家分享Linux下三种主流的跨进程内存操作方式,以及相应的防护思路。
一、ptrace:老牌的进程追踪神器
说到ptrace,搞过调试的都不陌生。gdb、strace这些工具底层用的就是它。说白了,ptrace就是系统给你开的一扇"后门",让你能控制另一个进程的执行流,顺带读写它的内存。
核心原理
ptrace是个系统调用,长这样:
c
long ptrace(enum __ptrace_request op, pid_t pid, void *addr, void *data);
要用它读写内存,主要靠这两个操作:
PTRACE_PEEKDATA:一次读一个long大小的数据PTRACE_POKEDATA:一次写一个long大小的数据
代码实战
我封装了个Tracer类,用起来比较直观:
cpp
// Tracer.h
class Tracer {
public:
bool attach(int pid); // 附加到目标进程
void detach(); // 脱离
bool continueRun(); // 让目标继续跑
void stop(); // 暂停目标
size_t readMemory(uintptr_t address, void* buffer, size_t size);
size_t writeMemory(uintptr_t address, void* buffer, size_t size);
};
实现的时候有个坑:必须先让目标进程进入STOP状态才能操作。来看核心代码:
cpp
size_t Tracer::readMemory(uintptr_t address, void *buffer, size_t size) {
// 按long大小分块读写,效率很一般
size_t readsize = 0;
long tmp;
for(size_t i = 0; i < size / sizeof(long); ++i) {
errno = 0;
tmp = ptrace(PTRACE_PEEKDATA, pid_, (void*)(address + readsize), nullptr);
if(tmp == -1 && errno != 0) return readsize; // 出错了
memcpy((uint8_t*)buffer + i * sizeof(long), &tmp, sizeof(tmp));
readsize += sizeof(tmp);
}
// 处理剩余字节...
}
写内存更麻烦,最后几个字节要"读-改-写"合并,不然会把后面的数据覆盖掉。
优缺点总结:
- ✅ 权限控制严格,需要root或同用户
- ✅ 功能强大,还能单步调试
- ❌ 效率低,每次都是单次syscall
- ❌ 目标进程必须暂停,影响业务
二、/proc/[pid]/mem:虚拟文件的妙用
这招更直接。Linux的/proc目录下,每个进程都有一个mem文件,映射了它的整个虚拟内存空间。我们用普通的文件API就能操作。
实现要点
关键代码很简单,就是open/read/write:
cpp
bool ProcFile::openMemory() {
char path[64];
snprintf(path, sizeof(path), "/proc/%d/mem", pid_);
mem_fd_ = open(path, O_RDWR); // 要读写权限
return mem_fd_ != -1;
}
size_t ProcFile::readMemory(intptr_t address, void *buffer, size_t size) {
if (mem_fd_ == -1) return 0;
// 定位到目标地址
lseek(mem_fd_, static_cast<off_t>(address), SEEK_SET);
// 必须STOP目标进程
kill(pid_, SIGSTOP);
size_t read_size = read(mem_fd_, buffer, size);
kill(pid_, SIGCONT); // 操作完赶紧恢复
return read_size;
}
注意几个细节:
- 需要
O_RDWR权限,普通用户只能读自己进程的 - 操作前后要用SIGSTOP/SIGCONT包裹,不然会报错
- 地址必须用
lseek定位,不能像普通文件那样顺序读
优缺点总结:
- ✅ 代码简单,符合Unix一切皆文件的理念
- ✅ 批量读写效率比ptrace高
- ❌ 还是要暂停进程
- ❌ 需要处理权限问题,SELinux可能会拦截
三、process_vm_readv/writev:现代API
这是Linux 3.2+引入的"官方"解决方案。专门为跨进程内存操作设计,用起来最优雅。
使用方法
cpp
#include <sys/uio.h>
ssize_t process_vm_readv(pid_t pid,
const struct iovec *local_iov, // 本地缓冲区
unsigned long liovcnt,
const struct iovec *remote_iov, // 远程进程地址
unsigned long riovcnt,
unsigned long flags);
看实战代码:
cpp
int api_memory(int pid) {
intptr_t address;
std::string content(256, '\0');
// 读内存
iovec local_iov = {content.data(), content.size()};
iovec remote_iov = {(void*)address, content.size()};
ssize_t nread = process_vm_readv(pid, &local_iov, 1, &remote_iov, 1, 0);
// 写内存
std::string new_data = "hacked";
local_iov.iov_base = new_data.data();
local_iov.iov_len = new_data.size();
process_vm_writev(pid, &local_iov, 1, &remote_iov, 1, 0);
}
这个API最爽的地方:不需要暂停目标进程。系统帮你处理好原子性问题。
优缺点总结:
- ✅ 无需STOP,对业务无影响
- ✅ 支持向量读写,一次多个不连续内存块
- ✅ 效率最高
- ❌ 需要Linux 3.2+
- ❌ 权限限制严格,只能读写同用户进程
三种方案对比
| 特性 | ptrace | /proc/[pid]/mem | process_vm_xxx |
|---|---|---|---|
| 效率 | 低 | 中 | 高 |
| 需暂停进程 | 是 | 是 | 否 |
| 权限要求 | 中 | 中 | 严格 |
| 代码复杂度 | 高 | 低 | 最低 |
| 兼容性 | 所有Linux | 所有Linux | Linux 3.2+ |
| 典型应用 | 调试器 | 监控工具 | 现代注入工具 |
四、防护方案:让攻击者知难而退
了解攻击手段是为了更好防护。跨进程内存攻击通常分两步:先静态/动态分析找漏洞,再实施内存读写。
防静态分析
- 代码虚拟化:把核心函数翻译成自定义指令集,在虚拟机里执行。反汇编工具看到的是虚拟机代码,完全摸不清逻辑。
- 代码混淆:控制流平坦化+虚假分支,把代码逻辑搅成一锅粥。
- 符号隐藏:strip掉符号表,加密导入表,让攻击者找不到关键函数。
防动态调试
- 反ptrace:检测父进程是否在用ptrace调试,是就直接退出。还可以fork子进程互相ptrace,占用调试接口。
- 内存校验:定时检查代码段hash值,发现被修改就自毁。可以用信号或线程做后台监控。
- 权限最小化:以最低权限运行,减少被攻击面。敏感操作放独立进程,IPC通信。
产品化方案
如果是商业软件,建议用专业保护方案。比如Virbox Protector这类工具,把上述防护手段打包,提供一站式保护。从代码加密、虚拟化到反调试一条龙,比自己造轮子靠谱。当然,任何防护都不是绝对的安全,只是大幅提高攻击成本。
五、总结
三种方案各有适用场景:
- 做调试器?用ptrace
- 做监控采集?用/proc/mem
- 做游戏注入?用process_vm_xxx
从攻防角度看,没有绝对安全的系统。了解攻击原理,针对性加固,把攻击成本抬高到不划算,就是胜利。