ptrace 注入
jos 好几把多,先上工其它的
ref: https://m0nkee.github.io/2015/08/20/play-ptrace/
https://www.52pojie.cn/thread-1355860-1-1.html
简介
ptrace 是一种系统调用,通过这个方法,可以实现父进程对子进程的观察与控制,并能检查与修改子进程的内存和寄存器。gdb starce 等都是基于 ptrace 的。
#include <sys/ptrace.h>
long ptrace(enum __ptrace_request request, pid_t pid, void *addr, void *data);
- request:要执行的操作类型
- pid:子进程的进程 ID
- addr:被监控的目标内存地址
- data:保存读取出或者要写入的数据,根据 request 变化
显然 request 的值决定了系统调用的功能。https://man7.org/linux/man-pages/man2/ptrace.2.html 这里面有介绍,也可以在 sys/user.h 里面去翻一下。要用到的时候现看即可。
ptrace 通过下面这个调用进入内核
// linux/kernel/ptrace.c
SYSCALL_DEFINE4(ptrace, long, request, long, pid, long, addr, long, data)
另外,ptrace 是以线程为单位,若子进程中存在多个线程,则父进程需要 ptrace 其所有线程。
最简单的用法
PTRACE_ME 实现
#include <sys/ptrace.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/user.h>
#include <sys/reg.h>
#include <unistd.h>
#include <stdio.h>
int main()
{
pid_t child;
long orig_rax;
child = fork();
if (child == 0)
{
ptrace(PTRACE_TRACEME, 0, NULL, NULL);
execl("/bin/ls", "ls", NULL);
}
else
{
wait(NULL);
orig_rax = ptrace(PTRACE_PEEKUSER, child, sizeof(long) * ORIG_RAX, NULL);
printf("Tracee made a system call %ld\n", orig_rax);
ptrace(PTRACE_CONT, child, NULL, NULL);
}
return 0;
}
运行效果如下
![[ptrace 注入] 1](https://cdn.jsdelivr.net/gh/iPlayForSG/photo_house/img/%5Bptrace%20%E6%B3%A8%E5%85%A5%5D%201.png)
简单解释一下
if (child == 0) 这块是对子进程做的。ptrace(PTRACE_TRACEME, 0, NULL, NULL); 向内核发送请求,告诉内核我希望作为子进程被父进程跟踪,然后执行程序 /bin/ls,这里的 execl 本身会触发一次 execve 系统调用,系统调用发生的时候,tracee 就停止,并向父进程发送 SIGTRAP 信号。
else 这块就是父进程了。首先需要等待子进程的状态发生变化。它会一直等到子进程因为执行 execl 系统调用而暂停。然后我们读取了子进程的寄存器值,获取存在 rax 内的系统调用号。PTRACE_PEEKUSER 就是从子进程的 USER area 读取数据,sizeof(long) * ORIG_RAX 是系统调用号在 USER area 中的偏移量,这里的 ORIG_RAX 是一个宏,代表原始 rax 寄存器的位置。
PTRACE_CONT 表示 "continue"。父进程把控制权还给子进程,此时子进程会从它暂停的地方继续执行,也就是完成 ls 命令。
可以看见,系统调用号是 59,这是 64 位 linux 中 execve 的系统调用号,然后执行了 ls。
进程中断
实现一个类似 gdb 断点的功能,通过 ptrace 运行时注入代码,劫持目标进程的执行流。其实这里就只注入了个 int 3
首先是 injector
#include <sys/ptrace.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <sys/user.h>
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
const int long_size = sizeof(long);
void getdata(pid_t child, unsigned long long addr, char *str, int len)
{
char *laddr = str;
int i = 0;
int j = len / long_size;
union u
{
long val;
char chars[long_size];
} data;
while (i < j)
{
data.val = ptrace(PTRACE_PEEKDATA, child, addr + i * long_size, NULL);
memcpy(laddr, data.chars, long_size);
i++;
laddr += long_size;
}
j = len % long_size;
if (j != 0)
{
data.val = ptrace(PTRACE_PEEKDATA, child, addr + i * long_size, NULL);
memcpy(laddr, data.chars, j);
}
str[len] = '\0';
}
void putdata(pid_t child, unsigned long long addr, char *str, int len)
{
char *laddr = str;
int i = 0;
int j = len / long_size;
union u
{
long val;
char chars[long_size];
} data;
while (i < j)
{
memcpy(data.chars, laddr, long_size);
ptrace(PTRACE_POKEDATA, child, addr + i * long_size, data.val);
i++;
laddr += long_size;
}
j = len % long_size;
if (j != 0)
{
data.val = ptrace(PTRACE_PEEKDATA, child, addr + i * long_size, NULL);
memcpy(data.chars, laddr, j);
ptrace(PTRACE_POKEDATA, child, addr + i * long_size, data.val);
}
}
int main(int argc, char *argv[])
{
pid_t target_process;
struct user_regs_struct regs;
char breakpoint_code[] = {'\xcc'};
char backup[2];
if (argc != 2)
{
printf("[x] Usage: %s <pid to be traced>\n", argv[0]);
exit(1);
}
target_process = atoi(argv[1]);
printf("[*] Attaching to process %d...\n", target_process);
if (ptrace(PTRACE_ATTACH, target_process, NULL, NULL) < 0)
{
printf("[x] PTRACE_ATTACH error\n");
perror("PTRACE_ATTACH");
exit(1);
}
wait(NULL);
printf("[*] Attached. Getting registers...\n");
ptrace(PTRACE_GETREGS, target_process, NULL, ®s);
printf("[*] Instruction pointer is at 0x%llx\n", regs.rip);
getdata(target_process, regs.rip, backup, 1);
printf("[*] Original instruction byte: 0x%02x\n", (unsigned char)backup[0]);
putdata(target_process, regs.rip, breakpoint_code, 1);
printf("[*] Injected breakpoint (0xcc).\n");
printf("[*] Continuing process...\n");
ptrace(PTRACE_CONT, target_process, NULL, NULL);
wait(NULL);
printf("\n[*] Breakpoint hit!\n[*] The process is stopped.\n");
printf("[*] Press enter to restore and detach.");
getchar();
printf("[*] Restoring original instruction...\n");
putdata(target_process, regs.rip, backup, 1);
printf("[*] Setting registers back...\n");
ptrace(PTRACE_SETREGS, target_process, NULL, ®s);
printf("[*] Detaching from process...\n");
ptrace(PTRACE_DETACH, target_process, NULL, NULL);
return 0;
}
然后是 target
#include <unistd.h>
#include <stdio.h>
int main()
{
printf("pid = %d\n", getpid());
for (int i = 0; i < 10; ++i)
{
printf("------------- Counter: %d -------------\n", i);
sleep(2);
}
return 0;
}
简单解释一下
整体思路非常简单,先 attach 上目标进程,目标进程立刻暂停了。此时我们 PTRACE_GETREGS 去获取目标进程的所有寄存器信息,特别是 rip 寄存器,它会指向下一条将要执行的汇编指令。
getdata 函数是读取目标位置的指令,这里是为了保存 rip 指向的下一条指令,putdata 函数是修改目标位置的指令,这里主要就是为了写入 0xcc,也就是 int 3。int 3 就是软件断点,CPU 执行到这个指令时会立即产生一个 Trap,生成 SIGTRAP 信号,这就相当于下断点了。然后我们 PTRACE_CONT,让它继续运行,这个时候目标进程就会执行 int 3,SIGTRAP 触发,内核再次暂停目标进程,wait(NULL) 收到这个信号,就知道目标进程再次被 Trap 暂停了。
此时想让目标进程再次恢复运行,首先就得把 int 3 给清理掉,恢复成原来的指令,并且,rip 现在已经指向恢复后的指令了,我们还得把 rip 重置为原来的位置,不然我们恢复的指令就跳过了。最后 PTRACE_DETACH,分离目标进程,结束。
展开说一下 getdata 和 putdata。这俩差不多,以 getdata 为例,核心是 PTRACE_PEEKDATA。这个 ptrace 会且仅会读取 1 个 word 的数据,也就是 8 字节。我们想多读几个字节,就得重复这个函数,所以我们封装了 getdata 函数,从目标内存地址 addr 处连续读取 len 个字节,并存入当前进程的缓冲区 str 中。我们循环读取,边读边存入 str,最后再处理一下不足 1 个 word 的数据的读取,最终完成整个读取任务。putdata 对应的就是写任务,原理是一模一样的。
最终效果就像这样
![[ptrace 注入] 2](https://cdn.jsdelivr.net/gh/iPlayForSG/photo_house/img/%5Bptrace%20%E6%B3%A8%E5%85%A5%5D%202.png)
可以看见,target 程序就卡在计数器 3 这里了。
按回车后
![[ptrace 注入] 3](https://cdn.jsdelivr.net/gh/iPlayForSG/photo_house/img/%5Bptrace%20%E6%B3%A8%E5%85%A5%5D%203.png)
injector 开始恢复 target,然后 target 继续照常执行了。
进程代码注入
现在注入多一点,注一段 shellcode 进去
#include <sys/ptrace.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <sys/user.h>
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
const int long_size = sizeof(long);
void getdata(pid_t child, unsigned long long addr, char *str, int len)
{
char *laddr = str;
int i = 0;
int j = len / long_size;
union u {
long val;
char chars[long_size];
} data;
while (i < j) {
data.val = ptrace(PTRACE_PEEKDATA, child, addr + i * long_size, NULL);
memcpy(laddr, data.chars, long_size);
i++;
laddr += long_size;
}
j = len % long_size;
if (j != 0) {
data.val = ptrace(PTRACE_PEEKDATA, child, addr + i * long_size, NULL);
memcpy(laddr, data.chars, j);
}
str[len] = '\0';
}
void putdata(pid_t child, unsigned long long addr, char *str, int len)
{
char *laddr = str;
int i = 0;
int j = len / long_size;
union u {
long val;
char chars[long_size];
} data;
while (i < j) {
memcpy(data.chars, laddr, long_size);
ptrace(PTRACE_POKEDATA, child, addr + i * long_size, data.val);
i++;
laddr += long_size;
}
j = len % long_size;
if (j != 0) {
data.val = ptrace(PTRACE_PEEKDATA, child, addr + i * long_size, NULL);
memcpy(data.chars, laddr, j);
ptrace(PTRACE_POKEDATA, child, addr + i * long_size, data.val);
}
}
int main(int argc, char *argv[])
{
pid_t target_process;
struct user_regs_struct regs;
// 打印 "Hello, world!\n" 然后执行 int3 返回控制权
//
// mov rax, 1 ; sys_write 系统调用号
// mov rdi, 1 ; 文件描述符 (1 = stdout)
// mov rsi, [string_address] ; 指向字符串的指针
// mov rdx, 14 ; 字符串长度
// syscall ; 执行系统调用
// int3 ; 触发断点,返回给注入器
// string_data:
// "Hello, world!\n"
unsigned char shellcode[] = {
0x48, 0xc7, 0xc0, 0x01, 0x00, 0x00, 0x00, // mov rax, 1
0x48, 0xc7, 0xc7, 0x01, 0x00, 0x00, 0x00, // mov rdi, 1
0x48, 0xbe, 0xEF, 0xBE, 0xAD, 0xDE, 0xBE, 0xBA, 0xFE, 0xCA, // mov rsi, 0xCAFEBABE_DEADBEEF (地址占位符)
0x48, 0xc7, 0xc2, 0x0e, 0x00, 0x00, 0x00, // mov rdx, 14
0x0f, 0x05, // syscall
0xcc, // int3
'H', 'e', 'l', 'l', 'o', ',', ' ', 'w', 'o', 'r', 'l', 'd', '!', '\n'};
const int code_part_len = 34;
const int shellcode_len = sizeof(shellcode);
unsigned char backup[shellcode_len + 1];
if (argc != 2)
{
printf("[x] Usage: %s <pid to be traced>\n", argv[0]);
exit(1);
}
target_process = atoi(argv[1]);
printf("[*] Attaching to process %d...\n", target_process);
if (ptrace(PTRACE_ATTACH, target_process, NULL, NULL) < 0)
{
printf("[x] PTRACE_ATTACH error\n");
perror("PTRACE_ATTACH");
exit(1);
}
wait(NULL);
printf("[*] Attached. Getting registers...\n");
ptrace(PTRACE_GETREGS, target_process, NULL, ®s);
printf("[*] Instruction pointer is at 0x%llx\n", regs.rip);
unsigned long long string_addr = regs.rip + code_part_len;
printf("[*] String will be at address: 0x%llx\n", string_addr);
memcpy(&shellcode[16], &string_addr, sizeof(unsigned long long));
printf("[*] Backing up %d bytes of original code...\n", shellcode_len);
getdata(target_process, regs.rip, (char *)backup, shellcode_len);
printf("[*] Injecting shellcode...\n");
putdata(target_process, regs.rip, (char *)shellcode, shellcode_len);
printf("[*] Continuing process to execute shellcode...\n");
ptrace(PTRACE_CONT, target_process, NULL, NULL);
wait(NULL);
printf("\n[*] Shellcode executed and hit the breakpoint!\n");
printf("[*] The process is stopped.\n");
printf("[*] Press enter to restore and detach.");
getchar();
printf("[*] Restoring original %d bytes...\n", shellcode_len);
putdata(target_process, regs.rip, (char *)backup, shellcode_len);
printf("[*] Setting registers back...\n");
ptrace(PTRACE_SETREGS, target_process, NULL, ®s);
printf("[*] Detaching from process.\n");
ptrace(PTRACE_DETACH, target_process, NULL, NULL);
return 0;
}
target 还是刚刚那个,injector 也没啥变化,就是把注入的 0xCC 改成一段 shellcode 了,这里我们就写了个打印 Hello world,最后用int 3暂停一下程序,移交控制权。
![[ptrace 注入] 4](https://cdn.jsdelivr.net/gh/iPlayForSG/photo_house/img/%5Bptrace%20%E6%B3%A8%E5%85%A5%5D%204.png)
效果如上
.so 注入
先关保护
# 保护 1: yama,这个是在当前终端临时关闭
cat /proc/sys/kernel/yama/ptrace_scope
echo 0 | sudo tee /proc/sys/kernel/yama/ptrace_scope
# 保护 2: AppArmor
sudo apt install apparmor-utils
sudo aa-complain /etc/apparmor.d/*
# 放在 tmp 里面去执行,这里安全权限比较高
cp ./libtest.so /tmp/
./sudo ./so_inj_test so_target /tmp/libtest.so testEntry
libtest.c
#include <stdio.h>
void testEntry()
{
FILE *f = fopen("/tmp/test.txt","w");
fputs("Ok to load",f);
fclose(f);
}
so_target.c
#include <stdio.h>
#include <dlfcn.h>
#include <sys/mman.h>
#include <unistd.h>
int main()
{
printf("Hello, World!\n");
// mmap(0, 0x1000, PROT_READ | PROT_WRITE | PROT_EXEC, MAP_ANONYMOUS | MAP_PRIVATE, 0, 0);
printf("pid: %d\n", getpid());
printf("mmap: %p\ndlopen: %p\ndlclose: %p\ndlsym: %p\ndlerror: %p\n", mmap, dlopen, dlclose, dlsym, dlerror);
int a;
while (1)
{
// scanf("%d", &a);
// printf("%d\n", a);
sleep(1);
}
return 0;
}
so_injector.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/ptrace.h>
#include <sys/wait.h>
#include <sys/user.h>
#include <sys/types.h>
#include <dirent.h>
#include <dlfcn.h>
#include <errno.h>
#include <sys/mman.h>
#define MAX_PATH 0x400
#define long_size sizeof(long)
const char *libc_path = "libc.so.6";
/**
* @brief 使用 ptrace 附加到目标进程。
* @param pid 目标进程的 PID。
*/
void ptrace_attach(pid_t pid)
{
if (ptrace(PTRACE_ATTACH, pid, NULL, NULL) == -1)
{
perror("[ERROR] ptrace_attach");
exit(1);
}
int status;
waitpid(pid, &status, WUNTRACED);
printf("[INFO] Attached to PID: %d\n", pid);
}
/**
* @brief 从目标进程分离,使其恢复正常执行。
* @param pid 目标进程的 PID。
*/
void ptrace_detach(pid_t pid)
{
ptrace(PTRACE_DETACH, pid, NULL, NULL);
printf("[INFO] Detached from PID: %d\n", pid);
}
/**
* @brief 获取目标进程的寄存器状态。
* @param pid 目标进程的 PID。
* @param regs 用于存储寄存器数据的结构体指针。
*/
void ptrace_getregs(pid_t pid, struct user_regs_struct *regs)
{
if (ptrace(PTRACE_GETREGS, pid, NULL, regs) == -1)
{
perror("[ERROR] ptrace_getregs");
exit(1);
}
}
/**
* @brief 设置目标进程的寄存器状态。
* @param pid 目标进程的 PID。
* @param regs 包含要设置的寄存器值的结构体指针。
*/
void ptrace_setregs(pid_t pid, struct user_regs_struct *regs)
{
if (ptrace(PTRACE_SETREGS, pid, NULL, regs) == -1)
{
perror("[ERROR] ptrace_setregs");
exit(1);
}
}
/**
* @brief 让被跟踪的进程继续执行。
* @param pid 目标进程的 PID。
*/
void ptrace_continue(pid_t pid)
{
if (ptrace(PTRACE_CONT, pid, NULL, NULL) == -1)
{
perror("[ERROR] ptrace_continue");
exit(1);
}
}
/**
* @brief 向目标进程的指定地址写入数据。
* @param pid 目标进程的 PID。
* @param addr 目标进程中的内存地址。
* @param data 要写入的数据。
* @param len 数据的长度。
*/
void ptrace_writedata(pid_t pid, unsigned long addr, const char *data, size_t len)
{
union
{
long val;
char chars[long_size];
} d;
size_t i;
for (i = 0; i < len / long_size; ++i)
{
memcpy(d.chars, data + i * long_size, long_size);
ptrace(PTRACE_POKEDATA, pid, addr + i * long_size, d.val);
}
if (len % long_size != 0)
{
// 为了避免破坏目标内存,先读出原来的数据
d.val = ptrace(PTRACE_PEEKDATA, pid, addr + i * long_size, NULL);
memcpy(d.chars, data + i * long_size, len % long_size);
ptrace(PTRACE_POKEDATA, pid, addr + i * long_size, d.val);
}
}
/**
* @brief 获取指定进程中某个模块(如 libc.so.6)的基地址。
* @param pid 目标进程的 PID。如果为 -1,则查找当前进程。
* @param module_name 模块的名称。
* @return 成功则返回模块的基地址,失败则返回 NULL。
*/
void *get_module_base_addr(pid_t pid, const char *module_name)
{
char map_path[MAX_PATH];
// 根据 pid 构建 /proc/[pid]/maps 文件的路径。该文件记录了进程的内存映射。
if (pid < 0) snprintf(map_path, sizeof(map_path), "/proc/self/maps"); // -1 代表查找自身进程
else snprintf(map_path, sizeof(map_path), "/proc/%d/maps", pid);
FILE *f = fopen(map_path, "r");
if (!f)
{
printf("[ERROR] Failed to open maps file: %s\n", map_path);
return NULL;
}
char line[1024];
void *base_addr = NULL;
// 逐行读取 maps 文件。
while (fgets(line, sizeof(line), f))
{
// 我们要找的是模块的第一个可执行段,应该是 "r-xp" (可读可执行私有) 权限。
// 同时必须包含我们想要的模块名。
if (strstr(line, module_name) && strstr(line, "r-xp"))
{
// 行的开头就是内存区域的基地址,以十六进制表示。
base_addr = (void *)strtoul(line, NULL, 16);
break;
}
}
fclose(f);
return base_addr;
}
/**
* @brief 计算一个函数在远程进程中的地址。
* @param remote_pid 远程进程的 PID。
* @param module_name 函数所在的模块名。
* @param local_func_addr 函数在当前(注入器)进程中的地址。
* @return 成功则返回函数在远程进程中的地址,失败则返回 NULL。
*/
// 主要是为了绕过 ASLR。同一个库文件中的函数,其相对于库基地址的偏移是固定的。
void *get_remote_func_addr(pid_t remote_pid, const char *module_name, void *local_func_addr)
{
// 获取 injector 进程中模块的基地址。
void *local_module_base = get_module_base_addr(-1, module_name);
if (!local_module_base)
{
printf("[ERROR] Failed to get local module base for %s\n", module_name);
return NULL;
}
printf("[DEBUG] Local module base for %s: %p\n", module_name, local_module_base);
printf("[DEBUG] Local function address: %p\n", local_func_addr);
// 计算函数在本地进程中相对于模块基地址的偏移量。
// 函数偏移 = 函数绝对地址 - 模块基地址
unsigned long func_offset = (unsigned long)local_func_addr - (unsigned long)local_module_base;
printf("[DEBUG] Function offset: 0x%lx\n", func_offset);
// 获取远程目标进程中同一个模块的基地址。
void *remote_module_base = get_module_base_addr(remote_pid, module_name);
if (!remote_module_base)
{
printf("[ERROR] Failed to get remote module base for %s in PID %d\n", module_name, remote_pid);
return NULL;
}
printf("[DEBUG] Remote module base for %s: %p\n", module_name, remote_module_base);
// 计算函数在远程进程中的绝对地址。
// 远程函数地址 = 远程模块基地址 + 函数偏移
return (void *)((unsigned long)remote_module_base + func_offset);
}
/**
* @brief 在远程进程中执行一个函数调用。
* @param pid 目标进程的 PID。
* @param func_addr 要调用的函数在远程进程中的地址。
* @param params 传递给函数的参数数组。
* @param num_params 参数数量。
* @param regs 寄存器结构体指针。
* @return 函数的返回值 (存储在 RAX 寄存器中)。
*/
long ptrace_call(pid_t pid, unsigned long func_addr, long *params, int num_params, struct user_regs_struct *regs)
{
// 触发 SIGSEGV 来捕获函数的返回
// x86-64 调用约定,前 6 个参数通过寄存器传递。
// RDI, RSI, RDX, RCX, R8, R9
regs->rdi = (num_params > 0) ? params[0] : 0;
regs->rsi = (num_params > 1) ? params[1] : 0;
regs->rdx = (num_params > 2) ? params[2] : 0;
regs->rcx = (num_params > 3) ? params[3] : 0;
regs->r8 = (num_params > 4) ? params[4] : 0;
regs->r9 = (num_params > 5) ? params[5] : 0;
// 设置指令指针(RIP)为要调用的函数地址
regs->rip = func_addr;
// 为了在函数返回后能捕获到控制权,这里伪造了一个返回地址。
// 将 RSP 向下移动 8 字节,为返回地址腾出空间。这样函数执行ret时会触发SIGSEGV
regs->rsp -= long_size;
// 在这个新的栈顶写入一个 0。这是一个无效的地址。
// 当远程函数执行 ret 指令时,CPU会尝试跳转到地址 0,这将立即引发一个段错误。
ptrace_writedata(pid, regs->rsp, "\x00\x00\x00\x00\x00\x00\x00\x00", long_size);
// 设置寄存器并继续执行
ptrace_setregs(pid, regs);
ptrace_continue(pid);
// 等待SIGSEGV信号
int status;
waitpid(pid, &status, WUNTRACED);
// 检查进程是否确实因为 SIGSEGV 停止。
if (WIFSTOPPED(status) && WSTOPSIG(status) == SIGSEGV)
{
// 如果是,说明函数调用已完成。获取当前寄存器状态以读取返回值。
ptrace_getregs(pid, regs);
// 返回值存储在 RAX 寄存器中。
long ret = regs->rax;
printf("[DEBUG] Remote call returned: 0x%lx\n", ret);
return ret;
}
else
{
// 如果进程因其他原因停止或终止,说明调用失败。
printf("[ERROR] Process did not stop with SIGSEGV. Status: 0x%x\n", status);
if (WIFSTOPPED(status))
{
printf("[ERROR] Stopped by signal: %s\n", strsignal(WSTOPSIG(status)));
}
return -1;
}
}
/**
* @brief SO 注入流程。
* @param pid 目标进程 PID。
* @param lib_path 要注入的 .so 的路径。
* @param func_name 要在注入后调用的函数名。
*/
void inject_so(pid_t pid, const char *lib_path, const char *func_name)
{
struct user_regs_struct original_regs, current_regs;
// 获取 .so 的绝对路径
char full_lib_path[MAX_PATH] = {0};
if (realpath(lib_path, full_lib_path) == NULL)
{
perror("[ERROR] realpath");
return;
}
printf("[INFO] Full library path: %s\n", full_lib_path);
// 附加到进程并备份原始寄存器状态,以便最后恢复。
ptrace_attach(pid);
ptrace_getregs(pid, &original_regs);
memcpy(¤t_regs, &original_regs, sizeof(struct user_regs_struct));
// 步骤 1: 在远程进程中调用 mmap 分配内存
printf("\n[STEP 1] Calling mmap in remote process...\n");
// 计算 mmap 函数在远程进程中的地址。
void *remote_mmap_addr = get_remote_func_addr(pid, libc_path, (void *)mmap);
if (!remote_mmap_addr)
{
ptrace_detach(pid);
return;
}
printf("[INFO] Remote mmap address: %p\n", remote_mmap_addr);
long mmap_params[] =
{
0, // addr
0x1000, // length
PROT_READ | PROT_WRITE | PROT_EXEC, // prot
MAP_PRIVATE | MAP_ANONYMOUS, // flags
-1, // fd
0 // offset
};
// 准备 mmap 的参数:
// addr=0 (内核选择地址), length=0x1000, prot=RWX, flags=PRIVATE|ANONYMOUS, fd=-1, offset=0
long remote_mem_addr = ptrace_call(pid, (unsigned long)remote_mmap_addr, mmap_params, 6, ¤t_regs);
// 调用 ptrace_call 执行 mmap
if (remote_mem_addr == -1 || remote_mem_addr == 0) // mmap 失败返回 MAP_FAILED (-1)
{
printf("[ERROR] mmap call failed in remote process.\n");
ptrace_setregs(pid, &original_regs); ptrace_detach(pid); return;
}
printf("[SUCCESS] Allocated remote memory at: 0x%lx\n", remote_mem_addr);
// 将 so 库的完整路径写入刚刚分配的内存中
ptrace_writedata(pid, remote_mem_addr, full_lib_path, strlen(full_lib_path) + 1);
// 步骤 2: 在远程进程中调用 dlopen 加载 SO
printf("\n[STEP 2] Calling dlopen in remote process...\n");
// 计算 dlopen 在远程进程中的地址
void *remote_dlopen_addr = get_remote_func_addr(pid, libc_path, (void *)dlopen);
if (!remote_dlopen_addr)
{
ptrace_detach(pid);
return;
}
printf("[INFO] Remote dlopen address: %p\n", remote_dlopen_addr);
// 准备 dlopen 参数: (char *filename, int flags)
// filename 是刚才写入的库路径地址,flags 是 RTLD_LAZY
long dlopen_params[] = { remote_mem_addr, RTLD_LAZY };
long lib_handle = ptrace_call(pid, (unsigned long)remote_dlopen_addr, dlopen_params, 2, ¤t_regs);
if (lib_handle == 0)
{
printf("[ERROR] dlopen call failed in remote process.\n");
ptrace_setregs(pid, &original_regs);
ptrace_detach(pid);
return;
}
printf("[SUCCESS] SO loaded, remote handle: 0x%lx\n", lib_handle);
// 复用之前 mmap 的内存,现在写入要调用的函数名
ptrace_writedata(pid, remote_mem_addr, func_name, strlen(func_name) + 1);
// 步骤 3: 在远程进程中调用 dlsym 获取函数地址
printf("\n[STEP 3] Calling dlsym in remote process...\n");
void *remote_dlsym_addr = get_remote_func_addr(pid, libc_path, (void *)dlsym);
if (!remote_dlsym_addr)
{
ptrace_detach(pid);
return;
}
printf("[INFO] Remote dlsym address: %p\n", remote_dlsym_addr);
// 准备 dlsym 参数: (void *handle, const char *symbol)
// handle 是 dlopen 返回的句柄,symbol 是函数名的地址
long dlsym_params[] = {lib_handle, remote_mem_addr};
// 调用 dlsym,返回值是目标函数在远程进程中的地址
long target_func_addr = ptrace_call(pid, (unsigned long)remote_dlsym_addr, dlsym_params, 2, ¤t_regs);
if (target_func_addr == 0) // dlsym 失败返回 0
{
printf("[ERROR] dlsym call failed for function '%s'.\n", func_name);
ptrace_setregs(pid, &original_regs); ptrace_detach(pid); return;
}
printf("[SUCCESS] Got remote function '%s' address: 0x%lx\n", func_name, target_func_addr);
// 步骤 4: 调用目标函数
printf("\n[STEP 4] Calling target function '%s'...\n", func_name);
// 直接调用目标函数,这里没有传递参数 (NULL, 0)
ptrace_call(pid, target_func_addr, NULL, 0, ¤t_regs);
printf("[SUCCESS] Target function called.\n");
// 最终恢复现场并分离
printf("\n[FINAL] Restoring original state and detaching...\n");
// 将目标进程的寄存器恢复到附加之前的状态
ptrace_setregs(pid, &original_regs);
// 从目标进程分离,让它继续正常执行
ptrace_detach(pid);
printf("[COMPLETE] Injection finished.\n");
}
/**
* @brief 根据进程名查找其 PID。
* @param proc_name 目标进程的名称。
* @return 成功则返回 PID,失败则返回 -1。
*/
pid_t find_pid_by_name(const char *proc_name)
{
DIR *dir = opendir("/proc"); // /proc 目录包含了系统所有进程的信息
if (!dir)
{
perror("opendir /proc");
return -1;
}
struct dirent *entry;
// 遍历 /proc 目录下的所有条目
while ((entry = readdir(dir)) != NULL)
{
// 目录名是数字的就是进程的 PID
pid_t pid = atoi(entry->d_name);
if (pid > 0)
{
// 构建 /proc/[pid]/cmdline 文件的路径
char cmdline_path[MAX_PATH];
snprintf(cmdline_path, sizeof(cmdline_path), "/proc/%d/cmdline", pid);
FILE *f = fopen(cmdline_path, "r");
if (f)
{
// 读取 cmdline 文件,其中包含了启动进程的完整命令
char cmdline[MAX_PATH] = {0};
fgets(cmdline, sizeof(cmdline), f);
fclose(f);
// 我们只关心可执行文件名,所以取最后一部分
char *basename = strrchr(cmdline, '/');
if (basename)
basename++; // 指针移动到'/'之后
else basename = cmdline;
// 比较进程名是否与目标匹配
if (strcmp(basename, proc_name) == 0)
{
closedir(dir);
return pid;
}
}
}
}
closedir(dir);
return -1;
}
int main(int argc, char *argv[])
{
if (argc != 4)
{
fprintf(stderr, "Usage: %s <process_name> <library_path> <function_name>\n", argv[0]);
return 1;
}
// 加载 libc.so.6,确保能在本进程中正确地找到 mmap, dlopen 等函数的地址。
dlopen("libc.so.6", RTLD_LAZY | RTLD_GLOBAL);
const char *target_proc_name = argv[1];
const char *lib_path = argv[2];
const char *func_name = argv[3];
pid_t target_pid = find_pid_by_name(target_proc_name);
if (target_pid == -1)
{
printf("[ERROR] Could not find process: %s\n", target_proc_name);
return 1;
}
printf("[INFO] Found target process '%s' with PID: %d\n", target_proc_name, target_pid);
inject_so(target_pid, lib_path, func_name);
return 0;
}
编译
gcc -o libtest.so libtest.c -fPIC -shared
gcc -o so_inj_test so_inj_test.c -ldl
gcc -o so_target so_target.c
运行
# Terminal 1
./so_target
# Terminal 2
cp ./libtest.so /tmp/
./sudo ./so_inj_test so_target /tmp/libtest.so testEntry
![[ptrace 注入] 5](https://cdn.jsdelivr.net/gh/iPlayForSG/photo_house/img/%5Bptrace%20%E6%B3%A8%E5%85%A5%5D%205.png)
/tmp/test.txt 出现就是成功了

浙公网安备 33010602011771号