Linux 系统下跨进程内存读写技术解析
嗯,用户让我帮忙总结一下这篇文章的内容,控制在一百个字以内,而且不需要用“文章内容总结”或者“这篇文章”这样的开头。直接写描述就行。 首先,我得通读整篇文章,了解它的主要内容。看起来这篇文章主要讲的是Linux环境中跨进程内存读写的三种实现方式,以及相应的安全防护策略。 文章开头提到三种技术,可以用于读取或修改其他进程的内存数据,恶意程序可能会利用这些技术破坏程序的正常执行逻辑,窃取或篡改密钥、修改代码和数据,对程序安全构成威胁。然后在文末探讨了防护策略。 接下来,文章详细介绍了这三种方法:ptrace系统调用、/proc/[pid]/mem虚拟文件和专用系统调用process_vm_readv和process_vm_writev。每种方法都有具体的代码实现和使用示例。 最后,讨论了安全防护策略,包括防止静态分析和动态调试的方法,比如代码虚拟化、代码混淆、代码加密、导入表保护、移除调试信息、调试器检测和内存校验等。 现在我需要把这些内容浓缩到一百个字以内。重点是说明文章讲了什么内容以及主要结论。 可能的结构是:文章介绍了Linux环境中跨进程内存读写的三种方法及其安全风险,并提出了防护策略。 检查一下字数是否符合要求。大概控制在一百字左右。 </think> 本文介绍了Linux环境中跨进程内存读写的三种实现方式(ptrace、/proc/[pid]/mem虚拟文件及专用系统调用),探讨了其安全风险及防护策略。 2025-12-8 03:23:32 Author: www.freebuf.com(查看原文) 阅读量:12 收藏

本文将阐述Linux环境中三种跨进程内存读写的实现方式。这些技术可被用于读取或修改其他进程的内存数据,而恶意程序可能借助此类操作破坏目标程序的正常执行逻辑,进而窃取或篡改密钥、修改代码与数据,对程序安全构成严重威胁。文末将探讨相应的防护策略,以保障程序安全。

ptrace

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进行内存读写需借助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虚拟文件

/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实现的调试行为,内存校验则可防止程序代码被篡改。


文章来源: https://www.freebuf.com/articles/sectool/460901.html
如有侵权请联系:admin#unsafe.sh