获取指定进程的加载基址

背景

之前,自己写过一个进程内存分析的小程序,其中,就有一个功能是获取进程在内存中的加载基址。由于现在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
    指定快照中包含的系统内容,这个参数能够使用下列数值(常量)中的一个或多个:
VALUEMEANING
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黑客编程技术详解》一书

  • 2
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
获取指定进程的特定模块中的变量或函数地址,可以使用 `Process` 类来实现。以下是一个获取指定进程模块中变量地址的示例代码: ```csharp using System; using System.Diagnostics; using System.Runtime.InteropServices; class Program { static void Main(string[] args) { // 获取目标进程 Process targetProcess = Process.GetProcessesByName("process_name")[0]; // 获取模块基址 ProcessModule module = targetProcess.MainModule; IntPtr baseAddress = module.BaseAddress; // 定义偏移 int offset = 0x10; // 计算变量地址 IntPtr variableAddress = baseAddress + offset; // 打开目标进程的句柄 IntPtr processHandle = OpenProcess(ProcessAccessFlags.VirtualMemoryRead, false, targetProcess.Id); // 读取变量值 int value; bool success = ReadProcessMemory(processHandle, variableAddress, out value, sizeof(int), IntPtr.Zero); // 关闭进程句柄 CloseHandle(processHandle); if (success) { Console.WriteLine("Variable value: " + value); } else { Console.WriteLine("Failed to read variable value."); } Console.ReadKey(); } [DllImport("kernel32.dll", SetLastError = true)] static extern IntPtr OpenProcess(ProcessAccessFlags dwDesiredAccess, bool bInheritHandle, int dwProcessId); [DllImport("kernel32.dll", SetLastError = true)] static extern bool ReadProcessMemory(IntPtr hProcess, IntPtr lpBaseAddress, out int lpBuffer, int dwSize, IntPtr lpNumberOfBytesRead); [DllImport("kernel32.dll", SetLastError = true)] static extern bool CloseHandle(IntPtr hObject); [Flags] public enum ProcessAccessFlags : uint { VirtualMemoryRead = 0x00000010, VirtualMemoryWrite = 0x00000020, VirtualMemoryOperation = 0x00000008, ProcessQueryInformation = 0x00000400, ProcessVmRead = 0x00001000 } } ``` 在这个示例中,我们使用 `Process.GetProcessesByName` 方法获取进程名为 "process_name" 的进程,并且通过 `OpenProcess` 打开了该进程的句柄,最后使用 `ReadProcessMemory` 方法读取了变量的值。需要注意的是,为了读取指定进程的内存,我们需要使用 `ProcessAccessFlags.VirtualMemoryRead` 权限打开进程句柄,否则会出现 "访问被拒绝" 的错误。 如果要获取函数地址,可以将偏移设置为函数在模块中的 RVA(相对虚拟地址)。例如,如果要获取 `kernel32.dll` 模块中的 `LoadLibraryA` 函数地址,可以将偏移设置为该函数在模块中的 RVA,即 `0x00013C20`。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值