Add RMHook core with configurable endpoint

Initialize the project with Android/Qt hook infrastructure and repository setup files
This commit is contained in:
√(noham)²
2026-08-25 23:04:08 +02:00
commit 4278531cad
5 changed files with 550 additions and 0 deletions

14
.gitignore vendored Normal file
View File

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

3
.gitmodules vendored Normal file
View File

@@ -0,0 +1,3 @@
[submodule "libs/shadowhook"]
path = libs/shadowhook
url = https://github.com/bytedance/android-inline-hook

320
src/Config.cpp Normal file
View File

@@ -0,0 +1,320 @@
#include "Config.h"
#include <jni.h>
#include <android/log.h>
#include <cstdio>
#include <cstdlib>
#include <cerrno>
#include <cstring>
#include <string>
#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<void **>(&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<int>(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<jstring>(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<jstring>(
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"

20
src/Config.h Normal file
View File

@@ -0,0 +1,20 @@
#ifndef RMHOOK_CONFIG_H
#define RMHOOK_CONFIG_H
#include <QtCore/QString>
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

193
src/rmhook.cpp Normal file
View File

@@ -0,0 +1,193 @@
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <dlfcn.h>
#include <unistd.h>
#include <android/log.h>
#include <pthread.h>
#include <stdint.h>
#include <QtCore/QObject>
#include <QtCore/QString>
#include <QtCore/QUrl>
#include <QtCore/QIODevice>
#include <QtCore/Qt>
#include <QtCore/QDebug>
#include <QtCore/QAnyStringView>
#include <QtNetwork/QNetworkAccessManager>
#include <QtNetwork/QNetworkRequest>
#include <QtNetwork/QNetworkReply>
#include <QtWebSockets/QWebSocket>
#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");
}