28 Commits
v0.9 ... v1.3

Author SHA1 Message Date
√(noham)²
36d4b1258f Add configurable client key algorithm 2026-08-27 16:21:40 +02:00
√(noham)²
3f1b4fd728 Proper implementation 2026-08-27 16:15:57 +02:00
√(noham)²
ce1df6f27d PoC 2026-08-27 16:06:10 +02:00
√(noham)²
e00ab1d5ed Update README.md 2026-08-26 00:04:09 +02:00
√(noham)²
a74031f228 Update compatibility for v3.28.1 2026-08-25 17:50:39 +02:00
√(noham)²
9089839f9b Update compatibility for Desktop v3.28 2026-08-20 16:33:57 +02:00
√(noham)²
8f95c65ba9 Update tested version to v3.27.2 and refresh image 2026-05-13 19:05:22 +02:00
√(noham)²
bde78170be Move 'How it works' section in README 2026-05-10 18:57:51 +02:00
√(noham)²
35423e1c1a Update auto-install.sh 2026-05-10 16:30:04 +02:00
√(noham)²
190fd02092 Add auto-install script and update README 2026-05-10 16:28:01 +02:00
√(noham)²
8991f7fbcb Update README.md 2026-05-09 23:25:26 +02:00
√(noham)²
49aa0ec507 Update README.md 2026-05-09 18:15:42 +02:00
√(noham)²
90f50ec2a0 Add MQTT URI patching and hook for Paho 2026-05-09 18:13:53 +02:00
√(noham)²
0bb96ecceb Update license year, README release, and image 2026-05-07 23:29:37 +02:00
√(noham)²
427ee012c9 Bump reMarkable Desktop compatibility to v3.27 2026-05-06 18:44:57 +02:00
√(noham)²
03b2b4c794 Update README compatibility to v3.26.0 2026-03-27 09:46:06 +01:00
√(noham)²
0db8a14ef7 Bump reMarkable Desktop compatibility to v3.25.0 2026-02-02 19:56:33 +01:00
√(noham)²
3e89d8118e Add MessageBroker for QML and native communication
Introduces MessageBroker to enable communication between the dylib and QML via signals. Updates CMakeLists.txt to include Qml components and conditionally add MessageBroker sources in qmlrebuild mode. reMarkable.m is updated to register the QML type, set up native callbacks, and demonstrate broadcasting signals.
2025-12-06 17:39:06 +01:00
√(noham)²
3765bcd584 Rename build mode from qmldiff to qmlrebuild
Replaces all references to the 'qmldiff' build mode with 'qmlrebuild' across CMakeLists.txt, README.md, build scripts, and source files.
2025-12-06 16:51:28 +01:00
√(noham)²
55a15fb035 Add QML resource replacement support for specific files
Implements a mechanism to replace specific QML resource files at runtime by reading replacement files from a designated directory. Updates the resource registration hook to rebuild resource data tables when replacements are present, and adds utility functions and structures for managing replacement entries. Only selected files are eligible for replacement, and the README is updated with instructions for using this feature.
2025-12-06 16:47:15 +01:00
√(noham)²
9322b0319e Move dev hooks to separate DevHooks files
Extracted development/reverse engineering hooks and helpers from reMarkable.m into new DevHooks.h and DevHooks.m files. Updated CMakeLists.txt to include the new files and their directory. This improves code organization and maintainability for development-only instrumentation.
2025-12-05 18:28:59 +01:00
√(noham)²
83b12e297e Switch to static zstd library and update build config
Replaces dynamic libzstd.1.dylib with static libzstd.a in CMakeLists.txt and updates architecture to support arm64. Adds zstd headers and static library, updates documentation and credits to reflect zstd usage, and removes unused tinyhook source files after moving headers and libraries.
2025-12-03 20:51:26 +01:00
√(noham)²
0bdda7bdf7 Add hooks for new reMarkable functions and improve build script 2025-12-02 22:52:47 +01:00
√(noham)²
e8f204057e Update README 2025-11-30 21:48:54 +01:00
√(noham)²
15e5dabc37 Improve Qt path detection in build script 2025-11-29 14:37:41 +01:00
√(noham)²
1619fda631 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.
2025-11-29 14:34:07 +01:00
√(noham)²
18abae42b7 Update README and inject.sh for compatibility and ownership
Added compatibility information for reMarkable Desktop v3.23.0 and included a new screenshot in the README. Moved the ownership change step earlier in scripts/inject.sh to ensure correct permissions before code signing.
2025-11-05 22:00:37 +01:00
√(noham)²
55c41e78bf Add xovi-rmfakecloud to credits in README 2025-10-23 21:49:22 +02:00
36 changed files with 5390 additions and 709 deletions

3
.gitignore vendored
View File

@@ -1,3 +1,6 @@
build/ build/
.DS_Store .DS_Store
/.vscode /.vscode
/research
/aqt_venv
docs/rmfakecloud_hooking.md

View File

@@ -6,12 +6,20 @@ enable_language(OBJC OBJCXX)
set(CMAKE_C_STANDARD 11) set(CMAKE_C_STANDARD 11)
set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD 17)
# Build mode options
# - rmfakecloud: Redirect reMarkable cloud to rmfakecloud server (default)
# - qmlrebuild: Qt resource data registration hooking
# - dev: Development/reverse engineering mode with all hooks
option(BUILD_MODE_RMFAKECLOUD "Build with rmfakecloud support" ON)
option(BUILD_MODE_QMLREBUILD "Build with QML resource rebuilding" OFF)
option(BUILD_MODE_DEV "Build with dev/reverse engineering hooks" OFF)
# Compiler settings for macOS # Compiler settings for macOS
set(CMAKE_MACOSX_RPATH 1) set(CMAKE_MACOSX_RPATH 1)
set(CMAKE_OSX_DEPLOYMENT_TARGET "10.15" CACHE STRING "Minimum OS X deployment version") set(CMAKE_OSX_DEPLOYMENT_TARGET "10.15" CACHE STRING "Minimum OS X deployment version")
# Architecture: x86_64 only for reMarkable # Architecture: x86_64 only for reMarkable
set(CMAKE_OSX_ARCHITECTURES "x86_64") set(CMAKE_OSX_ARCHITECTURES "x86_64" "arm64")
# Project root directory # Project root directory
set(PROJECT_ROOT_DIR ${CMAKE_CURRENT_SOURCE_DIR}) set(PROJECT_ROOT_DIR ${CMAKE_CURRENT_SOURCE_DIR})
@@ -20,6 +28,8 @@ set(PROJECT_ROOT_DIR ${CMAKE_CURRENT_SOURCE_DIR})
include_directories( include_directories(
${PROJECT_ROOT_DIR}/src/core ${PROJECT_ROOT_DIR}/src/core
${PROJECT_ROOT_DIR}/src/utils ${PROJECT_ROOT_DIR}/src/utils
${PROJECT_ROOT_DIR}/src/RMHook
${PROJECT_ROOT_DIR}/libs/include
) )
# Find required libraries # Find required libraries
@@ -32,13 +42,14 @@ set(LIBS
${FOUNDATION_LIBRARY} ${FOUNDATION_LIBRARY}
${COCOA_LIBRARY} ${COCOA_LIBRARY}
${SECURITY_LIBRARY} ${SECURITY_LIBRARY}
${PROJECT_ROOT_DIR}/libs/libtinyhook.a ${PROJECT_ROOT_DIR}/libs/tinyhook.a # tinyhook for function hooking
${PROJECT_ROOT_DIR}/libs/libzstd.a # libzstd for compression/decompression
z # zlib for compression/decompression z # zlib for compression/decompression
) )
# Locate Qt libraries # Locate Qt libraries
set(QT_LIB_TARGETS "") set(QT_LIB_TARGETS "")
set(_qt_candidate_roots "$ENV{HOME}/Qt/6.10.0") set(_qt_candidate_roots "$ENV{HOME}/Qt/6.10.3/macos")
foreach(_qt_root ${_qt_candidate_roots}) foreach(_qt_root ${_qt_candidate_roots})
if(_qt_root AND EXISTS "${_qt_root}") if(_qt_root AND EXISTS "${_qt_root}")
@@ -46,13 +57,13 @@ foreach(_qt_root ${_qt_candidate_roots})
endif() endif()
endforeach() endforeach()
find_package(Qt6 COMPONENTS Core Network WebSockets QUIET) find_package(Qt6 COMPONENTS Core Network WebSockets Qml QUIET)
if(Qt6_FOUND) if(Qt6_FOUND)
set(QT_LIB_TARGETS Qt6::Core Qt6::Network Qt6::WebSockets) set(QT_LIB_TARGETS Qt6::Core Qt6::Network Qt6::WebSockets Qt6::Qml)
else() else()
find_package(Qt5 COMPONENTS Core Network WebSockets QUIET) find_package(Qt5 COMPONENTS Core Network WebSockets Qml QUIET)
if(Qt5_FOUND) if(Qt5_FOUND)
set(QT_LIB_TARGETS Qt5::Core Qt5::Network Qt5::WebSockets) set(QT_LIB_TARGETS Qt5::Core Qt5::Network Qt5::WebSockets Qt5::Qml)
endif() endif()
endif() endif()
@@ -67,34 +78,60 @@ set(COMMON_SOURCES
${PROJECT_ROOT_DIR}/src/utils/ResourceUtils.m ${PROJECT_ROOT_DIR}/src/utils/ResourceUtils.m
) )
# reMarkable dylib # RMHook dylib
set(REMARKABLE_SOURCES set(RMHOOK_SOURCES
${PROJECT_ROOT_DIR}/src/reMarkable/reMarkable.m ${PROJECT_ROOT_DIR}/src/RMHook/RMHook.m
${PROJECT_ROOT_DIR}/src/RMHook/Config.m
${PROJECT_ROOT_DIR}/src/RMHook/SSLConfig.m
${PROJECT_ROOT_DIR}/src/RMHook/DevHooks.m
) )
add_library(reMarkable SHARED add_library(RMHook SHARED
${COMMON_SOURCES} ${COMMON_SOURCES}
${REMARKABLE_SOURCES} ${RMHOOK_SOURCES}
) )
# Set source files as Objective-C++ # Set source files as Objective-C++
set_source_files_properties( set_source_files_properties(
${REMARKABLE_SOURCES} ${RMHOOK_SOURCES}
PROPERTIES LANGUAGE OBJCXX PROPERTIES LANGUAGE OBJCXX
) )
set_target_properties(reMarkable PROPERTIES set_target_properties(RMHook PROPERTIES
PREFIX "" PREFIX ""
SUFFIX ".dylib" SUFFIX ".dylib"
OUTPUT_NAME "reMarkable" OUTPUT_NAME "RMHook"
LIBRARY_OUTPUT_DIRECTORY "${PROJECT_ROOT_DIR}/build/dylibs" LIBRARY_OUTPUT_DIRECTORY "${PROJECT_ROOT_DIR}/build/dylibs"
MACOSX_RPATH ON MACOSX_RPATH ON
) )
add_definitions(-DQT_NO_VERSION_TAGGING) add_definitions(-DQT_NO_VERSION_TAGGING)
target_link_libraries(reMarkable PRIVATE # Add build mode compile definitions and conditionally add sources
if(BUILD_MODE_RMFAKECLOUD)
target_compile_definitions(RMHook PRIVATE BUILD_MODE_RMFAKECLOUD=1)
message(STATUS "Build mode: rmfakecloud (cloud redirection)")
endif()
if(BUILD_MODE_QMLREBUILD)
target_compile_definitions(RMHook PRIVATE BUILD_MODE_QMLREBUILD=1)
# Enable Qt MOC for MessageBroker
set_target_properties(RMHook PROPERTIES AUTOMOC ON)
# Add MessageBroker source (needs MOC processing)
target_sources(RMHook PRIVATE
${PROJECT_ROOT_DIR}/src/utils/MessageBroker.mm
)
message(STATUS "Build mode: qmlrebuild (resource hooking)")
endif()
if(BUILD_MODE_DEV)
target_compile_definitions(RMHook PRIVATE BUILD_MODE_DEV=1)
message(STATUS "Build mode: dev (reverse engineering)")
endif()
target_link_libraries(RMHook PRIVATE
${LIBS} ${LIBS}
${QT_LIB_TARGETS} ${QT_LIB_TARGETS}
/opt/homebrew/Cellar/libzip/1.11.4/lib/intel/libzstd.1.5.7.dylib
) )

View File

@@ -1,6 +1,6 @@
MIT License MIT License
Copyright (c) 2025 Rivoirard Noham Copyright (c) 2026 Rivoirard Noham
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal

179
README.md
View File

@@ -4,24 +4,49 @@ A dynamic library injection tool for the reMarkable Desktop macOS application, e
## Overview ## 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. RMHook hooks into the reMarkable Desktop app's network layer to redirect API calls from reMarkable's official cloud services to your own [rmfakecloud](https://github.com/ddvk/rmfakecloud) server. This allows you to maintain full control over your documents and data.
## Other platforms
- **[RMHook-Win](https://github.com/NohamR/RMHook-Win)**: Windows Desktop
- **[RMHook-iOS](https://github.com/NohamR/RMHook-iOS)**: iOS
- **[RMHook-Android](https://github.com/NohamR/RMHook-Android)**: Android
## Features ## Features
- Network request interception and redirection - Network request interception and redirection
- WebSocket connection patching - WebSocket connection patching
- MQTT URI modification for screen sharing features
- mTLS support for external access via Cloudflare Tunnel
## Compatibility
**Tested and working on:**
- reMarkable Desktop v3.28.1 (released 2026-08-24)
<p align="center">
<img src="docs/latest.png" width="45%" />
<img src="docs/rm.png" width="45%" />
</p>
## Installation and usage ## 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. ⚠️ **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 ### Auto installation
Run in a terminal:
```bash
bash <(curl -sL https://raw.githubusercontent.com/NohamR/RMHook/refs/heads/main/scripts/auto-install.sh)
```
### Manual installation
#### 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). 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 #### Step 2: Inject the dylib
Use the provided injection script: Use the provided injection script:
```bash ```bash
@@ -35,9 +60,9 @@ This script will:
- Remove the `_MASReceipt` folder - Remove the `_MASReceipt` folder
- Fix file ownership - Fix file ownership
### Step 3: Handle document storage #### Step 3: Handle document storage
#### Important path changes ##### Important path changes
The original Mac App Store version stores data in sandboxed locations: The original Mac App Store version stores data in sandboxed locations:
**Original sandboxed paths:** **Original sandboxed paths:**
@@ -48,7 +73,7 @@ The original Mac App Store version stores data in sandboxed locations:
- Config: `~/Library/Preferences/rmfakecloud.config` - Config: `~/Library/Preferences/rmfakecloud.config`
- Documents: `~/Library/Application Support/remarkable` - Documents: `~/Library/Application Support/remarkable`
#### Migration options ##### Migration options
**Option 1: Create a symbolic link** (recommended) **Option 1: Create a symbolic link** (recommended)
```bash ```bash
@@ -63,9 +88,10 @@ mv ~/Library/Containers/com.remarkable.desktop/Data/Library/Application\ Support
~/Library/Application\ Support/remarkable ~/Library/Application\ Support/remarkable
``` ```
### Step 4: Configure rmfakecloud server #### Step 4: Configure rmfakecloud server
Quickly access the configuration file from the app's Help menu: Quickly access the configuration file from the app's Help menu:
![help-config.png](docs/help-config.png) ![help-config.png](docs/help-config.png)
Edit the configuration file at: Edit the configuration file at:
``` ```
~/Library/Preferences/rmfakecloud.config ~/Library/Preferences/rmfakecloud.config
@@ -79,25 +105,112 @@ Example configuration:
} }
``` ```
### Step 5: Launch the patched app :p #### 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 ## Configuration
The config file (`~/Library/Preferences/rmfakecloud.config`) supports the following keys: The config file (`~/Library/Preferences/rmfakecloud.config`) supports the following keys:
| Key | Type | Default | Description | | Key | Type | Default | Description |
|--------|---------|-------------------|--------------------------------| |-----|------|---------|-------------|
| `host` | String | `example.com` | Your rmfakecloud server host | | `host` | String | `example.com` | Your rmfakecloud server host |
| `port` | Number | `443` | Your rmfakecloud server port | | `port` | Number | `443` | Your rmfakecloud server port |
| `client_cert` | String | (none) | Path to client certificate file (PEM) for mTLS |
| `client_key` | String | (none) | Path to client private key file (PEM) for mTLS |
| `key_algorithm` | String | `rsa` | Key algorithm: `rsa`, `ec`, `dsa`, `dh`, or `opaque` |
| `ca_cert` | String | (none) | Path to custom CA certificate file (PEM) |
| `disable_ssl_verification` | Boolean | `false` | Disable SSL peer verification (not recommended) |
If the config file doesn't exist, it will be created automatically with default values on first launch. If the config file doesn't exist, it will be created automatically with default values on first launch.
### External Access (Cloudflare Tunnel with Client Certificate)
If your rmfakecloud instance is exposed via Cloudflare Tunnel with client certificate authentication, configure the additional TLS settings in `~/Library/Preferences/rmfakecloud.config`:
```json
{
"host": "rmfakecloud.example.com",
"port": 443,
"client_cert": "/path/to/client.crt",
"client_key": "/path/to/client.key"
}
```
If your server uses a self-signed certificate or a certificate signed by a private CA:
```json
{
"host": "rmfakecloud.example.com",
"port": 443,
"client_cert": "/path/to/client.crt",
"client_key": "/path/to/client.key",
"ca_cert": "/path/to/ca.crt"
}
```
See the [rmfakecloud external access guide](https://ddvk.github.io/rmfakecloud/install/external-access/) for generating the client certificate.
## Building
1. **Clone the repository:**
```bash
git clone http://github.com/NohamR/RMHook
cd RMHook
```
Create a Python environment and install `aqtinstall`:
```bash
python3 -m venv aqt_venv
source aqt_venv/bin/activate
pip install aqtinstall
aqt install-qt mac desktop 6.10.3 -m qtwebsockets --outputdir ~/Qt
```
2. **Compile the dylib:**
```bash
./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) |
| `qmlrebuild` | Qt resource data registration hooking for QML replacement |
| `dev` | Development/reverse engineering mode with all hooks |
| `all` | Enable all modes |
**Note (qmlrebuild mode):** When using the `qmlrebuild` feature, you must clear the Qt QML cache before launching the app:
```bash
rm -rf ~/Library/Caches/remarkable
```
Qt caches compiled QML files, so changes to QML resources won't take effect until the cache is cleared.
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
```
## Debugging
You can stream the macOS console logs to see output from the hooks:
```bash
log stream --predicate 'process == "reMarkable"' --level debug
```
## 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
3. **MQTTAsync_createWithOptions** - Modifies MQTT URIs for screen sharing features
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.
## Troubleshooting ## Troubleshooting
### App won't launch ### App won't launch
@@ -110,9 +223,14 @@ If the config file doesn't exist, it will be created automatically with default
- Verify the storage path migration was completed - Verify the storage path migration was completed
## Credits ## Credits
- **tinyhook**: [Antibioticss/tinyhook](https://github.com/Antibioticss/tinyhook/) - Function hooking framework - xovi-rmfakecloud: [asivery/xovi-rmfakecloud](https://github.com/asivery/xovi-rmfakecloud) - Original hooking information
- **rmfakecloud**: [ddvk/rmfakecloud](https://github.com/ddvk/rmfakecloud) - Self-hosted reMarkable cloud - rm-xovi-extensions: [asivery/rm-xovi-extensions](https://github.com/asivery/rm-xovi-extensions) - Extension framework for reMarkable, used as reference for hooking Qt functions
- **optool**: [alexzielenski/optool](https://github.com/alexzielenski/optool) - Mach-O binary modification tool - [qt-resource-rebuilder](https://github.com/asivery/rm-xovi-extensions/tree/master/qt-resource-rebuilder)
- [xovi-message-broker](https://github.com/asivery/rm-xovi-extensions/tree/master/xovi-message-broker)
- 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
- zstd: [facebook/zstd](https://github.com/facebook/zstd) - Compression library
## License ## License
@@ -125,16 +243,3 @@ This project is not affiliated with, endorsed by, or sponsored by reMarkable AS.
## Contributing ## Contributing
Contributions are welcome! Please feel free to submit issues or pull requests. 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
```

BIN
docs/latest.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 245 KiB

BIN
docs/rm.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 467 KiB

481
libs/include/zdict.h Normal file
View File

@@ -0,0 +1,481 @@
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
* All rights reserved.
*
* This source code is licensed under both the BSD-style license (found in the
* LICENSE file in the root directory of this source tree) and the GPLv2 (found
* in the COPYING file in the root directory of this source tree).
* You may select, at your option, one of the above-listed licenses.
*/
#ifndef ZSTD_ZDICT_H
#define ZSTD_ZDICT_H
/*====== Dependencies ======*/
#include <stddef.h> /* size_t */
#if defined (__cplusplus)
extern "C" {
#endif
/* ===== ZDICTLIB_API : control library symbols visibility ===== */
#ifndef ZDICTLIB_VISIBLE
/* Backwards compatibility with old macro name */
# ifdef ZDICTLIB_VISIBILITY
# define ZDICTLIB_VISIBLE ZDICTLIB_VISIBILITY
# elif defined(__GNUC__) && (__GNUC__ >= 4) && !defined(__MINGW32__)
# define ZDICTLIB_VISIBLE __attribute__ ((visibility ("default")))
# else
# define ZDICTLIB_VISIBLE
# endif
#endif
#ifndef ZDICTLIB_HIDDEN
# if defined(__GNUC__) && (__GNUC__ >= 4) && !defined(__MINGW32__)
# define ZDICTLIB_HIDDEN __attribute__ ((visibility ("hidden")))
# else
# define ZDICTLIB_HIDDEN
# endif
#endif
#if defined(ZSTD_DLL_EXPORT) && (ZSTD_DLL_EXPORT==1)
# define ZDICTLIB_API __declspec(dllexport) ZDICTLIB_VISIBLE
#elif defined(ZSTD_DLL_IMPORT) && (ZSTD_DLL_IMPORT==1)
# define ZDICTLIB_API __declspec(dllimport) ZDICTLIB_VISIBLE /* It isn't required but allows to generate better code, saving a function pointer load from the IAT and an indirect jump.*/
#else
# define ZDICTLIB_API ZDICTLIB_VISIBLE
#endif
/*******************************************************************************
* Zstd dictionary builder
*
* FAQ
* ===
* Why should I use a dictionary?
* ------------------------------
*
* Zstd can use dictionaries to improve compression ratio of small data.
* Traditionally small files don't compress well because there is very little
* repetition in a single sample, since it is small. But, if you are compressing
* many similar files, like a bunch of JSON records that share the same
* structure, you can train a dictionary on ahead of time on some samples of
* these files. Then, zstd can use the dictionary to find repetitions that are
* present across samples. This can vastly improve compression ratio.
*
* When is a dictionary useful?
* ----------------------------
*
* Dictionaries are useful when compressing many small files that are similar.
* The larger a file is, the less benefit a dictionary will have. Generally,
* we don't expect dictionary compression to be effective past 100KB. And the
* smaller a file is, the more we would expect the dictionary to help.
*
* How do I use a dictionary?
* --------------------------
*
* Simply pass the dictionary to the zstd compressor with
* `ZSTD_CCtx_loadDictionary()`. The same dictionary must then be passed to
* the decompressor, using `ZSTD_DCtx_loadDictionary()`. There are other
* more advanced functions that allow selecting some options, see zstd.h for
* complete documentation.
*
* What is a zstd dictionary?
* --------------------------
*
* A zstd dictionary has two pieces: Its header, and its content. The header
* contains a magic number, the dictionary ID, and entropy tables. These
* entropy tables allow zstd to save on header costs in the compressed file,
* which really matters for small data. The content is just bytes, which are
* repeated content that is common across many samples.
*
* What is a raw content dictionary?
* ---------------------------------
*
* A raw content dictionary is just bytes. It doesn't have a zstd dictionary
* header, a dictionary ID, or entropy tables. Any buffer is a valid raw
* content dictionary.
*
* How do I train a dictionary?
* ----------------------------
*
* Gather samples from your use case. These samples should be similar to each
* other. If you have several use cases, you could try to train one dictionary
* per use case.
*
* Pass those samples to `ZDICT_trainFromBuffer()` and that will train your
* dictionary. There are a few advanced versions of this function, but this
* is a great starting point. If you want to further tune your dictionary
* you could try `ZDICT_optimizeTrainFromBuffer_cover()`. If that is too slow
* you can try `ZDICT_optimizeTrainFromBuffer_fastCover()`.
*
* If the dictionary training function fails, that is likely because you
* either passed too few samples, or a dictionary would not be effective
* for your data. Look at the messages that the dictionary trainer printed,
* if it doesn't say too few samples, then a dictionary would not be effective.
*
* How large should my dictionary be?
* ----------------------------------
*
* A reasonable dictionary size, the `dictBufferCapacity`, is about 100KB.
* The zstd CLI defaults to a 110KB dictionary. You likely don't need a
* dictionary larger than that. But, most use cases can get away with a
* smaller dictionary. The advanced dictionary builders can automatically
* shrink the dictionary for you, and select the smallest size that doesn't
* hurt compression ratio too much. See the `shrinkDict` parameter.
* A smaller dictionary can save memory, and potentially speed up
* compression.
*
* How many samples should I provide to the dictionary builder?
* ------------------------------------------------------------
*
* We generally recommend passing ~100x the size of the dictionary
* in samples. A few thousand should suffice. Having too few samples
* can hurt the dictionaries effectiveness. Having more samples will
* only improve the dictionaries effectiveness. But having too many
* samples can slow down the dictionary builder.
*
* How do I determine if a dictionary will be effective?
* -----------------------------------------------------
*
* Simply train a dictionary and try it out. You can use zstd's built in
* benchmarking tool to test the dictionary effectiveness.
*
* # Benchmark levels 1-3 without a dictionary
* zstd -b1e3 -r /path/to/my/files
* # Benchmark levels 1-3 with a dictionary
* zstd -b1e3 -r /path/to/my/files -D /path/to/my/dictionary
*
* When should I retrain a dictionary?
* -----------------------------------
*
* You should retrain a dictionary when its effectiveness drops. Dictionary
* effectiveness drops as the data you are compressing changes. Generally, we do
* expect dictionaries to "decay" over time, as your data changes, but the rate
* at which they decay depends on your use case. Internally, we regularly
* retrain dictionaries, and if the new dictionary performs significantly
* better than the old dictionary, we will ship the new dictionary.
*
* I have a raw content dictionary, how do I turn it into a zstd dictionary?
* -------------------------------------------------------------------------
*
* If you have a raw content dictionary, e.g. by manually constructing it, or
* using a third-party dictionary builder, you can turn it into a zstd
* dictionary by using `ZDICT_finalizeDictionary()`. You'll also have to
* provide some samples of the data. It will add the zstd header to the
* raw content, which contains a dictionary ID and entropy tables, which
* will improve compression ratio, and allow zstd to write the dictionary ID
* into the frame, if you so choose.
*
* Do I have to use zstd's dictionary builder?
* -------------------------------------------
*
* No! You can construct dictionary content however you please, it is just
* bytes. It will always be valid as a raw content dictionary. If you want
* a zstd dictionary, which can improve compression ratio, use
* `ZDICT_finalizeDictionary()`.
*
* What is the attack surface of a zstd dictionary?
* ------------------------------------------------
*
* Zstd is heavily fuzz tested, including loading fuzzed dictionaries, so
* zstd should never crash, or access out-of-bounds memory no matter what
* the dictionary is. However, if an attacker can control the dictionary
* during decompression, they can cause zstd to generate arbitrary bytes,
* just like if they controlled the compressed data.
*
******************************************************************************/
/*! ZDICT_trainFromBuffer():
* Train a dictionary from an array of samples.
* Redirect towards ZDICT_optimizeTrainFromBuffer_fastCover() single-threaded, with d=8, steps=4,
* f=20, and accel=1.
* Samples must be stored concatenated in a single flat buffer `samplesBuffer`,
* supplied with an array of sizes `samplesSizes`, providing the size of each sample, in order.
* The resulting dictionary will be saved into `dictBuffer`.
* @return: size of dictionary stored into `dictBuffer` (<= `dictBufferCapacity`)
* or an error code, which can be tested with ZDICT_isError().
* Note: Dictionary training will fail if there are not enough samples to construct a
* dictionary, or if most of the samples are too small (< 8 bytes being the lower limit).
* If dictionary training fails, you should use zstd without a dictionary, as the dictionary
* would've been ineffective anyways. If you believe your samples would benefit from a dictionary
* please open an issue with details, and we can look into it.
* Note: ZDICT_trainFromBuffer()'s memory usage is about 6 MB.
* Tips: In general, a reasonable dictionary has a size of ~ 100 KB.
* It's possible to select smaller or larger size, just by specifying `dictBufferCapacity`.
* In general, it's recommended to provide a few thousands samples, though this can vary a lot.
* It's recommended that total size of all samples be about ~x100 times the target size of dictionary.
*/
ZDICTLIB_API size_t ZDICT_trainFromBuffer(void* dictBuffer, size_t dictBufferCapacity,
const void* samplesBuffer,
const size_t* samplesSizes, unsigned nbSamples);
typedef struct {
int compressionLevel; /**< optimize for a specific zstd compression level; 0 means default */
unsigned notificationLevel; /**< Write log to stderr; 0 = none (default); 1 = errors; 2 = progression; 3 = details; 4 = debug; */
unsigned dictID; /**< force dictID value; 0 means auto mode (32-bits random value)
* NOTE: The zstd format reserves some dictionary IDs for future use.
* You may use them in private settings, but be warned that they
* may be used by zstd in a public dictionary registry in the future.
* These dictionary IDs are:
* - low range : <= 32767
* - high range : >= (2^31)
*/
} ZDICT_params_t;
/*! ZDICT_finalizeDictionary():
* Given a custom content as a basis for dictionary, and a set of samples,
* finalize dictionary by adding headers and statistics according to the zstd
* dictionary format.
*
* Samples must be stored concatenated in a flat buffer `samplesBuffer`,
* supplied with an array of sizes `samplesSizes`, providing the size of each
* sample in order. The samples are used to construct the statistics, so they
* should be representative of what you will compress with this dictionary.
*
* The compression level can be set in `parameters`. You should pass the
* compression level you expect to use in production. The statistics for each
* compression level differ, so tuning the dictionary for the compression level
* can help quite a bit.
*
* You can set an explicit dictionary ID in `parameters`, or allow us to pick
* a random dictionary ID for you, but we can't guarantee no collisions.
*
* The dstDictBuffer and the dictContent may overlap, and the content will be
* appended to the end of the header. If the header + the content doesn't fit in
* maxDictSize the beginning of the content is truncated to make room, since it
* is presumed that the most profitable content is at the end of the dictionary,
* since that is the cheapest to reference.
*
* `maxDictSize` must be >= max(dictContentSize, ZDICT_DICTSIZE_MIN).
*
* @return: size of dictionary stored into `dstDictBuffer` (<= `maxDictSize`),
* or an error code, which can be tested by ZDICT_isError().
* Note: ZDICT_finalizeDictionary() will push notifications into stderr if
* instructed to, using notificationLevel>0.
* NOTE: This function currently may fail in several edge cases including:
* * Not enough samples
* * Samples are uncompressible
* * Samples are all exactly the same
*/
ZDICTLIB_API size_t ZDICT_finalizeDictionary(void* dstDictBuffer, size_t maxDictSize,
const void* dictContent, size_t dictContentSize,
const void* samplesBuffer, const size_t* samplesSizes, unsigned nbSamples,
ZDICT_params_t parameters);
/*====== Helper functions ======*/
ZDICTLIB_API unsigned ZDICT_getDictID(const void* dictBuffer, size_t dictSize); /**< extracts dictID; @return zero if error (not a valid dictionary) */
ZDICTLIB_API size_t ZDICT_getDictHeaderSize(const void* dictBuffer, size_t dictSize); /* returns dict header size; returns a ZSTD error code on failure */
ZDICTLIB_API unsigned ZDICT_isError(size_t errorCode);
ZDICTLIB_API const char* ZDICT_getErrorName(size_t errorCode);
#if defined (__cplusplus)
}
#endif
#endif /* ZSTD_ZDICT_H */
#if defined(ZDICT_STATIC_LINKING_ONLY) && !defined(ZSTD_ZDICT_H_STATIC)
#define ZSTD_ZDICT_H_STATIC
#if defined (__cplusplus)
extern "C" {
#endif
/* This can be overridden externally to hide static symbols. */
#ifndef ZDICTLIB_STATIC_API
# if defined(ZSTD_DLL_EXPORT) && (ZSTD_DLL_EXPORT==1)
# define ZDICTLIB_STATIC_API __declspec(dllexport) ZDICTLIB_VISIBLE
# elif defined(ZSTD_DLL_IMPORT) && (ZSTD_DLL_IMPORT==1)
# define ZDICTLIB_STATIC_API __declspec(dllimport) ZDICTLIB_VISIBLE
# else
# define ZDICTLIB_STATIC_API ZDICTLIB_VISIBLE
# endif
#endif
/* ====================================================================================
* The definitions in this section are considered experimental.
* They should never be used with a dynamic library, as they may change in the future.
* They are provided for advanced usages.
* Use them only in association with static linking.
* ==================================================================================== */
#define ZDICT_DICTSIZE_MIN 256
/* Deprecated: Remove in v1.6.0 */
#define ZDICT_CONTENTSIZE_MIN 128
/*! ZDICT_cover_params_t:
* k and d are the only required parameters.
* For others, value 0 means default.
*/
typedef struct {
unsigned k; /* Segment size : constraint: 0 < k : Reasonable range [16, 2048+] */
unsigned d; /* dmer size : constraint: 0 < d <= k : Reasonable range [6, 16] */
unsigned steps; /* Number of steps : Only used for optimization : 0 means default (40) : Higher means more parameters checked */
unsigned nbThreads; /* Number of threads : constraint: 0 < nbThreads : 1 means single-threaded : Only used for optimization : Ignored if ZSTD_MULTITHREAD is not defined */
double splitPoint; /* Percentage of samples used for training: Only used for optimization : the first nbSamples * splitPoint samples will be used to training, the last nbSamples * (1 - splitPoint) samples will be used for testing, 0 means default (1.0), 1.0 when all samples are used for both training and testing */
unsigned shrinkDict; /* Train dictionaries to shrink in size starting from the minimum size and selects the smallest dictionary that is shrinkDictMaxRegression% worse than the largest dictionary. 0 means no shrinking and 1 means shrinking */
unsigned shrinkDictMaxRegression; /* Sets shrinkDictMaxRegression so that a smaller dictionary can be at worse shrinkDictMaxRegression% worse than the max dict size dictionary. */
ZDICT_params_t zParams;
} ZDICT_cover_params_t;
typedef struct {
unsigned k; /* Segment size : constraint: 0 < k : Reasonable range [16, 2048+] */
unsigned d; /* dmer size : constraint: 0 < d <= k : Reasonable range [6, 16] */
unsigned f; /* log of size of frequency array : constraint: 0 < f <= 31 : 1 means default(20)*/
unsigned steps; /* Number of steps : Only used for optimization : 0 means default (40) : Higher means more parameters checked */
unsigned nbThreads; /* Number of threads : constraint: 0 < nbThreads : 1 means single-threaded : Only used for optimization : Ignored if ZSTD_MULTITHREAD is not defined */
double splitPoint; /* Percentage of samples used for training: Only used for optimization : the first nbSamples * splitPoint samples will be used to training, the last nbSamples * (1 - splitPoint) samples will be used for testing, 0 means default (0.75), 1.0 when all samples are used for both training and testing */
unsigned accel; /* Acceleration level: constraint: 0 < accel <= 10, higher means faster and less accurate, 0 means default(1) */
unsigned shrinkDict; /* Train dictionaries to shrink in size starting from the minimum size and selects the smallest dictionary that is shrinkDictMaxRegression% worse than the largest dictionary. 0 means no shrinking and 1 means shrinking */
unsigned shrinkDictMaxRegression; /* Sets shrinkDictMaxRegression so that a smaller dictionary can be at worse shrinkDictMaxRegression% worse than the max dict size dictionary. */
ZDICT_params_t zParams;
} ZDICT_fastCover_params_t;
/*! ZDICT_trainFromBuffer_cover():
* Train a dictionary from an array of samples using the COVER algorithm.
* Samples must be stored concatenated in a single flat buffer `samplesBuffer`,
* supplied with an array of sizes `samplesSizes`, providing the size of each sample, in order.
* The resulting dictionary will be saved into `dictBuffer`.
* @return: size of dictionary stored into `dictBuffer` (<= `dictBufferCapacity`)
* or an error code, which can be tested with ZDICT_isError().
* See ZDICT_trainFromBuffer() for details on failure modes.
* Note: ZDICT_trainFromBuffer_cover() requires about 9 bytes of memory for each input byte.
* Tips: In general, a reasonable dictionary has a size of ~ 100 KB.
* It's possible to select smaller or larger size, just by specifying `dictBufferCapacity`.
* In general, it's recommended to provide a few thousands samples, though this can vary a lot.
* It's recommended that total size of all samples be about ~x100 times the target size of dictionary.
*/
ZDICTLIB_STATIC_API size_t ZDICT_trainFromBuffer_cover(
void *dictBuffer, size_t dictBufferCapacity,
const void *samplesBuffer, const size_t *samplesSizes, unsigned nbSamples,
ZDICT_cover_params_t parameters);
/*! ZDICT_optimizeTrainFromBuffer_cover():
* The same requirements as above hold for all the parameters except `parameters`.
* This function tries many parameter combinations and picks the best parameters.
* `*parameters` is filled with the best parameters found,
* dictionary constructed with those parameters is stored in `dictBuffer`.
*
* All of the parameters d, k, steps are optional.
* If d is non-zero then we don't check multiple values of d, otherwise we check d = {6, 8}.
* if steps is zero it defaults to its default value.
* If k is non-zero then we don't check multiple values of k, otherwise we check steps values in [50, 2000].
*
* @return: size of dictionary stored into `dictBuffer` (<= `dictBufferCapacity`)
* or an error code, which can be tested with ZDICT_isError().
* On success `*parameters` contains the parameters selected.
* See ZDICT_trainFromBuffer() for details on failure modes.
* Note: ZDICT_optimizeTrainFromBuffer_cover() requires about 8 bytes of memory for each input byte and additionally another 5 bytes of memory for each byte of memory for each thread.
*/
ZDICTLIB_STATIC_API size_t ZDICT_optimizeTrainFromBuffer_cover(
void* dictBuffer, size_t dictBufferCapacity,
const void* samplesBuffer, const size_t* samplesSizes, unsigned nbSamples,
ZDICT_cover_params_t* parameters);
/*! ZDICT_trainFromBuffer_fastCover():
* Train a dictionary from an array of samples using a modified version of COVER algorithm.
* Samples must be stored concatenated in a single flat buffer `samplesBuffer`,
* supplied with an array of sizes `samplesSizes`, providing the size of each sample, in order.
* d and k are required.
* All other parameters are optional, will use default values if not provided
* The resulting dictionary will be saved into `dictBuffer`.
* @return: size of dictionary stored into `dictBuffer` (<= `dictBufferCapacity`)
* or an error code, which can be tested with ZDICT_isError().
* See ZDICT_trainFromBuffer() for details on failure modes.
* Note: ZDICT_trainFromBuffer_fastCover() requires 6 * 2^f bytes of memory.
* Tips: In general, a reasonable dictionary has a size of ~ 100 KB.
* It's possible to select smaller or larger size, just by specifying `dictBufferCapacity`.
* In general, it's recommended to provide a few thousands samples, though this can vary a lot.
* It's recommended that total size of all samples be about ~x100 times the target size of dictionary.
*/
ZDICTLIB_STATIC_API size_t ZDICT_trainFromBuffer_fastCover(void *dictBuffer,
size_t dictBufferCapacity, const void *samplesBuffer,
const size_t *samplesSizes, unsigned nbSamples,
ZDICT_fastCover_params_t parameters);
/*! ZDICT_optimizeTrainFromBuffer_fastCover():
* The same requirements as above hold for all the parameters except `parameters`.
* This function tries many parameter combinations (specifically, k and d combinations)
* and picks the best parameters. `*parameters` is filled with the best parameters found,
* dictionary constructed with those parameters is stored in `dictBuffer`.
* All of the parameters d, k, steps, f, and accel are optional.
* If d is non-zero then we don't check multiple values of d, otherwise we check d = {6, 8}.
* if steps is zero it defaults to its default value.
* If k is non-zero then we don't check multiple values of k, otherwise we check steps values in [50, 2000].
* If f is zero, default value of 20 is used.
* If accel is zero, default value of 1 is used.
*
* @return: size of dictionary stored into `dictBuffer` (<= `dictBufferCapacity`)
* or an error code, which can be tested with ZDICT_isError().
* On success `*parameters` contains the parameters selected.
* See ZDICT_trainFromBuffer() for details on failure modes.
* Note: ZDICT_optimizeTrainFromBuffer_fastCover() requires about 6 * 2^f bytes of memory for each thread.
*/
ZDICTLIB_STATIC_API size_t ZDICT_optimizeTrainFromBuffer_fastCover(void* dictBuffer,
size_t dictBufferCapacity, const void* samplesBuffer,
const size_t* samplesSizes, unsigned nbSamples,
ZDICT_fastCover_params_t* parameters);
typedef struct {
unsigned selectivityLevel; /* 0 means default; larger => select more => larger dictionary */
ZDICT_params_t zParams;
} ZDICT_legacy_params_t;
/*! ZDICT_trainFromBuffer_legacy():
* Train a dictionary from an array of samples.
* Samples must be stored concatenated in a single flat buffer `samplesBuffer`,
* supplied with an array of sizes `samplesSizes`, providing the size of each sample, in order.
* The resulting dictionary will be saved into `dictBuffer`.
* `parameters` is optional and can be provided with values set to 0 to mean "default".
* @return: size of dictionary stored into `dictBuffer` (<= `dictBufferCapacity`)
* or an error code, which can be tested with ZDICT_isError().
* See ZDICT_trainFromBuffer() for details on failure modes.
* Tips: In general, a reasonable dictionary has a size of ~ 100 KB.
* It's possible to select smaller or larger size, just by specifying `dictBufferCapacity`.
* In general, it's recommended to provide a few thousands samples, though this can vary a lot.
* It's recommended that total size of all samples be about ~x100 times the target size of dictionary.
* Note: ZDICT_trainFromBuffer_legacy() will send notifications into stderr if instructed to, using notificationLevel>0.
*/
ZDICTLIB_STATIC_API size_t ZDICT_trainFromBuffer_legacy(
void* dictBuffer, size_t dictBufferCapacity,
const void* samplesBuffer, const size_t* samplesSizes, unsigned nbSamples,
ZDICT_legacy_params_t parameters);
/* Deprecation warnings */
/* It is generally possible to disable deprecation warnings from compiler,
for example with -Wno-deprecated-declarations for gcc
or _CRT_SECURE_NO_WARNINGS in Visual.
Otherwise, it's also possible to manually define ZDICT_DISABLE_DEPRECATE_WARNINGS */
#ifdef ZDICT_DISABLE_DEPRECATE_WARNINGS
# define ZDICT_DEPRECATED(message) /* disable deprecation warnings */
#else
# define ZDICT_GCC_VERSION (__GNUC__ * 100 + __GNUC_MINOR__)
# if defined (__cplusplus) && (__cplusplus >= 201402) /* C++14 or greater */
# define ZDICT_DEPRECATED(message) [[deprecated(message)]]
# elif defined(__clang__) || (ZDICT_GCC_VERSION >= 405)
# define ZDICT_DEPRECATED(message) __attribute__((deprecated(message)))
# elif (ZDICT_GCC_VERSION >= 301)
# define ZDICT_DEPRECATED(message) __attribute__((deprecated))
# elif defined(_MSC_VER)
# define ZDICT_DEPRECATED(message) __declspec(deprecated(message))
# else
# pragma message("WARNING: You need to implement ZDICT_DEPRECATED for this compiler")
# define ZDICT_DEPRECATED(message)
# endif
#endif /* ZDICT_DISABLE_DEPRECATE_WARNINGS */
ZDICT_DEPRECATED("use ZDICT_finalizeDictionary() instead")
ZDICTLIB_STATIC_API
size_t ZDICT_addEntropyTablesFromBuffer(void* dictBuffer, size_t dictContentSize, size_t dictBufferCapacity,
const void* samplesBuffer, const size_t* samplesSizes, unsigned nbSamples);
#if defined (__cplusplus)
}
#endif
#endif /* ZSTD_ZDICT_H_STATIC */

3209
libs/include/zstd.h Normal file

File diff suppressed because it is too large Load Diff

107
libs/include/zstd_errors.h Normal file
View File

@@ -0,0 +1,107 @@
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
* All rights reserved.
*
* This source code is licensed under both the BSD-style license (found in the
* LICENSE file in the root directory of this source tree) and the GPLv2 (found
* in the COPYING file in the root directory of this source tree).
* You may select, at your option, one of the above-listed licenses.
*/
#ifndef ZSTD_ERRORS_H_398273423
#define ZSTD_ERRORS_H_398273423
#if defined (__cplusplus)
extern "C" {
#endif
/* ===== ZSTDERRORLIB_API : control library symbols visibility ===== */
#ifndef ZSTDERRORLIB_VISIBLE
/* Backwards compatibility with old macro name */
# ifdef ZSTDERRORLIB_VISIBILITY
# define ZSTDERRORLIB_VISIBLE ZSTDERRORLIB_VISIBILITY
# elif defined(__GNUC__) && (__GNUC__ >= 4) && !defined(__MINGW32__)
# define ZSTDERRORLIB_VISIBLE __attribute__ ((visibility ("default")))
# else
# define ZSTDERRORLIB_VISIBLE
# endif
#endif
#ifndef ZSTDERRORLIB_HIDDEN
# if defined(__GNUC__) && (__GNUC__ >= 4) && !defined(__MINGW32__)
# define ZSTDERRORLIB_HIDDEN __attribute__ ((visibility ("hidden")))
# else
# define ZSTDERRORLIB_HIDDEN
# endif
#endif
#if defined(ZSTD_DLL_EXPORT) && (ZSTD_DLL_EXPORT==1)
# define ZSTDERRORLIB_API __declspec(dllexport) ZSTDERRORLIB_VISIBLE
#elif defined(ZSTD_DLL_IMPORT) && (ZSTD_DLL_IMPORT==1)
# define ZSTDERRORLIB_API __declspec(dllimport) ZSTDERRORLIB_VISIBLE /* It isn't required but allows to generate better code, saving a function pointer load from the IAT and an indirect jump.*/
#else
# define ZSTDERRORLIB_API ZSTDERRORLIB_VISIBLE
#endif
/*-*********************************************
* Error codes list
*-*********************************************
* Error codes _values_ are pinned down since v1.3.1 only.
* Therefore, don't rely on values if you may link to any version < v1.3.1.
*
* Only values < 100 are considered stable.
*
* note 1 : this API shall be used with static linking only.
* dynamic linking is not yet officially supported.
* note 2 : Prefer relying on the enum than on its value whenever possible
* This is the only supported way to use the error list < v1.3.1
* note 3 : ZSTD_isError() is always correct, whatever the library version.
**********************************************/
typedef enum {
ZSTD_error_no_error = 0,
ZSTD_error_GENERIC = 1,
ZSTD_error_prefix_unknown = 10,
ZSTD_error_version_unsupported = 12,
ZSTD_error_frameParameter_unsupported = 14,
ZSTD_error_frameParameter_windowTooLarge = 16,
ZSTD_error_corruption_detected = 20,
ZSTD_error_checksum_wrong = 22,
ZSTD_error_literals_headerWrong = 24,
ZSTD_error_dictionary_corrupted = 30,
ZSTD_error_dictionary_wrong = 32,
ZSTD_error_dictionaryCreation_failed = 34,
ZSTD_error_parameter_unsupported = 40,
ZSTD_error_parameter_combination_unsupported = 41,
ZSTD_error_parameter_outOfBound = 42,
ZSTD_error_tableLog_tooLarge = 44,
ZSTD_error_maxSymbolValue_tooLarge = 46,
ZSTD_error_maxSymbolValue_tooSmall = 48,
ZSTD_error_cannotProduce_uncompressedBlock = 49,
ZSTD_error_stabilityCondition_notRespected = 50,
ZSTD_error_stage_wrong = 60,
ZSTD_error_init_missing = 62,
ZSTD_error_memory_allocation = 64,
ZSTD_error_workSpace_tooSmall= 66,
ZSTD_error_dstSize_tooSmall = 70,
ZSTD_error_srcSize_wrong = 72,
ZSTD_error_dstBuffer_null = 74,
ZSTD_error_noForwardProgress_destFull = 80,
ZSTD_error_noForwardProgress_inputEmpty = 82,
/* following error codes are __NOT STABLE__, they can be removed or changed in future versions */
ZSTD_error_frameIndex_tooLarge = 100,
ZSTD_error_seekableIO = 102,
ZSTD_error_dstBuffer_wrong = 104,
ZSTD_error_srcBuffer_wrong = 105,
ZSTD_error_sequenceProducer_failed = 106,
ZSTD_error_externalSequences_invalid = 107,
ZSTD_error_maxCode = 120 /* never EVER use this value directly, it can change in future versions! Use ZSTD_isError() instead */
} ZSTD_ErrorCode;
ZSTDERRORLIB_API const char* ZSTD_getErrorString(ZSTD_ErrorCode code); /**< Same as ZSTD_getErrorName, but using a `ZSTD_ErrorCode` enum argument */
#if defined (__cplusplus)
}
#endif
#endif /* ZSTD_ERRORS_H_398273423 */

BIN
libs/libzstd.a Normal file

Binary file not shown.

28
scripts/auto-install.sh Executable file
View File

@@ -0,0 +1,28 @@
#!/usr/bin/env bash
REPO="NohamR/RMHook"
FILE="rmfakecloud.dylib"
APP_PATH="/Applications/remarkable.app"
echo "[INFO] Downloading $FILE..."
curl -sL \
-o "/tmp/$FILE" \
"https://github.com/$REPO/releases/latest/download/$FILE"
# Fix the sandbox
echo "[INFO] Linking sandbox directory..."
ln -sf ~/Library/Containers/com.remarkable.desktop/Data/Library/Application\ Support/remarkable \
~/Library/Application\ Support/remarkable
echo "[INFO] Downloading inject script..."
curl -sL \
-o "/tmp/inject.sh" \
"https://raw.githubusercontent.com/$REPO/refs/heads/main/scripts/inject.sh"
echo "[INFO] Downloading optool..."
curl -sL \
-o "/tmp/optool" \
"https://raw.githubusercontent.com/$REPO/refs/heads/main/scripts/optool"
chmod +x /tmp/inject.sh /tmp/optool
echo "[INFO] Running inject script..."
/tmp/inject.sh "/tmp/$FILE" "$APP_PATH"

View File

@@ -1,14 +1,61 @@
#!/bin/bash #!/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)
# qmlrebuild - Qt resource data registration hooking
# dev - Development/reverse engineering mode with all hooks
# By default, compile reMarkable
APP_NAME=${1:-reMarkable}
PROJECT_DIR=$(cd "$(dirname "$0")/.." && pwd) PROJECT_DIR=$(cd "$(dirname "$0")/.." && pwd)
# Qt path detection (adjust according to your installation) # Qt path detection
QT_PATH=${QT_PATH:-"$HOME/Qt/6.10.0"} QT_PATH=${QT_PATH:-$(ls -d "$HOME/Qt/6."* 2>/dev/null | sort -V | tail -n1)}
echo "🔨 Compiling $APP_NAME.dylib..." # Parse build mode argument
BUILD_MODE=${1:-rmfakecloud}
# Determine final dylib name for the selected build mode
case "$BUILD_MODE" in
rmfakecloud)
DYLIB_NAME="rmfakecloud.dylib"
;;
qmlrebuild)
DYLIB_NAME="qmlrebuild.dylib"
;;
dev)
DYLIB_NAME="dev.dylib"
;;
all)
DYLIB_NAME="all.dylib"
;;
*)
DYLIB_NAME="RMHook.dylib"
;;
esac
# Set CMake options based on build mode
CMAKE_OPTIONS=""
case "$BUILD_MODE" in
rmfakecloud)
CMAKE_OPTIONS="-DBUILD_MODE_RMFAKECLOUD=ON -DBUILD_MODE_QMLREBUILD=OFF -DBUILD_MODE_DEV=OFF"
;;
qmlrebuild)
CMAKE_OPTIONS="-DBUILD_MODE_RMFAKECLOUD=OFF -DBUILD_MODE_QMLREBUILD=ON -DBUILD_MODE_DEV=OFF"
;;
dev)
CMAKE_OPTIONS="-DBUILD_MODE_RMFAKECLOUD=OFF -DBUILD_MODE_QMLREBUILD=OFF -DBUILD_MODE_DEV=ON"
;;
all)
CMAKE_OPTIONS="-DBUILD_MODE_RMFAKECLOUD=ON -DBUILD_MODE_QMLREBUILD=ON -DBUILD_MODE_DEV=ON"
;;
*)
echo "❌ Unknown build mode: $BUILD_MODE"
echo "Available modes: rmfakecloud (default), qmlrebuild, dev, all"
exit 1
;;
esac
echo "🔨 Compiling RMHook.dylib (mode: $BUILD_MODE)..."
echo "📦 Qt path: $QT_PATH" echo "📦 Qt path: $QT_PATH"
# Create build directories if necessary # Create build directories if necessary
@@ -17,21 +64,32 @@ cd "$PROJECT_DIR/build"
# Configure with CMake and compile # Configure with CMake and compile
if [ -d "$QT_PATH" ]; then if [ -d "$QT_PATH" ]; then
cmake -DCMAKE_PREFIX_PATH="$QT_PATH" .. cmake -DCMAKE_PREFIX_PATH="$QT_PATH" $CMAKE_OPTIONS ..
else else
echo "⚠️ Qt not found at $QT_PATH, trying without specifying path..." echo "⚠️ Qt not found at $QT_PATH, trying without specifying path..."
cmake .. cmake $CMAKE_OPTIONS ..
fi fi
make $APP_NAME make RMHook
if [ $? -eq 0 ]; then if [ $? -eq 0 ]; then
# Rename the produced dylib so each build mode has a distinct file name
DYLIB_DIR="$PROJECT_DIR/build/dylibs"
DEFAULT_DYLIB="$DYLIB_DIR/RMHook.dylib"
TARGET_DYLIB="$DYLIB_DIR/$DYLIB_NAME"
if [ -f "$DEFAULT_DYLIB" ]; then
mv "$DEFAULT_DYLIB" "$TARGET_DYLIB"
else
echo "⚠️ Expected dylib not found at $DEFAULT_DYLIB"
fi
echo "" echo ""
echo "✅ Compilation successful!" echo "✅ Compilation successful!"
echo "📍 Dylib: $PROJECT_DIR/build/dylibs/$APP_NAME.dylib" echo "📍 Dylib: $TARGET_DYLIB"
echo "" echo ""
echo "🚀 To inject into the reMarkable application:" 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=\"$TARGET_DYLIB\" /Applications/reMarkable.app/Contents/MacOS/reMarkable"
echo "" echo ""
else else
echo "❌ Compilation failed" echo "❌ Compilation failed"

View File

@@ -45,6 +45,12 @@ fi
echo "[INFO] Executable name: $APP_NAME" echo "[INFO] Executable name: $APP_NAME"
# 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"
EXECUTABLE_PATH="$APP_PATH/Contents/MacOS/$APP_NAME" EXECUTABLE_PATH="$APP_PATH/Contents/MacOS/$APP_NAME"
if [ ! -f "$EXECUTABLE_PATH" ]; then if [ ! -f "$EXECUTABLE_PATH" ]; then
echo "[ERROR] The specified executable ($EXECUTABLE_PATH) does not exist." echo "[ERROR] The specified executable ($EXECUTABLE_PATH) does not exist."
@@ -57,7 +63,10 @@ mkdir -p "$APP_PATH/Contents/Resources/"
cp "$DYLIB" "$APP_PATH/Contents/Resources/" cp "$DYLIB" "$APP_PATH/Contents/Resources/"
echo "[INFO] Copied $DYLIB to $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)"
"$SCRIPT_DIR/optool" install -c load -p "@executable_path/../Resources/$(basename "$DYLIB")" -t "$EXECUTABLE_PATH"
echo "[INFO] Injected $DYLIB into $EXECUTABLE_PATH" echo "[INFO] Injected $DYLIB into $EXECUTABLE_PATH"
sudo codesign --remove-signature "$EXECUTABLE_PATH" sudo codesign --remove-signature "$EXECUTABLE_PATH"
@@ -67,12 +76,6 @@ sudo xattr -cr "$EXECUTABLE_PATH"
echo "Signed successfully." 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 # Remove _MASReceipt if it exists
RECEIPT_PATH="$APP_PATH/Contents/_MASReceipt" RECEIPT_PATH="$APP_PATH/Contents/_MASReceipt"
if [ -d "$RECEIPT_PATH" ]; then if [ -d "$RECEIPT_PATH" ]; then

BIN
scripts/optool Executable file

Binary file not shown.

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

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

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

@@ -0,0 +1,152 @@
#import "Config.h"
#import "Logger.h"
#include <QtCore/QString>
static NSString *const kConfigFileName = @"rmfakecloud.config";
static NSString *const kConfigHostKey = @"host";
static NSString *const kConfigPortKey = @"port";
static NSString *const kConfigClientCertKey = @"client_cert";
static NSString *const kConfigClientKeyKey = @"client_key";
static NSString *const kConfigCACertKey = @"ca_cert";
static NSString *const kConfigKeyAlgorithmKey = @"key_algorithm";
static NSString *const kConfigDisableSSLVerifyKey = @"disable_ssl_verification";
static NSString *const kDefaultHost = @"example.com";
static NSNumber *const kDefaultPort = @(443);
NSString *gConfiguredHostObjC = @"example.com";
NSNumber *gConfiguredPortObjC = @(443);
QString gConfiguredHost = QString::fromUtf8("example.com");
NSNumber *gConfiguredPort = @(443);
QString gConfiguredClientCertPath;
QString gConfiguredClientKeyPath;
QString gConfiguredCACertPath;
QSsl::KeyAlgorithm gConfiguredKeyAlgorithm = QSsl::Rsa;
bool gDisableSSLVerification = false;
static NSString *PreferencesDirectory(void) {
NSArray<NSString *> *libraryPaths = NSSearchPathForDirectoriesInDomains(NSLibraryDirectory, NSUserDomainMask, YES);
NSString *libraryDir = [libraryPaths firstObject];
if (![libraryDir length]) {
libraryDir = [NSHomeDirectory() stringByAppendingPathComponent:@"Library"];
}
return [libraryDir stringByAppendingPathComponent:@"Preferences"];
}
NSString *ConfigFilePath(void) {
return [PreferencesDirectory() stringByAppendingPathComponent:kConfigFileName];
}
static BOOL WriteConfig(NSString *path, NSDictionary<NSString *, id> *config) {
NSError *error = nil;
NSData *jsonData = [NSJSONSerialization dataWithJSONObject:config options:NSJSONWritingPrettyPrinted error:&error];
if (!jsonData || error) {
NSLogger(@"[RMHook] Failed to serialize config: %@", error);
return NO;
}
if (![jsonData writeToFile:path atomically:YES]) {
NSLogger(@"[RMHook] Failed to write config file at %@", path);
return NO;
}
return YES;
}
static inline QString QStringFromNSStringSafe(NSString *string) {
if (!string) {
return QString();
}
return QString::fromUtf8([string UTF8String]);
}
void ConfigLoadOrCreate(void) {
NSString *configPath = ConfigFilePath();
NSString *directory = [configPath stringByDeletingLastPathComponent];
NSFileManager *fileManager = [NSFileManager defaultManager];
BOOL isDirectory = NO;
NSError *error = nil;
if (![fileManager fileExistsAtPath:directory isDirectory:&isDirectory] || !isDirectory) {
if (![fileManager createDirectoryAtPath:directory withIntermediateDirectories:YES attributes:nil error:&error]) {
NSLogger(@"[RMHook] Failed to create config directory %@: %@", directory, error);
}
}
NSDictionary<NSString *, id> *defaults = @{kConfigHostKey : kDefaultHost,
kConfigPortKey : kDefaultPort};
if ([fileManager fileExistsAtPath:configPath isDirectory:&isDirectory] && !isDirectory) {
NSData *data = [NSData dataWithContentsOfFile:configPath];
if ([data length] > 0) {
NSError *jsonError = nil;
id jsonObject = [NSJSONSerialization JSONObjectWithData:data options:0 error:&jsonError];
if (!jsonError && [jsonObject isKindOfClass:[NSDictionary class]]) {
NSDictionary *configDict = (NSDictionary *)jsonObject;
NSString *hostValue = configDict[kConfigHostKey];
NSNumber *portValue = configDict[kConfigPortKey];
NSString *resolvedHost = ([hostValue isKindOfClass:[NSString class]] && [hostValue length]) ? hostValue : kDefaultHost;
NSInteger portCandidate = kDefaultPort.integerValue;
if ([portValue respondsToSelector:@selector(integerValue)]) {
NSInteger candidate = [portValue integerValue];
if (candidate > 0 && candidate <= 65535) {
portCandidate = candidate;
} else {
NSLogger(@"[RMHook] Ignoring invalid port value %@, falling back to default.", portValue);
}
}
gConfiguredHostObjC = [resolvedHost copy];
gConfiguredPortObjC = @(portCandidate);
gConfiguredHost = QStringFromNSStringSafe(resolvedHost);
gConfiguredPort = @(portCandidate);
NSString *certPathValue = configDict[kConfigClientCertKey];
NSString *keyPathValue = configDict[kConfigClientKeyKey];
NSString *caPathValue = configDict[kConfigCACertKey];
NSNumber *disableSSLValue = configDict[kConfigDisableSSLVerifyKey];
if ([certPathValue isKindOfClass:[NSString class]] && [certPathValue length]) {
gConfiguredClientCertPath = QStringFromNSStringSafe(certPathValue);
}
if ([keyPathValue isKindOfClass:[NSString class]] && [keyPathValue length]) {
gConfiguredClientKeyPath = QStringFromNSStringSafe(keyPathValue);
}
if ([caPathValue isKindOfClass:[NSString class]] && [caPathValue length]) {
gConfiguredCACertPath = QStringFromNSStringSafe(caPathValue);
}
NSString *keyAlgoValue = configDict[kConfigKeyAlgorithmKey];
if ([keyAlgoValue isKindOfClass:[NSString class]] && [keyAlgoValue length]) {
NSString *lower = [keyAlgoValue lowercaseString];
if ([lower isEqualToString:@"ec"]) {
gConfiguredKeyAlgorithm = QSsl::Ec;
} else if ([lower isEqualToString:@"dsa"]) {
gConfiguredKeyAlgorithm = QSsl::Dsa;
} else if ([lower isEqualToString:@"dh"]) {
gConfiguredKeyAlgorithm = QSsl::Dh;
} else if ([lower isEqualToString:@"opaque"]) {
gConfiguredKeyAlgorithm = QSsl::Opaque;
} else {
gConfiguredKeyAlgorithm = QSsl::Rsa;
}
}
if ([disableSSLValue respondsToSelector:@selector(boolValue)]) {
gDisableSSLVerification = [disableSSLValue boolValue];
}
NSLogger(@"[RMHook] Loaded config from %@ with host %@ and port %@", configPath, gConfiguredHostObjC, gConfiguredPortObjC);
return;
} else {
NSLogger(@"[RMHook] Failed to parse config file %@: %@", configPath, jsonError);
}
} else {
NSLogger(@"[RMHook] Config file %@ was empty, rewriting with defaults.", configPath);
}
}
if (WriteConfig(configPath, defaults)) {
NSLogger(@"[RMHook] Created default config at %@", configPath);
}
gConfiguredHostObjC = [kDefaultHost copy];
gConfiguredPortObjC = kDefaultPort;
gConfiguredHost = QString::fromUtf8("example.com");
gConfiguredPort = kDefaultPort;
}

47
src/RMHook/DevHooks.h Normal file
View File

@@ -0,0 +1,47 @@
#ifndef DEV_HOOKS_H
#define DEV_HOOKS_H
#ifdef BUILD_MODE_DEV
#import <Foundation/Foundation.h>
#include <stdint.h>
// Forward declarations for Qt types
class QIODevice;
class QObject;
namespace QtSharedPointer {
struct ExternalRefCountData;
}
extern ssize_t (*original_qIODevice_write)(QIODevice *self, const char *data, int64_t maxSize);
extern int64_t (*original_qmlregister)(int64_t, int64_t, int64_t, int64_t, int64_t, int64_t, int, int64_t, int, int64_t);
extern int64_t (*original_function_at_0x100011790)(uint64_t *a1);
extern int64_t (*original_function_at_0x100011CE0)(int64_t, const QObject *, unsigned char, int64_t, QtSharedPointer::ExternalRefCountData *);
extern int64_t (*original_function_at_0x10015A130)(int64_t, int64_t);
extern void (*original_function_at_0x10015BC90)(int64_t, int64_t);
extern int64_t (*original_function_at_0x10016D520)(int64_t, int64_t *, unsigned int, int64_t);
extern void (*original_function_at_0x1001B6EE0)(int64_t, int64_t *, unsigned int);
#ifdef __cplusplus
extern "C" {
#endif
ssize_t hooked_qIODevice_write(QIODevice *self, const char *data, int64_t maxSize);
int64_t hooked_qmlregister(int64_t, int64_t, int64_t, int64_t, int64_t, int64_t, int, int64_t, int, int64_t);
int64_t hooked_function_at_0x100011790(uint64_t *a1);
int64_t hooked_function_at_0x100011CE0(int64_t, const QObject *, unsigned char, int64_t, QtSharedPointer::ExternalRefCountData *);
int64_t hooked_function_at_0x10015A130(int64_t, int64_t);
void hooked_function_at_0x10015BC90(int64_t, int64_t);
int64_t hooked_function_at_0x10016D520(int64_t, int64_t *, unsigned int, int64_t);
void hooked_function_at_0x1001B6EE0(int64_t, int64_t *, unsigned int);
#ifdef __cplusplus
}
#endif
void logMemory(const char *label, void *address, size_t length);
void logStackTrace(const char *label);
#endif // BUILD_MODE_DEV
#endif // DEV_HOOKS_H

389
src/RMHook/DevHooks.m Normal file
View File

@@ -0,0 +1,389 @@
#ifdef BUILD_MODE_DEV
#import "DevHooks.h"
#import "Logger.h"
#import <Foundation/Foundation.h>
#include <stdint.h>
#include <string.h>
#include <QtCore/QIODevice>
#include <QtCore/QObject>
// Original function pointers
ssize_t (*original_qIODevice_write)(QIODevice *self, const char *data, int64_t maxSize) = NULL;
int64_t (*original_qmlregister)(
int64_t a1,
int64_t a2,
int64_t a3,
int64_t a4,
int64_t a5,
int64_t a6,
int a7,
int64_t a8,
int a9,
int64_t a10) = NULL;
int64_t (*original_function_at_0x100011790)(uint64_t *a1) = NULL;
int64_t (*original_function_at_0x100011CE0)(int64_t a1, const QObject *a2, unsigned char a3, int64_t a4, QtSharedPointer::ExternalRefCountData *a5) = NULL;
int64_t (*original_function_at_0x10015A130)(int64_t a1, int64_t a2) = NULL;
void (*original_function_at_0x10015BC90)(int64_t a1, int64_t a2) = NULL;
int64_t (*original_function_at_0x10016D520)(int64_t a1, int64_t *a2, unsigned int a3, int64_t a4) = NULL;
void (*original_function_at_0x1001B6EE0)(int64_t a1, int64_t *a2, unsigned int a3) = NULL;
#pragma mark - Helper Functions
void logMemory(const char *label, void *address, size_t length) {
if (!address) {
NSLogger(@"[RMHook] %s: (null)", label);
return;
}
unsigned char *ptr = (unsigned char *)address;
NSMutableString *hexLine = [NSMutableString stringWithFormat:@"[RMHook] %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:@"[RMHook] "];
}
}
// Log remaining bytes if any
if ([hexLine length] > 28) { // More than just the prefix
NSLogger(@"%@", hexLine);
}
}
void logStackTrace(const char *label) {
NSLogger(@"[RMHook] %s - Stack trace:", label);
NSArray<NSString *> *callStack = [NSThread callStackSymbols];
NSUInteger count = [callStack count];
for (NSUInteger i = 0; i < count; i++) {
NSString *frame = callStack[i];
NSLogger(@"[RMHook] #%lu: %@", (unsigned long)i, frame);
}
}
#pragma mark - Hook Implementations
extern "C" ssize_t hooked_qIODevice_write(
QIODevice *self,
const char *data,
int64_t maxSize) {
NSLogger(@"[RMHook] QIODevice::write called with maxSize: %lld", (long long)maxSize);
logStackTrace("QIODevice::write call stack");
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(@"[RMHook] QIODevice::write result: %zd", result);
return result;
}
NSLogger(@"[RMHook] WARNING: Original QIODevice::write not available, returning 0");
return 0;
}
extern "C" int64_t hooked_function_at_0x100011790(uint64_t *a1) {
NSLogger(@"[RMHook] Hook at 0x100011790 called!");
NSLogger(@"[RMHook] a1 = %p", a1);
if (a1) {
NSLogger(@"[RMHook] *a1 = 0x%llx", (unsigned long long)*a1);
logMemory("Memory at a1", (void *)a1, 64);
logMemory("Memory at *a1", (void *)(*a1), 64);
} else {
NSLogger(@"[RMHook] a1 is NULL");
}
if (original_function_at_0x100011790) {
int64_t result = original_function_at_0x100011790(a1);
NSLogger(@"[RMHook] result = 0x%llx", (unsigned long long)result);
return result;
}
NSLogger(@"[RMHook] WARNING: Original function at 0x100011790 not available, returning 0");
return 0;
}
extern "C" int64_t hooked_function_at_0x100011CE0(
int64_t a1,
const QObject *a2,
unsigned char a3,
int64_t a4,
QtSharedPointer::ExternalRefCountData *a5) {
// This function appears to be a QML type registration wrapper
// It calls QQmlPrivate::qmlregister(3, &registrationData)
//
// Based on IDA analysis:
// - a1: stored at offset +0x8 in registration struct (likely type metadata ptr)
// - a2: NOT actually a QObject* - low bits used as: ((_WORD)a2 << 8) | a3
// This suggests a2's low 16 bits are a version/revision number
// - a3: combined with a2 to form v17 (flags/version field)
// - a4: stored at offset +0x18 (likely URI or type info pointer)
// - a5: ExternalRefCountData* for shared pointer ref counting
NSLogger(@"[RMHook] ========================================");
NSLogger(@"[RMHook] Hook at 0x100011CE0 (QML Type Registration)");
NSLogger(@"[RMHook] ========================================");
NSLogger(@"[RMHook] a1 (typeMetadata?) = 0x%llx", (unsigned long long)a1);
uint16_t a2_low = (uint16_t)(uintptr_t)a2;
uint16_t combined_v17 = (a2_low << 8) | a3;
NSLogger(@"[RMHook] a2 (raw) = %p (0x%llx)", a2, (unsigned long long)(uintptr_t)a2);
NSLogger(@"[RMHook] a2 low 16 bits = 0x%04x (%u)", a2_low, a2_low);
NSLogger(@"[RMHook] a3 (flags/version) = 0x%02x (%u)", a3, a3);
NSLogger(@"[RMHook] v17 = (a2<<8)|a3 = 0x%04x (%u)", combined_v17, combined_v17);
NSLogger(@"[RMHook] a4 (typeInfo/URI?) = 0x%llx", (unsigned long long)a4);
NSLogger(@"[RMHook] a5 (refCountData) = %p", a5);
if (a1) {
logMemory("Memory at a1 (typeMetadata)", (void *)a1, 64);
void **vtable = (void **)a1;
NSLogger(@"[RMHook] a1 vtable/first ptr = %p", *vtable);
}
if (a4) {
logMemory("Memory at a4 (typeInfo)", (void *)a4, 64);
const char *maybeStr = (const char *)a4;
bool isPrintable = true;
int len = 0;
for (int i = 0; i < 64 && maybeStr[i]; i++) {
if (maybeStr[i] < 0x20 || maybeStr[i] > 0x7e) {
isPrintable = false;
break;
}
len++;
}
if (isPrintable && len > 0) {
NSLogger(@"[RMHook] a4 as string: \"%.*s\"", len, maybeStr);
}
}
if (a5) {
logMemory("Memory at a5 (refCountData)", (void *)a5, 32);
}
logStackTrace("QML Registration context");
if (original_function_at_0x100011CE0) {
int64_t result = original_function_at_0x100011CE0(a1, a2, a3, a4, a5);
NSLogger(@"[RMHook] result (qmlregister return) = %u (0x%x)", (unsigned int)result, (unsigned int)result);
NSLogger(@"[RMHook] ========================================");
return result;
}
NSLogger(@"[RMHook] WARNING: Original function at 0x100011CE0 not available, returning 0");
return 0;
}
extern "C" int64_t hooked_function_at_0x10015A130(int64_t a1, int64_t a2) {
NSLogger(@"[RMHook] Hook at 0x10015A130 called!");
NSLogger(@"[RMHook] a1 = 0x%llx", (unsigned long long)a1);
NSLogger(@"[RMHook] a2 = 0x%llx", (unsigned long long)a2);
logMemory("Memory at a1", (void *)a1, 64);
logMemory("Memory at a2", (void *)a2, 64);
if (original_function_at_0x10015A130) {
int64_t result = original_function_at_0x10015A130(a1, a2);
NSLogger(@"[RMHook] result = 0x%llx", (unsigned long long)result);
return result;
}
NSLogger(@"[RMHook] WARNING: Original function at 0x10015A130 not available, returning 0");
return 0;
}
extern "C" void hooked_function_at_0x10015BC90(int64_t a1, int64_t a2) {
NSLogger(@"[RMHook] Hook at 0x10015BC90 called!");
NSLogger(@"[RMHook] a1 = 0x%llx", (unsigned long long)a1);
NSLogger(@"[RMHook] a2 = 0x%llx", (unsigned long long)a2);
logMemory("Memory at a1", (void *)a1, 64);
logMemory("Memory at a2", (void *)a2, 64);
if (original_function_at_0x10015BC90) {
original_function_at_0x10015BC90(a1, a2);
NSLogger(@"[RMHook] original function returned (void)");
return;
}
NSLogger(@"[RMHook] WARNING: Original function at 0x10015BC90 not available");
}
extern "C" int64_t hooked_function_at_0x10016D520(int64_t a1, int64_t *a2, unsigned int a3, int64_t a4) {
NSLogger(@"[RMHook] Hook at 0x10016D520 called!");
NSLogger(@"[RMHook] a1 = 0x%llx", (unsigned long long)a1);
NSLogger(@"[RMHook] a2 = %p", a2);
if (a2) {
NSLogger(@"[RMHook] *a2 = 0x%llx", (unsigned long long)*a2);
}
NSLogger(@"[RMHook] a3 = %u (0x%x)", a3, a3);
NSLogger(@"[RMHook] a4 = 0x%llx", (unsigned long long)a4);
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(@"[RMHook] result = 0x%llx", (unsigned long long)result);
return result;
}
NSLogger(@"[RMHook] 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(@"[RMHook] Hook at 0x1001B6EE0 called!");
NSLogger(@"[RMHook] a1 = 0x%llx", (unsigned long long)a1);
// At a1 (PdfExporter object):
// +0x10 contains a QString (likely document name)
NSLogger(@"[RMHook] Reading QString at a1+0x10:");
logMemory("a1 + 0x10 (raw)", (void *)(a1 + 0x10), 64);
void **qstrPtr = (void **)(a1 + 0x10);
void *dataPtr = *qstrPtr;
if (!dataPtr) {
NSLogger(@"[RMHook] 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(@"[RMHook] QString plausible size=%d (found at -%d)", size, delta);
break;
}
}
if (size > 0) {
NSString *qstringValue = [[NSString alloc] initWithCharacters:(unichar *)dataPtr length:size];
NSLogger(@"[RMHook] QString value: \"%@\"", qstringValue);
} else {
NSLogger(@"[RMHook] QString: could not find valid size");
}
NSLogger(@"[RMHook] a2 = %p", a2);
if (a2) {
NSLogger(@"[RMHook] *a2 = 0x%llx", (unsigned long long)*a2);
}
NSLogger(@"[RMHook] a3 = %u (0x%x)", a3, a3);
if (original_function_at_0x1001B6EE0) {
original_function_at_0x1001B6EE0(a1, a2, a3);
NSLogger(@"[RMHook] Original function at 0x1001B6EE0 executed");
} else {
NSLogger(@"[RMHook] WARNING: Original function not available");
}
}
extern "C" int64_t hooked_qmlregister(
int64_t a1,
int64_t a2,
int64_t a3,
int64_t a4,
int64_t a5,
int64_t a6,
int a7,
int64_t a8,
int a9,
int64_t a10) {
NSLogger(@"[RMHook] ========================================");
NSLogger(@"[RMHook] QQmlPrivate::qmlregister called!");
NSLogger(@"[RMHook] ========================================");
NSLogger(@"[RMHook] a1 (RegistrationType) = 0x%llx (%lld)", (unsigned long long)a1, (long long)a1);
NSLogger(@"[RMHook] a2 = 0x%llx (%lld)", (unsigned long long)a2, (long long)a2);
NSLogger(@"[RMHook] a3 = 0x%llx (%lld)", (unsigned long long)a3, (long long)a3);
NSLogger(@"[RMHook] a4 = 0x%llx (%lld)", (unsigned long long)a4, (long long)a4);
NSLogger(@"[RMHook] a5 = 0x%llx (%lld)", (unsigned long long)a5, (long long)a5);
NSLogger(@"[RMHook] a6 = 0x%llx (%lld)", (unsigned long long)a6, (long long)a6);
NSLogger(@"[RMHook] a7 = 0x%x (%d)", a7, a7);
NSLogger(@"[RMHook] a8 = 0x%llx (%lld)", (unsigned long long)a8, (long long)a8);
NSLogger(@"[RMHook] a9 = 0x%x (%d)", a9, a9);
NSLogger(@"[RMHook] a10 = 0x%llx (%lld)", (unsigned long long)a10, (long long)a10);
// Check for PlatformHelpers registration
// a1 == 0 means TypeRegistration (object registration)
// a4 must be a valid pointer (not a small integer like 0, 1, 2, etc.)
if (a1 == 0 && a4 > 0x10000) {
const char *typeName = (const char *)a4;
int len = 0;
bool isValid = true;
for (int i = 0; i < 256; i++) {
char c = typeName[i];
if (c == '\0') {
break;
}
if (c < 0x20 || c > 0x7e) {
isValid = false;
break;
}
len++;
}
if (isValid && len > 0) {
NSLogger(@"[RMHook] typeName (a4) = \"%.*s\"", len, typeName);
if (len == 15 && strncmp(typeName, "PlatformHelpers", 15) == 0) {
NSLogger(@"[RMHook] !!! FOUND PlatformHelpers type registration !!!");
NSLogger(@"[RMHook] factory ptr (a2) = %p", (void *)a2);
NSLogger(@"[RMHook] a3 (metaObject?) = %p", (void *)a3);
NSLogger(@"[RMHook] a5 = %p", (void *)a5);
NSLogger(@"[RMHook] a6 = %p", (void *)a6);
logMemory("Factory ptr memory", (void *)a2, 64);
logMemory("a3 memory (metaObject?)", (void *)a3, 64);
logStackTrace("PlatformHelpers registration");
}
}
}
// Try to interpret a2 as memory region for other registration types
if (a2 > 0x10000 && a1 != 0) {
logMemory("Memory at a2", (void *)a2, 64);
const char *maybeStr = (const char *)a2;
bool isPrintable = true;
int len = 0;
for (int i = 0; i < 128 && maybeStr[i]; i++) {
if (maybeStr[i] < 0x20 || maybeStr[i] > 0x7e) {
isPrintable = false;
break;
}
len++;
}
if (isPrintable && len > 0) {
NSLogger(@"[RMHook] a2 as string: \"%.*s\"", len, maybeStr);
}
}
int64_t result = 0;
if (original_qmlregister) {
result = original_qmlregister(a1, a2, a3, a4, a5, a6, a7, a8, a9, a10);
NSLogger(@"[RMHook] result = 0x%llx (%lld)", (unsigned long long)result, (long long)result);
} else {
NSLogger(@"[RMHook] WARNING: Original qmlregister not available!");
}
NSLogger(@"[RMHook] ========================================");
return result;
}
#endif // BUILD_MODE_DEV

View File

@@ -1,5 +1,5 @@
#import <Foundation/Foundation.h> #import <Foundation/Foundation.h>
@interface reMarkable : NSObject @interface RMHook : NSObject
@end @end

View File

@@ -1,9 +1,17 @@
#import "reMarkable.h" #import "RMHook.h"
#import <Foundation/Foundation.h> #import <Foundation/Foundation.h>
#import "Constant.h" #import "Constant.h"
#import "MemoryUtils.h" #import "MemoryUtils.h"
#import "Logger.h" #import "Logger.h"
#import "ResourceUtils.h" #import "ResourceUtils.h"
#import "Config.h"
#import "SSLConfig.h"
#ifdef BUILD_MODE_DEV
#import "DevHooks.h"
#endif
#ifdef BUILD_MODE_QMLREBUILD
#import "MessageBroker.h"
#endif
#import <objc/runtime.h> #import <objc/runtime.h>
#import <Cocoa/Cocoa.h> #import <Cocoa/Cocoa.h>
#include <stdint.h> #include <stdint.h>
@@ -12,6 +20,7 @@
#include <stdlib.h> #include <stdlib.h>
#include <string.h> #include <string.h>
#include <dispatch/dispatch.h> #include <dispatch/dispatch.h>
#include <string>
#include <QtNetwork/QNetworkAccessManager> #include <QtNetwork/QNetworkAccessManager>
#include <QtNetwork/QNetworkRequest> #include <QtNetwork/QNetworkRequest>
@@ -22,112 +31,9 @@
#include <QtCore/QString> #include <QtCore/QString>
#include <QtCore/Qt> #include <QtCore/Qt>
#include <QtWebSockets/QWebSocket> #include <QtWebSockets/QWebSocket>
#include <QtCore/QSettings>
#include <QtCore/QVariant>
#include <QtCore/QAnyStringView>
static NSString *const kReMarkableConfigFileName = @"rmfakecloud.config";
static NSString *const kReMarkableConfigHostKey = @"host";
static NSString *const kReMarkableConfigPortKey = @"port";
static NSString *const kReMarkableDefaultHost = @"example.com";
static NSNumber *const kReMarkableDefaultPort = @(443);
static NSString *gConfiguredHost = @"example.com";
static NSNumber *gConfiguredPort = @(443);
static pthread_mutex_t gResourceMutex = PTHREAD_MUTEX_INITIALIZER; static pthread_mutex_t gResourceMutex = PTHREAD_MUTEX_INITIALIZER;
static NSString *ReMarkablePreferencesDirectory(void);
static NSString *ReMarkablePreferencesDirectory(void) {
NSArray<NSString *> *libraryPaths = NSSearchPathForDirectoriesInDomains(NSLibraryDirectory, NSUserDomainMask, YES);
NSString *libraryDir = [libraryPaths firstObject];
if (![libraryDir length]) {
libraryDir = [NSHomeDirectory() stringByAppendingPathComponent:@"Library"];
}
return [libraryDir stringByAppendingPathComponent:@"Preferences"];
}
static NSString *ReMarkableConfigFilePath(void) {
return [ReMarkablePreferencesDirectory() stringByAppendingPathComponent:kReMarkableConfigFileName];
}
static BOOL ReMarkableWriteConfig(NSString *path, NSDictionary<NSString *, id> *config) {
NSError *error = nil;
NSData *jsonData = [NSJSONSerialization dataWithJSONObject:config options:NSJSONWritingPrettyPrinted error:&error];
if (!jsonData || error) {
NSLogger(@"[reMarkable] Failed to serialize config: %@", error);
return NO;
}
if (![jsonData writeToFile:path atomically:YES]) {
NSLogger(@"[reMarkable] Failed to write config file at %@", path);
return NO;
}
return YES;
}
static void ReMarkableLoadOrCreateConfig(void) {
NSString *configPath = ReMarkableConfigFilePath();
NSString *directory = [configPath stringByDeletingLastPathComponent];
NSFileManager *fileManager = [NSFileManager defaultManager];
BOOL isDirectory = NO;
NSError *error = nil;
if (![fileManager fileExistsAtPath:directory isDirectory:&isDirectory] || !isDirectory) {
if (![fileManager createDirectoryAtPath:directory withIntermediateDirectories:YES attributes:nil error:&error]) {
NSLogger(@"[reMarkable] Failed to create config directory %@: %@", directory, error);
}
}
NSDictionary<NSString *, id> *defaults = @{kReMarkableConfigHostKey : kReMarkableDefaultHost,
kReMarkableConfigPortKey : kReMarkableDefaultPort};
if ([fileManager fileExistsAtPath:configPath isDirectory:&isDirectory] && !isDirectory) {
NSData *data = [NSData dataWithContentsOfFile:configPath];
if ([data length] > 0) {
NSError *jsonError = nil;
id jsonObject = [NSJSONSerialization JSONObjectWithData:data options:0 error:&jsonError];
if (!jsonError && [jsonObject isKindOfClass:[NSDictionary class]]) {
NSDictionary *configDict = (NSDictionary *)jsonObject;
NSString *hostValue = configDict[kReMarkableConfigHostKey];
NSNumber *portValue = configDict[kReMarkableConfigPortKey];
NSString *resolvedHost = ([hostValue isKindOfClass:[NSString class]] && [hostValue length]) ? hostValue : kReMarkableDefaultHost;
NSInteger portCandidate = kReMarkableDefaultPort.integerValue;
if ([portValue respondsToSelector:@selector(integerValue)]) {
NSInteger candidate = [portValue integerValue];
if (candidate > 0 && candidate <= 65535) {
portCandidate = candidate;
} else {
NSLogger(@"[reMarkable] Ignoring invalid port value %@, falling back to default.", portValue);
}
}
gConfiguredHost = [resolvedHost copy];
gConfiguredPort = @(portCandidate);
NSLogger(@"[reMarkable] Loaded config from %@ with host %@ and port %@", configPath, gConfiguredHost, gConfiguredPort);
return;
} else {
NSLogger(@"[reMarkable] Failed to parse config file %@: %@", configPath, jsonError);
}
} else {
NSLogger(@"[reMarkable] Config file %@ was empty, rewriting with defaults.", configPath);
}
}
if (ReMarkableWriteConfig(configPath, defaults)) {
NSLogger(@"[reMarkable] Created default config at %@", configPath);
}
gConfiguredHost = [kReMarkableDefaultHost copy];
gConfiguredPort = kReMarkableDefaultPort;
}
static inline QString QStringFromNSStringSafe(NSString *string) {
if (!string) {
return QString();
}
return QString::fromUtf8([string UTF8String]);
}
@interface MenuActionController : NSObject @interface MenuActionController : NSObject
@property (strong, nonatomic) NSURL *targetURL; @property (strong, nonatomic) NSURL *targetURL;
- (void)openURLAction:(id)sender; - (void)openURLAction:(id)sender;
@@ -199,33 +105,34 @@ static inline QString QStringFromNSStringSafe(NSString *string) {
@end @end
@interface reMarkableDylib : NSObject @interface RMHookDylib : NSObject
- (BOOL)hook; - (BOOL)hook;
@end @end
@implementation reMarkable @implementation RMHook
+ (void)load { + (void)load {
NSLogger(@"reMarkable dylib loaded successfully"); NSLogger(@"RMHook dylib loaded successfully");
// Initialize the hook RMHookDylib *dylib = [[RMHookDylib alloc] init];
reMarkableDylib *dylib = [[reMarkableDylib alloc] init];
[dylib hook]; [dylib hook];
// Add custom Help menu entry to open config file #ifdef BUILD_MODE_RMFAKECLOUD
NSString *configPath = ReMarkableConfigFilePath(); NSString *configPath = ConfigFilePath();
NSString *fileURL = [NSString stringWithFormat:@"file://%@", configPath]; NSString *fileURL = [NSString stringWithFormat:@"file://%@", configPath];
[MenuActionController addCustomHelpMenuEntry:@"Open rmfakecloud config" [MenuActionController addCustomHelpMenuEntry:@"Open rmfakecloud config"
withURL:fileURL withURL:fileURL
withDelay:2.0]; withDelay:2.0];
#endif
} }
@end @end
@implementation reMarkableDylib @implementation RMHookDylib
#ifdef BUILD_MODE_RMFAKECLOUD
static QNetworkReply *(*original_qNetworkAccessManager_createRequest)( static QNetworkReply *(*original_qNetworkAccessManager_createRequest)(
QNetworkAccessManager *self, QNetworkAccessManager *self,
QNetworkAccessManager::Operation op, QNetworkAccessManager::Operation op,
@@ -236,12 +143,27 @@ static void (*original_qWebSocket_open)(
QWebSocket *self, QWebSocket *self,
const QNetworkRequest &request) = NULL; const QNetworkRequest &request) = NULL;
typedef void* MQTTAsync;
typedef void* MQTTAsync_createOptions;
static int (*original_MQTTAsync_createWithOptions)(
MQTTAsync *handle,
const char *serverURI,
const char *clientId,
int persistence_type,
void *persistence_context,
MQTTAsync_createOptions *options) = NULL;
#endif
#ifdef BUILD_MODE_QMLREBUILD
static int (*original_qRegisterResourceData)( static int (*original_qRegisterResourceData)(
int, int,
const unsigned char *, const unsigned char *,
const unsigned char *, const unsigned char *,
const unsigned char *) = NULL; const unsigned char *) = NULL;
#endif
#ifdef BUILD_MODE_RMFAKECLOUD
static inline bool shouldPatchURL(const QString &host) { static inline bool shouldPatchURL(const QString &host) {
if (host.isEmpty()) { if (host.isEmpty()) {
return false; return false;
@@ -264,35 +186,71 @@ static inline bool shouldPatchURL(const QString &host) {
)""") )""")
.contains(host, Qt::CaseInsensitive); .contains(host, Qt::CaseInsensitive);
} }
#endif
static inline QString QStringFromNSStringSafe(NSString *string) {
if (!string) {
return QString();
}
return QString::fromUtf8([string UTF8String]);
}
- (BOOL)hook { - (BOOL)hook {
NSLogger(@"[reMarkable] Starting hooks..."); NSLogger(@"[RMHook] Starting hooks...");
ReMarkableLoadOrCreateConfig(); #ifdef BUILD_MODE_RMFAKECLOUD
NSLogger(@"[reMarkable] Using override host %@ and port %@", gConfiguredHost, gConfiguredPort); NSLogger(@"[RMHook] Build mode: rmfakecloud");
ConfigLoadOrCreate();
SSLConfigLoad();
NSLogger(@"[RMHook] Using override host %@ and port %@", gConfiguredHostObjC, gConfiguredPortObjC);
[MemoryUtils hookSymbol:@"QtNetwork" [MemoryUtils hookSymbol:@"QtNetwork"
symbolName:@"__ZN21QNetworkAccessManager13createRequestENS_9OperationERK15QNetworkRequestP9QIODevice" symbolName:@"__ZN21QNetworkAccessManager13createRequestENS_9OperationERK15QNetworkRequestP9QIODevice"
hookFunction:(void *)hooked_qNetworkAccessManager_createRequest hookFunction:(void *)hooked_qNetworkAccessManager_createRequest
originalFunction:(void **)&original_qNetworkAccessManager_createRequest originalFunction:(void **)&original_qNetworkAccessManager_createRequest
logPrefix:@"[reMarkable]"]; logPrefix:@"[RMHook]"];
[MemoryUtils hookSymbol:@"QtWebSockets" [MemoryUtils hookSymbol:@"QtWebSockets"
symbolName:@"__ZN10QWebSocket4openERK15QNetworkRequest" symbolName:@"__ZN10QWebSocket4openERK15QNetworkRequest"
hookFunction:(void *)hooked_qWebSocket_open hookFunction:(void *)hooked_qWebSocket_open
originalFunction:(void **)&original_qWebSocket_open originalFunction:(void **)&original_qWebSocket_open
logPrefix:@"[reMarkable]"]; logPrefix:@"[RMHook]"];
// WIP: Implement resource data registration hooking [MemoryUtils hookSymbol:@"libpaho-mqtt3as.1.dylib"
// [MemoryUtils hookSymbol:@"QtCore" symbolName:@"_MQTTAsync_createWithOptions"
// symbolName:@"__Z21qRegisterResourceDataiPKhS0_S0_" hookFunction:(void *)hooked_MQTTAsync_createWithOptions
// hookFunction:(void *)hooked_qRegisterResourceData originalFunction:(void **)&original_MQTTAsync_createWithOptions
// originalFunction:(void **)&original_qRegisterResourceData logPrefix:@"[RMHook]"];
// logPrefix:@"[reMarkable]"]; #endif
#ifdef BUILD_MODE_QMLREBUILD
NSLogger(@"[RMHook] Build mode: qmlrebuild");
messagebroker::registerQmlType();
messagebroker::setNativeCallback([](const char *signal, const char *value) {
NSLogger(@"[RMHook] Native callback received signal '%s' with value '%s'", signal, value);
});
[MemoryUtils hookSymbol:@"QtCore"
symbolName:@"__Z21qRegisterResourceDataiPKhS0_S0_"
hookFunction:(void *)hooked_qRegisterResourceData
originalFunction:(void **)&original_qRegisterResourceData
logPrefix:@"[RMHook]"];
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
messagebroker::broadcast("signalName", "Hello from dylib!");
});
#endif
#ifdef BUILD_MODE_DEV
NSLogger(@"[RMHook] Build mode: dev/reverse engineering");
#endif
return YES; return YES;
} }
#ifdef BUILD_MODE_RMFAKECLOUD
extern "C" QNetworkReply* hooked_qNetworkAccessManager_createRequest( extern "C" QNetworkReply* hooked_qNetworkAccessManager_createRequest(
QNetworkAccessManager* self, QNetworkAccessManager* self,
QNetworkAccessManager::Operation op, QNetworkAccessManager::Operation op,
@@ -301,14 +259,15 @@ extern "C" QNetworkReply* hooked_qNetworkAccessManager_createRequest(
) { ) {
const QString host = req.url().host(); const QString host = req.url().host();
if (shouldPatchURL(host)) { if (shouldPatchURL(host)) {
// Clone request to keep original immutable
QNetworkRequest newReq(req); QNetworkRequest newReq(req);
QUrl newUrl = req.url(); QUrl newUrl = req.url();
const QString overrideHost = QStringFromNSStringSafe(gConfiguredHost); const QString overrideHost = QStringFromNSStringSafe(gConfiguredHostObjC);
newUrl.setHost(overrideHost); newUrl.setHost(overrideHost);
newUrl.setPort([gConfiguredPort intValue]); newUrl.setPort([gConfiguredPortObjC intValue]);
newReq.setUrl(newUrl); newReq.setUrl(newUrl);
SSLConfigApplyToRequest(newReq);
if (original_qNetworkAccessManager_createRequest) { if (original_qNetworkAccessManager_createRequest) {
return original_qNetworkAccessManager_createRequest(self, op, newReq, outgoingData); return original_qNetworkAccessManager_createRequest(self, op, newReq, outgoingData);
} }
@@ -332,13 +291,15 @@ extern "C" void hooked_qWebSocket_open(
const QString host = req.url().host(); const QString host = req.url().host();
if (shouldPatchURL(host)) { if (shouldPatchURL(host)) {
QUrl newUrl = req.url(); QUrl newUrl = req.url();
const QString overrideHost = QStringFromNSStringSafe(gConfiguredHost); const QString overrideHost = QStringFromNSStringSafe(gConfiguredHostObjC);
newUrl.setHost(overrideHost); newUrl.setHost(overrideHost);
newUrl.setPort([gConfiguredPort intValue]); newUrl.setPort([gConfiguredPortObjC intValue]);
QNetworkRequest newReq(req); QNetworkRequest newReq(req);
newReq.setUrl(newUrl); newReq.setUrl(newUrl);
SSLConfigApplyToRequest(newReq);
original_qWebSocket_open(self, newReq); original_qWebSocket_open(self, newReq);
return; return;
} }
@@ -346,6 +307,76 @@ extern "C" void hooked_qWebSocket_open(
original_qWebSocket_open(self, req); original_qWebSocket_open(self, req);
} }
static std::string PatchMqttUri(const char* uri)
{
if (!uri) return {};
const std::string original(uri);
size_t schemeEnd = original.find("://");
size_t hostStart = (schemeEnd != std::string::npos) ? schemeEnd + 3 : 0;
size_t hostEnd = original.find_first_of(":/", hostStart);
if (hostEnd == std::string::npos) hostEnd = original.size();
const std::string origHost = original.substr(hostStart, hostEnd - hostStart);
static const char* kSuffixes[] = {
".remarkable.com",
".remarkable.engineering",
nullptr
};
bool shouldPatch = false;
for (int i = 0; kSuffixes[i]; ++i)
{
const std::string suffix(kSuffixes[i]);
if (origHost.size() >= suffix.size() &&
origHost.compare(origHost.size() - suffix.size(),
suffix.size(), suffix) == 0)
{
shouldPatch = true;
break;
}
}
if (!shouldPatch) return {};
std::string patched = original;
std::string proxyHost = [gConfiguredHostObjC UTF8String];
patched.replace(hostStart, hostEnd - hostStart, proxyHost);
size_t colonPos = patched.find(':', hostStart + proxyHost.size());
if (colonPos != std::string::npos)
{
size_t numEnd = patched.find_first_not_of("0123456789", colonPos + 1);
if (numEnd == std::string::npos) numEnd = patched.size();
patched.replace(colonPos + 1, numEnd - colonPos - 1,
std::to_string([gConfiguredPortObjC intValue]));
}
return patched;
}
extern "C" int hooked_MQTTAsync_createWithOptions(
MQTTAsync *handle,
const char *serverURI,
const char *clientId,
int persistence_type,
void *persistence_context,
MQTTAsync_createOptions *options)
{
if (!original_MQTTAsync_createWithOptions) {
return -1;
}
std::string patchedUri = PatchMqttUri(serverURI);
if (!patchedUri.empty()) {
NSLogger(@"[RMHook] Patching MQTT URI from %s to %s", serverURI, patchedUri.c_str());
return original_MQTTAsync_createWithOptions(handle, patchedUri.c_str(), clientId, persistence_type, persistence_context, options);
}
return original_MQTTAsync_createWithOptions(handle, serverURI, clientId, persistence_type, persistence_context, options);
}
#endif // BUILD_MODE_RMFAKECLOUD
#ifdef BUILD_MODE_QMLREBUILD
extern "C" int hooked_qRegisterResourceData( extern "C" int hooked_qRegisterResourceData(
int version, int version,
const unsigned char *tree, const unsigned char *tree,
@@ -357,34 +388,82 @@ extern "C" int hooked_qRegisterResourceData(
} }
pthread_mutex_lock(&gResourceMutex); pthread_mutex_lock(&gResourceMutex);
struct ResourceRoot resource = { struct ResourceRoot resource = {
.data = (uint8_t *)data, .data = (uint8_t *)data,
.name = (uint8_t *)name, .name = (uint8_t *)name,
.tree = (uint8_t *)tree, .tree = (uint8_t *)tree,
.treeSize = 0, .treeSize = 0,
.dataSize = 0, .dataSize = 0,
.originalDataSize = 0, .originalDataSize = 0,
.nameSize = 0, .nameSize = 0,
.entriesAffected = 0, .entriesAffected = 0,
}; };
NSLogger(@"[RMHook] Registering Qt resource version %d tree:%p name:%p data:%p",
version, tree, name, data);
statArchive(&resource, 0); statArchive(&resource, 0);
processNode(&resource, 0, "");
resource.tree = (uint8_t *)malloc(resource.treeSize); resource.tree = (uint8_t *)malloc(resource.treeSize);
if (resource.tree) { if (!resource.tree) {
memcpy(resource.tree, tree, resource.treeSize); NSLogger(@"[RMHook] Failed to allocate tree buffer");
pthread_mutex_unlock(&gResourceMutex);
return original_qRegisterResourceData(version, tree, name, data);
}
memcpy(resource.tree, tree, resource.treeSize);
processNode(&resource, 0, "");
NSLogger(@"[RMHook] Processing done! Entries affected: %d, dataSize: %zu, originalDataSize: %zu",
resource.entriesAffected, resource.dataSize, resource.originalDataSize);
const unsigned char *finalTree = tree;
const unsigned char *finalData = data;
uint8_t *newDataBuffer = NULL;
if (resource.entriesAffected > 0) {
NSLogger(@"[RMHook] Rebuilding data tables... (entries: %d)", resource.entriesAffected);
newDataBuffer = (uint8_t *)malloc(resource.dataSize);
if (!newDataBuffer) {
NSLogger(@"[RMHook] Failed to allocate new data buffer (%zu bytes)", resource.dataSize);
free(resource.tree);
clearReplacementEntries();
pthread_mutex_unlock(&gResourceMutex);
return original_qRegisterResourceData(version, tree, name, data);
}
memcpy(newDataBuffer, data, resource.originalDataSize);
struct ReplacementEntry *entry = getReplacementEntries();
while (entry) {
writeUint32(newDataBuffer, (int)entry->copyToOffset, (uint32_t)entry->size);
memcpy(newDataBuffer + entry->copyToOffset + 4, entry->data, entry->size);
NSLogger(@"[RMHook] Copied replacement for node %d at offset %zu (%zu bytes)",
entry->node, entry->copyToOffset, entry->size);
entry = entry->next;
}
finalTree = resource.tree;
finalData = newDataBuffer;
NSLogger(@"[RMHook] Data buffer rebuilt: original %zu bytes -> new %zu bytes",
resource.originalDataSize, resource.dataSize);
} }
NSLogger(@"[reMarkable] Registering Qt resource version %d tree:%p (size:%zu) name:%p (size:%zu) data:%p (size:%zu)", int status = original_qRegisterResourceData(version, finalTree, name, finalData);
version, tree, resource.treeSize, name, resource.nameSize, data, resource.dataSize);
int status = original_qRegisterResourceData(version, tree, name, data); clearReplacementEntries();
pthread_mutex_unlock(&gResourceMutex); if (resource.tree && resource.entriesAffected == 0) {
if (resource.tree) {
free(resource.tree); free(resource.tree);
} }
pthread_mutex_unlock(&gResourceMutex);
return status; return status;
} }
#endif // BUILD_MODE_QMLREBUILD
@end @end

4
src/RMHook/SSLConfig.h Normal file
View File

@@ -0,0 +1,4 @@
#include <QtNetwork/QNetworkRequest>
void SSLConfigLoad(void);
void SSLConfigApplyToRequest(QNetworkRequest &request);

92
src/RMHook/SSLConfig.m Normal file
View File

@@ -0,0 +1,92 @@
#import "SSLConfig.h"
#import "Config.h"
#import "Logger.h"
#include <QtNetwork/QSslConfiguration>
#include <QtNetwork/QSslCertificate>
#include <QtNetwork/QSslKey>
#include <QtNetwork/QSslSocket>
#include <QtCore/QFile>
#include <QtCore/QIODevice>
static QSslCertificate gClientCert;
static QSslKey gClientKey;
static QSslCertificate gCACert;
static bool gSSLInitialized = false;
void SSLConfigLoad(void) {
if (gSSLInitialized) return;
if (!gConfiguredClientCertPath.isEmpty() && !gConfiguredClientKeyPath.isEmpty()) {
QString certPath = gConfiguredClientCertPath;
QString keyPath = gConfiguredClientKeyPath;
QFile certFile(QString::fromUtf8(certPath.toUtf8()));
if (certFile.open(QIODevice::ReadOnly)) {
gClientCert = QSslCertificate(certFile.readAll(), QSsl::Pem);
certFile.close();
if (gClientCert.isNull()) {
NSLogger(@"[RMHook] Failed to parse client certificate from %s", certPath.toUtf8().constData());
} else {
NSLogger(@"[RMHook] Loaded client certificate from %s", certPath.toUtf8().constData());
}
} else {
NSLogger(@"[RMHook] Failed to open client certificate file %s", certPath.toUtf8().constData());
}
QFile keyFile(QString::fromUtf8(keyPath.toUtf8()));
if (keyFile.open(QIODevice::ReadOnly)) {
gClientKey = QSslKey(keyFile.readAll(), gConfiguredKeyAlgorithm, QSsl::Pem, QSsl::PrivateKey);
keyFile.close();
if (gClientKey.isNull()) {
NSLogger(@"[RMHook] Failed to parse client key from %s", keyPath.toUtf8().constData());
} else {
NSLogger(@"[RMHook] Loaded client key from %s", keyPath.toUtf8().constData());
}
} else {
NSLogger(@"[RMHook] Failed to open client key file %s", keyPath.toUtf8().constData());
}
}
if (!gConfiguredCACertPath.isEmpty()) {
QString caPath = gConfiguredCACertPath;
QFile caFile(QString::fromUtf8(caPath.toUtf8()));
if (caFile.open(QIODevice::ReadOnly)) {
gCACert = QSslCertificate(caFile.readAll(), QSsl::Pem);
caFile.close();
if (gCACert.isNull()) {
NSLogger(@"[RMHook] Failed to parse CA certificate from %s", caPath.toUtf8().constData());
} else {
NSLogger(@"[RMHook] Loaded CA certificate from %s", caPath.toUtf8().constData());
}
} else {
NSLogger(@"[RMHook] Failed to open CA certificate file %s", caPath.toUtf8().constData());
}
}
gSSLInitialized = true;
}
void SSLConfigApplyToRequest(QNetworkRequest &request) {
if (!gSSLInitialized) return;
QSslConfiguration sslConfig = QSslConfiguration::defaultConfiguration();
if (!gClientCert.isNull() && !gClientKey.isNull()) {
QList<QSslCertificate> localCerts = sslConfig.localCertificateChain();
localCerts.append(gClientCert);
sslConfig.setLocalCertificateChain(localCerts);
sslConfig.setPrivateKey(gClientKey);
}
if (!gCACert.isNull()) {
QList<QSslCertificate> caCerts = sslConfig.caCertificates();
caCerts.append(gCACert);
sslConfig.setCaCertificates(caCerts);
}
if (gDisableSSLVerification) {
sslConfig.setPeerVerifyMode(QSslSocket::VerifyNone);
}
request.setSslConfiguration(sslConfig);
}

View File

@@ -1,39 +0,0 @@
#include <mach/mach_init.h> // mach_task_self()
#include <mach/mach_vm.h> // mach_vm_*
#include <string.h> // memcpy()
#ifndef COMPACT
#include <mach/mach_error.h> // mach_error_string()
#include <printf.h> // 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;
}

View File

@@ -1,66 +0,0 @@
#include <objc/runtime.h> // objc_*, ...
#ifndef COMPACT
#include <mach/mach_error.h> // mach_error_string()
#include <printf.h> // 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;
}

View File

@@ -1,23 +0,0 @@
#ifndef COMPACT
#include <printf.h> // 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);

View File

@@ -1,121 +0,0 @@
#include <mach-o/dyld.h> // _dyld_*
#include <mach-o/loader.h> // mach_header_64, load_command...
#include <mach-o/nlist.h> // nlist_64
#include <string.h> // strcmp()
#ifndef COMPACT
#include <printf.h> // 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 <mach-o/loader.h>, 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;
}

View File

@@ -1,60 +0,0 @@
#include <mach-o/dyld.h> // _dyld_*
#include <mach-o/loader.h> // mach_header_64, load_command...
#include <mach-o/nlist.h> // nlist_64
#include <string.h> // strcmp()
#ifndef COMPACT
#include <printf.h> // 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;
}

View File

@@ -1,183 +0,0 @@
#include <mach/mach_init.h> // mach_task_self()
#include <mach/mach_vm.h> // mach_vm_*
#include <stdlib.h> // atexit()
#include <string.h> // memcpy()
#ifndef COMPACT
#include <mach/mach_error.h> // mach_error_string()
#include <printf.h> // 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;
}

View File

@@ -30,4 +30,20 @@
logPrefix:(NSString *)logPrefix logPrefix:(NSString *)logPrefix
delayInSeconds:(NSTimeInterval)delayInSeconds; 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 @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 @end

63
src/utils/MessageBroker.h Normal file
View File

@@ -0,0 +1,63 @@
// Credits: asivery/rm-xovi-extensions
// (https://github.com/asivery/rm-xovi-extensions/blob/master/xovi-message-broker/src/XoviMessageBroker.h)
// Simplified for RMHook dylib <-> QML communication
#pragma once
#include <QObject>
#include <QStringList>
#include <QString>
#include <QDebug>
#include <QtQml/QQmlEngine>
// Forward declaration
class MessageBroker;
// Native callback type for C++ listeners
typedef void (*NativeSignalCallback)(const char *signal, const char *value);
namespace messagebroker {
void addBroadcastListener(MessageBroker *ref);
void removeBroadcastListener(MessageBroker *ref);
void broadcast(const char *signal, const char *value);
void registerQmlType();
// Register a native C++ callback to receive all signals
void setNativeCallback(NativeSignalCallback callback);
}
class MessageBroker : public QObject
{
Q_OBJECT
Q_PROPERTY(QStringList listeningFor READ getListeningFor WRITE setListeningFor)
public:
explicit MessageBroker(QObject *parent = nullptr) : QObject(parent) {
messagebroker::addBroadcastListener(this);
}
~MessageBroker() {
messagebroker::removeBroadcastListener(this);
}
// Send a signal from QML to all listeners (including C++ side)
Q_INVOKABLE void sendSignal(const QString &signal, const QString &message) {
QByteArray signalUtf8 = signal.toUtf8();
QByteArray messageUtf8 = message.toUtf8();
messagebroker::broadcast(signalUtf8.constData(), messageUtf8.constData());
}
void setListeningFor(const QStringList &l) {
_listeningFor = l;
}
const QStringList& getListeningFor() const {
return _listeningFor;
}
signals:
void signalReceived(const QString &signal, const QString &message);
private:
QStringList _listeningFor;
};

View File

@@ -0,0 +1,58 @@
// Credits: asivery/rm-xovi-extensions
// (https://github.com/asivery/rm-xovi-extensions/blob/master/xovi-message-broker/src/XoviMessageBroker.h)
#import <Foundation/Foundation.h>
#include "MessageBroker.h"
#include "Logger.h"
#include <vector>
#include <cstring>
#include <algorithm>
static std::vector<MessageBroker *> brokers;
static NativeSignalCallback nativeCallback = nullptr;
void messagebroker::setNativeCallback(NativeSignalCallback callback) {
nativeCallback = callback;
NSLogger(@"[MessageBroker] Native callback registered");
}
void messagebroker::addBroadcastListener(MessageBroker *ref) {
// Cannot have more than one.
if(std::find(brokers.begin(), brokers.end(), ref) == brokers.end()) {
brokers.push_back(ref);
NSLogger(@"[MessageBroker] Added broadcast listener, total: %zu", brokers.size());
}
}
void messagebroker::removeBroadcastListener(MessageBroker *ref) {
std::vector<MessageBroker *>::iterator iter;
if((iter = std::find(brokers.begin(), brokers.end(), ref)) != brokers.end()) {
brokers.erase(iter);
NSLogger(@"[MessageBroker] Removed broadcast listener, remaining: %zu", brokers.size());
}
}
void messagebroker::broadcast(const char *signal, const char *value) {
QString qSignal(signal), qValue(value);
NSLogger(@"[MessageBroker] Broadcasting signal '%s' with value '%s'", signal, value);
// Call native C++ callback if registered
if (nativeCallback) {
nativeCallback(signal, value);
}
// Notify QML listeners
for(auto &ref : brokers) {
if(ref->getListeningFor().contains(qSignal)) {
emit ref->signalReceived(qSignal, qValue);
}
}
}
void messagebroker::registerQmlType() {
qmlRegisterType<MessageBroker>("net.noham.MessageBroker", 1, 0, "MessageBroker");
NSLogger(@"[MessageBroker] Registered QML type net.noham.MessageBroker");
}
// Include MOC output for MessageBroker class (generated by Qt's Meta-Object Compiler)
#include "moc_MessageBroker.cpp"

View File

@@ -3,6 +3,7 @@
#include <stdint.h> #include <stdint.h>
#include <stdlib.h> #include <stdlib.h>
#include <stdbool.h>
#ifdef __cplusplus #ifdef __cplusplus
extern "C" { extern "C" {
@@ -19,6 +20,16 @@ struct ResourceRoot {
int entriesAffected; int entriesAffected;
}; };
// Replacement entry for storing new data to be appended
struct ReplacementEntry {
int node;
uint8_t *data;
size_t size;
size_t copyToOffset;
bool freeAfterwards;
struct ReplacementEntry *next;
};
#define TREE_ENTRY_SIZE 22 #define TREE_ENTRY_SIZE 22
#define DIRECTORY 0x02 #define DIRECTORY 0x02
@@ -35,8 +46,14 @@ void statArchive(struct ResourceRoot *root, int node);
void processNode(struct ResourceRoot *root, int node, const char *rootName); 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); void ReMarkableDumpResourceFile(struct ResourceRoot *root, int node, const char *rootName, const char *fileName, uint16_t flags);
// Replacement utilities
void addReplacementEntry(struct ReplacementEntry *entry);
struct ReplacementEntry *getReplacementEntries(void);
void clearReplacementEntries(void);
void replaceNode(struct ResourceRoot *root, int node, const char *fullPath, int treeOffset);
#ifdef __cplusplus #ifdef __cplusplus
} }
#endif #endif
#endif /* ResourceUtils_h */ #endif

View File

@@ -36,6 +36,7 @@ static NSString *ReMarkableDumpRootDirectory(void) {
return dumpDirectory; return dumpDirectory;
} }
#ifdef BUILD_MODE_QMLREBUILD
uint32_t readUInt32(uint8_t *addr, int offset) { uint32_t readUInt32(uint8_t *addr, int offset) {
return (uint32_t)(addr[offset + 0] << 24) | return (uint32_t)(addr[offset + 0] << 24) |
(uint32_t)(addr[offset + 1] << 16) | (uint32_t)(addr[offset + 1] << 16) |
@@ -343,6 +344,147 @@ void ReMarkableDumpResourceFile(struct ResourceRoot *root, int node, const char
} }
} }
// List of files to process with replaceNode
static const char *kFilesToReplace[] = {
"/qml/client/dialogs/ExportDialog.qml",
"/qml/client/settings/GeneralSettings.qml",
NULL // Sentinel to mark end of list
};
static bool shouldReplaceFile(const char *fullPath) {
if (!fullPath) return false;
for (int i = 0; kFilesToReplace[i] != NULL; i++) {
if (strcmp(fullPath, kFilesToReplace[i]) == 0) {
return true;
}
}
return false;
}
// Get the path to replacement files directory
static NSString *ReMarkableReplacementDirectory(void) {
static NSString *replacementDirectory = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
NSString *preferencesDir = ReMarkablePreferencesDirectory();
NSString *candidate = [preferencesDir stringByAppendingPathComponent:@"replacements"];
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 replacements directory %@: %@", candidate, error);
}
}
replacementDirectory = [candidate copy];
});
return replacementDirectory;
}
// Global linked list of replacement entries
static struct ReplacementEntry *g_replacementEntries = NULL;
void addReplacementEntry(struct ReplacementEntry *entry) {
entry->next = g_replacementEntries;
g_replacementEntries = entry;
}
struct ReplacementEntry *getReplacementEntries(void) {
return g_replacementEntries;
}
void clearReplacementEntries(void) {
struct ReplacementEntry *current = g_replacementEntries;
while (current) {
struct ReplacementEntry *next = current->next;
if (current->freeAfterwards && current->data) {
free(current->data);
}
free(current);
current = next;
}
g_replacementEntries = NULL;
}
void replaceNode(struct ResourceRoot *root, int node, const char *fullPath, int treeOffset) {
NSLogger(@"[reMarkable] replaceNode called for: %s", fullPath);
if (!root || !root->tree || !fullPath) {
NSLogger(@"[reMarkable] replaceNode: invalid parameters");
return;
}
// Build path to replacement file on disk
NSString *replacementDir = ReMarkableReplacementDirectory();
if (![replacementDir length]) {
NSLogger(@"[reMarkable] replaceNode: no replacement directory");
return;
}
NSString *relativePath = [NSString stringWithUTF8String:fullPath];
if ([relativePath hasPrefix:@"/"]) {
relativePath = [relativePath substringFromIndex:1];
}
NSString *replacementFilePath = [replacementDir stringByAppendingPathComponent:relativePath];
NSFileManager *fileManager = [NSFileManager defaultManager];
if (![fileManager fileExistsAtPath:replacementFilePath]) {
NSLogger(@"[reMarkable] replaceNode: replacement file not found at %@", replacementFilePath);
return;
}
// Read the replacement file
NSError *readError = nil;
NSData *replacementData = [NSData dataWithContentsOfFile:replacementFilePath options:0 error:&readError];
if (!replacementData || readError) {
NSLogger(@"[reMarkable] replaceNode: failed to read replacement file %@: %@", replacementFilePath, readError);
return;
}
size_t dataSize = [replacementData length];
NSLogger(@"[reMarkable] replaceNode: loaded replacement file %@ (%zu bytes)", replacementFilePath, dataSize);
// Allocate and copy the replacement data
uint8_t *newData = (uint8_t *)malloc(dataSize);
if (!newData) {
NSLogger(@"[reMarkable] replaceNode: failed to allocate %zu bytes", dataSize);
return;
}
memcpy(newData, [replacementData bytes], dataSize);
// Create a replacement entry
struct ReplacementEntry *entry = (struct ReplacementEntry *)malloc(sizeof(struct ReplacementEntry));
if (!entry) {
NSLogger(@"[reMarkable] replaceNode: failed to allocate replacement entry");
free(newData);
return;
}
entry->node = node;
entry->data = newData;
entry->size = dataSize;
entry->freeAfterwards = true;
entry->copyToOffset = root->dataSize; // Will be appended at the end of data
entry->next = NULL;
// Update the tree entry:
writeUint16(root->tree, treeOffset - 2, 0); // Set flag to raw (uncompressed)
writeUint32(root->tree, treeOffset + 4, (uint32_t)entry->copyToOffset); // Update data offset
NSLogger(@"[reMarkable] replaceNode: updated tree - flags at offset %d, dataOffset at offset %d -> %zu",
treeOffset - 2, treeOffset + 4, entry->copyToOffset);
// Update dataSize to account for the new data (size prefix + data)
root->dataSize += entry->size + 4;
root->entriesAffected++;
// Add to replacement entries list
addReplacementEntry(entry);
NSLogger(@"[reMarkable] replaceNode: marked for replacement - %s (new offset: %zu, size: %zu)",
fullPath, entry->copyToOffset, entry->size);
}
void processNode(struct ResourceRoot *root, int node, const char *rootName) { void processNode(struct ResourceRoot *root, int node, const char *rootName) {
int offset = findOffset(node) + 4; int offset = findOffset(node) + 4;
uint16_t flags = readUInt16(root->tree, offset); uint16_t flags = readUInt16(root->tree, offset);
@@ -374,8 +516,42 @@ void processNode(struct ResourceRoot *root, int node, const char *rootName) {
free(tempRoot); free(tempRoot);
} else { } else {
NSLogger(@"[reMarkable] Processing node %d: %s%s", (int)node, rootName ? rootName : "", nameBuffer); uint16_t fileFlag = readUInt16(root->tree, offset - 2);
uint16_t fileFlags = readUInt16(root->tree, offset - 2); const char *type;
ReMarkableDumpResourceFile(root, node, rootName ? rootName : "", nameBuffer, fileFlags); if (fileFlag == 1) {
type = "zlib";
} else if (fileFlag == 4) {
type = "zstd";
} else if (fileFlag == 0) {
type = "raw";
} else {
type = "unknown";
}
// Build full path: rootName + nameBuffer
const size_t rootLen = rootName ? strlen(rootName) : 0;
const size_t nameLen = strlen(nameBuffer);
char *fullPath = (char *)malloc(rootLen + nameLen + 1);
if (fullPath) {
if (rootLen > 0) {
memcpy(fullPath, rootName, rootLen);
}
memcpy(fullPath + rootLen, nameBuffer, nameLen);
fullPath[rootLen + nameLen] = '\0';
NSLogger(@"[reMarkable] Processing node %d: %s (type: %s)", (int)node, fullPath, type);
// Check if this file should be replaced
if (shouldReplaceFile(fullPath)) {
replaceNode(root, node, fullPath, offset);
}
free(fullPath);
} else {
NSLogger(@"[reMarkable] Processing node %d: %s%s (type: %s)", (int)node, rootName ? rootName : "", nameBuffer, type);
}
// ReMarkableDumpResourceFile(root, node, rootName ? rootName : "", nameBuffer, fileFlag);
} }
} }
#endif // BUILD_MODE_QMLREBUILD