[0x01背景]
前几天去玩了Hack.lu,因为工作比较忙,也没做出什么题来,不过感觉里面的题目还是挺有意思的,有些题目虽然不难,但是涵盖的知识点很多,这篇文章要分析mealtime就是这样一个例子。
下面是mealtime的题目描述:
Heading up the steeple gave you and your companion a nice view over the outbreak situation in your city. But it also attracted a lot of unwanted attention. Zombies are surrounding your spot and are looking for an entrance to the building. You obviously need some bait to lure them away so you can flee safely. Solve this challenge to find out which human bodypart zombies like the most.
http://dl.ctftime.org/38/142/mealtime.exe
这是一道逆向分析类的题目,用了TEA作为其算法,有很多队伍都完成了,下面是两篇来自其它队伍的writeup:
http://leetmore.ctf.su/wp/hack-lu-2012-ctf-challenge-25-200/?utm_campaign=ctftime (貌似需要翻墙)
http://blog.lse.epita.fr/articles/35-hacklu-ctf-2012-mealtime-200-points.html
这两篇文章对算法的分析已经非常清晰了,所以我不打算再复述了。但是可惜的是,这道题里面使用了非常多的反调试技术,在这两篇文章都只进行了轻描淡写的描述。算法固然是解决这道题目的核心,但是对于其中比较经典的反调试技术,我觉得也有必要和大家分享一下,所以才有了这篇文章。
[0x02 IsDebuggerPresent]
这个API的出场率极高,从名字就能看出它的作用,其内部实现非常简单:
7C813123 64:A1 18000000 mov eax,dword ptr fs:[18]
7C813129 8B40 30 mov eax,dword ptr ds:[eax+30]
7C81312C 0FB640 02 movzx eax,byte ptr ds:[eax+2]
7C813130 C3 retn
如果一个调试器通过Debug API调试一个进程的话,它会把表示进程运行状态的一个结构体中的一个标志位置位,因此只需要检测这个标志位,就知道程序是否在被调试了。那么如何获取这标志位呢?
这里就不得不谈到一个特殊的寄存器——FS寄存器。系统会将当前线程的相关信息存储在一个叫做线程环境块(TEB)的数据结构中,而FS寄存器就是用来存储该数据结构的指针的。TEB的第一个成员是一个叫做线程信息块的结构(TIB),而线程信息块中偏移0×18的位置,正好是线程环境块(TEB)的反向指针,所以我们可以从FS:[18]这个位置获得TEB这个结构,得到了TEB之后,我们就可以在其偏移0×30的地方,找到存储当前进程信息的一个结构体——进程环境块(PEB),而进程环境块中偏移0×02的地方,就是我们刚刚提到的标志进程是否被调试的标志位了。由此可见,上面列出的IsDebuggerPresent的代码,就是在寻找这个标志位。
[0x03破坏程序执行]
检测到程序被调试,或者程序验证不通过,我们往往会将程序的执行流转移到一个错误处理中,例如弹出一个对话框说程序执行错误或者要求用户重新进行验证。但是这道题目却用了一种非常暴力的方式来处理这些情况,那就是让程序产生异常,进而退出。
让程序异常的方法很多,有些很容易识别,例如该程序中用到了下面的代码:
示例一:
.text:00401507 pop esp
示例二:
.text:0040197C mov esp, eax
示例三:
.text:004011B7 push 0BADBADh
.text:004011BC retn
示例一和示例二都是直接破坏了指向堆栈的指针,使得程序在进行堆栈相关的操作时就会产生异常,示例三是直接修改了函数的返回地址,从而导致函数异常执行。
同时,这个程序里面还用了一种比较隐蔽的导致程序异常执行的方式,那就是std指令。看下面这段代码:
.text:004015C3 cmp al, [ecx]
.text:004015C5 jz short loc_4015C8
.text:004015C7 std
这段代码很简单,先用cmp做了一个比较,如果不相等,就执行std指令。std指令的作用非常小,他只会将标志寄存器中一个和地址增长方向有关的标志——DF标志置位。不过微小的改变有时候也可能引起巨大的变化,所以是不能忽略的。一旦DF标志被置位了,当我们进行字符串拷贝的时候,本应该按照地址递增的方向进行拷贝,但是由于DF的影响,却变成了按照地址递减的方向进行拷贝,这将会产生什么样的后果是难以预料的。在这个程序中,这个小小的改变直接导致了后面GetModuleHandle这个API调用失败,从而使程序无法正常执行。
[0x04 进程检测]
进程检测是一种非常常用的反调试手段,这个程序执行的时候如果检测到有如下三个进程存在,它就会尝试结束他们:
Ollydbg.exe
Immunitydebugger.exe
Idag.exe
但是这个程序所使用的进程检测手法与其它一些常见的进程检测手法不同。它不仅将需要检测的进程名进行了加密处理,而且没有使用我们熟悉的CreateToolhelp32Snapshot(), Process32First(), Process32Next() 或者 EnumProcesses() 来枚举进程,而是使用了一个更加底层的API——NtQuerySystemInformation来获取进程列表。通过NtQuerySystemInformation可以获取一个存储当前系统运行进程信息的结构体链表,再通过枚举这个链表,就可以得到当前运行的进程了。
[0x05 VEH异常处理]
VEH是SEH的扩展,其优点是可以先于SEH捕获到异常并进行处理,以防止新增的SEH异常处理打乱异常处理函数的执行顺序。
异常处理一直是一种非常优秀的反调试手段,因为在调试过程中,所有异常都会先分发给调试器进行处理,而不是异常处理函数,对于一些对逆向分析不太熟悉的人来说,如果没有关注异常处理函数,一旦调试器报告异常,便不知道应该怎样处理这个异常了。
这个程序中注册VEH异常处理函数的方式比较隐蔽,它会在一开始创建一个被挂起的线程,而这个线程的功能,便是注册VEH异常处理函数。但是因为线程创建的时候处于挂起状态,所以这个异常处理函数并没有立即被注册,而是等到后面通过了一些检测后,程序才调用ResumeThread恢复该线程的执行,从而完成VEH异常处理函数的注册。程序后面会用一个int 3中断来触发异常,从而进入这个VEH异常处理函数中,在这个函数中完成了对该题目一部分key的验证过程。
[0x06 远程代码注入与代码窃取的结合]
远程代码注入与代码窃取这两种技术本来是没有什么相关性的,但是在这个程序中却把这两种技术结合在了一起,我觉得这算是一个比较经典的地方吧。如果要把这里使用的手法详细的叙述清楚,可能再写5页都不够,所以我这里只大体叙述一下主要的实现过程。
首先说一下代码窃取,就是将正常程序中的一部分代码分离出去,然后运行的时候再动态还原回来。这种技术在外壳中非常常见,特别是针对原始入口点的那部分代码,很多外壳程序都会将其移动到其它区域(比如堆中)去执行,执行之后再跳转到原始程序中去执行,这样不仅使程序还原相对复杂一点,而且原程序的入口点也不那么明显了。
远程线程注入,则是将一段代码注入带其它进程中去执行。现在的调试器往往只能针对一个进程进行调试,对于这种远程注入过去的代码就无法直接调试了。
这个程序是如何将这两种技术结合起来的呢?故事是这样的:
(1) 首先,程序会创建一个新线程,然后将主线程挂起。
(2) 新创建的这个线程,会将一段代码注入到另一个具有SeDebug权限的进程中(这个进程是之前进行进程检测枚举进程的时候得到的),然后通过CreateRemoteThread执行那段代码。
(3) 注入到另一个进程的代码的功能,则是将偷取的代码片段注入回原来的那个进程,然后唤醒之前挂起的那个主线程让其继续执行。
这样,通过两次远程注入,就可以将主线程中被偷取的代码还原回来了。
[0x07 补充:远程代码注入的调试方法]
远程代码注入给调试增加了不少难度,这里我总结一下远程代码注入的几种情况,并给出调试方法。
(1) 将一段代码注入到非系统关键进程里面执行,如IE(iexplorer.exe)中:
这个又分两种情况:
1) 注入到现有进程中:
这个处理起来比较简单,直接启动调试器附加将被注入的进程,然后让程序执行直到代码注入完成(但还未执行远程代码),在注入代码的入口处下断点,等到那边启动远程代码,这边就能够中断下来了。
2) 新创建一个挂起的进程并注入:
这种情况没办法直接附加进程,因此需要我们手动将注入代码入口的第一个字节改成0xcc(即int 3中断),然后直接执行远程代码。由于第一条指令被改成了cc,所以远程代码一执行就会触发异常,这个时候再用调试器附加,并把修改的指令改回来就行了。
(2) 将一段代码注入到系统关键进程里面执行,如explorer.exe中:
如果注入的是系统的关键进程,不管用什么方式进行调试,都可能产生意想不到结果,所以最好的办法还是在进行注入的时候,将被注入进程替换成一个非系统关键进程,然后再用(1)中的方法就行了。
(3) 将完整PE镜像注入到其它进程中:
这种情况最简单,直接将注入的PE镜像dump下来调试就行了,不过需要注意的是因为注入的PE是内存布局的,dump下来后可能还需要自己手动做一些调整。
最后再说一种通用的方法,就是直接将注入的代码提取出来并构造好参数单独调试,不过如果代码对程序的依赖性比较强(例如要通过硬编码调用被注入进程的API,很多盗号木马就是这样的)或者参数比较复杂,亦或是代码片段很分散,这种方式就不太适用了。
[0x08 代码转移]
这个方法不是太常见,就是直接创建一个新的文件,然后将部分代码拷贝过去,构造好参数,最后加载这个新文件使代码得以执行。当然,直接调试那个新文件就可以知道它要干嘛了,也可以直接把拷贝的代码提取出来调试。这道题中的一部分key,就是通过这种方式进行验证的。
[0x09 总结]
麻雀虽小,五脏俱全。虽然这道题目难度不是太大,但是也有很多值得挖掘的地方。这道题里面所使用到的反调试的技巧,都是比较常见的,希望我这个简单的总结能对那些对反调试技术还不太熟悉的人有所帮助。当然,自己调试一下,才能有更加深入的理解。
[0x10 附录:解密程序]
#include “stdio.h”
int main(int argc, char* argv[])
{
unsigned int i, j, Key, C, D, tKey, tC, tD;
Key = 0x78756C66;
C = 0x4FA2D1F6;
D = 0x3D6906FC;
/*
Key = 0x676E6966;
C = 0x80AF74A4;
D = 0x0DD9CDC44;
Key = 0×63737265;
C = 0x131AF1BE;
D = 0x4BB34049;
Key = 0×31216674;
C = 0x904A05D2;
D = 0x0DA4C7A90;
*/
j = 0xc6ef3720;
tC = tD = 0;
for (i = 0; i < 32; i++)
{
tKey = Key + j;
tD = D – ((((C>>5)^(C<<4))+C)^tKey);
j = j + 0x61c88647;
tKey = Key + j;
tC = C – ((((tD>>5)^(tD<<4))+tD)^tKey);
C = tC;
D = tD;
}
printf(“%x, %x\n”, C, D);
return 0;
}
//最后解出来Key是:–delicious_brainz_are_delicious
|