From 5cd067008f481edbaa673b9680ae58df8e6e563b Mon Sep 17 00:00:00 2001 From: Bartosz Maciej Date: Fri, 19 Apr 2024 18:22:16 +0200 Subject: [PATCH] Root detection --- .gitignore | 4 +- README.md | 36 ++- build.gradle.kts | 4 +- native/Cargo.toml | 7 +- native/src/common.rs | 8 + native/src/{ => common}/build.rs | 1 + native/src/{ => common}/files.rs | 61 ++++- native/src/{ => common}/info.rs | 10 +- native/src/{ => common}/logging.rs | 0 native/src/common/package.rs | 131 ++++++++++ native/src/common/property.rs | 152 ++++++++++++ native/src/{ => common}/system.rs | 7 +- native/src/{ => common}/util.rs | 0 native/src/debuggable.rs | 2 +- native/src/emulator/device.rs | 2 +- native/src/emulator/general.rs | 33 +-- native/src/emulator/packages.rs | 51 +--- native/src/emulator/properties.rs | 127 +--------- native/src/emulator/sensors.rs | 2 +- native/src/lib.rs | 11 +- native/src/root.rs | 3 + native/src/root/packages.rs | 26 ++ native/src/root/properties.rs | 13 + native/src/root/superuser.rs | 231 ++++++++++++++++++ rasp/build.gradle.kts | 4 +- .../rasp/android/api/SecureAppChecker.kt | 6 +- .../rasp/android/api/result/Checks.kt | 13 + .../rasp/android/api/result/Result.kt | 1 + .../rasp/android/check/ChecksMediator.kt | 36 ++- .../securevale/rasp/android/root/RootCheck.kt | 66 +++++ .../rasp/android/root/checks/AppsChecks.kt | 15 ++ .../rasp/android/root/checks/DeviceChecks.kt | 15 ++ .../rasp/android/root/checks/FileChecks.kt | 10 + .../android/root/checks/PropertyChecks.kt | 14 ++ .../android/sample/CheckDetailsActivity.kt | 4 +- .../rasp/android/sample/MainActivity.kt | 6 +- .../sample/check/CheckResultAdapter.kt | 4 + .../sample/check/DebuggerCheckFragment.kt | 4 +- .../sample/check/EmulatorCheckFragment.kt | 2 + .../android/sample/check/RootCheckFragment.kt | 50 ++++ .../src/main/jniLibs/arm64-v8a/libnative.so | Bin 438832 -> 878984 bytes .../src/main/jniLibs/armeabi-v7a/libnative.so | Bin 182604 -> 687380 bytes sample-app/src/main/jniLibs/x86/libnative.so | Bin 294268 -> 741116 bytes .../src/main/jniLibs/x86_64/libnative.so | Bin 500792 -> 843896 bytes 44 files changed, 935 insertions(+), 237 deletions(-) create mode 100644 native/src/common.rs rename native/src/{ => common}/build.rs (96%) rename native/src/{ => common}/files.rs (76%) rename native/src/{ => common}/info.rs (92%) rename native/src/{ => common}/logging.rs (100%) create mode 100644 native/src/common/package.rs create mode 100644 native/src/common/property.rs rename native/src/{ => common}/system.rs (97%) rename native/src/{ => common}/util.rs (100%) create mode 100644 native/src/root.rs create mode 100644 native/src/root/packages.rs create mode 100644 native/src/root/properties.rs create mode 100644 native/src/root/superuser.rs create mode 100644 rasp/src/main/java/com/securevale/rasp/android/root/RootCheck.kt create mode 100644 rasp/src/main/java/com/securevale/rasp/android/root/checks/AppsChecks.kt create mode 100644 rasp/src/main/java/com/securevale/rasp/android/root/checks/DeviceChecks.kt create mode 100644 rasp/src/main/java/com/securevale/rasp/android/root/checks/FileChecks.kt create mode 100644 rasp/src/main/java/com/securevale/rasp/android/root/checks/PropertyChecks.kt create mode 100644 sample-app/src/main/java/com/securevale/rasp/android/sample/check/RootCheckFragment.kt diff --git a/.gitignore b/.gitignore index 4b3c735..9efbca2 100644 --- a/.gitignore +++ b/.gitignore @@ -11,4 +11,6 @@ local.properties local.keystore output-metadata.json -*.apk \ No newline at end of file +*.apk + +roadmap \ No newline at end of file diff --git a/README.md b/README.md index 5772d6d..9252ba4 100644 --- a/README.md +++ b/README.md @@ -76,11 +76,13 @@ import com.securevale.rasp.android.api.SecureAppChecker val shouldCheckForEmulator = true val shouldCheckForDebugger = true +val shouldCheckForRoot = true val builder = SecureAppChecker.Builder( - context, - checkEmulator = shouldCheckForEmulator, - checkDebugger = shouldCheckForDebugger + this, + checkEmulator = shouldCheckForEmulator, + checkDebugger = shouldCheckForDebugger, + checkRoot = shouldCheckForRoot ) ``` @@ -95,6 +97,7 @@ val checkResult = check.check() when (checkResult) { is Result.EmulatorFound -> {} // app is most likely running on emulator is Result.DebuggerEnabled -> {} // app is in debug mode + is Result.Rooted -> {} // app is most likely rooted is Result.Secure -> {} // OK, no threats detected } ``` @@ -123,6 +126,7 @@ the `checkOnlyFor` parameter. ```kotlin import com.securevale.rasp.android.api.result.DebuggerChecks import com.securevale.rasp.android.api.result.EmulatorChecks +import com.securevale.rasp.android.api.result.RootChecks val check = builder.build() check.subscribeVulnerabilitiesOnly( @@ -133,7 +137,13 @@ check.subscribeVulnerabilitiesOnly( EmulatorChecks.Genymotion, EmulatorChecks.Nox, DebuggerChecks.Debuggable, - DebuggerChecks.DebugField + DebuggerChecks.DebugField, + RootChecks.SuUser, + RootChecks.TestTags, + RootChecks.RootApps, + RootChecks.RootCloakingApps, + RootChecks.WritablePaths, + RootChecks.SuspiciousProperties ) ) { // examine result(s) here @@ -182,6 +192,24 @@ never ending cat and mouse game, so there is no guarantee that all emulators wil accurately reported as such. The library shall be continuously updated with new emulator detection techniques with the aim of catching the emulators that slip through the existing checks. +## Root Detection + +Includes: + +- Checks for superuser indicators; +- Checks for test tags present on device; +- Checks for rooting and root cloaking apps (still in progress); +- Checks for paths that should not be writable; +- Checks for suspicious properties; + +>[!IMPORTANT] +> Since rooting is dynamic domain, checks associated with it must continuously evolve. It's important to understand that these checks are not exhaustive, and +> should be expected to undergo continuous improvement. + + +> [!NOTE] +> Although some basics checks for Magisk are already applied, "real" Magisk detection is expected to be implemented in future releases. + ## ProGuard Android RASP ships with its own ProGuard rules, except one caveat regarding the `DebugField` diff --git a/build.gradle.kts b/build.gradle.kts index 3244af9..f4cdd86 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -9,8 +9,8 @@ plugins { id("com.vanniktech.maven.publish") version "0.25.2" } -val versionCode by extra { 5 } -val versionName by extra { "0.5.0" } +val versionCode by extra { 6 } +val versionName by extra { "0.6.0" } val minSdkVersion by extra { 24 } val compileSdkVersion by extra { 34 } val targetSdkVersion by extra { 34 } diff --git a/native/Cargo.toml b/native/Cargo.toml index f1a8516..45608af 100644 --- a/native/Cargo.toml +++ b/native/Cargo.toml @@ -4,17 +4,16 @@ version = "0.1.0" edition = "2021" [dependencies] -log = "0.4.19" +log = "0.4.21" jni = "0.21.1" -android_logger = "0.13.1" -once_cell = "1.18.0" +android_logger = "0.13.3" +once_cell = "1.19.0" [lib] name = "native" crate-type = ["cdylib"] [profile.release] -strip = true lto = true #[profile.release-with-debug] diff --git a/native/src/common.rs b/native/src/common.rs new file mode 100644 index 0000000..a3af243 --- /dev/null +++ b/native/src/common.rs @@ -0,0 +1,8 @@ +pub mod build; +pub mod files; +pub mod info; +pub mod logging; +pub mod package; +pub mod property; +pub mod system; +pub mod util; diff --git a/native/src/build.rs b/native/src/common/build.rs similarity index 96% rename from native/src/build.rs rename to native/src/common/build.rs index 1204475..882d1ae 100644 --- a/native/src/build.rs +++ b/native/src/common/build.rs @@ -27,6 +27,7 @@ pub const BRAND: &str = "BRAND"; pub const BOARD: &str = "BOARD"; pub const MANUFACTURER: &str = "MANUFACTURER"; pub const HARDWARE: &str = "HARDWARE"; +pub const DISPLAY: &str = "DISPLAY"; pub const FINGERPRINT: &str = "FINGERPRINT"; pub const PRODUCT: &str = "PRODUCT"; pub const TAGS: &str = "TAGS"; diff --git a/native/src/files.rs b/native/src/common/files.rs similarity index 76% rename from native/src/files.rs rename to native/src/common/files.rs index b63e7e6..c6b8f47 100644 --- a/native/src/files.rs +++ b/native/src/common/files.rs @@ -3,10 +3,11 @@ use std::io; use std::io::{BufRead, BufReader}; use std::path::Path; +#[cfg(debug_assertions)] use log::LevelFilter::Error; #[cfg(debug_assertions)] -use crate::logging::log_android; +use crate::common::logging::log_android; pub fn has_genymotion_files() -> bool { check_files_present(&GENYMOTION_FILES) @@ -40,12 +41,22 @@ pub fn has_phoenix_files() -> bool { check_files_present(&PHOENIX_FILES) } +pub fn has_su_files() -> bool { + check_files_present(&SU_FILES) +} + +pub fn has_busybox_files() -> bool { + check_files_present(&BUSYBOX_FILES) +} + fn check_files_present(suspicious_file_paths: &[&str]) -> bool { + let mut check_result = false; for file_path in suspicious_file_paths.iter() { match Path::new(file_path).try_exists() { Ok(result) => { if result { - return true; + check_result = true; + break; } } Err(err) => { @@ -58,7 +69,7 @@ fn check_files_present(suspicious_file_paths: &[&str]) -> bool { } } - false + check_result } pub fn find_in_file(file_path: &str, search_phrases: &[&str]) -> Result { @@ -109,6 +120,40 @@ const EMU_FILES: [&str; 19] = [ "/data/data/com.ldmnq.launcher3/files/launcher.preferences", ]; +/// Su files (indicates superuser - root privileges) +const SU_FILES: [&str; 15] = [ + "/sbin/su", + "su/bin/su", + "/system/bin/su", + "/system/bin/.ext/su", + "/system/xbin/su", + "/data/local/xbin/su", + "/data/local/bin/su", + "/system/sd/xbin/su", + "/system/bin/failsafe/su", + "/system/usr/we-need-root/su", + "/data/local/su", + "/cache/su", + "/dev/su", + "/data/su", + "/system/app/Superuser.apk", +]; + +/// Busybox files TODO check what is busybox +const BUSYBOX_FILES: [&str; 11] = [ + "/sbin/busybox", + "su/bin/busybox", + "/system/bin/busybox", + "/system/bin/.ext/busybox", + "/system/xbin/busybox", + "/data/local/xbin/busybox", + "/data/local/bin/busybox", + "/system/sd/xbin/busybox", + "/system/bin/failsafe/busybox", + "/system/usr/we-need-root/busybox", + "/data/local/busybox", +]; + /// Pipes indicate that it is most likely an emulator. const X86_FILES: [&str; 10] = [ "ueventd.android_x86.rc", @@ -167,3 +212,13 @@ const PHOENIX_FILES: [&str; 3] = [ "/data/system/phoenixlog.addr", "/system/phoenixos", ]; + +pub const NON_WRITABLE_PATHS: [&str; 7] = [ + "/system", + "/system/bin", + "/system/sbin", + "/system/xbin", + "/vendor/bin", + "/sbin", + "/etc", +]; diff --git a/native/src/info.rs b/native/src/common/info.rs similarity index 92% rename from native/src/info.rs rename to native/src/common/info.rs index dd31ff6..8393e30 100644 --- a/native/src/info.rs +++ b/native/src/common/info.rs @@ -4,9 +4,11 @@ use jni::objects::{JClass, JObject, JString, JValue}; use jni::sys::jstring; use jni::JNIEnv; -use crate::build::get_build_config_value; -use crate::{build, emulator, files, system}; +use crate::common::build::get_build_config_value; +use crate::common::property::emulator_properties_found; +use crate::{common::build, common::files, common::system}; +// TODO add info about root #[no_mangle] pub unsafe extern "C" fn Java_com_securevale_rasp_android_util_DeviceInfoKt_deviceInfo( mut env: JNIEnv, @@ -58,7 +60,7 @@ pub unsafe extern "C" fn Java_com_securevale_rasp_android_util_DeviceInfoKt_exte Genymotion files: {} Emulator Pipes: {} Qemu Property ro.kernel.qemu: {} - Qemu Properties count: {} + Qemu Properties found: {} ", files::has_nox_files(), files::has_andy_files(), @@ -68,7 +70,7 @@ pub unsafe extern "C" fn Java_com_securevale_rasp_android_util_DeviceInfoKt_exte files::has_genymotion_files(), files::has_pipes(), system::get_prop(&mut env, &"ro.kernel.qemu".to_string()) == "1", - emulator::properties::properties_count(&mut env), + emulator_properties_found(&mut env), ); env.new_string(result) diff --git a/native/src/logging.rs b/native/src/common/logging.rs similarity index 100% rename from native/src/logging.rs rename to native/src/common/logging.rs diff --git a/native/src/common/package.rs b/native/src/common/package.rs new file mode 100644 index 0000000..e5664a9 --- /dev/null +++ b/native/src/common/package.rs @@ -0,0 +1,131 @@ +use jni::JNIEnv; +use jni::objects::{JObject, JValue}; + +use crate::common::util; + +pub fn has_emulator_packages(env: &mut JNIEnv, context: &JObject) -> bool { + is_package_suspicious(env, context, &SUSPICIOUS_EMULATOR_PACKAGES) +} + +pub fn has_root_packages(env: &mut JNIEnv, context: &JObject) -> bool { + is_package_suspicious(env, context, &ROOT_APP_PACKAGES) +} + +pub fn has_root_cloaking_packages(env: &mut JNIEnv, context: &JObject) -> bool { + is_package_suspicious(env, context, &ROOT_CLOAKING_APP_PACKAGES) +} + +fn is_package_suspicious(env: &mut JNIEnv, context: &JObject, suspicious_package: &[&str]) -> bool { + let package_manager = JObject::try_from( + env.call_method( + context, + "getPackageManager", + "()Landroid/content/pm/PackageManager;", + &[], + ) + .unwrap(), + ) + .unwrap(); + + let mut result = false; + + for package in suspicious_package.iter() { + let package_info = env.call_method( + &package_manager, + "getPackageInfo", + "(Ljava/lang/String;I)Landroid/content/pm/PackageInfo;", + &[ + JValue::Object(&JObject::from(env.new_string(package).unwrap())), + JValue::Int(0), + ], + ); + + util::ignore_error(env); + + if package_info.is_ok() { + result = true; + break; + } + } + + result +} + +/** + * Suspicious emulator packages. + */ +const SUSPICIOUS_EMULATOR_PACKAGES: [&str; 10] = [ + "com.google.android.launcher.layouts.genymotion", + "com.bluestacks", + "com.vphone.launcher", + "com.bluestacks.appmart", + "com.bignox", + "com.bignox.app", + "com.google.android.launcher.layouts.genymotion", + "com.microvirt.tools", + "com.microvirt.download", + "com.mumu.store", +]; + +/** + * Known root app packages. + */ +const ROOT_APP_PACKAGES: [&str; 43] = [ + "com.noshufou.android.su", + "com.noshufou.android.su.elite", + "com.koushikdutta.superuser", + "com.thirdparty.superuser", + "com.topjohnwu.magisk", + "com.kingo.roo", + "com.zhiqupk.root.global", + "com.smedialink.oneclickroot", + "com.alephzain.framaroo", + "com.yellowes.su", + "com.kingroot.kinguser", + "com.zachspong.temprootremovejb", + "com.ramdroid.appquarantine", + "eu.chainfire.supersu", + "stericson.busybox", + "com.alephzain.framaroot", + "com.kingo.root", + "com.koushikdutta.rommanager", + "com.koushikdutta.rommanager.license", + "com.dimonvideo.luckypatcher", + "com.chelpus.lackypatch", + "com.ramdroid.appquarantinepro", + "com.xmodgame", + "com.cih.game_cih", + "com.charles.lpoqasert", + "catch_.me_.if_.you_.can_", + "org.blackmart.market", + "com.allinone.free", + "com.repodroid.app", + "org.creeplays.hack", + "com.baseappfull.fwd", + "com.zmapp", + "com.dv.marketmod.installer", + "org.mobilism.android", + "com.android.wp.net.log", + "com.android.camera.update", + "cc.madkite.freedom", + "com.solohsu.android.edxp.manager", + "org.meowcat.edxposed.manager", + "com.android.vending.billing.InAppBillingService.COIN", + "com.android.vending.billing.InAppBillingService.LUCK", + "com.chelpus.luckypatcher", + "com.blackmartalpha", +]; + +/** + * Known root cloaking app packages. + */ +const ROOT_CLOAKING_APP_PACKAGES: [&str; 8] = [ + "com.devadvance.rootcloak", + "com.devadvance.rootcloakplus", + "de.robv.android.xposed.installer", + "com.saurik.substrate", + "com.amphoras.hidemyroot", + "com.amphoras.hidemyrootadfree", + "com.formyhm.hiderootPremium", + "com.formyhm.hideroot", +]; diff --git a/native/src/common/property.rs b/native/src/common/property.rs new file mode 100644 index 0000000..e6cdd99 --- /dev/null +++ b/native/src/common/property.rs @@ -0,0 +1,152 @@ +use crate::common::system; +use jni::JNIEnv; +#[cfg(debug_assertions)] +use log::LevelFilter::Debug; + +#[cfg(debug_assertions)] +use crate::common::logging::log_android; + +pub struct Property<'a> { + pub name: &'a str, + pub suspicious_value: Option<&'a str>, +} + +impl<'a> Property<'a> { + pub fn looks_suspicious(&self, found_value: Option<&str>) -> bool { + found_value == self.suspicious_value + } +} + +pub fn emulator_properties_found(env: &mut JNIEnv) -> bool { + properties_count(env, &KNOWN_SUSPICIOUS_EMU_PROPERTIES) >= EMULATOR_PROPERTIES_THRESHOLD +} + +pub fn root_properties_found(env: &mut JNIEnv) -> bool { + properties_count(env, &KNOWN_ROOT_PROPERTIES) > 0 +} + +fn properties_count(env: &mut JNIEnv, properties: &[Property]) -> u8 { + let mut counter = 0; + + properties.iter().for_each(|property| { + let found_property = system::get_prop(env, &property.name.to_string()); + let looks_like_emulator = property.looks_suspicious(if found_property.is_empty() { + None + } else { + Some(found_property.as_str()) + }); + + if looks_like_emulator { + counter += 1; + } + }); + + #[cfg(debug_assertions)] + log_android(Debug, format!("Properties count: {}", counter).as_str()); + counter +} + +/** + * The minimum qemu properties threshold which indicates whether device is suspicious or not. + */ +const EMULATOR_PROPERTIES_THRESHOLD: u8 = 10; + +/** + * Known qemu properties. + */ +pub const KNOWN_SUSPICIOUS_EMU_PROPERTIES: [Property; 20] = [ + Property { + name: "init.svc.qemud", + suspicious_value: None, + }, + Property { + name: "init.svc.qemu-props", + suspicious_value: None, + }, + Property { + name: "qemu.hw.mainkeys", + suspicious_value: None, + }, + Property { + name: "qemu.sf.fake_camera", + suspicious_value: None, + }, + Property { + name: "qemu.sf.lcd_density", + suspicious_value: None, + }, + Property { + name: "ro.bootloader", + suspicious_value: Some("unknown"), + }, + Property { + name: "ro.bootmode", + suspicious_value: Some("unknown"), + }, + Property { + name: "ro.hardware", + suspicious_value: Some("goldfish"), + }, + Property { + name: "ro.hardware", + suspicious_value: Some("ranchu"), + }, + Property { + name: "ro.kernel.android.qemud", + suspicious_value: None, + }, + Property { + name: "ro.kernel.qemu.gles", + suspicious_value: None, + }, + Property { + name: "ro.kernel.qemu", + suspicious_value: Some("1"), + }, + Property { + name: "ro.product.device", + suspicious_value: Some("generic"), + }, + Property { + name: "ro.product.model", + suspicious_value: Some("sdk"), + }, + Property { + name: "ro.product.name", + suspicious_value: Some("sdk"), + }, + Property { + name: "ro.serialno", + suspicious_value: None, + }, + Property { + name: "ro.secure", + suspicious_value: Some("0"), + }, + Property { + name: "ro.product.cpu.abilist", + suspicious_value: Some("x86"), + }, + Property { + name: "ro.product.model", + suspicious_value: Some("vmos"), + }, + Property { + name: "ro.product.vendor.name", + suspicious_value: Some("vmos"), + }, +]; + +/** + * Known root properties. + */ +pub const KNOWN_ROOT_PROPERTIES: [Property; 2] = [ + Property { + name: "ro.secure", + suspicious_value: Some("0"), + }, + Property { + name: "ro.debuggable", + suspicious_value: Some("1"), + }, +]; diff --git a/native/src/system.rs b/native/src/common/system.rs similarity index 97% rename from native/src/system.rs rename to native/src/common/system.rs index 94513a1..e7b00b3 100644 --- a/native/src/system.rs +++ b/native/src/common/system.rs @@ -1,7 +1,7 @@ use std::collections::HashMap; use std::sync::Mutex; -use crate::util; +use crate::common::util; use jni::errors::Error; use jni::objects::{JObject, JString, JValue}; use jni::JNIEnv; @@ -50,10 +50,7 @@ pub fn get_prop(env: &mut JNIEnv, property_name: &String) -> String { } fn flatten_result(result: Option) -> String { - match result { - Some(value) => value, - None => "".to_string(), - } + result.unwrap_or_default() } fn try_with_system_out(env: &mut JNIEnv, property_name: &String) -> Option { diff --git a/native/src/util.rs b/native/src/common/util.rs similarity index 100% rename from native/src/util.rs rename to native/src/common/util.rs diff --git a/native/src/debuggable.rs b/native/src/debuggable.rs index 6ec0abe..5fe2439 100644 --- a/native/src/debuggable.rs +++ b/native/src/debuggable.rs @@ -1,6 +1,6 @@ #![allow(non_snake_case)] -use crate::util; +use crate::common::util; use jni::objects::{JClass, JObject, JString, JValue}; use jni::sys::jboolean; use jni::JNIEnv; diff --git a/native/src/emulator/device.rs b/native/src/emulator/device.rs index ff2e1bb..1d1c2dd 100644 --- a/native/src/emulator/device.rs +++ b/native/src/emulator/device.rs @@ -2,7 +2,7 @@ use jni::objects::{JClass, JObject, JString}; use jni::sys::jboolean; use jni::JNIEnv; -use crate::system; +use crate::common::system; // isRadioVersionSuspicious #[no_mangle] diff --git a/native/src/emulator/general.rs b/native/src/emulator/general.rs index 60b5574..c0fdd2b 100644 --- a/native/src/emulator/general.rs +++ b/native/src/emulator/general.rs @@ -1,11 +1,11 @@ #![allow(non_snake_case)] -use jni::JNIEnv; use jni::objects::JClass; use jni::sys::jboolean; +use jni::JNIEnv; -use crate::{build, files, system}; -use crate::build::get_build_config_value; +use crate::common::build::get_build_config_value; +use crate::{common, common::build, common::system}; const AVD_DEVICES: [&str; 4] = ["generic_x86_arm", "generic_x86", "generic", "x86"]; @@ -61,7 +61,7 @@ pub unsafe extern "C" fn Java_com_securevale_rasp_android_emulator_checks_Genera let manufacturer = get_build_config_value(&mut env, build::MANUFACTURER); let product = get_build_config_value(&mut env, build::PRODUCT); - let hasGenymotionFiles = files::has_genymotion_files(); + let hasGenymotionFiles = common::files::has_genymotion_files(); let result = manufacturer.contains("Genymotion") || product == "vbox86p" @@ -81,7 +81,7 @@ pub unsafe extern "C" fn Java_com_securevale_rasp_android_emulator_checks_Genera let product = get_build_config_value(&mut env, build::PRODUCT); let board = get_build_config_value(&mut env, build::BOARD); - let hasNoxFiles = files::has_nox_files(); + let hasNoxFiles = common::files::has_nox_files(); let result = hardware.to_lowercase().contains("nox") || product.to_lowercase().contains("nox") @@ -97,11 +97,11 @@ pub unsafe extern "C" fn Java_com_securevale_rasp_android_emulator_checks_Genera _env: JNIEnv, _class: JClass, ) -> jboolean { - let has_andy_files = files::has_andy_files(); - let has_blue_files = files::has_bluestack_files(); - let has_x_86_files = files::has_x86_files(); - let has_emulator_files = files::has_emu_files(); - let has_phoenix_files = files::has_phoenix_files(); + let has_andy_files = common::files::has_andy_files(); + let has_blue_files = common::files::has_bluestack_files(); + let has_x_86_files = common::files::has_x86_files(); + let has_emulator_files = common::files::has_emu_files(); + let has_phoenix_files = common::files::has_phoenix_files(); let result = has_andy_files || has_blue_files @@ -141,7 +141,7 @@ pub unsafe extern "C" fn Java_com_securevale_rasp_android_emulator_checks_Genera let device = get_build_config_value(&mut env, build::DEVICE); let tags = get_build_config_value(&mut env, build::TAGS); - let hasEmulatorPipes = files::has_pipes(); + let hasEmulatorPipes = common::files::has_pipes(); let result = model.contains("Android SDK built for x86") || model.contains("google_sdk") @@ -169,7 +169,7 @@ pub unsafe extern "C" fn Java_com_securevale_rasp_android_emulator_checks_Genera _env: JNIEnv, _class: JClass, ) -> jboolean { - let result = files::find_in_file("/proc/mounts", &["vboxsf"]).unwrap_or(false); + let result = common::files::find_in_file("/proc/mounts", &["vboxsf"]).unwrap_or(false); u8::from(result) } @@ -180,7 +180,8 @@ pub unsafe extern "C" fn Java_com_securevale_rasp_android_emulator_checks_Genera _env: JNIEnv, _class: JClass, ) -> jboolean { - let result = files::find_in_file("/proc/cpuinfo", &["hypervisor", "Goldfish"]).unwrap_or(false); + let result = + common::files::find_in_file("/proc/cpuinfo", &["hypervisor", "Goldfish"]).unwrap_or(false); u8::from(result) } @@ -191,7 +192,7 @@ pub unsafe extern "C" fn Java_com_securevale_rasp_android_emulator_checks_Genera _env: JNIEnv, _class: JClass, ) -> jboolean { - let result = files::find_in_file( + let result = common::files::find_in_file( "/proc/cpuinfo", &[ "vboxsf", @@ -202,8 +203,8 @@ pub unsafe extern "C" fn Java_com_securevale_rasp_android_emulator_checks_Genera "bstinput", "bstvmsg", ], - ).unwrap_or(false); - + ) + .unwrap_or(false); u8::from(result) } diff --git a/native/src/emulator/packages.rs b/native/src/emulator/packages.rs index e92c703..1b8b4f0 100644 --- a/native/src/emulator/packages.rs +++ b/native/src/emulator/packages.rs @@ -1,8 +1,8 @@ -use jni::objects::{JClass, JObject, JValue}; +use jni::objects::{JClass, JObject}; use jni::sys::jboolean; use jni::JNIEnv; -use crate::util; +use crate::common::package::has_emulator_packages; // hasSuspiciousPackages #[no_mangle] @@ -11,50 +11,5 @@ pub unsafe extern "C" fn Java_com_securevale_rasp_android_emulator_checks_Packag _class: JClass, context: JObject<'a>, ) -> jboolean { - let package_manager = JObject::try_from( - env.call_method( - context, - "getPackageManager", - "()Landroid/content/pm/PackageManager;", - &[], - ) - .unwrap(), - ) - .unwrap(); - - for package in SUSPICIOUS_PACKAGES { - let package_info = env.call_method( - &package_manager, - "getPackageInfo", - "(Ljava/lang/String;I)Landroid/content/pm/PackageInfo;", - &[ - JValue::Object(&JObject::from(env.new_string(package).unwrap())), - JValue::Int(0), - ], - ); - - util::ignore_error(&mut env); - - if package_info.is_ok() { - return u8::from(true); - } - } - - u8::from(false) + u8::from(has_emulator_packages(&mut env, &context)) } - -/** - * Suspicious emulator packages. - */ -const SUSPICIOUS_PACKAGES: [&str; 10] = [ - "com.google.android.launcher.layouts.genymotion", - "com.bluestacks", - "com.vphone.launcher", - "com.bluestacks.appmart", - "com.bignox", - "com.bignox.app", - "com.google.android.launcher.layouts.genymotion", - "com.microvirt.tools", - "com.microvirt.download", - "com.mumu.store", -]; diff --git a/native/src/emulator/properties.rs b/native/src/emulator/properties.rs index 87fdf73..6459cde 100644 --- a/native/src/emulator/properties.rs +++ b/native/src/emulator/properties.rs @@ -1,21 +1,8 @@ -use std::string::ToString; - use jni::objects::JClass; use jni::sys::jboolean; use jni::JNIEnv; -use crate::system; - -struct Property<'a> { - name: &'a str, - wanted_value: Option<&'a str>, -} - -impl<'a> Property<'a> { - fn indicates_emulator(&self, found_value: Option<&str>) -> bool { - found_value == self.wanted_value - } -} +use crate::common::property::emulator_properties_found; // hasQemuProperties #[no_mangle] @@ -23,115 +10,5 @@ pub unsafe extern "C" fn Java_com_securevale_rasp_android_emulator_checks_Proper mut env: JNIEnv, _class: JClass, ) -> jboolean { - u8::from(properties_count(&mut env) >= EMULATOR_PROPERTIES_THRESHOLD) -} - -pub fn properties_count(env: &mut JNIEnv) -> u8 { - let mut counter = 0; - - KNOWN_SUSPICIOUS_PROPERTIES.iter().for_each(|property| { - let found_property = system::get_prop(env, &property.name.to_string()); - let looks_like_emulator = property.indicates_emulator(if found_property.is_empty() { - None - } else { - Some(found_property.as_str()) - }); - - if looks_like_emulator { - counter += 1; - } - }); - - counter + u8::from(emulator_properties_found(&mut env)) } - -/** - * The minimum qemu properties threshold which indicates whether device is suspicious or not. - */ -const EMULATOR_PROPERTIES_THRESHOLD: u8 = 10; - -/** - * Known qemu properties. - */ -const KNOWN_SUSPICIOUS_PROPERTIES: [Property; 20] = [ - Property { - name: "init.svc.qemud", - wanted_value: None, - }, - Property { - name: "init.svc.qemu-props", - wanted_value: None, - }, - Property { - name: "qemu.hw.mainkeys", - wanted_value: None, - }, - Property { - name: "qemu.sf.fake_camera", - wanted_value: None, - }, - Property { - name: "qemu.sf.lcd_density", - wanted_value: None, - }, - Property { - name: "ro.bootloader", - wanted_value: Some("unknown"), - }, - Property { - name: "ro.bootmode", - wanted_value: Some("unknown"), - }, - Property { - name: "ro.hardware", - wanted_value: Some("goldfish"), - }, - Property { - name: "ro.hardware", - wanted_value: Some("ranchu"), - }, - Property { - name: "ro.kernel.android.qemud", - wanted_value: None, - }, - Property { - name: "ro.kernel.qemu.gles", - wanted_value: None, - }, - Property { - name: "ro.kernel.qemu", - wanted_value: Some("1"), - }, - Property { - name: "ro.product.device", - wanted_value: Some("generic"), - }, - Property { - name: "ro.product.model", - wanted_value: Some("sdk"), - }, - Property { - name: "ro.product.name", - wanted_value: Some("sdk"), - }, - Property { - name: "ro.serialno", - wanted_value: None, - }, - Property { - name: "ro.secure", - wanted_value: Some("0"), - }, - Property { - name: "ro.product.cpu.abilist", - wanted_value: Some("x86"), - }, - Property { - name: "ro.product.model", - wanted_value: Some("vmos"), - }, - Property { - name: "ro.product.vendor.name", - wanted_value: Some("vmos"), - }, -]; diff --git a/native/src/emulator/sensors.rs b/native/src/emulator/sensors.rs index bb24f83..9d7f717 100644 --- a/native/src/emulator/sensors.rs +++ b/native/src/emulator/sensors.rs @@ -2,7 +2,7 @@ use jni::objects::{JClass, JObject, JString, JValue}; use jni::sys::jboolean; use jni::JNIEnv; -use crate::system; +use crate::common::system; // areSensorsFromEmulator #[no_mangle] diff --git a/native/src/lib.rs b/native/src/lib.rs index baf0a97..f4d3629 100644 --- a/native/src/lib.rs +++ b/native/src/lib.rs @@ -7,14 +7,11 @@ extern crate android_logger; use jni::objects::JClass; use jni::JNIEnv; -mod build; mod debuggable; mod emulator; -mod files; -mod info; -mod logging; -mod system; -mod util; + +mod common; +mod root; const EXTENDED_LOGGING: bool = false; @@ -24,5 +21,5 @@ pub unsafe extern "C" fn Java_com_securevale_rasp_android_native_SecureApp_initJ _env: JNIEnv, _class: JClass, ) { - logging::init_logger(); + common::logging::init_logger(); } diff --git a/native/src/root.rs b/native/src/root.rs new file mode 100644 index 0000000..354f7c0 --- /dev/null +++ b/native/src/root.rs @@ -0,0 +1,3 @@ +mod packages; +mod properties; +mod superuser; diff --git a/native/src/root/packages.rs b/native/src/root/packages.rs new file mode 100644 index 0000000..9f33c1d --- /dev/null +++ b/native/src/root/packages.rs @@ -0,0 +1,26 @@ +use jni::objects::{JClass, JObject}; +use jni::sys::jboolean; +use jni::JNIEnv; + +use crate::common::package::{has_root_cloaking_packages, has_root_packages}; + +// hasRootAppPackages +#[no_mangle] +pub unsafe extern "C" fn Java_com_securevale_rasp_android_root_checks_AppsChecks_r<'a>( + mut env: JNIEnv<'a>, + _class: JClass, + context: JObject<'a>, +) -> jboolean { + let result = has_root_packages(&mut env, &context); + u8::from(result) +} + +// hasRootCloakingAppPackages +#[no_mangle] +pub unsafe extern "C" fn Java_com_securevale_rasp_android_root_checks_AppsChecks_k<'a>( + mut env: JNIEnv<'a>, + _class: JClass, + context: JObject<'a>, +) -> jboolean { + u8::from(has_root_cloaking_packages(&mut env, &context)) +} diff --git a/native/src/root/properties.rs b/native/src/root/properties.rs new file mode 100644 index 0000000..1706ba6 --- /dev/null +++ b/native/src/root/properties.rs @@ -0,0 +1,13 @@ +use crate::common::property; +use jni::objects::JClass; +use jni::sys::jboolean; +use jni::JNIEnv; + +// hasRootProperties +#[no_mangle] +pub unsafe extern "C" fn Java_com_securevale_rasp_android_root_checks_PropertyChecks_y( + mut env: JNIEnv, + _class: JClass, +) -> jboolean { + u8::from(property::root_properties_found(&mut env)) +} diff --git a/native/src/root/superuser.rs b/native/src/root/superuser.rs new file mode 100644 index 0000000..772cad0 --- /dev/null +++ b/native/src/root/superuser.rs @@ -0,0 +1,231 @@ +#![allow(non_snake_case)] + +use jni::objects::{JClass, JObject, JObjectArray, JString, JValue}; +use jni::sys::jboolean; +use jni::JNIEnv; +#[cfg(debug_assertions)] +use log::LevelFilter::Debug; + +use crate::common::build::get_build_config_value; +use crate::common::files::{has_busybox_files, has_su_files, NON_WRITABLE_PATHS}; +#[cfg(debug_assertions)] +use crate::common::logging::log_android; +use crate::{common::build, common::util}; + +// isSuperUser +#[no_mangle] +pub unsafe extern "C" fn Java_com_securevale_rasp_android_root_checks_DeviceChecks_p( + mut env: JNIEnv, + _class: JClass, +) -> jboolean { + let has_su_files = has_su_files(); + + let has_busybox_files = has_busybox_files(); + + let runtime_clz = env.find_class("java/lang/Runtime").unwrap(); + + let runtime_obj = JObject::try_from( + env.call_static_method(runtime_clz, "getRuntime", "()Ljava/lang/Runtime;", &[]) + .unwrap(), + ) + .unwrap(); + + let exec = env.call_method( + runtime_obj, + "exec", + "(Ljava/lang/String;)Ljava/lang/Process;", + &[JValue::Object(&JObject::from( + env.new_string("su").unwrap(), + ))], + ); + + let isSu = exec.is_ok(); + + if !isSu { + util::ignore_error(&mut env); + } + + let result = has_su_files || isSu || has_busybox_files; + + u8::from(result) +} + +// hasTestTags +#[no_mangle] +pub unsafe extern "C" fn Java_com_securevale_rasp_android_root_checks_DeviceChecks_c( + mut env: JNIEnv, + _class: JClass, +) -> jboolean { + let tags = get_build_config_value(&mut env, build::TAGS).contains("test-keys"); + let fingerprint = + get_build_config_value(&mut env, build::FINGERPRINT).contains("genric.*test-keys"); + let display = get_build_config_value(&mut env, build::DISPLAY).contains(".*test-keys"); + + u8::from(tags || fingerprint || display) +} + +// hasWritableNonWritablePaths +#[no_mangle] +pub unsafe extern "C" fn Java_com_securevale_rasp_android_root_checks_FileChecks_w( + mut env: JNIEnv, + _class: JClass, +) -> jboolean { + let runtime_clz = env.find_class("java/lang/Runtime").unwrap(); + + let runtime_obj = JObject::try_from( + env.call_static_method(runtime_clz, "getRuntime", "()Ljava/lang/Runtime;", &[]) + .unwrap(), + ) + .unwrap(); + + let exec = env.call_method( + runtime_obj, + "exec", + "(Ljava/lang/String;)Ljava/lang/Process;", + &[JValue::Object(&JObject::from( + env.new_string("mount").unwrap(), + ))], + ); + + if exec.is_err() { + #[cfg(debug_assertions)] + log_android( + Debug, + format!("RW paths: {}", "process exec failed").as_str(), + ); + return u8::from(false); + } + + let process_obj = JObject::try_from(exec.unwrap()).unwrap(); + + let input_stream_obj = JObject::try_from( + env.call_method( + process_obj, + "getInputStream", + "()Ljava/io/InputStream;", + &[], + ) + .unwrap(), + ) + .unwrap(); + + if input_stream_obj.is_null() { + #[cfg(debug_assertions)] + log_android(Debug, format!("RW paths: {}", "null").as_str()); + return u8::from(false); + } + + let scanner_clz = env.find_class("java/util/Scanner").unwrap(); + + let scanner_obj = &env + .new_object( + &scanner_clz, + "(Ljava/io/InputStream;)V", + &[JValue::Object(&input_stream_obj)], + ) + .unwrap(); + + let prop = env.call_method( + scanner_obj, + "useDelimiter", + "(Ljava/lang/String;)Ljava/util/Scanner;", + &[JValue::Object(&JObject::from( + env.new_string("\\A").unwrap(), + ))], + ); + + let prop_val = env.call_method( + JObject::try_from(prop.unwrap()).unwrap(), + "next", + "()Ljava/lang/String;", + &[], + ); + + let mountRead = env.call_method( + JObject::try_from(prop_val.unwrap()).unwrap(), + "split", + "(Ljava/lang/String;)[Ljava/lang/String;", + &[JValue::Object(&JObject::from( + env.new_string("\n").unwrap(), + ))], + ); + + let lines = JObjectArray::from(JObject::try_from(mountRead.unwrap()).unwrap()); + + if lines.is_null() { + return u8::from(false); + } + + let lines_length = env.get_array_length(&lines).unwrap(); + + for n in 0..lines_length { + let line = env.get_object_array_element(&lines, n); + + #[cfg(debug_assertions)] + { + let line_str = &JString::from(env.get_object_array_element(&lines, n).unwrap()); + + let line_to_log = env.get_string(line_str).unwrap(); + + log_android( + Debug, + format!("LINE: {}", line_to_log.to_str().unwrap()).as_str(), + ); + } + + let args_array = env.call_method( + line.unwrap(), + "split", + "(Ljava/lang/String;)[Ljava/lang/String;", + &[JValue::Object(&JObject::from(env.new_string(" ").unwrap()))], + ); + + let args = JObjectArray::from(JObject::try_from(args_array.unwrap()).unwrap()); + + let args_length = env.get_array_length(&args).unwrap(); + + if args_length < 6 { + // Not enough options per line, just skip. + continue; + } + + let mountPointString = &JString::from(env.get_object_array_element(&args, 2).unwrap()); + + let mountPoint = env.get_string(mountPointString).unwrap(); + + #[cfg(debug_assertions)] + log_android( + Debug, + format!("MOUNT POINT {}", mountPoint.to_str().unwrap()).as_str(), + ); + + let mountOptionsString = &JString::from(env.get_object_array_element(&args, 5).unwrap()); + + let mountOptions = env.get_string(mountOptionsString).unwrap(); + + #[cfg(debug_assertions)] + log_android( + Debug, + format!("MOUNT OPTIONS {}", mountOptions.to_str().unwrap()).as_str(), + ); + + for suspicious_path in NON_WRITABLE_PATHS.iter() { + if mountPoint + .to_str() + .unwrap() + .eq_ignore_ascii_case(suspicious_path) + { + let mut correctedMountOptions = mountOptions.to_str().unwrap().replace('(', ""); + correctedMountOptions = correctedMountOptions.replace(')', ""); + + for option in correctedMountOptions.split(',') { + if option.eq_ignore_ascii_case("rw") { + return u8::from(true); + } + } + } + } + } + + u8::from(false) +} diff --git a/rasp/build.gradle.kts b/rasp/build.gradle.kts index eae5a99..3c2eb6a 100644 --- a/rasp/build.gradle.kts +++ b/rasp/build.gradle.kts @@ -94,8 +94,8 @@ fun configureDokka(dokkaTask: DokkaTask, outputDir: String) = dokkaTask.apply { mavenPublishing { group = "com.securevale.rasp" - version = "0.5.0" - coordinates("com.securevale", "rasp-android", "0.5.0") + version = "0.6.0" + coordinates("com.securevale", "rasp-android", "0.6.0") publishToMavenCentral(SonatypeHost.S01, true) signAllPublications() diff --git a/rasp/src/main/java/com/securevale/rasp/android/api/SecureAppChecker.kt b/rasp/src/main/java/com/securevale/rasp/android/api/SecureAppChecker.kt index e9d9214..1aeb90f 100644 --- a/rasp/src/main/java/com/securevale/rasp/android/api/SecureAppChecker.kt +++ b/rasp/src/main/java/com/securevale/rasp/android/api/SecureAppChecker.kt @@ -70,11 +70,13 @@ class SecureAppChecker private constructor() { * @property context the Context used for configuring checks. * @property checkEmulator whether emulator checks should be triggered. * @property checkDebugger whether checks for debug should be triggered. + * @property checkRoot whether checks for root should be triggered. */ class Builder( private val context: Context, private val checkEmulator: Boolean = false, - private val checkDebugger: Boolean = false + private val checkDebugger: Boolean = false, + private val checkRoot: Boolean = false ) { /** @@ -90,7 +92,7 @@ class SecureAppChecker private constructor() { * @return the configured [SecureAppChecker] instance. */ fun build() = SecureAppChecker().apply { - mediator = ChecksMediator(context, checkEmulator, checkDebugger) + mediator = ChecksMediator(context, checkEmulator, checkDebugger, checkRoot) } } } diff --git a/rasp/src/main/java/com/securevale/rasp/android/api/result/Checks.kt b/rasp/src/main/java/com/securevale/rasp/android/api/result/Checks.kt index 63ebdd2..090f9e0 100644 --- a/rasp/src/main/java/com/securevale/rasp/android/api/result/Checks.kt +++ b/rasp/src/main/java/com/securevale/rasp/android/api/result/Checks.kt @@ -34,3 +34,16 @@ enum class EmulatorChecks : CheckType { CPU, Modules } + +/** + * The enum containing all root checks. + */ +enum class RootChecks : CheckType { + RootCheck, + SuUser, + TestTags, + RootApps, + RootCloakingApps, + WritablePaths, + SuspiciousProperties +} diff --git a/rasp/src/main/java/com/securevale/rasp/android/api/result/Result.kt b/rasp/src/main/java/com/securevale/rasp/android/api/result/Result.kt index a3a70ba..3d8d8b1 100644 --- a/rasp/src/main/java/com/securevale/rasp/android/api/result/Result.kt +++ b/rasp/src/main/java/com/securevale/rasp/android/api/result/Result.kt @@ -6,6 +6,7 @@ package com.securevale.rasp.android.api.result sealed class Result { data object EmulatorFound : Result() data object DebuggerEnabled : Result() + data object Rooted : Result() data object Secure : Result() } diff --git a/rasp/src/main/java/com/securevale/rasp/android/check/ChecksMediator.kt b/rasp/src/main/java/com/securevale/rasp/android/check/ChecksMediator.kt index b3de91f..e35e536 100644 --- a/rasp/src/main/java/com/securevale/rasp/android/check/ChecksMediator.kt +++ b/rasp/src/main/java/com/securevale/rasp/android/check/ChecksMediator.kt @@ -7,19 +7,23 @@ import com.securevale.rasp.android.api.result.DebuggerChecks import com.securevale.rasp.android.api.result.EmulatorChecks import com.securevale.rasp.android.api.result.ExtendedResult import com.securevale.rasp.android.api.result.Result +import com.securevale.rasp.android.api.result.RootChecks import com.securevale.rasp.android.debugger.DebuggerCheck import com.securevale.rasp.android.emulator.EmulatorCheck +import com.securevale.rasp.android.root.RootCheck /** * Class that acts as a mediator between the public's API of the library and its internals. * @param context the Context used for checks configuration. * @param emulator whether emulator checks should be triggered. * @param debugger whether checks for debug should be triggered. + * @param root whether checks for root should be triggered. */ class ChecksMediator( context: Context, emulator: Boolean, - debugger: Boolean + debugger: Boolean, + root: Boolean ) { /** @@ -32,9 +36,15 @@ class ChecksMediator( */ private var debugCheck: DebuggerCheck? = null + /** + * The root check instance. + */ + private var rootCheck: RootCheck? = null + init { if (emulator) emulatorCheck = EmulatorCheck(context) if (debugger) debugCheck = DebuggerCheck(context) + if (root) rootCheck = RootCheck(context) } /** @@ -51,6 +61,7 @@ class ChecksMediator( val internalSubscriber = getSubscriber(vulnerabilitiesOnly, subscriber) emulatorCheck?.check(checkOnlyFor, true, internalSubscriber) debugCheck?.check(checkOnlyFor, true, internalSubscriber) + rootCheck?.check(checkOnlyFor, true, internalSubscriber) } /** @@ -81,6 +92,14 @@ class ChecksMediator( debugger?.vulnerabilityFound() ?: false ) ) + + val root = rootCheck?.check(checkOnlyFor) + internalSubscriber.onCheck( + ExtendedResult( + RootChecks.RootCheck, + root?.vulnerabilityFound() ?: false + ) + ) } /** @@ -94,6 +113,9 @@ class ChecksMediator( debugCheck?.check()?.vulnerabilityFound() ?: false -> Result.DebuggerEnabled + rootCheck?.check()?.vulnerabilityFound() + ?: false -> Result.Rooted + else -> Result.Secure } @@ -104,13 +126,11 @@ class ChecksMediator( * @return the wrapped subscriber. */ private fun getSubscriber(vulnerabilitiesOnly: Boolean, externalSubscriber: CheckSubscriber) = - object : CheckSubscriber { - override fun onCheck(result: ExtendedResult) { - if (vulnerabilitiesOnly) { - if (result.vulnerable) externalSubscriber.onCheck(result) - } else { - externalSubscriber.onCheck(result) - } + CheckSubscriber { result -> + if (vulnerabilitiesOnly) { + if (result.vulnerable) externalSubscriber.onCheck(result) + } else { + externalSubscriber.onCheck(result) } } } diff --git a/rasp/src/main/java/com/securevale/rasp/android/root/RootCheck.kt b/rasp/src/main/java/com/securevale/rasp/android/root/RootCheck.kt new file mode 100644 index 0000000..9eaa6dc --- /dev/null +++ b/rasp/src/main/java/com/securevale/rasp/android/root/RootCheck.kt @@ -0,0 +1,66 @@ +package com.securevale.rasp.android.root + +import android.content.Context +import com.securevale.rasp.android.api.result.CheckType +import com.securevale.rasp.android.api.result.RootChecks.RootApps +import com.securevale.rasp.android.api.result.RootChecks.RootCheck +import com.securevale.rasp.android.api.result.RootChecks.RootCloakingApps +import com.securevale.rasp.android.api.result.RootChecks.SuUser +import com.securevale.rasp.android.api.result.RootChecks.SuspiciousProperties +import com.securevale.rasp.android.api.result.RootChecks.TestTags +import com.securevale.rasp.android.api.result.RootChecks.WritablePaths +import com.securevale.rasp.android.check.ProbabilityCheck +import com.securevale.rasp.android.check.WrappedCheckResult +import com.securevale.rasp.android.check.wrappedCheck +import com.securevale.rasp.android.root.checks.AppsChecks.hasRootAppPackages +import com.securevale.rasp.android.root.checks.AppsChecks.hasRootCloakingAppPackages +import com.securevale.rasp.android.root.checks.DeviceChecks.hasTestTags +import com.securevale.rasp.android.root.checks.DeviceChecks.isSuperUser +import com.securevale.rasp.android.root.checks.FileChecks.hasWritableNonWritablePaths +import com.securevale.rasp.android.root.checks.PropertyChecks.hasRootProperties + +/** + * Root detection check. + * + * @property context the app's Context. + */ +@PublishedApi +internal class RootCheck(private val context: Context) : ProbabilityCheck() { + + override val checksMap: Map WrappedCheckResult> = mapOf( + SuUser to ::checkIsSuperUser, + TestTags to ::checkTestTags, + RootApps to ::rootApps, + RootCloakingApps to ::rootCloakingApps, + WritablePaths to ::nonWritablePathsAreWritable, + SuspiciousProperties to ::hasSuspiciousProperties + ) + + override val threshold: Int = 10 + + override val checkType: String = RootCheck::class.java.simpleName + + private fun checkIsSuperUser() = wrappedCheck(10, SuUser) { + isSuperUser(context) + } + + private fun checkTestTags() = wrappedCheck(3, TestTags) { + hasTestTags() + } + + private fun rootApps() = wrappedCheck(10, RootApps) { + hasRootAppPackages(context) + } + + private fun rootCloakingApps() = wrappedCheck(7, RootCloakingApps) { + hasRootCloakingAppPackages(context) + } + + private fun nonWritablePathsAreWritable() = wrappedCheck(10, WritablePaths) { + hasWritableNonWritablePaths() + } + + private fun hasSuspiciousProperties() = wrappedCheck(10, SuspiciousProperties) { + hasRootProperties() + } +} diff --git a/rasp/src/main/java/com/securevale/rasp/android/root/checks/AppsChecks.kt b/rasp/src/main/java/com/securevale/rasp/android/root/checks/AppsChecks.kt new file mode 100644 index 0000000..ef58d2e --- /dev/null +++ b/rasp/src/main/java/com/securevale/rasp/android/root/checks/AppsChecks.kt @@ -0,0 +1,15 @@ +package com.securevale.rasp.android.root.checks + +import android.content.Context + +/** + * An object that contains all app-related check functions. + */ +object AppsChecks { + + @JvmName("r") + external fun hasRootAppPackages(context: Context): Boolean + + @JvmName("k") + external fun hasRootCloakingAppPackages(context: Context): Boolean +} diff --git a/rasp/src/main/java/com/securevale/rasp/android/root/checks/DeviceChecks.kt b/rasp/src/main/java/com/securevale/rasp/android/root/checks/DeviceChecks.kt new file mode 100644 index 0000000..d9b9d1e --- /dev/null +++ b/rasp/src/main/java/com/securevale/rasp/android/root/checks/DeviceChecks.kt @@ -0,0 +1,15 @@ +package com.securevale.rasp.android.root.checks + +import android.content.Context + +/** + * An object that contains all device-related check functions. + */ +internal object DeviceChecks { + + @JvmName("p") + external fun isSuperUser(context: Context): Boolean + + @JvmName("c") + external fun hasTestTags(): Boolean +} diff --git a/rasp/src/main/java/com/securevale/rasp/android/root/checks/FileChecks.kt b/rasp/src/main/java/com/securevale/rasp/android/root/checks/FileChecks.kt new file mode 100644 index 0000000..400e640 --- /dev/null +++ b/rasp/src/main/java/com/securevale/rasp/android/root/checks/FileChecks.kt @@ -0,0 +1,10 @@ +package com.securevale.rasp.android.root.checks + +/** + * An object that contains all file-related check functions. + */ +object FileChecks { + + @JvmName("w") + external fun hasWritableNonWritablePaths(): Boolean +} diff --git a/rasp/src/main/java/com/securevale/rasp/android/root/checks/PropertyChecks.kt b/rasp/src/main/java/com/securevale/rasp/android/root/checks/PropertyChecks.kt new file mode 100644 index 0000000..54b2935 --- /dev/null +++ b/rasp/src/main/java/com/securevale/rasp/android/root/checks/PropertyChecks.kt @@ -0,0 +1,14 @@ +package com.securevale.rasp.android.root.checks + +/** + * An object that contains all properties-related check functions. + */ +internal object PropertyChecks { + + /** + * Checks whether there are any qemu properties found. + * @return result of the check. + */ + @JvmName("y") + external fun hasRootProperties(): Boolean +} diff --git a/sample-app/src/main/java/com/securevale/rasp/android/sample/CheckDetailsActivity.kt b/sample-app/src/main/java/com/securevale/rasp/android/sample/CheckDetailsActivity.kt index fae9e34..6c51483 100644 --- a/sample-app/src/main/java/com/securevale/rasp/android/sample/CheckDetailsActivity.kt +++ b/sample-app/src/main/java/com/securevale/rasp/android/sample/CheckDetailsActivity.kt @@ -9,6 +9,7 @@ import androidx.appcompat.app.AppCompatActivity import androidx.fragment.app.Fragment import com.securevale.rasp.android.sample.check.DebuggerCheckFragment import com.securevale.rasp.android.sample.check.EmulatorCheckFragment +import com.securevale.rasp.android.sample.check.RootCheckFragment class CheckDetailsActivity : AppCompatActivity() { @@ -34,6 +35,7 @@ class CheckDetailsActivity : AppCompatActivity() { private fun getCheckFragment(type: TestType): Fragment = when (type) { TestType.EMULATOR -> EmulatorCheckFragment() TestType.DEBUGGER -> DebuggerCheckFragment() + TestType.ROOT -> RootCheckFragment() } @Suppress("OVERRIDE_DEPRECATION") @@ -55,4 +57,4 @@ class CheckDetailsActivity : AppCompatActivity() { } } -enum class TestType { EMULATOR, DEBUGGER } \ No newline at end of file +enum class TestType { EMULATOR, DEBUGGER, ROOT } \ No newline at end of file diff --git a/sample-app/src/main/java/com/securevale/rasp/android/sample/MainActivity.kt b/sample-app/src/main/java/com/securevale/rasp/android/sample/MainActivity.kt index 44919fa..49cc712 100644 --- a/sample-app/src/main/java/com/securevale/rasp/android/sample/MainActivity.kt +++ b/sample-app/src/main/java/com/securevale/rasp/android/sample/MainActivity.kt @@ -27,5 +27,9 @@ class MainActivity : AppCompatActivity() { } private val items = - listOf(Check("Emulator", TestType.EMULATOR), Check("Debugger", TestType.DEBUGGER)) + listOf( + Check("Emulator", TestType.EMULATOR), + Check("Debugger", TestType.DEBUGGER), + Check("Root", TestType.ROOT) + ) } diff --git a/sample-app/src/main/java/com/securevale/rasp/android/sample/check/CheckResultAdapter.kt b/sample-app/src/main/java/com/securevale/rasp/android/sample/check/CheckResultAdapter.kt index 2e73b08..6f65cb8 100644 --- a/sample-app/src/main/java/com/securevale/rasp/android/sample/check/CheckResultAdapter.kt +++ b/sample-app/src/main/java/com/securevale/rasp/android/sample/check/CheckResultAdapter.kt @@ -22,6 +22,10 @@ class CheckResultAdapter : RecyclerView.Adapter() holder.bind(items[position]) } + fun clearResults(){ + items.clear() + } + fun updateItems(extendedResult: ExtendedResult) { items.add(extendedResult) notifyDataSetChanged() diff --git a/sample-app/src/main/java/com/securevale/rasp/android/sample/check/DebuggerCheckFragment.kt b/sample-app/src/main/java/com/securevale/rasp/android/sample/check/DebuggerCheckFragment.kt index fbbaf9b..e4a7145 100644 --- a/sample-app/src/main/java/com/securevale/rasp/android/sample/check/DebuggerCheckFragment.kt +++ b/sample-app/src/main/java/com/securevale/rasp/android/sample/check/DebuggerCheckFragment.kt @@ -11,7 +11,7 @@ import androidx.recyclerview.widget.RecyclerView import com.securevale.rasp.android.api.SecureAppChecker import com.securevale.rasp.android.sample.R -class DebuggerCheckFragment: Fragment() { +class DebuggerCheckFragment : Fragment() { private lateinit var checkResultsAdapter: CheckResultAdapter @@ -38,6 +38,8 @@ class DebuggerCheckFragment: Fragment() { val emulatorCheck = SecureAppChecker.Builder(requireContext(), checkDebugger = true) .build() + checkResultsAdapter.clearResults() + emulatorCheck.subscribe(granular = true) { checkResultsAdapter.updateItems(it) } diff --git a/sample-app/src/main/java/com/securevale/rasp/android/sample/check/EmulatorCheckFragment.kt b/sample-app/src/main/java/com/securevale/rasp/android/sample/check/EmulatorCheckFragment.kt index ee5d804..c3bf412 100644 --- a/sample-app/src/main/java/com/securevale/rasp/android/sample/check/EmulatorCheckFragment.kt +++ b/sample-app/src/main/java/com/securevale/rasp/android/sample/check/EmulatorCheckFragment.kt @@ -42,6 +42,8 @@ class EmulatorCheckFragment : Fragment() { val emulatorCheck = SecureAppChecker.Builder(requireContext(), checkEmulator = true) .build() + checkResultsAdapter.clearResults() + emulatorCheck.subscribe(granular = true) { checkResultsAdapter.updateItems(it) } diff --git a/sample-app/src/main/java/com/securevale/rasp/android/sample/check/RootCheckFragment.kt b/sample-app/src/main/java/com/securevale/rasp/android/sample/check/RootCheckFragment.kt new file mode 100644 index 0000000..2e0410a --- /dev/null +++ b/sample-app/src/main/java/com/securevale/rasp/android/sample/check/RootCheckFragment.kt @@ -0,0 +1,50 @@ +package com.securevale.rasp.android.sample.check + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.Button +import androidx.fragment.app.Fragment +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import com.securevale.rasp.android.api.SecureAppChecker +import com.securevale.rasp.android.sample.R + +class RootCheckFragment : Fragment() { + + private lateinit var checkResultsAdapter: CheckResultAdapter + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + return inflater.inflate(R.layout.fragment_check, container, false) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + checkResultsAdapter = CheckResultAdapter() + view.findViewById(R.id.checks_results).apply { + layoutManager = LinearLayoutManager(requireContext()) + adapter = checkResultsAdapter + } + + view.findViewById