fix(native): composer loader hook

This commit is contained in:
rhunk
2024-07-28 20:13:27 +02:00
parent bc9af4105c
commit bcec1f5651
10 changed files with 382 additions and 118 deletions

View File

@@ -1,9 +1,9 @@
import { getConfig, log } from "./imports";
import { Module } from "./types";
import { Module, modules } from "./types";
import operaDownloadButton from "./modules/operaDownloadButton";
import firstCreatedUsername from "./modules/firstCreatedUsername";
import bypassCameraRollSelectionLimit from "./modules/bypassCameraRollSelectionLimit";
import "./modules/operaDownloadButton";
import "./modules/firstCreatedUsername";
import "./modules/bypassCameraRollSelectionLimit";
try {
@@ -15,22 +15,18 @@ try {
})
}
const modules: Module[] = [
operaDownloadButton,
firstCreatedUsername,
bypassCameraRollSelectionLimit
];
modules.forEach(module => {
if (!module.enabled(config)) return
modules.forEach(m => {
if (!m.enabled(config)) {
return
}
try {
module.init();
m.init();
} catch (e) {
console.error(`failed to initialize module ${module.name}`, e, e.stack);
console.error(`failed to initialize module ${m.name}`, e, e.stack);
}
});
console.log("composer modules loaded!");
console.log("modules loaded!");
} catch (e) {
log("error", "Failed to load composer modules\n" + e + "\n" + e.stack)
}

View File

@@ -39,6 +39,9 @@ export interface Module {
init: () => void
}
export const modules: Module[] = []
export function defineModule<T extends Module>(module: T & Record<string, any>): T {
modules.push(module)
return module
}

View File

@@ -15,7 +15,7 @@ abstract class Feature(
runCatching {
block()
}.onFailure {
context.log.error("Failed to run onNextActivityCreate callback", it)
context.log.error("Failed to run defer callback", it)
}
}
}

View File

@@ -1,14 +1,22 @@
package me.rhunk.snapenhance.core.features.impl.experiments
import android.os.ParcelFileDescriptor
import android.app.Activity
import android.view.View
import android.widget.FrameLayout
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.BugReport
import androidx.compose.material3.*
import androidx.compose.material3.Button
import androidx.compose.material3.FilledIconButton
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.material3.TextField
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
@@ -48,7 +56,7 @@ class ComposerHooks: Feature("ComposerHooks") {
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
var result by remember { mutableStateOf("") }
var codeContent by remember { mutableStateOf("") }
var codeContent by remember { mutableStateOf("return 1 + 2") }
Text("Composer Console", fontSize = 18.sp, fontWeight = FontWeight.Bold)
@@ -95,8 +103,11 @@ class ComposerHooks: Feature("ComposerHooks") {
private val composerConsoleTag = Random.nextLong().toString()
private fun injectConsole() {
val root = context.mainActivity!!.findViewById<FrameLayout>(android.R.id.content)
private fun injectConsole(activity: Activity) {
val root = activity.findViewById<FrameLayout>(android.R.id.content) ?: run {
context.log.warn("Unable to find root view. Can't inject console.")
return
}
root.post {
if (root.findViewWithTag<View>(composerConsoleTag) != null) return@post
root.addView(createComposeView(root.context) {
@@ -202,16 +213,20 @@ class ComposerHooks: Feature("ComposerHooks") {
context.native.setComposerLoader("""
(() => { const _getImportsFunctionName = "$getImportsFunctionName"; $loaderScript })();
""".trimIndent().trim())
}
loadHooks()
onNextActivityCreate { activity ->
if (config.composerConsole.get()) {
injectConsole()
injectConsole(activity)
}
}
findClass("com.snapchat.client.composer.NativeBridge").apply {
hook("registerNativeModuleFactory", HookStage.BEFORE) { param ->
val moduleFactory = param.argNullable<Any>(1) ?: return@hook
if (moduleFactory.javaClass.getMethod("getModulePath").invoke(moduleFactory)?.toString() != "DeviceBridge") return@hook
if (moduleFactory.javaClass.getMethod("getModulePath").invoke(moduleFactory)?.toString()?.contains("DeviceBridge") != true) return@hook
Hooker.ephemeralHookObjectMethod(moduleFactory.javaClass, moduleFactory, "loadModule", HookStage.AFTER) { methodParam ->
val result = methodParam.getResult() as? MutableMap<String, Any?> ?: return@ephemeralHookObjectMethod
result[getImportsFunctionName] = newComposerFunction {
@@ -219,7 +234,6 @@ class ComposerHooks: Feature("ComposerHooks") {
true
}
}
loadHooks()
}
}
}

48
native/rust/Cargo.lock generated
View File

@@ -78,6 +78,10 @@ name = "cc"
version = "1.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2aba8f4e9906c7ce3c73463f62a7f0c65183ada1a2d47e397cc8810827f9694f"
dependencies = [
"jobserver",
"libc",
]
[[package]]
name = "cesu8"
@@ -237,6 +241,15 @@ version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130"
[[package]]
name = "jobserver"
version = "0.1.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "48d1dbcbbeb6a7fec7e059840aa538bd62aaccf972c7346c4d9d2059312853d0"
dependencies = [
"libc",
]
[[package]]
name = "js-sys"
version = "0.3.69"
@@ -318,6 +331,12 @@ version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
[[package]]
name = "pkg-config"
version = "0.3.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec"
[[package]]
name = "proc-macro2"
version = "1.0.86"
@@ -463,6 +482,7 @@ dependencies = [
"paste",
"procfs",
"serde_json",
"zstd",
]
[[package]]
@@ -722,3 +742,31 @@ name = "windows_x86_64_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
[[package]]
name = "zstd"
version = "0.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fcf2b778a664581e31e389454a7072dab1647606d44f7feea22cd5abb9c9f3f9"
dependencies = [
"zstd-safe",
]
[[package]]
name = "zstd-safe"
version = "7.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fa556e971e7b568dc775c136fc9de8c779b1c2fc3a63defaafadffdbd3181afa"
dependencies = [
"zstd-sys",
]
[[package]]
name = "zstd-sys"
version = "2.0.12+zstd.1.5.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0a4e40c320c3cb459d9a9ff6de98cff88f4751ee9275d140e2be94a2b74e4c13"
dependencies = [
"cc",
"pkg-config",
]

View File

@@ -17,3 +17,4 @@ once_cell = "1.19.0"
paste = "1.0.15"
procfs = "0.16.0"
serde_json = "1.0.120"
zstd = "0.13.2"

View File

@@ -1,8 +1,10 @@
#![allow(dead_code, unused_mut)]
#![allow(dead_code, unused_imports)]
use std::{cell::Cell, ffi::{c_void, CStr}, sync::Mutex};
use super::util::composer_utils::ComposerModule;
use std::{collections::HashMap, ffi::{c_void, CStr}, sync::Mutex};
use jni::{objects::JString, sys::jobject, JNIEnv};
use crate::{common, config, def_hook, dobby_hook, sig, util::get_jni_string};
use once_cell::sync::Lazy;
use crate::{common, config, def_hook, dobby_hook, dobby_hook_sym, sig, util::get_jni_string};
const JS_TAG_BIG_DECIMAL: i64 = -11;
const JS_TAG_BIG_INT: i64 = -10;
@@ -61,124 +63,181 @@ struct JsValue {
tag: i64,
}
static mut GLOBAL_INSTANCE: Option<*mut c_void> = None;
static mut GLOBAL_CTX: Option<*mut c_void> = None;
static COMPOSER_LOADER_DATA: Mutex<Cell<Option<Box<String>>>> = Mutex::new(Cell::new(None));
static AASSET_MAP: Lazy<Mutex<HashMap<usize, Vec<u8>>>> = Lazy::new(|| Mutex::new(HashMap::new()));
static COMPOSER_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);
if !handle.is_null() && CStr::from_ptr(arg1).to_str().unwrap().ends_with("blizzard.composermodule") {
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 composer_loader = COMPOSER_LOADER_DATA.lock().unwrap().clone().expect("No composer 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 composer archive");
let mut composer_module = ComposerModule::parse(decompressed).expect("Failed to parse composer module");
let mut tags = composer_module.get_tags();
for (tag1, tag2) in tags.iter_mut() {
if tag1.to_string().unwrap().ends_with("BlizzardEventLogger.js") {
let mut buffer = Vec::new();
buffer.extend(composer_loader.as_bytes());
buffer.extend(tag2.get_buffer());
tag2.set_buffer(buffer);
debug!("composer loader injected in {}", tag1.to_string().unwrap());
}
}
composer_module.set_tags(tags);
let compressed = composer_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)
}
);
#[cfg(target_arch = "aarch64")]
static mut GLOBAL_INSTANCE: Option<*mut c_void> = None;
#[cfg(target_arch = "aarch64")]
static mut GLOBAL_CTX: Option<*mut c_void> = None;
#[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;
def_hook!(
js_eval,
*mut c_void,
|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| {
let mut arg3 = arg3;
let mut arg4 = arg4;
let mut arg5 = arg5;
if GLOBAL_INSTANCE.is_none() || GLOBAL_CTX.is_none() {
GLOBAL_INSTANCE = Some(arg0);
GLOBAL_CTX = Some(arg1);
let mut loader_data = COMPOSER_LOADER_DATA.lock().unwrap();
let mut loader_data = loader_data.get_mut();
let loader_data = loader_data.as_mut().unwrap();
loader_data.push_str("\n");
#[cfg(target_arch = "aarch64")]
{
loader_data.push_str(CStr::from_ptr(arg3).to_str().unwrap());
arg3 = loader_data.as_mut_ptr();
arg4 = loader_data.len() as *const u8;
#[cfg(target_arch = "aarch64")]
{
if GLOBAL_INSTANCE.is_none() || GLOBAL_CTX.is_none() {
GLOBAL_INSTANCE = Some(arg0);
GLOBAL_CTX = Some(arg1);
}
// On arm the original JS_Eval function is inlined so the arguments are shifted
#[cfg(target_arch = "arm")]
{
loader_data.push_str(CStr::from_ptr(arg4).to_str().unwrap());
arg4 = loader_data.as_mut_ptr();
arg5 = loader_data.len() as *const u8;
}
debug!("injected composer loader!");
} else {
COMPOSER_LOADER_DATA.lock().unwrap().take();
}
js_eval_original.unwrap()(arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7)
}
);
pub fn set_composer_loader(mut env: JNIEnv, _: *mut c_void, code: JString) {
let new_code = get_jni_string(&mut env, code).expect("Failed to get code");
COMPOSER_LOADER_DATA.lock().unwrap().replace(Some(Box::new(new_code)));
let new_code = get_jni_string(&mut env, code).expect("Failed to get composer loader code");
COMPOSER_LOADER_DATA.lock().unwrap().replace(new_code);
}
#[allow(unreachable_code, unused_variables)]
pub unsafe fn composer_eval(env: JNIEnv, _: *mut c_void, script: JString) -> jobject {
#[cfg(not(target_arch = "aarch64"))]
#[cfg(target_arch = "aarch64")]
{
return env.new_string("Architecture not supported").unwrap().into_raw();
let mut env = env;
let script_str = get_jni_string(&mut env, script).expect("Failed to get script");
let script_length = script_str.len();
let js_value = JS_EVAL_ORIGINAL2.expect("No js eval found")(
GLOBAL_INSTANCE.expect("No global instance found"),
GLOBAL_CTX.expect("No global context found"),
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;
CStr::from_ptr((*string).str8.as_ptr() as *const u8).to_str().unwrap().into()
} else if js_value.tag == JS_TAG_INT {
js_value.u.int32.to_string()
} else if js_value.tag == JS_TAG_BOOL {
if js_value.u.int32 == 1 { "true" } else { "false" }.into()
} else if js_value.tag == JS_TAG_NULL {
"null".into()
} else if js_value.tag == JS_TAG_UNDEFINED {
"undefined".into()
} else if js_value.tag == JS_TAG_OBJECT {
"[object]".into()
} else if js_value.tag == JS_TAG_FLOAT64 {
js_value.u.float64.to_string()
} else if js_value.tag == JS_TAG_EXCEPTION {
"Failed to evaluate script".into()
} else {
"[unknown tag ".to_owned() + &js_value.tag.to_string() + "]".into()
};
return env.new_string(result).unwrap().into_raw()
}
let mut env = env;
let script_str = get_jni_string(&mut env, script).expect("Failed to get script");
let script_length = script_str.len();
let js_value = JS_EVAL_ORIGINAL2.expect("No js eval found")(
GLOBAL_INSTANCE.expect("No global instance found"),
GLOBAL_CTX.expect("No global context found"),
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;
CStr::from_ptr((*string).str8.as_ptr() as *const u8).to_str().unwrap().into()
} else if js_value.tag == JS_TAG_INT {
js_value.u.int32.to_string()
} else if js_value.tag == JS_TAG_BOOL {
if js_value.u.int32 == 1 { "true" } else { "false" }.into()
} else if js_value.tag == JS_TAG_NULL {
"null".into()
} else if js_value.tag == JS_TAG_UNDEFINED {
"undefined".into()
} else if js_value.tag == JS_TAG_OBJECT {
"[object]".into()
} else if js_value.tag == JS_TAG_FLOAT64 {
js_value.u.float64.to_string()
} else if js_value.tag == JS_TAG_EXCEPTION {
"Failed to evaluate script".into()
} else {
"[unknown tag ".to_owned() + &js_value.tag.to_string() + "]".into()
};
env.new_string(result).unwrap().into_raw()
return env.new_string("Architecture not supported").unwrap().into_raw();
}
pub fn init() {
if !config::native_config().composer_hooks {
return
}
if let Some(signature) = sig::find_signature(
&common::CLIENT_MODULE,
"00 E4 00 6F 29 00 80 52 76 00 04 8B", -0x28,
"A1 B0 07 92 81 46", -0x7
) {
dobby_hook!(signature as *mut c_void, js_eval);
unsafe {
JS_EVAL_ORIGINAL2 = Some(std::mem::transmute(js_eval_original.unwrap()));
}
debug!("js_eval {:#x}", signature);
} else {
warn!("Unable to find js_eval signature");
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);
#[cfg(target_arch = "aarch64")]
{
if let Some(signature) = sig::find_signature(
&common::CLIENT_MODULE,
"00 E4 00 6F 29 00 80 52 76 00 04 8B", -0x28,
"A1 B0 07 92 81 46", -0x7
) {
dobby_hook!(signature as *mut c_void, js_eval);
unsafe {
JS_EVAL_ORIGINAL2 = Some(std::mem::transmute(js_eval_original.unwrap()));
}
debug!("js_eval {:#x}", signature);
} else {
warn!("Unable to find js_eval signature");
}
}
}

View File

@@ -1,3 +1,4 @@
pub mod util;
pub mod linker_hook;
pub mod duplex_hook;
pub mod sqlite_hook;

View File

@@ -0,0 +1,141 @@
use std::{io::Error, string::FromUtf8Error};
#[derive(Debug, Clone)]
pub struct ModuleTag {
tag_type: u8,
buffer: Vec<u8>,
}
impl ModuleTag {
pub fn new(module_type: u8, buffer: Vec<u8>) -> ModuleTag {
ModuleTag {
tag_type: module_type,
buffer,
}
}
pub fn to_string(&self) -> Result<String, FromUtf8Error> {
Ok(String::from_utf8(self.buffer.clone())?)
}
pub fn get_tag_type(&self) -> u8 {
self.tag_type
}
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 ComposerModule {
tags: Vec<(ModuleTag, ModuleTag)>, // file name => file content
}
impl ComposerModule {
pub fn parse(buffer: Vec<u8>) -> Result<ComposerModule, 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_u24(buffer: &Vec<u8>, offset: &mut usize) -> Result<u32, Error> {
let b1 = buffer[*offset] as u32;
let b2 = buffer[*offset + 1] as u32;
let b3 = buffer[*offset + 2] as u32;
*offset += 3;
Ok(b1 | (b2 << 8) | (b3 << 16))
}
let tag_size = read_u24(&buffer, &mut offset)?;
let tag_type = buffer[offset];
offset += 1;
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(tag_type, tag_buffer));
}
let tags = tags.chunks(2).map(|chunk| {
(chunk[0].clone(), chunk[1].clone())
}).collect();
Ok(ComposerModule {
tags,
})
}
pub fn to_bytes(&self) -> Vec<u8> {
let mut tag_buffer = Vec::new();
fn write_u24(buffer: &mut Vec<u8>, value: u32) {
buffer.push((value & 0xff) as u8);
buffer.push(((value >> 8) & 0xff) as u8);
buffer.push(((value >> 16) & 0xff) as u8);
}
fn write_tag(buffer: &mut Vec<u8>, tag: ModuleTag) {
write_u24(buffer, tag.get_size() as u32);
buffer.push(tag.get_tag_type());
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;
}
}

View File

@@ -0,0 +1 @@
pub mod composer_utils;