一、DLL基础
Windows应用程序编程接口提供的所有函数都包含在DLL中,其中最重要的DLL为:
- kernel32.dll 用来管理内存、进程以及线程
- User32.dll 用来执行与用户界面相关的任务,如创建窗口和发送消息
- GDI32.dll 包含的函数用来绘制图像和显示文字
使用DLL的原因
- 扩展了应用程序的特性
- 由于DLL可以被动态地载入到进程的地址空间中,应用程序可以在运行的时候检测应该执行何种操作, 并在需要的时候载入DLL来执行这些操作。
- 简化了项目管理
- 有助于节省内存
- 如果两个或两个以上的应用程序使用同一个DLL,name该DLL只需载入内存一次,之后所有的应用程序就可以共享该DLL在内存中的页面。例如C/C++运行库。
- 如果应用程序都链接到运行库的静态版本,那么很多函数在内存中会出现多次,占用过多的内存
- 如果应用程序都链接到运行库的DLL版本,那么这些函数在内存中只出现一次,内存的使用率更高
- 促进了资源的共享
- DLL能够包含诸如对话框模板、字符串、图标以及位图之类的资源。 多个应用程序可以使用 DLL 来共享这些资源。
- 类似上一条 节省内存
- 促进本地化
- DLL 常常用来对应用程序进行本地化。
- 例如,一个应用程序可以只包含代码但不包含用户界面组件,DLL用来存放本地化的用户界面组件,供应用程序载入并使用。
- 有助于解决平台间的差异
- 可以用于特殊目的
DLL和进程的地址空间
创建DLL通常比创建应用程序容易, 因为DLL通常由一组可供任何应用程序使用的独立函数组成。在DLL中,通常没有用来处理消息循环或创建窗口的代码。DLL 只不过是一组源代码模块, 每个模块包含一些可供应用程序(可执行文件)或其他DLL 调用的函数。在所有的源文件编译完成之后, 链接器会像链接应用程序的可执行文件那样,对它们进行链接,但在创建DLL的时候, 我们必须给链接器指定DLL开关。这个开关会使链接器在生成的DLL文件映像中保存一些与可执行文件略微不同的信息, 这样操作系统的加载程序就能够将该文件映像识别为DLL,而不会将它识别为应用程序。
在应用程序(或其他DLL)能够调用一个DLL中的函数之前,必须将该DLL的文件映像映射到调用进程的地址空间中,可以通过 隐式载入时链接或显式运行时链接 来达到这一目的。
一旦系统将一个DLL的文件映像映射到调用进程的地址空间中之后,进程中的所有线程就可以调用该DLL中的函数了。事实上, 该DLL几乎完全丧失了它的DLL身份:对进程中的线程来说, 该DLL中的代码和数据就像是一些附加的代码和数据,碰巧被放在进程地址空间中。当线程调用DLL中的一个函数的时候, 该函数会在线程栈中取得传给它的参数,并使用线程栈来存放它需要的局部变量。此外, 该DLL中的函数创建的任何对象都为调用线程或调用进程所拥有——DLL绝对不会拥有任何对象。
如果运行同一个可执行文件的多个实例,这些实例将不会共享可执行文件中的全局变量和静态变量。
DLL中的全局变量和局部变量也是通过完全相同的方法来处理。当一个进程将一个DLL映像文件映射到自己的地址空间中时,系统也会为全局变量和静态变量创建新的实例。
DLL 创建过程及应用程序隐式链接到DLL的过程:
1.构建DLL
- 头文件,其中包含到处函数的原型、结构和符号的声明
- C/C++源文件,其中包含待导出函数的实现和变量
- 编译器为每个C/C++源文件生成 .obj 文件
- 链接器将每个 .obj 文件合并,从而生成DLL
- 如果至少导出了一个函数/变量,那么链接器会同时生成 .lib 文件
.lib 文件非常小,不包含任何函数或变量,只是列出了所有被导出的函数和变量的符号名。
2.构建exe
- 头文件,其中包含到处函数的原型、结构和符号的声明
- C/C++源文件,其中包含待导出函数的实现和变量
- 编译器为每个C/C++源文件生成 .obj 文件
- 链接器将每个 .obj 文件合并,并使用 .lib文件来解析对导入的函数/变量的引用,从而生成 .exe(它包含了一个导入表,其中列出了必需的DLL和导入的符号)
3.运行应用程序
- 加载程序为 .exe 创建地址控件
- 加载程序将必需的DLL载入到地址空间中。
进程的主线程开始执行,应用程序开始运行。
运行可执行模块
启动一个可执行模块的时候,操作系统的加载程序会先为进程创建虚拟地址空间,接着把可执行模块映射到进程的地址空间中。之后加载程序会检查可执行模块的导入段,试图对所需的DLL进行定位并将它们映射到进程的地址空间中。
由于导入段只包含DLL的名称,不包含DLL的路径,因此加载程序必须在用户的磁盘上搜索DLL。下面是加载程序的搜索顺序。
- 包含可执行文件的目录
- Windows 的系统目录,该目录可以通过GetSystemDirectory得到
- 16 位的系统目录,即Windows目录中的System子目录
- Windows目录,该目录可以通过 GetWindowsDirectory 得到
- 进程的当前目录
- PATH 环境变量中所列出的目录
对应用当前目录的搜索位于Windows目录之后,目的是为了防止加载程序在应用程序的当前目录中找到伪造的系统DLL并将它们载入,从而保证系统 DLL 始终都是从它们在 Windows 目录中的正式位置载入的
可通过注册表改变上述搜索顺序
二、DLL注入和API拦截
应用程序需要跨越进程边界来访问另一进程的地址空间的情况:
- 想要从另一个进程创建的窗口派生子类窗口
- 需要一些手段辅助调试 — 例如,需要确定另一个进程正在使用哪些DLL
- 想给另一个进程安装挂钩
2.1 使用注册表来注入DLL
- 整个系统的配置都保存在注册表中,可以通过调整其中的设置来改变系统的行为,
- 注册表项:
HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Windows

使用:
- 修改AppInit_Dlls 键的值,该值可能包含一个或一组DLL的文件名(空格或逗号分隔)
- 修改LoadAppInit_Dlls,类型为DWORD的注册表项,并将它的值设为1
加载:
- 当User32.dll被映射到一个新的进程时,会收到DLL_PROCESS_ATTACH通知。当User32.dll对它进行处理的时候,会取得上述注册表键的值,并调用LoadLibrary来载入这个字符串中指定的每个DLL.
- 载入DLL后,会调用DLL的DllMain函数并将参数fdwReason的值设为DLL_PROCESS_ATTACH,完成DLL的初始化
注:
- 由于被注入的DLL是在进程声明期的早期被载入的,因此在调用函数的时候应该慎重
- 调用Kernel32.dll中的函数应该没有问题,但是调用其他DLL中的函数可能会导致问题,甚至可能会导致蓝屏
- User32.dll不会检查每个DLL的载入或初始化是否成功
缺点:
- DLL只会映射User32.dll的进程中。即GUI的应用程序会使用这些DLL;但CUI的应用程序不会使用它们,例如编译器或者链接器
- DLL会映射到所有User32.dll的进程中,不能指定注入某应用程序。DLL被映射到越多的进程,导致“容器”进程崩溃的可能性也就越大
- 注入DLL的应用程序终止之前,这些DLL都一直存在于进程的地址空间中。理想情况下,应该把DLL映射到需要的进程中去,并让映射的时间越短越好
2.2 使用Windows挂钩来注入DLL
例子:进程A为了查看系统中各窗口处理了哪些消息,安装了一个WH_GETMESSAGE挂钩。
2.2.1 使用的API
HHOOK hHook = SetWindowsHookEx(WH_GETMESSAGE, GetMsgProc, hInstall, 0);
第1个参数:要安装的挂钩的类型
第2个参数:一个函数的地址,在窗口即将处理一条消息的时候,系统应该调用这个函数
第3个参数:标识一个DLL,这个DLL中包含了GetMsgProc函数。在Windows中,hInstall的值是进程地址控件中DLL被映射到的虚拟内存地址
第4个参数:标识要给哪个线程安装挂钩。一个线程可能会调用SetWindowsHookEx并传入系统中另一个线程的线程标识符。传0指给所有GUI线程安装挂钩。
2.2 接下来发生的事情
- 进程B中的一个线程准备向一个窗口派送一条消息
- 系统检查该线程是否已经安装了WH_GETMESSAGE挂钩
系统检查GetMsgProc所在的DLL是否已经被映射到进程B的地址空间中
- 如果DLL尚未被映射,系统会强制将该DLL映射到进程B的地址空间中,并将进程B中该DLL的锁计时器递增
- 如果DLL的hInstall是在进程B中映射的,因此系统会对它进行检查,看它与该DLL在进程A中的位置是否相同
- hInstall相同:两个进程的地址空间中,GetMsgProc函数位于相同的位置,系统可直接在进程A的地址空间中调用GetMsgProc
- hInstall不同:系统会确定GetMsgProc在进程B的地址空间中的虚拟内存地址
公式:GetMsgProc B = hInstDll B + ( GetMsgProc A - hInstDll A )
- 通过GetMsgProc A减去hInstDll A,可以得到GetMsgProc函数的偏移量(以字节为单位)。
- 把这个偏移量与hInstDll B相加就得到了GetMsgProc函数在进程B的地址空间中的位置。
系统在进程B中递增该DLL的锁计数器
- 系统在进程B中的地址空间中调用GetMsgProc函数
- 当GetMsgProc返回的时候,系统递减该DLL在进程B中的锁计数器
当系统把挂钩过滤函数所在的DLL注入或映射到地址空间中时,会映射整个DLL,而不仅仅只是挂钩过滤函数。 即,该DLL内的所有函数存在于进程B中,能够为进程B中的任何线程调用。
2.3 卸载钩子
和利用注册表来注入DLL的方法相比,这种方法允许我们在不需要该DLL的时候从进程的地址空间中撤销对它的映射,即调用:
BOOL UnhookWindowsHookEx(HHOOK hHook);
当一个线程调用UnhookWindowsHookEx的时候,系统会遍历自己内部的一个已经注入该DLL的进程列表,并将该DLL的锁计数器递减。当锁计数器减到0的时候,系统会自动从进程的地址空间中撤销对该DLL的映射。
该示例由于派生了子类窗口,不能将挂钩清除,因为会引起内存访问违规。所以在子类窗口的整个生命周期内,这个挂钩必须一直有效
2.4 示例
三、读写ini文件
http://blog.sina.com.cn/s/blog_4d11e5f20100fm2s.html
使用Windows API
读:
DWORD GetPrivateProfileString(LPCTSTR lpAppName, // INI文件中的一个字段名[节名]可以有很多个节名LPCTSTR lpKeyName, // lpAppName下的一个键名,也就是里面具体的变量名LPCTSTR lpDefault, // 如果lpReturnedString为空,则把个变量赋给lpReturnedStringLPTSTR lpReturnedString, // 存放键值的指针变量,用于接收INI文件中键值(数据)的接收缓冲区DWORD nSize, // lpReturnedString的缓冲区大小LPCTSTR lpFileName // INI文件的路径);
写:
BOOL WritePrivateProfileString(LPCTSTR lpAppName, // INI文件中的一个字段名[节名]可以有很多个节名LPCTSTR lpKeyName, // lpAppName 下的一个键名,也就是里面具体的变量名LPCTSTR lpString, // 键值,也就是数据LPCTSTR lpFileName // INI文件的路径);
示例:
写:
LPTSTR lpPath = new char[MAX_PATH];strcpy(lpPath, "D://IniFileName.ini");WritePrivateProfileString("LiMing", "Sex", "Man", lpPath);WritePrivateProfileString("LiMing", "Age", "20", lpPath);WritePrivateProfileString("Fangfang", "Sex", "Woman", lpPath);WritePrivateProfileString("Fangfang", "Age", "21", lpPath);delete [] lpPath;
读:
LPTSTR lpPath = new char[MAX_PATH];LPTSTR LiMingSex = new char[6];int LiMingAge;LPTSTR FangfangSex = new char[6];int FangfangAge;strcpy(lpPath, "..//IniFileName.ini");GetPrivateProfileString("LiMing", "Sex", "", LiMingSex, 6, lpPath);LiMingAge = GetPrivateProfileInt("LiMing", "Age", 0, lpPath);GetPrivateProfileString("Fangfang", "Sex", "", FangfangSex, 6, lpPath);FangfangAge = GetPrivateProfileInt("Fangfang", "Age", 0, lpPath);delete [] lpPath;
