Compare commits
7 Commits
3e77fe422b
...
v3.25.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
77cff94826 | ||
|
|
ed6fbd9e38 | ||
|
|
6aa11a2345 | ||
|
|
39687a43d4 | ||
|
|
fb3f8fbc50 | ||
|
|
33f9772c90 | ||
|
|
bb174602ba |
1
.gitignore
vendored
@@ -2,3 +2,4 @@
|
|||||||
/src/packages
|
/src/packages
|
||||||
.DS_Store
|
.DS_Store
|
||||||
/rev
|
/rev
|
||||||
|
/patched
|
||||||
|
|||||||
21
LICENSE
Normal file
@@ -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.
|
||||||
145
README.md
@@ -1,32 +1,143 @@
|
|||||||
# RMHook-iOS
|
# RMHook-iOS
|
||||||
|
|
||||||
This project is an early proof of concept for hooking or modifying the reMarkable iOS app. It currently does not work, possibly due to SSL/TLS issues in the custom networking stack used by the app.
|
A dynamic library injection tweak for the reMarkable iOS app, enabling connection to self-hosted [rmfakecloud](https://github.com/ddvk/rmfakecloud) servers.
|
||||||
|
|
||||||
## Project Structure
|
## Overview
|
||||||
- `src/` – Contains the tweak source code and configuration files.
|
|
||||||
- `IPAs/` – Example reMarkable app IPA files for analysis.
|
|
||||||
|
|
||||||
## Build & Usage
|
RMHook-iOS hooks into the reMarkable iOS app's network layer to redirect API calls from reMarkable's official cloud services to your own [rmfakecloud](https://github.com/ddvk/rmfakecloud) server. This allows you to maintain full control over your documents and data on your mobile device.
|
||||||
|
|
||||||
### Build the Tweak
|
Looking for the Desktop versions? Check out:
|
||||||
```sh
|
- **[RMHook](https://github.com/NohamR/RMHook)**: For macOS Desktop
|
||||||
cd src && make clean && make package THEOS_PACKAGE_SCHEME=rootless
|
- **[RMHook-Win](https://github.com/NohamR/RMHook-Win)**: For Windows Desktop
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- Network request interception and redirection
|
||||||
|
- WebSocket connection patching
|
||||||
|
- In-app configuration UI to set the custom host and port
|
||||||
|
- Built specifically for iOS rootless environments
|
||||||
|
|
||||||
|
## Compatibility
|
||||||
|
|
||||||
|
**Tested and working on:**
|
||||||
|
- reMarkable iOS v3.25.0
|
||||||
|
- reMarkable iOS v3.27.1
|
||||||
|
|
||||||
|
<p align="center">
|
||||||
|
<img src="docs/3.25.0.png" width="45%" />
|
||||||
|
<img src="docs/syncing.png" width="45%" />
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p align="center">
|
||||||
|
<img src="docs/3.27.1.png" width="45%" />
|
||||||
|
<img src="docs/acc.png" width="45%" />
|
||||||
|
</p>
|
||||||
|
|
||||||
|
## Installation and usage
|
||||||
|
|
||||||
|
⚠️ **For legal reasons, this repository does not include the reMarkable app.** However, a pre-built `.deb` package and a pre-patched `.ipa` are available in the [Releases](https://github.com/NohamR/RMHook-iOS/releases) section.
|
||||||
|
|
||||||
|
### Installation Options
|
||||||
|
|
||||||
|
#### Option 1: Sideloading the Patched IPA
|
||||||
|
If you do not have a jailbroken device, you can use sideloading tools like [AltStore](https://altstore.io/), [Sideloadly](https://sideloadly.io/), or TrollStore to install the pre-patched `.ipa` from the Releases page.
|
||||||
|
|
||||||
|
#### Option 2: Jailbroken Installation (.deb)
|
||||||
|
If you are on a rootless jailbreak, you can install the `.deb` package using Sileo, Zebra, or from the terminal:
|
||||||
|
```bash
|
||||||
|
dpkg -i xyz.noham.rmhook_version_iphoneos-arm64.deb
|
||||||
```
|
```
|
||||||
|
|
||||||
### Patch the IPA with the Tweak
|
#### Option 3: Manual Patching
|
||||||
```sh
|
You can patch your own reMarkable `.ipa` using [Cyan](https://github.com/asdfzxcvbn/cyan):
|
||||||
|
```bash
|
||||||
cyan -i reMarkable.ipa \
|
cyan -i reMarkable.ipa \
|
||||||
-o reMarkable_patched.ipa \
|
-o reMarkable_patched.ipa \
|
||||||
-f xyz.noham.rmhook_0.0.1-1+debug_iphoneos-arm64.deb -u
|
-f xyz.noham.rmhook_version_iphoneos-arm64.deb -u
|
||||||
```
|
```
|
||||||
|
|
||||||
### Debug Logging
|
### Setup & Configuration
|
||||||
```sh
|
|
||||||
log stream | grep RMHook
|
Upon launching the app for the first time, you will be prompted to enter your `rmfakecloud` host and port via an in-app alert dialog.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## Building
|
||||||
|
|
||||||
|
### 1. Prerequisites (Qt for iOS)
|
||||||
|
|
||||||
|
The tweak statically links against iOS Qt libraries and uses customized C++17 flags. You will need to install the Qt headers and libraries locally using the `aqtinstall` Python tool.
|
||||||
|
|
||||||
|
Create a Python environment and install `aqtinstall`:
|
||||||
|
```bash
|
||||||
|
python3 -m venv aqt_venv
|
||||||
|
source aqt_venv/bin/activate
|
||||||
|
pip install aqtinstall
|
||||||
```
|
```
|
||||||
|
|
||||||
## Technical Notes
|
Then, download the required Qt versions for iOS to your home directory (e.g., `6.8.2` for the 3.25.0 target and `6.10.0` for the 3.27.1 target):
|
||||||
See [info.md](info.md) for detailed analysis of the app's networking stack and reverse engineering notes.
|
```bash
|
||||||
|
# Install Qt 6.8.2 for iOS (used for v3.25.0)
|
||||||
|
aqt install-qt mac ios 6.8.2 -m qtwebsockets --outputdir ~/Qt
|
||||||
|
|
||||||
|
# Install Qt 6.10.0 for iOS (used for v3.27.1)
|
||||||
|
aqt install-qt mac ios 6.10.0 -m qtwebsockets --outputdir ~/Qt
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Finding Specific App Function Offsets
|
||||||
|
|
||||||
|
Because iOS apps are stripped and standard symbol hooking isn't always viable on heavily optimized C++ applications, RMHook-iOS hooks specific memory offsets within the binary.
|
||||||
|
|
||||||
|
If you're compiling for a new version of the reMarkable app, you must disassemble the app binary (e.g., using IDA Pro) to find the correct static memory addresses (offsets) for functions like `QNetworkAccessManager::createRequest` and `QWebSocket::open`.
|
||||||
|
|
||||||
|
Once found, you will add the macros to `src/Tweak.xm` using a new `#elif NEW_VERSION` block to define these offsets, allowing the dynamic hook resolver to intercept them at runtime.
|
||||||
|
|
||||||
|
### 3. Clone and Compile
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/NohamR/RMHook-iOS.git
|
||||||
|
cd RMHook-iOS
|
||||||
|
```
|
||||||
|
|
||||||
|
Use `script/build.sh` to compile the tweak for a specific version. Make sure to run it from inside the `src` directory or point directly to the script.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd src
|
||||||
|
|
||||||
|
# Build for version 3.25.0 in development mode (default)
|
||||||
|
../script/build.sh 3.25.0
|
||||||
|
|
||||||
|
# Build for version 3.27.1 in development mode
|
||||||
|
../script/build.sh 3.27.1 dev
|
||||||
|
|
||||||
|
# Build in release mode (updates the control file package version and builds final)
|
||||||
|
../script/build.sh 3.27.1 release
|
||||||
|
```
|
||||||
|
|
||||||
|
## Debugging
|
||||||
|
To debug the tweak, you can use `lldb` to attach to the `remarkable_mobile` process on your device.
|
||||||
|
You can stream the device logs using `idevicesyslog` from the `libimobiledevice` suite to see the output from your hooks and any potential errors.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
idevicesyslog | grep 'remarkable_mobile' | grep 'RMHook'
|
||||||
|
```
|
||||||
|
|
||||||
|
## How it works
|
||||||
|
RMHook-iOS uses Memory Hooking (`MSHookFunction`) via Theos to patch Qt framework functions statically linked inside the iOS app:
|
||||||
|
1. **QNetworkAccessManager::createRequest** - Intercepts HTTP/HTTPS requests
|
||||||
|
2. **QWebSocket::open** - Patches WebSocket connections
|
||||||
|
|
||||||
|
When the app attempts to connect to reMarkable's servers (e.g., `internal.cloud.remarkable.com`), the hooks redirect these requests to your configured host and port, which is saved persistently on the device via `NSUserDefaults` and presented via `UIKit` alerts.
|
||||||
|
|
||||||
|
## Credits
|
||||||
|
- xovi-rmfakecloud: [asivery/xovi-rmfakecloud](https://github.com/asivery/xovi-rmfakecloud) - Original hooking information
|
||||||
|
- rm-xovi-extensions: [asivery/rm-xovi-extensions](https://github.com/asivery/rm-xovi-extensions) - Extension framework for reMarkable
|
||||||
|
- rmfakecloud: [ddvk/rmfakecloud](https://github.com/ddvk/rmfakecloud) - Self-hosted reMarkable cloud
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details.
|
||||||
|
|
||||||
## Disclaimer
|
## Disclaimer
|
||||||
This project is for educational and research purposes only. It is not affiliated with or endorsed by reMarkable.
|
|
||||||
|
This project is not affiliated with, endorsed by, or sponsored by reMarkable AS. Use at your own risk. This tool modifies the reMarkable iOS application and may violate the application's terms of service.
|
||||||
|
|||||||
BIN
docs/3.25.0.png
Normal file
|
After Width: | Height: | Size: 67 KiB |
BIN
docs/3.27.1.png
Normal file
|
After Width: | Height: | Size: 97 KiB |
BIN
docs/acc.png
Normal file
|
After Width: | Height: | Size: 135 KiB |
|
Before Width: | Height: | Size: 247 KiB |
BIN
docs/setup.png
Normal file
|
After Width: | Height: | Size: 494 KiB |
BIN
docs/syncing.png
Normal file
|
After Width: | Height: | Size: 80 KiB |
|
Before Width: | Height: | Size: 220 KiB |
218
info.md
@@ -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@<X0>, _QWORD *a2@<X8>)
|
|
||||||
{
|
|
||||||
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@<X0>, __int64 a2@<X8>)
|
|
||||||
{
|
|
||||||
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..
|
|
||||||

|
|
||||||

|
|
||||||
43
script/build.sh
Executable file
@@ -0,0 +1,43 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Default to 3.25.0 if no argument is provided
|
||||||
|
VERSION=${1:-3.25.0}
|
||||||
|
|
||||||
|
if [ "$VERSION" == "3.17.0" ]; then
|
||||||
|
MACRO="-DV3_17_0=1"
|
||||||
|
QT_VERSION="6.5.3"
|
||||||
|
TARGET="iphone:latest:14.0"
|
||||||
|
elif [ "$VERSION" == "3.25.0" ]; then
|
||||||
|
MACRO="-DV3_25_0=1"
|
||||||
|
QT_VERSION="6.8.2"
|
||||||
|
TARGET="iphone:latest:16.0"
|
||||||
|
elif [ "$VERSION" == "3.26.0" ]; then
|
||||||
|
MACRO="-DV3_26_0=1"
|
||||||
|
QT_VERSION="6.8.2"
|
||||||
|
TARGET="iphone:latest:17.0"
|
||||||
|
elif [ "$VERSION" == "3.27.0" ]; then
|
||||||
|
MACRO="-DV3_27_0=1"
|
||||||
|
QT_VERSION="6.10.0"
|
||||||
|
TARGET="iphone:latest:17.0"
|
||||||
|
elif [ "$VERSION" == "3.27.1" ]; then
|
||||||
|
MACRO="-DV3_27_1=1"
|
||||||
|
QT_VERSION="6.10.0"
|
||||||
|
TARGET="iphone:latest:17.0"
|
||||||
|
else
|
||||||
|
echo "Error: Unknown version '$VERSION'. Supported versions are: 3.17.0, 3.25.0, 3.26.0, 3.27.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" TARGET="$TARGET"
|
||||||
|
else
|
||||||
|
make package THEOS_PACKAGE_SCHEME=rootless DEBUG=0 RM_VERSION_FLAG="$MACRO" QT_VERSION="$QT_VERSION" TARGET="$TARGET"
|
||||||
|
fi
|
||||||
17
src/Config.h
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
#import <Foundation/Foundation.h>
|
||||||
|
#import <UIKit/UIKit.h>
|
||||||
|
|
||||||
|
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
|
||||||
86
src/Config.mm
Normal file
@@ -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];
|
||||||
|
});
|
||||||
|
}
|
||||||
17
src/Makefile
@@ -1,13 +1,20 @@
|
|||||||
TARGET = iphone:latest:14.0
|
TARGET = $(target)
|
||||||
INSTALL_TARGET_PROCESSES = remarkable_mobile
|
INSTALL_TARGET_PROCESSES = remarkable_mobile
|
||||||
ARCHS = arm64 arm64e
|
ARCHS = arm64
|
||||||
|
|
||||||
include $(THEOS)/makefiles/common.mk
|
include $(THEOS)/makefiles/common.mk
|
||||||
|
|
||||||
TWEAK_NAME = RMHook
|
TWEAK_NAME = RMHook
|
||||||
|
|
||||||
RMHook_FILES = Tweak.x
|
QT_VERSION ?= 6.8.2
|
||||||
RMHook_CFLAGS = -fobjc-arc
|
|
||||||
RMHook_FRAMEWORKS = Foundation
|
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
|
include $(THEOS_MAKE_PATH)/tweak.mk
|
||||||
|
|||||||
169
src/Tweak.x
@@ -1,169 +0,0 @@
|
|||||||
// RMHook-iOS Tweak (POC)
|
|
||||||
#import <substrate.h>
|
|
||||||
#import <Foundation/Foundation.h>
|
|
||||||
#import <mach-o/dyld.h>
|
|
||||||
#import <string.h>
|
|
||||||
#import <stdint.h>
|
|
||||||
|
|
||||||
// Target binary name inside the IPA
|
|
||||||
#define TARGET_MODULE "remarkable_mobile"
|
|
||||||
#define IDA_BASE 0x100000000ULL
|
|
||||||
|
|
||||||
// --- sub_1000C81B8 ---
|
|
||||||
// void __usercall sub_1000C81B8(__int64 a1@<X0>, _QWORD *a2@<X8>)
|
|
||||||
// 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 ?: @"<decode error>");
|
|
||||||
}
|
|
||||||
// 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@<X0>, __int64 a2@<X8>)
|
|
||||||
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 ?: @"<decode error>");
|
|
||||||
}
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
202
src/Tweak.xm
Normal file
@@ -0,0 +1,202 @@
|
|||||||
|
#import <substrate.h>
|
||||||
|
#import <Foundation/Foundation.h>
|
||||||
|
#import <mach-o/dyld.h>
|
||||||
|
#import <string.h>
|
||||||
|
#import <stdint.h>
|
||||||
|
|
||||||
|
#include <QtNetwork/QNetworkAccessManager>
|
||||||
|
#include <QtNetwork/QNetworkRequest>
|
||||||
|
#include <QtNetwork/QNetworkReply>
|
||||||
|
#include <QtCore/QDebug>
|
||||||
|
#include <QtCore/QIODevice>
|
||||||
|
#include <QtCore/QUrl>
|
||||||
|
#include <QtCore/QString>
|
||||||
|
#include <QtCore/Qt>
|
||||||
|
#include <QtWebSockets/QWebSocket>
|
||||||
|
#include <QtCore/QSettings>
|
||||||
|
#include <QtCore/QVariant>
|
||||||
|
#include <QtCore/QAnyStringView>
|
||||||
|
|
||||||
|
#import <UIKit/UIKit.h>
|
||||||
|
|
||||||
|
#define TARGET_MODULE "remarkable_mobile"
|
||||||
|
#define IDA_BASE 0x100000000
|
||||||
|
|
||||||
|
|
||||||
|
// __ZN21QNetworkAccessManager13createRequestENS_9OperationERK15QNetworkRequestP9QIODevice
|
||||||
|
// Search for strings: "local+http"; "data"; "qrc"; "unix"; "https";
|
||||||
|
// last function of the vtable of QNetworkAccessManager ("21QNetworkAccessManager")
|
||||||
|
#if V3_17_0
|
||||||
|
#define QtNetworkAccessManager_createRequest 0x101673958 // sub_101673958
|
||||||
|
#elif V3_25_0
|
||||||
|
#define QtNetworkAccessManager_createRequest 0x1017FB9F4 // sub_1017FB9F4
|
||||||
|
#elif V3_26_0
|
||||||
|
#define QtNetworkAccessManager_createRequest 0x1018236C0 // sub_1018236C0
|
||||||
|
#elif V3_27_0
|
||||||
|
#define QtNetworkAccessManager_createRequest 0x101924584 // sub_101924584
|
||||||
|
#elif V3_27_1
|
||||||
|
#define QtNetworkAccessManager_createRequest 0x10192472C // sub_10192472C
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// __ZN10QWebSocket4openERK15QNetworkRequest
|
||||||
|
// Search for strings: "Invalid URL."; "wss"; "SSL Sockets are not supported on this platform."; "w"; "http"; "Unsupported WebSocket scheme: %1"; "Invalid resource name."
|
||||||
|
// Then take 2nd xref
|
||||||
|
// The function is called by a function referencing "openNotificationSocket" 5 times for RM logs and another one being a big WebSocket function
|
||||||
|
#if V3_17_0
|
||||||
|
# define QtWebSocket_open 0x100824C5C // sub_100824C5C
|
||||||
|
#elif V3_25_0
|
||||||
|
# define QtWebSocket_open 0x100526A18 // sub_100526A18
|
||||||
|
#elif V3_26_0
|
||||||
|
# define QtWebSocket_open 0x100566CF8 // sub_100566CF8
|
||||||
|
#elif V3_27_0
|
||||||
|
# define QtWebSocket_open 0x10056487C // sub_10056487C
|
||||||
|
#elif V3_27_1
|
||||||
|
# define QtWebSocket_open 0x100564A24 // sub_100564A24
|
||||||
|
#endif
|
||||||
|
|
||||||
|
|
||||||
|
#import "Config.h"
|
||||||
|
|
||||||
|
static inline QString QStringFromNSStringSafe(NSString* str) {
|
||||||
|
if (!str) return QString();
|
||||||
|
return QString::fromNSString(str);
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
%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);
|
||||||
|
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 *)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 *)hooked_qWebSocket_open, (void **)&original_qWebSocket_open);
|
||||||
|
NSLog(@"[RMHook-iOS] Hooked QtWebSocket_open @ 0x%lx (offset 0x%lx)", (unsigned long)addr2, (unsigned long)offset2);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
Package: xyz.noham.rmhook
|
Package: xyz.noham.rmhook
|
||||||
Name: RMHook
|
Name: RMHook
|
||||||
Version: 0.0.1
|
Version: 3.27.1
|
||||||
Architecture: iphoneos-arm
|
Architecture: iphoneos-arm
|
||||||
Description: An awesome MobileSubstrate tweak!
|
Description: A tweak to hook remarkable mobile app.
|
||||||
Maintainer: NohamR
|
Maintainer: NohamR
|
||||||
Author: NohamR
|
Author: NohamR
|
||||||
Section: Tweaks
|
Section: Tweaks
|
||||||
|
|||||||