Major Update!

This features a new bypass, new theme, etc
This commit is contained in:
ΞTΞRNAL
2025-12-23 17:09:18 +05:30
parent e85a54d0b1
commit 544d618f87
685 changed files with 63893 additions and 81215 deletions

View File

@@ -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")

View File

@@ -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
}
}

View File

@@ -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() {
}
}
}

View File

@@ -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");
}
}
}

View 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)
}

View 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
}