11.4.6 加载KERNEL32.DLL
你可能还记得,在使用这个迂回的办法处理RDTSC线程之前,你曾经看到在代码的中间位置有一个“KERNEL32.DLL”字符串。我们来弄清楚程序是怎样处理这个字符串。
紧接着这个字符串出现的位置之后的代码是在搜索指向两个NTDLL函数的指针,其中一个的函数名校验和是1974C,另一个是我们熟悉的6DEF20(NtDelayExecution的校验和)。代码首先调用NtDelayExecution,然后调用校验和为1974C的函数。通过在SoftICE中跟踪第二个函数,你看到的是一幅更加令人费解的景像。这个API并不是像前几个API那样直接去调用内核功能,而它似乎是在NTDLL内部实现的一个函数,也就是说,它现在是在拷贝过来的代码内实现的。这样一来,我们要确定它是哪一个API函数就更困难了。
实际上你可以采取我们前边讨论中提到的一种方法来在这个混乱的接口中确定所调用的API。具体思路是:在初始化校验和/RVA表时,这些API函数拷入表中的顺序是从NTDLL的导出目录中读出这些函数的顺序。你现在所能做的就:一有API函数被通过校验和查找的方法找到,你就要确定其在校验和/RVA表中的编号(entry number)。这个编号应当也是NTDLL的导出目录中的合法索引,这就有望为我们准确地揭示出正在处理的函数是哪一个API。
为此,你必须在Defender查找这个API函数的代码之后放置一个断点(这是在表中查找1974C。译注:原书的这一句话中出现了两处笔误,“放置”一词写成了but,1974C写成了1973C)。当代码执行到这个断点时,你就用指向当前项的指针减去指向表开始位置的指针,再将结果除以8(每一项的长度为8个字节),这样你就得到了这个API在表中的索引。现在,你可以使用DUMPBIN或类似的工具转储NTDLL的导出表,并查找与这个索引对应的API函数。此时,你得到的索引是0x3E(比方说在我做这个工作的时候,这个表是从53830000开始的,而当前项的地址是538301F0,但是我已经告诉过你了,这些都是随机选取的地址——译注:指的是整个表的位置在内存中是随机的,但表内各个项之间的相对位置关系是确定的)。用DUMPBIN快速浏览一下NTDLL.DLL的导出列表,你就会找到答案。
哦,此处调用的API是LdrLoadDll,它等价于LoadLibrary函数的本地API。现在你已经知道加载的是哪个DLL了,因为你在前边已经看到它的名字了:KERNEL32.DLL。
在加载了KERNEL32.DLL后,Defender又执行了一遍我们熟悉的那段代码——在内存中一个随机的地址上分配了一块内存,并在这块内存中为KERNEL32.DLL的所有导出函数生成了函数名校验和/RVA表。在拷贝好的模块可以使用了之后,这个函数又调用了一次NtDelayExecution,然后你会看到另外一个滑稽的跳转,这个跳转跳过了大约30个字节。转储紧接着这个CALL指令之后的内存,你会看到它们的文本,如下所示:
终于看到一些熟悉的东西了,这不就是Defender的欢迎消息吗!显然,Defender是想要把它显示出来。这条CALL指令跳过了这个字符串,把我们带到了下面的这段代码:
这段代码取出CALL指令压入的“返回地址(return address)”后,又把这个地址压入了堆栈(尽管这个地址已经在堆栈中了),并调用一个函数。你甚至都不需要阅读函数的代码(毫无疑问,这个函数的代码全是对拷贝的KERNEL32.DLL代码的间接调用)就可以猜出这个函数将会输出你刚才压入堆栈的那个欢迎消息。我们就直接执行过这个函数,Defender毫无悬念地输出了那个欢迎消息。