================================================================================
S H R O U D W A R E
A Dual-Header C99 Obfuscation Library for Windows
mochabyte0x
April 2026
================================================================================
ABSTRACT
Shroudware is a compile-time obfuscation library implemented entirely in
the C preprocessor. It targets the three primary artifacts that reverse
engineers rely on during static analysis: readable strings in the .rdata
section, recognizable integer constants in the disassembly, and named
entries in the PE import table.
The library is distributed as a dual header file (shroudware.h and structs.h)
with no external dependencies. It requires only a C99 compiler and produces
standard Windows executables. All transformations occur at compile time
through constant folding. There is no runtime unpacking phase, no
initialization routine, and no external tooling.
Each build produces unique ciphertexts. The encryption seed is derived
from __TIME__ and __DATE__, making every compilation polymorphic.
================================================================================
1. MOTIVATION
================================================================================
When a compiled binary is loaded into a disassembler (IDA Pro, Ghidra,
Binary Ninja), the analyst's first actions are predictable:
1. Open the strings window (Shift+F12 in IDA)
2. Review the import table
3. Search for known constants (0x40, 0x3000, etc.)
These three data points provide immediate context. String references
reveal file paths, registry keys, error messages, and API names. The
import table shows every DLL and function the binary calls. Known
constants like PAGE_EXECUTE_READWRITE (0x40) or MEM_COMMIT (0x1000)
betray the program's intent without reading a single instruction.
Most obfuscation tooling addresses this at the compiler level (LLVM
passes like OLLVM, Hikari) or through post-build transformation
(packers, crypters, VMProtect). These work, but they introduce
toolchain dependencies, increase build complexity, and often require
specific compiler versions or commercial licenses.
Shroudware operates entirely within the C preprocessor. The compiler's
own constant folding pass performs the encryption. No plugins, no
post-build steps, no runtime unpacking stubs.
================================================================================
2. DESIGN
================================================================================
2.1 Constraints
- Dual header (shroudware.h + structs.h), zero external dependencies
- C99 standard (compound literals, no C++ features)
- All encryption at compile time via constant folding
- Cross-compiler: GCC (MSYS2), Clang
- Per-build polymorphism from timestamp-derived seed
2.2 Architecture
All internal constants are derived from a single root seed through
cascading bit-mix operations:
OBF_SEED
(__TIME__/__DATE__)
|
+------------+------------+
| | |
_OBF_C1 _OBF_C2 _OBF_C3
(primary) (secondary) (tertiary)
| | |
+-------+------+----+----+--------+-------+
| | | |
OBFSTR OBF_HASH OBF_IMPORT OBF_CONST
(strings) (hashing) (PEB walk) (integers)
No well-known magic numbers appear in the binary. The constants
change with every build. The | 1u operation ensures odd multipliers
to avoid degenerate zero multiplication.
================================================================================
3. STRING ENCRYPTION (OBFSTR)
================================================================================
Problem:
String literals are stored verbatim in the .rdata section. Running
strings.exe on the binary reveals every hardcoded path, API name,
error message, and configuration value.
Mechanism:
OBFSTR("text") expands into a C99 compound literal containing 256
XOR-encrypted bytes. Each byte position gets a unique subkey derived
from: the call site's line number, the string's length, and the
global seed. The compiler constant-folds the XOR at compile time.
The plaintext never reaches the object file.
At runtime, _obf_dec() reverses the XOR. This function is forced
noinline -- without it, the compiler inlines the loop, recognizes
the XOR-then-XOR identity, and optimizes the encryption away
entirely. A memory barrier further prevents dead-store elimination.
Usage:
char* secret = OBFSTR("This is a secret message");
printf("%s\n", secret);
printf("%s\n", OBFSTR("also works inline"));
Properties:
- Max 255 characters per string
- Position-dependent subkeys (same char encrypts differently
at different positions)
- Per-call-site keys (same string on different lines produces
different ciphertext)
- Stack-allocated (compound literal, not in .rdata)
- Different ciphertext every build
================================================================================
4. COMPILE-TIME HASHING (OBF_HASH)
================================================================================
Problem:
Resolving functions by name against PE export tables requires the
name string to exist somewhere. Plaintext names are trivially found.
Encrypting with OBFSTR adds runtime overhead and a reversible
decryption function.
Mechanism:
OBF_HASH("NtClose") expands into 16 nested FNV-1a macro steps.
Each step conditionally XORs the next character and multiplies by
the FNV-1a prime (0x01000193). The compiler evaluates the chain
at compile time and emits a single uint32_t constant. The string
literal is consumed by the preprocessor and never reaches the
object file.
Variants:
OBF_HASH(s) Case-sensitive, for export name matching
OBF_HASH_CI(s) Case-insensitive, for module name matching
obf_hash_rt() Runtime hash for dynamic strings
obf_hash_rt_wci() Runtime hash for wide strings (PEB names)
Limits:
15 characters at compile time. For longer names, OBF_IMPORT falls
back to runtime hashing automatically. For standalone use:
obf_hash_rt(OBFSTR("LongFunctionName"))
================================================================================
5. IMPORT HIDING (OBF_IMPORT)
================================================================================
Problem:
The PE import table is a manifest of every DLL and function the
binary calls. An analyst reads it in seconds: VirtualAlloc means
memory allocation, CreateRemoteThread means injection,
InternetOpenA means network activity.
Mechanism:
OBF_IMPORT bypasses the import table through manual resolution:
Step 1: Module resolution via PEB walk
The Process Environment Block contains a linked list of loaded
modules (PEB->Ldr->InLoadOrderModuleList). The library reads the
TEB segment register directly (gs:0x60 on x64, fs:0x30 on x86)
to reach the PEB without calling any API. Each module's
BaseDllName is hashed at runtime and compared against the
compile-time hash of the target module.
Step 2: Function resolution via export table walk
The library parses PE headers to locate the export directory.
Each exported name is hashed at runtime and compared against
the target hash. The function name is encrypted with a 64-byte
OBFSTR variant and hashed at runtime, so there is no length
restriction on function names.
Step 3: Forwarded export handling
Some exports point to forwarding strings rather than code
(e.g., kernel32!HeapAlloc -> NTDLL.RtlAllocateHeap). The
library detects this (RVA within export directory bounds),
parses the forwarding string, resolves the target module by
hash, and resolves the function recursively.
Usage:
typedef DWORD (WINAPI *GetTickCount_t)(void);
GetTickCount_t pGTC = (GetTickCount_t)OBF_IMPORT("kernel32.dll",
"GetTickCount");
DWORD tick = pGTC();
// forwarded exports work transparently
HeapAlloc_t pHA = (HeapAlloc_t)OBF_IMPORT("kernel32.dll",
"HeapAlloc");
// long names (>15 chars) work automatically
void* p = OBF_IMPORT("ntdll.dll", "NtQueryInformationProcess");
Result:
The import table is clean. No VirtualAlloc, no GetProcAddress,
no suspicious entries. Only CRT imports (printf, etc.) remain.
================================================================================
6. VALUE OBFUSCATION (OBF_CONST)
================================================================================
Problem:
Integer constants are recognizable signatures. Searching for 0x40
finds PAGE_EXECUTE_READWRITE. Searching for 0x3000 finds
MEM_COMMIT | MEM_RESERVE. These reveal intent without reading
surrounding code.
Mechanism:
OBF_CONST(0x40) XORs the value with a per-call-site key at compile
time. At runtime, _obf_deconst() reverses the XOR in a noinline
function. The key varies by line number and seed, so identical
values at different call sites produce different ciphertexts.
Usage:
DWORD prot = OBF_CONST(0x40); // PAGE_EXECUTE_READWRITE
DWORD flags = OBF_CONST(0x3000); // MEM_COMMIT | MEM_RESERVE
SIZE_T size = (SIZE_T)OBF_CONST(4096);
Returns unsigned int. Cast for other types as needed.
================================================================================
7. SEED SYSTEM
================================================================================
All encryption derives from a single root:
#define OBF_SEED (_OBF_AUTO_SEED | 1u)
_OBF_AUTO_SEED XOR-mixes each character of __TIME__ and __DATE__ with
unique multipliers. The | 1u ensures odd values. From the seed, three
constants cascade:
_OBF_C1 = mix(SEED) Primary mixing constant
_OBF_C2 = mix(_OBF_C1) Secondary
_OBF_C3 = mix(_OBF_C2) Tertiary
To pin a seed for reproducible builds:
#define OBF_SEED 0xDEADBEEFu
#include "shroudware.h"
================================================================================
8. COMPILER SUPPORT
================================================================================
Compiler Version Tested Notes
------- --------------- --------------------------------
Clang 21.1.0 GCC-style barriers, source
location limits on heavy usage
GCC (MSYS2) x86_64-pc-msys Inline asm PEB access, no
_WIN32 (uses __MSYS__ guard)
Clang note: Clang maintains a 32-bit source location counter. Heavy
macro usage (many OBFSTR + OBF_HASH in one TU) can exhaust it. Split
obfuscated code across multiple .c files if this occurs.
================================================================================
9. API REFERENCE
================================================================================
Macro Input Output
----- ----- ------
OBFSTR(s) string, max 255 chars char* (stack)
OBF_HASH(s) string, max 15 chars unsigned int
OBF_HASH_CI(s) string, max 15 chars unsigned int
OBF_IMPORT(m, f) two string literals void*
OBF_CONST(v) integer constant unsigned int
Function Purpose
-------- -------
obf_hash_rt(s) FNV-1a of char* (case sensitive)
obf_hash_rt_wci(s) FNV-1a of wchar_t* (case insensitive)
obf_get_module(h) PEB walk, find module base by hash
obf_get_proc(b, h) Export table walk, resolve by hash
================================================================================
10. LIMITATIONS
================================================================================
- OBFSTR: 255 char max (256 byte compound literal)
- OBF_HASH: 15 char max at compile time
- OBF_CONST: 32-bit unsigned only
- OBFSTR lifetime: stack-scoped, do not return from a function
- Requires -O1 or higher (constant folding needed)
- Windows only (PEB/PE structures)
- Clang source location budget on heavy usage
- Not a packer. Removes static artifacts only. Does not protect
against dynamic analysis, debugging, or memory dumping.
================================================================================
11. BUILDING
================================================================================
gcc -O2 -o demo.exe demo.c
clang -O2 -o demo.exe demo.c
Verify:
strings demo.exe | findstr "secret" (should find nothing)
================================================================================
github.com/Hotel-Zero
================================================================================
Hotel-Zero/shroudware
Folders and files
| Name | Name | Last commit date | ||
|---|---|---|---|---|