首 页文章中心黑客软件黑客动画绿色软件私服技术私服下载本站论坛
您当前的位置:黑客之 家文章中心脱壳破解软件破解 → 文章内容 退出登录 用户管理
本类热门文章
相关文章
站内广告


这个函数接着又进入另一个对NTDLL导出列表的迭代,再一次为每个函数名计算了那个奇怪的校验和。在这个循环中,有两行往新分配的内存块中写入数据的代码比较有趣:

11.4.2 分析解密后的代码(图)
作者:佚名  来源:不详  发布时间:2008-9-25 2:16:33

减小字体 增大字体

11.4.2  分析解密后的代码

这个函数开始的时候对你在前面获得的NTDLL基地址执行了某个指针操作。这个函数在NTDLL的PE头部寻找到NTDLL的导出目录(OllyDbg会告诉你这一点,因为如果函数获得指向导出目录的指针,Olly就会将函数注释为ntdll.$VProc_ ImageExportDirectory)。接下来函数会扫描各个导出函数,并对函数名字符串上执行一个有趣的(也很少会见到的)算术运算。看我们来看一下完成这部分工作的代码

 

 

在调试器中很容易看出[EBP-68]中存放的是当前字符串的长度(在前面计算好的),而[EBP-64]中存放是当前字符串起始地址。然后函数进入一个循环,这个循环依次读取字符串内的每一个字符并将它左移——左移的位数等于[EBP-68]对24求模([EBP-68]中存放的是该字符在字符串中的位置),然后将结果加到[EBP-6C]处的累加器中。这样会得到一个32位的数,这个数好像是该字符串的校验和。至于此处为什么需要这个校验和我们还不清楚。在字符串中所有的字符都处理完后,将会执行下面的代码

 

如果[EBP-6C]不等于39DBA17A,这个函数就接着计算下一个NTDLL导出项(export entry)的校验和;如果相等的话,则终止循环。也就是说其中有一个NTDLL导出项会得到39DBA17A的校验和。你可以在代码中紧跟着JNZ之后的那条指令(在地址004035D9处)上设置一个断点,并让程序开始运行。这样我们就能确定出程序要找的是哪一个函数了。当执行到Olly的这个中断时,就可以到[EBP-64]处查看当前加载的是哪个函数名了——是NtAllocateVirtualMemory。所以,我们可以推测这个函数感兴趣的是NtAllocateVirtualMemory。NtAllocateVirtualMemory这个本地API等价于VirtualAlloc。函数VirtualAlloc是用于分配内存页(memory pages)的公开的(有文档可查)Win32 API。

计算出NtAllocateVirtualMemory的准确地址(存放在[EBP-10]中)之后,这个函数接着就调用了这个API函数。下面列出了调用代码序列:

 

注意这段代码一开始的指令RDTSC,这是一个不太常用的指令,以前我们没有遇到过。通过参考Intel指令集参考手册[Intel2, Intel3],我们知道RDTSC执行一个读出时间戳计数器(Read Time-Stamp Counter)的操作。时间戳计数器是一个高速的64位计数器,其值每个时钟周期增加1。也就是说,在3.4GHz的系统中,这个计数器大约每秒钟增加34亿次。RDTSC将计数器加载到EDX:EAX,EDX存放计数器的高32位,而EAX存放计数器的低32位。Defender从EAX中取出低32位,并与7FFF000进行按位与(AND)操作。然后,将计算结果(实际上是指向该计算结果的指针)作为调用NtAllocateVirtualMemory函数的第二个参数传过去。

为什么Defender要把时间戳计数器的低32位部分作为参数传递给NtAllocateVirtualMemory函数呢?让我们看一下NtAllocateVirtualMemory的函数原型,来搞清楚系统要求第二个参数是什么。下面给出的NtAllocateVirtualMemory函数原型是我从网站http://undocumented.ntinternals.net找到的,这是网站有关未公开Windows API函数的非常好的资源。当然,有关本地API函数最权威的著作是Gary Nebbett所写的Windows NT/2000 Native API Reference[Nebbett]。

 

看上去第二个参数好像是一个指向基地址的指针。参数前面的“IN OUT”表明NtAllocateVirtualMemory函数会读出BaseAddress中的值,然后再对它进行写操作。这个函数完成的工作是试着在指定的地址分配内存,然后把实际分配的内存块地址写入BaseAddress。所以,Defender实际上就是把时间戳计数器当作它所希望的分配内存空间的起始地址了……你可能感觉有些奇怪,但实际上一点儿也不奇怪——这个程序所做的就是在内存中随机找一个地方来分配内存。时间戳计数器是一种获得某个级别随机值(译注:其随机程度待定)的好方法

这个调用另一个有趣的地方是它的第四个参数,即请求分配的内存块大小。Defender是从[EBP-4]中取得这个块大小参数的。我们再回到代码中,你会发现下面这段代码,这段代码看上去好像参与了生成块大小的计算:

 

 

这段代码首先从[EBP+8]处获取NTDLL基地址,继而访问了PE的头部。然后,将指向PE头部的指针存储到 [EBP-74]中,并访问PE头中偏移地址+1C处。因为PE头部是由好几个数据结构组成,所以弄清楚某个偏移地址处存放的是什么有一定的难度。WinDbg中的DT命令可以很好地解决这个问题

 

很显然,偏移地址+1处是OptionalHeader数据结构的一部分,因为OptionalHeader是从偏移地址+18处开始的,显然偏移地址+1C对应到OptionalHeader数据结构中的偏移地址是+4,偏移量+4处存放的是SizeofCode。这里还有另一小段代码好像与需要分配的内存空间大小的计算有关系:

 

在这段代码中,Defender从[EBP-7C]处获取一个指针,并从这个指针的偏移地址+18处读取数据。如果你看一下0040363D处的指令读到EAX寄存器中的值,你就会明白它指向NTDLL头部的某个地方(具体的值很可能会随着每次更新操作系统而改变)。使用DUMPBIN快速地看一下NTDLL头部,你会发现EAX中的地址就是NTDLL导出目录的起始位置。找到IMAGE_EXPORT_DIRECTORY数据结构的定义,你会发现偏移地址+18处是NumberOfFunctions成员。下面给出的代码是计算内存块大小的最后几个操作

 

计算所需分配的内存块大小的完整公式为:Block-Size = NTDLLCodeSize + (TotalExports + 1) * 8。虽然我们还是看不出这里Defender在做什么,但可以确认的是这与NTDLL的代码段及其导出目录有关。

 

前面这两行代码对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——但我们很快就可以弄清楚了。

上一页 1 2 

[] [返回上一页] [打 印]
关于本站 - 网站帮助 - 广告合作 - 下载声明 - 友情连接 - 网站地图 - 文章投稿 - 软件发布 - 购物资讯网 - _