From 4278531cadc4fba9298b3f1c554ef4a1277b4220 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=88=9A=28noham=29=C2=B2?= <100566912+NohamR@users.noreply.github.com> Date: Tue, 25 Aug 2026 23:04:08 +0200 Subject: [PATCH] Add RMHook core with configurable endpoint Initialize the project with Android/Qt hook infrastructure and repository setup files --- .gitignore | 14 +++ .gitmodules | 3 + src/Config.cpp | 320 +++++++++++++++++++++++++++++++++++++++++++++++++ src/Config.h | 20 ++++ src/rmhook.cpp | 193 +++++++++++++++++++++++++++++ 5 files changed, 550 insertions(+) create mode 100644 .gitignore create mode 100644 .gitmodules create mode 100644 src/Config.cpp create mode 100644 src/Config.h create mode 100644 src/rmhook.cpp diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7c8f2aa --- /dev/null +++ b/.gitignore @@ -0,0 +1,14 @@ +aqt_venv/ +rev/ +app/ +build/ +output/ +config/ +*.apk +*.apk.idsig +*.keystore +libs/*.a +libs/*.o +libs/*.so +.DS_Store +rmhook.conf \ No newline at end of file diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..bdb7290 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "libs/shadowhook"] + path = libs/shadowhook + url = https://github.com/bytedance/android-inline-hook diff --git a/src/Config.cpp b/src/Config.cpp new file mode 100644 index 0000000..71ed35b --- /dev/null +++ b/src/Config.cpp @@ -0,0 +1,320 @@ +#include "Config.h" + +#include +#include + +#include +#include +#include +#include +#include + +#define TAG "RMHook" +#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, TAG, __VA_ARGS__) +#define LOGW(...) __android_log_print(ANDROID_LOG_WARN, TAG, __VA_ARGS__) + +QString gConfiguredHost; +int gConfiguredPort = 0; + +static JavaVM *gJvm = nullptr; +static jobject gContext = nullptr; + +extern "C" void startHooks(); + +static JNIEnv *getJNIEnv() { + if (!gJvm) { + return nullptr; + } + + JNIEnv *env = nullptr; + jint result = gJvm->GetEnv(reinterpret_cast(&env), JNI_VERSION_1_6); + if (result == JNI_EDETACHED) { + if (gJvm->AttachCurrentThread(&env, nullptr) != JNI_OK) { + return nullptr; + } + return env; + } + return result == JNI_OK ? env : nullptr; +} + +static bool clearJavaException(JNIEnv *env, const char *operation) { + if (!env || !env->ExceptionCheck()) { + return false; + } + + LOGW("JNI exception while %s", operation); + env->ExceptionClear(); + return true; +} + +static bool isValidConfig(const QString &host, int port) { + return !host.trimmed().isEmpty() && port > 0 && port <= 65535; +} + +static bool setConfiguration(const QString &host, int port) { + if (!isValidConfig(host, port)) { + return false; + } + + gConfiguredHost = host.trimmed(); + gConfiguredPort = port; + return true; +} + +static std::string trimLine(const char *value) { + std::string line(value ? value : ""); + while (!line.empty() && (line.back() == '\n' || line.back() == '\r' || line.back() == ' ' || line.back() == '\t')) { + line.pop_back(); + } + size_t first = 0; + while (first < line.size() && (line[first] == ' ' || line[first] == '\t')) { + ++first; + } + return line.substr(first); +} + +static bool loadFromConfigFile(const char *path) { + FILE *file = std::fopen(path, "r"); + if (!file) { + LOGW("Could not open %s: %s", path, std::strerror(errno)); + return false; + } + + std::string host; + int port = 0; + char line[256]; + while (std::fgets(line, sizeof(line), file)) { + std::string value = trimLine(line); + if (value.compare(0, 5, "host=") == 0) { + host = trimLine(value.c_str() + 5); + } else if (value.compare(0, 5, "port=") == 0) { + char *end = nullptr; + long parsed = std::strtol(value.c_str() + 5, &end, 10); + if (end != value.c_str() + 5 && *end == '\0') { + port = static_cast(parsed); + } + } + } + std::fclose(file); + + if (!setConfiguration(QString::fromUtf8(host.c_str()), port)) { + return false; + } + + LOGI("Loaded config from %s - Host: %s, Port: %d", path, + gConfiguredHost.toStdString().c_str(), gConfiguredPort); + return true; +} + +static bool loadFromSharedPrefs() { + JNIEnv *env = getJNIEnv(); + if (!env || !gContext) { + return false; + } + + jclass contextClass = env->FindClass("android/content/Context"); + jmethodID getSharedPreferences = contextClass ? env->GetMethodID( + contextClass, "getSharedPreferences", + "(Ljava/lang/String;I)Landroid/content/SharedPreferences;") : nullptr; + if (clearJavaException(env, "finding SharedPreferences") || !getSharedPreferences) { + if (contextClass) env->DeleteLocalRef(contextClass); + return false; + } + + jstring prefsName = env->NewStringUTF("rmhook"); + jobject prefs = env->CallObjectMethod(gContext, getSharedPreferences, prefsName, 0); + env->DeleteLocalRef(prefsName); + env->DeleteLocalRef(contextClass); + if (clearJavaException(env, "opening SharedPreferences") || !prefs) { + return false; + } + + jclass prefsClass = env->FindClass("android/content/SharedPreferences"); + jmethodID getString = prefsClass ? env->GetMethodID( + prefsClass, "getString", "(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;") : nullptr; + jmethodID getInt = prefsClass ? env->GetMethodID(prefsClass, "getInt", "(Ljava/lang/String;I)I") : nullptr; + if (clearJavaException(env, "finding SharedPreferences methods") || !getString || !getInt) { + if (prefsClass) env->DeleteLocalRef(prefsClass); + env->DeleteLocalRef(prefs); + return false; + } + + jstring hostKey = env->NewStringUTF("host"); + jstring portKey = env->NewStringUTF("port"); + jstring empty = env->NewStringUTF(""); + jstring hostValue = static_cast(env->CallObjectMethod(prefs, getString, hostKey, empty)); + jint port = env->CallIntMethod(prefs, getInt, portKey, 0); + bool failed = clearJavaException(env, "reading configuration values"); + env->DeleteLocalRef(hostKey); + env->DeleteLocalRef(portKey); + env->DeleteLocalRef(empty); + env->DeleteLocalRef(prefsClass); + env->DeleteLocalRef(prefs); + if (failed || !hostValue) { + return false; + } + + const char *hostChars = env->GetStringUTFChars(hostValue, nullptr); + QString host = hostChars ? QString::fromUtf8(hostChars) : QString(); + if (hostChars) env->ReleaseStringUTFChars(hostValue, hostChars); + env->DeleteLocalRef(hostValue); + + if (!setConfiguration(host, port)) { + return false; + } + + LOGI("Loaded config from SharedPreferences - Host: %s, Port: %d", + gConfiguredHost.toStdString().c_str(), gConfiguredPort); + return true; +} + +static bool getExternalConfigPath(std::string *path) { + JNIEnv *env = getJNIEnv(); + if (!env || !gContext || !path) { + return false; + } + + jclass contextClass = env->FindClass("android/content/Context"); + jmethodID getExternalFilesDir = contextClass ? env->GetMethodID( + contextClass, "getExternalFilesDir", "(Ljava/lang/String;)Ljava/io/File;") : nullptr; + if (clearJavaException(env, "finding external files directory") || !getExternalFilesDir) { + if (contextClass) env->DeleteLocalRef(contextClass); + return false; + } + + jobject directory = env->CallObjectMethod(gContext, getExternalFilesDir, nullptr); + env->DeleteLocalRef(contextClass); + if (clearJavaException(env, "getting external files directory") || !directory) { + return false; + } + + jclass fileClass = env->FindClass("java/io/File"); + jmethodID getAbsolutePath = fileClass ? env->GetMethodID( + fileClass, "getAbsolutePath", "()Ljava/lang/String;") : nullptr; + jstring directoryPath = getAbsolutePath ? static_cast( + env->CallObjectMethod(directory, getAbsolutePath)) : nullptr; + if (fileClass) env->DeleteLocalRef(fileClass); + env->DeleteLocalRef(directory); + if (clearJavaException(env, "getting external files path") || !directoryPath) { + return false; + } + + const char *pathChars = env->GetStringUTFChars(directoryPath, nullptr); + if (pathChars) { + *path = std::string(pathChars) + "/rmhook.conf"; + env->ReleaseStringUTFChars(directoryPath, pathChars); + } + env->DeleteLocalRef(directoryPath); + return pathChars != nullptr; +} + +static void saveToSharedPrefs(const char *host, int port) { + JNIEnv *env = getJNIEnv(); + if (!env || !gContext) { + return; + } + + jclass contextClass = env->FindClass("android/content/Context"); + jmethodID getSharedPreferences = contextClass ? env->GetMethodID( + contextClass, "getSharedPreferences", + "(Ljava/lang/String;I)Landroid/content/SharedPreferences;") : nullptr; + jstring prefsName = env->NewStringUTF("rmhook"); + jobject prefs = getSharedPreferences ? env->CallObjectMethod(gContext, getSharedPreferences, prefsName, 0) : nullptr; + env->DeleteLocalRef(prefsName); + if (contextClass) env->DeleteLocalRef(contextClass); + if (clearJavaException(env, "opening SharedPreferences") || !prefs) return; + + jclass prefsClass = env->FindClass("android/content/SharedPreferences"); + jmethodID edit = prefsClass ? env->GetMethodID( + prefsClass, "edit", "()Landroid/content/SharedPreferences$Editor;") : nullptr; + jobject editor = edit ? env->CallObjectMethod(prefs, edit) : nullptr; + if (prefsClass) env->DeleteLocalRef(prefsClass); + env->DeleteLocalRef(prefs); + if (clearJavaException(env, "creating SharedPreferences editor") || !editor) return; + + jclass editorClass = env->FindClass("android/content/SharedPreferences$Editor"); + jmethodID putString = editorClass ? env->GetMethodID(editorClass, "putString", + "(Ljava/lang/String;Ljava/lang/String;)Landroid/content/SharedPreferences$Editor;") : nullptr; + jmethodID putInt = editorClass ? env->GetMethodID(editorClass, "putInt", + "(Ljava/lang/String;I)Landroid/content/SharedPreferences$Editor;") : nullptr; + jmethodID apply = editorClass ? env->GetMethodID(editorClass, "apply", "()V") : nullptr; + jstring hostKey = env->NewStringUTF("host"); + jstring portKey = env->NewStringUTF("port"); + jstring hostValue = env->NewStringUTF(host); + if (putString && putInt && apply) { + env->CallObjectMethod(editor, putString, hostKey, hostValue); + env->CallObjectMethod(editor, putInt, portKey, port); + env->CallVoidMethod(editor, apply); + clearJavaException(env, "saving configuration"); + } + env->DeleteLocalRef(hostKey); + env->DeleteLocalRef(portKey); + env->DeleteLocalRef(hostValue); + if (editorClass) env->DeleteLocalRef(editorClass); + env->DeleteLocalRef(editor); +} + +extern "C" { + +void loadConfiguration(void) { + if (gConfiguredPort > 0) { + return; + } + + if (loadFromSharedPrefs()) { + return; + } + + std::string externalPath; + if (getExternalConfigPath(&externalPath) && loadFromConfigFile(externalPath.c_str())) { + return; + } + + gConfiguredHost = "example.com"; + gConfiguredPort = 443; + LOGI("No saved config found. Using defaults - Host: %s, Port: %d", + gConfiguredHost.toStdString().c_str(), gConfiguredPort); +} + +void saveConfiguration(const char *host, int port) { + if (!host || !setConfiguration(QString::fromUtf8(host), port)) { + LOGW("Rejected invalid configuration"); + return; + } + + QByteArray hostBytes = gConfiguredHost.toUtf8(); + saveToSharedPrefs(hostBytes.constData(), gConfiguredPort); + LOGI("Saved config - Host: %s, Port: %d", gConfiguredHost.toStdString().c_str(), gConfiguredPort); +} + +JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *) { + gJvm = vm; + + JNIEnv *env = getJNIEnv(); + if (!env) { + return JNI_ERR; + } + + jclass activityThread = env->FindClass("android/app/ActivityThread"); + jmethodID currentApplication = activityThread ? env->GetStaticMethodID( + activityThread, "currentApplication", "()Landroid/app/Application;") : nullptr; + jobject application = currentApplication ? env->CallStaticObjectMethod(activityThread, currentApplication) : nullptr; + if (activityThread) env->DeleteLocalRef(activityThread); + if (clearJavaException(env, "getting current application") || !application) { + LOGW("JNI_OnLoad could not acquire application context"); + return JNI_VERSION_1_6; + } + + gContext = env->NewGlobalRef(application); + env->DeleteLocalRef(application); + if (!gContext) { + return JNI_ERR; + } + + loadConfiguration(); + LOGI("JNI_OnLoad: config loaded (%s:%d)", gConfiguredHost.toStdString().c_str(), gConfiguredPort); + startHooks(); + return JNI_VERSION_1_6; +} + +} // extern "C" diff --git a/src/Config.h b/src/Config.h new file mode 100644 index 0000000..69496f9 --- /dev/null +++ b/src/Config.h @@ -0,0 +1,20 @@ +#ifndef RMHOOK_CONFIG_H +#define RMHOOK_CONFIG_H + +#include + +extern QString gConfiguredHost; +extern int gConfiguredPort; + +#ifdef __cplusplus +extern "C" { +#endif + +void loadConfiguration(void); +void saveConfiguration(const char *host, int port); + +#ifdef __cplusplus +} +#endif + +#endif // RMHOOK_CONFIG_H diff --git a/src/rmhook.cpp b/src/rmhook.cpp new file mode 100644 index 0000000..0dbb971 --- /dev/null +++ b/src/rmhook.cpp @@ -0,0 +1,193 @@ +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "shadowhook.h" +#include "Config.h" + +#define TAG "RMHook" +#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, TAG, __VA_ARGS__) +#define LOGW(...) __android_log_print(ANDROID_LOG_WARN, TAG, __VA_ARGS__) +#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, TAG, __VA_ARGS__) + +static bool shouldPatchURL(const QString &host) { + if (host.isEmpty()) { + return false; + } + + return QString(R"""( + hwr-production-dot-remarkable-production.appspot.com + service-manager-production-dot-remarkable-production.appspot.com + local.appspot.com + my.remarkable.com + ping.remarkable.com + internal.cloud.remarkable.com + eu.tectonic.remarkable.com + backtrace-proxy.cloud.remarkable.engineering + dev.ping.remarkable.com + dev.tectonic.remarkable.com + dev.internal.cloud.remarkable.com + eu.internal.tctn.cloud.remarkable.com + webapp-prod.cloud.remarkable.engineering + )""") + .contains(host, Qt::CaseInsensitive); +} + +// QObject *QNetworkAccessManager::createRequest(Operation op, const QNetworkRequest &req, QIODevice *outgoingData) +static QNetworkReply* (*original_qNetworkAccessManager_createRequest)( + QNetworkAccessManager* self, + QNetworkAccessManager::Operation op, + const QNetworkRequest& req, + QIODevice* outgoingData +); + +QNetworkReply* hooked_qNetworkAccessManager_createRequest( + QNetworkAccessManager* self, + QNetworkAccessManager::Operation op, + const QNetworkRequest& req, + QIODevice* outgoingData +) { + LOGI("createRequest called for URL: %s", req.url().toString().toStdString().c_str()); + const QString host = req.url().host(); + if (shouldPatchURL(host)) { + QNetworkRequest newReq(req); + QUrl newUrl = req.url(); + newUrl.setHost(gConfiguredHost); + newUrl.setPort(gConfiguredPort); + newReq.setUrl(newUrl); + + if (original_qNetworkAccessManager_createRequest) { + return original_qNetworkAccessManager_createRequest(self, op, newReq, outgoingData); + } + return nullptr; + } + + if (original_qNetworkAccessManager_createRequest) { + return original_qNetworkAccessManager_createRequest(self, op, req, outgoingData); + } + return nullptr; +} + +// void QWebSocket::open(const QNetworkRequest &req) +static void (*original_qWebSocket_open)( + QWebSocket* self, + const QNetworkRequest& req +); + +static void *resolve_qt_symbol(const char *library, const char *symbol) { + void *handle = dlopen(library, RTLD_NOW); + if (!handle) { + LOGE("Failed to load %s: %s", library, dlerror()); + return nullptr; + } + + void *address = dlsym(handle, symbol); + if (!address) { + LOGE("Failed to resolve %s in %s: %s", symbol, library, dlerror()); + } + dlclose(handle); + return address; +} + +void hooked_qWebSocket_open( + QWebSocket* self, + const QNetworkRequest& req +) { + LOGI("QWebSocket::open called for URL: %s", req.url().toString().toStdString().c_str()); + if (!original_qWebSocket_open) { + return; + } + + const QString host = req.url().host(); + if (shouldPatchURL(host)) { + QUrl newUrl = req.url(); + newUrl.setHost(gConfiguredHost); + newUrl.setPort(gConfiguredPort); + + QNetworkRequest newReq(req); + newReq.setUrl(newUrl); + + original_qWebSocket_open(self, newReq); + return; + } + + original_qWebSocket_open(self, req); +} + +static void install_hooks() { + LOGI("Installing hooks via ShadowHook..."); + + int init_result = shadowhook_init(SHADOWHOOK_MODE_UNIQUE, false); + if (init_result != 0) { + LOGE("ShadowHook initialization failed: %s", + shadowhook_to_errmsg(shadowhook_get_init_errno())); + return; + } + + // Hook QNetworkAccessManager::createRequest + void *create_request = resolve_qt_symbol( + "libQt6Network_arm64-v8a.so", + "_ZN21QNetworkAccessManager13createRequestENS_9OperationERK15QNetworkRequestP9QIODevice"); + void *stub1 = shadowhook_hook_sym_addr( + create_request, + (void *)hooked_qNetworkAccessManager_createRequest, + (void **)&original_qNetworkAccessManager_createRequest); + + if (stub1 != nullptr) { + LOGI("Hooked createRequest"); + } else { + LOGE("Failed to hook createRequest: %s", + shadowhook_to_errmsg(shadowhook_get_errno())); + } + + // Hook QWebSocket::open + void *websocket_open = resolve_qt_symbol( + "libQt6WebSockets_arm64-v8a.so", + "_ZN10QWebSocket4openERK15QNetworkRequest"); + void *stub2 = shadowhook_hook_sym_addr( + websocket_open, + (void *)hooked_qWebSocket_open, + (void **)&original_qWebSocket_open); + + if (stub2 != nullptr) { + LOGI("Hooked QWebSocket::open"); + } else { + LOGE("Failed to hook QWebSocket::open: %s", + shadowhook_to_errmsg(shadowhook_get_errno())); + } + + LOGI("All hooks active"); +} + +extern "C" void startHooks() { + pthread_t thread; + pthread_attr_t attr; + pthread_attr_init(&attr); + pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED); + if (pthread_create(&thread, &attr, (void *(*)(void *))install_hooks, nullptr) != 0) { + LOGE("Failed to start hook thread"); + } + pthread_attr_destroy(&attr); +} + +__attribute__((constructor)) +static void rmhook_init(void) { + LOGI("RMHook loaded"); +}