本文将阐述Linux环境中三种跨进程内存读写的实现方式。这些技术可被用于读取或修改其他进程的内存数据,而恶意程序可能借助此类操作破坏目标程序的正常执行逻辑,进而窃取或篡改密钥、修改代码与数据,对程序安全构成严重威胁。文末将探讨相应的防护策略,以保障程序安全。
ptrace全称为Process trace,它是Linux系统中的一个系统调用(syscall),可为一个进程提供监视和控制另一个进程执行过程的能力。
诸如gdb、strace、ltrace等知名工具均基于ptrace实现,我们也可利用ptrace完成内存数据的读写操作。
#include <sys/ptrace.h>
long ptrace(enum __ptrace_request op, pid_t pid, void *addr, void *data);
op:ptrace操作类型
pid:被追踪的进程ID
addr:目标内存地址,具体使用方式取决于op参数
data:操作数据,具体使用方式取决于op参数
通过ptrace进行内存读写需借助PTRACE_PEEKDATA/PTRACE_POKEDATA操作,不过这种方式在处理大数据量内存的读写时效率较低。
需要注意的是,仅当目标进程处于SIGSTOP状态时,我们才能对其内存进行操作。
Tracer.h
#ifndef _TRACER_12312421_H
#define _TRACER_12312421_H
#include <stdint.h>
#include <stddef.h>
#include <sys/ptrace.h>
#include <asm/ptrace.h>
class Tracer
{
public:
Tracer(/* args */);
~Tracer();
bool attach(int pid);
void detach();
bool continueRun();
void wait(int *status);
void stop();
size_t readMemory(uintptr_t address, void* buffer, size_t size);
size_t writeMemory(uintptr_t address, void* buffer, size_t size);
private:
int pid_;
};
#endif _TRACER_12312421_H
Tracer.cpp
#include "Tracer.h"
#include <errno.h>
#include <memory.h>
#include <sys/types.h>
#include <sys/wait.h>
#define INVALID_PID (-1)
Tracer::Tracer()
:pid_(INVALID_PID)
{
}
Tracer::~Tracer()
{
}
bool Tracer::attach(int pid)
{
long ret = ptrace(PTRACE_ATTACH, pid, nullptr,nullptr);
if( ret == -1)
return false;
pid_ = pid;
int status = 0;
wait(&status);
return true;
}
void Tracer::detach()
{
ptrace(PTRACE_DETACH, pid_, NULL, 0);
}
bool Tracer::continueRun()
{
return ptrace(PTRACE_CONT, pid_, NULL, 0) != -1;
}
void Tracer::wait(int *status)
{
waitpid(pid_, status, 0);
}
void Tracer::stop()
{
kill(pid_, SIGSTOP);
}
size_t Tracer::readMemory(uintptr_t address, void *buffer, size_t size)
{
size_t read_count = size / sizeof(long);
size_t remain_bytes = size % sizeof(long);
uint8_t* _buffer = (uint8_t*)buffer;
size_t readsize = 0;
long tmp;
for(size_t i = 0; i < read_count; ++i)
{
errno = 0;
tmp = ptrace(PTRACE_PEEKDATA, pid_, (void*)(address + readsize), nullptr);
if(tmp == -1 && errno != 0)
{
return readsize;
}
memcpy(_buffer + i * sizeof(long), &tmp, sizeof(tmp));
readsize += sizeof(tmp);
}
if(remain_bytes)
{
errno = 0;
tmp = ptrace(PTRACE_PEEKDATA, pid_, (void*)(address + readsize), nullptr);
if(tmp == -1 && errno != 0)
{
return readsize;
}
memcpy(_buffer + read_count * sizeof(long), &tmp, remain_bytes);
readsize += remain_bytes;
}
return readsize;
}
size_t Tracer::writeMemory(uintptr_t address, void *buffer, size_t size)
{
size_t read_count = size / sizeof(long);
size_t remain_bytes = size % sizeof(long);
uint8_t* _buffer = (uint8_t*)buffer;
size_t writesize = 0;
long tmp, result;
for(size_t i = 0; i < read_count; ++i)
{
memcpy(&tmp, _buffer + i * sizeof(long), sizeof(tmp));
errno = 0;
result = ptrace(PTRACE_POKEDATA, pid_, (void*)(address + writesize ), tmp);
if(result == -1 && errno != 0)
{
return writesize;
}
writesize += sizeof(tmp);
}
if(remain_bytes)
{
long original = ptrace(PTRACE_PEEKDATA, pid_, (void*)(address + writesize), nullptr);
if(original == -1 && errno != 0)
{
return writesize;
}
long new_data = 0;
memcpy(&new_data, _buffer + writesize, remain_bytes);
long mask = (1UL << (remain_bytes * 8)) - 1;
long merged = (original & ~mask) | (new_data & mask);
result = ptrace(PTRACE_POKEDATA, pid_, (void*)(address + writesize ), merged);
if(original == -1 && errno != 0)
{
return writesize;
}
writesize += remain_bytes;
}
return writesize;
}
以下是具体的使用示例:
int tracer_memory(int pid)
{
Tracer tracer;
if(!tracer.attach(pid))
{
printf("failed to ptrace %d\n", pid);
return 1;
}
tracer.continueRun();
intptr_t address;
std::string content;
std::string inputLine;
int statue = 0;
std::cout <<"input address: ";
std::getline(std::cin, inputLine);
std::stringstream(inputLine) >> std::hex >>address;
content.resize(256);
tracer.stop();
tracer.wait(&statue);
size_t read_size = tracer.readMemory(address, (void*)content.c_str(), content.size());
tracer.continueRun();
printf("%p context:%s size:%d\n", address, content.c_str(), read_size);
std::cout <<"input context: ";
std::getline(std::cin, content);
tracer.stop();
tracer.wait(&statue);
tracer.writeMemory(address, (void*)content.c_str(), content.size() + 1);
tracer.continueRun();
read_size = tracer.readMemory(address, (void*)content.c_str(), content.size());
tracer.continueRun();
printf("%p context:%s size:%d\n", address, content.c_str(), read_size);
return 0;
}
/proc/[pid]/mem是由系统内核生成的虚拟文件,通过该文件可直接访问进程的整个虚拟内存空间,并且支持对内存数据的读取和修改。我们可借助open、lseek、read、write、close等文件操作API来操作指定进程的内存数据,这种方式在处理大数据量内存读写时效率较高。
需要注意的是,同样只有当目标程序处于SIGSTOP状态时,才能对其内存进行操作。
ProcFile.h
#ifndef _PROCFILE_12312421_H
#define _PROCFILE_12312421_H
#include <stdint.h>
#include <stddef.h>
class ProcFile
{
public:
ProcFile(int pid);
~ProcFile();
bool openMemory();
void closeMemory();
size_t readMemory(intptr_t address, void* buffer, size_t size);
size_t writeMemory(intptr_t address, const void* buffer, size_t size);
private:
int pid_;
int mem_fd_ = -1;
};
#endif _PROCFILE_12312421_H
ProcFile.cpp
#include "ProcFile.h"
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <string>
#include <signal.h>
#include <wait.h>
#include <errno.h>
ProcFile::ProcFile(int pid)
: pid_(pid)
{
}
ProcFile::~ProcFile()
{
closeMemory();
}
bool ProcFile::openMemory()
{
char path[64];
snprintf(path, sizeof(path), "/proc/%d/mem", pid_);
mem_fd_ = open(path, O_RDWR); // 需要读写权限
return mem_fd_ != -1;
}
void ProcFile::closeMemory()
{
if (mem_fd_ != -1)
{
close(mem_fd_);
mem_fd_ = -1;
}
}
size_t ProcFile::readMemory(intptr_t address, void *buffer, size_t size)
{
if (mem_fd_ == -1)
return 0;
off_t offset = lseek(mem_fd_, static_cast<off_t>(address), SEEK_SET);
if (offset != static_cast<off_t>(address))
{
return 0;
}
size_t read_size = 0;
if (kill(pid_, SIGSTOP) == -1)
{
return 0;
}
read_size = read(mem_fd_, buffer, size);
if (kill(pid_, SIGCONT) == -1)
{
abort();
}
return read_size;
}
size_t ProcFile::writeMemory(intptr_t address, const void *buffer, size_t size)
{
if (mem_fd_ == -1)
return 0;
off_t offset = lseek(mem_fd_, static_cast<off_t>(address), SEEK_SET);
if (offset != static_cast<off_t>(address))
{
return 0;
}
size_t write_size = 0;
if (kill(pid_, SIGSTOP) == -1)
{
return 0;
}
write_size = write(mem_fd_, buffer, size);
if (kill(pid_, SIGCONT) == -1)
{
abort();
}
return write_size;
}
以下是具体的使用示例:
int proc_memory(int pid)
{
ProcFile proc(pid);
if(!proc.openMemory())
{
printf("failed to open memory %d\n", pid);
return 1;
}
intptr_t address;
std::string content;
std::string inputLine;
int statue = 0;
std::cout <<"input address: ";
std::getline(std::cin, inputLine);
std::stringstream(inputLine) >> std::hex >>address;
content.resize(256);
size_t read_size = proc.readMemory(address, (void*)content.c_str(), content.size());
printf("%p context:%s size:%d\n", address, content.c_str(), read_size);
std::cout <<"input context: ";
std::getline(std::cin, content);
proc.writeMemory(address, (void*)content.c_str(), content.size() + 1);
read_size = proc.readMemory(address, (void*)content.c_str(), content.size());
printf("%p context:%s size:%d\n", address, content.c_str(), read_size);
return 0;
}
Linux系统直接提供了两个专门用于读写其他进程内存的系统调用API。
#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);
ssize_t process_vm_writev(pid_t pid,
const struct iovec *local_iov,
unsigned long liovcnt,
const struct iovec *remote_iov,
unsigned long riovcnt,
unsigned long flags);
int api_memory(int pid)
{
intptr_t address;
std::string content;
std::string inputLine;
int statue = 0;
std::cout <<"input address: ";
std::getline(std::cin, inputLine);
std::stringstream(inputLine) >> std::hex >>address;
content.resize(256);
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);
printf("%p context:%s size:%d\n", address, content.c_str(), nread);
std::cout <<"input context: ";
std::getline(std::cin, content);
process_vm_writev(pid, &local_iov, 1, &remote_iov, 1, 0 );
nread = process_vm_readv(pid, &local_iov, 1, &remote_iov, 1, 0);
printf("%p context:%s size:%d\n", address, content.c_str(), nread);
return 0;
}
恶意攻击者若要攻击一个程序,通常首先会通过静态分析工具和动态分析工具找出程序的可攻击点,随后通过读取或修改内存等一系列攻击手段达成目的。
Virbox Protector提供了针对静态分析和动态分析的整体保护方案,从多个维度提升Linux程序的安全性。
防止静态分析:代码虚拟化将核心函数转换为专有虚拟机指令集,代码混淆通过控制流平坦化与虚假分支将执行逻辑打散为复杂的跳转网络,代码加密则对代码段进行加密存储并在运行时按需解密,三者结合使反汇编工具无法还原程序的真实逻辑。同时,导入表保护隐藏了程序对外部库函数的依赖关系,移除调试信息则清除了符号表与函数名称等关键信息,让逆向者难以分析出程序中可被攻击的代码和内存区域。
防止动态调试:调试器检测能够识别基于ptrace实现的调试行为,内存校验则可防止程序代码被篡改。