3 Commits

Author SHA1 Message Date
√(noham)²
36d4b1258f Add configurable client key algorithm 2026-08-27 16:21:40 +02:00
√(noham)²
3f1b4fd728 Proper implementation 2026-08-27 16:15:57 +02:00
√(noham)²
ce1df6f27d PoC 2026-08-27 16:06:10 +02:00
12 changed files with 465 additions and 326 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,45 @@ 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 |
| `key_algorithm` | String | `rsa` | Key algorithm: `rsa`, `ec`, `dsa`, `dh`, or `opaque` |
| `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 +157,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

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

@@ -0,0 +1,16 @@
#import <Foundation/Foundation.h>
#include <QtCore/QString>
#include <QtNetwork/QSsl>
extern NSString *gConfiguredHostObjC;
extern NSNumber *gConfiguredPortObjC;
extern QString gConfiguredHost;
extern NSNumber *gConfiguredPort;
extern QString gConfiguredClientCertPath;
extern QString gConfiguredClientKeyPath;
extern QString gConfiguredCACertPath;
extern QSsl::KeyAlgorithm gConfiguredKeyAlgorithm;
extern bool gDisableSSLVerification;
void ConfigLoadOrCreate(void);
NSString *ConfigFilePath(void);

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

@@ -0,0 +1,152 @@
#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 kConfigKeyAlgorithmKey = @"key_algorithm";
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;
QSsl::KeyAlgorithm gConfiguredKeyAlgorithm = QSsl::Rsa;
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);
}
NSString *keyAlgoValue = configDict[kConfigKeyAlgorithmKey];
if ([keyAlgoValue isKindOfClass:[NSString class]] && [keyAlgoValue length]) {
NSString *lower = [keyAlgoValue lowercaseString];
if ([lower isEqualToString:@"ec"]) {
gConfiguredKeyAlgorithm = QSsl::Ec;
} else if ([lower isEqualToString:@"dsa"]) {
gConfiguredKeyAlgorithm = QSsl::Dsa;
} else if ([lower isEqualToString:@"dh"]) {
gConfiguredKeyAlgorithm = QSsl::Dh;
} else if ([lower isEqualToString:@"opaque"]) {
gConfiguredKeyAlgorithm = QSsl::Opaque;
} else {
gConfiguredKeyAlgorithm = QSsl::Rsa;
}
}
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

View File

@@ -1,9 +1,11 @@
#import "reMarkable.h" #import "RMHook.h"
#import <Foundation/Foundation.h> #import <Foundation/Foundation.h>
#import "Constant.h" #import "Constant.h"
#import "MemoryUtils.h" #import "MemoryUtils.h"
#import "Logger.h" #import "Logger.h"
#import "ResourceUtils.h" #import "ResourceUtils.h"
#import "Config.h"
#import "SSLConfig.h"
#ifdef BUILD_MODE_DEV #ifdef BUILD_MODE_DEV
#import "DevHooks.h" #import "DevHooks.h"
#endif #endif
@@ -29,113 +31,9 @@
#include <QtCore/QString> #include <QtCore/QString>
#include <QtCore/Qt> #include <QtCore/Qt>
#include <QtWebSockets/QWebSocket> #include <QtWebSockets/QWebSocket>
#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 kReMarkableDefaultHost = @"example.com";
static NSNumber *const kReMarkableDefaultPort = @(443);
static NSString *gConfiguredHost = @"example.com";
static NSNumber *gConfiguredPort = @(443);
static pthread_mutex_t gResourceMutex = PTHREAD_MUTEX_INITIALIZER; static pthread_mutex_t gResourceMutex = PTHREAD_MUTEX_INITIALIZER;
static NSString *ReMarkablePreferencesDirectory(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 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 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);
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;
}
static inline QString QStringFromNSStringSafe(NSString *string) {
if (!string) {
return QString();
}
return QString::fromUtf8([string UTF8String]);
}
@interface MenuActionController : NSObject @interface MenuActionController : NSObject
@property (strong, nonatomic) NSURL *targetURL; @property (strong, nonatomic) NSURL *targetURL;
- (void)openURLAction:(id)sender; - (void)openURLAction:(id)sender;
@@ -207,24 +105,22 @@ static inline QString QStringFromNSStringSafe(NSString *string) {
@end @end
@interface reMarkableDylib : NSObject @interface RMHookDylib : NSObject
- (BOOL)hook; - (BOOL)hook;
@end @end
@implementation reMarkable @implementation RMHook
+ (void)load { + (void)load {
NSLogger(@"reMarkable dylib loaded successfully"); NSLogger(@"RMHook dylib loaded successfully");
// Initialize the hook RMHookDylib *dylib = [[RMHookDylib alloc] init];
reMarkableDylib *dylib = [[reMarkableDylib alloc] init];
[dylib hook]; [dylib hook];
#ifdef BUILD_MODE_RMFAKECLOUD #ifdef BUILD_MODE_RMFAKECLOUD
// Add custom Help menu entry to open config file NSString *configPath = ConfigFilePath();
NSString *configPath = ReMarkableConfigFilePath();
NSString *fileURL = [NSString stringWithFormat:@"file://%@", configPath]; NSString *fileURL = [NSString stringWithFormat:@"file://%@", configPath];
[MenuActionController addCustomHelpMenuEntry:@"Open rmfakecloud config" [MenuActionController addCustomHelpMenuEntry:@"Open rmfakecloud config"
withURL:fileURL withURL:fileURL
@@ -234,7 +130,7 @@ static inline QString QStringFromNSStringSafe(NSString *string) {
@end @end
@implementation reMarkableDylib @implementation RMHookDylib
#ifdef BUILD_MODE_RMFAKECLOUD #ifdef BUILD_MODE_RMFAKECLOUD
static QNetworkReply *(*original_qNetworkAccessManager_createRequest)( static QNetworkReply *(*original_qNetworkAccessManager_createRequest)(
@@ -267,8 +163,6 @@ static int (*original_qRegisterResourceData)(
const unsigned char *) = NULL; const unsigned char *) = NULL;
#endif #endif
#ifdef BUILD_MODE_RMFAKECLOUD #ifdef BUILD_MODE_RMFAKECLOUD
static inline bool shouldPatchURL(const QString &host) { static inline bool shouldPatchURL(const QString &host) {
if (host.isEmpty()) { if (host.isEmpty()) {
@@ -294,114 +188,63 @@ static inline bool shouldPatchURL(const QString &host) {
} }
#endif #endif
static inline QString QStringFromNSStringSafe(NSString *string) {
if (!string) {
return QString();
}
return QString::fromUtf8([string UTF8String]);
}
- (BOOL)hook { - (BOOL)hook {
NSLogger(@"[reMarkable] Starting hooks..."); NSLogger(@"[RMHook] Starting hooks...");
#ifdef BUILD_MODE_RMFAKECLOUD #ifdef BUILD_MODE_RMFAKECLOUD
NSLogger(@"[reMarkable] Build mode: rmfakecloud"); NSLogger(@"[RMHook] Build mode: rmfakecloud");
ReMarkableLoadOrCreateConfig(); ConfigLoadOrCreate();
NSLogger(@"[reMarkable] Using override host %@ and port %@", gConfiguredHost, gConfiguredPort); SSLConfigLoad();
NSLogger(@"[RMHook] Using override host %@ and port %@", gConfiguredHostObjC, gConfiguredPortObjC);
[MemoryUtils hookSymbol:@"QtNetwork" [MemoryUtils hookSymbol:@"QtNetwork"
symbolName:@"__ZN21QNetworkAccessManager13createRequestENS_9OperationERK15QNetworkRequestP9QIODevice" symbolName:@"__ZN21QNetworkAccessManager13createRequestENS_9OperationERK15QNetworkRequestP9QIODevice"
hookFunction:(void *)hooked_qNetworkAccessManager_createRequest hookFunction:(void *)hooked_qNetworkAccessManager_createRequest
originalFunction:(void **)&original_qNetworkAccessManager_createRequest originalFunction:(void **)&original_qNetworkAccessManager_createRequest
logPrefix:@"[reMarkable]"]; logPrefix:@"[RMHook]"];
[MemoryUtils hookSymbol:@"QtWebSockets" [MemoryUtils hookSymbol:@"QtWebSockets"
symbolName:@"__ZN10QWebSocket4openERK15QNetworkRequest" symbolName:@"__ZN10QWebSocket4openERK15QNetworkRequest"
hookFunction:(void *)hooked_qWebSocket_open hookFunction:(void *)hooked_qWebSocket_open
originalFunction:(void **)&original_qWebSocket_open originalFunction:(void **)&original_qWebSocket_open
logPrefix:@"[reMarkable]"]; logPrefix:@"[RMHook]"];
[MemoryUtils hookSymbol:@"libpaho-mqtt3as.1.dylib" [MemoryUtils hookSymbol:@"libpaho-mqtt3as.1.dylib"
symbolName:@"_MQTTAsync_createWithOptions" symbolName:@"_MQTTAsync_createWithOptions"
hookFunction:(void *)hooked_MQTTAsync_createWithOptions hookFunction:(void *)hooked_MQTTAsync_createWithOptions
originalFunction:(void **)&original_MQTTAsync_createWithOptions originalFunction:(void **)&original_MQTTAsync_createWithOptions
logPrefix:@"[reMarkable]"]; logPrefix:@"[RMHook]"];
#endif #endif
#ifdef BUILD_MODE_QMLREBUILD #ifdef BUILD_MODE_QMLREBUILD
NSLogger(@"[reMarkable] Build mode: qmlrebuild"); NSLogger(@"[RMHook] Build mode: qmlrebuild");
// Register MessageBroker QML type for dylib <-> QML communication
messagebroker::registerQmlType(); messagebroker::registerQmlType();
// Register native callback to receive signals from QML
messagebroker::setNativeCallback([](const char *signal, const char *value) { messagebroker::setNativeCallback([](const char *signal, const char *value) {
NSLogger(@"[reMarkable] Native callback received signal '%s' with value '%s'", signal, value); NSLogger(@"[RMHook] Native callback received signal '%s' with value '%s'", signal, value);
}); });
[MemoryUtils hookSymbol:@"QtCore" [MemoryUtils hookSymbol:@"QtCore"
symbolName:@"__Z21qRegisterResourceDataiPKhS0_S0_" symbolName:@"__Z21qRegisterResourceDataiPKhS0_S0_"
hookFunction:(void *)hooked_qRegisterResourceData hookFunction:(void *)hooked_qRegisterResourceData
originalFunction:(void **)&original_qRegisterResourceData originalFunction:(void **)&original_qRegisterResourceData
logPrefix:@"[reMarkable]"]; logPrefix:@"[RMHook]"];
// 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(), ^{ dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
messagebroker::broadcast("signalName", "Hello from dylib!"); messagebroker::broadcast("signalName", "Hello from dylib!");
}); });
#endif #endif
#ifdef BUILD_MODE_DEV #ifdef BUILD_MODE_DEV
NSLogger(@"[reMarkable] Build mode: dev/reverse engineering"); NSLogger(@"[RMHook] 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 #endif
return YES; return YES;
@@ -416,14 +259,15 @@ extern "C" QNetworkReply* hooked_qNetworkAccessManager_createRequest(
) { ) {
const QString host = req.url().host(); const QString host = req.url().host();
if (shouldPatchURL(host)) { if (shouldPatchURL(host)) {
// Clone request to keep original immutable
QNetworkRequest newReq(req); QNetworkRequest newReq(req);
QUrl newUrl = req.url(); QUrl newUrl = req.url();
const QString overrideHost = QStringFromNSStringSafe(gConfiguredHost); const QString overrideHost = QStringFromNSStringSafe(gConfiguredHostObjC);
newUrl.setHost(overrideHost); newUrl.setHost(overrideHost);
newUrl.setPort([gConfiguredPort intValue]); newUrl.setPort([gConfiguredPortObjC intValue]);
newReq.setUrl(newUrl); newReq.setUrl(newUrl);
SSLConfigApplyToRequest(newReq);
if (original_qNetworkAccessManager_createRequest) { if (original_qNetworkAccessManager_createRequest) {
return original_qNetworkAccessManager_createRequest(self, op, newReq, outgoingData); return original_qNetworkAccessManager_createRequest(self, op, newReq, outgoingData);
} }
@@ -447,13 +291,15 @@ extern "C" void hooked_qWebSocket_open(
const QString host = req.url().host(); const QString host = req.url().host();
if (shouldPatchURL(host)) { if (shouldPatchURL(host)) {
QUrl newUrl = req.url(); QUrl newUrl = req.url();
const QString overrideHost = QStringFromNSStringSafe(gConfiguredHost); const QString overrideHost = QStringFromNSStringSafe(gConfiguredHostObjC);
newUrl.setHost(overrideHost); newUrl.setHost(overrideHost);
newUrl.setPort([gConfiguredPort intValue]); newUrl.setPort([gConfiguredPortObjC intValue]);
QNetworkRequest newReq(req); QNetworkRequest newReq(req);
newReq.setUrl(newUrl); newReq.setUrl(newUrl);
SSLConfigApplyToRequest(newReq);
original_qWebSocket_open(self, newReq); original_qWebSocket_open(self, newReq);
return; return;
} }
@@ -461,8 +307,6 @@ extern "C" void hooked_qWebSocket_open(
original_qWebSocket_open(self, req); 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) static std::string PatchMqttUri(const char* uri)
{ {
if (!uri) return {}; if (!uri) return {};
@@ -475,7 +319,6 @@ static std::string PatchMqttUri(const char* uri)
const std::string origHost = original.substr(hostStart, hostEnd - hostStart); const std::string origHost = original.substr(hostStart, hostEnd - hostStart);
// Match any *.remarkable.com or *.remarkable.engineering host
static const char* kSuffixes[] = { static const char* kSuffixes[] = {
".remarkable.com", ".remarkable.com",
".remarkable.engineering", ".remarkable.engineering",
@@ -496,17 +339,16 @@ static std::string PatchMqttUri(const char* uri)
if (!shouldPatch) return {}; if (!shouldPatch) return {};
std::string patched = original; std::string patched = original;
std::string proxyHost = [gConfiguredHost UTF8String]; std::string proxyHost = [gConfiguredHostObjC UTF8String];
patched.replace(hostStart, hostEnd - hostStart, proxyHost); patched.replace(hostStart, hostEnd - hostStart, proxyHost);
// Fix port
size_t colonPos = patched.find(':', hostStart + proxyHost.size()); size_t colonPos = patched.find(':', hostStart + proxyHost.size());
if (colonPos != std::string::npos) if (colonPos != std::string::npos)
{ {
size_t numEnd = patched.find_first_not_of("0123456789", colonPos + 1); size_t numEnd = patched.find_first_not_of("0123456789", colonPos + 1);
if (numEnd == std::string::npos) numEnd = patched.size(); if (numEnd == std::string::npos) numEnd = patched.size();
patched.replace(colonPos + 1, numEnd - colonPos - 1, patched.replace(colonPos + 1, numEnd - colonPos - 1,
std::to_string([gConfiguredPort intValue])); std::to_string([gConfiguredPortObjC intValue]));
} }
return patched; return patched;
} }
@@ -520,12 +362,12 @@ extern "C" int hooked_MQTTAsync_createWithOptions(
MQTTAsync_createOptions *options) MQTTAsync_createOptions *options)
{ {
if (!original_MQTTAsync_createWithOptions) { if (!original_MQTTAsync_createWithOptions) {
return -1; // error code for MQTTAsync_create failure return -1;
} }
std::string patchedUri = PatchMqttUri(serverURI); std::string patchedUri = PatchMqttUri(serverURI);
if (!patchedUri.empty()) { if (!patchedUri.empty()) {
NSLogger(@"[reMarkable] Patching MQTT URI from %s to %s", serverURI, patchedUri.c_str()); 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, patchedUri.c_str(), clientId, persistence_type, persistence_context, options);
} }
@@ -535,8 +377,6 @@ extern "C" int hooked_MQTTAsync_createWithOptions(
#ifdef BUILD_MODE_QMLREBUILD #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( extern "C" int hooked_qRegisterResourceData(
int version, int version,
const unsigned char *tree, const unsigned char *tree,
@@ -561,23 +401,21 @@ extern "C" int hooked_qRegisterResourceData(
.entriesAffected = 0, .entriesAffected = 0,
}; };
NSLogger(@"[reMarkable] Registering Qt resource version %d tree:%p name:%p data:%p", NSLogger(@"[RMHook] Registering Qt resource version %d tree:%p name:%p data:%p",
version, tree, name, data); version, tree, name, data);
statArchive(&resource, 0); statArchive(&resource, 0);
// Make a writable copy of the tree (we need to modify offsets)
resource.tree = (uint8_t *)malloc(resource.treeSize); resource.tree = (uint8_t *)malloc(resource.treeSize);
if (!resource.tree) { if (!resource.tree) {
NSLogger(@"[reMarkable] Failed to allocate tree buffer"); NSLogger(@"[RMHook] Failed to allocate tree buffer");
pthread_mutex_unlock(&gResourceMutex); pthread_mutex_unlock(&gResourceMutex);
return original_qRegisterResourceData(version, tree, name, data); return original_qRegisterResourceData(version, tree, name, data);
} }
memcpy(resource.tree, tree, resource.treeSize); memcpy(resource.tree, tree, resource.treeSize);
// Process nodes and mark replacements
processNode(&resource, 0, ""); processNode(&resource, 0, "");
NSLogger(@"[reMarkable] Processing done! Entries affected: %d, dataSize: %zu, originalDataSize: %zu", NSLogger(@"[RMHook] Processing done! Entries affected: %d, dataSize: %zu, originalDataSize: %zu",
resource.entriesAffected, resource.dataSize, resource.originalDataSize); resource.entriesAffected, resource.dataSize, resource.originalDataSize);
const unsigned char *finalTree = tree; const unsigned char *finalTree = tree;
@@ -585,30 +423,25 @@ extern "C" int hooked_qRegisterResourceData(
uint8_t *newDataBuffer = NULL; uint8_t *newDataBuffer = NULL;
if (resource.entriesAffected > 0) { if (resource.entriesAffected > 0) {
NSLogger(@"[reMarkable] Rebuilding data tables... (entries: %d)", resource.entriesAffected); NSLogger(@"[RMHook] Rebuilding data tables... (entries: %d)", resource.entriesAffected);
// Allocate new data buffer (original size + space for replacements)
newDataBuffer = (uint8_t *)malloc(resource.dataSize); newDataBuffer = (uint8_t *)malloc(resource.dataSize);
if (!newDataBuffer) { if (!newDataBuffer) {
NSLogger(@"[reMarkable] Failed to allocate new data buffer (%zu bytes)", resource.dataSize); NSLogger(@"[RMHook] Failed to allocate new data buffer (%zu bytes)", resource.dataSize);
free(resource.tree); free(resource.tree);
clearReplacementEntries(); clearReplacementEntries();
pthread_mutex_unlock(&gResourceMutex); pthread_mutex_unlock(&gResourceMutex);
return original_qRegisterResourceData(version, tree, name, data); return original_qRegisterResourceData(version, tree, name, data);
} }
// Copy original data
memcpy(newDataBuffer, data, resource.originalDataSize); memcpy(newDataBuffer, data, resource.originalDataSize);
// Copy replacement entries to their designated offsets
struct ReplacementEntry *entry = getReplacementEntries(); struct ReplacementEntry *entry = getReplacementEntries();
while (entry) { while (entry) {
// Write size prefix (4 bytes, big-endian)
writeUint32(newDataBuffer, (int)entry->copyToOffset, (uint32_t)entry->size); writeUint32(newDataBuffer, (int)entry->copyToOffset, (uint32_t)entry->size);
// Write data after size prefix
memcpy(newDataBuffer + entry->copyToOffset + 4, entry->data, entry->size); memcpy(newDataBuffer + entry->copyToOffset + 4, entry->data, entry->size);
NSLogger(@"[reMarkable] Copied replacement for node %d at offset %zu (%zu bytes)", NSLogger(@"[RMHook] Copied replacement for node %d at offset %zu (%zu bytes)",
entry->node, entry->copyToOffset, entry->size); entry->node, entry->copyToOffset, entry->size);
entry = entry->next; entry = entry->next;
@@ -617,19 +450,16 @@ extern "C" int hooked_qRegisterResourceData(
finalTree = resource.tree; finalTree = resource.tree;
finalData = newDataBuffer; finalData = newDataBuffer;
NSLogger(@"[reMarkable] Data buffer rebuilt: original %zu bytes -> new %zu bytes", NSLogger(@"[RMHook] Data buffer rebuilt: original %zu bytes -> new %zu bytes",
resource.originalDataSize, resource.dataSize); resource.originalDataSize, resource.dataSize);
} }
int status = original_qRegisterResourceData(version, finalTree, name, finalData); int status = original_qRegisterResourceData(version, finalTree, name, finalData);
// Cleanup
clearReplacementEntries(); clearReplacementEntries();
if (resource.tree && resource.entriesAffected == 0) { if (resource.tree && resource.entriesAffected == 0) {
free(resource.tree); 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); pthread_mutex_unlock(&gResourceMutex);
return status; return status;

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(), gConfiguredKeyAlgorithm, 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);
}