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

简单解释一下

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, &regs);
    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, &regs);

    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 3int 3 就是软件断点,CPU 执行到这个指令时会立即产生一个 Trap,生成 SIGTRAP 信号,这就相当于下断点了。然后我们 PTRACE_CONT,让它继续运行,这个时候目标进程就会执行 int 3,SIGTRAP 触发,内核再次暂停目标进程,wait(NULL) 收到这个信号,就知道目标进程再次被 Trap 暂停了。

此时想让目标进程再次恢复运行,首先就得把 int 3 给清理掉,恢复成原来的指令,并且,rip 现在已经指向恢复后的指令了,我们还得把 rip 重置为原来的位置,不然我们恢复的指令就跳过了。最后 PTRACE_DETACH,分离目标进程,结束。

展开说一下 getdataputdata。这俩差不多,以 getdata 为例,核心是 PTRACE_PEEKDATA。这个 ptrace 会且仅会读取 1 个 word 的数据,也就是 8 字节。我们想多读几个字节,就得重复这个函数,所以我们封装了 getdata 函数,从目标内存地址 addr 处连续读取 len 个字节,并存入当前进程的缓冲区 str 中。我们循环读取,边读边存入 str,最后再处理一下不足 1 个 word 的数据的读取,最终完成整个读取任务。putdata 对应的就是写任务,原理是一模一样的。

最终效果就像这样

[ptrace 注入] 2

可以看见,target 程序就卡在计数器 3 这里了。

按回车后

[ptrace 注入] 3

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, &regs);
    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, &regs);

    printf("[*] Detaching from process.\n");
    ptrace(PTRACE_DETACH, target_process, NULL, NULL);

    return 0;
}

target 还是刚刚那个,injector 也没啥变化,就是把注入的 0xCC 改成一段 shellcode 了,这里我们就写了个打印 Hello world,最后用int 3暂停一下程序,移交控制权。

[ptrace 注入] 4

效果如上

.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(&current_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, &current_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, &current_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, &current_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, &current_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

/tmp/test.txt 出现就是成功了

posted @ 2025-07-14 15:56  iPlayForSG  阅读(80)  评论(0)    收藏  举报