From e7baead2e3e134965e335ef37367128bd177cd5a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=88=9A=28noham=29=C2=B2?= <100566912+NohamR@users.noreply.github.com> Date: Tue, 25 Aug 2026 23:04:23 +0200 Subject: [PATCH] Add Android build and APK injection scripts --- libs/build_deps.sh | 136 +++++++++++++++++++++++++++++++++++++ script/build.sh | 123 +++++++++++++++++++++++++++++++++ script/inject.sh | 166 +++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 425 insertions(+) create mode 100755 libs/build_deps.sh create mode 100755 script/build.sh create mode 100755 script/inject.sh diff --git a/libs/build_deps.sh b/libs/build_deps.sh new file mode 100755 index 0000000..fdb24d5 --- /dev/null +++ b/libs/build_deps.sh @@ -0,0 +1,136 @@ +#!/bin/bash +set -euo pipefail + +# Builds shadowhook as a static library for Android arm64. +# Produces: libs/libshadowhook.a and libs/libshadowhook_nothing.so + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +PROJECT_DIR="$(dirname "$SCRIPT_DIR")" +BUILD_DIR="$PROJECT_DIR/build/deps" +mkdir -p "$BUILD_DIR" + +# --- Auto-detect Android NDK (same logic as build.sh) --- +find_ndk() { + if [ -n "${ANDROID_NDK_HOME:-}" ] && [ -d "$ANDROID_NDK_HOME" ]; then + echo "$ANDROID_NDK_HOME"; return + fi + local brew_ndk + brew_ndk=$(find /opt/homebrew /usr/local -path "*/android-ndk/*/toolchains/llvm/prebuilt" -type d 2>/dev/null | head -1) + if [ -n "$brew_ndk" ]; then + echo "$(dirname "$(dirname "$(dirname "$brew_ndk")")")"; return + fi + local sdk_ndk + sdk_ndk=$(find "$HOME/Library/Android/sdk/ndk" -maxdepth 1 -type d 2>/dev/null | sort -V | tail -1) + if [ -n "$sdk_ndk" ] && [ -d "$sdk_ndk" ]; then + echo "$sdk_ndk"; return + fi + for p in /opt/android-ndk /usr/local/android-ndk "$HOME/android-ndk"; do + if [ -d "$p" ]; then echo "$p"; return; fi + done + echo "" +} + +NDK_HOME="$(find_ndk)" +API_LEVEL="${ANDROID_API_LEVEL:-21}" +CC="" +if [ -n "$NDK_HOME" ]; then + CC=$(find "$NDK_HOME/toolchains/llvm/prebuilt" -type f \ + -name "aarch64-linux-android${API_LEVEL}-clang" -print -quit 2>/dev/null) +fi +if [ -z "$CC" ]; then + echo "ERROR: aarch64-linux-android clang not found." + echo " Set ANDROID_NDK_HOME to your NDK path." + exit 1 +fi + +TOOLCHAIN_DIR="$(dirname "$CC")" +AR="$TOOLCHAIN_DIR/llvm-ar" +CFLAGS="-O2 -ffunction-sections -fdata-sections -fPIC -DANDROID -Wall -Wextra -Wno-unused-parameter -Wno-unused-function" + +OUT_DIR="$SCRIPT_DIR" + +echo "=== Building shadowhook (arm64-v8a) ===" + +SH="$SCRIPT_DIR/shadowhook/shadowhook/src/main/cpp" + +SH_SRCS=" +$SH/sh_elf.c +$SH/sh_enter.c +$SH/sh_hub.c +$SH/sh_island.c +$SH/sh_linker.c +$SH/sh_recorder.c +$SH/sh_safe.c +$SH/sh_switch.c +$SH/sh_task.c +$SH/sh_xdl.c +$SH/shadowhook.c +$SH/arch/arm64/sh_a64.c +$SH/arch/arm64/sh_inst.c +$SH/arch/arm64/sh_glue.S +$SH/common/bytesig.c +$SH/common/sh_errno.c +$SH/common/sh_log.c +$SH/common/sh_ref.c +$SH/common/sh_trampo.c +$SH/common/sh_util.c +$SH/third_party/xdl/xdl.c +$SH/third_party/xdl/xdl_iterate.c +$SH/third_party/xdl/xdl_linker.c +$SH/third_party/xdl/xdl_lzma.c +$SH/third_party/xdl/xdl_util.c +" + +SH_INCLUDES=" +-I$SH +-I$SH/include +-I$SH/arch/arm64 +-I$SH/common +-I$SH/third_party/xdl +-I$SH/third_party/bsd +-I$SH/third_party/lss +" + +SH_OBJS="" +for src in $SH_SRCS; do + [ -z "$src" ] && continue + src=$(echo "$src" | xargs) + if [ ! -f "$src" ]; then + echo "ERROR: shadowhook source not found: $src" + exit 1 + fi + + basename=$(echo "$src" | sed "s|$SH/||" | tr '/' '_') + obj="$BUILD_DIR/sh_${basename%.c}.o" + obj="${obj%.S.o}.o" + + if [[ "$src" == *.S ]]; then + "$CC" $CFLAGS $SH_INCLUDES -c "$src" -o "$obj" + else + "$CC" $CFLAGS $SH_INCLUDES -c "$src" -o "$obj" + fi + SH_OBJS="$SH_OBJS $obj" +done + +if [ -z "$SH_OBJS" ]; then + echo "ERROR: shadowhook produced no object files." + exit 1 +fi + +"$AR" rcs "$OUT_DIR/libshadowhook.a" $SH_OBJS +SH_SIZE=$(wc -c < "$OUT_DIR/libshadowhook.a" 2>/dev/null || echo "unknown") +echo " Built $OUT_DIR/libshadowhook.a ($SH_SIZE bytes)" + +NOTHING_SRC="$SH/nothing/sh_nothing.c" +if [ ! -f "$NOTHING_SRC" ]; then + echo "ERROR: ShadowHook companion source not found: $NOTHING_SRC" + exit 1 +fi + +"$CC" -shared -Oz -fPIC -fno-ident -fno-unwind-tables -fno-asynchronous-unwind-tables \ + -nostdlib -nostartfiles -nodefaultlibs \ + -Wl,-z,max-page-size=16384 \ + -Wl,--strip-all -Wl,--as-needed -Wl,--hash-style=sysv \ + -Wl,--build-id=none \ + -o "$OUT_DIR/libshadowhook_nothing.so" "$NOTHING_SRC" +echo " Built $OUT_DIR/libshadowhook_nothing.so" diff --git a/script/build.sh b/script/build.sh new file mode 100755 index 0000000..4d2d022 --- /dev/null +++ b/script/build.sh @@ -0,0 +1,123 @@ +#!/bin/bash +# Script to compile the native hooking library for Android + +set -euo pipefail + +PROJECT_DIR=$(cd "$(dirname "$0")/.." && pwd) +SRC_DIR="$PROJECT_DIR/src" +BUILD_DIR="$PROJECT_DIR/build" +LIBS_DIR="$PROJECT_DIR/libs" +API_LEVEL="${ANDROID_API_LEVEL:-21}" + +# Qt path detection +QT_DIR="${QT_DIR:-$HOME/Qt}" +QT_VERSION="${QT_VERSION:-6.10.2}" +QT_HOST_DIR="$QT_DIR/$QT_VERSION/android_arm64_v8a" +QT_LIB_DIR="$QT_HOST_DIR/lib" + +SO_NAME="librmhook.so" + +# --- Auto-detect Android NDK --- +find_ndk() { + if [ -n "${ANDROID_NDK_HOME:-}" ] && [ -d "$ANDROID_NDK_HOME" ]; then + echo "$ANDROID_NDK_HOME"; return + fi + local brew_ndk + brew_ndk=$(find /opt/homebrew /usr/local -path "*/android-ndk/*/toolchains/llvm/prebuilt" -type d 2>/dev/null | head -1) + if [ -n "$brew_ndk" ]; then + echo "$(dirname "$(dirname "$(dirname "$brew_ndk")")")"; return + fi + local sdk_ndk + sdk_ndk=$(find "$HOME/Library/Android/sdk/ndk" -maxdepth 1 -type d 2>/dev/null | sort -V | tail -1) + if [ -n "$sdk_ndk" ] && [ -d "$sdk_ndk" ]; then + echo "$sdk_ndk"; return + fi + for p in /opt/android-ndk /usr/local/android-ndk "$HOME/android-ndk"; do + if [ -d "$p" ]; then echo "$p"; return; fi + done + echo "" +} + +NDK_HOME="$(find_ndk)" +CC="" +if [ -n "$NDK_HOME" ]; then + CC=$(find "$NDK_HOME/toolchains/llvm/prebuilt" -type f \ + -name "aarch64-linux-android${API_LEVEL}-clang" -print -quit 2>/dev/null) +fi +if [ -z "$CC" ]; then + echo "❌ aarch64-linux-android clang not found." + echo " Set ANDROID_NDK_HOME to your NDK path, or install via:" + echo " brew install --cask android-ndk" + exit 1 +fi + +echo "🔨 Compiling $SO_NAME..." +echo "📦 Qt path: $QT_HOST_DIR" +echo "📦 NDK: $NDK_HOME" +echo "" + +# Clean and create build directories +rm -rf "$BUILD_DIR" +mkdir -p "$BUILD_DIR" + +# --- Step 0: Build dependencies (shadowhook) --- +echo "[0/2] Building hooking library (shadowhook)..." + +"$LIBS_DIR/build_deps.sh" + +# --- Step 1: Prepare Qt headers --- +echo "[1/2] Preparing Qt headers..." + +if [ ! -d "$QT_HOST_DIR/include" ]; then + echo "⚠️ Qt not found at $QT_HOST_DIR/include" + echo " Install with: aqt install-qt all_os android $QT_VERSION -m qtwebsockets --outputdir $QT_DIR" + exit 1 +fi + +for qt_lib in \ + "$QT_LIB_DIR/libQt6Core_arm64-v8a.so" \ + "$QT_LIB_DIR/libQt6Network_arm64-v8a.so" \ + "$QT_LIB_DIR/libQt6WebSockets_arm64-v8a.so"; do + if [ ! -f "$qt_lib" ]; then + echo "ERROR: Required Qt library not found: $qt_lib" + exit 1 + fi +done + +QT_INCLUDES="-I$QT_HOST_DIR/include" +for subdir in "$QT_HOST_DIR/include"/*/; do + QT_INCLUDES="$QT_INCLUDES -I$subdir" +done + +echo "✅ Qt headers ready at $QT_HOST_DIR" +echo "✅ Qt libraries ready at $QT_LIB_DIR" + +# --- Step 2: Compile native hooking library --- +echo "[2/2] Compiling $SO_NAME..." + +"$CC" -shared -fPIC -O2 -std=c++17 -stdlib=libc++ -fno-exceptions -fno-rtti \ + -DQT_NO_VERSION_TAGGING \ + -o "$BUILD_DIR/$SO_NAME" \ + "$SRC_DIR/rmhook.cpp" \ + "$SRC_DIR/Config.cpp" \ + $QT_INCLUDES \ + -I"$LIBS_DIR/shadowhook/shadowhook/src/main/cpp/include" \ + -I"$SRC_DIR" \ + -L"$LIBS_DIR" -lshadowhook \ + -L"$QT_LIB_DIR" \ + -Wl,--no-undefined \ + -Wl,-z,defs \ + -l:libQt6WebSockets_arm64-v8a.so \ + -l:libQt6Network_arm64-v8a.so \ + -l:libQt6Core_arm64-v8a.so \ + -lc++_shared \ + -llog -ldl -lm \ + -Wno-incompatible-pointer-types + +echo "" +echo "✅ Compilation successful!" +echo "📍 Library: $BUILD_DIR/$SO_NAME ($(ls -lh "$BUILD_DIR/$SO_NAME" | awk '{print $5}'))" +echo "" +echo "🚀 To inject into the reMarkable application:" +echo " ./script/inject.sh" +echo "" diff --git a/script/inject.sh b/script/inject.sh new file mode 100755 index 0000000..26c123c --- /dev/null +++ b/script/inject.sh @@ -0,0 +1,166 @@ +#!/bin/bash +# Script to patch and sign the reMarkable Android APKs + +set -euo pipefail + +PROJECT_DIR=$(cd "$(dirname "$0")/.." && pwd) +BUILD_DIR="$PROJECT_DIR/build" +OUTPUT_DIR="$PROJECT_DIR/output" +LIBS_DIR="$PROJECT_DIR/libs" + +# --- Auto-detect Android SDK build tools --- +find_build_tools() { + local sdk_root="${ANDROID_SDK_ROOT:-$HOME/Library/Android/sdk}" + local bt + bt=$(find "$sdk_root/build-tools" -maxdepth 1 -type d 2>/dev/null | sort -V | tail -1) + if [ -n "$bt" ] && [ -d "$bt" ]; then + echo "$bt"; return + fi + echo "" +} + +BUILD_TOOLS="${ANDROID_SDK_BUILD_TOOLS:-$(find_build_tools)}" +if [ -z "$BUILD_TOOLS" ] || [ ! -d "$BUILD_TOOLS" ]; then + echo "❌ Android SDK build-tools not found." + echo " Set ANDROID_SDK_BUILD_TOOLS to your build-tools path." + exit 1 +fi + +ZIPALIGN="$BUILD_TOOLS/zipalign" +APKSIGNER="$BUILD_TOOLS/apksigner" + +KEYSTORE="$PROJECT_DIR/config/rmhook.keystore" +KEYSTORE_PASS="rmhook" +KEY_ALIAS="rmhook" + +usage() { + echo "Usage: $0 [apk_dir]" + echo "" + echo " apk_dir - Directory containing the split APKs (default: ~/reMarkable/app)" + echo "" + echo "Output: patched APKs in $OUTPUT_DIR/" + exit 1 +} + +APK_DIR="${1:-$HOME/reMarkable/app}" + +echo "💉 Injecting RMHook into reMarkable APKs..." +echo "📦 APK source: $APK_DIR" +echo "" + +if [ ! -f "$BUILD_DIR/librmhook.so" ]; then + echo "❌ librmhook.so not found. Run ./script/build.sh first." + exit 1 +fi + +# Clean and create output directory +rm -rf "$OUTPUT_DIR" +mkdir -p "$OUTPUT_DIR" + +# --- Step 1: Decompile and patch base.apk (smali injection only, no resources) --- +echo "[1/4] Patching base.apk (smali injection)..." +# Use -r (--no-res) to skip decoding/recoding resources. We only need to modify the DEX. +apktool d -r "$APK_DIR/base.apk" -o "$BUILD_DIR/base" -f + +MAIN_SMALI="$BUILD_DIR/base/smali/com/remarkable/mobile/MainActivity.smali" +if [ ! -f "$MAIN_SMALI" ]; then + echo "❌ MainActivity.smali not found" + exit 1 +fi + +# Insert System.loadLibrary("rmhook") at the very start of +sed -i '' '/.method static constructor ()V/{ +n +/.locals/{ +a\ +\ + const-string v0, "rmhook"\ +\ + invoke-static {v0}, Ljava/lang/System;->loadLibrary(Ljava/lang/String;)V +} +}' "$MAIN_SMALI" + +echo "✅ Injected System.loadLibrary(\"rmhook\") into MainActivity." + +apktool b "$BUILD_DIR/base" -o "$BUILD_DIR/base_patched.apk" + +# --- Step 2: Patch split_config.arm64_v8a.apk (add hook library) --- +echo "[2/4] Injecting ShadowHook libraries into arm64 split..." +SPLIT_ARM64="$APK_DIR/split_config.arm64_v8a.apk" +if [ ! -f "$SPLIT_ARM64" ]; then + echo "❌ $SPLIT_ARM64 not found" + exit 1 +fi + +mkdir -p "$BUILD_DIR/split_arm64" +unzip -q "$SPLIT_ARM64" -d "$BUILD_DIR/split_arm64" + +cp "$BUILD_DIR/librmhook.so" "$BUILD_DIR/split_arm64/lib/arm64-v8a/" +cp "$LIBS_DIR/libshadowhook_nothing.so" "$BUILD_DIR/split_arm64/lib/arm64-v8a/" + +rm -rf "$BUILD_DIR/split_arm64/META-INF" + +(cd "$BUILD_DIR/split_arm64" && zip -q -r -0 "$BUILD_DIR/split_arm64_raw.apk" . -x "*.so") +(cd "$BUILD_DIR/split_arm64" && zip -q -r "$BUILD_DIR/split_arm64_raw.apk" lib/) + +# --- Step 3: Align APKs --- +echo "[3/4] Aligning APKs..." +"$ZIPALIGN" -p -f 4 "$BUILD_DIR/base_patched.apk" "$BUILD_DIR/base_aligned.apk" +"$ZIPALIGN" -p -f 4 "$BUILD_DIR/split_arm64_raw.apk" "$BUILD_DIR/split_arm64_aligned.apk" + +SPLIT_XXHDPI="$APK_DIR/split_config.xxhdpi.apk" +if [ -f "$SPLIT_XXHDPI" ]; then + mkdir -p "$BUILD_DIR/split_xxhdpi" + unzip -q "$SPLIT_XXHDPI" -d "$BUILD_DIR/split_xxhdpi" + rm -rf "$BUILD_DIR/split_xxhdpi/META-INF" + (cd "$BUILD_DIR/split_xxhdpi" && zip -q -r -0 "$BUILD_DIR/split_xxhdpi_raw.apk" .) + "$ZIPALIGN" -p -f 4 "$BUILD_DIR/split_xxhdpi_raw.apk" "$BUILD_DIR/split_xxhdpi_aligned.apk" +fi + +# --- Step 4: Sign all APKs --- +echo "[4/4] Signing APKs..." + +if [ ! -f "$KEYSTORE" ]; then + mkdir -p "$(dirname "$KEYSTORE")" + echo "🔑 Generating signing keystore..." + keytool -genkeypair -v \ + -keystore "$KEYSTORE" \ + -alias "$KEY_ALIAS" \ + -keyalg RSA -keysize 2048 \ + -validity 10000 \ + -storepass "$KEYSTORE_PASS" \ + -keypass "$KEYSTORE_PASS" \ + -dname "CN=RMHook, OU=Dev, O=RMHook, L=Unknown, ST=Unknown, C=US" \ + 2>/dev/null +fi + +sign_apk() { + local input="$1" + local output="$2" + "$APKSIGNER" sign \ + --ks "$KEYSTORE" \ + --ks-pass "pass:$KEYSTORE_PASS" \ + --ks-key-alias "$KEY_ALIAS" \ + --key-pass "pass:$KEYSTORE_PASS" \ + --out "$output" \ + "$input" +} + +sign_apk "$BUILD_DIR/base_aligned.apk" "$OUTPUT_DIR/base.apk" +sign_apk "$BUILD_DIR/split_arm64_aligned.apk" "$OUTPUT_DIR/split_config.arm64_v8a.apk" +if [ -f "$BUILD_DIR/split_xxhdpi_aligned.apk" ]; then + sign_apk "$BUILD_DIR/split_xxhdpi_aligned.apk" "$OUTPUT_DIR/split_config.xxhdpi.apk" +fi + +echo "" +echo "✅ Injection successful!" +echo "📍 Patched APKs in: $OUTPUT_DIR/" +ls -lh "$OUTPUT_DIR/"*.apk +echo "" +echo "🚀 To install on your reMarkable:" +echo " adb uninstall com.remarkable.mobile" +echo " adb install-multiple --no-streaming $OUTPUT_DIR/base.apk $OUTPUT_DIR/split_config.arm64_v8a.apk $OUTPUT_DIR/split_config.xxhdpi.apk" +echo "" +echo "🔍 To debug:" +echo " adb logcat -s 'RMHook:*'" +echo ""