1、不知道诸位看官是否有过这样的经历:在不经意之间发现一个 DLL 文件,它里边有不少有趣的导出函数但是由于你不知道如何调用这些函数,所以只能大发感慨而又无能为力焉。固然有些知名的 DLL 可以直接通过搜索引擎来找到它的使用方式(比如本文中的例子 ipsearcher.dll) ,不过我们诚然不能希望自己总能交到这样的好运。所以在本文中,李马希望通过自己文理不甚通达的讲解能够给大家以授人以渔的效果。不知道诸位看官是否有过这样的经历:在不经意之间发现一个 DLL 文件,它里边有不少有趣的导出函数但是由于你不知道如何调用这些函数,所以只能大发感慨而又无能为力焉。固然有些知名的 DLL 可以直接通过搜索
2、引擎来找到它的使用方式(比如本文中的例子 ipsearcher.dll) ,不过我们诚然不能希望自己总能交到这样的好运。所以在本文中,李马希望通过自己文理不甚通达的讲解能够给大家以授人以渔的效果。先决条件阅读本文,你需要具备以下先决条件: 初步了解汇编语言,虽然你并不一定需要去读懂 DLL 中导出函数的汇编代码,但是你至少应该了解诸如 push、mov 这些常用的汇编指令。 一个能够查看 DLL 中导出函数的工具, Visual Studio 中自带的 Dependency Walker 就足够胜任了,当然你也可以选择 eXeScope。 一个调试器。理论上讲 VC 也可以完成调试的工作,但它
3、毕竟是更加针对于源代码一级调试的工具,所以你最好选择一个专用的汇编调试器。在本文中我用的是 OllyDbg我不会介绍有关这个调试工具的任何东西,而只是简要介绍我的调试过程。 准备好了吗?那么我们做一个热身运动吧先。热身函数调用约定这里要详细介绍的是有关函数调用约定的内容,如果你已经了解了这方面的内容,可以跳过本节。你可能在学习 Windows 程序设计的时候早已接触过“函数调用约定”这个词汇了,那个时候你所了解的内容可能是一个笼统的概念,内容大抵是说函数调用约定就是指的函数参数进栈顺序以及堆栈修正方式。譬如 cdecl 调用约定是函数参数自右而左进栈,由调用者修复堆栈;stdcall 调用约定
4、亦是函数参数自右而左进栈,但是由被调用者修复堆栈噢不,这太晦涩了在源代码上我们是无法看到这些东西的!那么我们别无选择,只有深入到汇编一层了。考虑以下 C+代码:#include int _cdecl max1( int a, int b )return a b ? a : b;int _stdcall max2( int a, int b )return a b ? a : b;int main()printf( “max( 1, 2 ) of cdecl version: %dn“, max1( 1, 2 ) );printf( “max( 1, 2 ) of stdcall version
5、: %dn“, max2( 1, 2 ) );return 0;对应的汇编代码为:; int _cdecl max1( int a, int b )00401000 MOV EAX,DWORD PTR SS:ESP+400401004 MOV ECX,DWORD PTR SS:ESP+800401008 CMP EAX,ECX0040100A JG SHORT CppTest.0040100E0040100C MOV EAX,ECX0040100E RETN; int _stdcall max2( int a, int b )00401010 MOV EAX,DWORD PTR SS:ESP+
6、400401014 MOV ECX,DWORD PTR SS:ESP+800401018 CMP EAX,ECX0040101A JG SHORT CppTest.0040101E0040101C MOV EAX,ECX0040101E RETN 8 ; 被调用者的堆栈修正; max1( 1, 2 )00401030 PUSH 200401032 PUSH 100401034 CALL CppTest.0040100000401039 ADD ESP,8 ; 调用者的堆栈修正; max2( 1, 2 )0040104A PUSH 20040104C PUSH 10040104E CALL Cp
7、pTest.00401010好了,我来简要介绍一下。函数参数传入函数体是借由堆栈段完成的,也就是将各个参数依某种次序推入SS 中在 cdecl 与 stdcall 约定中,这个次序都是自右而左的。另外,由于将参数推入了堆栈致使堆栈指针 ESP 发生了变化,所以要在函数结束的时候重新修正 ESP。从上边的汇编代码中你也可以很清楚地看到,cdecl 约定是在调用 max1 之后修正的 ESP,而 stdcall 约定则是在 max2 返回时借由 RETN 8 完成了这个修正工作。另外,从上边的汇编代码中还可以看到,函数的返回值是由 EAX 带回的。庖丁解牛在了解了以上的知识后,我们就可以使用调试器
8、来调试那个未知的 DLL 了。可以说,这整个的调试过程充满了惊险和刺激,而且我们还需要一定的技巧如果你像我一样不喜欢阅读汇编代码的话。在本文中,我所选择的调试示例是 FTerm 中附带的 ipsearcher.dll,它提供了对纯真 IP 数据库的查询接口。下图是用 Dependency Walker 对其分析的结果:你可以看到,这里边有两个导出函数:LookupAddress 和_GetAddress,那么我们可以按照返回值、调用约定、函数名、参数列表的顺序将它们声明如下:? ? LookupAddress( ? );? ? _GetAddress( ? );是的,有太多的未知,下面李马将要
9、逐一地破解这些问号。调试器不可能孤立地对 DLL 进行调试,我们所需要的应该是一个合适的 EXE,这样有助于我们的探究工作。在这里我选择的 EXE 是我编写的 ipsearcher.exe,当然这可能会让你认为我这篇文章的组织顺序有问题毕竟是我已经知道了这两个导出函数之后(编写了 ipsearcher.exe)还要假装成不知道的样子来对 ipsearcher.dll 来进行探究,所以我决定在下文中不对 ipsearcher.exe 的代码进行任何关注,而是直接进入到 ipsearcher.dll 的领空。打开调试器,载入 ipsearcher.exe。当 ipsearcher.dll 被装载后
10、,会引发一个访问异常,可以忽略这个异常继续调试。根据 Dependency Walker 的分析结果,在 ipsearcher.dll 的 0x00001BB0 和0x00001C40 处各下一个断点。现在在“IP 地址”中输入一个 IP 地址(这里以 寒泉 BBS 的 IP 为例) ,点击“ 查询”,会发现指令跳入 0x00001C40 中(也就是_GetAddress) ,它的代码如下:10001C40 MOV EAX,DWORD PTR SS:ESP+4 ; 一个参数10001C44 PUSH ipsear_1.10009BE810001C49 PUSH EAX10001C4A CALL
11、 ipsear_1.LookupAddress ; 两个参数10001C4F ADD ESP,8 ; LookupAddress 是 cdecl 调用约定10001C52 MOV EAX,ipsear_1.10009BE810001C57 RETN ; _GetAddress 这厮也是 cdecl 调用约定很短的几行代码,不过它已经可以提供这些信息了: 从 SS 的使用来看,_GetAddress 只带有一个参数。 _GetAddress 中调用了 LookupAddress,后者带有两个参数。 调用 LookupAddress 之后进行了堆栈修正,所以 LookupAddress 是 cde
12、cl 调用约定。 _GetAddress 返回时并未进行堆栈修正,所以_GetAddress 也是 cdecl 调用约定。 于是,我们可以替换一下刚才的问号了:? CDECL LookupAddress( ?, ? );? CDECL _GetAddress( ? );下面可以进行单步调试了,当代码步至 10001C44 时,你会发现寄存器窗口发生了如下的变化:“202.207.177.9”终于出现了,这样一来我们可以继续对问号进行替换了:? CDECL LookupAddress( PCSTR, ? );? CDECL _GetAddress( PCSTR );现在继续对代码进行跟踪,是进入
13、 LookupAddress 的时候了。我们可以从先前_GetAddress 的代码中可以发现,这两个导出函数一直在围绕 10009BE8 这个地址做文章,那么我们就要在单步调试LookupAddress 的同时关注这个地址的数据改变。几步跟踪之后,你会发现 10009BE8 开头的 8 字节(两个 DWORD)数据发生了改变,变成了 10009AB4 和 10009B1C。那么我们再转向这两个地址,会发现:这样一来就很清楚了,10009BE8 是一个字符串指针的数组,它有两个元素。也就是说,我们的函数声明可以换成这样:? CDECL LookupAddress( PCSTR, PSTR* )
14、;PSTR* CDECL _GetAddress( PCSTR );接下来需要确定的就是 LookupAddress 的返回值了。纵观 LookupAddress 的返回代码,你会发现这样的片断:; 片断 110001C0B XOR EAX,EAX10001C0D POP ESI10001C0E RETN; 片断 210001C2B MOV EAX,110001C30 POP ESI10001C31 RETN也就是说,这个函数有两个返回值:0 或 1。那么最后的真相终于大白于天下BOOL CDECL LookupAddress( PCSTR, PSTR* );PSTR* CDECL _GetA
15、ddress( PCSTR );GetProcAddress?到此为止,这两个函数的声明终于让我们找出来了。也许你会觉得这就够了接下来就是用 typedef定义函数指针,然后使用 LoadLibrary、GetProcAddress 调用这些函数的事情了。如果你真的这么认为的话,那我认为我有必要向你介绍这另外的一种方式。首先请你建立一个名为 ipsearcher.def 的文件,然后在其中写入如下内容:LIBRARY “ipsearcher“EXPORTSLookupAddress 1_GetAddress 2将文件保存后,进入到命令行模式下,输入以下命令(前提是你拥有 Visual Stud
16、io 的附带工具 lib.exe并有正确的路径指向。以 Visual Studio 6.0 为例,这个工具通常位于 Microsoft Visual StudioVC98Bin 下):lib /def:ipsearcher.def执行的结果有一个警告,不必理会。这时候我们会发现,lib 为我们生成了一个 ipsearcher.lib。然后,我们继续编写 ipsearcher.h 文件,如下:#ifndef IPSEARCHER_H#define IPSEARCHER_H#include #pragma comment( lib, “ipsearcher.lib“ )extern “C“BOOL CDECL LookupAddress( PCSTR, PSTR* );PSTR* CDECL _GetAddress( PCSTR );#endif / IPSEARCHER_H大功告成!这样我们就为这个光秃秃的 ipsearcher.dll 做了一份 SDK 开发包,而不必再使用动态加载的方法了。总结一下再其实,探究一个 DLL 并非像我这里所讲述的这么简单。这项工作很可能需要阅读大量的汇编代码,了解DLL 函数体的流程才能使真相大白于天下。另外,还不能排除有的 DLL 被加密、加壳、反跟踪也就是说对于 ipsearcher.dll,那简直就是我捡了个便宜来借花献佛了。