VC 获取指定进程命令行

in 开源 with 0 comment

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 中所描述的原理得出大致实现流程如下:

  1. 通过 LoadLibrary(已 load 则 GetModuleHandle)和 GetProcAddress 计算得出 NTDLL!ZwQueryInformationProcess 的地址
  2. 调用 NTDLL!ZwQueryInformationProcess 并传入 PROCESSINFOCLASS::ProcessBasicInformation 以查询目标进程的 PEB 信息
  3. 调用 ReadProcessMemory 跨进程读取 PROCESS_BASIC_INFORMATION -> PebBaseAddress 进本地 PEB 结构缓冲区
  4. 调用 ReadProcessMemory 跨进程读取 PEB -> ProcessParameters 进本地 RTL_USER_PROCESS_PARAMETERS 缓冲区
  5. 通过 RTL_USER_PROCESS_PARAMETERS -> CommandLine(UNICODE_STRING) -> Length 字节数预先分配 Uniocde 缓冲区
  6. 调用 ReadProcessMemory 跨进程读取 RTL_USER_PROCESS_PARAMETERS -> CommandLine(UNICODE_STRING) -> Buffer 进预先分配的 Unicode 缓冲区
  7. 目标进程的命令行字符串位于第 6 步所分配 Unicode 缓冲区中

代码实现:

通过整理上述实现流程,代码实现如下:

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 个版本的实现,分别是:

pscmd.h 头文件中通过对宏定义的检查以确定当前项目的字符集类型,从而自动选择对应的字符集实现版本(与 Windows SDK 相同)

PS:应用程序应尽可能使用 GetProcessCommandLine 的 Unicode 版本,这有助于提高应用程序执行性能并防止在部分语言下丢失数据

缓冲区类型

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
);

函数参数

0x05 源代码下载

将 pscmd.h 和 pscmd.cpp 加入项目后包含 pscmd.h 即可。

#include "pscmd.h"

pscmd-18518.1-release.zip

0x06 总结

由于 GetProcessCommandLine 的实现原理为调用 NTDLL!ZwQueryInformationProcess 查询目标进程 PEB 信息并跨进程读内存实现,微软可能会在今后的 Windows 版本中修改 NTDLL!ZwQueryInformationProcess。另外,GetProcessCommandLine 支持 Win7 或更高的 Windows x86 / x64 版本,并不支持 Windows XP。代码实现方面如有错误或不足之处,请在下面评论区指正或通过 Blog 关于页面联系我,多谢!

分享您的观点