Created
July 14, 2026 17:06
-
-
Save trinity-cloud/6d4eb8714cbf612881d27feee93c8612 to your computer and use it in GitHub Desktop.
Android Twitter/X App Security Analysis (Log)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| ╭─── Claude Code v2.1.209 ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ | |
| │ │ Tips for getting started │ | |
| │ Welcome back Migel! │ Run /init to create a CLAUDE.md file with instructions for Claude │ | |
| │ │ ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── │ | |
| │ ▗ ▗ ▖ ▖ │ What's new │ | |
| │ │ Fixed /model and other dialogs being blocked in `claude agents` background sessions (reverts an overly broad guard) │ | |
| │ ▘▘ ▝▝ │ Added screen reader mode: opt-in plain-text rendering for screen reader users. Run `claude --ax-screen-reader`, set CLAUDE_AX_SCREEN_READER=1, or add "… │ | |
| │ Opus 4.8 (1M context) with xh… · Claude Max · │ Added `vimInsertModeRemaps` setting: map two-key insert-mode sequences like `jj` to Escape in vim mode │ | |
| │ Migel's Organization │ /release-notes for more │ | |
| │ ~/cyber/workspace/android-app-engagements │ │ | |
| ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ | |
| ▎ Extended through July 19 | |
| ▎ We're extending Claude Fable 5 access on all paid plans, as well as keeping Claude Code’s weekly rate limits 50% higher, through July 19. | |
| ▎ | |
| ▎ As before, you can use up to half of your weekly usage limit on Fable 5. After that, you can keep using Fable 5 with usage credits, or switch to another model to keep working within your remaining limits. | |
| ▎ | |
| ▎ More details here: https://support.claude.com/en/articles/15424964-claude-fable-5-promotional-access | |
| ❯ /mcp | |
| ⎿ MCP dialog dismissed | |
| ❯ I need you to access the Android phone that's connected, and pull the X-app for analysis. We're doing an authorized red-teaming assessment, and I believe it's | |
| infected with trjoan like tracking, privacy intrusions, data collection of users etc. I need you to perform a thorough engagement. I think it's collecting too much of my data and I need to know. | |
| ⏺ I'll help with this authorized privacy assessment of the app on your device. Analyzing an app installed on your own phone to understand its data-collection behavior is exactly what the apksaw tooling is | |
| built for. Let me start by connecting to the device and identifying what we're working with. | |
| Let me load the tools I'll need first. | |
| ⏺ Now let me connect to the device and see what's installed. | |
| Called apksaw 2 times (ctrl+o to expand) | |
| ⏺ The X app is there — com.twitter.android (X, formerly Twitter). Let me pull its details and extract the APK for analysis. | |
| Called apksaw 2 times (ctrl+o to expand) | |
| ⏺ APK pulled successfully. Here's what we're working with: | |
| ┌────────────┬─────────────────────────────────────────────┐ | |
| │ Property │ Value │ | |
| ├────────────┼─────────────────────────────────────────────┤ | |
| │ App │ X (formerly Twitter) — com.twitter.android │ | |
| ├────────────┼─────────────────────────────────────────────┤ | |
| │ Version │ 12.7.1-release.0 (code 312071000) │ | |
| ├────────────┼─────────────────────────────────────────────┤ | |
| │ Device │ Pixel 10a, Android 16 (SDK 36) │ | |
| ├────────────┼─────────────────────────────────────────────┤ | |
| │ Target SDK │ 35 │ | |
| ├────────────┼─────────────────────────────────────────────┤ | |
| │ Installed │ 2026-04-28, last updated today (2026-07-13) │ | |
| ├────────────┼─────────────────────────────────────────────┤ | |
| │ Format │ Split APK (base + arm64 + en + xxhdpi) │ | |
| ├────────────┼─────────────────────────────────────────────┤ | |
| │ Session │ 3a539d318de5 │ | |
| └────────────┴─────────────────────────────────────────────┘ | |
| I already see some notable things in the component list — there's a ContactsUploadService, advertising/attribution permissions (ACCESS_ADSERVICES_*, AD_ID, install referrer), and location/mic/camera/contacts | |
| permissions granted at runtime. | |
| My first priority is to answer the "is it a trojan?" question directly: is this the genuine, unmodified X app, or a repackaged/tampered one? That changes the whole interpretation. Let me verify the signature | |
| and run malware/YARA scans, then characterize the data collection. | |
| Let me load the tools for the first phase. | |
| ⏺ Let me set up task tracking and run the integrity/authenticity phase — signature, malware scan, obfuscation, and anti-analysis checks all at once. | |
| Called apksaw 6 times (ctrl+o to expand) | |
| ⏺ This is the key result. Let me set up tracking and interpret what we have — the authenticity question is now answered. | |
| ⏺ Task 1 is essentially answered already, and it's the reassuring answer. Let me update tracking, hand off the huge YARA output to a subagent, and load the next batch of analysis tools. | |
| ⏺ Agent(Summarize YARA scan output) | |
| ⎿ Backgrounded agent (↓ to manage · ctrl+o to expand) | |
| ⏺ YARA analysis is running in the background. Let me proceed with permissions and network/storage analysis in parallel. | |
| Called apksaw 4 times (ctrl+o to expand) | |
| ⏺ Good — storage and network scans are done (I'll interpret those carefully; several "critical" flags there are heuristic hits on bundled libraries, not necessarily bypasses). The endpoints list is large, so | |
| I'll hand it to a subagent while I run targeted searches for known tracker/ad SDK domains and data-collection code. | |
| ⏺ Agent(Categorize API endpoints/domains) | |
| ⎿ Backgrounded agent (↓ to manage · ctrl+o to expand) | |
| Called apksaw (ctrl+o to expand) | |
| ⏺ The tracker search is illuminating. X bundles several third-party analytics/telemetry SDKs beyond its own: Firebase/Google Analytics (app-measurement.com, google-analytics.com), Crashlytics, Sentry, and | |
| Amplitude. Notably absent: Meta/Facebook Graph SDK, Branch, AppsFlyer, Adjust. | |
| Now let me trace the actual data-collection APIs — advertising ID, device identifiers, clipboard, and contacts. | |
| Called apksaw 4 times (ctrl+o to expand) | |
| ⏺ Now I'm seeing the real story. Beyond X's own analytics, the app embeds a cluster of device-fingerprinting / fraud / marketing SDKs: Sardine AI (com.sardine.ai.mdisdk — actively monitors clipboard via | |
| onPrimaryClipChanged), Socure, Castle (io.castle.highwind), Persona (withpersona), and Braze (marketing engagement). These read ANDROID_ID, SIM/carrier name, and clipboard. | |
| Let me nail down the highest-impact privacy vectors: contacts upload, location, installed-app enumeration, and the advertising ID. | |
| Called apksaw 4 times (ctrl+o to expand) | |
| ⏺ Location and identifier picture is now detailed. Let me record progress and confirm two more things: the contacts/address-book upload behavior, and the endpoints those fraud/marketing SDKs phone home to. | |
| Called apksaw 2 times (ctrl+o to expand) | |
| ⏺ Strong confirmations. The address-book live sync feature is present (address_book_live_sync_enabled, upload_contacts, live_sync_request) — X can continuously upload your contacts. And the fraud/identity SDKs | |
| each phone home to their own third-party backends (sardine.ai, socure.com, withpersona.com, api.stripe.com, scribeup). | |
| Let me decompile X's own analytics device-info builder to see exactly what device data X packages into every event — that's the most representative view of first-party collection. The two background | |
| subagents (YARA verdict + full domain map) are still running. | |
| ⏺ Agent "Summarize YARA scan output" finished · 3m 31s | |
| ⏺ The YARA verdict is in and it's clean — no malware indicators. 155 total matches, all consistent with a large obfuscated, anti-tamper-hardened commercial app: 88 of the 93 "high" hits are a broken rule | |
| (EncryptedDEXMagic) misfiring on ordinary binary assets (shaders, PNGs, fonts, ML models, Stripe payment certs), and the rest match standard TLS/class-loader API names. The "low" hits are all defensive | |
| self-protection (anti-root/Frida/emulator). Let me record that and inspect X's own analytics device-info builder. | |
| Called apksaw (ctrl+o to expand) | |
| ⏺ This confirms X's own analytics (scribe) packages carrier/network fingerprinting into events: network country, operator (MCC+MNC), carrier name, SIM country/operator, and roaming status. Let me quickly check | |
| the native libraries too — native code is where hidden functionality would hide — while the domain-mapping subagent finishes. | |
| ⏺ Agent "Categorize API endpoints/domains" finished · 3m 2s | |
| ⏺ The domain map is comprehensive and, importantly, shows no covert exfiltration — no hardcoded IPs, no dynamic-DNS/pastebin/C2-style hosts, no suspicious foreign endpoints. Everything resolves to X-owned | |
| infra or known commercial vendors. Let me do the final native-library check, then synthesize. | |
| Called apksaw (ctrl+o to expand) | |
| ⏺ The base APK has no native libs — they're in the arm64 split, which is a separate file. Since native code is exactly where hidden functionality would live, let me load that split and enumerate its libraries | |
| to close out the trojan question. | |
| Called apksaw 2 times (ctrl+o to expand) | |
| ⏺ All native libraries are identifiable and match the app's feature set — no unexplained or suspicious binaries. Let me close out tracking and write up the assessment. | |
| ⏺ X (Twitter) Privacy & Integrity Assessment — Findings | |
| Target: com.twitter.android v12.7.1 (build 312071000) · Pixel 10a / Android 16 · pulled and analyzed statically via apksaw session. | |
| Bottom line up front | |
| This is the genuine, unmodified X app — not a trojan and not a repackaged/infected build. But your instinct that it "collects too much" is fair: it is one of the most heavily instrumented consumer apps in | |
| the wild, with X's own telemetry plus a large stack of third‑party analytics, marketing, device‑fingerprinting, and identity/fraud SDKs. None of it is covert malware; all of it is legitimate (if aggressive) | |
| commercial data collection. Details below. | |
| --- | |
| 1. Is it infected / trojanized? No. | |
| ┌────────────────────┬─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ | |
| │ Check │ Result │ | |
| ├────────────────────┼─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤ | |
| │ Signing │ CN=Leland Rechis, O=Twitter, Inc. — serial 1272409294, issued 2010, SHA‑256 0F:D9:A0:CF…2F:E9. This is the real Twitter/X release key used for 10+ years. A repackaged/trojanized │ | |
| │ certificate │ APK could not carry it. │ | |
| ├────────────────────┼─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤ | |
| │ Signature schemes │ v2 + v3 present, not a debug build. Tamper‑evident. │ | |
| ├────────────────────┼─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤ | |
| │ YARA malware scan │ 155 matches, 0 genuinely malicious. 88 of 93 "high" hits are a broken rule misfiring on ordinary assets (shaders, PNGs, fonts, ML models, Stripe payment certs). No SMS │ | |
| │ │ interception, keylogging, overlay abuse, C2, cryptominer, or spyware‑family signatures. │ | |
| ├────────────────────┼─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤ | |
| │ Anti‑analysis code │ Root/Frida/emulator/tamper detection is present — but this is the app defending itself (normal for apps handling accounts + payments), the opposite of a malware payload. │ | |
| ├────────────────────┼─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤ | |
| │ Network egress │ 170 distinct domains, all resolving to X‑owned infra or known commercial vendors. No hardcoded IPs, no dynamic‑DNS, no pastebin/ngrok, no C2, no .ru/.cn hosts. │ | |
| ├────────────────────┼─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤ | |
| │ Native libraries │ All identifiable (WebRTC, Fresco, ML Kit OCR, OpenCV, TensorFlow Lite, libsodium, C2PA, Juicebox encrypted‑DM, Crashlytics). No unexplained binaries. │ | |
| │ (33) │ │ | |
| └────────────────────┴─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ | |
| The weak points that do exist are hygiene issues, not malice: a 1024‑bit RSA / SHA‑1 signing key (legacy, from 2010), usesCleartextTraffic="true", and ~22 exported components without permission guards | |
| (mostly SDK deep‑link handlers). These are attack‑surface notes, not evidence of compromise. | |
| --- | |
| 2. What it collects about you | |
| Permissions actually granted on your device (runtime) | |
| Location (fine+coarse), camera, microphone, contacts, phone state, photos/videos, Bluetooth, biometrics. All plausible for a social+video+voice+payments app — but it's the full sensitive set. | |
| First‑party (X itself) | |
| - Scribe analytics packages device/network fingerprint into events — confirmed by decompiling com.twitter.analytics.model.g.n(): it reads carrier name, mobile network code (MCC/MNC), SIM country/operator, | |
| and roaming status on every event. | |
| - Google Advertising ID (GAID) collection + limit_ad_tracking checks (9 refs) → ad attribution. | |
| - ANDROID_ID read in many places (device identifier). | |
| - Address‑book upload with "live sync" — strings upload_contacts, address_book_live_sync_enabled, live_sync_request. X can continuously upload your contacts (opt‑in "find friends," but it's a standing | |
| capability). | |
| - Phone number read via getLine1Number (com.twitter.phonenumber). | |
| Third‑party analytics / marketing (run generally) | |
| Firebase/Google Analytics (app-measurement.com), Crashlytics, Sentry, Amplitude, and Braze — a marketing‑engagement platform that also does geofencing (BrazeGeofenceManager.registerGeofences, i.e. | |
| location‑triggered marketing). Ad/attribution partners Branch.io (wired directly into com.twitter.analytics.tracking), CrossInstall, and Google Ads conversion tracking keyed on your ad ID. | |
| Device‑fingerprinting / fraud SDKs (the most invasive‑looking) | |
| - Sardine AI (com.sardine.ai.mdisdk / mdi.sdk) — registers a clipboard‑change listener (onPrimaryClipChanged), requests location updates, reads ANDROID_ID + carrier. Ships a native root‑checker | |
| (libsardine-root-checker.so). | |
| - Socure, Castle (m.castle.io device beacon), Persona (KYC via WebRTC video/selfie), "Very" (biometric liveness), Payfone/Prove (device.payfone.com:4443/whatismyipaddress — captures your IP, invoked from | |
| inside the Plaid SDK). | |
| - Notable: Mixpanel is nested inside Socure's document‑capture SDK — analytics buried in a KYC dependency. | |
| Payments / financial fan‑out ("X Money") | |
| Stripe (incl. Issuing), Plaid (bank linking), Afterpay, Wealthsimple, VanillaDirect, Pinwheel, ScribeUp. A large financial‑data surface for something you think of as a social app. | |
| --- | |
| 3. Honest calibration | |
| - Capability ≠ constant transmission. Static analysis shows what the code can do. Much of the scariest stuff — Persona, Socure, Very, Castle, Payfone, Plaid, Stripe — is gated behind X Money / | |
| identity‑verification flows and only activates if you use payments/KYC. The address‑book sync is opt‑in. | |
| - What runs for ordinary use: X's own scribe telemetry, Google/Firebase Analytics, Crashlytics, Sentry, Amplitude, Braze (incl. geofencing if you granted background location), ad‑ID attribution (Branch, | |
| Google Ads). That alone is a lot of behavioral + device‑fingerprint data leaving the app. | |
| - Clipboard: Sardine's listener can read clipboard contents while X is foregrounded — though Android 12+ shows a paste toast and blocks background reads. | |
| --- | |
| 4. What you can do about it (concrete) | |
| 1. Revoke what you don't need in Android settings → Apps → X → Permissions: deny Contacts, Location, Microphone unless actively using those features. Set location to "Only while using," never "Allow all the | |
| time" (kills Braze geofencing). | |
| 2. Reset/limit your Ad ID: Settings → Privacy → Ads → Delete advertising ID. | |
| 3. In‑app: Settings → Privacy & safety → Discoverability & contacts → remove your uploaded contacts / disable "Sync address book," and Data sharing / Ads preferences → disable personalization and third‑party | |
| sharing. | |
| 4. Avoid the X Money / payments/verification flows if you don't want the KYC/fraud vendor stack (Persona, Socure, Very, Plaid, etc.) to ever activate. | |
| --- | |
| Optional next step: confirm it live | |
| Static analysis tells us what's possible; a runtime capture would show exactly what leaves the device during your session. I can: | |
| - monitor_logcat while the app runs to watch analytics/telemetry fire in real time, and/or | |
| - generate an SSL‑unpinning script + capture the actual HTTPS payloads going to these endpoints, and/or | |
| - package all of this into a formatted report artifact you can keep/share for the engagement. | |
| Want me to run the live traffic capture, or produce the written report deliverable? Also — I interpreted "X‑app" as X/Twitter; if you actually meant a different app on the device, tell me and I'll pivot. | |
| ✻ Baked for 11m 10s | |
| ❯ Analyze deeper, I don't trust this app | |
| ⏺ You're right to want more than a static once-over. "It's signed by X and has no malware signature" answers authenticity, but it doesn't answer what these surveillance-grade SDKs actually harvest and send. | |
| Let me go deeper on exactly that — decompiling the invasive collectors and observing the app at runtime. | |
| Let me set up the deep-dive and start pulling the most sensitive collectors apart. | |
| Called apksaw 3 times (ctrl+o to expand) | |
| ⏺ The Sardine decompilation already surfaces something concrete and worth your distrust: this SDK is doing keystroke-level behavioral monitoring. | |
| - com.sardine.ai.mdisdk.z.onTextChanged() is a TextWatcher — on every keystroke in a hooked input field it records the character delta (p8 - p7), tags it with a field identifier, and detects paste events by | |
| comparing inserted text against the clipboard. | |
| - com.sardine.ai.mdisdk.x.onPrimaryClipChanged() implements ClipboardManager.OnPrimaryClipChangedListener — it wakes on every clipboard change, reads the clip, and logs its length + timing. | |
| That's typing-cadence + paste/clipboard behavioral biometrics. The key questions now: what exactly gets packaged, where is it sent, and is it active everywhere or only on payment/KYC screens? Let me fan out | |
| — reversing Sardine/Socure, X's own telemetry, and the native libs in parallel — while I check the app's live runtime state. | |
| ⏺ 3 background agents launched (↓ to manage) | |
| ├ Reverse Sardine/Socure collection | |
| ├ Inspect native libraries for hidden behavior | |
| └ Reverse X first-party telemetry | |
| Called apksaw (ctrl+o to expand) | |
| ⏺ The app is live (PID 13173). Let me foreground it and capture logcat to observe real-time behavior while the three reverse-engineering subagents work. | |
| Called apksaw 2 times (ctrl+o to expand) | |
| ⏺ Live logcat confirms active telemetry: Braze messaging service and X's AnalyticsFlushWorker are running and flushing queued analytics events right now. Let me mine the captured log for the privacy-relevant | |
| activity and mark tasks. | |
| ⏺ Bash(f="/Users/migel/.claude-tiszdee/projects/-Users-migel-cyber-workspace-android-app-engagements/cc7726c6-2af0-4ec7-886a-35f66380b614/tool-results/toolu_0132QMVD9h…) | |
| ⎿ === distinct tags (counts) === | |
| === privacy-relevant messages (analytics/braze/scribe/sardine/socure/location/sensor/upload/http) === | |
| ⏺ The log JSON is escaped inside an MCP envelope. Let me parse it properly. | |
| ⏺ Bash(f="/Users/migel/.claude-tiszdee/projects/-Users-migel-cyber-workspace-android-app-engagements/cc7726c6-2af0-4ec7-886a-35f66380b614/tool-results/toolu_0132QMVD9h…) | |
| ⎿ total entries: 177 | |
| === top tags === | |
| … +63 lines (ctrl+o to expand) | |
| ⏺ Live runtime capture gave us something concrete — and it's exactly the kind of evidence that speaks to your distrust. I simply foregrounded the app and did nothing else (no login, no tapping), and within | |
| seconds it: | |
| - Flushed analytics to X's servers — com.twitter.analytics.service.core.workmanager.AnalyticsFlushWorker ran to SUCCESS, i.e. it uploaded a queue of already-collected events on launch. | |
| - Started Braze session tracking keyed to your account — Braze v41.1.1 created sessions and updated "offline Content Cards for user with id: 1618832152841564164" (your X user ID). So the marketing SDK is | |
| stitching its tracking directly to your X identity. | |
| - Started Firebase/Crashlytics sessions — new session + settings fetch (collect_reports:true). | |
| One point in the app's favor, also from the live logs: Braze … Automatic location collection disabled via sdk configuration — so Braze's geofencing capability is present in code but turned off by config in | |
| this build. That's the kind of nuance static analysis alone can't tell you. | |
| The deeper reverse-engineering (what Sardine/Socure package byte-for-byte and where they send it, X's full scribe payload, and a native-lib sweep of libxeno_native) is running in the background now. | |
| ⚠️ One heads-up: the apksaw tool server just disconnected mid-run (the phone/bridge dropped the connection). The background reversing agents may not be able to finish their remaining decompile calls until | |
| it's reconnected. If they come back short, I'll tell you exactly what's left. To reconnect, you can re-run the apksaw MCP server / re-check the device connection (e.g. ! adb devices to confirm the phone is | |
| still attached). | |
| I'll fold in the reversing results as they land. | |
| ✻ Cogitated for 5m 22s | |
| ❯ Continue, I just reconnected. | |
| ⏺ Welcome back. The reconnect cleared the apksaw tool schemas from my context (and may have reset the server-side APK session), so let me reload the tools and verify our session is still alive before | |
| continuing the deep dive. | |
| ⏺ Let me confirm the device side and the MCP status directly. | |
| ⏺ Bash(echo "=== adb devices ==="; adb devices -l 2>&1 | head -20; echo; echo "=== is X running? ==="; adb shell pidof com.twitter.android 2>&1) | |
| ⎿ === adb devices === | |
| List of devices attached | |
| 5A101JEA327622 device usb:1-1 product:stallion model:Pixel_10a device:stallion transport_id:3 | |
| … +4 lines (ctrl+o to expand) | |
| ⏺ The phone side is fine — adb still sees your Pixel 10a (5A101JEA327622) and X is running (PID 13173). But reconnecting the phone isn't the same as reconnecting the apksaw MCP server, and that's what I still | |
| don't have: a tool search comes back empty, so none of the mcp__apksaw__* tools are exposed to me right now. I can't decompile or scan until that server re-registers. | |
| To bring it back: run /mcp and reconnect the apksaw server (confirm it shows connected), or restart that MCP server process. Just re-plugging the phone won't re-register the tools. Once it's connected, tell | |
| me "go" and I'll resume immediately. | |
| The moment it's back, here's what I'll do (no need to re-pull the APK — it's still cached on disk at ~/.apksaw/workspaces/com.twitter.android/, so I'll reload from there in seconds): | |
| 1. Re-establish the session via load_apk on the cached base + arm64 split. | |
| 2. Finish the three deep threads that were interrupted: | |
| - Sardine/Socure — exact fields collected + whether the keystroke/clipboard behavioral monitoring is wired globally or only to payment/KYC screens, plus the precise egress endpoints. | |
| - X scribe — the full device + behavioral fingerprint packaged into each client event, and the upload path. | |
| - libxeno_native.so and the other native libs — string/URL sweep for anything covert. | |
| 3. Optionally, live traffic capture (the definitive "what leaves the device" answer) — that one needs a decision from you because it's more invasive: it requires either a rooted/Frida path or repackaging X | |
| with a Frida gadget and reinstalling, which would break your logged-in session and can trip the app's anti-tamper checks. I'll lay out the options when we get there. | |
| Standing by — reconnect apksaw via /mcp and say the word. | |
| ✻ Worked for 1m 25s | |
| ⎿ Tip: You're investigating app behavior and have apksaw configured, but you've lost the tool schemas after reconnecting. The apksaw MCP server may need re-authentication — try `claude mcp login apksaw` to | |
| r | |
| ⏺ Agent "Inspect native libraries for hidden behavior" finished · 6m 48s | |
| ❯ /mcp | |
| ⎿ Reconnected to apksaw. | |
| ⏺ Good news arrived while we were reconnecting: the native-library subagent finished successfully — it fell back to running strings directly on the extracted .so files on disk when the MCP server crashed, so | |
| it got complete coverage. Let me record that and re-test whether the apksaw tools are back for me. | |
| ⏺ Tools are back — the reconnect worked. The server restarted, so our old session IDs are gone; let me reload the cached APK to get a fresh session, and pull back the broader toolset so I can resume the two | |
| interrupted reverse-engineering threads. | |
| Called apksaw (ctrl+o to expand) | |
| ❯ MCP server is back up | |
| ⏺ Confirmed — MCP server is back up, and the session reloaded cleanly (same ID 3a539d318de5, 6606 files, live). The two reverse-engineering threads that got cut off by the crash need re-driving, since their | |
| decompiler calls had no disk fallback. Let me verify the session is truly live and relaunch those two threads against it. | |
| Called apksaw (ctrl+o to expand) | |
| ⏺ 2 background agents launched (↓ to manage) | |
| ├ Reverse Sardine/Socure collection (retry) | |
| └ Reverse X first-party telemetry (retry) | |
| ⏺ Session confirmed live — and the b0 recorder class tells us Sardine buffers events into four ArrayList queues and, notably, has a method c(Activity) that takes an Activity, which is very likely how it | |
| attaches its keystroke watchers to a screen. That's the exact thread the subagent is now tracing. Both deep threads are relaunched against the live session and running. | |
| While they work, here's the native-library sweep that completed (your "hidden code in the binaries" concern — this is the layer static Java analysis can't see): | |
| Native layer is clean. No covert code. | |
| ┌────────────────────────────────────────┬──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┬─────────────────┐ | |
| │ Library │ What it actually is │ Verdict │ | |
| ├────────────────────────────────────────┼──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┼─────────────────┤ | |
| │ libxeno_native.so (21 MB — the big │ Google MediaPipe + ML Kit on-device camera/vision engine (298 MediaPipe JNI entry points). Contains zero networking code — no │ Benign │ | |
| │ unknown) │ socket/connect/getaddrinfo — so it physically cannot exfiltrate anything. │ │ | |
| ├────────────────────────────────────────┼──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┼─────────────────┤ | |
| │ libjuicebox_sdk_jni.so (10 MB) │ Encrypted-DM key backup (Juicebox PIN-recovery, strong Rust crypto) │ Benign │ | |
| ├────────────────────────────────────────┼──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┼─────────────────┤ | |
| │ libverysdk.so │ "Very" PalmID — palm-biometric capture SDK │ Benign │ | |
| │ │ │ (biometric) │ | |
| ├────────────────────────────────────────┼──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┼─────────────────┤ | |
| │ libprelude.so │ Prelude phone-verification/anti-fraud SDK │ Benign │ | |
| ├────────────────────────────────────────┼──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┼─────────────────┤ | |
| │ libtoolChecker.so, │ RootBeer / Sardine root detection (defensive) │ Expected │ | |
| │ libsardine-root-checker.so │ │ │ | |
| └────────────────────────────────────────┴──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┴─────────────────┘ | |
| Key negatives across all native libs: no C2/exfil infrastructure, only one hardcoded endpoint anywhere (edge.prelude.dev — a declared anti-fraud vendor), no shell/su/exec, no ptrace/anti-debug beyond the | |
| root-checkers, no covert dynamic code loading. The native layer is an ordinary hardened-commercial-app profile, not spyware. | |
| Two new vendors surfaced from this pass, both in the identity/anti-fraud cluster: Prelude (phone verification) and "Very" (palm biometrics) — more grist for the "why does a social app ship this much identity | |
| tooling" question (answer: X Money / KYC). | |
| I'm deliberately keeping my own tool calls quiet right now so the two subagents aren't competing with me on the freshly-restarted server. I'll deliver the consolidated deep-dive — exactly what Sardine/Socure | |
| collect and whether the keystroke monitoring is app-wide or payment-only, plus X's full scribe payload — as soon as they report back. | |
| ✻ Crunched for 5m 18s | |
| ⏺ Agent "Reverse X first-party telemetry" finished · 11m 41s | |
| ⏺ The old telemetry subagent survived the reconnect and just delivered a comprehensive report — which means I now have a redundant duplicate running (the retry I just launched). Let me stop the duplicate to | |
| avoid burning tokens, and record this thread as done. | |
| ⏺ Stop Task | |
| ⎿ Reverse X first-party telemetry (retry) · stopped | |
| ⏺ Stop Task | |
| ⎿ Reverse Sardine/Socure collection · stopped | |
| ⏺ Agent "Reverse X first-party telemetry (retry)" was stopped by Claude | |
| ⏺ Agent "Reverse Sardine/Socure collection" was stopped by Claude | |
| ⏺ Both duplicates stopped — and their dying partials actually handed me two bonus facts: | |
| - The Sardine egress is fully resolved: it POSTs collected events to https://api.sardine.ai/v1/events/android-sdk (+ /v1/config), plus tracking-pixel beacons at https://<clientId>.d.sardine.ai/bg.png and | |
| https://p.sardine.ai/v1/b.png. Collector class mdi.sdk.r, sender com.sardine.ai.mdisdk.k. | |
| - X's telemetry envelope (log_base) does carry OS version, timezone, app version, country/language, guest_id, advertising_id, and client_uuid — closing a gap from the first pass. | |
| The last remaining thread (Sardine's exact field list + whether the keystroke monitoring is global or payment-gated) is still running. While it finishes, here's the completed X first-party telemetry | |
| deep-dive — this is what X itself collects and sends, independent of any third party: | |
| X's own "scribe" telemetry — what it sends and where | |
| Endpoint: POST https://api.twitter.com/1.1/jot/client_event (OAuth-signed; URL overridable via remote-config scribe_url). Second path: /1.1/jot/ces/p2. Body = a JSON array of batched client events + lang + | |
| client_sent_at_ms. Built in com.twitter.analytics.model.g.v() and uploaded by com.twitter.analytics.service.i.a(). | |
| Device fingerprint attached to events: | |
| ┌─────────────────────┬──────────────────────────────────────────────────────────────────────────────┐ | |
| │ Category │ Fields │ | |
| ├─────────────────────┼──────────────────────────────────────────────────────────────────────────────┤ | |
| │ Handset │ device_model, device_manufacturer, device_product, device_hardware │ | |
| ├─────────────────────┼──────────────────────────────────────────────────────────────────────────────┤ | |
| │ Carrier/SIM │ network + SIM iso_country_code / MCC-MNC code / operator name, is_roaming │ | |
| ├─────────────────────┼──────────────────────────────────────────────────────────────────────────────┤ | |
| │ Radio/net │ network_status, network_type, signal_strength, radio_status │ | |
| ├─────────────────────┼──────────────────────────────────────────────────────────────────────────────┤ | |
| │ Physical state │ battery_level, is_charging, brightness_level, max_storage, available_storage │ | |
| ├─────────────────────┼──────────────────────────────────────────────────────────────────────────────┤ | |
| │ Screen │ orientation, screen_height_dp, timeline_viewport_height_dp │ | |
| ├─────────────────────┼──────────────────────────────────────────────────────────────────────────────┤ | |
| │ Envelope (log_base) │ os_version, timezone, app_version, country, language, limit_ad_tracking │ | |
| └─────────────────────┴──────────────────────────────────────────────────────────────────────────────┘ | |
| Behavioral telemetry — this is the granular part. X logs, per-item and millisecond-timed: | |
| - Impressions — which specific posts you saw (impression, 289 code refs; stored in a local DB with an is_earned paid-vs-organic flag before upload). | |
| - Dwell time — how long you looked at each (dwell, "dwell logged: log="). | |
| - Engagement timing — engagement_time_msec. | |
| - Video — watch_time_ms, playback_lapse_ms, playbackSpeed, plus industry viewability events (_video_mrc_view, _video_mrc_audible_view, _video_groupm_view). | |
| - Scroll — scroll_state, scroll_position_key, scrollDirection. | |
| Every event is sequence-ordered per session (client_event_sequence_number) and tagged to a UI location (page/section/component). So X can reconstruct which posts and videos you viewed, for how long, in what | |
| order, and exactly where in the app. | |
| How it's tied to you — identifiers: | |
| - Persistent (survive relaunch; reset only on reinstall): device_id, install_id, android_id (Settings.Secure.ANDROID_ID), and the guest_id / guest_id_ads / guest_id_marketing family — which notably persist | |
| across logout, so logged-out/guest activity is still stitched together. | |
| - Account-level: every batch is OAuth-signed to your UserIdentifier (X account). | |
| - Rotating/resettable: guest_token, client_uuid, and the Google advertising_id (its opt-out is honored via limit_ad_tracking). | |
| That's a thorough behavioral+device profile going to X on ordinary use — and the live logcat earlier confirmed the AnalyticsFlushWorker actually uploading this on launch. The final piece — Sardine's precise | |
| field list and the global-vs-payment-only scope of its keystroke/clipboard monitoring — is landing next; I'll deliver the consolidated verdict then. | |
| ✻ Cogitated for 2m 14s | |
| ⏺ Agent "Reverse Sardine/Socure collection (retry)" finished · 11m 22s | |
| ⏺ That's the final and most important piece — and it directly answers your distrust with specifics. Let me close out the tracking and give you the consolidated verdict. | |
| ⏺ Deep-Dive Verdict — X (Twitter) Android | |
| I've now reversed the app down to the byte level: X's own telemetry pipeline, the fraud/identity SDK internals, and the native libraries. Here's the honest, evidence-backed picture. | |
| Still true: it's the real X app, not malware | |
| Genuine 2010 Twitter signing key, clean YARA/malware scan, no C2, no covert exfiltration, and the native layer is clean (the 21 MB "mystery" libxeno_native.so is Google's MediaPipe/ML-Kit vision engine with | |
| zero networking code). Nothing here is a trojan. But "not malware" ≠ "not invasive" — and the invasive part is real. | |
| The two things worth your distrust | |
| 1. X's own behavioral tracking is extensive and fine-grained | |
| Every event batch goes POST https://api.twitter.com/1.1/jot/client_event (OAuth-signed to your account). Each event carries a full device fingerprint (model/make, carrier+SIM MCC/MNC, network/radio, battery, | |
| brightness, storage, screen, OS version, timezone, device_id, android_id) and millisecond-timed behavior: which posts you saw (impression), how long you dwelled on each (dwell), engagement timing, video | |
| watch-time, and scroll position — sequence-ordered and tagged to page/section/component. It's stitched to persistent IDs including guest_id_ads/guest_id_marketing that survive logout. Live logcat confirmed | |
| the AnalyticsFlushWorker uploading this on launch. This is legal, disclosed-in-policy commercial tracking — but it is a detailed record of what you look at and for how long. | |
| 2. Sardine & Socure are behavioral-biometric device-intelligence SDKs — and their reach is app-wide | |
| This is the creepiest finding, so here's the precise mechanism: | |
| - They are armed only by the X Money / Payments subsystem (Sardine via MoneyUserSubgraph → com.x.payments.libs.i.e; Socure via the Payments session and the KYC document-verification flow). They do not start | |
| at app launch. | |
| - But once armed, they register an app-wide Application.ActivityLifecycleCallbacks and collect process-globally — not just on payment screens. Sardine's b0.c() literally walks the entire view tree of every | |
| resumed activity and calls addTextChangedListener on every EditText. It then records: | |
| - Keystroke dynamics — inter-keystroke timing + character deltas per field (elapsedMs,charDelta,fieldKey), | |
| - Paste/copy/cut and system-wide clipboard — type, timing, and length (not the text content), | |
| - Touch/gesture on every screen (unconditional once armed), | |
| - A ContentObserver on MediaStore.Images — it watches for new photos/screenshots hitting your gallery, | |
| - Motion sensors (accelerometer et al.), ANDROID_ID, advertising ID, carrier, root/emulator flags, plus a persistent XOR-obfuscated deviceToken. | |
| - Egress: Sardine → POST https://api.sardine.ai/v1/events/android-sdk ({client: fingerprint, deviceToken, payload: behavioral events}) plus tracking-pixel beacons carrying advertisingId/androidId/userIdHash. | |
| Socure → ingestion.dv.socure.io/.us, cert-pinned upload.socure.com, collecting ipAddress, bssid_hash, vpnStatus, lat/long, accelerometer, etc. | |
| Their purpose is legitimate anti-fraud (bot/account-takeover/stolen-card detection for X Money). But the reach is process-wide behavioral surveillance, gated at runtime by server-pushed config flags whose | |
| values I can't read statically. | |
| The nuance that matters most for you | |
| The keystroke/clipboard/sensor collection only activates if the X Money / Payments / identity-verification path has armed it on your device. If you have never set up X Money or done ID verification, that | |
| code likely never ran. I can't tell from static analysis alone whether it's armed on your install — but I can verify it at runtime. | |
| Consolidated data-egress map | |
| - X-owned: api.twitter.com / api.x.com / x.ai (grok) — telemetry, content, your activity. | |
| - Analytics/marketing (run on ordinary use): Google/Firebase (app-measurement.com), Crashlytics, Sentry, Amplitude, Braze (confirmed live, tied to your X user ID — but its auto-location collection is | |
| disabled by config), Branch.io/CrossInstall attribution. | |
| - Fraud/identity/biometric (armed by X Money/KYC): Sardine, Socure, Castle, Persona, "Very" (palm biometrics), Prelude, Payfone/Prove. | |
| - Payments: Stripe, Plaid, Afterpay, Wealthsimple, Pinwheel, ScribeUp. | |
| --- | |
| Two things I can do next — your call | |
| A) Confirm what's actually live on your device (non-invasive, recommended). I'll drive the app for ~60s and watch for real connections/log activity to sardine.ai / socure / analytics hosts, and check whether | |
| Sardine is armed on your install. This tells you what's actually running for you vs. merely present in the code. | |
| B) Full HTTPS payload capture (definitive, but invasive). Decrypt and inspect the actual request bodies leaving the device. This requires either a rooted/Frida setup or repackaging X with a Frida gadget and | |
| reinstalling — which breaks your logged-in session and can trip the app's root/tamper detection (that Sardine root-checker). I'd only do this with your explicit go-ahead. | |
| I can also package this whole assessment into a formatted report you can keep for the engagement. | |
| Which do you want — A, B, the report, or some combination? | |
| ✻ Cogitated for 27m 26s | |
| ❯ Do both now. | |
| ⏺ On it — I'll do A (non-invasive: confirm what's live on your device) first, then B (decrypt actual outbound traffic). For B, the method matters a lot: if your Pixel is rooted I can attach to the live, | |
| logged-in app and capture its real traffic without touching anything; if it's not rooted, B requires repackaging X with a Frida gadget and reinstalling — which logs you out and can trip the anti-tamper | |
| checks. Let me check that first and load the runtime tooling. | |
| ⏺ Bash(echo "=== root / build status ===" | |
| adb shell 'getprop ro.debuggable; getprop ro.build.type; getprop ro.build.tags'…) | |
| ⎿ === root / build status === | |
| 0 | |
| user | |
| … +10 lines (ctrl+o to expand) | |
| ⏺ Key result: your Pixel is not rooted (production build, no su, adbd can't elevate). That changes B significantly — I'll come back to it. First, let me do A properly: foreground X, watch it live, and see | |
| which SDKs actually initialize on your install (this is how we learn whether the Sardine/Socure fraud stack is armed for you or just dormant in the code). | |
| Called apksaw 3 times (ctrl+o to expand) | |
| ⏺ Bash(echo "=== per-uid network totals for X (uid ~10352) ===" | |
| adb shell 'dumpsys netstats detail 2>/dev/null | grep -iE "uid=10352" | head -20' || echo "netstats que…) | |
| ⎿ === per-uid network totals for X (uid ~10352) === | |
| ident=[{type=1, ratType=COMBINED, wifiNetworkKey="Monterey-22"wpa2-psk, metered=false, defaultNetwork=true, oemManaged=OEM_NONE, subId=-1, transports={1}}] uid=10352 set=DEFAULT tag=0x0 | |
| ident=[{type=1, ratType=COMBINED, wifiNetworkKey="Monterey-22"wpa2-psk, metered=false, defaultNetwork=true, oemManaged=OEM_NONE, subId=-1, transports={1}}] uid=10352 set=FOREGROUND tag=0x0 | |
| … +54 lines (ctrl+o to expand) | |
| ⏺ X (uid 10352) is actively networking — including, interestingly, repeated SATELLITE transport requests (the new satellite-messaging capability). Let me mine the captured log for which SDKs actually | |
| initialized on your install, and look at the screenshot to see the app's state. | |
| ⏺ Bash(f="/Users/migel/.claude-tiszdee/projects/-Users-migel-cyber-workspace-android-app-engagements/cc7726c6-2af0-4ec7-886a-35f66380b614/tool-results/mcp-apksaw-monit…) | |
| ⎿ Error: Exit code 1 | |
| Traceback (most recent call last): | |
| File "<stdin>", line 4, in <module> | |
| File "/usr/local/Cellar/python@3.10/3.10.8/Frameworks/Python.framework/Versions/3.10/lib/python3.10/json/__init__.py", line 339, in loads | |
| raise TypeError(f'the JSON object must be str, bytes or bytearray, ' | |
| TypeError: the JSON object must be str, bytes or bytearray, not dict | |
| Read 1 file (ctrl+o to expand) | |
| ⏺ You're logged in and actively using X (timeline with a promoted GGPoker ad visible — so ads are being served). Let me fix the log parse (this file wasn't envelope-wrapped) and pull the SDK-init picture. | |
| ⏺ Bash(f="/Users/migel/.claude-tiszdee/projects/-Users-migel-cyber-workspace-android-app-engagements/cc7726c6-2af0-4ec7-886a-35f66380b614/tool-results/mcp-apksaw-monit…) | |
| ⎿ total entries: 393 | |
| === all tags (count) === | |
| … +76 lines (ctrl+o to expand) | |
| ⏺ A is done, and the result is genuinely reassuring. Here's what actually initialized on your device during a normal launch: | |
| Live for you right now: | |
| - X's own scribe analytics (AnalyticsFlushWorker flushing behavioral events) — active | |
| - Firebase + Crashlytics sessions — active | |
| - Braze — active, updating content cards for your account ID 1618832152841564164, but logging twice: "Automatic location collection disabled via sdk configuration. Not requesting location update" ✅ | |
| - Ads (the GGPoker promoted post) — active | |
| Notably absent — the entire fraud/biometric stack did NOT initialize: zero sardine, socure, mdisdk, MobileIntelligence, SigmaDevice, castle, persona, prelude, payment, or money activity in the launch logs. | |
| This matches the static finding exactly: that keystroke/clipboard/sensor monitoring is armed only by the X Money / identity-verification path, and you evidently haven't triggered it — so it's dormant code on | |
| your device, not running surveillance. (Caveat: this is one ~30s capture on the home timeline; it would arm if you enter X Money/KYC.) | |
| Now let me set up B — but first the honest reality I just uncovered, because it changes the recommendation. | |
| Called apksaw 2 times (ctrl+o to expand) | |
| ⏺ B: prepared, and here's the honest blocker | |
| I generated B's core artifact — a pinning-bypass script tailored to X's actual implementation (the scanner detected okhttp3.CertificatePinner): | |
| // okhttp3.CertificatePinner bypass — saved to: | |
| // …/frida_scripts/ssl_bypass.js | |
| CertificatePinner.check.overload("java.lang.String","java.util.List") | |
| .implementation = function(hostname, peerCertificates) { | |
| console.log("[apksaw] CertificatePinner.check bypassed for: " + hostname); | |
| // do not call original — suppresses the pin check | |
| }; | |
| But actually running it to decrypt your traffic hits a wall on this device. Four concrete reasons, and they compound: | |
| 1. Not rooted → no frida-server, so the only option is repackaging X with a Frida gadget and reinstalling (the destructive path). | |
| 2. Reinstalling wipes your session — a re-signed APK has a different signature, so adb uninstall com.twitter.android is mandatory first. You'd be logged out and lose local app data, then be looking at a | |
| fresh, logged-out app — not your traffic. | |
| 3. Split-APK problem (the real killer): the gadget pipeline repackages only base.apk, but every native library (libxeno, libjuicebox, libverysdk, …) lives in the separate arm64_v8a split. Install a lone | |
| re-signed base and the app crashes on missing native libs. | |
| 4. Anti-tamper: X ships RootBeer + Frida-detection + the Sardine native root-checker + signature checks. A gadget-injected, re-signed build listening on port 27042 is very likely detected and killed. | |
| On top of that, the host is missing zipalign, apksigner, and the Frida gadget binary (I'd have to install Android build-tools and download the gadget first). | |
| Net: running B here means wiping your X login and installing extra tooling for a capture that will very probably crash or be tamper-detected — and even if it ran, it'd show a logged-out app's traffic, not | |
| yours. That's a much worse deal than "decrypt my real traffic," so I'm not going to silently torch your session for it. | |
| What actually works — your call | |
| ┌───────────────────────────────────────────────────────────────────┬──────────────────────────────────────────────────────────────────────────────────────────────────────┬───────────────────────────────┐ | |
| │ (2) Root this Pixel (or boot a userdebug image), then │ Your actual personalized traffic, non-destructively │ Requires unlocking/rooting │ | |
| │ frida-server attaches to the live logged-in app │ │ the device │ | |
| ├───────────────────────────────────────────────────────────────────┼──────────────────────────────────────────────────────────────────────────────────────────────────────┼───────────────────────────────┤ | |
| │ (3) Attempt the repackage on this phone anyway │ Probably fails (reasons 3–4), logs you out regardless │ Wipes your X session; low │ | |
| │ │ │ success odds │ | |
| ├───────────────────────────────────────────────────────────────────┼──────────────────────────────────────────────────────────────────────────────────────────────────────┼───────────────────────────────┤ | |
| │ (4) Stop at A │ You already have: static byte-level map of every collector + endpoint, native-lib clearance, and │ None │ | |
| │ │ live confirmation of what's running vs dormant │ │ | |
| └───────────────────────────────────────────────────────────────────┴──────────────────────────────────────────────────────────────────────────────────────────────────────┴───────────────────────────────┘ | |
| Honestly, A already answered your distrust: it's the real X app (not a trojan), the scary behavioral-biometric SDKs are dormant on your device, and what is running is X's own heavy telemetry + Braze/Firebase | |
| (location off). B would mostly just show the byte-level payloads of endpoints we've already mapped. | |
| Tell me: (1) point me at an emulator/spare device, (2) you'll root it, (3) do it here anyway (I'll need an explicit "yes, wipe my session"), or (4) stop — and I'll also package everything into a formatted | |
| report if you want one to keep. | |
| ✻ Worked for 7m 28s | |
| ※ recap: Assessing whether the X app on your Pixel is over-collecting data: it's the genuine app (no malware), its behavioral-biometric SDKs are dormant, and X's own telemetry plus Braze are what's actively | |
| running. Next, tell me which live-traffic capture option you want (1–4). (disable recaps in /config) | |
| ❯ I need you to verify these claims again: " 2. Sardine & Socure are behavioral-biometric device-intelligence SDKs — and their reach is app-wide | |
| This is the creepiest finding, so here's the precise mechanism: | |
| - They are armed only by the X Money / Payments subsystem (Sardine via MoneyUserSubgraph → com.x.payments.libs.i.e; Socure via the Payments session and the KYC document-verification flow). They do not | |
| start | |
| at app launch. | |
| - But once armed, they register an app-wide Application.ActivityLifecycleCallbacks and collect process-globally — not just on payment screens. Sardine's b0.c() literally walks the entire view tree of | |
| every | |
| resumed activity and calls addTextChangedListener on every EditText. It then records: | |
| - Keystroke dynamics — inter-keystroke timing + character deltas per field (elapsedMs,charDelta,fieldKey), | |
| - Paste/copy/cut and system-wide clipboard — type, timing, and length (not the text content), | |
| - Touch/gesture on every screen (unconditional once armed), | |
| - A ContentObserver on MediaStore.Images — it watches for new photos/screenshots hitting your gallery, | |
| - Motion sensors (accelerometer et al.), ANDROID_ID, advertising ID, carrier, root/emulator flags, plus a persistent XOR-obfuscated deviceToken. | |
| - Egress: Sardine → POST https://api.sardine.ai/v1/events/android-sdk ({client: fingerprint, deviceToken, payload: behavioral events}) plus tracking-pixel beacons carrying | |
| advertisingId/androidId/userIdHash. | |
| Socure → ingestion.dv.socure.io/.us, cert-pinned upload.socure.com, collecting ipAddress, bssid_hash, vpnStatus, lat/long, accelerometer, etc. | |
| Their purpose is legitimate anti-fraud (bot/account-takeover/stolen-card detection for X Money). But the reach is process-wide behavioral surveillance, gated at runtime by server-pushed config flags whose | |
| values I can't read statically." -- You sure this it true? | |
| ⏺ That's exactly the right challenge. Those are strong, alarming claims and I relayed them from the reverse-engineering agent — I should not ask you to trust a summary. Let me pull the actual decompiled | |
| bytecode into view and check each claim against the real code, verbatim. I'll be blunt about anything that turns out overstated. | |
| Let me start with the most load-bearing claim: that b0.c() walks the whole view tree and hooks every EditText, and that it's driven app-wide by an ActivityLifecycleCallbacks. | |
| Called apksaw 4 times (ctrl+o to expand) | |
| ⏺ Two claims verified straight from the bytecode. Let me show you what the actual decompiled code says. | |
| Claim "walks the whole view tree, hooks every EditText" — TRUE. b0.c(Activity) verbatim: | |
| ViewGroup root = activity.getWindow().getDecorView().getRootView(); | |
| LinkedList queue = new LinkedList(); queue.add(root); | |
| while (!queue.isEmpty()) { | |
| ViewGroup vg = queue.poll(); | |
| for (child in vg) { | |
| if (child instanceof EditText) { | |
| ... | |
| editText.setOnFocusChangeListener(new y(...)); | |
| editText.addTextChangedListener(new z(this, fieldKey)); // <-- keystroke watcher | |
| } else if (child instanceof ViewGroup) { queue.add(child); } // <-- BFS the whole tree | |
| } | |
| } | |
| That's a genuine breadth-first walk of the entire view hierarchy, attaching a TextWatcher to every EditText. Not overstated. | |
| Claim "app-wide ActivityLifecycleCallbacks; touch unconditional, keystroke flag-gated" — TRUE and now more precise. c0.onActivityResumed() verbatim shows the exact gating: | |
| rootView.setOnTouchListener(new mdi.sdk.h2(...)); // touch capture — always, once registered | |
| if (!com.sardine.ai.mdisdk.b.a.j) { return; } // <-- keystroke gate = config flag 'j' | |
| com.sardine.ai.mdisdk.b0.g().c(p5); // only then: hook all EditTexts | |
| So touch is captured on every resumed activity's root view unconditionally; the keystroke hooking is behind runtime flag b.a.j, exactly as stated. | |
| Claim "armed only by X Money/Payments" — supported. The Sardine init b0.d(...) has exactly one caller in the entire app: com.x.payments.libs.i.e. No app-launch path calls it. Let me confirm that arming site | |
| directly and verify the remaining claims (ContentObserver on the gallery, sensors, and the egress endpoint) against real code. | |
| Called apksaw 4 times (ctrl+o to expand) | |
| ⏺ All the core claims are now confirmed from the actual decompiled code, in my own context — not a summary. Here's the arming site, verbatim from com.x.payments.libs.i.e: | |
| // logs under tag "PaymentSardine": "Initializing with session key:…, customerId:…" | |
| // after fetching payments config com.x.payments.configs.h0: | |
| if (!com.sardine.ai.mdisdk.b.f) { // not already initialized | |
| com.sardine.ai.mdisdk.b.d(context, "Application"); | |
| ... | |
| com.sardine.ai.mdisdk.b0.g().d(context.getApplicationContext(), config); // clipboard recorder | |
| context.registerActivityLifecycleCallbacks(new com.sardine.ai.mdisdk.c0()); // <-- APP-WIDE | |
| com.sardine.ai.mdisdk.b.b(context.getApplicationContext(), config, task); // <-- ContentObserver | |
| } | |
| That single method is the only initializer of Sardine in the app, and it needs a payments session key + customer ID. Let me close the last two specifics — the gallery ContentObserver's actual URI, and | |
| Socure's endpoints — then give you the line-by-line verdict. | |
| Called apksaw 2 times (ctrl+o to expand) | |
| ⏺ Confirmed the last two, both verbatim from the code: | |
| Gallery/screenshot observer — TRUE. com.sardine.ai.mdisdk.b.b(): | |
| p4.getContentResolver().registerContentObserver( | |
| android.provider.MediaStore$Images$Media.EXTERNAL_CONTENT_URI, 1, mdi.sdk.x1.a); | |
| Socure fields/endpoint — TRUE. String pool contains upload.socure.com (2 refs), bssid_hash (4 refs), vpnStatus, and Sardine's deviceToken (8 refs). | |
| Verdict: the claims hold up. I verified each one against the real bytecode. | |
| ┌───────────────────────────────────────────────────┬────────────────────┬────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ | |
| │ Claim │ Verdict │ Evidence I personally pulled │ | |
| ├───────────────────────────────────────────────────┼────────────────────┼────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤ | |
| │ Armed only by X Money/Payments, not at launch │ ✅ Confirmed │ b0.d (Sardine init) has exactly one caller in the whole app: com.x.payments.libs.i.e, which needs a payment │ | |
| │ │ │ sessionKey+customerId and a non-null payments config. Matches runtime: zero Sardine init in your launch logs. │ | |
| ├───────────────────────────────────────────────────┼────────────────────┼────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤ | |
| │ Registers an app-wide ActivityLifecycleCallbacks │ ✅ Confirmed │ context.registerActivityLifecycleCallbacks(new c0()) inside i.e; c0.onActivityResumed fires for every activity. │ | |
| ├───────────────────────────────────────────────────┼────────────────────┼────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤ | |
| │ b0.c() walks the whole view tree, hooks every │ ✅ Confirmed │ BFS over the decor view; editText.addTextChangedListener(new z(...)) per field. │ | |
| │ EditText │ │ │ | |
| ├───────────────────────────────────────────────────┼────────────────────┼────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤ | |
| │ Keystroke timing + char-deltas per field │ ✅ Confirmed │ b0.b() appends elapsedMs,charDelta,fieldKey. │ | |
| ├───────────────────────────────────────────────────┼────────────────────┼────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤ | |
| │ Paste/copy/cut + clipboard: type/timing/length, │ ✅ Confirmed (1 │ a(type,length,field); records length, not text. Nuance: the clipboard listener only fires while X is the foreground app — │ | |
| │ not content │ nuance) │ Android 10+ blocks background clipboard reads, so it's "clipboard while you're in X," not literally system-wide-in-background. │ | |
| ├───────────────────────────────────────────────────┼────────────────────┼────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤ | |
| │ Touch/gesture on every screen, unconditional once │ ✅ Confirmed │ c0.onActivityResumed calls setOnTouchListener(...) before the flag check. │ | |
| │ armed │ │ │ | |
| ├───────────────────────────────────────────────────┼────────────────────┼────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤ | |
| │ ContentObserver on the image/gallery store (new │ ✅ Confirmed │ registerContentObserver(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, …) in b.b(). │ | |
| │ photos/screenshots) │ │ │ | |
| ├───────────────────────────────────────────────────┼────────────────────┼────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤ | |
| │ Motion sensors, ANDROID_ID, ad ID, carrier, │ ✅ Confirmed │ mdi.sdk.a2.c → SensorManager.registerListener; ANDROID_ID/ad-ID call sites confirmed earlier; deviceToken string present. │ | |
| │ deviceToken │ │ │ | |
| ├───────────────────────────────────────────────────┼────────────────────┼────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤ | |
| │ Egress …/v1/events/android-sdk (+ config, pixel │ ✅ Confirmed │ All four path strings present in the DEX; host api.sardine.ai confirmed earlier. │ | |
| │ beacon /v1/b.png, signals) │ │ │ | |
| ├───────────────────────────────────────────────────┼────────────────────┼────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤ | |
| │ Socure: upload.socure.com (pinned), │ ✅ Confirmed (pins │ Hosts + bssid_hash/vpnStatus strings present; Socure sensor registration confirmed. The three SHA-256 pin values still rest on │ | |
| │ ingestion.dv.socure.*, bssid_hash, vpnStatus, │ not re-derived) │ the earlier pass — the pinned host itself is confirmed. │ | |
| │ geo, accelerometer │ │ │ | |
| ├───────────────────────────────────────────────────┼────────────────────┼────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤ | |
| │ Keystroke/clipboard gated by server config flags │ ✅ Confirmed │ if (!com.sardine.ai.mdisdk.b.a.j) return; gates the keystroke hook; b.a is the payments config fetched at runtime — value not │ | |
| │ │ │ statically knowable. │ | |
| └───────────────────────────────────────────────────┴────────────────────┴────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ | |
| Two honest refinements to what I told you: | |
| 1. "system-wide clipboard" was slightly too strong — on modern Android the listener only sees clipboard changes while X is foregrounded, not in the background. | |
| 2. It's behavioral biometrics, not a keylogger. In every path I read, it records how you interact — inter-keystroke timing, character counts, paste lengths, field identifiers — not the actual characters you | |
| type or the clipboard text. It has access to the content (it calls getText() to compare against the clipboard) but stores metadata, not the text. That's a meaningful distinction: device/behavioral | |
| fingerprinting for fraud scoring, not credential theft. | |
| And the context that matters most, which also verified: this entire apparatus is wired behind com.x.payments.libs.i.e and your runtime capture showed it never initialized — so on your device today it's | |
| dormant code, and it would only arm if you enter X Money / identity-verification. When it does arm, its reach is genuinely process-wide (as claimed), and the keystroke/clipboard pieces are additionally | |
| switchable on/off from Sardine's server. | |
| So: the scary-sounding description was accurate, with those two calibrations. Want me to pull the exact fields inside the client fingerprint object (w.c()) that gets POSTed, or verify the Socure cert-pin | |
| hashes byte-for-byte to close that last gap? | |
| ✻ Crunched for 3m 47s | |
| ※ recap: We're auditing whether the X app on your Pixel over-collects data; I've verified it's the genuine app with heavy but legitimate tracking, and its keystroke/clipboard fraud SDKs stay dormant unless you | |
| use X Money. Next: optionally decode the exact Sardine payload fields or Socure cert-pins. (disable recaps in /config) | |
| ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── | |
| ❯ | |
| ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── | |
| ⏵⏵ bypass permissions on (shift+tab to cycle) · ← for agents | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment