Unicode Control-Character Name Obfuscation
.NET malware technique where all type names, method names, and sometimes field names are replaced with sequences of Unicode control characters (C0/C1 controls such as U+0002, U+0003, U+0005) and zero-width format characters (U+2000–U+200F). This breaks naive string-based clustering, renders decompiler output unreadable, and defeats tools that expect printable identifiers.
Detection / Fingerprint
- dnfile / dnlib enumeration shows TypeDef and MethodDef tables filled with
\u0002,\u0003,\u0005combined with zero-width spaces - No human-readable class or method names remain after obfuscation
- Strings output may show the raw control bytes as unprintable sequences
- Ghidra / ILSpy / dnSpy output is dominated by identical-looking or unrenderable identifiers
Implementation Patterns Observed
In sample d9c0bc24 (unclassified-dotnet-whisper):
- 40+ TypeDefs, all named
\u0002,\u0003,\u0005with zero-width variants - 60+ MethodDefs, similarly renamed; only
.ctor,.cctor,Disposesurvive - Module name
vVth.exeis preserved (likely needed for PE loader compatibility) - Namespace
Whisper.Properties.Resourcesis preserved (likely referenced by the obfuscator's resource handling)
In sample 5f54948e (unclassified-dotnet-crypter-loader sibling):
- 7 TypeDefs, all renamed to Hangul jamo / CJK Unified Ideograph Extension A / Arabic Extended-A sequences (e.g.,
㇣ㇿᄊᇧᇕㅩ㈢ꦅᅜᅂ) - 32 MethodDefs; most are obfuscated, but plaintext
.ctor,get_Tag,set_Tag,HookProc,RunDialog,OnHelpRequest,OwnerWndProc,Reset,VirtualProtect,LoadLibrary,GetProcAddress, and.cctorsurvive as unobfuscated symbols - Assembly name
㇣ㇿᄊᇧᇕㅩ㈢ꦅᅜᅂ— same character-set family as the type names - Module name
ᅮㅾ㇂㇗ᇩㅛᇾㆲᆔᆄᆃ㈭ᇱᇊᇅ㇔ㆵힰꦫ㈛ᅶ— extended Unicode block mixing Hangul, Khmer, Myanmar, Georgian, and CJK extensions - This is the same crypter-loader codebase as siblings
4e31a886and673e6738(shared method namesComplementaryStrings,Get_AmsiOpenSession_Byte, etc.), demonstrating that Unicode name obfuscation can be layered on top of an existing unobfuscated builder without changing the underlying logic ^[/intel/analyses/5f54948ef4ea19feac07408f4109df54c07137d35b34804250cd20eeccfcd254.html]
Reproduce on Your Own VMs
- Build a simple .NET Framework 4.0 Console app in Visual Studio.
- Download dnlib (https://github.com/0xd4d/dnlib) or install via NuGet.
- Post-build, run a dnlib script to rename all types and methods:
// dnlib rename script (run as post-build step)
var mod = ModuleDefMD.Load("YourApp.exe");
uint cp = 0x0002;
foreach (var type in mod.Types) {
if (type.IsGlobalModuleType) continue;
type.Name = new UTF8String($"\u0002\u2000\u2001{(char)(cp++)}");
foreach (var method in type.Methods) {
if (method.IsConstructor || method.IsStaticConstructor) continue;
method.Name = new UTF8String($"\u0003\u2002\u2003{(char)(cp++)}");
}
}
mod.Write("YourApp_obf.exe");
- Verify with
dnfileordnlib: all TypeDefs and MethodDefs should show control-character names. - Open in dnSpy — the tree view should be unreadable.
Defensive Countermeasures
- Do not rely on string-based IOCs for .NET malware when this obfuscation is present
- Use capa / behavioral detection (API call patterns) instead of symbol names
- Monitor for
GetManifestResourceStream+RijndaelManaged/AesManagedsequences in .NET process telemetry - Track by MVID (.NET module GUID) rather than type names
Pages Where Observed
- unclassified-dotnet-whisper —
d9c0bc24(first confirmed sample)