diff --git a/IPAs/com.remarkable.mobile-3.17.0.ipa b/IPAs/com.remarkable.mobile-3.17.0.ipa deleted file mode 100644 index f2af8aa..0000000 Binary files a/IPAs/com.remarkable.mobile-3.17.0.ipa and /dev/null differ diff --git a/IPAs/com.remarkable.mobile-3.25.0.ipa b/IPAs/com.remarkable.mobile-3.25.0.ipa deleted file mode 100644 index d5dee15..0000000 Binary files a/IPAs/com.remarkable.mobile-3.25.0.ipa and /dev/null differ diff --git a/info.md b/info.md deleted file mode 100644 index 78f2510..0000000 --- a/info.md +++ /dev/null @@ -1,218 +0,0 @@ - -# Technical Notes: RMHook-iOS - -For project overview and status, see [README.md](README.md). - -## Network Stack Analysis - -### URL Resolution -The app uses a custom function to select the target hostname based on environment flags: - -| Condition | Hostname | -|----------------------------|--------------------------------------------| -| Custom URL set (offset 136)| [custom].tectonic.remarkable.com | -| QA env flag | qa.internal.cloud.remarkable.com | -| Dev env flag | dev.internal.cloud.remarkable.com | -| Stage env flag | stage.internal.cloud.remarkable.com | -| Default (production) | internal.cloud.remarkable.com | - -### network::HttpManager::setupTransaction -Confirmed via RTTI: `network::HttpManager` (vtable entry 14). Source: `xochitl/src/xofm/libs/network/src/http-manager.cpp`. - -#### Flow -- Resolves hostname via custom resolver or appends `.tectonic.remarkable.com` to a base URL -- Constructs a `network::detail::HttpTransaction` object -- Sets up a `network::ReplyReader` for response handling -- Dispatches via a virtual call on a queue/scheduler object -- Logs through `rm.network.http.manager` using obfuscated string literals - -### Networking Library Used -This is a fully custom C++ HTTP client (`xofm/libs/network`), not NSURLSession, CFHTTPMessage, or Qt's QNetworkAccessManager. - -| Layer | Technology | -|-------------- |----------------------------------------------------------------------------| -| TLS/HTTPS | Apple SecureTransport (SSLCreateContext, SSLHandshake, etc.) | -| TCP transport | POSIX BSD sockets (_socket, _connect, _recv/_read, _write/_sendmsg) | -| DNS | _getaddrinfo / _freeaddrinfo | -| Event loop | CFSocket + CFRunLoop integration | -| Async exec | GCD (dispatch_queue_create, dispatch_async) | -| Cert pinning | SecTrustEvaluate, SecTrustSetAnchorCertificates, SecPKCS12Import | -| TLS ALPN | SSLSetALPNProtocols / SSLCopyALPNProtocols (HTTP/2 or gRPC support) | - -The app is Qt-based (QIOS* classes), but the networking layer bypasses Qt entirely. It talks directly to either `*.internal.cloud.remarkable.com` (REST API) or a tectonic-suffixed host with raw TLS sockets. - -## Hook Candidate Analysis -| Hook candidate | Pros | Cons | -| -------------- | ----------------------------------------------------- | ---------------------------------------------------------------------------------------------- | -| sub_1000C81B8 | Single purpose, small, all env variants go through it | Need to handle X8 output ABI | -| sub_1000AA31C | (setupTransaction) High level | Complex function, 0x738 bytes, hard to isolate the host field | -| sub_10180B1BC | (string ctor) Leaf function | Called from many places, need to filter callers | -| sub_1000AB478 | (URL builder for tectonic) | Catches tectonic-path specifically Only covers one of the two code paths (the custom-URL path) | - -```cpp -void __usercall sub_1000C81B8(__int64 a1@, _QWORD *a2@) -{ - unsigned int *v4; // x19 - __int64 v5; // x21 - __int64 v6; // x22 - unsigned int v7; // w8 - unsigned int v8; // w8 - unsigned int v9; // w8 - unsigned int *v10; // x8 - unsigned int v11; // w9 - unsigned int v12; // w9 - void *v13[2]; // [xsp+8h] [xbp-58h] BYREF - __int64 v14; // [xsp+18h] [xbp-48h] - __int128 v15; // [xsp+20h] [xbp-40h] BYREF - __int64 v16; // [xsp+30h] [xbp-30h] - - if ( *(_BYTE *)(a1 + 136) ) - { - (*(void (__fastcall **)(void **__return_ptr))(**(_QWORD **)(a1 + 128) + 112LL))(v13); - } - else - { - v13[0] = 0; - v13[1] = 0; - v14 = 0; - } - v4 = *(unsigned int **)(a1 + 240); - v5 = *(_QWORD *)(a1 + 248); - v6 = *(_QWORD *)(a1 + 256); - if ( v4 ) - { - do - v7 = __ldaxr(v4); - while ( __stlxr(v7 + 1, v4) ); - } - if ( v14 ) - { - *(_QWORD *)&v15 = v13; - *((_QWORD *)&v15 + 1) = ".tectonic.remarkable.com"; - sub_1000AB478(a2, &v15); - if ( !v4 ) - goto LABEL_25; - goto LABEL_22; - } - if ( v6 == qword_1028F6140 && (unsigned int)sub_10180246C(v6, v5, v6, *((_QWORD *)&xmmword_1028F6130 + 1)) ) - goto LABEL_20; - if ( v6 == qword_1028F6158 && (unsigned int)sub_10180246C(v6, v5, v6, qword_1028F6150) ) - { - sub_10180B1BC(&v15, 35, "stage.internal.cloud.remarkable.com"); - goto LABEL_21; - } - if ( v6 == qword_1028F6188 && (unsigned int)sub_10180246C(v6, v5, v6, qword_1028F6180) ) - { - sub_10180B1BC(&v15, 33, "dev.internal.cloud.remarkable.com"); - goto LABEL_21; - } - if ( v6 != qword_1028F6170 || !(unsigned int)sub_10180246C(v6, v5, v6, qword_1028F6168) ) -LABEL_20: - sub_10180B1BC(&v15, 29, "internal.cloud.remarkable.com"); - else - sub_10180B1BC(&v15, 32, "qa.internal.cloud.remarkable.com"); -LABEL_21: - *(_OWORD *)a2 = v15; - a2[2] = v16; - if ( !v4 ) - goto LABEL_25; - do - { -LABEL_22: - v8 = __ldaxr(v4); - v9 = v8 - 1; - } - while ( __stlxr(v9, v4) ); - if ( !v9 ) - j__free(v4); -LABEL_25: - v10 = (unsigned int *)v13[0]; - if ( v13[0] ) - { - do - { - v11 = __ldaxr(v10); - v12 = v11 - 1; - } - while ( __stlxr(v12, v10) ); - if ( !v12 ) - j__free(v13[0]); - } -} -``` - -```cpp -void __usercall sub_1000C8444(_QWORD *a1@, __int64 a2@) -{ - unsigned int *v3; // x19 - __int64 v4; // x21 - __int64 v5; // x22 - unsigned int v6; // w8 - unsigned int v7; // w8 - unsigned int v8; // w8 - __int128 v9; // [xsp+0h] [xbp-40h] BYREF - __int64 v10; // [xsp+10h] [xbp-30h] - - v3 = (unsigned int *)a1[30]; - v4 = a1[31]; - v5 = a1[32]; - if ( v3 ) - { - do - v6 = __ldaxr(v3); - while ( __stlxr(v6 + 1, v3) ); - } - if ( v5 == qword_1028F6140 && (unsigned int)sub_10180246C(v5, v4, v5, *((_QWORD *)&xmmword_1028F6130 + 1)) ) - goto LABEL_14; - if ( v5 == qword_1028F6158 && (unsigned int)sub_10180246C(v5, v4, v5, qword_1028F6150) ) - { - sub_10180B1BC(25, (__int64)"staging.my.remarkable.com", &v9); - goto LABEL_15; - } - if ( v5 == qword_1028F6188 && (unsigned int)sub_10180246C(v5, v4, v5, qword_1028F6180) ) - { - sub_10180B1BC(29, (__int64)"development.my.remarkable.com", &v9); - goto LABEL_15; - } - if ( v5 != qword_1028F6170 || !(unsigned int)sub_10180246C(v5, v4, v5, qword_1028F6168) ) -LABEL_14: - sub_10180B1BC(17, (__int64)"my.remarkable.com", &v9); - else - sub_10180B1BC(20, (__int64)"qa.my.remarkable.com", &v9); -LABEL_15: - *(_OWORD *)a2 = v9; - *(_QWORD *)(a2 + 16) = v10; - if ( v3 ) - { - do - { - v7 = __ldaxr(v3); - v8 = v7 - 1; - } - while ( __stlxr(v8, v3) ); - if ( !v8 ) - j__free(v3); - } -} -``` - -## Current Status - -```log -2026-02-22 15:53:36.296954+0100 0x736818 Default 0x0 46841 0 remarkable_mobile: (RMHook.dylib) [RMHook] 'remarkable_mobile' base = 0x102c28000 -2026-02-22 15:53:36.298221+0100 0x736818 Default 0x0 46841 0 remarkable_mobile: (RMHook.dylib) [RMHook] Hooked sub_1000C81B8 @ 0x102cf01b8 (offset 0xc81b8) -2026-02-22 15:53:36.298343+0100 0x736818 Default 0x0 46841 0 remarkable_mobile: (RMHook.dylib) [RMHook] Hooked sub_1000C8444 @ 0x102cf0444 (offset 0xc8444) -2026-02-22 15:53:36.416893+0100 0x736818 Default 0x0 46841 0 remarkable_mobile: (RMHook.dylib) [RMHook] >>> sub_1000C8444 ENTER a1=0x12070d5e0 x8(out)=0x16d1d2bf0 -2026-02-22 15:53:36.416985+0100 0x736818 Default 0x0 46841 0 remarkable_mobile: (RMHook.dylib) [RMHook] <<< sub_1000C8444 RETURN original (17 chars) = "my.remarkable.com" -2026-02-22 15:53:36.417039+0100 0x736818 Default 0x0 46841 0 remarkable_mobile: (RMHook.dylib) [RMHook] <<< sub_1000C8444 patched → "rm.noh.am" -2026-02-22 15:53:36.466471+0100 0x736818 Default 0x0 46841 0 remarkable_mobile: (RMHook.dylib) [RMHook] >>> sub_1000C8444 ENTER a1=0x12070d5e0 x8(out)=0x16d1d34b0 -2026-02-22 15:53:36.466558+0100 0x736818 Default 0x0 46841 0 remarkable_mobile: (RMHook.dylib) [RMHook] <<< sub_1000C8444 RETURN original (17 chars) = "my.remarkable.com" -2026-02-22 15:53:36.466604+0100 0x736818 Default 0x0 46841 0 remarkable_mobile: (RMHook.dylib) [RMHook] <<< sub_1000C8444 patched → "rm.noh.am" -2026-02-22 15:53:47.331051+0100 0x736818 Default 0x0 46841 0 remarkable_mobile: (RMHook.dylib) [RMHook] >>> sub_1000C81B8 ENTER a1=0x12070d5e0 x8(out)=0x16d1497e0 -2026-02-22 15:53:47.331226+0100 0x736818 Default 0x0 46841 0 remarkable_mobile: (RMHook.dylib) [RMHook] <<< sub_1000C81B8 RETURN original (29 chars) = "internal.cloud.remarkable.com" -2026-02-22 15:53:47.331350+0100 0x736818 Default 0x0 46841 0 remarkable_mobile: (RMHook.dylib) [RMHook] <<< patched → "rm.noh.am" -``` - -The above log shows that both hooks are successfully intercepting the hostname resolution and replacing it with "rm.noh.am". However, the app still fails to fetch documents.. -![docs/settings.png](docs/settings.png) -![docs/unsynced.png](docs/unsynced.png) \ No newline at end of file diff --git a/src/Makefile b/src/Makefile index 40f513e..323eafc 100644 --- a/src/Makefile +++ b/src/Makefile @@ -6,8 +6,13 @@ include $(THEOS)/makefiles/common.mk TWEAK_NAME = RMHook -RMHook_FILES = Tweak.x -RMHook_CFLAGS = -fobjc-arc +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 include $(THEOS_MAKE_PATH)/tweak.mk diff --git a/src/Tweak.x b/src/Tweak.x deleted file mode 100644 index dbbe0ef..0000000 --- a/src/Tweak.x +++ /dev/null @@ -1,169 +0,0 @@ -// RMHook-iOS Tweak (POC) -#import -#import -#import -#import -#import - -// Target binary name inside the IPA -#define TARGET_MODULE "remarkable_mobile" -#define IDA_BASE 0x100000000ULL - -// --- sub_1000C81B8 --- -// void __usercall sub_1000C81B8(__int64 a1@, _QWORD *a2@) -// X8 is ARM64's indirect-result register (not a normal parameter). -// We use a naked trampoline to handle this. -static void (*orig_sub_1000C81B8)(int64_t a1); - -// Pre-call logger -__attribute__((used)) -static void rmhook_pre(int64_t a1, uint64_t *out) { - NSLog(@"[RMHook] >>> sub_1000C81B8 ENTER a1=0x%llx x8(out)=%p", - (unsigned long long)a1, (void *)out); -} - -// Post-call logger and patcher -__attribute__((used)) -static void rmhook_post(uint64_t *out) { - if (!out) { - NSLog(@"[RMHook] <<< sub_1000C81B8 RETURN (out=NULL)"); - return; - } - uint64_t base_ptr = out[0]; - uint64_t data_ptr = out[1]; - uint64_t char_count = out[2]; - if (data_ptr && char_count > 0 && char_count <= 4096) { - NSString *orig = [[NSString alloc] initWithBytes:(const void *)(uintptr_t)data_ptr - length:(NSUInteger)(char_count * 2) - encoding:NSUTF16LittleEndianStringEncoding]; - NSLog(@"[RMHook] <<< sub_1000C81B8 RETURN original (%llu chars) = \"%@\"", - char_count, orig ?: @""); - } - // Patch: replace returned string with custom value - NSString *replacement = @"rm.noh.am"; - NSUInteger newCount = [replacement length]; - if (data_ptr && newCount <= char_count) { - NSData *utf16 = [replacement dataUsingEncoding:NSUTF16LittleEndianStringEncoding]; - memcpy((void *)(uintptr_t)data_ptr, utf16.bytes, utf16.length); - out[2] = newCount; - *(uint64_t *)(uintptr_t)(base_ptr + 8) = newCount; - NSLog(@"[RMHook] <<< patched → \"%@\"", replacement); - } else { - NSLog(@"[RMHook] <<< patch skipped (replacement too long or no buffer)"); - } -} - -// Naked trampoline for sub_1000C81B8 -__attribute__((naked)) -static void hook_sub_1000C81B8(void) { - __asm__ volatile( - "sub sp, sp, #32 \n" - "stp x29, x30, [sp, #16] \n" - "add x29, sp, #16 \n" - "stp x0, x8, [sp, #0] \n" - "mov x1, x8 \n" - "bl _rmhook_pre \n" - "ldp x0, x8, [sp, #0] \n" - "adrp x9, _orig_sub_1000C81B8@PAGE \n" - "ldr x9, [x9, _orig_sub_1000C81B8@PAGEOFF] \n" - "blr x9 \n" - "ldr x0, [sp, #8] \n" - "bl _rmhook_post \n" - "ldp x29, x30, [sp, #16] \n" - "add sp, sp, #32 \n" - "ret \n" - ); -} - -// --- sub_1000C8444 --- -// void __usercall sub_1000C8444(_QWORD *a1@, __int64 a2@) -static void (*orig_sub_1000C8444)(int64_t a1); - -__attribute__((used)) -static void rmhook_pre_8444(int64_t a1, uint64_t *out) { - NSLog(@"[RMHook] >>> sub_1000C8444 ENTER a1=0x%llx x8(out)=%p", - (unsigned long long)a1, (void *)out); -} - -__attribute__((used)) -static void rmhook_post_8444(uint64_t *out) { - if (!out) { - NSLog(@"[RMHook] <<< sub_1000C8444 RETURN (out=NULL)"); - return; - } - uint64_t base_ptr = out[0]; - uint64_t data_ptr = out[1]; - uint64_t char_count = out[2]; - if (data_ptr && char_count > 0 && char_count <= 4096) { - NSString *orig = [[NSString alloc] initWithBytes:(const void *)(uintptr_t)data_ptr - length:(NSUInteger)(char_count * 2) - encoding:NSUTF16LittleEndianStringEncoding]; - NSLog(@"[RMHook] <<< sub_1000C8444 RETURN original (%llu chars) = \"%@\"", - char_count, orig ?: @""); - } - NSString *replacement = @"rm.noh.am"; - NSUInteger newCount = [replacement length]; - if (data_ptr && newCount <= char_count) { - NSData *utf16 = [replacement dataUsingEncoding:NSUTF16LittleEndianStringEncoding]; - memcpy((void *)(uintptr_t)data_ptr, utf16.bytes, utf16.length); - out[2] = newCount; - *(uint64_t *)(uintptr_t)(base_ptr + 8) = newCount; - NSLog(@"[RMHook] <<< sub_1000C8444 patched → \"%@\"", replacement); - } else { - NSLog(@"[RMHook] <<< sub_1000C8444 patch skipped (replacement too long or no buffer)"); - } -} - -__attribute__((naked)) -static void hook_sub_1000C8444(void) { - __asm__ volatile( - "sub sp, sp, #32 \n" - "stp x29, x30, [sp, #16] \n" - "add x29, sp, #16 \n" - "stp x0, x8, [sp, #0] \n" - "mov x1, x8 \n" - "bl _rmhook_pre_8444 \n" - "ldp x0, x8, [sp, #0] \n" - "adrp x9, _orig_sub_1000C8444@PAGE \n" - "ldr x9, [x9, _orig_sub_1000C8444@PAGEOFF] \n" - "blr x9 \n" - "ldr x0, [sp, #8] \n" - "bl _rmhook_post_8444 \n" - "ldp x29, x30, [sp, #16] \n" - "add sp, sp, #32 \n" - "ret \n" - ); -} - -// Helpers -static uintptr_t findModuleBase(const char *moduleName) { - uint32_t count = _dyld_image_count(); - for (uint32_t i = 0; i < count; i++) { - const char *name = _dyld_get_image_name(i); - if (name && strstr(name, moduleName)) - return (uintptr_t)_dyld_get_image_header(i); - } - return 0; -} - -// Constructor -%ctor { - @autoreleasepool { - uintptr_t base = findModuleBase(TARGET_MODULE); - if (!base) { - NSLog(@"[RMHook] Module '%s' not found – tweak inactive.", TARGET_MODULE); - return; - } - NSLog(@"[RMHook] '%s' base = 0x%lx", TARGET_MODULE, (unsigned long)base); - - uintptr_t offset = 0x1000C81B8ULL - IDA_BASE; - uintptr_t addr = base + offset; - MSHookFunction((void *)addr, (void *)hook_sub_1000C81B8, (void **)&orig_sub_1000C81B8); - NSLog(@"[RMHook] Hooked sub_1000C81B8 @ 0x%lx (offset 0x%lx)", (unsigned long)addr, (unsigned long)offset); - - uintptr_t offset2 = 0x1000C8444ULL - IDA_BASE; - uintptr_t addr2 = base + offset2; - MSHookFunction((void *)addr2, (void *)hook_sub_1000C8444, (void **)&orig_sub_1000C8444); - NSLog(@"[RMHook] Hooked sub_1000C8444 @ 0x%lx (offset 0x%lx)", (unsigned long)addr2, (unsigned long)offset2); - } -} diff --git a/src/Tweak.xm b/src/Tweak.xm new file mode 100644 index 0000000..15e679e --- /dev/null +++ b/src/Tweak.xm @@ -0,0 +1,81 @@ +#import +#import +#import +#import +#import + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define TARGET_MODULE "remarkable_mobile" +#define IDA_BASE 0x100000000 + +// __ZN21QNetworkAccessManager13createRequestENS_9OperationERK15QNetworkRequestP9QIODevice +#define QtNetworkAccessManager_createRequest 0x1017FB9F4 // sub_1017FB9F4 + +// __ZN10QWebSocket4openERK15QNetworkRequest +# define QtWebSocket_open 0x100526A18 // sub_100526A18 + + +// 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); + +void *hook_createRequest(void *a1, int a2, const void *a3, void *a4) { + NSLog(@"[RMHook-iOS] createRequest called"); + return orig_createRequest(a1, a2, a3, a4); +} + +// 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); + +void hook_open(void *this_ptr, const void *a2, const void *a3) { + NSLog(@"[RMHook-iOS] QWebSocket::open called"); + orig_open(this_ptr, a2, a3); +} + + +static uintptr_t findModuleBase(const char *moduleName) { + uint32_t count = _dyld_image_count(); + for (uint32_t i = 0; i < count; i++) { + const char *name = _dyld_get_image_name(i); + if (name && strstr(name, moduleName)) + return (uintptr_t)_dyld_get_image_header(i); + } + return 0; +} + +// Constructor +%ctor { + @autoreleasepool { + uintptr_t base = findModuleBase(TARGET_MODULE); + if (!base) { + NSLog(@"[RMHook-iOS] Module '%s' not found, tweak inactive.", TARGET_MODULE); + return; + } + NSLog(@"[RMHook-iOS] '%s' base = 0x%lx", TARGET_MODULE, (unsigned long)base); + + uintptr_t offset = QtNetworkAccessManager_createRequest - IDA_BASE; + uintptr_t addr = base + offset; + MSHookFunction((void *)addr, (void *)hook_createRequest, (void **)&orig_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); + NSLog(@"[RMHook-iOS] Hooked QtWebSocket_open @ 0x%lx (offset 0x%lx)", (unsigned long)addr2, (unsigned long)offset2); + } +}