The Backdoor That Shreds Its Own Source Code

Kazuar, Turla, and the one wrong hex digit that hid all of it for a week

Malware Analysis
Attribution
.NET
Reverse engineering a Turla-attributed Kazuar sample: reflection-based API hiding, post-JIT IL erasure, string key recovery, and the C2 URL that confirmed attribution.
Published

August 6, 2026

The sample is 1.9 MB of .NET, compiled in July 2023, unsigned, unpacked, and it imports exactly one function: mscoree.dll._CorExeMain. That single import is the entire native surface. Everything else the binary does to a machine, and it does plenty, it does without ever declaring it.

That’s the first thing worth saying about Kazuar. Most malware lies to you somewhere. This one has an import table that is technically, completely honest and still tells you nothing at all.

What it is

Kazuar is a .NET backdoor first documented by Unit 42 in 2017 and tracked by MITRE as S0265, attributed to Turla — also called Secret Blizzard, Snake, or Uroburos depending on who’s writing. It’s a second-stage implant. Something else gets in first, then Kazuar arrives to stay.

Our copy is 436cfce7...c85, pulled for analysis in June. I’ll get to how we confirmed the Turla attribution at the end, because it turned out to be the cleanest part of the whole exercise, and because I’d rather show the work before the conclusion.

Three layers of hiding, none of them a packer

The binary is not packed. Entropy across .text sits at 6.13, which is ordinary compiled code, and any tool will happily decompile it. You get 397 files of C# out of it in a few seconds.

They’re unreadable. Every class is named something like OsticalHionerte or FomecexeGuety, ConfuserEx-style. That much is routine. What isn’t routine is the layer underneath.

There is no DllImport anywhere in the assembly. Not one. Every Win32 call goes through a class we ended up naming Win32Proxy, which holds 226 static IntPtr slots, one per API. First call to any of them resolves the DLL by LoadLibrary, resolves the function by GetProcAddress, caches the pointer, and marshals it to a delegate. Every call after that skips straight to the cached slot. It’s structurally the same idea as the ELF procedure linkage table, rebuilt by hand in C#.

Above that sits a second layer doing the same thing to the .NET framework itself. Every base class library call — every string operation, every file handle, every socket — routes through a reflection dispatcher we named ReflectUtil, which does type.GetMethod(name).Invoke(args) and nothing else. It has a call in-degree of 618.

And the names? Both layers take their target names as encrypted integer arrays and decrypt them at the moment of use. So the static picture is a program that calls nothing, imports nothing, and names nothing.

To read any of it, you have to break the string cipher first.

The hex digit

The cipher isn’t complicated. A counter-mode PRNG seeded from two values: the first element of each ciphertext array, XORed with a constant baked into the assembly, XORed with a value derived from the module’s MVID — the GUID .NET stamps into every compiled assembly.

We had both. The constant is sitting in the decompiled source in plain decimal, 1942153855u. The MVID is in the PE header. Decryption produced garbage.

So we went hunting. Maybe the MVID gets patched at runtime, which would be a clever trick and would explain everything. We dumped memory nine times across a sixty second run, found the assembly’s metadata header at 0x00C5DC44 in every single dump, and confirmed the MVID was byte for byte identical to the on-disk value the whole time. Not patched. There’s even a function in the binary that looks like it was written to patch it, and it has zero callers. Dead code.

Then we brute-forced the entire 32-bit constant space against a known plaintext, and it came back with a hit. The correct value was 0x73C2EA7F.

The script had 0x73C0527F.

Someone had converted 1942153855 to hex by hand at some point and gotten one digit wrong. Not the key — the key was correct in the source the entire time, sitting there in decimal, readable, right. Just the hex transcription of it. And because three further PRNG constants are derived from that value by XOR, one wrong digit poisoned all four, and every output was noise regardless of what else you tried.

The really irritating part is what that error hides. A search of memory for the wrong constant returns zero hits, which is exactly what you’d expect if the value were being patched at runtime. The bug produced evidence for the theory it caused. That’s the kind of failure that costs a week, and it should have cost an hour with a single assertion comparing the decimal in the source against the hex in the script.

With the right constant, all 5,935 call sites decrypt. Registry paths, the KeePass config file it goes looking for, the task command vocabulary, the C2 URL.

It destroys itself as it runs

The other reason static analysis alone won’t get you there: Kazuar erases its own bytecode while running.

There’s a class we named PostJitILEraser. It walks every method in its own assembly, and for each one it calls RuntimeHelpers.PrepareMethod to force the JIT to compile that method to native code, then overwrites the original IL bytes in the mapped image with output from System.Random.

The ordering is the whole trick. JIT first, destroy second. The compiled native code lives in the CLR’s code cache and runs perfectly. The IL it was compiled from is gone, replaced with noise. Dump the process an hour in and you get a .NET assembly whose method bodies are random bytes. All those encrypted string arrays that were sitting in the IL as ldc.i4 constants only exist now as immediate operands inside JIT-compiled x86.

Getting there requires a small pile of unpleasant work on the malware’s part. It has to find its own load address, walk its own PE headers by pointer arithmetic to locate .text, call VirtualProtect to make its own code section writable, and reach into the CLR through reflection to pull out the private IMetaDataImport COM interface so it can turn method tokens into addresses. It’s genuinely well built. I don’t say that often.

It’s also defeatable in about four ways, which is the pattern with anti-analysis features generally. The simplest: before it overwrites a method, it compares the IL at the computed address against what the runtime reports for that method, and bails if they differ, so it never double-erases. That check means the erasure is idempotent and, more usefully, that a dump taken before the eraser’s entry point is complete and intact. Break there, dump, done. Or flip the config flag that gates the whole thing, since the value is read from a field you can watch and write. The feature is real protection against a lazy dump. It is not protection against someone who sets a breakpoint.

And a detail I enjoyed: Main is a decoy. Three lines. It sets a config path, then loops forever calling Thread.Sleep on a one millisecond interval. Every piece of actual initialization happens in the type’s static constructor, which the CLR runs before Main is ever entered. If you attach a debugger and break on the entry point, you are already late.

What it does once it’s up

We ran it for sixty seconds on an isolated bare-metal host under Intel Processor Trace and caught 20.8 million packets. About 99% of that is runtime overhead, WOW64 thunks and CLR internals, because a 32-bit .NET process on a 64-bit Windows box spends most of its instruction budget just being a 32-bit .NET process. The malware’s own logic shows up as 733 trace hits at heap addresses belonging to no module at all, which is what JIT-compiled code looks like from the outside.

Filtering out the noise, the interesting behavior:

It talks to the Windows Event Log through wevtapi, and not just to read it. EvtSubscribe, then EvtSeek, then EvtIntWriteXmlEventToLocalLogfile. Kazuar is documented as using the event log as a covert channel between its own components: commands go in as event records, another component reads them out. The log becomes a message queue that every forensic tool on the box is already configured to ignore as routine. Watching a sample subscribe and write in the same sixty second window is a nice confirmation of a technique that sounds too cute to be real.

Seven calls to WldpCheckRetailConfiguration land in the startup window, all asking the same question: is this a locked-down retail system. It wants to know where it woke up before it commits to anything.

WMI queries go out over DCOM asynchronously, with a callback object registered so results come back on their own rather than blocking the caller. Configuration comes out of DPAPI protected blobs in the registry, which means the encryption key is the user account itself and the data is worthless if you carry the file off the machine without the profile.

The network capture was a dud, for an honest reason worth recording: 1,662 outbound HTTPS connections, all of them Windows telemetry redirected to our INetSim box. No C2 domain in the DNS log at all. Either the beacon interval was longer than our capture window or resolution happened outside it. The C2 address came out of memory and string decryption instead, not off the wire.

Confirming Turla

Here’s where it lands. Family attribution deserves better evidence than “the code looks like the writeups,” and after spending a month inside the archive watching antivirus family tags fail almost completely, I’m not inclined to accept a label on convention.

The decrypted strings gave us a C2 URL: https://arianeconseil[.]online/wp-includes/sitemaps/html/, defanged here. A compromised WordPress install, which is unremarkable in itself.

That exact URL, path and all, appears in an independently published January 2026 analysis of Turla’s Kazuar v3 loader. Not the same domain used differently. The same string.

That’s infrastructure overlap, and it’s the strongest kind of evidence available here, because it doesn’t depend on trusting any of our own reversing work. Anyone can go read that report and compare.

The supporting matches are the kind you’d expect once you know what you’re holding. My favourite is the config key. The flag that gates the IL eraser decrypts to antidumpmethods, and Unit 42’s writeup describes Kazuar checking an antidump_methods setting before wiping its own methods out of memory. Same feature, same name, recovered independently from ciphertext by someone who hadn’t read that paragraph yet.

Past that it’s the whole documented shape of the family: reflection instead of imports, ConfuserEx, protobuf on the wire, HTTP and WebSocket and Exchange Web Services transports, a numbered task model whose log strings still say Solving task #{0}, and a hardcoded interest in KeePass\KeePass.config.xml.

What doesn’t match, stated plainly: our hash appears in none of the published lists. That January report profiles a loader chain with kernel, worker, and bridge components. Ours is a standalone payload that arrives after all of that. Same family and same operator, different build or different stage. And the July 2023 compile timestamp sits suggestively close to CERT-UA reporting a fresh Kazuar version against Ukrainian defense targets that same month, which I mention only to say I’m not counting it. PE timestamps are a text field. Anyone can type in one.

One shared URL, five architectural matches, one honest gap. That’s the case, and I’d rather publish it with the gap visible than round it off.

Two things I’d do differently

Assert your constants against the source before you believe a decryption failure. The week we lost was spent building increasingly elaborate theories about runtime MVID patching, and the binary even supplied a plausible suspect in the form of a patcher function that turned out to have no callers. A four-line check comparing the decimal in the decompilation against the hex in the script would have ended it immediately.

And take the dump at the eraser’s entry point. We spent real effort thinking about how to defeat the IL erasure and the answer was to arrive before it happened.

Nothing about this sample is hard once the strings come out. Almost nothing about it is possible before that. The distance between those two states was one hex digit wide.