0x01 前言
近期在写项目时遇到需要获取其他进程命令行字符串的需求,查阅 MSDN 后发现微软并未在 Windows SDK 中提供相关的 WINAPI。Google 后发现了一些文章,一小部分包含具体实现(通过分析 WINAPI GetCommandLine 的反汇编实现并计算偏移量地址后通过 WINAPI ReadProcessMemory 跨进程读内存),但都不符合我的预期,因此就自己实现了一个。
0x02 原理
深入学习过 Windows 或做 Windows 驱动开发的开发者应该都知道在 NTDLL 中有导出符号为 ZwQueryInformationProcess(NtQueryInformationProcess,这两个 NTAPI 在用户层上没有区别) 的 NTAPI,而其参数 ProcessInformationClass 也即 PROCESSINFOCLASS 枚举器的 ProcessBasicInformation 可以使其查询指定进程的 PEB 信息(PROCESS_BASIC_INFORMATION -> PPEB)。而 PPEB 则保存着位于目标进程虚拟地址空间中的 PEB 结构的地址。在 PEB 结构中有 ProcessParameters 成员,该成员指向 RTL_USER_PROCESS_PARAMETERS 结构。而在 RTL_USER_PROCESS_PARAMETERS 结构中的 CommandLine 成员(UNICODE_STRING),则保存着目标进程的命令行字符串。
0x03 实现与描述
列出实现流程:
通过 0x02 中所描述的原理得出大致实现流程如下:
- 通过 LoadLibrary(已 load 则 GetModuleHandle)和 GetProcAddress 计算得出 NTDLL!ZwQueryInformationProcess 的地址
- 调用 NTDLL!ZwQueryInformationProcess 并传入 PROCESSINFOCLASS::ProcessBasicInformation 以查询目标进程的 PEB 信息
- 调用 ReadProcessMemory 跨进程读取 PROCESS_BASIC_INFORMATION -> PebBaseAddress 进本地 PEB 结构缓冲区
- 调用 ReadProcessMemory 跨进程读取 PEB -> ProcessParameters 进本地 RTL_USER_PROCESS_PARAMETERS 缓冲区
- 通过 RTL_USER_PROCESS_PARAMETERS -> CommandLine(UNICODE_STRING) -> Length 字节数预先分配 Uniocde 缓冲区
- 调用 ReadProcessMemory 跨进程读取 RTL_USER_PROCESS_PARAMETERS -> CommandLine(UNICODE_STRING) -> Buffer 进预先分配的 Unicode 缓冲区
- 目标进程的命令行字符串位于第 6 步所分配 Unicode 缓冲区中
代码实现:
通过整理上述实现流程,代码实现如下:
- 实现 GetProcessCommandLineW 即 Unicode Version
- 实现 GetProcessCommandLineA 即 Ansi Version
PS:代码无需复制,文章结尾有提供 源代码 和 Demo 的下载。
头文件(pscmd.h):
#pragma once
/* Contains the necessary Windows SDK header files */
#include "windows.h"
#include "winternl.h"
/* Defining the request result status code in NTSTATUS */
#define STATUS_SUCCESS ((NTSTATUS)0x00000000L)
/* Automatically selects the corresponding API based on the character set type of the current project */
#ifdef _UNICODE
typedef WCHAR* PCMDBUFFER_T;
#define GetProcessCommandLine GetProcessCommandLineW
#else
typedef CHAR* PCMDBUFFER_T;
#define GetProcessCommandLine GetProcessCommandLineA
#endif
/* Multiple version declarations for GetProcessCommandLine */
BOOL
WINAPI
GetProcessCommandLineW(
_In_ HANDLE hProcess,
_In_opt_ LPCWSTR lpcBuffer,
_In_opt_ SIZE_T nSize,
_In_opt_ SIZE_T* lpNumberOfBytesCopied
);
BOOL
WINAPI
GetProcessCommandLineA(
_In_ HANDLE hProcess,
_In_opt_ LPCSTR lpcBuffer,
_In_opt_ SIZE_T nSize,
_In_opt_ SIZE_T* lpNumberOfBytesCopied
);
cpp 文件(pscmd.cpp):
#include "pscmd.h"
/* NTAPI ZwQueryInformationProcess */
typedef NTSTATUS(NTAPI * Typedef_ZwQueryInformationProcess)(HANDLE, PROCESSINFOCLASS, PVOID, ULONG, PULONG);
Typedef_ZwQueryInformationProcess pNTAPI_ZwQueryInformationProcess =
(Typedef_ZwQueryInformationProcess)GetProcAddress(GetModuleHandle(L"ntdll.dll"), "ZwQueryInformationProcess");
/*
获取指定进程命令行字符串,失败返回 FALSE(Unicode Version)
*/
BOOL WINAPI GetProcessCommandLineW(HANDLE hProcess, LPCWSTR lpcBuffer, SIZE_T nSize, SIZE_T* lpNumberOfBytesCopied)
{
BOOL result = FALSE;
if (pNTAPI_ZwQueryInformationProcess)
{
PROCESS_BASIC_INFORMATION BasicInfo; memset(&BasicInfo, NULL, sizeof(BasicInfo));
PEB PebBaseInfo; memset(&PebBaseInfo, NULL, sizeof(PebBaseInfo));
RTL_USER_PROCESS_PARAMETERS ProcessParameters; memset(&ProcessParameters, NULL, sizeof(ProcessParameters));
if (pNTAPI_ZwQueryInformationProcess(hProcess, PROCESSINFOCLASS::ProcessBasicInformation, &BasicInfo, sizeof(BasicInfo), NULL) == STATUS_SUCCESS)
{
if (ReadProcessMemory(hProcess, BasicInfo.PebBaseAddress, &PebBaseInfo, sizeof(PebBaseInfo), NULL)
&& ReadProcessMemory(hProcess, PebBaseInfo.ProcessParameters, &ProcessParameters, sizeof(ProcessParameters), NULL))
{
if (lpcBuffer && nSize >= ProcessParameters.CommandLine.Length + 2)
result = ReadProcessMemory(hProcess, ProcessParameters.CommandLine.Buffer, (LPVOID)lpcBuffer,
ProcessParameters.CommandLine.Length, lpNumberOfBytesCopied);
else if (lpNumberOfBytesCopied) { *lpNumberOfBytesCopied = ProcessParameters.CommandLine.Length + 2; result = TRUE; }
}
}
}
return result;
}
/*
获取指定进程命令行字符串,失败返回 FALSE(Ansi Version)
--------
GetProcessCommandLineA 是基于 GetProcessCommandLineW 的 Ansi 版本,应用程序应尽可能使用 GetProcessCommandLineW,而不是此 GetProcessCommandLineA
*/
BOOL WINAPI GetProcessCommandLineA(HANDLE hProcess, LPCSTR lpcBuffer, SIZE_T nSize, SIZE_T* lpNumberOfBytesCopied)
{
BOOL result = FALSE;
SIZE_T nCommandLineSize = NULL;
if (GetProcessCommandLineW(hProcess, NULL, NULL, &nCommandLineSize))
{
WCHAR* lpLocalBuffer = (WCHAR*)malloc(nCommandLineSize);
if (lpLocalBuffer)
{
memset(lpLocalBuffer, NULL, nCommandLineSize);
if (GetProcessCommandLineW(hProcess, lpLocalBuffer, nCommandLineSize, &nCommandLineSize))
{
INT iNumberOfBytes = WideCharToMultiByte(CP_ACP, NULL, lpLocalBuffer, nCommandLineSize, (LPSTR)lpcBuffer, nSize, NULL, NULL);
if (lpNumberOfBytesCopied) *lpNumberOfBytesCopied = (!lpcBuffer || (nSize < (iNumberOfBytes + 1))) ? iNumberOfBytes + 1 : iNumberOfBytes;
result = iNumberOfBytes > 0;
}
free(lpLocalBuffer);
}
}
return result;
}
Demo(consoleapp1.cpp):
#include "pscmd.h"
#include "windows.h"
int main()
{
/* 以当前进程为例,应使用 OpneProcess 打开当前进程,而不是调用 GetCurrentProcess 获得伪句柄 */
HANDLE hProcess = OpenProcess(PROCESS_QUERY_INFORMATION | PROCESS_VM_READ, FALSE, GetCurrentProcessId());
if (hProcess)
{
/*
进程句柄 hProcess 应具有 PROCESS_QUERY_INFORMATION | PROCESS_VM_READ 访问权限,否则将导致 GetProcessCommandLine 失败。
应用程序应尽可能使用 GetProcessCommandLineW 即 Unicode 版本,而不是 Ansi 版本,这有助于提高应用程序执行性能且防止在特定语言下丢失数据。
pscmd.h header file 默认通过当前项目的字符集设置来确定所调用的 GetProcessCommandLine 版本与 PCMDBUFFER_T 缓冲区类型定义(Unicode 或 Ansi)。
*/
SIZE_T nCommandLineSize = NULL;
if (GetProcessCommandLine(hProcess, NULL, NULL, &nCommandLineSize)) // 将 lpcBuffer 和 nSize 设置为 NULL 以获取建议缓冲区大小(nCommandLineSize)
{
/* 在堆上分配建议大小的 Unicode 缓冲区 */
PCMDBUFFER_T lpUnicodeBuffer = (PCMDBUFFER_T)malloc(nCommandLineSize);
if (lpUnicodeBuffer)
{
/* 使用 memset(或 WINAPI ZeroMemory)将分配的 Unicode 缓冲区初始化为 zero */
memset(lpUnicodeBuffer, NULL, nCommandLineSize);
// ZeroMemory(lpUnicodeBuffer, nCommandLineSize);
/* 再次调用 GetProcessCommandLine 并传入所分配的 Unicode 缓冲区,以取得实际数据 */
if (GetProcessCommandLine(hProcess, lpUnicodeBuffer, nCommandLineSize, &nCommandLineSize))
{
/* nCommandLineSize 的值当前为 GetProcessCommandLine 实际复制到 Uniocde 缓冲区 lpUnicodeBuffer 中的字节数,打印 lpUnicodeBuffer */
wprintf_s(lpUnicodeBuffer);
}
/* 释放 Unicode 缓冲区 */
free(lpUnicodeBuffer);
}
}
/* 关闭进程句柄 */
CloseHandle(hProcess);
}
return 0;
}
0x04 使用方式
API 命名与版本
GetProcessCommandLine 一共有 2 个版本的实现,分别是:
- GetProcessCommandLineA,也即 Ansi 版本
- GetProcessCommandLineW,也即 Unicode 版本
pscmd.h 头文件中通过对宏定义的检查以确定当前项目的字符集类型,从而自动选择对应的字符集实现版本(与 Windows SDK 相同)
PS:应用程序应尽可能使用 GetProcessCommandLine 的 Unicode 版本,这有助于提高应用程序执行性能并防止在部分语言下丢失数据
缓冲区类型
- Ansi 版本的 GetProcessCommandLine 则缓冲区类型应为 Ansi 多字节缓冲区,也即 CHAR*
- Unicode 版本的 GetProcessCommandLine 则缓冲区类型应为 Unicode 宽字符缓冲区, 也即 WCHAR*
pscmd.h 头文件中定义了 PCMDBUFFER_T 类型,通过对宏定义的检查以确定当前项目的字符集类型,从而使 PCMDBUFFER_T 自动选择对应的字符集实现类型(CHAR* / WCHAR*)
PS:应用程序应尽可能使用 PCMDBUFFER_T 的 Unicode 版本,避免在部分语言下丢失数据
函数原型
Unicode 版本:
BOOL
WINAPI
GetProcessCommandLineW(
_In_ HANDLE hProcess,
_In_opt_ LPCWSTR lpcBuffer,
_In_opt_ SIZE_T nSize,
_In_opt_ SIZE_T* lpNumberOfBytesCopied
);
Ansi 版本:
BOOL
WINAPI
GetProcessCommandLineA(
_In_ HANDLE hProcess,
_In_opt_ LPCSTR lpcBuffer,
_In_opt_ SIZE_T nSize,
_In_opt_ SIZE_T* lpNumberOfBytesCopied
);
函数参数
- hProcess [in]:
需获取其命令行字符串的进程句柄,该句柄应具有 PROCESS_QUERY_INFORMATION 和 PROCESS_VM_READ 访问权限 - lpcBuffer [in, optional]:
指向接收命令行字符串的宽字符(Ansi 版本应为多字节)缓冲区指针,缓冲区应使用 memset 初始化为 zero,该参数可以为 NULL - nSize [in, optional]:
参数 lpcBuffer 指向的宽字符缓冲区有效大小(Bytes),该参数可以为 NULL - lpNumberOfBytesCopied [in, optional]:
实际复制到 lpcBuffer 指向的宽字符缓冲区中的字节数(Bytes),如 lpcBuffer 为 NULL 或 nSize 太小,该参数将返回所需缓冲区的建议大小(Bytes),该参数可以为 NULL
0x05 源代码下载
将 pscmd.h 和 pscmd.cpp 加入项目后包含 pscmd.h 即可。
#include "pscmd.h"
0x06 总结
由于 GetProcessCommandLine 的实现原理为调用 NTDLL!ZwQueryInformationProcess 查询目标进程 PEB 信息并跨进程读内存实现,微软可能会在今后的 Windows 版本中修改 NTDLL!ZwQueryInformationProcess。另外,GetProcessCommandLine 支持 Win7 或更高的 Windows x86 / x64 版本,并不支持 Windows XP。代码实现方面如有错误或不足之处,请在下面评论区指正或通过 Blog 关于页面联系我,多谢!
本文由 小云云 创作,采用 知识共享署名4.0 国际许可协议进行许可
本站文章除注明转载/出处外,均为本站原创或翻译,转载前请务必署名
最后编辑时间为: Apr 4, 2020 at 06:56 pm