SReverseby Simpa Labs

Runtime code loading

Understanding runtime-loaded DEX during APK analysis

Some apps keep a loader in the packaged dex and place the real code somewhere else. The classes that matter arrive after the process starts, and the file on disk never contains them.

The dex you decompile is not always the dex that runs

A protected app can keep a small loader in the packaged dex and hide the application behind it: an encrypted asset, a file written to internal storage on first launch, or a payload fetched from a server and held in memory. A decompiler reading the package on disk sees the loader and misses the logic behind it. The static view is not incomplete because the tool failed. The code genuinely is not there yet.

How the loading works

Android provides class loaders that accept a dex file or a byte buffer. One takes a path to a dex or jar together with a directory for optimized output, a native library search path and a parent loader. Another takes a buffer and builds the classes from memory, so nothing has to reach the filesystem. Both hand the resulting classes to the application, which then reaches them through reflection or an interface the loader publishes.

The parent loader matters more than it first appears. Class resolution is delegated upward, so a class name present in both the packaged dex and the loaded dex usually resolves to the packaged copy. A class loaded from the runtime dex also carries a different loader identity, which breaks equality checks and casts across the boundary. When a reflection call fails with a class-not-found error or a type mismatch on a class you can see in the dump, the loader that owns it is usually the explanation.

Where the payload comes from

The source of the dex shapes where you look. A payload in assets is decrypted by app code or by a native routine that is called once during startup, and the read of that asset is the moment to watch. A payload fetched from the network appears after a request the app makes itself, so the load will not finish offline and the fetch shows up in the app's traffic when you can see it. A payload written to internal storage on first run leaves a file under the app's private directory, which is the simplest case because a plaintext dex exists on disk after one launch.

Each route gives a different first observation. For an asset, hook the read that returns the encrypted bytes. For a network payload, capture the fetch and the key material that accompanies it. For a cached file, copy it out of the app's data directory from a debuggable or rooted environment and open it with the same tools you would use on a packaged dex.

Finding the handoff

Watch the loader rather than the payload. Every runtime-loaded dex passes through a small number of entry points, and those survive obfuscation because the platform API names them.

  • Construction of a class loader that carries a dex path or a buffer.
  • Loads of a dex from a byte array or a mapped region.
  • Reads of the asset or file that holds the encrypted payload.
  • Native calls that return decrypted bytes to Java.
  • New read-only or executable mappings of dex-shaped memory after startup.

A DEX file begins with a recognisable magic value that survives decryption, so it appears in memory even when nothing hits the disk. Watching for that value finds the moment the payload becomes usable. From there you either dump the dex or enumerate the classes through the loader that received it, and the second option keeps any per-class metadata that a raw dump can lose.

Reading the loaded code

Once the classes exist, ordinary analysis resumes. Strings are the fastest entry point. Hostnames, HTTP client annotations, JSON field names and header constants are usually intact in the decrypted dex, and they point straight at the request path. If the loaded code builds requests through an interceptor chain, log the outgoing request after the last interceptor so you see the final bytes rather than an intermediate object.

Timing decides most of the practical questions. Some apps load the payload on a background thread after the first screen, and some re-encrypt or discard state once initialisation finishes. Attach before the target action and give the process time to settle. Where the app reacts to instrumentation, compare a hooked run against an uninstrumented one and treat any difference in the request as part of the protection rather than part of the protocol.

What this means for reconstruction

Runtime loading raises the cost of reading code. It does not change the API specification the server enforces. A rebuild reproduces the requests the loaded classes produce, and the loader stops mattering once those requests are understood. Where a class loader boundary hides a value, the value is captured at the network boundary rather than re-derived from the loader. The native side of Android analysis covers the case where the decryption routine itself lives in a compiled library rather than in Java.

Reviewed 28 September 2026 · SReverse research desk

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?