From 33f9772c9004084f187fbae784363f8e21f72acf 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: Fri, 15 May 2026 19:44:54 +0200 Subject: [PATCH] Add build script and hook updates --- .gitignore | 2 +- LICENSE | 21 ++++++++ script/build.sh | 29 ++++++++++ src/Config.h | 17 ++++++ src/Config.mm | 86 ++++++++++++++++++++++++++++++ src/Makefile | 22 ++++---- src/Tweak.xm | 138 ++++++++++++++++++++++++++++++++++++++++++------ src/control | 4 +- 8 files changed, 289 insertions(+), 30 deletions(-) create mode 100644 LICENSE create mode 100755 script/build.sh create mode 100644 src/Config.h create mode 100644 src/Config.mm diff --git a/.gitignore b/.gitignore index d988680..9e47b29 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,4 @@ /src/.theos /src/packages .DS_Store -/rev +/rev \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..4df63b3 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Rivoirard Noham + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/script/build.sh b/script/build.sh new file mode 100755 index 0000000..34f9c15 --- /dev/null +++ b/script/build.sh @@ -0,0 +1,29 @@ +#!/bin/bash + +# Default to 3.25.0 if no argument is provided +VERSION=${1:-3.25.0} + +if [ "$VERSION" == "3.25.0" ]; then + MACRO="-DV3_25_0=1" + QT_VERSION="6.8.2" +elif [ "$VERSION" == "3.27.1" ]; then + MACRO="-DV3_27_1=1" + QT_VERSION="6.10.0" +else + echo "Error: Unknown version '$VERSION'. Supported versions are: 3.25.0, 3.27.1" + exit 1 +fi + +MODE=${2:-dev} + +echo "Building for reMarkable version: $VERSION ($MACRO) in $MODE mode" + +make clean + +if [ "$MODE" == "release" ]; then + # Modify control file to set the version to match the target app version + sed -i '' "s/^Version: .*/Version: $VERSION/" control + make package THEOS_PACKAGE_SCHEME=rootless FINALPACKAGE=1 RM_VERSION_FLAG="$MACRO" QT_VERSION="$QT_VERSION" +else + make package THEOS_PACKAGE_SCHEME=rootless DEBUG=0 RM_VERSION_FLAG="$MACRO" QT_VERSION="$QT_VERSION" +fi diff --git a/src/Config.h b/src/Config.h new file mode 100644 index 0000000..f6f4675 --- /dev/null +++ b/src/Config.h @@ -0,0 +1,17 @@ +#import +#import + +extern NSString *gConfiguredHost; +extern NSNumber *gConfiguredPort; + +#ifdef __cplusplus +extern "C" { +#endif + +void loadConfiguration(void); +void saveConfiguration(NSString *host, NSNumber *port); +void showConfigAlert(void); + +#ifdef __cplusplus +} +#endif diff --git a/src/Config.mm b/src/Config.mm new file mode 100644 index 0000000..45c8ea7 --- /dev/null +++ b/src/Config.mm @@ -0,0 +1,86 @@ +#import "Config.h" + +NSString *gConfiguredHost = @""; +NSNumber *gConfiguredPort = @(0); + +void saveConfiguration(NSString *host, NSNumber *port) { + NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; + [defaults setObject:host forKey:@"RMHook_Host"]; + [defaults setObject:port forKey:@"RMHook_Port"]; + [defaults synchronize]; + gConfiguredHost = host; + gConfiguredPort = port; + NSLog(@"[RMHook-iOS] Saved config - Host: %@, Port: %@", gConfiguredHost, gConfiguredPort); +} + +void loadConfiguration() { + NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; + NSString *host = [defaults stringForKey:@"RMHook_Host"]; + NSNumber *port = [defaults objectForKey:@"RMHook_Port"]; + + if (host && host.length > 0 && port && [port intValue] > 0) { + gConfiguredHost = host; + gConfiguredPort = port; + NSLog(@"[RMHook-iOS] Loaded config - Host: %@, Port: %@", gConfiguredHost, gConfiguredPort); + } +} + +void showConfigAlert() { + dispatch_async(dispatch_get_main_queue(), ^{ + UIWindow *window = nil; + if (@available(iOS 13.0, *)) { + for (UIWindowScene *scene in [UIApplication sharedApplication].connectedScenes) { + if (scene.activationState == UISceneActivationStateForegroundActive) { + for (UIWindow *w in scene.windows) { + if (w.isKeyWindow) { + window = w; + break; + } + } + } + } + } + if (!window) { +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + window = [UIApplication sharedApplication].keyWindow; +#pragma clang diagnostic pop + } + + if (!window || !window.rootViewController) { + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 1 * NSEC_PER_SEC), dispatch_get_main_queue(), ^{ + showConfigAlert(); + }); + return; + } + + UIViewController *rootVC = window.rootViewController; + while (rootVC.presentedViewController) { + rootVC = rootVC.presentedViewController; + } + + UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"RMHook" + message:@"First Launch: Enter Host and Port" + preferredStyle:UIAlertControllerStyleAlert]; + + [alert addTextFieldWithConfigurationHandler:^(UITextField * _Nonnull textField) { + textField.placeholder = @"Host (e.g. example.com)"; + textField.text = @"example.com"; + }]; + + [alert addTextFieldWithConfigurationHandler:^(UITextField * _Nonnull textField) { + textField.placeholder = @"Port (e.g. 443)"; + textField.text = @"443"; + textField.keyboardType = UIKeyboardTypeNumberPad; + }]; + + UIAlertAction *saveAction = [UIAlertAction actionWithTitle:@"Save" style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) { + NSString *host = alert.textFields[0].text; + NSNumber *port = @([alert.textFields[1].text integerValue]); + saveConfiguration(host, port); + }]; + + [alert addAction:saveAction]; + [rootVC presentViewController:alert animated:YES completion:nil]; + }); +} diff --git a/src/Makefile b/src/Makefile index 323eafc..d4d56b4 100644 --- a/src/Makefile +++ b/src/Makefile @@ -1,18 +1,20 @@ -TARGET = iphone:latest:14.0 +TARGET = iphone:latest:17.0 INSTALL_TARGET_PROCESSES = remarkable_mobile -ARCHS = arm64 arm64e +ARCHS = arm64 include $(THEOS)/makefiles/common.mk TWEAK_NAME = RMHook -RMHook_FILES = Tweak.xm -RMHook_CFLAGS = -fobjc-arc -F$(HOME)/Qt/6.10.0/lib -RMHook_CXXFLAGS = -fobjc-arc -F$(HOME)/Qt/6.10.0/lib -std=c++17 -ADDITIONAL_CFLAGS = -std=c++17 -Wno-c++17-extensions -ADDITIONAL_CXXFLAGS = -std=c++17 -Wno-c++17-extensions -DQT_NO_VERSION_TAGGING -ADDITIONAL_OBJCCFLAGS = -std=c++17 -Wno-c++17-extensions -DQT_NO_VERSION_TAGGING -RMHook_LDFLAGS = -RMHook_FRAMEWORKS = Foundation +QT_VERSION ?= 6.8.2 + +RMHook_FILES = Tweak.xm Config.mm +RMHook_CFLAGS = -fobjc-arc -F$(HOME)/Qt/$(QT_VERSION)/ios/lib +RMHook_CXXFLAGS = -fobjc-arc -F$(HOME)/Qt/$(QT_VERSION)/ios/lib -std=c++17 -DQT_NO_VERSION_TAGGING +ADDITIONAL_CFLAGS = -std=c++17 -Wno-c++17-extensions $(RM_VERSION_FLAG) +ADDITIONAL_CXXFLAGS = -std=c++17 -Wno-c++17-extensions -DQT_NO_VERSION_TAGGING $(RM_VERSION_FLAG) +ADDITIONAL_OBJCCFLAGS = -std=c++17 -Wno-c++17-extensions -DQT_NO_VERSION_TAGGING $(RM_VERSION_FLAG) +RMHook_FRAMEWORKS = Foundation QtNetwork QtCore QtWebSockets UIKit UniformTypeIdentifiers Network SystemConfiguration Security IOKit +RMHook_LDFLAGS = -F$(HOME)/Qt/$(QT_VERSION)/ios/lib -lz $(HOME)/Qt/$(QT_VERSION)/ios/lib/libQt6BundledPcre2.a include $(THEOS_MAKE_PATH)/tweak.mk diff --git a/src/Tweak.xm b/src/Tweak.xm index 15e679e..2d22468 100644 --- a/src/Tweak.xm +++ b/src/Tweak.xm @@ -17,34 +17,127 @@ #include #include +#import + #define TARGET_MODULE "remarkable_mobile" #define IDA_BASE 0x100000000 + // __ZN21QNetworkAccessManager13createRequestENS_9OperationERK15QNetworkRequestP9QIODevice +#if V3_25_0 #define QtNetworkAccessManager_createRequest 0x1017FB9F4 // sub_1017FB9F4 +#elif V3_27_1 +#define QtNetworkAccessManager_createRequest 0x10192472C // sub_10192472C +#endif // __ZN10QWebSocket4openERK15QNetworkRequest +#if V3_25_0 # define QtWebSocket_open 0x100526A18 // sub_100526A18 +#elif V3_27_1 +# define QtWebSocket_open 0x100564A24 // sub_100564A24 +#endif -// QObject *__fastcall QNetworkAccessManager::createRequest( -// QtSharedPointer::ExternalRefCountData *a1, -// __int64 a2, -// const QNetworkRequest *a3, -// __int64 a4) -static void *(*orig_createRequest)(void *a1, int a2, const void *a3, void *a4); +#import "Config.h" -void *hook_createRequest(void *a1, int a2, const void *a3, void *a4) { - NSLog(@"[RMHook-iOS] createRequest called"); - return orig_createRequest(a1, a2, a3, a4); +static inline QString QStringFromNSStringSafe(NSString* str) { + if (!str) return QString(); + return QString::fromNSString(str); } -// void __fastcall QWebSocket::open(QWebSocket *this, const QUrl *a2, const QWebSocketHandshakeOptions *a3) -static void (*orig_open)(void *this_ptr, const void *a2, const void *a3); +static inline bool shouldPatchURL(const QString &host) { + if (host.isEmpty()) { + return false; + } -void hook_open(void *this_ptr, const void *a2, const void *a3) { - NSLog(@"[RMHook-iOS] QWebSocket::open called"); - orig_open(this_ptr, a2, a3); + 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 *__fastcall QNetworkAccessManager::createRequest( +// QtSharedPointer::ExternalRefCountData *self, +// __int64 op, +// const QNetworkRequest *req, +// __int64 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 +) { + NSLog(@"[RMHook-iOS] 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(); + const QString overrideHost = QStringFromNSStringSafe(gConfiguredHost); + newUrl.setHost(overrideHost); + newUrl.setPort([gConfiguredPort intValue]); + 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 __fastcall QWebSocket::open(QWebSocket *self, const QNetworkRequest *req) +static void (*original_qWebSocket_open)( + QWebSocket* self, + const QNetworkRequest& req +); + +void hooked_qWebSocket_open( + QWebSocket* self, + const QNetworkRequest& req +) { + NSLog(@"[RMHook-iOS] 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(); + const QString overrideHost = QStringFromNSStringSafe(gConfiguredHost); + newUrl.setHost(overrideHost); + newUrl.setPort([gConfiguredPort intValue]); + + QNetworkRequest newReq(req); + newReq.setUrl(newUrl); + + original_qWebSocket_open(self, newReq); + return; + } + + original_qWebSocket_open(self, req); } @@ -58,9 +151,20 @@ static uintptr_t findModuleBase(const char *moduleName) { return 0; } -// Constructor + %ctor { @autoreleasepool { + loadConfiguration(); + + if (gConfiguredHost.length == 0 || [gConfiguredPort intValue] == 0) { + [[NSNotificationCenter defaultCenter] addObserverForName:UIApplicationDidFinishLaunchingNotification + object:nil + queue:[NSOperationQueue mainQueue] + usingBlock:^(NSNotification * _Nonnull note) { + showConfigAlert(); + }]; + } + uintptr_t base = findModuleBase(TARGET_MODULE); if (!base) { NSLog(@"[RMHook-iOS] Module '%s' not found, tweak inactive.", TARGET_MODULE); @@ -70,12 +174,12 @@ static uintptr_t findModuleBase(const char *moduleName) { uintptr_t offset = QtNetworkAccessManager_createRequest - IDA_BASE; uintptr_t addr = base + offset; - MSHookFunction((void *)addr, (void *)hook_createRequest, (void **)&orig_createRequest); + MSHookFunction((void *)addr, (void *)hooked_qNetworkAccessManager_createRequest, (void **)&original_qNetworkAccessManager_createRequest); NSLog(@"[RMHook-iOS] Hooked QtNetworkAccessManager_createRequest @ 0x%lx (offset 0x%lx)", (unsigned long)addr, (unsigned long)offset); uintptr_t offset2 = QtWebSocket_open - IDA_BASE; uintptr_t addr2 = base + offset2; - MSHookFunction((void *)addr2, (void *)hook_open, (void **)&orig_open); + MSHookFunction((void *)addr2, (void *)hooked_qWebSocket_open, (void **)&original_qWebSocket_open); NSLog(@"[RMHook-iOS] Hooked QtWebSocket_open @ 0x%lx (offset 0x%lx)", (unsigned long)addr2, (unsigned long)offset2); } } diff --git a/src/control b/src/control index d21a241..e252071 100644 --- a/src/control +++ b/src/control @@ -1,8 +1,8 @@ Package: xyz.noham.rmhook Name: RMHook -Version: 0.0.1 +Version: 3.27.1 Architecture: iphoneos-arm -Description: An awesome MobileSubstrate tweak! +Description: A tweak to hook remarkable mobile app. Maintainer: NohamR Author: NohamR Section: Tweaks