From 3f1b4fd728037cdd39edb1f4af8c2bead53e9ea1 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: Thu, 27 Aug 2026 16:15:57 +0200 Subject: [PATCH] Proper implementation --- .gitignore | 2 + CMakeLists.txt | 36 +- README.md | 48 +- scripts/build.sh | 8 +- src/RMHook/Config.h | 14 + src/RMHook/Config.m | 135 +++ src/{reMarkable => RMHook}/DevHooks.h | 0 src/{reMarkable => RMHook}/DevHooks.m | 164 ++-- .../reMarkable.h => RMHook/RMHook.h} | 2 +- src/RMHook/RMHook.m | 469 +++++++++++ src/RMHook/SSLConfig.h | 4 + src/RMHook/SSLConfig.m | 92 ++ src/reMarkable/reMarkable.m | 783 ------------------ 13 files changed, 866 insertions(+), 891 deletions(-) create mode 100644 src/RMHook/Config.h create mode 100644 src/RMHook/Config.m rename src/{reMarkable => RMHook}/DevHooks.h (100%) rename src/{reMarkable => RMHook}/DevHooks.m (58%) rename src/{reMarkable/reMarkable.h => RMHook/RMHook.h} (54%) create mode 100644 src/RMHook/RMHook.m create mode 100644 src/RMHook/SSLConfig.h create mode 100644 src/RMHook/SSLConfig.m delete mode 100644 src/reMarkable/reMarkable.m diff --git a/.gitignore b/.gitignore index 552e9be..43ba7ab 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,5 @@ build/ .DS_Store /.vscode /research +/aqt_venv +docs/rmfakecloud_hooking.md diff --git a/CMakeLists.txt b/CMakeLists.txt index 862a1ac..a1723e1 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -28,7 +28,7 @@ set(PROJECT_ROOT_DIR ${CMAKE_CURRENT_SOURCE_DIR}) include_directories( ${PROJECT_ROOT_DIR}/src/core ${PROJECT_ROOT_DIR}/src/utils - ${PROJECT_ROOT_DIR}/src/reMarkable + ${PROJECT_ROOT_DIR}/src/RMHook ${PROJECT_ROOT_DIR}/libs/include ) @@ -49,7 +49,7 @@ set(LIBS # Locate Qt libraries set(QT_LIB_TARGETS "") -set(_qt_candidate_roots "$ENV{HOME}/Qt/6.10.0") +set(_qt_candidate_roots "$ENV{HOME}/Qt/6.10.3/macos") foreach(_qt_root ${_qt_candidate_roots}) if(_qt_root AND EXISTS "${_qt_root}") @@ -78,27 +78,29 @@ set(COMMON_SOURCES ${PROJECT_ROOT_DIR}/src/utils/ResourceUtils.m ) -# reMarkable dylib -set(REMARKABLE_SOURCES - ${PROJECT_ROOT_DIR}/src/reMarkable/reMarkable.m - ${PROJECT_ROOT_DIR}/src/reMarkable/DevHooks.m +# RMHook dylib +set(RMHOOK_SOURCES + ${PROJECT_ROOT_DIR}/src/RMHook/RMHook.m + ${PROJECT_ROOT_DIR}/src/RMHook/Config.m + ${PROJECT_ROOT_DIR}/src/RMHook/SSLConfig.m + ${PROJECT_ROOT_DIR}/src/RMHook/DevHooks.m ) -add_library(reMarkable SHARED +add_library(RMHook SHARED ${COMMON_SOURCES} - ${REMARKABLE_SOURCES} + ${RMHOOK_SOURCES} ) # Set source files as Objective-C++ set_source_files_properties( - ${REMARKABLE_SOURCES} + ${RMHOOK_SOURCES} PROPERTIES LANGUAGE OBJCXX ) -set_target_properties(reMarkable PROPERTIES +set_target_properties(RMHook PROPERTIES PREFIX "" SUFFIX ".dylib" - OUTPUT_NAME "reMarkable" + OUTPUT_NAME "RMHook" LIBRARY_OUTPUT_DIRECTORY "${PROJECT_ROOT_DIR}/build/dylibs" MACOSX_RPATH ON ) @@ -107,29 +109,29 @@ add_definitions(-DQT_NO_VERSION_TAGGING) # Add build mode compile definitions and conditionally add sources if(BUILD_MODE_RMFAKECLOUD) - target_compile_definitions(reMarkable PRIVATE BUILD_MODE_RMFAKECLOUD=1) + target_compile_definitions(RMHook PRIVATE BUILD_MODE_RMFAKECLOUD=1) message(STATUS "Build mode: rmfakecloud (cloud redirection)") endif() if(BUILD_MODE_QMLREBUILD) - target_compile_definitions(reMarkable PRIVATE BUILD_MODE_QMLREBUILD=1) + target_compile_definitions(RMHook PRIVATE BUILD_MODE_QMLREBUILD=1) # Enable Qt MOC for MessageBroker - set_target_properties(reMarkable PROPERTIES AUTOMOC ON) + set_target_properties(RMHook PROPERTIES AUTOMOC ON) # Add MessageBroker source (needs MOC processing) - target_sources(reMarkable PRIVATE + target_sources(RMHook PRIVATE ${PROJECT_ROOT_DIR}/src/utils/MessageBroker.mm ) message(STATUS "Build mode: qmlrebuild (resource hooking)") endif() if(BUILD_MODE_DEV) - target_compile_definitions(reMarkable PRIVATE BUILD_MODE_DEV=1) + target_compile_definitions(RMHook PRIVATE BUILD_MODE_DEV=1) message(STATUS "Build mode: dev (reverse engineering)") endif() -target_link_libraries(reMarkable PRIVATE +target_link_libraries(RMHook PRIVATE ${LIBS} ${QT_LIB_TARGETS} ) \ No newline at end of file diff --git a/README.md b/README.md index 2448e60..7a23815 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,7 @@ RMHook hooks into the reMarkable Desktop app's network layer to redirect API cal - Network request interception and redirection - WebSocket connection patching - MQTT URI modification for screen sharing features +- mTLS support for external access via Cloudflare Tunnel ## Compatibility @@ -109,13 +110,44 @@ Example configuration: ## Configuration The config file (`~/Library/Preferences/rmfakecloud.config`) supports the following keys: -| Key | Type | Default | Description | -|--------|---------|-------------------|--------------------------------| -| `host` | String | `example.com` | Your rmfakecloud server host | -| `port` | Number | `443` | Your rmfakecloud server port | +| Key | Type | Default | Description | +|-----|------|---------|-------------| +| `host` | String | `example.com` | Your rmfakecloud server host | +| `port` | Number | `443` | Your rmfakecloud server port | +| `client_cert` | String | (none) | Path to client certificate file (PEM) for mTLS | +| `client_key` | String | (none) | Path to client private key file (PEM) for mTLS | +| `ca_cert` | String | (none) | Path to custom CA certificate file (PEM) | +| `disable_ssl_verification` | Boolean | `false` | Disable SSL peer verification (not recommended) | If the config file doesn't exist, it will be created automatically with default values on first launch. +### External Access (Cloudflare Tunnel with Client Certificate) + +If your rmfakecloud instance is exposed via Cloudflare Tunnel with client certificate authentication, configure the additional TLS settings in `~/Library/Preferences/rmfakecloud.config`: + +```json +{ + "host": "rmfakecloud.example.com", + "port": 443, + "client_cert": "/path/to/client.crt", + "client_key": "/path/to/client.key" +} +``` + +If your server uses a self-signed certificate or a certificate signed by a private CA: + +```json +{ + "host": "rmfakecloud.example.com", + "port": 443, + "client_cert": "/path/to/client.crt", + "client_key": "/path/to/client.key", + "ca_cert": "/path/to/ca.crt" +} +``` + +See the [rmfakecloud external access guide](https://ddvk.github.io/rmfakecloud/install/external-access/) for generating the client certificate. + ## Building 1. **Clone the repository:** @@ -124,6 +156,14 @@ git clone http://github.com/NohamR/RMHook cd RMHook ``` +Create a Python environment and install `aqtinstall`: +```bash +python3 -m venv aqt_venv +source aqt_venv/bin/activate +pip install aqtinstall +aqt install-qt mac desktop 6.10.3 -m qtwebsockets --outputdir ~/Qt +``` + 2. **Compile the dylib:** ```bash ./scripts/build.sh [mode] diff --git a/scripts/build.sh b/scripts/build.sh index 65b6342..7880220 100755 --- a/scripts/build.sh +++ b/scripts/build.sh @@ -29,7 +29,7 @@ case "$BUILD_MODE" in DYLIB_NAME="all.dylib" ;; *) - DYLIB_NAME="reMarkable.dylib" + DYLIB_NAME="RMHook.dylib" ;; esac @@ -55,7 +55,7 @@ case "$BUILD_MODE" in ;; esac -echo "🔨 Compiling reMarkable.dylib (mode: $BUILD_MODE)..." +echo "🔨 Compiling RMHook.dylib (mode: $BUILD_MODE)..." echo "📦 Qt path: $QT_PATH" # Create build directories if necessary @@ -70,12 +70,12 @@ else cmake $CMAKE_OPTIONS .. fi -make reMarkable +make RMHook if [ $? -eq 0 ]; then # Rename the produced dylib so each build mode has a distinct file name DYLIB_DIR="$PROJECT_DIR/build/dylibs" - DEFAULT_DYLIB="$DYLIB_DIR/reMarkable.dylib" + DEFAULT_DYLIB="$DYLIB_DIR/RMHook.dylib" TARGET_DYLIB="$DYLIB_DIR/$DYLIB_NAME" if [ -f "$DEFAULT_DYLIB" ]; then diff --git a/src/RMHook/Config.h b/src/RMHook/Config.h new file mode 100644 index 0000000..4e603f6 --- /dev/null +++ b/src/RMHook/Config.h @@ -0,0 +1,14 @@ +#import +#include + +extern NSString *gConfiguredHostObjC; +extern NSNumber *gConfiguredPortObjC; +extern QString gConfiguredHost; +extern NSNumber *gConfiguredPort; +extern QString gConfiguredClientCertPath; +extern QString gConfiguredClientKeyPath; +extern QString gConfiguredCACertPath; +extern bool gDisableSSLVerification; + +void ConfigLoadOrCreate(void); +NSString *ConfigFilePath(void); diff --git a/src/RMHook/Config.m b/src/RMHook/Config.m new file mode 100644 index 0000000..80fe28b --- /dev/null +++ b/src/RMHook/Config.m @@ -0,0 +1,135 @@ +#import "Config.h" +#import "Logger.h" +#include + +static NSString *const kConfigFileName = @"rmfakecloud.config"; +static NSString *const kConfigHostKey = @"host"; +static NSString *const kConfigPortKey = @"port"; +static NSString *const kConfigClientCertKey = @"client_cert"; +static NSString *const kConfigClientKeyKey = @"client_key"; +static NSString *const kConfigCACertKey = @"ca_cert"; +static NSString *const kConfigDisableSSLVerifyKey = @"disable_ssl_verification"; +static NSString *const kDefaultHost = @"example.com"; +static NSNumber *const kDefaultPort = @(443); + +NSString *gConfiguredHostObjC = @"example.com"; +NSNumber *gConfiguredPortObjC = @(443); +QString gConfiguredHost = QString::fromUtf8("example.com"); +NSNumber *gConfiguredPort = @(443); +QString gConfiguredClientCertPath; +QString gConfiguredClientKeyPath; +QString gConfiguredCACertPath; +bool gDisableSSLVerification = false; + +static NSString *PreferencesDirectory(void) { + NSArray *libraryPaths = NSSearchPathForDirectoriesInDomains(NSLibraryDirectory, NSUserDomainMask, YES); + NSString *libraryDir = [libraryPaths firstObject]; + if (![libraryDir length]) { + libraryDir = [NSHomeDirectory() stringByAppendingPathComponent:@"Library"]; + } + return [libraryDir stringByAppendingPathComponent:@"Preferences"]; +} + +NSString *ConfigFilePath(void) { + return [PreferencesDirectory() stringByAppendingPathComponent:kConfigFileName]; +} + +static BOOL WriteConfig(NSString *path, NSDictionary *config) { + NSError *error = nil; + NSData *jsonData = [NSJSONSerialization dataWithJSONObject:config options:NSJSONWritingPrettyPrinted error:&error]; + if (!jsonData || error) { + NSLogger(@"[RMHook] Failed to serialize config: %@", error); + return NO; + } + if (![jsonData writeToFile:path atomically:YES]) { + NSLogger(@"[RMHook] Failed to write config file at %@", path); + return NO; + } + return YES; +} + +static inline QString QStringFromNSStringSafe(NSString *string) { + if (!string) { + return QString(); + } + return QString::fromUtf8([string UTF8String]); +} + +void ConfigLoadOrCreate(void) { + NSString *configPath = ConfigFilePath(); + NSString *directory = [configPath stringByDeletingLastPathComponent]; + NSFileManager *fileManager = [NSFileManager defaultManager]; + BOOL isDirectory = NO; + NSError *error = nil; + + if (![fileManager fileExistsAtPath:directory isDirectory:&isDirectory] || !isDirectory) { + if (![fileManager createDirectoryAtPath:directory withIntermediateDirectories:YES attributes:nil error:&error]) { + NSLogger(@"[RMHook] Failed to create config directory %@: %@", directory, error); + } + } + + NSDictionary *defaults = @{kConfigHostKey : kDefaultHost, + kConfigPortKey : kDefaultPort}; + + if ([fileManager fileExistsAtPath:configPath isDirectory:&isDirectory] && !isDirectory) { + NSData *data = [NSData dataWithContentsOfFile:configPath]; + if ([data length] > 0) { + NSError *jsonError = nil; + id jsonObject = [NSJSONSerialization JSONObjectWithData:data options:0 error:&jsonError]; + if (!jsonError && [jsonObject isKindOfClass:[NSDictionary class]]) { + NSDictionary *configDict = (NSDictionary *)jsonObject; + NSString *hostValue = configDict[kConfigHostKey]; + NSNumber *portValue = configDict[kConfigPortKey]; + + NSString *resolvedHost = ([hostValue isKindOfClass:[NSString class]] && [hostValue length]) ? hostValue : kDefaultHost; + NSInteger portCandidate = kDefaultPort.integerValue; + if ([portValue respondsToSelector:@selector(integerValue)]) { + NSInteger candidate = [portValue integerValue]; + if (candidate > 0 && candidate <= 65535) { + portCandidate = candidate; + } else { + NSLogger(@"[RMHook] Ignoring invalid port value %@, falling back to default.", portValue); + } + } + + gConfiguredHostObjC = [resolvedHost copy]; + gConfiguredPortObjC = @(portCandidate); + gConfiguredHost = QStringFromNSStringSafe(resolvedHost); + gConfiguredPort = @(portCandidate); + + NSString *certPathValue = configDict[kConfigClientCertKey]; + NSString *keyPathValue = configDict[kConfigClientKeyKey]; + NSString *caPathValue = configDict[kConfigCACertKey]; + NSNumber *disableSSLValue = configDict[kConfigDisableSSLVerifyKey]; + + if ([certPathValue isKindOfClass:[NSString class]] && [certPathValue length]) { + gConfiguredClientCertPath = QStringFromNSStringSafe(certPathValue); + } + if ([keyPathValue isKindOfClass:[NSString class]] && [keyPathValue length]) { + gConfiguredClientKeyPath = QStringFromNSStringSafe(keyPathValue); + } + if ([caPathValue isKindOfClass:[NSString class]] && [caPathValue length]) { + gConfiguredCACertPath = QStringFromNSStringSafe(caPathValue); + } + if ([disableSSLValue respondsToSelector:@selector(boolValue)]) { + gDisableSSLVerification = [disableSSLValue boolValue]; + } + + NSLogger(@"[RMHook] Loaded config from %@ with host %@ and port %@", configPath, gConfiguredHostObjC, gConfiguredPortObjC); + return; + } else { + NSLogger(@"[RMHook] Failed to parse config file %@: %@", configPath, jsonError); + } + } else { + NSLogger(@"[RMHook] Config file %@ was empty, rewriting with defaults.", configPath); + } + } + + if (WriteConfig(configPath, defaults)) { + NSLogger(@"[RMHook] Created default config at %@", configPath); + } + gConfiguredHostObjC = [kDefaultHost copy]; + gConfiguredPortObjC = kDefaultPort; + gConfiguredHost = QString::fromUtf8("example.com"); + gConfiguredPort = kDefaultPort; +} diff --git a/src/reMarkable/DevHooks.h b/src/RMHook/DevHooks.h similarity index 100% rename from src/reMarkable/DevHooks.h rename to src/RMHook/DevHooks.h diff --git a/src/reMarkable/DevHooks.m b/src/RMHook/DevHooks.m similarity index 58% rename from src/reMarkable/DevHooks.m rename to src/RMHook/DevHooks.m index 591d126..c57b5be 100644 --- a/src/reMarkable/DevHooks.m +++ b/src/RMHook/DevHooks.m @@ -35,18 +35,18 @@ void (*original_function_at_0x1001B6EE0)(int64_t a1, int64_t *a2, unsigned int a void logMemory(const char *label, void *address, size_t length) { if (!address) { - NSLogger(@"[reMarkable] %s: (null)", label); + NSLogger(@"[RMHook] %s: (null)", label); return; } unsigned char *ptr = (unsigned char *)address; - NSMutableString *hexLine = [NSMutableString stringWithFormat:@"[reMarkable] %s: ", label]; + NSMutableString *hexLine = [NSMutableString stringWithFormat:@"[RMHook] %s: ", label]; for (size_t i = 0; i < length; i++) { [hexLine appendFormat:@"%02x ", ptr[i]]; if ((i + 1) % 16 == 0 && i < length - 1) { NSLogger(@"%@", hexLine); - hexLine = [NSMutableString stringWithString:@"[reMarkable] "]; + hexLine = [NSMutableString stringWithString:@"[RMHook] "]; } } @@ -57,13 +57,13 @@ void logMemory(const char *label, void *address, size_t length) { } void logStackTrace(const char *label) { - NSLogger(@"[reMarkable] %s - Stack trace:", label); + NSLogger(@"[RMHook] %s - Stack trace:", label); NSArray *callStack = [NSThread callStackSymbols]; NSUInteger count = [callStack count]; for (NSUInteger i = 0; i < count; i++) { NSString *frame = callStack[i]; - NSLogger(@"[reMarkable] #%lu: %@", (unsigned long)i, frame); + NSLogger(@"[RMHook] #%lu: %@", (unsigned long)i, frame); } } @@ -73,39 +73,39 @@ extern "C" ssize_t hooked_qIODevice_write( QIODevice *self, const char *data, int64_t maxSize) { - NSLogger(@"[reMarkable] QIODevice::write called with maxSize: %lld", (long long)maxSize); + NSLogger(@"[RMHook] QIODevice::write called with maxSize: %lld", (long long)maxSize); logStackTrace("QIODevice::write call stack"); logMemory("Data to write", (void *)data, (size_t)(maxSize < 64 ? maxSize : 64)); if (original_qIODevice_write) { ssize_t result = original_qIODevice_write(self, data, maxSize); - NSLogger(@"[reMarkable] QIODevice::write result: %zd", result); + NSLogger(@"[RMHook] QIODevice::write result: %zd", result); return result; } - NSLogger(@"[reMarkable] WARNING: Original QIODevice::write not available, returning 0"); + NSLogger(@"[RMHook] WARNING: Original QIODevice::write not available, returning 0"); return 0; } extern "C" int64_t hooked_function_at_0x100011790(uint64_t *a1) { - NSLogger(@"[reMarkable] Hook at 0x100011790 called!"); - NSLogger(@"[reMarkable] a1 = %p", a1); + NSLogger(@"[RMHook] Hook at 0x100011790 called!"); + NSLogger(@"[RMHook] a1 = %p", a1); if (a1) { - NSLogger(@"[reMarkable] *a1 = 0x%llx", (unsigned long long)*a1); + NSLogger(@"[RMHook] *a1 = 0x%llx", (unsigned long long)*a1); logMemory("Memory at a1", (void *)a1, 64); logMemory("Memory at *a1", (void *)(*a1), 64); } else { - NSLogger(@"[reMarkable] a1 is NULL"); + NSLogger(@"[RMHook] a1 is NULL"); } if (original_function_at_0x100011790) { int64_t result = original_function_at_0x100011790(a1); - NSLogger(@"[reMarkable] result = 0x%llx", (unsigned long long)result); + NSLogger(@"[RMHook] result = 0x%llx", (unsigned long long)result); return result; } - NSLogger(@"[reMarkable] WARNING: Original function at 0x100011790 not available, returning 0"); + NSLogger(@"[RMHook] WARNING: Original function at 0x100011790 not available, returning 0"); return 0; } @@ -126,25 +126,25 @@ extern "C" int64_t hooked_function_at_0x100011CE0( // - a4: stored at offset +0x18 (likely URI or type info pointer) // - a5: ExternalRefCountData* for shared pointer ref counting - NSLogger(@"[reMarkable] ========================================"); - NSLogger(@"[reMarkable] Hook at 0x100011CE0 (QML Type Registration)"); - NSLogger(@"[reMarkable] ========================================"); + NSLogger(@"[RMHook] ========================================"); + NSLogger(@"[RMHook] Hook at 0x100011CE0 (QML Type Registration)"); + NSLogger(@"[RMHook] ========================================"); - NSLogger(@"[reMarkable] a1 (typeMetadata?) = 0x%llx", (unsigned long long)a1); + NSLogger(@"[RMHook] a1 (typeMetadata?) = 0x%llx", (unsigned long long)a1); uint16_t a2_low = (uint16_t)(uintptr_t)a2; uint16_t combined_v17 = (a2_low << 8) | a3; - NSLogger(@"[reMarkable] a2 (raw) = %p (0x%llx)", a2, (unsigned long long)(uintptr_t)a2); - NSLogger(@"[reMarkable] a2 low 16 bits = 0x%04x (%u)", a2_low, a2_low); - NSLogger(@"[reMarkable] a3 (flags/version) = 0x%02x (%u)", a3, a3); - NSLogger(@"[reMarkable] v17 = (a2<<8)|a3 = 0x%04x (%u)", combined_v17, combined_v17); - NSLogger(@"[reMarkable] a4 (typeInfo/URI?) = 0x%llx", (unsigned long long)a4); - NSLogger(@"[reMarkable] a5 (refCountData) = %p", a5); + NSLogger(@"[RMHook] a2 (raw) = %p (0x%llx)", a2, (unsigned long long)(uintptr_t)a2); + NSLogger(@"[RMHook] a2 low 16 bits = 0x%04x (%u)", a2_low, a2_low); + NSLogger(@"[RMHook] a3 (flags/version) = 0x%02x (%u)", a3, a3); + NSLogger(@"[RMHook] v17 = (a2<<8)|a3 = 0x%04x (%u)", combined_v17, combined_v17); + NSLogger(@"[RMHook] a4 (typeInfo/URI?) = 0x%llx", (unsigned long long)a4); + NSLogger(@"[RMHook] a5 (refCountData) = %p", a5); if (a1) { logMemory("Memory at a1 (typeMetadata)", (void *)a1, 64); void **vtable = (void **)a1; - NSLogger(@"[reMarkable] a1 vtable/first ptr = %p", *vtable); + NSLogger(@"[RMHook] a1 vtable/first ptr = %p", *vtable); } if (a4) { @@ -160,7 +160,7 @@ extern "C" int64_t hooked_function_at_0x100011CE0( len++; } if (isPrintable && len > 0) { - NSLogger(@"[reMarkable] a4 as string: \"%.*s\"", len, maybeStr); + NSLogger(@"[RMHook] a4 as string: \"%.*s\"", len, maybeStr); } } @@ -172,59 +172,59 @@ extern "C" int64_t hooked_function_at_0x100011CE0( if (original_function_at_0x100011CE0) { int64_t result = original_function_at_0x100011CE0(a1, a2, a3, a4, a5); - NSLogger(@"[reMarkable] result (qmlregister return) = %u (0x%x)", (unsigned int)result, (unsigned int)result); - NSLogger(@"[reMarkable] ========================================"); + NSLogger(@"[RMHook] result (qmlregister return) = %u (0x%x)", (unsigned int)result, (unsigned int)result); + NSLogger(@"[RMHook] ========================================"); return result; } - NSLogger(@"[reMarkable] WARNING: Original function at 0x100011CE0 not available, returning 0"); + NSLogger(@"[RMHook] WARNING: Original function at 0x100011CE0 not available, returning 0"); return 0; } extern "C" int64_t hooked_function_at_0x10015A130(int64_t a1, int64_t a2) { - NSLogger(@"[reMarkable] Hook at 0x10015A130 called!"); - NSLogger(@"[reMarkable] a1 = 0x%llx", (unsigned long long)a1); - NSLogger(@"[reMarkable] a2 = 0x%llx", (unsigned long long)a2); + NSLogger(@"[RMHook] Hook at 0x10015A130 called!"); + NSLogger(@"[RMHook] a1 = 0x%llx", (unsigned long long)a1); + NSLogger(@"[RMHook] a2 = 0x%llx", (unsigned long long)a2); logMemory("Memory at a1", (void *)a1, 64); logMemory("Memory at a2", (void *)a2, 64); if (original_function_at_0x10015A130) { int64_t result = original_function_at_0x10015A130(a1, a2); - NSLogger(@"[reMarkable] result = 0x%llx", (unsigned long long)result); + NSLogger(@"[RMHook] result = 0x%llx", (unsigned long long)result); return result; } - NSLogger(@"[reMarkable] WARNING: Original function at 0x10015A130 not available, returning 0"); + NSLogger(@"[RMHook] WARNING: Original function at 0x10015A130 not available, returning 0"); return 0; } extern "C" void hooked_function_at_0x10015BC90(int64_t a1, int64_t a2) { - NSLogger(@"[reMarkable] Hook at 0x10015BC90 called!"); - NSLogger(@"[reMarkable] a1 = 0x%llx", (unsigned long long)a1); - NSLogger(@"[reMarkable] a2 = 0x%llx", (unsigned long long)a2); + NSLogger(@"[RMHook] Hook at 0x10015BC90 called!"); + NSLogger(@"[RMHook] a1 = 0x%llx", (unsigned long long)a1); + NSLogger(@"[RMHook] a2 = 0x%llx", (unsigned long long)a2); logMemory("Memory at a1", (void *)a1, 64); logMemory("Memory at a2", (void *)a2, 64); if (original_function_at_0x10015BC90) { original_function_at_0x10015BC90(a1, a2); - NSLogger(@"[reMarkable] original function returned (void)"); + NSLogger(@"[RMHook] original function returned (void)"); return; } - NSLogger(@"[reMarkable] WARNING: Original function at 0x10015BC90 not available"); + NSLogger(@"[RMHook] WARNING: Original function at 0x10015BC90 not available"); } extern "C" int64_t hooked_function_at_0x10016D520(int64_t a1, int64_t *a2, unsigned int a3, int64_t a4) { - NSLogger(@"[reMarkable] Hook at 0x10016D520 called!"); - NSLogger(@"[reMarkable] a1 = 0x%llx", (unsigned long long)a1); - NSLogger(@"[reMarkable] a2 = %p", a2); + NSLogger(@"[RMHook] Hook at 0x10016D520 called!"); + NSLogger(@"[RMHook] a1 = 0x%llx", (unsigned long long)a1); + NSLogger(@"[RMHook] a2 = %p", a2); if (a2) { - NSLogger(@"[reMarkable] *a2 = 0x%llx", (unsigned long long)*a2); + NSLogger(@"[RMHook] *a2 = 0x%llx", (unsigned long long)*a2); } - NSLogger(@"[reMarkable] a3 = %u (0x%x)", a3, a3); - NSLogger(@"[reMarkable] a4 = 0x%llx", (unsigned long long)a4); + NSLogger(@"[RMHook] a3 = %u (0x%x)", a3, a3); + NSLogger(@"[RMHook] a4 = 0x%llx", (unsigned long long)a4); logMemory("Memory at a1", (void *)a1, 64); logMemory("Memory at a2", (void *)a2, 64); @@ -237,28 +237,28 @@ extern "C" int64_t hooked_function_at_0x10016D520(int64_t a1, int64_t *a2, unsig if (original_function_at_0x10016D520) { int64_t result = original_function_at_0x10016D520(a1, a2, a3, a4); - NSLogger(@"[reMarkable] result = 0x%llx", (unsigned long long)result); + NSLogger(@"[RMHook] result = 0x%llx", (unsigned long long)result); return result; } - NSLogger(@"[reMarkable] WARNING: Original function not available, returning 0"); + NSLogger(@"[RMHook] WARNING: Original function not available, returning 0"); return 0; } extern "C" void hooked_function_at_0x1001B6EE0(int64_t a1, int64_t *a2, unsigned int a3) { - NSLogger(@"[reMarkable] Hook at 0x1001B6EE0 called!"); - NSLogger(@"[reMarkable] a1 = 0x%llx", (unsigned long long)a1); + NSLogger(@"[RMHook] Hook at 0x1001B6EE0 called!"); + NSLogger(@"[RMHook] a1 = 0x%llx", (unsigned long long)a1); // At a1 (PdfExporter object): // +0x10 contains a QString (likely document name) - NSLogger(@"[reMarkable] Reading QString at a1+0x10:"); + NSLogger(@"[RMHook] Reading QString at a1+0x10:"); logMemory("a1 + 0x10 (raw)", (void *)(a1 + 0x10), 64); void **qstrPtr = (void **)(a1 + 0x10); void *dataPtr = *qstrPtr; if (!dataPtr) { - NSLogger(@"[reMarkable] QString has null data pointer"); + NSLogger(@"[RMHook] QString has null data pointer"); return; } @@ -268,29 +268,29 @@ extern "C" void hooked_function_at_0x1001B6EE0(int64_t a1, int64_t *a2, unsigned int32_t candidate = *(int32_t *)((char *)dataPtr - delta); if (candidate > 0 && candidate < 10000) { size = candidate; - NSLogger(@"[reMarkable] QString plausible size=%d (found at -%d)", size, delta); + NSLogger(@"[RMHook] QString plausible size=%d (found at -%d)", size, delta); break; } } if (size > 0) { NSString *qstringValue = [[NSString alloc] initWithCharacters:(unichar *)dataPtr length:size]; - NSLogger(@"[reMarkable] QString value: \"%@\"", qstringValue); + NSLogger(@"[RMHook] QString value: \"%@\"", qstringValue); } else { - NSLogger(@"[reMarkable] QString: could not find valid size"); + NSLogger(@"[RMHook] QString: could not find valid size"); } - NSLogger(@"[reMarkable] a2 = %p", a2); + NSLogger(@"[RMHook] a2 = %p", a2); if (a2) { - NSLogger(@"[reMarkable] *a2 = 0x%llx", (unsigned long long)*a2); + NSLogger(@"[RMHook] *a2 = 0x%llx", (unsigned long long)*a2); } - NSLogger(@"[reMarkable] a3 = %u (0x%x)", a3, a3); + NSLogger(@"[RMHook] a3 = %u (0x%x)", a3, a3); if (original_function_at_0x1001B6EE0) { original_function_at_0x1001B6EE0(a1, a2, a3); - NSLogger(@"[reMarkable] Original function at 0x1001B6EE0 executed"); + NSLogger(@"[RMHook] Original function at 0x1001B6EE0 executed"); } else { - NSLogger(@"[reMarkable] WARNING: Original function not available"); + NSLogger(@"[RMHook] WARNING: Original function not available"); } } @@ -306,19 +306,19 @@ extern "C" int64_t hooked_qmlregister( int a9, int64_t a10) { - NSLogger(@"[reMarkable] ========================================"); - NSLogger(@"[reMarkable] QQmlPrivate::qmlregister called!"); - NSLogger(@"[reMarkable] ========================================"); - NSLogger(@"[reMarkable] a1 (RegistrationType) = 0x%llx (%lld)", (unsigned long long)a1, (long long)a1); - NSLogger(@"[reMarkable] a2 = 0x%llx (%lld)", (unsigned long long)a2, (long long)a2); - NSLogger(@"[reMarkable] a3 = 0x%llx (%lld)", (unsigned long long)a3, (long long)a3); - NSLogger(@"[reMarkable] a4 = 0x%llx (%lld)", (unsigned long long)a4, (long long)a4); - NSLogger(@"[reMarkable] a5 = 0x%llx (%lld)", (unsigned long long)a5, (long long)a5); - NSLogger(@"[reMarkable] a6 = 0x%llx (%lld)", (unsigned long long)a6, (long long)a6); - NSLogger(@"[reMarkable] a7 = 0x%x (%d)", a7, a7); - NSLogger(@"[reMarkable] a8 = 0x%llx (%lld)", (unsigned long long)a8, (long long)a8); - NSLogger(@"[reMarkable] a9 = 0x%x (%d)", a9, a9); - NSLogger(@"[reMarkable] a10 = 0x%llx (%lld)", (unsigned long long)a10, (long long)a10); + NSLogger(@"[RMHook] ========================================"); + NSLogger(@"[RMHook] QQmlPrivate::qmlregister called!"); + NSLogger(@"[RMHook] ========================================"); + NSLogger(@"[RMHook] a1 (RegistrationType) = 0x%llx (%lld)", (unsigned long long)a1, (long long)a1); + NSLogger(@"[RMHook] a2 = 0x%llx (%lld)", (unsigned long long)a2, (long long)a2); + NSLogger(@"[RMHook] a3 = 0x%llx (%lld)", (unsigned long long)a3, (long long)a3); + NSLogger(@"[RMHook] a4 = 0x%llx (%lld)", (unsigned long long)a4, (long long)a4); + NSLogger(@"[RMHook] a5 = 0x%llx (%lld)", (unsigned long long)a5, (long long)a5); + NSLogger(@"[RMHook] a6 = 0x%llx (%lld)", (unsigned long long)a6, (long long)a6); + NSLogger(@"[RMHook] a7 = 0x%x (%d)", a7, a7); + NSLogger(@"[RMHook] a8 = 0x%llx (%lld)", (unsigned long long)a8, (long long)a8); + NSLogger(@"[RMHook] a9 = 0x%x (%d)", a9, a9); + NSLogger(@"[RMHook] a10 = 0x%llx (%lld)", (unsigned long long)a10, (long long)a10); // Check for PlatformHelpers registration // a1 == 0 means TypeRegistration (object registration) @@ -341,14 +341,14 @@ extern "C" int64_t hooked_qmlregister( } if (isValid && len > 0) { - NSLogger(@"[reMarkable] typeName (a4) = \"%.*s\"", len, typeName); + NSLogger(@"[RMHook] typeName (a4) = \"%.*s\"", len, typeName); if (len == 15 && strncmp(typeName, "PlatformHelpers", 15) == 0) { - NSLogger(@"[reMarkable] !!! FOUND PlatformHelpers type registration !!!"); - NSLogger(@"[reMarkable] factory ptr (a2) = %p", (void *)a2); - NSLogger(@"[reMarkable] a3 (metaObject?) = %p", (void *)a3); - NSLogger(@"[reMarkable] a5 = %p", (void *)a5); - NSLogger(@"[reMarkable] a6 = %p", (void *)a6); + NSLogger(@"[RMHook] !!! FOUND PlatformHelpers type registration !!!"); + NSLogger(@"[RMHook] factory ptr (a2) = %p", (void *)a2); + NSLogger(@"[RMHook] a3 (metaObject?) = %p", (void *)a3); + NSLogger(@"[RMHook] a5 = %p", (void *)a5); + NSLogger(@"[RMHook] a6 = %p", (void *)a6); logMemory("Factory ptr memory", (void *)a2, 64); logMemory("a3 memory (metaObject?)", (void *)a3, 64); logStackTrace("PlatformHelpers registration"); @@ -370,19 +370,19 @@ extern "C" int64_t hooked_qmlregister( len++; } if (isPrintable && len > 0) { - NSLogger(@"[reMarkable] a2 as string: \"%.*s\"", len, maybeStr); + NSLogger(@"[RMHook] a2 as string: \"%.*s\"", len, maybeStr); } } int64_t result = 0; if (original_qmlregister) { result = original_qmlregister(a1, a2, a3, a4, a5, a6, a7, a8, a9, a10); - NSLogger(@"[reMarkable] result = 0x%llx (%lld)", (unsigned long long)result, (long long)result); + NSLogger(@"[RMHook] result = 0x%llx (%lld)", (unsigned long long)result, (long long)result); } else { - NSLogger(@"[reMarkable] WARNING: Original qmlregister not available!"); + NSLogger(@"[RMHook] WARNING: Original qmlregister not available!"); } - NSLogger(@"[reMarkable] ========================================"); + NSLogger(@"[RMHook] ========================================"); return result; } diff --git a/src/reMarkable/reMarkable.h b/src/RMHook/RMHook.h similarity index 54% rename from src/reMarkable/reMarkable.h rename to src/RMHook/RMHook.h index ba2cdcb..44d8f44 100644 --- a/src/reMarkable/reMarkable.h +++ b/src/RMHook/RMHook.h @@ -1,5 +1,5 @@ #import -@interface reMarkable : NSObject +@interface RMHook : NSObject @end \ No newline at end of file diff --git a/src/RMHook/RMHook.m b/src/RMHook/RMHook.m new file mode 100644 index 0000000..0cfa2a6 --- /dev/null +++ b/src/RMHook/RMHook.m @@ -0,0 +1,469 @@ +#import "RMHook.h" +#import +#import "Constant.h" +#import "MemoryUtils.h" +#import "Logger.h" +#import "ResourceUtils.h" +#import "Config.h" +#import "SSLConfig.h" +#ifdef BUILD_MODE_DEV +#import "DevHooks.h" +#endif +#ifdef BUILD_MODE_QMLREBUILD +#import "MessageBroker.h" +#endif +#import +#import +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +static pthread_mutex_t gResourceMutex = PTHREAD_MUTEX_INITIALIZER; + +@interface MenuActionController : NSObject +@property (strong, nonatomic) NSURL *targetURL; +- (void)openURLAction:(id)sender; ++ (void)addCustomHelpMenuEntry:(NSString *)title withURL:(NSString *)url; ++ (void)addCustomHelpMenuEntry:(NSString *)title withURL:(NSString *)url withDelay:(NSTimeInterval)delay; +@end + +@implementation MenuActionController + +- (void)openURLAction:(id)sender { + if (self.targetURL) { + [[NSWorkspace sharedWorkspace] openURL:self.targetURL]; + NSLogger(@"[+] URL opened successfully: %@", self.targetURL); + } +} + ++ (void)addCustomHelpMenuEntry:(NSString *)title withURL:(NSString *)url { + [self addCustomHelpMenuEntry:title withURL:url withDelay:1.0]; +} + ++ (void)addCustomHelpMenuEntry:(NSString *)title withURL:(NSString *)url withDelay:(NSTimeInterval)delay { + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delay * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ + MenuActionController *controller = [[MenuActionController alloc] init]; + controller.targetURL = [NSURL URLWithString:url]; + + NSMenu *mainMenu = [NSApp mainMenu]; + if (!mainMenu) { + NSLogger(@"[-] Main menu not found"); + return; + } + + NSMenuItem *helpMenuItem = nil; + for (NSMenuItem *item in [mainMenu itemArray]) { + if ([[item title] isEqualToString:@"Help"]) { + helpMenuItem = item; + break; + } + } + + if (!helpMenuItem) { + NSLogger(@"[-] Help menu item not found"); + return; + } + + NSMenu *helpMenu = [helpMenuItem submenu]; + if (!helpMenu) { + NSLogger(@"[-] Help submenu not found"); + return; + } + + if ([helpMenu numberOfItems] > 0) { + [helpMenu addItem:[NSMenuItem separatorItem]]; + } + + NSMenuItem *customMenuItem = [[NSMenuItem alloc] initWithTitle:title + action:@selector(openURLAction:) + keyEquivalent:@""]; + [customMenuItem setTarget:controller]; + [helpMenu addItem:customMenuItem]; + + objc_setAssociatedObject(helpMenu, + [title UTF8String], + controller, + OBJC_ASSOCIATION_RETAIN); + + NSLogger(@"[+] Custom menu item '%@' added successfully", title); + }); +} + +@end + +@interface RMHookDylib : NSObject + +- (BOOL)hook; + +@end + +@implementation RMHook + ++ (void)load { + NSLogger(@"RMHook dylib loaded successfully"); + + RMHookDylib *dylib = [[RMHookDylib alloc] init]; + [dylib hook]; + +#ifdef BUILD_MODE_RMFAKECLOUD + NSString *configPath = ConfigFilePath(); + NSString *fileURL = [NSString stringWithFormat:@"file://%@", configPath]; + [MenuActionController addCustomHelpMenuEntry:@"Open rmfakecloud config" + withURL:fileURL + withDelay:2.0]; +#endif +} + +@end + +@implementation RMHookDylib + +#ifdef BUILD_MODE_RMFAKECLOUD +static QNetworkReply *(*original_qNetworkAccessManager_createRequest)( + QNetworkAccessManager *self, + QNetworkAccessManager::Operation op, + const QNetworkRequest &request, + QIODevice *outgoingData) = NULL; + +static void (*original_qWebSocket_open)( + QWebSocket *self, + const QNetworkRequest &request) = NULL; + +typedef void* MQTTAsync; +typedef void* MQTTAsync_createOptions; + +static int (*original_MQTTAsync_createWithOptions)( + MQTTAsync *handle, + const char *serverURI, + const char *clientId, + int persistence_type, + void *persistence_context, + MQTTAsync_createOptions *options) = NULL; +#endif + +#ifdef BUILD_MODE_QMLREBUILD +static int (*original_qRegisterResourceData)( + int, + const unsigned char *, + const unsigned char *, + const unsigned char *) = NULL; +#endif + +#ifdef BUILD_MODE_RMFAKECLOUD +static inline 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); +} +#endif + +static inline QString QStringFromNSStringSafe(NSString *string) { + if (!string) { + return QString(); + } + return QString::fromUtf8([string UTF8String]); +} + +- (BOOL)hook { + NSLogger(@"[RMHook] Starting hooks..."); + +#ifdef BUILD_MODE_RMFAKECLOUD + NSLogger(@"[RMHook] Build mode: rmfakecloud"); + ConfigLoadOrCreate(); + SSLConfigLoad(); + NSLogger(@"[RMHook] Using override host %@ and port %@", gConfiguredHostObjC, gConfiguredPortObjC); + + [MemoryUtils hookSymbol:@"QtNetwork" + symbolName:@"__ZN21QNetworkAccessManager13createRequestENS_9OperationERK15QNetworkRequestP9QIODevice" + hookFunction:(void *)hooked_qNetworkAccessManager_createRequest + originalFunction:(void **)&original_qNetworkAccessManager_createRequest + logPrefix:@"[RMHook]"]; + + [MemoryUtils hookSymbol:@"QtWebSockets" + symbolName:@"__ZN10QWebSocket4openERK15QNetworkRequest" + hookFunction:(void *)hooked_qWebSocket_open + originalFunction:(void **)&original_qWebSocket_open + logPrefix:@"[RMHook]"]; + + [MemoryUtils hookSymbol:@"libpaho-mqtt3as.1.dylib" + symbolName:@"_MQTTAsync_createWithOptions" + hookFunction:(void *)hooked_MQTTAsync_createWithOptions + originalFunction:(void **)&original_MQTTAsync_createWithOptions + logPrefix:@"[RMHook]"]; +#endif + +#ifdef BUILD_MODE_QMLREBUILD + NSLogger(@"[RMHook] Build mode: qmlrebuild"); + + messagebroker::registerQmlType(); + + messagebroker::setNativeCallback([](const char *signal, const char *value) { + NSLogger(@"[RMHook] Native callback received signal '%s' with value '%s'", signal, value); + }); + + [MemoryUtils hookSymbol:@"QtCore" + symbolName:@"__Z21qRegisterResourceDataiPKhS0_S0_" + hookFunction:(void *)hooked_qRegisterResourceData + originalFunction:(void **)&original_qRegisterResourceData + logPrefix:@"[RMHook]"]; + + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ + messagebroker::broadcast("signalName", "Hello from dylib!"); + }); +#endif + +#ifdef BUILD_MODE_DEV + NSLogger(@"[RMHook] Build mode: dev/reverse engineering"); +#endif + + return YES; +} + +#ifdef BUILD_MODE_RMFAKECLOUD +extern "C" QNetworkReply* hooked_qNetworkAccessManager_createRequest( + QNetworkAccessManager* self, + QNetworkAccessManager::Operation op, + const QNetworkRequest& req, + QIODevice* outgoingData +) { + const QString host = req.url().host(); + if (shouldPatchURL(host)) { + QNetworkRequest newReq(req); + QUrl newUrl = req.url(); + const QString overrideHost = QStringFromNSStringSafe(gConfiguredHostObjC); + newUrl.setHost(overrideHost); + newUrl.setPort([gConfiguredPortObjC intValue]); + newReq.setUrl(newUrl); + + SSLConfigApplyToRequest(newReq); + + 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; +} + +extern "C" void hooked_qWebSocket_open( + QWebSocket* self, + const QNetworkRequest& req +) { + if (!original_qWebSocket_open) { + return; + } + + const QString host = req.url().host(); + if (shouldPatchURL(host)) { + QUrl newUrl = req.url(); + const QString overrideHost = QStringFromNSStringSafe(gConfiguredHostObjC); + newUrl.setHost(overrideHost); + newUrl.setPort([gConfiguredPortObjC intValue]); + + QNetworkRequest newReq(req); + newReq.setUrl(newUrl); + + SSLConfigApplyToRequest(newReq); + + original_qWebSocket_open(self, newReq); + return; + } + + original_qWebSocket_open(self, req); +} + +static std::string PatchMqttUri(const char* uri) +{ + if (!uri) return {}; + const std::string original(uri); + + size_t schemeEnd = original.find("://"); + size_t hostStart = (schemeEnd != std::string::npos) ? schemeEnd + 3 : 0; + size_t hostEnd = original.find_first_of(":/", hostStart); + if (hostEnd == std::string::npos) hostEnd = original.size(); + + const std::string origHost = original.substr(hostStart, hostEnd - hostStart); + + static const char* kSuffixes[] = { + ".remarkable.com", + ".remarkable.engineering", + nullptr + }; + bool shouldPatch = false; + for (int i = 0; kSuffixes[i]; ++i) + { + const std::string suffix(kSuffixes[i]); + if (origHost.size() >= suffix.size() && + origHost.compare(origHost.size() - suffix.size(), + suffix.size(), suffix) == 0) + { + shouldPatch = true; + break; + } + } + if (!shouldPatch) return {}; + + std::string patched = original; + std::string proxyHost = [gConfiguredHostObjC UTF8String]; + patched.replace(hostStart, hostEnd - hostStart, proxyHost); + + size_t colonPos = patched.find(':', hostStart + proxyHost.size()); + if (colonPos != std::string::npos) + { + size_t numEnd = patched.find_first_not_of("0123456789", colonPos + 1); + if (numEnd == std::string::npos) numEnd = patched.size(); + patched.replace(colonPos + 1, numEnd - colonPos - 1, + std::to_string([gConfiguredPortObjC intValue])); + } + return patched; +} + +extern "C" int hooked_MQTTAsync_createWithOptions( + MQTTAsync *handle, + const char *serverURI, + const char *clientId, + int persistence_type, + void *persistence_context, + MQTTAsync_createOptions *options) +{ + if (!original_MQTTAsync_createWithOptions) { + return -1; + } + + std::string patchedUri = PatchMqttUri(serverURI); + if (!patchedUri.empty()) { + NSLogger(@"[RMHook] Patching MQTT URI from %s to %s", serverURI, patchedUri.c_str()); + return original_MQTTAsync_createWithOptions(handle, patchedUri.c_str(), clientId, persistence_type, persistence_context, options); + } + + return original_MQTTAsync_createWithOptions(handle, serverURI, clientId, persistence_type, persistence_context, options); +} +#endif // BUILD_MODE_RMFAKECLOUD + +#ifdef BUILD_MODE_QMLREBUILD + +extern "C" int hooked_qRegisterResourceData( + int version, + const unsigned char *tree, + const unsigned char *name, + const unsigned char *data +) { + if (!original_qRegisterResourceData) { + return 0; + } + + pthread_mutex_lock(&gResourceMutex); + struct ResourceRoot resource = { + .data = (uint8_t *)data, + .name = (uint8_t *)name, + .tree = (uint8_t *)tree, + + .treeSize = 0, + .dataSize = 0, + .originalDataSize = 0, + .nameSize = 0, + + .entriesAffected = 0, + }; + + NSLogger(@"[RMHook] Registering Qt resource version %d tree:%p name:%p data:%p", + version, tree, name, data); + + statArchive(&resource, 0); + + resource.tree = (uint8_t *)malloc(resource.treeSize); + if (!resource.tree) { + NSLogger(@"[RMHook] Failed to allocate tree buffer"); + pthread_mutex_unlock(&gResourceMutex); + return original_qRegisterResourceData(version, tree, name, data); + } + memcpy(resource.tree, tree, resource.treeSize); + + processNode(&resource, 0, ""); + NSLogger(@"[RMHook] Processing done! Entries affected: %d, dataSize: %zu, originalDataSize: %zu", + resource.entriesAffected, resource.dataSize, resource.originalDataSize); + + const unsigned char *finalTree = tree; + const unsigned char *finalData = data; + uint8_t *newDataBuffer = NULL; + + if (resource.entriesAffected > 0) { + NSLogger(@"[RMHook] Rebuilding data tables... (entries: %d)", resource.entriesAffected); + + newDataBuffer = (uint8_t *)malloc(resource.dataSize); + if (!newDataBuffer) { + NSLogger(@"[RMHook] Failed to allocate new data buffer (%zu bytes)", resource.dataSize); + free(resource.tree); + clearReplacementEntries(); + pthread_mutex_unlock(&gResourceMutex); + return original_qRegisterResourceData(version, tree, name, data); + } + + memcpy(newDataBuffer, data, resource.originalDataSize); + + struct ReplacementEntry *entry = getReplacementEntries(); + while (entry) { + writeUint32(newDataBuffer, (int)entry->copyToOffset, (uint32_t)entry->size); + memcpy(newDataBuffer + entry->copyToOffset + 4, entry->data, entry->size); + + NSLogger(@"[RMHook] Copied replacement for node %d at offset %zu (%zu bytes)", + entry->node, entry->copyToOffset, entry->size); + + entry = entry->next; + } + + finalTree = resource.tree; + finalData = newDataBuffer; + + NSLogger(@"[RMHook] Data buffer rebuilt: original %zu bytes -> new %zu bytes", + resource.originalDataSize, resource.dataSize); + } + + int status = original_qRegisterResourceData(version, finalTree, name, finalData); + + clearReplacementEntries(); + if (resource.tree && resource.entriesAffected == 0) { + free(resource.tree); + } + + pthread_mutex_unlock(&gResourceMutex); + return status; +} +#endif // BUILD_MODE_QMLREBUILD + +@end diff --git a/src/RMHook/SSLConfig.h b/src/RMHook/SSLConfig.h new file mode 100644 index 0000000..0e2baf7 --- /dev/null +++ b/src/RMHook/SSLConfig.h @@ -0,0 +1,4 @@ +#include + +void SSLConfigLoad(void); +void SSLConfigApplyToRequest(QNetworkRequest &request); diff --git a/src/RMHook/SSLConfig.m b/src/RMHook/SSLConfig.m new file mode 100644 index 0000000..59f74f7 --- /dev/null +++ b/src/RMHook/SSLConfig.m @@ -0,0 +1,92 @@ +#import "SSLConfig.h" +#import "Config.h" +#import "Logger.h" +#include +#include +#include +#include +#include +#include + +static QSslCertificate gClientCert; +static QSslKey gClientKey; +static QSslCertificate gCACert; +static bool gSSLInitialized = false; + +void SSLConfigLoad(void) { + if (gSSLInitialized) return; + + if (!gConfiguredClientCertPath.isEmpty() && !gConfiguredClientKeyPath.isEmpty()) { + QString certPath = gConfiguredClientCertPath; + QString keyPath = gConfiguredClientKeyPath; + + QFile certFile(QString::fromUtf8(certPath.toUtf8())); + if (certFile.open(QIODevice::ReadOnly)) { + gClientCert = QSslCertificate(certFile.readAll(), QSsl::Pem); + certFile.close(); + if (gClientCert.isNull()) { + NSLogger(@"[RMHook] Failed to parse client certificate from %s", certPath.toUtf8().constData()); + } else { + NSLogger(@"[RMHook] Loaded client certificate from %s", certPath.toUtf8().constData()); + } + } else { + NSLogger(@"[RMHook] Failed to open client certificate file %s", certPath.toUtf8().constData()); + } + + QFile keyFile(QString::fromUtf8(keyPath.toUtf8())); + if (keyFile.open(QIODevice::ReadOnly)) { + gClientKey = QSslKey(keyFile.readAll(), QSsl::Rsa, QSsl::Pem, QSsl::PrivateKey); + keyFile.close(); + if (gClientKey.isNull()) { + NSLogger(@"[RMHook] Failed to parse client key from %s", keyPath.toUtf8().constData()); + } else { + NSLogger(@"[RMHook] Loaded client key from %s", keyPath.toUtf8().constData()); + } + } else { + NSLogger(@"[RMHook] Failed to open client key file %s", keyPath.toUtf8().constData()); + } + } + + if (!gConfiguredCACertPath.isEmpty()) { + QString caPath = gConfiguredCACertPath; + QFile caFile(QString::fromUtf8(caPath.toUtf8())); + if (caFile.open(QIODevice::ReadOnly)) { + gCACert = QSslCertificate(caFile.readAll(), QSsl::Pem); + caFile.close(); + if (gCACert.isNull()) { + NSLogger(@"[RMHook] Failed to parse CA certificate from %s", caPath.toUtf8().constData()); + } else { + NSLogger(@"[RMHook] Loaded CA certificate from %s", caPath.toUtf8().constData()); + } + } else { + NSLogger(@"[RMHook] Failed to open CA certificate file %s", caPath.toUtf8().constData()); + } + } + + gSSLInitialized = true; +} + +void SSLConfigApplyToRequest(QNetworkRequest &request) { + if (!gSSLInitialized) return; + + QSslConfiguration sslConfig = QSslConfiguration::defaultConfiguration(); + + if (!gClientCert.isNull() && !gClientKey.isNull()) { + QList localCerts = sslConfig.localCertificateChain(); + localCerts.append(gClientCert); + sslConfig.setLocalCertificateChain(localCerts); + sslConfig.setPrivateKey(gClientKey); + } + + if (!gCACert.isNull()) { + QList caCerts = sslConfig.caCertificates(); + caCerts.append(gCACert); + sslConfig.setCaCertificates(caCerts); + } + + if (gDisableSSLVerification) { + sslConfig.setPeerVerifyMode(QSslSocket::VerifyNone); + } + + request.setSslConfiguration(sslConfig); +} diff --git a/src/reMarkable/reMarkable.m b/src/reMarkable/reMarkable.m deleted file mode 100644 index 657d1ed..0000000 --- a/src/reMarkable/reMarkable.m +++ /dev/null @@ -1,783 +0,0 @@ -#import "reMarkable.h" -#import -#import "Constant.h" -#import "MemoryUtils.h" -#import "Logger.h" -#import "ResourceUtils.h" -#ifdef BUILD_MODE_DEV -#import "DevHooks.h" -#endif -#ifdef BUILD_MODE_QMLREBUILD -#import "MessageBroker.h" -#endif -#import -#import -#include -#include -#include -#include -#include -#include -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - - -static NSString *const kReMarkableConfigFileName = @"rmfakecloud.config"; -static NSString *const kReMarkableConfigHostKey = @"host"; -static NSString *const kReMarkableConfigPortKey = @"port"; -static NSString *const kReMarkableConfigClientCertKey = @"client_cert"; -static NSString *const kReMarkableConfigClientKeyKey = @"client_key"; -static NSString *const kReMarkableConfigCACertKey = @"ca_cert"; -static NSString *const kReMarkableConfigDisableSSLVerifyKey = @"disable_ssl_verification"; -static NSString *const kReMarkableDefaultHost = @"example.com"; -static NSNumber *const kReMarkableDefaultPort = @(443); - -static NSString *gConfiguredHost = @"example.com"; -static NSNumber *gConfiguredPort = @(443); -static QString gConfiguredClientCertPath; -static QString gConfiguredClientKeyPath; -static QString gConfiguredCACertPath; -static bool gDisableSSLVerification = false; - -// Loaded SSL objects (populated once at startup) -static QSslCertificate gClientCert; -static QSslKey gClientKey; -static QSslCertificate gCACert; -static bool gSSLInitialized = false; - -static pthread_mutex_t gResourceMutex = PTHREAD_MUTEX_INITIALIZER; - -static NSString *ReMarkablePreferencesDirectory(void); - -static void ReMarkableLoadSSLConfig(void); - -static NSString *ReMarkablePreferencesDirectory(void) { - NSArray *libraryPaths = NSSearchPathForDirectoriesInDomains(NSLibraryDirectory, NSUserDomainMask, YES); - NSString *libraryDir = [libraryPaths firstObject]; - if (![libraryDir length]) { - libraryDir = [NSHomeDirectory() stringByAppendingPathComponent:@"Library"]; - } - return [libraryDir stringByAppendingPathComponent:@"Preferences"]; -} - -static NSString *ReMarkableConfigFilePath(void) { - return [ReMarkablePreferencesDirectory() stringByAppendingPathComponent:kReMarkableConfigFileName]; -} - -static void ReMarkableLoadSSLConfig(void) { - if (gSSLInitialized) return; - - if (!gConfiguredClientCertPath.isEmpty() && !gConfiguredClientKeyPath.isEmpty()) { - QString certPath = gConfiguredClientCertPath; - QString keyPath = gConfiguredClientKeyPath; - - QFile certFile(QString::fromUtf8(certPath.toUtf8())); - if (certFile.open(QIODevice::ReadOnly)) { - gClientCert = QSslCertificate(certFile.readAll(), QSsl::Pem); - certFile.close(); - if (gClientCert.isNull()) { - NSLogger(@"[reMarkable] Failed to parse client certificate from %s", certPath.toUtf8().constData()); - } else { - NSLogger(@"[reMarkable] Loaded client certificate from %s", certPath.toUtf8().constData()); - } - } else { - NSLogger(@"[reMarkable] Failed to open client certificate file %s", certPath.toUtf8().constData()); - } - - QFile keyFile(QString::fromUtf8(keyPath.toUtf8())); - if (keyFile.open(QIODevice::ReadOnly)) { - gClientKey = QSslKey(keyFile.readAll(), QSsl::Rsa, QSsl::Pem, QSsl::PrivateKey); - keyFile.close(); - if (gClientKey.isNull()) { - NSLogger(@"[reMarkable] Failed to parse client key from %s", keyPath.toUtf8().constData()); - } else { - NSLogger(@"[reMarkable] Loaded client key from %s", keyPath.toUtf8().constData()); - } - } else { - NSLogger(@"[reMarkable] Failed to open client key file %s", keyPath.toUtf8().constData()); - } - } - - if (!gConfiguredCACertPath.isEmpty()) { - QString caPath = gConfiguredCACertPath; - QFile caFile(QString::fromUtf8(caPath.toUtf8())); - if (caFile.open(QIODevice::ReadOnly)) { - gCACert = QSslCertificate(caFile.readAll(), QSsl::Pem); - caFile.close(); - if (gCACert.isNull()) { - NSLogger(@"[reMarkable] Failed to parse CA certificate from %s", caPath.toUtf8().constData()); - } else { - NSLogger(@"[reMarkable] Loaded CA certificate from %s", caPath.toUtf8().constData()); - } - } else { - NSLogger(@"[reMarkable] Failed to open CA certificate file %s", caPath.toUtf8().constData()); - } - } - - gSSLInitialized = true; -} - -static BOOL ReMarkableWriteConfig(NSString *path, NSDictionary *config) { - NSError *error = nil; - NSData *jsonData = [NSJSONSerialization dataWithJSONObject:config options:NSJSONWritingPrettyPrinted error:&error]; - if (!jsonData || error) { - NSLogger(@"[reMarkable] Failed to serialize config: %@", error); - return NO; - } - if (![jsonData writeToFile:path atomically:YES]) { - NSLogger(@"[reMarkable] Failed to write config file at %@", path); - return NO; - } - return YES; -} - -static inline QString QStringFromNSStringSafe(NSString *string) { - if (!string) { - return QString(); - } - return QString::fromUtf8([string UTF8String]); -} - -static void ReMarkableLoadOrCreateConfig(void) { - NSString *configPath = ReMarkableConfigFilePath(); - NSString *directory = [configPath stringByDeletingLastPathComponent]; - NSFileManager *fileManager = [NSFileManager defaultManager]; - BOOL isDirectory = NO; - NSError *error = nil; - - if (![fileManager fileExistsAtPath:directory isDirectory:&isDirectory] || !isDirectory) { - if (![fileManager createDirectoryAtPath:directory withIntermediateDirectories:YES attributes:nil error:&error]) { - NSLogger(@"[reMarkable] Failed to create config directory %@: %@", directory, error); - } - } - - NSDictionary *defaults = @{kReMarkableConfigHostKey : kReMarkableDefaultHost, - kReMarkableConfigPortKey : kReMarkableDefaultPort}; - - if ([fileManager fileExistsAtPath:configPath isDirectory:&isDirectory] && !isDirectory) { - NSData *data = [NSData dataWithContentsOfFile:configPath]; - if ([data length] > 0) { - NSError *jsonError = nil; - id jsonObject = [NSJSONSerialization JSONObjectWithData:data options:0 error:&jsonError]; - if (!jsonError && [jsonObject isKindOfClass:[NSDictionary class]]) { - NSDictionary *configDict = (NSDictionary *)jsonObject; - NSString *hostValue = configDict[kReMarkableConfigHostKey]; - NSNumber *portValue = configDict[kReMarkableConfigPortKey]; - - NSString *resolvedHost = ([hostValue isKindOfClass:[NSString class]] && [hostValue length]) ? hostValue : kReMarkableDefaultHost; - NSInteger portCandidate = kReMarkableDefaultPort.integerValue; - if ([portValue respondsToSelector:@selector(integerValue)]) { - NSInteger candidate = [portValue integerValue]; - if (candidate > 0 && candidate <= 65535) { - portCandidate = candidate; - } else { - NSLogger(@"[reMarkable] Ignoring invalid port value %@, falling back to default.", portValue); - } - } - - gConfiguredHost = [resolvedHost copy]; - gConfiguredPort = @(portCandidate); - - // TLS config keys (optional) - NSString *certPathValue = configDict[kReMarkableConfigClientCertKey]; - NSString *keyPathValue = configDict[kReMarkableConfigClientKeyKey]; - NSString *caPathValue = configDict[kReMarkableConfigCACertKey]; - NSNumber *disableSSLValue = configDict[kReMarkableConfigDisableSSLVerifyKey]; - - if ([certPathValue isKindOfClass:[NSString class]] && [certPathValue length]) { - gConfiguredClientCertPath = QStringFromNSStringSafe(certPathValue); - } - if ([keyPathValue isKindOfClass:[NSString class]] && [keyPathValue length]) { - gConfiguredClientKeyPath = QStringFromNSStringSafe(keyPathValue); - } - if ([caPathValue isKindOfClass:[NSString class]] && [caPathValue length]) { - gConfiguredCACertPath = QStringFromNSStringSafe(caPathValue); - } - if ([disableSSLValue respondsToSelector:@selector(boolValue)]) { - gDisableSSLVerification = [disableSSLValue boolValue]; - } - - NSLogger(@"[reMarkable] Loaded config from %@ with host %@ and port %@", configPath, gConfiguredHost, gConfiguredPort); - return; - } else { - NSLogger(@"[reMarkable] Failed to parse config file %@: %@", configPath, jsonError); - } - } else { - NSLogger(@"[reMarkable] Config file %@ was empty, rewriting with defaults.", configPath); - } - } - - if (ReMarkableWriteConfig(configPath, defaults)) { - NSLogger(@"[reMarkable] Created default config at %@", configPath); - } - gConfiguredHost = [kReMarkableDefaultHost copy]; - gConfiguredPort = kReMarkableDefaultPort; -} - -@interface MenuActionController : NSObject -@property (strong, nonatomic) NSURL *targetURL; -- (void)openURLAction:(id)sender; -+ (void)addCustomHelpMenuEntry:(NSString *)title withURL:(NSString *)url; -+ (void)addCustomHelpMenuEntry:(NSString *)title withURL:(NSString *)url withDelay:(NSTimeInterval)delay; -@end - -@implementation MenuActionController - -- (void)openURLAction:(id)sender { - if (self.targetURL) { - [[NSWorkspace sharedWorkspace] openURL:self.targetURL]; - NSLogger(@"[+] URL opened successfully: %@", self.targetURL); - } -} - -+ (void)addCustomHelpMenuEntry:(NSString *)title withURL:(NSString *)url { - [self addCustomHelpMenuEntry:title withURL:url withDelay:1.0]; -} - -+ (void)addCustomHelpMenuEntry:(NSString *)title withURL:(NSString *)url withDelay:(NSTimeInterval)delay { - dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delay * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ - MenuActionController *controller = [[MenuActionController alloc] init]; - controller.targetURL = [NSURL URLWithString:url]; - - NSMenu *mainMenu = [NSApp mainMenu]; - if (!mainMenu) { - NSLogger(@"[-] Main menu not found"); - return; - } - - NSMenuItem *helpMenuItem = nil; - for (NSMenuItem *item in [mainMenu itemArray]) { - if ([[item title] isEqualToString:@"Help"]) { - helpMenuItem = item; - break; - } - } - - if (!helpMenuItem) { - NSLogger(@"[-] Help menu item not found"); - return; - } - - NSMenu *helpMenu = [helpMenuItem submenu]; - if (!helpMenu) { - NSLogger(@"[-] Help submenu not found"); - return; - } - - if ([helpMenu numberOfItems] > 0) { - [helpMenu addItem:[NSMenuItem separatorItem]]; - } - - NSMenuItem *customMenuItem = [[NSMenuItem alloc] initWithTitle:title - action:@selector(openURLAction:) - keyEquivalent:@""]; - [customMenuItem setTarget:controller]; - [helpMenu addItem:customMenuItem]; - - objc_setAssociatedObject(helpMenu, - [title UTF8String], - controller, - OBJC_ASSOCIATION_RETAIN); - - NSLogger(@"[+] Custom menu item '%@' added successfully", title); - }); -} - -@end - -@interface reMarkableDylib : NSObject - -- (BOOL)hook; - -@end - -@implementation reMarkable - -+ (void)load { - NSLogger(@"reMarkable dylib loaded successfully"); - - // Initialize the hook - reMarkableDylib *dylib = [[reMarkableDylib alloc] init]; - [dylib hook]; - -#ifdef BUILD_MODE_RMFAKECLOUD - // Add custom Help menu entry to open config file - NSString *configPath = ReMarkableConfigFilePath(); - NSString *fileURL = [NSString stringWithFormat:@"file://%@", configPath]; - [MenuActionController addCustomHelpMenuEntry:@"Open rmfakecloud config" - withURL:fileURL - withDelay:2.0]; -#endif -} - -@end - -@implementation reMarkableDylib - -#ifdef BUILD_MODE_RMFAKECLOUD -static QNetworkReply *(*original_qNetworkAccessManager_createRequest)( - QNetworkAccessManager *self, - QNetworkAccessManager::Operation op, - const QNetworkRequest &request, - QIODevice *outgoingData) = NULL; - -static void (*original_qWebSocket_open)( - QWebSocket *self, - const QNetworkRequest &request) = NULL; - -typedef void* MQTTAsync; -typedef void* MQTTAsync_createOptions; - -static int (*original_MQTTAsync_createWithOptions)( - MQTTAsync *handle, - const char *serverURI, - const char *clientId, - int persistence_type, - void *persistence_context, - MQTTAsync_createOptions *options) = NULL; -#endif - -#ifdef BUILD_MODE_QMLREBUILD -static int (*original_qRegisterResourceData)( - int, - const unsigned char *, - const unsigned char *, - const unsigned char *) = NULL; -#endif - - - -#ifdef BUILD_MODE_RMFAKECLOUD -static inline 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); -} -#endif - -- (BOOL)hook { - NSLogger(@"[reMarkable] Starting hooks..."); - -#ifdef BUILD_MODE_RMFAKECLOUD - NSLogger(@"[reMarkable] Build mode: rmfakecloud"); - ReMarkableLoadOrCreateConfig(); - ReMarkableLoadSSLConfig(); - NSLogger(@"[reMarkable] Using override host %@ and port %@", gConfiguredHost, gConfiguredPort); - - [MemoryUtils hookSymbol:@"QtNetwork" - symbolName:@"__ZN21QNetworkAccessManager13createRequestENS_9OperationERK15QNetworkRequestP9QIODevice" - hookFunction:(void *)hooked_qNetworkAccessManager_createRequest - originalFunction:(void **)&original_qNetworkAccessManager_createRequest - logPrefix:@"[reMarkable]"]; - - [MemoryUtils hookSymbol:@"QtWebSockets" - symbolName:@"__ZN10QWebSocket4openERK15QNetworkRequest" - hookFunction:(void *)hooked_qWebSocket_open - originalFunction:(void **)&original_qWebSocket_open - logPrefix:@"[reMarkable]"]; - - [MemoryUtils hookSymbol:@"libpaho-mqtt3as.1.dylib" - symbolName:@"_MQTTAsync_createWithOptions" - hookFunction:(void *)hooked_MQTTAsync_createWithOptions - originalFunction:(void **)&original_MQTTAsync_createWithOptions - logPrefix:@"[reMarkable]"]; -#endif - -#ifdef BUILD_MODE_QMLREBUILD - NSLogger(@"[reMarkable] Build mode: qmlrebuild"); - - // Register MessageBroker QML type for dylib <-> QML communication - messagebroker::registerQmlType(); - - // Register native callback to receive signals from QML - messagebroker::setNativeCallback([](const char *signal, const char *value) { - NSLogger(@"[reMarkable] Native callback received signal '%s' with value '%s'", signal, value); - }); - - [MemoryUtils hookSymbol:@"QtCore" - symbolName:@"__Z21qRegisterResourceDataiPKhS0_S0_" - hookFunction:(void *)hooked_qRegisterResourceData - originalFunction:(void **)&original_qRegisterResourceData - logPrefix:@"[reMarkable]"]; - - // Send a delayed broadcast to QML (after UI has loaded) - dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ - messagebroker::broadcast("signalName", "Hello from dylib!"); - }); -#endif - -#ifdef BUILD_MODE_DEV - NSLogger(@"[reMarkable] Build mode: dev/reverse engineering"); - // [MemoryUtils hookSymbol:@"QtCore" - // symbolName:@"__ZN9QIODevice5writeEPKcx" - // hookFunction:(void *)hooked_qIODevice_write - // originalFunction:(void **)&original_qIODevice_write - // logPrefix:@"[reMarkable]"]; - - // // Hook function at address 0x10015A130 - // [MemoryUtils hookAddress:@"reMarkable" - // staticAddress:0x10015A130 - // hookFunction:(void *)hooked_function_at_0x10015A130 - // originalFunction:(void **)&original_function_at_0x10015A130 - // logPrefix:@"[reMarkable]"]; - - // // Hook function at address 0x10015BC90 - // [MemoryUtils hookAddress:@"reMarkable" - // staticAddress:0x10015BC90 - // hookFunction:(void *)hooked_function_at_0x10015BC90 - // originalFunction:(void **)&original_function_at_0x10015BC90 - // logPrefix:@"[reMarkable]"]; - - // // Hook function at address 0x10016D520 - // [MemoryUtils hookAddress:@"reMarkable" - // staticAddress:0x10016D520 - // hookFunction:(void *)hooked_function_at_0x10016D520 - // originalFunction:(void **)&original_function_at_0x10016D520 - // logPrefix:@"[reMarkable]"]; - - // // Hook function at address 0x1001B6EE0 - // [MemoryUtils hookAddress:@"reMarkable" - // staticAddress:0x1001B6EE0 - // hookFunction:(void *)hooked_function_at_0x1001B6EE0 - // originalFunction:(void **)&original_function_at_0x1001B6EE0 - // logPrefix:@"[reMarkable]"]; - - // PlatformHelpers.exportFile implementation WIP - - // // Hook function at address 0x100011790 - // [MemoryUtils hookAddress:@"reMarkable" - // staticAddress:0x100011790 - // hookFunction:(void *)hooked_function_at_0x100011790 - // originalFunction:(void **)&original_function_at_0x100011790 - // logPrefix:@"[reMarkable]"]; - - // // Hook function at address 0x100011CE0 - // [MemoryUtils hookAddress:@"reMarkable" - // staticAddress:0x100011CE0 - // hookFunction:(void *)hooked_function_at_0x100011CE0 - // originalFunction:(void **)&original_function_at_0x100011CE0 - // logPrefix:@"[reMarkable]"]; - - // [MemoryUtils hookSymbol:@"QtQml" - // symbolName:@"__ZN11QQmlPrivate11qmlregisterENS_16RegistrationTypeEPv" - // hookFunction:(void *)hooked_qmlregister - // originalFunction:(void **)&original_qmlregister - // logPrefix:@"[reMarkable]"]; - -#endif - - return YES; -} - -#ifdef BUILD_MODE_RMFAKECLOUD -extern "C" QNetworkReply* hooked_qNetworkAccessManager_createRequest( - QNetworkAccessManager* self, - QNetworkAccessManager::Operation op, - const QNetworkRequest& req, - QIODevice* outgoingData -) { - const QString host = req.url().host(); - if (shouldPatchURL(host)) { - // Clone request to keep original immutable - QNetworkRequest newReq(req); - QUrl newUrl = req.url(); - const QString overrideHost = QStringFromNSStringSafe(gConfiguredHost); - newUrl.setHost(overrideHost); - newUrl.setPort([gConfiguredPort intValue]); - newReq.setUrl(newUrl); - - // Apply SSL configuration for mTLS / custom CA - if (gSSLInitialized) { - QSslConfiguration sslConfig = QSslConfiguration::defaultConfiguration(); - - if (!gClientCert.isNull() && !gClientKey.isNull()) { - QList localCerts = sslConfig.localCertificateChain(); - localCerts.append(gClientCert); - sslConfig.setLocalCertificateChain(localCerts); - sslConfig.setPrivateKey(gClientKey); - } - - if (!gCACert.isNull()) { - QList caCerts = sslConfig.caCertificates(); - caCerts.append(gCACert); - sslConfig.setCaCertificates(caCerts); - } - - if (gDisableSSLVerification) { - sslConfig.setPeerVerifyMode(QSslSocket::VerifyNone); - } - - newReq.setSslConfiguration(sslConfig); - } - - 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; -} - -extern "C" void hooked_qWebSocket_open( - QWebSocket* self, - const QNetworkRequest& req -) { - if (!original_qWebSocket_open) { - return; - } - - const QString host = req.url().host(); - if (shouldPatchURL(host)) { - QUrl newUrl = req.url(); - const QString overrideHost = QStringFromNSStringSafe(gConfiguredHost); - newUrl.setHost(overrideHost); - newUrl.setPort([gConfiguredPort intValue]); - - QNetworkRequest newReq(req); - newReq.setUrl(newUrl); - - // Apply SSL configuration for mTLS / custom CA - if (gSSLInitialized) { - QSslConfiguration sslConfig = QSslConfiguration::defaultConfiguration(); - - if (!gClientCert.isNull() && !gClientKey.isNull()) { - QList localCerts = sslConfig.localCertificateChain(); - localCerts.append(gClientCert); - sslConfig.setLocalCertificateChain(localCerts); - sslConfig.setPrivateKey(gClientKey); - } - - if (!gCACert.isNull()) { - QList caCerts = sslConfig.caCertificates(); - caCerts.append(gCACert); - sslConfig.setCaCertificates(caCerts); - } - - if (gDisableSSLVerification) { - sslConfig.setPeerVerifyMode(QSslSocket::VerifyNone); - } - - newReq.setSslConfiguration(sslConfig); - } - - original_qWebSocket_open(self, newReq); - return; - } - - original_qWebSocket_open(self, req); -} - -// Patch a paho URI: "ssl://host.remarkable.com:port" -> "ssl://proxy:port" -// Returns patched string, or empty if no patch needed. -static std::string PatchMqttUri(const char* uri) -{ - if (!uri) return {}; - const std::string original(uri); - - size_t schemeEnd = original.find("://"); - size_t hostStart = (schemeEnd != std::string::npos) ? schemeEnd + 3 : 0; - size_t hostEnd = original.find_first_of(":/", hostStart); - if (hostEnd == std::string::npos) hostEnd = original.size(); - - const std::string origHost = original.substr(hostStart, hostEnd - hostStart); - - // Match any *.remarkable.com or *.remarkable.engineering host - static const char* kSuffixes[] = { - ".remarkable.com", - ".remarkable.engineering", - nullptr - }; - bool shouldPatch = false; - for (int i = 0; kSuffixes[i]; ++i) - { - const std::string suffix(kSuffixes[i]); - if (origHost.size() >= suffix.size() && - origHost.compare(origHost.size() - suffix.size(), - suffix.size(), suffix) == 0) - { - shouldPatch = true; - break; - } - } - if (!shouldPatch) return {}; - - std::string patched = original; - std::string proxyHost = [gConfiguredHost UTF8String]; - patched.replace(hostStart, hostEnd - hostStart, proxyHost); - - // Fix port - size_t colonPos = patched.find(':', hostStart + proxyHost.size()); - if (colonPos != std::string::npos) - { - size_t numEnd = patched.find_first_not_of("0123456789", colonPos + 1); - if (numEnd == std::string::npos) numEnd = patched.size(); - patched.replace(colonPos + 1, numEnd - colonPos - 1, - std::to_string([gConfiguredPort intValue])); - } - return patched; -} - -extern "C" int hooked_MQTTAsync_createWithOptions( - MQTTAsync *handle, - const char *serverURI, - const char *clientId, - int persistence_type, - void *persistence_context, - MQTTAsync_createOptions *options) -{ - if (!original_MQTTAsync_createWithOptions) { - return -1; // error code for MQTTAsync_create failure - } - - std::string patchedUri = PatchMqttUri(serverURI); - if (!patchedUri.empty()) { - NSLogger(@"[reMarkable] Patching MQTT URI from %s to %s", serverURI, patchedUri.c_str()); - return original_MQTTAsync_createWithOptions(handle, patchedUri.c_str(), clientId, persistence_type, persistence_context, options); - } - - return original_MQTTAsync_createWithOptions(handle, serverURI, clientId, persistence_type, persistence_context, options); -} -#endif // BUILD_MODE_RMFAKECLOUD - -#ifdef BUILD_MODE_QMLREBUILD - -// See https://deepwiki.com/search/once-the-qrr-file-parsed-take_871f24a0-8636-4aee-bddf-7405b6e32584 for details on qmlrebuild replacement strategy - -extern "C" int hooked_qRegisterResourceData( - int version, - const unsigned char *tree, - const unsigned char *name, - const unsigned char *data -) { - if (!original_qRegisterResourceData) { - return 0; - } - - pthread_mutex_lock(&gResourceMutex); - struct ResourceRoot resource = { - .data = (uint8_t *)data, - .name = (uint8_t *)name, - .tree = (uint8_t *)tree, - - .treeSize = 0, - .dataSize = 0, - .originalDataSize = 0, - .nameSize = 0, - - .entriesAffected = 0, - }; - - NSLogger(@"[reMarkable] Registering Qt resource version %d tree:%p name:%p data:%p", - version, tree, name, data); - - statArchive(&resource, 0); - - // Make a writable copy of the tree (we need to modify offsets) - resource.tree = (uint8_t *)malloc(resource.treeSize); - if (!resource.tree) { - NSLogger(@"[reMarkable] Failed to allocate tree buffer"); - pthread_mutex_unlock(&gResourceMutex); - return original_qRegisterResourceData(version, tree, name, data); - } - memcpy(resource.tree, tree, resource.treeSize); - - // Process nodes and mark replacements - processNode(&resource, 0, ""); - NSLogger(@"[reMarkable] Processing done! Entries affected: %d, dataSize: %zu, originalDataSize: %zu", - resource.entriesAffected, resource.dataSize, resource.originalDataSize); - - const unsigned char *finalTree = tree; - const unsigned char *finalData = data; - uint8_t *newDataBuffer = NULL; - - if (resource.entriesAffected > 0) { - NSLogger(@"[reMarkable] Rebuilding data tables... (entries: %d)", resource.entriesAffected); - - // Allocate new data buffer (original size + space for replacements) - newDataBuffer = (uint8_t *)malloc(resource.dataSize); - if (!newDataBuffer) { - NSLogger(@"[reMarkable] Failed to allocate new data buffer (%zu bytes)", resource.dataSize); - free(resource.tree); - clearReplacementEntries(); - pthread_mutex_unlock(&gResourceMutex); - return original_qRegisterResourceData(version, tree, name, data); - } - - // Copy original data - memcpy(newDataBuffer, data, resource.originalDataSize); - - // Copy replacement entries to their designated offsets - struct ReplacementEntry *entry = getReplacementEntries(); - while (entry) { - // Write size prefix (4 bytes, big-endian) - writeUint32(newDataBuffer, (int)entry->copyToOffset, (uint32_t)entry->size); - // Write data after size prefix - memcpy(newDataBuffer + entry->copyToOffset + 4, entry->data, entry->size); - - NSLogger(@"[reMarkable] Copied replacement for node %d at offset %zu (%zu bytes)", - entry->node, entry->copyToOffset, entry->size); - - entry = entry->next; - } - - finalTree = resource.tree; - finalData = newDataBuffer; - - NSLogger(@"[reMarkable] Data buffer rebuilt: original %zu bytes -> new %zu bytes", - resource.originalDataSize, resource.dataSize); - } - - int status = original_qRegisterResourceData(version, finalTree, name, finalData); - - // Cleanup - clearReplacementEntries(); - if (resource.tree && resource.entriesAffected == 0) { - free(resource.tree); - } - // Note: We intentionally don't free newDataBuffer or resource.tree when entriesAffected > 0 - // because Qt will use these buffers for the lifetime of the application - - pthread_mutex_unlock(&gResourceMutex); - return status; -} -#endif // BUILD_MODE_QMLREBUILD - -@end \ No newline at end of file