Implementing Root Detection and File Existence Security Checks in React Native (Android) - 1

ajmal_hasan

Ajmal Hasan

Posted on November 11, 2024

Implementing Root Detection and File Existence Security Checks in React Native (Android) - 1

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:

  1. Root/Jailbreak Detection: Check if the device is compromised using tools like JailMonkey and custom native modules.
  2. 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
---
}
Enter fullscreen mode Exit fullscreen mode

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());
       }
   }
}
Enter fullscreen mode Exit fullscreen mode

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);
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

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;
    }
}
Enter fullscreen mode Exit fullscreen mode
// 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;
   }
}
Enter fullscreen mode Exit fullscreen mode

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;
        }
Enter fullscreen mode Exit fullscreen mode

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.

import { isEmulator } from 'react-native-device-info';

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;


      const emulatorCheck = await isEmulator();

      // Final security check combining all methods
      const deviceCompromised = isJailBroken || zygiskDetected || emulatorCheck;
      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}\nIs Emulator: ${emulatorCheck}`;
      // 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()
  }, []);
Enter fullscreen mode Exit fullscreen mode

Why Each Check is Important

  1. JailMonkey: Offers cross-platform checks for root or jailbreak status, as well as debug mode detection.
  2. RootCheckModule: Uses RootBeer to perform comprehensive root detection on Android.
  3. 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.

iOS PART-->

💖 💪 🙅 🚩
ajmal_hasan
Ajmal Hasan

Posted on November 11, 2024

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related