探索逆向领域壳的反逆向技术
2014-02-12
(天津现代职业技术学院,天津 300350)
加密壳与压缩壳具有最基本的反分析技术,而且只是在技术上的初步设防,防止逆向分析人员直接在调试器内加载受保护的程序,然后没有任何困难地开始分析。加密壳通常既加密本身代码也加密受保护的程序本身。不同的壳所采用的加密算法几乎不相同,有非常简单的循环,也有执行数次运算且非常复杂的循环。对于某些多态变形壳,为了防止检测壳的工具正确地识别壳,所以每次加壳所采用的加密算法都不近相同,解密代码也通过变形技术使其显得很不一样。
压缩即“外壳”是包裹在可执行的文件上一个壳。压缩主要目的是为了缩小可执行文件代码和数据的大小,但是基于原始的、包含可读字符的、可执行文件变成了压缩数据,因此也有那么一些混淆的作用。用户在执行程序中首先是这个外壳的程序运行,而这个外壳程序负责把用户原始的程序在内存中解开压缩,把控制权交还给解开后的真正的程序。所有这些解压工作都是在内存中运行完成,使用者根本不知道也不需要知道其运行过程。并且对执行速度基本没有什么影响。在逆向探索过程中我们还发现,解密和解压缩的循环很容易被躲过,逆向分析人员只需要知道解密和解压缩循环何时结束,然后在循环结束后面的指令上下断点。但是,有些壳会在解密循环中检测断点。在壳的例程中插入垃圾代码是另一种迷惑逆向分析人员的方法,它的目的是在加密例程中注入调试器检测的反逆向例程,掩饰真正目的的代码,迷惑调试器的断点。壳在调试器检测代码中藏匿了许多无关的、不起作用的、混乱的代码指令,这些垃圾代码可以有效地增加这些检测调试器的调试效果。此外,有效的垃圾代码是那些看似合法、有用的代码。
校验线程方法之一:利用处理器时间戳校验线程,使得逆向者要对受保护的应用程序实施在线调试变得非常困难。其思路是:创建一个专用的线程进行校验,并且不断地监视处理器的时间戳计数,如果发现该进程有被停止执行的迹象(比如说在调试器中设置的断点延误了进程)就“杀掉”该进程。Defender即使用了诸如RDTSC这样的底层指令来直接访问计数器,而没有使用系统的API函数,这样逆向人员就不能在程序中放钩子(hook)或是替换掉获得计数器值的函数,这是非常重要的一点。且每个关键函数都做了加密,校验线程使得逆向工作的开展比原来更令人头痛。
论述与探讨,如何让时间戳校验线程变得更难对付:在主线程中添加一个定期的校验和计算,用来校验线程。如果发现校验之和不匹配,就说明逆向人员对校验线程做了修补——立即终止进程。必须把校验存放在代码内,而不要集中放在某个地方。对实际的校验和验证代码也等同,必须把它们写成内联(inlined)函数,而不是用单个函数来实现。这样,消除检查或者修改校验和就变得非常困难了。为校验线程存放一个全局句柄,用每一个校验和验证来确保线程仍在运行。如果线程不运行了,就立刻终止程序。该提到的一点是,目前我所论述的这个校验线程方法只是探讨。时间戳计数器常数很多,而且可能会要求在运行时根据计数器的更新速度而计算间隔的周期。我们在逆向过程中,为什么不可以那么轻松地就避开了时间戳校验线程。
示例分析
下面是一个简单的时间戳校验的例子。在某一段指令的前后用RDTSC指令(Read Time-Stamp Counter)并计算相应的增量。增量值0x200取决于两个RDTSC指令之间的代码执行量。
rdtsc
mov ecx,eax
mov ebx,edx
;…more instructions
nop
push eax
pop eax
nop
;…more instructions
;compute delta between RDTSC instructions
rdtsc
;Check high order bits
cmp edx,ebx
ja .debugger_found
;Check low order bits
sub eax,ecx
cmp eax,0x200
ja .debugger_found
其它的时间检查手段包括使用kernel32!GetTickCount()API,或者手工检查位于0x7FFE0000地址的SharedUserData7数据结构的TickCountLow及TickCountMultiplier成员。
使用垃圾代码或者其它混淆技术进行隐藏以后,这些时间检查手段尤其是使用RDTSC将会变得难于识别。
探索分析一:
通过上面的示例,找出时间检查代码的确切位置,避免步过这些代码。逆向分析人员可以在增量比较代码之前下断然后用运行、代替、步过、直到断点断下来(OllyDbg调试器为例)。也可以下GetTickCount()断点以确定这个API在什么地方被调用或者用来修改其返回值。
1.设置控制寄存器CR48中的时间戳禁止位(TSD),当这个位被设置后如果RDTSC指令在非Ring0下执行将会触发一个通用保护异常(GP)。
2.中断描述表(IDT)被设置以挂钩GP异常并且RTDSC的执行被过滤。如果是由于RDTSC指令引发的GP,那么仅仅将前次调用返回的时间戳加1。因此,为了避免跟踪进入难懂的指令,在壳最常用的API处下断点。并把这些API当作跟踪的标志。如果在这些跟踪标志之间出了错,这时候就对这一段代码进行详细的跟踪。另外,设置内存访问、写入断点也可使逆向分析人员具有针对性地分析那些修改、访问受保护进程最有趣的部分的例程代码,而不是跟踪大量的代码最终却仅发现一个确定的例程。
最有趣的方法之一就是在壳中注入检查DebugObject10类型内核对象的数量。这种方法之所以有效是因为每当一个应用程序被调试的时候,将会为调试对话在内核中创建一个DebugObject类型的对象。
DebugObject的数量可以通过ntdll!NtQueryObject()检索所有对象类型的信息而获得。NtQueryObject接受5个参数,为了查询所有的对象类型,ObjectHandle参数被设为NULL,ObjectInformationClass参数设为ObjectAllTypeInformation(3):
NTSTATUS NTAPI NtQueryObject(
HANDLE ObjectHandle,
OBJECT_INFORMATION_CLASS ObjectInformationClass,
PVOID ObjectInformation,
ULONG Length,
PULONG ResultLength
)
这个API返回一个OBJECT_ALL_INFORMATION结构,其中NumberOfObjectsTypes成员为所有的对象类型在ObjectTypeInformation数组中的计数:
typedef struct _OBJECT_ALL_INFORMATION{
ULONG NumberOfObjectsTypes;
OBJECT_TYPE_INFORMATION ObjectTypeInformation[1];
}
检测例程将遍历拥有如下结构的ObjectTypeInformation数组:
typedef struct _OBJECT_TYPE_INFORMATION{
[00]UNICODE_STRING TypeName;
[08]ULONG TotalNumberofHandles;
[0C]ULONG TotalNumberofObjects;
…more fields…
}
TypeName成员与UNICODE字符串"DebugObject"比较,然后检查TotalNumberofObjects或TotalNumberofHandles是否为非0值。
探索分析二:
与NtQueryInformationProcess()解决方法类似,在NtQueryObject()返回处设断点,然后补丁返回的OBJECT_ALL_INFORMATION结构,另外NumberOfObjectsTypes成员可以置为0以防止壳遍历ObjectTypeInformation数组。可以通过创建一个类似于NtQueryInformationProcess()解决方法的ollyscript脚本来执行这个操作。
类似地,Olly Advanced插件向NtQueryObject() API中注入代码,如果检索的是ObjectAllTypeInformation类型则用0清空整个返回的缓冲区。
通过分析上述代码,清楚的看到清空整个返回的缓冲区,就可以得到我们探索的效果。所以任何一个程序的加密与压缩有很多种方法。诸如注入垃圾代码和代码变形是一种用来考验耐心和浪费逆向分析人员的时间的方式。因此,重要的是了解这些混淆技术背后隐藏的指令是否值得去理解。
最终满足程序合理、快捷运行的前提下,以最科学的方法减缓逆向分析人员对程序的分析。用最安全的加密方法保护程序,这才是根本地目的。
参考文献:
[1]Kris KaspersKY著,谭金明译.黑客反汇编揭秘[M].北京:电子工业出版社,2004.
[2]Kris KaspersKY著,周长发译.黑客调试技术揭秘[M].北京:电子工业出版社,2006.
[3]谭文,邵坚磊.天书夜读从汇编语言到Window内核编程[M].北京:电子工业出版社,2008.