一、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载入到地址空间中。

进程的主线程开始执行,应用程序开始运行。
image.png
运行可执行模块
启动一个可执行模块的时候,操作系统的加载程序会先为进程创建虚拟地址空间,接着把可执行模块映射到进程的地址空间中。之后加载程序会检查可执行模块的导入段,试图对所需的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

image.png
使用:

  1. 修改AppInit_Dlls 键的值,该值可能包含一个或一组DLL的文件名(空格或逗号分隔)
  2. 修改LoadAppInit_Dlls,类型为DWORD的注册表项,并将它的值设为1

加载:

  1. 当User32.dll被映射到一个新的进程时,会收到DLL_PROCESS_ATTACH通知。当User32.dll对它进行处理的时候,会取得上述注册表键的值,并调用LoadLibrary来载入这个字符串中指定的每个DLL.
  2. 载入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

  1. HHOOK hHook = SetWindowsHookEx(WH_GETMESSAGE, GetMsgProc, hInstall, 0);

第1个参数:要安装的挂钩的类型
第2个参数:一个函数的地址,在窗口即将处理一条消息的时候,系统应该调用这个函数
第3个参数:标识一个DLL,这个DLL中包含了GetMsgProc函数。在Windows中,hInstall的值是进程地址控件中DLL被映射到的虚拟内存地址
第4个参数:标识要给哪个线程安装挂钩。一个线程可能会调用SetWindowsHookEx并传入系统中另一个线程的线程标识符。传0指给所有GUI线程安装挂钩。

2.2 接下来发生的事情

  1. 进程B中的一个线程准备向一个窗口派送一条消息
  2. 系统检查该线程是否已经安装了WH_GETMESSAGE挂钩
  3. 系统检查GetMsgProc所在的DLL是否已经被映射到进程B的地址空间中

    1. 如果DLL尚未被映射,系统会强制将该DLL映射到进程B的地址空间中,并将进程B中该DLL的锁计时器递增
    2. 如果DLL的hInstall是在进程B中映射的,因此系统会对它进行检查,看它与该DLL在进程A中的位置是否相同
      1. hInstall相同:两个进程的地址空间中,GetMsgProc函数位于相同的位置,系统可直接在进程A的地址空间中调用GetMsgProc
      2. hInstall不同:系统会确定GetMsgProc在进程B的地址空间中的虚拟内存地址

        公式:GetMsgProc B = hInstDll B + ( GetMsgProc A - hInstDll A )

        • 通过GetMsgProc A减去hInstDll A,可以得到GetMsgProc函数的偏移量(以字节为单位)。
        • 把这个偏移量与hInstDll B相加就得到了GetMsgProc函数在进程B的地址空间中的位置。
  4. 系统在进程B中递增该DLL的锁计数器

  5. 系统在进程B中的地址空间中调用GetMsgProc函数
  6. 当GetMsgProc返回的时候,系统递减该DLL在进程B中的锁计数器

    当系统把挂钩过滤函数所在的DLL注入或映射到地址空间中时,会映射整个DLL,而不仅仅只是挂钩过滤函数。 即,该DLL内的所有函数存在于进程B中,能够为进程B中的任何线程调用。

2.3 卸载钩子

和利用注册表来注入DLL的方法相比,这种方法允许我们在不需要该DLL的时候从进程的地址空间中撤销对它的映射,即调用:

  1. BOOL UnhookWindowsHookEx(HHOOK hHook);

当一个线程调用UnhookWindowsHookEx的时候,系统会遍历自己内部的一个已经注入该DLL的进程列表,并将该DLL的锁计数器递减。当锁计数器减到0的时候,系统会自动从进程的地址空间中撤销对该DLL的映射。

该示例由于派生了子类窗口,不能将挂钩清除,因为会引起内存访问违规。所以在子类窗口的整个生命周期内,这个挂钩必须一直有效

2.4 示例

三、读写ini文件

http://blog.sina.com.cn/s/blog_4d11e5f20100fm2s.html
使用Windows API
读:

  1. DWORD GetPrivateProfileString(
  2. LPCTSTR lpAppName, // INI文件中的一个字段名[节名]可以有很多个节名
  3. LPCTSTR lpKeyName, // lpAppName下的一个键名,也就是里面具体的变量名
  4. LPCTSTR lpDefault, // 如果lpReturnedString为空,则把个变量赋给lpReturnedString
  5. LPTSTR lpReturnedString, // 存放键值的指针变量,用于接收INI文件中键值(数据)的接收缓冲区
  6. DWORD nSize, // lpReturnedString的缓冲区大小
  7. LPCTSTR lpFileName // INI文件的路径
  8. );

写:

  1. BOOL WritePrivateProfileString(
  2. LPCTSTR lpAppName, // INI文件中的一个字段名[节名]可以有很多个节名
  3. LPCTSTR lpKeyName, // lpAppName 下的一个键名,也就是里面具体的变量名
  4. LPCTSTR lpString, // 键值,也就是数据
  5. LPCTSTR lpFileName // INI文件的路径
  6. );

示例:
写:

  1. LPTSTR lpPath = new char[MAX_PATH];
  2. strcpy(lpPath, "D://IniFileName.ini");
  3. WritePrivateProfileString("LiMing", "Sex", "Man", lpPath);
  4. WritePrivateProfileString("LiMing", "Age", "20", lpPath);
  5. WritePrivateProfileString("Fangfang", "Sex", "Woman", lpPath);
  6. WritePrivateProfileString("Fangfang", "Age", "21", lpPath);
  7. delete [] lpPath;

读:

  1. LPTSTR lpPath = new char[MAX_PATH];
  2. LPTSTR LiMingSex = new char[6];
  3. int LiMingAge;
  4. LPTSTR FangfangSex = new char[6];
  5. int FangfangAge;
  6. strcpy(lpPath, "..//IniFileName.ini");
  7. GetPrivateProfileString("LiMing", "Sex", "", LiMingSex, 6, lpPath);
  8. LiMingAge = GetPrivateProfileInt("LiMing", "Age", 0, lpPath);
  9. GetPrivateProfileString("Fangfang", "Sex", "", FangfangSex, 6, lpPath);
  10. FangfangAge = GetPrivateProfileInt("Fangfang", "Age", 0, lpPath);
  11. delete [] lpPath;

四、使用
https://segmentfault.com/a/1190000012993626