# DexGuard Protection Analysis & Frida Bypass — AI Agent Methodology
## Objective
You are an AI agent tasked with analyzing and bypassing DexGuard root detection on an Android application. The target app detects rooted devices and terminates itself immediately upon launch. Your goal is to use Frida to dynamically analyze the app, identify all detection mechanisms, and bypass them so the app runs normally on a rooted device.
---
## 1. What is DexGuard?
DexGuard is a commercial protection tool developed by **Guardsquare**, designed specifically for Android applications. It is the enterprise-grade evolution of ProGuard/R8 and provides multiple layers of protection including code obfuscation, runtime self-protection, integrity verification, and environment checks.
---
## 2. Protection Types Provided by DexGuard
### 2.1 Code Obfuscation
- **Class/Method/Field Renaming** — Converts meaningful names into unreadable symbols.
- **Control Flow Obfuscation** — Injects opaque predicates, dead code, and flattened control flow.
- **String Encryption** — All hardcoded strings are encrypted at compile time and decrypted only at runtime.
- **Arithmetic Obfuscation** — Replaces simple arithmetic with complex equivalent expressions.
### 2.2 Runtime Application Self-Protection (RASP)
- **Root Detection** — Detects rooted devices through multiple file system, package, property, and native checks.
- **Hooking Framework Detection** — Detects Frida, Xposed, Cydia Substrate, and similar frameworks.
- **Debugger Detection** — Detects attached debuggers via `TracerPid`, `isDebuggerConnected()`, and ptrace checks.
- **Emulator Detection** — Detects execution on emulators via build properties, sensors, and hardware fingerprints.
### 2.3 Integrity Protection
- **APK Signature Verification** — Validates the APK signing certificate at runtime.
- **DEX File Integrity** — Checks checksums/hashes of DEX files to detect tampering.
- **Anti-Repackaging** — Prevents modified APKs from running.
### 2.4 Environment Checks
- **SafetyNet / Play Integrity API** — Queries Google's attestation service.
- **USB Debugging Detection** — Checks if `adb_enabled` is set.
- **Developer Options Detection** — Checks if developer settings are enabled.
---
## 3. Root Detection Mechanisms in Detail
DexGuard uses a combination of the following techniques to detect root:
### 3.1 File System Checks
The app checks for the existence of these files/paths:
```
/system/app/Superuser.apk
/system/xbin/su
/system/bin/su
/sbin/su
/data/local/xbin/su
/data/local/bin/su
/su/bin/su
/system/bin/failsafe/su
/system/sd/xbin/su
/data/local/su
/system/bin/.ext/.su
/system/usr/we-need-root/su-backup
/system/xbin/mu
/system/app/Superuser.apk
/system/etc/init.d/99telecominfra
/dev/com.koushikdutta.superuser.daemon/
```
### 3.2 Package Manager Checks
The app queries the PackageManager for known root-related packages:
```
com.topjohnwu.magisk
eu.chainfire.supersu
com.koushikdutta.superuser
com.noshufou.android.su
com.thirdparty.superuser
com.yellowes.su
com.devadvance.rootcloak
com.devadvance.rootcloakplus
de.robv.android.xposed.installer
com.saurik.substrate
```
### 3.3 System Property Checks
```
ro.build.tags == "test-keys" → indicates custom build
ro.debuggable == "1" → debuggable build
ro.secure == "0" → insecure build
```
### 3.4 Command Execution Checks
The app attempts to execute:
```
su
which su
id (checks for uid=0)
```
### 3.5 Native Layer Checks
- Reads `/proc/self/maps` looking for Frida libraries (`frida-agent`, `frida-gadget`, `gmain`, `gum-js-loop`).
- Reads `/proc/self/status` checking `TracerPid` for attached debuggers.
- Uses `dlopen` / `dlsym` to check for injected shared libraries.
- Scans open ports (default Frida port `27042`).
- Uses `strstr` on memory maps to find hooking framework signatures.
### 3.6 Magisk-Specific Checks
- Checks for MagiskHide / Zygisk presence.
- Inspects mount namespaces for overlay mounts.
- Looks for Magisk Manager package under randomized names.
---
## 4. App Termination Methods
When any check returns positive, DexGuard triggers termination via one or more of:
- `System.exit(0)` or `System.exit(1)`
- `android.os.Process.killProcess(Process.myPid())`
- `Activity.finish()` or `Activity.finishAffinity()`
- Throwing an unhandled exception
- Native `exit()` or `abort()` via JNI
---
## 5. AI Agent Step-by-Step Bypass Workflow
Follow these steps in order. Each step builds on the previous one.
### Step 1: Static Analysis (Pre-Frida)
Decompile the APK and gather intelligence before dynamic analysis.
**Actions:**
1. Decompile with `apktool d target.apk` and `jadx target.apk`.
2. Search for root-related strings: `grep -ri "su" --include="*.smali"`, `grep -ri "root"`, `grep -ri "magisk"`, `grep -ri "detect"`.
3. Identify suspicious class names containing: `root`, `detect`, `guard`, `security`, `integrity`, `check`, `safe`, `tamper`, `hook`, `frida`, `xposed`.
4. Analyze `AndroidManifest.xml` for custom Application classes (root checks often run in `Application.attachBaseContext()` or `Application.onCreate()`).
5. Check for native libraries (`.so` files) in `lib/` — DexGuard may perform checks in native code.
6. Document all findings before proceeding.
### Step 2: Launch App with Frida in Spawn Mode and Block Termination
**Critical:** Always use spawn mode (`-f`) because detection runs at app startup.
**Critical:** Change frida-server name and port to avoid detection:
```bash
# On the device
mv frida-server frida-server-renamed
./frida-server-renamed -l 0.0.0.0:1337 &
# On host
frida -H 192.168.x.x:1337 -f com.target.app -l bypass.js --no-pause
```
**First script — block all termination methods and capture stack traces:**
```javascript
Java.perform(function() {
// Block System.exit
var System = Java.use("java.lang.System");
System.exit.implementation = function(code) {
console.log("[BLOCKED] System.exit(" + code + ")");
console.log("[STACK] " + Java.use("android.util.Log").getStackTraceString(Java.use("java.lang.Exception").$new()));
};
// Block Process.killProcess
var Process = Java.use("android.os.Process");
Process.killProcess.implementation = function(pid) {
console.log("[BLOCKED] Process.killProcess(" + pid + ")");
console.log("[STACK] " + Java.use("android.util.Log").getStackTraceString(Java.use("java.lang.Exception").$new()));
};
// Block Activity.finish
var Activity = Java.use("android.app.Activity");
Activity.finish.overload().implementation = function() {
console.log("[BLOCKED] Activity.finish()");
console.log("[STACK] " + Java.use("android.util.Log").getStackTraceString(Java.use("java.lang.Exception").$new()));
};
Activity.finishAffinity.implementation = function() {
console.log("[BLOCKED] Activity.finishAffinity()");
console.log("[STACK] " + Java.use("android.util.Log").getStackTraceString(Java.use("java.lang.Exception").$new()));
};
console.log("[+] Termination blockers installed");
});
```
**Analyze the output:** The stack traces will reveal exactly which classes and methods trigger the termination. Record every class and method name from the traces.
### Step 3: Enumerate and Identify Detection Classes
```javascript
Java.perform(function() {
Java.enumerateLoadedClasses({
onMatch: function(className) {
var keywords = ["root", "detect", "guard", "security", "integrity",
"check", "safe", "tamper", "emulator", "hook",
"frida", "xposed", "magisk", "superuser", "dexguard",
"protect", "verify", "shield"];
var lower = className.toLowerCase();
for (var i = 0; i < keywords.length; i++) {
if (lower.indexOf(keywords[i]) !== -1) {
console.log("[SUSPECT] " + className);
// List all methods in this class
try {
var clazz = Java.use(className);
var methods = clazz.class.getDeclaredMethods();
for (var j = 0; j < methods.length; j++) {
console.log(" → " + methods[j].getName());
}
} catch(e) {}
break;
}
}
},
onComplete: function() {
console.log("[*] Enumeration complete");
}
});
});
```
### Step 4: Hook File System Checks
```javascript
Java.perform(function() {
var File = Java.use("java.io.File");
File.exists.implementation = function() {
var path = this.getAbsolutePath();
var blocked = ["su", "magisk", "superuser", "Superuser", "SuperSU",
"busybox", "xposed", "substrate", "frida", "daemonsu",
".magisk", "rootcloak", "hideroot", "sbin/su"];
for (var i = 0; i < blocked.length; i++) {
if (path.indexOf(blocked[i]) !== -1) {
console.log("[FS BYPASS] File.exists(" + path + ") → false");
return false;
}
}
return this.exists();
};
File.canRead.implementation = function() {
var path = this.getAbsolutePath();
if (path.indexOf("su") !== -1 || path.indexOf("magisk") !== -1) {
console.log("[FS BYPASS] File.canRead(" + path + ") → false");
return false;
}
return this.canRead();
};
console.log("[+] File system hooks installed");
});
```
### Step 5: Hook Package Manager Checks
```javascript
Java.perform(function() {
var PM = Java.use("android.app.ApplicationPackageManager");
PM.getPackageInfo.overload('java.lang.String', 'int').implementation = function(pkg, flags) {
var rootPackages = [
"com.topjohnwu.magisk", "eu.chainfire.supersu",
"com.koushikdutta.superuser", "com.noshufou.android.su",
"com.thirdparty.superuser", "com.yellowes.su",
"com.devadvance.rootcloak", "com.devadvance.rootcloakplus",
"de.robv.android.xposed.installer", "com.saurik.substrate",
"com.zachspong.temprootremovejb", "com.amphoras.hidemyroot",
"com.formyhm.hiderootPremium", "com.koushikdutta.rommanager"
];
for (var i = 0; i < rootPackages.length; i++) {
if (pkg === rootPackages[i]) {
console.log("[PM BYPASS] Blocked: " + pkg);
throw Java.use("android.content.pm.PackageManager$NameNotFoundException").$new(pkg);
}
}
return this.getPackageInfo(pkg, flags);
};
// Also hook getInstalledPackages and getInstalledApplications
PM.getInstalledPackages.overload('int').implementation = function(flags) {
var packages = this.getInstalledPackages(flags);
console.log("[PM] getInstalledPackages called, filtering root packages");
return packages; // Agent should filter if needed
};
console.log("[+] Package manager hooks installed");
});
```
### Step 6: Hook Runtime Command Execution
```javascript
Java.perform(function() {
var Runtime = Java.use("java.lang.Runtime");
Runtime.exec.overload('java.lang.String').implementation = function(cmd) {
if (cmd.indexOf("su") !== -1 || cmd.indexOf("which") !== -1 || cmd.indexOf("id") !== -1) {
console.log("[EXEC BYPASS] Blocked: " + cmd);
throw Java.use("java.io.IOException").$new("Permission denied");
}
return this.exec(cmd);
};
Runtime.exec.overload('[Ljava.lang.String;').implementation = function(cmds) {
var joined = cmds.join(" ");
if (joined.indexOf("su") !== -1 || joined.indexOf("which") !== -1) {
console.log("[EXEC BYPASS] Blocked: " + joined);
throw Java.use("java.io.IOException").$new("Permission denied");
}
return this.exec(cmds);
};
console.log("[+] Runtime.exec hooks installed");
});
```
### Step 7: Hook System Properties and Build Fields
```javascript
Java.perform(function() {
// Spoof Build fields
var Build = Java.use("android.os.Build");
Build.TAGS.value = "release-keys";
Build.FINGERPRINT.value = Build.FINGERPRINT.value.replace("test-keys", "release-keys");
Build.TYPE.value = "user";
// Hook SystemProperties.get
try {
var SystemProperties = Java.use("android.os.SystemProperties");
SystemProperties.get.overload('java.lang.String').implementation = function(key) {
var result = this.get(key);
if (key === "ro.build.tags") return "release-keys";
if (key === "ro.debuggable") return "0";
if (key === "ro.secure") return "1";
if (key === "ro.build.type") return "user";
return result;
};
SystemProperties.get.overload('java.lang.String', 'java.lang.String').implementation = function(key, def) {
if (key === "ro.build.tags") return "release-keys";
if (key === "ro.debuggable") return "0";
if (key === "ro.secure") return "1";
return this.get(key, def);
};
} catch(e) {
console.log("[!] SystemProperties hook failed: " + e);
}
// Hook Settings.Secure for developer options and ADB
var Secure = Java.use("android.provider.Settings$Secure");
Secure.getInt.overload('android.content.ContentResolver', 'java.lang.String', 'int').implementation = function(resolver, name, def) {
if (name === "adb_enabled") { console.log("[SETTINGS] adb_enabled → 0"); return 0; }
if (name === "development_settings_enabled") { console.log("[SETTINGS] dev_settings → 0"); return 0; }
return this.getInt(resolver, name, def);
};
console.log("[+] Property and settings hooks installed");
});
```
### Step 8: Hook Native Layer Checks
```javascript
// Hook fopen — blocks reading sensitive proc files and su paths
Interceptor.attach(Module.findExportByName("libc.so", "fopen"), {
onEnter: function(args) {
try { this.path = args[0].readUtf8String(); } catch(e) { this.path = null; }
},
onLeave: function(retval) {
if (this.path) {
var blocked = ["su", "magisk", "frida", "xposed", "substrate",
"/proc/self/maps", "/proc/self/status", "superuser"];
for (var i = 0; i < blocked.length; i++) {
if (this.path.indexOf(blocked[i]) !== -1) {
console.log("[NATIVE] fopen blocked: " + this.path);
retval.replace(ptr(0));
break;
}
}
}
}
});
// Hook access — used to check file existence at native level
Interceptor.attach(Module.findExportByName("libc.so", "access"), {
onEnter: function(args) {
try { this.path = args[0].readUtf8String(); } catch(e) { this.path = null; }
},
onLeave: function(retval) {
if (this.path) {
if (this.path.indexOf("su") !== -1 || this.path.indexOf("magisk") !== -1 ||
this.path.indexOf("superuser") !== -1) {
console.log("[NATIVE] access blocked: " + this.path);
retval.replace(-1);
}
}
}
});
// Hook stat
Interceptor.attach(Module.findExportByName("libc.so", "stat"), {
onEnter: function(args) {
try { this.path = args[0].readUtf8String(); } catch(e) { this.path = null; }
},
onLeave: function(retval) {
if (this.path && (this.path.indexOf("su") !== -1 || this.path.indexOf("magisk") !== -1)) {
console.log("[NATIVE] stat blocked: " + this.path);
retval.replace(-1);
}
}
});
// Hook strstr — primary anti-Frida detection bypass
Interceptor.attach(Module.findExportByName("libc.so", "strstr"), {
onEnter: function(args) {
try { this.needle = args[1].readUtf8String(); } catch(e) { this.needle = null; }
},
onLeave: function(retval) {
if (this.needle) {
var blocked = ["frida", "gadget", "gmain", "gum-js-loop",
"linjector", "agent", "server"];
for (var i = 0; i < blocked.length; i++) {
if (this.needle.indexOf(blocked[i]) !== -1) {
console.log("[NATIVE] strstr blocked needle: " + this.needle);
retval.replace(ptr(0));
break;
}
}
}
}
});
// Hook connect — block Frida default port detection
Interceptor.attach(Module.findExportByName("libc.so", "connect"), {
onEnter: function(args) {
var sockAddr = args[1];
var family = sockAddr.readU16();
if (family === 2) { // AF_INET
var port = (sockAddr.add(2).readU8() << 8) | sockAddr.add(3).readU8();
if (port === 27042 || port === 27043) {
console.log("[NATIVE] connect to Frida port " + port + " → redirecting");
sockAddr.add(2).writeU8(0);
sockAddr.add(3).writeU8(0);
}
}
}
});
console.log("[+] Native hooks installed");
```
### Step 9: Verify and Iterate
After loading all hooks:
1. **Check if the app stays open** — If it still crashes, check Frida console for new stack traces.
2. **Look for additional detection methods** — DexGuard may use multiple layers. If one bypass works but the app still closes, there are more checks.
3. **Check for delayed checks** — Some checks run on timers or background threads. Monitor for several minutes.
4. **Check for native-only checks** — If Java hooks are not enough, the detection may be entirely in native `.so` libraries. Use `Interceptor` on the specific native library.
5. **Test app functionality** — Ensure the app works normally after bypass (network calls, login, etc.). Some apps have server-side root detection via SafetyNet/Play Integrity.
**If the app still terminates after all hooks:**
- Look for `Thread` creation in detection classes — hooks may need to target spawned threads.
- Check for `ContentProvider` or `BroadcastReceiver` based checks.
- Analyze native `.so` files with Ghidra for hardcoded detection logic.
- Consider using Frida Gadget injected into the APK instead of frida-server.
### Step 10: Consolidate Final Bypass Script
Combine all working hooks into a single clean script. Remove any hooks that were not needed. Add error handling. Test the final script from a clean app start to confirm full bypass.
---
## 6. Execution Command
```bash
# Rename and run frida-server on non-default port
adb shell "mv /data/local/tmp/frida-server /data/local/tmp/fs-custom"
adb shell "/data/local/tmp/fs-custom -l 0.0.0.0:1337 &"
# Launch with spawn mode
frida -H <device-ip>:1337 -f <package.name> -l dexguard_bypass.js --no-pause
```
---
## 7. Important Notes
- **Always use spawn mode** (`-f` flag) — detection runs at `Application.attachBaseContext()` or `Application.onCreate()`, before any activity is visible.
- **Rename frida-server binary** — DexGuard scans process names for "frida".
- **Change Frida port** — Default port 27042 is a known detection vector.
- **Consider Frida Gadget** — If frida-server is consistently detected, inject `frida-gadget.so` directly into the APK as a last resort.
- **Check for certificate pinning** — After bypassing root detection, the app may also have SSL pinning that needs separate bypass for traffic interception.
- **Server-side checks** — SafetyNet/Play Integrity cannot be bypassed with Frida alone. These require additional tools or approaches.