获取程序的PID和模块基址

​ PID是Process ID的缩写,每个正在运行的软件都会有各自的PID,类似于身份证号。只要进程被运行,操作系统会给每个运行的进程分配一个数字标识符。获得PID就可以对指定的进程进行内存修改,附加调试器等等操作。正常情况下打开Windows系统的任务管理器就能看到所有进程的PID信息:

​ 模块基址(Module Base Address)指的是被加载到内存中程序的内存空间起始地址。每打开一个进程操作系统除了分配PID之外还会给程序分配一段内存地址用来存放程序的代码和数据。这个内存地址的起点就是模块基址。PID是身份证号情况下模块基址相当于是家庭住址。

​ PID和模块基址对于逆向分析和内存分析来说至关重要,下面我会介绍外部程序(.exe程序)和DLL注入(内部注入)获取程序的PID和模块基址的具体方法。

一,外部程序

1.PID获取

Windows API

​ 外部程序主要是通过调用 Windows API 的方式来获取其他程序的所有信息。要获取PID有几个API需要先了解分别是CreateToolhelp32Snapshot 函数:

1
2
3
4
HANDLE CreateToolhelp32Snapshot(
[in] DWORD dwFlags,
[in] DWORD th32ProcessID
);

​ 通过这个函数可以获取程序的快照信息,第二个参数为NULL时可以获取所有进程的快照信息。这里的快照包含了系统当前状态的各种信息,比如进程信息,线程信息,堆信息,模块信息等等。剩下两个API分别是Process32First 函数和Process32Next 函数:

1
2
3
4
5
6
7
8
BOOL Process32First(
[in] HANDLE hSnapshot,
[in, out] LPPROCESSENTRY32 lppe
);
BOOL Process32Next(
[in] HANDLE hSnapshot,
[out] LPPROCESSENTRY32 lppe
);

​ 这两个函数分别用来检索快照当中的第一个和下一个进程信息,可以用来遍历所有进程找到适合自己的进程然后获取所需要的信息。

具体实现

​ 利用这两个API就可以很轻松的实现找到PID的操作,首先要先提供一个自己所需的程序名然后创建所有进程的快照。把这些快照中的程序名信息一个个拿出来跟我们提供的程序名进行对比,把对比成功的快照留下再从中获取我们需要的PID信息就行。下面是具体实现方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
DWORD mem::getProcId(const wchar_t* procName)
{
DWORD retProcID = 0; // 初始化返回的进程ID

// 创建系统进程快照
HANDLE hSnap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, NULL);

if (hSnap != INVALID_HANDLE_VALUE)
{
PROCESSENTRY32 procEntry;
procEntry.dwSize = sizeof(PROCESSENTRY32); // 设置结构体大小

// 获取第一个进程信息
if (Process32First(hSnap, &procEntry))
{
do
{
// 比较进程名(不区分大小写)
if (!_wcsicmp(procName, procEntry.szExeFile))
{
retProcID = procEntry.th32ProcessID; // 找到目标进程,保存ID
break; // 结束循环
}
} while (Process32Next(hSnap, &procEntry)); // 获取下一个进程信息
}
else
{
return 0; // 如果无法获取第一个进程,返回0
}
}
CloseHandle(hSnap); // 关闭快照句柄
return retProcID; // 返回找到的进程ID,如果未找到则为0
}

2.模块基址的获取

Windows API

​ 模块基址的获取跟获取PID类似都是通过遍历系统快照来获取的,CreateToolhelp32Snapshot 函数的第二个参数中需要明确表明PID为了只针对单个进程遍历模块名。Module32First 函数和 Module32Next 函数:

1
2
3
4
5
6
7
8
BOOL Module32First(
[in] HANDLE hSnapshot,
[in, out] LPMODULEENTRY32 lpme
);
BOOL Module32Next(
[in] HANDLE hSnapshot,
[out] LPMODULEENTRY32 lpme
);

​ 这两个是用来检索进程或线程关联的下一个模块的相关信息。比如在大部分软件当中一个可执行程序(.exe)程序会附带一堆dll模块,这两个函数可以遍历每一个模块直到找到自己所需要的。

具体实现

​ 获取模块基址的步骤跟获取PID的步骤大差不差,都是通过快照遍历的方式来获取。获取模块基址之前要先获取PID作为CreateToolhelp32Snapshot 函数的第二个参数来传入。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
uintptr_t mem::getModuleBase(const wchar_t* procName, DWORD procID)
{
uintptr_t retAddress = 0; // 初始化返回的模块基址

// 创建指定进程的模块快照
HANDLE hSnap = CreateToolhelp32Snapshot(TH32CS_SNAPMODULE | TH32CS_SNAPMODULE32, procID);

if (hSnap != INVALID_HANDLE_VALUE)
{
MODULEENTRY32 modEntry;
modEntry.dwSize = sizeof(MODULEENTRY32); // 设置结构体大小

// 获取第一个模块信息
if (Module32First(hSnap, &modEntry))
{
do
{
// 比较模块名(不区分大小写)
if (!_wcsicmp(procName, modEntry.szModule))
{
retAddress = (uintptr_t)modEntry.modBaseAddr; // 找到目标模块,保存基址
break; // 结束循环
}
} while (Module32Next(hSnap, &modEntry)); // 获取下一个模块信息
}
}
CloseHandle(hSnap); // 关闭快照句柄
return retAddress; // 返回找到的模块基址,如果未找到则为0
}

二,DLL注入

​ DLL注入的全称为 Dynamic-Link Library Injection ,是一种将DLL模块强制加载到进程内存空间的技术。这个技术有多重用途比如安全监控,恶意软件攻击,调试程序等等。DLL注入说白了就是把自己写的代码强行写入到指定的程序让目标程序加载自己的代码。由于自身的代码已经在目标进程的内存空间当中了所以没有必要获取目标进程的PID了。

Windows API

​ 在内部代码中获取基址变得极其简单,只需要调用Windows API中的GetModuleHandle 函数,这个函数只有一个参数接受模块的名称(.dll 或 .exe 文件)。这个参数为NULL的话,返回的则是当前进程主模块的基地址。

1
2
3
HMODULE GetModuleHandleA(
[in, optional] LPCSTR lpModuleName
);

​ 可以发现这个函数的返回值并不是地址值而是句柄(HMODULE),在Windows内存管理体系中模块句柄直接等价于模块的基地址指针。在使用中直接把返回值强制转换成指针就行了。

具体实现

​ 在实际使用中需要获取的基址为主要可执行文件的话(.exe)可以直接传NULL参即可。要是需要获取DLL模块的基址的话就需要传入模块名,传入模块名不加后缀默认按.dll来算。

1
2
uintptr_t baseAddress = (uintptr_t)GetModuleHandle(NULL);
uintptr_t baseAddress = (uintptr_t)GetModuleHandle(L"xxx.dll");

有需要补充的或者需要整改的可在评论区回复!