内存快速搜索遍历

OrdinAry

发布日期: 2018-11-23 18:38:50 浏览量: 1294
评分:
star star star star star star star star star_border star_border
*转载请注明来自write-bug.com

背景

相比很多人都用过内存搜索软件 Cheat Engine 吧,它里面提供了强大进程内存搜索功能,搜索速度很快,搜索结果也很精确。我之前对内存搜索也稍微专研了一下,是因为当时需要写一个小程序,那个小程序的功能就是可以搜索出指定进程指定值的内存地址,这个CE就能做,只不过是要在自己的程序里实现内存的搜索。

内存的遍历搜索,说难也不难,说容易也不容易。因为你可以做得比较简单,也可以做得比较完美,这主要是在搜索效率上的区别而已。简单的搜索方法就是直接暴力搜索内存,直接从0地址搜索到0x7FFFFFFF地址处,因为低 2GB 进程空间是用户空间。然后,匹配值就可以了。难点的,就是过滤掉一些内存地址,不用搜索。例如,进程加载基址之前的地址空间,就可以不用搜索等等,以此来缩小搜索的范围,提升搜索效率。

本文就是对内存遍历实现快速搜索,当然,这肯定不会是最快的搜索方式,只是相对的快速。我们也是通过加载基址,以及内存空间地址信息,缩小搜索的范围,提升搜索效率。现在,就把实现过程整理成文档,分享给大家。

函数介绍

VirtualQueryEx 函数

查询地址空间中内存地址的信息。

函数声明

  1. DWORD VirtualQueryEx(
  2. HANDLE hProcess,
  3. LPCVOID lpAddress,
  4. PMEMORY_BASIC_INFORMATION lpBuffer,
  5. DWORD dwLength
  6. );

参数

  • hProcess:进程句柄。
  • lpAddress:查询内存的地址。
  • lpBuffer:指向MEMORY_BASIC_INFORMATION结构的指针,用于接收内存信息。
  • dwLength:MEMORY_BASIC_INFORMATION结构的大小。

返回值

  • 返回值是信息缓冲区中返回的实际字节数。
  • 如果函数失败,返回值是 0。为了获得更多的错误信息,调用GetLastError。

MEMORY_BASIC_INFORMATION 结构体

结构体声明

  1. typedef struct _MEMORY_BASIC_INFORMATION {
  2. PVOID BaseAddress; // 区域基地址
  3. PVOID AllocationBase; // 分配基地址
  4. DWORD AllocationProtect; // 区域被初次保留时赋予的保护属性
  5. SIZE_T RegionSize; // 区域大小(以字节为计量单位)
  6. DWORD State; // 状态(MEM_FREE、MEM_RESERVE或 MEM_COMMIT)
  7. DWORD Protect; // 保护属性
  8. DWORD Type; // 类型
  9. } MEMORY_BASIC_INFORMATION, *PMEMORY_BASIC_INFORMATION;

成员

  • BaseAddress:与lpAddress参数的值相同,但是四舍五入为页面的边界值。
  • AllocationBas:指明用VirtualAlloc函数分配内存区域的基地址。lpAddress在该区域之内。
  • AllocationProtect:指明该地址空间区域被初次保留时赋予该区域的保护属性。
    PAGE_READONLY:只读属性,如果试图进行写操作,将引发访问违规。如果系统区分只读、执行两种属性,那么试图在该区域执行代码也将引发访问违规。
    PAGE_READWRITE:允许读写。
    PAGE_EXECUTE:只允许执行代码,对该区域试图进行读写操作将引发访问违规。
    PAGE_EXECUTE_READ:允许执行和读取。
    PAGE_EXECUTE_READWRITE:允许读写和执行代码。
    PAGE_EXECUTE_WRITECOPY:对于该地址空间的区域,不管执行什么操作,都不会引发访问违规。如果试图在该页面上的内存中进行写入操作,就会将它自己的私有页面(受页文件的支持)拷贝赋予该进程。
    PAGE_GUARD:在页面上写入一个字节时使应用程序收到一个通知(通过一个异常条件)。
    PAGE_NOACCESS:禁止一切访问。
    PAGE_NOCACHE:停用已提交页面的高速缓存。一般情况下最好不要使用该标志,因为它主要是供需要处理内存缓冲区的硬件设备驱动程序的开发人员使用的。
    RegionSize 用于指明内存块从基地址即BaseAddress开始的所有页面的大小(以字节为计量单位)这些页面与含有用LpAddress参数设定的地址的页面拥有相同的保护属性、状态和类型。
  • State:用于指明所有相邻页面的状态。
    MEM_COMMIT:指明已分配物理内存或者系统页文件。
    MEM_FREE:空闲状态。该区域的虚拟地址不受任何内存的支持。该地址空间没有被保留。该状态下AllocationBase、AllocationProtect、Protect和Type等成员均未定义。
    MEM_RESERVE:指明页面被保留,但是没有分配任何物理内存。该状态下Protect成员未定。
  • Protect:用于指明所有相邻页面(内存块)的保护属性。这些页面与含有拥有相同的保属性、状态和类型。意义同AllocationProtect。
  • Type:用于指明支持所有相邻页面的物理存储器的类型(MEM_IMAGE,MEM_MAPPED或MEM_PRIVATE)。这些相邻页面拥有相同的保护属性、状态和类型。如果是Windows 98,那么这个成员将总是MEM_PRIVATE 。
    MEM_IMAGE:指明该区域的虚拟地址原先受内存映射的映像文件(如.exe或DLL文件)的支持,但也许不再受映像文件的支持。例如,当写入模块映像中的全局变量时,“写入时拷贝”的机制将由页文件来支持特定的页面,而不是受原始映像文件的支持。
    MEM_MAPPED:该区域的虚拟地址原先是受内存映射的数据文件的支持,但也许不再受数据文件的支持。例如,数据文件可以使用“写入时拷贝”的保护属性来映射。对文件的任何写入操作都将导致页文件而不是原始数据支持特定的页面。
    MEM_PRIVATE:指明该内存区域是私有的。不被其他进程共享。

ReadProcessMemory 函数

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

函数声明

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

参数

  • hProcess [in]
    具有正在读取的内存的进程的句柄。 句柄必须具有进程的PROCESS_VM_READ访问权限。
  • lpBaseAddress [in]
    指向从中读取的指定进程中的基地址的指针。 在发生任何数据传输之前,系统将验证指定大小的基地址和内存中的所有数据都可访问以进行读取访问,如果不可访问,则该函数将失败。
  • lpBuffer [out]
    指向从指定进程的地址空间接收内容的缓冲区的指针。
  • nSize [in]
    要从指定进程读取的字节数。
  • lpNumberOfBytesRead [out]
    指向一个变量的指针,该变量接收传输到指定缓冲区的字节数。 如果lpNumberOfBytesRead为NULL,则该参数将被忽略。

返回值

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

实现原理

实现内存的快速搜索,我们主要是缩小搜索的范围,来提升的搜索效率。缩小搜索范围的方法有,一是过滤掉进程加载基址之前的内存地址;二是获取内存空间地址信息,把内存状态不是 MEM_COMMIT 以及保护属性没有读权限的内存区域都过滤掉。

所以,获取进程加载基址以及获取内存空间地址信息比较关键。

大概分为以下 7 个步骤:

  • 首先,我们调用 OpenProcess 函数根据进程 PID 打开进程,并获取进程的句柄,进程句柄的权限为 PROCESS_ALL_ACCESS

  • 然后,我们根据进程句柄,获取指定进程的加载基址。对于进程加载基址的获取,我们使用的是 EnumProcessModules 函数来获取。其它的的进程基址获取方法,可以参考本站上其他网友写的“获取指定进程的加载基址”这篇文章

  • 接着,以进程加载基址作为内存搜索的起始地址,调用 VirtualQueryEx 函数查询地址空间中内存地址的信息,然后将内存页面状态不是MEM_COMMIT 过滤掉,即过滤掉没有分配物理内存或者系统页文件。同时,也把没有读权限的页面属性保护都过滤掉

  • 通过内存地址的信息过滤之后,我们就可以调用 ReadProcessMemory 函数把对应的内存区域读取到自己进程的缓冲区中

  • 接着,我们就可以匹配内存,搜索指定的内存,并获取制定进程内存地址

  • 然后,获取下一块内存区域的起始地址,继续重复上面3、4、5步操作,直到满足退出条件

  • 最后,我们就释放内存,并关闭进程句柄

这样,就是先了内存的快速遍历搜索。

编码实现

  1. // 搜索内存
  2. BOOL SearchMemory(DWORD dwProcessId, PVOID pSearchBuffer, DWORD dwSearchBufferSize)
  3. {
  4. // 根据PID, 打开进程获取进程句柄
  5. HANDLE hProcess = ::OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwProcessId);
  6. if (NULL == hProcess)
  7. {
  8. ShowError("OpenProcess");
  9. return FALSE;
  10. }
  11. // 获取进程加载基址
  12. HMODULE hModule = NULL;
  13. ::EnumProcessModules(hProcess, &hModule, sizeof(HMODULE), NULL);
  14. // 把加载基址作为遍历内存的起始地址, 开始遍历
  15. BYTE *pSearchAddress = (BYTE *)hModule;
  16. MEMORY_BASIC_INFORMATION mbi = {0};
  17. DWORD dwRet = 0;
  18. BOOL bRet = FALSE;
  19. BYTE *pTemp = NULL;
  20. DWORD i = 0;
  21. BYTE *pBuf = NULL;
  22. while (TRUE)
  23. {
  24. // 查询地址空间中内存地址的信息
  25. ::RtlZeroMemory(&mbi, sizeof(mbi));
  26. dwRet = ::VirtualQueryEx(hProcess, pSearchAddress, &mbi, sizeof(mbi));
  27. if (0 == dwRet)
  28. {
  29. break;
  30. }
  31. // 过滤内存空间, 根据内存的状态和保护属性进行过滤
  32. if ((MEM_COMMIT == mbi.State) &&
  33. (PAGE_READONLY == mbi.Protect || PAGE_READWRITE == mbi.Protect ||
  34. PAGE_EXECUTE_READ == mbi.Protect || PAGE_EXECUTE_READWRITE == mbi.Protect))
  35. {
  36. // 申请动态内存
  37. pBuf = new BYTE[mbi.RegionSize];
  38. ::RtlZeroMemory(pBuf, mbi.RegionSize);
  39. // 读取整块内存
  40. bRet = ::ReadProcessMemory(hProcess, mbi.BaseAddress, pBuf, mbi.RegionSize, &dwRet);
  41. if (FALSE == bRet)
  42. {
  43. ShowError("ReadProcessMemory");
  44. break;
  45. }
  46. // 匹配内存
  47. for (i = 0; i < (mbi.RegionSize - dwSearchBufferSize); i++)
  48. {
  49. pTemp = (BYTE *)pBuf + i;
  50. if (RtlEqualMemory(pTemp, pSearchBuffer, dwSearchBufferSize))
  51. {
  52. // 显示搜索到的地址
  53. printf("0x%p\n", ((BYTE *)mbi.BaseAddress + i));
  54. }
  55. }
  56. // 释放内存
  57. delete[]pBuf;
  58. pBuf = NULL;
  59. }
  60. // 继续对下一块内存区域进行遍历
  61. pSearchAddress = pSearchAddress + mbi.RegionSize;
  62. }
  63. // 释放内存, 关闭句柄
  64. if (pBuf)
  65. {
  66. delete[]pBuf;
  67. pBuf = NULL;
  68. }
  69. ::CloseHandle(hProcess);
  70. return TRUE;
  71. }

程序测试

我们直接运行程序,对 520.exe 进程进行搜索,搜索值为 0x00905A4D 的地址都哪些,程序成功列举所有的地址。

总结

这个程序,通过以进程加载基址为搜索起始地址、判断地址内存空间信息过滤一些内存状态不是 MEM_COMMIT 以及保护属性没有读权限的内存区域,以此来缩小搜索的范围,提升搜索的效率。

大家注意理解 VirtualQueryEx 函数以及 ReadProcessMemory 函数的参数使用方式,同时也要注意不同进程内存空间的转换。

参考

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

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

发送私信

有没有那样一种永远,永远永远不改变

12
文章数
32
评论数
最近文章
eject