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
(DJB2 initial value)
toupper(byte)
+ hash + byte
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
| Property | AceLdr | Stardust |
|---|---|---|
| Algorithm | DJB2 | DJB2 |
| Initial value | 5381 | 5381 |
| Operation | (hash << 5) + hash + char | (hash << 5) + hash + char |
| Multiplier | 33 (implicit via shift) | 33 (implicit via shift) |
| Case handling | Case-insensitive (uppercase) | Case-insensitive (uppercase) |
| When computed? | Python script pre-computes | constexpr computes in compiler |
| String in binary? | No (hashes in #define) | No (constexpr eliminates them) |
| Adding new API | Run Python script, paste hash | Just 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?
Q2: What does the # operator do in RESOLVE_TYPE(#s)?