Difficulty: Intermediate

Module 5: Compile-Time DJB2 Hashing

Strings disappear at build time. Only hashes remain.

Stardust Uses DJB2

Stardust uses the DJB2 hash algorithm (hash * 33 + char, starting from 5381) — the same fundamental algorithm used by AceLdr. The key difference is when the hash is computed: AceLdr pre-computes hashes with a Python script and stores them as #define constants, while Stardust computes them at compile time using C++ constexpr/consteval. The string never appears in the binary either way, but Stardust's approach is more maintainable.

Note on Hash Algorithm Variations

Different versions or forks of Stardust may use different hash algorithms. Some forks use FNV-1a (offset basis: 0x811c9dc5, prime: 0x01000193) instead of DJB2. The technique and purpose remain identical regardless of which hash function is used — only the constants change. Always check the source code of the specific version you are working with.

The DJB2 Algorithm

DJB2 Hash Computation

hash = 5381
(DJB2 initial value)
For each byte:
toupper(byte)
hash = (hash << 5)
+ hash + byte
32-bit hash
output

The core operation (hash << 5) + hash + byte is equivalent to hash * 33 + byte. The shift-and-add form is preferred because it avoids an explicit multiply instruction, though modern compilers optimize both forms identically.

Two Versions: Compile-Time and Runtime

Compile-Time (constexpr) — Used for Constants

C++ - constexpr.htemplate <typename T = char>
constexpr auto hash_string( const T* string ) -> uint32_t {
    uint32_t hash = 5381;             // DJB2 initial value
    uint8_t  byte = 0;
    while ( *string ) {
        byte = static_cast<uint8_t>( *string++ );
        if ( byte >= 'a' ) byte -= 0x20;  // uppercase (case-insensitive)
        hash = ( ( hash << 5 ) + hash ) + byte;  // hash * 33 + byte
    }
    return hash;
}

// Usage (compile time - string NEVER in binary):
auto h = expr::hash_string( "NtAllocateVirtualMemory" ); // => constant

Runtime — Used for PEB Walking

C++ - common.h// Same algorithm but callable at runtime (for hashing DLL export names)
template<typename T = char>
inline auto declfn hash_string( const T* string ) -> uint32_t {
    uint32_t hash = 5381;
    uint8_t  byte = 0;
    while ( *string ) {
        byte = static_cast<uint8_t>( *string++ );
        if ( byte >= 'a' ) byte -= 0x20;
        hash = ( ( hash << 5 ) + hash ) + byte;
    }
    return hash;
}

// Usage (runtime - hashing export names found in DLL memory):
if ( stardust::hash_string( exportName ) == target_hash ) { ... }

Why Two Identical Functions?

The compile-time version lives in the expr namespace and is marked constexpr (or consteval in C++20). It runs inside the compiler. The runtime version lives in the stardust namespace and is a regular inline function that runs when the shellcode executes. They use the same algorithm so that a hash computed at build time matches a hash computed at runtime against real export names.

Wide String Support (UTF-16)

Module names in the PEB are stored as wide strings (wchar_t, UTF-16). The template parameter <wchar_t> handles this:

C++// Compile-time hash of a wide string (for matching DLL names from PEB)
resolve::module( expr::hash_string<wchar_t>( L"ntdll.dll" ) )
//                                 ^^^^^^^^    ^^
//                                 template     wide string literal
//                                 param

AceLdr vs Stardust: Hash Comparison

PropertyAceLdrStardust
AlgorithmDJB2DJB2
Initial value53815381
Operation(hash << 5) + hash + char(hash << 5) + hash + char
Multiplier33 (implicit via shift)33 (implicit via shift)
Case handlingCase-insensitive (uppercase)Case-insensitive (uppercase)
When computed?Python script pre-computesconstexpr computes in compiler
String in binary?No (hashes in #define)No (constexpr eliminates them)
Adding new APIRun Python script, paste hashJust write the API name in code

The Clever Bit: RESOLVE_TYPE

#define RESOLVE_TYPE( s ) .s = reinterpret_cast<decltype(s)*>( expr::hash_string( #s ) )

The #s operator "stringifies" the macro argument. So RESOLVE_TYPE(LoadLibraryA) expands to:

.LoadLibraryA = reinterpret_cast<decltype(LoadLibraryA)*>( expr::hash_string("LoadLibraryA") )

At compile time, the hash is computed and the function pointer is temporarily set to the hash value. Later, RESOLVE_IMPORT replaces these hash values with real function addresses.

Why This Matters for Development

With AceLdr's approach, adding a new API call means: (1) run the Python hash script, (2) copy the hex value, (3) paste it as a #define. With Stardust, you just write RESOLVE_TYPE(NewFunction) and the compiler does the rest. This is a significant quality-of-life improvement for implant development while producing identical runtime behavior.

Pop Quiz: DJB2 Hashing

Q1: What is the DJB2 initial hash value and core operation?

DJB2 uses an initial value of 5381 and the operation (hash << 5) + hash + byte, which is equivalent to hash * 33 + byte. This is the same algorithm used in both AceLdr and Stardust, with the difference being that Stardust computes it at compile time via constexpr.

Q2: What does the # operator do in RESOLVE_TYPE(#s)?

The # operator in a macro converts the argument to a string literal. So #s where s=LoadLibraryA produces "LoadLibraryA". This string is then fed to the constexpr hash_string function which computes the DJB2 hash at compile time. The string never appears in the final binary.