Skip to content

.NET Reactor: Fix CflowConstantsInliner corrupting exception handlers - #40

Closed
GarethWright wants to merge 1 commit into
GDATAAdvancedAnalytics:masterfrom
GarethWright:fix/cflow-constants-inliner-instruction-identity
Closed

GarethWright wants to merge 1 commit into
GDATAAdvancedAnalytics:masterfrom
GarethWright:fix/cflow-constants-inliner-instruction-identity

Conversation

@GarethWright

@GarethWright GarethWright commented Sep 21, 2026 •

Copy link
Copy Markdown

Problem

CflowConstantsInliner.InlineAllConstants replaces whole Instruction objects when inlining the cflow constant fields:

instrs[i] = Instruction.CreateLdcI4(value);
if (nopNext)
    instrs[i + 1] = Instruction.Create(OpCodes.Nop);

If the original ldsfld/ldfld happens to be an exception-handler boundary (e.g. HandlerEnd) or a branch target, the handler/branch keeps referencing the old, now-detached instruction, which is no longer in body.Instructions.

InstructionListParser then throws KeyNotFoundException from instrToIndex[...] (via MarkAsBranchTarget / GetInstrIndex) when it rebuilds that method's blocks. Because this happens during normal method deobfuscation, it aborts deobfuscation of the entire assembly, not just the affected method.

Observed stack trace against current master:

ERROR: Stack trace:
ERROR:    at de4dot.blocks.InstructionListParser.MarkAsBranchTarget(Instruction instr) in InstructionListParser.cs:line 58
ERROR:    at de4dot.blocks.InstructionListParser.CreateExceptionBranches() in InstructionListParser.cs:line 68
ERROR:    at de4dot.blocks.InstructionListParser..ctor(...)
...
ERROR:   System.Collections.Generic.KeyNotFoundException

Fix

Write the constant in place (OpCode / Operand) instead of substituting new Instruction objects, so exception handlers and branches keep pointing at a live instruction. The emitted IL is unchanged — only object identity is preserved.

Trigger condition

The bug isn't specific to a particular .NET Reactor version. It only requires an inlined cflow-constant field access to coincide with an exception-handler boundary or a branch target, which is why it shows up on some protected assemblies and not others.

Testing

  • On an affected assembly (a protected binary where an inlined constant field sits on an exception-handler boundary): KeyNotFoundException crash → deobfuscates and saves normally.
  • No regression across a corpus of ~50 other .NET Reactor-protected assemblies: all still save, no new crashes.

InlineAllConstants replaced whole Instruction objects when inlining the
cflow constant fields:

    instrs[i] = Instruction.CreateLdcI4(value);
    instrs[i + 1] = Instruction.Create(OpCodes.Nop);

If the original ldsfld/ldfld was an exception-handler boundary or a branch
target, that reference kept pointing at the now-detached instruction, which
is no longer part of the method body. InstructionListParser then threw
KeyNotFoundException from instrToIndex[...] while rebuilding the method's
blocks, aborting deobfuscation of the whole assembly.

Write the constant in place (OpCode/Operand) so exception handlers and
branches keep referring to a live instruction. The emitted IL is unchanged.

The trigger is not version-specific: it only requires an inlined
cflow-constant field access to coincide with an exception-handler boundary
or a branch target.
@GarethWright
GarethWright force-pushed the fix/cflow-constants-inliner-instruction-identity branch from 52de45c to 15d0765 Compare September 21, 2026 16:38
@GarethWright

Copy link
Copy Markdown
Author

Closing this and reopening as a fresh PR with a tidied-up description. No change to the code.

@GarethWright
GarethWright deleted the fix/cflow-constants-inliner-instruction-identity branch September 21, 2026 16:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant