进程挂起再恢复实现进程内存替换

Reindeer

发布日期: 2019-01-22 17:34:57 浏览量: 1081
评分:
star star star star star star star star star_border star_border
*转载请注明来自write-bug.com

背景

所谓的进程内存替换,就是指将一个进程的内存数据清空,写入任意我们想写入的数据,并更改执行顺序,执行我们写入的数据代码。

本文介绍的就是这样的编程技术,但是,我们做了一些简化,简化成实现创建一个挂起主线程的进程,在新进程的地址空间内申请一块内存,写入我们的 Shellcode,并更改新进程执行顺序,执行我们的 Shellcode 代码。现在,我就把实现过程和原理整理成文档,分享给大家。

函数介绍

GetThreadContext 函数

获取指定线程的上下文。64 位应用程序可以使用 Wow64GetThreadContext 函数来检索 WOW64 线程的上下文。

函数声明

  1. BOOL WINAPI GetThreadContext(
  2. _In_ HANDLE hThread,
  3. _Inout_ LPCONTEXT lpContext
  4. );

参数

  • hThread [in]
    要检索其上下文的线程的句柄。 该句柄必须具有对线程的THREAD_GET_CONTEXT访问权限。 有关更多信息,请参阅线程安全和访问权限。
    WOW64:手柄也必须有THREAD_QUERY_INFORMATION访问权限。
  • lpContext [in,out]
    指向 CONTEXT 结构的指针,它接收指定线程的适当上下文。 该结构的ContextFlags成员的值指定检索线程的上下文的哪些部分。 CONTEXT结构具有高度的处理器特性。 请参阅WinNT.h头文件,了解该结构的处理器特定定义和任何对齐要求。

返回值

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

SetThreadContext 函数

设置指定线程的上下文。
64 位应用程序可以使用 Wow64SetThreadContext 函数设置 WOW64 线程的上下文。

函数声明

  1. BOOL WINAPI SetThreadContext(
  2. _In_ HANDLE hThread,
  3. _In_ const CONTEXT *lpContext
  4. );

参数

  • hThread [in]
    线程的句柄,其上下文将被设置。 该句柄必须具有线程的THREAD_SET_CONTEXT权限。 有关更多信息,请参阅线程安全和访问权限。
  • lpContext [in]
    指向包含要在指定线程中设置的上下文的CONTEXT结构的指针。 此结构的ContextFlags成员的值指定要设置的线程的上下文的哪些部分。 无法指定的CONTEXT结构中的某些值将默认设置为正确的值。 这包括指定特权处理器模式的CPU状态寄存器中的位,调试寄存器中的全局使能位以及必须由操作系统控制的其他状态。

返回值

  • 如果设置了上下文,则返回值为非零。
  • 如果函数失败,返回值为零。 要获取扩展错误信息,请调用GetLastError。

ResumeThread 函数

减少线程的暂停计数。 当暂停计数递减到零时,线程的执行被恢复。

函数声明

  1. DWORD WINAPI ResumeThread(
  2. _In_ HANDLE hThread
  3. );

参数

  • hThread [in]

    要重新启动的线程的句柄。

    该句柄必须具有THREAD_SUSPEND_RESUME权限。 有关更多信息,请参阅线程安全和访问权限。

返回值

  • 如果函数成功,则返回值是线程先前的挂起计数。
  • 如果函数失败,返回值为(DWORD)-1。 要获取扩展错误信息,请调用GetLastError。

实现原理

我们实现进程内存替换,或者说实现进程数据写入,更改执行顺序的原理是:

  • 首先,使用 CreateProcess 函数创建进程,并且设置创建进程的标志为 CREATE_SUSPENDED,即表示新进程的主线程被挂起。

  • 然后,使用 VirtualAllocEx 函数在新进程中申请一块可读、可写、可执行的内存,并使用 WriteProcessMemory 函数写入Shellcode 数据。

  • 接着,使用 GetThreadContext,设置获取标志为 CONTEXT_FULL,即获取新进程中所有的线程上下文。并修改线程上下文的指令指针 EIP 的值,更改主线程的执行顺序。再将修改过的线程上下文设置回主线程中。

  • 最后,我们调用 ResumeThread 恢复主线程,让进程按照修改后的 EIP 继续运行,执行我们的 Shellcode 代码。

其中,当使用 CreateProcess 创建进程时,创建标志为 CREATE_SUSPENDED,则表示新进程的主线程被创建为挂起状态,直到使用 ResumeThread 函数恢复主线程,进程才会继续运行。

其中,要注意的是,在使用 GetThreadContext 获取线程上下文的时候,一定要对 CONTEXT 机构中的 ContextFlags 成员赋值,表示指明要检索线程的上下文的哪些部分,否则会导致程序实现不到想要的效果。我们可以指明 CONTEXT_FULL,表示获取所有的线程上下文信息。

编码实现

  1. // 创建进程并替换进程内存数据, 更改执行顺序
  2. BOOL ReplaceProcess(char *pszFilePath, PVOID pReplaceData, DWORD dwReplaceDataSize, DWORD dwRunOffset)
  3. {
  4. STARTUPINFO si = { 0 };
  5. PROCESS_INFORMATION pi = { 0 };
  6. CONTEXT threadContext = { 0 };
  7. BOOL bRet = FALSE;
  8. ::RtlZeroMemory(&si, sizeof(si));
  9. ::RtlZeroMemory(&pi, sizeof(pi));
  10. ::RtlZeroMemory(&threadContext, sizeof(threadContext));
  11. si.cb = sizeof(si);
  12. // 创建进程并挂起主线程
  13. bRet = ::CreateProcess(pszFilePath, NULL, NULL, NULL, FALSE, CREATE_SUSPENDED, NULL, NULL, &si, &pi);
  14. if (FALSE == bRet)
  15. {
  16. ShowError("CreateProcess");
  17. return FALSE;
  18. }
  19. // 在替换的进程中申请一块内存
  20. LPVOID lpDestBaseAddr = ::VirtualAllocEx(pi.hProcess, NULL, dwReplaceDataSize, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
  21. if (NULL == lpDestBaseAddr)
  22. {
  23. ShowError("VirtualAllocEx");
  24. return FALSE;
  25. }
  26. // 写入替换的数据
  27. bRet = ::WriteProcessMemory(pi.hProcess, lpDestBaseAddr, pReplaceData, dwReplaceDataSize, NULL);
  28. if (FALSE == bRet)
  29. {
  30. ShowError("WriteProcessError");
  31. return FALSE;
  32. }
  33. // 获取线程上下文
  34. // 注意此处标志,一定要写!!!
  35. threadContext.ContextFlags = CONTEXT_FULL;
  36. bRet = ::GetThreadContext(pi.hThread, &threadContext);
  37. if (FALSE == bRet)
  38. {
  39. ShowError("GetThreadContext");
  40. return FALSE;
  41. }
  42. // 修改进程的PE文件的入口地址以及映像大小,先获取原来进程PE结构的加载基址
  43. threadContext.Eip = (DWORD)lpDestBaseAddr + dwRunOffset;
  44. // 设置挂起进程的线程上下文
  45. bRet = ::SetThreadContext(pi.hThread, &threadContext);
  46. if (FALSE == bRet)
  47. {
  48. ShowError("SetThreadContext");
  49. return FALSE;
  50. }
  51. // 恢复挂起的进程的线程
  52. ::ResumeThread(pi.hThread);
  53. return TRUE;
  54. }

程序测试

我们运行程序,对 520.exe 程序创建挂起进程,并写入 Shellcode,更改进程执行顺序,恢复进程,执行 Shellcode 部分代码:

总结

这个程序在理解原理后,你会发现我们上述的例子并没有替换原来新进程的内存,只是申请了一块新内存,并写入 Shellcode 数据。但是,要替换新进程的内存倒也不难,只要获取了新进程的加载基址,然后根据 PE 格式,获取加载映像大小,并对数据全部置零清空,在写入我们的新数据,这样就实现了替换。但是,只要你理解了本文演示的例子,实现替换操作也不难了。

如果你仍然不明白,可以多看看 CreateProcess、GetThreadContext、SetThreadContext、ResumeThread 等函数的具体参数说明,理解清楚参数含义就可以了。

其中,要注意的是,在使用 GetThreadContext 获取线程上下文的时候,一定要对 CONTEXT 机构中的 ContextFlags 成员赋值,表示指明要检索线程的上下文的哪些部分,否则会导致程序实现不到想要的效果。我们可以指明 CONTEXT_FULL,表示获取所有的线程上下文信息。

参考

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

上传的附件 cloud_download ReplaceProcess_Test.7z ( 146.84kb, 23次下载 )

发送私信

看见你笑眼一弯呀,所有天上的星星,就被叮叮当当地摇响

10
文章数
13
评论数
最近文章
eject