Skip to content

fixing ConfuserEx for #249 and some extra bugs - #250

Merged
BinFlip merged 13 commits into
developfrom
review/pr-248
Jul 27, 2026
Merged

fixing ConfuserEx for #249 and some extra bugs#250
BinFlip merged 13 commits into
developfrom
review/pr-248

Conversation

@BinFlip

@BinFlip BinFlip commented Jul 27, 2026

Copy link
Copy Markdown
Owner

Builds on #248 by @agski331. Fixes #249.

Their fix makes unreferenced-type analysis transitive; this reworks it into an O(V+E) worklist, handles the nested-type case they identified, and fixes the ConfuserEx constant-decryption chain their sample exposed — decryptor signature width, three CIL emulation defects, the LZMA header variant, and a cleanup pass that deleted infrastructure regardless of whether decryption succeeded.

agski331 and others added 13 commits July 26, 2026 16:16
Re-scanning the whole call graph once per rescue round is O(depth x edges);
collapsing it to a type-level graph once and draining a worklist is O(V+E)
and needs no convergence loop. Also seeds CustomAttribute constructor types
as reachability roots rather than filtering them afterwards, so the types
those constructors call stay alive too, and sorts the result for a
reproducible deletion order.
Deleting an enclosing type cascades to its nested types through
expand_type_tokens, so a nested type that live code still calls was being
dropped along with a parent that only looked dead. Liveness now propagates
from a nested type to the type that encloses it.
ECMA-335 III.4.29 defines stobj over any typeTok and makes it equivalent to
stind.ref when that type is a reference type, which is what 'stobj !!T'
compiles to once T is instantiated with one. The handler rejected anything
outside a value-type whitelist that also omitted U4, Boolean, Char and the
remaining integer widths.
ldelem and stelem both go through extract_array_index, which accepts every
integer width plus boxed indices. ldelema hand-rolled a match on I32 and
NativeInt only, so a native uint index aborted emulation.
ldelema on a value-type array yields a pointer to the element, so a stfld
through it updates one field inside that struct. Falling through to
store_through_pointer replaced the entire element with the field's value
instead. Read-modify-write the element, and route reference-type elements
to the object they point at.
The constant-blob index was matched against I4 alone. Stock ConfuserEx 1.6.0
emits 'T Get<T>(int32)' but other builds emit uint32, and those decryptors
went undetected entirely. The surrounding constraints (static, <Module> or
non-ASCII owner, arity 1, string or !!0 return) carry the selectivity, so
the width does not need pinning.
Two defects kept real constant blobs from decompressing. The sniffer
required the payload to be smaller than its declared output, but LZMA
expands small high-entropy input and the blob is XOR-encrypted before
compression, so a 44-byte blob compresses to 51. And the header was modelled
as 5 property bytes plus a 4-byte size, while builds using the LZMA SDK's
stream API write the standard 13-byte header with an 8-byte size — reading
that as 9 bytes shifts the payload and corrupts the range coder.

Both layouts are now tried and validated against the size each declares. The
size guard is documented as an allocation guard rather than a format rule
and raised to a value no real blob approaches.
A technique builds its cleanup request from detection findings alone, so it
schedules the decryptor, its infrastructure type, the initializer and the
encrypted data for deletion whether or not a single call site was reversed.
When decryption fails the call sites remain and are left pointing at
metadata that no longer exists, which empties the methods holding them.

Removal now requires that nothing still calls the decryptor: a successful
decryption rewrites its call site to the constant, so no remaining callers
is the evidence that it was reversed. Absence of recorded failures is not,
since a decryptor that was never exercised has none either.
reactor_virtualization is not devirtualized by any technique, so the VM
interpreter and its handler types stay reachable through the stubs left in
the virtualized methods. Non-transitive reachability read that cluster as
isolated infrastructure and cut the assembly from 854 methods to 45.
Pushing to master triggered the release workflow, guarded only by a check
that the version in Cargo.toml had no release yet. That made 'will this
publish?' something to reason about rather than read, and it is the reason
merging anywhere near master felt unsafe.

Publishing is now driven by publishing a GitHub release. The version comes
from the release tag and is checked against Cargo.toml, so a mistyped tag
fails before anything ships rather than releasing under the wrong version.
Creating the release and writing its notes is a manual step; the workflow
attaches the CLI binaries and publishes the crate.
@codecov

codecov Bot commented Jul 27, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 80.72289% with 32 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
...scope/src/emulation/engine/interpreter/handlers.rs 4.76% 20 Missing ⚠️
dotscope/src/utils/decompress.rs 90.66% 7 Missing ⚠️
dotscope/src/deobfuscation/cleanup.rs 82.14% 5 Missing ⚠️

📢 Thoughts on this report? Let us know!

@BinFlip
BinFlip merged commit 7402177 into develop Jul 27, 2026
7 of 8 checks passed
@BinFlip
BinFlip deleted the review/pr-248 branch July 27, 2026 03:02
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