SReverseby Simpa Labs

Application Protection

Understanding runtime-loaded DEX

Not all dex ships in the APK. Some is loaded at runtime, and that is where the real code lives.

Read the static load, not the loaded code

A runtime-loader APK ships a small stub and keeps the useful DEX encrypted, compressed or inside an asset. Static decompilation shows the stub because nothing has unpacked the real code yet. The analysis point is after the process loads the DEX, so the first task is to find the loader and where it writes.

apktool d app.apk -o out
grep -rhoE 'Ldalvik/system/(Dex|InMemoryDex|Path)ClassLoader;' out/smali* | sort -u
grep -rE 'getDir|filesDir|code_cache|openFileOutput|writeTo' out/smali* | head

Static evidence tells you the loader class and the directory the runtime writes to. It does not contain the loaded code.

Dump the DEX that is actually running

The most direct route is to dump the loaded DEX from the live process. frida-dexdump walks the ART class linker and writes each loaded dex file to disk; then open the result with jadx as a normal app.

pip install frida-dexdump
frida-dexdump -U -f com.example.app   # writes com.example.app_*.dex nearby
jadx com.example.app_*.dex

If dumping is blocked, hook the loader itself. BaseDexClassLoader receives the path or the bytes in its constructor. Log the path, then pull the file, or read the bytes in memory. Hook DexClassLoader and InMemoryDexClassLoader separately, because the in-memory case has no file on disk to pull.

Java.perform(function () {
  Java.use('dalvik.system.DexClassLoader').$init.overload('java.lang.String', 'java.lang.String', 'java.lang.String', 'java.lang.ClassLoader').implementation =
    function (dexPath, optDir, libPath, parent) {
      console.log('dex load', dexPath);
      return this.$init(dexPath, optDir, libPath, parent);
    };
});

Find where the real class starts

The loader unpacks, then hands control to the original Application class. Find that class and hook it: log which application class the process actually instantiates, then hook its onCreate to confirm the point of control. That is where normal code and network analysis can begin.

Java.perform(function () {
  Java.enumerateLoadedClasses({
    onMatch: function (name) {
      if (name.indexOf('Application') !== -1) console.log(name);
    },
    onComplete: function () {}
  });
});

Work the dump end to end

Put the steps in order and stop guessing. Decode the package and find the loader. Run the app so the loader runs. Dump the loaded DEX from the live process. Decompile the dump. Find the real Application and hook it. Then treat the code you now have as a normal Android app and trace the request layer.

A dump often returns more than one dex file, because multidex builds split a class library across several files and runtime loaders can add more. Open every dex the dump writes; the class you need may be in the one you skipped. When you want a single merged view, reassemble with baksmali or open each file in jadx separately and search across all of them.

Confirm you are looking at the running code rather than the stub before drawing conclusions. A method that only appears in the dump is the runtime-loaded part, and that is where the request logic usually lives.

Protection keeps running after the dump

Unpacking code does not end the job. Integrity checks, debugger checks and hook detection can remain active around the code you now have, and string decryption may only happen when a method runs. A request signature may still sit inside a virtualized native routine. Treat each control as part of the execution path rather than one giant protection problem.

Choose the right route by symptom

SymptomLikely causeRoute
Static dex shows only a stubCode is loaded at runtime.Dump the live dex, then run normal code analysis.
App exits right after launchThe unpacker failed an integrity check.Verify the build is untouched and the unpack path ran fully.
App runs, no request leavesThe request builder never reached transport.Trace the request layer after the gate.
Server rejects a requestRequest generation, not the protection.Reproduce the signing, session and nonce the app builds.

Once you are past the dump, the request layer is the target again. Integrity, debugger and hook checks around a request can still change the bytes you capture, so record which checks run before each call and compare the output when you change an input. A check that runs only once can look like a one-off mystery until you trigger the same action twice.

Keep the original APK and its hash with every finding, and note the device, ABI and install source for each run. A missing split or a different install route can change startup before any API request occurs, so a failure in one environment is not proof the request logic is wrong.

Reviewed 30 August 2026 · SReverse research desk

Related

Start a project

Send the full APK

Send the full APK. We review the application and quote its complete API reconstruction.

Projects start at $120. Most are delivered in 24 to 72 hours.

Start your full APK reconstruction

Send the full APK

Projects start at $120. Choose WhatsApp or email, then attach the APK in the app that opens. We reply within one hour with the next step and send the fixed quote after review.

Want us to contact you?