DLL动态链接库

/ 0

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-typelib 时,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