# 修改指定进程PEB中路径和命令行信息实现进程伪装
# 背景
所谓的进程伪装,指的修改任意一个指定进程的信息,是它的信息在系统中的显示是另一个进程的信息,这样看来,指定的进程就像是被伪装的进程一样,因为进程信息相同,但实际上,它还是原来的进程,做着原来的进程操作。
本文就是要介绍进程伪装的技术,实现在 32 位系统和 64 位系统上的进程伪装。现在,我就把实现过程和原理整理成文档,分享给大家。
# 函数介绍
## NtQueryInformationProcess 函数
> [NtQueryInformationProcess可能在Windows的未来版本中更改或不可用]
> 获取指定进程的信息。
>
> 函数声明
>
> ```c++
> NTSTATUS WINAPI NtQueryInformationProcess(
> _In_ HANDLE ProcessHandle,
> _In_ PROCESSINFOCLASS ProcessInformationClass,
> _Out_ PVOID ProcessInformation,
> _In_ ULONG ProcessInformationLength,
> _Out_opt_ PULONG ReturnLength
> );
> ```
>
> 参数
>
> - ProcessHandle [in]
> 要获取信息的进程的句柄。
>
> - ProcessInformationClass [in]
> 要获取的进程信息的类型。 该参数可以是PROCESSINFOCLASS枚举中的以下值之一:
>
> | VALUE | MEANING |
> | --------------------------- | ---------------------------------------- |
> | ProcessBasicInformation | 检索指向PEB结构的指针,该结构可用于确定是否正在调试指定的进程,以及系统用于标识指定进程的唯一值 |
> | ProcessDebugPort | 获取作为进程调试器的端口号的DWORD_PTR值。 非零值表示该进程正在Ring3调试器的控制下运行 |
> | ProcessWow64Information | 确定进程是否在WOW64环境中运行(WOW64是允许基于Win32的应用程序在64位Windows上运行的x86模拟器) |
> | ProcessImageFileName | 检索包含该进程的映像文件名称的UNICODE_STRING值 |
> | ProcessBreakOnTermination | 检索指示进程是否被视为关键的ULONG值。注意可以在具有SP3的Windows XP中启动该值。 从Windows 8.1开始,应该使用IsProcessCritical |
> | ProcessSubsystemInformation | 检索指示进程的子系统类型的SUBSYSTEM_INFORMATION_TYPE值。 ProcessInformation参数指向的缓冲区应足够大以容纳单个SUBSYSTEM_INFORMATION_TYPE枚举 |
>
> - *ProcessInformation* [out]
> 指向由调用应用程序提供的缓冲区的指针,函数写入请求的信息。 所写信息的大小取决于ProcessInformationClass参数的数据类型:
>
> - PROCESS_BASIC_INFORMATION
> 当ProcessInformationClass参数为ProcessBasicInformation时,ProcessInformation参数指向的缓冲区应足够大,以容纳具有以下布局的单个PROCESS_BASIC_INFORMATION结构:
>
> - ```c++
> typedef struct _PROCESS_BASIC_INFORMATION {
> PVOID Reserved1;
> PPEB PebBaseAddress;
> PVOID Reserved2[2];
> ULONG_PTR UniqueProcessId;
> PVOID Reserved3;
> } PROCESS_BASIC_INFORMATION;
> ```
>
> UniqueProcessId成员指向该过程的系统唯一标识符。 最好使用GetProcessId函数来检索这些信息。
> PebBaseAddress成员指向PEB结构。
> 该结构的其他成员保留供操作系统内部使用。
>
> - ProcessInformationLength [in]
> ProcessInformation参数指向的缓冲区的大小(以字节为单位)。
>
> - ReturnLength [out,optional]
> 指向变量的指针,其中函数返回所请求信息的大小。 如果函数成功,这是由ProcessInformation参数指向缓冲区的信息的大小,但是如果缓冲区太小,这是成功接收信息所需的最小缓冲区大小。
>
> 返回值
>
> - 该函数返回一个NTSTATUS成功或错误代码。
> - NTSTATUS错误代码的形式和意义在DDK中提供的Ntstatus.h头文件中列出,并在DDK文档中介绍,内核模式驱动程序架构/设计指南/驱动程序编程技术/日志记录错误。
>
> 注意
>
> - NtQueryInformationProcess函数及其返回的结构在操作系统内部,并可能会从一个版本的Windows更改为另一个版本。 为了保持应用程序的兼容性,最好使用ProcessInformationClass参数描述中提到的公共函数。
> - 如果您使用NtQueryInformationProcess,请通过运行时动态链接访问该函数。 如果功能已被更改或从操作系统中删除,这将使您的代码有机会正常响应。 但签名变更可能无法检测。
> - 此功能没有关联的导入库。 您必须使用LoadLibrary和GetProcAddress函数动态链接到Ntdll.dll。
# 实现原理
进程伪装的原理不是很复杂,就是修改指定进程的进程环境块 PEB 中的进程路径以及命令行信息,即可达到进程伪装的效果。
具体实现原理分析如下:
- 首先,我们根据进程的 PID 号打开指定进程,并获取进程的句柄。
- 然后,我们要从 ntdll.dll 中获取 NtQueryInformationProcess 函数的导出地址,因为 NtQueryInformationProcess 函数没有关联导入库,所以只能动态获取,这个函数是这个程序功能实现的关键步骤。
- 接着,使用 NtQueryInformationProcess 函数获取指定进程的进程基本信息 PROCESS_BASIC_INFORMATION,并从中获取指定进程的进程环境块 PEB。
- 然后,我们就可以根据进程环境块中的 ProcessParameters,获取指定进程的 RTL_USER_PROCESS_PARAMETERS 信息,因为PEB的路径信息、命令行信息存储在这个结构体中。
- 最后,我们开始对指定进程 PEB 中路径信息、命令行信息进行更改,实现进程伪装。
# 编码实现
```c++
// 修改指定进程的进程环境块PEB中的路径和命令行信息, 实现进程伪装
BOOL DisguiseProcess(DWORD dwProcessId, wchar_t *lpwszPath, wchar_t *lpwszCmd)
{
// 打开进程获取句柄
HANDLE hProcess = ::OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwProcessId);
if (NULL == hProcess)
{
ShowError("OpenProcess");
return FALSE;
}
typedef_NtQueryInformationProcess NtQueryInformationProcess = NULL;
PROCESS_BASIC_INFORMATION pbi = { 0 };
PEB peb = { 0 };
RTL_USER_PROCESS_PARAMETERS Param = { 0 };
USHORT usCmdLen = 0;
USHORT usPathLen = 0;
// 需要通过 LoadLibrary、GetProcessAddress 从 ntdll.dll 中获取地址
NtQueryInformationProcess = (typedef_NtQueryInformationProcess)::GetProcAddress(
::LoadLibrary("ntdll.dll"), "NtQueryInformationProcess");
if (NULL == NtQueryInformationProcess)
{
ShowError("GetProcAddress");
return FALSE;
}
// 获取指定进程的基本信息
NTSTATUS status = NtQueryInformationProcess(hProcess, ProcessBasicInformation, &pbi, sizeof(pbi), NULL);
if (!NT_SUCCESS(status))
{
ShowError("NtQueryInformationProcess");
return FALSE;
}
/*
注意在读写其他进程的时候,注意要使用ReadProcessMemory/WriteProcessMemory进行操作,
每个指针指向的内容都需要获取,因为指针只能指向本进程的地址空间,必须要读取到本进程空间。
要不然一直提示位置访问错误!
*/
// 获取指定进程进本信息结构中的PebBaseAddress
::ReadProcessMemory(hProcess, pbi.PebBaseAddress, &peb, sizeof(peb), NULL);
// 获取指定进程环境块结构中的ProcessParameters, 注意指针指向的是指定进程空间中
::ReadProcessMemory(hProcess, peb.ProcessParameters, &Param, sizeof(Param), NULL);
// 修改指定进程环境块PEB中命令行信息, 注意指针指向的是指定进程空间中
usCmdLen = 2 + 2 * ::wcslen(lpwszCmd);
::WriteProcessMemory(hProcess, Param.CommandLine.Buffer, lpwszCmd, usCmdLen, NULL);
::WriteProcessMemory(hProcess, &Param.CommandLine.Length, &usCmdLen, sizeof(usCmdLen), NULL);
// 修改指定进程环境块PEB中路径信息, 注意指针指向的是指定进程空间中
usPathLen = 2 + 2 * ::wcslen(lpwszPath);
::WriteProcessM