首页
社区
课程
招聘
[原创]ptrace注入代码在不同平台的区别(ARM64和x86-64)
发表于: 1天前 536

[原创]ptrace注入代码在不同平台的区别(ARM64和x86-64)

1天前
536

⚠️ 免责声明:本文仅供学习和研究目的,介绍 ptrace 在 Linux 系统编程中的技术细节。ptrace 是 Linux 提供的标准系统调用,广泛用于调试器(如 gdb)开发。使用 ptrace 需要 root 权限,且应遵守相关法律法规。请勿将本文技术用于非法目的。

之前学过 ARM64(aarch64) 的 ptrace 注入,最近尝试使用 ptrace 在 x86-64 上注入时,发现两者实现上有些区别,在ARM64上能成功注入的代码,修改相关寄存器和调用约定之后在x86-64上却一直注入失败 (dlopen返回一个很小的值,比如0x8e,正常情况下dlopen应该返回一个堆地址)。本文用来说明两个平台之间的ptrace注入代码区别,方便后面参考。

ptrace 是 Linux 提供的一个系统调用,允许一个进程(tracer)观察和控制另一个进程(tracee)的执行。它是调试器(如 gdb)和进程注入技术的基础。如果想要操作其它进程,需要有root权限。

ptrace 的核心能力包括:

基本的函数原型:

ptrace 注入的核心思路是:附加到目标进程,让目标进程调用 dlopen 来加载我们指定的 so 库,从而在目标进程中执行我们的代码。

通用的注入流程如下:

附加目标进程:调用 ptrace(PTRACE_ATTACH, pid, ...) 附加到目标进程,目标进程会收到 SIGSTOP 信号暂停执行。

保存寄存器现场:调用 ptrace(PTRACE_GETREGS, pid, ...) 保存目标进程当前的寄存器状态,以便注入完成后恢复。

获取目标进程中的关键函数地址:在目标进程的内存空间中找到 dlopendlsymdlclose 等函数的地址。由于 ASLR 的存在,同一个库在不同进程中的加载地址是不同的,但同一个库内部函数的偏移是固定的。因此可以通过以下公式计算:

具体步骤:

这个方法的前提是注入工具和目标进程加载了同一版本的库,这在同一台机器上通常是成立的。

向目标进程内存写入参数:将 so 库路径字符串等参数写入目标进程的内存中(通常写入栈上或 mmap 的内存区域)。

设置寄存器,调用 dlopen:按照目标平台的调用约定设置寄存器和栈,使目标进程执行 dlopen 调用。

触发执行并等待返回:让目标进程继续执行,等待调用完成。

获取返回值:读取寄存器获取 dlopen 的返回值(so 库的句柄/基址)。

恢复寄存器现场并分离:将之前保存的寄存器状态恢复,调用 ptrace(PTRACE_DETACH, pid, ...) 分离目标进程,目标进程继续正常执行。

虽然整体流程在两个平台上是一致的,但第 5 步和第 6 步的具体实现因平台差异而有所不同,这也是本文的重点。

ARM64 调用约定:

ARM64 使用 struct user_pt_regs 或通过 iovec 配合 PTRACE_GETREGSET 来获取寄存器:

在 ARM64 上调用 dlopen 的设置比较直观:

ARM64 的关键点:

下面是完整的 ARM64 (Android) 上的 ptrace 注入代码(如果要注入Android APP,需要将so放在app对应的lib目录下,因为有selinux的限制)。

上面的调用约定和寄存器设置看起来很简单,但在 Android 手机上实际测试时发现,不能直接把 SO 路径写入目标进程的栈空间。

最初的实现:

运行结果:dlopen 的返回值恰好等于我们传入的 path_addr,说明 x0 根本没有被修改——dlopen 没有真正执行。

原因:在 Android 上,dlopen 内部的 linker 实现对内存区域有要求。直接写在栈上的路径字符串,可能因为 linker 内部的内存访问检查等原因导致执行异常。

解决方法:先在目标进程中调用 mmap 分配一块独立的内存,再将 SO 路径写入这块内存:

x86-64 的 ptrace 注入流程和 ARM64 基本一致,但在调用约定和返回地址处理上有重要差异。下面按照相同的结构介绍,并说明两个平台的关键区别。

x86-64 调用约定:

x86-64 使用 struct user_regs_struct,通过 PTRACE_GETREGS 获取:

按照 ARM64 的思路,直接设置寄存器调用 dlopen:

完整代码如下:

使用上述代码调用 dlopen,会返回异常值:

handle = 0xdb 明显错误——正常情况下 dlopen 应该返回一个较大的堆地址。直接设置寄存器的方式在 x86-64 上失败了。

为什么 mmap 可以成功,dlopen 不行?

这个问题研究了很久也没有找到原因,下面的内容是问的AI,有大佬知道的话可以指点一下。

在 ARM64 实现中,我们通过设置 lr = 0 让函数返回时触发 SIGSEGV。x86-64 没有 lr 寄存器,但可以通过手动压栈来设置返回地址:

这种方式对 mmap 有效,但对 dlopen 失败了。原因在于两者的复杂度不同:

mmap 是内核直接实现的系统调用,内部逻辑简单,即使栈状态不完美也能正常工作。

dlopen 则完全不同:

这些操作都要求严格的栈 16 字节对齐,而手动操作 rsp 很难保证这一点。即使对齐正确,dlopen 内部调用的其他函数也可能因为栈状态异常而崩溃,导致返回错误值(如 0xdb)。

失败原因总结

解决方案:在目标进程内存中写入 trampoline 代码,让目标进程通过trampoline调用dlopen。

优势

完整代码如下:

总结两个平台在 ptrace 注入上的关键区别:

核心区别在于返回地址的处理方式

ARM64 有独立的链接寄存器 lr,直接设置 lr = 0 即可让函数返回时触发 SIGSEGV,非常直观。栈对齐通常也不是问题。

x86-64 没有链接寄存器,返回地址保存在栈上。虽然可以手动模拟 call 指令(rsp -= 8,压入返回地址),但实际测试中发现很难正确维护栈对齐和寄存器状态,导致 dlopen 返回异常值(如 0xdb)。

推荐方案:x86-64 上应该使用 trampoline 技术——在目标进程内存中写入 call rax; int3 这样的代码,让目标进程执行这段代码。这样:

这也是为什么 ARM64 可以直接设置寄存器注入成功,而 x86-64 必须使用 trampoline 才能成功的原因。

ptrace 注入的核心流程在不同架构上是一致的,差异主要体现在调用约定和返回地址机制上:

记住一个原则:x86-64 的栈机制比 ARM64 复杂,直接操作寄存器容易踩坑,使用 trampoline 是更可靠的选择

对比项 mmap dlopen
实现复杂度 简单,直接 syscall 复杂,涉及动态链接器
内部调用链 几乎无 多层(_dl_open → 锁操作 → malloc → 构造函数)
栈对齐要求 宽松 严格(16字节对齐)
栈状态依赖
对比项 ARM64 x86-64
参数寄存器 x0 - x7 rdi, rsi, rdx, rcx, r8, r9
返回值寄存器 x0 rax
程序计数器 pc rip
栈顶指针 sp rsp
返回地址机制 链接寄存器 lr (x30) 栈(call 压栈 / ret 弹栈)
设置返回地址 直接设 lr = 0 手动压栈或使用 trampoline
栈对齐 16字节(通常不是问题) 16字节(手动保证困难,推荐用 trampoline)
推荐实现方式 直接设置寄存器 使用 trampoline 代码
long ptrace(enum __ptrace_request request, pid_t pid, void *addr, void *data);
远程函数地址 = 本进程函数地址 - 本进程模块基址 + 目标进程模块基址
struct user_pt_regs {
    __u64 regs[31];  // x0 - x30
    __u64 sp;
    __u64 pc;
    __u64 pstate;
};

// 获取寄存器
struct iovec iov;
struct user_pt_regs regs;
iov.iov_base = &regs;
iov.iov_len = sizeof(regs);
ptrace(PTRACE_GETREGSET, pid, (void*)NT_PRSTATUS, &iov);
// 设置参数
regs.regs[0] = so_path_addr;   // x0 = 第一个参数:so库路径地址
regs.regs[1] = RTLD_NOW;       // x1 = 第二个参数:dlopen flags
regs.pc = dlopen_addr;          // pc = dlopen 函数地址
regs.regs[30] = 0;              // lr = 0,使 dlopen 返回后触发 SIGSEGV

ptrace(PTRACE_SETREGSET, pid, (void*)NT_PRSTATUS, &iov);
ptrace(PTRACE_CONT, pid, NULL, NULL);
// 等待目标进程触发 SIGSEGV(因为 lr = 0,dlopen 返回后会跳转到地址 0)
waitpid(pid, &status, 0);
// injector_arm64.c - ARM64 ptrace 注入工具
// 用法: ./injector_arm64 &lt;pid&gt; <so_absolute_path>
// 需要 root 权限

#define _GNU_SOURCE
#include &lt;stdio.h&gt;
#include &lt;stdlib.h&gt;
#include &lt;string.h&gt;
#include &lt;errno.h&gt;
#include &lt;unistd.h&gt;
#include &lt;sys/ptrace.h&gt;
#include &lt;sys/types.h&gt;
#include &lt;sys/wait.h&gt;
#include &lt;sys/uio.h&gt;
#include &lt;sys/mman.h&gt;
#include &lt;linux/elf.h&gt;
#include &lt;dlfcn.h&gt;
#include &lt;asm/ptrace.h&gt;

// 获取寄存器(ARM64 使用 PTRACE_GETREGSET + iovec)
static int get_regs(pid_t pid, struct user_pt_regs *regs) {
    struct iovec iov;
    iov.iov_base = regs;
    iov.iov_len = sizeof(*regs);
    if (ptrace(PTRACE_GETREGSET, pid, (void *)NT_PRSTATUS, &iov) < 0) {
        perror("PTRACE_GETREGSET");
        return -1;
    }
    return 0;
}

// 设置寄存器
static int set_regs(pid_t pid, struct user_pt_regs *regs) {
    struct iovec iov;
    iov.iov_base = regs;
    iov.iov_len = sizeof(*regs);
    if (ptrace(PTRACE_SETREGSET, pid, (void *)NT_PRSTATUS, &iov) < 0) {
        perror("PTRACE_SETREGSET");
        return -1;
    }
    return 0;
}

// 从 /proc/pid/maps 中查找指定库的基地址
static long get_module_base(pid_t pid, const char *module_name) {
    char path[256];
    char line[512];
    long base = 0;

    if (pid == 0)
        snprintf(path, sizeof(path), "/proc/self/maps");
    else
        snprintf(path, sizeof(path), "/proc/%d/maps", pid);

    FILE *fp = fopen(path, "r");
    if (!fp) { perror("fopen maps"); return 0; }

    while (fgets(line, sizeof(line), fp)) {
        if (strstr(line, module_name)) {
            base = strtol(line, NULL, 16);
            break;
        }
    }
    fclose(fp);
    return base;
}

// 通过地址查找其所在的模块名和模块基址
static int find_module_by_addr(void *addr, char *module_name, size_t name_len, long *base) {
    char line[512];
    FILE *fp = fopen("/proc/self/maps", "r");
    if (!fp) return -1;

    unsigned long target = (unsigned long)addr;
    char found_module[256] = {0};

    while (fgets(line, sizeof(line), fp)) {
        unsigned long start, end;
        if (sscanf(line, "%lx-%lx", &start, &end) != 2) continue;
        if (target >= start && target < end) {
            char *path = strrchr(line, '/');
            if (path) {
                char *nl = strchr(path, '\n');
                if (nl) *nl = '\0';
                strncpy(found_module, path, sizeof(found_module) - 1);
            }
            break;
        }
    }
    fclose(fp);
    if (found_module[0] == '\0') return -1;

    *base = get_module_base(0, found_module);
    strncpy(module_name, found_module, name_len - 1);
    module_name[name_len - 1] = '\0';
    return 0;
}

// 计算目标进程中某函数的地址(自动检测所在模块)
static long get_remote_func_addr(pid_t pid, void *local_func) {
    char module_name[256];
    long local_base;

    if (find_module_by_addr(local_func, module_name, sizeof(module_name), &local_base) < 0) {
        fprintf(stderr, "Failed to find module for addr %p\n", local_func);
        return 0;
    }

    long remote_base = get_module_base(pid, module_name);
    if (!local_base || !remote_base) {
        fprintf(stderr, "Failed to get module base for %s\n", module_name);
        return 0;
    }

    return remote_base + ((long)local_func - local_base);
}

// 在远程进程中调用一个函数,最多支持 6 个参数
// 原理: 设置 x0-x5 为参数, pc 为函数地址, lr = 0
//       PTRACE_CONT 后等待 SIGSEGV (因为 lr=0, ret 跳转到地址 0)
//       读取 x0 获取返回值
static long call_remote_func(pid_t pid, struct user_pt_regs *orig_regs,
                             long func_addr,
                             long arg0, long arg1, long arg2,
                             long arg3, long arg4, long arg5) {
    // 基于原始寄存器来设置,保证 sp 等关键寄存器正确
    struct user_pt_regs regs;
    memcpy(&regs, orig_regs, sizeof(regs));

    regs.regs[0] = arg0;       // x0 - x5: 参数
    regs.regs[1] = arg1;
    regs.regs[2] = arg2;
    regs.regs[3] = arg3;
    regs.regs[4] = arg4;
    regs.regs[5] = arg5;
    regs.pc = func_addr;        // pc = 目标函数地址
    regs.regs[30] = 0;          // lr = 0,函数返回时触发 SIGSEGV

    if (set_regs(pid, &regs) < 0) return -1;

    if (ptrace(PTRACE_CONT, pid, NULL, NULL) < 0) {
        perror("PTRACE_CONT");
        return -1;
    }

    int status;
    waitpid(pid, &status, WUNTRACED);

    // 读取返回值 (ARM64 返回值在 x0)
    if (get_regs(pid, &regs) < 0) return -1;
    return (long)regs.regs[0];
}

// 向目标进程写入数据(以 long 为单位,按需补齐)
static int ptrace_write_data(pid_t pid, unsigned long addr, const void *data, size_t len) {
    const unsigned char *src = (const unsigned char *)data;
    size_t i = 0;

    for (i = 0; i + sizeof(long) <= len; i += sizeof(long)) {
        long val;
        memcpy(&val, src + i, sizeof(long));
        if (ptrace(PTRACE_POKEDATA, pid, (void *)(addr + i), (void *)val) < 0) {
            perror("PTRACE_POKEDATA");
            return -1;
        }
    }
    if (i < len) {
        long val = ptrace(PTRACE_PEEKDATA, pid, (void *)(addr + i), NULL);
        memcpy(&val, src + i, len - i);
        if (ptrace(PTRACE_POKEDATA, pid, (void *)(addr + i), (void *)val) < 0) {
            perror("PTRACE_POKEDATA tail");
            return -1;
        }
    }
    return 0;
}

int main(int argc, char *argv[]) {
    if (argc != 3) {
        fprintf(stderr, "用法: %s &lt;pid&gt; <so_absolute_path>\n", argv[0]);
        return 1;
    }

    pid_t pid = atoi(argv[1]);
    const char *so_path = argv[2];
    int status;

    // 1. 附加目标进程
    if (ptrace(PTRACE_ATTACH, pid, NULL, NULL) < 0) {
        perror("PTRACE_ATTACH");
        return 1;
    }
    waitpid(pid, &status, WUNTRACED);

    // 2. 保存原始寄存器
    struct user_pt_regs orig_regs;
    if (get_regs(pid, &orig_regs) < 0) {
        ptrace(PTRACE_DETACH, pid, NULL, NULL);
        return 1;
    }

    // 3. 在远程进程中调用 mmap 分配内存
    long remote_mmap = get_remote_func_addr(pid, (void *)mmap);
    long mmap_result = call_remote_func(pid, &orig_regs, remote_mmap,
        0, 0x1000, PROT_READ | PROT_WRITE,
        MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);

    if (mmap_result == -1 || mmap_result == 0) {
        fprintf(stderr, "[-] 远程 mmap 失败\n");
        set_regs(pid, &orig_regs);
        ptrace(PTRACE_DETACH, pid, NULL, NULL);
        return 1;
    }
    printf("[+] 远程 mmap 成功, 地址: 0x%lx\n", mmap_result);

    // 4. 将 so 路径写入 mmap 分配的内存
    ptrace_write_data(pid, (unsigned long)mmap_result, so_path, strlen(so_path) + 1);

    // 5. 获取远程 dlopen 地址并调用
    void *local_dlopen = dlsym(RTLD_DEFAULT, "dlopen");
    long remote_dlopen = get_remote_func_addr(pid, local_dlopen);
    long dlopen_ret = call_remote_func(pid, &orig_regs, remote_dlopen,
        mmap_result, RTLD_NOW, 0, 0, 0, 0);

    printf("[+] dlopen 返回值: 0x%lx\n", dlopen_ret);
    if (dlopen_ret == 0)
        printf("[-] dlopen 失败!\n");
    else
        printf("[+] 注入成功!handle = 0x%lx\n", dlopen_ret);

    // 6. 恢复寄存器现场并分离
    set_regs(pid, &orig_regs);
    ptrace(PTRACE_DETACH, pid, NULL, NULL);
    return 0;
}
// ❌ 错误做法:直接写到栈上
unsigned long path_addr = (regs.sp - path_len) & ~0xF;
regs.sp = path_addr - 128;
ptrace_write_data(pid, path_addr, so_path, path_len);

regs.regs[0] = path_addr;   // x0 = so 路径
regs.regs[1] = RTLD_NOW;    // x1 = flags
regs.pc = dlopen_addr;
regs.regs[30] = 0;          // lr = 0
// ✅ 正确做法:先 mmap 分配内存,再写入路径
// 1. 远程调用 mmap
long mmap_result = call_remote_func(pid, &orig_regs, remote_mmap,
    0, 0x1000, PROT_READ | PROT_WRITE,
    MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);

// 2. 将路径写入 mmap 分配的内存
ptrace_write_data(pid, mmap_result, so_path, path_len);

// 3. 用 mmap 的地址作为 dlopen 的参数
long dlopen_ret = call_remote_func(pid, &orig_regs, remote_dlopen,
    mmap_result, RTLD_NOW, 0, 0, 0, 0);
struct user_regs_struct regs;
ptrace(PTRACE_GETREGS, pid, NULL, &regs);
// 设置参数
regs.rdi = path_addr;       // rdi = so 路径
regs.rsi = RTLD_NOW;        // rsi = flags
regs.rip = dlopen_addr;     // rip = dlopen 函数地址

// 模拟 call 指令:将返回地址压栈
regs.rsp -= 8;
ptrace(PTRACE_POKEDATA, pid, (void *)regs.rsp, (void *)0);  // 返回地址设为 0

ptrace(PTRACE_SETREGS, pid, NULL, &regs);
ptrace(PTRACE_CONT, pid, NULL, NULL);
// 等待 SIGSEGV(因为返回地址为 0)
waitpid(pid, &status, 0);

传播安全知识、拓宽行业人脉——看雪讲师团队等你加入!

收藏
免费 1
支持
分享
最新回复 (0)
游客
登录 | 注册 方可回帖
返回