11.4.4 逆向分析第二个线程
让我们回到初始化子程序中的线程创建代码部分,来弄清楚这个线程究竟执行了什么样的代码。在动手之前,你必须先学习一些有关NTCreateThread函数的工作原理。不同于与之相当的Win32 API函数CreateThread,NTCreateThread是一个更加底层的函数。CreateThread函数接收了一个lpStartAddress参数,而NTCreateThread接收的是一个CONTEXT数据结构,CONTEXT数据结构准确地定义了该线程在第一次开始运行时的状态。
CONTEXT数据结构包含了所有的线程状态信息,其中包括了所有CPU寄存器的内容,当然也包括指令指针寄存器(instruction pointer。译注:即EIP寄存器,它指向当前执行的指令)。要告诉新创建的线程做什么,Defender需要初始化CONTEXT数据结构,并将EIP成员指向线程的入口点地址。除了指令指针外,Defender还必须亲自为线程分配堆栈空间,并在CONTEXT数据结构中将ESP寄存器设置指向为线程新创建的堆栈空间的起始地址(这恰好解释了为什么在调用NtCreateThread之前会有一个NtAllocateVirtualMemory函数的调用)。这一大段代码就是要让你知道通过调用Win32的CreateThread API函数来创建线程可以省多少事儿。
在这个线程创建的过程中,你需要在代码中找到Defender设置CONTEXT中EIP成员的地方。查看一下NtCreateThread的原型定义,你可以看到CONTEXT数据结构是作为第六个参数传给NtCreateThread函数的。这个函数把地址[EBP-310]作为第六个参数,所以我们可以确定地址[EBP-310]就是CONTEXT数据结构的起始地址。通过观察CONTEXT在WinDbg中的定义,你可以看到EIP成员位于偏移地址+b8处。这样,你就知道线程子程序应该是被拷贝到了[EBP-258](310 - b8=258,这些都是十六进制数)。下面这条指令似乎就是你要找的东西了:
查看一下地址402EEF处,你可以看到它里面存放的确实是代码,这一定是我们要找的线程子程序。快速浏览一遍这个函数便可知道这个函数的序言(prologue)和我们前面研究过的列表11.7中的函数是一样的,这表明这个函数也被加密了。让我们重新运行这个程序,并在这个函数上设置一个断点(这次不需要使用内核模式调试器)。最适合设置断点的位置是402FF4处,这里正好就在解密程序(decrypter)开始执行解密代码的之前(译注:代码在这里刚好已经解密出来了,CPU也就可以执行它们了)。当程序执行到这里,你就可以看到解密后的线程程序代码了。这段代码很有意思,所以我把全部代码都列出来了,参见列表11.8。
表11.8 Defender中
地址00402FFE处的函数的反汇编
代码这是一个很有意思的函数,它看上去是在运行一个无限次的循环(注意从4030C2到地址402FFE的JMP指令,以及地址00403001处的代码是怎样将EAX置为1然后又测试它的值是否为0的)。这个函数从一个RDTSC指令开始,并将时间戳计数器存放在[EBP-8]中(译注:时间戳计数器的长度是8字节)。代码接着搜索了你已经很熟悉的拷贝版本的NTDLL表,还是为了查找那个倍受关注的6DEF20——现在你已经知道它就是NtDelayExecution函数了。这个函数调用NtDelayExecution时,传给NtDelayExecution的第二个参数是一个指向8个字节的内存区域的指针,而且这8个字节中的内容全为0。这一点很重要,因为NtDelayExecution的第二个参数是延迟间隔时间(一个64位的值)。将其设置为0意味着调用NtDelayExecution是要这个线程放弃CPU的使用权。只有所有的其他线程放弃CPU的使用权或者用完了分配给它们的CPU使用时间后,这个线程才能继续运行。
在NtDelayExecution返回之后,函数又一次调用了RDTSC。这次,RDTSC的输出存放在了[EBP-18]处。接下来在00403063处是一段完成64位的减法运算的代码。首先,用SUB完成低32位字相减,接下来用SBB(subtract with borrow,带借位的减法)完成带借位的32位字相减。SBB指令的作用是对两个整数做减法,并把进位标记位(carry flag,CF)作为借位指示符(borrow indicator)参与减法运算(以防止漏掉前面减法产生的借位)。更多关于64位算术运算的信息请参考附录B中“64位算术运算”一节。
减法所得的结果与77359400进行了比较。如果结果小于77359400,这个函数就跳回开头部分重新执行(译者:注意是通过连续的两个跳转指令跳回去的),如果大于或等于(或者如果SBB指令产生了一个非0值,说明高32位部分发生了改变),这个函数就往下执行,查找另一个导出函数——这次要找的导出函数的字符串校验和是1BF08AE,找到后就调用这个API函数。这时候我们还不能确定这是哪个API函数,但是如果你具有一定的洞察力你应该跳过这个API函数。结果表明当你单步调试了这个API的话,上面的测试几乎总是不成立(实际上成不成立完全取决于你的CPU有多快以及你是以多快的速度单步执行这段代码的)。当你执行到这个API调用时,在SoftICE中跟踪进入这个API,你可以看到程序调用的实际上是NtTerminateProcess。
到这里,你应该逐渐理解我们这个线程是干什么的了吧。实质上,这个线程是一个定时监视器(timing monitor),其目的是检测这个程序进程是否被“暂停过”,如果是的话就直接当场终止掉进程。为此,Defender利用两条RDTSC指令来测试中间所用的时间是否满足一个合理的时长。如果两次调用RDTSC的间隔时间过长(这里“过长”指的是77359400个时钟周期,亦即十进制的20亿个时钟周期),就直接调用内核函数NtTerminateProcess终止进程。