diff --git a/.github/workflows/build-for-x86_64.yml b/.github/workflows/build-for-x86_64.yml index 67dd2a8..f60c056 100644 --- a/.github/workflows/build-for-x86_64.yml +++ b/.github/workflows/build-for-x86_64.yml @@ -26,6 +26,9 @@ jobs: curl -fLO https://dl.google.com/android/repository/android-ndk-r23b-linux.zip unzip -qd . android-ndk-r23b-linux.zip + - name: Wrapper drift check + run: python3 scripts/check-drift.py + - name: Build run: | mkdir build diff --git a/Dockerfile.build b/Dockerfile.build new file mode 100644 index 0000000..66f64c1 --- /dev/null +++ b/Dockerfile.build @@ -0,0 +1,36 @@ +FROM debian:13.2 + +ARG TARGET_ARCH=amd64 +ARG NDK_VERSION=23 + +WORKDIR /app + +RUN apt-get update && apt-get install -y --no-install-recommends \ + build-essential \ + cmake \ + unzip \ + git \ + ca-certificates \ + aria2 \ + && rm -rf /var/lib/apt/lists/* + +# NOTE: No system LLVM install needed — the Android NDK bundles its own +# clang at android-ndk-rb/toolchains/llvm/prebuilt/linux-x86_64/bin/ + +# Download Android NDK +RUN aria2c -x 16 -o ndk.zip \ + https://dl.google.com/android/repository/android-ndk-r${NDK_VERSION}b-linux.zip \ + && unzip -q -d /app ndk.zip \ + && rm ndk.zip + +# Copy source (no glob — explicit) +COPY cmdline.c cmdline.h wrapper.ggo ./ +COPY main.c main.cpp ./ +COPY wrapper.c wrapper-rootless.c ./ +COPY import.h ./ +COPY CMakeLists.txt ./ +COPY rootfs ./rootfs + +RUN mkdir -p build \ + && cmake -S /app -B /app/build -DTARGET_ARCH=${TARGET_ARCH} \ + && cmake --build /app/build -j$(nproc) diff --git a/README.md b/README.md index 0f70be8..d5913be 100644 --- a/README.md +++ b/README.md @@ -1,93 +1,163 @@ # wrapper -A tool to decrypt Apple Music songs. An active subscription is still needed. +A high-performance daemon and native library to decrypt Apple Music streams on Linux. An active subscription is required. -Supports only x86_64 and arm64 Linux. +Supports **x86_64** and **arm64** Linux. -## Installation +--- -Installation methods: +## Architecture & Modes -- [Docker](#docker) (recommended) -- Prebuilt binaries (from [releases](https://github.com/WorldObservationLog/wrapper/releases) or [actions](https://github.com/WorldObservationLog/wrapper/actions)) -- [Build from source](#build-from-source) +`wrapper` supports three execution modes depending on your deployment environment: -### Docker +| Mode | Binary / Target | Isolation | Description | +|---|---|---|---| +| **Host-Native (libhybris)** | `drm-native`
`libdrm-native.so` | None (In-Process) | **Recommended.** Loads Android Bionic `.so` libraries directly into a native glibc Linux process via libhybris. Eliminates container/proot overhead and enables in-process CGO linking. | +| **Rootless Container** | `wrapper-rootless` | User namespaces / proot | Runs unprivileged in userspace without requiring Docker or root permissions. | +| **Docker Container** | `wrapper` | Privileged container | Containerized deployment using Docker. | -Available for x86_64 and arm64. Need to download prebuilt version from releases or actions. +### Daemon Resilience & Auto-Recovery +The daemon implements a recoverable state machine (`Running`, `Scheduled`, `Refreshing`, `Failed`): +- **Non-blocking Lease Callbacks:** Lease expiry (`endLeaseCb`) and playback error (`pbErrCb`) events are queued without blocking the library thread. +- **Dedicated Recovery Worker:** Automatically coalesces lease refreshes with exponential backoff (1s → 2s → 5s → 10s → 30s). +- **Request Gating & Thread Safety:** Mutex-protected FairPlay context reads; HTTP requests gracefully gate during re-authentication rather than terminating the process with `exit(1)`. -1. Build image: +--- -``` -docker build --tag ghcr.io/worldobservationlog/wrapper:local . -``` +## Installation & Building + +### 1. Host-Native Build via Libhybris (Fastest, No NDK Required) + +Compiles `drm-native` and `libdrm-native.so` directly against glibc using host `gcc`/`g++` and libhybris. -2. Login: +#### Prerequisites +- Host build tools: `gcc`, `g++`, `curl`, `patchelf` +- Built `libhybris-core.so` and linker plugin `q.so` +- libhybris headers: set `HYBRIS_INC` to the `hybris/include` directory +- Dobby (tested at commit `e9fe7fb`): `dobby.h` and a built `libdobby.a` (set `DOBBY_SRC` / `DOBBY_BUILD`; defaults `/tmp/dobby-src`, `/tmp/dobby-build`) + - On newer GCC, configure with `-DCMAKE_C_FLAGS="-include sys/time.h"`. + - Dobby's `external/logging/logging/logging.h` needs `inline` on the `Logger::Shared()` definition, or linking fails with multiple definitions. +- Optional overrides: `HYBRIS_LIB` / `LINKER_SO` (paths to `libhybris-core.so` and `q.so`), `DEPLOY_DIR_EXTRA`. +#### Build +```bash +# One-shot build (outputs to /tmp/wrapper-native) +bash build-native.sh + +# Or build and deploy to a target directory: +DEPLOY_DIR=/path/to/drm bash build-and-deploy.sh ``` -docker run --privileged --rm -it -v ./rootfs/data:/app/rootfs/data --entrypoint ./wrapper ghcr.io/worldobservationlog/wrapper:local -L "username:password" -H 0.0.0.0 + +#### Runtime Environment +When executing `drm-native` directly, configure the hybris environment paths: +```bash +export HYBRIS_LINKER_DIR=/path/to/hybris-linker +export HYBRIS_LD_LIBRARY_PATH=/path/to/rootfs/system/lib64 +export HYBRIS_ANDROID_LIB64=/path/to/rootfs/system/lib64 + +./drm-native --base-dir /path/to/data/files ``` -Quit after this (using Ctrl-C). +--- -3. Run: +### 2. In-Process C / CGO Library (`drm_lib`) -``` -docker run --privileged -v ./rootfs/data:/app/rootfs/data -p 10020:10020 -p 20020:20020 -p 30020:30020 -e args="-H 0.0.0.0" ghcr.io/worldobservationlog/wrapper:local +When compiled with `build-native.sh`, `libdrm-native.so` exposes a C API defined in [drm_lib.h](drm_lib.h) that can be embedded directly into Go (via CGO) or C/C++ applications without socket IPC: + +```c +#include "drm_lib.h" + +drm_lib_config_t cfg = { + .base_dir = "/path/to/data", + .lib64_dir = "/path/to/rootfs/system/lib64", + .auth_cb = my_auth_callback, + .state_cb = my_state_callback, +}; + +if (drm_lib_init(&cfg) == 0) { + /* decrypt sample in-process */ + drm_lib_decrypt(kd_ctx, sample_buffer, sample_size); + drm_lib_shutdown(); +} ``` +--- -### Build from source +### 3. Docker -1. Install dependencies: +Available for x86_64 and arm64. -- Build tools: +1. **Build image:** + ```bash + docker build --tag ghcr.io/worldobservationlog/wrapper:local . + ``` - ``` - sudo apt install build-essential cmake curl unzip git - ``` +2. **Initial Login:** + ```bash + docker run --privileged --rm -it \ + -v ./rootfs/data:/app/rootfs/data \ + -entrypoint ./wrapper ghcr.io/worldobservationlog/wrapper:local \ + -L "username:password" -H 0.0.0.0 + ``` + *(Exit using Ctrl-C after authentication succeeds).* -- LLVM: +3. **Run Daemon:** + ```bash + docker run --privileged \ + -v ./rootfs/data:/app/rootfs/data \ + -p 10020:10020 -p 20020:20020 -p 30020:30020 -p 40020:40020 -p 50020:50020 -p 60020:60020 \ + -e args="-H 0.0.0.0" \ + ghcr.io/worldobservationlog/wrapper:local + ``` - ``` - sudo bash -c "$(wget -O - https://apt.llvm.org/llvm.sh)" - ``` +--- -- Android NDK r23b: - ``` - curl -fLO https://dl.google.com/android/repository/android-ndk-r23b-linux.zip - unzip -d . android-ndk-r23b-linux.zip - ``` +### 4. Build from Source via Android NDK (Legacy / Rootless) -2. Build: +Builds the Bionic-linked `main` executable, `wrapper`, and `wrapper-rootless`. -``` -git clone https://github.com/WorldObservationLog/wrapper -cd wrapper -mkdir build -cd build -cmake .. -make -j$(nproc) -``` +1. **Install dependencies:** + ```bash + sudo apt install build-essential cmake curl unzip git + sudo bash -c "$(wget -O - https://apt.llvm.org/llvm.sh)" + ``` -## Usage +2. **Download Android NDK r23b:** + ```bash + curl -fLO https://dl.google.com/android/repository/android-ndk-r23b-linux.zip + unzip -d . android-ndk-r23b-linux.zip + ``` -``` +3. **Build:** + ```bash + mkdir build && cd build + cmake .. + make -j$(nproc) + ``` + +--- + +## Usage & CLI Options + +```text Usage: wrapper [OPTION]... - -h, --help Print help and exit - -V, --version Print version and exit - -H, --host=STRING (default=`127.0.0.1') - -D, --decrypt-port=INT (default=`10020') - -M, --m3u8-port=INT (default=`20020') - -A, --account-port=INT (default=`30020') - -K, --key-port=INT (default=`40020') - -P, --proxy=STRING (default=`') - -L, --login=STRING (username:password) - -F, --code-from-file (default=off) + -h, --help Print help and exit + -V, --version Print version and exit + -H, --host=STRING Host to bind on (default: 127.0.0.1) + -D, --decrypt-port=INT Decryption server port (default: 10020) + -M, --m3u8-port=INT M3U8 / playlist proxy port (default: 20020) + -A, --account-port=INT Account management port (default: 30020) + -K, --key-port=INT Key service port (default: 40020) + -G, --mv-port=INT Music video port (default: 50020) + -P, --proxy=STRING HTTP proxy URL (default: none) + -L, --login=STRING Apple ID login credentials (username:password) + -F, --code-from-file Read 2FA code from file rather than stdin (default: off) + -B, --base-dir=STRING Base data directory (default: /data/data/com.apple.android.music/files) + -I, --device-info=STRING 9-field client device descriptor ``` -## Services (4 TCP ports) +## Services (6 TCP ports) | Port | Option | Protocol | Purpose | |------|--------|----------|---------| @@ -95,6 +165,8 @@ Usage: wrapper [OPTION]... | 20020 | `-M` | Binary | M3U8 stream URL: `[1B len][adamId digits]` → M3U8 URL | | 30020 | `-A` | HTTP | Account info JSON | | 40020 | `-K` | HTTP | Key service: `?adamId=&uri=` → `{contentKey, ctx, state, rcx/rax/rdx/r9/rbp}` decryption template | +| 50020 | `-G` | see source | Progressive music-video (MV) service (`new_socket_mv`) | +| 60020 | `-G` + 10000 | see source | itun FairPlay decrypt for progressive MV (`new_socket_itun`) | ### 40020 key service @@ -108,6 +180,17 @@ curl "http://127.0.0.1:40020/?adamId=1720704575&uri=skd%3A%2F%2Fitunes.apple.com The template is captured by a Dobby hook at the R1 entry (`libCoreLSKD+0x1d5709`) in debug builds. +--- + +## Development & Testing + +- **Wrapper Drift Check:** Verify synchronization between privileged and rootless wrapper code: + ```bash + python3 scripts/check-drift.py + ``` + +--- + ## Special thanks - Anonymous, for providing the original version of this project and the legacy Frida decryption method. diff --git a/build-and-deploy.sh b/build-and-deploy.sh new file mode 100755 index 0000000..baa4fff --- /dev/null +++ b/build-and-deploy.sh @@ -0,0 +1,60 @@ +#!/usr/bin/env bash +# build-and-deploy.sh: compile drm-native and copy it, with its runtime +# dependencies, into a target directory. +# +# Usage: +# DEPLOY_DIR=/path/to/drm bash build-and-deploy.sh +# +# Optional environment variables: +# DEPLOY_DIR_EXTRA second directory to receive the same files +# NATIVE_BIN, NATIVE_SO, HYBRIS_LIB, LINKER_SO override artefact paths +# +# Layout produced in DEPLOY_DIR: +# drm-native +# libdrm-native.so +# libhybris-core.so (rpath dependency, co-located with the binary) +# hybris-linker/q.so (use as HYBRIS_LINKER_DIR) + +set -euo pipefail + +HERE="$(cd "$(dirname "$0")" && pwd)" + +: "${DEPLOY_DIR:?Set DEPLOY_DIR to the target directory}" +NATIVE_BIN="${NATIVE_BIN:-/tmp/wrapper-native/drm-native}" +NATIVE_SO="${NATIVE_SO:-/tmp/wrapper-native/libdrm-native.so}" +HYBRIS_LIB="${HYBRIS_LIB:-/tmp/hybris-x86_64-build/libhybris-core.so}" +LINKER_SO="${LINKER_SO:-/tmp/hybris-linker/q.so}" + +echo "=== Building drm-native ===" +bash "$HERE/build-native.sh" + +for f in "$NATIVE_BIN" "$NATIVE_SO" "$HYBRIS_LIB" "$LINKER_SO"; do + [[ -f "$f" ]] || { echo "ERROR: required file not found: $f"; exit 1; } +done + +deploy() { + local dir="$1" + echo; echo "=== Deploying to $dir ===" + mkdir -p "$dir/hybris-linker" + cp -v "$NATIVE_BIN" "$dir/drm-native" + cp -v "$NATIVE_SO" "$dir/libdrm-native.so" + cp -v "$HYBRIS_LIB" "$dir/libhybris-core.so" + cp -v "$LINKER_SO" "$dir/hybris-linker/q.so" + chmod +x "$dir/drm-native" + # build-native.sh hard-codes an rpath under /tmp; make it relative. + if command -v patchelf &>/dev/null; then + patchelf --set-rpath '$ORIGIN' "$dir/drm-native" + echo "rpath patched to \$ORIGIN" + else + echo "WARNING: patchelf not found; binary will only run on this machine" + fi +} + +deploy "$DEPLOY_DIR" +[[ -n "${DEPLOY_DIR_EXTRA:-}" ]] && deploy "$DEPLOY_DIR_EXTRA" + +echo; echo "=== Done ===" +echo "Runtime env for drm-native:" +echo " HYBRIS_LINKER_DIR=$DEPLOY_DIR/hybris-linker" +echo " HYBRIS_LD_LIBRARY_PATH=/system/lib64" +echo " HYBRIS_ANDROID_LIB64=/system/lib64" diff --git a/build-native.sh b/build-native.sh new file mode 100755 index 0000000..a98a15b --- /dev/null +++ b/build-native.sh @@ -0,0 +1,122 @@ +#!/usr/bin/env bash +# build-native.sh — compile the DRM wrapper as a host-native glibc x86-64 binary. +# +# Prerequisites: +# - libhybris already built: /tmp/hybris-x86_64-build/libhybris-core.so +# - linker plugin: /tmp/hybris-linker/q.so +# - Android rootfs: drm/rootfs/system/lib64/ (relative to the deploy directory) +# +# Environment expected at runtime: +# HYBRIS_LINKER_DIR=/tmp/hybris-linker +# HYBRIS_LD_LIBRARY_PATH= +# HYBRIS_ANDROID_LIB64= + +set -euo pipefail + +HERE="$(cd "$(dirname "$0")" && pwd)" +BUILD=/tmp/wrapper-native +# Directory holding libhybris-core.so; override to link against another copy +# (e.g. the one deployed in $DEPLOY_DIR). +HYBRIS_BUILD="${HYBRIS_BUILD:-/tmp/hybris-x86_64-build}" +HYBRIS_INC="${HYBRIS_INC:?Set HYBRIS_INC to the libhybris include directory}" +DOBBY_SRC="${DOBBY_SRC:-/tmp/dobby-src}" +DOBBY_BUILD="${DOBBY_BUILD:-/tmp/dobby-build}" +CJSON_DIR="$BUILD/cjson" + +echo "=== host-native DRM wrapper ===" +mkdir -p "$BUILD" "$CJSON_DIR" + +# ── Fetch cJSON (single .c + .h) ───────────────────────────────────────────── +if [[ ! -f "$CJSON_DIR/cJSON.h" ]]; then + echo "--- fetching cJSON v1.7.19 ---" + curl -fsSL "https://raw.githubusercontent.com/DaveGamble/cJSON/v1.7.19/cJSON.c" -o "$CJSON_DIR/cJSON.c" + curl -fsSL "https://raw.githubusercontent.com/DaveGamble/cJSON/v1.7.19/cJSON.h" -o "$CJSON_DIR/cJSON.h" +fi + +CFLAGS=( + -O2 + -Wall + -DMyRelease # disables curl/log debug hooks only; the Dobby R1 hook stays enabled + -D_GNU_SOURCE + -include sys/time.h # gettimeofday — not explicitly included in main.c, provided by NDK headers + -I"$HERE" # import.h, cmdline.h + -I"$CJSON_DIR" # cJSON.h + -I"$DOBBY_SRC/include" # dobby.h + -I"$HYBRIS_INC" # hybris/android/dlopen.h etc. (for reference, not required) +) + +# ── Compile C sources ───────────────────────────────────────────────────────── +echo "--- compiling main.c ---" +gcc "${CFLAGS[@]}" -fPIC -c "$HERE/main.c" -o "$BUILD/main.o" + +echo "--- compiling cmdline.c ---" +gcc "${CFLAGS[@]}" -fPIC -c "$HERE/cmdline.c" -o "$BUILD/cmdline.o" + +echo "--- compiling hybris_stubs.c ---" +gcc "${CFLAGS[@]}" -fPIC -c "$HERE/hybris_stubs.c" -o "$BUILD/hybris_stubs.o" + +echo "--- compiling hybris_ctor.c ---" +gcc "${CFLAGS[@]}" -fPIC -c "$HERE/hybris_ctor.c" -o "$BUILD/hybris_ctor.o" + +echo "--- compiling cJSON.c ---" +gcc -O2 -fPIC -c "$CJSON_DIR/cJSON.c" -o "$BUILD/cjson.o" + +# ── Compile C++ source ──────────────────────────────────────────────────────── +echo "--- compiling main.cpp ---" +g++ -std=c++17 "${CFLAGS[@]}" -fPIC -c "$HERE/main.cpp" -o "$BUILD/main_cpp.o" + +# ── Link binary ─────────────────────────────────────────────────────────────── +echo "--- linking drm-native ---" +g++ \ + "$BUILD/main.o" \ + "$BUILD/cmdline.o" \ + "$BUILD/hybris_stubs.o" \ + "$BUILD/hybris_ctor.o" \ + "$BUILD/cjson.o" \ + "$BUILD/main_cpp.o" \ + "$DOBBY_BUILD/libdobby.a" \ + -L"$HYBRIS_BUILD" -lhybris-core \ + -lcurl \ + -lpthread \ + -ldl \ + -lm \ + -Wl,-rpath,"$HYBRIS_BUILD" \ + -o "$BUILD/drm-native" + +# ── Compile drm_lib.c for the shared library ────────────────────────────────── +echo "--- compiling drm_lib.c ---" +gcc "${CFLAGS[@]}" -fPIC -DDRM_LIB_BUILD -c "$HERE/drm_lib.c" -o "$BUILD/drm_lib.o" + +# Re-compile main.c with -DDRM_LIB_BUILD (guards out int main()) for the .so +echo "--- compiling main.c (lib mode) ---" +gcc "${CFLAGS[@]}" -fPIC -DDRM_LIB_BUILD -c "$HERE/main.c" -o "$BUILD/main_lib.o" + +# ── Link shared library ─────────────────────────────────────────────────────── +echo "--- linking libdrm-native.so ---" +g++ -shared \ + "$BUILD/main_lib.o" \ + "$BUILD/cmdline.o" \ + "$BUILD/hybris_stubs.o" \ + "$BUILD/hybris_ctor.o" \ + "$BUILD/cjson.o" \ + "$BUILD/main_cpp.o" \ + "$BUILD/drm_lib.o" \ + "$DOBBY_BUILD/libdobby.a" \ + -L"$HYBRIS_BUILD" -lhybris-core \ + -lcurl \ + -lpthread \ + -ldl \ + -lm \ + -Wl,-rpath,"\$ORIGIN" \ + -o "$BUILD/libdrm-native.so" + +echo "" +echo "=== Build successful ===" +echo " Binary: $BUILD/drm-native" +echo " Library: $BUILD/libdrm-native.so" +echo "" +echo "Run with:" +echo " HYBRIS_LINKER_DIR=/tmp/hybris-linker \\" +echo " HYBRIS_LD_LIBRARY_PATH=/system/lib64 \\" +echo " HYBRIS_ANDROID_LIB64=/system/lib64 \\" +echo " $BUILD/drm-native --base-dir ..." diff --git a/cmdline.c b/cmdline.c index 370b6b7..c26da17 100644 --- a/cmdline.c +++ b/cmdline.c @@ -41,6 +41,7 @@ const char *gengetopt_args_info_help[] = { " -M, --m3u8-port=INT (default=`20020')", " -A, --account-port=INT (default=`30020')", " -K, --key-port=INT (default=`40020')", + " -G, --mv-port=INT (default=`50020')", " -P, --proxy=STRING (default=`')", " -L, --login=STRING username:password", " -F, --code-from-file (default=off)", @@ -78,6 +79,7 @@ void clear_given (struct gengetopt_args_info *args_info) args_info->m3u8_port_given = 0 ; args_info->account_port_given = 0 ; args_info->key_port_given = 0 ; + args_info->mv_port_given = 0 ; args_info->proxy_given = 0 ; args_info->login_given = 0 ; args_info->code_from_file_given = 0 ; @@ -99,6 +101,8 @@ void clear_args (struct gengetopt_args_info *args_info) args_info->account_port_orig = NULL; args_info->key_port_arg = 40020; args_info->key_port_orig = NULL; + args_info->mv_port_arg = 50020; + args_info->mv_port_orig = NULL; args_info->proxy_arg = gengetopt_strdup (""); args_info->proxy_orig = NULL; args_info->login_arg = NULL; @@ -123,11 +127,12 @@ void init_args_info(struct gengetopt_args_info *args_info) args_info->m3u8_port_help = gengetopt_args_info_help[4] ; args_info->account_port_help = gengetopt_args_info_help[5] ; args_info->key_port_help = gengetopt_args_info_help[6] ; - args_info->proxy_help = gengetopt_args_info_help[7] ; - args_info->login_help = gengetopt_args_info_help[8] ; - args_info->code_from_file_help = gengetopt_args_info_help[9] ; - args_info->base_dir_help = gengetopt_args_info_help[10] ; - args_info->device_info_help = gengetopt_args_info_help[11] ; + args_info->mv_port_help = gengetopt_args_info_help[7] ; + args_info->proxy_help = gengetopt_args_info_help[8] ; + args_info->login_help = gengetopt_args_info_help[9] ; + args_info->code_from_file_help = gengetopt_args_info_help[10] ; + args_info->base_dir_help = gengetopt_args_info_help[11] ; + args_info->device_info_help = gengetopt_args_info_help[12] ; } @@ -223,6 +228,7 @@ cmdline_parser_release (struct gengetopt_args_info *args_info) free_string_field (&(args_info->m3u8_port_orig)); free_string_field (&(args_info->account_port_orig)); free_string_field (&(args_info->key_port_orig)); + free_string_field (&(args_info->mv_port_orig)); free_string_field (&(args_info->proxy_arg)); free_string_field (&(args_info->proxy_orig)); free_string_field (&(args_info->login_arg)); @@ -275,6 +281,8 @@ cmdline_parser_dump(FILE *outfile, struct gengetopt_args_info *args_info) write_into_file(outfile, "account-port", args_info->account_port_orig, 0); if (args_info->key_port_given) write_into_file(outfile, "key-port", args_info->key_port_orig, 0); + if (args_info->mv_port_given) + write_into_file(outfile, "mv-port", args_info->mv_port_orig, 0); if (args_info->proxy_given) write_into_file(outfile, "proxy", args_info->proxy_orig, 0); if (args_info->login_given) @@ -552,6 +560,7 @@ cmdline_parser_internal ( { "m3u8-port", 1, NULL, 'M' }, { "account-port", 1, NULL, 'A' }, { "key-port", 1, NULL, 'K' }, + { "mv-port", 1, NULL, 'G' }, { "proxy", 1, NULL, 'P' }, { "login", 1, NULL, 'L' }, { "code-from-file", 0, NULL, 'F' }, @@ -560,7 +569,7 @@ cmdline_parser_internal ( { 0, 0, 0, 0 } }; - c = getopt_long (argc, argv, "hVH:D:M:A:K:P:L:FB:I:", long_options, &option_index); + c = getopt_long (argc, argv, "hVH:D:M:A:K:G:P:L:FB:I:", long_options, &option_index); if (c == -1) break; /* Exit from `while (1)' loop. */ @@ -635,11 +644,23 @@ cmdline_parser_internal ( additional_error)) goto failure; + break; + case 'G': /* . */ + + + if (update_arg( (void *)&(args_info->mv_port_arg), + &(args_info->mv_port_orig), &(args_info->mv_port_given), + &(local_args_info.mv_port_given), optarg, 0, "50020", ARG_INT, + check_ambiguity, override, 0, 0, + "mv-port", 'G', + additional_error)) + goto failure; + break; case 'P': /* . */ - - - if (update_arg( (void *)&(args_info->proxy_arg), + + + if (update_arg( (void *)&(args_info->proxy_arg), &(args_info->proxy_orig), &(args_info->proxy_given), &(local_args_info.proxy_given), optarg, 0, "", ARG_STRING, check_ambiguity, override, 0, 0, diff --git a/cmdline.h b/cmdline.h index 7690a28..b2f1e20 100644 --- a/cmdline.h +++ b/cmdline.h @@ -54,6 +54,9 @@ struct gengetopt_args_info int key_port_arg; /**< @brief (default='40020'). */ char * key_port_orig; /**< @brief original value given at command line. */ const char *key_port_help; /**< @brief help description. */ + int mv_port_arg; /**< @brief (default='50020'). */ + char * mv_port_orig; /**< @brief original value given at command line. */ + const char *mv_port_help; /**< @brief help description. */ char * proxy_arg; /**< @brief (default=''). */ char * proxy_orig; /**< @brief original value given at command line. */ const char *proxy_help; /**< @brief help description. */ @@ -76,6 +79,7 @@ struct gengetopt_args_info unsigned int m3u8_port_given ; /**< @brief Whether m3u8-port was given. */ unsigned int account_port_given ; /**< @brief Whether account-port was given. */ unsigned int key_port_given ; /**< @brief Whether key-port was given. */ + unsigned int mv_port_given ; /**< @brief Whether mv-port was given. */ unsigned int proxy_given ; /**< @brief Whether proxy was given. */ unsigned int login_given ; /**< @brief Whether login was given. */ unsigned int code_from_file_given ; /**< @brief Whether code-from-file was given. */ diff --git a/drm_lib.c b/drm_lib.c new file mode 100644 index 0000000..4e28980 --- /dev/null +++ b/drm_lib.c @@ -0,0 +1,361 @@ +/* + * drm_lib.c — in-process C API implementation. + * + * Built with -DDRM_LIB_BUILD alongside main.c (which guards int main() with + * #ifndef DRM_LIB_BUILD). The result is libdrm-native.so, loaded by the Go + * engine via CGO instead of forking drm-native as a subprocess. + */ + +#include +#include +#include +#include +#include +#include + +#define IMPORT_H_NO_DATA_DEFS +#include "import.h" +#include "drm_lib.h" +#include "cmdline.h" + +/* ── Externs from hybris_stubs.c ────────────────────────────────────────────*/ + +extern uint8_t endLeaseCallback[32]; +extern uint8_t pbErrCallback[32]; + +/* ── Forward declarations for functions in main.c ──────────────────────────*/ + +/* init helpers (defined in main.c, de-staticized) */ +extern void drm_init_internal(void); +extern struct shared_ptr drm_init_ctx(void); + +/* globals in main.c (de-staticized) */ +extern struct shared_ptr apInf; +extern uint8_t leaseMgr[16]; +extern struct shared_ptr reqCtx; +extern struct gengetopt_args_info args_info; +extern char *amUsername, *amPassword; +extern int decryptCount; +extern int offlineFlag; +extern char *device_infos[9]; +extern void *FHinstance; +extern void *preshareCtx; +extern struct shared_ptr g_itun_decryptor; +extern unsigned long g_itun_adam_id; +extern pthread_mutex_t g_itun_mutex; + +extern drm_auth_cb_t g_drm_auth_cb; +extern void *g_drm_auth_ud; +extern drm_state_cb_t g_drm_state_cb; +extern void *g_drm_state_ud; + +/* init/codec helpers in main.c / hybris_stubs.c */ +extern void hybris_init_callbacks(void); +extern void start_recovery_thread(void); +extern int hybris_init_libs(const char *lib64_path); + +/* account helpers */ +extern char *get_account_storefront_id(struct shared_ptr reqCtx); +extern char *get_dev_token(struct shared_ptr reqCtx); +extern char *get_music_user_token(char *guid, char *authToken, struct shared_ptr reqCtx); +extern char *get_guid(void); +extern void write_storefront_id(void); +extern void write_music_token(void); +extern int offline_available(void); + +/* decrypt helpers */ +extern void *getKdContext(const char *adam, const char *uri); + +/* m3u8 / progressive helpers */ +extern const char *get_m3u8_method_play(uint8_t leaseMgr[16], unsigned long adamID, char **dk); +extern const char *get_m3u8_method_download(struct shared_ptr reqCtx, unsigned long adamID, char **dk); +extern const char *get_progressive_method_play(uint8_t leaseMgr[16], unsigned long adamID, char **dk); + +/* NfcRKVnxuKZy04KWbdFu71Ou and _ZN17SVPastisDecryptor13decryptSampleEPKhRKjPj + * are declared by import.h (included above with IMPORT_H_NO_DATA_DEFS). */ + +/* cached account info (in main.c) */ +extern char *g_storefront_id; +extern char *g_dev_token; +extern char *g_music_token; + +/* ── Module state ───────────────────────────────────────────────────────────*/ + +static int g_init_result = 0; /* 0 = success, -1 = failed */ +static pthread_mutex_t g_init_mutex = PTHREAD_MUTEX_INITIALIZER; + +/* ── drm_lib_init ───────────────────────────────────────────────────────────*/ + +int drm_lib_init(const drm_lib_config_t *cfg) +{ + pthread_mutex_lock(&g_init_mutex); + + /* Store callbacks before any library call so write_drm_state fires them */ + g_drm_auth_cb = cfg->auth_cb; + g_drm_auth_ud = cfg->auth_ud; + g_drm_state_cb = cfg->state_cb; + g_drm_state_ud = cfg->state_ud; + + /* If the Android lib64 dir is provided, initialise hybris now. + * The hybris_ctor constructor may have been a no-op (env var not set at + * load time when running in-process), so we re-run init here. */ + if (cfg->lib64_dir && cfg->lib64_dir[0]) { + setenv("HYBRIS_ANDROID_LIB64", cfg->lib64_dir, 1); + if (hybris_init_libs(cfg->lib64_dir) != 0) { + g_init_result = -1; + pthread_mutex_unlock(&g_init_mutex); + return -1; + } + } + + /* Synthesise a fake args_info from cfg so the rest of main.c still works */ + memset(&args_info, 0, sizeof(args_info)); + if (cfg->base_dir) { + args_info.base_dir_arg = (char *)cfg->base_dir; + args_info.base_dir_given = 1; + } + { + /* Use provided device_info or fall back to the same default as cmdline.c */ + const char *di = (cfg->device_info && cfg->device_info[0]) + ? cfg->device_info + : "Music/4.9/Android/10/Samsung S9/7663313/en-US/en-US/dc28071e981c439e"; + args_info.device_info_arg = (char *)di; + args_info.device_info_given = (cfg->device_info && cfg->device_info[0]) ? 1 : 0; + /* split device_infos just like main() does */ + static char *di_copy = NULL; + if (di_copy) free(di_copy); + di_copy = strdup(di); + char *tok = strtok(di_copy, "/"); + for (int i = 0; i < 9 && tok; i++) { + device_infos[i] = tok; + tok = strtok(NULL, "/"); + } + } + offlineFlag = cfg->offline_only ? 1 : 0; + + /* Credentials for fresh login */ + if (cfg->username && cfg->password) { + amUsername = (char *)cfg->username; + /* Allocate a mutable buffer (credentialHandler appends the 2FA code) */ + static char pw_buf[256]; + strncpy(pw_buf, cfg->password, sizeof(pw_buf) - 1); + pw_buf[sizeof(pw_buf) - 1] = '\0'; + amPassword = pw_buf; + args_info.login_arg = NULL; /* not used in library mode */ + args_info.login_given = 0; + } + + /* + * Run the exact same sequence as main() up to RUNNING state. + * hybris_init_libs() was already called by hybris_ctor.c's __attribute__((constructor)). + */ + drm_init_internal(); + hybris_init_callbacks(); + + reqCtx = drm_init_ctx(); + if (reqCtx.obj == NULL) { + fprintf(stderr, "[drm_lib] drm_init_ctx failed\n"); + g_init_result = -1; + pthread_mutex_unlock(&g_init_mutex); + return -1; + } + + /* Login if credentials supplied */ + extern uint8_t login(struct shared_ptr ctx); + if (cfg->username && cfg->password) { + if (!login(reqCtx)) { + fprintf(stderr, "[drm_lib] login failed\n"); + g_init_result = -1; + pthread_mutex_unlock(&g_init_mutex); + return -1; + } + } + + /* Lease */ + _ZN22SVPlaybackLeaseManagerC2ERKNSt6__ndk18functionIFvRKiEEERKNS1_IFvRKNS0_10shared_ptrIN17storeservicescore19StoreErrorConditionEEEEEE( + leaseMgr, &endLeaseCallback, &pbErrCallback); + uint8_t autom = 1; + _ZN22SVPlaybackLeaseManager25refreshLeaseAutomaticallyERKb(leaseMgr, &autom); + _ZN22SVPlaybackLeaseManager12requestLeaseERKb(leaseMgr, &autom); + + fprintf(stderr, "[drm_lib] FHinstance\n"); fflush(stderr); + FHinstance = _ZN21SVFootHillSessionCtrl8instanceEv(); + fprintf(stderr, "[drm_lib] start_recovery_thread\n"); fflush(stderr); + start_recovery_thread(); + fprintf(stderr, "[drm_lib] offline_available\n"); fflush(stderr); + + offlineFlag = offline_available(); + fprintf(stderr, "[drm_lib] offline=%d, get_storefront_id\n", offlineFlag); fflush(stderr); + + /* Cache account tokens */ + g_storefront_id = get_account_storefront_id(reqCtx); + if (!g_storefront_id) { + fprintf(stderr, "[drm_lib] failed to get storefront ID\n"); + g_init_result = -1; + pthread_mutex_unlock(&g_init_mutex); + return -1; + } + fprintf(stderr, "[drm_lib] storefront_id=%s, get_dev_token\n", g_storefront_id); fflush(stderr); + g_dev_token = get_dev_token(reqCtx); + if (!g_dev_token) { + fprintf(stderr, "[drm_lib] failed to get dev token\n"); + g_init_result = -1; + pthread_mutex_unlock(&g_init_mutex); + return -1; + } + fprintf(stderr, "[drm_lib] dev_token ok, get_music_token\n"); fflush(stderr); + g_music_token = get_music_user_token(get_guid(), g_dev_token, reqCtx); + if (!g_music_token) { + fprintf(stderr, "[drm_lib] failed to get music token\n"); + g_init_result = -1; + pthread_mutex_unlock(&g_init_mutex); + return -1; + } + fprintf(stderr, "[drm_lib] music_token ok\n"); fflush(stderr); + + if (cfg->base_dir) { + fprintf(stderr, "[drm_lib] write_storefront_id\n"); fflush(stderr); + write_storefront_id(); + fprintf(stderr, "[drm_lib] write_music_token\n"); fflush(stderr); + write_music_token(); + } + fprintf(stderr, "[drm_lib] calling state_cb RUNNING\n"); fflush(stderr); + + if (g_drm_state_cb) + g_drm_state_cb("RUNNING", g_drm_state_ud); + + g_init_result = 0; + pthread_mutex_unlock(&g_init_mutex); + return 0; +} + +/* ── drm_lib_shutdown ───────────────────────────────────────────────────────*/ + +void drm_lib_shutdown(void) +{ + /* No clean shutdown API in the underlying library — just reset callbacks + * so no further callbacks fire after the Go engine destroys its context. */ + pthread_mutex_lock(&g_init_mutex); + g_drm_auth_cb = NULL; + g_drm_state_cb = NULL; + pthread_mutex_unlock(&g_init_mutex); +} + +/* ── drm_lib_get_m3u8 ───────────────────────────────────────────────────────*/ + +char *drm_lib_get_m3u8(unsigned long adam_id) +{ + const char *url; + if (offlineFlag) + url = get_m3u8_method_download(reqCtx, adam_id, NULL); + else + url = get_m3u8_method_play(leaseMgr, adam_id, NULL); + + if (!url) + return NULL; + char *ret = strdup(url); + free((void *)url); + return ret; +} + +/* ── drm_lib_get_account ────────────────────────────────────────────────────*/ + +char *drm_lib_get_account(void) +{ + if (!g_storefront_id || !g_dev_token || !g_music_token) + return NULL; + + /* Build JSON: {"storefront_id":"…","dev_token":"…","music_token":"…"} */ + size_t len = strlen(g_storefront_id) + strlen(g_dev_token) + + strlen(g_music_token) + 80; + char *buf = malloc(len); + if (!buf) + return NULL; + snprintf(buf, len, + "{\"storefront_id\":\"%s\",\"dev_token\":\"%s\",\"music_token\":\"%s\"}", + g_storefront_id, g_dev_token, g_music_token); + return buf; +} + +/* ── drm_lib_get_mv ─────────────────────────────────────────────────────────*/ + +int drm_lib_get_mv(unsigned long adam_id, char **out_url, char **out_dk, + int *out_has_itun) +{ + char *dk = NULL; + const char *url = get_progressive_method_play(leaseMgr, adam_id, &dk); + if (!url) { + url = get_m3u8_method_play(leaseMgr, adam_id, &dk); + } + if (!url) + return -1; + + *out_url = strdup(url); + free((void *)url); + *out_dk = dk ? strdup(dk) : NULL; + if (dk) free(dk); + + pthread_mutex_lock(&g_itun_mutex); + *out_has_itun = (g_itun_decryptor.obj != NULL && g_itun_adam_id == adam_id) ? 1 : 0; + pthread_mutex_unlock(&g_itun_mutex); + + return 0; +} + +/* ── drm_lib_open_kd_ctx ────────────────────────────────────────────────────*/ + +void *drm_lib_open_kd_ctx(const char *adam_id, const char *uri) +{ + return getKdContext(adam_id, uri); +} + +/* ── drm_lib_decrypt ────────────────────────────────────────────────────────*/ + +int drm_lib_decrypt(void *kd_ctx, uint8_t *sample, uint32_t size) +{ + if (!kd_ctx || !sample) + return -1; + /* getKdContext() returns the key-context slot, not the context itself: + * handle() in main.c (the proven TCP path) calls the decryptor with + * *kdContext. Passing kd_ctx directly decrypts with the wrong context and + * yields garbage samples. */ + void *ctx = *(void **)kd_ctx; + if (!ctx) + return -1; + NfcRKVnxuKZy04KWbdFu71Ou(ctx, (uint32_t)5, sample, sample, (size_t)size); + return 0; +} + +/* ── drm_lib_decrypt_itun ───────────────────────────────────────────────────*/ + +int drm_lib_decrypt_itun(unsigned long adam_id, uint8_t *sample, + uint32_t in_size, uint32_t *out_size) +{ + pthread_mutex_lock(&g_itun_mutex); + void *dec_obj = g_itun_decryptor.obj; + unsigned long dec_adam = g_itun_adam_id; + pthread_mutex_unlock(&g_itun_mutex); + + if (!dec_obj) { + fprintf(stderr, "[drm_lib] itun: no decryptor (call drm_lib_get_mv first)\n"); + return -1; + } + if (dec_adam != adam_id) { + fprintf(stderr, "[drm_lib] itun: adamId mismatch: have %lu, got %lu\n", + dec_adam, adam_id); + return -1; + } + + uint32_t out_len = 0; + _ZN17SVPastisDecryptor13decryptSampleEPKhRKjPj(dec_obj, sample, &in_size, &out_len); + *out_size = out_len; + return 0; +} + +/* ── drm_lib_is_recovery_active ─────────────────────────────────────────────*/ + +int drm_lib_is_recovery_active(void) +{ + extern int is_recovery_active(void); + return is_recovery_active(); +} diff --git a/drm_lib.h b/drm_lib.h new file mode 100644 index 0000000..9d4f2c3 --- /dev/null +++ b/drm_lib.h @@ -0,0 +1,100 @@ +/* + * drm_lib.h — in-process C API for drm-native. + * + * When drm-native is built as a shared library (-DDRM_LIB_BUILD), the Go + * engine loads it via CGO and calls these functions directly instead of + * communicating over TCP sockets. + * + * Thread safety: all functions are safe to call from multiple goroutines. + * drm_lib_init() must complete before any other call. + */ + +#pragma once +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/* ── Callback types ────────────────────────────────────────────────────────── + * + * drm_auth_cb_t — called when the DRM library needs a credential or 2FA code. + * type: "credentials" → write "user:pass\0" into out_buf + * "2fa" → write 6-digit code string into out_buf + * out_buf: caller-provided buffer, out_size bytes + * userdata: opaque pointer passed to drm_lib_init + * + * drm_state_cb_t — called when DRM state transitions. + * state: one of "STARTING", "LOGIN", "WAITING_2FA", "RUNNING", + * "INITIALIZING_FAIRPLAY", "FAILED" + */ +typedef void (*drm_auth_cb_t)(const char *type, char *out_buf, int out_size, + void *userdata); +typedef void (*drm_state_cb_t)(const char *state, void *userdata); + +/* ── Init config ─────────────────────────────────────────────────────────────*/ +typedef struct { + const char *base_dir; /* path containing mpl_db/ (e.g. drm/rootfs/…) */ + const char *lib64_dir; /* path to system/lib64/ with Android .so files */ + const char *username; /* NULL → session-reuse; set for fresh login */ + const char *password; /* NULL → session-reuse; set for fresh login */ + const char *device_info; /* 9-field slash-separated device string, or NULL */ + int offline_only; /* 1 = force download method; 0 = play method */ + drm_auth_cb_t auth_cb; /* called for credential/2FA challenges */ + void *auth_ud; + drm_state_cb_t state_cb; /* called on state transitions */ + void *state_ud; +} drm_lib_config_t; + +/* ── Lifecycle ───────────────────────────────────────────────────────────────*/ + +/* drm_lib_init — run the full DRM initialisation sequence and return. + * Blocks until the FairPlay lease is acquired and account tokens are cached. + * Returns 0 on success, -1 on failure (check stderr for details). + * Must be called exactly once before any other drm_lib_* function. */ +int drm_lib_init(const drm_lib_config_t *cfg); + +/* drm_lib_shutdown — stop background threads and free global state. + * After this returns, no further calls to drm_lib_* are valid. */ +void drm_lib_shutdown(void); + +/* ── DRM operations ──────────────────────────────────────────────────────────*/ + +/* drm_lib_get_m3u8 — return the HLS m3u8 URL for adamId. + * Returns a malloc'd string the caller must free(), or NULL on error. */ +char *drm_lib_get_m3u8(unsigned long adam_id); + +/* drm_lib_get_account — return {"storefront_id":…,"dev_token":…,"music_token":…}. + * Returns a malloc'd JSON string the caller must free(), or NULL on error. */ +char *drm_lib_get_account(void); + +/* drm_lib_get_mv — progressive MV URL + download key for adamId. + * out_url, out_dk: set to malloc'd strings; caller must free both. + * out_has_itun: set to 1 if an itun decryptor is ready for this adamId. + * Returns 0 on success, -1 on error. */ +int drm_lib_get_mv(unsigned long adam_id, char **out_url, char **out_dk, + int *out_has_itun); + +/* drm_lib_open_kd_ctx — acquire a FairPlay key-delivery context for + * (adam_id, uri). The context is internally cached; the returned pointer + * is valid until drm_lib_shutdown(). Returns NULL on failure. */ +void *drm_lib_open_kd_ctx(const char *adam_id, const char *uri); + +/* drm_lib_decrypt — decrypt one FP-encrypted sample in-place. + * kd_ctx: value returned by drm_lib_open_kd_ctx. + * Returns 0 on success. */ +int drm_lib_decrypt(void *kd_ctx, uint8_t *sample, uint32_t size); + +/* drm_lib_decrypt_itun — decrypt one itun-encrypted sample in-place. + * Must call drm_lib_get_mv first (creates the itun decryptor). + * out_size: number of output bytes (may differ from in_size). + * Returns 0 on success, -1 on error. */ +int drm_lib_decrypt_itun(unsigned long adam_id, uint8_t *sample, + uint32_t in_size, uint32_t *out_size); + +/* drm_lib_is_recovery_active — 1 if lease recovery is in progress. */ +int drm_lib_is_recovery_active(void); + +#ifdef __cplusplus +} +#endif diff --git a/hybris_ctor.c b/hybris_ctor.c new file mode 100644 index 0000000..d050de2 --- /dev/null +++ b/hybris_ctor.c @@ -0,0 +1,28 @@ +/* + * hybris_ctor.c — automatic pre-main init for the host-native DRM wrapper. + * + * The constructor runs before main() and loads the Android libs via hybris. + * It reads the lib64 path from HYBRIS_ANDROID_LIB64, which the Go engine + * must set before exec()'ing the wrapper process. + * + * Build with: gcc -c hybris_ctor.c -o hybris_ctor.o + * Link into the final binary alongside main.o and hybris_stubs.o. + */ + +#include +#include + +extern int hybris_init_libs(const char *lib64_path); + +__attribute__((constructor)) +static void hybris_auto_init(void) { + const char *lib64 = getenv("HYBRIS_ANDROID_LIB64"); + if (!lib64) { + /* Normal in library mode: drm_lib_init() loads the libs later. */ + fprintf(stderr, "[hybris] HYBRIS_ANDROID_LIB64 not set — deferring Android lib load\n"); + return; + } + /* Standalone drm-native binary: nothing can work without the libs. */ + if (hybris_init_libs(lib64) != 0) + exit(1); +} diff --git a/hybris_stubs.c b/hybris_stubs.c new file mode 100644 index 0000000..662ad6c --- /dev/null +++ b/hybris_stubs.c @@ -0,0 +1,797 @@ +/* + * hybris_stubs.c — provides all symbols declared in import.h via android_dlsym. + * + * Compiled into the host-native wrapper (glibc x86-64). + * hybris_init_libs() must be called once before any stub is used. + */ + +#include +#include +#include +#include +#include +#include "hybris_types.h" + +/* hybris public API (from libhybris-core.so) */ +extern void *android_dlopen(const char *filename, int flag); +extern void *android_dlsym(void *handle, const char *symbol); +extern const char *android_dlerror(void); + +#define RTLD_NOW_GLOBAL 0x102 + +static void *h_ssc = NULL; /* libstoreservicescore.so */ +static void *h_apm = NULL; /* libandroidappmusic.so */ + +/* ── vtable data arrays ────────────────────────────────────────────────────── + * import.h declares these as "extern void *_ZTV...;" (single pointer). + * main.c uses them as "&_ZTV... + 2" — taking the address of the symbol and + * offsetting by 2 void* widths to reach vtable slot [2] (first virtual func). + * + * In the normal Android dlopen build the linker resolves the extern reference + * so that &_ZTV... == the vtable address in the .so. In the hybris build we + * cannot alias Android memory, so we define each symbol as an 8-element array + * and memcpy the vtable content from Android memory into it. Then: + * &_ZTV... (from main.c's extern void* view) == &array[0] + * &_ZTV... + 2 == &array[2] == vtable slot [2] ✓ + * ──────────────────────────────────────────────────────────────────────────── */ +void *_ZTVNSt6__ndk120__shared_ptr_emplaceIN17storeservicescore22ProtocolDialogResponseENS_9allocatorIS2_EEEE[8]; +void *_ZTVNSt6__ndk120__shared_ptr_emplaceIN17storeservicescore19CredentialsResponseENS_9allocatorIS2_EEEE[8]; +void *_ZTVNSt6__ndk120__shared_ptr_emplaceIN17storeservicescore20RequestContextConfigENS_9allocatorIS2_EEEE[8]; +void *_ZTVNSt6__ndk120__shared_ptr_emplaceIN13mediaplatform11HTTPMessageENS_9allocatorIS2_EEEE[8]; + +/* ── symbol resolver ─────────────────────────────────────────────────────── */ +static void *sym(const char *name) { + void *p = android_dlsym(h_ssc, name); + if (!p) p = android_dlsym(h_apm, name); + if (!p) { fprintf(stderr, "[hybris] FATAL: %s\n", name); abort(); } + return p; +} + +/* ── init: load Android libs, patch vtables ─────────────────────────────── */ +/* Returns 0 on success, -1 if an Android lib cannot be loaded. It must not + * exit: in library mode (libdrm-native.so inside the Go engine) that would + * take the whole engine down instead of just disabling DRM. */ +int hybris_init_libs(const char *lib64) { + char path[512]; + + /* pre-load implicit system libs so verneed checks pass */ + static const char *preload[] = { "libdl.so", "libc.so", "libm.so", NULL }; + for (int i = 0; preload[i]; i++) { + snprintf(path, sizeof(path), "%s/%s", lib64, preload[i]); + android_dlopen(path, RTLD_NOW_GLOBAL); + } + + snprintf(path, sizeof(path), "%s/libstoreservicescore.so", lib64); + h_ssc = android_dlopen(path, RTLD_NOW_GLOBAL); + if (!h_ssc) { fprintf(stderr, "[hybris] cannot load libstoreservicescore.so: %s\n", android_dlerror()); return -1; } + + snprintf(path, sizeof(path), "%s/libandroidappmusic.so", lib64); + h_apm = android_dlopen(path, RTLD_NOW_GLOBAL); + if (!h_apm) { fprintf(stderr, "[hybris] cannot load libandroidappmusic.so: %s\n", android_dlerror()); return -1; } + + /* copy vtable content into our local arrays so &_ZTV... + 2 == vtable[2] */ +#define PATCHV(arr, sym_name) do { \ + void *p = android_dlsym(h_ssc, sym_name); \ + if (!p) p = android_dlsym(h_apm, sym_name); \ + if (p) memcpy(arr, p, 8 * sizeof(void*)); \ + else fprintf(stderr, "[hybris] warning: vtable %s not found\n", sym_name); \ +} while(0) + + PATCHV(_ZTVNSt6__ndk120__shared_ptr_emplaceIN17storeservicescore22ProtocolDialogResponseENS_9allocatorIS2_EEEE, + "_ZTVNSt6__ndk120__shared_ptr_emplaceIN17storeservicescore22ProtocolDialogResponseENS_9allocatorIS2_EEEE"); + PATCHV(_ZTVNSt6__ndk120__shared_ptr_emplaceIN17storeservicescore19CredentialsResponseENS_9allocatorIS2_EEEE, + "_ZTVNSt6__ndk120__shared_ptr_emplaceIN17storeservicescore19CredentialsResponseENS_9allocatorIS2_EEEE"); + PATCHV(_ZTVNSt6__ndk120__shared_ptr_emplaceIN17storeservicescore20RequestContextConfigENS_9allocatorIS2_EEEE, + "_ZTVNSt6__ndk120__shared_ptr_emplaceIN17storeservicescore20RequestContextConfigENS_9allocatorIS2_EEEE"); + PATCHV(_ZTVNSt6__ndk120__shared_ptr_emplaceIN13mediaplatform11HTTPMessageENS_9allocatorIS2_EEEE, + "_ZTVNSt6__ndk120__shared_ptr_emplaceIN13mediaplatform11HTTPMessageENS_9allocatorIS2_EEEE"); + + fprintf(stderr, "[hybris] libs loaded\n"); + return 0; +} + +/* ── special cases ───────────────────────────────────────────────────────── */ + +/* curl: provided by system libcurl, no stub needed */ + +/* Android log → stderr */ +int __android_log_print(int prio, const char *tag, const char *fmt, ...) { + (void)prio; + char buf[1024]; + va_list ap; va_start(ap, fmt); vsnprintf(buf, sizeof(buf), fmt, ap); va_end(ap); + fprintf(stderr, "[%s] %s\n", tag, buf); + return 0; +} +int __android_log_write(int prio, const char *tag, const char *text) { + (void)prio; fprintf(stderr, "[%s] %s\n", tag, text); return 0; +} + +/* resolv: no-op on glibc */ +void _resolv_set_nameservers_for_net(unsigned netid, const char **servers, + int numservers, const char *domains) { + (void)netid; (void)servers; (void)numservers; (void)domains; +} + +/* ── Bionic-compatible std::function callback objects ──────────────────────── + * + * SVPlaybackLeaseManagerC2 expects two Bionic std::__ndk1::function<> objects + * passed by const reference. Passing glibc std::function objects crashes + * because the copy constructor calls through Bionic's vtable which doesn't + * match glibc's internal layout. + * + * Bionic NDK r21 libc++ std::__ndk1::function uses the OLD field order: + * __f_ comes FIRST, __buf_ comes SECOND (opposite of newer LLVM libc++): + * + * std::function (32 bytes): + * [0..7]: __f_ = &__buf_[0] when SBO (= self + 8); NULL = empty + * [8..15]: __buf_[0] = vptr of __func (= &vtab[2]) + * [16..23]: __buf_[1] = fn pointer (the stored callable) + * [24..31]: __buf_[2] = allocator state (zeroed for stateless) + * + * SBO path: Bionic checks (void*)__f_ == &__buf_[0], i.e. self[0] == self+8. + * If true it calls __f_->__clone(__base* dst) to copy __func into dest's __buf_. + * + * The vtable layout for __base (Bionic NDK r21, vptr → vtab[2]): + * vptr[0] = vtab[2] ~__base() regular destructor + * vptr[1] = vtab[3] ~__base() deleting destructor + * vptr[2] = vtab[4] __clone() const → heap copy + * vptr[3] = vtab[5] __clone(__base*) const → SBO placement copy + * vptr[4] = vtab[6] __destroy() → SBO destroy (no free) + * vptr[5] = vtab[7] __destroy_and_delete() → destroy + heap free + * vptr[6] = vtab[8] operator()(Args&&...) → invoke + * + * Virtual functions receive `self = &__buf_[0]` (the __func object), so: + * self[0] = vptr, self[1] = fn ptr, self[2] = allocator. + * ──────────────────────────────────────────────────────────────────────────── */ + +/* callbacks exported from main.cpp */ +extern void endLeaseCbExport(const int *code_ptr); +extern void pbErrCbExport(void *arg); + +/* ── endLeaseCallback: std::function ─────────────────────*/ + +static void vf_el_destroy(void *self) { (void)self; } +static void vf_el_destroy_and_delete(void *self) { free(self); } +static void *vf_el_clone_heap(void *self) { + void *copy = malloc(3 * sizeof(void *)); + memcpy(copy, self, 3 * sizeof(void *)); + return copy; +} +static void vf_el_clone_sbo(void *self, void *dst) { + memcpy(dst, self, 3 * sizeof(void *)); +} +static void vf_el_invoke(void *self, const int *code) { + void (*fn)(const int *) = (void (*)(const int *))((void **)self)[1]; + fn(code); +} + +static void *vtab_endlease[9] = { + (void *)0, /* [0] offset-to-top */ + (void *)0, /* [1] RTTI */ + (void *)vf_el_destroy, /* [2] ~__base() regular */ + (void *)vf_el_destroy_and_delete, /* [3] ~__base() deleting */ + (void *)vf_el_clone_heap, /* [4] __clone() → heap */ + (void *)vf_el_clone_sbo, /* [5] __clone(dst) → SBO */ + (void *)vf_el_destroy, /* [6] __destroy() */ + (void *)vf_el_destroy_and_delete, /* [7] __destroy_and_delete() */ + (void *)vf_el_invoke, /* [8] operator()(const int&) */ +}; + +/* 32-byte buffer; set up by hybris_init_callbacks() */ +uint8_t endLeaseCallback[32]; + +/* ── pbErrCallback: std::function&)> ─*/ + +static void vf_pe_destroy(void *self) { (void)self; } +static void vf_pe_destroy_and_delete(void *self) { free(self); } +static void *vf_pe_clone_heap(void *self) { + void *copy = malloc(3 * sizeof(void *)); + memcpy(copy, self, 3 * sizeof(void *)); + return copy; +} +static void vf_pe_clone_sbo(void *self, void *dst) { + memcpy(dst, self, 3 * sizeof(void *)); +} +static void vf_pe_invoke(void *self, void *arg) { + void (*fn)(void *) = (void (*)(void *))((void **)self)[1]; + fn(arg); +} + +static void *vtab_pberr[9] = { + (void *)0, + (void *)0, + (void *)vf_pe_destroy, + (void *)vf_pe_destroy_and_delete, + (void *)vf_pe_clone_heap, + (void *)vf_pe_clone_sbo, + (void *)vf_pe_destroy, + (void *)vf_pe_destroy_and_delete, + (void *)vf_pe_invoke, +}; + +uint8_t pbErrCallback[32]; + +/* Call once from main() after init(), before SVPlaybackLeaseManagerC2. + * + * Bionic NDK r21 libc++ std::function (32 bytes) OLD layout: + * [0..7]: __f_ = &__buf_[0] = self+8 (SBO path: __f_ points into __buf_) + * [8..15]: __buf_[0] = vptr of __func (= &vtab[2]) + * [16..23]: __buf_[1] = fn pointer + * [24..31]: __buf_[2] = allocator (zeroed for stateless) + * + * SBO check in Bionic: (void*)__f_ == &__buf_[0] → self[0] == self+8 + * Invoke: self->__f_->operator()(args) + * = obj at (self+8), vptr=*(self+8)=&vtab[2], call vptr[6] + */ +void hybris_init_callbacks(void) { + uint8_t *el = endLeaseCallback; + ((void **)el)[0] = el + 8; /* __f_ = &__buf_[0] */ + ((void **)el)[1] = &vtab_endlease[2]; /* __func vptr */ + ((void **)el)[2] = (void *)endLeaseCbExport; /* fn ptr */ + ((void **)el)[3] = NULL; /* allocator (zeroed) */ + + uint8_t *pe = pbErrCallback; + ((void **)pe)[0] = pe + 8; + ((void **)pe)[1] = &vtab_pberr[2]; + ((void **)pe)[2] = (void *)pbErrCbExport; + ((void **)pe)[3] = NULL; +} + +/* ── function stubs ──────────────────────────────────────────────────────── */ + +void _ZN20androidstoreservices30SVSubscriptionStatusMgrFactory6createEv(struct shared_ptr *out) { + static void (*fn)(struct shared_ptr*) = NULL; + if (!fn) fn = sym("_ZN20androidstoreservices30SVSubscriptionStatusMgrFactory6createEv"); + fn(out); +} + +void _ZN20androidstoreservices27SVSubscriptionStatusMgrImpl33checkSubscriptionStatusFromSourceERKNSt6__ndk110shared_ptrIN17storeservicescore14RequestContextEEERKNS_23SVSubscriptionStatusMgr26SVSubscriptionStatusSourceE( + struct shared_ptr *a, void *b, struct shared_ptr *c, int *d) { + static void (*fn)(struct shared_ptr*,void*,struct shared_ptr*,int*) = NULL; + if (!fn) fn = sym("_ZN20androidstoreservices27SVSubscriptionStatusMgrImpl33checkSubscriptionStatusFromSourceERKNSt6__ndk110shared_ptrIN17storeservicescore14RequestContextEEERKNS_23SVSubscriptionStatusMgr26SVSubscriptionStatusSourceE"); + fn(a,b,c,d); +} + +void _ZN17storeservicescore14RequestContext24setFairPlayDirectoryPathERKNSt6__ndk112basic_stringIcNS1_11char_traitsIcEENS1_9allocatorIcEEEE(void *obj, union std_string *path) { + static void (*fn)(void*,union std_string*) = NULL; + if (!fn) fn = sym("_ZN17storeservicescore14RequestContext24setFairPlayDirectoryPathERKNSt6__ndk112basic_stringIcNS1_11char_traitsIcEENS1_9allocatorIcEEEE"); + fn(obj,path); +} + +void _ZN14FootHillConfig6configERKNSt6__ndk112basic_stringIcNS0_11char_traitsIcEENS0_9allocatorIcEEEE(union std_string *cfg) { + static void (*fn)(union std_string*) = NULL; + if (!fn) fn = sym("_ZN14FootHillConfig6configERKNSt6__ndk112basic_stringIcNS0_11char_traitsIcEENS0_9allocatorIcEEEE"); + fn(cfg); +} + +void _ZNSt6__ndk110shared_ptrIN17storeservicescore14RequestContextEE11make_sharedIJRNS_12basic_stringIcNS_11char_traitsIcEENS_9allocatorIcEEEEEEES3_DpOT_( + struct shared_ptr *out, union std_string *str) { + static void (*fn)(struct shared_ptr*,union std_string*) = NULL; + if (!fn) fn = sym("_ZNSt6__ndk110shared_ptrIN17storeservicescore14RequestContextEE11make_sharedIJRNS_12basic_stringIcNS_11char_traitsIcEENS_9allocatorIcEEEEEEES3_DpOT_"); + fn(out,str); +} + +void _ZNSt6__ndk110shared_ptrIN20androidstoreservices28AndroidPresentationInterfaceEE11make_sharedIJEEES3_DpOT_(struct shared_ptr *out) { + static void (*fn)(struct shared_ptr*) = NULL; + if (!fn) fn = sym("_ZNSt6__ndk110shared_ptrIN20androidstoreservices28AndroidPresentationInterfaceEE11make_sharedIJEEES3_DpOT_"); + fn(out); +} + +void _ZN20androidstoreservices28AndroidPresentationInterface16setDialogHandlerEPFvlNSt6__ndk110shared_ptrIN17storeservicescore14ProtocolDialogEEENS2_INS_36AndroidProtocolDialogResponseHandlerEEEE( + void *obj, void (*handler)(long, struct shared_ptr *, struct shared_ptr *)) { + static void (*fn)(void*,void*) = NULL; + if (!fn) fn = sym("_ZN20androidstoreservices28AndroidPresentationInterface16setDialogHandlerEPFvlNSt6__ndk110shared_ptrIN17storeservicescore14ProtocolDialogEEENS2_INS_36AndroidProtocolDialogResponseHandlerEEEE"); + fn(obj,(void*)handler); +} + +void _ZN20androidstoreservices28AndroidPresentationInterface21setCredentialsHandlerEPFvNSt6__ndk110shared_ptrIN17storeservicescore18CredentialsRequestEEENS2_INS_33AndroidCredentialsResponseHandlerEEEE( + void *obj, void (*handler)(struct shared_ptr *, struct shared_ptr *)) { + static void (*fn)(void*,void*) = NULL; + if (!fn) fn = sym("_ZN20androidstoreservices28AndroidPresentationInterface21setCredentialsHandlerEPFvNSt6__ndk110shared_ptrIN17storeservicescore18CredentialsRequestEEENS2_INS_33AndroidCredentialsResponseHandlerEEEE"); + fn(obj,(void*)handler); +} + +void _ZN17storeservicescore14RequestContext24setPresentationInterfaceERKNSt6__ndk110shared_ptrINS_21PresentationInterfaceEEE( + void *obj, struct shared_ptr *iface) { + static void (*fn)(void*,struct shared_ptr*) = NULL; + if (!fn) fn = sym("_ZN17storeservicescore14RequestContext24setPresentationInterfaceERKNSt6__ndk110shared_ptrINS_21PresentationInterfaceEEE"); + fn(obj,iface); +} + +void _ZNSt6__ndk110shared_ptrIN17storeservicescore16AuthenticateFlowEE11make_sharedIJRNS0_INS1_14RequestContextEEEEEES3_DpOT_( + struct shared_ptr *out, struct shared_ptr *ctx) { + static void (*fn)(struct shared_ptr*,struct shared_ptr*) = NULL; + if (!fn) fn = sym("_ZNSt6__ndk110shared_ptrIN17storeservicescore16AuthenticateFlowEE11make_sharedIJRNS0_INS1_14RequestContextEEEEEES3_DpOT_"); + fn(out,ctx); +} + +void _ZN17storeservicescore16AuthenticateFlow3runEv(void *obj) { + static void (*fn)(void*) = NULL; + if (!fn) fn = sym("_ZN17storeservicescore16AuthenticateFlow3runEv"); + fn(obj); +} + +struct shared_ptr *_ZNK17storeservicescore16AuthenticateFlow8responseEv(void *obj) { + static struct shared_ptr *(*fn)(void*) = NULL; + if (!fn) fn = sym("_ZNK17storeservicescore16AuthenticateFlow8responseEv"); + return fn(obj); +} + +int _ZNK17storeservicescore20AuthenticateResponse12responseTypeEv(void *obj) { + static int (*fn)(void*) = NULL; + if (!fn) fn = sym("_ZNK17storeservicescore20AuthenticateResponse12responseTypeEv"); + return fn(obj); +} + +void _ZN22SVPlaybackLeaseManagerC2ERKNSt6__ndk18functionIFvRKiEEERKNS1_IFvRKNS0_10shared_ptrIN17storeservicescore19StoreErrorConditionEEEEEE( + void *obj, void *endcb, void *errcb) { + static void (*fn)(void*,void*,void*) = NULL; + if (!fn) fn = sym("_ZN22SVPlaybackLeaseManagerC2ERKNSt6__ndk18functionIFvRKiEEERKNS1_IFvRKNS0_10shared_ptrIN17storeservicescore19StoreErrorConditionEEEEEE"); + fn(obj,endcb,errcb); +} + +void _ZN22SVPlaybackLeaseManager25refreshLeaseAutomaticallyERKb(void *obj, uint8_t *flag) { + static void (*fn)(void*,uint8_t*) = NULL; + if (!fn) fn = sym("_ZN22SVPlaybackLeaseManager25refreshLeaseAutomaticallyERKb"); + fn(obj,flag); +} + +void _ZN22SVPlaybackLeaseManager12requestLeaseERKb(void *obj, uint8_t *flag) { + static void (*fn)(void*,uint8_t*) = NULL; + if (!fn) fn = sym("_ZN22SVPlaybackLeaseManager12requestLeaseERKb"); + fn(obj,flag); +} + +/* zero-arg functions */ +void *_ZN21SVFootHillSessionCtrl8instanceEv() { + static void *(*fn)(void) = NULL; + if (!fn) fn = sym("_ZN21SVFootHillSessionCtrl8instanceEv"); + return fn(); +} +void *_ZN21SVFootHillSessionCtrl7destroyEv() { + static void *(*fn)(void) = NULL; + if (!fn) fn = sym("_ZN21SVFootHillSessionCtrl7destroyEv"); + return fn(); +} + +void _ZN21SVFootHillSessionCtrl9cleanKeysERKNSt6__ndk112basic_stringIcNS0_11char_traitsIcEENS0_9allocatorIcEEEE( + void *obj, union std_string *key) { + static void (*fn)(void*,union std_string*) = NULL; + if (!fn) fn = sym("_ZN21SVFootHillSessionCtrl9cleanKeysERKNSt6__ndk112basic_stringIcNS0_11char_traitsIcEENS0_9allocatorIcEEEE"); + fn(obj,key); +} + +void _ZN21SVFootHillSessionCtrl16getPersistentKeyERKNSt6__ndk112basic_stringIcNS0_11char_traitsIcEENS0_9allocatorIcEEEES8_S8_S8_S8_S8_S8_S8_( + struct shared_ptr *ret, void *obj, + union std_string *a, union std_string *b, union std_string *c, union std_string *d, + union std_string *e, union std_string *f, union std_string *g, union std_string *h) { + static void (*fn)(struct shared_ptr*,void*, + union std_string*,union std_string*,union std_string*,union std_string*, + union std_string*,union std_string*,union std_string*,union std_string*) = NULL; + if (!fn) fn = sym("_ZN21SVFootHillSessionCtrl16getPersistentKeyERKNSt6__ndk112basic_stringIcNS0_11char_traitsIcEENS0_9allocatorIcEEEES8_S8_S8_S8_S8_S8_S8_"); + fn(ret,obj,a,b,c,d,e,f,g,h); +} + +void _ZN21SVFootHillSessionCtrl14decryptContextERKNSt6__ndk112basic_stringIcNS0_11char_traitsIcEENS0_9allocatorIcEEEERKN11SVDecryptor15SVDecryptorTypeERKb( + struct shared_ptr *ret, void *obj, union std_string *ckc) { + static void (*fn)(struct shared_ptr*,void*,union std_string*) = NULL; + if (!fn) fn = sym("_ZN21SVFootHillSessionCtrl14decryptContextERKNSt6__ndk112basic_stringIcNS0_11char_traitsIcEENS0_9allocatorIcEEEERKN11SVDecryptor15SVDecryptorTypeERKb"); + fn(ret,obj,ckc); +} + +void _ZNSt6__ndk110shared_ptrI18SVFootHillPContextED2Ev(struct shared_ptr *sp) { + static void (*fn)(struct shared_ptr*) = NULL; + if (!fn) fn = sym("_ZNSt6__ndk110shared_ptrI18SVFootHillPContextED2Ev"); + fn(sp); +} + +void **_ZNK18SVFootHillPContext9kdContextEv(void *obj) { + static void **(*fn)(void*) = NULL; + if (!fn) fn = sym("_ZNK18SVFootHillPContext9kdContextEv"); + return fn(obj); +} + +long NfcRKVnxuKZy04KWbdFu71Ou(void *a, uint32_t b, void *c, void *d, size_t e) { + static long (*fn)(void*,uint32_t,void*,void*,size_t) = NULL; + if (!fn) fn = sym("NfcRKVnxuKZy04KWbdFu71Ou"); + return fn(a,b,c,d,e); +} + +void _ZN17storeservicescore22ProtocolDialogResponse17setSelectedButtonERKNSt6__ndk110shared_ptrINS_14ProtocolButtonEEE( + void *obj, struct shared_ptr *btn) { + static void (*fn)(void*,struct shared_ptr*) = NULL; + if (!fn) fn = sym("_ZN17storeservicescore22ProtocolDialogResponse17setSelectedButtonERKNSt6__ndk110shared_ptrINS_14ProtocolButtonEEE"); + fn(obj,btn); +} + +union std_string *_ZNK17storeservicescore14ProtocolDialog5titleEv(void *obj) { + static union std_string *(*fn)(void*) = NULL; + if (!fn) fn = sym("_ZNK17storeservicescore14ProtocolDialog5titleEv"); + return fn(obj); +} +union std_string *_ZNK17storeservicescore14ProtocolDialog7messageEv(void *obj) { + static union std_string *(*fn)(void*) = NULL; + if (!fn) fn = sym("_ZNK17storeservicescore14ProtocolDialog7messageEv"); + return fn(obj); +} +union std_string *_ZNK17storeservicescore18CredentialsRequest5titleEv(void *obj) { + static union std_string *(*fn)(void*) = NULL; + if (!fn) fn = sym("_ZNK17storeservicescore18CredentialsRequest5titleEv"); + return fn(obj); +} +union std_string *_ZNK17storeservicescore18CredentialsRequest7messageEv(void *obj) { + static union std_string *(*fn)(void*) = NULL; + if (!fn) fn = sym("_ZNK17storeservicescore18CredentialsRequest7messageEv"); + return fn(obj); +} +uint8_t _ZNK17storeservicescore18CredentialsRequest28requiresHSA2VerificationCodeEv(void *obj) { + static uint8_t (*fn)(void*) = NULL; + if (!fn) fn = sym("_ZNK17storeservicescore18CredentialsRequest28requiresHSA2VerificationCodeEv"); + return fn(obj); +} + +void _ZN20androidstoreservices28AndroidPresentationInterface28handleProtocolDialogResponseERKlRKNSt6__ndk110shared_ptrIN17storeservicescore22ProtocolDialogResponseEEE( + void *obj, long *j, struct shared_ptr *resp) { + static void (*fn)(void*,long*,struct shared_ptr*) = NULL; + if (!fn) fn = sym("_ZN20androidstoreservices28AndroidPresentationInterface28handleProtocolDialogResponseERKlRKNSt6__ndk110shared_ptrIN17storeservicescore22ProtocolDialogResponseEEE"); + fn(obj,j,resp); +} + +void _ZN20androidstoreservices28AndroidPresentationInterface25handleCredentialsResponseERKNSt6__ndk110shared_ptrIN17storeservicescore19CredentialsResponseEEE( + void *obj, struct shared_ptr *resp) { + static void (*fn)(void*,struct shared_ptr*) = NULL; + if (!fn) fn = sym("_ZN20androidstoreservices28AndroidPresentationInterface25handleCredentialsResponseERKNSt6__ndk110shared_ptrIN17storeservicescore19CredentialsResponseEEE"); + fn(obj,resp); +} + +void _ZN17storeservicescore22ProtocolDialogResponseC1Ev(void *obj) { + static void (*fn)(void*) = NULL; + if (!fn) fn = sym("_ZN17storeservicescore22ProtocolDialogResponseC1Ev"); + fn(obj); +} +void _ZN17storeservicescore19CredentialsResponseC1Ev(void *obj) { + static void (*fn)(void*) = NULL; + if (!fn) fn = sym("_ZN17storeservicescore19CredentialsResponseC1Ev"); + fn(obj); +} +void _ZN17storeservicescore19CredentialsResponse11setUserNameERKNSt6__ndk112basic_stringIcNS1_11char_traitsIcEENS1_9allocatorIcEEEE(void *obj, union std_string *s) { + static void (*fn)(void*,union std_string*) = NULL; + if (!fn) fn = sym("_ZN17storeservicescore19CredentialsResponse11setUserNameERKNSt6__ndk112basic_stringIcNS1_11char_traitsIcEENS1_9allocatorIcEEEE"); + fn(obj,s); +} +void _ZN17storeservicescore19CredentialsResponse11setPasswordERKNSt6__ndk112basic_stringIcNS1_11char_traitsIcEENS1_9allocatorIcEEEE(void *obj, union std_string *s) { + static void (*fn)(void*,union std_string*) = NULL; + if (!fn) fn = sym("_ZN17storeservicescore19CredentialsResponse11setPasswordERKNSt6__ndk112basic_stringIcNS1_11char_traitsIcEENS1_9allocatorIcEEEE"); + fn(obj,s); +} +void _ZN17storeservicescore19CredentialsResponse15setResponseTypeENS0_12ResponseTypeE(void *obj, int t) { + static void (*fn)(void*,int) = NULL; + if (!fn) fn = sym("_ZN17storeservicescore19CredentialsResponse15setResponseTypeENS0_12ResponseTypeE"); + fn(obj,t); +} + +struct std_vector *_ZNK17storeservicescore14ProtocolDialog7buttonsEv(void *obj) { + static struct std_vector *(*fn)(void*) = NULL; + if (!fn) fn = sym("_ZNK17storeservicescore14ProtocolDialog7buttonsEv"); + return fn(obj); +} +union std_string *_ZNK17storeservicescore14ProtocolButton5titleEv(void *obj) { + static union std_string *(*fn)(void*) = NULL; + if (!fn) fn = sym("_ZNK17storeservicescore14ProtocolButton5titleEv"); + return fn(obj); +} + +void _ZN17storeservicescore10DeviceGUID8instanceEv(struct shared_ptr *out) { + static void (*fn)(struct shared_ptr*) = NULL; + if (!fn) fn = sym("_ZN17storeservicescore10DeviceGUID8instanceEv"); + fn(out); +} + +void _ZN17storeservicescore10DeviceGUID9configureERKNSt6__ndk112basic_stringIcNS1_11char_traitsIcEENS1_9allocatorIcEEEES9_RKjRKb( + void *ret, void *obj, union std_string *id1, union std_string *id2, + unsigned int *api, uint8_t *flag) { + static void (*fn)(void*,void*,union std_string*,union std_string*,unsigned int*,uint8_t*) = NULL; + if (!fn) fn = sym("_ZN17storeservicescore10DeviceGUID9configureERKNSt6__ndk112basic_stringIcNS1_11char_traitsIcEENS1_9allocatorIcEEEES9_RKjRKb"); + fn(ret,obj,id1,id2,api,flag); +} + +uint8_t _ZN13mediaplatform26DebugLogEnabledForPriorityENS_11LogPriorityE(void) { + /* always return 0 (disabled) — avoids spamming logs */ + return 0; +} + +void _ZN17storeservicescore20RequestContextConfigC2Ev(void *obj) { + static void (*fn)(void*) = NULL; + if (!fn) fn = sym("_ZN17storeservicescore20RequestContextConfigC2Ev"); + fn(obj); +} +void _ZN17storeservicescore20RequestContextConfig9setCPFlagEb(void *obj, uint8_t flag) { + static void (*fn)(void*,uint8_t) = NULL; + if (!fn) fn = sym("_ZN17storeservicescore20RequestContextConfig9setCPFlagEb"); + fn(obj,flag); +} + +#define CFG_SETTER(name) \ +void name(void *obj, union std_string *s) { \ + static void (*fn)(void*,union std_string*) = NULL; \ + if (!fn) fn = sym(#name); \ + fn(obj,s); \ +} +CFG_SETTER(_ZN17storeservicescore20RequestContextConfig20setBaseDirectoryPathERKNSt6__ndk112basic_stringIcNS1_11char_traitsIcEENS1_9allocatorIcEEEE) +CFG_SETTER(_ZN17storeservicescore20RequestContextConfig19setClientIdentifierERKNSt6__ndk112basic_stringIcNS1_11char_traitsIcEENS1_9allocatorIcEEEE) +CFG_SETTER(_ZN17storeservicescore20RequestContextConfig20setVersionIdentifierERKNSt6__ndk112basic_stringIcNS1_11char_traitsIcEENS1_9allocatorIcEEEE) +CFG_SETTER(_ZN17storeservicescore20RequestContextConfig21setPlatformIdentifierERKNSt6__ndk112basic_stringIcNS1_11char_traitsIcEENS1_9allocatorIcEEEE) +CFG_SETTER(_ZN17storeservicescore20RequestContextConfig17setProductVersionERKNSt6__ndk112basic_stringIcNS1_11char_traitsIcEENS1_9allocatorIcEEEE) +CFG_SETTER(_ZN17storeservicescore20RequestContextConfig14setDeviceModelERKNSt6__ndk112basic_stringIcNS1_11char_traitsIcEENS1_9allocatorIcEEEE) +CFG_SETTER(_ZN17storeservicescore20RequestContextConfig15setBuildVersionERKNSt6__ndk112basic_stringIcNS1_11char_traitsIcEENS1_9allocatorIcEEEE) +CFG_SETTER(_ZN17storeservicescore20RequestContextConfig19setLocaleIdentifierERKNSt6__ndk112basic_stringIcNS1_11char_traitsIcEENS1_9allocatorIcEEEE) +CFG_SETTER(_ZN17storeservicescore20RequestContextConfig21setLanguageIdentifierERKNSt6__ndk112basic_stringIcNS1_11char_traitsIcEENS1_9allocatorIcEEEE) +CFG_SETTER(_ZN17storeservicescore20RequestContextConfig24setFairPlayDirectoryPathERKNSt6__ndk112basic_stringIcNS1_11char_traitsIcEENS1_9allocatorIcEEEE) +#undef CFG_SETTER + +void _ZN17storeservicescore14RequestContext4initERKNSt6__ndk110shared_ptrINS_20RequestContextConfigEEE( + void *obj, void *unused, struct shared_ptr *cfg) { + static void (*fn)(void*,void*,struct shared_ptr*) = NULL; + if (!fn) fn = sym("_ZN17storeservicescore14RequestContext4initERKNSt6__ndk110shared_ptrINS_20RequestContextConfigEEE"); + fn(obj,unused,cfg); +} + +void _ZN21RequestContextManager9configureERKNSt6__ndk110shared_ptrIN17storeservicescore14RequestContextEEE(struct shared_ptr *ctx) { + static void (*fn)(struct shared_ptr*) = NULL; + if (!fn) fn = sym("_ZN21RequestContextManager9configureERKNSt6__ndk110shared_ptrIN17storeservicescore14RequestContextEEE"); + fn(ctx); +} + +struct shared_ptr *_ZN22SVPlaybackLeaseManager12requestAssetERKmRKNSt6__ndk16vectorINS2_12basic_stringIcNS2_11char_traitsIcEENS2_9allocatorIcEEEENS7_IS9_EEEERKb( + void *obj, void *unused, unsigned long *adamId, struct std_vector *flavors, uint8_t *offline) { + static struct shared_ptr *(*fn)(void*,void*,unsigned long*,struct std_vector*,uint8_t*) = NULL; + if (!fn) fn = sym("_ZN22SVPlaybackLeaseManager12requestAssetERKmRKNSt6__ndk16vectorINS2_12basic_stringIcNS2_11char_traitsIcEENS2_9allocatorIcEEEENS7_IS9_EEEERKb"); + return fn(obj,unused,adamId,flavors,offline); +} + +int _ZNK23SVPlaybackAssetResponse13hasValidAssetEv(void *obj) { + static int (*fn)(void*) = NULL; + if (!fn) fn = sym("_ZNK23SVPlaybackAssetResponse13hasValidAssetEv"); + return fn(obj); +} +struct shared_ptr *_ZNK23SVPlaybackAssetResponse13playbackAssetEv(void *obj) { + static struct shared_ptr *(*fn)(void*) = NULL; + if (!fn) fn = sym("_ZNK23SVPlaybackAssetResponse13playbackAssetEv"); + return fn(obj); +} +int _ZNK23SVPlaybackAssetResponse9errorCodeEv(void *obj) { + static int (*fn)(void*) = NULL; + if (!fn) fn = sym("_ZNK23SVPlaybackAssetResponse9errorCodeEv"); + return fn(obj); +} +union std_string *_ZNK23SVPlaybackAssetResponse12errorMessageEv(void *obj, void *out) { + static union std_string *(*fn)(void*,void*) = NULL; + if (!fn) fn = sym("_ZNK23SVPlaybackAssetResponse12errorMessageEv"); + return fn(obj,out); +} + +union std_string *_ZNK17storeservicescore13PlaybackAsset9URLStringEv(void *obj, uint8_t *out) { + static union std_string *(*fn)(void*,uint8_t*) = NULL; + if (!fn) fn = sym("_ZNK17storeservicescore13PlaybackAsset9URLStringEv"); + return fn(obj,out); +} +union std_string *_ZNK17storeservicescore13PlaybackAsset7flavorEv(void *obj, void *out) { + static union std_string *(*fn)(void*,void*) = NULL; + if (!fn) fn = sym("_ZNK17storeservicescore13PlaybackAsset7flavorEv"); + return fn(obj,out); +} +union std_string *_ZNK17storeservicescore13PlaybackAsset11downloadKeyEv(void *obj, void *out) { + static union std_string *(*fn)(void*,void*) = NULL; + if (!fn) fn = sym("_ZNK17storeservicescore13PlaybackAsset11downloadKeyEv"); + return fn(obj,out); +} + +union std_string *_ZNK17storeservicescore14RequestContext20storeFrontIdentifierERKNSt6__ndk110shared_ptrINS_6URLBagEEE( + void *obj, void *unused, struct shared_ptr *urlbag) { + static union std_string *(*fn)(void*,void*,struct shared_ptr*) = NULL; + if (!fn) fn = sym("_ZNK17storeservicescore14RequestContext20storeFrontIdentifierERKNSt6__ndk110shared_ptrINS_6URLBagEEE"); + return fn(obj,unused,urlbag); +} + +void _ZN21SVFootHillSessionCtrl16resetAllContextsEv(void *obj) { + static void (*fn)(void*) = NULL; + if (!fn) fn = sym("_ZN21SVFootHillSessionCtrl16resetAllContextsEv"); + fn(obj); +} + +void _ZN8FootHillC2ERKNSt6__ndk112basic_stringIcNS0_11char_traitsIcEENS0_9allocatorIcEEEES8_( + void *obj, union std_string *root, union std_string *lib) { + static void (*fn)(void*,union std_string*,union std_string*) = NULL; + if (!fn) fn = sym("_ZN8FootHillC2ERKNSt6__ndk112basic_stringIcNS0_11char_traitsIcEENS0_9allocatorIcEEEES8_"); + fn(obj,root,lib); +} +void _ZN8FootHill24defaultContextIdentifierEv(void *obj) { + static void (*fn)(void*) = NULL; + if (!fn) fn = sym("_ZN8FootHill24defaultContextIdentifierEv"); + fn(obj); +} + +/* HTTPMessageC2: (obj, url*, method*) */ +void *_ZN13mediaplatform11HTTPMessageC2ENSt6__ndk112basic_stringIcNS1_11char_traitsIcEENS1_9allocatorIcEEEES7_( + void *obj, union std_string *url, union std_string *method) { + static void *(*fn)(void*,union std_string*,union std_string*) = NULL; + if (!fn) fn = sym("_ZN13mediaplatform11HTTPMessageC2ENSt6__ndk112basic_stringIcNS1_11char_traitsIcEENS1_9allocatorIcEEEES7_"); + return fn(obj,url,method); +} + +void _ZN13mediaplatform11HTTPMessage9setHeaderERKNSt6__ndk112basic_stringIcNS1_11char_traitsIcEENS1_9allocatorIcEEEES9_( + void *obj, union std_string *name, union std_string *val) { + static void (*fn)(void*,union std_string*,union std_string*) = NULL; + if (!fn) fn = sym("_ZN13mediaplatform11HTTPMessage9setHeaderERKNSt6__ndk112basic_stringIcNS1_11char_traitsIcEENS1_9allocatorIcEEEES9_"); + fn(obj,name,val); +} + +void _ZN13mediaplatform11HTTPMessage11setBodyDataEPcm(void *obj, char *data, unsigned long len) { + static void (*fn)(void*,char*,unsigned long) = NULL; + if (!fn) fn = sym("_ZN13mediaplatform11HTTPMessage11setBodyDataEPcm"); + fn(obj,data,len); +} + +void *_ZN17storeservicescore10DeviceGUID4guidEv(void *obj, void *out) { + static void *(*fn)(void*,void*) = NULL; + if (!fn) fn = sym("_ZN17storeservicescore10DeviceGUID4guidEv"); + return fn(obj,out); +} + +char *_ZNK13mediaplatform4Data5bytesEv(void *data) { + static char *(*fn)(void*) = NULL; + if (!fn) fn = sym("_ZNK13mediaplatform4Data5bytesEv"); + return fn(data); +} + +size_t _ZNK13mediaplatform4Data6lengthEv(void *data) { + static size_t (*fn)(void*) = NULL; + if (!fn) fn = sym("_ZNK13mediaplatform4Data6lengthEv"); + return fn(data); +} + +void *_ZN17storeservicescore10URLRequestC2ERKNSt6__ndk110shared_ptrIN13mediaplatform11HTTPMessageEEERKNS2_INS_14RequestContextEEE( + void *obj, struct shared_ptr *msg, struct shared_ptr *ctx) { + static void *(*fn)(void*,struct shared_ptr*,struct shared_ptr*) = NULL; + if (!fn) fn = sym("_ZN17storeservicescore10URLRequestC2ERKNSt6__ndk110shared_ptrIN13mediaplatform11HTTPMessageEEERKNS2_INS_14RequestContextEEE"); + return fn(obj,msg,ctx); +} + +void *_ZN17storeservicescore10URLRequest19setRequestParameterERKNSt6__ndk112basic_stringIcNS1_11char_traitsIcEENS1_9allocatorIcEEEES9_( + void *obj, union std_string *key, union std_string *val) { + static void *(*fn)(void*,union std_string*,union std_string*) = NULL; + if (!fn) fn = sym("_ZN17storeservicescore10URLRequest19setRequestParameterERKNSt6__ndk112basic_stringIcNS1_11char_traitsIcEENS1_9allocatorIcEEEES9_"); + return fn(obj,key,val); +} + +void *_ZN17storeservicescore10URLRequest3runEv(void *obj) { + static void *(*fn)(void*) = NULL; + if (!fn) fn = sym("_ZN17storeservicescore10URLRequest3runEv"); + return fn(obj); +} +struct shared_ptr *_ZNK17storeservicescore10URLRequest5errorEv(void *obj) { + static struct shared_ptr *(*fn)(void*) = NULL; + if (!fn) fn = sym("_ZNK17storeservicescore10URLRequest5errorEv"); + return fn(obj); +} +struct shared_ptr *_ZNK17storeservicescore10URLRequest8responseEv(void *obj) { + static struct shared_ptr *(*fn)(void*) = NULL; + if (!fn) fn = sym("_ZNK17storeservicescore10URLRequest8responseEv"); + return fn(obj); +} +struct shared_ptr *_ZNK17storeservicescore11URLResponse18underlyingResponseEv(void *obj) { + static struct shared_ptr *(*fn)(void*) = NULL; + if (!fn) fn = sym("_ZNK17storeservicescore11URLResponse18underlyingResponseEv"); + return fn(obj); +} + +void *_ZN17storeservicescore15PurchaseRequestC2ERKNSt6__ndk110shared_ptrINS_14RequestContextEEE( + void *obj, struct shared_ptr *ctx) { + static void *(*fn)(void*,struct shared_ptr*) = NULL; + if (!fn) fn = sym("_ZN17storeservicescore15PurchaseRequestC2ERKNSt6__ndk110shared_ptrINS_14RequestContextEEE"); + return fn(obj,ctx); +} +void *_ZN17storeservicescore15PurchaseRequest23setProcessDialogActionsEb(void *obj, int flag) { + static void *(*fn)(void*,int) = NULL; + if (!fn) fn = sym("_ZN17storeservicescore15PurchaseRequest23setProcessDialogActionsEb"); + return fn(obj,flag); +} +void *_ZN17storeservicescore15PurchaseRequest12setURLBagKeyERKNSt6__ndk112basic_stringIcNS1_11char_traitsIcEENS1_9allocatorIcEEEE(void *obj, union std_string *s) { + static void *(*fn)(void*,union std_string*) = NULL; + if (!fn) fn = sym("_ZN17storeservicescore15PurchaseRequest12setURLBagKeyERKNSt6__ndk112basic_stringIcNS1_11char_traitsIcEENS1_9allocatorIcEEEE"); + return fn(obj,s); +} +void *_ZN17storeservicescore15PurchaseRequest16setBuyParametersERKNSt6__ndk112basic_stringIcNS1_11char_traitsIcEENS1_9allocatorIcEEEE(void *obj, union std_string *s) { + static void *(*fn)(void*,union std_string*) = NULL; + if (!fn) fn = sym("_ZN17storeservicescore15PurchaseRequest16setBuyParametersERKNSt6__ndk112basic_stringIcNS1_11char_traitsIcEENS1_9allocatorIcEEEE"); + return fn(obj,s); +} +void *_ZN17storeservicescore15PurchaseRequest3runEv(void *obj) { + static void *(*fn)(void*) = NULL; + if (!fn) fn = sym("_ZN17storeservicescore15PurchaseRequest3runEv"); + return fn(obj); +} +struct shared_ptr *_ZNK17storeservicescore15PurchaseRequest8responseEv(void *obj) { + static struct shared_ptr *(*fn)(void*) = NULL; + if (!fn) fn = sym("_ZNK17storeservicescore15PurchaseRequest8responseEv"); + return fn(obj); +} +struct shared_ptr *_ZN17storeservicescore16PurchaseResponse5errorEv(void *obj) { + static struct shared_ptr *(*fn)(void*) = NULL; + if (!fn) fn = sym("_ZN17storeservicescore16PurchaseResponse5errorEv"); + return fn(obj); +} +struct std_vector _ZNK17storeservicescore16PurchaseResponse5itemsEv(void *obj) { + static struct std_vector (*fn)(void*) = NULL; + if (!fn) fn = sym("_ZNK17storeservicescore16PurchaseResponse5itemsEv"); + return fn(obj); +} +struct std_vector _ZNK17storeservicescore12PurchaseItem6assetsEv(void *obj) { + static struct std_vector (*fn)(void*) = NULL; + if (!fn) fn = sym("_ZNK17storeservicescore12PurchaseItem6assetsEv"); + return fn(obj); +} +union std_string *_ZNK17storeservicescore13PurchaseAsset3URLEv(void *obj, void *out) { + static union std_string *(*fn)(void*,void*) = NULL; + if (!fn) fn = sym("_ZNK17storeservicescore13PurchaseAsset3URLEv"); + return fn(obj,out); +} +union std_string *_ZNK17storeservicescore13PurchaseAsset11downloadKeyEv(void *obj, void *out) { + static union std_string *(*fn)(void*,void*) = NULL; + if (!fn) fn = sym("_ZNK17storeservicescore13PurchaseAsset11downloadKeyEv"); + return fn(obj,out); +} +int _ZNK17storeservicescore19StoreErrorCondition9errorCodeEv(void *obj) { + static int (*fn)(void*) = NULL; + if (!fn) fn = sym("_ZNK17storeservicescore19StoreErrorCondition9errorCodeEv"); + return fn(obj); +} +const char *_ZNK17storeservicescore19StoreErrorCondition4whatEv(void *obj) { + static const char *(*fn)(void*) = NULL; + if (!fn) fn = sym("_ZNK17storeservicescore19StoreErrorCondition4whatEv"); + return fn(obj); +} +struct shared_ptr *_ZNK17storeservicescore20AuthenticateResponse5errorEv(void *obj) { + static struct shared_ptr *(*fn)(void*) = NULL; + if (!fn) fn = sym("_ZNK17storeservicescore20AuthenticateResponse5errorEv"); + return fn(obj); +} +union std_string *_ZNK17storeservicescore20AuthenticateResponse15customerMessageEv(void *obj) { + static union std_string *(*fn)(void*) = NULL; + if (!fn) fn = sym("_ZNK17storeservicescore20AuthenticateResponse15customerMessageEv"); + return fn(obj); +} +void *_ZN17storeservicescore14RequestContext8fairPlayEv(void *obj, void *out) { + static void *(*fn)(void*,void*) = NULL; + if (!fn) fn = sym("_ZN17storeservicescore14RequestContext8fairPlayEv"); + return fn(obj,out); +} +struct std_vector _ZN17storeservicescore8FairPlay21getSubscriptionStatusEv(void *obj) { + static struct std_vector (*fn)(void*) = NULL; + if (!fn) fn = sym("_ZN17storeservicescore8FairPlay21getSubscriptionStatusEv"); + return fn(obj); +} + +/* itun FairPlay decryption */ +struct std_vector *_ZNK17storeservicescore13PlaybackAsset5sinfsEv( + struct std_vector *ret, void *playbackAsset) { + static struct std_vector *(*fn)(struct std_vector*,void*) = NULL; + if (!fn) fn = sym("_ZNK17storeservicescore13PlaybackAsset5sinfsEv"); + return fn(ret,playbackAsset); +} + +struct shared_ptr *_ZN18SVDecryptorFactory6createERKN11SVDecryptor15SVDecryptorTypeEPKhRKjS5_S7_RKNS0_20SVDecryptorTrackTypeERKbSC_( + struct shared_ptr *ret, int *protType, const uint8_t *keyData, uint32_t *keyLen, + const uint8_t *ivData, uint32_t *ivLen, int *trackType, uint8_t *b1, uint8_t *b2) { + static struct shared_ptr *(*fn)(struct shared_ptr*,int*,const uint8_t*,uint32_t*, + const uint8_t*,uint32_t*,int*,uint8_t*,uint8_t*) = NULL; + if (!fn) fn = sym("_ZN18SVDecryptorFactory6createERKN11SVDecryptor15SVDecryptorTypeEPKhRKjS5_S7_RKNS0_20SVDecryptorTrackTypeERKbSC_"); + return fn(ret,protType,keyData,keyLen,ivData,ivLen,trackType,b1,b2); +} + +void _ZN17SVPastisDecryptor13decryptSampleEPKhRKjPj( + void *decryptor, const uint8_t *data, const uint32_t *len, uint32_t *outLen) { + static void (*fn)(void*,const uint8_t*,const uint32_t*,uint32_t*) = NULL; + if (!fn) fn = sym("_ZN17SVPastisDecryptor13decryptSampleEPKhRKjPj"); + fn(decryptor,data,len,outLen); +} diff --git a/hybris_types.h b/hybris_types.h new file mode 100644 index 0000000..13328e1 --- /dev/null +++ b/hybris_types.h @@ -0,0 +1,43 @@ +#pragma once +/* Minimal struct definitions for hybris_stubs.c — does NOT include the + * const data (android_id, fairplayCert) that import.h defines, which + * would cause duplicate-symbol errors when linked with main.o. */ +#include +#include +#include + +struct shared_ptr { + void *obj; + void *ctrl_blk; +}; + +union std_string { + struct { + uint8_t mark; + char str[0]; + }; + struct { + size_t cap; + size_t size; + const char *data; + }; +}; + +struct std_vector { + void *begin; + void *end; + void *end_capacity; +}; + +struct FairPlayData { + const uint8_t *bytes_ptr; + uint32_t dataType; + uint32_t length; +}; + +struct FairPlaySinf { + int64_t identifier; + struct shared_ptr dpInfoData; + struct shared_ptr sinfData; + struct shared_ptr sinf2Data; +}; diff --git a/import.h b/import.h index 4596cc4..85b7d11 100644 --- a/import.h +++ b/import.h @@ -252,7 +252,11 @@ extern struct shared_ptr *_ZN22SVPlaybackLeaseManager12requestAssetERKmRKNSt6__n ); extern int _ZNK23SVPlaybackAssetResponse13hasValidAssetEv(void *); extern struct shared_ptr *_ZNK23SVPlaybackAssetResponse13playbackAssetEv(void *); +extern int _ZNK23SVPlaybackAssetResponse9errorCodeEv(void *); +extern union std_string *_ZNK23SVPlaybackAssetResponse12errorMessageEv(void *, void *); extern union std_string *_ZNK17storeservicescore13PlaybackAsset9URLStringEv(void *, uint8_t *); +extern union std_string *_ZNK17storeservicescore13PlaybackAsset7flavorEv(void *, void *); +extern union std_string *_ZNK17storeservicescore13PlaybackAsset11downloadKeyEv(void *, void *); extern union std_string *_ZNK17storeservicescore14RequestContext20storeFrontIdentifierERKNSt6__ndk110shared_ptrINS_6URLBagEEE(void *, void *, struct shared_ptr *); @@ -266,6 +270,7 @@ extern void _ZN13mediaplatform11HTTPMessage9setHeaderERKNSt6__ndk112basic_string extern void _ZN13mediaplatform11HTTPMessage11setBodyDataEPcm(void *,char *, u_long); extern void *_ZN17storeservicescore10DeviceGUID4guidEv(void *, void *); extern char *_ZNK13mediaplatform4Data5bytesEv(void *); +extern size_t _ZNK13mediaplatform4Data6lengthEv(void *); extern void *_ZN17storeservicescore10URLRequestC2ERKNSt6__ndk110shared_ptrIN13mediaplatform11HTTPMessageEEERKNS2_INS_14RequestContextEEE(void *, struct shared_ptr *, struct shared_ptr *); extern void *_ZN17storeservicescore10URLRequest19setRequestParameterERKNSt6__ndk112basic_stringIcNS1_11char_traitsIcEENS1_9allocatorIcEEEES9_(void *, union std_string *, union std_string *); extern void *_ZN17storeservicescore10URLRequest3runEv(void *); @@ -283,6 +288,7 @@ extern struct shared_ptr *_ZN17storeservicescore16PurchaseResponse5errorEv(void extern struct std_vector _ZNK17storeservicescore16PurchaseResponse5itemsEv(void *); extern struct std_vector _ZNK17storeservicescore12PurchaseItem6assetsEv(void *); extern union std_string *_ZNK17storeservicescore13PurchaseAsset3URLEv(void *, void *); +extern union std_string *_ZNK17storeservicescore13PurchaseAsset11downloadKeyEv(void *, void *); extern int _ZNK17storeservicescore19StoreErrorCondition9errorCodeEv(void *); extern const char *_ZNK17storeservicescore19StoreErrorCondition4whatEv(void *); extern struct shared_ptr *_ZNK17storeservicescore20AuthenticateResponse5errorEv(void *); @@ -291,8 +297,46 @@ extern union std_string *_ZNK17storeservicescore20AuthenticateResponse15customer extern void *_ZN17storeservicescore14RequestContext8fairPlayEv(void *, void *); extern struct std_vector _ZN17storeservicescore8FairPlay21getSubscriptionStatusEv(void *); +// --- itun FairPlay decryption (progressive MV) --- +// FairPlayData layout: [bytes_ptr(8) | dataType(4) | length(4)] = 16 bytes +struct FairPlayData { + const uint8_t *bytes_ptr; + uint32_t dataType; + uint32_t length; +}; + +// FairPlaySinf layout: [identifier(8) | dpInfoData_sp(16) | sinfData_sp(16) | sinf2Data_sp(16)] = 56 bytes +struct FairPlaySinf { + int64_t identifier; + struct shared_ptr dpInfoData; + struct shared_ptr sinfData; + struct shared_ptr sinf2Data; +}; + +// PlaybackAsset::sinfs() const — returns std::vector by value +extern struct std_vector *_ZNK17storeservicescore13PlaybackAsset5sinfsEv( + struct std_vector *ret, void *playbackAsset); + +// SVDecryptorFactory::create(SVDecryptorType const&, uint8_t const*, uint32_t const&, +// uint8_t const*, uint32_t const&, SVDecryptorTrackType const&, bool const&, bool const&) +// Returns shared_ptr via hidden first param +extern struct shared_ptr *_ZN18SVDecryptorFactory6createERKN11SVDecryptor15SVDecryptorTypeEPKhRKjS5_S7_RKNS0_20SVDecryptorTrackTypeERKbSC_( + struct shared_ptr *ret, int *protType, const uint8_t *keyData, uint32_t *keyLen, + const uint8_t *ivData, uint32_t *ivLen, int *trackType, uint8_t *b1, uint8_t *b2); + +// SVPastisDecryptor::decryptSample(uint8_t const*, uint32_t const&, uint32_t*) +// Decrypts in-place; outLen receives the output byte count +extern void _ZN17SVPastisDecryptor13decryptSampleEPKhRKjPj( + void *decryptor, const uint8_t *data, const uint32_t *dataLen, uint32_t *outLen); + + +#ifndef IMPORT_H_NO_DATA_DEFS const char *const android_id = "dc28071e981c439e"; +#else +extern const char *const android_id; +#endif +#ifndef IMPORT_H_NO_DATA_DEFS const char *const fairplayCert = "MIIEzjCCA7agAwIBAgIIAXAVjHFZDjgwDQYJKoZIhvcNAQEFBQAwfzELMAkGA1UEBhMCVVMxEz" "ARBgNVBAoMCkFwcGxlIEluYy4xJjAkBgNVBAsMHUFwcGxlIENlcnRpZmljYXRpb24gQXV0aG9y" "aXR5MTMwMQYDVQQDDCpBcHBsZSBLZXkgU2VydmljZXMgQ2VydGlmaWNhdGlvbiBBdXRob3JpdH" @@ -339,4 +383,7 @@ const char *const fairplayCert = "MIIEzjCCA7agAwIBAgIIAXAVjHFZDjgwDQYJKoZIhvcNAQ "ymdxZ74WGZMuVv3ueJKcxG1jAhCRhr0lb6QaPaQQSNW+xnoesb3CLA0RzrcgBp/9WFZNdttJOS" "yC93lQmiE0r5RqPpe/IWUzwoZxri8qnsghVFxCBEcMB+U4PJR8WeAkPrji8po2JLYurvgNRhGk" "DKcAFPuGEpXdF86hPts+07zazsP0fBjBSVgP3jqb8G31w5W+O+wBW0B9uCf3s0vXU4LuJTAyww" - "s2ImZ7O/AaY/uXWOyIUMUKPgL1/QJieB7pBoENIJ2CeJS2M3iv00ssmCmTEJ"; \ No newline at end of file + "s2ImZ7O/AaY/uXWOyIUMUKPgL1/QJieB7pBoENIJ2CeJS2M3iv00ssmCmTEJ"; +#else +extern const char *const fairplayCert; +#endif \ No newline at end of file diff --git a/main.c b/main.c index 83e311c..a872df1 100644 --- a/main.c +++ b/main.c @@ -22,6 +22,7 @@ * hook (Dobby) 两种模式都编译; curl/log debug hook 仅 Debug */ #include +#include #include #include #include @@ -43,9 +44,9 @@ #include "dobby.h" #include -static struct shared_ptr apInf; -static uint8_t leaseMgr[16]; -static struct shared_ptr reqCtx; +struct shared_ptr apInf; +uint8_t leaseMgr[16]; +struct shared_ptr reqCtx; struct gengetopt_args_info args_info; char *amUsername, *amPassword; struct shared_ptr GUID; @@ -53,9 +54,43 @@ int decryptCount = 1000; int offlineFlag; char *device_infos[9]; -static char *g_storefront_id = NULL; -static char *g_dev_token = NULL; -static char *g_music_token = NULL; +char *g_storefront_id = NULL; +char *g_dev_token = NULL; +char *g_music_token = NULL; + +// itun FairPlay decryptor for progressive MV +pthread_mutex_t g_itun_mutex = PTHREAD_MUTEX_INITIALIZER; +struct shared_ptr g_itun_decryptor = {.obj = NULL, .ctrl_blk = NULL}; +unsigned long g_itun_adam_id = 0; + +/* Library-mode callbacks — set by drm_lib_init(), NULL in binary mode. */ +#include "drm_lib.h" +drm_auth_cb_t g_drm_auth_cb = NULL; +void *g_drm_auth_ud = NULL; +drm_state_cb_t g_drm_state_cb = NULL; +void *g_drm_state_ud = NULL; + +/* Write a single-word state token to base_dir/drm-state. + * The Go engine reads this file via inotify to track wrapper lifecycle. + * States: STARTING LOGIN WAITING_2FA INITIALIZING_FAIRPLAY RUNNING + * RECOVERY FAILED STOPPED + * In library mode, also fires g_drm_state_cb if set. + */ +static void write_drm_state(const char *state) { + if (g_drm_state_cb) g_drm_state_cb(state, g_drm_state_ud); + if (!args_info.base_dir_arg) return; + char path[512]; + snprintf(path, sizeof(path), "%s/drm-state", args_info.base_dir_arg); + FILE *fp = fopen(path, "w"); + if (!fp) return; + fprintf(fp, "%s\n", state); + fclose(fp); +} + +/* Protects preshareCtx against concurrent access from the main decrypt + * thread (handle/getKdContext) and the recovery worker (refresh_decrypt_ctx). + * Using PTHREAD_MUTEX_INITIALIZER avoids the need for an explicit init call. */ +static pthread_mutex_t g_ctx_mutex = PTHREAD_MUTEX_INITIALIZER; #ifndef MyRelease static int (*orig_debug_log_enabled)(void); @@ -224,9 +259,15 @@ static void credentialHandler(struct shared_ptr *credReqHandler, int passLen = strlen(amPassword); if (need2FA) { - if (args_info.code_from_file_flag) { + write_drm_state("WAITING_2FA"); + if (g_drm_auth_cb) { + /* library mode: ask the Go engine for the 2FA code */ + char code[16] = {0}; + g_drm_auth_cb("2fa", code, sizeof(code), g_drm_auth_ud); + strncat(amPassword, code, 6); + } else if (args_info.code_from_file_flag) { fprintf(stderr, "[!] Enter your 2FA code into rootfs/%s/2fa.txt\n", args_info.base_dir_arg); - fprintf(stderr, "[!] Example command: echo -n 114514 > rootfs/%s/2fa.txt\n", args_info.base_dir_arg); + fprintf(stderr, "[!] Example command: echo -n 123456 > rootfs/%s/2fa.txt\n", args_info.base_dir_arg); fprintf(stderr, "[!] Waiting for input...\n"); int count = 0; while (1) @@ -277,7 +318,7 @@ static void credentialHandler(struct shared_ptr *credReqHandler, } -static inline void init() { +void drm_init_internal(void) { // srand(time(0)); // raise(SIGSTOP); @@ -288,7 +329,7 @@ static inline void init() { setenv("all_proxy", args_info.proxy_arg, 1); } - static const char *resolvers[2] = {"223.5.5.5", "223.6.6.6"}; + static const char *resolvers[2] = {"1.1.1.1", "8.8.8.8"}; _resolv_set_nameservers_for_net(0, resolvers, 2, "."); // static char android_id[16]; @@ -316,78 +357,105 @@ static inline void init() { &ret, GUID.obj, &conf1, &conf2, &conf3, &conf4); } -static inline struct shared_ptr init_ctx() { +struct shared_ptr drm_init_ctx(void) { fprintf(stderr, "[+] initializing ctx...\n"); union std_string strBuf = new_std_string(strcat_b(args_info.base_dir_arg, "/mpl_db")); struct shared_ptr reqCtx; + fprintf(stderr, "[cp1] make_shared RequestContext\n"); fflush(stderr); _ZNSt6__ndk110shared_ptrIN17storeservicescore14RequestContextEE11make_sharedIJRNS_12basic_stringIcNS_11char_traitsIcEENS_9allocatorIcEEEEEEES3_DpOT_( &reqCtx, &strBuf); + fprintf(stderr, "[cp2] setup vtable ptr arr=%p arr+2=%p\n", + &_ZTVNSt6__ndk120__shared_ptr_emplaceIN17storeservicescore20RequestContextConfigENS_9allocatorIS2_EEEE, + &_ZTVNSt6__ndk120__shared_ptr_emplaceIN17storeservicescore20RequestContextConfigENS_9allocatorIS2_EEEE + 2); fflush(stderr); static uint8_t ptr[480]; *(void **)(ptr) = &_ZTVNSt6__ndk120__shared_ptr_emplaceIN17storeservicescore20RequestContextConfigENS_9allocatorIS2_EEEE + 2; struct shared_ptr reqCtxCfg = {.obj = ptr + 32, .ctrl_blk = ptr}; + fprintf(stderr, "[cp3] reqCtxCfg ctrl_blk=%p obj=%p vptr=%p\n", reqCtxCfg.ctrl_blk, reqCtxCfg.obj, *(void**)ptr); fflush(stderr); + fprintf(stderr, "[cp4] RequestContextConfigC2\n"); fflush(stderr); _ZN17storeservicescore20RequestContextConfigC2Ev(reqCtxCfg.obj); - // _ZN17storeservicescore20RequestContextConfig9setCPFlagEb(reqCtx.obj, 1); + fprintf(stderr, "[cp5] setBaseDirectoryPath\n"); fflush(stderr); _ZN17storeservicescore20RequestContextConfig20setBaseDirectoryPathERKNSt6__ndk112basic_stringIcNS1_11char_traitsIcEENS1_9allocatorIcEEEE( reqCtxCfg.obj, &strBuf); + fprintf(stderr, "[cp6] setClientIdentifier\n"); fflush(stderr); strBuf = new_std_string(device_infos[0]); _ZN17storeservicescore20RequestContextConfig19setClientIdentifierERKNSt6__ndk112basic_stringIcNS1_11char_traitsIcEENS1_9allocatorIcEEEE( reqCtxCfg.obj, &strBuf); + fprintf(stderr, "[cp7] setVersionIdentifier\n"); fflush(stderr); strBuf = new_std_string(device_infos[1]); _ZN17storeservicescore20RequestContextConfig20setVersionIdentifierERKNSt6__ndk112basic_stringIcNS1_11char_traitsIcEENS1_9allocatorIcEEEE( reqCtxCfg.obj, &strBuf); + fprintf(stderr, "[cp8] setPlatformIdentifier\n"); fflush(stderr); strBuf = new_std_string(device_infos[2]); _ZN17storeservicescore20RequestContextConfig21setPlatformIdentifierERKNSt6__ndk112basic_stringIcNS1_11char_traitsIcEENS1_9allocatorIcEEEE( reqCtxCfg.obj, &strBuf); + fprintf(stderr, "[cp9] setProductVersion\n"); fflush(stderr); strBuf = new_std_string(device_infos[3]); _ZN17storeservicescore20RequestContextConfig17setProductVersionERKNSt6__ndk112basic_stringIcNS1_11char_traitsIcEENS1_9allocatorIcEEEE( reqCtxCfg.obj, &strBuf); + fprintf(stderr, "[cp10] setDeviceModel\n"); fflush(stderr); strBuf = new_std_string(device_infos[4]); _ZN17storeservicescore20RequestContextConfig14setDeviceModelERKNSt6__ndk112basic_stringIcNS1_11char_traitsIcEENS1_9allocatorIcEEEE( reqCtxCfg.obj, &strBuf); + fprintf(stderr, "[cp11] setBuildVersion\n"); fflush(stderr); strBuf = new_std_string(device_infos[5]); _ZN17storeservicescore20RequestContextConfig15setBuildVersionERKNSt6__ndk112basic_stringIcNS1_11char_traitsIcEENS1_9allocatorIcEEEE( reqCtxCfg.obj, &strBuf); + fprintf(stderr, "[cp12] setLocaleIdentifier\n"); fflush(stderr); strBuf = new_std_string(device_infos[6]); _ZN17storeservicescore20RequestContextConfig19setLocaleIdentifierERKNSt6__ndk112basic_stringIcNS1_11char_traitsIcEENS1_9allocatorIcEEEE( reqCtxCfg.obj, &strBuf); + fprintf(stderr, "[cp13] setLanguageIdentifier\n"); fflush(stderr); strBuf = new_std_string(device_infos[7]); _ZN17storeservicescore20RequestContextConfig21setLanguageIdentifierERKNSt6__ndk112basic_stringIcNS1_11char_traitsIcEENS1_9allocatorIcEEEE( reqCtxCfg.obj, &strBuf); + fprintf(stderr, "[cp14] RequestContextManager::configure\n"); fflush(stderr); _ZN21RequestContextManager9configureERKNSt6__ndk110shared_ptrIN17storeservicescore14RequestContextEEE( &reqCtx); + fprintf(stderr, "[cp15] RequestContext::init\n"); fflush(stderr); static uint8_t buf[88]; _ZN17storeservicescore14RequestContext4initERKNSt6__ndk110shared_ptrINS_20RequestContextConfigEEE( &buf, reqCtx.obj, &reqCtxCfg); + fprintf(stderr, "[cp16] setFairPlayDirectoryPath\n"); fflush(stderr); strBuf = new_std_string(args_info.base_dir_arg); _ZN17storeservicescore14RequestContext24setFairPlayDirectoryPathERKNSt6__ndk112basic_stringIcNS1_11char_traitsIcEENS1_9allocatorIcEEEE( reqCtx.obj, &strBuf); + fprintf(stderr, "[cp17] make_shared AndroidPresentationInterface\n"); fflush(stderr); _ZNSt6__ndk110shared_ptrIN20androidstoreservices28AndroidPresentationInterfaceEE11make_sharedIJEEES3_DpOT_( &apInf); + fprintf(stderr, "[cp18] setDialogHandler\n"); fflush(stderr); _ZN20androidstoreservices28AndroidPresentationInterface16setDialogHandlerEPFvlNSt6__ndk110shared_ptrIN17storeservicescore14ProtocolDialogEEENS2_INS_36AndroidProtocolDialogResponseHandlerEEEE( apInf.obj, &dialogHandler); + fprintf(stderr, "[cp19] setCredentialsHandler\n"); fflush(stderr); _ZN20androidstoreservices28AndroidPresentationInterface21setCredentialsHandlerEPFvNSt6__ndk110shared_ptrIN17storeservicescore18CredentialsRequestEEENS2_INS_33AndroidCredentialsResponseHandlerEEEE( apInf.obj, &credentialHandler); + fprintf(stderr, "[cp20] setPresentationInterface\n"); fflush(stderr); _ZN17storeservicescore14RequestContext24setPresentationInterfaceERKNSt6__ndk110shared_ptrINS_21PresentationInterfaceEEE( reqCtx.obj, &apInf); + fprintf(stderr, "[cp21] init_ctx done\n"); fflush(stderr); return reqCtx; } -extern void *endLeaseCallback; -extern void *pbErrCallback; +extern uint8_t endLeaseCallback[32]; +extern uint8_t pbErrCallback[32]; +extern void hybris_init_callbacks(void); +extern void start_recovery_thread(void); +extern int is_recovery_active(void); +/* Returns current RecoveryState as int: 0=Running 1=Scheduled 2=Refreshing 3=Failed */ +extern int get_recovery_state(void); -inline static uint8_t login(struct shared_ptr reqCtx) { +uint8_t login(struct shared_ptr reqCtx) { fprintf(stderr, "[+] logging in...\n"); if (file_exists(strcat_b(args_info.base_dir_arg, "/STOREFRONT_ID"))) { remove(strcat_b(args_info.base_dir_arg, "/STOREFRONT_ID")); @@ -456,8 +524,8 @@ static inline void writefull(const int connfd, void *const buf, } } -static void *FHinstance = NULL; /* SVFootHillSessionCtrl 单例 (会话持有者) */ -static void *preshareCtx = NULL; /* prefetch 上下文缓存 (adam=="0" 时复用) */ +void *FHinstance = NULL; /* SVFootHillSessionCtrl 单例 (会话持有者) */ +void *preshareCtx = NULL; /* prefetch 上下文缓存 (adam=="0" 时复用) */ /* * 派生某 (adam, uri) 的 kdContext (解密上下文)。 @@ -473,12 +541,22 @@ static void *preshareCtx = NULL; /* prefetch 上下文缓存 (adam=="0" 时复 * - 每次调用都会重新 getPersistentKey (contentKey 每次会话会变), * 但 decryptContext 派生的 kdContext 是 per-track 稳定的。 */ -inline static void *getKdContext(const char *const adam, +void *getKdContext(const char *const adam, const char *const uri) { uint8_t isPreshare = (strcmp("0", adam) == 0); - if (isPreshare && preshareCtx != NULL) { - return preshareCtx; + + /* Fast-path: return cached preshare context if available. + * Lock only long enough to read the pointer — the long FairPlay + * network operations below must NOT be performed under this lock, + * or the recovery worker would block all decryption during reacquisition. */ + if (isPreshare) { + pthread_mutex_lock(&g_ctx_mutex); + void *cached = preshareCtx; + pthread_mutex_unlock(&g_ctx_mutex); + if (cached != NULL) + return cached; } + fprintf(stderr, "[.] adamId: %s, uri: %s\n", adam, uri); union std_string defaultId = new_std_string(adam); @@ -519,8 +597,15 @@ inline static void *getKdContext(const char *const adam, void *kdContext = *_ZNK18SVFootHillPContext9kdContextEv(SVFootHillPContext.obj); - if (kdContext != NULL && isPreshare) + + /* Store result under lock so the recovery worker sees a consistent value + * if it concurrently resets preshareCtx to NULL. */ + if (kdContext != NULL && isPreshare) { + pthread_mutex_lock(&g_ctx_mutex); preshareCtx = kdContext; + pthread_mutex_unlock(&g_ctx_mutex); + } + return kdContext; } @@ -529,15 +614,38 @@ inline static void *getKdContext(const char *const adam, * 在 40020 key server 捕获 content 模板前调用, 把 FHinstance 会话归一化到 * 与真实解密流程一致的状态 (否则 standalone 的 ctx/r1_entry 会不一致)。 */ -void refresh_decrypt_ctx() { +void refresh_decrypt_ctx(void) { uint8_t autom = 1; + + /* Request a new playback lease from Apple. */ _ZN22SVPlaybackLeaseManager12requestLeaseERKb(leaseMgr, &autom); + + /* Tear down all cached FairPlay key contexts so fresh keys are derived. */ _ZN21SVFootHillSessionCtrl16resetAllContextsEv(FHinstance); + + /* Invalidate the preshare cache under lock before rebuilding it. + * Any concurrent getKdContext() call will miss the cache and fall through + * to a full key derivation — correct behaviour during recovery. */ + pthread_mutex_lock(&g_ctx_mutex); preshareCtx = NULL; - preshareCtx = getKdContext("0", "skd://itunes.apple.com/P000000000/s1/e1"); + pthread_mutex_unlock(&g_ctx_mutex); + + /* Rebuild the preshare context. getKdContext() will write the new pointer + * under g_ctx_mutex internally. */ + getKdContext("0", "skd://itunes.apple.com/P000000000/s1/e1"); + fprintf(stderr, "[!] refreshed context\n"); } +/* Called by the recovery worker to determine whether reacquisition produced + * a usable decrypt context. Reads preshareCtx under g_ctx_mutex. */ +int is_preshare_ctx_ready(void) { + pthread_mutex_lock(&g_ctx_mutex); + int ready = (preshareCtx != NULL); + pthread_mutex_unlock(&g_ctx_mutex); + return ready; +} + /* * 10020 样本解密服务主循环 (与 main.go 测试协议对应): * 外层按 (adam, uri) 建立解密上下文 (每首歌一次连接, 每首歌 prefetch+content 两轮), @@ -552,6 +660,15 @@ void refresh_decrypt_ctx() { */ void handle(const int connfd) { while (1) { + /* Fail fast during lease recovery: avoid queuing FairPlay key- + * delivery requests to Apple's servers while the recovery worker + * is already performing a refresh cycle. The client receives an + * EOF/broken-pipe and should retry after a brief pause. */ + if (is_recovery_active()) { + fprintf(stderr, "[.] decrypt request refused: lease recovery in progress\n"); + return; + } + uint8_t adamSize; if (!readfull(connfd, &adamSize, sizeof(uint8_t))) return; @@ -664,7 +781,10 @@ inline static int new_socket() { } -const char* get_m3u8_method_download(struct shared_ptr reqCtx, unsigned long adam) { +/* out_key: if non-NULL, receives a strdup'd downloadKey string (caller must free). + * Set to NULL on failure or if the asset carries no downloadKey. */ +const char* get_m3u8_method_download(struct shared_ptr reqCtx, unsigned long adam, char **out_key) { + if (out_key) *out_key = NULL; void *purchase_request = malloc(1024); _ZN17storeservicescore15PurchaseRequestC2ERKNSt6__ndk110shared_ptrINS_14RequestContextEEE(purchase_request, &reqCtx); _ZN17storeservicescore15PurchaseRequest23setProcessDialogActionsEb(purchase_request, 1); @@ -686,16 +806,31 @@ const char* get_m3u8_method_download(struct shared_ptr reqCtx, unsigned long ada _ZNK17storeservicescore13PurchaseAsset3URLEv(url_str, lastAsset->obj); const char *url = std_string_data(url_str); if (url) { - char *result = strdup(url); // Make a copy + char *result = strdup(url); free(url_str); + if (out_key) { + union std_string *key_str = malloc(sizeof(union std_string)); + _ZNK17storeservicescore13PurchaseAsset11downloadKeyEv(key_str, lastAsset->obj); + const char *key = std_string_data(key_str); + if (key && *key) { + *out_key = strdup(key); + fprintf(stderr, "[.] downloadKey for %lu: %.16s...\n", adam, *out_key); + } else { + fprintf(stderr, "[.] downloadKey for %lu: empty\n", adam); + } + free(key_str); + } return result; } - } + } return NULL; } -const char* get_m3u8_method_play(uint8_t leaseMgr[16], unsigned long adam) { +/* out_key: if non-NULL, receives a strdup'd downloadKey from the lease PlaybackAsset + * (caller must free). NULL on failure or if the asset carries no downloadKey. */ +const char* get_m3u8_method_play(uint8_t leaseMgr[16], unsigned long adam, char **out_key) { + if (out_key) *out_key = NULL; union std_string HLS = new_std_string_short_mode("HLS"); struct std_vector HLSParam = new_std_vector(&HLS); static uint8_t z0 = 0; @@ -703,7 +838,7 @@ const char* get_m3u8_method_play(uint8_t leaseMgr[16], unsigned long adam) { _ZN22SVPlaybackLeaseManager12requestAssetERKmRKNSt6__ndk16vectorINS2_12basic_stringIcNS2_11char_traitsIcEENS2_9allocatorIcEEEENS7_IS9_EEEERKb( &ptr_result, leaseMgr, &adam, &HLSParam, &z0 ); - + if (ptr_result.obj == NULL) { return NULL; } @@ -714,36 +849,205 @@ const char* get_m3u8_method_play(uint8_t leaseMgr[16], unsigned long adam) { return NULL; } + void *playbackObj = playbackAsset->obj; + union std_string *m3u8 = malloc(sizeof(union std_string)); if (m3u8 == NULL) { return NULL; } - - void *playbackObj = playbackAsset->obj; _ZNK17storeservicescore13PlaybackAsset9URLStringEv(m3u8, playbackObj); - if (m3u8 == NULL || std_string_data(m3u8) == NULL) { + if (std_string_data(m3u8) == NULL) { free(m3u8); return NULL; } - + const char *m3u8_str = std_string_data(m3u8); - if (m3u8_str) { - char *result = strdup(m3u8_str); // Make a copy - free(m3u8); - return result; - } else { - return NULL; + char *result = m3u8_str ? strdup(m3u8_str) : NULL; + free(m3u8); + + if (result && out_key) { + union std_string *key_str = malloc(sizeof(union std_string)); + if (key_str) { + _ZNK17storeservicescore13PlaybackAsset11downloadKeyEv(key_str, playbackObj); + const char *key = std_string_data(key_str); + if (key && *key) { + *out_key = strdup(key); + fprintf(stderr, "[.] lease downloadKey for %lu: %.16s...\n", adam, *out_key); + } else { + fprintf(stderr, "[.] lease downloadKey for %lu: empty\n", adam); + } + free(key_str); + } } + + return result; } else { return NULL; } } +/* Request video flavors from the lease manager matching the real Android app behavior: + * pass all flavors as a single vector with force=true. + * Returns strdup'd URL on success, NULL on failure. */ +static const char* request_video_flavors(uint8_t leaseMgr[16], unsigned long adam, + char **out_key) { + if (out_key) *out_key = NULL; + static const char *flavor_names[] = {"720p", "hdmv", "480p", "sdmv"}; + union std_string flavors[4]; + for (int i = 0; i < 4; i++) + flavors[i] = new_std_string_short_mode(flavor_names[i]); + struct std_vector flavParam = { + .begin = flavors, + .end = (char*)flavors + sizeof(flavors), + .end_capacity = (char*)flavors + sizeof(flavors), + }; + static uint8_t z1 = 1; + struct shared_ptr ptr_result; + _ZN22SVPlaybackLeaseManager12requestAssetERKmRKNSt6__ndk16vectorINS2_12basic_stringIcNS2_11char_traitsIcEENS2_9allocatorIcEEEENS7_IS9_EEEERKb( + &ptr_result, leaseMgr, &adam, &flavParam, &z1 + ); + + if (ptr_result.obj == NULL) { + fprintf(stderr, "[!] video: requestAsset NULL for %lu\n", adam); + return NULL; + } + + if (!(_ZNK23SVPlaybackAssetResponse13hasValidAssetEv(ptr_result.obj) & 0xFF)) { + int ec = _ZNK23SVPlaybackAssetResponse9errorCodeEv(ptr_result.obj); + fprintf(stderr, "[!] video: hasValidAsset=false errorCode=%d for %lu\n", ec, adam); + return NULL; + } + + int hv = _ZNK23SVPlaybackAssetResponse13hasValidAssetEv(ptr_result.obj); + struct shared_ptr *playbackAsset = _ZNK23SVPlaybackAssetResponse13playbackAssetEv(ptr_result.obj); + fprintf(stderr, "[.] video: resp=%p hasValid=%d assetPtr=%p asset.obj=%p for %lu\n", + ptr_result.obj, hv, + (void*)playbackAsset, + playbackAsset ? (void*)playbackAsset->obj : NULL, + adam); + if (playbackAsset == NULL || playbackAsset->obj == NULL) { + fprintf(stderr, "[!] video: playbackAsset NULL for %lu\n", adam); + return NULL; + } + + void *playbackObj = playbackAsset->obj; + + union std_string *url_str = malloc(sizeof(union std_string)); + if (url_str == NULL) return NULL; + _ZNK17storeservicescore13PlaybackAsset9URLStringEv(url_str, playbackObj); + + const char *url = std_string_data(url_str); + if (url == NULL || *url == '\0') { + fprintf(stderr, "[!] video: URLString empty for %lu\n", adam); + free(url_str); + return NULL; + } + + char *result = strdup(url); + free(url_str); + + /* Extract downloadKey (may be empty for MV — that's OK) */ + if (result && out_key) { + union std_string *key_str = malloc(sizeof(union std_string)); + if (key_str) { + _ZNK17storeservicescore13PlaybackAsset11downloadKeyEv(key_str, playbackObj); + const char *key = std_string_data(key_str); + if (key && *key) { + *out_key = strdup(key); + fprintf(stderr, "[.] video: downloadKey for %lu len=%zu\n", adam, strlen(*out_key)); + } else { + fprintf(stderr, "[.] video: downloadKey empty for %lu (expected for MV)\n", adam); + } + free(key_str); + } + } + + /* Extract sinfs and create itun decryptor for this asset */ + { + struct std_vector sinf_vec = {0}; + _ZNK17storeservicescore13PlaybackAsset5sinfsEv(&sinf_vec, playbackObj); + + size_t sinf_count = 0; + if (sinf_vec.begin && sinf_vec.end > sinf_vec.begin) { + sinf_count = ((char*)sinf_vec.end - (char*)sinf_vec.begin) / sizeof(struct FairPlaySinf); + } + fprintf(stderr, "[.] video: sinfs count=%zu for %lu\n", sinf_count, adam); + + if (sinf_count > 0) { + struct FairPlaySinf *sinf = (struct FairPlaySinf *)sinf_vec.begin; + fprintf(stderr, "[.] video: sinf[0] id=%ld sinfData.obj=%p sinf2Data.obj=%p\n", + (long)sinf->identifier, sinf->sinfData.obj, sinf->sinf2Data.obj); + + const uint8_t *key_data = NULL; + uint32_t key_len = 0; + const uint8_t *iv_data = NULL; + uint32_t iv_len = 0; + + if (sinf->sinfData.obj) { + struct FairPlayData *fpd = (struct FairPlayData *)sinf->sinfData.obj; + key_data = fpd->bytes_ptr; + key_len = fpd->length; + fprintf(stderr, "[.] video: sinfData bytes=%p len=%u\n", key_data, key_len); + } + if (sinf->sinf2Data.obj) { + struct FairPlayData *fpd2 = (struct FairPlayData *)sinf->sinf2Data.obj; + iv_data = fpd2->bytes_ptr; + iv_len = fpd2->length; + fprintf(stderr, "[.] video: sinf2Data bytes=%p len=%u\n", iv_data, iv_len); + } + + if (key_data && key_len > 0) { + int prot_type = 3; // itun + int track_type = 1; // video + uint8_t b_true = 1; + uint8_t b_false = 0; + struct shared_ptr new_dec = {0}; + + fprintf(stderr, "[.] video: creating SVPastisDecryptor protType=%d trackType=%d keyLen=%u ivLen=%u\n", + prot_type, track_type, key_len, iv_len); + + _ZN18SVDecryptorFactory6createERKN11SVDecryptor15SVDecryptorTypeEPKhRKjS5_S7_RKNS0_20SVDecryptorTrackTypeERKbSC_( + &new_dec, &prot_type, key_data, &key_len, + iv_data ? iv_data : (const uint8_t*)"", &iv_len, + &track_type, &b_true, &b_false); + + if (new_dec.obj) { + fprintf(stderr, "[+] video: SVPastisDecryptor created at %p for %lu\n", new_dec.obj, adam); + pthread_mutex_lock(&g_itun_mutex); + g_itun_decryptor = new_dec; + g_itun_adam_id = adam; + pthread_mutex_unlock(&g_itun_mutex); + } else { + fprintf(stderr, "[!] video: SVDecryptorFactory::create returned NULL for %lu\n", adam); + } + } + } + } + + fprintf(stderr, "[.] video: URL for %lu = %.80s...\n", adam, result); + return result; +} + +/* Request progressive MV playback matching the real Android app: + all video flavors [720p, hdmv, 480p, sdmv] in a single vector call with force=true. + downloadKey is empty for MV content — the file is itun-encrypted and decrypted client-side. */ +const char* get_progressive_method_play(uint8_t leaseMgr[16], unsigned long adam, char **out_key) { + if (out_key) *out_key = NULL; + const char *url = request_video_flavors(leaseMgr, adam, out_key); + if (url) { + fprintf(stderr, "[.] progressive: got URL for %lu\n", adam); + return url; + } + fprintf(stderr, "[!] progressive: no valid asset for %lu\n", adam); + return NULL; +} + /* * 20020 M3U8 流地址服务: 收 [1B len][adamId 数字串] → 返回该歌的 M3U8 URL + 换行。 * 由 get_m3u8_method_download/play 经 PlaybackAsset 从 Apple 获取。 */ + void handle_m3u8(const int connfd) { while (1) { @@ -761,10 +1065,21 @@ void handle_m3u8(const int connfd) { char *ptr; unsigned long adamID = strtoul(adam, &ptr, 10); const char *m3u8; + + /* During lease recovery the decrypt context is being rebuilt. + * Return an empty line (same as a failed asset request) so the + * client can detect the condition and retry rather than waiting + * on a network call that will fail anyway. */ + if (is_recovery_active()) { + fprintf(stderr, "[.] m3u8 request refused: lease recovery in progress\n"); + writefull(connfd, "\n", 1); + continue; + } + if (offlineFlag) { - m3u8 = get_m3u8_method_download(reqCtx, adamID); + m3u8 = get_m3u8_method_download(reqCtx, adamID, NULL); } else { - m3u8 = get_m3u8_method_play(leaseMgr, adamID); + m3u8 = get_m3u8_method_play(leaseMgr, adamID, NULL); } if (m3u8 == NULL) { fprintf(stderr, "[.] failed to get m3u8 of adamId: %ld\n", adamID); @@ -919,7 +1234,7 @@ static uint64_t g_cap_rcx, g_cap_rax, g_cap_rdx, g_cap_r9, g_cap_rbp; static int g_r1_hooked = 0; /* R1 hook 是否已安装 */ /* R1 入口 Dobby hook 回调: 捕获 block0 的 ctx/state/寄存器到全局缓冲 */ -static void r1_capture_cb(RegisterContext *ctx, const HookEntryInfo *info) { +static void r1_capture_cb(void *address, DobbyRegisterContext *ctx) { uint64_t r9 = ctx->general.regs.r9; if (!g_cap_armed || g_cap_done) return; if ((ctx->general.regs.rsi & 0xff) != 8) return; /* 只捕获 block 0 (rsi==8) */ @@ -934,19 +1249,21 @@ static void r1_capture_cb(RegisterContext *ctx, const HookEntryInfo *info) { g_cap_done = 1; } -/* dl_iterate_phdr 回调: 定位 libCoreLSKD.so 加载基址 */ -static int _find_lib_cb(struct dl_phdr_info *info, size_t size, void *data) { - if (info->dlpi_name && strstr(info->dlpi_name, "libCoreLSKD.so")) { - *(uintptr_t *)data = info->dlpi_addr; - return 1; - } - return 0; -} - /* 返回 libCoreLSKD.so 的运行时加载基址 (失败返回 0) */ static uintptr_t get_lib_core_lskd_base(void) { + FILE *f = fopen("/proc/self/maps", "r"); + if (!f) return 0; + char line[512]; uintptr_t base = 0; - dl_iterate_phdr(_find_lib_cb, &base); + while (fgets(line, sizeof line, f)) { + if (!strstr(line, "libCoreLSKD.so")) continue; + unsigned long start, off; + if (sscanf(line, "%lx-%*lx %*4s %lx", &start, &off) == 2 && off == 0) { + base = start; + break; + } + } + fclose(f); return base; } @@ -1216,6 +1533,193 @@ void handle_account(const int connfd) free(json_body); } +void handle_progressive_mv(const int connfd) +{ + while (1) { + uint8_t adamSize; + if (!readfull(connfd, &adamSize, sizeof(uint8_t))) { + return; + } + if (adamSize <= 0) { + return; + } + char adam[adamSize + 1]; + for (int i = 0; i < adamSize; i++) { + readfull(connfd, &adam[i], sizeof(uint8_t)); + } + adam[adamSize] = '\0'; + char *ptr; + unsigned long adamID = strtoul(adam, &ptr, 10); + + char *dk = NULL; + /* Try HQ flavor first — returns progressive MP4 URL + downloadKey for + * CDN server-side decryption. Fall back to HLS flavor if HQ fails. */ + const char *url = get_progressive_method_play(leaseMgr, adamID, &dk); + if (url == NULL) { + fprintf(stderr, "[.] mv HQ flavor failed for %lu, falling back to HLS\n", adamID); + url = get_m3u8_method_play(leaseMgr, adamID, &dk); + } + if (url == NULL) { + fprintf(stderr, "[.] mv progressive failed for adamId: %ld\n", adamID); + writefull(connfd, "\n\n\n", 3); + } else { + /* Check if itun decryptor was created for this adamId */ + pthread_mutex_lock(&g_itun_mutex); + int has_itun = (g_itun_decryptor.obj != NULL && g_itun_adam_id == adamID); + pthread_mutex_unlock(&g_itun_mutex); + + const char *itun_flag = has_itun ? "ITUN" : ""; + fprintf(stderr, "[.] mv progressive adamId: %ld, url: %.80s..., key: %s, itun: %s\n", + adamID, url, dk ? dk : "(none)", has_itun ? "yes" : "no"); + + /* protocol: URL\n KEY\n ITUN_FLAG\n */ + size_t url_len = strlen(url); + size_t key_len = dk ? strlen(dk) : 0; + size_t flag_len = strlen(itun_flag); + size_t buf_len = url_len + 1 + key_len + 1 + flag_len + 1 + 1; + char *buf = malloc(buf_len); + if (buf) { + snprintf(buf, buf_len, "%s\n%s\n%s\n", url, dk ? dk : "", itun_flag); + writefull(connfd, buf, strlen(buf)); + free(buf); + } + free((void *)url); + if (dk) free(dk); + } + } +} + +static inline void *new_socket_mv(void *args) +{ + const int fd = socket(AF_INET, SOCK_STREAM | SOCK_CLOEXEC, IPPROTO_TCP); + if (fd == -1) { + perror("socket"); + return NULL; + } + const int optval = 1; + setsockopt(fd, SOL_SOCKET, SO_REUSEPORT, &optval, sizeof(optval)); + + static struct sockaddr_in serv_addr = {.sin_family = AF_INET}; + inet_pton(AF_INET, args_info.host_arg, &serv_addr.sin_addr); + serv_addr.sin_port = htons(args_info.mv_port_arg); + + if (bind(fd, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) == -1) { + perror("bind mv"); + return NULL; + } + listen(fd, 1); + fprintf(stderr, "[!] listening mv progressive request on %s:%d\n", + args_info.host_arg, args_info.mv_port_arg); + + while (1) { + const int connfd = accept(fd, NULL, NULL); + if (connfd == -1) continue; + handle_progressive_mv(connfd); + close(connfd); + } +} + +/* itun sample decryption handler (port 50020). + * Protocol: + * 1. Client sends adamId_len (1 byte) + adamId string + * 2. Loop: client sends sample_size (4 bytes LE) + sample_data + * server decrypts in-place, sends decrypted_size (4 bytes LE) + decrypted_data + * 3. sample_size == 0 signals end of stream */ +void handle_itun_decrypt(const int connfd) { + while (1) { + uint8_t adamSize; + if (!readfull(connfd, &adamSize, sizeof(uint8_t))) + return; + if (adamSize <= 0) + return; + + char adam[adamSize + 1]; + if (!readfull(connfd, adam, adamSize)) + return; + adam[adamSize] = '\0'; + + char *ptr; + unsigned long adamID = strtoul(adam, &ptr, 10); + + pthread_mutex_lock(&g_itun_mutex); + void *dec_obj = g_itun_decryptor.obj; + unsigned long dec_adam = g_itun_adam_id; + pthread_mutex_unlock(&g_itun_mutex); + + if (dec_obj == NULL) { + fprintf(stderr, "[!] itun: no decryptor available (request MV URL first)\n"); + return; + } + if (dec_adam != adamID) { + fprintf(stderr, "[!] itun: decryptor adamId mismatch: have %lu, got %lu\n", dec_adam, adamID); + return; + } + + fprintf(stderr, "[+] itun: decrypting samples for adamId %lu\n", adamID); + + while (1) { + uint32_t size; + if (!readfull(connfd, &size, sizeof(uint32_t))) { + perror("itun read size"); + return; + } + + if (size == 0) + break; + + uint8_t *sample = malloc(size); + if (sample == NULL) { + perror("itun malloc"); + return; + } + if (!readfull(connfd, sample, size)) { + free(sample); + perror("itun read data"); + return; + } + + uint32_t out_len = 0; + _ZN17SVPastisDecryptor13decryptSampleEPKhRKjPj( + dec_obj, sample, &size, &out_len); + + writefull(connfd, &out_len, sizeof(uint32_t)); + if (out_len > 0) { + writefull(connfd, sample, out_len); + } + free(sample); + } + } +} + +static inline void *new_socket_itun(void *args) { + const int fd = socket(AF_INET, SOCK_STREAM | SOCK_CLOEXEC, IPPROTO_TCP); + if (fd == -1) { + perror("socket itun"); + return NULL; + } + const int optval = 1; + setsockopt(fd, SOL_SOCKET, SO_REUSEPORT, &optval, sizeof(optval)); + + static struct sockaddr_in serv_addr = {.sin_family = AF_INET}; + inet_pton(AF_INET, args_info.host_arg, &serv_addr.sin_addr); + serv_addr.sin_port = htons(args_info.mv_port_arg + 10000); // 60020 + + if (bind(fd, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) == -1) { + perror("bind itun"); + return NULL; + } + listen(fd, 1); + fprintf(stderr, "[!] listening itun decrypt on %s:%d\n", + args_info.host_arg, args_info.mv_port_arg + 10000); + + while (1) { + const int connfd = accept(fd, NULL, NULL); + if (connfd == -1) continue; + handle_itun_decrypt(connfd); + close(connfd); + } +} + static inline void *new_socket_account(void *args) { const int fd = socket(AF_INET, SOCK_STREAM | SOCK_CLOEXEC, IPPROTO_TCP); @@ -1292,7 +1796,13 @@ void write_storefront_id(void) { char *get_guid() { char *ret[2]; _ZN17storeservicescore10DeviceGUID4guidEv(ret, GUID.obj); - char *guid = _ZNK13mediaplatform4Data5bytesEv(ret[0]); + char *raw = _ZNK13mediaplatform4Data5bytesEv(ret[0]); + size_t len = _ZNK13mediaplatform4Data6lengthEv(ret[0]); + /* Data::bytes() is NOT null-terminated — copy to a null-terminated buffer */ + char *guid = malloc(len + 1); + if (!guid) return NULL; + memcpy(guid, raw, len); + guid[len] = '\0'; return guid; } @@ -1304,7 +1814,8 @@ long long getCurrentTimeMillis() { char *get_music_user_token(char *guid, char *authToken, struct shared_ptr reqCtx){ - uint8_t ptr[480]; + uint8_t *ptr = (uint8_t *)calloc(1, 2048); + if (!ptr) return NULL; *(void **)(ptr) = &_ZTVNSt6__ndk120__shared_ptr_emplaceIN13mediaplatform11HTTPMessageENS_9allocatorIS2_EEEE + 2; @@ -1331,10 +1842,10 @@ char *get_music_user_token(char *guid, char *authToken, struct shared_ptr reqCtx } snprintf(body, body_size, "{\"guid\":\"%s\",\"assertion\":\"%s\",\"tcc-acceptance-date\":\"%lld\"}", guid, authToken, getCurrentTimeMillis()); - _ZN13mediaplatform11HTTPMessage11setBodyDataEPcm(httpMessage.obj, body, strlen(body)); - free(body); - uint8_t urlRequest[512]; + /* NOTE: do NOT free body before run() — new hybris stores pointer, not copy */ + uint8_t *urlRequest = (uint8_t *)calloc(1, 2048); + if (!urlRequest) { free(ptr); return NULL; } _ZN17storeservicescore10URLRequestC2ERKNSt6__ndk110shared_ptrIN13mediaplatform11HTTPMessageEEERKNS2_INS_14RequestContextEEE(urlRequest, &httpMessage, &reqCtx); _ZN17storeservicescore10URLRequest3runEv(urlRequest); struct shared_ptr *err = _ZNK17storeservicescore10URLRequest5errorEv(urlRequest); @@ -1347,9 +1858,8 @@ char *get_music_user_token(char *guid, char *authToken, struct shared_ptr reqCtx struct shared_ptr *urlResp = _ZNK17storeservicescore10URLRequest8responseEv(urlRequest); struct shared_ptr *resp = _ZNK17storeservicescore11URLResponse18underlyingResponseEv(urlResp->obj); void *http_message_obj = resp->obj; - void** data_ptr_location = (void**)((char*)http_message_obj + 48); - void* data_ptr = *data_ptr_location; - char *respBody = _ZNK13mediaplatform4Data5bytesEv(data_ptr); + void* data_ptr = *(void**)((char*)http_message_obj + 48); + char *respBody = data_ptr ? _ZNK13mediaplatform4Data5bytesEv(data_ptr) : NULL; cJSON *json = cJSON_Parse(respBody); cJSON *token_obj = cJSON_GetObjectItemCaseSensitive(json, "music_token"); char *token = cJSON_GetStringValue(token_obj); @@ -1367,7 +1877,8 @@ char *get_music_user_token(char *guid, char *authToken, struct shared_ptr reqCtx char* get_dev_token(struct shared_ptr reqCtx) { - uint8_t ptr[480]; + uint8_t *ptr = (uint8_t *)calloc(1, 2048); + if (!ptr) return NULL; *(void **)(ptr) = &_ZTVNSt6__ndk120__shared_ptr_emplaceIN13mediaplatform11HTTPMessageENS_9allocatorIS2_EEEE + 2; @@ -1375,7 +1886,8 @@ char* get_dev_token(struct shared_ptr reqCtx) { union std_string url = new_std_string("https://sf-api-token-service.itunes.apple.com/apiToken"); union std_string method = new_std_string("GET"); _ZN13mediaplatform11HTTPMessageC2ENSt6__ndk112basic_stringIcNS1_11char_traitsIcEENS1_9allocatorIcEEEES7_(httpMessage.obj, &url, &method); - uint8_t urlRequest[512]; + uint8_t *urlRequest = (uint8_t *)calloc(1, 2048); + if (!urlRequest) { free(ptr); return NULL; } _ZN17storeservicescore10URLRequestC2ERKNSt6__ndk110shared_ptrIN13mediaplatform11HTTPMessageEEERKNS2_INS_14RequestContextEEE(urlRequest, &httpMessage, &reqCtx); union std_string clientIdName = new_std_string("clientId"); union std_string clientIdValue = new_std_string("musicAndroid"); @@ -1447,6 +1959,7 @@ int offline_available() { return 0; } +#ifndef DRM_LIB_BUILD int main(int argc, char *argv[]) { cmdline_parser(argc, argv, &args_info); char *copy_that_needs_to_be_freed = NULL; @@ -1456,24 +1969,43 @@ int main(int argc, char *argv[]) { install_hooks(); #endif - init(); - reqCtx = init_ctx(); + drm_init_internal(); + hybris_init_callbacks(); + fprintf(stderr, "[main-cp1] calling init_ctx\n"); fflush(stderr); + reqCtx = drm_init_ctx(); + fprintf(stderr, "[main-cp2] init_ctx returned reqCtx.obj=%p ctrl=%p\n", reqCtx.obj, reqCtx.ctrl_blk); fflush(stderr); + write_drm_state("STARTING"); + fprintf(stderr, "[main-cp3] write_drm_state done\n"); fflush(stderr); if (args_info.login_given) { amUsername = strtok(args_info.login_arg, ":"); amPassword = strtok(NULL, ":"); } if (args_info.login_given && !login(reqCtx)) { fprintf(stderr, "[!] login failed\n"); + write_drm_state("FAILED"); return EXIT_FAILURE; } + fprintf(stderr, "[main-cp4] SVPlaybackLeaseManagerC2\n"); fflush(stderr); _ZN22SVPlaybackLeaseManagerC2ERKNSt6__ndk18functionIFvRKiEEERKNS1_IFvRKNS0_10shared_ptrIN17storeservicescore19StoreErrorConditionEEEEEE( leaseMgr, &endLeaseCallback, &pbErrCallback); + fprintf(stderr, "[main-cp5] refreshLeaseAutomatically\n"); fflush(stderr); uint8_t autom = 1; _ZN22SVPlaybackLeaseManager25refreshLeaseAutomaticallyERKb(leaseMgr, &autom); + fprintf(stderr, "[main-cp6] requestLease\n"); fflush(stderr); _ZN22SVPlaybackLeaseManager12requestLeaseERKb(leaseMgr, &autom); + fprintf(stderr, "[main-cp7] SVFootHillSessionCtrl::instance\n"); fflush(stderr); FHinstance = _ZN21SVFootHillSessionCtrl8instanceEv(); - + fprintf(stderr, "[main-cp8] start_recovery_thread\n"); fflush(stderr); + + /* Start the async recovery thread. Must be started after leaseMgr and + * FHinstance are initialised so that refresh_decrypt_ctx() is safe to call + * from the worker at any point after this. */ + start_recovery_thread(); + fprintf(stderr, "[main-cp9] write_drm_state INITIALIZING_FAIRPLAY\n"); fflush(stderr); + write_drm_state("INITIALIZING_FAIRPLAY"); + fprintf(stderr, "[main-cp10] offline_available\n"); fflush(stderr); offlineFlag = offline_available(); + fprintf(stderr, "[main-cp11] offline_available returned %d\n", offlineFlag); fflush(stderr); if (offlineFlag) { fprintf(stderr, "[+] This account supports offline channel\n"); } @@ -1482,11 +2014,13 @@ int main(int argc, char *argv[]) { g_storefront_id = get_account_storefront_id(reqCtx); if (g_storefront_id == NULL) { fprintf(stderr, "[!] failed to get storefront ID\n"); + write_drm_state("FAILED"); return EXIT_FAILURE; } g_dev_token = get_dev_token(reqCtx); if (g_dev_token == NULL) { fprintf(stderr, "[!] failed to get dev token\n"); + write_drm_state("FAILED"); return EXIT_FAILURE; } g_music_token = get_music_user_token(get_guid(), g_dev_token, reqCtx); @@ -1508,13 +2042,16 @@ int main(int argc, char *argv[]) { } if (g_music_token == NULL) { fprintf(stderr, "[!] failed to get music token\n"); + write_drm_state("FAILED"); return EXIT_FAILURE; } } + fprintf(stderr, "[+] account info cached successfully\n"); write_storefront_id(); write_music_token(); + write_drm_state("RUNNING"); pthread_t m3u8_thread; pthread_create(&m3u8_thread, NULL, &new_socket_m3u8, NULL); @@ -1528,5 +2065,15 @@ int main(int argc, char *argv[]) { pthread_create(&key_thread, NULL, &new_socket_key, NULL); pthread_detach(key_thread); + pthread_t mv_thread; + pthread_create(&mv_thread, NULL, &new_socket_mv, NULL); + pthread_detach(mv_thread); + + pthread_t itun_thread; + pthread_create(&itun_thread, NULL, &new_socket_itun, NULL); + pthread_detach(itun_thread); + + return new_socket(); } +#endif /* DRM_LIB_BUILD */ diff --git a/main.cpp b/main.cpp index 8a02db6..55003c7 100644 --- a/main.cpp +++ b/main.cpp @@ -1,18 +1,243 @@ -/* - * main.cpp — main.c 中 socket 处理器的 C++ 异常屏障 - * handle() 会调用可能抛出 SVError 的 C++ 函数 (如 getPersistentKey), - * 这里用 extern "C" 包装并捕获异常, 避免 C++ 异常穿过 C 边界导致崩溃。 - * 每个新 socket 服务 (decrypt/m3u8/key/account) 都经 *_cpp 分发。 +/** + * main.cpp — C++ glue: lease/playback callbacks and recovery subsystem. + * + * Recovery state machine + * ───────────────────── + * + * ┌──────────────────────────────────────────────────────┐ + * │ Running │◄──────────────┐ + * └──────────────────────────────┬───────────────────────┘ │ + * │ Lease end / Playback error │ + * ▼ │ + * ┌──────────────────────────────────────────────────────┐ │ + * │ Scheduled │ │ + * │ (event queued; worker waking; backoff pending) │ │ + * └──────────────────────────────┬───────────────────────┘ │ + * │ backoff elapsed │ + * ▼ │ + * ┌──────────────────────────────────────────────────────┐ │ + * │ Refreshing │ │ + * │ (refresh_decrypt_ctx() in progress; requests │ │ + * │ gated at handle() / handle_m3u8()) │ │ + * └───────────────┬──────────────────────────────┬───────┘ │ + * │ preshareCtx non-null │ preshareCtx NULL │ + * ▼ ▼ │ + * ┌───────────────────────────┐ ┌───────────────────────────┐ │ + * │ Running │ │ Failed │ │ + * │ consec_fails = 0 │ │ consec_fails++ │ │ + * │ normal service resumes │ │ auto-schedules retry │──────────┘ + * └───────────────────────────┘ └───────────────────────────┘ + * + * Auto-retry on failure + * ───────────────────── + * When refresh_decrypt_ctx() succeeds but preshareCtx remains NULL (e.g. + * transient Apple server error, network interruption), no further Apple event + * arrives, so the worker would stall waiting on the condition variable. + * To avoid this, the worker self-schedules a retry via schedule_recovery() + * before looping back. The retry goes through the same queue and backoff + * path, ensuring exponential spacing even for self-triggered retries. + * + * Coalescing + * ────────── + * The queue is drained completely on each wake. A burst of events + * (3084 → 3084 → PLAYBACK_ERR) results in one refresh cycle, not three. + * + * RecoveryState visibility + * ──────────────────────── + * get_recovery_state() is exported as extern "C" and returns RecoveryState + * as a plain int so main.c (and a future HTTP status endpoint or Electron IPC + * handler) can expose the daemon's current condition without C++ knowledge. + * Values map directly to the RecoveryState enum below. */ + +#include +#include #include #include #include #include -#include +#include +#include +#include +#include /* sleep() */ +#include +extern "C" void refresh_decrypt_ctx(void); +extern "C" int is_preshare_ctx_ready(void); extern "C" void handle(int fd); -extern "C" uint8_t handle_cpp(int fd) { +// --------------------------------------------------------------------------- +// RecoveryState — the daemon's lifecycle state machine +// --------------------------------------------------------------------------- + +enum class RecoveryState : int { + Running = 0, // Normal operation; all requests served + Scheduled = 1, // Recovery event queued; worker waking; backoff pending + Refreshing = 2, // refresh_decrypt_ctx() in flight; requests gated + Failed = 3, // Last cycle failed; auto-retry queued +}; + +static std::atomic g_recovery_state{RecoveryState::Running}; + +// Returns the current RecoveryState as a plain int (C-visible). +// 0 = Running, 1 = Scheduled, 2 = Refreshing, 3 = Failed. +extern "C" int get_recovery_state(void) +{ + return static_cast(g_recovery_state.load()); +} + +// Convenience predicate used by handle() / handle_m3u8() to gate requests. +// Returns 1 when the daemon is not in its normal Running state. +extern "C" int is_recovery_active(void) +{ + return (g_recovery_state.load() != RecoveryState::Running) ? 1 : 0; +} + +// --------------------------------------------------------------------------- +// Recovery queue +// --------------------------------------------------------------------------- + +// Event codes pushed into the recovery queue. +static const int kPlaybackErrSentinel = -1; // playback error from pbErrCb +static const int kRetryInternal = -2; // self-scheduled retry on failure + +// Exponential backoff: 1 → 2 → 5 → 10 → 30 seconds. +// kBackoffMaxIdx is reused indefinitely so retries never stop. +static const int kBackoffSecs[] = {1, 2, 5, 10, 30}; +static const int kBackoffMaxIdx = 4; + +static std::mutex g_recovery_mtx; +static std::condition_variable g_recovery_cv; +static std::queue g_recovery_q; + +static void schedule_recovery(int code) +{ + { + std::lock_guard lk(g_recovery_mtx); + g_recovery_q.push(code); + } + g_recovery_cv.notify_one(); +} + +// --------------------------------------------------------------------------- +// Recovery worker — the single owner of retry timing and reacquisition +// --------------------------------------------------------------------------- + +static void recovery_worker() +{ + int consec_fails = 0; + + while (true) { + + // ── Wait for at least one event ───────────────────────────────────── + std::vector burst; + { + std::unique_lock lk(g_recovery_mtx); + g_recovery_cv.wait(lk, []{ + return !g_recovery_q.empty(); + }); + // Drain the entire queue (coalescing). + while (!g_recovery_q.empty()) { + burst.push_back(g_recovery_q.front()); + g_recovery_q.pop(); + } + } + + g_recovery_state.store(RecoveryState::Scheduled); + + // ── Log every code in the burst ───────────────────────────────────── + fprintf(stderr, "[recovery] STATE=Scheduled — %zu event(s) coalesced:\n", + burst.size()); + for (int c : burst) { + switch (c) { + case kPlaybackErrSentinel: + fprintf(stderr, "[recovery] PLAYBACK_ERROR\n"); + break; + case kRetryInternal: + fprintf(stderr, "[recovery] RETRY_INTERNAL " + "(self-scheduled after failed refresh)\n"); + break; + case 3084: + fprintf(stderr, "[recovery] LEASE_END code=3084 " + "(server revoke — simultaneous playback " + "or natural expiry)\n"); + break; + default: + fprintf(stderr, "[recovery] LEASE_END code=%d " + "(unknown — logging for future mapping)\n", c); + break; + } + } + + // ── Exponential backoff ───────────────────────────────────────────── + int idx = (consec_fails <= kBackoffMaxIdx) ? consec_fails + : kBackoffMaxIdx; + int delay = kBackoffSecs[idx]; + + if (consec_fails == 0) { + fprintf(stderr, + "[recovery] RECOVERY_SCHEDULED attempt=1 backoff=%ds\n", + delay); + } else { + fprintf(stderr, + "[recovery] RECOVERY_SCHEDULED attempt=%d backoff=%ds " + "(Apple may still be rejecting — retrying indefinitely)\n", + consec_fails + 1, delay); + } + sleep(delay); + + // ── Enter Refreshing state — gate incoming client requests ────────── + g_recovery_state.store(RecoveryState::Refreshing); + fprintf(stderr, "[recovery] STATE=Refreshing — calling refresh_decrypt_ctx()\n"); + + // Single-threaded by design: the worker is the only caller of + // refresh_decrypt_ctx(). No other code path calls it directly. + refresh_decrypt_ctx(); + + // ── Evaluate success ──────────────────────────────────────────────── + // is_preshare_ctx_ready() reads preshareCtx under g_ctx_mutex (main.c). + // A non-null context means Apple accepted the new lease and FairPlay + // key derivation succeeded — requests can resume. + if (is_preshare_ctx_ready()) { + consec_fails = 0; + g_recovery_state.store(RecoveryState::Running); + fprintf(stderr, + "[recovery] STATE=Running — REFRESH_SUCCESS, " + "decrypt context ready, HTTP servers resuming normal operation\n"); + } else { + consec_fails++; + g_recovery_state.store(RecoveryState::Failed); + + int next_idx = (consec_fails <= kBackoffMaxIdx) ? consec_fails + : kBackoffMaxIdx; + int next_delay = kBackoffSecs[next_idx]; + fprintf(stderr, + "[recovery] STATE=Failed — REFRESH_FAILED " + "ctx_ready=false consecutive_fails=%d retrying_in=%ds\n", + consec_fails, next_delay); + + // Self-schedule a retry so the daemon keeps trying even when + // Apple sends no further lease-end event (e.g. transient network + // failure during the refresh). The retry goes through the normal + // queue + backoff path, so exponential spacing is preserved. + schedule_recovery(kRetryInternal); + } + } +} + +// Called from main() in main.c, after leaseMgr and FHinstance are ready. +extern "C" void start_recovery_thread(void) +{ + std::thread(recovery_worker).detach(); + fprintf(stderr, "[+] recovery thread started\n"); +} + +// --------------------------------------------------------------------------- +// FairPlay decrypt exception shim +// --------------------------------------------------------------------------- + +extern "C" uint8_t handle_cpp(int fd) +{ try { handle(fd); return 1; @@ -37,15 +262,30 @@ extern "C" uint8_t handle_key_request_cpp(int fd) { } } -static void endLeaseCb(int const &c) { - fprintf(stderr, "[.] end lease code %d\n", c); - exit(1); +// --------------------------------------------------------------------------- +// SVPlaybackLeaseManager callbacks +// +// CONTRACT: return as fast as possible. Do NOT call any library function, +// perform I/O, or acquire any lock that the lease manager might also hold. +// --------------------------------------------------------------------------- + +static void endLeaseCb(int const &c) +{ + fprintf(stderr, "[.] LEASE_END code=%d — scheduling async recovery\n", c); + schedule_recovery(c); + // Returns immediately. recovery_worker handles reacquisition. } -static void pbErrCb(void *) { - fprintf(stderr, "[.] playback error\n"); - exit(1); +static void pbErrCb(void *) +{ + fprintf(stderr, "[.] PLAYBACK_ERROR — scheduling async recovery\n"); + schedule_recovery(kPlaybackErrSentinel); + // Returns immediately. recovery_worker handles context refresh. } -extern "C" std::function endLeaseCallback(endLeaseCb); -extern "C" std::function pbErrCallback(pbErrCb); +// endLeaseCallback and pbErrCallback are Bionic-compatible std::function objects +// defined in hybris_stubs.c as 32-byte zero-initialized buffers (empty functions). +// Proper Bionic vtable-wrapped callbacks are set up by hybris_init_callbacks(). +extern "C" void endLeaseCbExport(const int *code_ptr) { endLeaseCb(*code_ptr); } +extern "C" void pbErrCbExport(void *arg) { pbErrCb(arg); } + diff --git a/scripts/check-drift.py b/scripts/check-drift.py new file mode 100644 index 0000000..f8a21dd --- /dev/null +++ b/scripts/check-drift.py @@ -0,0 +1,294 @@ +#!/usr/bin/env python3 +""" +Two-phase drift guard for wrapper.c / wrapper-rootless.c. + +Phase 1 — Skeleton comparison + Strip each file's implementation-specific regions, normalize whitespace, + then diff the skeletons. Any remaining difference is unexpected drift. + +Phase 2 — Allow-list verification + Verify that every region actually stripped matches a named entry in the + allow-list, and that each entry was matched the expected number of times. + A new region added to either file without updating the allow-list fails here + even if Phase 1 would have passed after a new strip rule was also added. + +Usage: + python3 scripts/check-drift.py (run from repo root) + +Exit codes: + 0 both phases pass + 1 unexpected drift or allow-list violation +""" + +import os +import re +import subprocess +import sys +import tempfile + + +# --------------------------------------------------------------------------- +# Allow-list +# +# Each entry names a region that is permitted to differ between the two files. +# 'file' which file the region lives in +# 'type' single_line | end | end_brace (see strip_regions) +# 'start' regex that identifies the first line of the region +# 'end' regex for the closing line (type "end" only) +# 'expect' how many times this region must appear (default 1) +# --------------------------------------------------------------------------- + +ALLOW_LIST = [ + # ── wrapper.c privileged-only regions ──────────────────────────────── + { + "name": "stdlib.h include", + "file": "wrapper.c", + "type": "single_line", + "start": re.compile(r"#include\s+"), + }, + { + "name": "CAP_SYS_ADMIN defines", + "file": "wrapper.c", + "type": "end", + "start": re.compile(r"#define CAP_SYS_ADMIN_IDX"), + "end": re.compile(r"#define CAP_SYS_ADMIN_BIT"), + }, + { + "name": "has_cap_sys_admin function", + "file": "wrapper.c", + "type": "end_brace", + "start": re.compile(r"^int has_cap_sys_admin\(\)"), + }, + { + "name": "conditional CLONE_NEWPID unshare", + "file": "wrapper.c", + "type": "end_brace", + "start": re.compile(r"if \(has_cap_sys_admin\(\)\)"), + }, + + # ── wrapper-rootless.c rootless-only regions ────────────────────────── + { + "name": "write_file comment", + "file": "wrapper-rootless.c", + "type": "single_line", + "start": re.compile(r"/\* rootless-only: write"), + }, + { + "name": "write_file function", + "file": "wrapper-rootless.c", + "type": "end_brace", + "start": re.compile(r"^static int write_file\("), + }, + { + "name": "setup_user_namespace comment", + "file": "wrapper-rootless.c", + "type": "single_line", + "start": re.compile(r"/\* rootless-only: create"), + }, + { + "name": "setup_user_namespace function", + "file": "wrapper-rootless.c", + "type": "end_brace", + "start": re.compile(r"^static int setup_user_namespace\(\)"), + }, + { + "name": "setup_user_namespace call-site comment", + "file": "wrapper-rootless.c", + "type": "single_line", + "start": re.compile(r"/\* rootless-only: establish"), + }, + { + "name": "setup_user_namespace call", + "file": "wrapper-rootless.c", + "type": "end_brace", + "start": re.compile(r"if \(setup_user_namespace\(\)"), + }, + { + "name": "unconditional CLONE_NEWPID comment", + "file": "wrapper-rootless.c", + "type": "single_line", + "start": re.compile(r"/\* rootless-only: user namespace"), + }, + { + "name": "unconditional CLONE_NEWPID unshare", + "file": "wrapper-rootless.c", + "type": "end_brace", + "start": re.compile(r"if \(unshare\(CLONE_NEWPID\)"), + }, +] + + +# --------------------------------------------------------------------------- +# Stripping +# --------------------------------------------------------------------------- + +def strip_end_brace(lines, start_idx): + """Return the index after the closing brace that matches the opening + brace on lines[start_idx].""" + depth, seen_open = 0, False + i = start_idx + while i < len(lines): + depth += lines[i].count("{") - lines[i].count("}") + i += 1 + if depth > 0: + seen_open = True + if seen_open and depth <= 0: + break + return i + + +def strip_regions(lines, rules): + """ + Remove every region matched by rules. + Returns (stripped_lines, hits) where hits is a dict mapping rule index + to the number of times that rule was matched. + """ + hits = {i: 0 for i in range(len(rules))} + result = [] + i = 0 + while i < len(lines): + line = lines[i] + matched_idx = next( + (idx for idx, r in enumerate(rules) if r["start"].search(line)), + None, + ) + + if matched_idx is None: + result.append(line) + i += 1 + continue + + hits[matched_idx] += 1 + rule = rules[matched_idx] + + if rule["type"] == "single_line": + i += 1 + continue + + if rule["type"] == "end_brace": + i = strip_end_brace(lines, i) + continue + + # type == "end": inclusive range + end_re = rule["end"] + i += 1 + while i < len(lines): + matched = end_re.search(lines[i]) + i += 1 + if matched: + break + + return result, hits + + +def normalize(lines): + out = [] + for line in lines: + s = line.rstrip() + if not out and "canonical implementation" in s: + continue + if s == "" and out and out[-1] == "": + continue + out.append(s) + return out + + +def skeleton(path, rules): + with open(path) as f: + raw = [l.rstrip("\n") for l in f] + stripped, hits = strip_regions(raw, rules) + return normalize(stripped), hits + + +# --------------------------------------------------------------------------- +# Phase 1 — skeleton comparison +# --------------------------------------------------------------------------- + +def phase1(canon_skel, rootless_skel): + with tempfile.NamedTemporaryFile("w", suffix=".canon", delete=False) as a: + a.write("\n".join(canon_skel) + "\n"); a_path = a.name + with tempfile.NamedTemporaryFile("w", suffix=".rootless", delete=False) as b: + b.write("\n".join(rootless_skel) + "\n"); b_path = b.name + try: + result = subprocess.run( + ["diff", "-u", + "--label", "wrapper.c (skeleton)", + "--label", "wrapper-rootless.c (skeleton)", + a_path, b_path], + capture_output=True, text=True, + ) + finally: + os.unlink(a_path) + os.unlink(b_path) + return result + + +# --------------------------------------------------------------------------- +# Phase 2 — allow-list verification +# --------------------------------------------------------------------------- + +def phase2(canon_hits, rootless_hits, canon_rules, rootless_rules): + failures = [] + + def check(hits, rules, label): + for idx, rule in enumerate(rules): + expected = rule.get("expect", 1) + actual = hits[idx] + if actual != expected: + failures.append( + f" [{label}] '{rule['name']}': " + f"expected {expected} match(es), got {actual}" + ) + + check(canon_hits, canon_rules, "wrapper.c") + check(rootless_hits, rootless_rules, "wrapper-rootless.c") + return failures + + +# --------------------------------------------------------------------------- +# Main +# --------------------------------------------------------------------------- + +def main(): + canon_rules = [r for r in ALLOW_LIST if r["file"] == "wrapper.c"] + rootless_rules = [r for r in ALLOW_LIST if r["file"] == "wrapper-rootless.c"] + + canon_skel, canon_hits = skeleton("wrapper.c", canon_rules) + rootless_skel, rootless_hits = skeleton("wrapper-rootless.c", rootless_rules) + + p1 = phase1(canon_skel, rootless_skel) + p2 = phase2(canon_hits, rootless_hits, canon_rules, rootless_rules) + + ok = True + + if p1.returncode != 0: + ok = False + print("PHASE 1 FAIL: unexpected skeleton drift.") + print(" Add a strip rule to ALLOW_LIST for any new approved difference.") + print() + print(p1.stdout) + + if p2: + ok = False + print("PHASE 2 FAIL: allow-list region count mismatch.") + print(" Update ALLOW_LIST if a region was intentionally added or removed.") + print() + for line in p2: + print(line) + print() + + if ok: + canon_count = sum(canon_hits.values()) + rootless_count = sum(rootless_hits.values()) + print( + f"OK: {canon_count} privileged-only region(s) in wrapper.c, " + f"{rootless_count} rootless-only region(s) in wrapper-rootless.c. " + f"No unexpected drift." + ) + return 0 + + return 1 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/wrapper-rootless.c b/wrapper-rootless.c index 84960b7..cee82b4 100644 --- a/wrapper-rootless.c +++ b/wrapper-rootless.c @@ -1,22 +1,17 @@ -/* - * wrapper-rootless.c — Apple Music 解密 wrapper 的宿主层 (免 root 版) - * 与 wrapper.c 相同, 但用 user namespace (unshare CLONE_NEWUSER|NEWNS|NEWPID) - * + 写 uid_map/gid_map + deny setgroups, 无需 CAP_SYS_ADMIN 即可 chroot。 - * 适用于无特权容器 / WSL 等环境。 - */ +/* canonical implementation: wrapper.c — this file may only differ where rootless operation requires it */ #define _GNU_SOURCE + #include +#include #include #include -#include #include #include #include #include -#include #include #include -#include +#include #include "cmdline.h" @@ -29,6 +24,7 @@ static void intHan(int signum) { } } +/* rootless-only: write a single line to a /proc file */ static int write_file(const char *path, const char *line) { int fd = open(path, O_WRONLY); if (fd < 0) return -1; @@ -38,13 +34,23 @@ static int write_file(const char *path, const char *line) { return (ret == len) ? 0 : -1; } -static int setup_unprivileged_namespaces() { +/* rootless-only: create user+mount namespace and map current uid/gid to root */ +static int setup_user_namespace() { uid_t uid = getuid(); gid_t gid = getgid(); char buf[128]; - if (unshare(CLONE_NEWUSER | CLONE_NEWNS | CLONE_NEWPID) == -1) { - perror("unshare"); + if (unshare(CLONE_NEWUSER | CLONE_NEWNS) == -1) { + if (errno == EPERM) { + fprintf(stderr, + "error: unprivileged user namespaces are not permitted on this system.\n" + " options:\n" + " 1. enable: sudo sysctl -w kernel.unprivileged_userns_clone=1\n" + " 2. use the privileged wrapper instead\n" + " 3. grant capabilities: sudo setcap cap_sys_admin+ep ./wrapper-rootless\n"); + } else { + perror("unshare"); + } return -1; } @@ -75,21 +81,11 @@ int main(int argc, char *argv[], char *envp[]) { return 1; } - if (setup_unprivileged_namespaces() != 0) { - return 1; - } - - child_proc = fork(); - if (child_proc == -1) { - perror("fork"); + /* rootless-only: establish user namespace before any privileged operations */ + if (setup_user_namespace() != 0) { return 1; } - if (child_proc > 0) { - wait(NULL); - return 0; - } - if (mkdir("./rootfs/dev", 0755) != 0 && errno != EEXIST) { perror("mkdir ./rootfs/dev failed"); return 1; @@ -107,41 +103,57 @@ int main(int argc, char *argv[], char *envp[]) { return 1; } - if (mkdir("./rootfs/proc", 0755) != 0 && errno != EEXIST) { - perror("mkdir ./rootfs/proc failed"); + if (chdir("./rootfs") != 0) { + perror("chdir"); + return 1; + } + if (chroot("./") != 0) { + perror("chroot"); return 1; } - if (mount("proc", "./rootfs/proc", "proc", 0, NULL) != 0) { - perror("mount proc failed"); + if (mkdir("/proc", 0755) != 0 && errno != EEXIST) { + perror("mkdir /proc failed"); return 1; } - // 5. 切换目录并 chroot - if (chdir("./rootfs") != 0) { - perror("chdir ./rootfs failed"); + chmod("/system/bin/linker64", 0755); + chmod("/system/bin/main", 0755); + + /* rootless-only: user namespace guarantees CLONE_NEWPID is always available */ + if (unshare(CLONE_NEWPID)) { + perror("unshare"); return 1; } - if (chroot(".") != 0) { - perror("chroot . failed"); + + child_proc = fork(); + if (child_proc == -1) { + perror("fork"); return 1; } - chmod("/system/bin/linker64", 0755); - chmod("/system/bin/main", 0755); + if (child_proc > 0) { + wait(NULL); + return 0; + } + + if (mount("proc", "/proc", "proc", 0, NULL) != 0) { + perror("mount proc failed"); + return 1; + } if (mkdir(args_info.base_dir_arg, 0777) != 0 && errno != EEXIST) { perror("mkdir base_dir_arg failed"); - } - - char db_path[512]; - snprintf(db_path, sizeof(db_path), "%s/mpl_db", args_info.base_dir_arg); - if (mkdir(db_path, 0777) != 0 && errno != EEXIST) { + } + + char db_dir[1024]; + snprintf(db_dir, sizeof(db_dir), "%s/mpl_db", args_info.base_dir_arg); + if (mkdir(db_dir, 0777) != 0 && errno != EEXIST) { perror("mkdir mpl_db failed"); } execve("/system/bin/main", argv, envp); - + perror("execve"); return 1; -} \ No newline at end of file +} diff --git a/wrapper.c b/wrapper.c index f3aa69b..7604bdd 100644 --- a/wrapper.c +++ b/wrapper.c @@ -1,11 +1,4 @@ -/* - * wrapper.c — Apple Music 解密 wrapper 的宿主层 (root 特权版) - * 流程: 解析命令行 → bind-mount rootfs/dev/urandom → chdir+chroot ./rootfs - * → 若有 CAP_SYS_ADMIN 则 unshare(CLONE_NEWPID) → fork 子进程 - * → mount proc, 建 base_dir/mpl_db → execve("/system/bin/main")。 - * main 在 chroot 内以 Android 环境运行, 提供 10020/20020/30020/40020 四个服务。 - * 免 root 版见 wrapper-rootless.c。 - */ +/* canonical implementation — any change here must be reviewed against wrapper-rootless.c */ #define _GNU_SOURCE #include @@ -137,7 +130,7 @@ int main(int argc, char *argv[], char *envp[]) { if (mkdir(args_info.base_dir_arg, 0777) != 0 && errno != EEXIST) { perror("mkdir base_dir_arg failed"); } - + char db_dir[1024]; snprintf(db_dir, sizeof(db_dir), "%s/mpl_db", args_info.base_dir_arg); if (mkdir(db_dir, 0777) != 0 && errno != EEXIST) { @@ -145,7 +138,7 @@ int main(int argc, char *argv[], char *envp[]) { } execve("/system/bin/main", argv, envp); - + perror("execve"); return 1; -} \ No newline at end of file +} diff --git a/wrapper.ggo b/wrapper.ggo index 55b5fa7..60ec417 100644 --- a/wrapper.ggo +++ b/wrapper.ggo @@ -6,8 +6,9 @@ option "decrypt-port" D "" int optional default="10020" option "m3u8-port" M "" int optional default="20020" option "account-port" A "" int optional default="30020" option "key-port" K "" int optional default="40020" +option "mv-port" G "" int optional default="50020" option "proxy" P "" string optional default="" option "login" L "username:password" string optional option "code-from-file" F "" flag off option "base-dir" B "" string optional default="/data/data/com.apple.android.music/files" -option "device-info" I "ClientIdentifier/VersionIdentifier/PlatformIdentifier/ProductVersion/DeviceModel/BuildVersion/LocaleIdentifier/LanguageIdentifier/AndroidID" string optional default="Music/4.9/Android/10/Samsung S9/7663313/en-US/en-US/dc28071e981c439e" \ No newline at end of file +option "device-info" I "ClientIdentifier/VersionIdentifier/PlatformIdentifier/ProductVersion/DeviceModel/BuildVersion/LocaleIdentifier/LanguageIdentifier/AndroidID" string optional default="Music/4.9/Android/10/Samsung S9/7663313/en-US/en-US/dc28071e981c439e"