commit 4fd268c4dc9131d83648701d9a524a604f666c53
parent 5563418d79596e7185a47fb51f65f14f9828d8d2
Author: rhunk <101876869+rhunk@users.noreply.github.com>
Date:   Thu, 25 Jan 2024 23:26:54 +0100

feat(native): database lock
- refactor native

Diffstat:
Mcore/src/main/kotlin/me/rhunk/snapenhance/core/SnapEnhance.kt | 2+-
Mcore/src/main/kotlin/me/rhunk/snapenhance/core/database/DatabaseAccess.kt | 11+++++++++--
Anative/jni/src/common.h | 24++++++++++++++++++++++++
Dnative/jni/src/config.h | 8--------
Dnative/jni/src/grpc.h | 16----------------
Anative/jni/src/hooks/asset_hook.h | 26++++++++++++++++++++++++++
Anative/jni/src/hooks/fstat_hook.h | 30++++++++++++++++++++++++++++++
Anative/jni/src/hooks/sqlite_mutex.h | 48++++++++++++++++++++++++++++++++++++++++++++++++
Anative/jni/src/hooks/unary_call.h | 89+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mnative/jni/src/library.cpp | 166++++++++++++++++++-------------------------------------------------------------
Mnative/src/main/kotlin/me/rhunk/snapenhance/nativelib/NativeLib.kt | 19+++++++++++++++----
11 files changed, 279 insertions(+), 160 deletions(-)

diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/SnapEnhance.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/SnapEnhance.kt @@ -183,7 +183,7 @@ class SnapEnhance { val libName = param.arg<String>(2) if (libName != "client") return@hook unhook() - appContext.native.initOnce(appContext.androidContext.classLoader) + appContext.native.initOnce() appContext.native.nativeUnaryCallCallback = { request -> appContext.event.post(NativeUnaryCallEvent(request.uri, request.buffer)) { request.buffer = buffer diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/database/DatabaseAccess.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/database/DatabaseAccess.kt @@ -17,6 +17,7 @@ import me.rhunk.snapenhance.common.util.ktx.getStringOrNull import me.rhunk.snapenhance.common.util.protobuf.ProtoReader import me.rhunk.snapenhance.core.ModContext import me.rhunk.snapenhance.core.manager.Manager +import me.rhunk.snapenhance.nativelib.NativeLib enum class DatabaseType( @@ -59,9 +60,15 @@ class DatabaseAccess( } - private inline fun <T> SQLiteDatabase.performOperation(crossinline query: SQLiteDatabase.() -> T?): T? { + private fun <T> SQLiteDatabase.performOperation(query: SQLiteDatabase.() -> T?): T? { return runCatching { - synchronized(this) { + if (NativeLib.initialized && openedDatabases[DatabaseType.ARROYO] == this) { + var result: T? = null + context.native.lockNativeDatabase(DatabaseType.ARROYO.fileName) { + result = query() + } + result + } else synchronized(this) { query() } }.onFailure { diff --git a/native/jni/src/common.h b/native/jni/src/common.h @@ -0,0 +1,24 @@ +#pragma once + +#include <stdint.h> +#include "util.h" + +#ifdef __aarch64__ +#define ARM64 true +#else +#define ARM64 false +#endif + +typedef struct { + bool disable_bitmoji; + bool disable_metrics; + bool hook_asset_open; +} native_config_t; + +namespace common { + static JavaVM *java_vm; + static jobject native_lib_object; + + static util::module_info_t client_module; + static native_config_t *native_config = new native_config_t; +} diff --git a/native/jni/src/config.h b/native/jni/src/config.h @@ -1,7 +0,0 @@ -#pragma once - -typedef struct { - bool disable_bitmoji; - bool disable_metrics; - bool hook_asset_open; -} native_config_t;- \ No newline at end of file diff --git a/native/jni/src/grpc.h b/native/jni/src/grpc.h @@ -1,16 +0,0 @@ -#pragma once - -namespace grpc { - typedef struct { - void* ref_counter; - size_t length; - uint8_t* data; - } ref_counted_slice_byte_buffer; - - typedef struct { - void* reserved; - void* type; - void* compression; - ref_counted_slice_byte_buffer *slice_buffer; - } grpc_byte_buffer; -} diff --git a/native/jni/src/hooks/asset_hook.h b/native/jni/src/hooks/asset_hook.h @@ -0,0 +1,25 @@ +#pragma once + + +namespace AssetHook { + jmethodID native_lib_on_asset_load; + AAsset* (*AAssetManager_open_original)(AAssetManager*, const char*, int) = nullptr; + + AAsset* AAssetManager_open_hook(AAssetManager* mgr, const char* filename, int mode) { + if (common::native_config->hook_asset_open) { + JNIEnv *env = nullptr; + common::java_vm->GetEnv((void **)&env, JNI_VERSION_1_6); + + if (!env->CallBooleanMethod(common::native_lib_object, native_lib_on_asset_load, env->NewStringUTF(filename))) { + return nullptr; + } + } + + return AAssetManager_open_original(mgr, filename, mode); + } + + void init(JNIEnv *env) { + native_lib_on_asset_load = env->GetMethodID(env->GetObjectClass(common::native_lib_object), "shouldLoadAsset", "(Ljava/lang/String;)Z"); + DobbyHook((void *) AAssetManager_open, (void *) AAssetManager_open_hook, (void **) &AAssetManager_open_original); + } +}+ \ No newline at end of file diff --git a/native/jni/src/hooks/fstat_hook.h b/native/jni/src/hooks/fstat_hook.h @@ -0,0 +1,29 @@ +#pragma once + +namespace FstatHook { + auto fstat_original = (int (*)(int, struct stat *)) nullptr; + + int fstat_hook(int fd, struct stat *buf) { + char name[256]; + memset(name, 0, sizeof(name)); + snprintf(name, sizeof(name), "/proc/self/fd/%d", fd); + readlink(name, name, sizeof(name)); + + std::string fileName(name); + + if (common::native_config->disable_metrics && fileName.find("files/blizzardv2/queues") != std::string::npos) { + unlink(name); + return -1; + } + + if (common::native_config->disable_bitmoji && fileName.find("com.snap.file_manager_4_SCContent") != std::string::npos) { + return -1; + } + + return fstat_original(fd, buf); + } + + void init() { + DobbyHook((void *)DobbySymbolResolver("libc.so", "fstat"), (void *)fstat_hook, (void **)&fstat_original); + } +}+ \ No newline at end of file diff --git a/native/jni/src/hooks/sqlite_mutex.h b/native/jni/src/hooks/sqlite_mutex.h @@ -0,0 +1,47 @@ +#pragma once + +#include <map> + +namespace SqliteMutexHook { + typedef struct { + pthread_mutex_t mutex; + } sqlite3_mutex; + + typedef struct { + void* pVfs; + void* pVdbe; + void* pDfltColl; + sqlite3_mutex* mutex; + } sqlite3; + + static std::map<std::string, sqlite3_mutex *> mutex_map = {}; + static int (*sqlite3_open_original)(const char *, sqlite3 **, unsigned int, const char *) = nullptr; + + int sqlite3_open_hook(const char *filename, sqlite3 **ppDb, unsigned int flags, const char *zVfs) { + auto result = sqlite3_open_original(filename, ppDb, flags, zVfs); + if (result == 0) { + auto mutex = (*ppDb)->mutex; + if (mutex == nullptr) return result; + + auto last_slash = strrchr(filename, '/') + 1; + if (last_slash > filename) { + LOGD("sqlite3_open_hook: %s", last_slash); + mutex_map[last_slash] = mutex; + } + } + return result; + } + + void init() { + auto open_database_sig = util::find_signature( + common::client_module.base, common::client_module.size, + ARM64 ? "FF FF 00 A9 3F 00 00 F9" : "8D B0 06 46 E7 48", + ARM64 ? -0x3C : -0x7 + ); + if (open_database_sig == 0) { + LOGE("sqlite3 openDatabase sig not found"); + return; + } + DobbyHook((void *) open_database_sig, (void *) sqlite3_open_hook, (void **) &sqlite3_open_original); + } +}+ \ No newline at end of file diff --git a/native/jni/src/hooks/unary_call.h b/native/jni/src/hooks/unary_call.h @@ -0,0 +1,88 @@ +#pragma once + +#include "../common.h" +#include "../util.h" + +namespace UnaryCallHook { + namespace grpc { + typedef struct { + void* ref_counter; + size_t length; + uint8_t* data; + } ref_counted_slice_byte_buffer; + + typedef struct { + void* reserved; + void* type; + void* compression; + ref_counted_slice_byte_buffer *slice_buffer; + } grpc_byte_buffer; + } + + static void *(*unaryCall_original)(void *, const char *, grpc::grpc_byte_buffer **, void *, void *, void *); + static jmethodID native_lib_on_unary_call_method; + + void *unaryCall_hook(void *unk1, const char *uri, grpc::grpc_byte_buffer **buffer_ptr, void *unk4, void *unk5, void *unk6) { + // request without reference counter can be hooked using xposed ig + auto slice_buffer = (*buffer_ptr)->slice_buffer; + if (slice_buffer->ref_counter == 0) { + return unaryCall_original(unk1, uri, buffer_ptr, unk4, unk5, unk6); + } + + JNIEnv *env = nullptr; + common::java_vm->GetEnv((void **)&env, JNI_VERSION_1_6); + + auto jni_buffer_array = env->NewByteArray(slice_buffer->length); + env->SetByteArrayRegion(jni_buffer_array, 0, slice_buffer->length, (jbyte *)slice_buffer->data); + + auto native_request_data_object = env->CallObjectMethod(common::native_lib_object, native_lib_on_unary_call_method, env->NewStringUTF(uri), jni_buffer_array); + + if (native_request_data_object != nullptr) { + auto native_request_data_class = env->GetObjectClass(native_request_data_object); + auto is_canceled = env->GetBooleanField(native_request_data_object, env->GetFieldID(native_request_data_class, "canceled", "Z")); + + if (is_canceled) { + LOGD("canceled request for %s", uri); + return nullptr; + } + + auto new_buffer = (jbyteArray)env->GetObjectField(native_request_data_object, env->GetFieldID(native_request_data_class, "buffer", "[B")); + auto new_buffer_length = env->GetArrayLength(new_buffer); + auto new_buffer_data = env->GetByteArrayElements(new_buffer, nullptr); + + //we need to allocate a new ref_counter struct and copy the old ref_counter and the new_buffer to it + const static auto ref_counter_struct_size = (uintptr_t)slice_buffer->data - (uintptr_t)slice_buffer->ref_counter; + + auto new_ref_counter = malloc(ref_counter_struct_size + new_buffer_length); + //copy the old ref_counter and the native_request_data_object + memcpy(new_ref_counter, slice_buffer->ref_counter, ref_counter_struct_size); + memcpy((void *)((uintptr_t)new_ref_counter + ref_counter_struct_size), new_buffer_data, new_buffer_length); + + //free the old ref_counter + free(slice_buffer->ref_counter); + + //update the slice_buffer + slice_buffer->ref_counter = new_ref_counter; + slice_buffer->length = new_buffer_length; + slice_buffer->data = (uint8_t *)((uintptr_t)new_ref_counter + ref_counter_struct_size); + } + + return unaryCall_original(unk1, uri, buffer_ptr, unk4, unk5, unk6); + } + + void init(JNIEnv *env) { + auto unaryCall_func = util::find_signature( + common::client_module.base, common::client_module.size, + ARM64 ? "A8 03 1F F8 C2 00 00 94" : "0A 90 00 F0 3F F9", + ARM64 ? -0x48 : -0x37 + ); + + native_lib_on_unary_call_method = env->GetMethodID(env->GetObjectClass(common::native_lib_object), "onNativeUnaryCall", "(Ljava/lang/String;[B)L" BUILD_NAMESPACE "/NativeRequestData;"); + + if (unaryCall_func != 0) { + DobbyHook((void *)unaryCall_func, (void *)unaryCall_hook, (void **)&unaryCall_original); + } else { + LOGE("can't find unaryCall signature"); + } + } +}+ \ No newline at end of file diff --git a/native/jni/src/library.cpp b/native/jni/src/library.cpp @@ -5,120 +5,19 @@ #include <android/asset_manager.h> #include "logger.h" -#include "config.h" -#include "util.h" -#include "grpc.h" - -#ifdef __aarch64__ -#define ARM64 true -#else -#define ARM64 false -#endif - -static native_config_t *native_config; -static JavaVM *java_vm; -static jobject native_lib_object; -static jmethodID native_lib_on_unary_call_method; -static jmethodID native_lib_on_asset_load; - -// original functions -static void *(*unaryCall_original)(void *, const char *, grpc::grpc_byte_buffer **, void *, void *, void *); -static auto fstat_original = (int (*)(int, struct stat *)) nullptr; -static AAsset* (*AAssetManager_open_original)(AAssetManager*, const char*, int) = nullptr; - -static int fstat_hook(int fd, struct stat *buf) { - char name[256]; - memset(name, 0, sizeof(name)); - snprintf(name, sizeof(name), "/proc/self/fd/%d", fd); - readlink(name, name, sizeof(name)); - - std::string fileName(name); - - if (native_config->disable_metrics && fileName.find("files/blizzardv2/queues") != std::string::npos) { - unlink(name); - return -1; - } - - if (native_config->disable_bitmoji && fileName.find("com.snap.file_manager_4_SCContent") != std::string::npos) { - return -1; - } - - return fstat_original(fd, buf); -} - - -static void *unaryCall_hook(void *unk1, const char *uri, grpc::grpc_byte_buffer **buffer_ptr, void *unk4, void *unk5, void *unk6) { - // request without reference counter can be hooked using xposed ig - auto slice_buffer = (*buffer_ptr)->slice_buffer; - if (slice_buffer->ref_counter == 0) { - return unaryCall_original(unk1, uri, buffer_ptr, unk4, unk5, unk6); - } - - JNIEnv *env = nullptr; - java_vm->GetEnv((void **)&env, JNI_VERSION_1_6); - - auto jni_buffer_array = env->NewByteArray(slice_buffer->length); - env->SetByteArrayRegion(jni_buffer_array, 0, slice_buffer->length, (jbyte *)slice_buffer->data); +#include "common.h" +#include "hooks/asset_hook.h" +#include "hooks/unary_call.h" +#include "hooks/fstat_hook.h" +#include "hooks/sqlite_mutex.h" - auto native_request_data_object = env->CallObjectMethod(native_lib_object, native_lib_on_unary_call_method, env->NewStringUTF(uri), jni_buffer_array); - - if (native_request_data_object != nullptr) { - auto native_request_data_class = env->GetObjectClass(native_request_data_object); - auto is_canceled = env->GetBooleanField(native_request_data_object, env->GetFieldID(native_request_data_class, "canceled", "Z")); - - if (is_canceled) { - LOGD("canceled request for %s", uri); - return nullptr; - } - - auto new_buffer = (jbyteArray)env->GetObjectField(native_request_data_object, env->GetFieldID(native_request_data_class, "buffer", "[B")); - auto new_buffer_length = env->GetArrayLength(new_buffer); - auto new_buffer_data = env->GetByteArrayElements(new_buffer, nullptr); - - //we need to allocate a new ref_counter struct and copy the old ref_counter and the new_buffer to it - const static auto ref_counter_struct_size = (uintptr_t)slice_buffer->data - (uintptr_t)slice_buffer->ref_counter; - - auto new_ref_counter = malloc(ref_counter_struct_size + new_buffer_length); - //copy the old ref_counter and the native_request_data_object - memcpy(new_ref_counter, slice_buffer->ref_counter, ref_counter_struct_size); - memcpy((void *)((uintptr_t)new_ref_counter + ref_counter_struct_size), new_buffer_data, new_buffer_length); - - //free the old ref_counter - free(slice_buffer->ref_counter); - - //update the slice_buffer - slice_buffer->ref_counter = new_ref_counter; - slice_buffer->length = new_buffer_length; - slice_buffer->data = (uint8_t *)((uintptr_t)new_ref_counter + ref_counter_struct_size); - } - - return unaryCall_original(unk1, uri, buffer_ptr, unk4, unk5, unk6); -} - -static AAsset* AAssetManager_open_hook(AAssetManager* mgr, const char* filename, int mode) { - if (native_config->hook_asset_open) { - JNIEnv *env = nullptr; - java_vm->GetEnv((void **)&env, JNI_VERSION_1_6); - - if (!env->CallBooleanMethod(native_lib_object, native_lib_on_asset_load, env->NewStringUTF(filename))) { - return nullptr; - } - } - - return AAssetManager_open_original(mgr, filename, mode); -} - -void JNICALL init(JNIEnv *env, jobject clazz, jobject classloader) { +void JNICALL init(JNIEnv *env, jobject clazz) { LOGD("Initializing native"); - // config - native_config = new native_config_t; + using namespace common; - // native lib object + native_config = new native_config_t; native_lib_object = env->NewGlobalRef(clazz); - native_lib_on_unary_call_method = env->GetMethodID(env->GetObjectClass(clazz), "onNativeUnaryCall", "(Ljava/lang/String;[B)L" BUILD_NAMESPACE "/NativeRequestData;"); - native_lib_on_asset_load = env->GetMethodID(env->GetObjectClass(clazz), "shouldLoadAsset", "(Ljava/lang/String;)Z"); - - auto client_module = util::get_module("libclient.so"); + client_module = util::get_module("libclient.so"); if (client_module.base == 0) { LOGE("libclient not loaded!"); @@ -127,44 +26,53 @@ void JNICALL init(JNIEnv *env, jobject clazz, jobject classloader) { LOGD("libclient.so base=0x%0lx, size=0x%0lx", client_module.base, client_module.size); - // hooks - DobbyHook((void *)DobbySymbolResolver("libc.so", "fstat"), (void *)fstat_hook, (void **)&fstat_original); - - auto unaryCall_func = util::find_signature( - client_module.base, client_module.size, - ARM64 ? "A8 03 1F F8 C2 00 00 94" : "0A 90 00 F0 3F F9", - ARM64 ? -0x48 : -0x37 - ); + AssetHook::init(env); + UnaryCallHook::init(env); + FstatHook::init(); + SqliteMutexHook::init(); - if (unaryCall_func != 0) { - LOGD("found unaryCall at 0x%0lx", unaryCall_func); - DobbyHook((void *)unaryCall_func, (void *)unaryCall_hook, (void **)&unaryCall_original); - } else { - LOGE("can't find unaryCall signature"); - } - - DobbyHook((void *) AAssetManager_open, (void *) AAssetManager_open_hook, (void **) &AAssetManager_open_original); LOGD("Native initialized"); } void JNICALL load_config(JNIEnv *env, jobject _, jobject config_object) { auto native_config_clazz = env->GetObjectClass(config_object); #define GET_CONFIG_BOOL(name) env->GetBooleanField(config_object, env->GetFieldID(native_config_clazz, name, "Z")) + auto native_config = common::native_config; 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"); } +void JNICALL lock_database(JNIEnv *env, jobject _, jstring database_name, jobject runnable) { + auto database_name_str = env->GetStringUTFChars(database_name, nullptr); + auto mutex = SqliteMutexHook::mutex_map[database_name_str]; + env->ReleaseStringUTFChars(database_name, database_name_str); + + if (mutex != nullptr) { + auto lock_result = pthread_mutex_lock(&mutex->mutex); + if (lock_result != 0) { + LOGE("pthread_mutex_lock failed: %d", lock_result); + return; + } + } + + env->CallVoidMethod(runnable, env->GetMethodID(env->GetObjectClass(runnable), "run", "()V")); + + if (mutex != nullptr) { + pthread_mutex_unlock(&mutex->mutex); + } +} + extern "C" JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *_) { - // register native methods - java_vm = vm; + common::java_vm = vm; JNIEnv *env = nullptr; vm->GetEnv((void **)&env, JNI_VERSION_1_6); auto methods = std::vector<JNINativeMethod>(); - methods.push_back({"init", "(Ljava/lang/ClassLoader;)V", (void *)init}); + methods.push_back({"init", "()V", (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}); env->RegisterNatives(env->FindClass(std::string(BUILD_NAMESPACE "/NativeLib").c_str()), methods.data(), methods.size()); return JNI_VERSION_1_6; diff --git a/native/src/main/kotlin/me/rhunk/snapenhance/nativelib/NativeLib.kt b/native/src/main/kotlin/me/rhunk/snapenhance/nativelib/NativeLib.kt @@ -11,11 +11,11 @@ class NativeLib { private set } - fun initOnce(classloader: ClassLoader) { + fun initOnce() { if (initialized) throw IllegalStateException("NativeLib already initialized") runCatching { System.loadLibrary(BuildConfig.NATIVE_NAME) - init(classloader) + init() initialized = true }.onFailure { Log.e("SnapEnhance", "NativeLib init failed") @@ -24,7 +24,6 @@ class NativeLib { @Suppress("unused") private fun onNativeUnaryCall(uri: String, buffer: ByteArray): NativeRequestData? { - // Log.d("SnapEnhance", "onNativeUnaryCall: uri=$uri, bufferSize=${buffer.size}, buffer=${buffer.contentToString()}") val nativeRequestData = NativeRequestData(uri, buffer) runCatching { nativeUnaryCallCallback(nativeRequestData) @@ -45,6 +44,18 @@ class NativeLib { loadConfig(config) } - private external fun init(classLoader: ClassLoader) + fun lockNativeDatabase(name: String, callback: () -> Unit) { + if (!initialized) return + lockDatabase(name) { + runCatching { + callback() + }.onFailure { + Log.e("SnapEnhance", "lockNativeDatabase callback failed", it) + } + } + } + + private external fun init() private external fun loadConfig(config: NativeConfig) + private external fun lockDatabase(name: String, callback: Runnable) } \ No newline at end of file