Browse documentation
Documentation / Protection passes / Decompiler-resistant wrapping

Decompiler-resistant wrapping

ImplementedEvery remaining method body is wrapped in a whole-method exception handler, with no change to observable behaviour.

What it does

Every non-constructor method that still has an ordinary bytecode body, anything left over after nativization has taken what it can, is wrapped in a whole-method catch (Throwable t) { throw t; }. This was chosen over more aggressive decompiler-confusion techniques (malformed exception ranges, corrupted attribute lengths) because it cannot change observable behaviour: every path still terminates exactly as before, and an escaping exception still propagates exactly as before. It only adds a control-flow shape, a handler covering an entire method body with no locally-visible reason to exist, that common decompilers' control-flow-to-source reconstruction tends to mishandle or render unreadably.

Where it sits in the pipeline

Applied once nativization decisions are already final, directly to whatever's left as ordinary bytecode, never to a method about to be nativized, since a nativized method has no Code attribute at all for a decompiler to see in the first place, and wrapping it first would only add an unnecessary nested-exception-handler shape the translator doesn't need to deal with.

Threat model

This is a decompiler-output-quality attack, not a logic change and not a runtime protection: it doesn't detect anything or fail anything closed. It only makes the decompiled source of a non-nativized method harder to read correctly.

Verified. Every wrapped class in a protected Apache Commons Exec build loads and verifies under full JVM bytecode verification (triggered via Class.forName across every output class, including those no test exercises), with javap -v confirming a correctly typed StackMapTable entry at each injected handler.