找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

4446

积分

0

好友

582

主题
发表于 5 小时前 | 查看: 3| 回复: 0

前面在 Linux 上用 mprotect 修改字符串常量 一文中我们介绍了通过 mprotect 系统调用来修改调用进程指定内存区域的访问保护属性,比如把只读数据段改成可写的,这样就可以直接修改其中的字符串常量。当时我们做实验是直接在代码中调用 mprotect,但这有三个硬性前提:你有源码、你能重新编译、你能重启进程。现实中大量目标并不满足这些条件。这篇文章就来介绍一种不用修改源代码的方法,通过注入的方式让目标进程执行 mprotect。对底层系统编程技术感兴趣的读者,欢迎到云栈社区交流探讨。

在进入正题之前我们先来介绍 Linux 上的另一个系统调用:ptrace 系统调用。这是一个强大且历史悠久的系统调用,它提供了一个进程观察和控制另一个进程执行的能力。简单来说,它就像一个“调试代理”,允许你“窥探”和“操纵”其他进程的内部状态,是众多调试和系统分析工具的基石,gdb 就是基于 ptrace 实现的。本文我们就打算利用 ptrace 来向目标进程注入执行 mprotect 系统调用:从进程外部,用 ptrace 让目标进程“自己”执行一次 mprotect

我们的需求是让目标进程在自己的地址空间里执行一次 mprotect(addr, len, PROT_WRITE)。为了完成这个目标,第一直觉通常是往目标进程地址空间里塞一段 shellcode,比如构造一小段机器码,它的功能就是调用 mprotect,执行完之后再让目标进程的执行流跳回原处。

mov rax, __NR_mprotect  ; rax 放系统调用号
mov rdi, addr           ; rdi 放系统调用的第一个参数
mov rsi, len            ; rsi 放系统调用的第二个参数
mov rdx, prot           ; rdx 放系统调用的第三个参数
syscall
; 跳回原 RIP

然后把这段 shellcode 写进目标进程的某块内存,再把它的 RIP 指向这段代码开头,让目标进程跳过来执行,等它执行完再恢复现场,跳回原来的执行流。听起来很合理,但是具体要做就会遇到困难。首先要把 shellcode 写进目标内存,你得先有一块可写的内存,否则你就没法利用之前我们讲过的方式(process_vm_writev)把 shellcode 写进目标进程的内存中。这样你就陷入了鸡生蛋、蛋生鸡的循环——为了获得写权限,你先得能写。其次,就算你找到了一块可写区域,shellcode 还需要可执行权限,现代系统普遍遵循 W^X(Write XOR Execute)原则:一块内存要么可写、要么可执行,不能两者兼有。你要么先找 RW 区写入、再 mprotect 成 R+X(又回到了“需要先能改权限”的原命题),要么赌目标里恰好有一块 RWX 区域——这在今天的可执行文件里几乎绝迹。你可以查看进程的 /proc/<pid>/maps 文件,看看有没有 W 和 X 属性共存的内存段,应该是不会有的。

目标进程 /proc/pid/maps 显示无 RWX 共存区域

于是我们意识到:shellcode 方案的本质矛盾是:为了改权限,我们反而需要先有写和执行的能力,而这恰恰是我们要达成的目标。我们不妨换个思路,系统调用的执行,靠的不是某段特定的“代码”,而是 CPU 执行到一条 syscall 指令时,按寄存器里的参数陷入内核,执行特定的动作。换句话说:只要目标进程的地址空间里存在一条 syscall 指令,我们就不需要自己写它,我们只需:

  1. 找到那条现成的 syscall 指令的地址;
  2. 把寄存器摆成 mprotect 所需的样子(rax/rdi/rsi/rdx);
  3. 把 RIP 指向它;
  4. 放行,让目标进程替我们跑这条指令。

要在目标进程的可执行内存段中找到一条 syscall 指令那可太简单了,哪个可执行程序不得调用系统调用,所以这一步是不成问题的。把寄存器摆成 mprotect 需要的样子以及修改 RIP 更改目标进程的执行流程,这都是 ptrace 擅长的事情,所以也不是问题。但是现在有个新的问题,就是 syscall 调完之后怎么办?我们是需要把目标进程的执行流恢复回去的。我们找到的那条 syscall 指令后面跟着的是什么?在目标进程的代码段里,它后面可能是任意指令(比较指令、跳转指令、返回指令……)。在目标进程执行完 syscall 指令后,CPU 会继续往下执行不属于我们想让它执行的逻辑,这会导致目标进程直接跑飞崩溃。我们希望的是:当目标进程把 syscall 执行完,立刻把控制权交还给我们(注入方),好让我们检查 mprotect 的调用结果、恢复现场,恢复目标进程的执行流。

怎么才能让目标进程在执行完 syscall 之后把控制权交回给我们呢?其实对于 ptrace 来说很简单,我们可以使用 PTRACE_SYSCALL 命令。当我们使用 ptrace 的 PTRACE_SYSCALL 命令让被调试进程继续运行时,被调试进程会在以下两个时机被内核挂起,并把控制权交回给 ptrace:

  • 进入系统调用前(entry):执行 syscall / int 0x80 等指令时。
  • 系统调用返回后(exit):系统调用执行完毕,即将返回用户态时。

所以我们只需要使用 PTRACE_SYSCALL 命令就可以让目标进程在执行 mprotect 系统调用之前和之后把控制权交还给我们,我们也就有机会对目标进程进行现场恢复,和执行流恢复。

好了,有了具体思路之后,我们就可以动手写代码了。这次我们就只写一个函数:add_write_permission,这个函数让之前写的 scan_modify 程序遇到只读数据段的时候去调。下面是 add_write_permission 函数的代码,我在重点部分都加了注释。

add_write_permission 函数通过 ptrace 注入 mprotect 的关键代码

总的说来 add_write_permission 的流程就是它首先使用 PTRACE_SEIZEPTRACE_INTERRUPT 附着到目标进程并使其暂停,同时保存完整的原始寄存器状态(目标进程暂停时的状态);接着在目标进程的可执行内存段中搜索一条 syscall (0F 05) 指令作为跳板,然后将指令指针(RIP)和参数寄存器(RAX、RDI、RSI、RDX)设置为执行 mprotect 所需的系统调用号、地址、长度和新权限,并利用 PTRACE_SYSCALL 分别在系统调用的入口和出口暂停目标进程,将目标进程的控制权交回给我们,重点是在出口阶段从 RAX 中读取 mprotect 系统调用的返回值以判断修改权限的操作是否成功;最后恢复目标进程原始寄存器状态并执行 PTRACE_DETACH 脱离目标进程,使目标进程完全透明地(从被 PTRACE_INTERRUPT 暂停时的位置)继续运行,仿佛从未被干扰过。至于 add_write_permission 函数调用的查找 syscall 指令的辅助函数 find_syscall_gadget,其实就是在目标进程的可执行代码段中搜索 syscall 指令的地址,这里就不细说了。

下面是在原始的 scan_modify 程序中针对只读内存段调用 add_write_permission 函数赋予其可写权限的代码。完整的 scan_modify 代码请参考前面的文章。

scan_modify 中检查权限并调用 add_write_permission 的代码

在做实验之前,我们还需要修改 victim 程序,把目标字符串修改成字符串常量(原来是字符串数组)。如果这样我们的 scan_modify 程序仍然能够把这个字符串给修改掉,那就说明我们注入执行 mprotect 系统调用是成功的。

victim 程序将字符串定义为常量

我们先用之前写的 scan_modify 程序来对修改后的 victim 程序中的字符串常量进行修改,看看能否成功。从下面的动图来看,修改是没有成功的,系统调用 process_vm_writev 报错:Bad address,victim 程序依旧打印着原始的字符串:helloworld。

未注入 mprotect 时 process_vm_writev 返回 Bad address 错误

下面我用这次修改过的带有注入 mprotectscan_modify 程序来对 victim 进程中的字符串常量进行修改。下面的动图是执行效果,可以看到这次是成功的,victim 进程中的 helloworld 字符串被替换成了 stacktrace。

注入 mprotect 后成功修改进程内存字符串常量

最后附上修改后的 scan_modify 的完整代码。Have Fun!😁


#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <sys/uio.h>
#include <sys/ptrace.h>
#include <sys/wait.h>
#include <sys/user.h>
#include <sys/syscall.h>
#include <sys/mman.h>
#include <signal.h>
#include <stdint.h>

static long find_byte_sequence(pid_t pid,
                               unsigned long start,
                               unsigned long end,
                               const unsigned char *seq,
                               size_t seq_len)
{
    char mem_path[64];
    snprintf(mem_path, sizeof(mem_path), "/proc/%d/mem", pid);
    int fd = open(mem_path, O_RDONLY);
    if (fd == -1) {
        perror("open /proc/pid/mem");
        return -1;
    }
    size_t total = end - start;
    unsigned char *buf = malloc(total);
    if (!buf) {
        perror("malloc");
        close(fd);
        return -1;
    }
    /* Seek to the start address and read the entire range into a buffer. */
    if (lseek(fd, start, SEEK_SET) == -1) {
        perror("lseek");
        free(buf);
        close(fd);
        return -1;
    }
    ssize_t n = read(fd, buf, total);
    if (n <= 0) {
        perror("read mem");
        free(buf);
        close(fd);
        return -1;
    }
    close(fd);
    /* Perform a simple linear scan for the byte sequence. */
    for (size_t i = 0; i + seq_len <= (size_t)n; i++) {
        if (memcmp(buf + i, seq, seq_len) == 0) {
            free(buf);
            return (long)i;   /* offset from start */
        }
    }
    free(buf);
    return -1;   /* not found */
}

/*
 * ============================================================================
 * Locate a 'syscall' instruction (0F 05) inside any executable mapping.
 * ============================================================================
 * Reads /proc/pid/maps to find all executable (r-xp) memory regions, then
 * searches each for the byte sequence 0F 05 (the x86-64 syscall instruction).
 *
 * Returns:
 *   - The virtual address of the first found 'syscall' instruction.
 *   - 0 if none is found (0 is never a valid user-space address).
 */
static unsigned long find_syscall_gadget(pid_t pid, const char *maps_path)
{
    FILE *fp = fopen(maps_path, "r");
    if (!fp) {
        perror("fopen maps");
        return 0;
    }
    unsigned long syscall_addr = 0;
    char line[512];
    const unsigned char seq[] = {0x0f, 0x05};   /* syscall opcode */
    while (fgets(line, sizeof(line), fp)) {
        unsigned long start, end;
        char perms[5];
        char path[256] = {0};
        /* Parse the line: start-end perms ... pathname */
        if (sscanf(line, "%lx-%lx %4s %*x %*x:%*x %*d %255[^\n]",
                   &start, &end, perms, path) < 3) {
            continue;
        }
        /* We only care about executable mappings (the 'x' flag is at perms[2]). */
        if (perms[2] != 'x')
            continue;
        long offset = find_byte_sequence(pid, start, end, seq, sizeof(seq));
        if (offset >= 0) {
            syscall_addr = start + offset;
            printf("[+] Found syscall at 0x%lx in %s\n",
                   syscall_addr, path[0] ? path : "???");
            break;
        }
    }
    fclose(fp);
    return syscall_addr;
}

static int get_page_perms(pid_t pid, unsigned long addr)
{
    char maps_path[64];
    snprintf(maps_path, sizeof(maps_path), "/proc/%d/maps", pid);
    FILE *fp = fopen(maps_path, "r");
    if (!fp) {
        perror("fopen maps");
        return -1;
    }
    char line[512];
    int result = -1;
    while (fgets(line, sizeof(line), fp)) {
        unsigned long s, e;
        char perms[5];
        if (sscanf(line, "%lx-%lx %4s", &s, &e, perms) < 3)
            continue;
        if (addr >= s && addr < e) {
            int prot = 0;
            if (perms[0] == 'r') prot |= PROT_READ;
            if (perms[1] == 'w') prot |= PROT_WRITE;
            if (perms[2] == 'x') prot |= PROT_EXEC;
            result = prot;
            break;
        }
    }
    fclose(fp);
    return result;
}

int add_write_permission(pid_t pid,
                         unsigned long seg_start,
                         unsigned long seg_end)
{
    long page_size = sysconf(_SC_PAGESIZE);
    if (page_size <= 0) page_size = 4096;   /* fallback */
    /*
     * mprotect() requires both addr and len to be page-aligned.
     * We round seg_start down and seg_end up to page boundaries.
     */
    unsigned long addr = seg_start & ~((unsigned long)page_size - 1);
    unsigned long end  = (seg_end + page_size - 1) & ~((unsigned long)page_size - 1);
    size_t len = end - addr;
    if (len == 0) {
        fprintf(stderr, "[-] Invalid segment range\n");
        return -1;
    }
    /*
     * Determine the current protection flags for the first page (we assume the
     * whole segment has uniform permissions; if not, this would need refinement).
     * Then add PROT_WRITE while preserving existing flags.
     */
    int cur_prot = get_page_perms(pid, addr);
    if (cur_prot < 0) {
        fprintf(stderr, "[-] Cannot read current permissions\n");
        return -1;
    }
    int new_prot = cur_prot | PROT_WRITE;
    char maps_path[64];
    snprintf(maps_path, sizeof(maps_path), "/proc/%d/maps", pid);
    /*
     * Use PTRACE_SEIZE to attach without sending SIGSTOP. This is a modern
     * (Linux 3.4+) interface that avoids interfering with the target's signal
     * handling. Then use PTRACE_INTERRUPT to force it to stop.
     */
    if (ptrace(PTRACE_SEIZE, pid, 0, 0) == -1) {
        perror("ptrace SEIZE");
        return -1;
    }
    if (ptrace(PTRACE_INTERRUPT, pid, 0, 0) == -1) {
        perror("ptrace INTERRUPT");
        ptrace(PTRACE_DETACH, pid, 0, 0);
        return -1;
    }
    /* Wait for the process to actually stop. */
    int status;
    if (waitpid(pid, &status, 0) == -1) {
        perror("waitpid");
        ptrace(PTRACE_DETACH, pid, 0, 0);
        return -1;
    }
    if (!WIFSTOPPED(status) || WSTOPSIG(status) != SIGTRAP) {
        fprintf(stderr, "[-] Unexpected stop signal (expected SIGTRAP)\n");
        ptrace(PTRACE_DETACH, pid, 0, 0);
        return -1;
    }
    /* Save the entire register context so we can restore it later. */
    struct user_regs_struct orig_regs;
    if (ptrace(PTRACE_GETREGS, pid, 0, &orig_regs) == -1) {
        perror("ptrace GETREGS");
        ptrace(PTRACE_DETACH, pid, 0, 0);
        return -1;
    }
    /* Find a 'syscall' gadget in the target's executable memory. */
    unsigned long syscall_addr = find_syscall_gadget(pid, maps_path);
    if (!syscall_addr) {
        fprintf(stderr, "[-] No syscall gadget found\n");
        ptrace(PTRACE_SETREGS, pid, 0, &orig_regs);
        ptrace(PTRACE_DETACH, pid, 0, 0);
        return -1;
    }
    /*
     * Build a new register context that will execute the syscall.
     * We only change the instruction pointer (RIP) and the syscall arguments.
     * PTRACE_SYSCALL will let us regain control after the
     * syscall completes.
     *
     * For x86-64 Linux:
     *   - rax: syscall number (__NR_mprotect)
     *   - rdi: addr
     *   - rsi: len
     *   - rdx: prot
     *   - rip: points to the 'syscall' instruction
     */
    struct user_regs_struct regs = orig_regs;
    regs.rip = syscall_addr;
    regs.rax = __NR_mprotect;
    regs.rdi = addr;
    regs.rsi = len;
    regs.rdx = new_prot;
    if (ptrace(PTRACE_SETREGS, pid, 0, ®s) == -1) {
        perror("ptrace SETREGS");
        ptrace(PTRACE_SETREGS, pid, 0, &orig_regs);
        ptrace(PTRACE_DETACH, pid, 0, 0);
        return -1;
    }
    /*
     * ---------------------------------------------------------------
     * Phase 1: Syscall entry
     * ---------------------------------------------------------------
     * PTRACE_SYSCALL resumes the tracee and stops it at the next syscall
     * entry and again at the syscall exit. We use it twice: first to reach
     * the entry, then to reach the exit.
     */
    if (ptrace(PTRACE_SYSCALL, pid, 0, 0) == -1) {
        perror("ptrace SYSCALL (entry)");
        ptrace(PTRACE_SETREGS, pid, 0, &orig_regs);
        ptrace(PTRACE_DETACH, pid, 0, 0);
        return -1;
    }
    if (waitpid(pid, &status, 0) == -1) {
        perror("waitpid (entry)");
        ptrace(PTRACE_SETREGS, pid, 0, &orig_regs);
        ptrace(PTRACE_DETACH, pid, 0, 0);
        return -1;
    }
    if (!WIFSTOPPED(status)) {
        fprintf(stderr, "[-] Unexpected wait status (entry)\n");
        ptrace(PTRACE_SETREGS, pid, 0, &orig_regs);
        ptrace(PTRACE_DETACH, pid, 0, 0);
        return -1;
    }
    /*
     * ---------------------------------------------------------------
     * Phase 2: Syscall exit
     * ---------------------------------------------------------------
     * Now we allow the tracee to run the syscall. The kernel executes it,
     * and when it returns to user space, the tracee stops again (exit stop).
     * At that point, the syscall return value is in RAX.
     */
    if (ptrace(PTRACE_SYSCALL, pid, 0, 0) == -1) {
        perror("ptrace SYSCALL (exit)");
        ptrace(PTRACE_SETREGS, pid, 0, &orig_regs);
        ptrace(PTRACE_DETACH, pid, 0, 0);
        return -1;
    }
    if (waitpid(pid, &status, 0) == -1) {
        perror("waitpid (exit)");
        ptrace(PTRACE_SETREGS, pid, 0, &orig_regs);
        ptrace(PTRACE_DETACH, pid, 0, 0);
        return -1;
    }
    if (!WIFSTOPPED(status)) {
        fprintf(stderr, "[-] Unexpected wait status (exit)\n");
        ptrace(PTRACE_SETREGS, pid, 0, &orig_regs);
        ptrace(PTRACE_DETACH, pid, 0, 0);
        return -1;
    }
    /* Read the syscall return value from the current RAX. */
    struct user_regs_struct after_regs;
    if (ptrace(PTRACE_GETREGS, pid, 0, &after_regs) == -1) {
        perror("ptrace GETREGS after syscall");
        ptrace(PTRACE_SETREGS, pid, 0, &orig_regs);
        ptrace(PTRACE_DETACH, pid, 0, 0);
        return -1;
    }
    long retval = after_regs.rax;
    int result = -1;
    if (retval == 0) {
        printf("[+] Segment 0x%lx-0x%lx is now writable\n", addr, end);
        result = 0;
    } else {
        fprintf(stderr, "[-] mprotect failed: %s\n", strerror(-retval));
    }
    /*
     * ---------------------------------------------------------------
     * Restore original context and detach
     * ---------------------------------------------------------------
     * We restore all registers to their original state so that the target
     * continues exactly where it left off, as if nothing happened.
     * Finally, PTRACE_DETACH releases the tracee and allows it to run freely.
     */
    if (ptrace(PTRACE_SETREGS, pid, 0, &orig_regs) == -1) {
        perror("ptrace SETREGS (restore)");
        /* Even if restore fails, we try to detach to avoid leaving the
         * process in a stuck state. */
    }
    if (ptrace(PTRACE_DETACH, pid, 0, 0) == -1) {
        perror("ptrace DETACH");
        return -1;
    }
    return result;
}

int main(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]);
  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;
  }

  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];

  while (fgets(line, sizeof(line), maps)) {
    uint64_t start, end;
    char perms[5];

    if (sscanf(line, "%lx-%lx %4s", &start, &end, perms) != 3)
      continue;

    /* Skip non-readable regions */
    if (perms[0] != 'r')
      continue;

    size_t region_size = end - start;
    if (region_size == 0)
      continue;

    unsigned char *buffer = malloc(region_size);
    if (!buffer)
      continue;

    struct iovec local_iov = {
      .iov_base = buffer,
      .iov_len  = region_size
    };
    struct iovec remote_iov = {
      .iov_base = (void *)start,
      .iov_len  = region_size
    };

    ssize_t nread = process_vm_readv(pid, &local_iov, 1, &remote_iov, 1, 0);
    if (nread <= 0) {
      free(buffer);
      continue;
    }

    unsigned char *ptr = buffer;
    while ((ptr = memmem(ptr, nread - (ptr - buffer), pattern, pattern_len)) != NULL) {
      uintptr_t offset = ptr - buffer;
      uintptr_t found_addr = start + offset;

      /* Prepare write buffer */
      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);
      if (perms[1] != 'w') {
        printf("[+] Adding write permission to segment 0x%lx-0x%lx\n", start, end);
        add_write_permission(pid, start, end);
      }
      struct iovec w_local_iov = {
        .iov_base = write_buffer,
        .iov_len  = pattern_len
      };
      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;
}



上一篇:Codex CLI 实战:一口气接入 Ace Data Cloud 全部 MCP 服务
下一篇:DRAM合约价涨幅超预期,谷歌资本开支或增50%存储需求
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-7-28 06:59 , Processed in 0.769169 second(s), 41 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表