From 29dba8170167d16ef99aa0f1c4617920145bf93e Mon Sep 17 00:00:00 2001 From: SilentOne <155584784+silentone12725@users.noreply.github.com> Date: Wed, 1 Jul 2026 16:54:21 +0530 Subject: [PATCH 1/4] daemon: replace exit(1) callbacks with recoverable state machine MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previously, both SVPlaybackLeaseManager callbacks (endLeaseCb, pbErrCb) called exit(1) unconditionally, making every Apple lease termination a fatal process death. wrapper-rootless was behaving like a short-lived utility rather than a persistent background service. This commit introduces a proper recovery lifecycle: Recovery state machine (main.cpp) - RecoveryState enum: Running / Scheduled / Refreshing / Failed - get_recovery_state() exported as extern C int for status endpoints and Electron IPC (0=Running, 1=Scheduled, 2=Refreshing, 3=Failed) - is_recovery_active() derived from state, used to gate HTTP requests Non-blocking callbacks - endLeaseCb / pbErrCb now push a code onto a queue and return immediately — no library calls from within the lease manager thread - Eliminates reentrancy and deadlock risk from callback context Dedicated recovery worker thread - Drains the entire queue on each wake (coalescing): a burst of 3084+3084+PLAYBACK_ERR produces one refresh cycle, not three - Exponential backoff: 1s -> 2s -> 5s -> 10s -> 30s (clamped, never stops) - Calls refresh_decrypt_ctx() as the sole owner of reacquisition logic - Verifies success via is_preshare_ctx_ready() — resets consec_fails only when preshareCtx is non-null after the refresh - Self-schedules retry on failure (kRetryInternal) so the daemon keeps retrying even when Apple sends no further lease-end event (e.g. transient network failure during refresh) Thread safety (main.c) - g_ctx_mutex (PTHREAD_MUTEX_INITIALIZER) protects all preshareCtx reads and writes against concurrent access from the decrypt thread and the recovery worker - Lock is released before FairPlay network calls to avoid blocking decryption during reacquisition - is_preshare_ctx_ready() reads preshareCtx under g_ctx_mutex Client-visible state gating (main.c) - handle() and handle_m3u8() check is_recovery_active() per request - Decrypt server: returns immediately (EOF) during recovery - M3U8 server: writes empty line, continues loop — no hanging requests - Prevents FairPlay key-delivery calls from racing the recovery worker HTTP servers (decrypt, m3u8, account) remain alive across all lease events. The Electron supervisor continues to provide the outer safety net for true process deaths (segfault, OOM, etc.). --- main.c | 84 ++++++++++++++++-- main.cpp | 265 ++++++++++++++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 330 insertions(+), 19 deletions(-) diff --git a/main.c b/main.c index 83e311c..c5440bd 100644 --- a/main.c +++ b/main.c @@ -57,6 +57,11 @@ static char *g_storefront_id = NULL; static char *g_dev_token = NULL; static char *g_music_token = NULL; +/* 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); static int (*orig_android_log_print)(int prio, const char *tag, const char *fmt, ...); @@ -386,6 +391,10 @@ static inline struct shared_ptr init_ctx() { extern void *endLeaseCallback; extern void *pbErrCallback; +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) { fprintf(stderr, "[+] logging in...\n"); @@ -476,9 +485,19 @@ static void *preshareCtx = NULL; /* prefetch 上下文缓存 (adam=="0" 时复 inline static 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 +538,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 +555,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 +601,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; @@ -761,6 +819,17 @@ 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); } else { @@ -1473,6 +1542,11 @@ int main(int argc, char *argv[]) { _ZN22SVPlaybackLeaseManager12requestLeaseERKb(leaseMgr, &autom); FHinstance = _ZN21SVFootHillSessionCtrl8instanceEv(); + /* 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(); + offlineFlag = offline_available(); if (offlineFlag) { fprintf(stderr, "[+] This account supports offline channel\n"); diff --git a/main.cpp b/main.cpp index 8a02db6..76c2c0d 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,27 @@ 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); +extern "C" std::function pbErrCallback(pbErrCb); + From dc3e55bb908b8b19ccfa62a21ff8a034c278f37e Mon Sep 17 00:00:00 2001 From: SilentOne <155584784+silentone12725@users.noreply.github.com> Date: Fri, 7 Aug 2026 09:40:23 +0530 Subject: [PATCH 2/4] Add DRM state tracking and wrapper drift check --- .github/workflows/build-for-x86_64.yml | 3 + Dockerfile.build | 36 +++ main.c | 28 ++- scripts/check-drift.py | 294 +++++++++++++++++++++++++ wrapper-rootless.c | 96 ++++---- wrapper.c | 15 +- 6 files changed, 417 insertions(+), 55 deletions(-) create mode 100644 Dockerfile.build create mode 100644 scripts/check-drift.py 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..b0f8b03 --- /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 cmdline.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/main.c b/main.c index c5440bd..e4e90d8 100644 --- a/main.c +++ b/main.c @@ -57,6 +57,21 @@ static char *g_storefront_id = NULL; static char *g_dev_token = NULL; static char *g_music_token = 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 + */ +static void write_drm_state(const char *state) { + 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. */ @@ -229,9 +244,10 @@ static void credentialHandler(struct shared_ptr *credReqHandler, int passLen = strlen(amPassword); if (need2FA) { + write_drm_state("WAITING_2FA"); 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) @@ -293,7 +309,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]; @@ -1527,12 +1543,14 @@ int main(int argc, char *argv[]) { init(); reqCtx = init_ctx(); + write_drm_state("STARTING"); 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; } _ZN22SVPlaybackLeaseManagerC2ERKNSt6__ndk18functionIFvRKiEEERKNS1_IFvRKNS0_10shared_ptrIN17storeservicescore19StoreErrorConditionEEEEEE( @@ -1546,6 +1564,7 @@ int main(int argc, char *argv[]) { * FHinstance are initialised so that refresh_decrypt_ctx() is safe to call * from the worker at any point after this. */ start_recovery_thread(); + write_drm_state("INITIALIZING_FAIRPLAY"); offlineFlag = offline_available(); if (offlineFlag) { @@ -1556,11 +1575,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); @@ -1582,13 +1603,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); 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 +} From acb73dedd79a435e4c966f89faa741652b7e6047 Mon Sep 17 00:00:00 2001 From: SilentOne <155584784+silentone12725@users.noreply.github.com> Date: Mon, 28 Sep 2026 02:45:18 +0530 Subject: [PATCH 3/4] Add host-native DRM wrapper build via libhybris, in-process CGO bridge and deploy script --- Dockerfile.build | 2 +- README.md | 195 +++++++---- build-and-deploy.sh | 60 ++++ build-native.sh | 122 +++++++ cmdline.c | 39 ++- cmdline.h | 4 + drm_lib.c | 361 ++++++++++++++++++++ drm_lib.h | 100 ++++++ hybris_ctor.c | 28 ++ hybris_stubs.c | 797 ++++++++++++++++++++++++++++++++++++++++++++ hybris_types.h | 43 +++ import.h | 49 ++- main.c | 541 +++++++++++++++++++++++++++--- main.cpp | 7 +- wrapper.ggo | 3 +- 15 files changed, 2234 insertions(+), 117 deletions(-) create mode 100755 build-and-deploy.sh create mode 100755 build-native.sh create mode 100644 drm_lib.c create mode 100644 drm_lib.h create mode 100644 hybris_ctor.c create mode 100644 hybris_stubs.c create mode 100644 hybris_types.h diff --git a/Dockerfile.build b/Dockerfile.build index b0f8b03..66f64c1 100644 --- a/Dockerfile.build +++ b/Dockerfile.build @@ -24,7 +24,7 @@ RUN aria2c -x 16 -o ndk.zip \ && rm ndk.zip # Copy source (no glob — explicit) -COPY cmdline.c cmdline.h cmdline.ggo ./ +COPY cmdline.c cmdline.h wrapper.ggo ./ COPY main.c main.cpp ./ COPY wrapper.c wrapper-rootless.c ./ COPY import.h ./ 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 e4e90d8..2a2c65f 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,16 +54,30 @@ 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); @@ -245,7 +260,12 @@ static void credentialHandler(struct shared_ptr *credReqHandler, if (need2FA) { write_drm_state("WAITING_2FA"); - if (args_info.code_from_file_flag) { + 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 123456 > rootfs/%s/2fa.txt\n", args_info.base_dir_arg); fprintf(stderr, "[!] Waiting for input...\n"); @@ -298,7 +318,7 @@ static void credentialHandler(struct shared_ptr *credReqHandler, } -static inline void init() { +void drm_init_internal(void) { // srand(time(0)); // raise(SIGSTOP); @@ -337,82 +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")); @@ -481,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 (解密上下文)。 @@ -498,7 +541,7 @@ 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); @@ -738,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); @@ -760,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; @@ -777,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; } @@ -788,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) { @@ -847,9 +1077,9 @@ void handle_m3u8(const int connfd) { } 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); @@ -1004,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) */ @@ -1301,6 +1531,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); @@ -1377,7 +1794,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; } @@ -1389,7 +1812,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; @@ -1416,10 +1840,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); @@ -1432,9 +1856,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); @@ -1452,7 +1875,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; @@ -1460,7 +1884,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"); @@ -1532,6 +1957,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; @@ -1541,9 +1967,13 @@ 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, ":"); @@ -1553,20 +1983,27 @@ int main(int argc, char *argv[]) { 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"); } @@ -1626,5 +2063,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 76c2c0d..55003c7 100644 --- a/main.cpp +++ b/main.cpp @@ -283,6 +283,9 @@ static void pbErrCb(void *) // 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/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" From 7402034f56dc63e34cea41fb93149d5a6e7c295e Mon Sep 17 00:00:00 2001 From: SilentOne <155584784+silentone12725@users.noreply.github.com> Date: Thu, 1 Oct 2026 15:07:24 +0530 Subject: [PATCH 4/4] native: locate libCoreLSKD via /proc/self/maps for the R1 hook --- main.c | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/main.c b/main.c index 2a2c65f..a872df1 100644 --- a/main.c +++ b/main.c @@ -1249,19 +1249,21 @@ static void r1_capture_cb(void *address, DobbyRegisterContext *ctx) { 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; }