由一道题看 Linux 的ptrace调试机制——cyber-apocalypse-2022\indefinite
由一道题看 Linux 的ptrace
调试机制——cyber-apocalypse-2022\indefinite
前置知识
ptrace 函数
ptrace 函数是 Linux 下用来控制进程的函数,可以检查和改变子进程的功能,如获取和写入寄存器的值,修改内存(Peek and poke)等。典型的使用 ptrace 的应用:gdb,strace(用来查看某进程运行过程中的系统调用)
ptrace函数的定义
1 |
|
request
: 表示要执行的操作类型。//反调试会用到PT_DENY_ATTACH
,调试会用到PTRACE_ATTACH
pid
: 要操作的目标进程IDaddr
: 要监控的目标内存地址data
: 保存读取出或者要写入的数据
request 的类型
- PTRACE_TRACEME:tracee表明自己想要被追踪,这会自动与父进程建立追踪关系,这也是唯一能被tracee使用的request,其他的request都由tracer指定。
- PTRACE_ATTACH:tracer用来附着一个进程tracee,以建立追踪关系,并向其发送SIGSTOP信号使其暂停。
- PTRACE_SEIZE:像PTRACE_ATTACH附着进程,但它不会让tracee暂停,addr参数须为0,data参数指定一位ptrace选项。
- PTRACE_DETACH:解除追踪关系,tracee将继续运行。
- PTRACE_POKETEXT, PTRACE_POKEDATA:往内存地址中写入一个字(32位为4字节,64位为8字节),内存地址由addr给出。
- PTRACE_PEEKTEXT, PTRACE_PEEKDATA:从内存地址中读取一个字,内存地址由addr给出
- PTRACE_ATTACH:跟踪指定pid 进程
- PTRACE_GETREGS:读取所有寄存器的值
- PTRACE_CONT:继续执行示被跟踪的子进程,signal为0则忽略引起调试进程中止的信号,若不为0则继续处理信号signal。
- PTRACE_SETREGS:设置寄存器
- PTRACE_DETACH:结束跟踪
ptrace 函数使用示例
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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
int main() {
pid_t child;/*定义子进程变量*/
long orig_rax;//定义rax寄存器的值的变量
int status;/*定义进程状态变量*/
int iscalling = 0;/*判断是否正在被调用*/
struct user_regs_struct regs;/*定义寄存器结构体数据类型*/
child = fork();/*利用fork函数创建子进程*/
if(child == 0)
{
ptrace(PTRACE_TRACEME, 0, 0);//发送信号给父进程表示已做好准备被跟踪(调试)
execl("/bin/ls", "ls", "-l", "-h", NULL);/*执行命令ls -l -h,注意,这里函数参数必须要要以NULL结尾来终止参数列表*/
}
else
{
while(1)
{
wait(&status);//等待子进程发来信号或者子进程退出
if(WIFEXITED(status))//WIFEXITED函数(宏)用来检查子进程是被ptrace暂停的还是准备退出
{
break;
}
orig_rax = ptrace(PTRACE_PEEKUSER, child, 8 * ORIG_RAX, 0);//获取rax值从而判断将要执行的系统调用号
if(orig_rax == SYS_write)//如果系统调用是write
{
ptrace(PTRACE_GETREGS, child, 0, ®s);
if(!iscalling)
{
iscalling = 1;
printf("SYS_write call with %lld, %lld, %lld\n",regs.rdi, regs.rsi, regs.rdx);//打印出系统调用write的各个参数内容
}
else
{
printf("SYS_write call return %lld\n", regs.rax);//打印出系统调用write函数结果的返回值
iscalling = 0;
}
}
ptrace(PTRACE_SYSCALL, child, 0, 0);//PTRACE_SYSCALL,其作用是使内核在子进程进入和退出系统调用时都将其暂停
//得到处于本次调用之后下次调用之前的状态
}
}
return 0;
}struct user_regs_struct 结构体
在使用 ptrace 获取寄存器值时,ptrace 函数的输出结果为该结构体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
30struct user_regs_struct
{
unsigned long r15;
unsigned long r14;
unsigned long r13;
unsigned long r12;
unsigned long rbp;
unsigned long rbx;
unsigned long r11;
unsigned long r10;
unsigned long r9;
unsigned long r8;
unsigned long rax;
unsigned long rcx;
unsigned long rdx;
unsigned long rsi;
unsigned long rdi;
unsigned long orig_rax;
unsigned long rip;
unsigned long cs;
unsigned long eflags;
unsigned long rsp;
unsigned long ss;
unsigned long fs_base;
unsigned long gs_base;
unsigned long ds;
unsigned long es;
unsigned long fs;
unsigned long gs;
};
indefinite 复现
初步分析
程序的主函数内容不多,但给出的信息很多
第 12 行使用
mprotect
函数修改了一块text
段的内存的属性:
而这段代码显然是可执行的,所以大概率本题使用了 smc 技术。
- 第 13 行使用
fork
函数复制了一个进程,并在 14 行的子进程中使用 ptrace 告知自己可被 trace ,以及用 int 3 断点暂停运行,等待调试器的指令。在恢复运行后应该直接执行 child 函数。 - 从第 19 行开始的主进程中使子进程继续运行,并执行
tracer
函数
tracer 函数
tracer 函数中,出现了 ptrace 的典型使用方法,并在第 20 行获取了寄存器的值。将v11
的类型修改为寄存器结构体类型后即可清楚的看到这段程序的逻辑:
在 21 行读取了寄存器的值后,在第 25 行读取了 rip 地址上的值,也即当前运行的字节码。而 0xB0F 就是上文中那串未被识别的汇编中 ud2 指令的字节码:
接下来在第 42 行中对这段汇编拷贝出来的数据进行了“解压缩”,之后通过process_vm_writev
将解压出的数据写回内存。
解密 smc
因此我们使用动态调试的办法进行解 smc ,动态调试的过程中注意 rip 的值,在开启地址随机化后 rip 的后 3 位与 ida 加载出来的地址是相同的,用此办法来确定当前解 smc 后的代码是哪一段。
解密后的分析
在解密所有 4 个函数后,即可看到大致的逻辑:
child函数打开随机数文件,读取8字节数据作为key,开始加密文件。
do_encrypt_file
对传进去的文件名后面加上了.enc
后缀,并获取文件大小,读取文件内容,新建一块内存,将 key 和文件内容写入 v9 ,在加密完成后再将 v9 写入.enc
后缀的文件中。
do_encryption
函数循环更新 key 并与刚才新建的内存异或。
advance
函数是一个魔改的crc32
,其中movbe
指令用于改变数据的大小端,因此我们之间用 C 语言将这个函数写出来,再将加密后的文件异或回去就可以了:
1 |
|