mirror of
https://github.com/NohamR/RMHook.git
synced 2026-08-27 18:29:42 +00:00
Proper implementation
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -2,3 +2,5 @@ build/
|
|||||||
.DS_Store
|
.DS_Store
|
||||||
/.vscode
|
/.vscode
|
||||||
/research
|
/research
|
||||||
|
/aqt_venv
|
||||||
|
docs/rmfakecloud_hooking.md
|
||||||
|
|||||||
@@ -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}
|
||||||
)
|
)
|
||||||
48
README.md
48
README.md
@@ -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]
|
||||||
|
|||||||
@@ -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
14
src/RMHook/Config.h
Normal 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
135
src/RMHook/Config.m
Normal 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;
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -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
469
src/RMHook/RMHook.m
Normal 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
4
src/RMHook/SSLConfig.h
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
#include <QtNetwork/QNetworkRequest>
|
||||||
|
|
||||||
|
void SSLConfigLoad(void);
|
||||||
|
void SSLConfigApplyToRequest(QNetworkRequest &request);
|
||||||
92
src/RMHook/SSLConfig.m
Normal file
92
src/RMHook/SSLConfig.m
Normal 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);
|
||||||
|
}
|
||||||
@@ -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
|
|
||||||
Reference in New Issue
Block a user