commit 5e98304b62be3560e317be92e4f0479b54625f57 Author: √(noham)² <100566912+NohamR@users.noreply.github.com> Date: Thu Oct 23 21:41:46 2025 +0200 First release diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9053de0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +build/ +.DS_Store +/.vscode diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..6f90501 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,100 @@ +cmake_minimum_required(VERSION 3.15) +project(RMHook) + +enable_language(OBJC OBJCXX) + +set(CMAKE_C_STANDARD 11) +set(CMAKE_CXX_STANDARD 17) + +# Compiler settings for macOS +set(CMAKE_MACOSX_RPATH 1) +set(CMAKE_OSX_DEPLOYMENT_TARGET "10.15" CACHE STRING "Minimum OS X deployment version") + +# Architecture: x86_64 only for reMarkable +set(CMAKE_OSX_ARCHITECTURES "x86_64") + +# Project root directory +set(PROJECT_ROOT_DIR ${CMAKE_CURRENT_SOURCE_DIR}) + +# Include directories +include_directories( + ${PROJECT_ROOT_DIR}/src/core + ${PROJECT_ROOT_DIR}/src/utils +) + +# Find required libraries +find_library(FOUNDATION_LIBRARY Foundation REQUIRED) +find_library(COCOA_LIBRARY Cocoa REQUIRED) +find_library(SECURITY_LIBRARY Security REQUIRED) + +# Common libraries +set(LIBS + ${FOUNDATION_LIBRARY} + ${COCOA_LIBRARY} + ${SECURITY_LIBRARY} + ${PROJECT_ROOT_DIR}/libs/libtinyhook.a + z # zlib for compression/decompression +) + +# Locate Qt libraries +set(QT_LIB_TARGETS "") +set(_qt_candidate_roots "$ENV{HOME}/Qt/6.10.0") + +foreach(_qt_root ${_qt_candidate_roots}) + if(_qt_root AND EXISTS "${_qt_root}") + list(APPEND CMAKE_PREFIX_PATH "${_qt_root}") + endif() +endforeach() + +find_package(Qt6 COMPONENTS Core Network WebSockets QUIET) +if(Qt6_FOUND) + set(QT_LIB_TARGETS Qt6::Core Qt6::Network Qt6::WebSockets) +else() + find_package(Qt5 COMPONENTS Core Network WebSockets QUIET) + if(Qt5_FOUND) + set(QT_LIB_TARGETS Qt5::Core Qt5::Network Qt5::WebSockets) + endif() +endif() + +if(NOT QT_LIB_TARGETS) + message(FATAL_ERROR "Qt Core, Network and WebSockets not found. Set CMAKE_PREFIX_PATH to your Qt installation.") +endif() + +# Common sources +set(COMMON_SOURCES + ${PROJECT_ROOT_DIR}/src/utils/MemoryUtils.m + ${PROJECT_ROOT_DIR}/src/utils/Constant.m + ${PROJECT_ROOT_DIR}/src/utils/ResourceUtils.m +) + +# reMarkable dylib +set(REMARKABLE_SOURCES + ${PROJECT_ROOT_DIR}/src/reMarkable/reMarkable.m +) + +add_library(reMarkable SHARED + ${COMMON_SOURCES} + ${REMARKABLE_SOURCES} +) + +# Set source files as Objective-C++ +set_source_files_properties( + ${REMARKABLE_SOURCES} + PROPERTIES LANGUAGE OBJCXX +) + +set_target_properties(reMarkable PROPERTIES + PREFIX "" + SUFFIX ".dylib" + OUTPUT_NAME "reMarkable" + LIBRARY_OUTPUT_DIRECTORY "${PROJECT_ROOT_DIR}/build/dylibs" + MACOSX_RPATH ON +) + +add_definitions(-DQT_NO_VERSION_TAGGING) + +target_link_libraries(reMarkable PRIVATE + ${LIBS} + ${QT_LIB_TARGETS} + /opt/homebrew/Cellar/libzip/1.11.4/lib/intel/libzstd.1.5.7.dylib +) \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..f2bb2bf --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Rivoirard Noham + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..77ffbec --- /dev/null +++ b/README.md @@ -0,0 +1,140 @@ +# RMHook + +A dynamic library injection tool for the reMarkable Desktop macOS application, enabling connection to self-hosted [rmfakecloud](https://github.com/ddvk/rmfakecloud) servers. + +## Overview + +RMHook hooks into the reMarkable Desktop app's network layer to redirect API calls from reMarkable's official cloud services to your own rmfakecloud server. This allows you to maintain full control over your documents and data. + +## Features + +- Network request interception and redirection +- WebSocket connection patching + +## Installation and usage + +### Important legal notice + +⚠️ **For legal reasons, this repository does not include a pre-patched reMarkable app.** However, the latest compiled dylib is available in the [Releases](https://github.com/NohamR/RMHook/releases/latest) section. + +### Step 1: Prepare the reMarkable app + +Uses the reMarkable Desktop app from your Applications folder or download it fresh from the [Mac App Store](https://apps.apple.com/app/remarkable-desktop/id1276493162). + +### Step 2: Inject the dylib + +Use the provided injection script: +```bash +./scripts/inject.sh reMarkable.dylib reMarkable.app +``` + +This script will: +- Copy the dylib to the app bundle's Resources folder +- Inject the load command into the executable using `optool` +- Remove the code signature and resign with ad-hoc signature +- Remove the `_MASReceipt` folder +- Fix file ownership + +### Step 3: Handle document storage + +#### Important path changes + +The original Mac App Store version stores data in sandboxed locations: +**Original sandboxed paths:** +- App data: `~/Library/Containers/com.remarkable.desktop/Data` +- Documents: `~/Library/Containers/com.remarkable.desktop/Data/Library/Application Support/remarkable` + +**After re-signing, the app is no longer sandboxed** and will use standard paths: +- Config: `~/Library/Preferences/rmfakecloud.config` +- Documents: `~/Library/Application Support/remarkable` + +#### Migration options + +**Option 1: Create a symbolic link** (recommended) +```bash +ln -s ~/Library/Containers/com.remarkable.desktop/Data/Library/Application\ Support/remarkable \ + ~/Library/Application\ Support/remarkable +``` +The symbolic link approach allows you to keep using the original App Store version alongside the patched version. + +**Option 2: Move files** +```bash +mv ~/Library/Containers/com.remarkable.desktop/Data/Library/Application\ Support/remarkable \ + ~/Library/Application\ Support/remarkable +``` + +### Step 4: Configure rmfakecloud server +Quickly access the configuration file from the app's Help menu: +![help-config.png](docs/help-config.png) +Edit the configuration file at: +``` +~/Library/Preferences/rmfakecloud.config +``` + +Example configuration: +```json +{ + "host": "your-server.example.com", + "port": 443 +} +``` + +### Step 5: Launch the patched app :p + +## How it works +RMHook uses [tinyhook](https://github.com/Antibioticss/tinyhook/) to hook into Qt framework functions at runtime: +1. **QNetworkAccessManager::createRequest** - Intercepts HTTP/HTTPS requests +2. **QWebSocket::open** - Patches WebSocket connections + +When the app attempts to connect to reMarkable's servers (e.g., `internal.cloud.remarkable.com`), the hooks redirect these requests to your configured host and port. + +## Configuration + +The config file (`~/Library/Preferences/rmfakecloud.config`) supports the following keys: +| Key | Type | Default | Description | +|--------|---------|-------------------|--------------------------------| +| `host` | String | `example.com` | Your rmfakecloud server host | +| `port` | Number | `443` | Your rmfakecloud server port | + +If the config file doesn't exist, it will be created automatically with default values on first launch. + +## Troubleshooting + +### App won't launch +- Ensure the code signature was properly applied +- Check that `xattr -cr` was run to clear quarantine attributes +- Verify the dylib is in `Contents/Resources/` folder + +### Document sync issues +- Ensure your rmfakecloud server is running and accessible +- Verify the storage path migration was completed + +## Credits +- **tinyhook**: [Antibioticss/tinyhook](https://github.com/Antibioticss/tinyhook/) - Function hooking framework +- **rmfakecloud**: [ddvk/rmfakecloud](https://github.com/ddvk/rmfakecloud) - Self-hosted reMarkable cloud +- **optool**: [alexzielenski/optool](https://github.com/alexzielenski/optool) - Mach-O binary modification tool + +## License + +This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details. + +## Disclaimer + +This project is not affiliated with, endorsed by, or sponsored by reMarkable AS. Use at your own risk. This tool modifies the reMarkable Desktop application and may violate the application's terms of service. + +## Contributing + +Contributions are welcome! Please feel free to submit issues or pull requests. + +## Building + +1. **Clone the repository:** +```bash +git clone http://github.com/NohamR/RMHook +cd RMHook +``` + +2. **Compile the dylib:** +```bash +./scripts/build.sh +``` \ No newline at end of file diff --git a/docs/help-config.png b/docs/help-config.png new file mode 100644 index 0000000..9b987c1 Binary files /dev/null and b/docs/help-config.png differ diff --git a/libs/libtinyhook.a b/libs/libtinyhook.a new file mode 100644 index 0000000..8312cfb Binary files /dev/null and b/libs/libtinyhook.a differ diff --git a/scripts/build.sh b/scripts/build.sh new file mode 100755 index 0000000..d58d02e --- /dev/null +++ b/scripts/build.sh @@ -0,0 +1,39 @@ +#!/bin/bash +# Script to compile the reMarkable dylib + +# 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..." +echo "📦 Qt path: $QT_PATH" + +# Create build directories if necessary +mkdir -p "$PROJECT_DIR/build" +cd "$PROJECT_DIR/build" + +# Configure with CMake and compile +if [ -d "$QT_PATH" ]; then + cmake -DCMAKE_PREFIX_PATH="$QT_PATH" .. +else + echo "⚠️ Qt not found at $QT_PATH, trying without specifying path..." + cmake .. +fi + +make $APP_NAME + +if [ $? -eq 0 ]; then + echo "" + echo "✅ Compilation successful!" + echo "📍 Dylib: $PROJECT_DIR/build/dylibs/$APP_NAME.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 "" +else + echo "❌ Compilation failed" + exit 1 +fi \ No newline at end of file diff --git a/scripts/inject.sh b/scripts/inject.sh new file mode 100755 index 0000000..f60e8ae --- /dev/null +++ b/scripts/inject.sh @@ -0,0 +1,85 @@ +#!/bin/bash + +set -e + +# Function to display usage instructions +usage() { + echo "Usage: $0 " + echo " dylib - The dynamic library to inject." + echo " app_path - The path to the .app bundle." + exit 1 +} + +# Ensure required arguments are provided +if [ "$#" -ne 2 ]; then + echo "[ERROR] Incorrect number of arguments." + usage +fi + +DYLIB=$1 +APP_PATH=$2 + +# Validate inputs +if [ ! -f "$DYLIB" ]; then + echo "[ERROR] The specified dynamic library ($DYLIB) does not exist." + exit 1 +fi + +if [ ! -d "$APP_PATH" ]; then + echo "[ERROR] The specified app path ($APP_PATH) does not exist." + exit 1 +fi + +INFO_PLIST_PATH="$APP_PATH/Contents/Info.plist" +if [ ! -f "$INFO_PLIST_PATH" ]; then + echo "[ERROR] Info.plist not found at $INFO_PLIST_PATH" + exit 1 +fi + +# Get the executable name from Info.plist +APP_NAME=$(/usr/libexec/PlistBuddy -c "Print CFBundleExecutable" "$INFO_PLIST_PATH") +if [ -z "$APP_NAME" ]; then + echo "[ERROR] Could not read CFBundleExecutable from $INFO_PLIST_PATH" + exit 1 +fi + +echo "[INFO] Executable name: $APP_NAME" + +EXECUTABLE_PATH="$APP_PATH/Contents/MacOS/$APP_NAME" +if [ ! -f "$EXECUTABLE_PATH" ]; then + echo "[ERROR] The specified executable ($EXECUTABLE_PATH) does not exist." + exit 1 +fi + +mkdir -p "$APP_PATH/Contents/Resources/" + +# Copy the dylib to the Resources folder +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" +echo "[INFO] Injected $DYLIB into $EXECUTABLE_PATH" + +sudo codesign --remove-signature "$EXECUTABLE_PATH" +sudo xattr -cr "$EXECUTABLE_PATH" +sudo codesign -f -s - --timestamp=none --all-architectures "$EXECUTABLE_PATH" +sudo xattr -cr "$EXECUTABLE_PATH" + +echo "Signed successfully." + +# Change ownership to current user +CURRENT_USER=$(whoami) +CURRENT_GROUP=$(id -gn) +echo "Changing ownership to $CURRENT_USER:$CURRENT_GROUP for $APP_PATH ..." +sudo chown -R "$CURRENT_USER:$CURRENT_GROUP" "$APP_PATH" + +# Remove _MASReceipt if it exists +RECEIPT_PATH="$APP_PATH/Contents/_MASReceipt" +if [ -d "$RECEIPT_PATH" ]; then + echo "Removing _MASReceipt..." + rm -r "$RECEIPT_PATH" +else + echo "No _MASReceipt directory found." +fi + +exit 0 \ No newline at end of file diff --git a/src/core/tinyhook.h b/src/core/tinyhook.h new file mode 100644 index 0000000..88eba0f --- /dev/null +++ b/src/core/tinyhook.h @@ -0,0 +1,51 @@ +#ifndef tinyhook_h +#define tinyhook_h + +#include +#include +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +#define CLASS_METHOD 0 +#define INSTANCE_METHOD 1 + +/* inline hook */ +int tiny_hook(void *function, void *destination, void **origin); + +int tiny_insert(void *address, void *destination, bool link); + +int tiny_insert_far(void *address, void *destination, bool link); + +/* objective-c runtime */ +int ocrt_hook(const char *cls, const char *sel, void *destination, void **origin); + +int ocrt_swap(const char *cls1, const char *sel1, const char *cls2, const char *sel2); + +void *ocrt_impl(const char *cls, const char *sel, bool type); + +Method ocrt_method(const char *cls, const char *sel, bool type); + +/* memory access */ +int read_mem(void *destination, const void *source, size_t len); + +int write_mem(void *destination, const void *source, size_t len); + +/* solve symbol */ +void *symtbl_solve(uint32_t image_index, const char *symbol_name); + +void *symexp_solve(uint32_t image_index, const char *symbol_name); + +/* find in memory */ +// int find_code(uint32_t image_index, const unsigned char *code, size_t len, int count, void **out); + +int find_data(void *start, void *end, const unsigned char *data, size_t len, int count, void **out); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/src/core/tinyhook/memory.c b/src/core/tinyhook/memory.c new file mode 100644 index 0000000..70b26d3 --- /dev/null +++ b/src/core/tinyhook/memory.c @@ -0,0 +1,39 @@ +#include // mach_task_self() +#include // mach_vm_* +#include // memcpy() + +#ifndef COMPACT +#include // mach_error_string() +#include // fprintf() +#endif + +#include "../include/tinyhook.h" + +int read_mem(void *destination, const void *source, size_t len) { + int kr = 0; + vm_offset_t data; + mach_msg_type_number_t dataCnt; + kr |= mach_vm_read(mach_task_self(), (mach_vm_address_t)source, len, &data, &dataCnt); + memcpy((unsigned char *)destination, (unsigned char *)data, dataCnt); + kr |= mach_vm_deallocate(mach_task_self(), data, dataCnt); +#ifndef COMPACT + if (kr != 0) { + fprintf(stderr, "read_mem: %s\n", mach_error_string(kr)); + } +#endif + return kr; +} + +int write_mem(void *destination, const void *source, size_t len) { + int kr = 0; + kr |= mach_vm_protect(mach_task_self(), (mach_vm_address_t)destination, len, FALSE, + VM_PROT_READ | VM_PROT_WRITE | VM_PROT_COPY); + kr |= mach_vm_write(mach_task_self(), (mach_vm_address_t)destination, (vm_offset_t)source, len); + kr |= mach_vm_protect(mach_task_self(), (mach_vm_address_t)destination, len, FALSE, VM_PROT_READ | VM_PROT_EXECUTE); +#ifndef COMPACT + if (kr != 0) { + fprintf(stderr, "write_mem: %s\n", mach_error_string(kr)); + } +#endif + return kr; +} \ No newline at end of file diff --git a/src/core/tinyhook/objcrt.c b/src/core/tinyhook/objcrt.c new file mode 100644 index 0000000..3b2ffb5 --- /dev/null +++ b/src/core/tinyhook/objcrt.c @@ -0,0 +1,66 @@ +#include // objc_*, ... + +#ifndef COMPACT +#include // mach_error_string() +#include // fprintf() +#endif + +#include "../include/tinyhook.h" + +Method ocrt_method(const char *cls, const char *sel, bool type) { + Method oc_method = NULL; + Class oc_class = objc_getClass(cls); + SEL oc_selector = sel_registerName(sel); + if (type == CLASS_METHOD) { + oc_method = class_getClassMethod(oc_class, oc_selector); + } else if (type == INSTANCE_METHOD) { + oc_method = class_getInstanceMethod(oc_class, oc_selector); + } +#ifndef COMPACT + else { + fprintf(stderr, "ocrt_method: invalid method type: %d\n", type); + } +#endif + return oc_method; +} + +void *ocrt_impl(const char *cls, const char *sel, bool type) { + return method_getImplementation(ocrt_method(cls, sel, type)); +} + +static Method ensure_method(const char *cls, const char *sel); + +int ocrt_swap(const char *cls1, const char *sel1, const char *cls2, const char *sel2) { + Method oc_method1 = ensure_method(cls1, sel1); + Method oc_method2 = ensure_method(cls2, sel2); + if (oc_method1 == NULL || oc_method2 == NULL) { + return 1; + } + method_exchangeImplementations(oc_method1, oc_method2); + return 0; +} + +int ocrt_hook(const char *cls, const char *sel, void *destination, void **origin) { + Method oc_method = ensure_method(cls, sel); + if (oc_method == NULL) { + return 1; + } + void *origin_imp = method_setImplementation(oc_method, destination); + if (origin != NULL) { + *origin = origin_imp; + } + return 0; +} + +static Method ensure_method(const char *cls, const char *sel) { + Method oc_method = ocrt_method(cls, sel, CLASS_METHOD); + if (oc_method == NULL) { + oc_method = ocrt_method(cls, sel, INSTANCE_METHOD); + } +#ifndef COMPACT + if (oc_method == NULL) { + fprintf(stderr, "ensure_method: method not found!\n"); + } +#endif + return oc_method; +} \ No newline at end of file diff --git a/src/core/tinyhook/search.c b/src/core/tinyhook/search.c new file mode 100644 index 0000000..e5f7192 --- /dev/null +++ b/src/core/tinyhook/search.c @@ -0,0 +1,23 @@ +#ifndef COMPACT +#include // fprintf() +#endif + +#include "skip/skip.h" + +#include "../include/tinyhook.h" + +int find_data(void *start, void *end, const unsigned char *data, size_t len, int count, void **out) { + int matched; + skipidx_t idx; + skip_init(&idx, len, data); + matched = skip_match(&idx, start, end, count, (offset_t *)out); + skip_release(&idx); +#ifndef COMPACT + if (matched == 0) { + fprintf(stderr, "find_data: data not found!\n"); + } +#endif + return matched; +} + +// int find_code(uint32_t image_index, const unsigned char *code, size_t len, int count, void **out); \ No newline at end of file diff --git a/src/core/tinyhook/symsolve/symexport.c b/src/core/tinyhook/symsolve/symexport.c new file mode 100644 index 0000000..43151a4 --- /dev/null +++ b/src/core/tinyhook/symsolve/symexport.c @@ -0,0 +1,121 @@ +#include // _dyld_* +#include // mach_header_64, load_command... +#include // nlist_64 +#include // strcmp() + +#ifndef COMPACT +#include // fprintf() +#endif + +#include "../../include/tinyhook.h" + +static void *trie_query(const uint8_t *export, const char *name); + +void *symexp_solve(uint32_t image_index, const char *symbol_name) { + void *symbol_address = NULL; + intptr_t image_slide = _dyld_get_image_vmaddr_slide(image_index); + struct mach_header_64 *mh_header = (struct mach_header_64 *)_dyld_get_image_header(image_index); + struct load_command *ld_command = (void *)mh_header + sizeof(struct mach_header_64); +#ifndef COMPACT + if (mh_header == NULL) { + fprintf(stderr, "symexp_solve: image_index out of range!\n"); + } +#endif + struct dyld_info_command *dyldinfo_cmd = NULL; + struct segment_command_64 *linkedit_cmd = NULL; + for (int i = 0; i < mh_header->ncmds; i++) { + if (ld_command->cmd == LC_SEGMENT_64) { + const struct segment_command_64 *segment = (struct segment_command_64 *)ld_command; + if (strcmp(segment->segname, "__LINKEDIT") == 0) { + linkedit_cmd = (struct segment_command_64 *)ld_command; + } + } else if (ld_command->cmd == LC_DYLD_INFO_ONLY || ld_command->cmd == LC_DYLD_INFO) { + dyldinfo_cmd = (struct dyld_info_command *)ld_command; + if (linkedit_cmd != NULL) { + break; + } + } + ld_command = (void *)ld_command + ld_command->cmdsize; + } + if (dyldinfo_cmd == NULL) { +#ifndef COMPACT + fprintf(stderr, "symexp_solve: LC_DYLD_INFO_ONLY segment not found!\n"); +#endif + return NULL; + } + // stroff and strtbl are in the __LINKEDIT segment + // Its offset will change when loaded into the memory, so we need to add this slide + intptr_t linkedit_slide = linkedit_cmd->vmaddr - linkedit_cmd->fileoff; + uint8_t *export_offset = (uint8_t *)image_slide + linkedit_slide + dyldinfo_cmd->export_off; + symbol_address = trie_query(export_offset, symbol_name); + + if (symbol_address != NULL) { + symbol_address += image_slide; + } +#ifndef COMPACT + else { + fprintf(stderr, "symexp_solve: symbol not found!\n"); + } +#endif + return symbol_address; +} + +inline uint64_t read_uleb128(const uint8_t **p) { + int bit = 0; + uint64_t result = 0; + do { + uint64_t slice = **p & 0x7f; + result |= (slice << bit); + bit += 7; + } while (*(*p)++ & 0x80); + return result; +} + +static void *trie_query(const uint8_t *export, const char *name) { + // most comments below are copied from , not AI generated :P + // a trie node starts with a uleb128 stored the lenth of the exported symbol information + uint64_t node_off = 0; + const char *rest_name = name; + void *symbol_address = NULL; + bool go_child = true; + while (go_child) { + const uint8_t *cur_pos = export + node_off; + uint64_t info_len = read_uleb128(&cur_pos); + // the exported symbol information is followed by the child edges + const uint8_t *child_off = cur_pos + info_len; + + if (rest_name[0] == '\0') { + if (info_len != 0) { + // first is a uleb128 containing flags + uint64_t flag = read_uleb128(&cur_pos); + if (flag == EXPORT_SYMBOL_FLAGS_KIND_REGULAR) { + // normally, it is followed by a uleb128 encoded function offset + uint64_t symbol_off = read_uleb128(&cur_pos); + symbol_address = (void *)symbol_off; + } + } + break; + } else { + go_child = false; + cur_pos = child_off; + // child edges start with a byte of how many edges (0-255) this node has + uint8_t child_count = *(uint8_t *)cur_pos++; + // then followed by each edge. + for (int i = 0; i < child_count; i++) { + // each edge is a zero terminated UTF8 of the addition chars + char *cur_str = (char *)cur_pos; + size_t cur_len = strlen(cur_str); + cur_pos += cur_len + 1; + // then followed by a uleb128 offset for the node that edge points to + uint64_t next_off = read_uleb128(&cur_pos); + if (strncmp(rest_name, cur_str, cur_len) == 0) { + go_child = true; + rest_name += cur_len; + node_off = next_off; + break; + } + } + } + } + return symbol_address; +} \ No newline at end of file diff --git a/src/core/tinyhook/symsolve/symtable.c b/src/core/tinyhook/symsolve/symtable.c new file mode 100644 index 0000000..e04b0d8 --- /dev/null +++ b/src/core/tinyhook/symsolve/symtable.c @@ -0,0 +1,60 @@ +#include // _dyld_* +#include // mach_header_64, load_command... +#include // nlist_64 +#include // strcmp() + +#ifndef COMPACT +#include // fprintf() +#endif + +#include "../../include/tinyhook.h" + +void *symtbl_solve(uint32_t image_index, const char *symbol_name) { + void *symbol_address = NULL; + intptr_t image_slide = _dyld_get_image_vmaddr_slide(image_index); + struct mach_header_64 *mh_header = (struct mach_header_64 *)_dyld_get_image_header(image_index); + struct load_command *ld_command = (void *)mh_header + sizeof(struct mach_header_64); +#ifndef COMPACT + if (mh_header == NULL) { + fprintf(stderr, "symtbl_solve: image_index out of range!\n"); + } +#endif + struct symtab_command *symtab_cmd = NULL; + struct segment_command_64 *linkedit_cmd = NULL; + for (int i = 0; i < mh_header->ncmds; i++) { + if (ld_command->cmd == LC_SEGMENT_64) { + const struct segment_command_64 *segment = (struct segment_command_64 *)ld_command; + if (strcmp(segment->segname, "__LINKEDIT") == 0) { + linkedit_cmd = (struct segment_command_64 *)ld_command; + } + } else if (ld_command->cmd == LC_SYMTAB) { + symtab_cmd = (struct symtab_command *)ld_command; + if (linkedit_cmd != NULL) { + break; + } + } + ld_command = (void *)ld_command + ld_command->cmdsize; + } + // stroff and strtbl are in the __LINKEDIT segment + // Its offset will change when loaded into the memory, so we need to add this slide + intptr_t linkedit_slide = linkedit_cmd->vmaddr - linkedit_cmd->fileoff; + struct nlist_64 *nl_tbl = (void *)image_slide + linkedit_slide + symtab_cmd->symoff; + char *str_tbl = (void *)image_slide + linkedit_slide + symtab_cmd->stroff; + for (int j = 0; j < symtab_cmd->nsyms; j++) { + if ((nl_tbl[j].n_type & N_TYPE) == N_SECT) { + if (strcmp(symbol_name, str_tbl + nl_tbl[j].n_un.n_strx) == 0) { + symbol_address = (void *)nl_tbl[j].n_value; + break; + } + } + } + if (symbol_address != NULL) { + symbol_address += image_slide; + } +#ifndef COMPACT + else { + fprintf(stderr, "symtbl_solve: symbol not found!\n"); + } +#endif + return symbol_address; +} \ No newline at end of file diff --git a/src/core/tinyhook/tinyhook.c b/src/core/tinyhook/tinyhook.c new file mode 100644 index 0000000..bbce0da --- /dev/null +++ b/src/core/tinyhook/tinyhook.c @@ -0,0 +1,183 @@ +#include // mach_task_self() +#include // mach_vm_* +#include // atexit() +#include // memcpy() + +#ifndef COMPACT +#include // mach_error_string() +#include // fprintf() +#endif + +#ifdef __x86_64__ +#include "fde64/fde64.h" +#endif + +#include "../include/tinyhook.h" + +#define MB (1ll << 20) +#define GB (1ll << 30) + +#ifdef __aarch64__ +#define AARCH64_B 0x14000000 // b +0 +#define AARCH64_BL 0x94000000 // bl +0 +#define AARCH64_ADRP 0x90000011 // adrp x17, 0 +#define AARCH64_BR 0xd61f0220 // br x17 +#define AARCH64_BLR 0xd63f0220 // blr x17 +#define AARCH64_ADD 0x91000231 // add x17, x17, 0 +#define AARCH64_SUB 0xd1000231 // sub x17, x17, 0 + +#define MAX_JUMP_SIZE 12 + +#elif __x86_64__ +#define X86_64_CALL 0xe8 // call +#define X86_64_JMP 0xe9 // jmp +#define X86_64_JMP_RIP 0x000025ff // jmp [rip] +#define X86_64_CALL_RIP 0x000015ff // call [rip] +#define X86_64_MOV_RI64 0xb848 // mov r64, m64 +#define X86_64_MOV_RM64 0x8b48 // mov r64, [r64] + +#define MAX_JUMP_SIZE 14 +#endif + +int tiny_insert(void *address, void *destination, bool link) { + size_t jump_size; + int assembly; + unsigned char bytes[MAX_JUMP_SIZE]; +#ifdef __aarch64__ + // b/bl imm ; go to destination + jump_size = 4; + assembly = (destination - address) >> 2 & 0x3ffffff; + assembly |= link ? AARCH64_BL : AARCH64_B; + *(int *)bytes = assembly; +#elif __x86_64__ + // jmp/call imm ; go to destination + jump_size = 5; + *bytes = link ? X86_64_CALL : X86_64_JMP; + assembly = (long)destination - (long)address - 5; + *(int *)(bytes + 1) = assembly; +#endif + write_mem(address, bytes, jump_size); + return jump_size; +} + +int tiny_insert_far(void *address, void *destination, bool link) { + size_t jump_size; + unsigned char bytes[MAX_JUMP_SIZE]; +#ifdef __aarch64__ + // adrp x17, imm + // add x17, x17, imm ; x17 -> destination + // br/blr x17 + jump_size = 12; + int assembly; + assembly = (((long)destination >> 12) - ((long)address >> 12)) & 0x1fffff; + assembly = ((assembly & 0x3) << 29) | (assembly >> 2 << 5) | AARCH64_ADRP; + *(int *)bytes = assembly; + assembly = ((long)destination & 0xfff) << 10 | AARCH64_ADD; + *(int *)(bytes + 4) = assembly; + *(int *)(bytes + 8) = link ? AARCH64_BLR : AARCH64_BR; +#elif __x86_64__ + jump_size = 14; + // jmp [rip] ; rip stored destination + *(int *)bytes = link ? X86_64_CALL_RIP : X86_64_JMP_RIP; + bytes[5] = bytes[6] = 0; + *(long long *)(bytes + 6) = (long long)destination; +#endif + write_mem(address, bytes, jump_size); + return jump_size; +} + +int position = 0; +mach_vm_address_t vm; + +static int get_jump_size(void *address, void *destination); +static int insert_jump(void *address, void *destination); +static int save_header(void *address, void *destination, int *skip_len); + +int tiny_hook(void *function, void *destination, void **origin) { + int kr = 0; + if (origin == NULL) + insert_jump(function, destination); + else { + if (!position) { + // alloc a vm to store headers and jumps + kr = mach_vm_allocate(mach_task_self(), &vm, PAGE_SIZE, VM_FLAGS_ANYWHERE); +#ifndef COMPACT + if (kr != 0) { + fprintf(stderr, "mach_vm_allocate: %s\n", mach_error_string(kr)); + } +#endif + } + int skip_len; + *origin = (void *)(vm + position); + position += save_header(function, (void *)(vm + position), &skip_len); + position += insert_jump((void *)(vm + position), function + skip_len); + insert_jump(function, destination); + } + return kr; +} + +static int get_jump_size(void *address, void *destination) { + long long distance = destination > address ? destination - address : address - destination; +#ifdef __aarch64__ + return distance < 128 * MB ? 4 : 12; +#elif __x86_64__ + return distance < 2 * GB ? 5 : 14; +#endif +} + +static int insert_jump(void *address, void *destination) { + if (get_jump_size(address, destination) <= 5) + return tiny_insert(address, destination, false); + else + return tiny_insert_far(address, destination, false); +} + +static int save_header(void *address, void *destination, int *skip_len) { + int header_len = 0; +#ifdef __aarch64__ + header_len = *skip_len = get_jump_size(address, destination); + unsigned char bytes_out[MAX_JUMP_SIZE]; + read_mem(bytes_out, address, MAX_JUMP_SIZE); + for (int i = 0; i < header_len; i += 4) { + int cur_asm = *(int *)(bytes_out + i); + long cur_addr = (long)address + i, cur_dst = (long)destination + i; + if (((cur_asm ^ 0x90000000) & 0x9f000000) == 0) { + // adrp + // modify the immediate + int len = (cur_asm >> 29 & 0x3) | ((cur_asm >> 3) & 0x1ffffc); + len += (cur_addr >> 12) - (cur_dst >> 12); + cur_asm &= 0x9f00001f; + cur_asm = ((len & 0x3) << 29) | (len >> 2 << 5) | cur_asm; + *(int *)(bytes_out + i) = cur_asm; + } + } +#elif __x86_64__ + int min_len; + struct fde64s assembly; + unsigned char bytes_in[MAX_JUMP_SIZE * 2], bytes_out[MAX_JUMP_SIZE * 4]; + read_mem(bytes_in, address, MAX_JUMP_SIZE * 2); + min_len = get_jump_size(address, destination); + for (*skip_len = 0; *skip_len < min_len; *skip_len += assembly.len) { + long long cur_addr = (long long)address + *skip_len; + decode(bytes_in + *skip_len, &assembly); + if (assembly.opcode == 0x8B && assembly.modrm_rm == 0b101) { + // mov r64, [rip+] + // split it into 2 instructions + // mov r64 $rip+(immediate) + // mov r64 [r64] + *(short *)(bytes_out + header_len) = X86_64_MOV_RI64; + bytes_out[header_len + 1] += assembly.modrm_reg; + *(long long *)(bytes_out + header_len + 2) = assembly.disp32 + cur_addr + assembly.len; + header_len += 10; + *(short *)(bytes_out + header_len) = X86_64_MOV_RM64; + bytes_out[header_len + 2] = assembly.modrm_reg << 3 | assembly.modrm_reg; + header_len += 3; + } else { + memcpy(bytes_out + header_len, bytes_in + *skip_len, assembly.len); + header_len += assembly.len; + } + } +#endif + write_mem(destination, bytes_out, header_len); + return header_len; +} \ No newline at end of file diff --git a/src/reMarkable/reMarkable.h b/src/reMarkable/reMarkable.h new file mode 100644 index 0000000..ba2cdcb --- /dev/null +++ b/src/reMarkable/reMarkable.h @@ -0,0 +1,5 @@ +#import + +@interface reMarkable : NSObject + +@end \ No newline at end of file diff --git a/src/reMarkable/reMarkable.m b/src/reMarkable/reMarkable.m new file mode 100644 index 0000000..c789c40 --- /dev/null +++ b/src/reMarkable/reMarkable.m @@ -0,0 +1,390 @@ +#import "reMarkable.h" +#import +#import "Constant.h" +#import "MemoryUtils.h" +#import "Logger.h" +#import "ResourceUtils.h" +#import +#import +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +static NSString *const kReMarkableConfigFileName = @"rmfakecloud.config"; +static NSString *const kReMarkableConfigHostKey = @"host"; +static NSString *const kReMarkableConfigPortKey = @"port"; +static NSString *const kReMarkableDefaultHost = @"example.com"; +static NSNumber *const kReMarkableDefaultPort = @(443); + +static NSString *gConfiguredHost = @"example.com"; +static NSNumber *gConfiguredPort = @(443); +static pthread_mutex_t gResourceMutex = PTHREAD_MUTEX_INITIALIZER; + +static NSString *ReMarkablePreferencesDirectory(void); + +static NSString *ReMarkablePreferencesDirectory(void) { + NSArray *libraryPaths = NSSearchPathForDirectoriesInDomains(NSLibraryDirectory, NSUserDomainMask, YES); + NSString *libraryDir = [libraryPaths firstObject]; + if (![libraryDir length]) { + libraryDir = [NSHomeDirectory() stringByAppendingPathComponent:@"Library"]; + } + return [libraryDir stringByAppendingPathComponent:@"Preferences"]; +} + +static NSString *ReMarkableConfigFilePath(void) { + return [ReMarkablePreferencesDirectory() stringByAppendingPathComponent:kReMarkableConfigFileName]; +} + +static BOOL ReMarkableWriteConfig(NSString *path, NSDictionary *config) { + NSError *error = nil; + NSData *jsonData = [NSJSONSerialization dataWithJSONObject:config options:NSJSONWritingPrettyPrinted error:&error]; + if (!jsonData || error) { + NSLogger(@"[reMarkable] Failed to serialize config: %@", error); + return NO; + } + if (![jsonData writeToFile:path atomically:YES]) { + NSLogger(@"[reMarkable] Failed to write config file at %@", path); + return NO; + } + return YES; +} + +static void ReMarkableLoadOrCreateConfig(void) { + NSString *configPath = ReMarkableConfigFilePath(); + NSString *directory = [configPath stringByDeletingLastPathComponent]; + NSFileManager *fileManager = [NSFileManager defaultManager]; + BOOL isDirectory = NO; + NSError *error = nil; + + if (![fileManager fileExistsAtPath:directory isDirectory:&isDirectory] || !isDirectory) { + if (![fileManager createDirectoryAtPath:directory withIntermediateDirectories:YES attributes:nil error:&error]) { + NSLogger(@"[reMarkable] Failed to create config directory %@: %@", directory, error); + } + } + + NSDictionary *defaults = @{kReMarkableConfigHostKey : kReMarkableDefaultHost, + kReMarkableConfigPortKey : kReMarkableDefaultPort}; + + if ([fileManager fileExistsAtPath:configPath isDirectory:&isDirectory] && !isDirectory) { + NSData *data = [NSData dataWithContentsOfFile:configPath]; + if ([data length] > 0) { + NSError *jsonError = nil; + id jsonObject = [NSJSONSerialization JSONObjectWithData:data options:0 error:&jsonError]; + if (!jsonError && [jsonObject isKindOfClass:[NSDictionary class]]) { + NSDictionary *configDict = (NSDictionary *)jsonObject; + NSString *hostValue = configDict[kReMarkableConfigHostKey]; + NSNumber *portValue = configDict[kReMarkableConfigPortKey]; + + NSString *resolvedHost = ([hostValue isKindOfClass:[NSString class]] && [hostValue length]) ? hostValue : kReMarkableDefaultHost; + NSInteger portCandidate = kReMarkableDefaultPort.integerValue; + if ([portValue respondsToSelector:@selector(integerValue)]) { + NSInteger candidate = [portValue integerValue]; + if (candidate > 0 && candidate <= 65535) { + portCandidate = candidate; + } else { + NSLogger(@"[reMarkable] Ignoring invalid port value %@, falling back to default.", portValue); + } + } + + gConfiguredHost = [resolvedHost copy]; + gConfiguredPort = @(portCandidate); + NSLogger(@"[reMarkable] Loaded config from %@ with host %@ and port %@", configPath, gConfiguredHost, gConfiguredPort); + return; + } else { + NSLogger(@"[reMarkable] Failed to parse config file %@: %@", configPath, jsonError); + } + } else { + NSLogger(@"[reMarkable] Config file %@ was empty, rewriting with defaults.", configPath); + } + } + + if (ReMarkableWriteConfig(configPath, defaults)) { + NSLogger(@"[reMarkable] Created default config at %@", configPath); + } + gConfiguredHost = [kReMarkableDefaultHost copy]; + gConfiguredPort = kReMarkableDefaultPort; +} + +static inline QString QStringFromNSStringSafe(NSString *string) { + if (!string) { + return QString(); + } + return QString::fromUtf8([string UTF8String]); +} + +@interface MenuActionController : NSObject +@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]; + + // 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]; +} + +@end + +@implementation reMarkableDylib + +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; + +static int (*original_qRegisterResourceData)( + int, + const unsigned char *, + const unsigned char *, + const unsigned char *) = NULL; + +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); +} + +- (BOOL)hook { + NSLogger(@"[reMarkable] Starting hooks..."); + + ReMarkableLoadOrCreateConfig(); + 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]"]; + + // WIP: Implement resource data registration hooking + // [MemoryUtils hookSymbol:@"QtCore" + // symbolName:@"__Z21qRegisterResourceDataiPKhS0_S0_" + // hookFunction:(void *)hooked_qRegisterResourceData + // originalFunction:(void **)&original_qRegisterResourceData + // logPrefix:@"[reMarkable]"]; + + return YES; +} + +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); + + 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); + + original_qWebSocket_open(self, newReq); + return; + } + + original_qWebSocket_open(self, req); +} + +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, + }; + + statArchive(&resource, 0); + processNode(&resource, 0, ""); + resource.tree = (uint8_t *)malloc(resource.treeSize); + if (resource.tree) { + memcpy(resource.tree, tree, resource.treeSize); + } + + NSLogger(@"[reMarkable] Registering Qt resource version %d tree:%p (size:%zu) name:%p (size:%zu) data:%p (size:%zu)", + version, tree, resource.treeSize, name, resource.nameSize, data, resource.dataSize); + + int status = original_qRegisterResourceData(version, tree, name, data); + pthread_mutex_unlock(&gResourceMutex); + if (resource.tree) { + free(resource.tree); + } + return status; +} + +@end \ No newline at end of file diff --git a/src/utils/Constant.h b/src/utils/Constant.h new file mode 100644 index 0000000..5659eb0 --- /dev/null +++ b/src/utils/Constant.h @@ -0,0 +1,7 @@ +#import + +@interface Constant : NSObject + ++ (NSString *)getCurrentAppPath; + +@end diff --git a/src/utils/Constant.m b/src/utils/Constant.m new file mode 100644 index 0000000..ece64be --- /dev/null +++ b/src/utils/Constant.m @@ -0,0 +1,25 @@ +#import +#import "Constant.h" +#import +#import "Logger.h" + +@implementation Constant + +static NSString *_currentAppPath; + ++ (void)initialize { + if (self == [Constant class]) { + NSLogger(@"[Constant] Initializing..."); + + NSBundle *app = [NSBundle mainBundle]; + _currentAppPath = [[app bundlePath] copy]; + + NSLogger(@"[Constant] App path: %@", _currentAppPath); + } +} + ++ (NSString *)getCurrentAppPath { + return _currentAppPath; +} + +@end diff --git a/src/utils/Logger.h b/src/utils/Logger.h new file mode 100644 index 0000000..5ab39ee --- /dev/null +++ b/src/utils/Logger.h @@ -0,0 +1,6 @@ +#ifndef Logger_h +#define Logger_h + +#define NSLogger(fmt, ...) NSLog((fmt), ##__VA_ARGS__) + +#endif /* Logger_h */ diff --git a/src/utils/MemoryUtils.h b/src/utils/MemoryUtils.h new file mode 100644 index 0000000..5836d18 --- /dev/null +++ b/src/utils/MemoryUtils.h @@ -0,0 +1,33 @@ +#import + +@interface MemoryUtils : NSObject + +/** + * Hooks a function by symbol name with automatic fallback from symtbl_solve to symexp_solve. + * + * @param imageName The name of the image/library to search in (e.g., "QtNetwork"). + * @param symbolName The mangled symbol name to hook. + * @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)hookSymbol:(NSString *)imageName + symbolName:(NSString *)symbolName + hookFunction:(void *)hookFunction + originalFunction:(void **)originalFunction + logPrefix:(NSString *)logPrefix; + +/** + * Hooks a function by symbol name with delay support. + * + * @param delayInSeconds The delay in seconds before installing the hook (use 0 for immediate hooking). + */ ++ (BOOL)hookSymbol:(NSString *)imageName + symbolName:(NSString *)symbolName + hookFunction:(void *)hookFunction + originalFunction:(void **)originalFunction + logPrefix:(NSString *)logPrefix + delayInSeconds:(NSTimeInterval)delayInSeconds; + +@end diff --git a/src/utils/MemoryUtils.m b/src/utils/MemoryUtils.m new file mode 100644 index 0000000..76c6608 --- /dev/null +++ b/src/utils/MemoryUtils.m @@ -0,0 +1,106 @@ +#import +#import "MemoryUtils.h" +#import +#import "Logger.h" +#import "tinyhook.h" + +@implementation MemoryUtils + ++ (int)indexForImageWithName:(NSString *)imageName { + uint32_t imageCount = _dyld_image_count(); + for (uint32_t i = 0; i < imageCount; i++) { + const char* currentImageName = _dyld_get_image_name(i); + NSString *currentImageNameString = [NSString stringWithUTF8String:currentImageName]; + + if ([currentImageNameString.lastPathComponent isEqualToString:imageName]) { + return i; + } + } + + return -1; +} + ++ (BOOL)hookSymbol:(NSString *)imageName + symbolName:(NSString *)symbolName + hookFunction:(void *)hookFunction + originalFunction:(void **)originalFunction + logPrefix:(NSString *)logPrefix { + return [self hookSymbol:imageName + symbolName:symbolName + hookFunction:hookFunction + originalFunction:originalFunction + logPrefix:logPrefix + delayInSeconds:0]; +} + ++ (BOOL)hookSymbol:(NSString *)imageName + symbolName:(NSString *)symbolName + hookFunction:(void *)hookFunction + originalFunction:(void **)originalFunction + logPrefix:(NSString *)logPrefix + delayInSeconds:(NSTimeInterval)delayInSeconds { + + if (delayInSeconds > 0) { + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayInSeconds * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ + [self performHookSymbol:imageName + symbolName:symbolName + hookFunction:hookFunction + originalFunction:originalFunction + logPrefix:logPrefix]; + }); + return YES; + } else { + return [self performHookSymbol:imageName + symbolName:symbolName + hookFunction:hookFunction + originalFunction:originalFunction + logPrefix:logPrefix]; + } +} + ++ (BOOL)performHookSymbol:(NSString *)imageName + symbolName:(NSString *)symbolName + hookFunction:(void *)hookFunction + originalFunction:(void **)originalFunction + logPrefix:(NSString *)logPrefix { + + NSLogger(@"%@ Starting hook installation for %@", logPrefix, symbolName); + + int imageIndex = [self indexForImageWithName:imageName]; + if (imageIndex < 0) { + NSLogger(@"%@ ERROR: Image %@ not found", logPrefix, imageName); + return NO; + } + + void* symbolAddress = NULL; + + // Try to find the symbol address using symtbl_solve first + symbolAddress = symtbl_solve(imageIndex, [symbolName UTF8String]); + + if (symbolAddress) { + NSLogger(@"%@ %@ found with symtbl_solve at address: %p", logPrefix, symbolName, symbolAddress); + } else { + NSLogger(@"%@ %@ not found with symtbl_solve, trying symexp_solve...", logPrefix, symbolName); + symbolAddress = symexp_solve(imageIndex, [symbolName UTF8String]); + + if (symbolAddress) { + NSLogger(@"%@ %@ found with symexp_solve at address: %p", logPrefix, symbolName, symbolAddress); + } else { + NSLogger(@"%@ ERROR: Unable to find symbol %@", logPrefix, symbolName); + return NO; + } + } + + // Install the hook using tiny_hook and get the original function trampoline if requested + int hookResult = tiny_hook(symbolAddress, hookFunction, originalFunction); + + if (hookResult == 0) { + NSLogger(@"%@ Hook successfully installed for %@", logPrefix, symbolName); + return YES; + } else { + NSLogger(@"%@ ERROR: Failed to install hook for %@ (code: %d)", logPrefix, symbolName, hookResult); + return NO; + } +} + +@end diff --git a/src/utils/ResourceUtils.h b/src/utils/ResourceUtils.h new file mode 100644 index 0000000..a6776e4 --- /dev/null +++ b/src/utils/ResourceUtils.h @@ -0,0 +1,42 @@ +#ifndef ResourceUtils_h +#define ResourceUtils_h + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +struct ResourceRoot { + uint8_t *data; + uint8_t *name; + uint8_t *tree; + size_t treeSize; + size_t dataSize; + size_t originalDataSize; + size_t nameSize; + int entriesAffected; +}; + +#define TREE_ENTRY_SIZE 22 +#define DIRECTORY 0x02 + +// Read/Write utilities +uint32_t readUInt32(uint8_t *addr, int offset); +uint16_t readUInt16(uint8_t *addr, int offset); +void writeUint32(uint8_t *addr, int offset, uint32_t value); +void writeUint16(uint8_t *addr, int offset, uint16_t value); + +// Resource tree utilities +int findOffset(int node); +void nameOfChild(struct ResourceRoot *root, int node, int *size, char *buffer, int max); +void statArchive(struct ResourceRoot *root, int node); +void processNode(struct ResourceRoot *root, int node, const char *rootName); +void ReMarkableDumpResourceFile(struct ResourceRoot *root, int node, const char *rootName, const char *fileName, uint16_t flags); + +#ifdef __cplusplus +} +#endif + +#endif /* ResourceUtils_h */ diff --git a/src/utils/ResourceUtils.m b/src/utils/ResourceUtils.m new file mode 100644 index 0000000..4a55117 --- /dev/null +++ b/src/utils/ResourceUtils.m @@ -0,0 +1,381 @@ +#import "ResourceUtils.h" +#import "Logger.h" +#import +#import +#import +#import +#import + +static NSString *ReMarkableDumpRootDirectory(void); +static NSString *ReMarkablePreferencesDirectory(void); + +static NSString *ReMarkablePreferencesDirectory(void) { + NSArray *libraryPaths = NSSearchPathForDirectoriesInDomains(NSLibraryDirectory, NSUserDomainMask, YES); + NSString *libraryDir = [libraryPaths firstObject]; + if (![libraryDir length]) { + libraryDir = [NSHomeDirectory() stringByAppendingPathComponent:@"Library"]; + } + return [libraryDir stringByAppendingPathComponent:@"Preferences"]; +} + +static NSString *ReMarkableDumpRootDirectory(void) { + static NSString *dumpDirectory = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + NSString *preferencesDir = ReMarkablePreferencesDirectory(); + NSString *candidate = [preferencesDir stringByAppendingPathComponent:@"dump"]; + NSFileManager *fileManager = [NSFileManager defaultManager]; + NSError *error = nil; + if (![fileManager fileExistsAtPath:candidate]) { + if (![fileManager createDirectoryAtPath:candidate withIntermediateDirectories:YES attributes:nil error:&error]) { + NSLogger(@"[reMarkable] Failed to create dump directory %@: %@", candidate, error); + } + } + dumpDirectory = [candidate copy]; + }); + return dumpDirectory; +} + +uint32_t readUInt32(uint8_t *addr, int offset) { + return (uint32_t)(addr[offset + 0] << 24) | + (uint32_t)(addr[offset + 1] << 16) | + (uint32_t)(addr[offset + 2] << 8) | + (uint32_t)(addr[offset + 3] << 0); +} + +void writeUint32(uint8_t *addr, int offset, uint32_t value) { + addr[offset + 0] = (uint8_t)(value >> 24); + addr[offset + 1] = (uint8_t)(value >> 16); + addr[offset + 2] = (uint8_t)(value >> 8); + addr[offset + 3] = (uint8_t)(value >> 0); +} + +void writeUint16(uint8_t *addr, int offset, uint16_t value) { + addr[offset + 0] = (uint8_t)(value >> 8); + addr[offset + 1] = (uint8_t)(value >> 0); +} + +uint16_t readUInt16(uint8_t *addr, int offset) { + return (uint16_t)((addr[offset + 0] << 8) | + (addr[offset + 1] << 0)); +} + +int findOffset(int node) { + return node * TREE_ENTRY_SIZE; +} + +void statArchive(struct ResourceRoot *root, int node) { + int offset = findOffset(node); + int thisMaxLength = offset + TREE_ENTRY_SIZE; + if (thisMaxLength > (int)root->treeSize) root->treeSize = (size_t)thisMaxLength; + uint32_t nameOffset = readUInt32(root->tree, offset); + uint32_t thisMaxNameLength = nameOffset + readUInt16(root->name, (int)nameOffset); + if (thisMaxNameLength > root->nameSize) root->nameSize = thisMaxNameLength; + int flags = readUInt16(root->tree, offset + 4); + if (!(flags & DIRECTORY)) { + uint32_t dataOffset = readUInt32(root->tree, offset + 4 + 2 + 4); + uint32_t dataSize = readUInt32(root->data, (int)dataOffset); + uint32_t thisMaxDataLength = dataOffset + dataSize + 4; + if (thisMaxDataLength > root->dataSize) root->dataSize = thisMaxDataLength; + } else { + uint32_t childCount = readUInt32(root->tree, offset + 4 + 2); + offset += 4 + 4 + 2; + uint32_t childOffset = readUInt32(root->tree, offset); + for (int child = (int)childOffset; child < (int)(childOffset + childCount); child++){ + statArchive(root, child); + } + } + root->originalDataSize = root->dataSize; +} + +void nameOfChild(struct ResourceRoot *root, int node, int *size, char *buffer, int max) { + if (!buffer || max <= 0) { + if (size) { + *size = 0; + } + return; + } + + if (!root || !root->tree || !root->name) { + if (size) { + *size = 0; + } + buffer[0] = '\0'; + return; + } + + if (!node) { + if (size) { + *size = 0; + } + buffer[0] = '\0'; + return; + } + + const int offset = findOffset(node); + uint32_t nameOffset = readUInt32(root->tree, offset); + uint16_t nameLength = readUInt16(root->name, (int)nameOffset); + + if (nameLength > (uint16_t)(max - 1)) { + nameLength = (uint16_t)(max - 1); + } + + nameOffset += 2; // skip length prefix + nameOffset += 4; // skip hash + + if (size) { + *size = (int)nameLength; + } + + for (int i = 1; i < (int)nameLength * 2; i += 2) { + buffer[i / 2] = ((const char *)root->name)[nameOffset + i]; + } + buffer[nameLength] = '\0'; +} + +void ReMarkableDumpResourceFile(struct ResourceRoot *root, int node, const char *rootName, const char *fileName, uint16_t flags) { + if (!root || !root->tree || !root->data || !fileName) { + return; + } + + const int baseOffset = findOffset(node); + const uint32_t dataOffset = readUInt32(root->tree, baseOffset + 4 + 2 + 4); + const uint32_t dataSize = readUInt32(root->data, (int)dataOffset); + if (dataSize == 0) { + return; + } + + const uint32_t payloadStart = dataOffset + 4; + if (root->dataSize && (payloadStart + dataSize) > root->dataSize) { + NSLogger(@"[reMarkable] Skipping dump for node %d due to size mismatch (%u bytes beyond bounds)", (int)node, dataSize); + return; + } + + const uint8_t *payload = root->data + payloadStart; + uint8_t *ownedBuffer = NULL; + size_t bytesToWrite = dataSize; + + if (flags == 4) { + size_t expectedSize = ZSTD_getFrameContentSize(payload, dataSize); + if (expectedSize == ZSTD_CONTENTSIZE_ERROR) { + NSLogger(@"[reMarkable] ZSTD frame content size error for node %d", (int)node); + return; + } + + size_t bufferSize; + if (expectedSize == ZSTD_CONTENTSIZE_UNKNOWN) { + if ((size_t)dataSize > SIZE_MAX / 4) { + bufferSize = (size_t)dataSize; + } else { + bufferSize = (size_t)dataSize * 4; + } + } else { + bufferSize = expectedSize; + } + if (bufferSize < (size_t)dataSize) { + bufferSize = (size_t)dataSize; + } + + if (bufferSize > SIZE_MAX - 1) { + NSLogger(@"[reMarkable] ZSTD decompression size too large for node %d", (int)node); + return; + } + + for (int attempt = 0; attempt < 6; ++attempt) { + ownedBuffer = (uint8_t *)malloc(bufferSize + 1); + if (!ownedBuffer) { + NSLogger(@"[reMarkable] Failed to allocate %zu bytes for ZSTD decompression", bufferSize + 1); + return; + } + + size_t decompressedSize = ZSTD_decompress(ownedBuffer, bufferSize, payload, dataSize); + if (!ZSTD_isError(decompressedSize)) { + bytesToWrite = decompressedSize; + ownedBuffer[bytesToWrite] = 0; + break; + } + + ZSTD_ErrorCode errorCode = ZSTD_getErrorCode(decompressedSize); + free(ownedBuffer); + ownedBuffer = NULL; + + if (errorCode == ZSTD_error_dstSize_tooSmall) { + if (bufferSize > SIZE_MAX / 2) { + NSLogger(@"[reMarkable] ZSTD decompression buffer would overflow for node %d", (int)node); + return; + } + bufferSize *= 2; + continue; + } + + NSLogger(@"[reMarkable] ZSTD decompression failed for node %d: %s", (int)node, ZSTD_getErrorName(decompressedSize)); + return; + } + + if (!ownedBuffer) { + NSLogger(@"[reMarkable] ZSTD decompression exhausted retries for node %d", (int)node); + return; + } + } else if (flags == 0) { + if ((size_t)dataSize > SIZE_MAX - 1) { + NSLogger(@"[reMarkable] Raw resource size too large for node %d", (int)node); + return; + } + ownedBuffer = (uint8_t *)malloc((size_t)dataSize + 1); + if (!ownedBuffer) { + NSLogger(@"[reMarkable] Failed to allocate %u bytes for raw copy", (unsigned)(dataSize + 1u)); + return; + } + memcpy(ownedBuffer, payload, dataSize); + ownedBuffer[dataSize] = 0; + bytesToWrite = dataSize; + + } else if (flags == 1) { + if (dataSize <= 4) { + NSLogger(@"[reMarkable] Zlib compressed resource too small for node %d", (int)node); + return; + } + + const uint32_t expectedSize = + ((uint32_t)payload[0] << 24) | + ((uint32_t)payload[1] << 16) | + ((uint32_t)payload[2] << 8) | + ((uint32_t)payload[3] << 0); + + if (!expectedSize) { + NSLogger(@"[reMarkable] Zlib resource reported zero size for node %d", (int)node); + return; + } + + const uint8_t *compressedPayload = payload + 4; + const size_t compressedSize = (size_t)dataSize - 4; + if (compressedSize > UINT_MAX) { + NSLogger(@"[reMarkable] Zlib compressed payload too large for node %d", (int)node); + return; + } + + z_stream stream; + memset(&stream, 0, sizeof(stream)); + stream.next_in = (Bytef *)compressedPayload; + stream.avail_in = (uInt)compressedSize; + + int status = inflateInit(&stream); + if (status != Z_OK) { + NSLogger(@"[reMarkable] Failed to initialize zlib for node %d: %d", (int)node, status); + return; + } + + ownedBuffer = (uint8_t *)malloc((size_t)expectedSize + 1); + if (!ownedBuffer) { + NSLogger(@"[reMarkable] Failed to allocate %u bytes for zlib decompression", (unsigned)expectedSize + 1u); + inflateEnd(&stream); + return; + } + + stream.next_out = ownedBuffer; + stream.avail_out = (uInt)expectedSize; + + status = inflate(&stream, Z_FINISH); + if (status != Z_STREAM_END) { + NSLogger(@"[reMarkable] Zlib decompression failed for node %d with status %d", (int)node, status); + free(ownedBuffer); + ownedBuffer = NULL; + inflateEnd(&stream); + return; + } + + bytesToWrite = (size_t)stream.total_out; + inflateEnd(&stream); + ownedBuffer[bytesToWrite] = 0; + } else { + NSLogger(@"[reMarkable] Unknown compression flag %u for node %d; skipping dump", flags, (int)node); + return; + } + + NSString *dumpRoot = ReMarkableDumpRootDirectory(); + if (![dumpRoot length]) { + if (ownedBuffer) { + free(ownedBuffer); + } + return; + } + + NSString *rootComponent = [NSString stringWithUTF8String:rootName ? rootName : ""]; + NSString *fileComponent = [NSString stringWithUTF8String:fileName]; + if (!rootComponent) { + rootComponent = @""; + } + if (!fileComponent) { + fileComponent = @""; + } + + NSString *relativePath = [rootComponent stringByAppendingString:fileComponent]; + if ([relativePath hasPrefix:@"/"]) { + relativePath = [relativePath substringFromIndex:1]; + } + if (![relativePath length]) { + return; + } + + NSString *fullPath = [dumpRoot stringByAppendingPathComponent:relativePath]; + NSString *directoryPath = [fullPath stringByDeletingLastPathComponent]; + NSFileManager *fileManager = [NSFileManager defaultManager]; + NSError *directoryError = nil; + if ([directoryPath length] && ![fileManager createDirectoryAtPath:directoryPath withIntermediateDirectories:YES attributes:nil error:&directoryError]) { + NSLogger(@"[reMarkable] Failed to create directory for dump %@: %@", directoryPath, directoryError); + if (ownedBuffer) { + free(ownedBuffer); + } + return; + } + + const void *dataSource = ownedBuffer ? (const void *)ownedBuffer : (const void *)payload; + NSData *dataObject = [NSData dataWithBytes:dataSource length:bytesToWrite]; + NSError *writeError = nil; + if (![dataObject writeToFile:fullPath options:NSDataWritingAtomic error:&writeError]) { + NSLogger(@"[reMarkable] Failed to write dump file %@: %@", fullPath, writeError); + } else { + NSLogger(@"[reMarkable] Dumped resource to %@ (%zu bytes)", fullPath, bytesToWrite); + } + + if (ownedBuffer) { + free(ownedBuffer); + } +} + +void processNode(struct ResourceRoot *root, int node, const char *rootName) { + int offset = findOffset(node) + 4; + uint16_t flags = readUInt16(root->tree, offset); + offset += 2; + int stringLength = 0; + char nameBuffer[256]; + nameOfChild(root, node, &stringLength, nameBuffer, (int)sizeof(nameBuffer)); + + if (flags & DIRECTORY) { + uint32_t childCount = readUInt32(root->tree, offset); + offset += 4; + uint32_t childOffset = readUInt32(root->tree, offset); + const size_t rootLength = rootName ? strlen(rootName) : 0; + char *tempRoot = (char *)malloc(rootLength + (size_t)stringLength + 2); + if (!tempRoot) { + return; + } + + if (rootLength > 0) { + memcpy(tempRoot, rootName, rootLength); + } + memcpy(tempRoot + rootLength, nameBuffer, (size_t)stringLength); + tempRoot[rootLength + stringLength] = '/'; + tempRoot[rootLength + stringLength + 1] = '\0'; + + for (uint32_t child = childOffset; child < childOffset + childCount; ++child) { + processNode(root, (int)child, tempRoot); + } + + free(tempRoot); + } else { + NSLogger(@"[reMarkable] Processing node %d: %s%s", (int)node, rootName ? rootName : "", nameBuffer); + uint16_t fileFlags = readUInt16(root->tree, offset - 2); + ReMarkableDumpResourceFile(root, node, rootName ? rootName : "", nameBuffer, fileFlags); + } +}