TLS 回调中挂钩 LdrLoadDll 实现监视模块加载过程

前言

通过 TLS 回调机制可以实现很多功能,本文简单地从挂钩 LdrLoadDll 函数说起,通过利用 TLS 回调比程序入口代码执行更早的特性,实现监视程序执行期间所有模块加载过程。

1  TLS 技术简介

TLS 全称为 Thread Local Storage,是 Windows 为解决一个进程中多个线程同时访问全局变量而提供的机制。TLS 可以简单地由操作系统代为完成整个互斥过程,也可以由用户自己编写控制信号量的函数。当进程中的线程访问预先制定的内存空间时,操作系统会调用系统默认的或用户自定义的信号量函数,保证数据的完整性与正确性。

1.1  TLS 回调函数

当用户选择使用自己编写的信号量函数时,在应用程序初始化阶段,系统将要调用一个由用户编写的初始化函数以完成信号量的初始化以及其他的一些初始化工作。此调用必须在程序真正开始执行到入口点之前就完成,以保证程序执行的正确性。

TLS回调函数具有如下的函数原型:

void NTAPI CallBackTlsFunc(PVOID Handle, DWORD Reason, PVOID Reserve);

1.2  TLS的数据结构

Windows 的可执行文件为 PE 格式,在 PE 格式中,专门为 TLS 数据开辟了一段空间,具体位置为:

IMAGE_NT_HEADERS.OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_TLS]

其中 DataDirectory 的元素具有如下结构:

typedef struct _IMAGE_DATA_DIRECTORY {
    DWORD   VirtualAddress;
    DWORD   Size;
} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;

对于 TLS 的 DataDirectory 元素,VirtualAddress 成员指向一个结构体,结构体中定义了访问需要互斥的内存地址、TLS 回调函数地址以及其他一些信息。

2 具体实现原理

充分利用 TLS 回调函数在程序入口点之前就能获得程序控制权的特性,在 TLS 回调函数中挂钩 LdrLoadDll 有更好的监视模块加载过程的作用。

2.1 启用 MSVC 链接器的 TLS 选项

Microsoft 提供的 VC 编译器都支持直接在程序中使用 TLS,下文都将使用 MSVC 进行操作。通过以下代码在代码中启用链接器的 TLS 回调选项:

// 告知链接器使用 TLS
#ifdef _WIN64       // 64位
#pragma comment (linker, "/INCLUDE:_tls_used")  
#pragma comment (linker, "/INCLUDE:tls_callback_func") 
#else               // 32位
#pragma comment (linker, "/INCLUDE:__tls_used") 
#pragma comment (linker, "/INCLUDE:_tls_callback_func")
#endif // _WIN64

2.2 添加程序的 TLS 段

通过选项为 TLS 添加固定的段信息,其中,tls_callback_func 数组集合中依次放需要执行的 TLS 回调函数。

#ifdef _WIN64                           // 64位
#pragma const_seg(".CRT$XLF")
EXTERN_C const
#else
#pragma data_seg(".CRT$XLF")        // 32位
EXTERN_C
#endif
PIMAGE_TLS_CALLBACK tls_callback_func[] = { TLS_CALLBACK, 0 }; // 这里填写回调函数名

#ifdef _WIN64                           // 64位
#pragma const_seg()
#else
#pragma data_seg()                  // 32位
#endif //_WIN64

2.3 编写 TLS 回调函数

除了 TLS 段信息,我们还需要对应编写 TLS_CALLBACK 回调函数的实现。例如下面的模式:

// 定义 TLS 回调函数
void NTAPI TLS_CALLBACK(PVOID Dllhandle, DWORD Reason, PVOID Reserved) {
    switch (Reason)
    {
    case DLL_PROCESS_ATTACH: {
        printf("DLL_PROCESS_ATTACH \n");
        break;
    }
    case DLL_THREAD_ATTACH: {
        printf("DLL_THREAD_ATTACH\n");
        break;
    }
    case DLL_THREAD_DETACH: {
        printf("DLL_THREAD_DETACH\n");
        break;
    }
    case DLL_PROCESS_DETACH: {
        printf("DLL_PROCESS_DETACH\n");
        break;
    }
    default:
        break;
    }
}

2.4 监视 Dll 加载过程

模块加载一般通过 LoadLibrary 系列的接口实现。而它们实际上调用的是 LdrLoadDll 函数,通过挂钩 LdrLoadDll 函数可以实现对 LoadLibrary 加载模块的监视和限制。

bats3c 提供了挂钩 LdrLoadDll 的模板代码:Hook LdrLoadDll to whitelist DLLs being loaded into a process · GitHub

其中,蹦床函数利用了将钩子函数的地址写入 r11 通用寄存器,并利用 jmp 无条件跳转到 r11 指向的地址来实现挂钩过程:

Disassembly Report

Raw Hex (zero bytes in bold):

49BB000000000000000041FFE3   

String Literal:

"\x49\xBB\x00\x00\x00\x00\x00\x00\x00\x00\x41\xFF\xE3"

Array Literal:

{ 0x49, 0xBB, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x41, 0xFF, 0xE3 }

Disassembly:

0:  49 bb 00 00 00 00 00    movabs r11,0x0             ; 这里填充钩子函数地址
7:  00 00 00
a:  41 ff e3                          jmp    r11                         ; 实现跳转

看得出这是只适用于 x64 的钩子代码。

挂钩代码如下:

VOID HookLoadDll(LPVOID lpAddr)
{
    DWORD oldProtect;
    void* hLdrLoadDll = &_LdrLoadDll;

    // our trampoline
    unsigned char boing[] = { 
        0x49, 0xbb, 0xde, 0xad, 
        0xc0, 0xde, 0xde, 0xad,
        0xc0, 0xde, 0x41, 0xff,
        0xe3 };

    // add in the address of our hook
    *(void**)(boing + 2) = &_LdrLoadDll;

    // write the hook
    VirtualProtect(lpAddr, 13, PAGE_EXECUTE_READWRITE, &oldProtect);
    memcpy(lpAddr, boing, sizeof(boing));
    VirtualProtect(lpAddr, 13, oldProtect, &oldProtect);

    return;
}

我们只需要获取到 ntdll.dll 的 LdrLoadDll 函数地址,备份并覆盖写入开头 13 个字节即可。

2.5 不要使用 GetProcAddress 函数

GetProcAddress 函数容易在程序初始化时被其他程序,例如调试器、安全软件等挂钩,我们在 TLS 回调或者系统回调中,修改 LdrLoadDll 的过程可能被重定向到伪造的地址上。

怎么检测 GetProcAddress 函数是否被修改?我提供一个简单的检测思路:

// 获取模块地址
HMODULE hNtdll = GetModuleHandleW(L"ntdll.dll");

uint64_t lpAddr = (uint64_t)GetProcAddress(hNtdll, "LdrLoadDll");

if (lpAddr == NULL)   // 检查返回值是否为空
    exit(-1);

/* 下面的代码是为了验证 GetProcAddress 是否被篡改的,
 * 如果使用自定义的搜索函数,则不需要下面的代码。
*/

uint64_t opCode = RealProc & 0x700000000000u;
if (opCode != 0x700000000000u)
{
     MessageBoxW(NULL, L"有程序篡改 GetProcAddress。", L"提示", MB_OK);
     exit(-1);
}

在这里,我们主要根据加载基址的特征,验证实际加载地址是否从 0x70000000 开始。这种验证可能不是很精确。

所以,我们最好的方法就是重写 GetProcAddress 的实现了,有很多方法重写这个函数,这里给出一种模板:

/// 
/// MyGetProcAddress64 是 64 位下,获取模块中函数入口地址的函数
/// 替代 GetProcAddress 函数。
/// 
/// 
/// 
/// 
UINT64 MyGetProcAddress64(PVOID BaseAddress, LPCSTR FunName)
{
    HANDLE hMod = NULL;
    IMAGE_DOS_HEADER dosheader = { 0 };
    IMAGE_OPTIONAL_HEADER64 opthdr = { 0 };// IMAGE_OPTIONAL_HEADER  -> 32
    IMAGE_EXPORT_DIRECTORY exports = { 0 };
    USHORT index = 0;
    ULONG addr = 0, j = 0;
    char pFuncName[30] = { 0 };
    PULONG pAddressOfFunctions = NULL;
    PULONG pAddressOfNames = NULL;
    PUSHORT pAddressOfNameOrdinals = NULL;
    int64_t handle = 0;

    // 获取模块基址
    if (!BaseAddress) return 0;

    // 使用 0xff 构造 long long 类型全1的数字,作为当前进程的句柄
    // 通过位操作将每个字节设置为0xff
    for (int i = 0; i < sizeof(long long); i++) {
        handle = (handle << 8) | 0xff;
    }
    // 获取PE头
    hMod = BaseAddress;
    ReadProcessMemory((HANDLE)handle, hMod,
        &dosheader, sizeof(IMAGE_DOS_HEADER), 0);
    ReadProcessMemory((HANDLE)handle,
        (BYTE*)hMod + dosheader.e_lfanew + 24,
        &opthdr, sizeof(IMAGE_OPTIONAL_HEADER), 0);

    // 查找导出表 
    ReadProcessMemory((HANDLE)handle,
        ((BYTE*)hMod + opthdr.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress),
        &exports, sizeof(IMAGE_EXPORT_DIRECTORY), 0);

    pAddressOfFunctions = (ULONG*)((BYTE*)hMod + exports.AddressOfFunctions);
    pAddressOfNames = (ULONG*)((BYTE*)hMod + exports.AddressOfNames);
    pAddressOfNameOrdinals = (USHORT*)((BYTE*)hMod + exports.AddressOfNameOrdinals);

    // 对比函数名 
    for (j = 0; j < exports.NumberOfNames; j++)
    {
        ReadProcessMemory((HANDLE)handle, pAddressOfNameOrdinals + j,
            &index, sizeof(USHORT), 0);
        ReadProcessMemory((HANDLE)handle, pAddressOfFunctions + index,
            &addr, sizeof(ULONG), 0);

        ULONG a = 0;
        ReadProcessMemory((HANDLE)handle, pAddressOfNames + j,
            &a, sizeof(ULONG), 0);
        ReadProcessMemory((HANDLE)handle, (BYTE*)hMod + a,
            pFuncName, 30, 0);
        ReadProcessMemory((HANDLE)handle, pAddressOfFunctions + index,
            &addr, sizeof(ULONG), 0);

        if (!_stricmp(pFuncName, FunName))
        {
            UINT64 funAddr = (UINT64)BaseAddress + addr;
            return funAddr;
        }
    }
    return 0;
}

这样就不用担心 GetProcAddress 会在加载期间被其他程序修改了。

2.6 LdrLoadDll 钩子函数中实现监视功能

编写钩子例程,cNotAllowDlls 列表相当于是黑名单,当然你也可以设置白名单,这将禁止/允许一些模块的加载。

NTSTATUS __stdcall _LdrLoadDll(
    PWSTR SearchPath OPTIONAL, 
    PULONG DllCharacteristics OPTIONAL, 
    PUNICODE_STRING DllName, 
    PVOID* BaseAddress)
{
    INT i = 0;
    DWORD dwOldProtect = 0;
    BOOL bAllow = FALSE;
    //DWORD dwbytesWritten = 0;
    CHAR cDllName[MAX_PATH] = { 0 };

    sprintf_s(cDllName, "%S", DllName->Buffer);

    for (i = 0; i < dwNotAllowDllCount; i++)
    {
        if (strstr(cDllName, cNotAllowDlls[i]) == 0)
        {
            bAllow = TRUE;

            printf("Loading DLL: %s\n", cDllName);

            VirtualProtect(lpAddr, sizeof(OriginalBytes),
                PAGE_EXECUTE_READWRITE, &dwOldProtect);
            memcpy(lpAddr, OriginalBytes, sizeof(OriginalBytes));
            VirtualProtect(lpAddr, sizeof(OriginalBytes),
                dwOldProtect, &dwOldProtect);

            LdrLoadDll_ LdrLoadDll = (LdrLoadDll_)
                MyGetProcAddress64(GetModuleHandleW(L"ntdll.dll"), "LdrLoadDll");
            if (LdrLoadDll == NULL)
                exit(-1);

            LdrLoadDll(SearchPath, DllCharacteristics, DllName, BaseAddress);

            HookLoadDll(lpAddr);
        }

    }

    if (!bAllow)
    {
        printf("Blocked DLL: %s\n", cDllName);
    }

    return TRUE;
}

3 完整代码和效果展示

下面给出完整的模板代码,仅供参考。

#include 
#include 
#include 
#include 

// 告知链接器使用 TLS
#ifdef _WIN64       // 64位
#pragma comment (linker, "/INCLUDE:_tls_used")  
#pragma comment (linker, "/INCLUDE:tls_callback_func") 
#else               // 32位
#pragma comment (linker, "/INCLUDE:__tls_used") 
#pragma comment (linker, "/INCLUDE:_tls_callback_func")
#endif // _WIN64

#define dwNotAllowDllCount 1
CHAR cNotAllowDlls[dwNotAllowDllCount][MAX_PATH] = {
                  "XXXX填写Dll路径或名称中的关键词"
};

VOID HookLoadDll(LPVOID lpAddr);
NTSTATUS __stdcall _LdrLoadDll(
    PWSTR SearchPath OPTIONAL, 
    PULONG DllCharacteristics OPTIONAL, 
    PUNICODE_STRING DllName, 
    PVOID* BaseAddress);

typedef void (WINAPI* LdrLoadDll_) (PWSTR SearchPath OPTIONAL,
    PULONG DllCharacteristics OPTIONAL,
    PUNICODE_STRING DllName,
    PVOID* BaseAddress);

LPVOID lpAddr;
CHAR OriginalBytes[50] = { 0 };
CHAR* gGlobalCheckSum = nullptr;

/// 
/// MyGetProcAddress64 是 64 位下,获取模块中函数入口地址的函数
/// 替代 GetProcAddress 函数。
/// 
/// 
/// 
/// 
UINT64 MyGetProcAddress64(PVOID BaseAddress, LPCSTR FunName)
{
    HANDLE hMod = NULL;
    IMAGE_DOS_HEADER dosheader = { 0 };
    IMAGE_OPTIONAL_HEADER64 opthdr = { 0 };// IMAGE_OPTIONAL_HEADER  -> 32
    IMAGE_EXPORT_DIRECTORY exports = { 0 };
    USHORT index = 0;
    ULONG addr = 0, j = 0;
    char pFuncName[30] = { 0 };
    PULONG pAddressOfFunctions = NULL;
    PULONG pAddressOfNames = NULL;
    PUSHORT pAddressOfNameOrdinals = NULL;
    int64_t handle = 0;

    // 获取模块基址
    if (!BaseAddress) return 0;


    // 使用 0xff 构造 long long 类型全1的数字,作为当前进程的句柄
    // 通过位操作将每个字节设置为0xff
    for (int i = 0; i < sizeof(long long); i++) {
        handle = (handle << 8) | 0xff;
    }
    // 获取PE头
    hMod = BaseAddress;
    ReadProcessMemory((HANDLE)handle, hMod,
        &dosheader, sizeof(IMAGE_DOS_HEADER), 0);
    ReadProcessMemory((HANDLE)handle,
        (BYTE*)hMod + dosheader.e_lfanew + 24,
        &opthdr, sizeof(IMAGE_OPTIONAL_HEADER), 0);


    // 查找导出表 
    ReadProcessMemory((HANDLE)handle,
        ((BYTE*)hMod + opthdr.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress),
        &exports, sizeof(IMAGE_EXPORT_DIRECTORY), 0);

    pAddressOfFunctions = (ULONG*)((BYTE*)hMod + exports.AddressOfFunctions);
    pAddressOfNames = (ULONG*)((BYTE*)hMod + exports.AddressOfNames);
    pAddressOfNameOrdinals = (USHORT*)((BYTE*)hMod + exports.AddressOfNameOrdinals);

    // 对比函数名 
    for (j = 0; j < exports.NumberOfNames; j++)
    {
        ReadProcessMemory((HANDLE)handle, pAddressOfNameOrdinals + j,
            &index, sizeof(USHORT), 0);
        ReadProcessMemory((HANDLE)handle, pAddressOfFunctions + index,
            &addr, sizeof(ULONG), 0);

        ULONG a = 0;
        ReadProcessMemory((HANDLE)handle, pAddressOfNames + j,
            &a, sizeof(ULONG), 0);
        ReadProcessMemory((HANDLE)handle, (BYTE*)hMod + a,
            pFuncName, 30, 0);
        ReadProcessMemory((HANDLE)handle, pAddressOfFunctions + index,
            &addr, sizeof(ULONG), 0);

        if (!_stricmp(pFuncName, FunName))
        {
            UINT64 funAddr = (UINT64)BaseAddress + addr;
            return funAddr;
        }
    }
    return 0;
}


NTSTATUS __stdcall _LdrLoadDll(
    PWSTR SearchPath OPTIONAL, 
    PULONG DllCharacteristics OPTIONAL, 
    PUNICODE_STRING DllName, 
    PVOID* BaseAddress)
{
    INT i = 0;
    DWORD dwOldProtect = 0;
    BOOL bAllow = FALSE;
    //DWORD dwbytesWritten = 0;
    CHAR cDllName[MAX_PATH] = { 0 };

    sprintf_s(cDllName, "%S", DllName->Buffer);

    for (i = 0; i < dwNotAllowDllCount; i++)
    {
        if (strstr(cDllName, cNotAllowDlls[i]) == 0)
        {
            bAllow = TRUE;

            printf("Loading DLL: %s\n", cDllName);

            VirtualProtect(lpAddr, sizeof(OriginalBytes),
                PAGE_EXECUTE_READWRITE, &dwOldProtect);
            memcpy(lpAddr, OriginalBytes, sizeof(OriginalBytes));
            VirtualProtect(lpAddr, sizeof(OriginalBytes),
                dwOldProtect, &dwOldProtect);

            LdrLoadDll_ LdrLoadDll = (LdrLoadDll_)
                MyGetProcAddress64(GetModuleHandleW(L"ntdll.dll"), "LdrLoadDll");
            if (LdrLoadDll == NULL)
                exit(-1);

            LdrLoadDll(SearchPath, DllCharacteristics, DllName, BaseAddress);

            HookLoadDll(lpAddr);
        }

    }

    if (!bAllow)
    {
        printf("Blocked DLL: %s\n", cDllName);
    }

    return TRUE;
}

VOID HookLoadDll(LPVOID lpAddr)
{
    DWORD oldProtect;
    void* hLdrLoadDll = &_LdrLoadDll;

    // our trampoline
    unsigned char boing[] = { 
        0x49, 0xbb, 0xde, 0xad, 
        0xc0, 0xde, 0xde, 0xad,
        0xc0, 0xde, 0x41, 0xff,
        0xe3 };

    // add in the address of our hook
    *(void**)(boing + 2) = &_LdrLoadDll;

    // write the hook
    VirtualProtect(lpAddr, 13, PAGE_EXECUTE_READWRITE, &oldProtect);
    memcpy(lpAddr, boing, sizeof(boing));
    VirtualProtect(lpAddr, 13, oldProtect, &oldProtect);

    return;
}


void NTAPI TLS_CALLBACK1(PVOID DllHandle, DWORD Reason, PVOID Reserved)
{
    if (Reason == DLL_PROCESS_ATTACH)
    {
        printf("LdrLoadDll hook example - @Lianyou516.\n\n");

        // get addresss of where the hook should be
        HMODULE hNtdll = GetModuleHandleW(L"ntdll.dll");
        lpAddr = (LPVOID)MyGetProcAddress64(hNtdll, "LdrLoadDll");

        if (lpAddr == NULL)
            exit(-1);

        /* 下面的代码是为了验证 GetProcAddress 是否被篡改的,
         * 如果使用自定义的搜索函数,则不需要下面的代码。
        */
        uint64_t RealProc = (uint64_t)GetProcAddress(hNtdll, "LdrLoadDll");
        uint64_t opCode = RealProc & 0x700000000000u;
        if (opCode != 0x700000000000u)
        {
            MessageBoxW(NULL, L"有程序篡改 GetProcAddress。", L"提示", MB_OK);
            exit(-1);
        }

        // save the original bytes
        memcpy(OriginalBytes, lpAddr, 50);

        // set the hook
        HookLoadDll(lpAddr);
    }
}

void NTAPI TLS_CALLBACK2(PVOID DllHandle, DWORD Reason, PVOID Reserved)
{
    if (Reason == DLL_PROCESS_ATTACH)
    {
        gGlobalCheckSum = (CHAR*)malloc(10);
        if (!gGlobalCheckSum)
            exit(-1);
    }
}


#ifdef _WIN64                           // 64位
#pragma const_seg(".CRT$XLF")
EXTERN_C const
#else
#pragma data_seg(".CRT$XLF")        // 32位
EXTERN_C
#endif
PIMAGE_TLS_CALLBACK tls_callback_func[] = { TLS_CALLBACK1, TLS_CALLBACK2, 0 };

#ifdef _WIN64                           // 64位
#pragma const_seg()
#else
#pragma data_seg()                  // 32位
#endif //_WIN64


int main(void)
{
    gGlobalCheckSum[6] = 12;  // 删除 TLS 回调就会引发异常
    LoadLibraryW(L"taskmgrhook.dll");  // 测试加载模块
    MessageBoxW(NULL, L"Main函数执行", L"提示", MB_OK);  // 测试函数执行
    system("pause");
    return 0;
}

测试运行效果如下:

TLS 回调中挂钩 LdrLoadDll 实现监视模块加载过程_第1张图片

TLS 回调中挂钩 LdrLoadDll 实现监视模块加载过程_第2张图片

总结&后记

本文简单地从挂钩 LdrLoadDll 函数说起,通过利用 TLS 回调比程序入口代码执行更早的特性,实现监视程序执行期间所有模块加载过程。代码写的很粗略,仅供参考思路使用。


更新于:2024.01.24

你可能感兴趣的:(Windows,基础编程,windows,微软,安全)