Add build modes and dev hooks, improve injection and docs

Introduces build mode options (rmfakecloud, qmldiff, dev, all) to CMake and build scripts, enabling selective compilation of hooks for cloud redirection, Qt resource hooking, and reverse engineering. Adds new hooks and memory logging for dev mode, updates injection script to handle libzstd.1.dylib dependency, and documents build modes in README.
This commit is contained in:
√(noham)²
2025-11-29 14:34:07 +01:00
parent 18abae42b7
commit 1619fda631
11 changed files with 356 additions and 18 deletions

1
.gitignore vendored
View File

@@ -1,3 +1,4 @@
build/
.DS_Store
/.vscode
/research

View File

@@ -6,6 +6,14 @@ enable_language(OBJC OBJCXX)
set(CMAKE_C_STANDARD 11)
set(CMAKE_CXX_STANDARD 17)
# Build mode options
# - rmfakecloud: Redirect reMarkable cloud to rmfakecloud server (default)
# - qmldiff: Qt resource data registration hooking (WIP)
# - dev: Development/reverse engineering mode with all hooks
option(BUILD_MODE_RMFAKECLOUD "Build with rmfakecloud support" ON)
option(BUILD_MODE_QMLDIFF "Build with QML diff/resource hooking" OFF)
option(BUILD_MODE_DEV "Build with dev/reverse engineering hooks" OFF)
# Compiler settings for macOS
set(CMAKE_MACOSX_RPATH 1)
set(CMAKE_OSX_DEPLOYMENT_TARGET "10.15" CACHE STRING "Minimum OS X deployment version")
@@ -93,8 +101,24 @@ set_target_properties(reMarkable PROPERTIES
add_definitions(-DQT_NO_VERSION_TAGGING)
# Add build mode compile definitions
if(BUILD_MODE_RMFAKECLOUD)
target_compile_definitions(reMarkable PRIVATE BUILD_MODE_RMFAKECLOUD=1)
message(STATUS "Build mode: rmfakecloud (cloud redirection)")
endif()
if(BUILD_MODE_QMLDIFF)
target_compile_definitions(reMarkable PRIVATE BUILD_MODE_QMLDIFF=1)
message(STATUS "Build mode: qmldiff (resource hooking)")
endif()
if(BUILD_MODE_DEV)
target_compile_definitions(reMarkable PRIVATE BUILD_MODE_DEV=1)
message(STATUS "Build mode: dev (reverse engineering)")
endif()
target_link_libraries(reMarkable PRIVATE
${LIBS}
${QT_LIB_TARGETS}
/opt/homebrew/Cellar/libzip/1.11.4/lib/intel/libzstd.1.5.7.dylib
${PROJECT_ROOT_DIR}/libs/libzstd.1.dylib
)

View File

@@ -37,6 +37,7 @@ Use the provided injection script:
This script will:
- Copy the dylib to the app bundle's Resources folder
- Copy the `libzstd.1.dylib` dependency and fix library references
- Inject the load command into the executable using `optool`
- Remove the code signature and resign with ad-hoc signature
- Remove the `_MASReceipt` folder
@@ -145,5 +146,24 @@ cd RMHook
2. **Compile the dylib:**
```bash
./scripts/build.sh
./scripts/build.sh [mode]
```
### Build modes
The build script supports different modes for various use cases:
| Mode | Description |
|------|-------------|
| `rmfakecloud` | Redirect reMarkable cloud to rmfakecloud server (default) |
| `qmldiff` | Qt resource data registration hooking (WIP) |
| `dev` | Development/reverse engineering mode with all hooks |
| `all` | Enable all modes |
Examples:
```bash
./scripts/build.sh # Build with rmfakecloud mode (default)
./scripts/build.sh rmfakecloud # Explicitly build rmfakecloud mode
./scripts/build.sh dev # Build with dev/reverse engineering hooks
./scripts/build.sh all # Build with all modes enabled
```

BIN
libs/libzstd.1.dylib Normal file

Binary file not shown.

View File

@@ -1,14 +1,42 @@
#!/bin/bash
# Script to compile the reMarkable dylib
# Script to compile the reMarkable dylib with different build modes
# Build modes:
# rmfakecloud - Redirect reMarkable cloud to rmfakecloud server (default)
# qmldiff - Qt resource data registration hooking (WIP)
# dev - Development/reverse engineering mode with all hooks
# By default, compile reMarkable
APP_NAME=${1:-reMarkable}
PROJECT_DIR=$(cd "$(dirname "$0")/.." && pwd)
# Qt path detection (adjust according to your installation)
QT_PATH=${QT_PATH:-"$HOME/Qt/6.10.0"}
echo "🔨 Compiling $APP_NAME.dylib..."
# Parse build mode argument
BUILD_MODE=${1:-rmfakecloud}
# Set CMake options based on build mode
CMAKE_OPTIONS=""
case "$BUILD_MODE" in
rmfakecloud)
CMAKE_OPTIONS="-DBUILD_MODE_RMFAKECLOUD=ON -DBUILD_MODE_QMLDIFF=OFF -DBUILD_MODE_DEV=OFF"
;;
qmldiff)
CMAKE_OPTIONS="-DBUILD_MODE_RMFAKECLOUD=OFF -DBUILD_MODE_QMLDIFF=ON -DBUILD_MODE_DEV=OFF"
;;
dev)
CMAKE_OPTIONS="-DBUILD_MODE_RMFAKECLOUD=OFF -DBUILD_MODE_QMLDIFF=OFF -DBUILD_MODE_DEV=ON"
;;
all)
CMAKE_OPTIONS="-DBUILD_MODE_RMFAKECLOUD=ON -DBUILD_MODE_QMLDIFF=ON -DBUILD_MODE_DEV=ON"
;;
*)
echo "❌ Unknown build mode: $BUILD_MODE"
echo "Available modes: rmfakecloud (default), qmldiff, dev, all"
exit 1
;;
esac
echo "🔨 Compiling reMarkable.dylib (mode: $BUILD_MODE)..."
echo "📦 Qt path: $QT_PATH"
# Create build directories if necessary
@@ -17,21 +45,21 @@ cd "$PROJECT_DIR/build"
# Configure with CMake and compile
if [ -d "$QT_PATH" ]; then
cmake -DCMAKE_PREFIX_PATH="$QT_PATH" ..
cmake -DCMAKE_PREFIX_PATH="$QT_PATH" $CMAKE_OPTIONS ..
else
echo "⚠️ Qt not found at $QT_PATH, trying without specifying path..."
cmake ..
cmake $CMAKE_OPTIONS ..
fi
make $APP_NAME
make reMarkable
if [ $? -eq 0 ]; then
echo ""
echo "✅ Compilation successful!"
echo "📍 Dylib: $PROJECT_DIR/build/dylibs/$APP_NAME.dylib"
echo "📍 Dylib: $PROJECT_DIR/build/dylibs/reMarkable.dylib"
echo ""
echo "🚀 To inject into the reMarkable application:"
echo " DYLD_INSERT_LIBRARIES=\"$PROJECT_DIR/build/dylibs/$APP_NAME.dylib\" /Applications/reMarkable.app/Contents/MacOS/reMarkable"
echo " DYLD_INSERT_LIBRARIES=\"$PROJECT_DIR/build/dylibs/reMarkable.dylib\" /Applications/reMarkable.app/Contents/MacOS/reMarkable"
echo ""
else
echo "❌ Compilation failed"

View File

@@ -63,7 +63,27 @@ mkdir -p "$APP_PATH/Contents/Resources/"
cp "$DYLIB" "$APP_PATH/Contents/Resources/"
echo "[INFO] Copied $DYLIB to $APP_PATH/Contents/Resources/"
optool install -c load -p "@executable_path/../Resources/$(basename "$DYLIB")" -t "$EXECUTABLE_PATH"
# Use optool from the scripts folder
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# Copy libzstd dependency and fix the reference in reMarkable.dylib
LIBZSTD_PATH="$SCRIPT_DIR/../libs/libzstd.1.dylib"
if [ -f "$LIBZSTD_PATH" ]; then
cp "$LIBZSTD_PATH" "$APP_PATH/Contents/Resources/"
echo "[INFO] Copied libzstd.1.dylib to $APP_PATH/Contents/Resources/"
# Update the dylib reference to @executable_path/../Resources (handle multiple possible source paths)
DYLIB_IN_APP="$APP_PATH/Contents/Resources/$(basename "$DYLIB")"
install_name_tool -change "/usr/local/lib/libzstd.1.dylib" "@executable_path/../Resources/libzstd.1.dylib" "$DYLIB_IN_APP"
install_name_tool -change "/usr/local/opt/zstd/lib/libzstd.1.dylib" "@executable_path/../Resources/libzstd.1.dylib" "$DYLIB_IN_APP"
install_name_tool -change "/opt/homebrew/lib/libzstd.1.dylib" "@executable_path/../Resources/libzstd.1.dylib" "$DYLIB_IN_APP"
install_name_tool -change "/opt/homebrew/opt/zstd/lib/libzstd.1.dylib" "@executable_path/../Resources/libzstd.1.dylib" "$DYLIB_IN_APP"
echo "[INFO] Updated libzstd references in $(basename "$DYLIB")"
else
echo "[WARNING] libzstd.1.dylib not found at $LIBZSTD_PATH - app may fail on systems without zstd"
fi
"$SCRIPT_DIR/optool" install -c load -p "@executable_path/../Resources/$(basename "$DYLIB")" -t "$EXECUTABLE_PATH"
echo "[INFO] Injected $DYLIB into $EXECUTABLE_PATH"
sudo codesign --remove-signature "$EXECUTABLE_PATH"

BIN
scripts/optool Executable file

Binary file not shown.

View File

@@ -214,18 +214,21 @@ static inline QString QStringFromNSStringSafe(NSString *string) {
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,
@@ -235,13 +238,69 @@ static QNetworkReply *(*original_qNetworkAccessManager_createRequest)(
static void (*original_qWebSocket_open)(
QWebSocket *self,
const QNetworkRequest &request) = NULL;
#endif
#ifdef BUILD_MODE_QMLDIFF
static int (*original_qRegisterResourceData)(
int,
const unsigned char *,
const unsigned char *,
const unsigned char *) = NULL;
#endif
#ifdef BUILD_MODE_DEV
static ssize_t (*original_qIODevice_write)(
QIODevice *self,
const char *data,
qint64 maxSize) = NULL;
// Hook for function at 0x10016D520
static int64_t (*original_function_at_0x10016D520)(int64_t a1, int64_t *a2, unsigned int a3, int64_t a4) = NULL;
// Hook for function at 0x1001B6EE0
static void (*original_function_at_0x1001B6EE0)(int64_t a1, int64_t *a2, unsigned int a3) = NULL;
#endif
#if defined(BUILD_MODE_DEV)
// Memory logging helper function
static void logMemory(const char *label, void *address, size_t length) {
if (!address) {
NSLogger(@"[reMarkable] %s: (null)", label);
return;
}
unsigned char *ptr = (unsigned char *)address;
NSMutableString *hexLine = [NSMutableString stringWithFormat:@"[reMarkable] %s: ", label];
for (size_t i = 0; i < length; i++) {
[hexLine appendFormat:@"%02x ", ptr[i]];
if ((i + 1) % 16 == 0 && i < length - 1) {
NSLogger(@"%@", hexLine);
hexLine = [NSMutableString stringWithString:@"[reMarkable] "];
}
}
// Log remaining bytes if any
if ([hexLine length] > 28) { // More than just the prefix
NSLogger(@"%@", hexLine);
}
}
// Stack trace logging helper function
static void logStackTrace(const char *label) {
NSLogger(@"[reMarkable] %s - Stack trace:", label);
NSArray<NSString *> *callStack = [NSThread callStackSymbols];
NSUInteger count = [callStack count];
// Skip first 2 frames (this function and the immediate caller's logging statement)
for (NSUInteger i = 0; i < count; i++) {
NSString *frame = callStack[i];
NSLogger(@"[reMarkable] #%lu: %@", (unsigned long)i, frame);
}
}
#endif
#ifdef BUILD_MODE_RMFAKECLOUD
static inline bool shouldPatchURL(const QString &host) {
if (host.isEmpty()) {
return false;
@@ -264,10 +323,13 @@ static inline bool shouldPatchURL(const QString &host) {
)""")
.contains(host, Qt::CaseInsensitive);
}
#endif
- (BOOL)hook {
NSLogger(@"[reMarkable] Starting hooks...");
#ifdef BUILD_MODE_RMFAKECLOUD
NSLogger(@"[reMarkable] Build mode: rmfakecloud");
ReMarkableLoadOrCreateConfig();
NSLogger(@"[reMarkable] Using override host %@ and port %@", gConfiguredHost, gConfiguredPort);
@@ -282,17 +344,44 @@ static inline bool shouldPatchURL(const QString &host) {
hookFunction:(void *)hooked_qWebSocket_open
originalFunction:(void **)&original_qWebSocket_open
logPrefix:@"[reMarkable]"];
#endif
// WIP: Implement resource data registration hooking
// [MemoryUtils hookSymbol:@"QtCore"
// symbolName:@"__Z21qRegisterResourceDataiPKhS0_S0_"
// hookFunction:(void *)hooked_qRegisterResourceData
// originalFunction:(void **)&original_qRegisterResourceData
// logPrefix:@"[reMarkable]"];
#ifdef BUILD_MODE_QMLDIFF
NSLogger(@"[reMarkable] Build mode: qmldiff");
[MemoryUtils hookSymbol:@"QtCore"
symbolName:@"__Z21qRegisterResourceDataiPKhS0_S0_"
hookFunction:(void *)hooked_qRegisterResourceData
originalFunction:(void **)&original_qRegisterResourceData
logPrefix:@"[reMarkable]"];
#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 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]"];
#endif
return YES;
}
#ifdef BUILD_MODE_RMFAKECLOUD
extern "C" QNetworkReply* hooked_qNetworkAccessManager_createRequest(
QNetworkAccessManager* self,
QNetworkAccessManager::Operation op,
@@ -345,7 +434,9 @@ extern "C" void hooked_qWebSocket_open(
original_qWebSocket_open(self, req);
}
#endif // BUILD_MODE_RMFAKECLOUD
#ifdef BUILD_MODE_QMLDIFF
extern "C" int hooked_qRegisterResourceData(
int version,
const unsigned char *tree,
@@ -386,5 +477,108 @@ extern "C" int hooked_qRegisterResourceData(
}
return status;
}
#endif // BUILD_MODE_QMLDIFF
#ifdef BUILD_MODE_DEV
extern "C" ssize_t hooked_qIODevice_write(
QIODevice *self,
const char *data,
qint64 maxSize) {
NSLogger(@"[reMarkable] QIODevice::write called with maxSize: %lld", (long long)maxSize);
// Log the call stack
logStackTrace("QIODevice::write call stack");
// Log the data to write
logMemory("Data to write", (void *)data, (size_t)(maxSize < 64 ? maxSize : 64));
if (original_qIODevice_write) {
ssize_t result = original_qIODevice_write(self, data, maxSize);
NSLogger(@"[reMarkable] QIODevice::write result: %zd", result);
return result;
}
NSLogger(@"[reMarkable] WARNING: Original QIODevice::write not available, returning 0");
return 0;
}
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(@"[reMarkable] a1 = 0x%llx", (unsigned long long)a1);
NSLogger(@"[reMarkable] a2 = %p", a2);
if (a2) {
NSLogger(@"[reMarkable] *a2 = 0x%llx", (unsigned long long)*a2);
}
NSLogger(@"[reMarkable] a3 = %u (0x%x)", a3, a3);
NSLogger(@"[reMarkable] a4 = 0x%llx", (unsigned long long)a4);
// Log memory contents using helper function
logMemory("Memory at a1", (void *)a1, 64);
logMemory("Memory at a2", (void *)a2, 64);
if (a2 && *a2 != 0) {
logMemory("Memory at *a2", (void *)*a2, 64);
}
logMemory("Memory at a4", (void *)a4, 64);
if (original_function_at_0x10016D520) {
int64_t result = original_function_at_0x10016D520(a1, a2, a3, a4);
NSLogger(@"[reMarkable] result = 0x%llx", (unsigned long long)result);
return result;
}
NSLogger(@"[reMarkable] WARNING: Original function not available, returning 0");
return 0;
}
extern "C" void hooked_function_at_0x1001B6EE0(int64_t a1, int64_t *a2, unsigned int a3) {
NSLogger(@"[reMarkable] Hook at 0x1001B6EE0 called!");
NSLogger(@"[reMarkable] a1 = 0x%llx", (unsigned long long)a1);
// At a1 (PdfExporter object at 0x7ff4c17391e0):
// +0x10 0x000600043EC10 QString (likely document name)
NSLogger(@"[reMarkable] Reading QString at a1+0x10:");
logMemory("a1 + 0x10 (raw)", (void *)(a1 + 0x10), 64);
void **qstrPtr = (void **)(a1 + 0x10);
void *dataPtr = *qstrPtr;
if (!dataPtr) {
NSLogger(@"[reMarkable] QString has null data pointer");
return;
}
// try reading potential size fields near dataPtr
int32_t size = 0;
for (int delta = 4; delta <= 32; delta += 4) {
int32_t candidate = *(int32_t *)((char *)dataPtr - delta);
if (candidate > 0 && candidate < 10000) {
size = candidate;
NSLogger(@"[reMarkable] QString plausible size=%d (found at -%d)", size, delta);
break;
}
}
if (size > 0) {
NSString *qstringValue = [[NSString alloc] initWithCharacters:(unichar *)dataPtr length:size];
NSLogger(@"[reMarkable] QString value: \"%@\"", qstringValue);
} else {
NSLogger(@"[reMarkable] QString: could not find valid size");
}
NSLogger(@"[reMarkable] a2 = %p", a2);
if (a2) {
NSLogger(@"[reMarkable] *a2 = 0x%llx", (unsigned long long)*a2);
}
NSLogger(@"[reMarkable] a3 = %u (0x%x)", a3, a3);
if (original_function_at_0x1001B6EE0) {
original_function_at_0x1001B6EE0(a1, a2, a3);
NSLogger(@"[reMarkable] Original function at 0x1001B6EE0 executed");
} else {
NSLogger(@"[reMarkable] WARNING: Original function not available");
}
}
#endif // BUILD_MODE_DEV
@end

View File

@@ -30,4 +30,20 @@
logPrefix:(NSString *)logPrefix
delayInSeconds:(NSTimeInterval)delayInSeconds;
/**
* Hooks a function at a specific address after calculating ASLR slide.
*
* @param imageName The name of the image/library (e.g., "QtNetwork" or "reMarkable").
* @param staticAddress The static address from the binary (before ASLR).
* @param hookFunction The function to replace the original with.
* @param originalFunction Pointer to store the original function address.
* @param logPrefix Prefix for log messages (optional, can be nil).
* @return YES if the hook was successfully installed, NO otherwise.
*/
+ (BOOL)hookAddress:(NSString *)imageName
staticAddress:(uintptr_t)staticAddress
hookFunction:(void *)hookFunction
originalFunction:(void **)originalFunction
logPrefix:(NSString *)logPrefix;
@end

View File

@@ -103,4 +103,37 @@
}
}
+ (BOOL)hookAddress:(NSString *)imageName
staticAddress:(uintptr_t)staticAddress
hookFunction:(void *)hookFunction
originalFunction:(void **)originalFunction
logPrefix:(NSString *)logPrefix {
NSLogger(@"%@ Starting hook installation at static address: 0x%lx", logPrefix, staticAddress);
int imageIndex = [self indexForImageWithName:imageName];
if (imageIndex < 0) {
NSLogger(@"%@ ERROR: Image %@ not found", logPrefix, imageName);
return NO;
}
// Calculate ASLR slide
intptr_t slide = _dyld_get_image_vmaddr_slide(imageIndex);
NSLogger(@"%@ Image %@ ASLR slide: 0x%lx", logPrefix, imageName, slide);
// Calculate actual runtime address
void *actualAddress = (void *)(staticAddress + slide);
NSLogger(@"%@ Calculated runtime address: %p (static: 0x%lx + slide: 0x%lx)", logPrefix, actualAddress, staticAddress, slide);
int hookResult = tiny_hook(actualAddress, hookFunction, originalFunction);
if (hookResult == 0) {
NSLogger(@"%@ Hook successfully installed at address %p", logPrefix, actualAddress);
return YES;
} else {
NSLogger(@"%@ ERROR: Failed to install hook at address %p (code: %d)", logPrefix, actualAddress, hookResult);
return NO;
}
}
@end

View File

@@ -36,6 +36,7 @@ static NSString *ReMarkableDumpRootDirectory(void) {
return dumpDirectory;
}
#ifdef BUILD_MODE_QMLDIFF
uint32_t readUInt32(uint8_t *addr, int offset) {
return (uint32_t)(addr[offset + 0] << 24) |
(uint32_t)(addr[offset + 1] << 16) |
@@ -379,3 +380,4 @@ void processNode(struct ResourceRoot *root, int node, const char *rootName) {
ReMarkableDumpResourceFile(root, node, rootName ? rootName : "", nameBuffer, fileFlags);
}
}
#endif // BUILD_MODE_QMLDIFF