Reverse Engineering Android Apps for Hardcoded Secrets and Insecure Storage

Reverse Engineering Android Apps for Hardcoded Secrets and Insecure Storage

A hands-on walkthrough of decompiling an APK, hunting for embedded credentials, and finding exported components that leak more than they should.

HackerSavanna Security Team

Security research and platform engineering at HackerSavanna.

4 min read29 views

Every Android APK you can install, you can also open. There's no equivalent of a server you can't reach or source code you'll never see. The app is sitting right there on the device, and with a handful of free tools you can walk through its logic, its strings, and very often its secrets, in an afternoon. This is a practical walkthrough of that process, aimed at finding real, reportable findings rather than just poking around.

Getting the APK

If the app is on the Play Store, pull it directly from a device you control with adb:

bash132 Bytes
1adb shell pm list packages | grep target
2adb shell pm path com.target.app
3adb pull /data/app/~~xxxx==/com.target.app-yyyy==/base.apk

No rooted device needed for this step. If you only have the Play Store listing, tools like apkeep or a browser-based APK extractor work too, as long as you're staying within the scope and rules of the program you're testing against.

Decompiling

Two tools do most of the heavy lifting:

  • jadx decompiles the DEX bytecode back into readable, close-to-original Java. It's the fastest way to actually read the app's logic.
  • apktool decodes resources and the manifest without fully decompiling code, useful for inspecting AndroidManifest.xml, exported components, and raw resource files.
bash61 Bytes
1jadx -d output_dir base.apk
2apktool d base.apk -o decoded_dir

Once decompiled, output_dir is a full Java source tree you can grep, and decoded_dir/res/values/strings.xml plus any raw assets/ folders are worth a manual pass on their own.

Hunting for hardcoded secrets

This is the single highest-value, lowest-effort step, and it's astonishing how often it pays off. Grep the decompiled source and resources for anything that looks like a credential:

bash107 Bytes
1grep -rniE "(api[_-]?key|secret|token|password|aws_access|firebase)" output_dir/ | grep -v "R.string\|R.id"

Common places real secrets end up baked into a shipped APK:

  • Firebase configuration with an overly permissive database or Firestore setup (this alone is worth checking against the app's actual backend rules, not just flagging the key)
  • Third-party API keys for services like Stripe, Mapbox, or push notification providers, sometimes with production-scope permissions that should have been server-side only
  • OAuth client secrets that were never meant to leave a backend
  • Hardcoded encryption keys used for "obfuscating" locally stored data, which defeats the purpose entirely once the key ships in the same package as the ciphertext

A key embedded in client code is not a secret. It's a string that happens to be inconvenient to read. Anyone can extract it the same way you just did.

Checking for insecure local storage

Beyond static strings, install the app on an emulator or test device and inspect what it writes to disk:

bash162 Bytes
1adb shell run-as com.target.app ls -la /data/data/com.target.app/shared_prefs/
2adb shell run-as com.target.app cat /data/data/com.target.app/shared_prefs/auth.xml

SharedPreferences files are plain XML by default. Auth tokens, session identifiers, or even plaintext passwords stored here are a common finding, particularly in apps that implement "remember me" functionality without using Android's EncryptedSharedPreferences or the Keystore system.

Exported components worth checking

AndroidManifest.xml declares which Activities, Services, Broadcast Receivers, and Content Providers are exported, meaning any other app on the device can interact with them.

xml228 Bytes
1<activity android:name=".DeepLinkHandler"
2 android:exported="true">
3 <intent-filter>
4 <action android:name="android.intent.action.VIEW" />
5 <data android:scheme="targetapp" />
6 </intent-filter>
7</activity>

An exported Activity that handles sensitive actions (password reset, payment confirmation, account linking) without validating the calling package or the intent's origin can often be triggered by a malicious app installed on the same device. Content Providers are worth special attention: an exported provider with a broad <path-permission> or none at all can expose a queryable path straight into the app's local database.

Putting it together in a report

A hardcoded secret finding is only as strong as its demonstrated impact. "I found an API key in the APK" is a weak report on its own. "I found an API key in the APK, and used it to read/write data belonging to other users of the production backend" is a critical one. Before submitting:

  1. Confirm the key or token is live and scoped against production, not a defunct staging environment.
  2. Demonstrate the actual capability it grants, with a proof of concept request and response.
  3. Note whether the key is shared across all installs (worse) or per-user (still bad, but different blast radius).
  4. Include the exact file path or code location where you found it, so the team can trace how it got there and rotate it.

Mobile apps get less scrutiny than web endpoints on most programs, purely because static analysis takes a bit more tooling to set up. That gap is exactly where the impactful findings are still sitting.

Share: