由一道题看 Linux 的ptrace调试机制——cyber-apocalypse-2022\indefinite

前置知识

ptrace 函数

ptrace 函数是 Linux 下用来控制进程的函数,可以检查和改变子进程的功能,如获取和写入寄存器的值,修改内存(Peek and poke)等。典型的使用 ptrace 的应用:gdb,strace(用来查看某进程运行过程中的系统调用)

ptrace函数的定义

1
2
#include <sys/ptrace.h>       
long ptrace(enum __ptrace_request request, pid_t pid, void *addr, void *data);
  • request: 表示要执行的操作类型。//反调试会用到PT_DENY_ATTACH,调试会用到PTRACE_ATTACH

  • pid: 要操作的目标进程ID

  • addr: 要监控的目标内存地址

  • 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
    #include<sys/wait.h>/*引入wait函数的头文件*/
    #include<sys/reg.h>/* 对寄存器的常量值进行定义,如Eax,EBX....... */
    #include<sys/user.h>/*gdb调试专用文件,里面有定义好的各种数据类型*/
    #include<sys/ptrace.h>/*引入prtace头文件*/
    #include<unistd.h>/*引入fork函数的头文件*/
    #include<sys/syscall.h> /* SYS_write */
    #include<stdio.h>
    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, &regs);
    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
    30
    struct 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 复现

初步分析

image-20220521162628213

程序的主函数内容不多,但给出的信息很多

  • 第 12 行使用mprotect函数修改了一块text段的内存的属性:

    image-20220521163124178

而这段代码显然是可执行的,所以大概率本题使用了 smc 技术。

  • 第 13 行使用fork函数复制了一个进程,并在 14 行的子进程中使用 ptrace 告知自己可被 trace ,以及用 int 3 断点暂停运行,等待调试器的指令。在恢复运行后应该直接执行 child 函数。
  • image-20220521171357966
  • 从第 19 行开始的主进程中使子进程继续运行,并执行tracer函数

tracer 函数

image-20220521163852934

tracer 函数中,出现了 ptrace 的典型使用方法,并在第 20 行获取了寄存器的值。将v11的类型修改为寄存器结构体类型后即可清楚的看到这段程序的逻辑:

image-20220521165118654

在 21 行读取了寄存器的值后,在第 25 行读取了 rip 地址上的值,也即当前运行的字节码。而 0xB0F 就是上文中那串未被识别的汇编中 ud2 指令的字节码:

image-20220521170529689

接下来在第 42 行中对这段汇编拷贝出来的数据进行了“解压缩”,之后通过process_vm_writev将解压出的数据写回内存。

解密 smc

因此我们使用动态调试的办法进行解 smc ,动态调试的过程中注意 rip 的值,在开启地址随机化后 rip 的后 3 位与 ida 加载出来的地址是相同的,用此办法来确定当前解 smc 后的代码是哪一段。

解密后的分析

在解密所有 4 个函数后,即可看到大致的逻辑:

image-20220521171441384child函数打开随机数文件,读取8字节数据作为key,开始加密文件。

image-20220521172019620

do_encrypt_file对传进去的文件名后面加上了.enc后缀,并获取文件大小,读取文件内容,新建一块内存,将 key 和文件内容写入 v9 ,在加密完成后再将 v9 写入.enc后缀的文件中。

image-20220521172218313

do_encryption函数循环更新 key 并与刚才新建的内存异或。

image-20220521172337931

advance函数是一个魔改的crc32,其中movbe指令用于改变数据的大小端,因此我们之间用 C 语言将这个函数写出来,再将加密后的文件异或回去就可以了:

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
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
#include<stdio.h>
__int64 enc(__int64 a1)
{
__int64 _RAX; // rax
__int64 v3; // [rsp+0h] [rbp-38h] BYREF
unsigned int v4; // [rsp+10h] [rbp-28h]
unsigned int v5; // [rsp+14h] [rbp-24h]
unsigned int v6; // [rsp+18h] [rbp-20h]
int v7; // [rsp+1Ch] [rbp-1Ch]
int i; // [rsp+20h] [rbp-18h]
unsigned int v9; // [rsp+24h] [rbp-14h]
__int64 v10; // [rsp+28h] [rbp-10h]
char* v11; // [rsp+30h] [rbp-8h]

v3 = a1;
v11 = (char*)&v3;
v4 = 8;
v5 = 0;
v6 = -1;
while (v4 > v5)
{
v7 = (unsigned __int8)v11[v5];
v6 ^= v7;
for (i = 7; i >= 0; --i)
{
v9 = -(v6 & 1);
v6 = (v6 >> 1) ^ v9 & 0xEDB88320;
}
++v5;
}
v10 = ~v6;
_RAX = v10;
char* p = (char*)&_RAX;
for (int i = 0; i < 4; i++)
{
char tmp = p[i];
p[i] = p[7 - i];
p[7 - i] = tmp;
}

return v10 | _RAX;
}
unsigned char hexData[220] = {
0x5E, 0xDE, 0x28, 0x24, 0xB7, 0xB6, 0x89, 0xD0, 0x0C, 0x39, 0x08, 0x1F, 0x1B, 0x1B, 0x7D, 0x6D,
0x4E, 0x33, 0xC4, 0xED, 0xEF, 0xDC, 0x3B, 0x6A, 0xC5, 0x24, 0xA2, 0xA3, 0xAB, 0xE7, 0x50, 0x92,
0x7A, 0x33, 0x30, 0xF2, 0xF7, 0x2B, 0x33, 0x7C, 0x80, 0x89, 0x01, 0xB4, 0xBF, 0x12, 0xC7, 0x8E,
0x32, 0x01, 0xD4, 0xC0, 0xC7, 0x9B, 0x4E, 0x33, 0x92, 0x6C, 0xD8, 0xC8, 0xCF, 0x97, 0x68, 0x8E,
0x86, 0x04, 0x17, 0x2D, 0x20, 0x3E, 0x04, 0x85, 0xC8, 0x1E, 0x50, 0xB3, 0xB4, 0x19, 0x4C, 0x93,
0x15, 0xDF, 0x60, 0x8D, 0x84, 0x7A, 0x92, 0x53, 0xE7, 0x19, 0x61, 0xEC, 0xB8, 0x64, 0x13, 0xB5,
0x6C, 0x3A, 0x32, 0x7B, 0x68, 0x3D, 0x74, 0x49, 0x8A, 0xE0, 0xB8, 0x2E, 0x33, 0xB8, 0xA6, 0x9C,
0xD7, 0x2D, 0x1A, 0x04, 0x0C, 0x1D, 0x70, 0x8E, 0xC1, 0x65, 0x4B, 0x8E, 0x86, 0x44, 0x65, 0xE1,
0x27, 0x62, 0x10, 0x76, 0x7B, 0x44, 0x32, 0x6A, 0xEC, 0x07, 0x9B, 0x62, 0x7F, 0x88, 0x54, 0xF9,
0x15, 0x23, 0xE2, 0x1F, 0x03, 0xA7, 0x71, 0x18, 0xBF, 0xE1, 0xC5, 0x67, 0x70, 0x8E, 0xEB, 0xA5,
0xD7, 0x58, 0xA9, 0x7E, 0x64, 0xA9, 0x62, 0xEC, 0xD6, 0xB9, 0x0D, 0x07, 0x1B, 0x4C, 0xB4, 0xA7,
0xCC, 0x7E, 0x6A, 0x9A, 0x93, 0x04, 0x66, 0xC8, 0x02, 0x77, 0x7F, 0x13, 0x47, 0x2F, 0x04, 0x44,
0x7F, 0xE8, 0x6E, 0xEA, 0xE2, 0x6B, 0xF6, 0x76, 0x9D, 0x29, 0xCD, 0x09, 0x65, 0xD6, 0x6E, 0xA1,
0x97, 0x03, 0xCD, 0xFA, 0xA9, 0xB3, 0xFB, 0xF6, 0x2B, 0x21, 0x7E, 0x00
};

int main()
{

__int64 key= *(__int64*)&hexData[0];
for (int j = 8; j < 220; j += 8)
{
key = enc(key);
*(__int64*)&hexData[j] ^= key;
}
puts((char*)&hexData[8]);
}