Module 3 — Static Analysis

This module teaches you how to inspect an Android APK without running it: how to extract meaningful artifacts, read decompiled code, identify where security-relevant logic lives (attestation, pinning, root checks, crypto), analyze native libraries, recognize obfuscation/anti-tamper, and produce an evidence-backed static-analysis report that drives safe follow-ups in dynamic testing or vendor validation. All commands and examples below are intended for authorized lab work only on test APKs or apps inside your written scope.

3.0 Learning objectives

After this module you will be able to:

  • Unpack and decompile an APK into readable artifacts (Manifest, resources, Java/Kotlin pseudo-code, native libs).
  • Locate security-critical code paths (auth, crypto, root checks, attestation, certificate pinning).
  • Identify and characterize native libraries (.so) and determine whether they contain symbols or look stripped.
  • Detect obfuscation / string-encryption / tamper-resistance patterns and estimate effort to reverse.
  • Produce a prioritized static-analysis finding list (file, line, justification, severity, next-action).

3.1 Prepare: tools you will use (lab)

Install/use these tools in your lab environment:

  • apktool — decode resources & manifest, rebuild.
  • jadx / jadx-gui — DEX → Java/Kotlin decompilation (interactive is very useful).
  • dex2jar + jd-gui — alternative Java decompilation pipeline.
  • aapt / aapt2 — quick APK metadata.
  • apksigner / keytool / openssl — inspect signing certs.
  • strings, grep, xmllint, jq — text extraction & lightweight parsing.
  • readelf, nm, objdump, file — native ELF inspection.
  • Ghidra, radare2/Cutter, or IDA — deep native reverse engineering (GUI + analysis).
  • MobSF — optional automated/static triage for large numbers of APKs.
  • python / small scripts — automate repetitive checks and produce JSON output.

3.2 Unpack & baseline inspection (commands + what to record)

Start by creating a working directory for the APK and recording checksums.

mkdir -p ~/lab/static_analysis/appA
cd ~/lab/static_analysis/appA
cp /path/to/app.apk .
sha256sum app.apk > app.apk.sha256

Quick metadata & manifest:

# Metadata
aapt dump badging app.apk > metadata.txt

# Signing certificates
apksigner verify --print-certs app.apk > apksigner.txt

# List files inside APK
unzip -l app.apk > filelist.txt

# Decode resources & manifest for readable XML
apktool d app.apk -o app_unpacked

Record (in your notes or JSON manifest):

  • package name, versionName, versionCode (from metadata.txt)
  • targetSdkVersion and minSdkVersion
  • Signing cert SHA-1 / SHA-256 (from apksigner.txt) — important for attestation contexts
  • Presence of lib/, assets/, number of .dex files, resources.arsc size, AndroidManifest.xml flags (allowBackup, exported components, sharedUserId)

3.3 Manifest analysis — what to inspect and why

Open app_unpacked/AndroidManifest.xml and look for:

  • android:exported="true" components — list them and note whether they require a permission. Exported components are high-value in static testing.
  • <provider> entries — note authorities and whether android:grantUriPermissions is set. ContentProviders are a frequent data-leak source.
  • android:allowBackup="true" — combined with sensitive data storage it becomes a clear risk.
  • uses-permission list — flag dangerous permissions (SMS, CONTACTS, STORAGE) and permissions that look excessive.
  • Signature-related restrictions (e.g., protectionLevel="signature" or custom permission usage).

Record line and file references for each finding so you can point to evidence in the report.

3.4 Searching for endpoints, secrets and suspicious strings (commands + heuristics)

Search both the raw APK and the unpacked resources. Use broad patterns and then refine.

# quick string search inside APK (fast)
strings app.apk | egrep -i "http|https|://|api|endpoint|token|key|secret|client|bearer" | sort -u > apk_strings.txt

# more precise search in unpacked resources
grep -R --line-number -iE "http[s]?://|api|token|key|secret|client|bearer|password|username" app_unpacked/ > resource_hits.txt

# search for crypto / keystore usage
grep -R --line-number -iE "KeyStore|KeyPairGenerator|KeyGenParameterSpec|AndroidKeyStore|Cipher|SecretKey|MessageDigest|Signature" app_unpacked/ > crypto_hits.txt

Heuristics:

  • Full URLs or base URLs in strings → record hostnames and endpoints.
  • Short-looking hex strings may be keys — validate where they are used (callsites).
  • Look inside assets/ and res/values/strings.xml for config files or API keys.

Always trace each suspicious string back to where it is used in code (classes.dex / decompiled code) — context matters.

3.5 Decompile DEX to readable Java/Kotlin (jadx + navigation tips)

Open the APK in jadx-gui (jadx-gui app.apk) for interactive inspection. If you prefer CLI:

jadx -d jadx_out app.apk

When reading decompiled code:

  • Search for entry points: Activities that start at login, classes that call network clients (OkHttpClient, HttpUrlConnection, Retrofit) and crypto APIs.
  • Locate authentication flows: look for methods that build requests for /auth, create JWTs, or handle refresh tokens.
  • Find pinning and TLS logic: search for CertificatePinner, X509TrustManager, HostnameVerifier, TrustManager, SSLSocketFactory.
  • Find attestation logic: search for PlayIntegrityManager, SafetyNet, attest, nonce, KeyAttestation or AndroidKeyStore.
  • Find root checks: search for common root-detection artifacts (calls that exec su, checks for su binaries, mount points, presence of magisk/magiskhide, reading /proc values). Also search for library-based checks references (strings containing rootbeer).

When you find a candidate method, inspect its callers and call graph — who uses the result of that check? Does only the client read it, or is something sent to server?

3.6 Identify certificate pinning & attestation implementation (what to look for)

Pinning indicators:

  • CertificatePinner or certificatePinner (OkHttp).
  • Custom X509TrustManager implementations or HostnameVerifier overrides.
  • Code that loads pinned certs from raw/ resources or assets/ (e.g., CertificateFactory.getInstance("X.509") + reading R.raw resource).
  • Calls to TrustManagerFactory with custom keystore from app resources.

Attestation indicators:

  • Calls to PlayIntegrityManager API, SafetyNet client, or Key Attestation code paths.
  • Generation of an attestation nonce and subsequent network call that sends a token to a backend endpoint.
  • Use of KeyStore.getInstance("AndroidKeyStore") and creation of keys with setIsStrongBoxBacked() or KeyGenParameterSpec.Builder — indicates hardware-backed attempts.

Important: presence of client-side attestation calls is only an indicator—strong security requires the token to be validated on the backend. Flag the code and suggest verifying server-side token validation (nonce, signature verification, certificate chain, timestamps).

3.7 Find local integrity checks & tamper-detection

Search code for common integrity check patterns:

  • Calls to getPackageManager().getPackageInfo(...).signatures or PackageManager signature checks.
  • Calls to compute hashes of classes.dex, resources.arsc, or loaded native libraries via MessageDigest (SHA-256/SHA1).
  • Checks reading /proc/self/exe, Runtime.getRuntime().exec("ls /system/xbin/su"), new File("/system/app/Superuser.apk").exists(), etc.
  • Checks against Build.TAGS (e.g., test-keys, dev-keys) or ro.build.tags system property.

If checks exist, inspect whether they are used locally, or whether the app reports results to server. Client-only checks are easy to bypass; flag them and recommend server-side validation.

3.8 Detect obfuscation & string-protection patterns

Obfuscation cues:

  • Many short class/method names like a.a.b or a$1, or classes named R$1, aa, ab → likely ProGuard/R8 obfuscation.
  • Lack of meaningful string literals (strings are randomized or encrypted).
  • Presence of string decryption routines (a small method that maps bytes through XOR/shift + uses it everywhere to restore strings).

To identify string encryption:

  • Search for repeated small methods that take an int[] or byte[] and return String — these often are decryptor stubs.
  • Search for large base64 or hex blobs in res/raw or assets.

Example grep to find possible decryptor methods:

grep -R --line-number -iE "decode|decrypt|xor|obfuscate|obf|deobf" app_unpacked/ | head

If string-encryption is present, note the decryptor location and the calling sites — those call-sites are high-value for further analysis.

3.9 Native library analysis (.so) — quick triage and next steps

List .so files:

unzip -l app.apk | grep '\.so' > native_list.txt

Extract a sample and inspect:

unzip -p app.apk lib/arm64-v8a/libexample.so > libexample.so
file libexample.so
readelf -h libexample.so
readelf -s libexample.so | head -n 40
nm -D libexample.so | head
strings libexample.so | egrep -i "JNI_OnLoad|Java_|AES|Cipher|RSA|attest|key|keystore|play" | sort -u

What to record:

  • Which ABIs are present (arm64-v8a, armeabi-v7a, x86).
  • Whether symbols are present or the binary looks stripped (absence of Java_com_ or clear function names suggests stripped).
  • Strings in .so that hint at functionality (JNI_OnLoad, Java_com_company_module_method, crypto-related identifiers).
  • Exports that look like Java_* — these indicate JNI bridges and help map native → Java callsites.

If binary looks non-trivial and contains security logic, plan to open it in Ghidra/Cutter for deeper reverse engineering (annotate function boundaries, identify cryptographic routines, search for attestation checks implemented in native code).

3.10 Static detection of anti-frida / anti-debug / anti-emulator

Search for code that detects instrumentation or debugger:

  • Methods checking loaded processes for frida-server, gdbserver, gdb, or checking ptrace status.
  • Use of android.os.Debug.isDebuggerConnected() or Debug.waitForDebugger() calls in suspicious contexts.
  • Existence of native anti-debugging logic (ptrace(PTRACE_TRACEME, ...) patterns, prctl(PR_SET_DUMPABLE, 0), anti_debug strings).
  • Checks for emulator indicators (files /system/bin/qemu_props, Android ID patterns, ro.kernel.qemu).

Example grep:

grep -R --line-number -iE "frida|gdb|ptrace|isDebuggerConnected|ro.kernel.qemu|qemu" app_unpacked/ | head

If anti-instrumentation code exists, document where it is and its control flow; this helps design safe lab workarounds (in lab only) and suggests hardening points.

3.11 Evaluate obfuscation vs real protection (risk estimation)

Important distinction:

  • Obfuscation (ProGuard/R8) is mainly an obstacle to reverse engineering; it raises required effort but is not a cryptographic defence.
  • String encryption + native attestation + hardware-backed keystore raise the bar significantly.
  • Kernel-level protections / verified boot are platform level and out of scope for pure static analysis to fully validate — but you can detect whether the app attempts to rely on them (calls to hardware-backed APIs).

Estimate effort:

  • Light obfuscation = low effort to recover with jadx + careful renaming.
  • String encryption + custom crypto in native = significant effort; require Ghidra & native expertise.
  • Native code with stripped symbols + anti-debugging = high effort.

Record an approximate Reverse Effort label (Low / Medium / High) for each security-relevant artifact.

3.12 Produce the static-analysis findings (structure & examples)

For each finding produce a structured entry:

  • ID: S-001
  • Title: Hardcoded API key in assets/config.json
  • Location: app_unpacked/assets/config.json: line 12 + sha256 of file
  • Description: The API key AK_abc... is stored in plain text and used by AuthClient.login()
  • Evidence: snippet of file & callsite in jadx_out/com/example/auth/AuthClient.java:123
  • Impact: Credential leakage → impersonation and API abuse.
  • Likelihood: High (found in multiple callsites & included in build)
  • Recommended remediation: Move to server-side, avoid embedding keys, use OAuth client credentials + ephemeral tokens.
  • Next action: Dynamic validation in lab; check whether this key is accepted by backend (requires vendor coordination).

Create a JSON/Markdown file with all findings to include in reports and to use as a checklist for Module 4/5 follow-ups.

3.13 Example commands & small scripts (useful snippets)

A few repeatable commands to build your static findings quickly.

  1. Extract and decompile (one-liner pipeline):
APK="app.apk"
WORKDIR="work_$(date -u +%Y%m%dT%H%M%SZ)"
mkdir -p "$WORKDIR" && cp "$APK" "$WORKDIR/"
cd "$WORKDIR"
apktool d "$APK" -o app_unpacked
jadx -d jadx_out "$APK"
sha256sum "$APK" > apk.sha256
  1. Search for attestation/pinning/root indicators:
grep -R --line-number -iE "PlayIntegrity|SafetyNet|attest|KeyAttestation|AndroidKeyStore|CertificatePinner|HostnameVerifier|X509TrustManager|TrustManager|su|magisk|root|frida|gdb|ptrace" app_unpacked/ jadx_out/ | tee static_suspicious_hits.txt
  1. Produce a native libs summary:
unzip -l "$APK" | grep '\.so' | awk '{print $4}' > native_paths.txt
while read -r p; do
  unzip -p "$APK" "$p" > "$(basename "$p")"
  file "$(basename "$p")"
  echo "Symbols for $(basename "$p")" >> native_summary.txt
  readelf -s "$(basename "$p")" 2>/dev/null | head -n 50 >> native_summary.txt
  echo -e "\n\n" >> native_summary.txt
done < native_paths.txt

3.14 Lab exercises (safe, authorized, with expected outcomes)

Lab A — Manifest & exported components audit

  • Objective: produce a list of exported components and simulate the risk model.
  • Steps: use apktool to decode manifest, list exported components, identify any with intent-filter and missing permission attributes.
  • Expected outcome: a table with component name, exported flag, permission, and suggested impact.

Lab B — Trace an auth flow in decompiled code

  • Objective: from login Activity -> network client -> server endpoint, map where tokens are created, stored, and sent.
  • Steps: open code in jadx, find login activity, follow method calls to network client, identify token storage (SharedPreferences / Keystore).
  • Expected outcome: call graph diagram (simple list), and identification of insecure storage or poor token management.

Lab C — Native library triage

  • Objective: find whether security logic is implemented in native code and whether it’s stripped.
  • Steps: list .so files, run strings and readelf, open in Ghidra if containing interesting strings.
  • Expected outcome: list of native functions of interest, classification: (contains crypto? yes/no), (stripped? yes/no), (recommended deeper analysis).

3.15 Common pitfalls & how to avoid them

  • False positives from grep: always open the found string in code and verify it’s actually used in a security-sensitive flow.
  • Assuming presence = protection: finding calls to attestation APIs doesn’t mean tokens are validated by the server — always require server-side evidence.
  • Over-relying on decompiler output: decompilers are imperfect; confirm suspicious code by reading bytecode or smali where needed.
  • Ignoring native code: some apps move critical checks into .so — treat them as first-class citizens during static analysis.

3.16 Deliverables for Module 3

Produce the following artifacts:

  1. static_manifest.json — APK metadata and checksums.
  2. manifest_findings.md — exported components & permission analysis (with line refs).
  3. strings_endpoints.csv — list of endpoints, hostnames, potential keys (with file + line + snippet).
  4. native_summary.txt — native libs inventory and quick readelf / strings snapshot.
  5. code_map.pdf or markdown — short call graph for auth, attestation, and pinning flows (where they live).
  6. static_findings.json — structured findings with impact & recommended next actions (dynamic verification steps or evidence to request from vendor).

3.17 Recommended follow-ups (after static)

  • Use the static findings to design focused dynamic tests: attempt to exercise specific flows, capture attestation tokens, and collect logs (lab-only).
  • If obfuscation or string encryption exists, plan native analysis (Ghidra) and allocate more time / expertise.
  • If you identify attestation/pinning/root-checks, prepare the forensic artifact checklist (Module 13) so you can validate any vendor claims of bypass with raw tokens and device images.

3.18 Quiz / self-check (short)

  1. How do you distinguish a genuine, used API key from a placeholder found in strings.xml?
  2. What does a missing Java_com_... export in a .so suggest about the binary?
  3. Why is it insufficient to only find a call to PlayIntegrityManager in the client to conclude the app is attested?

(Answers: 1) trace callsites where it is used and confirm backend acceptance; 2) likely stripped symbols — harder reverse; 3) need server-side token validation evidence: nonce, signature verification, timestamp.)