Stardust provides several preprocessor macros that simplify common shellcode patterns, including API pointer declarations, batch resolution, and linked list iteration.
D_API()
Declares a function pointer with automatic type inference from an existing symbol.
#define D_API(x) decltype(x) * x;
Name of an existing function or symbol to create a typed pointer for.
Purpose
This macro uses decltype to automatically infer the function signature type, creating a pointer variable with the same name. It’s primarily used for declaring API function pointers in structures.
Example Usage
// Instead of manually typing signatures:
HMODULE (WINAPI *LoadLibraryA)(LPCSTR);
FARPROC (WINAPI *GetProcAddress)(HMODULE, LPCSTR);
// Use D_API for automatic type inference:
struct Kernel32 {
uintptr_t handle;
D_API(LoadLibraryA) // Expands to: decltype(LoadLibraryA) * LoadLibraryA;
D_API(GetProcAddress) // Expands to: decltype(GetProcAddress) * GetProcAddress;
D_API(VirtualAlloc)
D_API(VirtualProtect)
};
Benefits
- Type Safety: Automatically matches the correct signature
- Maintainability: No need to manually write complex function pointer types
- Consistency: Ensures pointer types match Windows API declarations
- Readability: Clean, concise syntax
Complete Example
struct {
uintptr_t handle;
struct {
D_API(LoadLibraryA)
D_API(GetProcAddress)
D_API(VirtualAlloc)
};
} kernel32 = {
RESOLVE_TYPE(LoadLibraryA),
RESOLVE_TYPE(GetProcAddress),
RESOLVE_TYPE(VirtualAlloc)
};
// After RESOLVE_IMPORT(kernel32), use like normal functions:
auto user32 = kernel32.LoadLibraryA("user32.dll");
Combine D_API() with RESOLVE_TYPE() and RESOLVE_IMPORT() for a complete batch API resolution pattern.
RESOLVE_TYPE()
Initializes structure members with compile-time hash values for batch API resolution.
#define RESOLVE_TYPE(s) .s = reinterpret_cast<decltype(s)*>(expr::hash_string(#s))
Function name to hash. The macro stringifies the symbol, computes its compile-time hash, and assigns it to the corresponding structure member.
Purpose
This macro prepares structure members for batch resolution by storing hash values instead of actual function pointers. The hashes are later resolved to real addresses by RESOLVE_IMPORT().
Expansion Example
// Source:
RESOLVE_TYPE(LoadLibraryA)
// Expands to:
.LoadLibraryA = reinterpret_cast<decltype(LoadLibraryA)*>(
expr::hash_string("LoadLibraryA")
)
Usage Pattern
struct {
uintptr_t handle;
struct {
D_API(DbgPrint)
D_API(RtlAllocateHeap)
D_API(RtlFreeHeap)
};
} ntdll = {
RESOLVE_TYPE(DbgPrint),
RESOLVE_TYPE(RtlAllocateHeap),
RESOLVE_TYPE(RtlFreeHeap)
};
How It Works
#s stringifies the symbol name (e.g., "LoadLibraryA")
expr::hash_string() computes the FNV-1a hash at compile-time
- The hash is cast to a pointer type matching the function signature
- The pointer (containing the hash) is assigned to the structure member
- Later,
RESOLVE_IMPORT() replaces these hash values with actual addresses
The designated initializer syntax (.member = value) allows initializing nested anonymous structures in the correct order.
RESOLVE_API()
Convenience macro that combines hash computation and typed API resolution in a single call.
#define RESOLVE_API(m, s) resolve::api<decltype(s)>(m, expr::hash_string(#s))
Module base address to resolve the API from.
Function name to resolve (unquoted). The macro automatically computes its hash and casts the result.
Example Usage
// Resolve individual APIs
auto user32 = kernel32.LoadLibraryA("user32.dll");
auto user32_base = reinterpret_cast<uintptr_t>(user32);
// Instead of:
auto msgbox_hash = expr::hash_string("MessageBoxA");
auto msgbox = resolve::api<decltype(MessageBoxA)>(user32_base, msgbox_hash);
// Use RESOLVE_API:
decltype(MessageBoxA)* msgbox = RESOLVE_API(user32_base, MessageBoxA);
// Call the function
msgbox(
nullptr,
symbol<const char*>("Hello!"),
symbol<const char*>("Title"),
MB_OK
);
When to Use
Use RESOLVE_API() for:
- Single API resolution
- Dynamically loaded modules (via LoadLibraryA)
- One-off function lookups
Use RESOLVE_IMPORT() for:
- Batch resolution of multiple APIs
- Static module resolution (ntdll, kernel32)
- Organized API structure management
RESOLVE_API() is ideal for runtime-loaded modules where you don’t know the module address until after calling LoadLibraryA.
RESOLVE_IMPORT()
Batch resolves all API function pointers in a structure using stored hash values.
#define RESOLVE_IMPORT(m) { \
for (int i = 1; i < expr::struct_count<decltype(instance::m)>(); i++) { \
reinterpret_cast<uintptr_t*>(&m)[i] = resolve::_api(m.handle, reinterpret_cast<uintptr_t*>(&m)[i]); \
} \
}
Name of the API structure to resolve. Must contain a handle member as the first field, followed by function pointers declared with D_API().
How It Works
- Count Members: Uses
expr::struct_count() to determine how many pointers are in the structure
- Skip First Member: Loop starts at index
1 to skip the handle field
- Array Indexing: Treats the structure as an array of
uintptr_t pointers
- Hash to Address: For each member, calls
resolve::_api() with:
m.handle - Module base address
- Current pointer value - Contains the hash from
RESOLVE_TYPE()
- Replace Hash: Overwrites each hash value with the resolved function address
Complete Example
class instance {
// Kernel32 API structure
struct {
uintptr_t handle; // Index 0 - skipped by RESOLVE_IMPORT
struct {
D_API(LoadLibraryA) // Index 1
D_API(GetProcAddress) // Index 2
D_API(VirtualAlloc) // Index 3
};
} kernel32 = {
RESOLVE_TYPE(LoadLibraryA),
RESOLVE_TYPE(GetProcAddress),
RESOLVE_TYPE(VirtualAlloc)
};
// Ntdll API structure
struct {
uintptr_t handle;
struct {
D_API(DbgPrint)
D_API(RtlAllocateHeap)
};
} ntdll = {
RESOLVE_TYPE(DbgPrint),
RESOLVE_TYPE(RtlAllocateHeap)
};
public:
explicit instance() {
// Get module base addresses
ntdll.handle = resolve::module(
expr::hash_string<wchar_t>(L"ntdll.dll")
);
kernel32.handle = resolve::module(
expr::hash_string<wchar_t>(L"kernel32.dll")
);
// Batch resolve all APIs
RESOLVE_IMPORT(ntdll); // Resolves DbgPrint, RtlAllocateHeap
RESOLVE_IMPORT(kernel32); // Resolves LoadLibraryA, GetProcAddress, VirtualAlloc
}
};
// After resolution, call APIs directly:
ntdll.DbgPrint("Debug message\n");
auto user32 = kernel32.LoadLibraryA("user32.dll");
Memory Layout
Before RESOLVE_IMPORT:
kernel32:
+0x00: handle = 0x7FFE0000 (module base)
+0x08: LoadLibraryA = 0x12345678 (hash value)
+0x10: GetProcAddress = 0x87654321 (hash value)
+0x18: VirtualAlloc = 0xABCDEF00 (hash value)
After RESOLVE_IMPORT:
kernel32:
+0x00: handle = 0x7FFE0000 (unchanged)
+0x08: LoadLibraryA = 0x7FFE1234 (resolved address)
+0x10: GetProcAddress = 0x7FFE5678 (resolved address)
+0x18: VirtualAlloc = 0x7FFE9ABC (resolved address)
The structure must have handle as the first member, and all function pointers must be contiguous in memory. Anonymous nested structures ensure proper alignment.
declfn
Function attribute that places code into the .text$B section for proper position-independent code organization.
#define declfn __attribute__((section(".text$B")))
Purpose
The declfn attribute ensures functions are placed in a specific PE section during linking, which is critical for position-independent shellcode:
- Section Ordering:
.text$B comes after .text$A but before .text$C
- Contiguous Layout: All shellcode functions are grouped together
- PIC Compatibility: Enables proper calculation of shellcode boundaries
Usage
// Declare functions with declfn
auto declfn resolve::module(const uint32_t library_hash) -> uintptr_t {
// Implementation
}
auto declfn resolve::_api(const uintptr_t module_base, const uintptr_t symbol_hash) -> uintptr_t {
// Implementation
}
template <typename T>
inline auto declfn symbol(T s) -> T {
// Implementation
}
extern "C" auto declfn entry(_In_ void* args) -> void {
stardust::instance().start(args);
}
Section Organization
[PE File Sections]
.text$A - Startup code (entry points)
.text$B - Main shellcode functions (declfn)
.text$C - Auxiliary functions
.data - Initialized data
.rdata - Read-only data (strings)
The $ suffix in section names is a Microsoft extension that allows alphabetic ordering of subsections within a major section.
RangeHeadList()
Macro for iterating over Windows doubly-linked lists (LIST_ENTRY structures).
#define RangeHeadList(HEAD_LIST, TYPE, SCOPE) \
{ \
PLIST_ENTRY __Head = (&HEAD_LIST); \
PLIST_ENTRY __Next = {0}; \
TYPE Entry = (TYPE)__Head->Flink; \
for (; __Head != (PLIST_ENTRY)Entry;) { \
__Next = ((PLIST_ENTRY)Entry)->Flink; \
SCOPE \
Entry = (TYPE)(__Next); \
} \
}
Head node of the doubly-linked list to iterate.
Type to cast each list entry to (e.g., PLDR_DATA_TABLE_ENTRY).
Code to execute for each entry. Use Entry to access the current node.
Windows LIST_ENTRY Structure
typedef struct _LIST_ENTRY {
struct _LIST_ENTRY *Flink; // Forward link
struct _LIST_ENTRY *Blink; // Backward link
} LIST_ENTRY, *PLIST_ENTRY;
Example: Iterating Loaded Modules
auto declfn resolve::module(const uint32_t library_hash) -> uintptr_t {
// Iterate through loaded modules in PEB
RangeHeadList(
NtCurrentPeb()->Ldr->InLoadOrderModuleList,
PLDR_DATA_TABLE_ENTRY,
{
// Return first module if hash is 0
if (!library_hash) {
return reinterpret_cast<uintptr_t>(Entry->DllBase);
}
// Compare hash of module name
if (stardust::hash_string<wchar_t>(Entry->BaseDllName.Buffer) == library_hash) {
return reinterpret_cast<uintptr_t>(Entry->DllBase);
}
}
)
return 0; // Not found
}
How It Works
- Initialize: Save head node pointer
- Start at First Entry:
Entry = __Head->Flink
- Loop Until Back at Head: Continue while
Entry != __Head
- Save Next Pointer: Store
Entry->Flink before executing scope
- Execute Scope: Run user code with
Entry pointing to current node
- Advance: Move to next entry
LDR_DATA_TABLE_ENTRY
typedef struct _LDR_DATA_TABLE_ENTRY {
LIST_ENTRY InLoadOrderLinks;
LIST_ENTRY InMemoryOrderLinks;
LIST_ENTRY InInitializationOrderLinks;
PVOID DllBase;
PVOID EntryPoint;
ULONG SizeOfImage;
UNICODE_STRING FullDllName;
UNICODE_STRING BaseDllName;
// ... more fields
} LDR_DATA_TABLE_ENTRY, *PLDR_DATA_TABLE_ENTRY;
Common Use Cases
Enumerate all loaded modules:
RangeHeadList(
NtCurrentPeb()->Ldr->InLoadOrderModuleList,
PLDR_DATA_TABLE_ENTRY,
{
DBG_PRINTF("Module: %ls at %p\n",
Entry->BaseDllName.Buffer,
Entry->DllBase
);
}
)
Find specific module by name:
PVOID find_module(const wchar_t* name) {
RangeHeadList(
NtCurrentPeb()->Ldr->InLoadOrderModuleList,
PLDR_DATA_TABLE_ENTRY,
{
if (wcscmp(Entry->BaseDllName.Buffer, name) == 0) {
return Entry->DllBase;
}
}
)
return nullptr;
}
The macro saves the next pointer before executing the scope, allowing safe modification or early returns within the loop body.
Example: Complete API Resolution Pattern
class instance {
// Define API structures
struct {
uintptr_t handle;
struct {
D_API(LoadLibraryA)
D_API(GetProcAddress)
};
} kernel32 = {
RESOLVE_TYPE(LoadLibraryA),
RESOLVE_TYPE(GetProcAddress)
};
struct {
uintptr_t handle;
struct {
D_API(DbgPrint)
};
} ntdll = {
RESOLVE_TYPE(DbgPrint)
};
public:
declfn instance() {
// Step 1: Resolve module base addresses
ntdll.handle = resolve::module(
expr::hash_string<wchar_t>(L"ntdll.dll")
);
kernel32.handle = resolve::module(
expr::hash_string<wchar_t>(L"kernel32.dll")
);
// Step 2: Batch resolve all APIs
RESOLVE_IMPORT(ntdll);
RESOLVE_IMPORT(kernel32);
}
auto declfn start(_In_ void* arg) -> void {
// Step 3: Use resolved APIs
const auto user32 = kernel32.LoadLibraryA(
symbol<const char*>("user32.dll")
);
// Step 4: Resolve additional APIs
decltype(MessageBoxA)* msgbox = RESOLVE_API(
reinterpret_cast<uintptr_t>(user32),
MessageBoxA
);
msgbox(
nullptr,
symbol<const char*>("Hello from Stardust!"),
symbol<const char*>("Shellcode"),
MB_OK
);
}
};