第53章:高级反调试技术——Pespin

主要采用扰乱代码解析、压缩然后再解密执行、SEH 等来防止被攻破。

当程序中出现这种函数,内部是

可以直接判定是垃圾代码,实际上就是跳转到离 Call 指令 C 字节远的地方。

 

以 Stolen_Bytes_Pespin.exe 为例:

到达入口断点后,保持单步执行,不然很容易跑飞。可以看到,程序边解码边执行:

直到执行到此处:

此处的 ebp == 7CAE 将是后续解码的关键。后面的关于 ecx 的操作即是垃圾代码。

可以看到,绕了一大圈什么都没做,这样的指令后面会经常遇见。

接下来定位此程序的 NT 头,并存储到自己定义的变量里,这些偏移可以记一下,以便后来查看:

定位到此程序的导入表后可以发现,这不是正常的导入表,而是经过程序加密后的。

取数据,定位 40B050 开头的函数地址,并存储到自定义变量中

经过一系列的跳转,来到程序解码处:

pushfd 指令将标志位入栈,通过对 ZF 标志位进行检测,使用 mul 指令 影响 EAX,然后借助 EAX 实现程序的跳转

解码完成后会将内置值传递给 EDI ,继续实现解码(跳转的原理和前面一样):

 

解码完成后可以看到是一系列的报错字符串:

继续执行,取 Kernel32.LoadLibrary 地址,循环直到找到  kernel32 的 DOS 头(在 75C7000 )

程序定位各个需要的值,并存储到自定义变量中

接下来对函数区进行解码。EDI 是由前面程序在执行中给出的固定值( 40D48C )

首先对函数名称字符串的第三个字符进行对比

若不符合要求则进行下一个函数的对比

若符合要求则 Call 40D654,对函数名称中的每个字符进行计算,函数返回计算值,然后进行对比

若对比失败则跳转到 40D654 处,对下一个函数继续进行刚才的步骤。

若对比成功则,通过对函数名称的检测后,将地址写入程序内存区域加密处 [ edi+1 ] (edi 从 40D48C 开始),后面会调用该地址中存储部分函数

然后继续执行

解码完成后,从 40D48C 开始到遇见第一个 Dword Null 。由于不是四字节的对称,因此部分系统函数没有标出

经过跳转后继续进入解码循环,从 40B610 开始一共 613 字节

若 执行 dec ecx 后,ecx 不等于0,mul [esp+8] 后等于0 ,继续执行循环,若 ecx = 0 ,则 CF 标志位为 0,mul eax 后,eax == 程序给出的值,会跳出循环

继续执行,程序会刻意制造异常,调用异常函数(40B64D),就在函数的下方

进入异常处理函数

恢复 SEH 继续执行

注意,上面调用的系统函数都是之前在 40D48C 处开始解码得到的

使用 VirtualAlloc 函数分配一块内存空间

函数返回后将地址存入自定义变量中

调用函数 GetModuleFileNameA ,并将路径存储在系统分配的内存中(30000)

函数返回后,使用该路径调用 CreatFileA 函数,并返回一个文件句柄

使用之前得到的文件句柄调用 GetFileSize 函数,返回当前程序的大小(8E00)

然后根据返回的文件大小,调用 VirualAlloc 分配一个与当前程序文件同大小的内存空间(520000)

调用 ReadFile 函数,将当前程序文件的数据写入分配的内存空间(520000),此处是未载入内存即未解码的数据,从 FileAlignment 可以看出来

执行完后

然后调用 CloseHandle 函数,关闭文件句柄。

接下来对获取到的程序数据进行计算,从 MZ 开始循环计算 8C18 字节(即计算到 528C18 ),对程序进行校验

计算完值由 EAX 携带返回值

调用 VirtualFree 函数清理掉申请的两个内存空间(30000 和 520000 )

然后再次修改 SEH 链,引发除法异常(除数为 0)

进入异常处理函数后

异常处理函数返回值为 0 ,即利用 Contex -> Eip 进行返回

然后程序将持续进行解码

再次触发异常,跳转到异常处理函数,因为该异常处理函数的返回值为 0 ,会再次回到触发异常的指令处

接下来会遇到多个异常,但其实执行的指令都是挨着向下走的,和正常执行没什么两样

继续解码

程序会再次运行到前面运行过的解码区域,进行二次解码

再次引发异常

继续进行解码,代码相似,但是确实是第一次执行

再次对内存区域进行计算,此次是从 401000 到  4010021

返回后,再次对值进行检验,此时二者数值相等

经过跳转,再次调用 VirtualAlloc 函数分配一个内存空间(地址为 30000 )(之前分配了又释放了)

程序从 401000 起将数据传入 30000 开始的地址处,通过对 DL 进行反复的计算操作。

写入的字符来自 ESI 所指区域以及 EAX 中存储的值。解码的区域是 400000 到 404027 ,存储的区域是 30000 到 35000 。

然后将数据传回 401000 开始处,完成对 401000 到 406000 的解码

由此处代码可知,程序会进行循环解码,ecx 存储着循环次数(3)

解码区域是 406000 到 4063E4 ,覆盖存储在 30000 到 31000 处,传回的区域是 406000 —— 407000

解码区域是 407000 到 407211,覆盖存储在 30000 到 33000 处,传回的区域是 407000 —— 40A000

解码完毕后,调用 VirtualFree 函数清除掉 30000 处的所有数据

修改 4010C8 处的内存属性

接着开始修改 4010C8 

解码后的内存值

再次修改 SEH 链,实际上还是按照顺序执行的,只是为了防止被跟踪

得到程序的运行时间,处理后存入内存

连续处理并存储,从 40B000 到 40B0D4

经过一段无意义的跳转继续执行代码解码工作

解码后该区域的值,前面一小部分是未解码的,后面的解码后的数据很规整

程序继续对 406882 起始的地址进行解码,得到 kernel32 的库名

接着就调用 LoadLibrary 函数,装载库文件。经实际查看,在程序装载该库前,就已经装载过该库文件。

装载好后,定位库文件的导出表地址

循环,清除存储库名的内存空间

接着定位刚开始解码后的内存区域(406000)

上图地址指向的内存空间,一 一对应,下图是 函数首字符(1字节)+ 函数名称计算值(4字节)的一个组合体

后面执行的循环和前面找函数时使用的方法有些不同, 但是代码执行的地址有部分是重复的

若找到符合条件的,则进入 call 函数

每一个字符计算8次,然后回到 40D70E,进行对下一个字符进行计算

计算完后会对总的计算值进行对比

若不符合要求则跳转,然后继续对下一个函数名称进行查找、计算、比较

若符合要求则进行一系列的检测,条件都满足后将函数地址写入 40FE1E 开始处的地址中

若函数属于另外一个库文件,则不会跳转,而是调用 LoadLibrary 和 GetprocAddress 两个函数,取得该函数地址放入 EAX 然后继续执行一样的操作

将 EDX 与 [EDI] (40FE1E 开始)中的值进行对比,两个值都是前面解密出来的,40FE1E 开始每5个字节为一组,第一个字节为 00 ,后四个字节为函数地址

一共比较 D8 次,正常来说都会找到

找到后,对 edi - 1 进行检测,EAX 存储着函数 VA

然后回到 40C937 继续执行 

 

在重建 IAT 完后,单步执行没多久就可以看到和正常的程序一样的指令了

注意观察看到 push ebp , mov ebp,esp 则应该特别注意是否为程序的起始代码地址

 

posted @ 2020-10-17 19:53  Rev_omi  阅读(209)  评论(0编辑  收藏  举报