Step 223. A First Taste of Android APK Analysis — jadx
Level 3 — Advanced Reversing | Difficulty ★★★☆☆ | Estimated time: 3 hours
Prerequisites: Step 222 (.NET/Python Binary Reversing). Having seen zip files and XML before is enough.
⚠️ All exercises in this chapter are for your own lab and legal platforms only. Applying them to unauthorized systems is a crime. Decompiling someone else’s app to extract keys, or tampering and redistributing it, is illegal — practice only with dedicated sample apps (vulnerable apps like InsecureBankv2, CTF challenge APKs).
- What you need: Python 3 (measured: WSL python3 3.12.3, the
zipfilestandard module). jadx is not installed in this environment and is shown as screen examples. - Caution: verify the source when handling APKs. APKs from outside real app markets can be malware, and running them (installing on an emulator) and analyzing them (decompiling) carry different risk levels.
The last first-taste of reversing is mobile. An Android app’s distribution file, the APK, is really a zip, and the code inside (classes.dex) is Java bytecode — an extension of the "world that reads well" you learned in Step 222. Today you unpack an APK’s structure yourself (hands-on) and learn what to look for with jadx, mobile reversing’s standard tool (screen examples). One goal: the first step toward verifying for yourself, "can this app be trusted?"
1. Learning Objectives
By the end of this chapter, you will be able to:
- Explain an APK’s internal structure (AndroidManifest.xml, classes.dex, resources)
- Measure hands-on that an APK is a zip by unpacking it yourself
- Read permissions and exported components from AndroidManifest.xml
- Know the procedure for finding hardcoded keys in jadx-decompiled Java code
- Know the fallback when Java code won’t read — that the smali view exists
2. Background Knowledge — Today’s Tools and Concepts
Today’s Tools at a Glance
| Category | Details |
|---|---|
| Language/environment | Python 3 zipfile (measured: WSL python3 3.12.3) — unpacking APK structure |
| Today’s commands | zipfile.ZipFile(...).namelist(), file, jadx (screen example) |
| Concepts needed | APK = zip, DEX and smali, the manifest (permissions, exported), decompilation |
| Today’s deliverables | An APK structure measurement record + a "where to look first when analyzing" checklist |
2-1. APK = zip — Peeling the Wrapper First
An APK (Android Package) is a standard zip archive with only a special extension. Inside lives a fixed set of residents:
| File/folder | Identity |
|---|---|
AndroidManifest.xml |
The app’s ID card — package name, permissions, component declarations (in binary XML format, though) |
classes.dex |
The app’s body — Java/Kotlin code compiled into DEX bytecode |
resources.arsc |
Compiled resource table of strings, colors, etc. |
res/ |
Raw resources like layouts and images |
META-INF/ |
Signature info — "who made it and that it hasn’t been tampered with" |
One honest caveat: a real APK’s AndroidManifest.xml is encoded not as text but as binary XML, so even unzipped, you can’t read it as-is. Today’s step-3 measurement uses a mock-up for structure understanding (text XML); real manifests are decoded and shown by jadx.
2-2. DEX and smali — Android’s Bytecode
An Android app’s code compiles not into JVM bytecode (.class) but into the DEX (Dalvik Executable) format. It’s the bytecode ART (the Android Runtime) interprets and executes.
There are two ways for a human to read DEX. Go up and you get decompilation to Java source (jadx); go down and you get smali — a text representation corresponding to DEX’s assembly. It’s the same relationship as Step 222’s "when decompilation breaks, read the bytecode directly": when jadx’s Java breaks from obfuscation or the like, smali is the last line of defense.
2-3. jadx — Mobile Reversing’s First Tool
jadx is an open-source tool that decompiles DEX into Java source (both GUI and CLI). Open a whole APK and you can browse the manifest, resources, and code on one screen.
What an analyst looks for in jadx is fixed: ① hardcoded secrets (API keys, passwords, encryption keys), ② authentication logic (local verification or server verification), ③ hidden endpoints (test URLs, debug features), ④ dangerous permissions and exposed components. Both mobile CTFs and real-world app reviews start from these four.
2-4. Reading the Manifest — Permissions and exported
Two things to read in AndroidManifest.xml.
- Permissions: the
<uses-permission>list. Whether sensitive permissions like INTERNET, READ_SMS, ACCESS_FINE_LOCATION match the app’s described functionality — a flashlight app asking for contacts permission is suspicious. - exported components: activities and services declared with
android:exported="true"can be launched directly by other apps. If an internal screen that shouldn’t pass through login is exported, it becomes the seed of an authentication bypass.
3. Follow Along
3-1. The Lab — Building a Mock APK
Since this environment has no real APK, build a zip with the same structure in Python and measure that "APK = zip." The only differences from a real APK are ① the file contents are dummies and ② the manifest is text XML (see 2-1); the packaging structure is identical.
Input (make_fake_apk.py)
import zipfile
files = {
"AndroidManifest.xml": (
'<manifest package="com.example.vault">\n'
' <uses-permission android:name="android.permission.INTERNET"/>\n'
' <application>\n'
' <activity android:name=".LoginActivity" android:exported="true"/>\n'
' </application>\n'
'</manifest>\n'
),
"classes.dex": "dex\n035\x00...(dummy bytecode)...",
"resources.arsc": "(dummy resource table)",
"res/layout/main.xml": "<LinearLayout><!-- dummy --></LinearLayout>",
"META-INF/MANIFEST.MF": "Manifest-Version: 1.0\n",
}
with zipfile.ZipFile("vault_fake.apk", "w", zipfile.ZIP_DEFLATED) as z:
for name, data in files.items():
z.writestr(name, data)
print("creation complete")
cd ~/lab219_223
python3 make_fake_apk.py
ls -l vault_fake.apk
file vault_fake.apk
creation complete
-rw-r--r-- 1 root root 844 Sep 9 18:42 vault_fake.apk
vault_fake.apk: Android package (APK), with AndroidManifest.xml
(Measured 2026-09-09.)
How to read the output: an amazing moment — the file command recognized this file as an "Android package (APK)." It means the identification is made from the contents’ composition alone (AndroidManifest.xml + classes.dex + resource structure), and conversely, it’s measured evidence that an APK’s identity is "a zip with a promised structure."
3-2. Scanning the Listing — Draw the Map First
Since it’s a zip, the listing is free to view:
import zipfile
z = zipfile.ZipFile("vault_fake.apk")
for i in z.infolist():
print(f"{i.file_size:>6} {i.filename}")
218 AndroidManifest.xml
38 classes.dex
28 resources.arsc
44 res/layout/main.xml
22 META-INF/MANIFEST.MF
(Measured 2026-09-09.)
How to read it: all the residents from 2-1’s table are visible. This is exactly the first move of real analysis — are there multiple dex files (if classes2.dex exists, it’s multidex — a big app), are there native libraries (.so files in lib/), are there suspicious assets? Draw the map first, then enter the code.
3-3. Extracting the Manifest — Reading Permissions and exported
z = zipfile.ZipFile("vault_fake.apk")
print(z.read("AndroidManifest.xml").decode())
<manifest package="com.example.vault">
<uses-permission android:name="android.permission.INTERNET"/>
<application>
<activity android:name=".LoginActivity" android:exported="true"/>
</application>
</manifest>
(Measured 2026-09-09.)
How to read it: read through 2-4’s two lenses. Permissions: just one, INTERNET — plain for a login app. What deserves attention is LoginActivity‘s android:exported="true" — a declaration that other apps (or adb) can launch this screen directly. What if the "post"-login screen were exported? A bypass that skips login and directly wakes an internal screen becomes possible. In real vulnerable-app practice (InsecureBankv2), you’ll find exactly this pattern.
Note: to repeat, a real APK’s manifest is binary XML, so this method would show broken bytes. On real files you read the result decoded by jadx (or apktool) — knowing what the tool does for you is the purpose of today’s measurement.
3-4. Opening with jadx — Screen Example
Since this environment has no jadx (and we won’t install it), we show it as a screen example. If you want to try it yourself, get it from the jadx GitHub releases (Java required). Standard practice APKs are the publicly released vulnerability-teaching apps InsecureBankv2, DIVA, and CTF challenge APKs.
# Screen example — jadx-gui usage flow
1. Run jadx-gui → open the APK file
2. Left tree:
- Resources > AndroidManifest.xml ← displayed as decoded text
- Source code > com.example.vault > LoginActivity ← decompiled to Java
3. LoginActivity.java (example of decompilation output):
public void onCreate(Bundle bundle) {
...
this.et_password = findViewById(R.id.password);
}
public void login(View v) {
String u = this.et_username.getText().toString();
String p = this.et_password.getText().toString();
if (u.equals("admin") && p.equals("s3cur3P@ss")) { // ← hardcoded!
startActivity(new Intent(this, HomeActivity.class));
}
}
4. Search for "password", "api_key", "secret" via menu Navigation > Text search
How to read it: same as Step 222’s dnSpy experience — it’s bytecode, so names and structure survive and it reads at near-original level. When credentials are baked in as a plaintext comparison like the example’s login(), that itself is a finding. Sweeping patterns like "http://" and "BEGIN PRIVATE KEY" with the search feature is also textbook.
3-5. A Taste of smali — The Fallback When Java Breaks
For obfuscated apps (names broken into a.b.c) or stretches where decompilation looks off, drop down to jadx’s smali view:
# Screen example — smali (DEX assembly)
.method public login(Landroid/view/View;)V
.locals 3
...
const-string v1, "admin"
invoke-virtual {p0, v1}, Ljava/lang/String;.equals(Ljava/lang/Object;)Z
move-result v2
if-eqz v2, :cond_0 # jump to cond_0 if not equal
...
.end method
How to read it: verbose, but the structure is the same as assembly — constant loads (const-string), calls (invoke-*), branches (if-*). And look: even in smali, "admin" is plaintext. A hardcoded secret is plaintext no matter which layer you view it from — which is why string search always works even when decompilation breaks.
3-6. Analysis Checklist — The Real-World Order
| Order | What to look at | Tool | What you’re looking for |
|---|---|---|---|
| 1 | File listing | zip | dex count, .so files, suspicious assets |
| 2 | Manifest | jadx (decoded) | Sensitive permissions, exported components |
| 3 | String search | jadx text search | password, api_key, http://, BEGIN PRIVATE KEY |
| 4 | Auth logic | jadx Java | Local vs server verification, bypass potential |
| 5 | Broken stretches | jadx smali view | Direct reading where decompilation failed |
4. Missions & Exercises
Mission — Writing a "Dangerous Manifest" Assessment
- Modify 3-1’s script to build a mock APK that deliberately includes the elements below:
- Permissions: INTERNET, READ_SMS, READ_CONTACTS
PostLoginActivity(the post-login screen) declared asandroid:exported="true"- A
config.txtin assets/ (contents:api_key = FAKE-KEY-12345)
- Reopen the APK you made from an analyst’s viewpoint — scan the listing (3-2), extract the manifest (3-3), read assets
- Write an assessment: the 3 dangers found, in the format "what it is / why it’s dangerous / how you found it"
- Final paragraph: attach a mapping table of where in jadx you’d find the same elements
Exercises
Exercise 1. Explain the advantage the fact "an APK is just a zip" gives analysis, based on the procedures of 3-2 (listing) and 3-3 (extraction).
Exercise 2. Why is a component with android:exported="true" a focus of security analysis?
Exercise 3. Unzip a real APK’s AndroidManifest.xml and open it in a text editor, and it looks broken. Give the reason, and what jadx does for you instead.
Exercise 4. Name two analysis footholds that remain valid even in an app whose class names are broken into a.b.c by obfuscation.
5. Model Answers & Completion Criteria
Mission Model Answer
[Build] additions to the files dictionary:
2 permissions added as <uses-permission> lines
"assets/config.txt": "api_key = FAKE-KEY-12345"
manifest replaced with one declaring PostLoginActivity as exported="true"
[Assessment — 3 findings]
1. READ_SMS / READ_CONTACTS permissions — excessive permissions that don't match
the "vault app" feature description. Found: the manifest's <uses-permission> list.
2. PostLoginActivity exported — an internal screen can be woken from outside
without passing login, becoming an authentication-bypass path.
Found: the exported="true" declaration.
3. Hardcoded API key in assets/config.txt — readable by anyone who unzips the apk.
Found: suspicious asset in the zip listing → z.read("assets/config.txt").
[jadx mapping] permissions/exported → Resources > AndroidManifest.xml,
API key → Navigation > Text search "api_key",
code → class decompile view in the Source code tree.
How to verify: ① does each finding in the assessment specify "in which file/line," ② is each danger explained via contrast with the feature description (excessive permissions) or as an attack path (auth bypass, key leak), ③ does the jadx mapping match 3-6’s checklist? The honesty of not pretending to have analyzed with a tool a file you made without one — stating that it’s a mock-up is also a grading item.
Exercise Answers
Answer 1. The advantage is that standard zip tools let you look inside with no installation or execution. As in 3-2, namelist() gives you the file composition (the map) for free, and as in 3-3, individual files extract directly with read(). Since it’s not an exotic format, the cost of first recon is near zero, and the real analysis order (listing → manifest → code) becomes exactly the zip-browsing order.
Answer 2. Because an exported component can be launched directly from outside the app (other apps, adb). If a screen the developer intended for "inside the app only" is declared exported, an authentication bypass can work by waking that screen directly, skipping the normal path like login. That’s why exported declarations are the first item to pick up in manifest analysis.
Answer 3. Because a real APK’s manifest is encoded not as text XML but in Android’s own binary XML format — the string table and tag structure are packed into a binary format, so a text editor shows it broken. jadx (and apktool) decodes this binary XML and shows it restored as human-readable text XML. That today’s 3-3 measurement read as text is because it’s a mock file — and that is the substance of "what the tool does for you."
Answer 4. First, strings — names may be broken, but hardcoded passwords, URLs, and API keys remain in plaintext (confirmed in 3-5 that even smali keeps them plaintext). Second, resources and the manifest — obfuscation usually targets only code names, so layouts, string resources, and permission declarations still read. "When names are erased, read strings and structure" is the first instinct of obfuscation response.
Completion Criteria Checklist
- [ ] I can name an APK’s five components (manifest, dex, arsc, res, META-INF)
- [ ] I built a mock APK and measured
filerecognizing it as an APK - [ ] I extracted and read the listing and manifest with zip tools
- [ ] I can explain why we look at permissions and exported declarations
- [ ] I know that real manifests are binary XML and what jadx’s role is
- [ ] I know jadx’s screen layout (tree, decompile view, text search) and procedure
- [ ] Mission: I built a mock APK containing 3 danger elements and wrote an assessment
6. Common Pitfalls & Fixes
Wall 1. I unzipped a real APK’s manifest and got broken characters
Symptom: unzip -p app.apk AndroidManifest.xml prints binary garbage.
Cause: normal — a real manifest is binary XML (2-1). Only today’s mock file was text.
Fix: decode it with jadx or apktool. "Unzips with zip" and "reads as text" are separate problems — that’s today’s dividing line.
Wall 2. jadx’s Java code is full of a.b.c
Symptom: every class and method name is a single letter.
Cause: obfuscation (ProGuard/R8, etc.). The developer deliberately erased the names.
Fix: don’t read by name — read by strings and behavior: the code referencing the "password" string is the login logic. Starting from jadx’s text search and climbing back up the references is the textbook move. Full-scale response is Step 224’s topic.
Wall 3. The decompiled Java looks wrong (empty methods, strange branches)
Symptom: a function clearly does something, but the Java result looks empty.
Cause: decompilation is reconstruction, not the original. An optimized DEX can reconstruct awkwardly.
Fix: drop to the smali view to check, as in 3-5. smali is a 1:1 transcription of DEX, so it doesn’t lie. When the Java is suspect, smali is the judge.
Wall 4. Mid-analysis, you want to run the app
Symptom: the temptation of "shall I just install it on an emulator and run it."
Cause: the pull of dynamic analysis is natural, but running an APK of unknown origin is a different risk level.
Fix: don’t run it until you’ve made your judgment with static analysis (jadx). When running becomes necessary, use a dedicated emulator or analysis device with the network cut. Runtime observation (Frida, etc.) is the next stage’s territory.
Wall 5. You’re unsure where to get "practice apps"
Symptom: you don’t know which APK to practice on.
Cause: grabbing any app raises legal and safety issues.
Fix: pick apps publicly released for vulnerability education — InsecureBankv2, DIVA (Damn Insecure Vulnerable App), and mobile CTF challenge APKs are the standard textbooks. The very habit of checking "is this a problem made to be solved" is part of professional ethics.
7. Summary
Today’s Concepts
| Concept | One-line explanation |
|---|---|
| APK | An Android app’s distribution file — really a zip with a promised structure |
| AndroidManifest.xml | The app’s ID card — permission and component declarations (actually binary XML) |
| classes.dex | The app code’s body — DEX bytecode executed by ART |
| jadx | A DEX → Java decompiler — integrated view of manifest, resources, code |
| smali | DEX’s assembly representation — the last line of defense when Java breaks |
| exported component | A screen/service launchable from outside — a clue to authentication bypass |
| Hardcoded secret | Keys and passwords baked into code — plaintext from any layer |
Today’s Commands & Tools
| Command/tool | What it does |
|---|---|
zipfile.ZipFile(apk).namelist() |
APK file listing — drawing the analysis map |
z.read("AndroidManifest.xml") |
Extract the manifest (measured on the mock file) |
file app.apk |
Confirm APK recognition — measured that it’s identified by structure |
| jadx-gui | Manifest decoding + DEX Java decompilation + text search |
| jadx smali view | 1:1 reading of stretches where decompilation broke |
| Text search ("password", "api_key") | Finding hardcoded secrets — valid even when names are broken |
An Instinct More Important Than Commands
Mobile reversing is, in the end, the same grammar. Peel the wrapper (zip), draw the map (listing), read the ID card (manifest), then enter the code (jadx). And today’s conclusion: no matter which layer you view from — Java or smali — a hardcoded secret is plaintext.
From Step 178 to 223, the reversing track’s map is complete: machine code (crackmes) → interference (anti-debugging) → wrapping (packing) → proof of understanding (keygen) → worlds that read well (.NET/Python) → mobile (APK). The tools grew stronger along the way, but what you built isn’t tools — it’s order and instinct: identification first, lightest tool first, and when the file stays silent, ask the running program.
Once every box is checked, Step 223 is complete. Click the checkbox in the sidebar to save your progress.