feat(native): database lock

- refactor native
This commit is contained in:
rhunk
2024-01-25 23:26:54 +01:00
parent 5563418d79
commit 4fd268c4dc
11 changed files with 275 additions and 159 deletions

View File

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

View File

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

24
native/jni/src/common.h Normal file
View File

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

View File

@ -1,7 +0,0 @@
#pragma once
typedef struct {
bool disable_bitmoji;
bool disable_metrics;
bool hook_asset_open;
} native_config_t;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -5,120 +5,19 @@
#include <android/asset_manager.h>
#include "logger.h"
#include "config.h"
#include "util.h"
#include "grpc.h"
#include "common.h"
#include "hooks/asset_hook.h"
#include "hooks/unary_call.h"
#include "hooks/fstat_hook.h"
#include "hooks/sqlite_mutex.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);
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
using namespace common;
native_config = new native_config_t;
// native lib object
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);
AssetHook::init(env);
UnaryCallHook::init(env);
FstatHook::init();
SqliteMutexHook::init();
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
);
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;

View File

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