feat(experimental): composer hooks

This commit is contained in:
rhunk 2024-04-21 23:25:45 +02:00
parent c8195c5250
commit 17ad43ee92
12 changed files with 467 additions and 12 deletions

View File

@ -124,6 +124,13 @@ class RemoteScriptManager(
}
override fun getScriptContent(moduleName: String): String? {
if (moduleName.startsWith("composer/")) {
return runCatching {
context.androidContext.assets.open("composer/${moduleName.removePrefix("composer/")}").use {
it.bufferedReader().readText()
}
}.getOrNull()
}
return getScriptInputStream(moduleName) { it?.bufferedReader()?.readText() }
}

View File

@ -770,6 +770,24 @@
"name": "Native Hooks",
"description": "Unsafe Features that hook into Snapchat's native code",
"properties": {
"composer_hooks": {
"name": "Composer Hooks",
"description": "Injects code into the Composer cross-platform UI framework (arm64 only)",
"properties": {
"bypass_camera_roll_limit": {
"name": "Bypass Camera Roll Limit",
"description": "Increases the maximum amount of media you can send from the camera roll"
},
"composer_console": {
"name": "Composer Console",
"description": "Allows you to execute JavaScript code in Composer"
},
"composer_logs": {
"name": "Composer Logs",
"description": "Redirects console logs of Composer to SnapEnhance"
}
}
},
"disable_bitmoji": {
"name": "Disable Bitmoji",
"description": "Disables Friends Profile Bitmoji"

View File

@ -9,7 +9,14 @@ class Experimental : ConfigContainer() {
val allowRunningInBackground = boolean("allow_running_in_background", true)
}
class ComposerHooksConfig: ConfigContainer(hasGlobalState = true) {
val bypassCameraRollLimit = boolean("bypass_camera_roll_limit")
val composerConsole = boolean("composer_console")
val composerLogs = boolean("composer_logs")
}
class NativeHooks : ConfigContainer(hasGlobalState = true) {
val composerHooks = container("composer_hooks", ComposerHooksConfig()) { requireRestart() }
val disableBitmoji = boolean("disable_bitmoji")
}

View File

@ -0,0 +1,44 @@
const deviceBridge = require('composer_core/src/DeviceBridge');
if (LOADER_CONFIG.logPrefix) {
function internalLog(logLevel, args) {
deviceBridge.copyToClipBoard(LOADER_CONFIG.logPrefix + "|" + logLevel + "|" + Array.from(args).join(" "));
}
console.log = function() {
internalLog("info", arguments);
}
console.error = function() {
internalLog("error", arguments);
}
console.warn = function() {
internalLog("warn", arguments);
}
console.info = function() {
internalLog("info", arguments);
}
console.debug = function() {
internalLog("debug", arguments);
}
console.stacktrace = function() {
return new Error().stack;
}
}
if (LOADER_CONFIG.bypassCameraRollLimit) {
((module) => {
module.MultiSelectClickHandler = new Proxy(module.MultiSelectClickHandler, {
construct: function(target, args) {
args[1].selectionLimit = 9999999;
return new target(...args);
},
});
})(require('memories_ui/src/clickhandlers/MultiSelectClickHandler'))
}
console.info("loader.js loaded");

View File

@ -156,6 +156,7 @@ class ModContext(
disableBitmoji = config.experimental.nativeHooks.disableBitmoji.get(),
disableMetrics = config.global.disableMetrics.get(),
hookAssetOpen = config.experimental.disableComposerModules.get().isNotEmpty(),
composerHooks = config.experimental.nativeHooks.composerHooks.globalState == true
)
)
}

View File

@ -4,11 +4,7 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import me.rhunk.snapenhance.core.ModContext
import me.rhunk.snapenhance.core.features.impl.COFOverride
import me.rhunk.snapenhance.core.features.impl.ConfigurationOverride
import me.rhunk.snapenhance.core.features.impl.MixerStories
import me.rhunk.snapenhance.core.features.impl.OperaViewerParamsOverride
import me.rhunk.snapenhance.core.features.impl.ScopeSync
import me.rhunk.snapenhance.core.features.impl.*
import me.rhunk.snapenhance.core.features.impl.downloader.MediaDownloader
import me.rhunk.snapenhance.core.features.impl.downloader.ProfilePictureDownloader
import me.rhunk.snapenhance.core.features.impl.experiments.*
@ -16,7 +12,6 @@ import me.rhunk.snapenhance.core.features.impl.global.*
import me.rhunk.snapenhance.core.features.impl.messaging.*
import me.rhunk.snapenhance.core.features.impl.spying.HalfSwipeNotifier
import me.rhunk.snapenhance.core.features.impl.spying.MessageLogger
import me.rhunk.snapenhance.core.features.impl.FriendMutationObserver
import me.rhunk.snapenhance.core.features.impl.spying.StealthMode
import me.rhunk.snapenhance.core.features.impl.tweaks.*
import me.rhunk.snapenhance.core.features.impl.ui.*
@ -131,6 +126,7 @@ class FeatureManager(
HideActiveMusic(),
AutoOpenSnaps(),
CustomStreaksExpirationFormat(),
ComposerHooks(),
)
initializeFeatures()

View File

@ -0,0 +1,189 @@
package me.rhunk.snapenhance.core.features.impl.experiments
import android.content.ClipData
import android.content.ClipboardManager
import android.widget.FrameLayout
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.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.google.gson.JsonObject
import kotlinx.coroutines.launch
import me.rhunk.snapenhance.common.ui.AppMaterialTheme
import me.rhunk.snapenhance.common.ui.createComposeAlertDialog
import me.rhunk.snapenhance.common.ui.createComposeView
import me.rhunk.snapenhance.core.features.Feature
import me.rhunk.snapenhance.core.features.FeatureLoadParams
import me.rhunk.snapenhance.core.util.hook.HookStage
import me.rhunk.snapenhance.core.util.hook.hook
import me.rhunk.snapenhance.nativelib.NativeLib
import kotlin.random.Random
import kotlin.random.nextInt
class ComposerHooks: Feature("ComposerHooks", loadParams = FeatureLoadParams.ACTIVITY_CREATE_SYNC) {
private val config by lazy { context.config.experimental.nativeHooks.composerHooks }
private val composerConsole by lazy {
createComposeAlertDialog(context.mainActivity!!) {
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
var result by remember { mutableStateOf("") }
var codeContent by remember { mutableStateOf("") }
Text("Composer Console", fontSize = 18.sp, fontWeight = FontWeight.Bold)
TextField(
modifier = Modifier.fillMaxWidth(),
textStyle = TextStyle.Default.copy(fontSize = 12.sp),
value = codeContent,
placeholder = { Text("Enter your JS code here:") },
onValueChange = {
codeContent = it
}
)
Button(
modifier = Modifier.fillMaxWidth(),
onClick = {
context.log.verbose("input: $codeContent", "ComposerConsole")
result = "Running..."
context.coroutineScope.launch {
result = (context.native.composerEval("""
(() => {
try {
$codeContent
} catch (e) {
return e.toString()
}
})()
""".trimIndent()) ?: "(no result)").also {
context.log.verbose("result: $it", "ComposerConsole")
}
}
}
) {
Text("Run")
}
Column(
modifier = Modifier.verticalScroll(rememberScrollState())
) {
Text(result)
}
}
}
}
private fun injectConsole() {
val root = context.mainActivity!!.findViewById<FrameLayout>(android.R.id.content)
root.post {
root.addView(createComposeView(root.context) {
AppMaterialTheme {
FilledIconButton(
onClick = {
composerConsole.show()
},
modifier = Modifier.padding(top = 100.dp, end = 16.dp)
) {
Icon(Icons.Default.BugReport, contentDescription = "Debug Console")
}
}
}.apply {
layoutParams = FrameLayout.LayoutParams(FrameLayout.LayoutParams.WRAP_CONTENT, FrameLayout.LayoutParams.WRAP_CONTENT).apply {
gravity = android.view.Gravity.TOP or android.view.Gravity.END
}
})
}
}
private fun loadHooks() {
val loaderConfig = JsonObject()
if (config.composerLogs.get()) {
val logPrefix = Random.nextInt(100000..999999).toString()
val logTag = "ComposerLogs"
ClipboardManager::class.java.hook("setPrimaryClip", HookStage.BEFORE) { param ->
val clipData = param.arg<ClipData>(0).takeIf { it.itemCount == 1 } ?: return@hook
val logText = clipData.getItemAt(0).text ?: return@hook
if (!logText.startsWith("$logPrefix|")) return@hook
val logContainer = logText.removePrefix("$logPrefix|").toString()
val logType = logContainer.substringBefore("|")
val content = logContainer.substringAfter("|")
when (logType) {
"verbose" -> context.log.verbose(content, logTag)
"info" -> context.log.info(content, logTag)
"debug" -> context.log.debug(content, logTag)
"warn" -> context.log.warn(content, logTag)
"error" -> context.log.error(content, logTag)
else -> context.log.info(logContainer, logTag)
}
param.setResult(null)
}
loaderConfig.addProperty("logPrefix", logPrefix)
}
if (config.bypassCameraRollLimit.get()) {
loaderConfig.addProperty("bypassCameraRollLimit", true)
}
val loaderScript = context.scriptRuntime.scripting.getScriptContent("composer/loader.js") ?: run {
context.log.error("Failed to load composer loader script")
return
}
val hookResult = context.native.composerEval("""
(() => { try { const LOADER_CONFIG = $loaderConfig; $loaderScript
} catch (e) {
return e.toString() + "\n" + e.stack;
}
return "success";
})()
""".trimIndent().trim())
if (hookResult != "success") {
context.shortToast(("Composer loader failed : $hookResult").also {
context.log.error(it)
})
}
if (config.composerConsole.get()) {
injectConsole()
}
}
override fun onActivityCreate() {
if (!NativeLib.initialized || config.globalState != true) return
var composerThreadTask: (() -> Unit)? = null
findClass("com.snap.composer.callable.ComposerFunctionNative").hook("nativePerform", HookStage.BEFORE) {
composerThreadTask?.invoke()
composerThreadTask = null
}
context.coroutineScope.launch {
context.native.waitForComposer()
composerThreadTask = ::loadHooks
}
}
}

View File

@ -13,6 +13,7 @@ typedef struct {
bool disable_bitmoji;
bool disable_metrics;
bool hook_asset_open;
bool composer_hooks;
} native_config_t;
namespace common {

View File

@ -0,0 +1,173 @@
#pragma once
#include <stdio.h>
namespace ComposerHook {
enum {
JS_TAG_FIRST = -11,
JS_TAG_BIG_DECIMAL = -11,
JS_TAG_BIG_INT = -10,
JS_TAG_BIG_FLOAT = -9,
JS_TAG_SYMBOL = -8,
JS_TAG_STRING = -7,
JS_TAG_MODULE = -3,
JS_TAG_FUNCTION_BYTECODE = -2,
JS_TAG_OBJECT = -1,
JS_TAG_INT = 0,
JS_TAG_BOOL = 1,
JS_TAG_NULL = 2,
JS_TAG_UNDEFINED = 3,
JS_TAG_UNINITIALIZED = 4,
JS_TAG_CATCH_OFFSET = 5,
JS_TAG_EXCEPTION = 6,
JS_TAG_FLOAT64 = 7,
};
typedef struct JSRefCountHeader {
int ref_count;
} JSRefCountHeader;
struct JSString {
JSRefCountHeader header;
uint32_t len : 31;
uint8_t is_wide_char : 1;
uint32_t hash : 30;
uint8_t atom_type : 2;
uint32_t hash_next;
union {
uint8_t str8[0];
uint16_t str16[0];
} u;
};
typedef union JSValueUnion {
int32_t int32;
double float64;
void *ptr;
} JSValueUnion;
typedef struct JSValue {
JSValueUnion u;
int64_t tag;
} JSValue;
typedef struct list_head {
struct list_head *next, *prev;
} list_head;
struct JSGCObjectHeader {
int ref_count;
uint8_t gc_obj_type : 4;
uint8_t mark : 4;
uint8_t dummy1;
uint16_t dummy2;
struct list_head link;
};
struct JSContext {
JSGCObjectHeader header;
void *rt;
struct list_head link;
uint16_t binary_object_count;
int binary_object_size;
JSValue *array_shape;
JSValue *class_proto;
JSValue function_proto;
JSValue function_ctor;
JSValue array_ctor;
JSValue regexp_ctor;
JSValue promise_ctor;
JSValue native_error_proto[8];
JSValue iterator_proto;
JSValue async_iterator_proto;
JSValue array_proto_values;
JSValue throw_type_error;
JSValue eval_obj;
JSValue global_obj;
JSValue global_var_obj;
};
static uintptr_t global_instance;
static JSContext *global_ctx;
HOOK_DEF(JSValue, js_eval, uintptr_t instance, JSContext *ctx, uintptr_t this_obj, uint8_t *input, uintptr_t input_len, const char *filename, unsigned int flags, unsigned int scope_idx) {
if (global_instance == 0 || global_ctx == nullptr) {
global_instance = instance;
global_ctx = ctx;
}
return js_eval_original(instance, ctx, this_obj, input, input_len, filename, flags, scope_idx);
}
void waitForComposer(JNIEnv *, jobject) {
while (global_instance == 0 || global_ctx == nullptr) {
usleep(10000);
}
}
jstring composerEval(JNIEnv *env, jobject, jstring script) {
if (!ARM64) return env->NewStringUTF("Architecture not supported");
if (global_instance == 0 || global_ctx == nullptr) {
return env->NewStringUTF("Composer not ready");
}
auto script_str = env->GetStringUTFChars(script, nullptr);
auto length = env->GetStringUTFLength(script);
auto jsvalue = js_eval_original(global_instance, global_ctx, (uintptr_t) &global_ctx->global_obj, (uint8_t *) script_str, length, "<input>", 0, 0);
env->ReleaseStringUTFChars(script, script_str);
if (jsvalue.tag == JS_TAG_STRING) {
auto str = (JSString *) jsvalue.u.ptr;
return env->NewStringUTF((const char *) str->u.str8);
}
std::string result;
switch (jsvalue.tag) {
case JS_TAG_INT:
result = std::to_string(jsvalue.u.int32);
break;
case JS_TAG_BOOL:
result = jsvalue.u.int32 ? "true" : "false";
break;
case JS_TAG_NULL:
result = "null";
break;
case JS_TAG_UNDEFINED:
result = "undefined";
break;
case JS_TAG_OBJECT:
result = "[object Object]";
break;
case JS_TAG_EXCEPTION:
result = "Failed to evaluate script";
break;
case JS_TAG_FLOAT64:
result = std::to_string(jsvalue.u.float64);
break;
default:
result = "[unknown tag " + std::to_string(jsvalue.tag) + "]";
break;
}
return env->NewStringUTF(result.c_str());
}
void init() {
if (!ARM64) return;
auto js_eval_ptr = util::find_signature(
common::client_module.base,
common::client_module.size,
"00 E4 00 6F 29 00 80 52 76 00 04 8B",
-0x28
);
if (js_eval_ptr == 0) {
LOGE("js_eval_ptr signature not found");
return;
}
DobbyHook((void*) js_eval_ptr, (void *) js_eval, (void **) &js_eval_original);
}
}

View File

@ -2,6 +2,7 @@
#include <string>
#include <dobby.h>
#include <vector>
#include <thread>
#include "logger.h"
#include "common.h"
@ -10,6 +11,7 @@
#include "hooks/fstat_hook.h"
#include "hooks/sqlite_mutex.h"
#include "hooks/duplex_hook.h"
#include "hooks/composer_hook.h"
bool JNICALL init(JNIEnv *env, jobject clazz) {
LOGD("Initializing native");
@ -29,13 +31,24 @@ bool JNICALL init(JNIEnv *env, jobject clazz) {
LOGD("client_module offset=0x%lx, size=0x%zx", client_module.base, client_module.size);
AssetHook::init(env);
UnaryCallHook::init(env);
FstatHook::init();
SqliteMutexHook::init();
DuplexHook::init(env);
auto threads = std::vector<std::thread>();
util::remap_sections(BUILD_PACKAGE);
#define RUN(body) \
threads.push_back(std::thread([&] { body; }))
RUN(UnaryCallHook::init(env));
RUN(AssetHook::init(env));
RUN(FstatHook::init());
RUN(SqliteMutexHook::init());
RUN(DuplexHook::init(env));
if (common::native_config->composer_hooks) {
RUN(ComposerHook::init());
}
RUN(util::remap_sections(BUILD_PACKAGE));
for (auto &thread : threads) {
thread.join();
}
LOGD("Native initialized");
return true;
@ -49,6 +62,7 @@ void JNICALL load_config(JNIEnv *env, jobject, jobject config_object) {
native_config->disable_bitmoji = GET_CONFIG_BOOL("disableBitmoji");
native_config->disable_metrics = GET_CONFIG_BOOL("disableMetrics");
native_config->hook_asset_open = GET_CONFIG_BOOL("hookAssetOpen");
native_config->composer_hooks = GET_CONFIG_BOOL("composerHooks");
}
void JNICALL lock_database(JNIEnv *env, jobject, jstring database_name, jobject runnable) {
@ -80,6 +94,8 @@ extern "C" JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *_) {
methods.push_back({"init", "()Z", (void *)init});
methods.push_back({"loadConfig", "(L" BUILD_NAMESPACE "/NativeConfig;)V", (void *)load_config});
methods.push_back({"lockDatabase", "(Ljava/lang/String;Ljava/lang/Runnable;)V", (void *)lock_database});
methods.push_back({"waitForComposer", "()V", (void *) ComposerHook::waitForComposer});
methods.push_back({"composerEval", "(Ljava/lang/String;)Ljava/lang/String;",(void *) ComposerHook::composerEval});
env->RegisterNatives(env->FindClass(std::string(BUILD_NAMESPACE "/NativeLib").c_str()), methods.data(), methods.size());
return JNI_VERSION_1_6;

View File

@ -4,4 +4,5 @@ data class NativeConfig(
val disableBitmoji: Boolean = false,
val disableMetrics: Boolean = false,
val hookAssetOpen: Boolean = false,
val composerHooks: Boolean = false,
)

View File

@ -62,4 +62,6 @@ class NativeLib {
private external fun init(): Boolean
private external fun loadConfig(config: NativeConfig)
private external fun lockDatabase(name: String, callback: Runnable)
external fun waitForComposer()
external fun composerEval(code: String): String?
}