Windows 中的三种常用 DLL 注入技术

Windows 中的三种常用 DLL 注入技术

目录

Windows 中的三种常用 DLL 注入技术

一、前言——DLL 注入技术的用途

二、DLL 注入基础

2.1 进程虚拟地址空间

2.2 读写其他进程的内存

2.2.1 实践

2.2.2 主要代码

2.3 LoadLibraryW 函数

三、APC 注入

3.1 何为 APC

3.2 关键 API

3.3 实践

3.4 进程被注入 DLL 判断

四、远程线程注入

4.1 何为远程线程

4.2 关键 API

4.3 实践

4.4 主要代码

五、Windows 钩子

5.1 何为钩子

5.2 钩子链

5.3 钩子过程

5.4 钩子类型

六、全局钩子注入

6.1 关键 API

6.2 实践

6.3 主要代码

七、总结

参考资料


本文中代码仓库地址:

https://gitee.com/langshanglibie/dll-injection

一、前言——DLL 注入技术的用途

在 Windows 中,每个进程都有自己私有的虚拟地址空间。所以,一个进程没法访问另一个进程的内存。

但是,很多时候我们还是需要跨越进程的边界,来访问另一个进程的地址空间,比如:

  1. 获取其他进程的信息,如加载了哪些 DLL
  2. 对其他进程的某些操作进行拦截
  3. 从另一个进程创建的窗口来派生子类窗口,比如附着在资源管理器上的一些小插件等
  4. 藏身于别的进程,以达到隐藏自己的目的
  5. 假借其他进程之名做某些事情

二、DLL 注入基础

2.1 进程虚拟地址空间

Windows 中每个进程都有自己的虚拟地址空间。

32位进程:4GB,0x00000000~0xFFFFFFFF

64位进程:16EB,0x00000000'00000000~0xFFFFFFFF'FFFFFFFF

因为每个进程都有自己专有的虚拟地址空间,当进程中的各线程运行时,他们只能访问自己进程的内存。

线程既看不到属于其他进程的内存,也无法访问他们,更不能修改他们。

Windows 中的三种常用 DLL 注入技术_第1张图片

进程 A 可以在位于 0x12345678 地址处存储一个数据,进程 B 也可以在自己的地址空间中相同地址 0x12345678 处存储一个数据。

 当进程 A 中的线程访问位于地址 0x12345678 处的内存时,它访问的是进程 A 的数据。

进程 B 也一样,二者互不影响。

因为每个进程都有自己的地址空间,一个应用程序破坏另外一个应用程序的可能性就非常小,从而使得整个系统更加稳固。

2.2 读写其他进程的内存

Windows 提供了一组 API 读写其他进程的内存,甚至在其他进程的内存中分配内存。

OpenProcess

打开一个已存在的进程对象,并返回进程的句柄

VirtualAllocEx

在指定进程的虚拟地址空间中分配内存。

WriteProcessMemory

将当前进程中的地址空间中的数据,拷贝至指定进程的地址空间中。

ReadProcessMemory

将指定进程的指定范围内的地址空间内的数据,拷贝至当前进程的地址空间中。

HANDLE OpenProcess(DWORD dwDesiredAccess, BOOL bInheritHandle, DWORD dwProcessId);

dwDesiredAccess

欲获取的权限,一般传递 PROCESS_ALL_ACCESS 获取所有权限。

bInheritHandle

如果为 TRUE,返回的句柄可以被子进程继承,否则不能。

dwProcessId

欲打开的进程的ID。

返回值

如成功,返回值为指定进程的句柄。

如失败,返回值为 NULL

LPVOID VirtualAllocEx(HANDLE hProcess, LPVOID lpAddress, SIZE_T dwSize, DWORD flAllocationType, DWORD flProtect);

hProcess

欲在其中分配内存的进程的句柄。

lpAddress

指定从哪个地址开始分配内存,一般用 NULL 来自动分配。

dwSize

欲分配的内存的字节数。

flAllocationType

两个常用的值:

MEM_RESERVE预订进程的虚拟地址空间,而不调拨任何物理存储器。

MEM_COMMIT预订进程的虚拟地址空间,并且从物理内存或磁盘上的页交换文件中调拨物理存储器。

经常是两个一起组合使用 MEM_RESERVE | MEM_COMMIT,在一步中完成预订地址空间和调拨物理存储器。

返回值

执行成功就返回分配内存的起始地址,否则 NULL。

BOOL WriteProcessMemory(HANDLE hProcess, LPVOID lpBaseAddress, LPCVOID lpBuffer, SIZE_T nSize, SIZE_T * lpNumberOfBytesWritten);

hProcess

欲向其中写入数据的的目标进程的句柄,一般为 OpenProcess 函数的返回值。

lpBaseAddress

目标进程中的内存地址。

lpBuffer

本进程中的要写入数据的内存地址。

nSize

要写入的字节数。

返回值

非零代表成功,零代表失败。

BOOL ReadProcessMemory(HANDLE hProcess, LPCVOID lpBaseAddress, LPVOID lpBuffer, SIZE_T nSize, SIZE_T * lpNumberOfBytesRead);

hProcess

欲从其中读取数据的的目标进程的句柄,一般为 OpenProcess 函数的返回值。

lpBaseAddress

目标进程中的内存地址。

lpBuffer

本进程中存储要读取数据的内存地址。

nSize

要读取的字节数。

lpNumberOfBytesRead

实际读取到的字节数。

返回值

非零代表成功,零代表失败。

2.2.1 实践

向其他进程写入"Hello, world!",再修改掉,再读取修改后的内容。

2.2.2 主要代码

// 根据进程名获取进程 ID

const DWORD dwProcessId = GetProcessIdByName(processName);

// 在其他进程中分配的内存地址

LPVOID pAddress = nullptr;

// 向其它进程写入数据

{

    const char buffer[] = "Hello, world!"// 准备向其他进程中写入的字符串

    const SIZE_T bufferSize = sizeof(buffer);

    pAddress = WriteProcessMemory(dwProcessId, (const LPVOID)(buffer), bufferSize);

    if (pAddress == nullptr)

    {

       LOG("Write failed");

        return 0;

    }

    std::cout<< pAddress << std::endl;

}

// 从其它进程读取数据

{

    char readBbuffer[100] = "";

    SIZE_T readBbufferSize = sizeof(readBbuffer);

    const bool bSuccess = ReadProcessMemory(dwProcessId, pAddress,

             (const LPVOID)(readBbuffer),

             readBbufferSize);

    if (!bSuccess)

    {

        LOG("Read failed");

        return 0;

    }

    LOG(readBbuffer);

}

封装函数 WriteProcessMemory 的实现

LPVOID WriteProcessMemory(DWORD dwProcessId, const LPVOID pBuffer, SIZE_T bufferSize)

{

    if (pBuffer == nullptr || bufferSize == 0)

    {

        return nullptr;

    }

    // 打开注入进程

    const HANDLE hProcess = ::OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwProcessId);

    if (hProcess == nullptr)

    {

        return nullptr;

    }

    // 在注入进程申请内存

    const LPVOID pAddress = ::VirtualAllocEx(hProcess,

                                      NULL,

                                      bufferSize,

                                      MEM_COMMIT | MEM_RESERVE,

                                      PAGE_EXECUTE_READWRITE);

    if (pAddress == nullptr)

    {

        ::CloseHandle(hProcess);

        return nullptr;

    }

    // 向申请的内存中写入数据

    SIZE_T numberOfBytesWritten = 0;

    const BOOL bSuccess = ::WriteProcessMemory(hProcess,

                                                                            pAddress,

                                                                            pBuffer,

                                                                            bufferSize,

                                                                            &numberOfBytesWritten);

    if (!bSuccess || numberOfBytesWritten != bufferSize)

    {

        ::VirtualFreeEx(hProcess, pAddress, 0, MEM_RELEASE);

        ::CloseHandle(hProcess);

        return nullptr;

    }

    ::CloseHandle(hProcess);

    return pAddress;

}

封装函数 ReadProcessMemory 的实现

bool ReadProcessMemory(DWORD dwProcessId, const LPVOID pAddress, LPVOID pBuffer, SIZE_T bufferSize)

{

    if (pBuffer == nullptr || bufferSize == 0)

    {

        return false;

    }

    // 打开注入进程

    const HANDLE hProcess = ::OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwProcessId);

    if (hProcess == nullptr)

    {

        return false;

    }

    // 读取注入进程的内存

    SIZE_T numberOfBytesRead = 0;

    const BOOL bSuccess = ::ReadProcessMemory(hProcess,

                                                                                   pAddress,

                                                                                   pBuffer,

                                                                                   bufferSize,

                                                                                   &numberOfBytesRead);

    if (!bSuccess || numberOfBytesRead != bufferSize)

    {

        ::CloseHandle(hProcess);

        return false;

    }

    ::CloseHandle(hProcess);

    return true;

}

2.3 LoadLibraryW 函数

进行 APC 注入、远程线程注入时,都需要传递一个函数地址到目标进程中,分别为 APC 函数和远程线程函数。

为了实现 DLL 注入,需要:

1. 我们要在这个函数中实现加载注入 DLL。

2. 调用 VirtualAllocEx 在目标进程中分配一段能够容纳得下这个函数代码的内存,假如返回地址为 0x1234567

3. 然后调用 WriteProcessMemory 把这个函数代码拷贝到目标进程中以 0x1234567 为起始的内存中 。

0x1234567 这个地址就是后面进行 APC 注入、远程线程注入时传递的函数地址。

上面的过程略显麻烦。

幸运的是,Windows 系统 kernel32.dll 中有 2 个导出函数 LoadLibraryALoadLibraryW

我们看看其中 LoadLibraryW 的函数原型。

HMODULE WINAPI LoadLibraryW(LPCWSTR lpLibFileName);

再看看 APC 函数、远程线程函数原型。

typedef VOID (NTAPI *PAPCFUNC)(ULONG_PTR Parameter);

typedef DWORD (WINAPI *PTHREAD_START_ROUTINE)(LPVOID lpThreadParameter);

我们发现,这两个函数原型都可以兼容、接受 LoadLibraryW 函数。

并且,LoadLibraryW 在所有相同位数(32位、64位)的进程中地址一致。

所以,综上所述,我们不用另外实现 APC 函数和远程线程函数,直接将在本进程中通过 GetProcAddress 获取到的 LoadLibraryW 函数地址传递过去即可,省了不少工夫。

Windows 中的三种常用 DLL 注入技术_第2张图片

三、APC 注入

3.1 何为 APC

一个 APC(asynchronous procedure call,异步过程调用)是指一个在线程的上下文中异步运行的函数

由系统生成的 APC 称为内核模式 APC。

由应用程序生成的 APC 称为用户模式 APC。

每个线程都有一个 APC 队列。

当一个用户模式的 APC 被插入到线程的队列时,该线程不会立即调用该 APC 函数,直到线程处于警戒状态(alertable state

当一个线程调用以下等待类函数时(如果是前四个函数, 需要传递 TRUEbAlertable 参数),会进入该状态。

  1. DWORD SleepEx(  [in] DWORD dwMilliseconds,  [in] BOOL  bAlertable);
  2. DWORD SignalObjectAndWait(  [in] HANDLE hObjectToSignal,  [in] HANDLE hObjectToWaitOn,  [in] DWORD  dwMilliseconds,  [in] BOOL   bAlertable);
  3. DWORD WaitForMultipleObjectsEx(  [in] DWORD        nCount,  [in] const HANDLE *lpHandles,  [in] BOOL         bWaitAll,  [in] DWORD        dwMilliseconds,  [in] BOOL         bAlertable);
  4. DWORD WaitForSingleObjectEx(  [in] HANDLE hHandle,  [in] DWORD  dwMilliseconds,  [in] BOOL   bAlertable);
  5. DWORD MsgWaitForMultipleObjectsEx(  [in] DWORD        nCount,  [in] const HANDLE *pHandles,  [in] DWORD        dwMilliseconds,  [in] DWORD        dwWakeMask,  [in] DWORD        dwFlags);

进入警戒状态后,线程会按照先进先出的顺序,调用 APC 函数。

之前调用的等待类函数会返回 WAIT_IO_COMPLETION

Windows 中的三种常用 DLL 注入技术_第3张图片

应用程序通过调用 QueueUserAPC 函数将 APC 添加到线程的队列。

调用 QueueUserAPC 时,需要传递 APC 函数的地址。

3.2 关键 API

DWORD QueueUserAPC(PAPCFUNC pfnAPC, HANDLE hThread, ULONG_PTR dwData);

pfnAPC

指向 APC 函数的指针

hThread

要插入 APC 的目标线程的句柄,可以是其他进程的线程

dwData

传给 pfnAPC 参数指向的 APC 函数的自定义参数

返回值

非零代表成功,零代表失败。

3.3 实践

使用 APC 向指定进程的线程注入 TestDll.dll,然后显示被注入进程加载的所有模块及其基地址。

3.4 进程被注入 DLL 判断

Process hacker 查看可疑模块。

Windows 中的三种常用 DLL 注入技术_第4张图片

3.5 主要代码

Windows 中的三种常用 DLL 注入技术_第5张图片

bool ApcInjectDll(const TCHAR *pszProcessName, const TCHAR* pszDllFileName)

{

    // 根据进程名获取进程 ID

    const DWORD dwProcessId = GetProcessIdByName(pszProcessName);

    if (dwProcessId <= 0)

    {

        LOG("GetProcessIdByName failed");

        return false;

    }

    // 根据进程 ID 获取所有线程 ID

    DWORD* pThreadIdArray = nullptr;

    DWORD dwThreadIdLength = 0;

    #define FREE_THREAD_ID_ARRAY if (pThreadIdArray != nullptr) delete[] pThreadIdArray;

   

    if (!GetAllThreadIdByProcessId(dwProcessId, &pThreadIdArray, &dwThreadIdLength))

    {

        LOG("GetAllThreadIdByProcessId failed");

        return false;

    }

    // 在注入进程中申请内存、向申请的内存中写入欲注入的 DLL 的全路径

    const SIZE_T dwDllPathLen = (::lstrlen(pszDllFileName) + 1) * sizeof(pszDllFileName[0]);

    const LPVOID pDllPathAddress = WriteProcessMemory(dwProcessId,

                               (const LPVOID)pszDllFileName,

                               dwDllPathLen);

    if (pDllPathAddress == nullptr)

    {

        FREE_THREAD_ID_ARRAY

        LOG("WriteProcessMemory failed");

        return false;

    }

    // 获取 kernel32.dll 中 LoadLibraryW 函数的地址,注意不是 LoadLibrary !

    PVOID pLoadLibraryAFunc = ::GetProcAddress(::GetModuleHandle(_T("kernel32.dll")), "LoadLibraryW");

    if (pLoadLibraryAFunc == nullptr)

    {

        FREE_THREAD_ID_ARRAY

        LOG("GetProcessAddress failed");

        return false;

    }

    // 遍历线程,插入 APC

    for (DWORD i = 0; i < dwThreadIdLength; ++i)

    {

        // 打开线程

        const HANDLE hThread = ::OpenThread(THREAD_ALL_ACCESS, FALSE, pThreadIdArray[i]);

        if (hThread != nullptr)

        {

            // 插入 APC

           ::QueueUserAPC((PAPCFUNC)pLoadLibraryAFunc, hThread, (ULONG_PTR)pDllPathAddress);

            // 关闭线程句柄

           ::CloseHandle(hThread);

        }

    }

    // 释放线程 ID 数组

    FREE_THREAD_ID_ARRAY

    return true;

}

四、远程线程注入

4.1 何为远程线程

远程线程是指某个进程在其他进程的虚拟地址空间中创建并运行的线程。

创建远程线程时,可以指定线程函数、也可以传递自定义参数。

4.2 关键 API

HANDLE

CreateRemoteThread(

    HANDLE hProcess,

    LPSECURITY_ATTRIBUTES lpThreadAttributes,

    SIZE_T dwStackSize,

    LPTHREAD_START_ROUTINE lpStartAddress,

    LPVOID lpParameter,

    DWORD dwCreationFlags,

    LPDWORD lpThreadId

);

除了多了第一个参数 hProcess 外,其余参数和常用的在本进程创建线程的函数 CreateThread 一模一样。

hProcess

远程线程所属进程的句柄。

4.3 实践

在指定进程中创建远程线程,注入 TestDll.dll,然后显示被注入进程加载的所有模块及其基地址。

4.4 主要代码

Windows 中的三种常用 DLL 注入技术_第6张图片

bool CreateRemoteThreadInjectDll(const TCHAR* pszProcessName, const TCHAR* pszDllFileName)

{

    // 根据进程名获取进程 ID

    const DWORD dwProcessId = GetProcessIdByName(pszProcessName);

    if (dwProcessId <= 0)

    {

        LOG("GetProcessIdByNamefailed");

        return false;

    }

    // 打开注入进程,获取进程句柄

    const HANDLE hProcess = ::OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwProcessId);

    if (hProcess == nullptr)

    {

        LOG("OpenProcess failed");

        return false;

    }

    // 在注入进程申请内存、向申请的内存中写入数据

    const SIZE_T dwDllPathLen = (::lstrlen(pszDllFileName) + 1) * sizeof(pszDllFileName[0]);

    const LPVOID pDllPathAddress = WriteProcessMemory(hProcess,

                                (const LPVOID)pszDllFileName,

                                dwDllPathLen);

    if (pDllPathAddress == nullptr)

    {

        LOG("WriteProcessMemoryfailed");

        return false;;

    }

    // 获取 LoadLibraryW 函数地址

    const FARPROC pFuncProcAddr = ::GetProcAddress(::GetModuleHandle(_T("kernel32.dll")), "LoadLibraryW");

    if (pFuncProcAddr == nullptr)

    {

        LOG("GetProcAddressLoadLibraryW failed");

        return false;

    }

    // 使用 CreateRemoteThread 创建远程线程,创建完立即执行,实现 DLL 注入

    const HANDLE hRemoteThread = ::CreateRemoteThread(hProcess,

                                                                                                   nullptr,

                                                                                                   0,

                                                                                                  (LPTHREAD_START_ROUTINE)pFuncProcAddr,

                                                                                                   pDllPathAddress,

                                                                                                   0,

                                                                                                   nullptr);

    if (hRemoteThread == nullptr)

    {

        LOG("CreateRemoteThreadfailed");

        return false;

    }

    // 关闭句柄

    ::CloseHandle(hProcess);

    ::CloseHandle(hRemoteThread);

    return true;

}

五、Windows 钩子

5.1 何为钩子

钩子(Hook)是操作系统提供的一种机制,应用程序通过它可以监视各种事件,例如鼠标操作、键盘操作、窗口的创建、关闭等。

钩子往往会减慢系统的速度,因为它们延长了系统对每个事件的处理流程。所以应该只在必要时安装挂钩,用完尽快将其移除。

5.2 钩子链

Windows 系统支持很多不同类型的钩子。每种类型监视系统消息处理机制的不同方面。例如,应用程序可以使用 WH_MOUSE 钩子来监视鼠标消息。

系统为每种类型的钩子都维护一个单独的钩子链。

钩子链是一个包含应用程序定义的回调函数的列表,这个回调函数就是钩子过程。

当出现与某种类型的钩子相关联的消息时,系统会将该消息一个接一个地传递给该类型的钩子链中的每一个钩子过程。

线程按照后进先出的顺序调用钩子链中的钩子过程。

Windows 中的三种常用 DLL 注入技术_第7张图片

5.3 钩子过程

为了使用钩子,开发人员需要提供了一个钩子过程,并使用 SetWindowsHookEx 函数将其安装到该类型的钩子链中。钩子过程必须具有以下函数原型:

LRESULT CALLBACK HookProc(int nCode, WPARAM wParam, LPARAM lParam)

{

    // process event

    ...

    return CallNextHookEx(NULL, nCode, wParam, lParam);

}

nCode 参数是钩子过程用来确定要执行的操作的钩子代码,它的值取决于钩子的类型。每种类型的钩子都有自己的钩子代码集合。

wParamlParam 参数的值取决于 nCode,但它们通常包含与被发送的消息相关的信息。

SetWindowsHookEx 函数始终在钩子链的开头安装钩子过程。

当发生某种类型的钩子监视的事件时,系统先调用该类型钩子链中开头的钩子过程。

链中的每个钩子过程决定是否将事件传递给下一个过程。钩子过程通过调用 CallNextHookEx 函数将事件传递给下一个钩子过程。

请注意,某些类型的钩子的钩子过程只能监视消息,系统会将消息传递给每个钩子过程,而不管上一个钩子过程是否调用了 CallNextHookEx

某些类型的钩子的钩子过程只能监视消息,不能修改消息,或阻止系统将它们传到下一个钩子过程或目标窗口,比如 WH_CALLWNDPROC

全局钩子:监视系统中所有线程的消息。

线程钩子:只监视单个线程的消息。

全局钩子的钩子过程可以在任何应用程序中调用,因此该钩子过程必须位于独立的 DLL 中。

线程钩子的钩子过程仅在所监视的线程的中调用。如果应用程序为自己的一个线程安装了钩子过程,钩子过程可以与应用程序的其余代码位于同一模块中,也可以位于独立的 DLL 中。但是,如果应用程序为其它应用程序的某个线程安装了钩子过程,则该过程必须位于独立 DLL 中。

5.4 钩子类型

每种类型的钩子都使应用程序能够监视系统消息处理机制的不同方面。

以下是 Windows 中支持的钩子类型,共 15 种。

  1. WH_CALLWNDPROC 和 WH_CALLWNDPROCRET
  2. WH_CBT
  3. WH_DEBUG
  4. WH_FOREGROUNDIDLE
  5. WH_GETMESSAGE
  6. WH_JOURNALPLAYBACK
  7. WH_JOURNALRECORD
  8. WH_KEYBOARD_LL
  9. WH_KEYBOARD
  10. WH_MOUSE_LL
  11. WH_MOUSE
  12. WH_MSGFILTER 和 WH_SYSMSGFILTER
  13. WH_SHELL

WH_CALLWNDPROC & WH_CALLWNDPROCRET

能够监视发送到窗口过程的消息。

系统在将消息传递给接收窗口的窗口过程之前调用 WH_CALLWNDPROC 钩子过程,并在窗口过程处理完消息之后调用 WH_CALLWNDPROCRET 钩子过程。

比如:SendMessage(hWnd, WM_CLOSE, 0, 0);

WH_GETMESSAGE

当其它线程使用 GetMessagePeekMessage 函数取回线程消息队列中的消息之后,但是在开始处理之前,系统会将这个消息传给 WH_GETMESSAGE 钩子。

比如:PostMessage(hWnd, WM_CLOSE, 0, 0)

VS 中的 Spy++ 工具能够监视窗口消息,就是安装了上面三个钩子。

Windows 中的三种常用 DLL 注入技术_第8张图片  

WH_MOUSE_LL & WH_KEYBOARD_LL

有任何的鼠标和键盘操作时,系统会调用这两个钩子。

罗技项目中使用了这两个钩子,只要用户操作了鼠标或键盘,就停止语音识别。

WH_DEBUG

在系统调用任何其他类型钩子的钩子过程之前,系统都会先调用 WH_DEBUG 类型的钩子过程。

可以使用这个钩子来决定是否允许系统调用其他类型钩子的钩子过程。

六、全局钩子注入

Windows 中很多线程都有消息队列,都需要调用 GetMessagePeekMessage 从消息队列中取消息,

所以我们选用 WH_GETMESSAGE 钩子进行 DLL 注入。

6.1 关键 API

SetWindowsHookEx

安装某种类型的钩子

UnhookWindowsHookEx

卸载之前安装的钩子

CallNextHookEx

在钩子过程中,将消息传递给下一个钩子

HHOOK SetWindowsHookEx(int idHook, HOOKPROC lpfn, HINSTANCE hmod, DWORD dwThreadId);

idHook

欲安装的钩子类型

lpfn

指向相应的钩子过程。

hmod

指向了一个动态链接的句柄,该动态连接库包含了参数 lpfn 所指向的钩子过程。

若参数 dwThreadId 指向的线程由当前进程创建,并且相应的钩子过程定义于当前进程中,则参数 hMod 必须被设置为 NULL。

dwThreadId

如果指向了一个线程 ID,则安装的钩子只监视此线程。若此参数值为 0,则监视系统中所有线程。

返回值

如果函数执行成功,则返回值就是钩子过程的句柄。

否则,返回 NULL。

BOOL UnhookWindowsHookEx(HHOOK hhk);

hhk

欲卸载的钩子的句柄,即之前调用 SetWindowsHookEx 函数的返回值。

返回值

如果函数执行成功,返回非 0 值。

否则,返回 0。

LRESULT CallNextHookEx(HHOOK hhk, int nCode, WPARAM wParam, LPARAM lParam);

hhk:系统会忽略,传 NULL 就行了。

其他参数与钩子过程接收到的值保持一致就行了。

6.2 实践

安装 WH_GETMESSAGE 钩子监控指定进程,在钩子过程第一次被调用时,显示被注入进程加载的所有模块及其基地址。

6.3 主要代码

Windows 中的三种常用 DLL 注入技术_第9张图片

// DLL 模块句柄

HMODULE g_hDllModule = nullptr;

// 共享内存

#pragma data_seg("shared_data")

    HHOOK g_hHook = nullptr;   // 安装的钩子的句柄

    DWORD g_hookProcessId = 0; // Hook 目标进程 ID

#pragma data_seg()

#pragma comment(linker, "/SECTION:shared_data,RWS")

BOOL APIENTRY DllMain( HMODULE hModule,

                       DWORD  ul_reason_for_call,

                       LPVOID lpReserved

                    )

{

    switch (ul_reason_for_call)

    {

    case DLL_PROCESS_ATTACH:

        {

            // 若不是目标进程,则返回 FALSE,使得其不用加载注入 DLL,消除对其它进程的干扰。

            if (g_hookProcessId != 0&& ::GetCurrentProcessId() != g_hookProcessId)

            {

               return FALSE;

            }

            g_hDllModule = hModule;

        }

        break;

    case DLL_THREAD_ATTACH:

    case DLL_THREAD_DETACH:

    case DLL_PROCESS_DETACH:

        break;

    }

    return TRUE;

}

extern HMODULE g_hDllModule;

extern HHOOK g_hHook;

extern DWORD g_hookProcessId;

// 钩子过程

static LRESULT GetMsgProc(int code, WPARAM wParam, LPARAM lParam)

{

    // 判断是否是 Hook 目标进程

    if (::GetCurrentProcessId() == g_hookProcessId)

    {

        static bool bFirst = false;

        if (!bFirst)

        {

            bFirst = true;

            // 显示当前进程加载的所有模块及其基地址

            ShowCurrentProcessModules();

        }

    }

   

    return ::CallNextHookEx(g_hHook, code, wParam, lParam);

}

// 安装全局钩子

bool InstallGlobalHook(DWORD hookProcessId)

{

    g_hookProcessId = hookProcessId;

    if (g_hHook == nullptr)

    {

        g_hHook = ::SetWindowsHookEx(WH_GETMESSAGE, (HOOKPROC)GetMsgProc, g_hDllModule, 0);

    }

   

    return (g_hHook != nullptr);

}

// 卸载全局钩子

bool UninstallGlobalHook()

{

    if (g_hHook != nullptr)

    {

        ::UnhookWindowsHookEx(g_hHook);

        g_hHook = nullptr;

    }

    return true;

}

七、总结

通过上述三种方式,我们均可实现将一个 DLL 注入到另一个进程的地址空间。

一旦 DLL 代码进入另一个地址空间,那么我们就可以在那个进程中随心所欲,肆意妄为了。这听上去够吓人的,因此在真的打算这样做之前,请务必慎重考虑。

参考资料

  1. 《Windows核心编程》
  2. Asynchronous Procedure Calls
  3. CreateRemoteThread
  4. Hooks Overview

你可能感兴趣的:(C++,Windows,c++,windows)