Add LICENSE, expand README; tidy proxy code

This commit is contained in:
√(noham)²
2026-05-07 17:49:31 +02:00
parent fb18195b71
commit d724a017e3
4 changed files with 355 additions and 276 deletions

21
LICENSE Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 Rivoirard Noham
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

119
README.md
View File

@@ -1,9 +1,114 @@
Set-ExecutionPolicy # RMHook-Win
Run from an elevated PowerShell (script will attempt to relaunch elevated if not already) or right-click -> Run with PowerShell.
Example: .\scripts\install-hook.ps1 -Action install
To use a specific DLL: .\scripts\install-hook.ps1 -Action install -SourcePath "C:\path\to\your.dll"
To restore original: .\scripts\install-hook.ps1 -Action restore
A Windows port of [RMHook](https://github.com/NohamR/RMHook) for the reMarkable Desktop application. This repo builds a proxy DLL that hooks Qt network APIs and redirects reMarkable cloud traffic to a self-hosted [rmfakecloud](https://github.com/ddvk/rmfakecloud) server.
install-hook.bat -Action install ## Overview
install-hook.bat -Action restore
RMHook-Win intercepts the reMarkable Desktop app's Qt networking layer and patches outgoing requests to the configured host and port. It is designed for the Windows reMarkable Desktop client and uses a DLL proxy for `paho-mqtt3as.dll`.
## Features
- Redirect reMarkable cloud HTTP(s) requests to a self-hosted rmfakecloud server
- Patch Qt WebSocket connections used by the reMarkable app
## Compatibility
**Tested and working on:**
<!-- - reMarkable Desktop v3.27.0 (released 2026-06-05)
<p align="center">
<img src="docs/latest.png" width="40%" />
<img src="docs/rm.png" width="50%" />
</p> -->
## Installation and usage
### Important legal note
⚠️ **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-Win/releases/latest) section.
### Step 1: Build or obtain the proxy DLL
Build the `paho-mqtt3as-proxy` project with Visual Studio using `paho-mqtt3as-proxy.slnx`, or use an existing `paho-mqtt3as.dll` built from this repo.
### Step 2: Install the hook
Use the installer script from the `scripts` folder.
Note: Run from an elevated PowerShell session. The installer script will request administrator privileges if needed.
From PowerShell:
```powershell
.\scripts\install-hook.ps1 -Action install
```
Or with the batch wrapper:
```cmd
.\scripts\install-hook.bat -Action install
```
If you want to install a custom DLL build:
```powershell
.\scripts\install-hook.ps1 -Action install -SourcePath "C:\path\to\paho-mqtt3as.dll"
```
The script expects the Windows reMarkable install folder at:
```text
C:\Program Files\reMarkable
```
### Step 3: Restore the original DLL
To remove the proxy and restore the original `paho-mqtt3as.dll`:
```powershell
.\scripts\install-hook.ps1 -Action restore
```
## Configuration
Config path:
```text
%LOCALAPPDATA%\RMHook\config.json
```
Example config:
```json
{
"host": "your-server.example.com",
"port": 443
}
```
If the config file does not exist, it will be created automatically with default values on first launch.
## Troubleshooting
### Hook install fails
- Confirm the reMarkable install path is `C:\Program Files\reMarkable`
- Run PowerShell as administrator
- Verify the source DLL exists and is a valid proxy build
### App crashes or misbehaves
- Restore the original DLL with `-Action restore`
- Check the config file for valid JSON
- Make sure the `host` and `port` values point to a reachable rmfakecloud server
## Credits
- MinHook: [TsudaKageyu/minhook](https://github.com/TsudaKageyu/minhook) - API hooking framework used by the project
rmfakecloud: [ddvk/rmfakecloud](https://github.com/ddvk/rmfakecloud) - Self-hosted reMarkable cloud
- xovi-rmfakecloud: [asivery/xovi-rmfakecloud](https://github.com/asivery/xovi-rmfakecloud) - Original hooking information
- 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
- [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)
## License
This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details.
## Disclaimer
This project is not affiliated with, endorsed by, or sponsored by reMarkable AS. Use at your own risk. This tool modifies the reMarkable Desktop application and may violate the application's terms of service.
## Contributing
Contributions are welcome! Please feel free to submit issues or pull requests.

View File

@@ -17,321 +17,277 @@
#include <QtCore/QJsonObject> #include <QtCore/QJsonObject>
#include <QtCore/QJsonValue> #include <QtCore/QJsonValue>
static std::string GetLogPath() static std::string GetLogPath()
{ {
char localAppData[MAX_PATH]; char localAppData[MAX_PATH];
if (SUCCEEDED(SHGetFolderPathA( if (SUCCEEDED(SHGetFolderPathA(
NULL, NULL,
CSIDL_LOCAL_APPDATA, CSIDL_LOCAL_APPDATA,
NULL, NULL,
0, 0,
localAppData))) localAppData)))
{ {
std::filesystem::path dir = std::filesystem::path dir =
std::filesystem::path(localAppData) / "RMHook"; std::filesystem::path(localAppData) / "RMHook";
std::filesystem::create_directories(dir); std::filesystem::create_directories(dir);
return (dir / "rmhook.log").string(); return (dir / "rmhook.log").string();
} }
return "rmhook.log"; return "rmhook.log";
} }
// ------------------------------------------------------------
// Logging
// ------------------------------------------------------------
static void Log(const std::string& msg) static void Log(const std::string& msg)
{ {
std::ofstream file(GetLogPath(), std::ios::app); std::ofstream file(GetLogPath(), std::ios::app);
if (file.is_open()) if (file.is_open())
{ {
file << msg << std::endl; file << msg << std::endl;
} }
} }
// ------------------------------------------------------------
// Configuration
// ------------------------------------------------------------
static std::string gConfiguredHost = "example.com"; static std::string gConfiguredHost = "example.com";
static int gConfiguredPort = 443; static int gConfiguredPort = 443;
static void LoadConfig() static void LoadConfig()
{ {
char localAppData[MAX_PATH]; char localAppData[MAX_PATH];
if (SUCCEEDED(SHGetFolderPathA(NULL, CSIDL_LOCAL_APPDATA, NULL, 0, localAppData))) if (SUCCEEDED(SHGetFolderPathA(NULL, CSIDL_LOCAL_APPDATA, NULL, 0, localAppData)))
{ {
std::filesystem::path configPath = std::filesystem::path(localAppData) / "RMHook" / "config.json"; std::filesystem::path configPath = std::filesystem::path(localAppData) / "RMHook" / "config.json";
QString qConfigPath = QString::fromStdString(configPath.string()); QString qConfigPath = QString::fromStdString(configPath.string());
QFile file(qConfigPath);
if (file.exists() && file.open(QIODevice::ReadOnly))
{
QByteArray data = file.readAll();
file.close();
QJsonDocument doc = QJsonDocument::fromJson(data); QFile file(qConfigPath);
if (!doc.isNull() && doc.isObject()) if (file.exists() && file.open(QIODevice::ReadOnly))
{ {
QJsonObject obj = doc.object(); QByteArray data = file.readAll();
if (obj.contains("host")) gConfiguredHost = obj["host"].toString().toStdString(); file.close();
if (obj.contains("port")) gConfiguredPort = obj["port"].toInt();
return;
}
}
// If we reach here, no config exists or it failed to load. QJsonDocument doc = QJsonDocument::fromJson(data);
MessageBoxA(NULL, "First launch detected.\nUsing default config (example.com:443).\nYou can edit configuration in %LOCALAPPDATA%\\RMHook\\config.json", "RMHook Configuration", MB_OK | MB_ICONINFORMATION); if (!doc.isNull() && doc.isObject())
{
QJsonObject obj = doc.object();
if (obj.contains("host"))
{
gConfiguredHost = obj["host"].toString().toStdString();
}
if (obj.contains("port"))
{
gConfiguredPort = obj["port"].toInt();
}
// Save defaults as JSON return;
std::filesystem::create_directories(configPath.parent_path()); }
if (file.open(QIODevice::WriteOnly)) }
{
QJsonObject obj;
obj["host"] = QString::fromStdString(gConfiguredHost);
obj["port"] = gConfiguredPort;
QJsonDocument doc(obj); MessageBoxA(NULL, "First launch detected.\nUsing default config (example.com:443).\nYou can edit configuration in %LOCALAPPDATA%\\RMHook\\config.json", "RMHook Configuration", MB_OK | MB_ICONINFORMATION);
file.write(doc.toJson());
file.close(); std::filesystem::create_directories(configPath.parent_path());
} if (file.open(QIODevice::WriteOnly))
} {
QJsonObject obj;
obj["host"] = QString::fromStdString(gConfiguredHost);
obj["port"] = gConfiguredPort;
QJsonDocument doc(obj);
file.write(doc.toJson());
file.close();
}
}
} }
static inline bool shouldPatchURL(const QString& host) { static inline bool shouldPatchURL(const QString& host)
if (host.isEmpty()) { {
return false; if (host.isEmpty())
} {
return false;
}
return QString(R"""( return QString(R"""(
hwr-production-dot-remarkable-production.appspot.com hwr-production-dot-remarkable-production.appspot.com
service-manager-production-dot-remarkable-production.appspot.com service-manager-production-dot-remarkable-production.appspot.com
local.appspot.com local.appspot.com
my.remarkable.com my.remarkable.com
ping.remarkable.com ping.remarkable.com
internal.cloud.remarkable.com internal.cloud.remarkable.com
eu.tectonic.remarkable.com eu.tectonic.remarkable.com
backtrace-proxy.cloud.remarkable.engineering backtrace-proxy.cloud.remarkable.engineering
dev.ping.remarkable.com dev.ping.remarkable.com
dev.tectonic.remarkable.com dev.tectonic.remarkable.com
dev.internal.cloud.remarkable.com dev.internal.cloud.remarkable.com
eu.internal.tctn.cloud.remarkable.com eu.internal.tctn.cloud.remarkable.com
webapp-prod.cloud.remarkable.engineering webapp-prod.cloud.remarkable.engineering
)""") )""")
.contains(host, Qt::CaseInsensitive); .contains(host, Qt::CaseInsensitive);
} }
// ------------------------------------------------------------
// Original typedefs
// ------------------------------------------------------------
typedef QNetworkReply* (__fastcall* QNAM_CreateRequest_t)( typedef QNetworkReply* (__fastcall* QNAM_CreateRequest_t)(
QNetworkAccessManager* self, QNetworkAccessManager* self,
QNetworkAccessManager::Operation op, QNetworkAccessManager::Operation op,
const QNetworkRequest& req, const QNetworkRequest& req,
QIODevice* outgoingData QIODevice* outgoingData
); );
typedef void (__fastcall* QWebSocket_Open_t)( typedef void (__fastcall* QWebSocket_Open_t)(
QWebSocket* self, QWebSocket* self,
const QNetworkRequest& req const QNetworkRequest& req
); );
// ------------------------------------------------------------
// Originals
// ------------------------------------------------------------
static QNAM_CreateRequest_t originalCreateRequest = nullptr; static QNAM_CreateRequest_t originalCreateRequest = nullptr;
static QWebSocket_Open_t originalWebSocketOpen = nullptr; static QWebSocket_Open_t originalWebSocketOpen = nullptr;
// ------------------------------------------------------------
// Hooked createRequest
// ------------------------------------------------------------
QNetworkReply* __fastcall hookedCreateRequest( QNetworkReply* __fastcall hookedCreateRequest(
QNetworkAccessManager* self, QNetworkAccessManager* self,
QNetworkAccessManager::Operation op, QNetworkAccessManager::Operation op,
const QNetworkRequest& req, const QNetworkRequest& req,
QIODevice* outgoingData QIODevice* outgoingData
) )
{ {
const QString host = req.url().host(); const QString host = req.url().host();
if (shouldPatchURL(host)) { if (shouldPatchURL(host)) {
QNetworkRequest newReq(req); QNetworkRequest newReq(req);
QUrl newUrl = req.url(); QUrl newUrl = req.url();
newUrl.setHost(QString::fromStdString(gConfiguredHost)); newUrl.setHost(QString::fromStdString(gConfiguredHost));
newUrl.setPort(gConfiguredPort); newUrl.setPort(gConfiguredPort);
newReq.setUrl(newUrl); newReq.setUrl(newUrl);
Log("[HTTP PATCHED] " + host.toStdString() + " -> " + newUrl.toString().toStdString()); Log("[HTTP PATCHED] " + host.toStdString() + " -> " + newUrl.toString().toStdString());
if (originalCreateRequest) { if (originalCreateRequest) {
return originalCreateRequest(self, op, newReq, outgoingData); return originalCreateRequest(self, op, newReq, outgoingData);
} }
return nullptr; return nullptr;
} }
if (originalCreateRequest) { if (originalCreateRequest) {
return originalCreateRequest(self, op, req, outgoingData); return originalCreateRequest(self, op, req, outgoingData);
} }
return nullptr; return nullptr;
} }
// ------------------------------------------------------------
// Hooked websocket open
// ------------------------------------------------------------
void __fastcall hookedWebSocketOpen( void __fastcall hookedWebSocketOpen(
QWebSocket* self, QWebSocket* self,
const QNetworkRequest& req const QNetworkRequest& req
) )
{ {
if (!originalWebSocketOpen) { if (!originalWebSocketOpen) {
return; return;
} }
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();
newUrl.setHost(QString::fromStdString(gConfiguredHost)); newUrl.setHost(QString::fromStdString(gConfiguredHost));
newUrl.setPort(gConfiguredPort); newUrl.setPort(gConfiguredPort);
QNetworkRequest newReq(req); QNetworkRequest newReq(req);
newReq.setUrl(newUrl); newReq.setUrl(newUrl);
Log("[WS PATCHED] " + host.toStdString() + " -> " + newUrl.toString().toStdString()); Log("[WS PATCHED] " + host.toStdString() + " -> " + newUrl.toString().toStdString());
originalWebSocketOpen(self, newReq); originalWebSocketOpen(self, newReq);
return; return;
} }
originalWebSocketOpen(self, req); originalWebSocketOpen(self, req);
} }
// ------------------------------------------------------------
// Helpers
// ------------------------------------------------------------
void* ResolveExport(HMODULE module, const char* symbol) void* ResolveExport(HMODULE module, const char* symbol)
{ {
void* addr = (void*)GetProcAddress(module, symbol); void* addr = (void*)GetProcAddress(module, symbol);
if (!addr) if (!addr)
{ {
Log("[ERROR] Failed to resolve symbol"); Log("[ERROR] Failed to resolve symbol");
Log(symbol); Log(symbol);
} }
return addr; return addr;
} }
// ------------------------------------------------------------
// InstallHooks
// ------------------------------------------------------------
void InstallHooks() void InstallHooks()
{ {
LoadConfig(); LoadConfig();
// std::string logPath = GetLogPath(); Log("[*] Initializing MinHook");
// std::string message = "Proxy Hook Started.\nLog file: " + logPath;
// MessageBoxA(NULL, message.c_str(), "reMarkable Proxy", MB_OK | MB_ICONINFORMATION);
Log("[*] Initializing MinHook"); if (MH_Initialize() != MH_OK)
{
Log("[ERROR] MH_Initialize failed");
return;
}
if (MH_Initialize() != MH_OK) HMODULE qtNetwork = nullptr;
{ HMODULE qtWebSockets = nullptr;
Log("[ERROR] MH_Initialize failed");
return;
}
// -------------------------------------------------------- while (!qtNetwork)
// Wait for Qt DLLs {
// -------------------------------------------------------- qtNetwork = GetModuleHandleA("Qt6Network.dll");
Sleep(100);
}
HMODULE qtNetwork = nullptr; while (!qtWebSockets)
HMODULE qtWebSockets = nullptr; {
qtWebSockets = GetModuleHandleA("Qt6WebSockets.dll");
Sleep(100);
}
while (!qtNetwork) Log("[+] Qt DLLs loaded");
{
qtNetwork = GetModuleHandleA("Qt6Network.dll");
Sleep(100);
}
while (!qtWebSockets) void* createRequestAddr = ResolveExport(
{ qtNetwork,
qtWebSockets = GetModuleHandleA("Qt6WebSockets.dll"); "?createRequest@QNetworkAccessManager@@MEAAPEAVQNetworkReply@@W4Operation@1@AEBVQNetworkRequest@@PEAVQIODevice@@@Z"
Sleep(100); );
}
Log("[+] Qt DLLs loaded"); void* webSocketOpenAddr = ResolveExport(
qtWebSockets,
"?open@QWebSocket@@QEAAXAEBVQNetworkRequest@@@Z"
);
// -------------------------------------------------------- if (!createRequestAddr || !webSocketOpenAddr)
// Resolve symbols {
// -------------------------------------------------------- Log("[ERROR] Failed to resolve one or more symbols");
return;
}
void* createRequestAddr = ResolveExport( Log("[+] Symbols resolved");
qtNetwork,
"?createRequest@QNetworkAccessManager@@MEAAPEAVQNetworkReply@@W4Operation@1@AEBVQNetworkRequest@@PEAVQIODevice@@@Z"
);
void* webSocketOpenAddr = ResolveExport(
qtWebSockets,
"?open@QWebSocket@@QEAAXAEBVQNetworkRequest@@@Z"
);
if (!createRequestAddr || !webSocketOpenAddr) if (MH_CreateHook(
{ createRequestAddr,
Log("[ERROR] Failed to resolve one or more symbols"); &hookedCreateRequest,
return; reinterpret_cast<void**>(&originalCreateRequest)
} ) != MH_OK)
{
Log("[ERROR] Failed to hook createRequest");
}
else
{
Log("[+] Hooked createRequest");
}
Log("[+] Symbols resolved"); if (MH_CreateHook(
webSocketOpenAddr,
&hookedWebSocketOpen,
reinterpret_cast<void**>(&originalWebSocketOpen)
) != MH_OK)
{
Log("[ERROR] Failed to hook QWebSocket::open");
}
else
{
Log("[+] Hooked QWebSocket::open");
}
// -------------------------------------------------------- if (MH_EnableHook(MH_ALL_HOOKS) != MH_OK)
// Hook QNetworkAccessManager::createRequest {
// -------------------------------------------------------- Log("[ERROR] Failed to enable hooks");
}
if (MH_CreateHook( else
createRequestAddr, {
&hookedCreateRequest, Log("[+] Hooks enabled");
reinterpret_cast<void**>(&originalCreateRequest) }
) != MH_OK)
{
Log("[ERROR] Failed to hook createRequest");
}
else
{
Log("[+] Hooked createRequest");
}
// --------------------------------------------------------
// Hook QWebSocket::open
// --------------------------------------------------------
if (MH_CreateHook(
webSocketOpenAddr,
&hookedWebSocketOpen,
reinterpret_cast<void**>(&originalWebSocketOpen)
) != MH_OK)
{
Log("[ERROR] Failed to hook QWebSocket::open");
}
else
{
Log("[+] Hooked QWebSocket::open");
}
// --------------------------------------------------------
// Enable all hooks
// --------------------------------------------------------
if (MH_EnableHook(MH_ALL_HOOKS) != MH_OK)
{
Log("[ERROR] Failed to enable hooks");
}
else
{
Log("[+] Hooks enabled");
}
} }

View File

@@ -1,13 +1,7 @@
#include "common.h" #include "common.h"
DWORD WINAPI DelayedHelloThread(LPVOID lpParam) void LoadOriginalDllFunctions()
{ {
Sleep(10000); // 10 seconds
MessageBox(0, "Hello :)", "Proxy", MB_OK | MB_ICONINFORMATION);
return 0;
}
void LoadOriginalDllFunctions() {
paho_mqtt3as.OrignalMQTTAsync_connect = GetProcAddress(paho_mqtt3as.dll, "MQTTAsync_connect"); paho_mqtt3as.OrignalMQTTAsync_connect = GetProcAddress(paho_mqtt3as.dll, "MQTTAsync_connect");
paho_mqtt3as.OrignalMQTTAsync_create = GetProcAddress(paho_mqtt3as.dll, "MQTTAsync_create"); paho_mqtt3as.OrignalMQTTAsync_create = GetProcAddress(paho_mqtt3as.dll, "MQTTAsync_create");
paho_mqtt3as.OrignalMQTTAsync_createWithOptions = GetProcAddress(paho_mqtt3as.dll, "MQTTAsync_createWithOptions"); paho_mqtt3as.OrignalMQTTAsync_createWithOptions = GetProcAddress(paho_mqtt3as.dll, "MQTTAsync_createWithOptions");
@@ -60,30 +54,33 @@ void LoadOriginalDllFunctions() {
paho_mqtt3as.OrignalThread_unlock_mutex = GetProcAddress(paho_mqtt3as.dll, "Thread_unlock_mutex"); paho_mqtt3as.OrignalThread_unlock_mutex = GetProcAddress(paho_mqtt3as.dll, "Thread_unlock_mutex");
} }
BOOL APIENTRY DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved) { BOOL APIENTRY DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved)
{
switch (ul_reason_for_call) switch (ul_reason_for_call)
{ {
case DLL_PROCESS_ATTACH: case DLL_PROCESS_ATTACH:
{
DisableThreadLibraryCalls(hModule);
paho_mqtt3as.dll = LoadLibrary("paho-mqtt3as_orig.dll");
if (paho_mqtt3as.dll == NULL)
{ {
MessageBox(0, "Cannot load original paho_mqtt3as.dll library", "Proxy", MB_ICONERROR); DisableThreadLibraryCalls(hModule);
ExitProcess(0);
}
LoadOriginalDllFunctions();
InstallHooks();
break; paho_mqtt3as.dll = LoadLibrary("paho-mqtt3as_orig.dll");
} if (paho_mqtt3as.dll == NULL)
case DLL_PROCESS_DETACH: {
{ MessageBox(0, "Cannot load original paho_mqtt3as.dll library", "Proxy", MB_ICONERROR);
FreeLibrary(paho_mqtt3as.dll); ExitProcess(0);
} }
break;
LoadOriginalDllFunctions();
InstallHooks();
break;
}
case DLL_PROCESS_DETACH:
{
FreeLibrary(paho_mqtt3as.dll);
break;
}
} }
return TRUE; return TRUE;
} }