获取指定进程的加载基址

Palpitation

发布日期: 2018-11-06 22:45:42 浏览量: 1545
评分:
star star star star star star star star star star_border
*转载请注明来自write-bug.com

背景

之前,自己写过一个进程内存分析的小程序,其中,就有一个功能是获取进程在内存中的加载基址。由于现在Windows系统引入了ASLR (Address Space Layout Randomization)机制,加载程序时候不再使用固定的基址加载。VS默认是开启基址随机化的,我们也可以设置它使用固定加载基址。至于什么是ASLR?或者ASLR有什么作用?本文就不深入探讨了,感兴趣的,可以自己私下了解了解。

本文就是开发这样的一个小程序,使用两种方法来获取获取指定进程的加载基址。一种是使用进程模块快照方式,然后遍历加载模块并获取基址,另一种是直接调用WIN32 API函数EnumProcessModules,遍历加载模块基址。这两种方法本质上都是一样的。现在,我就把分析过程和实现方式写成文档,分享给大家。

函数介绍

CreateToolhelp32Snapshot 函数

可以通过获取进程信息为指定的进程、进程使用的堆[HEAP]、模块[MODULE]、线程建立一个快照。

函数声明

  1. HANDLE WINAPI CreateToolhelp32Snapshot(
  2. DWORD dwFlags,
  3. DWORD th32ProcessID
  4. );

参数

  • dwFlags
    指定快照中包含的系统内容,这个参数能够使用下列数值(常量)中的一个或多个:
VALUE MEANING
TH32CS_INHERIT 声明快照句柄是可继承的
TH32CS_SNAPALL 在快照中包含系统中所有的进程和线程
TH32CS_SNAPHEAPLIST 在快照中包含在th32ProcessID中指定的进程的所有的堆
TH32CS_SNAPMODULE 在快照中包含在th32ProcessID中指定的进程的所有的模块
TH32CS_SNAPPROCESS 在快照中包含系统中所有的进程
TH32CS_SNAPTHREAD 在快照中包含系统中所有的线程
  • th32ProcessID
    指定将要快照的进程ID。如果该参数为0表示快照当前进程。该参数只有在设置了TH32CS_SNAPHEAPLIST或者TH32CS_SNAPMODULE后才有效,在其他情况下该参数被忽略,所有的进程都会被快照。

返回值

  • 调用成功,返回快照的句柄;调用失败,返回INVALID_HANDLE_VALUE 。

Module32First 和 Module32Next 函数

当我们利用函数CreateToolhelp32Snapshot()获得指定进程的快照后,我们可以利用Module32First函数来获得进程第一个模块的句柄,Module32Next函数来获得进程下一个模块的句柄。

OpenProcess 函数

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

函数声明

  1. HANDLE OpenProcess(
  2. DWORD dwDesiredAccess, //渴望得到的访问权限(标志)
  3. BOOL bInheritHandle, // 是否继承句柄
  4. DWORD dwProcessId// 进程标示符
  5. );

参数

  • dwDesiredAccess [in]
    访问进程对象。 此访问权限将针对进程的安全描述符进行检查。 此参数可以是一个或多个进程访问权限。如果调用者启用了SeDebugPrivilege权限,则无论安全描述符的内容如何,都会授予所请求的访问权限。
  • bInheritHandle [in]
    如果此值为TRUE,则此进程创建的进程将继承该句柄。 否则,进程不会继承此句柄。
  • dwProcessId [in]
    要打开的本地进程的标识符。

返回值

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

EnumProcessModules 函数

在指定的进程中检索每个模块的句柄。要控制64位应用程序是否枚举32位模块,64位模块或两种类型的模块,请使用EnumProcessModulesEx函数。

函数声明

  1. BOOL WINAPI EnumProcessModules(
  2. _In_ HANDLE hProcess,
  3. _Out_ HMODULE *lphModule,
  4. _In_ DWORD cb,
  5. _Out_ LPDWORD lpcbNeeded
  6. );

参数

  • hProcess [in]
    过程的句柄。
  • lphModule [out]
    接收模块句柄列表的数组。
  • cb [in]
    lphModule数组的大小,以字节为单位。
  • lpcbNeeded [out]
    将所有模块句柄存储在lphModule数组中所需的字节数。

返回值

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

实现思路

该程序实现进程基址的主要原理是,遍历进程里的所有加载的模块,那么,第一个加载模块的加载基址就是该进程的加载基址。

那么,对于进程模块加载基址的遍历就有两种两种方法。一种是根据进程模块快照获取,另一种市直接调用EnumProcessModules函数获取。现在,对这两种方法的原理分别进行介绍。

使用进程模块快照的方式获取模块基址的原理是:

  1. 首先,使用CreateToolhelp32Snapshot 函数获取指定进程的所有模块快照。
  2. 然后,根据模块快照,使用Module32First 和 Module32Next 函数进行遍历快照,并获取快照信息。其中,就包括有模块的加载基址信息。第一个模块的加载基址便是该进程的加载基址。
  3. 最后,关闭上面获取的快照的句柄。

使用EnumProcessModules函数获取模块基址的原理:

  • 首先,我们需要使用OpenProcess函数打开指定进程并获取进程的句柄

  • 然后,根据进程句柄调用EnumProcessModules函数获取进程加载的所有模块的加载基址,并保存在数组中。那么,第一个模块的加载基址便是该进程的加载基址

  • 关闭打开的进程句柄

编程实现

获取指定进程模块快照的方式遍历进程模块

  1. PVOID GetProcessImageBase1(DWORD dwProcessId)
  2. {
  3. PVOID pProcessImageBase = NULL;
  4. MODULEENTRY32 me32 = { 0 };
  5. me32.dwSize = sizeof(MODULEENTRY32);
  6. // 获取指定进程全部模块的快照
  7. HANDLE hModuleSnap = ::CreateToolhelp32Snapshot(TH32CS_SNAPMODULE, dwProcessId);
  8. if (INVALID_HANDLE_VALUE == hModuleSnap)
  9. {
  10. ShowError("CreateToolhelp32Snapshot");
  11. return pProcessImageBase;
  12. }
  13. // 获取快照中第一条信息
  14. BOOL bRet = ::Module32First(hModuleSnap, &me32);
  15. if (bRet)
  16. {
  17. // 获取加载基址
  18. pProcessImageBase = (PVOID)me32.modBaseAddr;
  19. }
  20. // 关闭句柄
  21. ::CloseHandle(hModuleSnap);
  22. return pProcessImageBase;
  23. }

直接使用EnumProcessModules函数获取进程模块基址

  1. PVOID GetProcessImageBase2(DWORD dwProcessId)
  2. {
  3. PVOID pProcessImageBase = NULL;
  4. //打开进程, 获取进程句柄
  5. HANDLE hProcess = ::OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwProcessId);
  6. if (NULL == hProcess)
  7. {
  8. ShowError("OpenProcess");
  9. return pProcessImageBase;
  10. }
  11. // 遍历进程模块,
  12. HMODULE hModule[100] = {0};
  13. DWORD dwRet = 0;
  14. BOOL bRet = ::EnumProcessModules(hProcess, (HMODULE *)(hModule), sizeof(hModule), &dwRet);
  15. if (FALSE == bRet)
  16. {
  17. ::CloseHandle(hProcess);
  18. ShowError("EnumProcessModules");
  19. return pProcessImageBase;
  20. }
  21. // 获取第一个模块加载基址
  22. pProcessImageBase = hModule[0];
  23. // 关闭句柄
  24. ::CloseHandle(hProcess);
  25. return pProcessImageBase;
  26. }

程序测试

我们在 main 函数中调用上述封装的函数进行测试,main 函数为:

  1. int _tmain(int argc, _TCHAR* argv[])
  2. {
  3. PVOID pProcessImageBase1 = NULL;
  4. PVOID pProcessImageBase2 = NULL;
  5. pProcessImageBase1 = GetProcessImageBase1(4500);
  6. pProcessImageBase2 = GetProcessImageBase2(4500);
  7. printf("pProcessImageBase1=0x%p\npProcessImageBase2=0x%p\n",
  8. pProcessImageBase1, pProcessImageBase2);
  9. system("pause");
  10. return 0;
  11. }

测试结果

我们运行程序,程序执行成功,并显示两种方法获取的进程加载基址,而且获取结果都相同。

然后,我们使用 Process Explorer 软件查看PID为 4500 的进程的加载基址,程序基址获取正确,程序测试成功。

总结

这两种方式,本质上都是一样的,只是遍历进程加载模块所使用的方法不相同。那么,进程加载的第一个模块的加载基址,便是进程的加载基址。

参考

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

上传的附件 cloud_download GetProcessImageBase_Test.7z ( 144.72kb, 16次下载 )

keyboard_arrow_left上一篇 : PC微信逆向分析の强制输出微信调式信息 C语言学习笔记 : 下一篇keyboard_arrow_right



Palpitation
2018-11-06 22:46:15
使用两种方法来获取获取指定进程的加载基址:一种是使用进程模块快照方式,另一种是直接调用WIN32 API函数EnumProcessModules
Zero
2020-01-03 21:27:23
不能用啊

发送私信

做一个简单的人,踏实务实,不沉溺幻想,不庸人自扰

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