Ptrace注入代码在不同平台的区别(ARM64、x86-64、MIPS64)

admin 2026-04-28 05:45:30 网络安全文章 来源:ZONE.CI 全球网 0 阅读模式

文章总结: 本文详细解析了基于Ptrace的系统级代码注入技术在ARM64与x86-64等平台上的实现差异。核心流程包括附加目标进程、保存寄存器、通过解析maps文件计算远程函数地址以绕过ASLR、写入参数并调用dlopen加载指定SO库。文章指出不同平台在调用约定与寄存器设置上存在显著区别,例如ARM64利用lr寄存器置零触发异常信号来同步调用结束。作者还分享了实战经验,强调在Android环境下不能直接将SO路径写入目标进程栈空间,必须先调用mmap分配独立内存,否则会导致注入失败。 综合评分: 88 文章分类: 逆向分析,移动安全,二进制安全,实战经验


cover_image

Ptrace注入代码在不同平台的区别(ARM64、x86-64、MIPS64)

plight plight

看雪学苑

2026年4月27日 18:04 上海

在小说阅读器读本章

去阅读

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

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

Ptrace

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

ptrace 的核心能力包括:

  • 读写目标进程内存:通过PTRACE_PEEKDATA/PTRACE_POKEDATA读写目标进程的内存内容。
  • 读写目标进程寄存器:通过PTRACE_GETREGS/PTRACE_SETREGS获取和修改目标进程的寄存器状态。
  • 控制目标进程执行:通过PTRACE_CONT、PTRACE_SINGLESTEP等控制目标进程的继续执行或单步执行。
  • 附加/分离目标进程:通过PTRACE_ATTACH/PTRACE_DETACH附加到目标进程或从目标进程分离。

基本的函数原型:

longptrace(enum __ptrace_request request, pid_t pid, void *addr, void *data);

注入流程

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

通用的注入流程如下:

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

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

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

远程函数地址 = 本进程函数地址 - 本进程模块基址 + 目标进程模块基址

具体步骤:

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

  • 在注入工具(本进程)中,通过dlsym获取dlopen的地址,记为local_dlopen_addr
  • 解析/proc/self/maps,找到dlopen所在模块(如libc.so)的基地址,记为local_base
  • 解析/proc/<pid>/maps,找到目标进程中同一模块的基地址,记为remote_base
  • 计算偏移:offset = local_dlopen_addr - local_base
  • 得到目标进程中的地址:remote_dlopen_addr = remote_base + offset

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

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

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

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

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

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

ARM64下的注入

寄存器与调用约定

ARM64 调用约定:

  • 参数传递:前 8 个参数通过x0-x7寄存器传递。
  • 返回值:通过x0寄存器返回。
  • 程序计数器:pc寄存器,指向将要执行的指令。
  • 栈顶指针:sp寄存器。
  • 链接寄存器:lr(即x30),保存函数返回地址。

寄存器结构体

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

struct&nbsp;user_pt_regs&nbsp;{
&nbsp; &nbsp; __u64&nbsp;regs[31]; &nbsp;// x0 - x30
&nbsp; &nbsp; __u64&nbsp;sp;
&nbsp; &nbsp; __u64&nbsp;pc;
&nbsp; &nbsp; __u64&nbsp;pstate;
};

// 获取寄存器
struct&nbsp;iovec&nbsp;iov;
struct&nbsp;user_pt_regs&nbsp;regs;
iov.iov_base = ®s;
iov.iov_len =&nbsp;sizeof(regs);
ptrace(PTRACE_GETREGSET, pid, (void*)NT_PRSTATUS, &iov);

调用dlopen

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

// 设置参数
regs.regs[0] = so_path_addr; &nbsp;&nbsp;// x0 = 第一个参数:so库路径地址
regs.regs[1] = RTLD_NOW; &nbsp; &nbsp; &nbsp;&nbsp;// x1 = 第二个参数:dlopen flags
regs.pc = dlopen_addr; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;// pc = dlopen 函数地址
regs.regs[30] =&nbsp;0; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;// lr = 0,使 dlopen 返回后触发 SIGSEGV

ptrace(PTRACE_SETREGSET, pid, (void*)NT_PRSTATUS, &iov);
ptrace(PTRACE_CONT, pid,&nbsp;NULL,&nbsp;NULL);
// 等待目标进程触发 SIGSEGV(因为 lr = 0,dlopen 返回后会跳转到地址 0)
waitpid(pid, &status,&nbsp;0);

ARM64 的关键点:

  • lr(x30)设置为 0。当dlopen执行完毕后,会尝试返回到lr指向的地址(即地址 0),这会触发SIGSEGV信号。
  • 通过waitpid捕获这个SIGSEGV信号,就知道dlopen已经执行完毕。
  • 此时读取x0寄存器即可获得dlopen的返回值。

注入工具

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

// injector_arm64.c - ARM64 ptrace 注入工具
// 用法: ./injector_arm64 <pid> <so_absolute_path>
// 需要 root 权限

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

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

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

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

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

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

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

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

unsignedlong&nbsp;target = (unsignedlong)addr;
char&nbsp;found_module[256] = {0};

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

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

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

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

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

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

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

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

if&nbsp;(set_regs(pid, ®s) <&nbsp;0)&nbsp;return&nbsp;-1;

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

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

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

// 向目标进程写入数据(以 long 为单位,按需补齐)
staticintptrace_write_data(pid_t&nbsp;pid,&nbsp;unsignedlong&nbsp;addr,&nbsp;constvoid&nbsp;*data,&nbsp;size_t&nbsp;len)&nbsp;{
constunsignedchar&nbsp;*src = (constunsignedchar&nbsp;*)data;
size_t&nbsp;i =&nbsp;0;

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

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

pid_t&nbsp;pid =&nbsp;atoi(argv[1]);
constchar&nbsp;*so_path = argv[2];
int&nbsp;status;

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

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

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

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

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

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

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

// 6. 恢复寄存器现场并分离
set_regs(pid, &orig_regs);
ptrace(PTRACE_DETACH, pid,&nbsp;NULL,&nbsp;NULL);
return&nbsp;0;
}

ARM64 (Android) 注入踩坑:SO 路径不能直接写到栈上

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

最初的实现:

// ❌ 错误做法:直接写到栈上
unsignedlong&nbsp;path_addr = (regs.sp - path_len) & ~0xF;
regs.sp = path_addr -&nbsp;128;
ptrace_write_data(pid, path_addr, so_path, path_len);

regs.regs[0] = path_addr; &nbsp;&nbsp;// x0 = so 路径
regs.regs[1] = RTLD_NOW; &nbsp; &nbsp;// x1 = flags
regs.pc = dlopen_addr;
regs.regs[30] =&nbsp;0; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;// lr = 0

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

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

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

// ✅ 正确做法:先 mmap 分配内存,再写入路径
// 1. 远程调用 mmap
long&nbsp;mmap_result =&nbsp;call_remote_func(pid, &orig_regs, remote_mmap,
0,&nbsp;0x1000, PROT_READ | PROT_WRITE,
&nbsp; &nbsp; MAP_PRIVATE | MAP_ANONYMOUS,&nbsp;-1,&nbsp;0);

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

// 3. 用 mmap 的地址作为 dlopen 的参数
long&nbsp;dlopen_ret =&nbsp;call_remote_func(pid, &orig_regs, remote_dlopen,
&nbsp; &nbsp; mmap_result, RTLD_NOW,&nbsp;0,&nbsp;0,&nbsp;0,&nbsp;0);

x86-64下的注入

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

寄存器与调用约定

x86-64 调用约定:

  • 参数传递:前 6 个整数参数依次通过rdi、rsi、rdx、rcx、r8、r9传递
  • 返回值:通过rax寄存器返回
  • 程序计数器:rip寄存器
  • 栈指针:rsp寄存器
  • 没有链接寄存器:x86-64 使用栈来保存返回地址,call指令自动压栈,ret指令自动弹栈

寄存器结构体

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

struct&nbsp;user_regs_struct&nbsp;regs;
ptrace(PTRACE_GETREGS, pid,&nbsp;NULL, ®s);

调用 dlopen(错误的方式)

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

// 设置参数
regs.rdi = path_addr; &nbsp; &nbsp; &nbsp;&nbsp;// rdi = so 路径
regs.rsi = RTLD_NOW; &nbsp; &nbsp; &nbsp; &nbsp;// rsi = flags
regs.rip = dlopen_addr; &nbsp; &nbsp;&nbsp;// rip = dlopen 函数地址

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

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

完整代码如下:

// x86-64 ptrace 注入工具
// 用法: ./injector_x86_64 <pid> <so_absolute_path>
// 需要 root 权限
// 参考 ARM64 实现:先 mmap 分配内存,再写入 SO 路径,最后调用 dlopen

#define&nbsp;_GNU_SOURCE
#include&nbsp;<stdio.h>
#include&nbsp;<stdlib.h>
#include&nbsp;<string.h>
#include&nbsp;<errno.h>
#include&nbsp;<unistd.h>
#include&nbsp;<sys/ptrace.h>
#include&nbsp;<sys/types.h>
#include&nbsp;<sys/wait.h>
#include&nbsp;<sys/user.h>
#include&nbsp;<sys/mman.h>
#include&nbsp;<dlfcn.h>

// 获取寄存器
staticintget_regs(pid_t&nbsp;pid,&nbsp;struct&nbsp;user_regs_struct *regs)&nbsp;{
if&nbsp;(ptrace(PTRACE_GETREGS, pid,&nbsp;NULL, regs) <&nbsp;0) {
perror("PTRACE_GETREGS");
return&nbsp;-1;
&nbsp; &nbsp; }
return&nbsp;0;
}

// 设置寄存器
staticintset_regs(pid_t&nbsp;pid,&nbsp;struct&nbsp;user_regs_struct *regs)&nbsp;{
if&nbsp;(ptrace(PTRACE_SETREGS, pid,&nbsp;NULL, regs) <&nbsp;0) {
perror("PTRACE_SETREGS");
return&nbsp;-1;
&nbsp; &nbsp; }
return&nbsp;0;
}

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

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

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

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

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

unsignedlong&nbsp;target = (unsignedlong)addr;
char&nbsp;found_module[256] = {0};

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

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

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

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

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

printf("[*] %s: local_base=0x%lx, remote_base=0x%lx\n",
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;module_name, local_base, remote_base);
return&nbsp;remote_base + ((long)local_func - local_base);
}

// 在远程进程中调用一个函数,最多支持 6 个参数
// 原理: 设置 rdi/rsi/rdx/rcx/r8/r9 为参数, rip 为函数地址
// &nbsp; &nbsp; &nbsp; 手动将返回地址 0 压栈 (模拟 call 指令)
// &nbsp; &nbsp; &nbsp; PTRACE_CONT 后等待 SIGSEGV
// &nbsp; &nbsp; &nbsp; 读取 rax 获取返回值
staticlongcall_remote_func(pid_t&nbsp;pid,&nbsp;struct&nbsp;user_regs_struct *orig_regs,
long&nbsp;func_addr,
long&nbsp;arg0,&nbsp;long&nbsp;arg1,&nbsp;long&nbsp;arg2,
long&nbsp;arg3,&nbsp;long&nbsp;arg4,&nbsp;long&nbsp;arg5)&nbsp;{
// 基于原始寄存器来设置,保证 rsp 等关键寄存器正确
struct&nbsp;user_regs_struct&nbsp;regs;
memcpy(®s, orig_regs,&nbsp;sizeof(regs));

// x86-64 调用约定: 前6个整数参数依次通过 rdi, rsi, rdx, rcx, r8, r9 传递
&nbsp; &nbsp; regs.rdi = arg0;
&nbsp; &nbsp; regs.rsi = arg1;
&nbsp; &nbsp; regs.rdx = arg2;
&nbsp; &nbsp; regs.rcx = arg3;
&nbsp; &nbsp; regs.r8 &nbsp;= arg4;
&nbsp; &nbsp; regs.r9 &nbsp;= arg5;

// 设置 rip 为目标函数地址
&nbsp; &nbsp; regs.rip = func_addr;

// ★ x86-64 关键步骤: 模拟 call 指令,将返回地址压栈
// x86-64 没有链接寄存器,返回地址保存在栈上
// call 指令等价于: push 返回地址; jmp 函数地址
&nbsp; &nbsp; regs.rsp -=&nbsp;8;
if&nbsp;(ptrace(PTRACE_POKEDATA, pid, (void&nbsp;*)regs.rsp, (void&nbsp;*)0) <&nbsp;0) {
perror("PTRACE_POKEDATA (push return addr)");
return&nbsp;-1;
&nbsp; &nbsp; }

if&nbsp;(set_regs(pid, ®s) <&nbsp;0)&nbsp;return&nbsp;-1;

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

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

if&nbsp;(WIFSTOPPED(status) &&&nbsp;WSTOPSIG(status) == SIGSEGV) {
// 预期的 SIGSEGV,函数已执行完毕
&nbsp; &nbsp; }&nbsp;else&nbsp;if&nbsp;(WIFSTOPPED(status)) {
printf("[!] 收到非预期信号: %d\n",&nbsp;WSTOPSIG(status));
&nbsp; &nbsp; }

// 读取返回值 (x86-64 返回值在 rax)
if&nbsp;(get_regs(pid, ®s) <&nbsp;0)&nbsp;return&nbsp;-1;
return&nbsp;(long)regs.rax;
}

// 向目标进程写入数据(以 long 为单位,按需补齐)
staticintptrace_write_data(pid_t&nbsp;pid,&nbsp;unsignedlong&nbsp;addr,&nbsp;constvoid&nbsp;*data,&nbsp;size_t&nbsp;len)&nbsp;{
constunsignedchar&nbsp;*src = (constunsignedchar&nbsp;*)data;
size_t&nbsp;i =&nbsp;0;

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

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

pid_t&nbsp;pid =&nbsp;atoi(argv[1]);
constchar&nbsp;*so_path = argv[2];
int&nbsp;status;

printf("[*] 目标进程: %d\n", pid);
printf("[*] 注入 so: %s\n", so_path);

// 1. 附加目标进程
if&nbsp;(ptrace(PTRACE_ATTACH, pid,&nbsp;NULL,&nbsp;NULL) <&nbsp;0) {
perror("PTRACE_ATTACH");
return&nbsp;1;
&nbsp; &nbsp; }
waitpid(pid, &status, WUNTRACED);
printf("[+] 已附加到目标进程\n");

// 2. 保存原始寄存器
struct&nbsp;user_regs_struct&nbsp;orig_regs;
if&nbsp;(get_regs(pid, &orig_regs) <&nbsp;0) {
ptrace(PTRACE_DETACH, pid,&nbsp;NULL,&nbsp;NULL);
return&nbsp;1;
&nbsp; &nbsp; }
printf("[+] 已保存寄存器现场\n");

// 3. 在远程进程中调用 mmap 分配内存
long&nbsp;remote_mmap =&nbsp;get_remote_func_addr(pid, (void&nbsp;*)mmap);
if&nbsp;(!remote_mmap) {
fprintf(stderr,&nbsp;"[-] 获取远程 mmap 地址失败\n");
set_regs(pid, &orig_regs);
ptrace(PTRACE_DETACH, pid,&nbsp;NULL,&nbsp;NULL);
return&nbsp;1;
&nbsp; &nbsp; }
printf("[+] 远程 mmap 地址: 0x%lx\n", remote_mmap);

long&nbsp;mmap_result =&nbsp;call_remote_func(pid, &orig_regs, remote_mmap,
0, &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;// addr = NULL
0x1000, &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;// length = 4096
&nbsp; &nbsp; &nbsp; &nbsp; PROT_READ | PROT_WRITE, &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;// prot
&nbsp; &nbsp; &nbsp; &nbsp; MAP_PRIVATE | MAP_ANONYMOUS, &nbsp; &nbsp;// flags
-1, &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;// fd
0); &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;// offset

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

// 4. 将 so 路径写入 mmap 分配的内存
size_t&nbsp;path_len =&nbsp;strlen(so_path) +&nbsp;1;
if&nbsp;(ptrace_write_data(pid, (unsignedlong)mmap_result, so_path, path_len) <&nbsp;0) {
fprintf(stderr,&nbsp;"[-] 写入 so 路径失败\n");
set_regs(pid, &orig_regs);
ptrace(PTRACE_DETACH, pid,&nbsp;NULL,&nbsp;NULL);
return&nbsp;1;
&nbsp; &nbsp; }
printf("[+] so 路径已写入远程内存 @ 0x%lx\n", mmap_result);

// 5. 获取远程 dlopen 地址并调用
void&nbsp;*local_dlopen =&nbsp;dlsym(RTLD_DEFAULT,&nbsp;"dlopen");
if&nbsp;(!local_dlopen) {
fprintf(stderr,&nbsp;"[-] 无法获取 dlopen 地址\n");
set_regs(pid, &orig_regs);
ptrace(PTRACE_DETACH, pid,&nbsp;NULL,&nbsp;NULL);
return&nbsp;1;
&nbsp; &nbsp; }
printf("[*] 本地 dlopen 地址: %p\n", local_dlopen);

long&nbsp;remote_dlopen =&nbsp;get_remote_func_addr(pid, local_dlopen);
if&nbsp;(!remote_dlopen) {
fprintf(stderr,&nbsp;"[-] 获取远程 dlopen 地址失败\n");
set_regs(pid, &orig_regs);
ptrace(PTRACE_DETACH, pid,&nbsp;NULL,&nbsp;NULL);
return&nbsp;1;
&nbsp; &nbsp; }
printf("[+] 远程 dlopen 地址: 0x%lx\n", remote_dlopen);

long&nbsp;dlopen_ret =&nbsp;call_remote_func(pid, &orig_regs, remote_dlopen,
&nbsp; &nbsp; &nbsp; &nbsp; (long)mmap_result, &nbsp; &nbsp;// rdi = so 路径地址 (mmap 分配的)
&nbsp; &nbsp; &nbsp; &nbsp; RTLD_NOW, &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;// rsi = flags
0,&nbsp;0,&nbsp;0,&nbsp;0);

printf("[+] dlopen 返回值: 0x%lx\n", dlopen_ret);

if&nbsp;(dlopen_ret ==&nbsp;0) {
printf("[-] dlopen 失败!\n");
&nbsp; &nbsp; }&nbsp;else&nbsp;{
printf("[+] 注入成功!handle = 0x%lx\n", dlopen_ret);
&nbsp; &nbsp; }

// 6. 恢复寄存器现场并分离
if&nbsp;(set_regs(pid, &orig_regs) <&nbsp;0) {
perror("set_regs restore");
&nbsp; &nbsp; }
ptrace(PTRACE_DETACH, pid,&nbsp;NULL,&nbsp;NULL);
printf("[+] 已恢复现场并分离目标进程\n");

return&nbsp;0;
}

失败的输出

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

[*] 目标进程: 1114
[*] 注入 so: /path/to/inject.so
[+] 已附加到目标进程
[+] 已保存寄存器现场
[*] /libc.so.6: local_base=0x7bb2c7800000, remote_base=0x75a5dc400000
[+] 远程 mmap 地址: 0x75a5dc51ea90
[+] 远程 mmap 成功, 地址: 0x75a5dc7a5000
[+] so 路径已写入远程内存 @ 0x75a5dc7a5000
[*] 本地 dlopen 地址: 0x7bb2c7890680
[*] /libc.so.6: local_base=0x7bb2c7800000, remote_base=0x75a5dc400000
[+] 远程 dlopen 地址: 0x75a5dc490680
[+] dlopen 返回值: 0xdb
[+] 注入成功!handle = 0xdb
[+] 已恢复现场并分离目标进程

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

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

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

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

// 手动设置返回地址为 0
regs.rsp -=&nbsp;8;
ptrace(PTRACE_POKEDATA, pid, (void&nbsp;*)regs.rsp, (void&nbsp;*)0);

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

| 对比项 | mmap | dlopen | | — | — | — | | 实现复杂度 | 简单,直接 syscall | 复杂,涉及动态链接器 | | 内部调用链 | 几乎无 | 多层(_dl_open → 锁操作 → malloc → 构造函数) | | 栈对齐要求 | 宽松 | 严格(16字节对齐) | | 栈状态依赖 | 低 | 高 |

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

dlopen则完全不同:

  • 它内部会调用_dl_open,涉及动态链接器的锁和状态检查
  • 可能调用malloc分配内存
  • 执行 SO 的构造函数,可能有多层函数调用

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

失败原因总结

  • 栈对齐问题:glibc 函数要求栈 16 字节对齐,手动操作rsp很难保证
  • 栈状态依赖:dlopen 内部多层调用依赖正确的栈状态,直接设置寄存器无法满足

正确的实现:使用 Trampoline

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

// 使用 trampoline 调用dlopen(成功)

// 1. mmap 分配 RWX 内存
// 2. 前半部分存 SO 路径
// 3. 后半部分存 trampoline: call rax; int3

// trampoline 只有 3 字节:
unsignedchar&nbsp;trampoline[3] = {
0xFF,&nbsp;0xD0, &nbsp;&nbsp;// call rax &nbsp;(调用 dlopen,自动压栈返回地址)
0xCC&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;// int3 &nbsp; &nbsp; &nbsp;(dlopen 返回后触发 SIGTRAP)
};

// 执行流程:
// 1. call rax 自动将返回地址(即 int3 的地址)压入栈,跳转到 dlopen
// 2. dlopen 执行完毕 ret,弹出返回地址,回到 int3
// 3. int3 触发 SIGTRAP,注入器读取返回值

优势

  • call自动处理栈对齐和返回地址压栈
  • 代码在目标进程内存中执行,接近正常函数调用
  • 避免手动操作寄存器的各种坑

完整代码如下:

// x86-64 ptrace 注入工具 (v2)
// 用法: ./injector_x86_64_v2 <pid> <so_absolute_path>
// 需要 root 权限
// 思路:
// &nbsp; 1. mmap 分配一块 RWX 内存
// &nbsp; &nbsp; &nbsp;- 前半部分存 SO 路径字符串
// &nbsp; &nbsp; &nbsp;- 后半部分存 trampoline: call dlopen; int3
// &nbsp; 2. 设置 regs.rdi=路径, regs.rsi=flags, regs.rax=dlopen, regs.rip=trampoline
// &nbsp; 3. 跳转到 trampoline 执行,dlopen 返回后执行 int3 触发 SIGTRAP

#define&nbsp;_GNU_SOURCE
#include&nbsp;<stdio.h>
#include&nbsp;<stdlib.h>
#include&nbsp;<string.h>
#include&nbsp;<errno.h>
#include&nbsp;<unistd.h>
#include&nbsp;<sys/ptrace.h>
#include&nbsp;<sys/types.h>
#include&nbsp;<sys/wait.h>
#include&nbsp;<sys/user.h>
#include&nbsp;<sys/mman.h>
#include&nbsp;<dlfcn.h>

// 获取寄存器
staticintget_regs(pid_t&nbsp;pid,&nbsp;struct&nbsp;user_regs_struct *regs)&nbsp;{
if&nbsp;(ptrace(PTRACE_GETREGS, pid,&nbsp;NULL, regs) <&nbsp;0) {
perror("PTRACE_GETREGS");
return&nbsp;-1;
&nbsp; &nbsp; }
return&nbsp;0;
}

// 设置寄存器
staticintset_regs(pid_t&nbsp;pid,&nbsp;struct&nbsp;user_regs_struct *regs)&nbsp;{
if&nbsp;(ptrace(PTRACE_SETREGS, pid,&nbsp;NULL, regs) <&nbsp;0) {
perror("PTRACE_SETREGS");
return&nbsp;-1;
&nbsp; &nbsp; }
return&nbsp;0;
}

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

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

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

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

// 通过地址查找其所在的模块名和模块基址
// 扫描 /proc/self/maps,找到包含 addr 的映射区间,提取模块路径
staticintfind_module_by_addr(void&nbsp;*addr,&nbsp;char&nbsp;*module_name,&nbsp;size_t&nbsp;name_len,&nbsp;long&nbsp;*base)&nbsp;{
char&nbsp;line[512];
&nbsp; &nbsp; FILE *fp =&nbsp;fopen("/proc/self/maps",&nbsp;"r");
if&nbsp;(!fp)&nbsp;return&nbsp;-1;

unsignedlong&nbsp;target = (unsignedlong)addr;
char&nbsp;found_module[256] = {0};

while&nbsp;(fgets(line,&nbsp;sizeof(line), fp)) {
unsignedlong&nbsp;start, end;
if&nbsp;(sscanf(line,&nbsp;"%lx-%lx", &start, &end) !=&nbsp;2)&nbsp;continue;

if&nbsp;(target >= start && target < end) {
// 提取模块路径 (maps 格式: addr-addr perms offset dev inode pathname)
char&nbsp;*path =&nbsp;strrchr(line,&nbsp;'/');
if&nbsp;(path) {
// 去掉换行
char&nbsp;*nl =&nbsp;strchr(path,&nbsp;'\n');
if&nbsp;(nl) *nl =&nbsp;'\0';
strncpy(found_module, path,&nbsp;sizeof(found_module) -&nbsp;1);
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; }
break;
&nbsp; &nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; }
fclose(fp);

if&nbsp;(found_module[0] ==&nbsp;'\0')&nbsp;return&nbsp;-1;

// 找到模块后,获取该模块的基地址(第一个映射的起始地址)
&nbsp; &nbsp; *base =&nbsp;get_module_base(0, found_module);
strncpy(module_name, found_module, name_len -&nbsp;1);
&nbsp; &nbsp; module_name[name_len -&nbsp;1] =&nbsp;'\0';
return&nbsp;0;
}

// 计算目标进程中某函数的地址
// 自动检测函数所在模块,避免模块名猜错导致地址计算错误
staticlongget_remote_func_addr(pid_t&nbsp;pid,&nbsp;void&nbsp;*local_func,&nbsp;constchar&nbsp;**out_module)&nbsp;{
char&nbsp;module_name[256];
long&nbsp;local_base;

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

long&nbsp;remote_base =&nbsp;get_module_base(pid, module_name);

if&nbsp;(!local_base || !remote_base) {
fprintf(stderr,&nbsp;"Failed to get module base for %s (local: 0x%lx, remote: 0x%lx)\n",
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; module_name, local_base, remote_base);
return&nbsp;0;
&nbsp; &nbsp; }

if&nbsp;(out_module) *out_module =&nbsp;strdup(module_name);

long&nbsp;offset = (long)local_func - local_base;
printf("[*] %s: local_base=0x%lx, remote_base=0x%lx, offset=0x%lx\n",
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;module_name, local_base, remote_base, offset);
return&nbsp;remote_base + offset;
}

// 向目标进程写入数据(以 long 为单位,按需补齐)
staticintptrace_write_data(pid_t&nbsp;pid,&nbsp;unsignedlong&nbsp;addr,&nbsp;constvoid&nbsp;*data,&nbsp;size_t&nbsp;len)&nbsp;{
constunsignedchar&nbsp;*src = (constunsignedchar&nbsp;*)data;
size_t&nbsp;i =&nbsp;0;

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

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

return&nbsp;0;
}

// 在远程进程中调用 mmap(addr=NULL, size, prot, flags, fd=-1, offset=0)
// 原理: 设置 rdi, rsi, rdx, rcx, r8, r9 为参数, rip 为函数地址, rsp 指向的返回地址设为 0
// &nbsp; &nbsp; &nbsp; PTRACE_CONT 后等待 SIGSEGV (因为 ret 跳转到地址 0)
// &nbsp; &nbsp; &nbsp; 读取 rax 获取返回值
staticlongremote_mmap(pid_t&nbsp;pid,&nbsp;struct&nbsp;user_regs_struct *orig_regs,&nbsp;long&nbsp;size,&nbsp;int&nbsp;prot,&nbsp;int&nbsp;flags)&nbsp;{
// 获取远程 mmap 地址
long&nbsp;remote_mmap_addr =&nbsp;get_remote_func_addr(pid, (void&nbsp;*)mmap,&nbsp;NULL);
if&nbsp;(!remote_mmap_addr)&nbsp;return&nbsp;-1;

// 基于原始寄存器来设置,保证 rsp 等关键寄存器正确
struct&nbsp;user_regs_struct&nbsp;regs;
memcpy(®s, orig_regs,&nbsp;sizeof(regs));

// 设置参数 (x86-64 System V ABI: rdi, rsi, rdx, rcx, r8, r9 传参)
// mmap(NULL, size, prot, flags, fd=-1, offset=0)
&nbsp; &nbsp; regs.rdi =&nbsp;0; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;// addr = NULL
&nbsp; &nbsp; regs.rsi = size; &nbsp; &nbsp; &nbsp; &nbsp;// length
&nbsp; &nbsp; regs.rdx = prot; &nbsp; &nbsp; &nbsp; &nbsp;// prot
&nbsp; &nbsp; regs.rcx = flags; &nbsp; &nbsp; &nbsp;&nbsp;// flags
&nbsp; &nbsp; regs.r8 &nbsp;=&nbsp;-1; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;// fd
&nbsp; &nbsp; regs.r9 &nbsp;=&nbsp;0; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;// offset

// 设置 rip 为 mmap 地址
&nbsp; &nbsp; regs.rip = remote_mmap_addr;

// 关键步骤: 分配一个栈空间,将返回地址设为 0
// 函数执行完 ret 后会跳转到地址 0,触发 SIGSEGV
&nbsp; &nbsp; regs.rsp = ((orig_regs->rsp -&nbsp;256) & ~0xF) -&nbsp;8;
// 在栈顶写入 0 作为返回地址
long&nbsp;ret_addr =&nbsp;0;
if&nbsp;(ptrace(PTRACE_POKEDATA, pid, (void&nbsp;*)regs.rsp, (void&nbsp;*)ret_addr) <&nbsp;0) {
perror("PTRACE_POKEDATA (ret_addr)");
return&nbsp;-1;
&nbsp; &nbsp; }

if&nbsp;(set_regs(pid, ®s) <&nbsp;0)&nbsp;return&nbsp;-1;

// 继续执行
if&nbsp;(ptrace(PTRACE_CONT, pid,&nbsp;NULL,&nbsp;NULL) <&nbsp;0) {
perror("PTRACE_CONT");
return&nbsp;-1;
&nbsp; &nbsp; }

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

if&nbsp;(WIFSTOPPED(status) &&&nbsp;WSTOPSIG(status) == SIGSEGV) {
// 预期的 SIGSEGV,函数已执行完毕
&nbsp; &nbsp; }&nbsp;else&nbsp;if&nbsp;(WIFSTOPPED(status)) {
printf("[!] mmap: 收到非预期信号: %d\n",&nbsp;WSTOPSIG(status));
&nbsp; &nbsp; }

// 读取返回值 (x86-64 返回值在 rax)
if&nbsp;(get_regs(pid, ®s) <&nbsp;0)&nbsp;return&nbsp;-1;

return&nbsp;regs.rax;
}

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

pid_t&nbsp;pid =&nbsp;atoi(argv[1]);
constchar&nbsp;*so_path = argv[2];
int&nbsp;status;

printf("[*] 目标进程: %d\n", pid);
printf("[*] 注入 so: %s\n", so_path);

// 1. 附加目标进程
if&nbsp;(ptrace(PTRACE_ATTACH, pid,&nbsp;NULL,&nbsp;NULL) <&nbsp;0) {
perror("PTRACE_ATTACH");
return&nbsp;1;
&nbsp; &nbsp; }
waitpid(pid, &status, WUNTRACED);
printf("[+] 已附加到目标进程\n");

// 2. 保存原始寄存器
struct&nbsp;user_regs_struct&nbsp;orig_regs;
if&nbsp;(get_regs(pid, &orig_regs) <&nbsp;0) {
ptrace(PTRACE_DETACH, pid,&nbsp;NULL,&nbsp;NULL);
return&nbsp;1;
&nbsp; &nbsp; }
printf("[+] 已保存寄存器现场\n");

// 3. 在远程进程中调用 mmap 分配内存
// &nbsp; &nbsp;mmap(NULL, 0x2000, PROT_READ|PROT_WRITE|PROT_EXEC, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0)
// &nbsp; &nbsp;分配 8KB: 前半部分存路径,后半部分存 trampoline
long&nbsp;mmap_result =&nbsp;remote_mmap(pid, &orig_regs,
0x2000, &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;// length = 8192
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;PROT_READ | PROT_WRITE | PROT_EXEC,&nbsp;// prot = RWX
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;MAP_PRIVATE | MAP_ANONYMOUS); &nbsp;&nbsp;// flags
if&nbsp;(mmap_result ==&nbsp;-1&nbsp;|| mmap_result ==&nbsp;0) {
fprintf(stderr,&nbsp;"[-] 远程 mmap 失败, 返回: 0x%lx\n", mmap_result);
set_regs(pid, &orig_regs);
ptrace(PTRACE_DETACH, pid,&nbsp;NULL,&nbsp;NULL);
return&nbsp;1;
&nbsp; &nbsp; }
printf("[+] 远程 mmap 成功, 地址: 0x%lx\n", mmap_result);

long&nbsp;path_addr &nbsp;= mmap_result; &nbsp; &nbsp; &nbsp;&nbsp;// [0x0000] 路径
long&nbsp;tramp_addr = mmap_result +&nbsp;0x1000;&nbsp;// [0x1000] trampoline

// 5. 将 so 路径写入 mmap 分配的内存
size_t&nbsp;path_len =&nbsp;strlen(so_path) +&nbsp;1;
if&nbsp;(ptrace_write_data(pid, (unsignedlong)path_addr, so_path, path_len) <&nbsp;0) {
fprintf(stderr,&nbsp;"[-] 写入 so 路径失败\n");
set_regs(pid, &orig_regs);
ptrace(PTRACE_DETACH, pid,&nbsp;NULL,&nbsp;NULL);
return&nbsp;1;
&nbsp; &nbsp; }
printf("[+] so 路径已写入远程内存 @ 0x%lx\n", path_addr);

// 6. 写入 trampoline: call rax; int3
// &nbsp; &nbsp;call rax (0xFF 0xD0) 会调用 rax 指向的函数
// &nbsp; &nbsp;int3 (0xCC) 触发 SIGTRAP
unsignedchar&nbsp;trampoline[3] = {
0xFF,&nbsp;0xD0, &nbsp;&nbsp;// call rax
0xCC&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;// int3 (触发 SIGTRAP)
&nbsp; &nbsp; };
if&nbsp;(ptrace_write_data(pid, (unsignedlong)tramp_addr, trampoline,&nbsp;sizeof(trampoline)) <&nbsp;0) {
fprintf(stderr,&nbsp;"[-] 写入 trampoline 失败\n");
set_regs(pid, &orig_regs);
ptrace(PTRACE_DETACH, pid,&nbsp;NULL,&nbsp;NULL);
return&nbsp;1;
&nbsp; &nbsp; }
printf("[+] trampoline 已写入 @ 0x%lx (call rax; int3)\n", tramp_addr);

// 7. 获取远程 dlopen 地址 (自动检测所在模块)
void&nbsp;*local_dlopen =&nbsp;dlsym(RTLD_DEFAULT,&nbsp;"dlopen");
if&nbsp;(!local_dlopen) {
fprintf(stderr,&nbsp;"[-] 无法获取 dlopen 地址\n");
set_regs(pid, &orig_regs);
ptrace(PTRACE_DETACH, pid,&nbsp;NULL,&nbsp;NULL);
return&nbsp;1;
&nbsp; &nbsp; }
printf("[*] 本地 dlopen 地址: %p\n", local_dlopen);

constchar&nbsp;*dlopen_module =&nbsp;NULL;
long&nbsp;remote_dlopen =&nbsp;get_remote_func_addr(pid, local_dlopen, &dlopen_module);
if&nbsp;(!remote_dlopen) {
fprintf(stderr,&nbsp;"[-] 获取远程 dlopen 地址失败\n");
set_regs(pid, &orig_regs);
ptrace(PTRACE_DETACH, pid,&nbsp;NULL,&nbsp;NULL);
return&nbsp;1;
&nbsp; &nbsp; }
printf("[+] 远程 dlopen 地址: 0x%lx (模块: %s)\n", remote_dlopen,
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;dlopen_module ? dlopen_module :&nbsp;"unknown");

// 8. 使用 trampoline 方式调用 dlopen
// &nbsp; &nbsp;设置 rax = dlopen 地址 (call rax 用)
// &nbsp; &nbsp;设置 rdi = so 路径地址
// &nbsp; &nbsp;设置 rsi = RTLD_NOW
// &nbsp; &nbsp;设置 rip = trampoline 地址
struct&nbsp;user_regs_struct&nbsp;regs;
memcpy(®s, &orig_regs,&nbsp;sizeof(regs));
&nbsp; &nbsp; regs.rax = remote_dlopen; &nbsp;&nbsp;// rax = dlopen 地址 (call rax 用)
&nbsp; &nbsp; regs.rdi = path_addr; &nbsp; &nbsp; &nbsp;&nbsp;// rdi = so 路径
&nbsp; &nbsp; regs.rsi = RTLD_NOW; &nbsp; &nbsp; &nbsp; &nbsp;// rsi = flags
&nbsp; &nbsp; regs.rip = tramp_addr; &nbsp; &nbsp; &nbsp;// rip = trampoline

if&nbsp;(set_regs(pid, ®s) <&nbsp;0) {
fprintf(stderr,&nbsp;"[-] 设置寄存器失败\n");
set_regs(pid, &orig_regs);
ptrace(PTRACE_DETACH, pid,&nbsp;NULL,&nbsp;NULL);
return&nbsp;1;
&nbsp; &nbsp; }

if&nbsp;(ptrace(PTRACE_CONT, pid,&nbsp;NULL,&nbsp;NULL) <&nbsp;0) {
perror("PTRACE_CONT");
set_regs(pid, &orig_regs);
ptrace(PTRACE_DETACH, pid,&nbsp;NULL,&nbsp;NULL);
return&nbsp;1;
&nbsp; &nbsp; }

waitpid(pid, &status, WUNTRACED);

if&nbsp;(WIFSTOPPED(status)) {
int&nbsp;sig =&nbsp;WSTOPSIG(status);
printf("[*] 收到信号: %d (%s)\n", sig,
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;sig == SIGTRAP ?&nbsp;"SIGTRAP"&nbsp;:
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;sig == SIGSEGV ?&nbsp;"SIGSEGV"&nbsp;:&nbsp;"other");
&nbsp; &nbsp; }

// 9. 读取 dlopen 返回值
if&nbsp;(get_regs(pid, ®s) <&nbsp;0) {
fprintf(stderr,&nbsp;"[-] 读取寄存器失败\n");
set_regs(pid, &orig_regs);
ptrace(PTRACE_DETACH, pid,&nbsp;NULL,&nbsp;NULL);
return&nbsp;1;
&nbsp; &nbsp; }

long&nbsp;dlopen_ret = regs.rax;
printf("[+] dlopen 返回值: 0x%lx\n", dlopen_ret);

if&nbsp;(dlopen_ret ==&nbsp;0) {
printf("[-] dlopen 失败!\n");
&nbsp; &nbsp; }&nbsp;else&nbsp;{
printf("[+] 注入成功!handle = 0x%lx\n", dlopen_ret);
&nbsp; &nbsp; }

// 10. 恢复寄存器现场并分离
if&nbsp;(set_regs(pid, &orig_regs) <&nbsp;0) {
perror("set_regs restore");
&nbsp; &nbsp; }
ptrace(PTRACE_DETACH, pid,&nbsp;NULL,&nbsp;NULL);
printf("[+] 已恢复现场并分离目标进程\n");

return&nbsp;0;
}

MIPS64 下的注入经验与踩坑总结

之前尝试了x86-64和ARM64下的注入,这里补充一下MIPS64下的注入。

Buildroot + QEMU 环境搭建

这次测试环境不是实体 MIPS64 机器,而是用Buildroot + QEMU system mode搭出来的。其中,Buildroot用来编译出MIPS64的内核以及文件系统,QEMU用来模拟MIPS64的指令。

1. 下载并配置 Buildroot

我使用的是:

wget https://buildroot.org/downloads/buildroot-2024.02.1.tar.gz
tar xzf buildroot-2024.02.1.tar.gz
cd&nbsp;buildroot-2024.02.1

直接使用 Buildroot 自带的 QEMU MIPS64 默认配置:

make&nbsp;qemu_mips64_malta_defconfig

这里选的是MIPS64 big-endianmalta板级模型。执行完成后会生成.config

2. 开启 SSH(用于文件传输)

Buildroot 默认的 SSH 服务需要在配置文件中手动开启:

# 开启 openssh
cat >> .config << 'EOF'
BR2_PACKAGE_OPENSSH=y
BR2_PACKAGE_OPENSSH_SERVER=y
BR2_PACKAGE_OPENSSH_CLIENT=y
EOF

# 同步配置,会弹出一些选项让你选一路回车保持默认即可
make oldconfig

3. 编译 Buildroot

如果宿主机环境变量有问题,先清理 PATH:

export&nbsp;PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin

然后编译:

make -j$(nproc)

编译完成后,产物在:

output/images/

我这边最终得到的核心文件是:

output/images/vmlinux &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;//内核
output/images/rootfs.ext2 &nbsp; &nbsp; &nbsp;//文件系统
output/images/start-qemu.sh &nbsp; &nbsp;//qemu模拟脚本

4. 启动 QEMU

网络情况

QEMU 在 user-mode networking 下,通过ifconfig查看的网段一般是:

eth0 &nbsp;inet addr:10.0.2.15

这个10.0.2.x网段是 QEMU 自带的虚拟 NAT 网段,宿主机物理网卡上不会看到这个网段,因此需要将虚拟机的端口通过命令转发出来。

进入系统

Buildroot 已经生成了启动脚本,但是没有添加网络配置,不通过脚本启动而是直接使用下面的命令运行(这里将宿主机的2222端口映射到QEMU虚拟机中的22端口,用于SSH服务):

cd&nbsp;output/images
qemu-system-mips64 -M malta -kernel vmlinux &nbsp;-drive file=rootfs.ext2,format=raw -append&nbsp;"rootwait root=/dev/sda"&nbsp;-netdev user,id=net0,hostfwd=tcp::2222-:22 -device pcnet,netdev=net0 -nographic

启动后进入串口终端,登录提示类似:

buildroot login:root
#

Buildroot 默认 root 无密码,可以直接登录。

5. 配置 SSH

SSH登录需要设置密码,并修改SSH配置文件允许以root用户登录。

# 设置 root 密码
passwd
# 输入新密码,如 123456

# 允许 root 登录 SSH
sed -i&nbsp;'s/#PermitRootLogin.*/PermitRootLogin yes/'&nbsp;/etc/ssh/sshd_config

# 重新启动 sshd
killall sshd && /usr/sbin/sshd

配置完成之后,在宿主机上使用ssh -p2222 root@localhost即可通过SSH访问QEMU虚拟机。

使用scp -P2222 file root@localhost:/root/即可拷贝文件到QEMU虚拟机。

6. 交叉编译工具链

一开始我用宿主机系统自带的mips64-linux-gnuabi64-gcc,结果编出来的程序在 Buildroot 系统里运行时报:

error&nbsp;while&nbsp;loading&nbsp;shared&nbsp;libraries: libc.so.6: cannot open&nbsp;shared&nbsp;object&nbsp;file

原因是:交叉编译器的运行时库和 Buildroot 根文件系统不一致

最终改成直接使用 Buildroot 产出的交叉工具链:output/host/bin/mips64-buildroot-linux-gnu-gcc,这样编出来的程序才能和目标系统的 libc、动态链接器保持一致。

Mips64下的注入

寄存器与调用约定

MIPS64 使用 N64 ABI,调用约定:

  • 参数传递:前 8 个整数参数通过$a0-$a7寄存器传递(对应regs[4]-regs[11])。注意这里和 MIPS32 O32 ABI 不一样,O32 只用$a0-$a3四个参数寄存器,其余走栈。
  • 返回值:通过$v0寄存器返回(regs[2])。$v1(regs[3])用于返回 64 位结构的高位,一般单独返回值只用$v0。
  • 程序计数器:cp0_epc字段(用户态实际执行位置)。
  • 栈顶指针:$sp(regs[29])。
  • 返回地址寄存器:$ra(regs[31]),类似 ARM64 的lr,jal/jalr会自动把返回地址写入这里。
  • PIC 调用寄存器:$t9(regs[25]),MIPS 特有。调用位置无关代码(PIC)中的函数时,必须让$t9等于目标函数地址,函数序言会用$t9去重建$gp并访问 GOT,缺了它几乎必定跑飞。
  • syscall 返回约定:$v0存返回值,$a3存成功/失败标志——$a3 == 0表示成功,$a3 != 0表示出错,此时$v0是 errno。这点和 x86-64 / ARM64 都不同(那两个平台通常用返回值的正负来判断)。

寄存器结构体

MIPS64 使用struct pt_regs(定义在<asm/ptrace.h>中),通过PTRACE_GETREGS/PTRACE_SETREGS直接读写,不需要像 ARM64 那样走iovec + REGSET

struct&nbsp;pt_regs&nbsp;{
unsignedlong&nbsp;regs[32]; &nbsp;&nbsp;// $0 - $31
unsignedlong&nbsp;cp0_status;
unsignedlong&nbsp;lo;
unsignedlong&nbsp;hi;
unsignedlong&nbsp;cp0_badvaddr;
unsignedlong&nbsp;cp0_cause;
unsignedlong&nbsp;cp0_epc; &nbsp; &nbsp;// 用户态 PC
/* ... */
};

struct&nbsp;pt_regs&nbsp;regs;
ptrace(PTRACE_GETREGS, pid,&nbsp;NULL, ®s);

调用远程函数

在 MIPS64 上发起远程调用的关键设置:

regs.regs[4] &nbsp;= arg0; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;//&nbsp;$a0
regs.regs[5] &nbsp;= arg1; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;//&nbsp;$a1
/* ... a2~a7 同理 ... */
regs.regs[25] = func_addr; &nbsp; &nbsp; &nbsp;// $t9 = 目标函数地址(PIC 必须)
regs.cp0_epc &nbsp;= func_addr; &nbsp; &nbsp; &nbsp;// pc = 目标函数地址
regs.regs[31] = 0; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;// $ra =&nbsp;0,函数返回后跳到地址&nbsp;0&nbsp;触发 SIGSEGV

MIPS64 上的关键点:

  • 同时设置cp0_epc$t9。仅改pc进入函数后会因$gp计算错误而立刻崩溃。
  • $ra设为 0,作用和 ARM64 的lr = 0一样,函数返回时跳到 0 即可触发SIGSEGV作为”调用完成”的信号。
  • 读取$v0regs[2])获取返回值;如果是 syscall 路径,还要结合$a3判断是否成功。

先说最终可用方案

这次在 MIPS64(buildroot + qemu)环境下做 ptrace 注入,最终结论是:远程调用框架本身可以跑通,但mmap不能直接照搬其它架构的 libc 调用方式,最稳妥的方案是“直接 syscallmmap+ 远程dlopen

MIPS64 上最终跑通的流程是:

  • PTRACE_ATTACH
  • PTRACE_GETREGS / PTRACE_SETREGS
  • 直接发起mmap系统调用分配远程内存
  • 把 so 路径写入远程内存
  • 远程调用dlopen
  • 恢复寄存器并PTRACE_DETACH

也就是说:

  • 内存分配:不要再走远程 libcmmap()wrapper,改成syscall
  • 动态加载:dlopen仍然可以直接远程调用

踩坑 1:MIPS PIC 调用必须设置$t9

在 MIPS 上远程调用 libc 函数时,仅设置cp0_epc不够,还需要:

regs.regs[REG_T9] = func_addr;
regs.cp0_epc = func_addr;

原因是 MIPS 的 PIC(位置无关代码)通常依赖$t9来访问 GOT / 做函数内跳转。

代码示例如下:

af0:67bdffe0&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;daddiu&nbsp;&nbsp;sp,sp,-32
af4:ffbf0018&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;sd&nbsp; &nbsp; &nbsp;&nbsp;ra,24(sp)
af8:ffbe0010&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;sd&nbsp; &nbsp; &nbsp;&nbsp;s8,16(sp)
afc:ffbc0008&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;sd&nbsp; &nbsp; &nbsp;&nbsp;gp,8(sp)
b00:03a0f025&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;move&nbsp; &nbsp;&nbsp;s8,sp
b04:3c1c0002&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;lui&nbsp; &nbsp; &nbsp;gp,0x2
b08:0399e02d&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;daddu&nbsp; &nbsp;gp,gp,t9
b0c:679c7520&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;daddiu&nbsp;&nbsp;gp,gp,29984
b10:24050001&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;li&nbsp; &nbsp; &nbsp;&nbsp;a1,1
b14:24041388&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;li&nbsp; &nbsp; &nbsp;&nbsp;a0,5000
b18:df828070&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;ld&nbsp; &nbsp; &nbsp;&nbsp;v0,-32656(gp)
b1c:0040c825&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;move&nbsp; &nbsp;&nbsp;t9,v0
b20:0320f809&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;jalr&nbsp; &nbsp;&nbsp;t9
b24:00000000&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;nop

函数被调用时 t9 寄存器保存着该函数的运行时地址(通过 jalr t9 调用约定)。GP(用于定位GOT表的位置,通常为GOT表加上一定偏移的位置GOT + 0x7ff0)的计算依赖链接器提供的一个固定偏移量 _gp_disp:

&nbsp; GP = t9 + _gp_disp

&nbsp; 其中 _gp_disp = _gp符号值 - 函数起始地址,这个值在链接时已经确定。

&nbsp; b04: &nbsp; lui &nbsp; &nbsp; gp, 0x2 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;; gp = 0x0002_0000 = 0x20000 &nbsp;(高16位)
&nbsp; b08: &nbsp; daddu &nbsp; gp, gp, t9 &nbsp; &nbsp; &nbsp; ; gp = 0x20000 + t9
&nbsp; b0c: &nbsp; daddiu &nbsp;gp, gp, 29984 &nbsp; &nbsp;; gp = 0x20000 + t9 + 0x7520

&nbsp; 三步合并后:

&nbsp; GP = t9 + 0x20000 + 0x7520
&nbsp; GP = t9 + 0x27520

&nbsp; 偏移量的拆分

&nbsp; _gp_disp = 0x27520,被拆分为两部分:

&nbsp; ┌───────────────────────┬─────────────────────────────────┬────────────────────────┐
&nbsp; │ &nbsp; &nbsp; &nbsp; &nbsp; 指令 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;│ &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;作用 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; │ &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; 值 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; │
&nbsp; ├───────────────────────┼─────────────────────────────────┼────────────────────────┤
&nbsp; │ lui gp, 0x2 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; │ 加载高 16 位 (_gp_disp 的 hi16) │ 0x0002 <<&nbsp;16 = 0x20000 │
&nbsp; ├───────────────────────┼─────────────────────────────────┼────────────────────────┤
&nbsp; │ daddiu gp, gp, 0x7520 │ 加载低 16&nbsp;位 (_gp_disp 的 lo16) │ 0x7520 = 29984 &nbsp; &nbsp; &nbsp; &nbsp; │
&nbsp; └───────────────────────┴─────────────────────────────────┴────────────────────────

为什么需要这样做:

  • 位置无关:代码加载到任意地址时,t9 会反映真实的运行时地址,而函数到 GP 的偏移是固定的,因此 GP 总能被正确计算
  • GOT 访问:GP 计算完成后,后续通过 GP 的偏移来访问 GOT 表项,例如代码中的:
b30: &nbsp; ld &nbsp;v0,&nbsp;-32656(gp) &nbsp; &nbsp;; 从 GOT 加载函数指针
b34: &nbsp; move t9, v0
b38: &nbsp; jalr t9 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; ; 调用目标函数

如果只改pc,不改$t9,很容易出现:

  • 跳进函数后立刻异常
  • 进入了函数,但执行到一半崩掉

踩坑 2:libcmmap()wrapper 在远程调用场景下会失败

在目标进程里直接本地调用mmap是成功的:

mmap(NULL,&nbsp;0x1000, PROT_READ | PROT_WRITE,
&nbsp; &nbsp; &nbsp;MAP_PRIVATE | MAP_ANONYMOUS, -1,&nbsp;0)

但是通过 ptrace 远程调用 libc 的mmap()时,虽然:

  • pc/t9/a0-a5/sp/ra全部设置正确
  • 调用能正常进入并返回
  • pc最后也成功回到 dummy return address

返回值却始终是:

MAP_FAILED&nbsp;== -1

继续读取远程errno后发现:

errno&nbsp;=&nbsp;9&nbsp;(EBADF / Bad file descriptor)

这说明:

  • 远程 libcmmap()确实被调用了
  • 但它包装完再进入内核时,fd参数在这个场景下出了问题

最终解决方案:直接 syscallmmap

既然远程 libcmmap()wrapper 不可靠,最终改成了:

  • 不再调用远程 libcmmap
  • 改成直接执行 MIPS64 syscall

MIPS64 Linux 上mmap的 syscall 号是:

#define&nbsp;__NR_MMAP_MIPS64 5009

做法是:

  1. 保存目标进程当前pc处的原始指令
  2. 临时把这一条指令改成syscall指令(0x0000000c
  3. 设置:
  • v0 = syscall number
  • a0-a5 = mmap 参数
  1. PTRACE_SYSCALL跑进/跑出 syscall
  2. 读取返回寄存器:
  • a3 == 0

    表示成功

  • a3 != 0

    表示失败,v0为 errno

  1. 恢复原始指令

切到 syscall 之后,mmap立即成功,后续:

  • 写入 so 路径成功
  • 远程dlopen成功
  • 注入完成

结论

这次 MIPS64 ptrace 注入的几个关键经验可以总结为:

  1. 远程调用 PIC 函数(动态库中的函数)必须设置$t9
  2. 远程 libcmmap()wrapper 不可靠,直接 syscall 更稳

最终可工作的组合是:

  • mmap

    syscall

  • dlopen

    远程 libc 调用

这套组合在当前的 buildroot qemu mips64 环境下已经验证可用,完整代码如下:

// injector_mips64.c - MIPS64 ptrace 注入工具
// 用法: ./injector_mips64 <pid> <so_absolute_path>
// 需要 root 权限

#define&nbsp;_GNU_SOURCE
#include&nbsp;<stdio.h>
#include&nbsp;<stdlib.h>
#include&nbsp;<string.h>
#include&nbsp;<errno.h>
#include&nbsp;<unistd.h>
#include&nbsp;<sys/ptrace.h>
#include&nbsp;<sys/types.h>
#include&nbsp;<sys/wait.h>
#include&nbsp;<sys/mman.h>
#include&nbsp;<dlfcn.h>
#include&nbsp;<asm/ptrace.h>

#define&nbsp;mips64_regs pt_regs

// 寄存器别名定义
#define&nbsp;REG_V0 &nbsp;2 &nbsp;&nbsp;// 返回值
#define&nbsp;REG_A0 &nbsp;4 &nbsp;&nbsp;// 参数0
#define&nbsp;REG_A1 &nbsp;5 &nbsp;&nbsp;// 参数1
#define&nbsp;REG_A2 &nbsp;6 &nbsp;&nbsp;// 参数2
#define&nbsp;REG_A3 &nbsp;7 &nbsp;&nbsp;// 参数3
#define&nbsp;REG_A4 &nbsp;8 &nbsp;&nbsp;// 参数4 (MIPS64 N64 ABI)
#define&nbsp;REG_A5 &nbsp;9 &nbsp;&nbsp;// 参数5 (MIPS64 N64 ABI)
#define&nbsp;REG_SP &nbsp;29 &nbsp;// 栈指针
#define&nbsp;REG_RA &nbsp;31 &nbsp;// 返回地址
#define&nbsp;REG_T9 &nbsp;25 &nbsp;// 跳转目标 (PIC 代码需要)
#define&nbsp;DUMMY_RETURN_ADDRESS 0x0

staticvoiddump_regs(constchar&nbsp;*tag,&nbsp;conststruct&nbsp;mips64_regs *regs)&nbsp;{
printf("[*] %s pc=0x%lx sp=0x%lx ra=0x%lx t9=0x%lx v0=0x%lx\n",
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;tag,
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;(unsignedlonglong) regs->cp0_epc,
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;(unsignedlonglong) regs->regs[REG_SP],
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;(unsignedlonglong) regs->regs[REG_RA],
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;(unsignedlonglong) regs->regs[REG_T9],
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;(unsignedlonglong) regs->regs[REG_V0]);
printf("[*] %s a0=0x%llx a1=0x%llx a2=0x%llx a3=0x%llx a4=0x%llx a5=0x%llx\n",
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;tag,
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;(unsignedlonglong) regs->regs[REG_A0],
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;(unsignedlonglong) regs->regs[REG_A1],
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;(unsignedlonglong) regs->regs[REG_A2],
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;(unsignedlonglong) regs->regs[REG_A3],
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;(unsignedlonglong) regs->regs[REG_A4],
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;(unsignedlonglong) regs->regs[REG_A5]);
}

// 获取寄存器
staticintget_regs(pid_t&nbsp;pid,&nbsp;struct&nbsp;mips64_regs *regs)&nbsp;{
if&nbsp;(ptrace(PTRACE_GETREGS, pid,&nbsp;NULL, regs) <&nbsp;0) {
perror("PTRACE_GETREGS");
return&nbsp;-1;
&nbsp; &nbsp; }
return&nbsp;0;
}

// 设置寄存器
staticintset_regs(pid_t&nbsp;pid,&nbsp;struct&nbsp;mips64_regs *regs)&nbsp;{
if&nbsp;(ptrace(PTRACE_SETREGS, pid,&nbsp;NULL, regs) <&nbsp;0) {
perror("PTRACE_SETREGS");
return&nbsp;-1;
&nbsp; &nbsp; }
return&nbsp;0;
}

staticlongget_module_base(pid_t&nbsp;pid,&nbsp;constchar&nbsp;*module_name)&nbsp;{
char&nbsp;path[256];
char&nbsp;line[512];
long&nbsp;base =&nbsp;0;

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

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

while&nbsp;(fgets(line,&nbsp;sizeof(line), fp)) {
// 查找行中的路径部分(空格后的部分)
char&nbsp;*path_start =&nbsp;strchr(line,&nbsp;'/');
if&nbsp;(path_start) {
// 检查是否以 module_name 结尾
size_t&nbsp;path_len =&nbsp;strlen(path_start);
size_t&nbsp;name_len =&nbsp;strlen(module_name);
// 移除换行符
if&nbsp;(path_start[path_len-1] ==&nbsp;'\n') {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; path_start[path_len-1] =&nbsp;'\0';
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; path_len--;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; }
// 精确匹配:路径以 module_name 结尾
if&nbsp;(path_len >= name_len &&&nbsp;strcmp(path_start + path_len - name_len, module_name) ==&nbsp;0) {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; base =&nbsp;strtol(line,&nbsp;NULL,&nbsp;16);
printf("[*] Found module %s at base 0x%lx (pid=%d)\n", module_name, base, pid);
break;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; }
fclose(fp);
return&nbsp;base;
}

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

unsignedlong&nbsp;target = (unsignedlong)addr;
char&nbsp;found_module[256] = {0};

while&nbsp;(fgets(line,&nbsp;sizeof(line), fp)) {
unsignedlong&nbsp;start, end;
if&nbsp;(sscanf(line,&nbsp;"%lx-%lx", &start, &end) !=&nbsp;2)&nbsp;continue;

if&nbsp;(target >= start && target < end) {
char&nbsp;*path =&nbsp;strrchr(line,&nbsp;'/');
if&nbsp;(path) {
char&nbsp;*nl =&nbsp;strchr(path,&nbsp;'\n');
if&nbsp;(nl) *nl =&nbsp;'\0';
strncpy(found_module, path,&nbsp;sizeof(found_module) -&nbsp;1);
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; }
break;
&nbsp; &nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; }
fclose(fp);

if&nbsp;(found_module[0] ==&nbsp;'\0')&nbsp;return&nbsp;-1;

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

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

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

long&nbsp;remote_base =&nbsp;get_module_base(pid, module_name);

if&nbsp;(!local_base || !remote_base) {
fprintf(stderr,&nbsp;"Failed to get module base for %s (local: 0x%lx, remote: 0x%lx)\n",
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; module_name, local_base, remote_base);
return&nbsp;0;
&nbsp; &nbsp; }

if&nbsp;(out_module) *out_module =&nbsp;strdup(module_name);

long&nbsp;offset = (long)local_func - local_base;
long&nbsp;remote_addr = remote_base + offset;

printf("[*] %s: local_func=%p, local_base=0x%lx, remote_base=0x%lx, offset=0x%lx\n",
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;module_name, local_func, local_base, remote_base, offset);
printf("[*] 计算出的远程地址: 0x%lx\n", remote_addr);

return&nbsp;remote_addr;
}

// 向目标进程写入数据(以 long 为单位,按需补齐)
staticintptrace_write_data(pid_t&nbsp;pid,&nbsp;unsignedlong&nbsp;addr,&nbsp;constvoid&nbsp;*data,&nbsp;size_t&nbsp;len)&nbsp;{
constunsignedchar&nbsp;*src = (constunsignedchar&nbsp;*)data;
size_t&nbsp;i =&nbsp;0;

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

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

return&nbsp;0;
}

// 在远程进程中调用一个函数(支持6个参数,用于mmap)
// MIPS64 N64 ABI: 前8个整数参数通过 $a0-$a7 (gpr[4]-gpr[11]) 传递
// 关键: MIPS PIC 代码需要设置 $t9 寄存器为目标地址 (gpr[25])
// 返回值在 $v0 (gpr[2])
staticlongcall_remote_func(pid_t&nbsp;pid,&nbsp;struct&nbsp;mips64_regs *orig_regs,
long&nbsp;func_addr,
long&nbsp;arg0,&nbsp;long&nbsp;arg1,&nbsp;long&nbsp;arg2,&nbsp;long&nbsp;arg3,
long&nbsp;arg4,&nbsp;long&nbsp;arg5)&nbsp;{
struct&nbsp;mips64_regs&nbsp;regs;
memcpy(®s, orig_regs,&nbsp;sizeof(regs));

// MIPS64 N64 ABI: 前8个参数都通过寄存器传递
&nbsp; &nbsp; regs.regs[REG_A0] = arg0; &nbsp;&nbsp;// $a0
&nbsp; &nbsp; regs.regs[REG_A1] = arg1; &nbsp;&nbsp;// $a1
&nbsp; &nbsp; regs.regs[REG_A2] = arg2; &nbsp;&nbsp;// $a2
&nbsp; &nbsp; regs.regs[REG_A3] = arg3; &nbsp;&nbsp;// $a3
&nbsp; &nbsp; regs.regs[REG_A4] = arg4; &nbsp;&nbsp;// $a4 (第5个参数)
&nbsp; &nbsp; regs.regs[REG_A5] = arg5; &nbsp;&nbsp;// $a5 (第6个参数)

// ★ 关键: 设置 $t9 为目标地址 (MIPS PIC 代码需要)
// MIPS 的位置无关代码(PIC)使用 $t9 来访问全局偏移表(GOT)
&nbsp; &nbsp; regs.regs[REG_T9] = func_addr;

// 设置 pc 为目标函数地址
&nbsp; &nbsp; regs.cp0_epc = func_addr;

&nbsp; &nbsp; regs.regs[REG_RA] = DUMMY_RETURN_ADDRESS;

// 关键:保存原始 ra 和 pc,用于后续判断
unsignedlong&nbsp;orig_ra = orig_regs->regs[31];
unsignedlong&nbsp;orig_pc = orig_regs->cp0_epc;

printf("[*] call6: pc=0x%lx, t9=0x%lx, sp=0x%lx, ra=0x%lx\n",
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;regs.cp0_epc, regs.regs[REG_T9], regs.regs[REG_SP], regs.regs[REG_RA]);
printf("[*] args: a0=0x%lx, a1=0x%lx, a2=0x%lx, a3=0x%lx, a4=0x%lx, a5=0x%lx\n",
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;arg0, arg1, arg2, arg3, arg4, arg5);
printf("[*] orig: pc=0x%lx, ra=0x%lx\n", orig_pc, orig_ra);
dump_regs("before-set", ®s);

if&nbsp;(set_regs(pid, ®s) <&nbsp;0) {
perror("set_regs");
return&nbsp;-1;
&nbsp; &nbsp; }

struct&nbsp;mips64_regs&nbsp;verify_regs;
if&nbsp;(get_regs(pid, &verify_regs) <&nbsp;0) {
perror("get_regs after set");
return&nbsp;-1;
&nbsp; &nbsp; }
dump_regs("after-set", &verify_regs);

printf("[*] 调用 PTRACE_CONT...\n");

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

// 读取当前寄存器
if&nbsp;(get_regs(pid, ®s) <&nbsp;0)&nbsp;return&nbsp;-1;
printf("[*] current: pc=0x%lx, ra=0x%lx, v0=0x%lx\n",
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;regs.cp0_epc, regs.regs[REG_RA], regs.regs[REG_V0]);

// 正常返回时 pc == DUMMY_RETURN_ADDRESS
if&nbsp;(regs.cp0_epc == DUMMY_RETURN_ADDRESS) {
printf("[+] 函数正常返回(pc=0x%lx)\n", regs.cp0_epc);
printf("[*] return value v0=0x%lx\n", regs.regs[REG_V0]);
return&nbsp;(long) regs.regs[REG_V0];
&nbsp; &nbsp; }

printf("[!] 远程调用未正常返回,按失败处理\n");
return&nbsp;-1;
}

// 在远程进程中直接执行 MIPS64 syscall (最多6个参数)
staticlongcall_remote_syscall(pid_t&nbsp;pid,&nbsp;struct&nbsp;mips64_regs *orig_regs,
long&nbsp;syscall_no,
long&nbsp;arg0,&nbsp;long&nbsp;arg1,&nbsp;long&nbsp;arg2,
long&nbsp;arg3,&nbsp;long&nbsp;arg4,&nbsp;long&nbsp;arg5)&nbsp;{
struct&nbsp;mips64_regs&nbsp;regs;
memcpy(®s, orig_regs,&nbsp;sizeof(regs));

&nbsp; &nbsp; regs.regs[REG_V0] = syscall_no;
&nbsp; &nbsp; regs.regs[REG_A0] = arg0;
&nbsp; &nbsp; regs.regs[REG_A1] = arg1;
&nbsp; &nbsp; regs.regs[REG_A2] = arg2;
&nbsp; &nbsp; regs.regs[REG_A3] = arg3;
&nbsp; &nbsp; regs.regs[REG_A4] = arg4;
&nbsp; &nbsp; regs.regs[REG_A5] = arg5;

unsignedlonglong&nbsp;saved_word =&nbsp;ptrace(PTRACE_PEEKDATA, pid, (void&nbsp;*)orig_regs->cp0_epc,&nbsp;NULL);
unsignedlonglong&nbsp;patched_word = (saved_word &&nbsp;0xffffffff00000000ULL) |&nbsp;0x0000000cULL;

if&nbsp;(ptrace(PTRACE_POKEDATA, pid, (void&nbsp;*)orig_regs->cp0_epc, (void&nbsp;*)patched_word) <&nbsp;0) {
perror("PTRACE_POKEDATA syscall");
return&nbsp;-1;
&nbsp; &nbsp; }

&nbsp; &nbsp; regs.cp0_epc = orig_regs->cp0_epc;
&nbsp; &nbsp; regs.regs[REG_RA] = DUMMY_RETURN_ADDRESS;
dump_regs("before-syscall", ®s);

if&nbsp;(set_regs(pid, ®s) <&nbsp;0) {
ptrace(PTRACE_POKEDATA, pid, (void&nbsp;*)orig_regs->cp0_epc, (void&nbsp;*)saved_word);
return&nbsp;-1;
&nbsp; &nbsp; }

if&nbsp;(ptrace(PTRACE_SYSCALL, pid,&nbsp;NULL,&nbsp;NULL) <&nbsp;0) {
perror("PTRACE_SYSCALL enter");
ptrace(PTRACE_POKEDATA, pid, (void&nbsp;*)orig_regs->cp0_epc, (void&nbsp;*)saved_word);
return&nbsp;-1;
&nbsp; &nbsp; }

int&nbsp;status;
if&nbsp;(waitpid(pid, &status, WUNTRACED) <&nbsp;0) {
perror("waitpid syscall enter");
ptrace(PTRACE_POKEDATA, pid, (void&nbsp;*)orig_regs->cp0_epc, (void&nbsp;*)saved_word);
return&nbsp;-1;
&nbsp; &nbsp; }

if&nbsp;(ptrace(PTRACE_SYSCALL, pid,&nbsp;NULL,&nbsp;NULL) <&nbsp;0) {
perror("PTRACE_SYSCALL exit");
ptrace(PTRACE_POKEDATA, pid, (void&nbsp;*)orig_regs->cp0_epc, (void&nbsp;*)saved_word);
return&nbsp;-1;
&nbsp; &nbsp; }

if&nbsp;(waitpid(pid, &status, WUNTRACED) <&nbsp;0) {
perror("waitpid syscall exit");
ptrace(PTRACE_POKEDATA, pid, (void&nbsp;*)orig_regs->cp0_epc, (void&nbsp;*)saved_word);
return&nbsp;-1;
&nbsp; &nbsp; }

struct&nbsp;mips64_regs&nbsp;result_regs;
if&nbsp;(get_regs(pid, &result_regs) <&nbsp;0) {
ptrace(PTRACE_POKEDATA, pid, (void&nbsp;*)orig_regs->cp0_epc, (void&nbsp;*)saved_word);
return&nbsp;-1;
&nbsp; &nbsp; }

if&nbsp;(ptrace(PTRACE_POKEDATA, pid, (void&nbsp;*)orig_regs->cp0_epc, (void&nbsp;*)saved_word) <&nbsp;0) {
perror("PTRACE_POKEDATA restore");
&nbsp; &nbsp; }

dump_regs("after-syscall", &result_regs);

if&nbsp;(result_regs.regs[REG_A3] !=&nbsp;0) {
printf("[!] 远程 syscall 失败: errno=%ld (%s)\n",
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;(long) result_regs.regs[REG_V0],
strerror((int) result_regs.regs[REG_V0]));
return&nbsp;-1;
&nbsp; &nbsp; }

return&nbsp;(long) result_regs.regs[REG_V0];
}

// MIPS64 Linux syscall number for mmap
#define&nbsp;__NR_MMAP_MIPS64 5009

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

pid_t&nbsp;pid =&nbsp;atoi(argv[1]);
constchar&nbsp;*so_path = argv[2];
int&nbsp;status;

printf("[*] 目标进程: %d\n", pid);
printf("[*] 注入 so: %s\n", so_path);

// 1. 附加目标进程
if&nbsp;(ptrace(PTRACE_ATTACH, pid,&nbsp;NULL,&nbsp;NULL) <&nbsp;0) {
perror("PTRACE_ATTACH");
return&nbsp;1;
&nbsp; &nbsp; }
waitpid(pid, &status, WUNTRACED);
printf("[+] 已附加到目标进程\n");

// 2. 保存原始寄存器
struct&nbsp;mips64_regs&nbsp;orig_regs;
if&nbsp;(get_regs(pid, &orig_regs) <&nbsp;0) {
ptrace(PTRACE_DETACH, pid,&nbsp;NULL,&nbsp;NULL);
return&nbsp;1;
&nbsp; &nbsp; }
printf("[+] 已保存寄存器现场\n");
printf("[*] 原始寄存器: pc=0x%lx, ra=0x%lx, sp=0x%lx\n",
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;orig_regs.cp0_epc, orig_regs.regs[REG_RA], orig_regs.regs[REG_SP]);

// 检查 PC 是否有效
if&nbsp;(orig_regs.cp0_epc ==&nbsp;0&nbsp;|| orig_regs.cp0_epc >&nbsp;0xfffffffffffff000) {
fprintf(stderr,&nbsp;"[-] 警告: 目标进程 PC 无效 (0x%lx),进程可能已崩溃\n", orig_regs.cp0_epc);
ptrace(PTRACE_DETACH, pid,&nbsp;NULL,&nbsp;NULL);
return&nbsp;1;
&nbsp; &nbsp; }

// 3. 获取远程 mmap 地址 (自动检测所在模块)
// 尝试使用 __mmap(内部实现)而不是 mmap(可能是包装函数)
long&nbsp;remote_mmap =&nbsp;get_remote_func_addr(pid, (void&nbsp;*)mmap,&nbsp;NULL);
if&nbsp;(!remote_mmap) {
fprintf(stderr,&nbsp;"[-] 获取远程 mmap 地址失败\n");
ptrace(PTRACE_DETACH, pid,&nbsp;NULL,&nbsp;NULL);
return&nbsp;1;
&nbsp; &nbsp; }
printf("[*] 远程 mmap 地址: 0x%lx\n", remote_mmap);

// 4. 优先直接使用 MIPS64 mmap syscall,绕过 libc wrapper
long&nbsp;mmap_result =&nbsp;call_remote_syscall(pid, &orig_regs,
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; __NR_MMAP_MIPS64,
0, &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;// addr = NULL
0x1000, &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;// length = 4096
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; PROT_READ | PROT_WRITE, &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;// prot
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; MAP_PRIVATE | MAP_ANONYMOUS, &nbsp; &nbsp;// flags
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; (unsignedlong)-1, &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;// fd = -1
0); &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;// offset
// mmap 失败时返回 0 / MAP_FAILED / 异常高地址
if&nbsp;(mmap_result ==&nbsp;0&nbsp;|| mmap_result ==&nbsp;-1&nbsp;|| (unsignedlong)mmap_result >&nbsp;0xfffffffffffff000) {
fprintf(stderr,&nbsp;"[-] 远程 mmap 失败, 返回: 0x%lx\n", mmap_result);
set_regs(pid, &orig_regs);
ptrace(PTRACE_DETACH, pid,&nbsp;NULL,&nbsp;NULL);
return&nbsp;1;
&nbsp; &nbsp; }
printf("[+] 远程 mmap 成功, 地址: 0x%lx\n", mmap_result);

// 5. 将 so 路径写入 mmap 分配的内存
size_t&nbsp;path_len =&nbsp;strlen(so_path) +&nbsp;1;
if&nbsp;(ptrace_write_data(pid, (unsignedlong)mmap_result, so_path, path_len) <&nbsp;0) {
fprintf(stderr,&nbsp;"[-] 写入 so 路径失败\n");
set_regs(pid, &orig_regs);
ptrace(PTRACE_DETACH, pid,&nbsp;NULL,&nbsp;NULL);
return&nbsp;1;
&nbsp; &nbsp; }
printf("[+] so 路径已写入远程内存 @ 0x%lx\n", mmap_result);

// 6. 获取远程 dlopen 地址 (自动检测所在模块)
void&nbsp;*local_dlopen =&nbsp;dlsym(RTLD_DEFAULT,&nbsp;"dlopen");
if&nbsp;(!local_dlopen) {
fprintf(stderr,&nbsp;"[-] 无法获取 dlopen 地址\n");
set_regs(pid, &orig_regs);
ptrace(PTRACE_DETACH, pid,&nbsp;NULL,&nbsp;NULL);
return&nbsp;1;
&nbsp; &nbsp; }
printf("[*] 本地 dlopen 地址: %p\n", local_dlopen);

constchar&nbsp;*dlopen_module =&nbsp;NULL;
long&nbsp;remote_dlopen =&nbsp;get_remote_func_addr(pid, local_dlopen, &dlopen_module);
if&nbsp;(!remote_dlopen) {
fprintf(stderr,&nbsp;"[-] 获取远程 dlopen 地址失败\n");
set_regs(pid, &orig_regs);
ptrace(PTRACE_DETACH, pid,&nbsp;NULL,&nbsp;NULL);
return&nbsp;1;
&nbsp; &nbsp; }
printf("[+] 远程 dlopen 地址: 0x%lx (模块: %s)\n", remote_dlopen,
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;dlopen_module ? dlopen_module :&nbsp;"unknown");

// 7. 在远程进程中调用 dlopen(so_path, RTLD_NOW)
long&nbsp;dlopen_ret =&nbsp;call_remote_func(pid, &orig_regs, remote_dlopen,
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;mmap_result, &nbsp; &nbsp;// $a0 = so 路径地址 (mmap 分配的)
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;RTLD_NOW, &nbsp; &nbsp; &nbsp;&nbsp;// $a1 = flags
0,&nbsp;0, &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;// $a2, $a3
0,&nbsp;0); &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;// padding

printf("[+] dlopen 返回值: 0x%lx\n", dlopen_ret);

if&nbsp;(dlopen_ret ==&nbsp;0) {
printf("[-] dlopen 失败!\n");
&nbsp; &nbsp; }&nbsp;else&nbsp;{
printf("[+] 注入成功!handle = 0x%lx\n", dlopen_ret);
&nbsp; &nbsp; }

// 8. 恢复寄存器现场并分离
if&nbsp;(set_regs(pid, &orig_regs) <&nbsp;0) {
perror("set_regs restore");
&nbsp; &nbsp; }
ptrace(PTRACE_DETACH, pid,&nbsp;NULL,&nbsp;NULL);
printf("[+] 已恢复现场并分离目标进程\n");

return&nbsp;0;
}

ARM64、x86-64、MIPS64 对比

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

核心区别主要体现在四点:

1. 返回地址机制不同

  • ARM64

    有独立的链接寄存器lr,直接设置lr = 0,函数返回时跳到 0 触发SIGSEGV,实现最直接。

  • x86-64

    没有链接寄存器,返回地址保存在栈上。手动伪造返回地址很容易破坏栈状态,因此更推荐写入 trampoline,让call指令自己完成压栈。

  • MIPS64

    类似 ARM64,有独立的$ra寄存器,可以直接把$ra = 0触发SIGSEGV来检测返回,不需要 trampoline。

2. 调用现场要求不同

  • ARM64

    的远程函数调用模型最直观:设置x0-x7pclr,通常就能工作。

  • x86-64

    对栈对齐和调用现场非常敏感,尤其是dlopen这种复杂函数,手动改rsp很容易出错。

  • MIPS64

    必须同时设置cp0_epc$t9——这是 MIPS PIC 代码的硬性要求,$t9参与函数序言的$gp计算,缺了它函数无法正常取到 GOT,几乎必然跑飞。

3. mmap 的调用方式差异最大

  • ARM64:远程 libcmmap()通常可直接调用。
  • x86-64:远程mmap()一般也能正常工作。
  • MIPS64:远程 libcmmap()wrapper 在这个环境下不可靠,会返回EBADF。最终只能绕过 libc,直接 patch 一条syscall指令,走__NR_mmap = 5009的系统调用路径才稳定。

4. 结果判定约定不同

  • ARM64 / x86-64:通过返回值本身的数值判断(比如MAP_FAILED == -1)。
  • MIPS64:syscall 用$a3作为成功/失败标志位,$a3 == 0表示成功、$v0是返回值;$a3 != 0表示失败、$v0是 errno。判 syscall 结果时不能光看$v0。

可以记成一句话

  • ARM64:直接改寄存器,最省心
  • x86-64:返回地址在栈上,最好上 trampoline
  • MIPS64:别忘了$t9,mmap 直接走 syscall

小结

ptrace 注入的核心流程在不同架构上是一致的,但真正决定是否稳定成功的,是各自 ABI、返回地址机制和运行时实现细节:

  • ARM64:可以直接设置寄存器调用远程函数,利用lr = 0触发 SIGSEGV 检测返回
  • x86-64:推荐使用 trampoline,在目标进程内执行call,用int3检测返回
  • MIPS64:除常规寄存器外必须设置$t9,mmap绕过 libc wrapper 直接走 syscall,dlopen仍可远程调用

#

看雪ID:plight

https://bbs.kanxue.com/user-home-986455.htm

*本文为看雪论坛优秀文章,由 plight 原创,转载请注明来自看雪社区

往期推荐

安卓逆向基础知识之frida Hook

2025 强网杯和强网拟态部分题解

在逆向分析方面-unidbg真的适合 MCP 吗?

AI静态分析,内核模块隐藏 Frida 特征,绕过linker私有结构遍历崩溃链

某安全so库深度解析

球分享

球点赞

球在看

点击阅读原文查看更多


免责声明:

本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。

任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。

本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我

本文转载自:看雪学苑 plight plight《Ptrace注入代码在不同平台的区别(ARM64、x86-64、MIPS64)》

评论:0   参与:  0