The APK is the specification
When the source repository is gone, the build machine is gone, and the original team has moved on, the shipped APK is the only complete description of how the product talks to its backend. It contains compiled code, resources, native libraries and a manifest that records the components the app declared.
Some things do not come back. Comments, build scripts, server code and the reasoning behind a decision are not in the artifact. A private key is recoverable only if the build embedded it, and a key held in a hardware keystore is simply not there. Almost everything else can be reconstructed to the level the app itself relied on.
Version history helps when more than one build survives. Comparing two APKs shows which endpoints were added, renamed or dropped, and a diff of the string pools is often faster than reading code. With a single build, the app still describes one point in time completely.
Read the manifest before the code
The manifest lists activities, services, broadcast receivers and content providers, along with the permissions the app requests and any deep links it handles. That list is a feature inventory. An exported service that runs a background sync, or an intent filter for a custom URL scheme, tells you what the app can do and often where to look in the code.
The manifest also points at the network security configuration. That file names which domains the app trusts, whether user-installed certificates are accepted, and whether cleartext traffic is permitted. On an old build it tells you which TLS settings the app will accept, which matters when the backend has been updated since.
Identify the libraries and the build
Package names, native library names and string constants reveal the HTTP stack the app used. Retrofit, OkHttp, Volley and Ktor each leave a distinct footprint, and knowing which one is present tells you where request construction and interceptors live. Native libraries indicate JNI logic, and their file names usually map to the feature that uses them.
Check the minimum and target SDK values, whether the build is debuggable, and whether the APK is split. Those details explain behaviour that looks strange in a modern environment, such as a weak TLS preference or an old JSON library that formats numbers differently.
Decompilation and obfuscation
A decompiler that produces readable Java is the fastest way to understand a class, but it can fail on obfuscated or unusual bytecode. When it does, read the smali instead. It is verbose and exact. For native code, disassemble only the functions you need, starting from the JNI method names that the Java layer calls.
- R8 and ProGuard rename classes and methods but usually keep string constants.
- Kotlin metadata annotations often survive and restore property names.
- String encryption hides literals until they are decrypted at run time.
- Reflection hides the call graph, so a static read alone will miss the flow.
Where static reading stalls, run the app and watch. A request trace beside a decompiled class is usually enough to identify the routine that produced a value.
Resources carry evidence of their own. Layout files name screens and fields, string resources hold labels and error messages, and asset files may contain configuration or certificates. Read them beside the code, because a resource name is not obfuscated the way a class name is.
Dead backends and drifted behaviour
Legacy apps fail in ways a current app does not. An endpoint may have moved, a certificate may have expired, or the server may reject the TLS version the app offers. Test each endpoint on its own before assuming the app logic is wrong, and compare the response with what the code expects to parse.
When the backend is gone, the app still documents the API specification. Field names, error codes and request order are recoverable from the client even when no server answers, and a reimplementation can be built against that API specification. The result is a working interface to a service that has no live description anywhere else.
A server that still answers may answer differently than it did. Response fields can be added without breaking an old client, and an old client ignores what it does not read. Record which fields the app actually consumes rather than everything the response contains, so a reimplementation matches the app's behaviour.
Related work
Reviewed 28 September 2026 · SReverse research desk