Module 6: Beacon User Data (BUD)
The contract between loader and Beacon — pre-resolved syscalls, RTL functions, and full memory layout awareness delivered through a single structured data block.
Module Objective
Understand the USER_DATA structure and every sub-structure it contains. By the end of this module you will know how Crystal-Loaders populates BUD, what data Beacon expects to receive, and why this mechanism is critical for sleep masking, indirect syscalls, and clean memory cleanup. This is what makes Crystal-Loaders more than just a loader: it provides Beacon with a complete runtime environment.
1. What Is BUD?
BUD (Beacon User Data) is the mechanism by which a UDRL (User-Defined Reflective Loader) communicates loader-resolved data to the Beacon payload after loading it into memory. It is not a Cobalt Strike "feature" in the UI sense — it is a structured binary contract between the loader and Beacon's internal runtime.
The concept was introduced with the original UDRL feature in Cobalt Strike 4.4, but it was significantly expanded in CS 4.11 to support a much richer set of pre-resolved data. Crystal-Loaders targets the 4.11 BUD specification.
How BUD Is Delivered
The loader populates a USER_DATA structure in memory and then passes a pointer to it via a special DllMain call. Cobalt Strike defines a custom fdwReason value:
C#define DLL_BEACON_USER_DATA 0x0d // fdwReason = 13
// The loader calls Beacon's DllMain with:
DllMain(hInstance, DLL_BEACON_USER_DATA, (LPVOID)&userData);
Beacon's DllMain checks for this reason code and, when it receives it, copies the pointer and stores the USER_DATA internally. This call happens after the standard DLL_PROCESS_ATTACH call but before Beacon enters its main command loop.
Once Beacon has the USER_DATA pointer, it uses the pre-resolved data for four critical capabilities:
| Capability | BUD Field Used | Why It Matters |
|---|---|---|
| Indirect syscall execution | syscalls (SYSCALL_API) | Beacon can call Nt* functions through pre-resolved syscall;ret gadgets without resolving them itself |
| Sleep mask memory tracking | allocatedMemory | The sleep mask knows exactly which memory regions to encrypt/decrypt and what permissions to restore |
| Memory cleanup on exit | allocatedMemory | Beacon can cleanly free all loader-allocated memory using the correct deallocation API for each region |
| RTL function calls | rtls (RTL_API) | Beacon can convert DOS paths to NT paths and manage heap memory without resolving these functions itself |
2. The USER_DATA Structure
The top-level structure that ties everything together. Every field is a pointer to a sub-structure (or a fixed-size buffer), and Beacon validates the version field before touching anything else.
Ctypedef struct {
unsigned int version; // CS version: 0x041100 = 4.11
PSYSCALL_API syscalls; // Pre-resolved syscall entries (36 Nt*)
char custom[BEACON_USER_DATA_CUSTOM_SIZE]; // User-defined field
PRTL_API rtls; // Pre-resolved RTL functions
PALLOCATED_MEMORY allocatedMemory; // Memory region tracking
} USER_DATA;
Field-by-Field Breakdown
| Field | Type | Description |
|---|---|---|
version | unsigned int | Must match the Cobalt Strike version that built the payload. For CS 4.11 this is 0x041100. Beacon validates this value immediately — if it does not match, Beacon ignores the entire USER_DATA structure. This prevents version-mismatched loaders from corrupting Beacon's state. |
syscalls | PSYSCALL_API | Pointer to the SYSCALL_API structure containing all 36 Nt* function entries. Each entry holds the function address, a syscall;ret gadget address, and the System Service Number. Beacon uses these for all native API calls, routing them through indirect syscalls. |
custom[BEACON_USER_DATA_CUSTOM_SIZE] | char[] | A user-defined buffer (sized by BEACON_USER_DATA_CUSTOM_SIZE) that the loader can use to pass arbitrary data to Beacon. This could be encryption keys, configuration flags, a nonce, or any other operator-defined data. Beacon does not interpret these bytes — they are available to post-exploitation scripts and BOFs via the Beacon User Data API. |
rtls | PRTL_API | Pointer to the RTL_API structure with 3 resolved RTL (Runtime Library) function addresses from ntdll and the PEB. These are functions Beacon needs internally but that are not part of the syscall table. |
allocatedMemory | PALLOCATED_MEMORY | Pointer to the ALLOCATED_MEMORY structure that tracks up to 6 memory regions with up to 8 sections each. This gives Beacon (and its sleep mask) a complete map of every memory region the loader allocated, including section-level metadata for encryption and permission management. |
Version Validation Is Strict
If you build a loader targeting CS 4.11 but try to use it with a CS 4.9 Beacon DLL, the version check will fail and Beacon will proceed without any BUD data. This means no indirect syscalls, no sleep mask memory tracking, and no RTL functions. The loader will appear to work (Beacon runs), but all advanced evasion features that depend on BUD will be silently disabled.
3. SYSCALL_API — The Syscall Table
The SYSCALL_API structure is the largest component of BUD. It contains 36 entries, one for each Nt* function that Beacon may need to call during its lifecycle. Each entry is a SYSCALL_API_ENTRY with three fields:
Ctypedef struct {
PVOID fnAddr; // Address of the Nt* function in ntdll.dll
PVOID jmpAddr; // Address of a syscall;ret gadget in ntdll for indirect execution
DWORD sysnum; // The System Service Number (SSN) for this syscall
} SYSCALL_API_ENTRY;
Understanding the Three Fields
| Field | Purpose | Used By |
|---|---|---|
fnAddr | The actual address of the Nt* stub in ntdll.dll. This is where the function's code begins (the mov r10, rcx / mov eax, SSN prologue). Used for direct calls when indirect syscalls are not needed or as a fallback. | Beacon (direct calls) |
jmpAddr | The address of a syscall; ret instruction pair found within a different Nt* function in ntdll. By jumping to this gadget instead of executing syscall inline, the return address on the stack points into ntdll — not into Beacon's private memory. This is the core of indirect syscall execution. | LibGate trampoline |
sysnum | The System Service Number extracted by LibGate's Hell's Gate / Halo's Gate algorithm. This number is loaded into EAX before the syscall instruction to specify which kernel service to invoke. | LibGate SSN setup |
The full SYSCALL_API structure contains 36 named entries. Crystal-Loaders resolves all of them during the loading process using LibGate:
Ctypedef 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_ENTRY ntQueueApcThread;
SYSCALL_API_ENTRY ntCreateProcess;
SYSCALL_API_ENTRY ntOpenProcessToken;
SYSCALL_API_ENTRY ntTestAlert;
SYSCALL_API_ENTRY ntSuspendProcess;
SYSCALL_API_ENTRY ntResumeProcess;
SYSCALL_API_ENTRY ntQuerySystemInformation;
SYSCALL_API_ENTRY ntQueryDirectoryFile;
SYSCALL_API_ENTRY ntSetInformationProcess;
SYSCALL_API_ENTRY ntSetInformationThread;
SYSCALL_API_ENTRY ntQueryInformationProcess;
SYSCALL_API_ENTRY ntQueryInformationThread;
SYSCALL_API_ENTRY ntOpenSection;
SYSCALL_API_ENTRY ntAdjustPrivilegesToken;
SYSCALL_API_ENTRY ntDeviceIoControlFile;
SYSCALL_API_ENTRY ntWaitForMultipleObjects;
} SYSCALL_API;
Complete Syscall Reference Table
All 36 Nt* functions in the SYSCALL_API, listed in source order:
| # | Function | Purpose | Category |
|---|---|---|---|
| 1 | ntAllocateVirtualMemory | Allocate memory regions in a process address space | Memory |
| 2 | ntProtectVirtualMemory | Change memory page permissions (RW, RX, etc.) | Memory |
| 3 | ntFreeVirtualMemory | Free previously allocated memory regions | Memory |
| 4 | ntGetContextThread | Read a thread's CPU context (registers, flags) | Threads |
| 5 | ntSetContextThread | Set a thread's CPU context (used in sleep mask ROP chains) | Threads |
| 6 | ntResumeThread | Resume a suspended thread | Threads |
| 7 | ntCreateThreadEx | Create a new thread (modern API with extended parameters) | Threads |
| 8 | ntOpenProcess | Open a handle to a process | Process |
| 9 | ntOpenThread | Open a handle to an existing thread | Threads |
| 10 | ntClose | Close any NT handle (files, threads, processes, events, etc.) | Handles |
| 11 | ntCreateSection | Create a section object for memory mapping | Memory |
| 12 | ntMapViewOfSection | Map a section object into a process address space | Memory |
| 13 | ntUnmapViewOfSection | Unmap a previously mapped section from a process | Memory |
| 14 | ntQueryVirtualMemory | Query information about a memory region (type, protection, size) | Memory |
| 15 | ntDuplicateObject | Duplicate a handle (within or across processes) | Handles |
| 16 | ntReadVirtualMemory | Read memory from a remote process | Memory |
| 17 | ntWriteVirtualMemory | Write memory to a remote process | Memory |
| 18 | ntReadFile | Read data from a file | File I/O |
| 19 | ntWriteFile | Write data to a file | File I/O |
| 20 | ntCreateFile | Create or open a file (NT-level file I/O) | File I/O |
| 21 | ntQueueApcThread | Queue an Asynchronous Procedure Call to a thread | Threads |
| 22 | ntCreateProcess | Create a new process | Process |
| 23 | ntOpenProcessToken | Open the access token associated with a process | Token |
| 24 | ntTestAlert | Test and clear pending APCs for the current thread | Threads |
| 25 | ntSuspendProcess | Suspend all threads in a target process | Process |
| 26 | ntResumeProcess | Resume all threads in a previously suspended process | Process |
| 27 | ntQuerySystemInformation | Query system-wide information (process list, handle table, etc.) | System |
| 28 | ntQueryDirectoryFile | Query directory contents for file enumeration | File I/O |
| 29 | ntSetInformationProcess | Set process properties (e.g., DEP policy, mitigation settings) | Process |
| 30 | ntSetInformationThread | Set thread properties (e.g., ThreadHideFromDebugger) | Threads |
| 31 | ntQueryInformationProcess | Query process information (PEB address, debug status, etc.) | Process |
| 32 | ntQueryInformationThread | Query thread information (start address, priority, etc.) | Threads |
| 33 | ntOpenSection | Open an existing section object by name | Memory |
| 34 | ntAdjustPrivilegesToken | Enable or disable privileges in an access token | Token |
| 35 | ntDeviceIoControlFile | Send a device I/O control request to a driver | Device I/O |
| 36 | ntWaitForMultipleObjects | Wait on an array of handles simultaneously | Sync |
4. RTL_API — Runtime Library Functions
Not everything Beacon needs is a syscall. Three runtime library values are resolved by the loader and passed separately in the RTL_API structure:
Ctypedef struct {
PVOID rtlDosPathNameToNtPathNameUWithStatusAddr;
PVOID rtlFreeHeapAddr;
PVOID rtlGetProcessHeapAddr;
} RTL_API, *PRTL_API;
Why These Three?
| Field | What It Does | Why Beacon Needs It |
|---|---|---|
rtlDosPathNameToNtPathNameUWithStatusAddr | Converts a DOS-style path like C:\Windows\System32\cmd.exe into an NT-style path like \??\C:\Windows\System32\cmd.exe | The Nt* file APIs (NtCreateFile) require NT path format. Without this function, Beacon would need to resolve and call it dynamically every time it performs file I/O, adding overhead and resolution artifacts. |
rtlFreeHeapAddr | Frees a heap allocation made with RtlAllocateHeap (the underlying implementation of HeapFree) | Beacon uses heap allocations internally for buffers, strings, and temporary data. Having the free function pre-resolved avoids repeated runtime lookups into ntdll. |
rtlGetProcessHeapAddr | The address of RtlGetProcessHeap, which returns the default process heap handle | Both RtlAllocateHeap and RtlFreeHeap require a heap handle as their first argument. Rather than reading the PEB each time, the loader provides the address of the function that retrieves it. |
ResolveRtlFunctions Implementation
The loader resolves these three values during initialization, before the BUD handoff:
Cvoid ResolveRtlFunctions(PRTL_API rtls)
{
// Find ntdll base address via PEB walking + hash comparison
PVOID ntdll = findModuleByHash(NTDLL_HASH);
// Resolve the RTL functions from ntdll's export table
rtls->rtlDosPathNameToNtPathNameUWithStatusAddr = findFunctionByHash(
ntdll, RTL_DOSPATHNAME_HASH);
rtls->rtlFreeHeapAddr = findFunctionByHash(
ntdll, RTL_FREEHEAP_HASH);
rtls->rtlGetProcessHeapAddr = findFunctionByHash(
ntdll, RTL_GETPROCESSHEAP_HASH);
}
PEB Access on x64
On 64-bit Windows, the PEB is accessible via the GS segment register at offset 0x60. The __readgsqword(0x60) intrinsic reads an 8-byte pointer from GS:[0x60], giving the base address of the PEB structure. The ProcessHeap field is at offset 0x30 within the PEB. This is a well-known technique used across offensive tooling for resolving runtime data without calling any API functions.
5. ALLOCATED_MEMORY — Memory Region Tracking
This is the most complex component of BUD, and arguably the most important. The ALLOCATED_MEMORY structure gives Beacon a complete map of every memory region the loader allocated, with section-level granularity. This map is critical for sleep masking — without it, the sleep mask would have no way to know which regions to encrypt, what permissions to change, or how to restore them after waking up.
Ctypedef struct {
ALLOCATED_MEMORY_REGION AllocatedMemoryRegions[6]; // Up to 6 tracked regions
} ALLOCATED_MEMORY;
Each region represents a single top-level memory allocation (e.g., the Beacon DLL image, the sleep mask code, a BOF buffer). Within each region, up to 8 sections describe the internal layout:
Ctypedef struct {
ALLOCATED_MEMORY_PURPOSE Purpose;
PVOID AllocationBase;
SIZE_T RegionSize;
DWORD Type; // MEM_COMMIT, MEM_RESERVE, etc.
ALLOCATED_MEMORY_SECTION Sections[8]; // Up to 8 sections per region
ALLOCATED_MEMORY_CLEANUP_INFORMATION CleanupInformation;
} ALLOCATED_MEMORY_REGION;
Ctypedef struct {
ALLOCATED_MEMORY_LABEL Label; // TEXT, RDATA, DATA, PDATA, RELOC, etc.
PVOID BaseAddress;
SIZE_T VirtualSize;
DWORD CurrentProtect;
DWORD PreviousProtect;
BOOL MaskSection; // TRUE = encrypt this section during sleep
} ALLOCATED_MEMORY_SECTION;
The CleanupInformation field in each region tells Beacon how to clean up the allocation on exit. It contains the allocation method, a cleanup flag, and method-specific additional information:
Ctypedef struct {
BOOL Cleanup;
ALLOCATED_MEMORY_ALLOCATION_METHOD AllocationMethod;
ALLOCATED_MEMORY_ADDITIONAL_CLEANUP_INFORMATION AdditionalCleanupInformation;
} ALLOCATED_MEMORY_CLEANUP_INFORMATION;
The AdditionalCleanupInformation is a union that carries method-specific data needed for correct deallocation:
Ctypedef union {
HEAPALLOC_INFO HeapAllocInfo; // HeapHandle, DestroyHeap
MODULESTOMP_INFO ModuleStompInfo; // ModuleHandle
} ALLOCATED_MEMORY_ADDITIONAL_CLEANUP_INFORMATION;
typedef struct {
PVOID HeapHandle; // Handle to the heap this region was allocated from
BOOL DestroyHeap; // TRUE if the heap itself should be destroyed on cleanup
} HEAPALLOC_INFO;
typedef struct {
PVOID ModuleHandle; // Handle to the stomped module
} MODULESTOMP_INFO;
Section Fields Explained
| Field | Description |
|---|---|
Label | Identifies what the section contains: .text (code), .rdata (read-only data), .data (writable data), .pdata (exception handlers), .reloc (relocations), the PE header, or a generic buffer. |
BaseAddress | The virtual address where this section starts. Combined with VirtualSize, this defines the exact byte range. |
VirtualSize | The size of this section in bytes. May differ from the on-disk raw size due to alignment and uninitialized data (.bss). |
CurrentProtect | The current memory protection (e.g., PAGE_EXECUTE_READ for .text, PAGE_READWRITE for .data). The sleep mask saves this before changing protections. |
PreviousProtect | The previous memory protection before the last VirtualProtect call. Used to track protection transitions. |
MaskSection | A boolean flag. If TRUE, the sleep mask will encrypt this section's memory during sleep and decrypt it on wake. Typically TRUE for all sections containing Beacon code or data. |
ALLOCATED_MEMORY Layout Example
AllocationBase: 0x1A000000 | Size: 0x5C000
AllocationBase: 0x1B000000 | Size: 0x8000
Why MaskSection Is FALSE for Sleep Mask Memory
The sleep mask cannot encrypt itself. The sleep mask code must remain in cleartext and executable while it is running the encryption routine on all other Beacon regions. If the sleep mask encrypted its own .text section, execution would immediately crash. This is why the sleep mask's own region has MaskSection = FALSE for all its sections.
6. Enums Explained
Three enum types provide semantic meaning to the raw integer values stored in the ALLOCATED_MEMORY structures. Understanding these is essential for interpreting the memory map correctly.
ALLOCATED_MEMORY_PURPOSE
Cenum ALLOCATED_MEMORY_PURPOSE {
PURPOSE_EMPTY = 0, // Slot is unused
PURPOSE_GENERIC_BUFFER = 1, // General-purpose buffer (loader scratch space)
PURPOSE_BEACON_MEMORY = 2, // The Beacon DLL image
PURPOSE_SLEEPMASK_MEMORY = 3, // The sleep mask code
PURPOSE_BOF_MEMORY = 4 // A loaded BOF (Beacon Object File)
};
Purpose Enum Usage
| Value | When Used | Sleep Mask Behavior |
|---|---|---|
PURPOSE_EMPTY | Default. Indicates the region slot is not in use. | Skipped entirely |
PURPOSE_GENERIC_BUFFER | Loader scratch space, temporary allocations | Encrypted if MaskSection is TRUE |
PURPOSE_BEACON_MEMORY | The main Beacon DLL loaded by the UDRL | All sections encrypted during sleep |
PURPOSE_SLEEPMASK_MEMORY | The sleep mask's own code and data | Never encrypted (it would crash) |
PURPOSE_BOF_MEMORY | Beacon Object Files loaded at runtime | Encrypted if still resident during sleep |
ALLOCATED_MEMORY_LABEL
Cenum ALLOCATED_MEMORY_LABEL {
LABEL_EMPTY = 0, // Slot is unused / not assigned
LABEL_BUFFER = 1, // Generic buffer (non-PE data)
LABEL_PEHEADER = 2, // PE/COFF headers
LABEL_TEXT = 3, // .text section (executable code)
LABEL_RDATA = 4, // .rdata section (read-only data, vtables, strings)
LABEL_DATA = 5, // .data section (writable initialized data)
LABEL_PDATA = 6, // .pdata section (exception handler tables)
LABEL_RELOC = 7, // .reloc section (base relocation fixups)
LABEL_USER_DEFINED = 1000 // Starting value for user-defined labels
};
The label tells the sleep mask (and Beacon) what kind of data lives in each section. This matters because different sections need different permission transitions during sleep:
| Label | Normal Permission | Sleep Permission | Why |
|---|---|---|---|
LABEL_TEXT | PAGE_EXECUTE_READ | PAGE_READWRITE | Must become writable for encryption, non-executable to avoid memory scans |
LABEL_RDATA | PAGE_READONLY | PAGE_READWRITE | Must become writable for encryption |
LABEL_DATA | PAGE_READWRITE | PAGE_READWRITE | Already writable, just needs encryption |
LABEL_PDATA | PAGE_READONLY | PAGE_READWRITE | Must become writable for encryption |
LABEL_PEHEADER | PAGE_READWRITE | PAGE_READWRITE | Already writable, encrypt to hide PE header artifacts |
ALLOCATED_MEMORY_ALLOCATION_METHOD
Cenum ALLOCATED_MEMORY_ALLOCATION_METHOD {
METHOD_UNKNOWN = 0, // Unknown or unset allocation method
METHOD_VIRTUALALLOC = 1, // Allocated via VirtualAlloc / NtAllocateVirtualMemory
METHOD_HEAPALLOC = 2, // Allocated via HeapAlloc / RtlAllocateHeap
METHOD_MODULESTOMP = 3, // Overwrote an existing mapped module (module stomping)
METHOD_NTMAPVIEW = 4, // Mapped via NtMapViewOfSection
METHOD_USER_DEFINED = 1000 // Starting value for user-defined methods
};
Why Allocation Method Matters
Different allocation methods require different deallocation APIs. When Beacon exits or needs to clean up a memory region, it must call the correct free function:
| Allocation Method | Deallocation API |
|---|---|
METHOD_VIRTUALALLOC | NtFreeVirtualMemory |
METHOD_HEAPALLOC | RtlFreeHeap (via RTL_API) |
METHOD_MODULESTOMP | No deallocation needed (memory belongs to the stomped module) |
METHOD_NTMAPVIEW | NtUnmapViewOfSection |
Calling NtFreeVirtualMemory on a heap allocation, or NtUnmapViewOfSection on a VirtualAlloc region, would cause heap corruption or an access violation. The allocation method enum prevents these catastrophic cleanup errors.
7. Why BUD Matters for Sleep Masking
Sleep masking is the technique where Beacon encrypts its own memory during sleep intervals to avoid detection by memory scanners. Without BUD, the sleep mask would need to hardcode memory addresses, discover regions through heuristic scanning, or use a simplified single-region approach. BUD gives the sleep mask a complete, loader-provided map of every region and every section within those regions.
Sleep Mask Lifecycle with BUD
ALLOCATED_MEMORY from BUD to enumerate all tracked regionsSections[8] looking for MaskSection == TRUEPAGE_READWRITE (removes EXECUTE to avoid RX memory scans)NtWaitForSingleObject with the sleep duration (using the pre-resolved syscall from BUD)CurrentProtectThe Key Insight
Without BUD, the sleep mask would need to either:
- Hardcode addresses — Brittle, breaks with ASLR, different payload sizes, or multiple regions
- Scan for its own PE headers — Requires headers to still exist (Crystal-Loaders erases them)
- Use a single-region model — Cannot track sleep mask memory separately from Beacon memory
BUD solves all of these problems by giving the sleep mask a structured, complete, loader-provided memory map with section-level granularity and correct permission metadata. The sleep mask simply iterates the array and processes each entry — no guessing, no scanning, no hardcoding.
The sleep mask also benefits from the pre-resolved SYSCALL_API in BUD. During the sleep cycle, it needs to call NtProtectVirtualMemory (to change permissions) and NtWaitForSingleObject (to sleep). If it had to resolve these itself, it would need its own PEB walking and export table parsing code — duplicating work the loader already did. With BUD, the sleep mask just reads the pre-resolved entries.
8. The FixSectionPermissions Function
This function is the bridge between the PE loading logic (LibTCG) and the BUD memory tracking system. After the loader copies Beacon's PE sections into memory, FixSectionPermissions sets the correct per-section memory protections and populates the ALLOCATED_MEMORY_REGION structure for BUD.
Cvoid FixSectionPermissions(PDLLDATA dlldata, PBYTE dst, PALLOCATED_MEMORY_REGION region)
{
// Set the region-level metadata
region->AllocationBase = dst;
region->RegionSize = SizeOfDLL(dlldata);
region->Type = MEM_COMMIT;
// Iterate each PE section and apply granular protections
PIMAGE_SECTION_HEADER sectionHdr = IMAGE_FIRST_SECTION(dlldata->NtHeaders);
for (WORD i = 0; i < dlldata->NtHeaders->FileHeader.NumberOfSections; i++)
{
DWORD protect = PAGE_READONLY; // Default: read-only
// Determine the appropriate protection from section characteristics
// NOTE: Simplified for clarity. The actual source has 7 cascading
// conditions handling PAGE_WRITECOPY, PAGE_READONLY, PAGE_READWRITE,
// PAGE_EXECUTE, PAGE_EXECUTE_WRITECOPY, PAGE_EXECUTE_READ, and
// PAGE_EXECUTE_READWRITE based on the full set of characteristic flags.
if (sectionHdr->Characteristics & IMAGE_SCN_MEM_EXECUTE)
protect = PAGE_EXECUTE_READ; // .text: RX
else if (sectionHdr->Characteristics & IMAGE_SCN_MEM_WRITE)
protect = PAGE_READWRITE; // .data: RW
// Apply the protection
DWORD oldProtect;
KERNEL32$VirtualProtect(
dst + sectionHdr->VirtualAddress, // Section base address
sectionHdr->SizeOfRawData, // Section size
protect, // New protection
&oldProtect // Previous protection (saved)
);
// Track the section metadata in the BUD region
region->Sections[i].Label = GetLabelFromSectionHeader(sectionHdr->Name);
region->Sections[i].BaseAddress = dst + sectionHdr->VirtualAddress;
region->Sections[i].VirtualSize = sectionHdr->SizeOfRawData;
region->Sections[i].CurrentProtect = protect;
region->Sections[i].PreviousProtect = oldProtect;
region->Sections[i].MaskSection = TRUE; // All Beacon sections are maskable
sectionHdr++;
}
}
What This Function Accomplishes
| Step | What Happens | Why It Matters |
|---|---|---|
| Region metadata | Sets AllocationBase, RegionSize, and Type | Gives the sleep mask the top-level allocation boundaries for this region |
| Section iteration | Walks each PE section via IMAGE_FIRST_SECTION | Each section needs different permissions based on its characteristics flags |
| Protection logic | Maps PE characteristics to Windows memory protections | Replaces the monolithic RWX allocation with granular per-section permissions, eliminating detection vector 3 |
| VirtualProtect call | Actually changes the page permissions | The memory was initially allocated as RW for writing; now each section gets its correct final protection |
| BUD tracking | Records label, base, size, protection, and mask flag for each section | Builds the section map that the sleep mask will use to encrypt/decrypt and restore permissions |
The GetLabelFromSectionHeader helper function converts PE section names (like .text, .rdata) into the corresponding ALLOCATED_MEMORY_LABEL enum values. This abstraction allows the sleep mask to reason about section types without parsing PE section name strings.
FixSectionPermissions Data Flow
from NtHeaders
for each section
set RX/RW/R
label + base + size
+ protect + mask
9. Module Summary
Key Takeaways
- BUD (Beacon User Data) is the structured contract between the UDRL loader and Beacon, delivered via a
DllMaincall withfdwReason = 0x0d. - The USER_DATA structure contains a version field (
0x041100for CS 4.11), pointers to SYSCALL_API (36 Nt* entries), RTL_API (3 runtime values), ALLOCATED_MEMORY (up to 6 regions), and a 32-byte custom field. - The SYSCALL_API provides pre-resolved function addresses,
syscall;retgadget addresses, and SSNs for all 36 Nt* functions Beacon uses — enabling indirect syscall execution without runtime resolution. - The RTL_API provides path conversion, heap freeing, and the default process heap handle — three values Beacon needs that are not syscalls.
- The ALLOCATED_MEMORY structure gives Beacon and its sleep mask a complete, section-level memory map including labels, permissions, and encryption flags.
- The allocation method enum ensures correct cleanup by telling Beacon which deallocation API to use for each region.
- FixSectionPermissions is the function that bridges PE loading and BUD by setting granular per-section permissions and populating the BUD memory tracking data simultaneously.
Module 6 Quiz: Beacon User Data
Q1: What version number does Crystal-Loaders set in USER_DATA for Cobalt Strike 4.11?
0x041100, which represents Cobalt Strike 4.11. Beacon validates this value immediately when receiving the USER_DATA pointer — if it does not match the Beacon's own version, the entire BUD structure is ignored and all advanced evasion features that depend on it are silently disabled.Q2: What does the MaskSection field in ALLOCATED_MEMORY_SECTION control?
MaskSection boolean tells the sleep mask whether to encrypt this particular section during sleep intervals. When set to TRUE, the sleep mask will XOR/RC4/AES encrypt the section's contents, change its permissions to RW (removing the executable flag), and later decrypt and restore permissions on wake. It is set to FALSE for the sleep mask's own memory, since the sleep mask cannot encrypt itself while it is executing.Q3: Why does the loader track ALLOCATED_MEMORY_ALLOCATION_METHOD?
NtFreeVirtualMemory on a heap allocation would corrupt the heap. Calling NtUnmapViewOfSection on a VirtualAlloc region would cause an access violation. The allocation method enum tells Beacon exactly which free function to call for each region: NtFreeVirtualMemory for VirtualAlloc, RtlFreeHeap for HeapAlloc, nothing for module stomping, and NtUnmapViewOfSection for mapped sections.