---
title: .NET Reactor obfuscation pitfalls when the consumer is C++/CLI
title_en: .NET Reactor obfuscation pitfalls when the consumer is C++/CLI
description: Obfuscating LeYuECat's Managed.dll with .NET Reactor (Eziriz) blew up on the customer's machine during module cctor with mscorlib's recursive resource lookup bug, via the C++/CLI load path. This is a record of the incompatibility between NecroBit and mixed-mode assembly init, the final "weakened-but-compatible" protection profile, and the Visual Studio AfterBuild integration for Reactor.
sidebar_label: .NET Reactor × C++/CLI pitfalls
---

# .NET Reactor obfuscation pitfalls when the consumer is C++/CLI

> Scenario: after shipping the C# motion/IO plugin to a customer [wrapped as a native C++ wrapper via C++/CLI](./csharp-dll-to-cpp-interop), we also wanted to obfuscate the managed DLL with [.NET Reactor (Eziriz)](https://www.eziriz.com/reactor.htm) to prevent the customer from one-click-decompiling it with dnSpy. Two seemingly independent things — combined, they crash on the customer's machine.

---

## What broke

First deployment of the obfuscated `Managed.dll` to the customer site (Traditional Chinese Windows 11, CP950) running the C++ sample exe:

### Customer-side symptom

An `Assert Failure` dialog:

```
Expression: [mscorlib recursive resource lookup bug]
Description: Infinite recursion during resource lookup within mscorlib.
Resource name: Arg_NullReferenceException
```

Corresponding Windows Event Log (`.NET Runtime` provider, **EventID 1025**):

```
應用程式透過 System.Environment.FailFast(string message) 要求終止處理序。
訊息: Infinite recursion during resource lookup within mscorlib.
```

### Key stack trace (read bottom-up)

```
ECat_Service_CreateFromConfig(SByte*, Void**)         ← C++/CLI exported entry
↓
<Module>..cctor()                                     ← module init injected by Reactor
↓
[four levels of nested obfuscated cctor chain]
  ANUmTGFYQd8qoGra3l6.GlSORSFN8ZH7tAqY0C4..cctor()
    → OGvPt1pYkgbgnM9n3TH.ilqFOPpN6W3xlxKdrtt..cctor()
      → AiQ5gEp9SVeGYOOrn4F.l3peVMpP4OSZLavrqGZ..ctor()
        → NRMHOWpkBRE0JgXhfZX.CduiUepOtuoj6c7HJl5..cctor()
          → NullReferenceException..ctor()            ← cctor throws NRE
```

The cctor throws `NullReferenceException` → mscorlib tries to fetch the localized string for `Arg_NullReferenceException` → triggers `AppDomain.OnResourceResolveEvent` → Reactor's injected resolve handler throws NRE again → infinite recursion → CLR `FailFast`.

---

## First attempt (failed): enable NecroBit Reflection Compatibility Mode

The intuition was that NecroBit's ResourceResolve handler isn't reentrant, so the official-docs-recommended setting:

```ini
NecroBit_Reflection_Compatibility_Mode = true
```

**Result: same crash, no improvement.**

Reason: the real culprit isn't resource lookup itself — somewhere in the cctor chain, a method **actually** throws an NRE. Reflection Compat Mode only stops the resource resolve handler from blowing up on re-entry; it doesn't help with "init phase throws the first NRE".

---

## Root cause

A search of Eziriz's official Google Group turned up [C++/CLI (CLR Console App) with VS2005 Problem](https://groups.google.com/g/net-reactor-support/c/Yl1TpFIYJpw) — **an identical report**:

> A user protected even a Hello World C++/CLI console app with the default Quick Settings (**NecroBit + Obfuscation only**), and got an immediate crash with `System.Reflection.TargetInvocationException`.

In other words, **NecroBit is incompatible with the assembly load order of mixed-mode (C++/CLI `#using` load path)**. NecroBit's module-level init depends on certain CLR state, but C++/CLI's load timing for mixed-mode assemblies is different (see [Microsoft Learn: Initialization of Mixed Assemblies](https://learn.microsoft.com/en-us/cpp/dotnet/initialization-of-mixed-assemblies)) — NecroBit's init is triggered before it's ready → cctor throws NRE.

Our scenario fits exactly:

- `LeYu.ECat.CppCli.dll` is C++/CLI `/clr` mixed-mode
- Customer `.exe` loads it via the import lib
- It then loads the Reactor-protected `LeYu.ECat.Managed.dll` via `#using`
- The first touch to Managed.dll runs the module cctor → NecroBit init → boom

Pure C# EXEs loading an obfuscated DLL don't hit this; **only the C++/CLI mixed-mode init path triggers it**.

---

## Final solution: a weakened-but-compatible protection profile

### .nrproj configuration changes

| Setting | Was | Now | Reason |
|---|---|---|---|
| `NecroBit` | true | **false** | **The direct culprit — incompatible with C++/CLI** |
| `Anti_Tampering` | true | false | Hash checks fail on the `#using` load path |
| `Inject_Invalid_Metadata` | true | false | Metadata mutation interacts badly with the CLR loader |
| `String_Encryption` | true | false | Strings get referenced before the init-phase decryption is ready |
| `Resource_Encryption_And_Compression` | true | false | Same as above |
| `Control_Flow_Obfuscation` | false | **true** | **Newly enabled**: compensate for losing NecroBit by scrambling method bodies |
| `Obfuscation` | true | true | Name obfuscation — the most important customer-facing protection |
| `Anti_ILDASM` | true | true | Cheap, no side effects |
| `Obfuscate_Public_Types` | false | false | **Cannot be changed** — `#using` references and customer callback signatures rely on the public names |
| `NecroBit_Reflection_Compatibility_Mode` | false → true | true | With NecroBit off this no longer matters, but leaving it on is harmless |

### Protection level comparison

| What an attacker with the obfuscated DLL wants to do | Before (NecroBit on, but doesn't run) | After (NecroBit off, runs) |
|---|---|---|
| Decompile to readable C# with ILSpy / dnSpy | Fully blocked | IL is visible, but **garbled names + control-flow obfuscation** make the logic hard to reconstruct |
| ildasm IL dump | Blocked (Anti-ILDASM) | Blocked (Anti-ILDASM) |
| grep strings for algorithm names | Blocked | Strings visible (acceptable if the lib contains no sensitive strings) |
| Forge signature and repack | Blocked (Anti-Tampering) | Not blocked (if the lib has no license logic, no payoff) |

For the threat model "motor control lib + no license mechanism + no secret algorithm", this profile **is enough** — the goal is mainly to stop the "decompile with dnSpy and copy-paste" adversary.

---

## Visual Studio + .NET Reactor integration

Wire Reactor into MSBuild so every Release build obfuscates automatically — no manual `dotNET_Reactor.Console.exe` runs.

### 1. Environment variable (set once per build machine)

```powershell
[Environment]::SetEnvironmentVariable(
    'DOTNETREACTORROOT',
    'C:\Program Files (x86)\Eziriz\.NET Reactor',
    'User')
```

This keeps the vcxproj from hardcoding paths, and lets machines without Reactor still build (gracefully skipping rather than failing).

### 2. .nrproj next to the managed project

`src/Managed/LeYu.ECat.Managed.nrproj` — created via Reactor's GUI, with the configuration above saved into it. **Don't expose this file's master key.** If the repo is going public, remove `.nrproj` from git history first and regenerate the master key in the GUI.

### 3. AfterBuild target in the CppCli project

In `LeYu.ECat.CppCli.vcxproj` (or whichever project assembles the dist):

```xml
<Target Name="AfterBuild">
  <!-- First copy every DLL from OutDir to dist/bin/ -->
  <ItemGroup>
    <_DistBinDlls Include="$(OutDir)*.dll" />
  </ItemGroup>
  <Copy SourceFiles="@(_DistBinDlls)"
        DestinationFolder="$(SolutionDir)dist\bin\"
        SkipUnchangedFiles="true" />

  <!-- Reactor path: prefer env var, fall back to default install path -->
  <PropertyGroup>
    <NetReactorRoot Condition="'$(DOTNETREACTORROOT)' != ''">$(DOTNETREACTORROOT)</NetReactorRoot>
    <NetReactorRoot Condition="'$(NetReactorRoot)' == ''">C:\Program Files (x86)\Eziriz\.NET Reactor</NetReactorRoot>
    <NetReactorExe>$(NetReactorRoot)\dotNET_Reactor.Console.exe</NetReactorExe>
    <NetReactorProject>$(SolutionDir)src\Managed\LeYu.ECat.Managed.nrproj</NetReactorProject>
    <DistManagedDll>$(SolutionDir)dist\bin\LeYu.ECat.Managed.dll</DistManagedDll>
  </PropertyGroup>

  <!-- Only obfuscate in Release with Reactor present; otherwise skip, don't fail -->
  <Message Text="Protecting $(DistManagedDll) with .NET Reactor..." Importance="high"
           Condition="'$(Configuration)' == 'Release'
                      AND Exists('$(NetReactorExe)')
                      AND Exists('$(NetReactorProject)')" />
  <Exec Command="&quot;$(NetReactorExe)&quot; -licensed -q -project &quot;$(NetReactorProject)&quot; -file &quot;$(DistManagedDll)&quot; -targetfile &quot;$(DistManagedDll)&quot;"
        Condition="'$(Configuration)' == 'Release'
                   AND Exists('$(NetReactorExe)')
                   AND Exists('$(NetReactorProject)')" />
</Target>
```

Key points:

- **In-place overwrite**: `-file` and `-targetfile` point to the same path. Reactor replaces `dist/bin/Managed.dll` with the obfuscated version
- **No obfuscation on Debug builds**: the `'$(Configuration)' == 'Release'` condition; otherwise every F5 stalls waiting for obfuscation
- **No-Reactor machines don't fail**: `Exists('$(NetReactorExe)')` guards it, so CI servers and new-developer machines still build (their dist is just unobfuscated)
- **`-licensed -q`**: licensed mode + quiet. Without a license it falls back to trial mode and the build log fills with red warnings
- **Reactor processes the `dist/bin/` copy, not `OutDir`**: keep `OutDir` unobfuscated for local debugging; only the shipping copy is obfuscated

---

## Maintenance advice

### When upgrading .NET Reactor

A new version may have fixed NecroBit's C++/CLI compatibility. After upgrade:

1. On a dev machine, set `NecroBit = true` and build the dist
2. Take the dist to a test machine and run the sample exe
3. If it no longer crashes → it's fixed; you can raise the protection level
4. If it still crashes → keep it OFF

### Filling the gap left by NecroBit

Alternatives worth considering:
- **Hide_Method_Calls** — advanced method-call obfuscation, fewer side effects
- **Stealth_Obfuscation** — advanced name obfuscation mode
- A custom native obfuscation pass — obfuscate the C++/CLI bridge layer instead of the managed side

### Things to never do

- ❌ **Don't set `Obfuscate_Public_Types` to true** — customer callbacks and `#using` both break. Public class / method / property names **must be preserved**.
- ❌ **Don't re-enable NecroBit without first running it through a test machine** — we already know it doesn't run; only a Reactor update has any chance of making it work.
- ❌ **Don't publish the .nrproj master key to a public repo.**

---

## Verification checklist (reuse this for future tests)

1. **Build:** from a Developer Command Prompt
   ```
   msbuild LeYuECat.sln /p:Configuration=Release /p:Platform=x64
   ```
   Expected: build log shows `Protecting ...\dist\bin\LeYu.ECat.Managed.dll with .NET Reactor...`, 0 errors 0 warnings.

2. **Local managed tests:** the unobfuscated `src/Managed/bin` copy; obfuscation doesn't affect unit tests:
   ```
   dotnet test src\Managed\Managed.Tests\Managed.Tests.csproj -c Release
   ```

3. **Customer-side smoke test:** copy the full `dist/` to a clean test machine (or Windows VM):
   - Run `redist/VC_redist.x64.exe` once
   - Go into `samples/01_InitShutdown/`
   - `msbuild 01_InitShutdown.vcxproj /p:Configuration=Release /p:Platform=x64`
   - Run the produced `01_InitShutdown.exe`
   - **Expected**: exit code 0; `ethercat_wrapper.log` shows `EtherCAT initialization started ... completed`; no crash dialog

4. **Advanced verification (obfuscation is effective):**
   - Open `dist/bin/Managed.dll` in dnSpy
   - Expected: the public `LeYu.ECat.*` namespaces are visible and public class names are preserved, but inside method bodies the locals / control flow are garbage or full of meaningless goto

---

## TL;DR

If your C# DLL will be loaded by a C++/CLI `/clr` consumer:

1. **NecroBit always OFF.** Other options that depend on module init / metadata rewriting (Anti-Tampering, Inject Invalid Metadata, String/Resource Encryption) should also be off
2. **Keep Obfuscation + Anti-ILDASM + Control Flow Obfuscation** as the primary protection
3. **`Obfuscate_Public_Types` always OFF** — `#using` and callbacks both rely on the public names
4. Wire Reactor into MSBuild's `AfterBuild` target, with a `DOTNETREACTORROOT` env var + `Exists()` guard so non-Reactor machines can still build
5. Before shipping, always run a full sample on a **clean Traditional Chinese Windows test machine** — the CP950 environment is a commonly overlooked variable

---

## References

- [Eziriz forum: C++/CLI (CLR Console App) with VS2005 Problem](https://groups.google.com/g/net-reactor-support/c/Yl1TpFIYJpw) — thread with identical symptoms
- [Eziriz: .NET Reactor Features](https://www.eziriz.com/reactor_features.htm)
- [Eziriz: Command Line Parameters](https://www.eziriz.com/help/command_line_parameters/)
- [Microsoft Learn: Initialization of Mixed Assemblies](https://learn.microsoft.com/en-us/cpp/dotnet/initialization-of-mixed-assemblies)

### Related docs

- [Calling C# DLLs from C++ — survey + LeYuECat case study](./csharp-dll-to-cpp-interop) — the prerequisite context for this obfuscation pitfall (why there's a C++/CLI mixed-mode load path in the first place)
