Commercial Android code obfuscation and app-protection framework. Proprietary — use requires a license key issued by the author.
A DexGuard-style protection suite built on ASM and dexlib2. Full name obfuscation (classes, members, virtuals and manifest components), DEX-level string encryption, resource-name obfuscation, R8-compatible method virtualization (int/long/arrays/calls/fields via a bytecode dispatcher), native library encryption (encrypt the app's own
.so, strip plaintext, transparent runtime loader), a native C++ RASP with un-hookable signature verification, reflection/JNI-aware keep, ReTrace mapping, a config-driven CLI and a Gradle plugin. Created by Ivan Garibay.
DexShield is a clean, modular, documented reimplementation of the protection
techniques found in commercial tools like DexGuard, with a readable YAML config
and an extensible pipeline. It operates on two levels: the JVM bytecode
pipeline (pre-D8, ASM-based) and the DEX backend that rewrites the final
classes*.dex, AndroidManifest.xml and resources.arsc of a built APK
(post-D8/R8), plus a native .so RASP.
Every DEX-backend transformation in this README has been verified running on real hardware — installed and executed on real Android devices, not just in unit tests.
- docs/CONFIGURATION.md —
dexshield.ymlschema reference +protect-apkflags (stable 1.x surface). - docs/LIMITATIONS.md — known limitations and honest PCI/MPoC alignment (support, not certification).
- docs/ONBOARDING.md — licensee onboarding: get a key, install, integrate, verify.
- ✅ Classes — renamed at DEX level, remapping the entire reference graph
(superclasses, interfaces, field types, method signatures, and all in-body
references:
new-instance,invoke,check-cast,const-class, …). The original package is preserved (moving classes across packages would break package-private access →IllegalAccessError). - ✅ Methods & fields (
private/static) — the non-polymorphic subset, whose references always name the exact declaring class, so remapping is safe.main,<init>,<clinit>andnativemethods are kept. - ✅ Virtual methods — renamed consistently across the whole override chain,
only for hierarchies that are entirely internal to the DEX (with
java.lang.Objectas the only allowed library boundary). Framework overrides (onCreate,run,toString, …) are never touched. - ✅ Manifest components — Activities/Services/Receivers/Providers renamed in
the DEX and in the binary
AndroidManifest.xml, coordinated (relative.Namevalues resolved against thepackage). - ✅ Resource names (
resources.arsc) — entry names (app_name→a, …) shortened while resource IDs stay intact, so code and compiled XML keep resolving. - ✅ Reflection/JNI-aware keep (on by default) — scans the DEX and
automatically preserves classes referenced by name (
Class.forName("…"),nativemethods) and member names referenced by name (getMethod("…")/getField("…")), so aggressive obfuscation doesn't break real apps. - ✅
mapping.txt— ReTrace-compatible, for de-obfuscating stack traces.
- ✅ String encryption (DEX level) —
const-stringliterals in the final DEX are replaced with ciphertext and an auto-injecteddexrt.Sdecrypt runtime (AES-256). Covers code that already ships as Dalvik (merged libraries), not just your pre-D8 bytecode. - ✅ String encryption (JVM level) — AES-256 for
LDCliterals and the literal fragments inside"a" + x + "b"string concatenations (which javac compiles toinvokedynamic StringConcatFactory), rewritten toStringBuilder. - ✅ Strip debug info — removes line numbers, local-variable names and source-file from the final DEX.
- ✅ Control-flow obfuscation — opaque predicates with junk branches that complicate static analysis without changing behavior.
- ✅ Method virtualization — eligible methods are translated to a custom
stack-VM program executed by an embedded interpreter (
vmrt.V); the original logic no longer exists as readable JVM/Dalvik — a decompiler sees onlyreturn V.run("<isa-data>", …)(and with string encryption on, the ISA program itself is ciphertext). The ISA now coversintandlongarithmetic / bitwise / shifts / compares / conversions, primitive arrays (create, load, store, length), full control flow (branches, loops), method calls (static / virtual / interface / special) and constructors, instance and static fields, and reference types (null,String, casts). Static and instance methods whose params/return areint/long/reference/array qualify; anything else is left intact.- R8-compatible. External references (calls,
new, fields) are not resolved by name via reflection — that breaks under R8, which renames the targets. Instead each virtualized class gets a synthetic bytecode dispatcher (aswitchof realinvoke/new/get/put) that R8 renames consistently, so virtualization composes with full R8 minification. Works on API 26+ (noconst-method-handle, which needs 28). - Behaviour-preserving (58 round-trip tests: arithmetic, loops, calls,
fields/
new,long/arrays). Verified end-to-end on real hardware — a Verifone T650p (API 27) running a full app with virtualization + R8 + component renaming + string encryption boots and runs stably. - Trade-off: interpreter overhead per virtualized method — for production,
target sensitive methods (license/signature/key-derivation) via
excludesrather than the whole app.try/catchis supported — exception handlers are translated to a VM handler table and the interpreter dispatches by catch type via the same R8-safe dispatcher (instanceof, no name lookup), so methods that catch/throw(like license/signature logic) virtualize too. String concatenation is supported —invokedynamictoStringConcatFactory(makeConcat/makeConcatWithConstants, i.e. Kotlin/javac string templates and+) is translated to a VMCONCATop that bakes the constants into a recipe and interleaves the dynamic args.boolean/byte/char/short,instanceof,new T[]andswitch(table/lookup, i.e.when) are supported — enough that the license/activation client itself virtualizes end-to-end (its logic runs in the VM, not as readable bytecode, while still performing the real network check). Not yet covered: otherinvokedynamic(lambdas),DUP2/SWAP(compound assignment),charstring-concat.
- R8-compatible. External references (calls,
- ✅ Asset encryption — AES-256, with
getResourceAsStreamrewritten to decrypt transparently at runtime. - ✅ Class encryption (opt-in) — bytecode encrypted to a
.dxencresource + anEncryptedClassLoaderthat decrypts at runtime. - ✅ White-box AES-128 — T-box cipher that embeds the key (
cryptomodule), verified against standard AES and the FIPS-197 vector. - ✅ Native library encryption — the app's own
.sofiles are encrypted (AES-256, per-build 8-byte seed →K = SHA-256(seed ‖ tag), no plaintext key) intoassets/dexshield-nat/<abi>/, the plaintextlib/<abi>/*.sois stripped, and an injectedNativeLoaderruntime rewrites everySystem.loadLibrary(name)→NativeLoader.load(name)at the DEX level: it decrypts the matching asset to the app'scodeCacheDirandSystem.loads it (falling back to a normalSystem.loadLibraryfor any lib that was not encrypted, so rewriting all calls is safe). Driven byencrypt-natives -i <apk> -o <apk> [--strip] [--no-inject]. Verified end-to-end on real hardware — a Verifone T650p (armeabi-v7a, API 27) running a JNI probe with the plaintext.sostripped still executes the native method, proving the decrypt →System.load→ JNI-call path on ART.
- ✅ Java detectors — injected root / emulator / debugger / hook / tamper (anti-repackaging via signature check) detection, with a configurable callback or fail-fast.
- ✅ Native RASP (
.so) — detectors written in C++ (much harder to hook or patch than the Java layer), exposed over JNI asNativeRasp:- anti-debug —
TracerPidin/proc/self/status. - anti-Frida — scan of
/proc/self/maps(frida,gum-js-loop,linjector, …) + probe of the defaultfrida-serverport 27042. - native signature verification — reads the signing certificate directly
from the APK's v2 signing block in C++ (no hookable Android API), hashes
it (built-in SHA-256) and compares to a value baked into the
.so. Un-hookable anti-tamper: on a real device, a correctly signed APK runs; the same APK re-signed with another key aborts withFLAG_TAMPER. - Built with the NDK/CMake for
arm64-v8a+armeabi-v7a.
- anti-debug —
- ✅ Config-driven CLI — a single
dexshield.yml+protect-apkdrives the whole pipeline. Subcommands:protect,analyze,analyze-apk,protect-apk,strip-debug,encrypt-strings,rename-classes,rename-members,rename-virtual,verify-apk,encrypt-natives,validate. Plusprotect-apk --evidence <file>writes a before/after protection report (with optional--evidence-grep <regex>counting sensitive DEX strings), andverify-apkis a post-build gate (exit != 0 if sensitive patterns remain).protect-apk --keep-from-proguard <proguard-rules.pro>reads the app's-keep classrules and applies the same keeps, so keeps live in one place (the ProGuard/R8 rules) instead of being duplicated. - ✅ Gradle plugin —
com.dexshield(JAR pipeline) andcom.dexshield.android(AGP variant hook, between compilation and R8/D8). - ✅ Self-contained DEX parser — dependency-free
.dexreader foranalyze-apk(class/method/string inventory + obfuscation heuristic). - ✅ Plugin SDK — third parties extend the pipeline without touching the core.
Implement
com.dexshield.core.pipeline.Transform(a no-arg-constructor class withname/isEnabled(config)/apply(pool, config)) and register it either by listing its FQN underplugins:indexshield.yml, or by publishing it on the classpath viaMETA-INF/services/com.dexshield.core.pipeline.Transform(auto-discovered withServiceLoader). Custom transforms run after the built-in phases; each decides whether to run via its ownisEnabled. - ✅ Unit & integration tests — including real execution of the protected bytecode.
- ✅ License activation — use requires a license key issued by the author. A
self-service portal (
portal/, Node + SQLite, onedocker compose up) issues keys and records usage, so the author knows who is using DexShield. The CLI reads the key fromdexshield.yml(licensing.key), envDEXSHIELD_LICENSE, or~/.dexshield/license, and validates it at startup.- Enforcement modes —
licensing.mode:hard(recommended for distribution; requires a valid key, with an offline-grace cache window so CI/offline builds don't break),soft(registers and reports but doesn't block — for evaluation), oroff. - Privacy — activation sends only the key, a non-reversible machine hash (SHA-256 of hostname+user+arch, truncated), the tool version and the OS name. It never sends the APK, its code, strings or key material.
- Tamper-resistance — DexShield protects itself:
./gradlew :cli:hardenSelfproduces a distribution (cli-<ver>-hardened.zip) in which the activation client (check/resolveKey/validateOnline) is method-virtualized (its logic runs in the embedded VM, not as readable bytecode) and its strings are encrypted, so the check is not trivially strippable. The release workflow ships only this hardened distribution.
- Enforcement modes —
core/ Config parser, ClassPool, Visitor framework, Pipeline, I/O
transforms/ Transformations (NameObfuscator, StringEncryptor, RASP, …) + DexShield facade
rasp/ Runtime protection (Java detectors) + injected RASP
rasp-native/ Native C++/JNI RASP (.so): anti-debug, anti-Frida, signature verification
crypto/ Cryptography / white-box AES
dex/ DEX backend: self-contained parser + analysis + dexlib2 mutation of the final APK
cli/ Command-line tool
gradle-plugin/ Gradle integration (JAR + AGP plugins)
tools/ ReTrace, analyzer
The DEX backend applies its transformations in one pass per classes*.dex, in
order: rename-classes → rename-members → rename-virtual → encrypt-strings →
strip-debug, followed by post-passes for the manifest, resources.arsc and the
native .so. The output APK is unsigned — DexShield does not handle
keystores by design; re-sign with zipalign + apksigner.
# Everything driven by one config file:
dexshield protect-apk --input app.apk --output app-protected.apk --config dexshield.yml --mapping mapping.txt
# Then re-sign with your own key:
zipalign -p -f 4 app-protected.apk aligned.apk
apksigner sign --ks release.jks aligned.apkIndividual transforms are also available as subcommands, e.g.:
dexshield rename-classes -i app.apk -o out.apk -m mapping.txt
dexshield encrypt-strings -i app.apk -o out.apk
dexshield analyze-apk -i app.apk # inventory + obfuscation heuristicproject:
name: myapp
obfuscation:
enabled: true
level: aggressive
names:
classes: true
methods: true
fields: true
components: false # rename Activities/Services + update the manifest
reflectionAware: true # auto-keep classes/members used via Class.forName / JNI (recommended)
keep:
- "com.myapp.api.**"
strings:
enabled: true
resources:
enabled: false # obfuscate resources.arsc names (IDs kept intact)
keep:
- "app_name" # e.g. names accessed via getIdentifier
rasp:
# SHA-256 (hex, no ':') of the expected signing certificate, from
# keytool -list -v -keystore release.jks
signatureSha256: ""
native:
enabled: false # inject the native .so RASP into the APK
injectClass: com.myapp.MainActivity # entry class (call injected into onCreate)
licensing:
mode: soft # off | soft | hard (soft never blocks the build)
endpoint: http://localhost:8080 # your portal (see portal/)
# key: DXS-... # or env DEXSHIELD_LICENSE / ~/.dexshield/licenseCLI flags add to the config (--no-* flags disable), e.g.
--rename-components, --obfuscate-resources, --rasp-native <FQN>,
--rasp-signature <hex>, --no-auto-keep, --keep <pattern>.
dexshield protect --config dexshield.yml --input app.jar --output app-protected.jarDexShield.fromConfig(File("dexshield.yml"))
.protect(File("app.jar"), File("app-protected.jar"))plugins { id("com.dexshield") }
dexshield {
configFile.set(file("dexshield.yml"))
inputJar.set(tasks.named<Jar>("jar").flatMap { it.archiveFile })
outputJar.set(layout.buildDirectory.file("protected/app-protected.jar"))
}
tasks.named("dexshieldProtect") { dependsOn("jar") }There's a runnable example in examples/sample-app (it
uses includeBuild to consume the plugin without publishing it).
For an Android com.android.application module, the com.dexshield.android
plugin hooks AGP's Artifacts API and protects each variant's classes between
compilation and R8/D8. Since R8 already renames in an Android build, in that mode
prefer disabling name obfuscation and keeping string encryption + control-flow.
- Re-signing is opt-in — by default the protected APK is left unsigned. Pass
--sign --ks <keystore>toprotect-apkand DexShield orchestrates the SDK'szipalign+apksigner(found via--build-tools,ANDROID_HOME, orPATH). DexShield never stores key material — the password goes straight toapksignerwith its own scheme (env:/file:/pass:), so it need not appear on the command line. - Reflection keep covers name literals; names built dynamically at
runtime still need an explicit
keep. - Native RASP — anti-debug (
TracerPid) and native signature verification are verified end-to-end on device (a re-signed APK aborts). Both anti-Frida vectors (port 27042 and/proc/self/mapsgadget/agent signatures) are validated on realarmeabi-v7ahardware with the indicators simulated — no false positive at baseline, positive on each vector (seerasp-native/FRIDA_VALIDATION.md). Detection of a live, realfrida-serveris now verified end-to-end on an Android emulator via the port vector (baselinedetect()==2, with a runningfrida-serverdetect()==10= EMULATOR|HOOK). Only the maps/injection vector (gadget in/proc/self/maps) still needs a rooted image, sinceptraceinjection requires root. - Single-DEX re-emit (64K) —
encrypt-natives' loader injection and DEX-level string encryption re-emit into one DEX, so on very large multidex apps they can hit the 64K method-reference limit (Unsigned short value out of range). Split-aware writing is future work; typical agent apps (~20k methods) are well under the limit. - Class encryption (JVM) is not transparent yet — encrypted classes load via
EncryptedClassLoader, which must be installed at app startup; opt-in by pattern, and it must not encrypt the entry point. - Key handling — the DEX string-encryption AES key is not stored in the
DEX: only an 8-byte per-build seed is baked in, and the injected runtime
derives the key at load time (
K = SHA-256(seed ‖ tag)). Sostrings/ an array dump no longer yields the key, and the key differs every build. It still does not stop an attacker who runs the app under a debugger and reads the key atCipher.init— that is the intrinsic limit of any software key handling without secure hardware; full white-box crypto remains future work. (Asset AES still uses an embedded key.)
Requires JDK 17 (Android/AGP) and JDK 21 for the Spring-style modules; the DEX backend and CLI build with JDK 17. Native RASP needs the Android NDK + CMake.
./gradlew buildBehind a corporate MITM TLS proxy,
gradle.propertiesalready sets-Djavax.net.ssl.trustStoreType=WINDOWS-ROOTso Gradle trusts the Windows certificate store.
The library modules (dexshield-core, dexshield-transforms, dexshield-crypto,
dexshield-dex) publish with maven-publish (JAR + sources + javadoc + POM):
./gradlew publishToMavenLocalMaven Central requires OSSRH credentials + GPG signing (provided by the
maintainer, not versioned). The Gradle plugin publishes to the Gradle Plugin
Portal via com.gradle.plugin-publish.
The native RASP ships as a consumable AAR — ./gradlew :rasp-native:raspAar
assembles dexshield-rasp-native-<version>.aar (AndroidManifest + classes.jar +
jni/<abi>/libdexshieldrasp.so) without pulling AGP into the build; an Android
app consumes it with implementation(files("dexshield-rasp-native-<ver>.aar")).
Releases are automated: pushing a vX.Y.Z tag runs
.github/workflows/release.yml, which builds the
CLI distribution, the Gradle-plugin JAR and the native AAR and attaches them to a
GitHub Release.
DexShield is also a reproducible research platform. Two contributions set it apart from a plain DexGuard reimplementation, both with artifacts in this repo:
- Per-build diversification — every build produces a different name scheme
and a different string-encryption key, so analysis of one build does not
transfer to another (defeats pattern/signature-based deobfuscation). A fixed
--seedmakes builds reproducible. Verified on real devices. - LLM-assisted reverse-engineering resistance benchmark — a reproducible
benchmark (
bench/llm-resistance) that measures how well a large language model can reverse-engineer protected vs. plain code. Across a corpus of 8 samples in 8 domains, an assistant's recovery drops from 1.00 (plain) to 0.33 (protected) — resistance ≈ 0.67.
A draft paper is in paper/ —
"DexShield: An Open, Diversified and LLM-Measured Android Application Protection
Platform." The paper is explicit that the individual techniques are not novel;
the contribution is the open, diversified, measurable integration, and its
evaluation is stated as preliminary.
Measured, reproducible numbers — full write-up and charts at garibayivan.github.io/DexShield/benchmarks.html.
Size & speed (bench/perf, 60-class string-dense corpus):
| Metric | Plain | Protected | Δ |
|---|---|---|---|
classes.dex |
67.6 KB | 87.8 KB | +29.9% |
| Sensitive strings in cleartext | 295 | 0 | −100% |
| Protection time | — | 1.29 s | ≈21 ms/class |
Reverse-engineering resistance (bench/llm-resistance,
8 samples — see RESULTS-CORPUS.md):
| Signal (plain → protected) | Result |
|---|---|
| Secret visible in disassembly | 8/8 → 0/8 |
| Intent-carrying labels surviving | 32/32 → 0/32 |
Secret in raw string-pool (strings classes.dex) |
8/8 → 0/8 |
The 8-sample corpus surfaced a real leak — D8 keeps the default value of a
static final Stringin the DEXstatic_valuesarray, recoverable withstringseven when theconst-stringuses were encrypted. It was fixed (initializers are re-assigned encrypted in<clinit>), taking the raw-pool leak from 7/8 to 0/8.
| Phase | Content | Status |
|---|---|---|
| 1 | Name + string obfuscation, CLI, Gradle plugin | ✅ Done |
| 2 | RASP (root/emulator/hook/tamper) + native .so (anti-debug, anti-Frida, signature verification) |
✅ Done, verified on real hardware |
| 3 | Control-flow, class/asset encryption, white-box AES | ✅ Done |
| 4 | DEX backend: analysis + full mutation of the final APK (names, components, resources, strings, strip-debug), config-driven protect-apk, reflection/JNI-aware keep |
✅ Done, verified installing real APKs on real Android devices |
| 5 | Method virtualization: full ISA (int/long/arrays/control-flow/calls/fields/constructors), R8-compatible via bytecode dispatcher | ✅ Done, verified booting on a real Verifone T650p |
| 6 | Polish: opt-in integrated re-signing ✅, native AAR ✅, native root/emulator detectors ✅, plugin SDK ✅, automated releases ✅, live-Frida detection verified on an emulator (real frida-server, port vector) ✅; only the maps/injection vector still needs a rooted image |
✅ Done |
| 7 | Native library encryption: encrypt the app's own .so into assets, strip plaintext, inject NativeLoader + rewrite System.loadLibrary (DEX-level) |
✅ Done, verified end-to-end on a real Verifone T650p (armeabi-v7a) |
DexShield is cooked in Mexico with the same technique as a good mole: many layers, patience, and a very well-kept secret (AES-256 encrypted, of course). We wrap your code with more turns than a Periférico roundabout on a Monday at 8 a.m.
— Hey, why won't this APK decompile? — Oh, it's got DexShield. It's better protected than grandma's salsa recipe. 🌮🔐
If a reverse engineer manages to de-obfuscate this without the mapping.txt,
tacos al pastor are on us. 🎉 (offer void on Tuesdays)
DexShield was created and is maintained by Ivan Garibay — an Android developer and application-security engineer, focused on reverse engineering of SDKs and mobile app hardening.
- 👤 Ivan Garibay · ControlPos
- 🛠️ Focus: Android app hardening, SDK reverse engineering, mobile security
- 📦 Project: DexShield — open-source Android code protection and obfuscation
If you use DexShield in your project, a ⭐ on the repo is appreciated.
Proprietary — © 2026 Ivan Garibay. All rights reserved. Use requires an active license key issued by the author; see LICENSE. Versions published before 0.2.0 under Apache 2.0 remain available under those terms for that prior material only (snapshot). Licensing: garibay.ivan@gmail.com