fixing ConfuserEx for #249 and some extra bugs - #250
Merged
Conversation
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 Report❌ Patch coverage is 📢 Thoughts on this report? Let us know! |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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.