传统的远线程注入DLL技术剖析

Leftme

发布日期: 2019-01-12 09:21:05 浏览量: 1375
评分:
star star star star star star star star star_border star_border
*转载请注明来自write-bug.com

背景

想必很多人应该都听说过远线程注入DLL技术这个概念,的确,这是一个很巧妙,也很经典的DLL注入技术。为何说是巧妙,等你看完这篇文章就了解了。

本文讲解的是传统的远线程注入方法,也就是使用 CreateRemoteThread 函数实现的。那么,之所以说是传统,是因为在讲完传统的远线程注入方法后,我们会介绍目前最新的远线程注入方式,注入功能比传统的还要强大。

现在,我就先讲解传统的远线程注入,把实现过程和原理整理成文档,分享给大家。

函数介绍

OpenProcess 函数

打开现有的本地进程对象。

函数声明

  1. HANDLE WINAPI OpenProcess(
  2. _In_ DWORD dwDesiredAccess,
  3. _In_ BOOL bInheritHandle,
  4. _In_ DWORD dwProcessId
  5. );

参数

  • dwDesiredAccess [in]
    访问进程对象。此访问权限针对进程的安全描述符进行检查。此参数可以是一个或多个进程访问权限。如果调用该函数的进程启用了SeDebugPrivilege权限,则无论安全描述符的内容如何,都会授予所请求的访问权限。
  • bInheritHandle [in]
    若此值为TRUE,则此进程创建的进程将继承该句柄。否则,进程不会继承此句柄。
  • dwProcessId [in]
    要打开的本地进程的标识符。
    如果指定的进程是系统进程(0x00000000),则该函数失败,最后一个错误代码为ERROR_INVALID_PARAMETER。如果指定的进程是空闲进程或CSRSS进程之一,则此功能将失败,并且最后一个错误代码为ERROR_ACCESS_DENIED,因为它们的访问限制会阻止用户级代码打开它们。
    如果您使用GetCurrentProcessId作为此函数的参数,请考虑使用GetCurrentProcess而不是OpenProcess,以提高性能。

返回值

  • 如果函数成功,则返回值是指定进程的打开句柄。
  • 如果函数失败,返回值为NULL。 要获取扩展错误信息,请调用GetLastError。

VirtualAllocEx 函数

在指定进程的虚拟地址空间内保留,提交或更改内存区域的状态。 该函数初始化其分配给零的内存。

函数声明

  1. LPVOID WINAPI VirtualAllocEx(
  2. _In_ HANDLE hProcess,
  3. _In_opt_ LPVOID lpAddress,
  4. _In_ SIZE_T dwSize,
  5. _In_ DWORD flAllocationType,
  6. _In_ DWORD flProtect
  7. );

参数

  • hProcess [in]
    过程的句柄。该函数在该进程的虚拟地址空间内分配内存。
    句柄必须具有PROCESS_VM_OPERATION权限。有关更多信息,请参阅流程安全和访问权限。

  • lpAddress [in]
    指定要分配的页面的所需起始地址的指针。
    如果您正在保留内存,则该函数会将该地址舍入到分配粒度的最接近的倍数。
    如果您提交已经保留的内存,该功能会将该地址舍入到最接近的页面边界。要确定页面的大小和主机上的分配粒度,请使用GetSystemInfo函数。
    如果lpAddress为NULL,则该函数确定在哪里分配该区域。

  • dwSize [in]
    要分配的内存大小,以字节为单位。
    如果lpAddress为NULL,则函数将dwSize循环到下一个页面边界。
    如果lpAddress不为NULL,则该函数将从lpAddress到lpAddress + dwSize的范围内分配包含一个或多个字节的所有页面。这意味着,例如,跨越页面边界的2字节范围会导致功能分配两个页面。

  • flAllocationType [in]
    内存分配类型。此参数必须包含以下值之一:

VALUE MEANING
MEM_COMMIT 为指定的预留内存页分配内存费用(从磁盘上的内存和分页文件的总体大小)。 该函数还保证当调用者稍后初次访问存储器时,内容将为零。 除非/直到虚拟地址被实际访问,实际的物理页面才被分配
MEM_RESERVE 保留进程的虚拟地址空间的范围,而不会在内存或磁盘上的分页文件中分配任何实际物理存储
MEM_RESET 表示由lpAddress和dwSize指定的内存范围内的数据不再受关注。 页面不应从页面文件中读取或写入页面文件。 然而,内存块将在以后再次被使用,所以不应该被分解。 该值不能与任何其他值一起使用
MEM_RESET_UNDO 只能在早期成功应用了MEM_RESET的地址范围上调用MEM_RESET_UNDO。 它指示由lpAddress和dwSize指定的指定内存范围内的数据对呼叫者感兴趣,并尝试反转MEM_RESET的影响。 如果功能成功,则表示指定地址范围内的所有数据都是完整的。 如果功能失败,地址范围中的至少一些数据已被替换为零
  • flProtect [in]
    要分配的页面区域的内存保护。 如果页面被提交,您可以指定任何一个内存保护常量。
    如果lpAddress指定了一个地址,flProtect不能是以下值之一:
    PAGE_NOACCESS
    PAGE_GUARD
    PAGE_NOCACHE
    PAGE_WRITECOMBINE

返回值

  • 如果函数成功,则返回值是分配的页面区域的基址。
  • 如果函数失败,返回值为NULL。 要获取扩展错误信息,请调用GetLastError。

WriteProcessMemory 函数

在指定的进程中将数据写入内存区域。 要写入的整个区域必须可访问或操作失败。

函数声明

  1. BOOL WINAPI WriteProcessMemory(
  2. _In_ HANDLE hProcess,
  3. _In_ LPVOID lpBaseAddress,
  4. _In_ LPCVOID lpBuffer,
  5. _In_ SIZE_T nSize,
  6. _Out_ SIZE_T *lpNumberOfBytesWritten
  7. );

参数

  • hProcess [in]
    要修改的进程内存的句柄。 句柄必须具有PROCESS_VM_WRITE和PROCESS_VM_OPERATION访问进程。
  • lpBaseAddress [in]
    指向写入数据的指定进程中的基地址的指针。 在数据传输发生之前,系统会验证指定大小的基地址和内存中的所有数据是否可以进行写入访问,如果不可访问,则该函数将失败。
  • lpBuffer [in]
    指向缓冲区的指针,其中包含要写入指定进程的地址空间的数据。
  • nSize [in]
    要写入指定进程的字节数。
  • lpNumberOfBytesWritten [out]
    指向变量的指针,该变量接收传输到指定进程的字节数。 此参数是可选的。 如果lpNumberOfBytesWritten为NULL,则忽略该参数。

返回值

  • 如果函数成功,则返回值不为零。
  • 如果函数失败,返回值为0(零)。 要获取扩展错误信息,请调用GetLastError。

CreateRemoteThread 函数

创建在另一个进程的虚拟地址空间中运行的线程。
使用CreateRemoteThreadEx函数创建在另一个进程的虚拟地址空间中运行的线程,并可选地指定扩展属性。

函数声明

  1. HANDLE WINAPI CreateRemoteThread(
  2. _In_ HANDLE hProcess,
  3. _In_ LPSECURITY_ATTRIBUTES lpThreadAttributes,
  4. _In_ SIZE_T dwStackSize,
  5. _In_ LPTHREAD_START_ROUTINE lpStartAddress,
  6. _In_ LPVOID lpParameter,
  7. _In_ DWORD dwCreationFlags,
  8. _Out_ LPDWORD lpThreadId
  9. );

参数

  • hProcess [in]
    要创建线程的进程的句柄。 句柄必须具有PROCESS_CREATE_THREAD,PROCESS_QUERY_INFORMATION,PROCESS_VM_OPERATION,PROCESS_VM_WRITE和PROCESS_VM_READ访问权限,如果某些平台上没有这些权限,可能会失败。 有关更多信息,请参阅流程安全和访问权限。
  • lpThreadAttributes [in]
    指向SECURITY_ATTRIBUTES结构的指针,该结构指定新线程的安全描述符,并确定子进程是否可以继承返回的句柄。 如果lpThreadAttributes为NULL,则线程将获得默认安全描述符,并且该句柄不能被继承。 线程的默认安全描述符中的访问控制列表(ACL)来自创建者的主令牌。dwStackSize [in]
    堆栈的初始大小,以字节为单位。 系统将此值循环到最近的页面。 如果此参数为0(零),则新线程使用可执行文件的默认大小。 有关更多信息,请参阅线程堆栈大小。
  • lpStartAddress [in]
    指向由线程执行的类型为LPTHREAD_START_ROUTINE的应用程序定义函数的指针,并表示远程进程中线程的起始地址。 该功能必须存在于远程进程中。 有关更多信息,请参阅ThreadProc。
  • lpParameter [in]
    指向要传递给线程函数的变量的指针。
  • dwCreationFlags [in]
    控制线程创建的标志。若是 0,则表示线程在创建后立即运行。
  • lpThreadId [out]
    指向接收线程标识符的变量的指针。
    如果此参数为NULL,则不返回线程标识符。

返回值

  • 如果函数成功,则返回值是新线程的句柄。
  • 如果函数失败,返回值为NULL。 要获取扩展错误信息,请调用GetLastError。

实现原理

远线程注入 DLL 中的远线程,是因为使用的关键函数是 CreateRemoteThread,来在其它的进程空间中创建一个线程。那么,它为何能够使其它进程加载一个 DLL,实现 DLL 注入呢?接下来我就为大家一一分析。

首先,我们加载一个 DLL,通常使用 LoadLibrary 函数来实现 DLL 的动态加载。那么,先来看下 LoadLibrary 函数的声明:

  1. HMODULE WINAPI LoadLibrary(
  2. _In_ LPCTSTR lpFileName
  3. );

从上面的函数声明可以知道,LoadLibrary 函数的参数只有一个,传递的是要加载的 DLL 的路径字符串。

然后,我们再看下创建远线程的函数 CreateRemoteThread 的函数声明:

  1. HANDLE WINAPI CreateRemoteThread(
  2. _In_ HANDLE hProcess,
  3. _In_ LPSECURITY_ATTRIBUTES lpThreadAttributes,
  4. _In_ SIZE_T dwStackSize,
  5. _In_ LPTHREAD_START_ROUTINE lpStartAddress,
  6. _In_ LPVOID lpParameter,
  7. _In_ DWORD dwCreationFlags,
  8. _Out_ LPDWORD lpThreadId
  9. );

我们可以从声明中知道,CreateRemoteThread 需要传递的是目标进程空间的多线程函数地址,以及多线程的参数。

接下来,我们将上述两者结合,开始大胆设想一下,如果,我们能够获取目标进程的 LoadLibrary 函数的地址,而且还能够获取目标进程空间中某个 DLL 路径的字符串地址,那么,将LoadLibrary 函数的地址作为多线程函数的地址,某个 DLL 路径字符串作为多线程函数的参数,传递给 CreateRemoteThread 函数在目标进程空间创建一个多线程,这样能不能创建成功呢?答案是可以的。这样,就可以在目标进程空间中创建一个多线程,这个多线程就是 LoadLibrary 函数加载 DLL。

那么,这样远线程注入的原理大概就了解了吧。那么要实现远线程注入 DLL,还需要解决以下两个问题:一是目标进程空间的 LoadLibrary 函数地址是多少呢?二是如何向目标进程空间中写入 DLL 路径字符串数据呢?

对于第一个问题,我们知道由于机制随机化ASLR(Address space layout randomization),导致每次开机时加载的系统 DLL 的加载基址都不一样,从而导致了 DLL 的导出函数的地址也都不一样。即使如此,但要注意一个关键的一个知识点就是:

有些系统DLL中的指令是Position dependent的,要求所有进程中必须一致。比如kernel32中的新线程入口,ntdll中的异常处理入口等。其实这个地址只是要求系统启动之后必须固定,如果系统重新启动,其地址可以不同。

Copy-On-Write机制,不改多进程共享,改写内容的页系统会立即给当前进程复制一份,这样这个地址与其它进程中相同地址所映射的物理内存就不同了,怎么改都不会影响其它进程。

也就是说,虽然不同进程,但是其 Kernel32.dll 的加载基址是相同的,也就是说,自己程序空间的 LoadLibrary 函数地址和其它进程空间的 LoadLibrary 函数地址相同。

对于第二个问题,我们可以直接调用 VirtualAllocEx 函数在目标进程空间中申请一块内存,然后再调用 WriteProcessMemory 函数将指定的 DLL 路径写入到目标进程空间中。

这样,我们就可以调用 CreateRemoteThread 函数,实现远线程注入DLL了。

编码实现

  1. // 使用 CreateRemoteThread 实现远线程注入
  2. BOOL CreateRemoteThreadInjectDll(DWORD dwProcessId, char *pszDllFileName)
  3. {
  4. HANDLE hProcess = NULL;
  5. DWORD dwSize = 0;
  6. LPVOID pDllAddr = NULL;
  7. FARPROC pFuncProcAddr = NULL;
  8. // 打开注入进程,获取进程句柄
  9. hProcess = ::OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwProcessId);
  10. if (NULL == hProcess)
  11. {
  12. ShowError("OpenProcess");
  13. return FALSE;
  14. }
  15. // 在注入进程中申请内存
  16. dwSize = 1 + ::lstrlen(pszDllFileName);
  17. pDllAddr = ::VirtualAllocEx(hProcess, NULL, dwSize, MEM_COMMIT, PAGE_READWRITE);
  18. if (NULL == pDllAddr)
  19. {
  20. ShowError("VirtualAllocEx");
  21. return FALSE;
  22. }
  23. // 向申请的内存中写入数据
  24. if (FALSE == ::WriteProcessMemory(hProcess, pDllAddr, pszDllFileName, dwSize, NULL))
  25. {
  26. ShowError("WriteProcessMemory");
  27. return FALSE;
  28. }
  29. // 获取LoadLibraryA函数地址
  30. pFuncProcAddr = ::GetProcAddress(::GetModuleHandle("kernel32.dll"), "LoadLibraryA");
  31. if (NULL == pFuncProcAddr)
  32. {
  33. ShowError("GetProcAddress_LoadLibraryA");
  34. return FALSE;
  35. }
  36. // 使用 CreateRemoteThread 创建远线程, 实现 DLL 注入
  37. HANDLE hRemoteThread = ::CreateRemoteThread(hProcess, NULL, 0, (LPTHREAD_START_ROUTINE)pFuncProcAddr, pDllAddr, 0, NULL);
  38. if (NULL == hRemoteThread)
  39. {
  40. ShowError("CreateRemoteThread");
  41. return FALSE;
  42. }
  43. // 关闭句柄
  44. ::CloseHandle(hProcess);
  45. return TRUE;
  46. }

程序测试

我们对 520.exe 进程注入我们的测试 DLL,DLL 成功注入到 520.exe 进程空间中:

总结

要注意以管理员权限运行程序,因为由于 OpenProcess 函数的缘故,打开高权限的进程时,会因进程权限不足而无法打开进程,获取进程句柄。其中,我们将当前进程令牌权限提升至 SE_DEBUG_NAME权限,具体的进程令牌权限提升可以参考 “使用AdjustTokenPrivileges函数提升进程访问令牌的权限“ 这篇文章。

这个是传统的远线程注入 DLL 方法,有一个问题就是,不能成功注入到一些系统服务进程,因为系统存在 SESSION 0 隔离。接下来,就继续讲解突破 SESSION 0 隔离的远线程注入,成功向系统服务进程注入 DLL。

参考

参考自《Windows黑客编程技术详解》一书

上传的附件 cloud_download CreateRemoteThread_Test.7z ( 362.97kb, 9次下载 )

发送私信

告别错的,方可遇见对的

16
文章数
17
评论数
最近文章
eject