Skip to content
garibayivanPublic

About

DexShield - framework open-source de ofuscacion y proteccion de codigo Android (RASP, cifrado AES-256, ofuscacion). Creado por Ivan Garibay (ControlPos).

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Latest commit

 

History

80 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

DexShield

License Kotlin

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.


Documentation

Features

Name obfuscation (complete)

  • ✅ 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> and native methods are kept.
  • ✅ Virtual methods — renamed consistently across the whole override chain, only for hierarchies that are entirely internal to the DEX (with java.lang.Object as 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 .Name values resolved against the package).
  • ✅ 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("…"), native methods) 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.

Encryption & anti-analysis

  • ✅ String encryption (DEX level) — const-string literals in the final DEX are replaced with ciphertext and an auto-injected dexrt.S decrypt 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 LDC literals and the literal fragments inside "a" + x + "b" string concatenations (which javac compiles to invokedynamic StringConcatFactory), rewritten to StringBuilder.
  • ✅ 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 only return V.run("<isa-data>", …) (and with string encryption on, the ISA program itself is ciphertext). The ISA now covers int and long arithmetic / 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 are int/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 (a switch of real invoke/new/get/put) that R8 renames consistently, so virtualization composes with full R8 minification. Works on API 26+ (no const-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 excludes rather than the whole app. try/catch is 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 — invokedynamic to StringConcatFactory (makeConcat/makeConcatWithConstants, i.e. Kotlin/javac string templates and +) is translated to a VM CONCAT op that bakes the constants into a recipe and interleaves the dynamic args. boolean/byte/char/short, instanceof, new T[] and switch (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: other invokedynamic (lambdas), DUP2/SWAP (compound assignment), char string-concat.
  • ✅ Asset encryption — AES-256, with getResourceAsStream rewritten to decrypt transparently at runtime.
  • ✅ Class encryption (opt-in) — bytecode encrypted to a .dxenc resource + an EncryptedClassLoader that decrypts at runtime.
  • ✅ White-box AES-128 — T-box cipher that embeds the key (crypto module), verified against standard AES and the FIPS-197 vector.
  • ✅ Native library encryption — the app's own .so files are encrypted (AES-256, per-build 8-byte seed → K = SHA-256(seed ‖ tag), no plaintext key) into assets/dexshield-nat/<abi>/, the plaintext lib/<abi>/*.so is stripped, and an injected NativeLoader runtime rewrites every System.loadLibrary(name) → NativeLoader.load(name) at the DEX level: it decrypts the matching asset to the app's codeCacheDir and System.loads it (falling back to a normal System.loadLibrary for any lib that was not encrypted, so rewriting all calls is safe). Driven by encrypt-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 .so stripped still executes the native method, proving the decrypt → System.load → JNI-call path on ART.

RASP (runtime application self-protection)

  • ✅ 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 as NativeRasp:
    • anti-debug — TracerPid in /proc/self/status.
    • anti-Frida — scan of /proc/self/maps (frida, gum-js-loop, linjector, …) + probe of the default frida-server port 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 with FLAG_TAMPER.
    • Built with the NDK/CMake for arm64-v8a + armeabi-v7a.

Tooling

  • ✅ Config-driven CLI — a single dexshield.yml + protect-apk drives 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. Plus protect-apk --evidence <file> writes a before/after protection report (with optional --evidence-grep <regex> counting sensitive DEX strings), and verify-apk is a post-build gate (exit != 0 if sensitive patterns remain). protect-apk --keep-from-proguard <proguard-rules.pro> reads the app's -keep class rules 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) and com.dexshield.android (AGP variant hook, between compilation and R8/D8).
  • ✅ Self-contained DEX parser — dependency-free .dex reader for analyze-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 with name / isEnabled(config) / apply(pool, config)) and register it either by listing its FQN under plugins: in dexshield.yml, or by publishing it on the classpath via META-INF/services/com.dexshield.core.pipeline.Transform (auto-discovered with ServiceLoader). Custom transforms run after the built-in phases; each decides whether to run via its own isEnabled.
  • ✅ Unit & integration tests — including real execution of the protected bytecode.

Licensing & activation

  • ✅ License activation — use requires a license key issued by the author. A self-service portal (portal/, Node + SQLite, one docker compose up) issues keys and records usage, so the author knows who is using DexShield. The CLI reads the key from dexshield.yml (licensing.key), env DEXSHIELD_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), or off.
    • 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:hardenSelf produces 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.

Architecture

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.


Quick start

Protect a built APK (DEX backend)

# 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.apk

Individual 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 heuristic

Configuration (dexshield.yml)

project:
  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/license

CLI 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>.

JVM pipeline (JAR / library)

dexshield protect --config dexshield.yml --input app.jar --output app-protected.jar
DexShield.fromConfig(File("dexshield.yml"))
    .protect(File("app.jar"), File("app-protected.jar"))

Gradle plugin

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.


Known limitations

  • Re-signing is opt-in — by default the protected APK is left unsigned. Pass --sign --ks <keystore> to protect-apk and DexShield orchestrates the SDK's zipalign + apksigner (found via --build-tools, ANDROID_HOME, or PATH). DexShield never stores key material — the password goes straight to apksigner with 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/maps gadget/agent signatures) are validated on real armeabi-v7a hardware with the indicators simulated — no false positive at baseline, positive on each vector (see rasp-native/FRIDA_VALIDATION.md). Detection of a live, real frida-server is now verified end-to-end on an Android emulator via the port vector (baseline detect()==2, with a running frida-server detect()==10 = EMULATOR|HOOK). Only the maps/injection vector (gadget in /proc/self/maps) still needs a rooted image, since ptrace injection 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)). So strings / 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 at Cipher.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.)

Build

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 build

Behind a corporate MITM TLS proxy, gradle.properties already sets -Djavax.net.ssl.trustStoreType=WINDOWS-ROOT so Gradle trusts the Windows certificate store.

Publishing

The library modules (dexshield-core, dexshield-transforms, dexshield-crypto, dexshield-dex) publish with maven-publish (JAR + sources + javadoc + POM):

./gradlew publishToMavenLocal

Maven 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.


Research

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 --seed makes 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.

Benchmarks

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 String in the DEX static_values array, recoverable with strings even when the const-string uses were encrypted. It was fixed (initializers are re-assigned encrypted in <clinit>), taking the raw-pool leak from 7/8 to 0/8.

Roadmap

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)

Made in Mexico 🇲🇽, for the world

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)

Author

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.

License

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

About

DexShield - framework open-source de ofuscacion y proteccion de codigo Android (RASP, cifrado AES-256, ofuscacion). Creado por Ivan Garibay (ControlPos).

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages