DLL动态链接库
DLL 是 Dynamic Link Library
的缩写,翻译为动态链接库。
在 Windows 中,许多应用程序并不是一个完整的可执行文件,它们被分割成一些相对独立的动态链接库,即 DLL 文件,放置于系统中。
当我们执行某一程序时,相应的 DLL 文件就会被调用。一个应用程序可以用多个 DLL 文件,一个 DLL 文件也可能被多个应用程序所共用,这样的 DLL 文件被称为共享 DLL 文件。
想要查看某个程序加载的 DLL 文件,可以通过火绒安全软件的高级工具火绒剑来查看:
创建动态链接库
可以使用 Visual Studio 或易语言来创建动态链接库。
C++创建动态链接库
使用 Visual Studio 2019 创建一个具有导出项的(DLL)动态链接库项目:
项目结构:
dllmain.cpp
定义 DLL 应用程序的入口点。
#include "pch.h"
/*
动态链接库的入口函数
hModule: 句柄
ul_reason_for_call: 启动原因
lpReserved: 保留参数
*/
BOOL APIENTRY DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved)
{
switch (ul_reason_for_call)
{
case DLL_PROCESS_ATTACH:// 当DLL被进程加载时
case DLL_THREAD_ATTACH:// 有线程被创建时
case DLL_THREAD_DETACH:// 有线程结束时
case DLL_PROCESS_DETACH:// 当DLL被进程卸载时
break;
}
return TRUE;
}
易语言创建动态链接库
使用易语言5.9创建一个Windows动态链接库项目:
项目结构:
动态链接库最少需要一个公开的子程序才可以编译通过,修改代码:
注意:易语言静态编译时使用的链接器默认路径是 C:\Program Files (x86)\e\VC6linker\bin\link.exe
,如果我们将易语言安装到其他位置,则需要在安装目录下的配置文件 e\tools\link.ini
中修改链接器的正确路径。
导出DLL中的函数
导出函数的作用:将我们开发的 DLL 中的函数提供给其他模块调用。
C++导出DLL中的函数
extern "C"
会指示编译器这部分代码按 C 语言进行编译,防止函数名被编译器修改。如果本身是 C 函数,可以不加 "C"
。
__declspec(dllexport)
声明导出函数,将该函数从 DLL 开放提供给其他模块调用。
示例代码:
// 声明导出函数,把testExport()函数导出
extern "C" __declspec(dllexport) void testExport();
// 用作导出的测试函数
void testExport()
{
MessageBox(NULL, "导出函数被调用成功", "信息", MB_YESNO);
}
MessageBox
函数会报参数类型错误,可以通过修改项目属性中的字符集来解决:
易语言导出DLL中的函数
易语言编写 DLL 时,在 DLL 中任何程序集中勾选“公开”的子程序都作为对外接口。
应用程序与DLL导出函数的通讯方法
C++调用DLL
C++ 调用 DLL 导出函数可以使用静态调用和动态调用,使用 Visual Studio 2019 创建一个控制台项目,来测试调用 DLL 导出函数。
静态库
静态库是指在我们的应用中,有一些公共代码是需要反复使用,就把这些代码编译为“库”文件。在链接步骤,链接器将从库文件取得所需的代码,复制到生成的可执行文件种。
程序编译一般需要经预处理、编译、汇编和链接几个步骤,静态库的特点是可执行文件包含了库代码的一份完整拷贝,缺点就是被多次使用就会有多份冗余拷贝。
静态库和动态库是两种共享程序代码的方式,它们的区别是:静态库在程序的链接阶段被复制到了程序中,和程序运行的时候没有关系。动态库在链接阶段没有被复制到程序中,而是程序在运行时由系统动态加载到内存中供程序调用。使用动态库的有点是系统只需载入一次动态库,不同的程序可以得到内存中相同的动态库的副本,因此节省了很多内存。
FirstDll.lib
就是静态库:
静态调用
静态调用就是调用静态库(.lib
)。
导入静态库语法:
#pragma comment(comment-type, ["comment-string"])
-
comment-type
是一个预定义标识符,指定注释的类型,可选值compiler
、exestr
、lib
或linker
。 -
comment-string
是一个为comment-type
提供附加信息的字符串。
当 comment-type
为 lib
时,comment-string
为静态库的路径,可以填写相对路径:
#pragma comment(lib, "lib\\FirstDll")
将 DLL 库文件 FirstDll.lib
拷贝到需要调用 DLL 函数的项目中,这里我直接创建了一个 lib 目录来存放静态库文件:
调用 DLL 库文件中的导出测试函数:
#include <Windows.h>
#pragma comment(lib, "lib\\FirstDll")
extern "C" void testExport();
int main()
{
testExport();
return 0;
}
编译后,还需要把 DLL 拷贝到编译后的可执行文件同一目录下:
最后,双击运行 TestDLL.ext
,测试调用结果:
动态调用
动态调用就是调用动态库(.dll
)。
先使用 LoadLibrary()
函数将指定的模块加载到调用进程的地址空间中。
然后使用 GetProcAddress()
函数从指定的动态链接库(DLL)中检索导出的函数或变量的地址。
可选:最后使用 FreeLibrary()
函数卸载 DDL。
示例代码:
#include <Windows.h>
typedef void (*PTestExport)();
int main()
{
HMODULE hModule = LoadLibrary("FirstDll.dll");
if (hModule == NULL)
{
MessageBox(NULL, "句柄出错", "标题", MB_OK);
return -1;
}
PTestExport mTestExport = (PTestExport)GetProcAddress(hModule, "testExport");
mTestExport();
FreeLibrary(hModule);
return 0;
}
将编译生成的可执行文件和用到的动态链接库(DLL)放到同一个目录下,即可调用成功。
易语言调用DLL
新建一个窗口程序,放置一个按钮,用来点击测试调用 DLL :
然后菜单栏选择插入 - DLL命令,填入Dll命令名、库文件名、在库中对应的命令名:
最后在按钮点击事件里调用 Dll 命令:
最终编译项目,将编译生成的可执行文件和用到的动态链接库(DLL)放到同一个目录下,即可调用成功。
调用DDL总结
不管是 C++ 的静态调用和动态调用,还是易语言的奇葩调用,最终都是将编译生成的可执行文件和用到的动态链接库(DLL)放到同一个目录下,才能调用成功。如果不放到同一目录下,也需要指定路径,让可执行文件可以找到我们的动态链接库。
查看DLL导出函数
我们可以使用 PE 工具去查看 DLL 导出函数,比较好用的工具有 ExeinfoPe、StudyPE、CFF Explorer、PEditor 等。
下图是使用 StudyPE 工具查看结果,可以看到我们用易语言编写的动态链接库导出函数信息:
将DLL注入到指定程序中
上面的内容都是自己编写的程序去调用 DLL,我们还可以将 DLL 注入到别人的程序中,让别人的程序调用。
可以通过第三方工具 HideToolz3.0 注入 DLL 到指定进程中,也可以自己用代码实现,游戏第三方工具就是这样实现的。
DLL远线程注入
强制创建一个目标进程的虚拟地址空间内运行的线程,将 DLL 加载进去。
C++DLL远线程注入
/// <summary>
/// 将DLL注入到指定进程
/// </summary>
/// <param name="dwProcessID">进程ID</param>
/// <param name="lpszDll">DLL路径</param>
/// <returns>TRUE成功,FALSE失败</returns>
BOOL RemoteLoadLibrary(DWORD dwProcessID, char* lpszDll)
{
// 打开目标进程
HANDLE hProcess = OpenProcess(PROCESS_CREATE_THREAD | PROCESS_VM_OPERATION | PROCESS_VM_WRITE, FALSE, dwProcessID);
// 向目标进程地址空间写入DLL名称
DWORD dwSize, dwWritten;
dwSize = strlen(lpszDll) + 1;
LPVOID lpBuf = VirtualAllocEx(hProcess, NULL, dwSize, MEM_COMMIT, PAGE_READWRITE);
if (NULL == lpBuf)
{
CloseHandle(hProcess);
return FALSE;
}
if (WriteProcessMemory(hProcess, lpBuf, (LPVOID)lpszDll, dwSize, &dwWritten))
{
// 要写入字节数与实际写入字节数不相等,仍属失败
if (dwWritten != dwSize)
{
VirtualFreeEx(hProcess, lpBuf, dwSize, MEM_DECOMMIT);
CloseHandle(hProcess);
return FALSE;
}
}
else
{
CloseHandle(hProcess);
return FALSE;
}
// 不再加载已载入Dll(只加载一次)
{
// 使目标进程调用GetModuleHandle,获得DLL在目标进程中的句柄
DWORD dwHandle, dwID;
LPVOID pFunc = GetModuleHandleA;
HANDLE hThread = CreateRemoteThread(hProcess, NULL, 0, (LPTHREAD_START_ROUTINE)pFunc, lpBuf, 0, &dwID);
// 等待GetModuleHandle运行完毕
WaitForSingleObject(hThread, INFINITE);
// 获得GetModuleHandle的返回值
GetExitCodeThread(hThread, &dwHandle);
CloseHandle(hThread);
if (dwHandle != NULL)
{
VirtualFreeEx(hProcess, lpBuf, dwSize, MEM_DECOMMIT);
CloseHandle(hProcess);
return TRUE;
}
}
// 使目标进程调用LoadLibrary,加载DLL
DWORD dwID;
LPVOID pFunc = LoadLibraryA;
HANDLE hThread = CreateRemoteThread(hProcess, NULL, 0, (LPTHREAD_START_ROUTINE)pFunc, lpBuf, 0, &dwID);
// 等待LoadLibrary加载完毕
WaitForSingleObject(hThread, INFINITE);
// 释放目标进程中申请的空间
VirtualFreeEx(hProcess, lpBuf, dwSize, MEM_DECOMMIT);
CloseHandle(hThread);
CloseHandle(hProcess);
return TRUE;
}
易语言DLL远线程注入
可以直接调用精易模块封装好的子程序来实现:
调用:
DLL注入过检测
程序在运行过程中,可能会不断的扫描未知的 DLL,来判断是否被注入。所以将 DLL 注入到指定进程后,还需要从进程中卸载(移除)。
C++卸载DLL
/// <summary>
/// 将DLL从指定进程卸载
/// </summary>
/// <param name="dwProcessID">进程ID</param>
/// <param name="lpszDll">DLL路径</param>
/// <returns>TRUE成功,FALSE失败</returns>
BOOL RemoteFreeLibrary(DWORD dwProcessID, char* lpszDll)
{
// 打开目标进程
HANDLE hProcess = OpenProcess(PROCESS_CREATE_THREAD | PROCESS_VM_OPERATION | PROCESS_VM_WRITE, FALSE, dwProcessID);
// 向目标进程地址空间写入DLL名称
DWORD dwSize, dwWritten;
dwSize = strlen(lpszDll);
LPVOID lpBuf = VirtualAllocEx(hProcess, NULL, dwSize, MEM_COMMIT, PAGE_READWRITE);
if (NULL == lpBuf)
{
CloseHandle(hProcess);
return FALSE;
}
if (WriteProcessMemory(hProcess, lpBuf, (LPVOID)lpszDll, dwSize, &dwWritten))
{
// 要写入字节数与实际写入字节数不相等,仍属失败
if (dwWritten != dwSize)
{
VirtualFreeEx(hProcess, lpBuf, dwSize, MEM_DECOMMIT);
CloseHandle(hProcess);
return FALSE;
}
}
else
{
CloseHandle(hProcess);
return FALSE;
}
// 使目标进程调用GetModuleHandle,获得DLL在目标进程中的句柄
DWORD dwHandle, dwID;
LPVOID pFunc = GetModuleHandleA;
HANDLE hThread = CreateRemoteThread(hProcess, NULL, 0, (LPTHREAD_START_ROUTINE)pFunc, lpBuf, 0, &dwID);
// 等待GetModuleHandle运行完毕
WaitForSingleObject(hThread, INFINITE);
// 获得GetModuleHandle的返回值
GetExitCodeThread(hThread, &dwHandle);
// 释放目标进程中申请的空间
VirtualFreeEx(hProcess, lpBuf, dwSize, MEM_DECOMMIT);
CloseHandle(hThread);
// 使目标进程调用FreeLibrary,卸载DLL
pFunc = FreeLibrary;
hThread = CreateRemoteThread(hProcess, NULL, 0, (LPTHREAD_START_ROUTINE)pFunc, (LPVOID)dwHandle, 0, &dwID);
// 等待FreeLibrary卸载完毕
WaitForSingleObject(hThread, INFINITE);
CloseHandle(hThread);
CloseHandle(hProcess);
return TRUE;
}
易语言卸载DLL
也是直接调用精易模块封装好的子程序来实现:
注入工具
我用 MFC 写了一个支持64位注入/卸载 DLL 的桌面工具,源码地址:https://gitee.com/zhoujianfeng2016/dllinjector-mfc