In this blog, we’ll walk through how to implement security checks in a React Native application using custom native modules to detect potential threats, such as rooted or jailbroken devices. This approach adds a layer of security by identifying unauthorized changes to the device that could compromise the application.
Why Security Checks Are Important
Applications often handle sensitive information, and it’s crucial to ensure that they run in secure environments. Rooted or jailbroken devices have fewer restrictions, allowing unauthorized access to system files and the ability to bypass security controls. With this solution, we’ll implement:
- Root/Jailbreak Detection: Check if the device is compromised using tools like JailMonkey and custom native modules.
- File Existence Check: Detect specific system files or directories that indicate rooting tools like Zygisk or Magisk on Android.
Key Libraries and Tools
- JailMonkey: A React Native library for basic root/jailbreak detection.
- RootBeer: A library for detecting rooted Android devices.
- Custom Native Modules: For extended functionality like file existence checks.
Step 1: Creating Custom Native Modules in Java for Android
Inside (main/java/com/project_name)
1.1 Root Detection Module Using RootBeer
Add inside app/build.gradle:
// add inside dependecies
dependencies {
---
implementation 'com.scottyab:rootbeer-lib:0.1.0' // Add rootbeer-lib here
---
}
Then, we’ll create a module named RootCheckModule
to leverage RootBeer, which provides robust root detection on Android. Here’s how to implement it:
// RootCheckModule.java
package com.---;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;
import com.facebook.react.bridge.Promise;
import com.scottyab.rootbeer.RootBeer;
public class RootCheckModule extends ReactContextBaseJavaModule {
public RootCheckModule(ReactApplicationContext reactContext) {
super(reactContext);
}
@Override
public String getName() {
return "RootCheckModule";
}
@ReactMethod
public void isDeviceRooted(Promise promise) {
try {
RootBeer rootBeer = new RootBeer(getReactApplicationContext());
boolean isRooted = rootBeer.isRooted();
promise.resolve(isRooted);
} catch (Exception e) {
promise.reject("ROOT_CHECK_ERROR", e.getMessage());
}
}
}
Why This is Done
Using RootBeer provides a reliable method to detect rooting on Android devices without adding unnecessary complexity. The isDeviceRooted
method will return a boolean indicating whether the device is rooted, providing insight into the device’s security state.
1.2 File Existence Check Module
To detect files that indicate Zygisk or Magisk, we create FileCheckModule
, which checks for the existence of specific paths associated with rooting software.
// FileCheckModule.java
package com.---;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;
import com.facebook.react.bridge.Promise;
import java.io.File;
public class FileCheckModule extends ReactContextBaseJavaModule {
public FileCheckModule(ReactApplicationContext reactContext) {
super(reactContext);
}
@Override
public String getName() {
return "FileCheckModule";
}
@ReactMethod
public void doesFileExist(String filePath, Promise promise) {
try {
File file = new File(filePath);
promise.resolve(file.exists());
} catch (Exception e) {
promise.reject("FILE_CHECK_ERROR", e);
}
}
}
Why This is Done
By creating FileCheckModule
, we gain the ability to detect specific files on Android devices, which allows us to identify rooting tools like Magisk and Zygisk.
1.3 Registering the Modules
We register these modules in FileCheckPackage
and RootCheckPackage
, allowing them to be recognized by React Native.
// FileCheckPackage.java
package com.---;
import com.facebook.react.ReactPackage;
import com.facebook.react.bridge.NativeModule;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.uimanager.ViewManager;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class FileCheckPackage implements ReactPackage {
@Override
public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {
return Collections.emptyList();
}
@Override
public List<NativeModule> createNativeModules(ReactApplicationContext reactContext) {
List<NativeModule> modules = new ArrayList<>();
modules.add(new FileCheckModule(reactContext));
return modules;
}
}
// RootCheckPackage.java
package com.---;
import com.facebook.react.ReactPackage;
import com.facebook.react.bridge.NativeModule;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.uimanager.ViewManager;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class RootCheckPackage implements ReactPackage {
@Override
public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {
return Collections.emptyList();
}
@Override
public List<NativeModule> createNativeModules(ReactApplicationContext reactContext) {
List<NativeModule> modules = new ArrayList<>();
modules.add(new RootCheckModule(reactContext));
return modules;
}
}
Adding to MainApplication.java
In MainApplication.java
, we need to register these packages so that they are accessible from React Native.
// MainApplication.java
@Override
protected List<ReactPackage> getPackages() {
@SuppressWarnings("UnnecessaryLocalVariable")
List<ReactPackage> packages = new PackageList(this).getPackages();
// Packages that cannot be autolinked yet can be added manually here, for example:
packages.add(new RootCheckPackage()); //
packages.add(new FileCheckPackage()); //
return packages;
}
Step 2: Using the Security Checks in JavaScript
2.1 Import and Integrate JailMonkey and Custom Modules
In the JavaScript code, we’ll use JailMonkey
, along with the custom RootCheckModule
and FileCheckModule
, to detect rooted/jailbroken devices and check for specific files.
const { JailbreakDetector, RootCheckModule, FileCheckModule } = NativeModules;
export const ROOT_DETECTION_PATH = [
// Magisk and Zygisk-related paths
'/data/adb/magisk',
'/data/adb/zygisk',
'/system/lib/libzygisk.so',
'/system/lib64/libzygisk.so',
'/sbin/.magisk/',
'/dev/.magisk/',
'/data/adb/magisk/',
'/cache/magisk.log',
'/system/app/Superuser.apk',
// Common superuser binaries and SU paths
'/sbin/su',
'/system/bin/su',
'/system/xbin/su',
'/data/local/xbin/su',
'/data/local/bin/su',
'/system/sd/xbin/su',
'/system/bin/failsafe/su',
'/data/local/su',
'/su/bin/su',
// Common busybox binaries, often associated with rooted devices
'/system/xbin/busybox',
'/system/bin/busybox',
'/system/sbin/busybox',
'/vendor/bin/busybox',
'/data/local/xbin/busybox',
'/data/local/bin/busybox',
// Frida-related paths (often used for reverse engineering)
'/data/local/tmp/frida-server',
'/data/local/tmp/re.frida.server',
'/data/local/tmp/frida',
'/data/local/tmp/frida-inject',
'/data/local/tmp/frida-agent-32',
'/data/local/tmp/frida-agent-64',
'/system/lib/libfrida-gadget.so',
'/system/lib64/libfrida-gadget.so',
// Xposed Framework-related paths (another common rooting tool)
'/system/framework/XposedBridge.jar',
'/system/lib/libxposed_art.so',
'/system/lib64/libxposed_art.so',
'/system/xbin/daemonsu',
'/system/xbin/supolicy',
'/data/data/de.robv.android.xposed.installer',
'/system/bin/daemonsu',
'/data/app/de.robv.android.xposed.installer',
// Substrate-related files (jailbreak library often used for injecting code)
'/usr/libexec/substrate',
'/Library/MobileSubstrate/MobileSubstrate.dylib',
'/usr/lib/substitute-inserter.dylib',
'/usr/lib/libhooker.dylib',
// Other known files related to jailbreak/root detection
'/etc/apt',
'/bin/bash',
'/usr/sbin/sshd',
'/private/var/lib/apt/',
'/Applications/Cydia.app',
'/Applications/FakeCarrier.app',
'/Applications/Icy.app',
'/Applications/IntelliScreen.app',
'/Applications/SBSettings.app',
'/Applications/RockApp.app',
];
const checkForZygiskFiles = async () => {
try {
const results = await Promise.all(
ROOT_DETECTION_PATH.map((path) => FileCheckModule.doesFileExist(path))
);
return results.some((result) => result === true);
} catch (error) {
console.error('Error checking for Zygisk files:', error);
return false;
}
};
const checkDeviceSecurity = async () => {
try {
// Platform-specific root/jailbreak detection
const isCustomJailBroken =
Platform.OS === 'ios'
? false // await JailbreakDetector.isJailbroken()
: await RootCheckModule.isDeviceRooted();
// Cross-platform JailMonkey checks
// const isDebuggedMode = await JailMonkey.isDebuggedMode();
const isJailBroken =
JailMonkey.isOnExternalStorage() || // Check if app is on external storage
JailMonkey.isJailBroken() || // JailMonkey general jailbreak/root check
JailMonkey.trustFall() || // Trust fall detection
// isDebuggedMode || // Check if device is in debug mode
JailMonkey.canMockLocation() || // Check if mock location is allowed
isCustomJailBroken; // Platform-specific jailbreak/root check
// Additional Zygisk detection (Android only)
const zygiskDetected = Platform.OS === 'android' ? await checkForZygiskFiles() : false;
// Final security check combining all methods
const deviceCompromised = isJailBroken || zygiskDetected;
const message = `Device Compromised: ${deviceCompromised}\nIs JailBroken: ${isJailBroken}\nZygisk Detected: ${zygiskDetected}\nIs Debugged Mode: ${'-'}\nCan Mock Location: ${JailMonkey.canMockLocation()}\nTrust Fall: ${JailMonkey.trustFall()}\nIs JailBroken (JailMonkey): ${JailMonkey.isJailBroken()}\nIs On External Storage: ${JailMonkey.isOnExternalStorage()}\nNative Module Jailbreak: ${isCustomJailBroken}`;
// alert(message);
if (!__DEV__ && deviceCompromised) {
Alert.alert(
'Security Warning',
'This device appears to be compromised. The app will now close for security reasons.',
[
{
text: 'OK',
onPress: () => {
Clipboard.setString(message);
RNExitApp.exitApp();
},
},
],
{ cancelable: false }
);
}
} catch (error) {
console.error('Error in security check:', error);
}
};
useEffect(() => {
checkDeviceSecurity()
}, []);
Why Each Check is Important
- JailMonkey: Offers cross-platform checks for root or jailbreak status, as well as debug mode detection.
- RootCheckModule: Uses RootBeer to perform comprehensive root detection on Android.
- FileCheckModule: Searches for specific files linked to rooting tools, such as Magisk and Zygisk, on Android devices.
Conclusion
By implementing these security checks, we increase the security of our application, helping it to identify and respond to rooted or jailbroken devices. This ensures that the app can only run on secure devices, protecting sensitive data and reducing the risk of tampering.
Top comments (0)