从一道CTF题目中学习反调试

Windows平台常见反调试技术梳理(下) - 安全客,安全资讯平台

 

前言

上周五空指针的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公开赛

(完)