什么是双进程保护问题
本题目来自于NSSCTF平台的题目。
ID: 1442 [NSSRound#6 Team]void(V2)
首先,我们应该了解一下Linux下C++的多进程编写。
fork函数
本部分以一个实例来讲解一下fork函数产生的双进程。
__int64 sub_CDA() { __int64 result; int i; __pid_t v2;
v2 = fork(); if ( v2 < 0 ) exit(1); if ( v2 ) { result = sub_AD7(v2); } else { result = ptrace(PTRACE_TRACEME, 0LL, 0LL, 0LL); if ( result == -1 ) return result; } for ( i = 0; i <= 0; ++i ) result = ((sub_95A + i + 7))(11LL, 13LL, 17LL); return result; }
|
所有的关键部分都已经在上面这部分代码写上了注释。
双进程问题特征
- 父进程被当做一个调试器,父进程可能会对子进程进程改变
- 子进程是关键部分,关键代码会在子进程中出现
- 使用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; unsigned __int64 v3; unsigned __int64 v4; pt_regs reg; unsigned __int64 v6;
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 ) { ptrace(PTRACE_SYSCALL, son_pid, 0LL, 0LL); waitpid(son_pid, &stat_loc, 0); ptrace(PTRACE_GETREGS, son_pid, 0LL, ®); 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, ®); } 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, ®); } ptrace(PTRACE_SYSCALL, son_pid, 0LL, 0LL); waitpid(son_pid, &stat_loc, 0); } return __readfsqword(0x28u) ^ v6; }
|
其中父进程会大量使用prace函数,以此实现对子进程的调试,这一个部分我已经将ptrace函数分别实现的功能做了注释。可以看到,父进程的功能是这样的,在子进程的系统调用函数后下一个断点,然后再这一个断点处获取子进程的寄存器信息,如果寄存器中的orig_rax是12345或者67890,就对子进程的寄存器进行修改。
然而,orig_rax保存的是系统调号,这个编号不可能出现12345或者67890,因此这个函数对子进程不会进行任何操作。
应对方案
修改子进程
首先分析父进程的流程,了解清楚父进程对子进程进行了何种修改,之后编写IDApython脚本对子进程进行修改。
动态调试
在动态调试中,我们需要进行以下几个操作。
- nop掉fork函数,让程序只走子进程的部分
- nop掉子进程的ptrace函数或者修改ptrace函数的返回值,使其不要执行退出函数
- 在适当位置下断点(比如子进程结束时),查看子进程进行的操作等
之后的操作就是正常的逆向流程了