diff --git a/.gitignore b/.gitignore index 9053de0..552e9be 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ build/ .DS_Store /.vscode +/research diff --git a/CMakeLists.txt b/CMakeLists.txt index 6f90501..baf04b9 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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 ) \ No newline at end of file diff --git a/README.md b/README.md index 3a7a428..f52c91f 100644 --- a/README.md +++ b/README.md @@ -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 ``` \ No newline at end of file diff --git a/libs/libzstd.1.dylib b/libs/libzstd.1.dylib new file mode 100644 index 0000000..b2669fc Binary files /dev/null and b/libs/libzstd.1.dylib differ diff --git a/scripts/build.sh b/scripts/build.sh index d58d02e..896a4e8 100755 --- a/scripts/build.sh +++ b/scripts/build.sh @@ -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" diff --git a/scripts/inject.sh b/scripts/inject.sh index 1198a29..d30492a 100755 --- a/scripts/inject.sh +++ b/scripts/inject.sh @@ -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" diff --git a/scripts/optool b/scripts/optool new file mode 100755 index 0000000..d7142af Binary files /dev/null and b/scripts/optool differ diff --git a/src/reMarkable/reMarkable.m b/src/reMarkable/reMarkable.m index c789c40..ad41ca5 100644 --- a/src/reMarkable/reMarkable.m +++ b/src/reMarkable/reMarkable.m @@ -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 *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 \ No newline at end of file diff --git a/src/utils/MemoryUtils.h b/src/utils/MemoryUtils.h index 5836d18..10e0bad 100644 --- a/src/utils/MemoryUtils.h +++ b/src/utils/MemoryUtils.h @@ -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 diff --git a/src/utils/MemoryUtils.m b/src/utils/MemoryUtils.m index 76c6608..4f61bce 100644 --- a/src/utils/MemoryUtils.m +++ b/src/utils/MemoryUtils.m @@ -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 diff --git a/src/utils/ResourceUtils.m b/src/utils/ResourceUtils.m index 4a55117..d102614 100644 --- a/src/utils/ResourceUtils.m +++ b/src/utils/ResourceUtils.m @@ -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 \ No newline at end of file