Palpitation的文章

  • sdfsdfsdfdfdsf

    5656565656```
    0  留言 2020-06-06 10:11:00
  • 获取指定进程的加载基址

    背景之前,自己写过一个进程内存分析的小程序,其中,就有一个功能是获取进程在内存中的加载基址。由于现在Windows系统引入了ASLR (Address Space Layout Randomization)机制,加载程序时候不再使用固定的基址加载。VS默认是开启基址随机化的,我们也可以设置它使用固定加载基址。至于什么是ASLR?或者ASLR有什么作用?本文就不深入探讨了,感兴趣的,可以自己私下了解了解。
    本文就是开发这样的一个小程序,使用两种方法来获取获取指定进程的加载基址。一种是使用进程模块快照方式,然后遍历加载模块并获取基址,另一种是直接调用WIN32 API函数EnumProcessModules,遍历加载模块基址。这两种方法本质上都是一样的。现在,我就把分析过程和实现方式写成文档,分享给大家。
    函数介绍CreateToolhelp32Snapshot 函数
    可以通过获取进程信息为指定的进程、进程使用的堆[HEAP]、模块[MODULE]、线程建立一个快照。
    函数声明
    HANDLE WINAPI CreateToolhelp32Snapshot( DWORD dwFlags, DWORD th32ProcessID );
    参数

    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 函数
    打开一个已存在的进程对象,并返回进程的句柄。
    函数声明
    HANDLE OpenProcess( DWORD dwDesiredAccess, //渴望得到的访问权限(标志) BOOL bInheritHandle, // 是否继承句柄 DWORD dwProcessId// 进程标示符);
    参数

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

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

    EnumProcessModules 函数
    在指定的进程中检索每个模块的句柄。要控制64位应用程序是否枚举32位模块,64位模块或两种类型的模块,请使用EnumProcessModulesEx函数。
    函数声明
    BOOL WINAPI EnumProcessModules( _In_ HANDLE hProcess, _Out_ HMODULE *lphModule, _In_ DWORD cb, _Out_ LPDWORD lpcbNeeded);
    参数

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

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

    实现思路该程序实现进程基址的主要原理是,遍历进程里的所有加载的模块,那么,第一个加载模块的加载基址就是该进程的加载基址。
    那么,对于进程模块加载基址的遍历就有两种两种方法。一种是根据进程模块快照获取,另一种市直接调用EnumProcessModules函数获取。现在,对这两种方法的原理分别进行介绍。
    使用进程模块快照的方式获取模块基址的原理是:

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

    首先,我们需要使用OpenProcess函数打开指定进程并获取进程的句柄
    然后,根据进程句柄调用EnumProcessModules函数获取进程加载的所有模块的加载基址,并保存在数组中。那么,第一个模块的加载基址便是该进程的加载基址
    关闭打开的进程句柄

    编程实现获取指定进程模块快照的方式遍历进程模块PVOID GetProcessImageBase1(DWORD dwProcessId){ PVOID pProcessImageBase = NULL; MODULEENTRY32 me32 = { 0 }; me32.dwSize = sizeof(MODULEENTRY32); // 获取指定进程全部模块的快照 HANDLE hModuleSnap = ::CreateToolhelp32Snapshot(TH32CS_SNAPMODULE, dwProcessId); if (INVALID_HANDLE_VALUE == hModuleSnap) { ShowError("CreateToolhelp32Snapshot"); return pProcessImageBase; } // 获取快照中第一条信息 BOOL bRet = ::Module32First(hModuleSnap, &me32); if (bRet) { // 获取加载基址 pProcessImageBase = (PVOID)me32.modBaseAddr; } // 关闭句柄 ::CloseHandle(hModuleSnap); return pProcessImageBase;}
    直接使用EnumProcessModules函数获取进程模块基址PVOID GetProcessImageBase2(DWORD dwProcessId){ PVOID pProcessImageBase = NULL; //打开进程, 获取进程句柄 HANDLE hProcess = ::OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwProcessId); if (NULL == hProcess) { ShowError("OpenProcess"); return pProcessImageBase; } // 遍历进程模块, HMODULE hModule[100] = {0}; DWORD dwRet = 0; BOOL bRet = ::EnumProcessModules(hProcess, (HMODULE *)(hModule), sizeof(hModule), &dwRet); if (FALSE == bRet) { ::CloseHandle(hProcess); ShowError("EnumProcessModules"); return pProcessImageBase; } // 获取第一个模块加载基址 pProcessImageBase = hModule[0]; // 关闭句柄 ::CloseHandle(hProcess); return pProcessImageBase;}
    程序测试我们在 main 函数中调用上述封装的函数进行测试,main 函数为:
    int _tmain(int argc, _TCHAR* argv[]){ PVOID pProcessImageBase1 = NULL; PVOID pProcessImageBase2 = NULL; pProcessImageBase1 = GetProcessImageBase1(4500); pProcessImageBase2 = GetProcessImageBase2(4500); printf("pProcessImageBase1=0x%p\npProcessImageBase2=0x%p\n", pProcessImageBase1, pProcessImageBase2); system("pause"); return 0;}
    测试结果
    我们运行程序,程序执行成功,并显示两种方法获取的进程加载基址,而且获取结果都相同。

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

    总结这两种方式,本质上都是一样的,只是遍历进程加载模块所使用的方法不相同。那么,进程加载的第一个模块的加载基址,便是进程的加载基址。
    参考参考自《Windows黑客编程技术详解》一书
    3  留言 2018-12-20 12:37:03

发送私信

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

11
文章数
14
评论数
eject