安全矩阵

 找回密码
 立即注册
搜索
查看: 3200|回复: 0

网络安全编程:DLL编程

[复制链接]

991

主题

1063

帖子

4315

积分

论坛元老

Rank: 8Rank: 8

积分
4315
发表于 2021-3-4 21:59:22 | 显示全部楼层 |阅读模式
原文链接:网络安全编程:DLL编程

DLL(Dynamic Link Library,动态连接库)是一个可以被其他应用程序调用的程序模块,其中封装了可以被调用的资源或函数。动态连接库的扩展名一般是DLL,不过有时也可能是其他的扩展名。DLL文件属于可执行文件,它符合Windows系统的PE文件格式,不过它是依附于EXE文件创建的进程来执行的,不能单独运行。一个DLL文件可以被多个进程所装载调用。

Windows操作系统下有非常多的DLL文件,有的是操作系统的DLL文件,有的是应用程序的DLL文件。使用DLL文件有什么好处呢?DLL是动态连接库,相对应地,有静态连接库。动态连接库是在EXE文件运行时被加载执行的,而静态连接库是OBJ文件进行连接时同时被保存到程序中的。动态连接库可以减少可执行文件的体积,在需要的时候进入内存;将软件划分为多个模块,可以按照模块进行开发,对于发布与升级也非常方便。在某些情况下,必须使用DLL才能完成一些工作内容。

本文通过一个简单的DLL程序来初步了解DLL程序的编写。

1. 编写简单的DLL程序

首先从一个简单的DLL程序开始,并在DLL程序中添加一个导出函数。所谓导出函数,就是DLL提供给外部EXE或其他类型的可执行文件调用的函数。当然,DLL本身也可以自身进行调用。

DLL程序的入口函数不是main()函数,也不是WinMain()函数,而是DllMain()函数,该函数的定义如下:
  1. BOOL WINAPI DllMain(
  2. HINSTANCE hinstDLL, // handle to the DLL module
  3. DWORD fdwReason, // reason for calling function
  4. LPVOID lpvReserved // reserved
  5. );
复制代码

参数说明如下。

hinstDLL:该参数是当前 DLL 模块的句柄,即本动态连接库模块的实例句柄。

fdwReason:该参数表示 DllMain()函数被调用的原因。

该参数的取值有4种,也就是说存在4种调用DllMain()函数的情况,这4个值分别是DLL_PROCESS_ATTACH(当DLL被某进程加载时,DllMain()函数被调用)、DLL_PRO CESS_DETACH(当DLL被某进程卸载时,DllMain()函数被调用)、DLL_THREAD_ATTACH(当进程中有线程被创建时,DllMain()函数被调用)和DLL_THREAD_DETACH(当进程中有线程结束时,DllMain()函数被调用)。

lpvReserved:保留参数,即不被程序员使用的参数。

启动VC6集成开发环境,创建一个DLL工程。创建一个“A simple DLL Project”类型的工程,VC生成代码如下:
  1. BOOL APIENTRY DllMain( HANDLE hModule,DWORD ul_reason_for_call,LPVOID lpReserved)
  2. {
  3.   return TRUE;
  4. }
复制代码
在生成的代码中,函数定义处有一个APIENTRY的函数修饰符。该修饰符为一个宏,其定义如下:

  1. #define APIENTRY WINAPI
复制代码
由于DllMain()函数不止一次地被调用,根据调用的情况不同,需要执行不同的代码,比如当进程加载该DLL文件时,可能在DLL中要申请一些资源;而在卸载该DLL时,则需要将先前自身所申请的资源进行释放。出于种种原因,在编写DLL程序时,需要把DllMain()函数的结构写成如下形式:


  1. BOOL APIENTRY DllMain( HANDLE hModule,DWORD ul_reason_for_call,LPVOID lpReserved)
  2. {
  3.   switch ( ul_reason_for_call )
  4.   {
  5.   case DLL_PROCESS_ATTACH:
  6.     {
  7.       break;
  8.     }
  9.   case DLL_PROCESS_DETACH:
  10.     {
  11.       break;
  12.     }
  13.   case DLL_THREAD_ATTACH:
  14.     {
  15.       break;
  16.     }
  17.   case DLL_THREAD_DETACH:
  18.     {
  19.       break;
  20.     }
  21.   }
  22.   return TRUE;
  23. }
复制代码

这是一个switch/case结构,这样写可以达到根据不同的调用原因执行不同的代码。

2. 给DLL添加一个简单的导出函数

上面的代码只是一个简单的DLL程序的开始,并没有实际的意义。对于DLL文件来说,DllMain()并不是必需的。按照DLL文件的本质作用是为其他的可执行文件提供使用,那么DLL程序中需要编写能够提供其他程序使用的函数,这些公开提供给其他程序使用的函数被称为导出函数。在上面代码的基础上添加一个导出函数,定义如下:

  1. extern "C" __declspec(dllexport) VOID MsgBox(char *szMsg);
复制代码

extern "C"表示该函数以 C 方式导出。由于源代码是.CPP 文件,因此,如果按照 C++的方式导出的话,那么在编译后函数名会被名字粉碎,导致在动态调用该函数时就会极为不方便。__declspec(dllexport)的作用是声明一个导出函数,将该函数从本 DLL 中开放提供给其他模块使用。

MsgBox()函数的实现如下:

  1. VOID MsgBox(char *szMsg)
  2. {
  3.   char szModuleName[MAX_PATH] = { 0 };
  4.   GetModuleFileName(NULL, szModuleName, MAX_PATH);
  5.   MessageBox(NULL, szMsg, szModuleName, MB_OK);
  6. }
复制代码

该函数在被调用时会在MessageBox窗口的标题栏处显示其所在进程的进程名。


这样,第一个DLL文件的编写就完成了。编译连接该代码,查看编译和连接的输出情况会发现VC共生成了2个文件,分别是“FirstDll.dll”和“FirstDll.lib”,前者是供其他可执行程序使用的DLL文件,其中包含了程序员编写的代码、导出函数,而后者是一个库文件,其中包含一些导出函数的相关信息,供调用DLL文件中导出函数函数的程序员编译时使用。

导出DLL中的函数有两种方法,这是其中的一种。另外一种方式是建立一个.DEF的文件来定义导出哪些函数。函数除了可以通过函数名导出外,还可以通过序号进行导出。建立.DEF文件可以较为方便地管理DLL项目中的导出函数(总比在代码中逐个找__declspec(dllexport)要方便很多)。由于这里的代码比较短小,因此使用了__declspec(dllexport)这种定义方法。

3. 对DLL程序的调用方法一

DLL程序是无法单独运行的,它需要通过编写一个EXE程序(当然也可以在另外的DLL程序中调用)来调用这个DLL文件中的导出函数。在VC集成开发环境中添加一个测试项目,在工作区的“Workspace ‘FirstDll’:1 project(s)”上单击右键,在弹出的菜单中选择“Add New Project to Workspace”,如图1所示。

图1  添加对DLL进行测试的项目

添加一个控制台的项目,然后编写对DLL进行调用的测试代码,具体如下:


  1. #include <windows.h>
  2. #pragma comment (lib, "FirstDll")
  3. extern "C" VOID MsgBox(char *szMsg);
  4. int main(int argc, char* argv[])
  5. {
  6.   MsgBox("Hello First Dll !");
  7.   return 0;
  8. }
复制代码

#pragma comment (lib, "FirstDll")告诉连接器需要在FirstDll.lib文件中找到DLL中导出函数的信息。


对以上代码进行编译连接,VC会产生一个连接错误,如图2所示。

图2  连接出错信息

这个错误是因为连接器找不到“FirstDll.lib”文件。将“FirstDll.lib”复制到测试项目的目录下,然后添加到测试工程中,再次进行编译连接就成功了。运行编写好的测试程序,会弹出一个错误对话框,如图3所示。

图3  运行测试程序时的错误信息

根据错误提示可以看出是缺少要测试的DLL文件,也就是“FirstDll.dll”文件。将其复制到与可执行文件相同的目录下,然后再次运行,程序可以顺利地被执行。

一般在发布DLL文件时,需要将DLL文件、Lib文件和.h文件同时发布,当然有一个说明文档或手册会显得更加专业。

4. 对DLL程序的调用方法二

前一种方法属于静态调用,其方式是通过连接器将DLL函数的导出函数写进可执行文件。现在使用第二种方法来调用DLL中的函数,这种方法相对于前一种方法是动态调用。动态调用不是在连接时完成的,而是在运行时完成的。动态调用不会在可执行文件中写入DLL的相关信息。现在来写一个关于动态调用的测试程序,该程序的创建方法与静态调用的方法相同,这里不再复述。

动态调用DLL函数的代码如下:

  1. #include <windows.h>
  2. typedef VOID (*PFUNMSG)(char *);
  3. int main(int argc, char* argv[])
  4. {
  5.   HMODULE hModule = LoadLibrary("FirstDll.dll");
  6.   if ( hModule == NULL )
  7.   {
  8.     MessageBox(NULL, "FirstDll.dll 文件不存在","DLL 文件加载失败", MB_OK);
  9.     return -1;
  10.   }
  11.   PFUNMSG pFunMsg = (PFUNMSG)GetProcAddress(hModule, "MsgBox");
  12.   pFunMsg("Hello First Dll !");
  13.   return 0;
  14. }
复制代码

对代码进行编译连接都正常通过。但是请注意,这个程序中并没有用到#pragma comment()指令,也没有通过lib在程序中留下相关的导入信息。运行编译连接好的程序,程序会给出提示“FirstDll.dll文件不存在”。按照前面的方法,将FirstDll.dll文件复制到与测试程序相同的目录下,运行测试程序,程序执行成功。

DLL的动态加载调用是非常有用的。在第一个测试程序中,如果测试系统的装载器无法找到DLL文件,那么系统会直接报错而退出。而在第二个测试程序中,如果测试程序无法找到DLL文件,则由程序给出一个错误的提示,同时程序其实可以继续往下执行,而不会影响其他代码的运行(当然,由于DLL无法加载可能会损失部分的功能)。明白了动态加载调用和静态加载调用的区别,那么它们的优缺点就很清楚了。静态加载调用使用方便,而动态加载调用灵活性较好。

在有些情况下,必须使用动态加载调用的方法来使用DLL中的导出函数。比如函数OpenThread(),该函数在VC6自带的PSDK中没有提供LIB文件和函数原型定义,没有LIB文件就无法连接成功(在新版的PSDK中有该函数对应的LIB文件)。在这种情况下,只能使用LoadLibrary()和GetProcAddress()这两个函数来动态加载调用OpenThread()函数(其实有很多情况下,在使用DLL文件中的导出函数时是找不到对应的LIB文件的,比如ntdll.dll中的很多函数虽然有导出,但是系统没有提供与其对应的LIB文件)。

现在了解一下LoadLibrary()函数和GetProcAddress()函数的定义。LoadLibrary()函数的定义如下:


  1. HMODULE LoadLibrary( LPCTSTR lpFileName);
复制代码

该函数只有一个参数,即要加载的DLL文件的文件名。该函数调用成功,则返回一个模块句柄。

GetProcAddress()函数的定义如下:

  1. FARPROC GetProcAddress( HMODULE hModule, LPCSTR lpProcName);
复制代码

该函数有两个参数,分别如下。


hModule:该参数是模块句柄,通常通过 LoadLibrary()函数或 GetModuleHandle()函数获得;

lpProcName:该参数指定要获得函数地址的函数名称。

该函数调用成功,则返回lpProcName指向的函数名的函数地址。

5. 查看DLL程序导出函数的工具介绍

前面介绍DLL编程时提到了导出函数,这里介绍两款查看DLL程序的导出函数的工具。其中一款是VC自带的工具“Depends”,另一款工具是一个功能更加强大的可以用来查看PE结构和识别加壳信息的工具“PEID”。

首先用“Depends”来查看DLL的导出函数,该工具可以在VC6的安装菜单下找到,具体位置为“开始”→“程序”→“Microsoft Visual Studio 6.0”→“Microsoft Visual Studio 6.0 Tools”→“Depends”。打开该程序,依次单击菜单项“File”→“Open”,在“打开”对话框中找到所写的FirstDll.dll文件,选中并打开(也可以直接进行拖曳),其工作窗口中显示了FirstDll.dll的信息,如图4所示。

图4  Depends显示界面

在图4的右下角区域范围显示的是该DLL文件导出的函数。从图4中可以看出,FirstDll.dll文件只导出一个MsgBox函数。

对于Depends的介绍就这么多,现在来看另外一个工具“PEID”。该工具是用来识别软件“指纹”信息(开发环境、版本、加壳信息等)的。将FirstDll.dll文件拖曳到PEID界面上,PEID会自动解析出该DLL文件的PE结构信息,界面如图5所示。

图5  PEID显示界面

从图5可以看出,PEID最下方的只读编辑框中显示了FirstDll.dll文件是由VC6开发的,并且版本是Debug版本。单击“子系统”右边的“大于号”按钮,会显示PE结构的详细信息,如图6所示。

图6  PE结构详情

在图6中的PE结构详细信息的下半部分有个“目录信息”,其中的第一个目录信息就是导出表信息,单击“导出表”最右侧的“大于号”按钮,出现“导出查看器”界面,如图7所示。

图7  导出查看器

从图7中可以看出,FirstDll.dll文件只有一个导出函数MsgBox(),只存在一个导出项。导出函数的信息与Depends相同。

参考文献:C++ 黑客编程揭秘与防范(第3版)












回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|安全矩阵

GMT+8, 2024-4-19 08:48 , Processed in 0.016176 second(s), 18 queries .

Powered by Discuz! X4.0

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表