Proper implementation

This commit is contained in:
√(noham)²
2026-08-27 16:15:57 +02:00
parent ce1df6f27d
commit 3f1b4fd728
13 changed files with 866 additions and 891 deletions

2
.gitignore vendored
View File

@@ -2,3 +2,5 @@ build/
.DS_Store .DS_Store
/.vscode /.vscode
/research /research
/aqt_venv
docs/rmfakecloud_hooking.md

View File

@@ -28,7 +28,7 @@ set(PROJECT_ROOT_DIR ${CMAKE_CURRENT_SOURCE_DIR})
include_directories( include_directories(
${PROJECT_ROOT_DIR}/src/core ${PROJECT_ROOT_DIR}/src/core
${PROJECT_ROOT_DIR}/src/utils ${PROJECT_ROOT_DIR}/src/utils
${PROJECT_ROOT_DIR}/src/reMarkable ${PROJECT_ROOT_DIR}/src/RMHook
${PROJECT_ROOT_DIR}/libs/include ${PROJECT_ROOT_DIR}/libs/include
) )
@@ -49,7 +49,7 @@ set(LIBS
# Locate Qt libraries # Locate Qt libraries
set(QT_LIB_TARGETS "") 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}) foreach(_qt_root ${_qt_candidate_roots})
if(_qt_root AND EXISTS "${_qt_root}") if(_qt_root AND EXISTS "${_qt_root}")
@@ -78,27 +78,29 @@ set(COMMON_SOURCES
${PROJECT_ROOT_DIR}/src/utils/ResourceUtils.m ${PROJECT_ROOT_DIR}/src/utils/ResourceUtils.m
) )
# reMarkable dylib # RMHook dylib
set(REMARKABLE_SOURCES set(RMHOOK_SOURCES
${PROJECT_ROOT_DIR}/src/reMarkable/reMarkable.m ${PROJECT_ROOT_DIR}/src/RMHook/RMHook.m
${PROJECT_ROOT_DIR}/src/reMarkable/DevHooks.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} ${COMMON_SOURCES}
${REMARKABLE_SOURCES} ${RMHOOK_SOURCES}
) )
# Set source files as Objective-C++ # Set source files as Objective-C++
set_source_files_properties( set_source_files_properties(
${REMARKABLE_SOURCES} ${RMHOOK_SOURCES}
PROPERTIES LANGUAGE OBJCXX PROPERTIES LANGUAGE OBJCXX
) )
set_target_properties(reMarkable PROPERTIES set_target_properties(RMHook PROPERTIES
PREFIX "" PREFIX ""
SUFFIX ".dylib" SUFFIX ".dylib"
OUTPUT_NAME "reMarkable" OUTPUT_NAME "RMHook"
LIBRARY_OUTPUT_DIRECTORY "${PROJECT_ROOT_DIR}/build/dylibs" LIBRARY_OUTPUT_DIRECTORY "${PROJECT_ROOT_DIR}/build/dylibs"
MACOSX_RPATH ON MACOSX_RPATH ON
) )
@@ -107,29 +109,29 @@ add_definitions(-DQT_NO_VERSION_TAGGING)
# Add build mode compile definitions and conditionally add sources # Add build mode compile definitions and conditionally add sources
if(BUILD_MODE_RMFAKECLOUD) 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)") message(STATUS "Build mode: rmfakecloud (cloud redirection)")
endif() endif()
if(BUILD_MODE_QMLREBUILD) 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 # 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) # Add MessageBroker source (needs MOC processing)
target_sources(reMarkable PRIVATE target_sources(RMHook PRIVATE
${PROJECT_ROOT_DIR}/src/utils/MessageBroker.mm ${PROJECT_ROOT_DIR}/src/utils/MessageBroker.mm
) )
message(STATUS "Build mode: qmlrebuild (resource hooking)") message(STATUS "Build mode: qmlrebuild (resource hooking)")
endif() endif()
if(BUILD_MODE_DEV) 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)") message(STATUS "Build mode: dev (reverse engineering)")
endif() endif()
target_link_libraries(reMarkable PRIVATE target_link_libraries(RMHook PRIVATE
${LIBS} ${LIBS}
${QT_LIB_TARGETS} ${QT_LIB_TARGETS}
) )

View File

@@ -17,6 +17,7 @@ RMHook hooks into the reMarkable Desktop app's network layer to redirect API cal
- Network request interception and redirection - Network request interception and redirection
- WebSocket connection patching - WebSocket connection patching
- MQTT URI modification for screen sharing features - MQTT URI modification for screen sharing features
- mTLS support for external access via Cloudflare Tunnel
## Compatibility ## Compatibility
@@ -109,13 +110,44 @@ Example configuration:
## Configuration ## Configuration
The config file (`~/Library/Preferences/rmfakecloud.config`) supports the following keys: The config file (`~/Library/Preferences/rmfakecloud.config`) supports the following keys:
| Key | Type | Default | Description | | Key | Type | Default | Description |
|--------|---------|-------------------|--------------------------------| |-----|------|---------|-------------|
| `host` | String | `example.com` | Your rmfakecloud server host | | `host` | String | `example.com` | Your rmfakecloud server host |
| `port` | Number | `443` | Your rmfakecloud server port | | `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. 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 ## Building
1. **Clone the repository:** 1. **Clone the repository:**
@@ -124,6 +156,14 @@ git clone http://github.com/NohamR/RMHook
cd 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:** 2. **Compile the dylib:**
```bash ```bash
./scripts/build.sh [mode] ./scripts/build.sh [mode]

View File

@@ -29,7 +29,7 @@ case "$BUILD_MODE" in
DYLIB_NAME="all.dylib" DYLIB_NAME="all.dylib"
;; ;;
*) *)
DYLIB_NAME="reMarkable.dylib" DYLIB_NAME="RMHook.dylib"
;; ;;
esac esac
@@ -55,7 +55,7 @@ case "$BUILD_MODE" in
;; ;;
esac esac
echo "🔨 Compiling reMarkable.dylib (mode: $BUILD_MODE)..." echo "🔨 Compiling RMHook.dylib (mode: $BUILD_MODE)..."
echo "📦 Qt path: $QT_PATH" echo "📦 Qt path: $QT_PATH"
# Create build directories if necessary # Create build directories if necessary
@@ -70,12 +70,12 @@ else
cmake $CMAKE_OPTIONS .. cmake $CMAKE_OPTIONS ..
fi fi
make reMarkable make RMHook
if [ $? -eq 0 ]; then if [ $? -eq 0 ]; then
# Rename the produced dylib so each build mode has a distinct file name # Rename the produced dylib so each build mode has a distinct file name
DYLIB_DIR="$PROJECT_DIR/build/dylibs" DYLIB_DIR="$PROJECT_DIR/build/dylibs"
DEFAULT_DYLIB="$DYLIB_DIR/reMarkable.dylib" DEFAULT_DYLIB="$DYLIB_DIR/RMHook.dylib"
TARGET_DYLIB="$DYLIB_DIR/$DYLIB_NAME" TARGET_DYLIB="$DYLIB_DIR/$DYLIB_NAME"
if [ -f "$DEFAULT_DYLIB" ]; then if [ -f "$DEFAULT_DYLIB" ]; then

14
src/RMHook/Config.h Normal file
View File

@@ -0,0 +1,14 @@
#import <Foundation/Foundation.h>
#include <QtCore/QString>
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);

135
src/RMHook/Config.m Normal file
View File

@@ -0,0 +1,135 @@
#import "Config.h"
#import "Logger.h"
#include <QtCore/QString>
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<NSString *> *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<NSString *, id> *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<NSString *, id> *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;
}

View File

@@ -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) { void logMemory(const char *label, void *address, size_t length) {
if (!address) { if (!address) {
NSLogger(@"[reMarkable] %s: (null)", label); NSLogger(@"[RMHook] %s: (null)", label);
return; return;
} }
unsigned char *ptr = (unsigned char *)address; 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++) { for (size_t i = 0; i < length; i++) {
[hexLine appendFormat:@"%02x ", ptr[i]]; [hexLine appendFormat:@"%02x ", ptr[i]];
if ((i + 1) % 16 == 0 && i < length - 1) { if ((i + 1) % 16 == 0 && i < length - 1) {
NSLogger(@"%@", hexLine); 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) { void logStackTrace(const char *label) {
NSLogger(@"[reMarkable] %s - Stack trace:", label); NSLogger(@"[RMHook] %s - Stack trace:", label);
NSArray<NSString *> *callStack = [NSThread callStackSymbols]; NSArray<NSString *> *callStack = [NSThread callStackSymbols];
NSUInteger count = [callStack count]; NSUInteger count = [callStack count];
for (NSUInteger i = 0; i < count; i++) { for (NSUInteger i = 0; i < count; i++) {
NSString *frame = callStack[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, QIODevice *self,
const char *data, const char *data,
int64_t maxSize) { 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"); logStackTrace("QIODevice::write call stack");
logMemory("Data to write", (void *)data, (size_t)(maxSize < 64 ? maxSize : 64)); logMemory("Data to write", (void *)data, (size_t)(maxSize < 64 ? maxSize : 64));
if (original_qIODevice_write) { if (original_qIODevice_write) {
ssize_t result = original_qIODevice_write(self, data, maxSize); 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; return result;
} }
NSLogger(@"[reMarkable] WARNING: Original QIODevice::write not available, returning 0"); NSLogger(@"[RMHook] WARNING: Original QIODevice::write not available, returning 0");
return 0; return 0;
} }
extern "C" int64_t hooked_function_at_0x100011790(uint64_t *a1) { extern "C" int64_t hooked_function_at_0x100011790(uint64_t *a1) {
NSLogger(@"[reMarkable] Hook at 0x100011790 called!"); NSLogger(@"[RMHook] Hook at 0x100011790 called!");
NSLogger(@"[reMarkable] a1 = %p", a1); NSLogger(@"[RMHook] a1 = %p", a1);
if (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);
logMemory("Memory at *a1", (void *)(*a1), 64); logMemory("Memory at *a1", (void *)(*a1), 64);
} else { } else {
NSLogger(@"[reMarkable] a1 is NULL"); NSLogger(@"[RMHook] a1 is NULL");
} }
if (original_function_at_0x100011790) { if (original_function_at_0x100011790) {
int64_t result = original_function_at_0x100011790(a1); 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; 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; 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) // - a4: stored at offset +0x18 (likely URI or type info pointer)
// - a5: ExternalRefCountData* for shared pointer ref counting // - a5: ExternalRefCountData* for shared pointer ref counting
NSLogger(@"[reMarkable] ========================================"); NSLogger(@"[RMHook] ========================================");
NSLogger(@"[reMarkable] Hook at 0x100011CE0 (QML Type Registration)"); NSLogger(@"[RMHook] Hook at 0x100011CE0 (QML Type Registration)");
NSLogger(@"[reMarkable] ========================================"); 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 a2_low = (uint16_t)(uintptr_t)a2;
uint16_t combined_v17 = (a2_low << 8) | a3; uint16_t combined_v17 = (a2_low << 8) | a3;
NSLogger(@"[reMarkable] a2 (raw) = %p (0x%llx)", a2, (unsigned long long)(uintptr_t)a2); NSLogger(@"[RMHook] 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(@"[RMHook] a2 low 16 bits = 0x%04x (%u)", a2_low, a2_low);
NSLogger(@"[reMarkable] a3 (flags/version) = 0x%02x (%u)", a3, a3); NSLogger(@"[RMHook] a3 (flags/version) = 0x%02x (%u)", a3, a3);
NSLogger(@"[reMarkable] v17 = (a2<<8)|a3 = 0x%04x (%u)", combined_v17, combined_v17); NSLogger(@"[RMHook] v17 = (a2<<8)|a3 = 0x%04x (%u)", combined_v17, combined_v17);
NSLogger(@"[reMarkable] a4 (typeInfo/URI?) = 0x%llx", (unsigned long long)a4); NSLogger(@"[RMHook] a4 (typeInfo/URI?) = 0x%llx", (unsigned long long)a4);
NSLogger(@"[reMarkable] a5 (refCountData) = %p", a5); NSLogger(@"[RMHook] a5 (refCountData) = %p", a5);
if (a1) { if (a1) {
logMemory("Memory at a1 (typeMetadata)", (void *)a1, 64); logMemory("Memory at a1 (typeMetadata)", (void *)a1, 64);
void **vtable = (void **)a1; void **vtable = (void **)a1;
NSLogger(@"[reMarkable] a1 vtable/first ptr = %p", *vtable); NSLogger(@"[RMHook] a1 vtable/first ptr = %p", *vtable);
} }
if (a4) { if (a4) {
@@ -160,7 +160,7 @@ extern "C" int64_t hooked_function_at_0x100011CE0(
len++; len++;
} }
if (isPrintable && len > 0) { 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) { if (original_function_at_0x100011CE0) {
int64_t result = original_function_at_0x100011CE0(a1, a2, a3, a4, a5); 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(@"[RMHook] result (qmlregister return) = %u (0x%x)", (unsigned int)result, (unsigned int)result);
NSLogger(@"[reMarkable] ========================================"); NSLogger(@"[RMHook] ========================================");
return result; 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; return 0;
} }
extern "C" int64_t hooked_function_at_0x10015A130(int64_t a1, int64_t a2) { extern "C" int64_t hooked_function_at_0x10015A130(int64_t a1, int64_t a2) {
NSLogger(@"[reMarkable] Hook at 0x10015A130 called!"); NSLogger(@"[RMHook] Hook at 0x10015A130 called!");
NSLogger(@"[reMarkable] a1 = 0x%llx", (unsigned long long)a1); NSLogger(@"[RMHook] a1 = 0x%llx", (unsigned long long)a1);
NSLogger(@"[reMarkable] a2 = 0x%llx", (unsigned long long)a2); NSLogger(@"[RMHook] a2 = 0x%llx", (unsigned long long)a2);
logMemory("Memory at a1", (void *)a1, 64); logMemory("Memory at a1", (void *)a1, 64);
logMemory("Memory at a2", (void *)a2, 64); logMemory("Memory at a2", (void *)a2, 64);
if (original_function_at_0x10015A130) { if (original_function_at_0x10015A130) {
int64_t result = original_function_at_0x10015A130(a1, a2); 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; 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; return 0;
} }
extern "C" void hooked_function_at_0x10015BC90(int64_t a1, int64_t a2) { extern "C" void hooked_function_at_0x10015BC90(int64_t a1, int64_t a2) {
NSLogger(@"[reMarkable] Hook at 0x10015BC90 called!"); NSLogger(@"[RMHook] Hook at 0x10015BC90 called!");
NSLogger(@"[reMarkable] a1 = 0x%llx", (unsigned long long)a1); NSLogger(@"[RMHook] a1 = 0x%llx", (unsigned long long)a1);
NSLogger(@"[reMarkable] a2 = 0x%llx", (unsigned long long)a2); NSLogger(@"[RMHook] a2 = 0x%llx", (unsigned long long)a2);
logMemory("Memory at a1", (void *)a1, 64); logMemory("Memory at a1", (void *)a1, 64);
logMemory("Memory at a2", (void *)a2, 64); logMemory("Memory at a2", (void *)a2, 64);
if (original_function_at_0x10015BC90) { if (original_function_at_0x10015BC90) {
original_function_at_0x10015BC90(a1, a2); original_function_at_0x10015BC90(a1, a2);
NSLogger(@"[reMarkable] original function returned (void)"); NSLogger(@"[RMHook] original function returned (void)");
return; 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) { 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(@"[RMHook] Hook at 0x10016D520 called!");
NSLogger(@"[reMarkable] a1 = 0x%llx", (unsigned long long)a1); NSLogger(@"[RMHook] a1 = 0x%llx", (unsigned long long)a1);
NSLogger(@"[reMarkable] a2 = %p", a2); NSLogger(@"[RMHook] a2 = %p", a2);
if (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);
NSLogger(@"[reMarkable] a4 = 0x%llx", (unsigned long long)a4); NSLogger(@"[RMHook] a4 = 0x%llx", (unsigned long long)a4);
logMemory("Memory at a1", (void *)a1, 64); logMemory("Memory at a1", (void *)a1, 64);
logMemory("Memory at a2", (void *)a2, 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) { if (original_function_at_0x10016D520) {
int64_t result = original_function_at_0x10016D520(a1, a2, a3, a4); 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; return result;
} }
NSLogger(@"[reMarkable] WARNING: Original function not available, returning 0"); NSLogger(@"[RMHook] WARNING: Original function not available, returning 0");
return 0; return 0;
} }
extern "C" void hooked_function_at_0x1001B6EE0(int64_t a1, int64_t *a2, unsigned int a3) { extern "C" void hooked_function_at_0x1001B6EE0(int64_t a1, int64_t *a2, unsigned int a3) {
NSLogger(@"[reMarkable] Hook at 0x1001B6EE0 called!"); NSLogger(@"[RMHook] Hook at 0x1001B6EE0 called!");
NSLogger(@"[reMarkable] a1 = 0x%llx", (unsigned long long)a1); NSLogger(@"[RMHook] a1 = 0x%llx", (unsigned long long)a1);
// At a1 (PdfExporter object): // At a1 (PdfExporter object):
// +0x10 contains a QString (likely document name) // +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); logMemory("a1 + 0x10 (raw)", (void *)(a1 + 0x10), 64);
void **qstrPtr = (void **)(a1 + 0x10); void **qstrPtr = (void **)(a1 + 0x10);
void *dataPtr = *qstrPtr; void *dataPtr = *qstrPtr;
if (!dataPtr) { if (!dataPtr) {
NSLogger(@"[reMarkable] QString has null data pointer"); NSLogger(@"[RMHook] QString has null data pointer");
return; 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); int32_t candidate = *(int32_t *)((char *)dataPtr - delta);
if (candidate > 0 && candidate < 10000) { if (candidate > 0 && candidate < 10000) {
size = candidate; 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; break;
} }
} }
if (size > 0) { if (size > 0) {
NSString *qstringValue = [[NSString alloc] initWithCharacters:(unichar *)dataPtr length:size]; NSString *qstringValue = [[NSString alloc] initWithCharacters:(unichar *)dataPtr length:size];
NSLogger(@"[reMarkable] QString value: \"%@\"", qstringValue); NSLogger(@"[RMHook] QString value: \"%@\"", qstringValue);
} else { } 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) { 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) { if (original_function_at_0x1001B6EE0) {
original_function_at_0x1001B6EE0(a1, a2, a3); original_function_at_0x1001B6EE0(a1, a2, a3);
NSLogger(@"[reMarkable] Original function at 0x1001B6EE0 executed"); NSLogger(@"[RMHook] Original function at 0x1001B6EE0 executed");
} else { } 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, int a9,
int64_t a10) { int64_t a10) {
NSLogger(@"[reMarkable] ========================================"); NSLogger(@"[RMHook] ========================================");
NSLogger(@"[reMarkable] QQmlPrivate::qmlregister called!"); NSLogger(@"[RMHook] QQmlPrivate::qmlregister called!");
NSLogger(@"[reMarkable] ========================================"); NSLogger(@"[RMHook] ========================================");
NSLogger(@"[reMarkable] a1 (RegistrationType) = 0x%llx (%lld)", (unsigned long long)a1, (long long)a1); NSLogger(@"[RMHook] 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(@"[RMHook] 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(@"[RMHook] 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(@"[RMHook] 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(@"[RMHook] 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(@"[RMHook] a6 = 0x%llx (%lld)", (unsigned long long)a6, (long long)a6);
NSLogger(@"[reMarkable] a7 = 0x%x (%d)", a7, a7); NSLogger(@"[RMHook] a7 = 0x%x (%d)", a7, a7);
NSLogger(@"[reMarkable] a8 = 0x%llx (%lld)", (unsigned long long)a8, (long long)a8); NSLogger(@"[RMHook] a8 = 0x%llx (%lld)", (unsigned long long)a8, (long long)a8);
NSLogger(@"[reMarkable] a9 = 0x%x (%d)", a9, a9); NSLogger(@"[RMHook] a9 = 0x%x (%d)", a9, a9);
NSLogger(@"[reMarkable] a10 = 0x%llx (%lld)", (unsigned long long)a10, (long long)a10); NSLogger(@"[RMHook] a10 = 0x%llx (%lld)", (unsigned long long)a10, (long long)a10);
// Check for PlatformHelpers registration // Check for PlatformHelpers registration
// a1 == 0 means TypeRegistration (object registration) // a1 == 0 means TypeRegistration (object registration)
@@ -341,14 +341,14 @@ extern "C" int64_t hooked_qmlregister(
} }
if (isValid && len > 0) { 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) { if (len == 15 && strncmp(typeName, "PlatformHelpers", 15) == 0) {
NSLogger(@"[reMarkable] !!! FOUND PlatformHelpers type registration !!!"); NSLogger(@"[RMHook] !!! FOUND PlatformHelpers type registration !!!");
NSLogger(@"[reMarkable] factory ptr (a2) = %p", (void *)a2); NSLogger(@"[RMHook] factory ptr (a2) = %p", (void *)a2);
NSLogger(@"[reMarkable] a3 (metaObject?) = %p", (void *)a3); NSLogger(@"[RMHook] a3 (metaObject?) = %p", (void *)a3);
NSLogger(@"[reMarkable] a5 = %p", (void *)a5); NSLogger(@"[RMHook] a5 = %p", (void *)a5);
NSLogger(@"[reMarkable] a6 = %p", (void *)a6); NSLogger(@"[RMHook] a6 = %p", (void *)a6);
logMemory("Factory ptr memory", (void *)a2, 64); logMemory("Factory ptr memory", (void *)a2, 64);
logMemory("a3 memory (metaObject?)", (void *)a3, 64); logMemory("a3 memory (metaObject?)", (void *)a3, 64);
logStackTrace("PlatformHelpers registration"); logStackTrace("PlatformHelpers registration");
@@ -370,19 +370,19 @@ extern "C" int64_t hooked_qmlregister(
len++; len++;
} }
if (isPrintable && len > 0) { if (isPrintable && len > 0) {
NSLogger(@"[reMarkable] a2 as string: \"%.*s\"", len, maybeStr); NSLogger(@"[RMHook] a2 as string: \"%.*s\"", len, maybeStr);
} }
} }
int64_t result = 0; int64_t result = 0;
if (original_qmlregister) { if (original_qmlregister) {
result = original_qmlregister(a1, a2, a3, a4, a5, a6, a7, a8, a9, a10); 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 { } else {
NSLogger(@"[reMarkable] WARNING: Original qmlregister not available!"); NSLogger(@"[RMHook] WARNING: Original qmlregister not available!");
} }
NSLogger(@"[reMarkable] ========================================"); NSLogger(@"[RMHook] ========================================");
return result; return result;
} }

View File

@@ -1,5 +1,5 @@
#import <Foundation/Foundation.h> #import <Foundation/Foundation.h>
@interface reMarkable : NSObject @interface RMHook : NSObject
@end @end

469
src/RMHook/RMHook.m Normal file
View File

@@ -0,0 +1,469 @@
#import "RMHook.h"
#import <Foundation/Foundation.h>
#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 <objc/runtime.h>
#import <Cocoa/Cocoa.h>
#include <stdint.h>
#include <limits.h>
#include <pthread.h>
#include <stdlib.h>
#include <string.h>
#include <dispatch/dispatch.h>
#include <string>
#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>
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

4
src/RMHook/SSLConfig.h Normal file
View File

@@ -0,0 +1,4 @@
#include <QtNetwork/QNetworkRequest>
void SSLConfigLoad(void);
void SSLConfigApplyToRequest(QNetworkRequest &request);

92
src/RMHook/SSLConfig.m Normal file
View File

@@ -0,0 +1,92 @@
#import "SSLConfig.h"
#import "Config.h"
#import "Logger.h"
#include <QtNetwork/QSslConfiguration>
#include <QtNetwork/QSslCertificate>
#include <QtNetwork/QSslKey>
#include <QtNetwork/QSslSocket>
#include <QtCore/QFile>
#include <QtCore/QIODevice>
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<QSslCertificate> localCerts = sslConfig.localCertificateChain();
localCerts.append(gClientCert);
sslConfig.setLocalCertificateChain(localCerts);
sslConfig.setPrivateKey(gClientKey);
}
if (!gCACert.isNull()) {
QList<QSslCertificate> caCerts = sslConfig.caCertificates();
caCerts.append(gCACert);
sslConfig.setCaCertificates(caCerts);
}
if (gDisableSSLVerification) {
sslConfig.setPeerVerifyMode(QSslSocket::VerifyNone);
}
request.setSslConfiguration(sslConfig);
}

View File

@@ -1,783 +0,0 @@
#import "reMarkable.h"
#import <Foundation/Foundation.h>
#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 <objc/runtime.h>
#import <Cocoa/Cocoa.h>
#include <stdint.h>
#include <limits.h>
#include <pthread.h>
#include <stdlib.h>
#include <string.h>
#include <dispatch/dispatch.h>
#include <string>
#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 <QtNetwork/QSslConfiguration>
#include <QtNetwork/QSslCertificate>
#include <QtNetwork/QSslKey>
#include <QtNetwork/QSslSocket>
#include <QtCore/QFile>
#include <QtCore/QSettings>
#include <QtCore/QVariant>
#include <QtCore/QAnyStringView>
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<NSString *> *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<NSString *, id> *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<NSString *, id> *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<QSslCertificate> localCerts = sslConfig.localCertificateChain();
localCerts.append(gClientCert);
sslConfig.setLocalCertificateChain(localCerts);
sslConfig.setPrivateKey(gClientKey);
}
if (!gCACert.isNull()) {
QList<QSslCertificate> 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<QSslCertificate> localCerts = sslConfig.localCertificateChain();
localCerts.append(gClientCert);
sslConfig.setLocalCertificateChain(localCerts);
sslConfig.setPrivateKey(gClientKey);
}
if (!gCACert.isNull()) {
QList<QSslCertificate> 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