前言
上周五空指针的re第二次公开赛
本身是初学逆向,当时一直卡在反调试(太菜了..),smc一直未能解密得到正确代码,赛后找apeng大佬看wp又仔细琢磨了一下,有些收获,便写文章做个记录。
题目分析
首先运行一下程序
然后动态调试会发现代码出现异常,异常处理程序输出”not welcome!”,所以可以断定程序存在反调试。
拖进ida32打开,f5后分析main函数。
argc 是命令行总的参数个数,包含路径
argv[]包含argc个参数,第0个参数是路径
这里首先执行sub_402DC0函数进行smc修改loc_402B50处的代码,然后将argv[1]作为参数传递给函数sub_402B50,当然此时静态分析看到的loc_402B50处的数据还是一片混乱。
我们查看sub_402DC0函数,可以明显看到是一个smc,起始地址为loc_402B50,要改变的内存区域大小为0xBC。
再跟进sub_4032A0函数,发现是以byte_452F90为key,然后rc4解密sub_402b50的代码。所以我们的关键应该分析byte_452F90里数据的变化,得到正确的byte_452F90数据即可进行正确的smc解密。
然后我们查看byte_452F90的交叉引用,发现除了执行smc的函数以外,只有TopLevelExceptionFilter函数用到,而这个函数是一个顶层异常处理函数,暂时可以忽略。
但是我们看到byte_452F90上面还有一个数组,猜想可能是修改byte_452F80时修改了byte_452F90处的值。
我们查看交叉引用发现有6处修改吗,但是修改byte_452F80的代码都没有被识别为函数。
我们一一进行分析,按p定义函数(这里ida未识别的原因可能是这里的函数没有被交叉引用,我调试发现是在cinit处调用的),然后重命名,接下来我们依次分析这四个反调试。
反调试分析
debuging1
这里调用Ntdll.dll的NtQueryInformationProcess函数,它用来提取一个给定进程的信息。
它的第一个参数是进程句柄,第二个参数告诉我们它需要提取进程信息的类型。为第二个参数指定特定值并调用该函数,相关信息就会设置到第三个参数。第二个参数是一个枚举类型,其中与反调试有关的成员有ProcessDebugPort(0x7)、ProcessDebugObjectHandle(0x1E)和ProcessDebugFlags(0x1F)。例如将该参数置为ProcessDebugPort,如果进程正在被调试,则返回调试端口,否则返回0。
这里第二个参数v3为ProcessDebugObjectHandle(0x1E),如果进程正在被调试,DebugObjectHandle会设置到第三个参数v4(也就是v75)。
然后紧接着的是一个条件判断,正确的执行流程v75应该为0,也就是执行else语句的内容,所以我们把这里的jnz loc_401FE9 给nop掉即可。
patch后执行的是这一块代码,可以看到修改了byte_452F80为起始地址的32个字节,也就是确实有修改到我们前面说的byte_452F90(lc4的key),剩余的3个反调试函数也是一样对byte_452F90做了修改。
debuging2
这里检查进程环境块(PEB)中的调试标志。
Windows操作系统维护着每个正在运行的进程的PEB结构,它包含与这个进程相关的所有用户态参数。这些参数包括进程环境数据,环境数据包括环境变量、加载的模块列表、内存地址,以及调试器状态。
typedef struct _PEB {
BYTE Reserved1[2];
BYTE BeingDebugged; //被调试状态
BYTE Reserved2[1];
PVOID Reserved3[2];
PPEB_LDR_DATA Ldr;
PRTL_USER_PROCESS_PARAMETERS ProcessParameters;
BYTE Reserved4[104];
PVOID Reserved5[52];
PPS_POST_PROCESS_INIT_ROUTINE PostProcessInitRoutine;
BYTE Reserved6[128];
PVOID Reserved7[1];
ULONG SessionId;
} PEB, *PPEB;
进程运行时,位置fs:[30h]指向PEB的基地址,所以这里的v46就是检查BeingDebugged标志,我们要做的就是patch使得v46的值为0即可。
然后还可以看到byte_452F80的赋值代码和v37和v45有关,这两个变量的值为GetTickCount()函数的返回值,GetTickCount函数返回最近系统重启时间与当前时间的相差毫秒数,是一个时钟检测。
emm但是好像只要不在两个函数中间下断点应该不会有问题,v37-v45正常情况会是一个很小的值(我自己调试时是0),然后右移两位后应该为0,数据与0异或不发生任何改变。
debuging3
这里是对关键位置的代码求和
v35的值会被拿去异或
v37的值是一个基于rdtsc指令指令的时间检测,正常执行应该为0
v36的值是一个基于GetTickCount()函数的时间检测,正常执行应该为0
v30到v34是5处关键位置的代码求和,这涉及到调试器的软件断点原理,简单来说也就是会用int 3指令替换原代码,所以我们不能在这些代码处下断点
然后还有一项关键数据就是
(*(_DWORD *)(*(_DWORD *)(__readfsdword(0x2Cu) + 4 * TlsIndex) + 4) & 0xFF)
这里是另一处反调试的点,在TLS回调函数中,后面会讲,先说结果,这一项的值应该是0
v35由这么多项求和得到,调试得到,v35的值应该为0x21A,所以我们要做的将v35的值patch为0x21A
debuging4
这里是检查进程环境块(PEB)中的NTGlobalFlag
由于调试器中启动进程与正常模式下启动进程有些不同,所以它们创建内存堆的方式也不同。系统使用PEB结构偏移量0x68处的一个未公开位置,来决定如何创建堆结构。如果这个位置的值为0x70,我们就知道进程正运行在调试器中。
和debuging2类似,我们只需要patch使得v24的值为0即可。
TLS回调函数
上面debuging3中遇到的
*(_DWORD *)(*(_DWORD *)(__readfsdword(0x2Cu) + 4 * TlsIndex) + 4)
其实是在tls回调函数中被赋值的,这里的赋值操作同样是一个反调试
这个函数可以在导出表中看到,或者用xdbg调试也会断在tls回调函数
而ida的函数表里没有出现这个函数是因为有花指令阻碍了ida的分析
自行nop修改后按p重新定义函数即可看到函数反编译内容
我们可以看到这里的反调试和debuging1中的反调试是基本一样的,正确的执行流程result的值应该为0,v8的值也应该为0,所以我们把这里的
.text:00401D98 jz short loc_401DB5
jz指令patch为jump指令即可
总结
至此,此题的反调试点已经全部干掉,保存到原文件后就可以随意调试这个这个程序了
完成了smc后程序就进入了正常加密过程
首先sub_402B50对我们输入的key进行了移位
然后触发异常,第一个异常处理函数是一个较复杂的异或操作,第二个异常处理函数是魔改的sm4加密算法,最后会将加密的数据逐个进行比较,正确则输出”right!flag is npointer{your_key}”
详细的解密过程可以去看apeng大佬的博客,或者官方公众号的wp。
参考链接
apeng大佬的博客2020 空指针 5月RE公开赛