|
前面这两行代码对NTDLL中的每个导出函数都执行一次。这两行代码把这块分配好的内存当作一个数组来对待。第一行代码写的是当前函数的校验和;第二行代码写的是导出函数的RVA(相对虚拟地址,Relative Virtual Address),写入的地址为第一行写入地址加4。这表明,新分配的内存空间被当作一个数据结构数组,每个数组项有8字节长(即该数据结构的长度)。偏移地址+0处存放的是函数名的校验和,而偏移地址+4处存放的是这个函数的RVA。 下面又是一段比较有趣的代码:
这段代码执行内存拷贝,是汇编语言中很常见的“语句”。“REP MOVS”指令反复地从ESI指向的地址中拷贝DWORDs(双字)到EDI指向的内存地址,直到ECX的值减到0为止。每拷贝一个DWORD,ECX就自动减1,而ESI和EDI都自动增加4(代码每次拷贝32位的数据)。第二个“REP MOVS”指令执行的是对最后三个字节的逐字节拷贝操作(如果需要的话)。只有当数据没有32位对齐的时候才需要使用这个指令。 我们来看看在这段代码中拷贝的是什么内容。ESI中先加载了[EBP+8]中的数据(即NTDLL的基地址),然后又加了[EAX+2C]中的值。我们稍微回过头看看前边的代码,你可以看到EAX中的内容是前边一直在找的同一个PE头部地址。如果你再回过头去看看前面用WinDbg转储的PE头部,你会发现偏移地址+2C处是BaseOfCode。EDI寄存器中加载的是新分配的内存块中的地址,正好就在刚才我们已经搞定的那张存放校验和以及函数RVA的表(即在指导组)后面。实质上,这段代码是将NTDLL中的全部代码拷贝到这个内存缓冲区中。 好,我们来总结一下我们目前所掌握的情况。你知道有一块在运行时分配的内存,代码中还试着借助时间戳计数器来使这块内存分配到一个随机的内存地址上。这段代码中还包含一张表(即数组),表内记录的是从NTDLL的所有导出函数名的校验和以及这些函数的RVA。就在这张表的后面(就在这个内存块中)是整个NTDLL代码段的一份儿拷贝。图11.15用图示的方法给出了这个有趣而又不同寻常的数据结构。 不过,如果我是在一个普通的应用程序里看到这样的代码的话,我可能会想,我看的代码一定是一个疯子科学家的“杰作”。但在重要拷贝保护中这样的代码却非常有用。这是一段将内存块分配到随机虚拟地址并创建一个与操作系统模块“迷惑”接口的机制。你很快就能看到这个接口在干扰逆向工作方面是多么的高效(这大概就是这里使用这样代码的唯一原因吧)。 这个庞大的函数接下来调用了另一个函数(在4030E5处)。这个函数是从两个有趣的循环开始的,其中一个是:
这个循环搜索了导出表,并将每一个字符串(即函数名)的校验和与190BC2进行比较。我们很容易就能看出来这里发生了什么——代码正在NTDLL中查找一个特定的API函数。因为代码并没有通过字符串而是通过校验和进行搜索,所以我们目前还不知道代码要找的是哪一个API函数——这个API函数名正好是我们得不到的。代码在找到要找的API函数的时候执行的代码如下:
图11.15 Defender的NTDLL内存拷贝的布局 上面的代码读取了找到的函数项中偏移地址+4处(还记得吧,偏移地址+4处是函数的RVA)的数据,并将它加上拷贝NTDLL代码段的地址。稍后在这个函数中调用了在这个地址处的函数。勿庸置疑,这是在调用NTDLL API函数的拷贝版本。你可以在这个地址处看到下面这几行代码:
这段代码调用了7FFE0300处的代码,实质上这是对NTDLL API函数KiFastSystemCall的调用,而KiFastSystemCall正是我们调用系统内核功能的通用接口。需要指出的是,我之所以可以肯定是这个函数,是因为尽管Defender拷贝了NTDLL的整个代码段,代码还是通过地址显式地引用了这个函数。下面是KiFastSystemCall的代码——只有两行: 实际上,KiFastSystemCall所做的就是调用SYSENTER指令。SYSENTER指令执行的是内核模式切换,这表明该程序执行了一个系统调用。需要说明的是,这在Windows 2000或更早版本的Windows上会略有不同,因为Microsoft在Windows 2000之后改变了其系统调用机制(在Windows 2000及更早版本的Windows中使用INT 2E指令进行系统调用)。Windows XP和Windows Server 2003操作系统都使用了这种新的系统调用机制,更不用说更新版本的操作系统了,比如说目前被称为Longhorn的操作系统。如果你是在老版本的操作系统中进行调试,你所看到的肯定和这里给出的略微有一些区别。 你现在遇到了一点麻烦。因为你使用的是用户模式的调试程序,所以显然无法跟踪到SYSENTER中去。这就意味着我们很难确定程序试图完成的是哪一个系统调用。你现在有下面几种选择: 如果你有内核模式的调试器的话,切换过去,然后跟踪到这个系统调用中弄明白Defender在做什么。 回到前面的校验和/RVA表,找出当前这个系统调用的RVA——表中的RVA很可能与NTDLL.DLL导出目录中的RVA是一样的。然后你就可以用DUMPBIN处理NTDLL,进而确定出你要找的那个API函数。 通过在导出列表中的顺序找到是哪一个系统调用。显然,校验和/RVA表中导出函数的顺序和原来NTDLL导出目录中的顺序是相同的。只要知道了该系统调用的索引,你就可以通过查看NTDLL导出目录来确定这是哪一个系统调用。 在这里,我认为最好的选择是切换到内核模式的调试器。我在这里用的是NuMega公司的SoftICE,因为它非常易于安装,而且只需要一台计算机就够了。如果你手头没有SoftICE,而且因为硬件限制不能安装WinDbg,我建议你试试其他两种方案。可能使用函数的RVA是最容易的方案。无论如何,只要你做逆向不是为了玩玩而已的话,我还是建议你安装一个内核模式的调试程序——因为有些逆向工作在没有内核模式调试器的情况下是无法完成的。 有了内核模式调试器,我们就在SoftICE中跟入SYSENTER,我们到了NTOSKRNL中的KiFastCallEntry函数。接着又直接进入了KiSystemService,KiSystemService是一个通用的Windows系统调用调度程序(dispatcher)——所有的系统调用都要通过它。快速地执行这个函数中大部分代码,直到在函数接近尾部的地方看到“CALL EBX”指令为止。就是这条“CALL EBX”指令将控制交给了所调用的特定系统服务。此时,我们继续跟踪到这个函数中,发现程序又调用了一次NtAllocateVirtualMemory!按几下F12切换回用户模式,进入Defender的下一个调用。这是另外一个API调用,这个调用也是通过那个怪异的拷贝NTDLL接口实现的。这次,Defender调用的是NtCreateThread。现在你可以忽略这个新线程,继续跟踪我们正在跟踪的这个函数。该函数在创建这个新线程后就立即返回了。 在这个线程创建函数的调用之后的代码又一次对校验和表进行迭代,但这次它要找的是校验和006DEF20。之后,又立即从NTDLL的拷贝中调用了另外一个函数。你可以学着前边的方法跟踪这个函数,你会发现这次调用的是NtDelayExecution函数。如果你不了解NtDelayExecution,我这里简单解释一下:NtDelayExecution是一个本地API,其功能相当于Win32 API中的SleepEx。SleepEx的作用是让线程在所请求的时间段内放弃CPU。在我们这里,NtDelayExecution是在线程创建后立即被调用,看上去Defender好像是想要让新创建的线程立刻就开始运行。 NtDelayExecution函数一返回,Defender就立即调用另外一个在403A41处的(内部)函数。这个地址很有趣,因为这个函数的代码大约是从调用它的地方(4039FA)之后30个字节的地方开始的,而且更重要的是,从这个CALL指令到函数开始处之间的30个字节中SoftICE竟然没有找到任何合法的指令。所以,这里Defender很可能是跳过这个函数中间的一个数据块!事实上,转储4039FA,紧接着这个CALL指令后的这块地址空间存放的内容是: 好了,看上去这个函数中间存放的数据是Unicode字符串“KERNEL32.DLL”。显然这个CALL指令所做的一切就是为跳过这个字符串,以确保CPU不会试着“执行”它。这个字符串之后的代码又一次搜索了我们的校验和/RVA表,这次要找的是两个值:6DEF20和1974C。你可能还记得6DEF20是NtDelayExecution的函数名校验和吧。不过,我们还不能确定1974C代表的是哪一个API——但我们很快就可以弄清楚了。
[]
[返回上一页]
[打 印]
|
|
|