当前位置:首页>Linux>Linux 跨进程内存读写

Linux 跨进程内存读写

  • 2026-09-13 22:57:04
Linux 跨进程内存读写
前面写了一篇 Windows 跨进程内存读写,有网友说 Windows 是闭源的,没意思,那好我们这篇文章来介绍下开源的 Linux 的跨进程内存读写。
在 Windows 系统上,我们可以利用函数 ReadProcessMemory 和 WriteProcessMemory 进行跨进程的内存读写,也就是在一个进程中对另一个进程的地址空间进行读写,在 Linux 系统中也提供了两个有类似功能的函数:process_vm_readv 和 process_vm_writev。

这是两个系统调用,这对系统调用用于在调用进程("本地进程")的虚拟地址空间与由 pid 标识的进程("远程进程")的虚拟地址空间之间传输数据,数据直接在这两个进程的地址空间之间移动,不经过内核空间的缓冲区。

process_vm_readv 系统调用将数据从远程进程传输到本地进程。待传输的数据由 remote_iov 和 riovcnt 描述,remote_iov 是一个 iovec 指针,指向一个描述远程进程(pid)中地址范围的 iovec 数组,riovcnt 指定了 remote_iov 中的元素个数。数据被传输到由 local_iov 和 liovcnt 指定的位置,local_iov 也是一个 iovec 指针,指向一个描述调用/本地进程中地址范围的 iovec 数组,liovcnt 指定了 local_iov 中的元素个数。

process_vm_writev 系统调用与 process_vm_readv 相反,它将数据从本地进程写到远程进程。除传输方向不同外,参数 liovcnt、local_iov、riovcnt 和 remote_iov 的含义与 process_vm_readv 中的完全相同。

需要说明的是这两个系统调用在两个进程之间传输数据不经过内核的准确含义是,数据不会先被拷贝进内核缓冲区、再拷贝回用户态,而是由内核直接在两个进程的页表所映射的物理内存之间完成复制。换句话说,虽然拷贝动作由内核执行(毕竟是系统调用),但数据本身没有"绕道"内核的中间缓冲区,避免了数据在内核缓冲区与用户缓冲区之间的额外拷贝,所以这两个系统调用的性能还是不错的。

好了废话说了不少了,我们开始动手实现一个和上次在 Windows 上实现的一样的功能:在目标进程中查找特定的字符串,并且把这个字符串替换成我们指定的字符串。我们需要写两个程序,第一个程序是目标程序 victim,它很简单,就是循环打印 helloworld。
第二个程序是 scan_modify,它会利用 process_vm_readv 和process_vm_writev 这两个系统调用对 victim 程序进行扫描,并且把扫描到的字符串改成另一个字符串。
在写 scan_modify 之前我们还要解决另一个问题,那就是如何确定要扫描目标进程中的哪些内存区域。我们可以利用 Linux 的 procfs 提供的伪文件:/proc/<pid>/maps,这个文件可以用来查看某个进程当前虚拟地址空间(VMA)的布局,通过它我们可以知道进程的每一段地址映射到哪儿、权限如何、对应哪个文件,一目了然。
下面是 scan_modify 程序的代码,我在代码中加了比较详细的注释。总之就是遍历 /proc/<pid>/maps 提供的进程内存布局,筛选出可读可写的内存段,使用 process_vm_readv 把远程的内存读到本地,然后在本地搜索目标字符串,找到之后,算出该字符串在远程进程中的地址,最后通过 process_vm_writev 把要替换的字符串写入到远程进程的内存中。
下面是运行效果,运行 scan_modify 的时候需要以 root 权限运行,可以看到 scan_modify 是可以成功的改掉 victim 中的打印的字符串的。
最后附上代码文本,Have Fun!
#define _GNU_SOURCE#include<stdio.h>#include<stdlib.h>#include<stdint.h>#include<string.h>#include<fcntl.h>#include<unistd.h>#include<sys/uio.h>#include<sys/types.h>#include<errno.h>/* * 程序功能: *   在指定进程的可读可写内存区域中搜索字符串 pattern, *   并将其就地替换为 replace(要求 replace 长度 <= pattern 长度)。 * * 用法:./program <pid> <search_string> <replace_string> */intmain(int argc, char *argv[]){  if (argc != 4) {    fprintf(stderr, "Usage: %s <pid> <search_string> <replace_string>\n", argv[0]);    return EXIT_FAILURE;  }  pid_t pid = atoi(argv[1]);              /* 目标进程 PID */  const char *pattern = argv[2];          /* 要查找的原始字符串 */  const char *replace = argv[3];          /* 替换后的字符串 */  size_t pattern_len = strlen(pattern);   /* 原始串长度 */  size_t replace_len = strlen(replace);   /* 替换串长度 */  /* 由于是“就地覆盖”,替换串不能比原串长,否则会破坏相邻内存 */  if (replace_len > pattern_len) {    fprintf(stderr, "Error: Replacement string longer than original\n");    return EXIT_FAILURE;  }  /* ---------- 2. 打开目标进程的 maps 文件 ---------- */  /* /proc/<pid>/maps 描述了该进程各内存区域的地址范围与权限 */  char maps_path[64];  snprintf(maps_path, sizeof(maps_path), "/proc/%d/maps", pid);  FILE *maps = fopen(maps_path, "r");  if (!maps) {    perror("fopen maps");    return EXIT_FAILURE;  }  size_t modified_count = 0;   /* 统计成功修改的次数 */  char line[1024];             /* 每行 maps 内容的缓冲区 */  /* ---------- 3. 逐行解析 maps,遍历每个内存区域 ---------- */  while (fgets(line, sizeof(line), maps)) {    uint64_t start, end;       /* 区域起始 / 结束地址 */    char perms[5];             /* 权限字段,如 "rw-p" */    /* 解析一行:起始地址-结束地址 权限 */    if (sscanf(line, "%lx-%lx %4s", &start, &end, perms) != 3)      continue;    /* 跳过不可读的区域(无法读取内容就无从搜索) */    if (perms[0] != 'r')      continue;    /* 跳过不可写的区域(无法写入就不能修改) */    if (perms[1] != 'w')      continue;    size_t region_size = end - start;    if (region_size == 0)      continue;    /* ---------- 4. 分配本地缓冲区 ---------- */    unsigned char *buffer = malloc(region_size);    if (!buffer)      continue;    /* 本地 iovec:数据读到本进程的 buffer 中 */    struct iovec local_iov = {      .iov_base = buffer,      .iov_len  = region_size    };    /* 远程 iovec:从目标进程地址 start 开始读取 */    struct iovec remote_iov = {      .iov_base = (void *)start,      .iov_len  = region_size    };    /* 跨进程读取目标内存到本地 buffer */    ssize_t nread = process_vm_readv(pid, &local_iov, 1, &remote_iov, 1, 0);    if (nread <= 0) {      free(buffer);      continue;    }    /* ---------- 5. 在本地缓冲区中搜索要查找的字符串 ---------- */    unsigned char *ptr = buffer;    /*     * memmem:在 ptr 指向的剩余字节中查找 pattern。     * 每次找到后令 ptr 前进 pattern_len,继续向后搜索(支持重叠/连续匹配)。     */    while ((ptr = memmem(ptr, nread - (ptr - buffer), pattern, pattern_len)) != NULL) {      uintptr_t offset = ptr - buffer;          /* 命中位置在 buffer 内的偏移 */      uintptr_t found_addr = start + offset;    /* 换算成目标进程中的真实地址 */      /* 准备一个长度为 pattern_len 的写入缓冲 */      char *write_buffer = malloc(pattern_len);      if (!write_buffer) {        perror("malloc");        free(buffer);        fclose(maps);        return EXIT_FAILURE;      }      memset(write_buffer, 0, pattern_len);      memcpy(write_buffer, replace, replace_len); /* 拷入替换串 */      /* 本地 iovec:要写入的数据来自 write_buffer */      struct iovec w_local_iov = {        .iov_base = write_buffer,        .iov_len  = pattern_len      };      /* 远程 iovec:写入目标进程的 found_addr 处 */      struct iovec w_remote_iov = {        .iov_base = (void *)found_addr,        .iov_len  = pattern_len      };      /* 将替换内容写回目标进程内存 */      ssize_t nwritten = process_vm_writev(pid, &w_local_iov, 1, &w_remote_iov, 1, 0);      if (nwritten == (ssize_t)pattern_len) {        printf("[+] Modified at: 0x%lx\n", found_addr);        modified_count++;      } else if (nwritten < 0) {        perror("process_vm_writev");      }      free(write_buffer);      ptr += pattern_len;   /* 移动到下一次搜索起点 */    }    free(buffer);  }  fclose(maps);  printf("Finished. Total modified: %zu\n", modified_count);  return EXIT_SUCCESS;}

最新文章

随机文章