Please enable Javascript to view the contents

Linux ptrace机制初探

 ·  ☕  6 分钟

补充下基础知识

动态加载

动态加载Dynamic Loading:是一种程序运行机制,能让计算机程序在运行时(而不是编译时)装载库(或者其他二进制对象)到内存中,然后检索库中函数和变量的地址,并运行这些函数或访问这些变量,且能在不需要时将库从内存中卸载。

linux下的/proc虚拟文件系统

Linux 内核提供了一种通过 /proc 文件系统,在运行时访问内核内部数据结构、改变内核设置的机制。proc文件系统是一个伪文件系统,它只存在内存当中,而不占用外存空间。它以文件系统的方式为访问系统内核数据的操作提供接口。

  • /proc/<PID>/status / /proc/<PID>/stat: 查看进程信息
  • /proc/<PID>/maps:进程关联到的每个可执行文件和库文件在内存中的映射区域及其访问权限所组成的列表
  • /proc/self/maps: 当前进程的内存映射关系,通过读该文件的内容可以得到内存代码段基址。

什么是 Ptrace?

通过使用 ptrace(名称是 “process trace” 的缩写),一个进程可以控制另一个进程,从而使控制器能够检查和操纵目标的内部状态。

linux ptrace 函数提供了许多有用的调试操作:

  • PTRACE_ATTACH 允许一个进程将自身附加到另一个进程进行调试,从而暂停远程进程。
  • PTRACE_PEEKTEXT 允许从另一个进程地址空间读取内存。
  • PTRACE_POKETEXT 允许将内存写入另一个进程地址空间。
  • PTRACE_GETREGS 从进程中读取当前的处理器寄存器集。
  • PTRACE_SETREGS 写入进程的当前处理器寄存器集。
  • PTRACE_CONT 恢复附加进程的执行。
  • PTRACE_DETACH 是重新启动操作;它要求目标进程处于 ptrace-stop 中。

函数签名:

1
long ptrace(enum __ptrace_request request, pid_t pid, void *addr, void *data);

🌰 (Linux ubuntu 4.15.0-112-generic x86_64):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#include <stdio.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/ptrace.h>
#include <unistd.h>
#include <sys/user.h>

int main(int argc, char **argv)
{
    int child_pid;
    child_pid = fork();
    if (child_pid == 0)
    {
        // `PTRACE_TRACEME` 本进程将被其父进程跟踪(此时剩下的`pid、addr、data`参数都没有实际意义可以全部为 0)。
        ptrace(PTRACE_TRACEME, 0, 0, 0);
        execl("/bin/ls","ls",NULL); //执行系统调用
    }
    else
    {
        struct user_regs_struct regs;
        wait(NULL);
        printf("child process was stopped ...");
        // 读取子进程寄存器集
        ptrace(PTRACE_GETREGS, child_pid, NULL, &regs);
        printf("%llu", regs.orig_rax);
        // 恢复子进程
        ptrace(PTRACE_CONT, child_pid, NULL, NULL);
        sleep(1);
    }
    return 0;
}

踩坑 SSHD 注入

https://github.com/xpn/ssh-inject

分析下代码其运行流程图整理如下:

inject.cinjectme.c 中一些注入部分代码细节和 hook 代码细节已经看不懂了,先挖个坑吧…

Linux ptrace introduction AKA injecting into sshd for fun 原博客中有关于 ptarce 步骤更详细说明。

笔记📒

进程相关:

  • pid_t fork(void) 在当前进程下分叉出子进程,其子进程的 pid 为 0
  • pid_t wait (int * status) 等待子进程中断或结束
  • pid_t waitpid(pid_t pid, int *status, int options);(等待子进程中断或结束)
pid: 
    - pid<-1 等待进程组识别码为pid绝对值的任何子进程。
    - pid=-1 等待任何子进程,相当于wait()。
    - pid=0 等待进程组识别码与目前进程相同的任何子进程。
    - pid>0 等待任何子进程识别码为pid的子进程。

status(子进程的结束状态返回后存于status,底下有几个宏可判别结束情况):
    - WIFEXITED(status) 如果子进程正常结束则为非0值。
    - WEXITSTATUS(status) 取得子进程exit()返回的结束代码,一般会先用WIFEXITED 来判断是否正常结束才能使用此宏。
    - WIFSIGNALED(status) 如果子进程是因为信号而结束则此宏值为真
    - WTERMSIG(status) 取得子进程因信号而中止的信号代码,一般会先用WIFSIGNALED 来判断后才使用此宏。
    - WIFSTOPPED(status) 如果子进程处于暂停执行情况则此宏值为真。一般只有使用 WUNTRACED 时才会有此情况。
    - WSTOPSIG(status) 取得引发子进程暂停的信号代码,一般会先用WIFSTOPPED 来判断后才使用此宏。

options: 可以为 0 或下面的 OR 组合
    - WNOHANG 如果没有任何已经结束的子进程则马上返回,不予以等待。
    - WUNTRACED 如果子进程进入暂停执行情况则马上返回,但结束状态不予以理会。
  • exec 系列函数用来执行文件或者命令
1
2
3
4
5
6
7
8
#include <unistd.h>
...
int execl(const char *path, const char *arg, ...);
int execlp(const char *file, const char *arg, ...);
int execle(const char *path, const char *arg, ..., char *const envp[]);
int execv(const char *path, char *const argv[]);
int execvp(const char *file, char *const argv[]);
int execve(const char *path, char *const argv[], char *const envp[]);

注意: 系统调用 exec 是以新的进程去代替原来的进程,但进程的 PID 保持不变,只是替换了原来进程上下文的内容,原进程的代码段,数据段,堆栈段被新的进程所代替。

动态加载

dlopen() 函数以指定模式打开指定的动态链接库文件,并返回一个句柄给 dlsym() 的调用进程。使用 dlclose() 来卸载打开的库。

  • void * dlopen( const char *pathname, int mode ); :打开一个动态链接库。

mode 是打开方式,其值有多个,不同操作系统上实现的功能有所不同,在 linux 下,按功能可分为三类:

  1. 解析方式
    • RTLD_LAZY:在 dlopen() 返回前,对于动态库中的未定义的符号不执行解析(只对函数引用有效,对于变量引用总是立即解析)。
    • RTLD_NOW: 需要在 dlopen() 返回前,解析出所有未定义符号,如果解析不出来,在 dlopen() 会返回 NULL,错误为:: undefined symbol: xxxx…….
  2. 作用范围,可与解析方式通过 | 组合使用。
    • RTLD_GLOBAL:动态库中定义的符号可被其后打开的其它库解析。
    • RTLD_LOCAL: 与 RTLD_GLOBAL 作用相反,动态库中定义的符号不能被其后打开的其它库重定位。如果没有指明是 RTLD_GLOBAL还是 RTLD_LOCAL ,则缺省为 RTLD_LOCAL
  3. 作用方式
    • RTLD_NODELETE: 在 dlclose() 期间不卸载库,并且在以后使用 dlopen() 重新加载库时不初始化库中的静态变量。这个flag不是POSIX-2001标准。
    • RTLD_NOLOAD: 不加载库。可用于测试库是否已加载( dlopen() 返回NULL说明未加载,否则说明已加载),也可用于改变已加载库的flag,如:先前加载库的flag为 RTLD_LOCAL,用 dlopen(RTLD_NOLOAD|RTLD_GLOBAL) 后flag将变成 RTLD_GLOBAL 。这个flag不是POSIX-2001标准。
    • RTLD_DEEPBIND:在搜索全局符号前先搜索库内的符号,避免同名符号的冲突。这个flag不是POSIX-2001标准。
  • void * dlsym(void *handle, const char *symbol)dlsym() 根据动态链接库操作句柄 handle 与符号 symbol ,返回符号对应的地址。使用这个函数不但可以获取函数地址,也可以获取变量地址。

handle 是由 dlopen() 打开动态链接库后返回的指针,symbol 就是要求获取的函数或全局变量的名称。
返回值: void* 指向函数的地址,供调用使用。

  • dlclose() 用于关闭指定句柄的动态链接库,只有当此动态链接库的使用计数为 0 时,才会真正被系统卸载。

  • dlerror() 当动态链接库操作函数执行失败时,dlerror 可以返回出错信息,返回值为 NULL 时表示操作函数执行成功。

内存相关

  • void * malloc(size_t size):分配所需的内存空间,并返回一个指向它的指针;记得需要运行 free(addr` )
  • void * memcpy(void *dest, const void *src, size_t num);: 会复制 src 所指的内存内容的前 num 个字节到 dest 所指的内存地址上。

字符处理

  • int snprintf(char *str, size_t size, const char *format, ...);

设将可变参数 ... 按照 format 格式化成字符串,并将字符串复制到 str 中,size 为要写入的字符的最大数目,超过 size 会被截断。

  • char *strstr(const char *haystack, const char *needle);

在字符串 haystack 中查找第一次出现字符串 needle 的位置,不包含终止符 ‘\0’。该函数返回在 haystack 中第一次出现 needle 字符串的位置,如果未找到则返回 null。

  • unsigned long int strtoul(const char *str, char **endptr, int base);

把参数 str 所指向的字符串根据给定的 base 转换为一个无符号长整数(类型为 unsigned long int 型),base 必须介于 2 和 36(包含)之间,或者是特殊值 0。

  • int sscanf(const char *str, const char *format, ...); 从字符串读取格式化输入。

其他

  • struct 运算符 . 和箭头运算符 -> 异同点:
    • 相同点:两者都是二元操作符,而且右边的操作数都是成员的名称。
    • 不同点:点运算符 . 的左边操作数是一个结果为结构的表达式
      箭头运算符 -> 的左边的操作数是一个指向结构体的指针

总结

浅显的认知 Linux 下进程运行的机制,ptrace 的如何动态注入基本过程,C语言的基本用法等。。。

参考文章🙏

目录