commit bf5031d0d0adf72c7ca65c6c57a7284dc038fb0d
parent 84a4f87fb1cc7498d92e9a468272fd80f5393f44
Author: rhunk <101876869+rhunk@users.noreply.github.com>
Date:   Sun, 27 Aug 2023 21:09:48 +0200

feat(native): native grpc unaryCall hook

Diffstat:
Anative/jni/src/grpc.h | 16++++++++++++++++
Mnative/jni/src/library.cpp | 97+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------
Mnative/jni/src/util.h | 51+++++++++++++++++++++++++++++++++++++++++----------
Mnative/src/main/kotlin/me/rhunk/snapenhance/nativelib/NativeLib.kt | 18++++++++++++++++++
Anative/src/main/kotlin/me/rhunk/snapenhance/nativelib/NativeRequestData.kt | 8++++++++
5 files changed, 173 insertions(+), 17 deletions(-)

diff --git a/native/jni/src/grpc.h b/native/jni/src/grpc.h @@ -0,0 +1,16 @@ +#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/library.cpp b/native/jni/src/library.cpp @@ -7,8 +7,10 @@ #include "logger.h" #include "config.h" #include "util.h" +#include "grpc.h" static native_config_t *native_config; +static JavaVM *java_vm; static auto fstat_original = (int (*)(int, struct stat *)) nullptr; static int fstat_hook(int fd, struct stat *buf) { @@ -20,13 +22,15 @@ static int fstat_hook(int fd, struct stat *buf) { auto fileName = std::string(name); //prevent blizzardv2 metrics - if (native_config->disable_metrics && fileName.find("files/blizzardv2/queues") != std::string::npos) { + if (native_config->disable_metrics && + fileName.find("files/blizzardv2/queues") != std::string::npos) { unlink(name); return -1; } //prevent bitmoji to load - if (native_config->disable_bitmoji && fileName.find("com.snap.file_manager_4_SCContent") != std::string::npos) { + if (native_config->disable_bitmoji && + fileName.find("com.snap.file_manager_4_SCContent") != std::string::npos) { return -1; } @@ -34,12 +38,86 @@ static int fstat_hook(int fd, struct stat *buf) { } +static jobject native_lib_object; +static jmethodID native_lib_on_unary_call_method; + +static auto unaryCall_original = (void *(*)(void *, const char *, grpc::grpc_byte_buffer **, void *, + void *, void *)) nullptr; + +static void * +unaryCall_hook(void *unk1, const char *uri, grpc::grpc_byte_buffer **buffer_ptr, void *unk4, + void *unk5, void *unk6) { + auto slice_buffer = (*buffer_ptr)->slice_buffer; + // request without reference counter can be hooked using xposed ig + if (slice_buffer->ref_counter == nullptr) { + return unaryCall_original(unk1, uri, buffer_ptr, unk4, unk5, unk6); + } + + auto env = (JNIEnv *) 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) { + return nullptr; + } + + auto new_buffer = env->GetObjectField(native_request_data_object, + env->GetFieldID(native_request_data_class, "buffer", + "[B")); + auto new_buffer_length = env->GetArrayLength((jbyteArray) new_buffer); + auto new_buffer_data = env->GetByteArrayElements((jbyteArray) 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 auto ref_counter_struct_size = + (uintptr_t) slice_buffer->data - (uintptr_t) slice_buffer->ref_counter; + + auto new_ref_counter = (void *) 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((void *) 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); + + env->ReleaseByteArrayElements((jbyteArray) native_request_data_object, new_buffer_data, 0); + } + + return unaryCall_original(unk1, uri, buffer_ptr, unk4, unk5, unk6); +} + + extern "C" JNIEXPORT void JNICALL init(JNIEnv *env, jobject clazz, jobject classloader) { LOGD("initializing native"); // config 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)Lme/rhunk/snapenhance/nativelib/NativeRequestData;"); + // load libclient.so util::load_library(env, classloader, "client"); auto client_module = util::get_module("libclient.so"); @@ -53,15 +131,18 @@ init(JNIEnv *env, jobject clazz, jobject classloader) { // hooks DobbyHook((void *) DobbySymbolResolver("libc.so", "fstat"), (void *) fstat_hook, (void **) &fstat_original); + //signature might change in the future (unstable for now) + DobbyHook((void *) util::find_signature(client_module.base, client_module.size, + "FD 7B BA A9 FC 6F 01 A9 FA 67 02 A9 F8 5F 03 A9 F6 57 04 A9 F4 4F 05 A9 FD 03 00 91 FF 43 13 D1"), + (void *) unaryCall_hook, + (void **) &unaryCall_original); LOGD("native initialized"); } - -extern "C" JNIEXPORT void JNICALL -loadConfig(JNIEnv *env, jobject clazz, jobject config_object) { +void JNICALL load_config(JNIEnv *env, jobject clazz, 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")) +#define GET_CONFIG_BOOL(name) env->GetBooleanField(config_object, env->GetFieldID(native_config_clazz, name, "Z")) native_config->disable_bitmoji = GET_CONFIG_BOOL("disableBitmoji"); native_config->disable_metrics = GET_CONFIG_BOOL("disableMetrics"); @@ -72,13 +153,15 @@ loadConfig(JNIEnv *env, jobject clazz, jobject config_object) { //jni onload extern "C" JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *reserved) { + java_vm = vm; // register native methods 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({"loadConfig", "(Lme/rhunk/snapenhance/nativelib/NativeConfig;)V", (void *) loadConfig}); + methods.push_back({"loadConfig", "(Lme/rhunk/snapenhance/nativelib/NativeConfig;)V", + (void *) load_config}); env->RegisterNatives( env->FindClass("me/rhunk/snapenhance/nativelib/NativeLib"), diff --git a/native/jni/src/util.h b/native/jni/src/util.h @@ -1,4 +1,5 @@ #pragma once + #include <unistd.h> #include <dlfcn.h> #include <elf.h> @@ -9,8 +10,8 @@ namespace util { size_t size; } ModuleInfo; - static void hexDump(void* ptr, uint8_t line_length, uint32_t lines) { - auto* p = (unsigned char*)ptr; + static void hexDump(void *ptr, uint8_t line_length, uint32_t lines) { + auto *p = (unsigned char *) ptr; for (uint8_t i = 0; i < lines; i++) { std::string line; for (uint8_t j = 0; j < line_length; j++) { @@ -23,12 +24,11 @@ namespace util { } } - static ModuleInfo get_module(const char* libname) - { + static ModuleInfo get_module(const char *libname) { char path[256]; char buff[256]; int len_libname = strlen(libname); - FILE* file; + FILE *file; uintptr_t addr = 0; size_t size = 0; @@ -38,8 +38,8 @@ namespace util { return {0, 0}; while (fgets(buff, sizeof buff, file) != NULL) { - int len = strlen(buff); - if (len > 0 && buff[len-1] == '\n') { + int len = strlen(buff); + if (len > 0 && buff[len - 1] == '\n') { buff[--len] = '\0'; } if (len <= len_libname || memcmp(buff + len - len_libname, libname, len_libname)) { @@ -63,11 +63,42 @@ namespace util { return {addr, size}; } - void load_library(JNIEnv* env, jobject classLoader, const char* libName) { + void load_library(JNIEnv *env, jobject classLoader, const char *libName) { auto runtimeClass = env->FindClass("java/lang/Runtime"); - auto getRuntimeMethod = env->GetStaticMethodID(runtimeClass, "getRuntime", "()Ljava/lang/Runtime;"); + auto getRuntimeMethod = env->GetStaticMethodID(runtimeClass, "getRuntime", + "()Ljava/lang/Runtime;"); auto runtime = env->CallStaticObjectMethod(runtimeClass, getRuntimeMethod); - auto loadLibraryMethod = env->GetMethodID(runtimeClass, "loadLibrary0", "(Ljava/lang/ClassLoader;Ljava/lang/String;)V"); + auto loadLibraryMethod = env->GetMethodID(runtimeClass, "loadLibrary0", + "(Ljava/lang/ClassLoader;Ljava/lang/String;)V"); env->CallVoidMethod(runtime, loadLibraryMethod, classLoader, env->NewStringUTF(libName)); } + + uintptr_t find_signature(uintptr_t module_base, uintptr_t size, const std::string &pattern) { + std::vector<char> bytes; + std::vector<char> mask; + for (size_t i = 0; i < pattern.size(); i += 3) { + if (pattern[i] == '?') { + bytes.push_back(0); + mask.push_back('?'); + } else { + bytes.push_back(std::stoi(pattern.substr(i, 2), nullptr, 16)); + mask.push_back('x'); + } + } + + for (size_t i = 0; i < size; i++) { + bool found = true; + for (size_t j = 0; j < bytes.size(); j++) { + if (mask[j] == '?' || bytes[j] == *(char *) (module_base + i + j)) { + continue; + } + found = false; + break; + } + if (found) { + return module_base + i; + } + } + return 0; + } } \ No newline at end of file diff --git a/native/src/main/kotlin/me/rhunk/snapenhance/nativelib/NativeLib.kt b/native/src/main/kotlin/me/rhunk/snapenhance/nativelib/NativeLib.kt @@ -1,11 +1,29 @@ package me.rhunk.snapenhance.nativelib +import android.util.Log + class NativeLib { + var nativeUnaryCallCallback: (NativeRequestData) -> Unit = {} + fun initOnce(classloader: ClassLoader) { System.loadLibrary("nativelib") init(classloader) } + @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) + }.onFailure { + Log.e("SnapEnhance", "nativeUnaryCallCallback failed", it) + } + if (!nativeRequestData.buffer.contentEquals(buffer) || nativeRequestData.canceled) return nativeRequestData + return null + } + + external fun init(classLoader: ClassLoader) external fun loadConfig(config: NativeConfig) } \ No newline at end of file diff --git a/native/src/main/kotlin/me/rhunk/snapenhance/nativelib/NativeRequestData.kt b/native/src/main/kotlin/me/rhunk/snapenhance/nativelib/NativeRequestData.kt @@ -0,0 +1,7 @@ +package me.rhunk.snapenhance.nativelib + +data class NativeRequestData( + val uri: String, + var buffer: ByteArray, + var canceled: Boolean = false, +)+ \ No newline at end of file