v1.1.0: Stable!
This commit is contained in:
@@ -12,7 +12,7 @@ pub fn native_config() -> NativeConfig {
|
||||
pub(crate) struct NativeConfig {
|
||||
pub disable_bitmoji: bool,
|
||||
pub disable_metrics: bool,
|
||||
pub composer_hooks: bool,
|
||||
pub valdi_hooks: bool,
|
||||
pub custom_emoji_font_path: Option<String>,
|
||||
}
|
||||
|
||||
@@ -39,7 +39,7 @@ impl NativeConfig {
|
||||
Ok(Self {
|
||||
disable_bitmoji: get_boolean!("disableBitmoji"),
|
||||
disable_metrics: get_boolean!("disableMetrics"),
|
||||
composer_hooks: get_boolean!("composerHooks"),
|
||||
valdi_hooks: get_boolean!("valdiHooks"),
|
||||
custom_emoji_font_path: get_string!("customEmojiFontPath"),
|
||||
})
|
||||
}
|
||||
@@ -79,4 +79,4 @@ pub fn load_config(mut env: JNIEnv, _class: JObject, obj: JObject) {
|
||||
);
|
||||
|
||||
info!("Config loaded {:?}", native_config());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,19 +15,17 @@ mod secstrings;
|
||||
|
||||
use android_logger::Config;
|
||||
use log::LevelFilter;
|
||||
use modules::{composer_hook, custom_font_hook, duplex_hook, fstat_hook, linker_hook, sqlite_hook, unary_call_hook};
|
||||
use modules::{valdi_hook, custom_font_hook, duplex_hook, fstat_hook, linker_hook, sqlite_hook, unary_call_hook};
|
||||
|
||||
use jni::{JNIEnv, JavaVM, NativeMethod};
|
||||
use jni::objects::{JObject, JString, JClass, JValue};
|
||||
use jni::sys::{jint, jstring, JNI_VERSION_1_6, jboolean, JNI_FALSE, JNI_TRUE};
|
||||
use sha2::{Digest, Sha256};
|
||||
use std::ffi::c_void;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use once_cell::sync::Lazy;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Mutex;
|
||||
|
||||
static IS_VERIFIED: AtomicBool = AtomicBool::new(false);
|
||||
static TEST_MODE: AtomicBool = AtomicBool::new(false);
|
||||
static IN_LOGIN_SIGNUP: AtomicBool = AtomicBool::new(false);
|
||||
static CHECKSUMS: Lazy<Mutex<HashMap<String, u32>>> = Lazy::new(|| Mutex::new(HashMap::new()));
|
||||
@@ -37,6 +35,8 @@ struct BlockerDecision {
|
||||
reason: &'static str,
|
||||
keyword: Option<String>,
|
||||
keyword_context: Option<&'static str>,
|
||||
match_type: Option<&'static str>,
|
||||
match_value: Option<String>,
|
||||
}
|
||||
|
||||
#[allow(non_snake_case)]
|
||||
@@ -65,11 +65,6 @@ pub extern "system" fn JNI_OnLoad(_vm: JavaVM, _: *mut c_void) -> jint {
|
||||
env.register_native_methods(
|
||||
native_lib_class,
|
||||
&[
|
||||
NativeMethod {
|
||||
name: "verifyKey".into(),
|
||||
sig: "(Ljava/lang/String;)Z".into(),
|
||||
fn_ptr: verifyKey as *mut c_void,
|
||||
},
|
||||
NativeMethod {
|
||||
name: "preInit".into(),
|
||||
sig: "()V".into(),
|
||||
@@ -96,20 +91,20 @@ pub extern "system" fn JNI_OnLoad(_vm: JavaVM, _: *mut c_void) -> jint {
|
||||
fn_ptr: sqlite_hook::lock_database as *mut c_void,
|
||||
},
|
||||
NativeMethod {
|
||||
name: "setComposerLoader".into(),
|
||||
name: "setValdiLoader".into(),
|
||||
sig: "(Ljava/lang/String;)V".into(),
|
||||
fn_ptr: composer_hook::set_composer_loader as *mut c_void,
|
||||
},
|
||||
NativeMethod {
|
||||
name: "composerEval".into(),
|
||||
sig: "(Ljava/lang/String;)Ljava/lang/String;".into(),
|
||||
fn_ptr: composer_hook::composer_eval as *mut c_void,
|
||||
fn_ptr: valdi_hook::set_valdi_loader as *mut c_void,
|
||||
},
|
||||
NativeMethod {
|
||||
name: "evaluateEndpointNative".into(),
|
||||
sig: "(Ljava/lang/String;Ljava/lang/String;ZLme/eternal/purrfectsnap/nativelib/NativeDecision;)V".into(),
|
||||
fn_ptr: evaluateEndpoint as *mut c_void,
|
||||
},
|
||||
NativeMethod {
|
||||
name: "evaluateNetworkRequestNative".into(),
|
||||
sig: "(Ljava/lang/String;Lme/eternal/purrfectsnap/nativelib/NativeDecision;)V".into(),
|
||||
fn_ptr: evaluateNetworkRequest as *mut c_void,
|
||||
},
|
||||
NativeMethod {
|
||||
name: "shouldBlockDuplexClient".into(),
|
||||
sig: "(Ljava/lang/String;)Z".into(),
|
||||
@@ -203,7 +198,7 @@ fn init(mut env: JNIEnv, _class: JObject, signature_cache: JString) -> jstring {
|
||||
async_init!(
|
||||
duplex_hook::init(),
|
||||
unary_call_hook::init(),
|
||||
composer_hook::init(),
|
||||
valdi_hook::init(),
|
||||
sqlite_hook::init()
|
||||
);
|
||||
|
||||
@@ -219,114 +214,6 @@ fn init(mut env: JNIEnv, _class: JObject, signature_cache: JString) -> jstring {
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(non_snake_case)]
|
||||
fn verifyKey(mut env: JNIEnv, _class: JClass, key: JString) -> jboolean {
|
||||
fn bytes_to_hex(bytes: &[u8]) -> String {
|
||||
const LUT: &[u8; 16] = b"0123456789abcdef";
|
||||
let mut out = Vec::with_capacity(bytes.len() * 2);
|
||||
for &b in bytes {
|
||||
out.push(LUT[(b >> 4) as usize]);
|
||||
out.push(LUT[(b & 0x0f) as usize]);
|
||||
}
|
||||
String::from_utf8_lossy(&out).into_owned()
|
||||
}
|
||||
|
||||
fn normalize_hex(s: &str) -> String {
|
||||
s.chars()
|
||||
.filter(|c| c.is_ascii_hexdigit())
|
||||
.map(|c| c.to_ascii_lowercase())
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn get_pkg_cert_sha256_hex(env: &mut JNIEnv, pkg: &str) -> Option<String> {
|
||||
let at = env.find_class("android/app/ActivityThread").ok()?;
|
||||
let app_obj = env
|
||||
.call_static_method(at, "currentApplication", "()Landroid/app/Application;", &[])
|
||||
.ok()?
|
||||
.l()
|
||||
.ok()?;
|
||||
if app_obj.is_null() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let pm = env
|
||||
.call_method(&app_obj, "getPackageManager", "()Landroid/content/pm/PackageManager;", &[])
|
||||
.ok()?
|
||||
.l()
|
||||
.ok()?;
|
||||
|
||||
let pkg_j = env.new_string(pkg).ok()?.into();
|
||||
let flags = 0x08000000i32; // PackageManager.GET_SIGNING_CERTIFICATES (API 28+)
|
||||
let info = env
|
||||
.call_method(
|
||||
&pm,
|
||||
"getPackageInfo",
|
||||
"(Ljava/lang/String;I)Landroid/content/pm/PackageInfo;",
|
||||
&[JValue::Object(&pkg_j), JValue::Int(flags)],
|
||||
)
|
||||
.ok()?
|
||||
.l()
|
||||
.ok()?;
|
||||
|
||||
let signing_info = env
|
||||
.get_field(&info, "signingInfo", "Landroid/content/pm/SigningInfo;")
|
||||
.ok()?
|
||||
.l()
|
||||
.ok()?;
|
||||
if signing_info.is_null() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let signers = env
|
||||
.call_method(
|
||||
&signing_info,
|
||||
"getApkContentsSigners",
|
||||
"()[Landroid/content/pm/Signature;",
|
||||
&[],
|
||||
)
|
||||
.ok()?
|
||||
.l()
|
||||
.ok()?;
|
||||
let signers_arr = jni::objects::JObjectArray::from(signers);
|
||||
let first = env.get_object_array_element(&signers_arr, 0).ok()?;
|
||||
let sig_bytes_obj = env
|
||||
.call_method(&first, "toByteArray", "()[B", &[])
|
||||
.ok()?
|
||||
.l()
|
||||
.ok()?;
|
||||
let sig_bytes = env
|
||||
.convert_byte_array(jni::objects::JByteArray::from(sig_bytes_obj))
|
||||
.ok()?;
|
||||
let digest = Sha256::digest(&sig_bytes);
|
||||
Some(bytes_to_hex(&digest))
|
||||
}
|
||||
|
||||
// Harden the check: the provided value must match the module APK signing cert SHA-256.
|
||||
// This prevents trivial re-signing/repacking from passing verification without patching native code.
|
||||
let expected_cert = normalize_hex(
|
||||
&env.get_string(&key)
|
||||
.ok()
|
||||
.map(|s| s.to_string_lossy().into_owned())
|
||||
.unwrap_or_default(),
|
||||
);
|
||||
if expected_cert.is_empty() {
|
||||
return JNI_FALSE;
|
||||
}
|
||||
|
||||
let actual_cert = get_pkg_cert_sha256_hex(&mut env, "me.eternal.purrfectsnap")
|
||||
.map(|s| normalize_hex(&s))
|
||||
.unwrap_or_default();
|
||||
if actual_cert.is_empty() {
|
||||
return JNI_FALSE;
|
||||
}
|
||||
|
||||
if expected_cert == actual_cert {
|
||||
IS_VERIFIED.store(true, Ordering::Relaxed);
|
||||
JNI_TRUE
|
||||
} else {
|
||||
JNI_FALSE
|
||||
}
|
||||
}
|
||||
|
||||
fn find_keyword(paths: &[&str], keywords: &[String]) -> Option<String> {
|
||||
for path in paths {
|
||||
@@ -347,76 +234,76 @@ fn evaluate_endpoint_logic(
|
||||
has_attestation: bool,
|
||||
) -> BlockerDecision {
|
||||
let targets = [uri, arg0];
|
||||
let detection_keyword = find_keyword(&targets, &config.detection_keywords);
|
||||
let snap_security_block = targets.iter().any(|t| {
|
||||
let lower = t.to_lowercase();
|
||||
lower.starts_with("/snap.security") && !lower.starts_with("/snap.security.argosservice")
|
||||
});
|
||||
let block_convo_safety_prompt = targets.iter().any(|t| {
|
||||
t.eq_ignore_ascii_case("/snapchat.abuse.conversationsafety.conversationsafetyservice/getconvosafetyprompt")
|
||||
});
|
||||
let block_convo_safety_service = targets.iter().any(|t| {
|
||||
t.to_lowercase()
|
||||
.starts_with("/snapchat.abuse.conversationsafety.conversationsafetyservice/")
|
||||
});
|
||||
let block_device_state_report = targets.iter().any(|t| {
|
||||
t.eq_ignore_ascii_case("/snapchat.notif.devicestatereceiver/reportdevicestate")
|
||||
});
|
||||
if snap_security_block {
|
||||
return BlockerDecision {
|
||||
blocked: true,
|
||||
reason: "snap_security_block",
|
||||
keyword: detection_keyword,
|
||||
keyword_context: None,
|
||||
};
|
||||
}
|
||||
if block_convo_safety_prompt {
|
||||
return BlockerDecision {
|
||||
blocked: true,
|
||||
reason: "conversation_safety_prompt",
|
||||
keyword: detection_keyword,
|
||||
keyword_context: None,
|
||||
};
|
||||
}
|
||||
if block_convo_safety_service {
|
||||
return BlockerDecision {
|
||||
blocked: true,
|
||||
reason: "conversation_safety_service",
|
||||
keyword: detection_keyword,
|
||||
keyword_context: None,
|
||||
};
|
||||
}
|
||||
if block_device_state_report {
|
||||
return BlockerDecision {
|
||||
blocked: true,
|
||||
reason: "notif_report_device_state",
|
||||
keyword: detection_keyword,
|
||||
keyword_context: None,
|
||||
};
|
||||
}
|
||||
if find_keyword(&targets, &config.allowed_eps_active).is_some() {
|
||||
if let Some(matched) = find_keyword(&targets, &config.allowed_eps_active) {
|
||||
return BlockerDecision {
|
||||
blocked: false,
|
||||
reason: "allowed_whitelist",
|
||||
keyword: None,
|
||||
keyword_context: None,
|
||||
match_type: Some("allowed_whitelist"),
|
||||
match_value: Some(matched),
|
||||
};
|
||||
}
|
||||
let detection_keyword = find_keyword(&targets, &config.detection_keywords);
|
||||
if let Some(matched) = find_keyword(&targets, &config.risk_block_list) {
|
||||
return BlockerDecision {
|
||||
blocked: true,
|
||||
reason: "risk_blocklist",
|
||||
keyword: detection_keyword,
|
||||
keyword_context: None,
|
||||
match_type: Some("risk_blocklist"),
|
||||
match_value: Some(matched),
|
||||
};
|
||||
}
|
||||
|
||||
let reason = if detection_keyword.is_some() && has_attestation {
|
||||
"allowed_attestation_keyword"
|
||||
} else if detection_keyword.is_some() {
|
||||
"allowed_detection_keyword"
|
||||
} else {
|
||||
"allowed"
|
||||
};
|
||||
if let Some(keyword) = detection_keyword {
|
||||
let reason = if has_attestation {
|
||||
"attestation+keyword"
|
||||
} else {
|
||||
"detection_keyword"
|
||||
};
|
||||
return BlockerDecision {
|
||||
blocked: true,
|
||||
reason,
|
||||
keyword: Some(keyword.clone()),
|
||||
keyword_context: None,
|
||||
match_type: Some("detection_keyword"),
|
||||
match_value: Some(keyword),
|
||||
};
|
||||
}
|
||||
|
||||
let blocked = false;
|
||||
BlockerDecision {
|
||||
blocked,
|
||||
reason,
|
||||
keyword: detection_keyword,
|
||||
blocked: false,
|
||||
reason: "allowed",
|
||||
keyword: None,
|
||||
keyword_context: None,
|
||||
match_type: None,
|
||||
match_value: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn evaluate_network_request_logic(
|
||||
config: &config::BlockerConfig,
|
||||
url: &str,
|
||||
) -> BlockerDecision {
|
||||
if let Some(keyword) = find_keyword(&[url], &config.detection_keywords) {
|
||||
return BlockerDecision {
|
||||
blocked: true,
|
||||
reason: "detection_keyword",
|
||||
keyword: Some(keyword.clone()),
|
||||
keyword_context: None,
|
||||
match_type: Some("detection_keyword"),
|
||||
match_value: Some(keyword),
|
||||
};
|
||||
}
|
||||
|
||||
BlockerDecision {
|
||||
blocked: false,
|
||||
reason: "allowed",
|
||||
keyword: None,
|
||||
keyword_context: None,
|
||||
match_type: None,
|
||||
match_value: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -426,114 +313,41 @@ fn evaluate_auth_context_logic(
|
||||
attestation_required: bool,
|
||||
) -> BlockerDecision {
|
||||
let targets = [request_path];
|
||||
let detection_keyword = find_keyword(&targets, &config.detection_keywords);
|
||||
let snap_security_block = targets.iter().any(|t| {
|
||||
let lower = t.to_lowercase();
|
||||
lower.starts_with("/snap.security") && !lower.starts_with("/snap.security.argosservice")
|
||||
});
|
||||
let block_convo_safety_prompt = targets.iter().any(|t| {
|
||||
t.eq_ignore_ascii_case("/snapchat.abuse.conversationsafety.conversationsafetyservice/getconvosafetyprompt")
|
||||
});
|
||||
let block_convo_safety_service = targets.iter().any(|t| {
|
||||
t.to_lowercase()
|
||||
.starts_with("/snapchat.abuse.conversationsafety.conversationsafetyservice/")
|
||||
});
|
||||
let block_device_state_report = targets.iter().any(|t| {
|
||||
t.eq_ignore_ascii_case("/snapchat.notif.devicestatereceiver/reportdevicestate")
|
||||
});
|
||||
if snap_security_block {
|
||||
return BlockerDecision {
|
||||
blocked: true,
|
||||
reason: "snap_security_block",
|
||||
keyword: detection_keyword,
|
||||
keyword_context: None,
|
||||
};
|
||||
}
|
||||
if block_convo_safety_prompt {
|
||||
return BlockerDecision {
|
||||
blocked: true,
|
||||
reason: "conversation_safety_prompt",
|
||||
keyword: detection_keyword,
|
||||
keyword_context: None,
|
||||
};
|
||||
}
|
||||
if block_convo_safety_service {
|
||||
return BlockerDecision {
|
||||
blocked: true,
|
||||
reason: "conversation_safety_service",
|
||||
keyword: detection_keyword,
|
||||
keyword_context: None,
|
||||
};
|
||||
}
|
||||
if block_device_state_report {
|
||||
return BlockerDecision {
|
||||
blocked: true,
|
||||
reason: "notif_report_device_state",
|
||||
keyword: detection_keyword,
|
||||
keyword_context: None,
|
||||
};
|
||||
}
|
||||
if find_keyword(&targets, &config.allowed_eps_active).is_some() {
|
||||
if let Some(matched) = find_keyword(&targets, &config.allowed_eps_active) {
|
||||
return BlockerDecision {
|
||||
blocked: false,
|
||||
reason: "allowed_whitelist",
|
||||
keyword: detection_keyword,
|
||||
keyword_context: None,
|
||||
};
|
||||
}
|
||||
|
||||
let reason = if detection_keyword.is_some() && attestation_required {
|
||||
"allowed_attestation_keyword"
|
||||
} else if detection_keyword.is_some() {
|
||||
"allowed_detection_keyword"
|
||||
} else {
|
||||
"allowed"
|
||||
};
|
||||
|
||||
let blocked = false;
|
||||
BlockerDecision {
|
||||
blocked,
|
||||
reason,
|
||||
keyword: detection_keyword,
|
||||
keyword_context: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn evaluate_api_invocation_logic(
|
||||
config: &config::BlockerConfig,
|
||||
method_id: &str,
|
||||
annotations: &str,
|
||||
) -> BlockerDecision {
|
||||
// Hard allow specific API calls regardless of keyword matches
|
||||
const API_ALLOWLIST: &[&str] = &[
|
||||
"com.snap.identity.network.suggestion.bqsuggestfriendhttpinterface.fetchhighavailablesuggestedfriend",
|
||||
"com.snap.identity.network.suggestion.bqsuggestfriendhttpinterface.fetchlegacysuggestedfriend",
|
||||
];
|
||||
let method_lower = method_id.to_lowercase();
|
||||
if API_ALLOWLIST.iter().any(|m| method_lower == *m) {
|
||||
return BlockerDecision {
|
||||
blocked: false,
|
||||
reason: "allowed_api_whitelist",
|
||||
keyword: None,
|
||||
keyword_context: None,
|
||||
match_type: Some("allowed_whitelist"),
|
||||
match_value: Some(matched),
|
||||
};
|
||||
}
|
||||
let detection_keyword = find_keyword(&targets, &config.detection_keywords);
|
||||
if let Some(matched) = find_keyword(&targets, &config.risk_block_list) {
|
||||
return BlockerDecision {
|
||||
blocked: true,
|
||||
reason: "risk_blocklist",
|
||||
keyword: detection_keyword,
|
||||
keyword_context: None,
|
||||
match_type: Some("risk_blocklist"),
|
||||
match_value: Some(matched),
|
||||
};
|
||||
}
|
||||
|
||||
if let Some(keyword) = find_keyword(&[method_id], &config.detection_keywords) {
|
||||
return BlockerDecision {
|
||||
blocked: true,
|
||||
reason: "detection_keyword",
|
||||
keyword: Some(keyword),
|
||||
keyword_context: Some("method"),
|
||||
if let Some(keyword) = detection_keyword {
|
||||
let reason = if attestation_required {
|
||||
"attestation+keyword"
|
||||
} else {
|
||||
"detection_keyword"
|
||||
};
|
||||
}
|
||||
|
||||
if let Some(keyword) = find_keyword(&[annotations], &config.detection_keywords) {
|
||||
return BlockerDecision {
|
||||
blocked: true,
|
||||
reason: "detection_keyword",
|
||||
keyword: Some(keyword),
|
||||
keyword_context: Some("annotation"),
|
||||
reason,
|
||||
keyword: Some(keyword.clone()),
|
||||
keyword_context: None,
|
||||
match_type: Some("detection_keyword"),
|
||||
match_value: Some(keyword),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -542,6 +356,56 @@ fn evaluate_api_invocation_logic(
|
||||
reason: "allowed",
|
||||
keyword: None,
|
||||
keyword_context: None,
|
||||
match_type: None,
|
||||
match_value: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn evaluate_api_invocation_logic(
|
||||
config: &config::BlockerConfig,
|
||||
method_id: &str,
|
||||
annotations: &str,
|
||||
) -> BlockerDecision {
|
||||
if let Some(matched) = find_keyword(&[method_id], &config.allowed_eps_active) {
|
||||
return BlockerDecision {
|
||||
blocked: false,
|
||||
reason: "allowed_whitelist",
|
||||
keyword: None,
|
||||
keyword_context: None,
|
||||
match_type: Some("allowed_whitelist"),
|
||||
match_value: Some(matched),
|
||||
};
|
||||
}
|
||||
|
||||
if let Some(keyword) = find_keyword(&[method_id], &config.detection_keywords) {
|
||||
return BlockerDecision {
|
||||
blocked: true,
|
||||
reason: "detection_keyword",
|
||||
keyword: Some(keyword.clone()),
|
||||
keyword_context: Some("method"),
|
||||
match_type: Some("detection_keyword"),
|
||||
match_value: Some(keyword),
|
||||
};
|
||||
}
|
||||
|
||||
if let Some(keyword) = find_keyword(&[annotations], &config.detection_keywords) {
|
||||
return BlockerDecision {
|
||||
blocked: true,
|
||||
reason: "detection_keyword",
|
||||
keyword: Some(keyword.clone()),
|
||||
keyword_context: Some("annotation"),
|
||||
match_type: Some("detection_keyword"),
|
||||
match_value: Some(keyword),
|
||||
};
|
||||
}
|
||||
|
||||
BlockerDecision {
|
||||
blocked: false,
|
||||
reason: "allowed",
|
||||
keyword: None,
|
||||
keyword_context: None,
|
||||
match_type: None,
|
||||
match_value: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -553,27 +417,54 @@ fn write_blocker_decision(env: &mut JNIEnv, decision_obj: JObject, decision: &Bl
|
||||
decision.reason,
|
||||
decision.keyword.as_deref(),
|
||||
decision.keyword_context,
|
||||
decision.match_type,
|
||||
decision.match_value.as_deref(),
|
||||
);
|
||||
}
|
||||
|
||||
fn write_decision(env: &mut JNIEnv, decision: JObject, blocked: bool, reason: &str, keyword: Option<&str>, keyword_context: Option<&str>) {
|
||||
fn write_decision(
|
||||
env: &mut JNIEnv,
|
||||
decision: JObject,
|
||||
blocked: bool,
|
||||
reason: &str,
|
||||
keyword: Option<&str>,
|
||||
keyword_context: Option<&str>,
|
||||
match_type: Option<&str>,
|
||||
match_value: Option<&str>,
|
||||
) {
|
||||
let blocked_value = if blocked { JNI_TRUE } else { JNI_FALSE };
|
||||
env.set_field(&decision, "blocked", "Z", JValue::Bool(blocked_value)).expect("failed to set blocked");
|
||||
if let Err(err) = env.set_field(&decision, "blocked", "Z", JValue::Bool(blocked_value)) {
|
||||
error!("failed to set blocked: {:?}", err);
|
||||
}
|
||||
|
||||
set_string_field(env, &decision, "reason", Some(reason));
|
||||
set_string_field(env, &decision, "keyword", keyword);
|
||||
set_string_field(env, &decision, "keywordContext", keyword_context);
|
||||
set_string_field(env, &decision, "matchType", match_type);
|
||||
set_string_field(env, &decision, "matchValue", match_value);
|
||||
}
|
||||
|
||||
fn set_string_field(env: &mut JNIEnv, obj: &JObject, field: &str, value: Option<&str>) {
|
||||
if let Some(text) = value {
|
||||
let jstring = env.new_string(text).expect("failed to alloc string");
|
||||
let jstring = match env.new_string(text) {
|
||||
Ok(value) => value,
|
||||
Err(err) => {
|
||||
error!("failed to alloc string for {}: {:?}", field, err);
|
||||
return;
|
||||
}
|
||||
};
|
||||
let j_obj = JObject::from(jstring);
|
||||
env.set_field(obj, field, "Ljava/lang/String;", JValue::Object(&j_obj)).expect("failed to set string field");
|
||||
env.delete_local_ref(j_obj).expect("failed to delete local ref");
|
||||
if let Err(err) = env.set_field(obj, field, "Ljava/lang/String;", JValue::Object(&j_obj)) {
|
||||
error!("failed to set string field {}: {:?}", field, err);
|
||||
}
|
||||
if let Err(err) = env.delete_local_ref(j_obj) {
|
||||
error!("failed to delete local ref for {}: {:?}", field, err);
|
||||
}
|
||||
} else {
|
||||
let null_obj = JObject::null();
|
||||
env.set_field(obj, field, "Ljava/lang/String;", JValue::Object(&null_obj)).expect("failed to clear string field");
|
||||
if let Err(err) = env.set_field(obj, field, "Ljava/lang/String;", JValue::Object(&null_obj)) {
|
||||
error!("failed to clear string field {}: {:?}", field, err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -587,13 +478,7 @@ fn evaluateEndpoint(
|
||||
decision: JObject,
|
||||
) {
|
||||
if IN_LOGIN_SIGNUP.load(Ordering::Relaxed) {
|
||||
write_decision(&mut env, decision, false, "allowed_login_signup", None, None);
|
||||
return;
|
||||
}
|
||||
let is_verified = IS_VERIFIED.load(Ordering::Relaxed);
|
||||
let test_mode = TEST_MODE.load(Ordering::Relaxed);
|
||||
if !is_verified && !test_mode {
|
||||
write_decision(&mut env, decision, false, "allowed", None, None);
|
||||
write_decision(&mut env, decision, false, "allowed_login_signup", None, None, None, None);
|
||||
return;
|
||||
}
|
||||
let uri_str: String = env.get_string(&uri).unwrap().into();
|
||||
@@ -604,6 +489,24 @@ fn evaluateEndpoint(
|
||||
write_blocker_decision(&mut env, decision, &blocker_decision);
|
||||
}
|
||||
|
||||
#[allow(non_snake_case)]
|
||||
fn evaluateNetworkRequest(
|
||||
mut env: JNIEnv,
|
||||
_class: JClass,
|
||||
url: JString,
|
||||
decision: JObject,
|
||||
) {
|
||||
if IN_LOGIN_SIGNUP.load(Ordering::Relaxed) {
|
||||
write_decision(&mut env, decision, false, "allowed_login_signup", None, None, None, None);
|
||||
return;
|
||||
}
|
||||
let url_str: String = env.get_string(&url).unwrap().into();
|
||||
|
||||
let config = config::get_blocker_config();
|
||||
let blocker_decision = evaluate_network_request_logic(&config, &url_str);
|
||||
write_blocker_decision(&mut env, decision, &blocker_decision);
|
||||
}
|
||||
|
||||
#[allow(non_snake_case)]
|
||||
fn shouldBlockDuplexClient(
|
||||
mut env: JNIEnv,
|
||||
@@ -613,11 +516,6 @@ fn shouldBlockDuplexClient(
|
||||
if IN_LOGIN_SIGNUP.load(Ordering::Relaxed) {
|
||||
return JNI_FALSE;
|
||||
}
|
||||
let is_verified = IS_VERIFIED.load(Ordering::Relaxed);
|
||||
let test_mode = TEST_MODE.load(Ordering::Relaxed);
|
||||
if !is_verified && !test_mode {
|
||||
return JNI_FALSE;
|
||||
}
|
||||
|
||||
let path_str: String = env.get_string(&path).unwrap().into();
|
||||
|
||||
@@ -638,13 +536,7 @@ fn evaluateAuthContext(
|
||||
decision: JObject,
|
||||
) {
|
||||
if IN_LOGIN_SIGNUP.load(Ordering::Relaxed) {
|
||||
write_decision(&mut env, decision, false, "allowed_login_signup", None, None);
|
||||
return;
|
||||
}
|
||||
let is_verified = IS_VERIFIED.load(Ordering::Relaxed);
|
||||
let test_mode = TEST_MODE.load(Ordering::Relaxed);
|
||||
if !is_verified && !test_mode {
|
||||
write_decision(&mut env, decision, false, "allowed", None, None);
|
||||
write_decision(&mut env, decision, false, "allowed_login_signup", None, None, None, None);
|
||||
return;
|
||||
}
|
||||
let request_path_str: String = env.get_string(&request_path).unwrap().into();
|
||||
@@ -663,13 +555,7 @@ fn evaluateApiInvocation(
|
||||
decision: JObject,
|
||||
) {
|
||||
if IN_LOGIN_SIGNUP.load(Ordering::Relaxed) {
|
||||
write_decision(&mut env, decision, false, "allowed_login_signup", None, None);
|
||||
return;
|
||||
}
|
||||
let is_verified = IS_VERIFIED.load(Ordering::Relaxed);
|
||||
let test_mode = TEST_MODE.load(Ordering::Relaxed);
|
||||
if !is_verified && !test_mode {
|
||||
write_decision(&mut env, decision, false, "allowed", None, None);
|
||||
write_decision(&mut env, decision, false, "allowed_login_signup", None, None, None, None);
|
||||
return;
|
||||
}
|
||||
let method_id_str: String = env.get_string(&method_id).unwrap().into();
|
||||
@@ -681,9 +567,7 @@ fn evaluateApiInvocation(
|
||||
}
|
||||
|
||||
fn run_blocker_self_test(allow_unverified: bool) -> bool {
|
||||
if !IS_VERIFIED.load(Ordering::Relaxed) && !allow_unverified {
|
||||
return false;
|
||||
}
|
||||
let _ = allow_unverified;
|
||||
|
||||
let config = config::get_blocker_config();
|
||||
if config.allowed_eps_active.is_empty()
|
||||
@@ -704,22 +588,22 @@ fn run_blocker_self_test(allow_unverified: bool) -> bool {
|
||||
|
||||
let detection_path = format!("/self_test/{}", detection_sample);
|
||||
let detection_decision = evaluate_endpoint_logic(&config, &detection_path, "", false);
|
||||
if detection_decision.blocked {
|
||||
if !detection_decision.blocked {
|
||||
return false;
|
||||
}
|
||||
|
||||
let attestation_decision = evaluate_endpoint_logic(&config, &detection_path, "", true);
|
||||
if attestation_decision.blocked {
|
||||
if !attestation_decision.blocked {
|
||||
return false;
|
||||
}
|
||||
|
||||
let risk_decision = evaluate_endpoint_logic(&config, &risk_sample, "", false);
|
||||
if risk_decision.blocked {
|
||||
if !risk_decision.blocked {
|
||||
return false;
|
||||
}
|
||||
|
||||
let auth_detection = evaluate_auth_context_logic(&config, &detection_path, false);
|
||||
if auth_detection.blocked {
|
||||
if !auth_detection.blocked {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -10,17 +10,24 @@ def_hook!(
|
||||
i32,
|
||||
|fd: i32, statbuf: *mut libc::stat| {
|
||||
if let Ok(link) = fs::read_link("/proc/self/fd/".to_owned() + &fd.to_string()) {
|
||||
if let Some(filename) = link.file_name().map(|t| t.to_string_lossy()) {
|
||||
let config = native_config();
|
||||
if config.disable_metrics && filename.contains("files/blizzardv2/queues") {
|
||||
if libc::unlink((filename.to_owned() + "\0").as_ptr()) == -1 {
|
||||
warn!("Failed to unlink {}", filename);
|
||||
}
|
||||
let link_str = link.to_string_lossy();
|
||||
let config = native_config();
|
||||
if config.disable_metrics && link_str.contains("files/blizzardv2/queues") {
|
||||
if libc::unlink((link_str.to_string() + "\0").as_ptr()) == -1 {
|
||||
warn!("Failed to unlink {}", link_str);
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
if config.disable_bitmoji {
|
||||
if link_str.contains("com.snap.file_manager_4_SCContent") {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if config.disable_bitmoji && filename.contains("com.snap.file_manager_4_SCContent") {
|
||||
return -1;
|
||||
if link_str.contains("/files/file_manager/") {
|
||||
let lower = link_str.to_lowercase();
|
||||
if lower.contains("bitmoji") {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,5 +4,5 @@ pub mod duplex_hook;
|
||||
pub mod sqlite_hook;
|
||||
pub mod fstat_hook;
|
||||
pub mod unary_call_hook;
|
||||
pub mod composer_hook;
|
||||
pub mod custom_font_hook;
|
||||
pub mod valdi_hook;
|
||||
pub mod custom_font_hook;
|
||||
|
||||
@@ -104,7 +104,7 @@ def_hook!(
|
||||
pub fn init() {
|
||||
if let Some(signature) = sig::find_signature(
|
||||
&common::CLIENT_MODULE,
|
||||
"A8 03 1F F8 ?? 00 00 94 ?? ?? ?? 91", -0x48,
|
||||
"A8 03 1F F8 ?? ?? 00 94 ?? ?? ?? 91 ?? ?? ?? A9", -0x48,
|
||||
"0A 90 00 F0 3F F9", -0x37
|
||||
) {
|
||||
dobby_hook!(signature as *mut c_void, unary_call);
|
||||
|
||||
@@ -1 +1 @@
|
||||
pub mod composer_utils;
|
||||
pub mod valdi_utils;
|
||||
|
||||
141
native/rust/src/modules/util/valdi_utils.rs
Normal file
141
native/rust/src/modules/util/valdi_utils.rs
Normal file
@@ -0,0 +1,141 @@
|
||||
use std::{io::Error, string::FromUtf8Error};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ModuleTag {
|
||||
has_padding: bool,
|
||||
buffer: Vec<u8>,
|
||||
}
|
||||
|
||||
impl ModuleTag {
|
||||
pub fn new(has_padding: bool, buffer: Vec<u8>) -> ModuleTag {
|
||||
ModuleTag {
|
||||
has_padding,
|
||||
buffer,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_string(&self) -> Result<String, FromUtf8Error> {
|
||||
Ok(String::from_utf8(self.buffer.clone())?)
|
||||
}
|
||||
|
||||
pub fn get_has_padding(&self) -> bool {
|
||||
self.has_padding
|
||||
}
|
||||
|
||||
pub fn get_size(&self) -> usize {
|
||||
self.buffer.len()
|
||||
}
|
||||
|
||||
pub fn get_buffer(&self) -> &Vec<u8> {
|
||||
&self.buffer
|
||||
}
|
||||
|
||||
pub fn set_buffer(&mut self, buffer: Vec<u8>) {
|
||||
self.buffer = buffer;
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ValdiModule {
|
||||
tags: Vec<(ModuleTag, ModuleTag)>, // file name => file content
|
||||
}
|
||||
|
||||
impl ValdiModule {
|
||||
pub fn parse(buffer: Vec<u8>) -> Result<ValdiModule, Error> {
|
||||
let mut offset = 0;
|
||||
let magic = u32::from_be_bytes([buffer[offset], buffer[offset + 1], buffer[offset + 2], buffer[offset + 3]]);
|
||||
|
||||
offset += 4;
|
||||
|
||||
if magic != 0x33c60001 {
|
||||
return Err(Error::new(std::io::ErrorKind::InvalidData, "Invalid magic"));
|
||||
}
|
||||
|
||||
// skip content length
|
||||
offset += 4;
|
||||
|
||||
let mut tags = Vec::new();
|
||||
|
||||
loop {
|
||||
if offset >= buffer.len() {
|
||||
break;
|
||||
}
|
||||
|
||||
fn read_u32(buffer: &Vec<u8>, offset: &mut usize) -> Result<(u32, bool), Error> {
|
||||
let b1 = buffer[*offset] as u32;
|
||||
let b2 = buffer[*offset + 1] as u32;
|
||||
let b3 = buffer[*offset + 2] as u32;
|
||||
let b4 = (buffer[*offset + 3] & 0x7f) as u32;
|
||||
let has_padding = (buffer[*offset + 3] & 0x80) != 0;
|
||||
|
||||
*offset += 4;
|
||||
Ok((b1 | (b2 << 8) | (b3 << 16) | (b4 << 24), has_padding))
|
||||
}
|
||||
|
||||
let (tag_size, has_padding) = read_u32(&buffer, &mut offset)?;
|
||||
let tag_buffer = buffer[offset..offset + tag_size as usize].to_vec();
|
||||
offset += tag_size as usize;
|
||||
|
||||
let padding = 4 - (tag_size % 4);
|
||||
|
||||
if padding != 4 {
|
||||
offset += padding as usize;
|
||||
}
|
||||
|
||||
tags.push(ModuleTag::new(has_padding, tag_buffer));
|
||||
}
|
||||
|
||||
let tags = tags.chunks(2).map(|chunk| {
|
||||
(chunk[0].clone(), chunk[1].clone())
|
||||
}).collect();
|
||||
|
||||
Ok(ValdiModule {
|
||||
tags,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn to_bytes(&self) -> Vec<u8> {
|
||||
let mut tag_buffer = Vec::new();
|
||||
|
||||
fn write_u32(buffer: &mut Vec<u8>, value: u32, has_padding: bool) {
|
||||
buffer.push(value as u8);
|
||||
buffer.push(((value >> 8) & 0xff) as u8);
|
||||
buffer.push(((value >> 16) & 0xff) as u8);
|
||||
buffer.push(((value >> 24) & 0x7f) as u8 | if has_padding { 0x80 } else { 0x00 });
|
||||
}
|
||||
|
||||
fn write_tag(buffer: &mut Vec<u8>, tag: ModuleTag) {
|
||||
write_u32(buffer, tag.get_size() as u32, tag.get_has_padding());
|
||||
buffer.extend(tag.get_buffer());
|
||||
|
||||
let padding = 4 - (tag.get_size() % 4);
|
||||
|
||||
if padding != 4 {
|
||||
for _ in 0..padding {
|
||||
buffer.push(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (tag1, tag2) in &self.tags {
|
||||
write_tag(&mut tag_buffer, tag1.clone());
|
||||
write_tag(&mut tag_buffer, tag2.clone());
|
||||
}
|
||||
|
||||
let mut buffer = Vec::new();
|
||||
|
||||
buffer.extend_from_slice(&[0x33, 0xc6, 0, 1]);
|
||||
buffer.extend_from_slice(&(tag_buffer.len() as u32).to_le_bytes());
|
||||
buffer.extend(tag_buffer);
|
||||
|
||||
buffer
|
||||
}
|
||||
|
||||
pub fn get_tags(&self) -> Vec<(ModuleTag, ModuleTag)> {
|
||||
self.tags.clone()
|
||||
}
|
||||
|
||||
pub fn set_tags(&mut self, tags: Vec<(ModuleTag, ModuleTag)>) {
|
||||
self.tags = tags;
|
||||
}
|
||||
}
|
||||
113
native/rust/src/modules/valdi_hook.rs
Normal file
113
native/rust/src/modules/valdi_hook.rs
Normal file
@@ -0,0 +1,113 @@
|
||||
#![allow(dead_code, unused_imports)]
|
||||
|
||||
use super::util::valdi_utils::{ValdiModule, ModuleTag};
|
||||
use std::{collections::HashMap, ffi::c_void, sync::Mutex};
|
||||
use jni::{objects::JString, JNIEnv};
|
||||
use once_cell::sync::Lazy;
|
||||
use crate::{common, config, def_hook, dobby_hook, dobby_hook_sym, util::get_jni_string};
|
||||
|
||||
static AASSET_MAP: Lazy<Mutex<HashMap<usize, Vec<u8>>>> = Lazy::new(|| Mutex::new(HashMap::new()));
|
||||
static LOADER_DATA: Mutex<Option<String>> = Mutex::new(None);
|
||||
|
||||
def_hook!(
|
||||
aasset_get_length,
|
||||
i32,
|
||||
|arg0: *mut c_void| {
|
||||
if let Some(buffer) = AASSET_MAP.lock().unwrap().get(&(arg0 as usize)) {
|
||||
return buffer.len() as i32;
|
||||
}
|
||||
aasset_get_length_original.unwrap()(arg0)
|
||||
}
|
||||
);
|
||||
|
||||
def_hook!(
|
||||
aasset_get_buffer,
|
||||
*const c_void,
|
||||
|arg0: *mut c_void| {
|
||||
if let Some(buffer) = AASSET_MAP.lock().unwrap().get(&(arg0 as usize)) {
|
||||
return buffer.as_ptr() as *const c_void;
|
||||
}
|
||||
aasset_get_buffer_original.unwrap()(arg0)
|
||||
}
|
||||
);
|
||||
|
||||
def_hook!(
|
||||
aasset_manager_open,
|
||||
*mut c_void,
|
||||
|arg0: *mut c_void, arg1: *const u8, arg2: i32| {
|
||||
let handle = aasset_manager_open_original.unwrap()(arg0, arg1, arg2);
|
||||
|
||||
let path = std::ffi::CStr::from_ptr(arg1).to_str().unwrap_or_default();
|
||||
if !handle.is_null() && path.starts_with("bridge_observables") {
|
||||
let asset_buffer = aasset_get_buffer_original.unwrap()(handle);
|
||||
let asset_length = aasset_get_length_original.unwrap()(handle);
|
||||
debug!("asset buffer: {:p}, length: {}", asset_buffer, asset_length);
|
||||
|
||||
let loader_data = LOADER_DATA.lock().unwrap().clone().expect("No loader data");
|
||||
|
||||
let archive_buffer: Vec<u8> = std::slice::from_raw_parts(asset_buffer as *const u8, asset_length as usize).to_vec();
|
||||
let decompressed = zstd::stream::decode_all(&archive_buffer[..]).expect("Failed to decompress valdi archive");
|
||||
let mut valdi_module = ValdiModule::parse(decompressed).expect("Failed to parse valdi module");
|
||||
|
||||
let mut tags = valdi_module.get_tags();
|
||||
let mut new_tags = Vec::new();
|
||||
|
||||
for (tag1, _) in tags.iter_mut() {
|
||||
let name = tag1.to_string().unwrap_or_default();
|
||||
if !name.ends_with("src/utils/converter.js") {
|
||||
continue;
|
||||
}
|
||||
|
||||
let old_file_name = name.split_once(".").unwrap().0.to_owned() + rand::random::<u32>().to_string().as_str();
|
||||
tag1.set_buffer((old_file_name.to_owned() + ".js").as_bytes().to_vec());
|
||||
let original_module_path = path.split_once(".").unwrap().0.to_owned() + "/" + &old_file_name;
|
||||
|
||||
let hooked_module = format!("{};module.exports = require(\"{}\");", loader_data, original_module_path);
|
||||
|
||||
new_tags.push(
|
||||
(
|
||||
ModuleTag::new(true, name.as_bytes().to_vec()),
|
||||
ModuleTag::new(true, hooked_module.as_bytes().to_vec())
|
||||
)
|
||||
);
|
||||
|
||||
debug!("Valdi loader injected in {}", name);
|
||||
break;
|
||||
}
|
||||
|
||||
tags.extend(new_tags);
|
||||
valdi_module.set_tags(tags);
|
||||
|
||||
let compressed = valdi_module.to_bytes();
|
||||
let compressed = zstd::stream::encode_all(&compressed[..], 3).expect("Failed to compress");
|
||||
|
||||
AASSET_MAP.lock().unwrap().insert(handle as usize, compressed);
|
||||
}
|
||||
handle
|
||||
}
|
||||
);
|
||||
|
||||
def_hook!(
|
||||
aasset_close,
|
||||
c_void,
|
||||
|handle: *mut c_void| {
|
||||
AASSET_MAP.lock().unwrap().remove(&(handle as usize));
|
||||
aasset_close_original.unwrap()(handle)
|
||||
}
|
||||
);
|
||||
|
||||
pub fn set_valdi_loader(mut env: JNIEnv, _: *mut c_void, code: JString) {
|
||||
let new_code = get_jni_string(&mut env, code).expect("Failed to get loader code");
|
||||
LOADER_DATA.lock().unwrap().replace(new_code);
|
||||
}
|
||||
|
||||
pub fn init() {
|
||||
if !config::native_config().valdi_hooks {
|
||||
return
|
||||
}
|
||||
|
||||
dobby_hook_sym!("libandroid.so", "AAsset_getBuffer", aasset_get_buffer);
|
||||
dobby_hook_sym!("libandroid.so", "AAsset_getLength", aasset_get_length);
|
||||
dobby_hook_sym!("libandroid.so", "AAsset_close", aasset_close);
|
||||
dobby_hook_sym!("libandroid.so", "AAssetManager_open", aasset_manager_open);
|
||||
}
|
||||
@@ -4,6 +4,7 @@ extern crate libc;
|
||||
use jni::sys::{jboolean, JNI_FALSE, JNI_TRUE};
|
||||
use goldberg::goldberg_string;
|
||||
use std::{thread, time};
|
||||
use log::warn;
|
||||
use std::fs;
|
||||
use std::process;
|
||||
use crate::CHECKSUMS;
|
||||
@@ -14,12 +15,22 @@ fn check_for_debugger() {
|
||||
loop {
|
||||
let status = fs::read_to_string("/proc/self/status").unwrap_or_default();
|
||||
if !status.contains("TracerPid:\t0") {
|
||||
process::abort();
|
||||
if should_abort_on_tamper() {
|
||||
process::abort();
|
||||
} else {
|
||||
warn!("TracerPid detected; skipping abort under hooked environment");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
let maps = fs::read_to_string("/proc/self/maps").unwrap_or_default();
|
||||
if maps.contains("frida") || maps.contains("gumjs") || maps.contains("gdb") {
|
||||
process::abort();
|
||||
if should_abort_on_tamper() {
|
||||
process::abort();
|
||||
} else {
|
||||
warn!("Debugger markers detected; skipping abort under hooked environment");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
thread::sleep(time::Duration::from_secs(5));
|
||||
@@ -56,7 +67,12 @@ fn verify_library_checksum() {
|
||||
if let Some(abi) = get_abi() {
|
||||
if let Some(expected_checksum) = CHECKSUMS.lock().unwrap().get(&abi) {
|
||||
if checksum != *expected_checksum as u32 {
|
||||
process::abort();
|
||||
if should_abort_on_tamper() {
|
||||
process::abort();
|
||||
} else {
|
||||
warn!("Checksum mismatch; skipping abort under hooked environment");
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -66,6 +82,7 @@ fn verify_library_checksum() {
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn verify_key(key: &str) -> bool {
|
||||
// Transformed key verification
|
||||
let transformed_key = key.chars().rev().collect::<String>();
|
||||
@@ -75,6 +92,7 @@ pub fn verify_key(key: &str) -> bool {
|
||||
|
||||
// this function will be called from the JNI, it will verify the key and if it's correct, it will return true
|
||||
// otherwise it will return false
|
||||
#[allow(dead_code)]
|
||||
pub fn jni_verify_key(key: &str) -> jboolean {
|
||||
if verify_key(key) {
|
||||
JNI_TRUE
|
||||
@@ -94,3 +112,9 @@ fn get_library_path() -> Option<String> {
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn should_abort_on_tamper() -> bool {
|
||||
let maps = fs::read_to_string("/proc/self/maps").unwrap_or_default();
|
||||
let markers = ["lspatch", "lsposed", "xposed", "zygisk", "riru"];
|
||||
!markers.iter().any(|marker| maps.contains(marker))
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use std::sync::Mutex;
|
||||
use std::{fs::File, os::unix::io::AsRawFd, sync::Mutex};
|
||||
|
||||
use nix::libc;
|
||||
use procfs::process::MMPermissions;
|
||||
|
||||
use crate::mapped_lib::MappedLib;
|
||||
@@ -15,7 +16,46 @@ pub fn get_signatures() -> Vec<(String, Vec<usize>)> {
|
||||
SIGNATURE_CACHE.lock().unwrap().clone()
|
||||
}
|
||||
|
||||
pub fn find_signatures(module_base: usize, size: usize, pattern: &str, once: bool) -> Vec<usize> {
|
||||
fn read_region_bytes(start: usize, size: usize) -> Option<Vec<u8>> {
|
||||
let file = File::open("/proc/self/mem").ok();
|
||||
if let Some(file) = file {
|
||||
let fd = file.as_raw_fd();
|
||||
let mut buffer = vec![0u8; size];
|
||||
let mut offset = 0usize;
|
||||
|
||||
while offset < size {
|
||||
let read = unsafe {
|
||||
libc::pread(
|
||||
fd,
|
||||
buffer[offset..].as_mut_ptr() as *mut libc::c_void,
|
||||
(size - offset) as libc::size_t,
|
||||
(start + offset) as libc::off_t,
|
||||
)
|
||||
};
|
||||
if read < 0 {
|
||||
warn!(
|
||||
"Failed to read /proc/self/mem at {:#x}: {}",
|
||||
start,
|
||||
std::io::Error::last_os_error()
|
||||
);
|
||||
return None;
|
||||
}
|
||||
if read == 0 {
|
||||
break;
|
||||
}
|
||||
offset += read as usize;
|
||||
}
|
||||
|
||||
if offset == size {
|
||||
return Some(buffer);
|
||||
}
|
||||
warn!("Short read from /proc/self/mem at {:#x}: {} < {}", start, offset, size);
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
pub fn find_signatures(module_base: usize, bytes_buffer: &[u8], pattern: &str, once: bool) -> Vec<usize> {
|
||||
let mut results = Vec::new();
|
||||
let mut bytes = Vec::new();
|
||||
let mut mask = Vec::new();
|
||||
@@ -37,13 +77,13 @@ pub fn find_signatures(module_base: usize, size: usize, pattern: &str, once: boo
|
||||
}
|
||||
|
||||
let mut i = 0;
|
||||
let size = size - bytes.len();
|
||||
let size = bytes_buffer.len().saturating_sub(bytes.len());
|
||||
while i < size {
|
||||
let mut found = true;
|
||||
let mut j = 0;
|
||||
|
||||
while j < bytes.len() {
|
||||
if mask[j] == '?' || bytes[j] == unsafe { *(module_base as *const u8).offset(i as isize + j as isize) } {
|
||||
if mask[j] == '?' || bytes[j] == bytes_buffer[i + j] {
|
||||
j += 1;
|
||||
continue;
|
||||
}
|
||||
@@ -66,7 +106,7 @@ pub fn find_signatures(module_base: usize, size: usize, pattern: &str, once: boo
|
||||
|
||||
pub fn find_signature_executable(mapped_lib: &MappedLib, pattern: &str) -> Option<usize> {
|
||||
let executable_regions = mapped_lib.regions.iter().filter(|region| {
|
||||
region.perms.contains(MMPermissions::EXECUTE) && region.perms.contains(MMPermissions::READ)
|
||||
region.perms.contains(MMPermissions::EXECUTE)
|
||||
}).collect::<Vec<_>>();
|
||||
|
||||
for region in executable_regions {
|
||||
@@ -74,7 +114,14 @@ pub fn find_signature_executable(mapped_lib: &MappedLib, pattern: &str) -> Optio
|
||||
let module_base = region.start as usize;
|
||||
|
||||
if size > 0 {
|
||||
let results = find_signatures(module_base, size, pattern, true);
|
||||
let bytes_buffer = match read_region_bytes(module_base, size) {
|
||||
Some(buffer) => buffer,
|
||||
None => {
|
||||
warn!("Unable to read executable region: {:#x} - {:#x}", region.start, region.end);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
let results = find_signatures(module_base, &bytes_buffer, pattern, true);
|
||||
|
||||
if results.is_empty() {
|
||||
warn!("Signature not found in region: {:#x} - {:#x}", region.start, region.end);
|
||||
@@ -97,4 +144,4 @@ pub fn find_signature(mapped_lib: &MappedLib, _arm64_pattern: &str, _arm64_offse
|
||||
{
|
||||
return find_signature_executable(mapped_lib, _arm32_pattern).map(|address| (address as i64 + _arm32_offset) as usize);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,4 +5,4 @@ use jni::{objects::JString, JNIEnv};
|
||||
pub fn get_jni_string(env: &mut JNIEnv, obj: JString) -> Result<String, Box<dyn Error>> {
|
||||
let string = env.get_string(&obj)?;
|
||||
Ok(string.to_str()?.to_string())
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user