Frida lets you hook Java and native functions at runtime without modifying the APK. This is invaluable for:
- Understanding app behavior before patching
- Bypassing checks at runtime to test if your static patch would work
- Analyzing encrypted/obfuscated code
// bypass_ssl_pinning.js
Java.perform(function() {
// OkHttp CertificatePinner bypass
var CertificatePinner = Java.use('okhttp3.CertificatePinner');
CertificatePinner.check.overload('java.lang.String', 'java.util.List').implementation = function(hostname, peerCertificates) {
console.log('[+] SSL Pinning bypassed for: ' + hostname);
};
// TrustManager bypass
var X509TrustManager = Java.use('javax.net.ssl.X509TrustManager');
var SSLContext = Java.use('javax.net.ssl.SSLContext');
var TrustManager = Java.registerClass({
name: 'com.bypass.TrustManager',
implements: [X509TrustManager],
methods: {
checkClientTrusted: function(chain, authType) {},
checkServerTrusted: function(chain, authType) {},
getAcceptedIssuers: function() { return []; }
}
});
var trustManagers = [TrustManager.$new()];
var sslContext = SSLContext.getInstance('TLS');
sslContext.init(null, trustManagers, null);
var factory = sslContext.getSocketFactory();
// Hook HttpsURLConnection to use our factory
var HttpsURLConnection = Java.use('javax.net.ssl.HttpsURLConnection');
HttpsURLConnection.setDefaultSSLSocketFactory(factory);
HttpsURLConnection.setDefaultHostnameVerifier(Java.use('javax.net.ssl.HostnameVerifier').$new({
verify: function(hostname, session) { return true; }
}));
console.log('[+] SSL Pinning fully bypassed');
});// bypass_root.js
Java.perform(function() {
// Common root detection methods to bypass
// 1. File existence checks
var File = Java.use('java.io.File');
File.exists.implementation = function() {
var path = this.getAbsolutePath();
var rootPaths = ['/system/app/Superuser.apk', '/sbin/su', '/system/bin/su',
'/system/xbin/su', '/data/local/xbin/su', '/data/local/bin/su',
'/system/sd/xbin/su', '/system/bin/failsafe/su', '/su/bin/su'];
if (rootPaths.indexOf(path) !== -1) {
console.log('[*] Root detection bypassed: ' + path);
return false;
}
return this.exists.call(this);
};
// 2. PackageManager checks for root apps
var PackageManager = Java.use('android.app.ApplicationPackageManager');
PackageManager.getPackageInfo.overload('java.lang.String', 'int').implementation = function(pkg, flags) {
var rootPkgs = ['com.topjohnwu.magisk', 'eu.chainfire.supersu', 'com.koushikdutta.superuser',
'com.noshufou.android.su', 'com.thirdparty.superuser'];
if (rootPkgs.indexOf(pkg) !== -1) {
console.log('[*] Root app detection bypassed: ' + pkg);
throw Java.use('android.content.pm.PackageManager$NameNotFoundException').$new(pkg);
}
return this.getPackageInfo(pkg, flags);
};
});// bypass_signature.js
Java.perform(function() {
var targetPkg = 'com.example.app';
var fakeSignature = '308202e3308201eba0030201020...'; // Google's cert or desired signature hex
var PackageManager = Java.use('android.app.ApplicationPackageManager');
PackageManager.getPackageInfo.overload('java.lang.String', 'int').implementation = function(pkg, flags) {
var result = this.getPackageInfo(pkg, flags);
if (pkg === targetPkg && (flags & 0x40) !== 0) {
console.log('[*] Intercepted signature query for: ' + pkg);
// Create fake Signature object
var Signature = Java.use('android.content.pm.Signature');
var hexBytes = Java.use('android.util.HexDump');
var fakeSig = Signature.$new(
Java.array('byte', hexBytes.hexStringToByteArray(fakeSignature))
);
result.signatures.value = Java.array('android.content.pm.Signature', [fakeSig]);
}
return result;
};
// Also hook GET_SIGNING_CERTIFICATES (API 28+)
var PackageInfo = Java.use('android.content.pm.PackageInfo');
if (PackageInfo.signatures) {
PackageManager.getPackageInfo.overload('java.lang.String', 'android.os.PackageManager$PackageInfoFlags').implementation = function(pkg, flags) {
var result = this.getPackageInfo(pkg, flags);
console.log('[*] Intercepted signingInfo query for: ' + pkg);
return result;
};
}
});R8 (default since Android Gradle Plugin 7.0) does:
- Shrinking: Removes unused code/resources
- Optimization: Optimizes bytecode
- Obfuscation: Renames classes/methods/fields to short names (a, b, c, etc.)
- Desugaring: Converts Java 8+ features to older bytecode
Analysis strategies for obfuscated code:
# 1. Use jadx for decompilation — handles most R8 output
jadx -d decompiled/ target.apk
# 2. Search for string constants (not obfuscated)
# URLs, API keys, package names, error messages
# 3. Use jadx's built-in deobfuscation
jadx --deobf target.apk
# 4. Use Frida to trace method calls at runtime
# Even obfuscated, the API signatures are preserved
frida -U -f com.example.app -l trace_calls.jstrace_calls.js — Trace all methods in a class:
Java.perform(function() {
Java.enumerateLoadedClasses({
onMatch: function(className) {
if (className.includes('example')) {
console.log('[*] Found class: ' + className);
Java.use(className).$methods.forEach(function(method) {
console.log(' - ' + method);
});
}
},
onComplete: function() {}
});
});Apps implement various anti-tamper mechanisms. Here's how to deal with each:
Check 1: File hash verification
# App computes SHA-256 of its own APK file and compares to known value
# Patch: Find the MessageDigest usage and NOP the comparison
# In smali, look for:
invoke-static {v0}, Ljava/security/MessageDigest;->getInstance(Ljava/lang/String;)Ljava/security/MessageDigest;
# Change to: nop (won't compute hash)Check 2: Tamper detection via PackageManager
# App checks if its APK path has been modified
# Look for: getSourceDir(), getApplicationInfo()
# Patch: Redirect to a cached responseCheck 3: DEX integrity check
# App reads its own DEX files and verifies checksums
# Look for: openDexFile, DexFile, "/data/app/..."
# Patch: NOP the verification or return the expected hashCheck 4: Google Play Integrity
# Cannot be fully bypassed on emulator
# On physical device with root:
# Install TrickyStore + PIF NEXT + ZygiskNext
# See: 6.4 Play Integrity BypassFor modifications that would otherwise require system-level changes (framework patches, system app modifications, signature spoofing):
# module.prop
id=my_apk_patch
name=My APK Patch Module
version=v1.0
versionCode=1
author=YourName
description=Patches for target appRRO (Runtime Resource Overlay) for resource-only modifications:
<!-- overlay/SystemUIOverlay/AndroidManifest.xml -->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.systemui.overlay">
<application android:label="SystemUI Overlay">
<overlay
android:targetPackage="com.android.systemui"
android:targetName="SystemUI"
android:isStatic="true"
android:priority="100" />
</application>
</manifest>Replacing a system APK entirely:
my_module/
├── module.prop
└── system/
└── app/
└── Settings/
└── Settings.apk # Your patched Settings APK
Advantages over direct patching:
- Systemless — doesn't modify
/systempartition - Reversible — disable/uninstall the module
- OTA-safe — system updates don't conflict
- Works with Magisk, KernelSU, and APatch