一、关键的线程检查
在对抗外挂和木马的方案中,不可能将所有的检查操作放在主线程中,因此,在方案中总有一个扫描线程或者环境检查线程必须保持工作,而它们也就成了外挂和木马的重要攻击目标,外挂和木马只要搞定了它们,就能很方便地在客户端肆虐。
在客户端反木马方案SafeLogin和TEdit中,就经历过很长一段时间线程保护的对抗,通过了解客户端反外挂系统以及壳和Sanlix中的线程保护,现在在对抗线程保护方面,木马的对抗手段相对外挂跑在了前面,这和木马本身的特性关系很大,如果不搞定扫描线程和环境检查线程,那么它们在特征扫描、键盘钩子和窗口行为等检查过程下将无法遁形。
二、外挂和木马常用的攻击手段
外挂和木马对线程的攻击不外乎这样的几种手段:
1)最简单的,篡改线程函数代码,很容易被通过特征检查扫描到;
2)简单阻止线程创建,通过hook线程创建相关函数,也很容易被发现;
3)这种办法相对隐蔽,通过hook ntdll!CsrNewThread函数,替换线程函数地址,这样绕过了壳中通过GetExitCodeThread和ResumeThread的检查方法;
4)更为隐蔽,通过hook kernel32!Sleep, 当线程内调用Sleep的时候,通过篡改参数,让这个Sleep永远进行下去,不过在SafeLogin的根据数据的线程检查方法可以发现它;
5)非常隐蔽,通过hook ntdll!ZwDelayExecution, 并不是让线程永远Sleep下去(Sleep函数最终会调用这个native API),而是让线程Sleep的时间更长一些,而在这个间隙,木马一则掌握了检查线程的活动规律(相当于一个潜入者掌握了流动岗哨的活动规律)二则可以让检查线程的循环执行缓慢(相当于一个潜入者控制流动岗哨巡查变慢),为自己做坏事争取了时间。
其实这些手段也是我们在和木马对抗的过程中,木马不断进化出来的,尤其最后一种,其实在实际中,木马对ntdll!ZwDelayExecution的hook也不是长时间存在的,而是hook一下,如果拦截到了,则拦截,如果没拦截到,则赶紧恢复,以避开扫描线程的hook检查,整个过程更加隐蔽。
三、结合线程上下文切换和数据变化检测的方法
熟悉了外挂和木马的对抗手段,我们可以看出,如果仅对线程的代码,或者线程句柄进行检查,都将是不可靠的,我们需要有一种办法,让线程活动即对线程检查数据进行更新(对抗木马的手段4和5),如果发现线程长时间不活动,或者线程活动但是每没有更新线程检查数据,则可以判断这个线程有问题了。
简单介绍了背景,多余的内容不再赘述,直接描述方法。
我们知道在Windows系统的内核,会对所有的线程进行调度执行,当一个线程被切换到时,就会把它的上下文信息换入到CPU寄存器等环境中,而这时,内核中有一线程上下文切换次数(thread context switch count)会被自增1, 而一个线程在两个时刻这个数据的差值(下文简称delta值),即thread context switch delta, 反映了它在这两个时刻之间被切换的次数。
在sysinternals的process explorer工具中,对这个数据进行的显示,并可以看到它在不断更新。
通过对process explorer的研究,它以5号功能调用了系统函数NtQuerySystemInformation, 通过查阅第三方文档,线程切换记数存在与这个结构中(见图1),通过以5号功能调用NtQuerySystemInformation即可获取它(见图2)
图一
图二
通过检查线程在两个时刻的delta值,结合对线程活动更新数据的检查(见图3),就能有效发现木马手段4和5。另外,在检查过程中,如果通过delta值和更新数据都没有发现线程的活动,是否是线程始终没有被系统调度呢,通过Sleep或者SwitchToThread的方法,就可以主动触发线程调度(见图4),如果被检查的线程被“变慢”了,那么主线程也会跟着罢工。这样,一个比较可靠的线程检查方法就实现了。
图三
图四
四、小结
这种检查方法也是在对抗过程中进化出来的,当然现在它很可能继续被对抗,总之,在线程的对抗、防御和检查中,这样的思路是贯穿始终的,也就是对线程的代码、活动性同时进行多线程交叉检查,让一个线程不正常工作,其他线程全罢工。
作为积累和分享,将这种其实很简单的办法描述了一下,后续的对抗中,如果有更好的办法,也会接着分享出来。