本文将会介绍三种Linux系统下跨进程内存操作的方法,用于读取修改其他进程的内存数据,恶意程序会通过这些操作来破环程序的正常执行逻辑,以到达窃取/篡改密钥,修改代码和数据,严重危害程序安全。在文章末尾我会聊聊相应的对抗方案来保护程序安全。
ptrace
ptrace 全称叫 Process trace,ptrace是linux系统上的一个系统调用(syscall),它为一个进程提供观察和控制另一个进程执行过程的能力。
我们可以使用 ptrace来实现内存数据的读取和写入,还有一些知名的工具也是基于ptrace实现,比如:gdb, strace, ltrace等。
函数签名
#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操作需要通过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;
}
mem进程虚拟文件
/proc/[pid]/mem 是系统内核生成的虚拟文件,可以通过这个文件文件直接访问进程整个虚拟内存空间,并允许读取和修改内存数据。我们可以通过文件操作api来操作指定进程的内存数据,如:open, lseek, read,write, close。这种方式实现大数据内存的读取和写入操作效率较高。
需要注意的是 我们只能当程序处于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专门用于读取和写入其他进程内存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实现调试行为。内存校验可以防止程序代码被修改。