什么是双进程保护问题

本题目来自于NSSCTF平台的题目。

ID: 1442 [NSSRound#6 Team]void(V2)

首先,我们应该了解一下Linux下C++的多进程编写。

fork函数

本部分以一个实例来讲解一下fork函数产生的双进程。

__int64 sub_CDA()
{
__int64 result; // rax
int i; // [rsp+Ch] [rbp-14h]
__pid_t v2; // [rsp+10h] [rbp-10h]

v2 = fork(); // 产生双进程
if ( v2 < 0 ) // 产生双进程失败
exit(1);
if ( v2 ) // fork函数的返回值决定了这个进程是父进程还是子进程
{ // 父进程的返回值是子进程的pid,因此一定大于0
result = sub_AD7(v2); //父进程执行sub_AD7函数
}
else // 子进程的fork返回值是0
{
result = ptrace(PTRACE_TRACEME, 0LL, 0LL, 0LL);// 请求被调试
if ( result == -1 ) // 如果子进程被其他调试器调试,那么ptrace向父进程请求调试失败,返回-1
return result;
}
for ( i = 0; i <= 0; ++i ) // 如果ptrace向父进程请求成功,返回值是0
result = ((sub_95A + i + 7))(11LL, 13LL, 17LL);
return result;
}

所有的关键部分都已经在上面这部分代码写上了注释。

双进程问题特征

  1. 父进程被当做一个调试器,父进程可能会对子进程进程改变
  2. 子进程是关键部分,关键代码会在子进程中出现
  3. 使用ptrace用来连接两个进程

ptrace函数介绍

ptrace函数是调试器(比如gdb)所使用的函数,这也是为什么说父进程被当做一个调试器的原因。

子进程中会在开始调用一个ptrace函数:

result = ptrace(PTRACE_TRACEME, 0LL, 0LL, 0LL);

这个函数是请求父进程对本进程进行调试,调用成功返回0,失败返回-1。

这个函数也是一种反调试技术,但是我们可以很容易对其进行nop。

我们再来看一下父进程调用的函数。

// 父进程调用的函数
unsigned __int64 __fastcall sub_5622CB200AD7(unsigned int son_pid)
{
int stat_loc; // [rsp+1Ch] [rbp-F4h] BYREF
unsigned __int64 v3; // [rsp+20h] [rbp-F0h]
unsigned __int64 v4; // [rsp+28h] [rbp-E8h]
pt_regs reg; // [rsp+30h] [rbp-E0h] BYREF
unsigned __int64 v6; // [rsp+108h] [rbp-8h]

v6 = __readfsqword(0x28u);
waitpid(son_pid, &stat_loc, 0);
if ( stat_loc != 127 )
exit(1);
ptrace(PTRACE_SETOPTIONS, son_pid, 0LL, 0x100000LL);// 防止子进程进程脱离父进程控制
while ( stat_loc == 127 ) // 127表示子进程正在被跟踪
//
{
ptrace(PTRACE_SYSCALL, son_pid, 0LL, 0LL); // 当子进程执行一个系统调用时,它会被暂停
waitpid(son_pid, &stat_loc, 0);
ptrace(PTRACE_GETREGS, son_pid, 0LL, &reg); // 获取所有寄存器的值,放入V5
if ( reg.orig_rax == 67890 )
{
reg.orig_rax = 1LL;
v3 = reg.rdx;
reg.rdx = reg.rsi;
reg.rsi = reg.rdi;
reg.rdi = v3;
ptrace(PTRACE_SETREGS, son_pid, 0LL, &reg);
}
if ( reg.orig_rax == 12345 )
{
reg.orig_rax = 0LL;
v4 = reg.rdx;
reg.rdx = reg.rsi;
reg.rsi = reg.rdi;
reg.rdi = v4;
ptrace(PTRACE_SETREGS, son_pid, 0LL, &reg);
}
ptrace(PTRACE_SYSCALL, son_pid, 0LL, 0LL);
waitpid(son_pid, &stat_loc, 0);
} // while循环块
return __readfsqword(0x28u) ^ v6; // 返回0
}

其中父进程会大量使用prace函数,以此实现对子进程的调试,这一个部分我已经将ptrace函数分别实现的功能做了注释。可以看到,父进程的功能是这样的,在子进程的系统调用函数后下一个断点,然后再这一个断点处获取子进程的寄存器信息,如果寄存器中的orig_rax是12345或者67890,就对子进程的寄存器进行修改。

然而,orig_rax保存的是系统调号,这个编号不可能出现12345或者67890,因此这个函数对子进程不会进行任何操作。

应对方案

修改子进程

首先分析父进程的流程,了解清楚父进程对子进程进行了何种修改,之后编写IDApython脚本对子进程进行修改。

动态调试

在动态调试中,我们需要进行以下几个操作。

  1. nop掉fork函数,让程序只走子进程的部分
  2. nop掉子进程的ptrace函数或者修改ptrace函数的返回值,使其不要执行退出函数
  3. 在适当位置下断点(比如子进程结束时),查看子进程进行的操作等

之后的操作就是正常的逆向流程了