Skip to content

Commit 763f8c1

Browse files
committed
Updating Root detection utility method as per recommendations from https://mas.owasp.org/MASTG/knowledge/android/MASVS-RESILIENCE/MASTG-KNOW-0027/
1 parent 217bd35 commit 763f8c1

4 files changed

Lines changed: 89 additions & 24 deletions

File tree

firebase-crashlytics/src/androidTest/java/com/google/firebase/crashlytics/internal/common/CommonUtilsTest.java

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -151,9 +151,10 @@ public void testGetTotalRamInBytes() {
151151

152152
@Test
153153
public void testIsRooted() {
154+
final Context mockContext = mock(Context.class);
154155
// No good way to test the alternate case,
155156
// just want to ensure we can complete the call without an exception here.
156-
final boolean isRooted = CommonUtils.isRooted();
157+
final boolean isRooted = CommonUtils.isRooted(mockContext);
157158
Log.d(Logger.TAG, "isRooted: " + isRooted + " isEmulator: " + CommonUtils.isEmulator());
158159

159160
// We don't care about the actual result of isRooted, just that we didn't cause an exception
@@ -176,7 +177,8 @@ private boolean isBitSet(int data, int mask) {
176177
@Test
177178
public void testGetDeviceState() {
178179

179-
final int state = CommonUtils.getDeviceState();
180+
final Context mockContext = mock(Context.class);
181+
final int state = CommonUtils.getDeviceState(mockContext);
180182
Log.d(Logger.TAG, "testGetDeviceState: state=" + state);
181183

182184
if (CommonUtils.isEmulator()) {
@@ -191,7 +193,7 @@ public void testGetDeviceState() {
191193
assertFalse(isBitSet(state, CommonUtils.DEVICE_STATE_DEBUGGERATTACHED));
192194
}
193195

194-
if (CommonUtils.isRooted()) {
196+
if (CommonUtils.isRooted(mockContext)) {
195197
assertTrue(isBitSet(state, CommonUtils.DEVICE_STATE_JAILBROKEN));
196198
} else {
197199
assertFalse(isBitSet(state, CommonUtils.DEVICE_STATE_JAILBROKEN));

firebase-crashlytics/src/main/java/com/google/firebase/crashlytics/internal/common/CommonUtils.java

Lines changed: 78 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -318,34 +318,97 @@ public static boolean isEmulator() {
318318
|| Build.HARDWARE.contains(RANCHU);
319319
}
320320

321-
public static boolean isRooted() {
321+
/**
322+
* Utility method intended for root status validation within a local scope.
323+
* <p>
324+
* NOTE: Root detection is complex; compromised devices may spoof results
325+
* to bypass these basic security checks.
326+
* For high-security requirements, integrate solutions like Google Play Integrity API.
327+
* <p>
328+
* @return true if any rule is met.
329+
*/
330+
public static boolean isRooted(Context context) {
322331
// No reliable way to determine if an android phone is rooted, since a rooted phone could
323332
// always disguise itself as non-rooted. Some common approaches can be found on SO:
324-
// http://stackoverflow.com/questions/1101380/determine-if-running-on-a-rooted-device
333+
// http://stackoverflow.com/questions/1101380/determine-if-running-on-a-rooted-device
325334
//
326335
// http://stackoverflow.com/questions/3576989/how-can-you-detect-if-the-device-is-rooted-in-the-app
327336
//
328337
// http://stackoverflow.com/questions/7727021/how-can-androids-copy-protection-check-if-the-device-is-rooted
338+
339+
// Validate custom ROMs.
329340
final boolean isEmulator = isEmulator();
330341
final String buildTags = Build.TAGS;
331342
if (!isEmulator && buildTags != null && buildTags.contains("test-keys")) {
332343
return true;
333344
}
334345

335-
// Superuser.apk would only exist on a rooted device:
336-
File file = new File("/system/app/Superuser.apk");
337-
if (file.exists()) {
338-
return true;
346+
// Check for common Root-Related files and binaries.
347+
String[] paths = {
348+
"/system/app/Superuser.apk",
349+
"/sbin/su",
350+
"/system/bin/su",
351+
"/system/xbin/su",
352+
"/data/local/xbin/su",
353+
"/data/local/bin/su",
354+
"/system/sd/xbin/su",
355+
"/system/bin/failsafe/su",
356+
"/data/local/su",
357+
"/su/bin/su",
358+
"/su/xbin/su",
359+
"/su/bin/daemonsu",
360+
"/system/xbin/daemonsu",
361+
"/system/etc/init.d/99SuperSUDaemon",
362+
"/dev/com.koushikdutta.superuser.daemon/",
363+
"/system/xbin/busybox",
364+
"/data/magisk.img",
365+
"/sbin/.core/img/magisk.img",
366+
"/system/lib/libmagisk.so"
367+
};
368+
for (String path : paths) {
369+
if (new File(path).exists()) {
370+
return true;
371+
}
339372
}
340373

341-
// su is only available on a rooted device (or the emulator)
342-
// The user could rename or move to a non-standard location, but in that case they
343-
// probably don't want us to know they're root and they can pretty much subvert
344-
// any check anyway.
345-
file = new File("/system/xbin/su");
346-
if (!isEmulator && file.exists()) {
347-
return true;
374+
// Check if 'su' Executable is in the PATH.
375+
String pathVar = System.getenv("PATH");
376+
if (pathVar != null) {
377+
for (String pathDir : pathVar.split(":")) {
378+
if (new File(pathDir, "su").exists()) {
379+
return true;
380+
}
381+
}
348382
}
383+
384+
// Check for Installed Root Manager packages.
385+
// NOTE: For Android 11+, this requires <queries> declared in the app's AndroidManifest.xml.
386+
// See https://developer.android.com/training/package-visibility for more details.
387+
if (context != null) {
388+
String[] knownRootPackages = {
389+
"com.noshufou.android.su",
390+
"com.thirdparty.superuser",
391+
"eu.chainfire.supersu",
392+
"com.koushikdutta.superuser",
393+
"com.zachspong.repodroid",
394+
"com.ramdroid.repodroid",
395+
"com.topjohnwu.magisk"
396+
};
397+
PackageManager pm = context.getPackageManager();
398+
for (String pkg : knownRootPackages) {
399+
try {
400+
pm.getPackageInfo(pkg, 0);
401+
return true;
402+
} catch (PackageManager.NameNotFoundException e) {
403+
// Package is not installed or not visible.
404+
Logger.getLogger()
405+
.d(
406+
"Root check failed due to missing package or limited visibility. Further details: "
407+
+ e.getMessage());
408+
}
409+
}
410+
}
411+
349412
return false;
350413
}
351414

@@ -361,13 +424,13 @@ public static boolean isDebuggerAttached() {
361424
public static final int DEVICE_STATE_VENDORINTERNAL = 1 << 4;
362425
public static final int DEVICE_STATE_COMPROMISEDLIBRARIES = 1 << 5;
363426

364-
public static int getDeviceState() {
427+
public static int getDeviceState(Context context) {
365428
int deviceState = 0;
366429
if (CommonUtils.isEmulator()) {
367430
deviceState |= DEVICE_STATE_ISSIMULATOR;
368431
}
369432

370-
if (CommonUtils.isRooted()) {
433+
if (CommonUtils.isRooted(context)) {
371434
deviceState |= DEVICE_STATE_JAILBROKEN;
372435
}
373436

firebase-crashlytics/src/main/java/com/google/firebase/crashlytics/internal/common/CrashlyticsController.java

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -542,7 +542,7 @@ private void doOpenSession(String sessionIdentifier, Boolean isOnDemand) {
542542
String.format(Locale.US, GENERATOR_FORMAT, CrashlyticsCore.getVersion());
543543

544544
StaticSessionData.AppData appData = createAppData(idManager, this.appData);
545-
StaticSessionData.OsData osData = createOsData();
545+
StaticSessionData.OsData osData = createOsData(context);
546546
StaticSessionData.DeviceData deviceData = createDeviceData(context);
547547

548548
nativeComponent.prepareNativeSession(
@@ -765,9 +765,9 @@ private static StaticSessionData.AppData createAppData(IdManager idManager, AppD
765765
appData.developmentPlatformProvider);
766766
}
767767

768-
private static StaticSessionData.OsData createOsData() {
768+
private static StaticSessionData.OsData createOsData(Context context) {
769769
return StaticSessionData.OsData.create(
770-
VERSION.RELEASE, VERSION.CODENAME, CommonUtils.isRooted());
770+
VERSION.RELEASE, VERSION.CODENAME, CommonUtils.isRooted(context));
771771
}
772772

773773
private static StaticSessionData.DeviceData createDeviceData(Context context) {
@@ -781,7 +781,7 @@ private static StaticSessionData.DeviceData createDeviceData(Context context) {
781781
CommonUtils.calculateTotalRamInBytes(context),
782782
diskSpace,
783783
CommonUtils.isEmulator(),
784-
CommonUtils.getDeviceState(),
784+
CommonUtils.getDeviceState(context),
785785
Build.MANUFACTURER,
786786
Build.PRODUCT);
787787
}

firebase-crashlytics/src/main/java/com/google/firebase/crashlytics/internal/common/CrashlyticsReportDataCapture.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -208,7 +208,7 @@ private CrashlyticsReport.Session.OperatingSystem populateSessionOperatingSystem
208208
.setPlatform(SESSION_ANDROID_PLATFORM)
209209
.setVersion(VERSION.RELEASE)
210210
.setBuildVersion(VERSION.CODENAME)
211-
.setJailbroken(CommonUtils.isRooted())
211+
.setJailbroken(CommonUtils.isRooted(context))
212212
.build();
213213
}
214214

@@ -219,7 +219,7 @@ private CrashlyticsReport.Session.Device populateSessionDeviceData() {
219219
final long totalRam = CommonUtils.calculateTotalRamInBytes(context);
220220
final long diskSpace = (long) statFs.getBlockCount() * (long) statFs.getBlockSize();
221221
final boolean isEmulator = CommonUtils.isEmulator();
222-
final int state = CommonUtils.getDeviceState();
222+
final int state = CommonUtils.getDeviceState(context);
223223
final String manufacturer = Build.MANUFACTURER;
224224
final String modelClass = Build.PRODUCT;
225225

0 commit comments

Comments
 (0)