使用 Windows API
静态链接 (Static Linking) :
- 在编译时将 DLL 中的函数链接到可执行文件中。
- 应用程序在运行时不需要额外加载 DLL。
- 优点是加载和执行速度快,缺点是可执行文件体积较大。
动态链接 (Dynamic Linking) :
- 在运行时动态加载 DLL,并调用 DLL 中的函数。
- 应用程序需要使用
LoadLibrary
和GetProcAddress
等 Windows API 函数来加载和访问 DLL 中的函数。 - 优点是可执行文件体积较小,缺点是加载和执行速度稍慢,并且需要确保 DLL 文件在运行时可用。
隐式链接 (Implicit Linking) :
- 在编译时使用
#include
或#pragma comment
指令,将 DLL 中的函数声明添加到应用程序的源代码中。 - 编译器会自动生成调用 DLL 函数的代码,并在链接时将其链接到可执行文件中。
- 这种方式对开发人员来说更加简单和方便,但缺点是可执行文件体积较大。
显式链接 (Explicit Linking) :
- 在运行时使用
LoadLibrary
和GetProcAddress
等 Windows API 函数动态加载 DLL,并调用 DLL 中的函数。 - 这种方式给开发人员更多的灵活性和控制权,但需要编写更多的代码。
常用win api函数(列举了两种)
LoadLibraryEx
函数
1 | HMODULE LoadLibraryEx( |
参数说明:
lpLibFileName
: 要加载的 DLL 文件的完整路径。
hFile
: 可选参数,用于指定加载 DLL 的文件句柄。通常设置为NULL
。
dwFlags
: 用于指定加载 DLL 的行为方式,可以是以下值的组合:-
LOAD_LIBRARY_AS_DATAFILE
: 以数据文件的方式加载 DLL,不执行任何 DLL 初始化代码。 -
LOAD_LIBRARY_AS_IMAGE_RESOURCE
: 将 DLL 加载为映像资源,而不是作为可执行模块。 -
LOAD_LIBRARY_AS_DATAFILE_EXCLUSIVE
: 以独占方式加载 DLL 为数据文件。 -
LOAD_LIBRARY_REQUIRE_SIGNED_TARGET
: 要求目标 DLL 必须是经过数字签名的。 -
LOAD_LIBRARY_SEARCH_APPLICATION_DIR
: 在应用程序目录中搜索 DLL。 -
LOAD_LIBRARY_SEARCH_DEFAULT_DIRS
: 在默认的搜索目录中搜索 DLL。 - 等等。
-
函数返回值:
- 成功返回加载的 DLL 模块句柄(HMODULE)。
- 失败返回
NULL
。可以通过GetLastError()
函数获取错误代码。
进程调用 LoadLibrary 或 LoadLibraryEx 以显式链接到 DLL。如果函数执行成功,它会将指定的 DLL 映射到调用进程的地址空间中并返回该 DLL 的句柄。 此句柄可以与其他函数(如 GetProcAddress
和 FreeLibrary
)一起在显式链接中使用。
示例:
1 |
|
使用了 LoadLibraryEx()
函数来加载 MyDLL.dll
文件;加载成功后,使用 GetProcAddress()
函数获取 DLL 中 MyFunction
函数的地址,并将其转换为我们自定义的 MYFUNCTION
类型的函数指针。
最后,调用 MyFunction()
并打印结果,然后使用 FreeLibrary()
函数释放 DLL。
VirtualQueryEx()
和 ReadProcessMemory()
函数
VirtualQueryEx()
1 | SIZE_T VirtualQueryEx( |
作用:
- 查询指定进程中某个虚拟地址范围的内存基本信息,如内存状态、保护属性、分配类型等。
参数:
-
hProcess
: 要查询的进程句柄。 -
lpAddress
: 要查询的虚拟地址。 -
lpBuffer
: 指向MEMORY_BASIC_INFORMATION
结构体的指针,用于接收查询结果。 -
dwLength
:lpBuffer
结构体的大小。
-
返回值:
- 成功时返回
lpBuffer
中填充的字节数。 - 失败时返回 0,可以通过
GetLastError()
获取错误代码。
- 成功时返回
ReadProcessMemory()
1 | BOOL ReadProcessMemory( |
作用:
- 从指定进程的虚拟地址空间中读取数据。
参数:
-
hProcess
: 要读取的进程句柄。 -
lpBaseAddress
: 要读取的虚拟地址。 -
lpBuffer
: 指向接收读取数据的缓冲区。 -
nSize
: 要读取的字节数。 -
lpNumberOfBytesRead
: 指针,用于接收实际读取的字节数。
-
返回值:
- 成功时返回
TRUE
。 - 失败时返回
FALSE
,可以通过GetLastError()
获取错误代码。
- 成功时返回
加载dll示例:
1 |
|
这里使用 VirtualQueryEx()
函数来查找进程中可执行的内存区域,并检查是否存在 DLL 的标志信息。一旦找到 DLL 的基地址,我们就使用 ReadProcessMemory()
函数来读取 DLL 的头部信息。
win api调用dll的优\缺点
优:
- 使用简单,API 函数如
LoadLibrary
和GetProcAddress
易于使用。 - 可以实现动态和静态链接两种方式,提供更多的灵活性。
- 可以在运行时检查 DLL 是否已经被加载,避免重复加载。
缺:
- 需要手动编写加载和卸载 DLL 的代码。
- 需要处理 DLL 依赖关系和版本兼容性问题。
- 如果 DLL 文件不存在或无法访问,会导致应用程序崩溃。
使用遍历 PEB 的模块链表加载
通过PEB来遍历进程模块没有Win API的使用痕迹,在某些场合更加好用
遍历 PEB的模块链表加载 DLL 的详细过程
获取PEB地址
32位应用程序的 PEB 的地址可以通过 fs:[0x30]获取,fs:[0]为TEB结构的地址
在 64 位 Windows 上, TEB 地址存储在 gs:0x30
寄存器中PEB 地址存储在 TEB 结构的 0x60
偏移处。因此, 可以通过先获取 TEB 地址, 然后读取 0x60
偏移处的值来获取 PEB 地址。
遍历模块链表:
PEB 中包含一个指向模块列表头部的指针, 通过遍历这个模块链表, 可以获取到进程中加载的所有模块(包括 DLL)的信息。
每个模块在链表中都由一个 LDR_DATA_TABLE_ENTRY
结构体来表示, 其中包含了模块的文件路径、基地址、入口点等信息。
获取 DLL 的基地址和入口点:
遍历模块链表时, 可以根据 DLL 的文件名或路径, 找到对应的 LDR_DATA_TABLE_ENTRY
结构体, 从中获取 DLL 的基地址和入口点地址。
TEB结构体
1 | // TEB 结构体定义 |
TEB结构的偏移0x60就是PEB结构
PEB结构体
1 | typedef struct _PEB { |
偏移0x18处找到Ldr
PPEB_LDR_DATA结构体
1 | typedef struct _PEB_LDR_DATA { |
LIST_ENTRY InLoadOrderModuleList; // 按照加载顺序组织的模块链表
LIST_ENTRY InMemoryOrderModuleList; // 按照内存地址顺序组织的模块链表
LIST_ENTRY InInitializationOrderModuleList; // 按照初始化顺序组织的模块链表
LIST_ENTRY结构体
1 | typedef struct _LIST_ENTRY { |
Flink
: 指向下一个链表节点的指针
Blink
: 指向上一个链表节点的指针
可以看出来是个双向链表,这个结构体中的Flink
指向真正的模块链表,而模块链表的每一个成员都是一个LDR_DATA_TABLE_ENTRY
结构
_LDR_DATA_TABLE_ENTRY
结构
1 | typedef struct _LDR_DATA_TABLE_ENTRY { |
PEB中链表的Flink指向 _LDR_DATA_TABLE_ENTRY结构体
(借了大佬一张图)
具体实现就不写了,不太会写,让chat生成了一个
1 |
|
遍历 PEB 的模块链表加载dll的优缺点
优:
- 无需手动编写加载和卸载 DLL 的代码,可以自动加载所需的 DLL。
- 可以在不知道 DLL 文件路径的情况下加载 DLL。
- 可以避免 DLL 文件不存在或无法访问导致的应用程序崩溃。
缺:
- 实现相对复杂,需要了解 PEB 的结构和遍历模块链表的方法。
- 可能无法检测到某些 DLL 的加载状态,比如动态加载的 DLL。
- 需要处理 DLL 依赖关系和版本兼容性问题。
- 可能无法提供与 Windows API 调用 DLL 相同的灵活性和控制力。