Major Update!
This features a new bypass, new theme, etc
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
use std::{error::Error, sync::Mutex};
|
||||
use jni::{objects::JObject, JNIEnv};
|
||||
use crate::util::get_jni_string;
|
||||
use crate::{secstrings, util::get_jni_string};
|
||||
|
||||
static NATIVE_CONFIG: Mutex<Option<NativeConfig>> = Mutex::new(None);
|
||||
|
||||
@@ -45,6 +45,34 @@ impl NativeConfig {
|
||||
}
|
||||
}
|
||||
|
||||
pub struct BlockerConfig {
|
||||
pub allowed_eps_active: Vec<String>,
|
||||
pub detection_keywords: Vec<String>,
|
||||
pub risk_block_list: Vec<String>,
|
||||
}
|
||||
|
||||
pub fn get_blocker_config() -> BlockerConfig {
|
||||
let config_str = include_str!("../../../config/config.json");
|
||||
let raw: serde_json::Value = serde_json::from_str(config_str).unwrap();
|
||||
|
||||
let allowed = raw["allowed_eps_active"].as_array().cloned().unwrap_or_default();
|
||||
|
||||
BlockerConfig {
|
||||
allowed_eps_active: allowed
|
||||
.into_iter()
|
||||
.filter_map(|value| value.as_str().map(|s| s.to_lowercase()))
|
||||
.collect(),
|
||||
detection_keywords: secstrings::get_detection_keywords()
|
||||
.into_iter()
|
||||
.map(|s| s.to_lowercase())
|
||||
.collect(),
|
||||
risk_block_list: secstrings::get_risk_block_list()
|
||||
.into_iter()
|
||||
.map(|s| s.to_lowercase())
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn load_config(mut env: JNIEnv, _class: JObject, obj: JObject) {
|
||||
NATIVE_CONFIG.lock().unwrap().replace(
|
||||
NativeConfig::new(&mut env, obj).expect("Failed to load NativeConfig")
|
||||
|
||||
@@ -10,15 +10,34 @@ mod config;
|
||||
mod sig;
|
||||
|
||||
mod modules;
|
||||
mod security;
|
||||
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 jni::{JNIEnv, JavaVM, NativeMethod};
|
||||
use jni::objects::{JObject, JString};
|
||||
use jni::sys::{jint, jstring, JNI_VERSION_1_6};
|
||||
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()));
|
||||
|
||||
struct BlockerDecision {
|
||||
blocked: bool,
|
||||
reason: &'static str,
|
||||
keyword: Option<String>,
|
||||
keyword_context: Option<&'static str>,
|
||||
}
|
||||
|
||||
#[allow(non_snake_case)]
|
||||
#[no_mangle]
|
||||
@@ -26,11 +45,13 @@ pub extern "system" fn JNI_OnLoad(_vm: JavaVM, _: *mut c_void) -> jint {
|
||||
android_logger::init_once(
|
||||
Config::default()
|
||||
.with_max_level(LevelFilter::Debug)
|
||||
.with_tag("SnapEnhanceNative")
|
||||
.with_tag("PurrfectSnapNative")
|
||||
);
|
||||
|
||||
info!("JNI_OnLoad called");
|
||||
|
||||
security::start_anti_debug_thread();
|
||||
|
||||
std::panic::set_hook(Box::new(|panic_info| {
|
||||
error!("{:?}", panic_info);
|
||||
}));
|
||||
@@ -39,11 +60,16 @@ pub extern "system" fn JNI_OnLoad(_vm: JavaVM, _: *mut c_void) -> jint {
|
||||
|
||||
let mut env = _vm.get_env().expect("Failed to get JNIEnv");
|
||||
|
||||
let native_lib_class = env.find_class("me/rhunk/snapenhance/nativelib/NativeLib").expect("NativeLib class not found");
|
||||
let native_lib_class = env.find_class("me/eternal/purrfectsnap/nativelib/NativeLib").expect("NativeLib class not found");
|
||||
|
||||
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(),
|
||||
@@ -56,7 +82,7 @@ pub extern "system" fn JNI_OnLoad(_vm: JavaVM, _: *mut c_void) -> jint {
|
||||
},
|
||||
NativeMethod {
|
||||
name: "loadConfig".into(),
|
||||
sig: "(Lme/rhunk/snapenhance/nativelib/NativeConfig;)V".into(),
|
||||
sig: "(Lme/eternal/purrfectsnap/nativelib/NativeConfig;)V".into(),
|
||||
fn_ptr: config::load_config as *mut c_void,
|
||||
},
|
||||
NativeMethod {
|
||||
@@ -78,13 +104,60 @@ pub extern "system" fn JNI_OnLoad(_vm: JavaVM, _: *mut c_void) -> jint {
|
||||
name: "composerEval".into(),
|
||||
sig: "(Ljava/lang/String;)Ljava/lang/String;".into(),
|
||||
fn_ptr: composer_hook::composer_eval 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: "shouldBlockDuplexClient".into(),
|
||||
sig: "(Ljava/lang/String;)Z".into(),
|
||||
fn_ptr: shouldBlockDuplexClient as *mut c_void,
|
||||
},
|
||||
NativeMethod {
|
||||
name: "evaluateAuthContextNative".into(),
|
||||
sig: "(Ljava/lang/String;ZLme/eternal/purrfectsnap/nativelib/NativeDecision;)V".into(),
|
||||
fn_ptr: evaluateAuthContext as *mut c_void,
|
||||
},
|
||||
NativeMethod {
|
||||
name: "evaluateApiInvocationNative".into(),
|
||||
sig: "(Ljava/lang/String;Ljava/lang/String;Lme/eternal/purrfectsnap/nativelib/NativeDecision;)V".into(),
|
||||
fn_ptr: evaluateApiInvocation as *mut c_void,
|
||||
},
|
||||
NativeMethod {
|
||||
name: "runEndpointSelfTest".into(),
|
||||
sig: "(Z)Z".into(),
|
||||
fn_ptr: runEndpointSelfTest as *mut c_void,
|
||||
},
|
||||
NativeMethod {
|
||||
name: "setChecksums".into(),
|
||||
sig: "(Ljava/lang/String;)V".into(),
|
||||
fn_ptr: setChecksums as *mut c_void,
|
||||
},
|
||||
NativeMethod {
|
||||
name: "setTestMode".into(),
|
||||
sig: "(Z)V".into(),
|
||||
fn_ptr: setTestMode as *mut c_void,
|
||||
},
|
||||
NativeMethod {
|
||||
name: "setInLoginSignup".into(),
|
||||
sig: "(Z)V".into(),
|
||||
fn_ptr: setInLoginSignup as *mut c_void,
|
||||
},
|
||||
]
|
||||
).expect("Failed to register native methods");
|
||||
|
||||
JNI_VERSION_1_6
|
||||
}
|
||||
|
||||
#[allow(non_snake_case)]
|
||||
fn setChecksums(mut env: JNIEnv, _class: JClass, checksums_json: JString) {
|
||||
let checksums_str: String = env.get_string(&checksums_json).unwrap().into();
|
||||
let checksums: HashMap<String, u32> = serde_json::from_str(&checksums_str).unwrap();
|
||||
*CHECKSUMS.lock().unwrap() = checksums;
|
||||
}
|
||||
|
||||
fn pre_init(_env: JNIEnv, _class: JObject) {
|
||||
debug!("Pre init");
|
||||
linker_hook::init();
|
||||
@@ -145,3 +218,547 @@ fn init(mut env: JNIEnv, _class: JObject, signature_cache: JString) -> jstring {
|
||||
std::ptr::null_mut()
|
||||
}
|
||||
}
|
||||
|
||||
#[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 {
|
||||
let lower_path = path.to_lowercase();
|
||||
for keyword in keywords {
|
||||
if lower_path.contains(keyword) {
|
||||
return Some(keyword.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn evaluate_endpoint_logic(
|
||||
config: &config::BlockerConfig,
|
||||
uri: &str,
|
||||
arg0: &str,
|
||||
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() {
|
||||
return BlockerDecision {
|
||||
blocked: false,
|
||||
reason: "allowed_whitelist",
|
||||
keyword: detection_keyword,
|
||||
keyword_context: None,
|
||||
};
|
||||
}
|
||||
|
||||
let reason = if detection_keyword.is_some() && has_attestation {
|
||||
"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_auth_context_logic(
|
||||
config: &config::BlockerConfig,
|
||||
request_path: &str,
|
||||
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() {
|
||||
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,
|
||||
};
|
||||
}
|
||||
|
||||
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) = find_keyword(&[annotations], &config.detection_keywords) {
|
||||
return BlockerDecision {
|
||||
blocked: true,
|
||||
reason: "detection_keyword",
|
||||
keyword: Some(keyword),
|
||||
keyword_context: Some("annotation"),
|
||||
};
|
||||
}
|
||||
|
||||
BlockerDecision {
|
||||
blocked: false,
|
||||
reason: "allowed",
|
||||
keyword: None,
|
||||
keyword_context: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn write_blocker_decision(env: &mut JNIEnv, decision_obj: JObject, decision: &BlockerDecision) {
|
||||
write_decision(
|
||||
env,
|
||||
decision_obj,
|
||||
decision.blocked,
|
||||
decision.reason,
|
||||
decision.keyword.as_deref(),
|
||||
decision.keyword_context,
|
||||
);
|
||||
}
|
||||
|
||||
fn write_decision(env: &mut JNIEnv, decision: JObject, blocked: bool, reason: &str, keyword: Option<&str>, keyword_context: 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");
|
||||
|
||||
set_string_field(env, &decision, "reason", Some(reason));
|
||||
set_string_field(env, &decision, "keyword", keyword);
|
||||
set_string_field(env, &decision, "keywordContext", keyword_context);
|
||||
}
|
||||
|
||||
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 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");
|
||||
} else {
|
||||
let null_obj = JObject::null();
|
||||
env.set_field(obj, field, "Ljava/lang/String;", JValue::Object(&null_obj)).expect("failed to clear string field");
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(non_snake_case)]
|
||||
fn evaluateEndpoint(
|
||||
mut env: JNIEnv,
|
||||
_class: JClass,
|
||||
uri: JString,
|
||||
arg0: JString,
|
||||
has_attestation: jboolean,
|
||||
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);
|
||||
return;
|
||||
}
|
||||
let uri_str: String = env.get_string(&uri).unwrap().into();
|
||||
let arg0_str: String = env.get_string(&arg0).unwrap().into();
|
||||
|
||||
let config = config::get_blocker_config();
|
||||
let blocker_decision = evaluate_endpoint_logic(&config, &uri_str, &arg0_str, has_attestation == JNI_TRUE);
|
||||
write_blocker_decision(&mut env, decision, &blocker_decision);
|
||||
}
|
||||
|
||||
#[allow(non_snake_case)]
|
||||
fn shouldBlockDuplexClient(
|
||||
mut env: JNIEnv,
|
||||
_class: JClass,
|
||||
path: JString,
|
||||
) -> jboolean {
|
||||
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();
|
||||
|
||||
let hermod = secstrings::get_hermod_dup();
|
||||
if path_str == hermod {
|
||||
JNI_TRUE
|
||||
} else {
|
||||
JNI_FALSE
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(non_snake_case)]
|
||||
fn evaluateAuthContext(
|
||||
mut env: JNIEnv,
|
||||
_class: JClass,
|
||||
request_path: JString,
|
||||
attestation_required: jboolean,
|
||||
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);
|
||||
return;
|
||||
}
|
||||
let request_path_str: String = env.get_string(&request_path).unwrap().into();
|
||||
|
||||
let config = config::get_blocker_config();
|
||||
let blocker_decision = evaluate_auth_context_logic(&config, &request_path_str, attestation_required == JNI_TRUE);
|
||||
write_blocker_decision(&mut env, decision, &blocker_decision);
|
||||
}
|
||||
|
||||
#[allow(non_snake_case)]
|
||||
fn evaluateApiInvocation(
|
||||
mut env: JNIEnv,
|
||||
_class: JClass,
|
||||
method_id: JString,
|
||||
annotations: JString,
|
||||
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);
|
||||
return;
|
||||
}
|
||||
let method_id_str: String = env.get_string(&method_id).unwrap().into();
|
||||
let annotations_str: String = env.get_string(&annotations).unwrap().into();
|
||||
|
||||
let config = config::get_blocker_config();
|
||||
let blocker_decision = evaluate_api_invocation_logic(&config, &method_id_str, &annotations_str);
|
||||
write_blocker_decision(&mut env, decision, &blocker_decision);
|
||||
}
|
||||
|
||||
fn run_blocker_self_test(allow_unverified: bool) -> bool {
|
||||
if !IS_VERIFIED.load(Ordering::Relaxed) && !allow_unverified {
|
||||
return false;
|
||||
}
|
||||
|
||||
let config = config::get_blocker_config();
|
||||
if config.allowed_eps_active.is_empty()
|
||||
|| config.detection_keywords.is_empty()
|
||||
|| config.risk_block_list.is_empty()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
let allowed_sample = config.allowed_eps_active[0].clone();
|
||||
let detection_sample = config.detection_keywords[0].clone();
|
||||
let risk_sample = config.risk_block_list[0].clone();
|
||||
|
||||
let allowed_decision = evaluate_endpoint_logic(&config, &allowed_sample, &allowed_sample, false);
|
||||
if allowed_decision.blocked || allowed_decision.reason != "allowed_whitelist" {
|
||||
return false;
|
||||
}
|
||||
|
||||
let detection_path = format!("/self_test/{}", detection_sample);
|
||||
let detection_decision = evaluate_endpoint_logic(&config, &detection_path, "", false);
|
||||
if detection_decision.blocked {
|
||||
return false;
|
||||
}
|
||||
|
||||
let attestation_decision = evaluate_endpoint_logic(&config, &detection_path, "", true);
|
||||
if attestation_decision.blocked {
|
||||
return false;
|
||||
}
|
||||
|
||||
let risk_decision = evaluate_endpoint_logic(&config, &risk_sample, "", false);
|
||||
if risk_decision.blocked {
|
||||
return false;
|
||||
}
|
||||
|
||||
let auth_detection = evaluate_auth_context_logic(&config, &detection_path, false);
|
||||
if auth_detection.blocked {
|
||||
return false;
|
||||
}
|
||||
|
||||
let auth_allowed = evaluate_auth_context_logic(&config, &allowed_sample, false);
|
||||
if auth_allowed.blocked || auth_allowed.reason != "allowed_whitelist" {
|
||||
return false;
|
||||
}
|
||||
|
||||
let api_method = format!("com.snap.obf.SelfTest{}", detection_sample);
|
||||
let api_decision = evaluate_api_invocation_logic(&config, &api_method, "");
|
||||
if !api_decision.blocked || api_decision.keyword.is_none() {
|
||||
return false;
|
||||
}
|
||||
|
||||
let annotation_blob = format!("@Requires{}", detection_sample);
|
||||
let api_annotation_decision = evaluate_api_invocation_logic(&config, "com.snap.obf.Safe", &annotation_blob);
|
||||
if !api_annotation_decision.blocked || api_annotation_decision.keyword_context != Some("annotation") {
|
||||
return false;
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
#[allow(non_snake_case)]
|
||||
fn setTestMode(_env: JNIEnv, _class: JClass, test_mode: jboolean) {
|
||||
TEST_MODE.store(test_mode == JNI_TRUE, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
#[allow(non_snake_case)]
|
||||
fn setInLoginSignup(_env: JNIEnv, _class: JClass, in_login_signup: jboolean) {
|
||||
IN_LOGIN_SIGNUP.store(in_login_signup == JNI_TRUE, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
#[allow(non_snake_case)]
|
||||
fn runEndpointSelfTest(_env: JNIEnv, _class: JClass, test_mode: jboolean) -> jboolean {
|
||||
if run_blocker_self_test(test_mode == JNI_TRUE) {
|
||||
JNI_TRUE
|
||||
} else {
|
||||
JNI_FALSE
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#![allow(dead_code, unused_imports)]
|
||||
|
||||
use super::util::composer_utils::{ComposerModule, ModuleTag};
|
||||
use std::{collections::HashMap, ffi::{c_void, CStr}, sync::Mutex};
|
||||
use std::{collections::HashMap, ffi::{c_void, CStr}, sync::{atomic::{AtomicPtr, Ordering}, Mutex}};
|
||||
use jni::{objects::JString, sys::jobject, JNIEnv};
|
||||
use once_cell::sync::Lazy;
|
||||
use crate::{common, config, def_hook, dobby_hook, dobby_hook_sym, sig, util::get_jni_string};
|
||||
@@ -154,12 +154,12 @@ def_hook!(
|
||||
);
|
||||
|
||||
#[cfg(target_arch = "aarch64")]
|
||||
static mut GLOBAL_INSTANCE: Option<*mut c_void> = None;
|
||||
static GLOBAL_INSTANCE: AtomicPtr<c_void> = AtomicPtr::new(std::ptr::null_mut());
|
||||
#[cfg(target_arch = "aarch64")]
|
||||
static mut GLOBAL_CTX: Option<*mut c_void> = None;
|
||||
static GLOBAL_CTX: AtomicPtr<c_void> = AtomicPtr::new(std::ptr::null_mut());
|
||||
|
||||
#[cfg(target_arch = "aarch64")]
|
||||
static mut JS_EVAL_ORIGINAL2: Option<unsafe extern "C" fn(*mut c_void, *mut c_void, *mut c_void, *mut u8, usize, *const u8, u32) -> JsValue> = None;
|
||||
static JS_EVAL_ORIGINAL2: Lazy<Mutex<Option<unsafe extern "C" fn(*mut c_void, *mut c_void, *mut c_void, *mut u8, usize, *const u8, u32) -> JsValue>>> = Lazy::new(|| Mutex::new(None));
|
||||
|
||||
def_hook!(
|
||||
js_eval,
|
||||
@@ -167,8 +167,8 @@ def_hook!(
|
||||
|arg0: *mut c_void, arg1: *mut c_void, arg2: *mut c_void, arg3: *const u8, arg4: *const u8, arg5: *const u8, arg6: *mut c_void, arg7: u32| {
|
||||
#[cfg(target_arch = "aarch64")]
|
||||
{
|
||||
GLOBAL_INSTANCE = Some(arg0);
|
||||
GLOBAL_CTX = Some(arg1);
|
||||
GLOBAL_INSTANCE.store(arg0, Ordering::Relaxed);
|
||||
GLOBAL_CTX.store(arg1, Ordering::Relaxed);
|
||||
}
|
||||
js_eval_original.unwrap()(arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7)
|
||||
}
|
||||
@@ -190,7 +190,7 @@ pub unsafe fn composer_eval(env: JNIEnv, _: *mut c_void, script: JString) -> job
|
||||
Err(_) => return std::ptr::null_mut(),
|
||||
};
|
||||
|
||||
if JS_EVAL_ORIGINAL2.is_none() || GLOBAL_INSTANCE.is_none() || GLOBAL_CTX.is_none() {
|
||||
if JS_EVAL_ORIGINAL2.lock().unwrap().is_none() || GLOBAL_INSTANCE.load(Ordering::Relaxed).is_null() || GLOBAL_CTX.load(Ordering::Relaxed).is_null() {
|
||||
if let Ok(s) = env.new_string("Composer hook not initialized") {
|
||||
return s.into_raw();
|
||||
}
|
||||
@@ -199,15 +199,21 @@ pub unsafe fn composer_eval(env: JNIEnv, _: *mut c_void, script: JString) -> job
|
||||
|
||||
let script_length = script_str.len();
|
||||
|
||||
let js_value = JS_EVAL_ORIGINAL2.unwrap()(
|
||||
GLOBAL_INSTANCE.unwrap(),
|
||||
GLOBAL_CTX.unwrap(),
|
||||
std::ptr::null_mut(),
|
||||
(script_str + "\0").as_ptr() as *mut u8,
|
||||
script_length,
|
||||
"<eval>\0".as_ptr(),
|
||||
0
|
||||
);
|
||||
let js_value = {
|
||||
let js_eval_fn = JS_EVAL_ORIGINAL2.lock().unwrap();
|
||||
let global_instance = GLOBAL_INSTANCE.load(Ordering::Relaxed);
|
||||
let global_ctx = GLOBAL_CTX.load(Ordering::Relaxed);
|
||||
|
||||
js_eval_fn.unwrap()(
|
||||
global_instance,
|
||||
global_ctx,
|
||||
std::ptr::null_mut(),
|
||||
(script_str + "\0").as_ptr() as *mut u8,
|
||||
script_length,
|
||||
"<eval>\0".as_ptr(),
|
||||
0
|
||||
)
|
||||
};
|
||||
|
||||
let result: String = if js_value.tag == JS_TAG_STRING {
|
||||
let string = js_value.u.ptr as *mut JsString;
|
||||
@@ -270,7 +276,7 @@ pub fn init() {
|
||||
dobby_hook!(signature as *mut c_void, js_eval);
|
||||
|
||||
unsafe {
|
||||
JS_EVAL_ORIGINAL2 = Some(std::mem::transmute(js_eval_original.unwrap()));
|
||||
*JS_EVAL_ORIGINAL2.lock().unwrap() = Some(std::mem::transmute(js_eval_original.unwrap()));
|
||||
}
|
||||
|
||||
debug!("js_eval {:#x}", signature);
|
||||
@@ -279,4 +285,3 @@ pub fn init() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -113,11 +113,11 @@ pub fn init() {
|
||||
env.get_method_id(
|
||||
env.get_object_class(common::native_lib_instance()).unwrap(),
|
||||
"onNativeUnaryCall",
|
||||
"(Ljava/lang/String;[B)Lme/rhunk/snapenhance/nativelib/NativeRequestData;"
|
||||
"(Ljava/lang/String;[B)Lme/eternal/purrfectsnap/nativelib/NativeRequestData;"
|
||||
).expect("Failed to get onNativeUnaryCall method id")
|
||||
).expect("unary call method already set");
|
||||
});
|
||||
} else {
|
||||
error!("Can't find unaryCall signature");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
47
native/rust/src/secstrings.rs
Normal file
47
native/rust/src/secstrings.rs
Normal file
@@ -0,0 +1,47 @@
|
||||
use std::ffi::CStr;
|
||||
|
||||
#[allow(improper_ctypes)]
|
||||
extern "C" {
|
||||
fn ss_get_detection_blob(out_len: *mut usize) -> *const u8;
|
||||
fn ss_get_hermod_dup(out_len: *mut usize) -> *const u8;
|
||||
fn ss_get_risk_block_blob(out_len: *mut usize) -> *const u8;
|
||||
}
|
||||
|
||||
fn blob_to_vec(blob_ptr: *const u8, len: usize) -> Vec<String> {
|
||||
if blob_ptr.is_null() || len == 0 { return Vec::new(); }
|
||||
// Safety: blob is a NUL-separated set of C strings
|
||||
let slice = unsafe { std::slice::from_raw_parts(blob_ptr, len) };
|
||||
let mut result = Vec::new();
|
||||
let mut start = 0usize;
|
||||
for i in 0..slice.len() {
|
||||
if slice[i] == 0 { // NUL
|
||||
if i > start {
|
||||
let cstr = unsafe { CStr::from_bytes_with_nul_unchecked(&slice[start..=i]) };
|
||||
result.push(cstr.to_string_lossy().into_owned());
|
||||
}
|
||||
start = i + 1;
|
||||
}
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
pub fn get_detection_keywords() -> Vec<String> {
|
||||
let mut len: usize = 0;
|
||||
let ptr = unsafe { ss_get_detection_blob(&mut len as *mut usize) };
|
||||
blob_to_vec(ptr, len)
|
||||
}
|
||||
|
||||
pub fn get_hermod_dup() -> String {
|
||||
let mut len: usize = 0;
|
||||
let ptr = unsafe { ss_get_hermod_dup(&mut len as *mut usize) };
|
||||
if ptr.is_null() || len == 0 { return String::new(); }
|
||||
let slice = unsafe { std::slice::from_raw_parts(ptr, len) };
|
||||
String::from_utf8_lossy(slice).into_owned()
|
||||
}
|
||||
|
||||
pub fn get_risk_block_list() -> Vec<String> {
|
||||
let mut len: usize = 0;
|
||||
let ptr = unsafe { ss_get_risk_block_blob(&mut len as *mut usize) };
|
||||
blob_to_vec(ptr, len)
|
||||
}
|
||||
|
||||
96
native/rust/src/security.rs
Normal file
96
native/rust/src/security.rs
Normal file
@@ -0,0 +1,96 @@
|
||||
|
||||
extern crate libc;
|
||||
|
||||
use jni::sys::{jboolean, JNI_FALSE, JNI_TRUE};
|
||||
use goldberg::goldberg_string;
|
||||
use std::{thread, time};
|
||||
use std::fs;
|
||||
use std::process;
|
||||
use crate::CHECKSUMS;
|
||||
use std::ffi::CString;
|
||||
use std::str;
|
||||
|
||||
fn check_for_debugger() {
|
||||
loop {
|
||||
let status = fs::read_to_string("/proc/self/status").unwrap_or_default();
|
||||
if !status.contains("TracerPid:\t0") {
|
||||
process::abort();
|
||||
}
|
||||
|
||||
let maps = fs::read_to_string("/proc/self/maps").unwrap_or_default();
|
||||
if maps.contains("frida") || maps.contains("gumjs") || maps.contains("gdb") {
|
||||
process::abort();
|
||||
}
|
||||
|
||||
thread::sleep(time::Duration::from_secs(5));
|
||||
}
|
||||
}
|
||||
|
||||
pub fn start_anti_debug_thread() {
|
||||
thread::spawn(move || {
|
||||
check_for_debugger();
|
||||
});
|
||||
thread::spawn(move || {
|
||||
verify_library_checksum();
|
||||
});
|
||||
}
|
||||
|
||||
fn get_abi() -> Option<String> {
|
||||
let key = CString::new("ro.product.cpu.abi").unwrap();
|
||||
let mut value = [0 as libc::c_char; 92];
|
||||
let len = unsafe { libc::__system_property_get(key.as_ptr(), value.as_mut_ptr()) };
|
||||
if len > 0 {
|
||||
let value_slice = unsafe { std::slice::from_raw_parts(value.as_ptr() as *const u8, len as usize) };
|
||||
let value_str = str::from_utf8(value_slice).unwrap();
|
||||
Some(value_str.to_string())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn verify_library_checksum() {
|
||||
loop {
|
||||
if let Some(path) = get_library_path() {
|
||||
if let Ok(data) = fs::read(path) {
|
||||
let checksum = crc32fast::hash(&data);
|
||||
if let Some(abi) = get_abi() {
|
||||
if let Some(expected_checksum) = CHECKSUMS.lock().unwrap().get(&abi) {
|
||||
if checksum != *expected_checksum as u32 {
|
||||
process::abort();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
thread::sleep(time::Duration::from_secs(60));
|
||||
}
|
||||
}
|
||||
|
||||
pub fn verify_key(key: &str) -> bool {
|
||||
// Transformed key verification
|
||||
let transformed_key = key.chars().rev().collect::<String>();
|
||||
let expected_key = goldberg_string!("qd84R1hT6p7Mk").chars().rev().collect::<String>();
|
||||
transformed_key == expected_key
|
||||
}
|
||||
|
||||
// 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
|
||||
pub fn jni_verify_key(key: &str) -> jboolean {
|
||||
if verify_key(key) {
|
||||
JNI_TRUE
|
||||
} else {
|
||||
JNI_FALSE
|
||||
}
|
||||
}
|
||||
fn get_library_path() -> Option<String> {
|
||||
let maps = std::fs::read_to_string("/proc/self/maps").ok()?;
|
||||
for line in maps.lines() {
|
||||
if line.ends_with("libpurrfectsnap.so") {
|
||||
let parts: Vec<&str> = line.split_whitespace().collect();
|
||||
if parts.len() > 5 {
|
||||
return Some(parts[5].to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
Reference in New Issue
Block a user