{"articles":[{"author":"Mark Esler","content":"A MetaMask Android user enters their password, the wallet unlocks, they put the phone down. Thirty seconds later the autolock fires and the password screen returns. MetaMask\u0026rsquo;s UI indicates the keys are no longer reachable. The password the user just typed is in the process heap. So is the 12-word BIP39 mnemonic — in indexed form after every unlock, in contiguous plaintext from any session in which the user has viewed their seed phrase. MetaMask\u0026rsquo;s onboarding requires every new wallet to view and confirm it.\nThis is the same vulnerability class as Halborn\u0026rsquo;s 2022 Demonic finding — CVE-2022-32969 — in the MetaMask browser extension. Halborn\u0026rsquo;s disclosure described the extension bug\u0026rsquo;s attacker model as \u0026ldquo;an attacker with equal or greater permissions than the user may be able to retrieve the mnemonic phrase directly from memory\u0026rdquo; — a local-memory threat on the browser\u0026rsquo;s disk surface, not requiring root. MetaMask patched it in extension v10.11.3. The Android client does not meet that 2022 bar.\nFrom the source code MetaMask/metamask-mobile is an open-source repository on GitHub. The mobile app\u0026rsquo;s authentication module contains a function named wipeSensitiveData, called at ten sites to clear password and seed variables after use. The function, with the comment above it, at app/core/Authentication/Authentication.ts:295-303 — reformatted here for column width:\n/** * This method is used for password memory obfuscation * It simply returns an empty string so we can reset all the sensitive * params like passwords and SRPs. Since we cannot control memory in JS * the best we can do is remove the pointer to sensitive information in * memory * - see this thread for more details: * https://security.stackexchange.com/questions/192387/how-to-securely-erase-javascript-parameters-after-use * [Future improvement] to fully remove these values from memory we can * convert these params to Buffers or UInt8Array as is done in extension * - see: https://github.com/MetaMask/metamask-extension/commit/98f187c301176152a7f697e62e2ba6d78b018b68 */ private wipeSensitiveData = () =\u0026gt; \u0026#39;\u0026#39;; The function does one thing: return an empty string. In JavaScript, a variable holds a pointer to data in memory — not the data itself. Assigning a new value moves the pointer; the original bytes remain exactly where they were. There is no API to zero a string\u0026rsquo;s underlying bytes; only the runtime decides when to reclaim them. The comment names the fix: represent the seed phrase as a Uint8Array instead of a string. Typed arrays can be explicitly zeroed with .fill(0) — the same approach the extension commit implements — provided the engine does not elide the fill as a dead store; a read-after-write or a named secureZero wrapper makes the intent explicit and optimizer-resistant.\nThe lock orchestration in LockManagerService.#lockApp calls KeyringController.setLocked() and dispatches a Redux action. No buffer wipe.\nRuntime evidence Three places in memory hold key material in plaintext or near-plaintext until those heap regions are reallocated:\nAllocation Form Trigger Lifetime RN bridge onChange JSON {\u0026quot;text\u0026quot;:\u0026quot;...\u0026quot;}, one per keystroke, cumulative Every password keystroke Until heap reallocation KeyringController…keyrings[i].mnemonic Uint8Array of BIP39 word indices submitPassword decryption on every unlock Until heap reallocation HdKeyring.serialize intermediates space-separated plaintext String + TextEncoder bytes + Array copy Vault encrypt, or Reveal Secret Recovery Phrase Until heap reallocation Reaching the space-separated plaintext form requires HdKeyring.serialize to fire, which happens on Reveal SRP or vault encryption. MetaMask\u0026rsquo;s onboarding requires every new user to view and confirm their seed phrase to proceed past wallet creation, so every install has triggered this allocation at least once.\nReproduction: Android 14 emulator, io.metamask v7.76.0 from Google Play, adb root. Wallet unlocked, Settings → Security \u0026amp; Privacy → Reveal Secret Recovery Phrase opened, seed phrase displayed. Heap scan via Frida\u0026rsquo;s Memory.scanSync or a static NDK binary reading /proc/\u0026lt;pid\u0026gt;/mem — either works.\nResult, wallet unlocked:\n[FOUND] mnemonic at 0x7b23459f30 7b23459f30 70 79 72 61 6d 69 64 20 73 75 72 67 65 20 72 69 pyramid surge ri 7b23459f40 67 68 74 20 76 61 70 6f 72 20 63 68 69 63 6b 65 ght vapor chicke 7b23459f50 6e 20 72 65 61 64 79 20 73 6f 6c 64 69 65 72 20 n ready soldier 7b23459f60 77 61 6c 6c 20 77 68 61 74 20 66 69 74 20 73 79 wall what fit sy 7b23459f70 6d 62 6f 6c 20 62 69 74 74 65 72 00 00 00 00 00 mbol bitter..... Background the app, wait 30 seconds for autolock, verify the password-entry screen is showing. Re-scan the same PID:\n[FOUND] mnemonic at 0x7b23459f30 7b23459f30 70 79 72 61 6d 69 64 20 73 75 72 67 65 20 72 69 pyramid surge ri ... setLocked ran; the UI shows the locked screen; the mnemonic sits at the same address, unchanged. In a separate test the mnemonic persisted for ten minutes with the app backgrounded — zero bytes changed across the entire window. Re-unlocking allocates a fresh copy at a new address without zeroing the prior one:\n[FOUND] mnemonic (2 hit(s)) 0x788e025330 ← new copy, current unlock 0x788e08ee10 ← old copy, prior unlock — never zeroed The leak is cumulative. Each unlock/lock cycle adds another plaintext copy. adb shell am force-stop io.metamask followed by re-scan returns zero hits — heap residue, not disk persistence. The vulnerability window opens at unlock and closes on heap reallocation or process termination.\nThe password also reaches memory through a separate path. The React Native bridge serializes every TextInput onChange event as JSON containing the full password text typed so far:\n[FOUND] password event at 0x34328a9055 0034328a9045 75 6e 74 22 3a 33 34 2c 22 74 65 78 74 22 3a 22 unt\u0026#34;:34,\u0026#34;text\u0026#34;:\u0026#34; 0034328a9055 68 65 6c 6c 6f 64 61 77 67 22 7d 28 00 00 1a 00 hellodawg\u0026#34;}(.... Each keystroke generates a separate bridge event. None are zeroed. The password traverses the bridge as a series of plaintext copies. Closing this path requires bypassing onChangeText entirely — secureTextEntry={true} masks the UI but does not prevent the bridge from serializing each keystroke as a plaintext JS String.\ndroidsaw audit of io.metamask v7.76.0 confirms zero secure-erase primitives — secureZero, secureWipe, zeroize, memzero, secureClear — across the 245K-string bundle, including the bundled @metamask/keyring-controller, @metamask/eth-hd-keyring, and hdkey dependencies. The bundled hdkey does define HDKey.prototype.wipePrivateData, which zeroes private-key bytes correctly via buffer.fill(0); no caller from the lock path invokes it. The shipping APK behaviour matches the public source.\nDemonic vs Demonic Mobile Same vulnerability class — key material persisting beyond its intended lifecycle — in a different surface and a different medium.\nDemonic (CVE-2022-32969) Demonic Mobile (this finding) Surface MetaMask browser extension MetaMask Android, io.metamask v7.76.0 Medium Browser session-restore (disk) Hermes heap + RN bridge events (memory) Persistence Across browser restart, survived uninstall Unlock to heap reallocation; cumulative per unlock cycle Fix Buffer/integer-array SRP representation, extension v10.11.3, 2021-07-30 None; mobile-app comment labels it \u0026ldquo;[Future improvement]\u0026rdquo; MetaMask statement \u0026ldquo;The following does not impact MetaMask Mobile users.\u0026rdquo; — June 2022 — Disclosure outcome Accepted, patched, public CVE Closed Not Applicable The 2023 mobile-app source comment names the 2021 extension commit as the solution — same engineering organisation, same npm dependency tree, same documented fix. The mobile app has not adopted it.\nIn the 2022 disclosure MetaMask co-founder Dan Finlay cited Jeffrey Goldberg (1Password, Principal Security Architect) from a 2019 interview on ISE\u0026rsquo;s Windows password manager research:\n\u0026ldquo;This is a well-known issue that\u0026rsquo;s been publicly discussed many times before, but any plausible cure may be worse than the disease. Fixing this particular problem introduces new, greater security risks, and so we have chosen to stick with the security afforded by high-level memory management, even if it means that we cannot clear memory instantly.\u0026rdquo;\nFinlay quoted the excerpt above. The interview continued:\n\u0026ldquo;Long term, we may not need to make such a tradeoff. But given the tools and technologies at our disposal, we have had to make a decision as to how best to keep our users secure.\u0026rdquo;\n\u0026ldquo;The realistic threat from this issue is limited. An attacker who is in a position to exploit this information in memory is already in a very powerful position. No password manager (or anything else) can promise to run securely on a compromised computer.\u0026rdquo;\nGoldberg was describing 1Password7 on Windows in 2019. MetaMask\u0026rsquo;s extension fix had shipped eleven months before the disclosure. 1Password\u0026rsquo;s latest Android client (v8.12.12) is native Kotlin; its OpaqueString type wraps every sensitive field with byte-level control.\nThreat model Vector Access needed Stolen device, USB debugging on adb shell Co-installed app + Android RCE-to-root chain Prior exploit Forensic seizure (LE, border control) Physical device, lab tooling Crash report / analytics SDK heap snippet None additional — vendor-side capture Memory pressure → page swap → flash (devices with swap enabled) Disk image of swap-backed pages None require both root and Frida. For the forensic-seizure row in particular: 1Password Android uses a dedicated native type (OpaqueString) for sensitive strings — masterPassword, secretKey, paperKey — with byte-level control at lock. MetaMask\u0026rsquo;s UI documents the same defence at the lock screen. No code implements it.\nDisclosure surface This bug was reported to MetaMask through HackerOne because that is the only reporting channel MetaMask offers. The org-level Security Policy that GitHub renders on every MetaMask repository — including MetaMask/metamask-mobile — states the rule in one sentence: \u0026ldquo;Report the security issue on our MetaMask HackerOne program.\u0026rdquo; No alternate email, no PGP key, no CERT designation. Neither metamask.io/security.txt nor /.well-known/security.txt — the discoverable paths RFC 9116 defines for security contact, policy, and acknowledgments — is implemented. The GitHub Security Advisory channel that MetaMask has used in the past for at least one disclosure is not offered as an inbound reporting path. Single channel by design.\nHackerOne is a bug bounty platform; its triage applies bounty-program rules. The report was closed as Not Applicable under MetaMask\u0026rsquo;s Attacks Requiring a Compromised Device exclusion clause. The PoC\u0026rsquo;s adb root reproduction path gave the exclusion a clean landing; the crash-report and analytics-SDK vector in the threat model requires no compromised device and survives it. That close-out applies to the bounty program\u0026rsquo;s payment terms. Under MetaMask\u0026rsquo;s HackerOne program rules, reports that highlight a risk the team commits to addressing are closed Informative — immediately added to the security backlog. Not Applicable carries no such commitment. Report #3652547 was closed Not Applicable.\nBug bounty programs are payment mechanisms, not disclosure frameworks. RFPolicy, a precursor to modern coordinated disclosure, establishes that when a vendor closes the reporting channel without a remediation plan, full disclosure follows. For a vulnerability affecting every install of io.metamask v7.76.0, the single-channel design leaves users with no path to remediation.\nTimeline Date Event 2021-07-30 Extension fix 98f187c30117 lands: SRP converted to integer-array representation for proper buffer wipe. 2022-06-15 MetaMask/Halborn public disclosure of the extension issue. MetaMask states: \u0026ldquo;The following does not impact MetaMask Mobile users.\u0026rdquo; 2023-02-24 Mobile-app wipeSensitiveData = () =\u0026gt; '' stub and accompanying [Future improvement] comment introduced in PR #5374 (\u0026ldquo;Authentication refactor\u0026rdquo;). 2026-04-06 Report #3652547 submitted to MetaMask via HackerOne. Full PoC, no-Frida reproduction path, Demonic precedent cited. Weakness classified as Insufficiently Protected Credentials. 2026-04-30 Researcher follow-up: 24 days since submission, requesting a concrete plan by 2026-05-06. 2026-05-07 Report closed Not Applicable. Reason cited: \u0026ldquo;requires physical access… root access and Frida instrumentation\u0026rdquo;; excluded as \u0026ldquo;rooted device conditions.\u0026rdquo; 2026-05-16 Public disclosure. Source comment, lock path, and shipping APK behaviour re-verified on the current Google Play release (v7.76.0) and the current main branch of MetaMask/metamask-mobile. Mitigations For MetaMask Android users on v7.76.0: the autolock is not a security boundary against an attacker with /proc/pid/mem access. Force-stop the app (adb shell am force-stop io.metamask or Settings → Apps → MetaMask → Force Stop) when lock state matters. Disable USB debugging when not actively using it. Move significant balances to a hardware wallet; treat the mobile wallet as hot.\nFor MetaMask: the source comment names the fix; the linked extension commit implements it. Apply the Buffer / Uint8Array representation in wipeSensitiveData. Iterate the keyrings on lock and call the wipePrivateData code that already ships in the dependency tree. Handle the password field at the native layer and bypass onChangeText — secureTextEntry={true} masks the UI but each keystroke still traverses the RN bridge as a plaintext JS String.\nMetaMask patched the browser extension on 2021-07-30 and publicly disclosed on 2022-06-15 that the same vulnerability did not affect Mobile users. The Android app\u0026rsquo;s source labelled the equivalent fix [Future improvement] on 2023-02-24. The HackerOne report was closed Not Applicable on 2026-05-07. The stub private wipeSensitiveData = () =\u0026gt; ''; is in main today. Passwords are in the heap.\n","date":"2026-05-16","description":"MetaMask Android v7.76.0 does not zero the seed phrase or vault password after autolock. Same class as Demonic (CVE-2022-32969). Fix named in source since 2023.","readingTime":10,"title":"MetaMask Demonic Mobile: seed phrase survives autolock on Android","url":"https://hexproof.dev/datagrams/metamask-demonic-mobile-android/","wordCount":2058},{"author":"Mark Esler","content":"A notification appears on the victim\u0026rsquo;s locked phone with whatever text the abuser typed. The victim taps it. Fifteen seconds later, the front camera silently takes a photo, the phone records where it is, and any other actions the abuser set up run too. The victim sees none of it.\nThe lock-screen notification is one of many triggers. Cerberus runs whenever the phone does almost anything — turns on, turns off, gets unlocked, joins a different network, installs an app, crosses a place the abuser marked, picks up movement. Each run schedules the next one. Even if the abuser hasn\u0026rsquo;t logged in for weeks, Cerberus is still running all day — recording when it was set to, saving any photos or recordings to upload later, and picking up where it left off after a restart. The 44 remote commands the abuser can send from cerberusapp.com are on top of that.\nThe app is Cerberus Anti-theft, sold by LSDroid SRL of Milan, Italy, on a €5/month subscription. Google Play has hosted the current version since October 4, 2023.\nThe companion app Lock Screen Protector (com.lsdroid.lsp) is published by the same Google Play developer account. It requests the accessibility-service permission — the most sensitive permission on Android. Once granted, it reads all screen content, performs gestures, and captures screenshots. When the user tries to turn the phone off, it intercepts the power dialog, dismisses it, and broadcasts a screenshot of the lock screen to Cerberus. Both apps are live on Google Play today.\nWhat the operator can do The abuser — whom LSDroid\u0026rsquo;s dashboard calls the \u0026ldquo;operator\u0026rdquo; — can, from a web dashboard at cerberusapp.com or a paired smartwatch:\nSilently photograph using the front or rear camera, with or without flash Record video and audio from the microphone Record the screen Stream GPS location continuously Read the victim\u0026rsquo;s contacts, call log, and SMS history Send SMS messages from the victim\u0026rsquo;s phone, and place phone calls as the victim Display a chosen message on the victim\u0026rsquo;s lock screen Sound an alarm Lock the device Wipe internal storage or the SD card Launch any installed app; run any Tasker automation Intercept and hide incoming SMS matching a configured keyword Disable USB debugging remotely, to block investigators from connecting forensic tools Trigger a fake shutdown — a pixel-perfect Android power-off screen that dims the display to 1/255 brightness and blacks out, while the phone stays fully awake with the camera, microphone, and GPS active The abuser can hand Cerberus a Wi-Fi network name — a feature LSDroid calls \u0026ldquo;radar\u0026rdquo;. After that, when the victim\u0026rsquo;s phone comes within signal range of that network, Cerberus tells the abuser. Wi-Fi beacon scanning works indoors, in subways, inside buildings — including domestic-violence shelters, hospitals, courthouses, and police stations, where GPS is unreliable but Wi-Fi names are predictable. If a victim visits a shelter with a known Wi-Fi name, the abuser is alerted instantly.\nIf the victim\u0026rsquo;s phone has Tasker installed — a popular Android automation app — Cerberus can fire Tasker macros from seven distinct event categories: power-button press, wrong device-admin password, geofence crossing, motion-sensor trigger, location update, background-service lifecycle, and battery events. Tasker\u0026rsquo;s permissions are typically broader than Cerberus\u0026rsquo;s own. Whatever Tasker is set up to do, Cerberus can trigger.\nHow Cerberus got back on Google Play Cerberus is developed by LSDroid SRL in Milan, Italy. Its founder, Luca Sagaria, was interviewed by The Verge in April 2013, which at the time had 150,000 licenses sold, two engineers, and one customer service representative.\nIn November 2017, Google emailed LSDroid citing a Malicious Behavior Policy violation. In May 2018, Cornell Tech and NYU researchers named Cerberus as intimate-partner-violence spyware in their IEEE S\u0026amp;P paper and reported the apps to Google. Google did not act on the stalkerware finding. Months later, Google removed com.lsdroid.cerberus from Play, citing a different policy — \u0026ldquo;Apps that cause users to download or install applications from unknown sources outside of Google Play are prohibited\u0026rdquo;. LSDroid responded on their own website (archived snapshot) that they would \u0026ldquo;not bother to appeal\u0026rdquo; because \u0026ldquo;the full-featured app\u0026rdquo; would remain \u0026ldquo;always available on our website.\u0026rdquo; LSDroid had no Play Store presence for the next five years.\nIn 2020, Cerberus accounted for 52% of all stalkerware detections tracked by F-Secure globally — the single most detected stalkerware family on earth that year. Kaspersky has named Cerberus in five consecutive annual stalkerware reports. The Coalition Against Stalkerware tracks it through the stalkerware-indicators IOC database.\nThe return in October 2023 — three years after Google\u0026rsquo;s Stalkerware Policy took effect — was the same app under a new name (com.lsdroid.cerberus → com.ssurebrec), with the off-store link removed and the surveillance largely intact. Almost everything the 2018 paper named still runs on the Play app today.\nThe Play APKs ship one notable extra: an open-source library called HiddenApiBypass, whose purpose is to defeat Android\u0026rsquo;s restrictions on reaching internal system services that apps aren\u0026rsquo;t supposed to reach. The library\u0026rsquo;s own README opens with an explicit instruction to developers, verbatim:\nGoogle Play doesn\u0026rsquo;t allow apps to use hidden APIs, reporting library usage will cause your app to fail app review, you need to disable dependencies info reporting in build.gradle.\nThe README then publishes the exact build setting developers need to hide the library from Play\u0026rsquo;s automated review. LSDroid applied the setting. The library is now in three of the five Cerberus apps live on Google Play, and verifying its presence is one shell command per APK:\nunzip -l com.ssurebrec.apk | grep org/lsposed/hiddenapibypass unzip -l com.lsdroid.lsp.apk | grep org/lsposed/hiddenapibypass unzip -l com.lsdroid.cerberus.kids.apk | grep org/lsposed/hiddenapibypass Google has three enforcement surfaces Google Play distributes the apps. Five Cerberus apps are live on the Play Store under the LSDroid developer account. Three of them ship the HiddenApiBypass library, each a separate violation of the Malicious Behavior Policy. The capability set documented above — silent camera capture, microphone recording, GPS streaming, SMS reading, lock-screen lure, fake shutdown — meets every clause of Google\u0026rsquo;s Stalkerware Policy.\nGoogle AdMob pays LSDroid for ads served inside Cerberus. The Play Store APK contains a RewardedInterstitialAd with publisher ID pub-9848961826628138. Google\u0026rsquo;s own public compliance file at realtimebidding.google.com/sellers.json lists that publisher:\n{ \u0026#34;seller_id\u0026#34;: \u0026#34;pub-9848961826628138\u0026#34;, \u0026#34;seller_type\u0026#34;: \u0026#34;PUBLISHER\u0026#34;, \u0026#34;name\u0026#34;: \u0026#34;LSDroid SRL\u0026#34;, \u0026#34;domain\u0026#34;: \u0026#34;cerberusapp.com\u0026#34; } Google Payments has LSDroid SRL\u0026rsquo;s legal registration and billing address on file because Google wires them money. Google AdMob and Google Play both serve the same legal entity. The evidence is entirely in Google\u0026rsquo;s own public compliance data. AdMob is also the dominant ad channel for stalkerware as a category: 99% of ad-monetized stalkerware apps in a 6,432-app academic study (Gibson et al., PoPETs 2022) use Google AdMob.\nGoogle Firebase hosts the command-and-control (C2) backend. Five Firebase projects, all under the same developer account, host the Firebase Cloud Messaging (FCM) command channels — the mechanism by which an abuser sends a command like \u0026ldquo;take a photo\u0026rdquo; or \u0026ldquo;wipe the device\u0026rdquo; to an installed Cerberus — and the operator-state Realtime Database that synchronizes operator dashboards with installed devices. Suspending those projects under the Firebase Terms of Service would disable the remote-command channel on every active Play install simultaneously.\nAll three actions are available to Google unilaterally on the basis of public evidence. None have been used. Google was notified ahead of publication. The indicators of compromise from this research were submitted upstream to the stalkerware-indicators IOC database used by MVT, Quad9, AdGuard, TinyCheck, and MISP, so end-user detection works on every consumer of that feed regardless of platform-side enforcement.\nThe companion apps: Kids and \u0026ldquo;Women safe\u0026rdquo; Cerberus is not one app. The same Google Play developer account ships Kids (com.lsdroid.cerberus.kids), a \u0026ldquo;child safety\u0026rdquo; companion. Kids declares its own Android accessibility service. The system permission dialog the user must accept to grant that service states: \u0026ldquo;No data is collected or sent.\u0026rdquo; The Play Store listing closes its description with the same line. The data safety section on the same page lists four data types collected. The same app schedules a StatisticsWorker that uploads the child\u0026rsquo;s app-usage history, location, and activity to LSDroid\u0026rsquo;s backend every 30 minutes.\nThe Kids APK is signed with a 2009 self-issued RSA-1024 certificate in the name Luca Sagaria, minted four years before LSDroid SRL was incorporated — the only Cerberus app on Play still signed under a personal name with no corporate linkage.\nThe same account also ships Persona2 (com.lsdroid.cerberus.persona2), titled on the Play Store listing as \u0026ldquo;Personal security — Women safe.\u0026rdquo; Persona2 reads its device identity from the main stalkerware via the same unauthenticated ContentProvider that the Kids app uses. Persona2 refuses to register with the C2 if the main stalkerware isn\u0026rsquo;t installed. Both feed the same backend with the same device ID. The same developer account sells the \u0026ldquo;women safe\u0026rdquo; product, the children\u0026rsquo;s monitoring product, and the covert stalkerware. Same C2 domain.\nRegulatory exposure Stalkerware enforcement has prior FTC precedent. Google distributes, hosts, and monetizes a product fitting the same pattern.\nVendor Regulator action Outcome Retina-X Studios (MobileSpy / PhoneSheriff / TeenShield) 2019 FTC settlement — first stalkerware-vendor enforcement Required to demonstrate purchaser-consent verification before resuming sales; voluntarily shut down after 2018 data breach SpyFone (Support King) 2021 FTC settlement Banned from the surveillance industry; first FTC ban of a stalkerware vendor; followed 2018 data breach The FTC has acted on commercial stalkerware developers twice — Retina-X (2019, consent-verification requirement) and SpyFone (2021, industry ban). Cerberus has been on Google Play continuously since October 4, 2023 — distributed, hosted, and monetized by Google — despite fitting both FTC settlements\u0026rsquo; fact patterns: hidden launcher icon, no purchaser consent verification, fake shutdown, and capability set targeted at intimate-partner surveillance.\nDespite EFF urging the FTC to investigate similar networks like TheTruthSpy in 2022, no public FTC action followed; Cocospy — part of the TheTruthSpy network — went offline in May 2025 only after a data breach forced it. A WIRED investigation in April 2026 found a Cocospy repository publicly accessible a year after shutdown — 90,000 screenshots from one victim\u0026rsquo;s phone.\nIn Europe, Google Play is a designated Very Large Online Platform under the Digital Services Act; enforcement authority sits with the European Commission directly, with defined timelines once an EU citizen files a notice. Stalking statutes specifically covering tech-enabled surveillance exist across the globe — South Korea\u0026rsquo;s 2021 Stalking Punishment Act (expanded in 2023 to cyberstalking), India\u0026rsquo;s Section 354D (which explicitly covers monitoring of electronic communication), and Brazil\u0026rsquo;s 2021 stalking statute (Article 147-A of the Penal Code) among them. Google Play distributes globally; an abuser\u0026rsquo;s criminal exposure depends on where the abuse occurs.\nThis is the short version of a much deeper reverse engineering of the Cerberus stalkerware ecosystem.\nHow victims discover it When the abuser\u0026rsquo;s €5/month subscription lapses, Cerberus reads the timestamp on the next boot, finds it expired, and reverses its concealment. It cancels its hidden-mode notifications, builds a notification with a \u0026ldquo;Buy\u0026rdquo; button, re-enables the launcher icon, stops its background service, and relinquishes its device-admin claim — allowing the victim to uninstall normally. An unfamiliar app called Cerberus suddenly appears in the launcher with a persistent notification asking the user to renew a subscription they never bought.\nIf you are reading this because something like that just happened on a phone you use, the next section is for you.\nWhat a victim can do If you are in an abusive situation: contact the National Domestic Violence Hotline (1-800-799-7233 in the US) or the Coalition Against Stalkerware before doing anything on the device — even checking can alert the abuser. Cerberus and the Kids app report permission changes to the operator in real time. Removal can also destroy forensic evidence useful for a protection order or criminal complaint. Consider contacting from a different device or a trusted person\u0026rsquo;s device; the compromised phone is itself a risk surface — it intercepts SMS, reads notifications, and (with Lock Screen Protector installed) sees every screen. Cornell Tech\u0026rsquo;s Clinic to End Tech Abuse (CETA), the NNEDV Safety Net Project, Operation Safe Escape, and WESNET (Australia) are DV-aware technologists who can plan a removal with the survivor. Outside the US: regional helplines. Safety planning first.\nOnce you have support in place, the two package names a DV-aware technologist would look for in Settings → Apps (with \u0026ldquo;Show system apps\u0026rdquo; enabled) are:\ncom.ssurebrec — the current Play Store version. May appear as \u0026ldquo;Cerberus Anti-theft\u0026rdquo; or, in the disguised variant, as \u0026ldquo;System Framework\u0026rdquo; with a generic Android icon. com.lsdroid.lsp — the Lock Screen Protector companion. Appears as \u0026ldquo;Lock Screen Protector.\u0026rdquo; If either is present on a phone, the technologist will also check Settings → Accessibility for services from those packages and Settings → Security → Device admin apps for Cerberus. These checks are not a step a survivor should take alone on a compromised device — Lock Screen Protector reads every screen and reports permission changes to the operator in real time.\nThe full reverse engineering covers adb shell detection commands, the complete package-name list including companion apps (Kids, Persona2, Enterprise), network indicators, filesystem artifacts, and the SharedPreferences keys that mark active surveillance. If you are a reporter, an enforcement engineer, a detection-tool maintainer, or a technical contact for a DV support organization, that is the version you want.\nA pixel-perfect Android shutdown screen runs while the camera does. The same LSDroid developer account ships the APK on Google Play, takes payment from Google AdMob, and runs its command channel on Google Firebase — three Google products, one corporate identity, all visible in Google\u0026rsquo;s own public records. Cornell Tech and NYU named the app as intimate-partner-violence stalkerware in 2018; Google removed it under an unrelated policy. Google\u0026rsquo;s Stalkerware Policy took effect October 1, 2020. Cerberus came back to Play October 4, 2023.\n","date":"2026-04-30","description":"Cerberus on Google Play: €5/month buys silent camera, microphone, GPS, and SMS access on a victim's phone. Researchers reported it to Google as intimate-partner-violence spyware in 2018; Google removed it later that year on an unrelated policy and relisted it in 2023. Google AdMob pays the developer; Google Firebase hosts the command-and-control backend.","readingTime":11,"title":"Cerberus is stalkerware. Google Play hosts it.","url":"https://hexproof.dev/datagrams/cerberus-on-play/","wordCount":2274},{"author":"Mark Esler","content":"There is an app on Google Play called Lock Screen Protector (com.lsdroid.lsp). It requests accessibility service permissions — the most sensitive permission on Android. Once granted, it reads all screen content, performs gestures, and takes screenshots. It monitors for the power dialog and dismisses it — the phone cannot be turned off. It blocks the notification shade — airplane mode cannot be enabled. When someone tries to power off the device, it captures a screenshot and sends the JPEG bytes to another app: Cerberus.\nBackground Cerberus Anti-theft by LSDroid — not the unrelated banking trojan on Wikipedia\u0026rsquo;s Cerberus (Android) — is one of the longest-running stalkerware applications in the Android ecosystem. It has been a documented intimate-partner-violence threat since at least 2018, when researchers from Cornell Tech and NYU named it as one of two Play Store apps that \u0026ldquo;violate Play Store policy by hiding their app icon and showing no notifications, making them as covert as off-store spyware\u0026rdquo; in their IEEE S\u0026amp;P paper The Spyware Used in Intimate Partner Violence.\nEchap\u0026rsquo;s stalkerware-indicators database — the IOC feed used by MVT, Quad9, AdGuard, TinyCheck, and MISP — has listed Cerberus with 8 package names and the cerberusapp.com domain since its first release. This research led to commit fa248462 on 2026-04-10, which added com.lsdroid.lsp and com.lsdroid.cerberus.enterprise among other indicators to ioc.yaml. Every consumer of the database now sees both halves of the LSP ↔ main IPC pair.\nCerberus has been around since 2011. In April 2013, The Verge argued Google should ship lost-phone features as a built-in Android service, interviewing Luca Sagaria of LSDroid as one of the third-party fillers of that gap — 150,000 licenses sold, two engineers, one customer service representative. The same Luca Sagaria whose personal signing certificate (CN=Luca Sagaria, SHA-256 6B:CE:75:CE) signs the Kids app today. Four months later, in August 2013, Google launched Android Device Manager (now Find Hub, 1B+ Play Store installs) and made remote location, ring, lock, and wipe a free, built-in Android feature. The anti-theft product category Cerberus had been selling into became a platform feature that shipped on every Android phone.\nGoogle knows who these people are. In November 2017, Google emailed LSDroid citing a Malicious Behavior Policy violation for cloaking functionality and linking to external APKs. LSDroid posted the email publicly. In May 2018, the Chatterjee et al. IEEE S\u0026amp;P paper documented that Cerberus \u0026ldquo;provides all [anti-theft] functionality, along with a remote Android shell in its web portal\u0026rdquo; and reported the apps to Google. Google did not act on the stalkerware finding. Some months later — LSDroid\u0026rsquo;s own archived statement places the start in November 2018 — Google removed com.lsdroid.cerberus from the Play Store, citing a different policy: \u0026ldquo;Apps that cause users to download or install applications from unknown sources outside of Google Play are prohibited.\u0026rdquo; Google enforced against the off-store-distribution business model, not against the surveillance capability set the paper had named.\nLSDroid\u0026rsquo;s response, on their own website (archived snapshot): they would \u0026ldquo;not bother to appeal\u0026rdquo; because \u0026ldquo;the full-featured app\u0026rdquo; is \u0026ldquo;always available on our website.\u0026rdquo;\nIn 2020, Cerberus accounted for 52% of all stalkerware detections tracked by F-Secure — the single most detected stalkerware globally. On October 4, 2023, it returned to Google Play under a new package name (com.ssurebrec). LSDroid srl, Milan, same developer account. Their website describes the Play Store version as having \u0026ldquo;a slightly smaller set of commands.\u0026rdquo; The direct-download version remains free on cerberusapp.com. The removal-to-return gap was 5 years 5 months.\nThe Play APKs ship an open-source Android-modding library — HiddenApiBypass, published April 2021 — whose own maintainers explicitly warn developers that shipping it on Google Play is a policy violation.\nWhat \u0026ldquo;a slightly smaller set of commands\u0026rdquo; actually means, in the bytecode of the live Play Store APK, is what this writeup documents.\nLSDroid SRL on Google\u0026rsquo;s records LSDroid is not hiding. The Cerberus Play Store listing at play.google.com/store/apps/details?id=com.ssurebrec displays its developer publicly — \u0026ldquo;Cerberus • LSDroid srl\u0026rdquo;, 100K+ downloads, 4.2 stars, 2.28K reviews. The company is named on the listing itself, not behind a shell-name front. The \u0026ldquo;LS\u0026rdquo; in LSDroid almost certainly stands for the founder\u0026rsquo;s initials — Luca Sagaria — paired with \u0026ldquo;Droid\u0026rdquo; for Android; the company name itself encodes the personal identity behind it.\nThe same legal entity is the named beneficiary in Google\u0026rsquo;s AdMob compliance file. com.surebrec.Login.onCreate calls RewardedInterstitialAd.load() with a hardcoded ad-unit ID ca-app-pub-9848961826628138/7883431316 — Cerberus monetizes the Play Store version with a Google AdMob rewarded-interstitial ad that plays when the user opens the login screen without biometrics. The publisher ID is 9848961826628138. Google\u0026rsquo;s IAB-standard compliance file at https://realtimebidding.google.com/sellers.json (981,141 publishers as of this writing) maps that publisher ID to LSDroid SRL:\n{ \u0026#34;seller_id\u0026#34;: \u0026#34;pub-9848961826628138\u0026#34;, \u0026#34;seller_type\u0026#34;: \u0026#34;PUBLISHER\u0026#34;, \u0026#34;name\u0026#34;: \u0026#34;LSDroid SRL\u0026#34;, \u0026#34;domain\u0026#34;: \u0026#34;cerberusapp.com\u0026#34; } LSDroid SRL is, in Google\u0026rsquo;s own public compliance data, the entity being paid for ad impressions served inside Cerberus. In Italian corporate law \u0026ldquo;SRL\u0026rdquo; stands for Società a Responsabilità Limitata or a limited liability company. Google Payments has LSDroid SRL\u0026rsquo;s legal registration, bank details, and billing address on file because Google sends them money.\nThe same entity signs the APKs. droidsaw signing on com.surebrec v3.7.6 (the WearOS companion downloaded directly from cerberusapp.com) yields a v1 certificate with subject CN=LSDroid srl, C=IT and SHA-256 digest B6:74:AB:75:7F:83:23:C1:07:35:A6:00:BB:06:F9:03:54:F6:8F:4E:E6:1C:71:BA:F2:27:95:FA:2C:C9:11:E6. The same SHA-256 digest also signs com.ssurebrec v1.4.9 (Play Store main) and com.surebrec v3.8.0 standard (direct-download). All three are cryptographically bound to the same RSA-2048 private key held by LSDroid SRL in Milan, Italy.\nTwo apps, one product Cerberus (LSDroid srl) ships two versions.\nThe Google Play version (com.ssurebrec v1.4.9, targetSdk 36, €5/month) passes review, handles billing, and executes 44 remote commands via Firebase Cloud Messaging (FCM): silent front- and rear-camera capture, microphone recording, continuous GPS tracking, device wipe, lockout, launcher-icon concealment, fake shutdown, a wrong-unlock photo trigger, a dial-code re-entry mechanism, and a Wi-Fi-BSSID proximity tracker that fires when the victim\u0026rsquo;s phone comes within range of specified networks. It delegates accessibility-service permission to a second Play Store app by the same developer, Lock Screen Protector (com.lsdroid.lsp), which reads every screen, captures screenshots, intercepts power-dialog presses, and blocks the notification shade. Together, v1.4.9 + LSP are the deployment on current victims\u0026rsquo; phones.\nThe direct-download version (com.surebrec v3.8.0, targetSdk 26, free) is distributed from cerberusapp.com. On a rooted phone it is a standalone rootkit: Runtime.exec(\u0026quot;su\u0026quot;) invoked from 33 distinct callers, an interactive WebSocket shell at wss://cerberusapp.com:8443, an SMS command channel that intercepts its own messages before they reach the victim\u0026rsquo;s inbox, keylogging via a built-in accessibility service labeled \u0026ldquo;System Framework,\u0026rdquo; notification suppression, and a SuCommands toolkit that silently grants 16 permissions and blocks uninstallation via reflected Android framework APIs. Same command-and-control (C2) infrastructure, same operator dashboard, same subscription as the Play version.\nThreat model: which version a victim is likely running The threat model relevant to most current victims is the Play Store build (com.ssurebrec v1.4.9 + com.lsdroid.lsp v3.6). It installs cleanly, updates automatically, doesn\u0026rsquo;t trigger Play Protect, and doesn\u0026rsquo;t need root. The abuser buys a €5 subscription, installs it during a moment of physical access, grants the permissions, enables device admin, installs Lock Screen Protector alongside it, and hides both from the launcher. The phone looks normal. The abuser controls it from cerberusapp.com. The capabilities documented later in this writeup that explicitly require root — the SuCommands reflected-binder-stub toolkit, /system/etc/cerberus_conf.xml factory-reset survival, status-bar disable from arbitrary code paths — apply only to the v3.8.0 direct-download build on rooted devices.\nSilent sign-in is built into the Login activity. com.surebrec.Login.onCreate reads user and pass extras from the launching Intent; when both are present, onStart auto-fills the fields and invokes the login button\u0026rsquo;s OnClickListener.onClick programmatically. An abuser launching com.ssurebrec/com.surebrec.Login via ADB or a provisioning APK passes credentials as Intent extras and the sign-in completes without any on-screen interaction. The same activity sets a null ActivityManager.TaskDescription(null, null, color) which removes the app from the Android recents screen — a victim swiping through recents after the moment of access sees a blank card instead of the Cerberus login.\nOn rooted phones with unlocked bootloaders, v3.8.0 writes configuration to /system/etc/cerberus_conf.xml — a system-partition path that survives factory reset. Modern Android\u0026rsquo;s verified boot prevents the write; on older or unlocked devices, a factory reset does not remove Cerberus.\nFor detection tools and indicators of compromise (IOCs), both package names matter: com.ssurebrec for current victims, com.surebrec for historical and rooted-device cases. And com.lsdroid.lsp matters for both — it\u0026rsquo;s the common denominator across all deployment scenarios.\nThe HiddenApiBypass policy violation v3.8.0\u0026rsquo;s rootkit capabilities — silent permission grant, uninstall block, status-bar disable, notification suppression, power-menu control, background-network-throttle bypass — depend on Java reflection against internal Android binder stubs (IPackageManager, IStatusBarService, IPowerManager, IDevicePolicyManager, INotificationManager, INetworkPolicyManager, ILockSettings, IPermissionManager).\nSince Android 9 (API 28), reflection on these classes is blocked by default. v3.8.0 targets targetSdk 26, which receives Android\u0026rsquo;s hidden-API leniency, so its SuCommands reflection chain runs without a bypass library. The Play builds (com.ssurebrec, LSP, Kids) target targetSdk 36, do not ship SuCommands, and do not invoke the reflection chain.\nThe Play-Store builds nonetheless ship a library whose class-level annotation declares it exists specifically to defeat the Android 9+ hidden-API restriction. The decompiled source tree of com.ssurebrec v1.4.9 contains the file org/lsposed/hiddenapibypass/i.java, whose first declared field is a static final sun.misc.Unsafe. The library is HiddenApiBypass, published by the LSPosed Android-modding project, Apache-2.0-licensed, actively maintained, distributed on Maven Central as org.lsposed.hiddenapibypass:hiddenapibypass. Its class-level @RequiresApi(Build.VERSION_CODES.P) annotation targets Android 9+ — the exact Android versions where the hidden-API restriction exists. Its presence in a Play APK is the policy violation, independent of how narrowly the library is actually used downstream.\nA note on naming. The \u0026ldquo;LSP\u0026rdquo; acronym appears twice in this writeup. LSPosed is the open-source Android-modding project above. Lock Screen Protector (com.lsdroid.lsp) is LSDroid\u0026rsquo;s accessibility-service companion app.\nHow the library works, and what it silences The library obtains sun.misc.Unsafe via Unsafe.class.getDeclaredMethod(\u0026quot;getUnsafe\u0026quot;).invoke(null), uses it to read internal memory offsets inside the Android Runtime (ART), and constructs a reference to the hidden dalvik.system.VMRuntime.setHiddenApiExemptions(String[]) — itself a hidden API, reached via the Unsafe path. It then calls setHiddenApiExemptions(\u0026quot;\u0026quot;) with an empty-string prefix that matches every class.\nThe javadoc on setHiddenApiExemptions:\nAll matching APIs are treated as if they were on the whitelist: access permitted, and no logging.\nThe \u0026ldquo;no logging\u0026rdquo; half matters separately. The bypass also silences Android\u0026rsquo;s own audit trail for non-SDK-API usage — the telemetry channel Google Play Protect uses to detect hidden-API calls after install. A Play-side scanner on a Cerberus-installed device sees nothing, because the library told the runtime to stop reporting.\nWhat the bypass is used for, and how many Play apps ship it HiddenApiBypass ships in three of the five Cerberus Play Store apps: com.ssurebrec, com.lsdroid.lsp, and com.lsdroid.cerberus.kids. Persona2 and Enterprise do not ship the library. The v3.8.0 direct-download APK doesn\u0026rsquo;t ship it either — targetSdk 26 receives Android\u0026rsquo;s hidden-API leniency, and v3.8.0\u0026rsquo;s SuCommands reflection chain runs natively on that leniency.\nA whole-DEX call-graph search across each of the three Play apps finds exactly one external caller of the renamed setHiddenApiExemptions entry point in each. All three callers follow the same shape:\nif (!Build.MANUFACTURER.equalsIgnoreCase(\u0026#34;xiaomi\u0026#34;)) { throw new Exception(\u0026#34;Not a Xiaomi device\u0026#34;); } if (Build.VERSION.SDK_INT \u0026gt;= 28) { org.lsposed.hiddenapibypass.\u0026lt;X\u0026gt;.b(new String[]{ \u0026#34;\u0026#34; }); } activated = true; The trigger differs per app:\ncom.ssurebrec: Lw0/e;-\u0026gt;\u0026lt;init\u0026gt;(FakeShutSetupActivity), called from FakeShutSetupActivity.onResume() during initial fake-shutdown configuration. com.lsdroid.lsp: LI2/e;-\u0026gt;\u0026lt;init\u0026gt;(SettingsActivity), called from LSP\u0026rsquo;s settings screen. com.lsdroid.cerberus.kids: Lcom/lsdroid/cerberus/kids/f;-\u0026gt;\u0026lt;init\u0026gt;(Context), called from the Kids app\u0026rsquo;s onboarding flow. In every case the immediately-following use is identical: reflect on android.miui.AppOpsUtils.getApplicationAutoStart(Context, String), a Xiaomi MIUI internal hidden API, to query whether the app has been granted MIUI\u0026rsquo;s autostart permission. MIUI aggressively kills background apps that lack autostart permission; each Cerberus app needs to know its own autostart-permission state to know whether its persistence is going to work on a Xiaomi device. Each app runs in its own Android process, so each needs its own copy of the library and its own activation.\nThe demonstrated use is narrow — a single Xiaomi-OEM-specific permission probe per app, gated on MANUFACTURER == \u0026quot;xiaomi\u0026quot;, behind a static one-shot flag. The library\u0026rsquo;s broader capability is gated open (the empty-prefix setHiddenApiExemptions(\u0026quot;\u0026quot;) call exempts every class once activated), but the only subsequent hidden-API call this writeup observes in any of the three Play apps is the MIUI AppOpsUtils reflection. Whether other call sites exist through Class.forName-style indirection or runtime-loaded code isn\u0026rsquo;t externally verifiable from static analysis.\nThree Play Store apps each ship org.lsposed.hiddenapibypass; each ship requires the README\u0026rsquo;s dependenciesInfo evasion snippet at build time; each passed Play review without the library being flagged. The find . -path '*org/lsposed/hiddenapibypass/*' grep test runs against any of the three APKs independently, and each grep is a separate dispositive policy citation.\nThe library\u0026rsquo;s own README warns against Play deployment The HiddenApiBypass README.md documents:\nGoogle Play doesn\u0026rsquo;t allow apps to use hidden APIs, reporting library usage will cause your app to fail app review, you need to disable dependencies info reporting in build.gradle.\nThe README then publishes the exact build.gradle evasion snippet:\nandroid { dependenciesInfo { includeInApk = false includeInBundle = false } } com.ssurebrec v1.4.9 is live on Play, its APK ships the library, and Play\u0026rsquo;s automated dependency reporting, apparently, does not flag it. That combination requires applying the evasion snippet at build time — the snippet documented in the library\u0026rsquo;s own README.\nTimeline: the rebuild window and the relisting Cerberus was removed from Google Play in late 2018 / early 2019, citing the off-store-distribution policy — months after the Chatterjee et al. IEEE S\u0026amp;P paper named it as intimate-partner-violence spyware in May 2018. It returned in October 2023. The five-year gap reflects LSDroid\u0026rsquo;s choice of when to repackage and resubmit; the surveillance capability set the paper named was achievable in 2018 and remains achievable through documented Android APIs without any hidden-API bypass. The chronology of LSDroid\u0026rsquo;s actual rebuild window is anchored on certificate-issuance dates, not on library availability:\nDate Event May 2018 Chatterjee et al. IEEE S\u0026amp;P paper names Cerberus as IPV spyware; researchers report to Google Late 2018 / early 2019 Google Play removes com.lsdroid.cerberus, citing off-store-distribution policy — not the stalkerware finding 2018 – 2023 Cerberus is direct-download-only; no Play presence 2021-04-07 Initial commit of AndroidHiddenApiBypass published on GitHub 2021 – 2023 The library matures across Android 10–13; published to Maven Central 2023-01-07 LSDroid enrolls com.lsdroid.lsp in Play App Signing — earliest mark of bridge-app construction 2023-06-22 LSDroid SRL issues the disguised RSA-2048 signing certificate 42:1F:4D:1D:… 2023-06-26 LSDroid SRL issues the clean RSA-2048 signing certificate B6:74:AB:75:… 2023-06-26 → 2023-10-04 Rebuild window: LSDroid integrates HiddenApiBypass, applies the README\u0026rsquo;s evasion snippet, submits com.ssurebrec to Play review 2023-10-04 com.ssurebrec v1.0 goes live on Google Play After April 2021, integrating a hidden-API bypass became a single Gradle dependency line — what previously required hand-rolled sun.misc.Unsafe ART-offset code became one import. LSDroid took advantage of that on the way back to Play, but only for one Xiaomi MIUI probe per app (see use-site call graph above); the surveillance capability set the Play apps actually deliver does not depend on hidden-API reflection at all. The cert-issue-to-Play-live interval of roughly three months is consistent with a standard Android app rewrite-and-review cycle: integrate dependencies, rebrand the APK, submit to Play review.\nA grep is the enforcement test Three statements: (1) Google Play\u0026rsquo;s Malicious Behavior Policy bans non-SDK interface access. (2) Three Cerberus Play APKs (com.ssurebrec v1.4.9, com.lsdroid.lsp v3.6, com.lsdroid.cerberus.kids v1.2.9) each ship org.lsposed.hiddenapibypass; find . -path '*org/lsposed/hiddenapibypass/*' after unzipping any one of them is dispositive. (3) The library\u0026rsquo;s name is literally \u0026ldquo;Hidden API Bypass\u0026rdquo; and the policy bans Play deployment of such tools. The enforcement decision is a grep — a Play engineer verifies it in under a minute without LSDroid cooperation, and three independent Cerberus Play apps fail the same grep.\nRuntime gating: which reflected calls actually resolve Whether the reflected calls succeed at runtime depends on each method\u0026rsquo;s server-side permission gate inside system_server. IPackageManager.grantRuntimePermission requires GRANT_RUNTIME_PERMISSIONS; IStatusBarService.disable requires STATUS_BAR_SERVICE. A Play-store-signed app does not hold these. On rooted devices, Runtime.exec(\u0026quot;su\u0026quot;) + app_process invocation of SuCommands runs with system-equivalent UID and the full chain resolves. The reflected-binder-stub call sites live in SuCommands.main() in v3.8.0; they are not present in com.ssurebrec v1.4.9. The device-admin-mediated subset (lockNow, force-password-quality) works on both. The root-gated set lives in Root capabilities (v3.8.0) below.\nIn the Play APKs the library has two demonstrable effects once activated: (1) the entire process is exempted from hidden-API restrictions — every hidden API in the runtime is reachable from any code path, not just the line that calls the bypass; (2) Android\u0026rsquo;s hidden-API telemetry is silenced — the channel Play Protect uses to detect hidden-API calls on installed devices reports nothing for the lifetime of the process. The only direct call site externally verifiable from static analysis is the Xiaomi MIUI autostart-permission probe per app. Whether the Play apps reach additional hidden APIs through Class.forName-style runtime string indirection — Cerberus does construct strings at runtime in U2.U1 for other purposes — isn\u0026rsquo;t externally verifiable.\nAnalysis scope Layer Target Source Play Store phone app com.ssurebrec v1.4.9_play (versionCode 285) Play Store Play Store LSP module com.lsdroid.lsp v3.6 (versionCode 39) Play Store Direct phone app (standard) com.surebrec v3.8.0 (versionCode 333800) cerberusapp.com Direct phone app (disguised) com.ssurebrec v3.8.0 (disguised build) cerberusapp.com WearOS companion (standard) com.surebrec v3.7.6 (versionCode 283761) cerberusapp.com WearOS companion (disguised) com.ssurebrec v3.7.6 (versionCode 283761, disguised build) cerberusapp.com Enterprise (marketed as MDM) com.lsdroid.cerberus.enterprise v1.7 (versionCode 17) Play Store Kids / Child Safety com.lsdroid.cerberus.kids v1.2.9 (versionCode 331290) Play Store Persona / \u0026ldquo;Personal security — Women safe\u0026rdquo; com.lsdroid.cerberus.persona2 v1.8 (versionCode 21) Play Store Out of scope: Runtime traffic capture, server-side behavior, active API probing, installation on non-test devices. No server contact attempted. No exploitation. Static analysis only of lawfully obtained APKs from Google Play and cerberusapp.com (public download).\nLegal basis: CFAA — no computer access occurs (Van Buren 2021). DMCA 2024 rulemaking security research exemption (through Oct 2027). Washington RCW 9A.90 white hat protection.\nFindings # Finding Scope 1 Three Play Store-distributed APKs (com.ssurebrec, com.lsdroid.lsp, com.lsdroid.cerberus.kids) ship org.lsposed.hiddenapibypass — Malicious Behavior Policy violation, verifiable by find -path '*org/lsposed/hiddenapibypass/*' against any of the three Play (main + LSP + Kids) 2 Silent permission grant via reflected IPackageManager — 16 dangerous permissions, no consent dialog v3.8.0 rooted 3 Uninstall blocking via reflected IPackageManager.setBlockUninstallForUser v3.8.0 rooted 4 Interactive root shell over WebSocket (wss://cerberusapp.com:8443) v3.8.0 5 SMS C2 with abortBroadcast + priority=2147483647 on SMS_RECEIVED — command SMS hidden from victim\u0026rsquo;s inbox v3.8.0 6 NotificationListener suppresses notifications within ±5 s of a command SMS v3.8.0 7 Remote anti-forensics: preventusbdebug disables ADB v3.8.0 8 Status bar disabled for 1 hour via IStatusBarService.disable() v3.8.0 rooted 9 Screen-scrape credential capture via LSP AccessibilityService getText on input fields Both 10 Fake shutdown — screen blacks, phone continues recording and uploading Both 11 No certificate pinning, no network_security_config.xml Both 12 Unsalted SHA-1 password hashing Both 13 AES/CBC with static null IV for SMS command encryption and config restore; key derived from raw IMEI or ANDROID_ID with no hashing — 2^53^ to 2^64^ effective entropy claimed as AES-128, recoverable in O(1) from device-side access (the IMEI/ANDROID_ID is directly readable on a co-installed device) Both 14 Lock-screen passcode is the first four characters of the device ID — the same ID displayed on the lock screen itself, and readable by any zero-permission app through com.surebrec.IdProvider Both 15 Exported ContentProviders (IdProvider, ConfProvider) with no permission guard Both 16 Google AdMob embedded in stalkerware — publisher pub-9848961826628138, attributed to LSDroid SRL in Google\u0026rsquo;s own sellers.json Both 17 Unprotected broadcast delivers LSP screenshot JPEGs to the main app — any third-party app on the device can fire the full shutdown-attempt chain with a forged extra Play Store 18 Dropbox + Google Drive exfiltration — BACKUP uploads contacts, SMS, call log, photos, videos to the operator\u0026rsquo;s cloud storage v3.8.0 19 Dial-code re-entry — dialing 23723787 (T9 mapping of CERBERUS, configurable per-install) on the victim\u0026rsquo;s own keypad launches the admin UI after the launcher icon is hidden v3.8.0 20 Wi-Fi BSSID radar — operator names an SSID; Cerberus reports when the victim\u0026rsquo;s phone enters or leaves its range; works indoors where GPS doesn\u0026rsquo;t Both 21 Tasker integration gives the operator seven event-triggered macro entry points, including wrong-admin-password, geofence crossings, and significant-motion sensor events, parameterised with live device state v3.8.0 22 System-partition persistence — on rooted devices, config and automation DB install to /system/etc/cerberus_* and survive factory reset v3.8.0 rooted 23 Six signing certificates across three custody regimes — three LSDroid-controlled (including the child-monitoring APK still signed by a 2009 RSA-1024 SHA-1 personal cert in Luca Sagaria\u0026rsquo;s name with every identity field Unknown), three held by Google via Play App Signing Suite 24 \u0026ldquo;Cerberus Enterprise\u0026rdquo; is marketed as MDM but declares no BIND_DEVICE_ADMIN, ships no DeviceAdminReceiver, and has no device-policy code — the APK is a location tracker with geofencing, not managed-device software Enterprise 25 Dual distribution — the same developer, same subscription, and same Firebase C2 backend serve a Play-compliant build and a direct-download rootkit side by side Both 26 Five separate Firebase projects across the suite (api-project-999803017449, cerberus-lock-screen-protector, cerberus-enterprise, cerberus-kids, cerberus-persona); three carry FCM command channels (main 44, Enterprise 5, Kids 17 bidirectional); main is additionally backed by a server-side Firebase Realtime Database that persists operator-state on Google\u0026rsquo;s infrastructure Suite Methodology Static analysis only, against the nine-APK corpus — no privileged data, no running device, no subpoena, no runtime instrumentation, no server interaction. Tooling: droidsaw (Android APK/DEX decompiler and static analyzer) for manifest, signing, resources, decompile, xrefs, call-graph analysis, source extraction, and audit; Semgrep for policy-violation pattern matching against the extracted source tree; TruffleHog for hardcoded-secret scans; apksigner and openssl for v2/v3 signing-block certificate extraction, modulus comparison (gcd over the two LSDroid SRL public moduli to verify independent RNG draws), and notBefore/notAfter validity dates.\nWhat static analysis cannot show. Claims about runtime state — that setAndAllowWhileIdle alarms actually fire under Doze, that WorkManager periodic uploads occur on the configured cadence, that ConnectivityReceiver actually drains cached surveillance media on next CONNECTED transition, that the bypass library\u0026rsquo;s empty-prefix setHiddenApiExemptions call exempts the running process — are inferred from the configuration encoded in static bytecode, not observed via runtime instrumentation. Server-side behavior (operator dashboard, license validation, command-and-control delivery) is entirely unobserved; no requests were made to cerberusapp.com during this analysis. Class.forName(stringConcat) reflective indirection cannot be exhaustively traced from static analysis; the surveillance-feature attribution to documented Android APIs is the strongest claim static analysis can make, not a guarantee that no hidden-API access exists. Where the article uses phrasing like \u0026ldquo;Cerberus does X,\u0026rdquo; read it as \u0026ldquo;the bytecode encodes X to happen at runtime under documented Android behavior.\u0026rdquo;\ndroidsaw itself was built using Claude Code (running Claude Opus 4.6), which also helped draft this article and the disclosure letters that followed it; AI tooling sits at the engineering layer of this work, not at the analytical-claim layer — every finding in this article is verifiable byte-by-byte against the cited APKs.\nArchitecture Cerberus ships as multiple packages across two distribution channels.\nDistribution channels Channel Package Version targetSdk Root Installed on Google Play com.ssurebrec 1.4.9_play 36 No Victim Google Play com.lsdroid.lsp 3.6 36 No Victim Google Play com.lsdroid.cerberus.enterprise 1.7 36 No Employee (victim) Google Play com.lsdroid.cerberus.kids 1.2.9 33 No Child (victim) + parent (operator) Google Play com.lsdroid.cerberus.persona2 1.8 36 No Victim cerberusapp.com com.surebrec (standard) 3.8.0 26 Yes Victim cerberusapp.com com.ssurebrec (disguised) 3.8.0 26 Yes Victim cerberusapp.com com.surebrec (WearOS standard) 3.7.6 28 No Abuser cerberusapp.com com.ssurebrec (WearOS disguised) 3.7.6 28 No Abuser Package ecosystem Package Role com.ssurebrec Play Store phone app — passes review, handles billing, 44 FCM commands com.surebrec Direct download — full RAT with root, shell, SMS C2, keylogging com.surebrec (WearOS) Watch companion — alarm, emergency mode, Bluetooth proximity (installed on the abuser\u0026rsquo;s watch) com.lsdroid.lsp Lock Screen Protector — accessibility, anti-power-off, screenshot com.lsdroid.cerberus Package alias for com.surebrec (HIDE / UNHIDE via pm enable/disable) com.lsdroid.cerberuss Package alias for com.surebrec (HIDE / UNHIDE via pm enable/disable) com.lsdroid.cerberus.enterprise Sold as \u0026ldquo;Enterprise MDM\u0026rdquo; — shipped APK is a location tracker with geofencing; no device-admin capability com.lsdroid.cerberus.kids Kids / \u0026ldquo;Child Safety\u0026rdquo; — app monitoring, screen time, location, notification suppression com.lsdroid.cerberus.persona2 Persona / \u0026ldquo;Personal security — Women safe\u0026rdquo; — emergency SMS, GPS streaming, hard dependency on main app Component comparison The Play Store deployment is a pair: com.ssurebrec v1.4.9 and com.lsdroid.lsp v3.6 by the same developer, both required, both installed together during abuser setup. The capability column for the Play side reflects the combined v1.4.9 + LSP deployment, because that is what is actually on a victim\u0026rsquo;s phone. A row marked \u0026ldquo;Via LSP\u0026rdquo; means the capability is supplied by the companion app\u0026rsquo;s accessibility service, not by the main app\u0026rsquo;s own bytecode.\nCapability v3.8.0 (direct) v1.4.9 + LSP (Play) Root shell (su) 50+ calls Absent WebSocket shell wss://cerberusapp.com:8443 Absent SMS C2 channel Yes (configurable keyword) Absent Accessibility service AccService (\u0026ldquo;System Framework\u0026rdquo;) Via LSP (canRetrieveWindowContent + canPerformGestures + canTakeScreenshot) Screen-scrape credential capture canRequestFilterKeyEvents (key-event filter) Via LSP (AccessibilityNodeInfo.getText on input fields) Power-off interception + fake shutdown AccService detects system power dialog Via LSP (dismiss + broadcast to main app\u0026rsquo;s ShutdownDialogActivity) Status-bar blocking IStatusBarService.disable(1 hour) Via LSP (AccessibilityService overlay) NotificationListener Intercepts + suppresses Absent SIM change detection SIMChangeReceiver Absent SMS interception READ_SMS, RECEIVE_SMS, abortBroadcast Absent Contact exfiltration READ_CONTACTS Absent Call interception PROCESS_OUTGOING_CALLS Absent NFC command trigger Yes Absent Anti-forensics (preventusbdebug) Settings.Secure ContentObserver Absent Uninstall blocking SuCommands.block_uninstall (reflected) Device admin only What the Play Store version still does The Play Store version accepts 44 FCM commands including WIPE, FAKE_SHUTDOWN, HIDE, and TAKEPICTURE. It has no certificate pinning. It exports a ContentProvider that leaks the device registration ID to any app on the device.\nThe FCM command handler declares EXEC_TERM_COMMAND and STARTSHELL — the dispatch code exists, but the execution handlers are absent from the bytecode.\nWhat the direct version adds The direct version targets SDK 26 (Android 8) and is distributed from cerberusapp.com — neither subject to Play Store\u0026rsquo;s policy restrictions on SMS / Call Log permissions (which since 2019 require default-handler status or compliance review) nor to Android-level permission tightening at later API levels (e.g., ACCESS_BACKGROUND_LOCATION separated as its own permission at API 29; before that, foreground location implicitly granted background access). It adds READ_SMS, RECEIVE_SMS, SEND_SMS, READ_CALL_LOG, READ_CONTACTS, CALL_PHONE, and MODIFY_PHONE_STATE — permissions the Play Store version doesn\u0026rsquo;t request.\nSigning keys: a mixed-custody picture The Cerberus suite is signed by six distinct certificates across three custody regimes. Pulling apksigner verify --print-certs across the nine-APK sample, then openssl x509 -noout -subject -dates on each PEM, yields:\nSHA-256 (first 8) Subject Issued Signs Custody 6B:CE:75:CE (RSA-1024, SHA-1) CN=Luca Sagaria, all other fields Unknown 2009-03-13 Kids LSDroid Play App Signing CN=Android, O=Google Inc. 2021-10-07 Enterprise Google Play App Signing CN=Android, O=Google Inc. 2023-01-07 LSP Google 42:1F:4D:1D CN=LSDroid srl, C=IT 2023-06-22 Disguised phone (v3.8.0) + disguised WearOS (v3.7.6) LSDroid B6:74:AB:75 CN=LSDroid srl, C=IT 2023-06-26 Play Store main (com.ssurebrec), direct-download v3.8.0 standard, WearOS standard LSDroid Play App Signing CN=Android, O=Google Inc. 2024-02-29 Persona2 (\u0026ldquo;Personal security — Women safe\u0026rdquo;) Google The 2009 personal cert still signs a child-monitoring app in 2026. Luca Sagaria generated a self-issued RSA-1024, SHA-1 developer certificate on 2009-03-13, years before LSDroid SRL was incorporated; every identity field other than the common name reads Unknown. NIST disallowed RSA-1024 for digital-signature generation effective 2014 (SP 800-131A); public CAs stopped issuing SHA-1 code-signing certs by January 2016. Seventeen years on, that same personal cert — not LSDroid SRL\u0026rsquo;s corporate cert, not a Play App Signing key held by Google — is what signs com.lsdroid.cerberus.kids on children\u0026rsquo;s phones, valid through 2036-07-29. LSDroid SRL\u0026rsquo;s name does not appear in the signing chain of its own child-monitoring product.\nThe cert is self-signed, so the standard PKI revocation path doesn\u0026rsquo;t apply — there\u0026rsquo;s no issuing CA to publish a CRL or OCSP response. The validity window remains open until 2036 unless Google blocks new uploads under this cert, refuses to distribute APKs signed by it, or LSDroid migrates Kids to Play App Signing (which would change the signing key going forward and requires LSDroid\u0026rsquo;s cooperation). The modulus is publicly extractable from any Cerberus Kids APK on Google Play; one apksigner verify --print-certs followed by openssl x509 -text returns the full 1024-bit modulus and the SHA-1 signature value to anyone with the APK on disk.\nThe direct-download pair is four days apart. The 42:1F:4D and B6:74:AB keys were both issued as CN=LSDroid srl, C=IT in June 2023, 96 hours apart, signing non-overlapping APK distributions: the B6:74:AB key signs the openly-published releases (Play Store main, direct-download v3.8.0 standard, WearOS standard); the 42:1F:4D key signs only the disguised variants. The two keys share no prime factors — they were generated from independent RNG draws, not from a single low-entropy keygen window where two adjacent calls might have shared internal state.\nC2 protocol Primary channel: Google\u0026rsquo;s Firebase Cloud Messaging Every command that reaches a victim\u0026rsquo;s device transits Google\u0026rsquo;s push-notification infrastructure. The main app com.ssurebrec registers under Firebase project api-project-999803017449; commands arrive via FCM push to FCMMessagingService. Each message contains a message field with the command string and optional param field. The handler dispatches to SurebrecService or BackgroundService via intent extras. The Play Store version exposes 44 commands; the direct version adds SMS-triggered commands for a total of 60+ distinct operations.\nThe Cerberus suite spans five Firebase projects, not one. Each LSDroid product is configured against its own Firebase project: api-project-999803017449 (main), cerberus-lock-screen-protector (LSP), cerberus-enterprise (Enterprise), cerberus-kids (Kids), cerberus-persona (Persona2). Each project ID is in the corresponding APK\u0026rsquo;s google-services.json. The five projects\u0026rsquo; billing-account ownership on Google\u0026rsquo;s side is not externally verifiable. Three of the five carry FCM command channels — main (44 commands), Enterprise (5 commands, see below), Kids (17 bidirectional tokens, see Kids subsection). LSP and Persona2 use Firebase only for Analytics + Crashlytics telemetry; their operational paths are HTTP POST to cerberusapp.com and, for LSP, broadcast IPC from the main app.\nMain\u0026rsquo;s api-project-999803017449 is additionally backed by a Firebase Realtime Database (api-project-999803017449.firebaseio.com) — but the device-side APK does not bundle the Firebase Database SDK. The RTDB is server-facing only: LSDroid\u0026rsquo;s PHP backend writes operator-state into it; the operator\u0026rsquo;s web dashboard reads from it. Google\u0026rsquo;s Firebase RTDB is LSDroid\u0026rsquo;s operator-data persistence layer.\nCommand table (FCM) Command Description TAKEPICTURE Capture photo (front/rear camera, flash control) CAPTUREVIDEO Record video (camera, flash, duration) RECORDAUDIO Record audio (configurable duration, max 300s) SCREENRECORD Record screen SCREENSHOT Capture screenshot START_TRACKING / STOP_TRACKING Continuous GPS tracking STARTEMERGENCY / STOPEMERGENCY Emergency mode (all sensors active) SMSLOG Exfiltrate SMS messages CALLLOG Exfiltrate call history HISTORY Exfiltrate browser history GET_APP_LIST List installed applications BACKUP / STOP_BACKUP Bulk data exfiltration SMS Send SMS as victim (impersonation) CALL Place phone call WIPE / WIPESD Factory reset / wipe SD card LOCK / UNLOCK Lock/unlock device ALARM Trigger alarm with message MESSAGE Display message on screen HIDE / UNHIDE / HIDESHOW Toggle launcher icon visibility FAKE_SHUTDOWN / START_SHUTDOWN Fake power-off (screen blacks, phone stays on) EXEC_TERM_COMMAND Execute arbitrary shell command (v3.8.0 only) STARTSHELL Interactive WebSocket shell (v3.8.0 only) STARTRADAR / STOPRADAR Proximity detection service ENABLEBLUETOOTH / DISABLEBLUETOOTH Toggle Bluetooth ENABLEHOTSPOT / DISABLEHOTSPOT Toggle WiFi hotspot LAUNCH_APP Launch arbitrary application START_SERVICE Start arbitrary Android service SEND_BROADCAST Send arbitrary broadcast intent GETAPPCONF / SETAPPCONF Read/write full app configuration REBOOT Reboot device CONNECT Force connectivity check Secondary channel: SMS (v3.8.0 only) The v3.8.0 direct-download build accepts remote commands over SMS. SmsReceiver is registered with android:priority=\u0026quot;2147483647\u0026quot; (Integer.MAX_VALUE) on SMS_RECEIVED, so it fires before the system inbox, and calls abortBroadcast() to suppress delivery of command messages to the inbox. The NotificationListener cancels any notification arriving within ±5 seconds of the command timestamp.\nAuthorization is a three-slot allowlist of phone numbers stored in SharedPreferences as number1, number2, number3. Messages from any other sender pass through untouched. The receiver\u0026rsquo;s command-parsing flow, extracted from the decompiled com.surebrec.SmsReceiver.onReceive:\nIf the SMS sender doesn\u0026rsquo;t match one of the three numbers, pass the message through. Check the body against a magic keyword stored in SharedPref smskeyword (default cerberus). If the body contains ##, split on ##. If the body contains ** instead, split on \\\\*\\\\*. Two supported delimiters. The placeholder =CR= inside any field is substituted with an actual carriage return (SMS bodies can\u0026rsquo;t cleanly embed newlines). Decrypt the command field with AES/CBC/PKCS5PADDING: a 16-byte key derived from the device identifier via String.format(\u0026quot;%-16s\u0026quot;, id).replace(' ', '0'), reversed, with a literal 16-byte null IV. Match the plaintext against the command dictionary and dispatch to SurebrecService. The command dictionary:\nSIMINFO WIPE WIPESD LOCK UNLOCK ALARM TAKEPICTURE CAPTUREVIDEO CALL MESSAGE SCREENSHOT REBOOT SPEAK STARTEMERGENCY STOPEMERGENCY ENABLEWIFI DISABLEWIFI ENABLEDATA DISABLEDATA ENABLEROAMING ENABLEBLUETOOTH DISABLEBLUETOOTH FIND Three SMS-only commands do not appear in the FCM dispatcher:\nSPEAK — Cerberus uses the victim\u0026rsquo;s phone\u0026rsquo;s text-to-speech engine to announce an operator-chosen message out loud. The operator sends the sentence via SMS. ENABLEROAMING — remotely turn on international roaming, keeping the stalkerware connected when the victim travels internationally. ENABLEDATA / DISABLEDATA — remotely toggle the victim\u0026rsquo;s mobile data connection. Two tokens that might look like commands in the decompiled source are internal flags, not SMS keywords. findnopass is a SharedPreferences boolean key (getBoolean(\u0026quot;findnopass\u0026quot;, false)): when true, it lets the FIND command execute without a password check. datasms is an Intent extra key (putExtra(\u0026quot;datasms\u0026quot;, true)) that flags to the downstream SurebrecService handler that the command arrived via Android\u0026rsquo;s binary DATA_SMS_RECEIVED channel rather than text SMS. Neither is a keyword the abuser types.\nThe authentication model is the three-number allowlist plus the keyword, both of which are SharedPref values. There is no second factor, no out-of-band acknowledgement, no observable rate limit in the receiver code. An abuser with physical access during provisioning only needs to register a phone number in one of the three slots and remember the keyword. Anyone spoofing the caller ID of one of the three trusted numbers — which is trivial through commercial SMS spoofing services — can issue commands to the victim\u0026rsquo;s device without physical access.\nRadar: Wi-Fi BSSID proximity tracking FCM opcodes STARTRADAR and STOPRADAR are explained by com.surebrec.RadarService. RadarService.onStartCommand takes an Intent extra named \u0026quot;ssid\u0026quot; — a Wi-Fi network name — acquires a wake lock named \u0026quot;RadarService\u0026quot;, registers a BroadcastReceiver for android.net.wifi.SCAN_RESULTS, and posts a handler message to kick off the scan loop. Each SCAN_RESULTS callback checks whether the target SSID is present in the returned scan list and reports presence or absence transitions to the operator over the C2.\nRadar is Wi-Fi BSSID proximity tracking, not GPS. The operator tells Cerberus \u0026ldquo;start radar for SSID HomeWifi\u0026rdquo; via FCM, and Cerberus reports when the victim\u0026rsquo;s phone comes within Wi-Fi range of that SSID or leaves it. It works indoors, in subways, inside buildings, in underground parking garages, and in urban canyons where GPS is unreliable.\nIn domestic-violence contexts an abuser can configure \u0026ldquo;radar for my home Wi-Fi,\u0026rdquo; \u0026ldquo;radar for my workplace Wi-Fi,\u0026rdquo; or \u0026ldquo;radar for the SSID I saw at the shelter intake interview\u0026rdquo; and be notified every time the victim\u0026rsquo;s phone enters or leaves that specific building — including buildings where GPS doesn\u0026rsquo;t work reliably, like shelters, hospitals, courthouses, and police stations. The dedicated C2 endpoint for radar telemetry is cerberusapp.com/comm/radar.php.\nThe 68-opcode dispatch in direct-download SurebrecService com.surebrec.SurebrecService.onStartCommand in the direct-download v3.8.0 build dispatches 68 contiguous integer opcodes (1 through 68). The Play Store com.ssurebrec v1.4.9 dispatcher exposes 16 case labels — 8, 9, 10, 12, 13, 14, 15, 18, 19, 20, 21, 22, 67, 68, 69, 70 — that collapse into four multi-label shared-body handlers (cases 8–10 share one body, 12–15 share another, 18–22 share another, 67–70 share another). Each shared body branches internally on the opcode value. The four functional groups are: (1) location-tracking + init + HIDE; (2) silent capture (camera, microphone, screenshot); (3) alarm + emergency suite; (4) shutdown + FORCED_UNLOCK + hide-show. Combined body size across the four groups is approximately 233 KB of decompiled Java.\nv3.8.0\u0026rsquo;s 68 opcodes are LSDroid\u0026rsquo;s full C2 protocol grammar; the Play build ships a subset, with opcodes 1–7, 11, 16–17, 23–66 absent.\nNot every case is reachable from FCM. Some cases are reachable only from internal paths — ShutdownAttemptReceiver, GeofenceBroadcastReceiver, AdminReceiver, SmsReceiver, and the wearable message listener — each with its own authentication model. The FCM command dispatcher exposes 44 of the 68 opcodes as named string commands; the SMS dispatcher exposes roughly 23; at least one opcode (31, \u0026ldquo;capture location as shutdown response\u0026rdquo;) is reachable only from the shutdown receiver. The C2 protocol grammar has 27 internal-path handlers in addition to the 44 string commands the FCM dispatcher exposes — geofence-crossing, wrong-admin-password, low-power-motion-sensor, and wearable-message paths, each its own capability category.\nC2 endpoints The main app references 30+ HTTPS endpoints across the /comm/ (device→server) and /api/ (operator-facing) namespaces on cerberusapp.com:\nNamespace Endpoint Purpose device /comm/ping2.php Heartbeat device /comm/register.php Device registration device /comm/sendregid.php FCM token upload device /comm/sendpicture.php Photo exfiltration (JPEG) device /comm/sendvideo3.php Video exfiltration (MP4) device /comm/sendaudiofile.php Audio exfiltration (3GP) device /comm/sendlocation.php, /comm/sendlocation2.php Real-time GPS (v1 + v2) device /comm/sendtrack2.php Location history (KML in ZIP) device /comm/sendmessage.php Status messages device /comm/sendemail.php Email notifications to operator device /comm/restoreconf2.php Encrypted config download device /comm/radar.php Wi-Fi BSSID radar telemetry device /comm/cs.php, /comm/s.php, /comm/us.php Short-named upload/download channels device /comm/login2.php, /comm/verify.php Auth flow device /comm/recoveruser.php, /comm/resetpass.php Account recovery device /comm/getdevices.php, /comm/getgtoken.php, /comm/getlictype.php Device-listing, GCM-token-fetch, license-tier queries operator /api/getdevices.php, /api/getdevicestatus.php Operator device-list + status queries operator /api/login.php Operator login operator /api/sendcommand.php Operator → device command operator /api/setdevicename.php Operator names a device billing /buy_license.php?username= License purchase flow shell wss://cerberusapp.com:8443 Interactive root-shell WebSocket Versioned suffixes (ping2, restoreconf2, sendlocation2, sendtrack2, sendvideo3) indicate protocol revisions kept reachable for legacy installed clients. sendvideo3 is at v3, so two video-upload protocol revisions have happened.\nNo certificate pinning. No network_security_config.xml. The Order header sent with every request is a 24-character random string — not a session token, not an HMAC. Zero authentication beyond the device ID.\nCloud exfiltration: Dropbox and Google Drive (v3.8.0) The BACKUP command (call=47) accepts 6 boolean flags controlling what to exfiltrate: contacts, SMS, call log, photos, videos, and status notification. The backup data is uploaded to either Dropbox or Google Drive — operator\u0026rsquo;s choice.\nDropbox: Full Dropbox Core SDK v2 bundled. Auth token stored in SharedPreferences as dropboxAccessToken. Photos uploaded as image/jpeg, videos as video/mpeg, with retry logic (3 attempts, 5-second backoff). Upload progress tracked per-file to avoid re-uploading. Google Drive: Google API Client with DRIVE_FILE OAuth2 scope. Uses GoogleAccountCredential.usingOAuth2 — can leverage the victim\u0026rsquo;s own Google account or an operator-provided one. These channels target api.dropboxapi.com and www.googleapis.com — legitimate cloud services. The victim\u0026rsquo;s surveillance data resides in the operator\u0026rsquo;s cloud storage.\nThe autonomous persistence layer The 44 FCM commands are what operators request. They sit on top of a much larger autonomous layer that runs without any operator action — Cerberus reacts to dozens of system events per hour, schedules forward alarms from each reaction, and re-arms itself across reboots.\nThree independent wake-up tiers in com.ssurebrec:\nTier 1 — system-event-driven receivers (11+ hooks). Each receiver launches EmptyActivity and/or the appropriate service, restoring foreground priority for downstream work that would otherwise be blocked by Android\u0026rsquo;s background-service restrictions:\nReceiver System events Triggers per day, typical use BootReceiver BOOT_COMPLETED once per reboot Lw7/p; BATTERY_LOW, BATTERY_CHANGED, ACTION_POWER_CONNECTED, ACTION_POWER_DISCONNECTED, ACTION_SHUTDOWN, internal DEVICE_STARTED continuous (BATTERY_CHANGED fires roughly every minute) LockScreenReceiver SCREEN_ON, SCREEN_OFF, USER_PRESENT, CLOSE_SYSTEM_DIALOGS, internal shutshut every screen wake/sleep, every unlock, every power-menu open ConnectivityReceiver network state changes every Wi-Fi/cell transition PackageUpdateReceiver PACKAGE_ADDED, PACKAGE_REPLACED, PACKAGE_REMOVED every app install/update/uninstall GeofenceBroadcastReceiver geofence enter/exit per operator configuration ShutdownAttemptReceiver LSP IPC com.lsdroid.shutdownattempt every power-button press AdminReceiver.onPasswordFailed wrong device-admin password per failed unlock attempt PackageUpdateReceiver is the most direct example. Its entire body is five lines: build an Intent for EmptyActivity, set FLAG_ACTIVITY_NEW_TASK, call startActivity. It does not log the package event, does not detect specific apps, does not report. The only effect of receiving a PACKAGE_* broadcast is that Cerberus\u0026rsquo;s process is started: every Play Protect update, every OEM bloatware refresh, every app install or removal wakes the process.\nTier 2 — scheduled setAndAllowWhileIdle alarms (17 call sites). Every receiver/service that gets woken by Tier 1 also schedules forward alarms. setAndAllowWhileIdle is the AlarmManager method that bypasses Doze — it forces the alarm to fire even when the device is in low-power state. Cerberus has 17 distinct call sites scheduling these alarms:\nAdminReceiver.onPasswordFailed, BackgroundService (lifecycle), BootReceiver, ConnectivityReceiver, GeofenceBroadcastReceiver, ShutdownAttemptReceiver, ShutdownDialogActivity.onCreate, SurebrecService.d/onStartCommand, TrackServiceFused.onStartCommand, Lw7/b1;-\u0026gt;run, Lw7/i3;-\u0026gt;f (preference click handler with 2-second background-launch bypass), Lw7/n;-\u0026gt;onTrigger (low-power motion sensor trigger handler), Lw7/p;-\u0026gt;onReceive, Lw7/x1;-\u0026gt;b. Plus one setExactAndAllowWhileIdle in BackgroundService.\nBootReceiver itself sets up two alarms: com.surebrec.DEVICE_STARTED 60 seconds after boot, com.surebrec.DAILY_PING daily. Every boot re-arms both, and ConnectivityReceiver re-arms DAILY_PING whenever it fires.\nTier 3 — offline persistence + storage-context migration. When the device is offline, Cerberus caches surveillance media to disk as pic-* (JPEG) and video-* (MP4) under getFilesDir(). When ConnectivityReceiver next fires on a CONNECTED network state, it scans the directory and dispatches one upload thread (Lw7/c1;) per cached file.\nAcross the locked/unlocked storage boundary (Android\u0026rsquo;s direct-boot model), ConnectivityReceiver migrates Tasker rules (AutoTaskRules.db), location coord files (coord01, coord02), and metadata (header) between credential-encrypted and device-protected storage contexts, calling android.system.Os.chmod and Os.chown to preserve the original file mode/uid/gid bits. The directBootAware declaration on LSP keeps that companion alive in the pre-first-unlock window. setAndAllowWhileIdle alarms persist across reboots through Android\u0026rsquo;s alarm subsystem — Cerberus\u0026rsquo;s wake-up schedule survives device restarts without re-registration.\nA Cerberus install on a phone that\u0026rsquo;s never received a single FCM command still runs, schedules, re-arms, caches for upload, and migrates state across reboots and storage transitions.\nRoot capabilities (v3.8.0) The capabilities below require root at runtime. Runtime.exec(\u0026quot;su\u0026quot;) succeeds, /system/bin/sh spawns under elevated UID, and app_process invocation of SuCommands runs with system-equivalent privileges — at which point the reflected binder-stub calls resolve past system_server\u0026rsquo;s permission gates.\nThe reflected-binder-stub call sites described below live in SuCommands.main() in v3.8.0; they run natively because v3.8.0 declares targetSdk 26, which receives Android\u0026rsquo;s hidden-API leniency for low-targetSdk apps. They are not present in com.ssurebrec v1.4.9. This is the v3.8.0-on-rooted-devices capability set.\nSuCommands — reflected Android framework API abuse SuCommands.main() is invoked via Runtime.exec(\u0026quot;su\u0026quot;) → app_process. It uses Java reflection to access hidden Android system service APIs:\nCommand API Effect grant_permissions IPackageManager.grantRuntimePermission() Silently grants CAMERA, RECORD_AUDIO, READ_SMS, SEND_SMS, RECEIVE_SMS, READ_CALL_LOG, READ_CONTACTS, ACCESS_FINE_LOCATION, ACCESS_BACKGROUND_LOCATION, READ_PHONE_STATE, CALL_PHONE, PROCESS_OUTGOING_CALLS, GET_ACCOUNTS, READ_EXTERNAL_STORAGE, WRITE_EXTERNAL_STORAGE, ACCESS_COARSE_LOCATION block_uninstall IPackageManager.setBlockUninstallForUser() Prevents uninstallation — fails silently enable_admin IDevicePolicyManager.setActiveAdmin() Enables device admin without consent cancel_system_notifications INotificationManager.cancelAllNotifications(\u0026quot;android\u0026quot;) Clears all system notifications disable_status IStatusBarService.disable(65536) + disable2(1) Disables entire status bar for 1 hour shutdown_dialog ActivityThread.systemMain() System-level fake shutdown progress dialog set_owner_message ILockSettings.setString(\u0026quot;lock_screen_owner_info\u0026quot;) Sets lock screen message removerestrict INetworkPolicyManager.setRestrictBackground(false) Removes background data restriction allow_uninstall IPackageManager.setBlockUninstallForUser(..., false) Reverses block_uninstall — server-gated \u0026ldquo;legitimate uninstall\u0026rdquo; path, surfaced via LockActivity\u0026rsquo;s Button L (visibility=GONE until server config enables it) enable_status IStatusBarService.disable(0) + disable2(0) Re-enables status bar after disable_status disable_owner_message ILockSettings.setBoolean(\u0026quot;lock_screen_owner_info_enabled\u0026quot;, false) Clears the lock-screen owner-info text previously set by set_owner_message enable_powersave IPowerManager.setPowerSaveMode(true) Forces battery-saver on — reduces visible battery drain disable_powersave IPowerManager.setPowerSaveMode(false) Forces battery-saver off — keeps camera/mic/upload running at full rate restricted_permissions IPackageManager runtime-permission-flag write Marks granted runtime permissions as fixed by system policy; the victim cannot revoke them from Settings → Apps Dumping every string literal out of com.surebrec.SuCommands.main(String[]) in the decompiled v3.8.0 APK yields 93 unique literals. Eight of them are the fully-qualified names of Android internal binder stubs:\nBinder stub What it controls android.content.pm.IPackageManager$Stub Runtime-permission grants, package uninstall blocking android.permission.IPermissionManager$Stub Runtime-permission grants on API 30+ (separate code path) com.android.internal.statusbar.IStatusBarService$Stub Status bar disable, power-menu dismissal android.os.IPowerManager$Stub Shutdown, reboot, wake-lock control com.android.internal.widget.ILockSettings$Stub Lock-screen PIN/password manipulation android.app.admin.IDevicePolicyManager$Stub Device-owner policy without being a registered admin android.app.INotificationManager$Stub Programmatic notification suppression android.net.INetworkPolicyManager$Stub Data-saver / background-network / battery-optimization bypass INetworkPolicyManager controls the data-saver toggle, the battery-optimization whitelist, and the background-network restrictions Android 9+ applies to every app. Reflection access on that binder lets Cerberus whitelist itself from every Android-level network throttle, so its C2 beacons reach cerberusapp.com regardless of background-data restrictions. There is no user-visible \u0026ldquo;allow unrestricted background data\u0026rdquo; dialog.\nOn a rooted device, the reflected call loop inside SuCommands.main() grants 16 dangerous runtime permissions without the Android runtime-permission dialog ever firing: READ_EXTERNAL_STORAGE, WRITE_EXTERNAL_STORAGE, GET_ACCOUNTS, ACCESS_FINE_LOCATION, ACCESS_COARSE_LOCATION, ACCESS_BACKGROUND_LOCATION, READ_SMS, RECEIVE_SMS, SEND_SMS, READ_CALL_LOG, PROCESS_OUTGOING_CALLS, CALL_PHONE, READ_CONTACTS, READ_PHONE_STATE, CAMERA, RECORD_AUDIO.\nACCESS_BACKGROUND_LOCATION and SEND_SMS are both so policy-sensitive that Google Play requires the developer to fill out a separate compliance declaration explaining why the app needs them before Play will publish a build that requests them. By granting the permissions reflectively at runtime after install, Cerberus never has to declare them in the manifest at submission time and never has to fill out the form.\nInteractive shell ShellService opens a persistent WebSocket to wss://www.cerberusapp.com:8443. On connection, it spawns /system/bin/sh via ProcessBuilder, streams stdin/stdout over the WebSocket in JSON frames, and constructs a real shell prompt by running pwd and id. Supports su escalation within the session, CTRLCPROC (SIGINT to foreground process), and process tracking via /proc enumeration. 20-minute inactivity timeout with auto-reconnect on disconnect.\nScreen recording via the system screenrecord binary Opcode 66 (SCREENRECORD) in SurebrecService contains the literal shell-command template /system/bin/screenrecord --size \u0026lt;WxH\u0026gt; --bit-rate 1000000 --time-limit \u0026lt;seconds\u0026gt;. Cerberus invokes the Android system binary at /system/bin/screenrecord directly via Runtime.exec — the same binary Android\u0026rsquo;s developer-tools screen-capture workflow uses — produces an MP4 on-device, uploads it to /comm/sendvideo3.php, and acquires a wake lock named \u0026quot;Screenrecord\u0026quot; while the recording is in progress.\nThe --bit-rate 1000000 value is 1 Mbps — below the screenrecord binary\u0026rsquo;s own default of 20 Mbps on Android 11+.\nSystem partition persistence On rooted devices, Cerberus installs configuration to the system partition:\n/system/etc/cerberus_conf.xml — full config (survives factory reset) /system/etc/cerberus_at.db — automation rules database U1.F() reads this config on first boot, imports all settings, copies the automation database, and silently enables device admin via setActiveAdmin reflection — all without user interaction.\nReflection paths beyond SuCommands SuCommands.main() is the most concentrated reflection chain in v3.8.0, but it is not the only one. The main utility class LU2/U1; contains its own reflection paths: setActiveAdmin reflection used at first-boot config import (described above), and AppOpsManager.setMode(int op, int uid, String packageName, int mode) reflection that allows v3.8.0 to set any AppOp on any package to MODE_IGNORED — silently denying that operation for the target app without notifying the user. On Android ≤27 the reflection succeeds directly under targetSdk 26 leniency; on Android 9+, v3.8.0 falls back to Runtime.exec(\u0026quot;su\u0026quot;) + appops set shell command.\nIStatusBarService.disable/disable2 reflection is reachable not only from SuCommands but from broadcast receiver LL2/B;-\u0026gt;onReceive, handler LP2/P;-\u0026gt;handleMessage, BackgroundService lifecycle methods, and AccService.onAccessibilityEvent — so status-bar disable can fire opportunistically from accessibility events, not only after a deliberate root invocation.\nThe reflection chain reaches Android internal services through the canonical mechanism: Class.forName(\u0026quot;android.os.ServiceManager\u0026quot;).getMethod(\u0026quot;getService\u0026quot;, String.class).invoke(null, \u0026quot;\u0026lt;service-name\u0026gt;\u0026quot;) returns an IBinder, and IFoo$Stub.asInterface(binder) wraps it as a callable IFoo. Confirmed for IPackageManager, IStatusBarService, INotificationManager, IDevicePolicyManager, IPowerManager, IPermissionManager, ILockSettings, INetworkPolicyManager.\nSignature-permission probing (WRITE_SECURE_SETTINGS, READ_PRIVILEGED_PHONE_STATE) com.ssurebrec v1.4.9 declares android.permission.WRITE_SECURE_SETTINGS in its manifest. v3.8.0 declares both WRITE_SECURE_SETTINGS and android.permission.READ_PRIVILEGED_PHONE_STATE. These are signature-protected permissions — the Play Store will not grant them on install (the user-facing permission dialog never appears for signature permissions). The permissions become active only via adb shell pm grant \u0026lt;package\u0026gt; \u0026lt;permission\u0026gt;, which an abuser with USB-debug access during the install moment can run, or via root + pm grant.\nWhen WRITE_SECURE_SETTINGS is granted, Cerberus\u0026rsquo;s TrackServiceFused programmatically writes Settings.Secure.location_mode via Settings.Secure.putInt(...), silently overriding the user\u0026rsquo;s location-services setting (which Android\u0026rsquo;s UI presents as user-controlled and which a legitimate non-system app cannot change). When READ_PRIVILEGED_PHONE_STATE is granted on Android 10+, the IMEI access path in LU2/U1;-\u0026gt;i() reaches TelephonyManager.getDeviceId() even though the public READ_PHONE_STATE permission would no longer suffice.\nThe code paths that use these permissions are present and functional when an installer has run pm grant over ADB. An installer with USB-debug access at install time can grant signature permissions that Play Store does not expose through its consent dialogs.\nStealth mechanisms App hiding HIDE command calls PackageManager.setComponentEnabledSetting() to disable the launcher activity. The app disappears from the home screen and app drawer. SharedPreferences stores hidden=true. On rooted devices, UNHIDE runs pm enable for com.lsdroid.cerberus and com.lsdroid.cerberuss — additional package aliases (same APK code as com.surebrec) distributed from cerberusapp.com.\nFake shutdown ShutdownDialogActivity renders a fake Android power-off dialog using the system\u0026rsquo;s own theme (Theme.DeviceDefault.System) and power icon (ic_lock_power_off). On \u0026ldquo;power off\u0026rdquo; tap, sets screen brightness to 1/255 and button brightness to 0. The phone appears off. Surveillance continues.\nLSDroid markets fake shutdown explicitly. A YouTube explainer video for the Lock Screen Protector setup — hardcoded inside the LSP APK at com.lsdroid.lsp.c line 27, opened from within LSP\u0026rsquo;s own setup UI — describes the feature as showing a fake power menu so that the person holding the phone believes it has been switched off while the device continues to track, record, and report. A similar promotional video for the main Cerberus Anti-theft app frames the same capability the same way.\nDisguised build The \u0026ldquo;disguised\u0026rdquo; APK is identical to the standard build with two manifest changes: android:label=\u0026quot;System Framework\u0026quot; and android:icon=\u0026quot;@android:drawable/sym_def_app_icon\u0026quot;. In the victim\u0026rsquo;s app list, it looks like a system service.\nBoth disguised builds additionally rename their package: the standard direct-download phone and WearOS APKs declare com.surebrec (matching the direct-download main app), but the disguised phone (v3.8.0) and disguised WearOS (v3.7.6) both declare com.ssurebrec — the Play Store main app\u0026rsquo;s package name. The rename has no apparent technical purpose; on a victim\u0026rsquo;s device with the Play Store main app already installed, an inspector seeing two com.ssurebrec-prefixed processes might assume both are the legitimate Play app.\nDial-code re-entry com.surebrec.StartReceiver is declared with intent-filter priority 2147483647 (the maximum Android accepts) on android.intent.action.NEW_OUTGOING_CALL. On every outbound call the victim dials, the receiver reads SharedPreferences(\u0026quot;conf\u0026quot;).getString(\u0026quot;dialcode\u0026quot;, \u0026quot;23723787\u0026quot;) and compares it byte-for-byte against the dialed number. A match calls abortBroadcast() — the call does not go through — and launches com.surebrec.StartActivity2, the administrative UI used to reconfigure the app after the launcher icon is hidden. A non-match calls clearAbortBroadcast() and the call proceeds normally, so the victim never observes that every outbound call is being intercepted.\nThe default value 23723787 is the T9 keypad mapping of \u0026ldquo;CERBERUS.\u0026rdquo; Operators can overwrite dialcode per-install with any numeric string through the standard SETAPPCONF command, and the new code is delivered inside the zero-IV AES-CBC restoreconf2.php blob that the device trusts by key derivation from its own ID. The mechanism is the canonical operator re-entry point after HIDE: once the launcher icon is disabled, dialing the configured code from the victim\u0026rsquo;s own dialer is how the abuser reaches the administrative screen.\nFragmented string literals Three string constants in com.ssurebrec v1.4.9 are fragmented across multiple literals and concatenated at runtime. com.surebrec.U2.U1 constructs admin@cerberusapp.com from \u0026quot;admin@cerb\u0026quot; + \u0026quot;erusapp.com\u0026quot;, constructs support@cerberusapp.com from \u0026quot;support@cer\u0026quot; + \u0026quot;berusapp.com\u0026quot;, and constructs the sideload self-update URL https://cerberusapp.com/download/version from \u0026quot;https://cerb\u0026quot; + \u0026quot;erusapp.com\u0026quot; + \u0026quot;/download/version\u0026quot; at U2.U1.java:2982. A suite-wide sweep for \u0026quot;X\u0026quot; + \u0026quot;Y\u0026quot; compile-time string concatenation surfaces exactly these three instances. Everything else — the 33-endpoint C2 URL list, the 44 FCM command-verb strings, the Firebase project identifier cerberus-lock-screen-protector, the Play Store deep links — ships as plain literals.\nA grep for cerberusapp.com in the decompiled output returns a complete URL inventory on the first try.\nMixed obfuscation across the suite Three of LSDroid\u0026rsquo;s five Cerberus Play apps — LSP, Kids, and Persona2 — ship with a post-R8 commercial obfuscator on top of standard Android R8. Main com.ssurebrec and Enterprise use plain R8 (main with heavy w7/-style repackaging, Enterprise with almost no repackaging at all). The three commercially-obfuscated APKs flatten library packages from canonical names like androidx/core/content/ down to single-uppercase-letter packages (A/, B/, C/) with single-letter class names within each — a shape consistent with DexGuard, DashO, or similar tools that run after R8.\nLSP is the most heavily affected: droidsaw flags 59% of LSP\u0026rsquo;s 2869 classes as adversarially obfuscated, with a naming distribution that doesn\u0026rsquo;t match R8\u0026rsquo;s expected output at all. LSP\u0026rsquo;s decompiled classes carry // Source: SourceFile instead of the // Source: r8-map-id-\u0026lt;hash\u0026gt; that the standard-R8 builds (main and Enterprise) emit — the R8 map ID has been stripped from the LSP artifact. Across all three commercially-obfuscated apps the obfuscation scope is the same: third-party library packages get flattened; LSDroid\u0026rsquo;s own com.lsdroid.* namespaces stay canonical because the Android manifest binds framework callbacks by string and renaming them would break the apps.\nWhat it defeats: pipeline-grade scanners that detect OkHttp, AndroidX, Firebase by package-path string-matching see the flattened classes as unrecognised code instead of well-known libraries. What it doesn\u0026rsquo;t defeat: type-signature matching against known library API shapes recovers every flattened class. The obfuscation imposes audit cost on automated tools — the same automated tools that gate Play-submission review.\nAnti-analysis countermeasures: absent Reading the binary, there is no protective layer beyond the partial obfuscation described above. There is no native code in the Play APKs beyond stock AndroidX (libdatastore_shared_counter.so and libandroidx.graphics.path.so, both Google-published, both \u0026lt;11 KB) — no JNI tamper-resistance, no native key derivation, no anti-debug. There is no runtime DEX loading: no LSDroid code calls DexClassLoader, PathClassLoader, InMemoryDexClassLoader, or equivalent. There are no LSDroid-attributable anti-Frida, anti-emulator, root-detection, or isDebuggerConnected checks; the only such checks in the Play binary belong to the vendored Firebase Crashlytics library and are passive (they enrich crash-report metadata, they don\u0026rsquo;t gate operation). String literals are not encrypted. The obfuscation is plain R8 — class and method names renamed, strings preserved.\nThe complete surveillance behavior is in the static bytecode of the shipped APKs. Reverse engineering the product requires no runtime instrumentation, no emulator-evasion countermeasures, no native-code disassembly.\nNotification suppression (v3.8.0) NotificationListener tracks all notifications with timestamps. When a command SMS arrives, any notification within ±5 seconds is cancelled via cancelNotification(). On root, it runs appops set \u0026lt;package\u0026gt; RUN_IN_BACKGROUND allow to prevent Android from killing it.\nAnti-forensics (v3.8.0) SETAPPCONF can set preventusbdebug=true remotely. This blocks USB debugging, preventing a forensic investigator from connecting via ADB to extract evidence.\nOEM background-restriction awareness Inside ShutdownAttemptReceiver.onReceive, as a literal string shipped in the compiled DEX: \u0026ldquo;Not able to execute action in background. To fix, please check www.dontkillmyapp.com\u0026rdquo;. dontkillmyapp.com is a community-maintained reference for OEM background-process restrictions on Android. Cerberus surfaces the URL when an action handler fails to run in the background, telling the operator\u0026rsquo;s dashboard which device-side workaround to apply.\nOEM-specific code paths Cerberus contains four distinct OEM-specific code paths, each targeting a different manufacturer behavior:\nXiaomi MIUI autostart-permission probe (main, LSP, Kids — via HiddenApiBypass). Each app reflects on android.miui.AppOpsUtils.getApplicationAutoStart to query MIUI\u0026rsquo;s autostart-permission state, gated on Build.MANUFACTURER == \u0026quot;xiaomi\u0026quot;. Detailed in What the bypass is used for. OPPO ColorOS power-menu fingerprints (LSP AccessibilityService). Hardcoded com.oplus.systemui.shutdown, COUIVerticalSeekBar, and COUISeekBar literals. Detailed in Detecting the power menu across OEMs. 17-OEM launcher whitelist (Kids MyAccessibilityService). Covers Xiaomi (com.miui.home), Huawei, OPPO, OnePlus, Lenovo, LG, Motorola, Sony, TCT, ZTE, Vivo\u0026rsquo;s BBK Electronics, CyanogenMod, LineageOS, AOSP, and Pixel. Samsung\u0026rsquo;s launcher (com.sec.android.app.launcher) is not in the list, though Samsung\u0026rsquo;s setup wizard is — the launcher omission appears unintentional. Detailed in Kids: \u0026ldquo;Cerberus Child Safety\u0026rdquo;. dontkillmyapp.com helper URL (ShutdownAttemptReceiver.onReceive). Literal helper-text shipped in the compiled DEX, surfaced when an action handler fails to run in the background. Detailed in OEM background-restriction awareness above. Three engineering investment cost tiers: cheap (launcher classification, list-lookup) reaches 17+ OEMs; medium (power-menu UI fingerprinting in LSP) reaches AOSP + OPPO + Samsung; expensive (Xiaomi MIUI persistence probe via HiddenApiBypass) reaches one OEM.\nLicense-expiration self-reveal com.surebrec.BootReceiver.onReceive reads the auth SharedPref (a reversed-timestamp encoding of license validity) on every boot and compares it against the current time. When the timestamp is in the past — the abuser\u0026rsquo;s subscription has lapsed — Cerberus exits covert mode. The boot-receiver code path that runs on expired-license boot:\nCancels Cerberus\u0026rsquo;s own background notifications (IDs 1 and 54321). Builds a \u0026ldquo;Forced unlock\u0026rdquo; notification with two action buttons — one launches StartActivity2 (the admin re-entry UI), the other launches Buy (the subscription-renewal activity). Sets hidden=false in SharedPreferences and re-enables the StartActivity launcher component via PackageManager.setComponentEnabledSetting(..., COMPONENT_ENABLED_STATE_ENABLED, DONT_KILL_APP) — undoing the launcher-icon hiding from HIDE. Stops BackgroundService. Calls DevicePolicyManager.removeActiveAdmin(...) — Cerberus relinquishes its device-admin claim, allowing the victim to uninstall normally. Logs a Firebase Analytics event select_content / item_id=buy_notif_shown — LSDroid\u0026rsquo;s analytics dashboard tracks each lapsed-license user who reaches this state. This is the inverse of HIDE: the same install transitions from invisible-to-victim to visible-with-buy-prompt based on subscription state, automatically, on the next reboot after expiration. The Firebase Analytics event select_content / item_id=buy_notif_shown records the transition; the same event name is used by Play Store apps to track conversion-funnel impressions.\nWhat the victim sees: two notification channels SurebrecApplication.onCreate declares two notification channels with opposite configurations:\nChannel Cerberus — IMPORTANCE_LOW (no sound, no vibration, no heads-up banner), lights off, vibration off, lockScreenVisibility = VISIBILITY_SECRET (-1) (hidden on the lock screen entirely). Used for the foreground-service notification posted during silent surveillance. Channel Forced unlock — IMPORTANCE_HIGH (heads-up banner pops up), lights off, vibration off, sound null, lockScreenVisibility = VISIBILITY_PUBLIC (1) (fully visible on the lock screen). Used for the FORCED_UNLOCK lure described below and the license-expiration buy prompt described above. The foreground-service notification. Whenever Cerberus invokes the camera, microphone, screen recorder, or location service, BackgroundService calls startForeground(1, notification) with FOREGROUND_SERVICE_TYPE_CAMERA (Android 10+) and the notification:\nBody text: \u0026quot;This device is protected by Cerberus\u0026quot; (resource string 2131886633) No content title (no setContentTitle call) Channel: Cerberus — IMPORTANCE_LOW, hidden on lock screen Tap intent: StartActivity2 with extra notification=true — opens the operator/abuser admin UI Notification ID: 1 This is the only persistent visible signal during silent capture. Android additionally shows the camera and microphone privacy indicators (the green/orange dots in the status bar, since Android 12) for the duration of capture. Those indicators are hardware-backed by the OS and cannot be suppressed without root. On com.ssurebrec (Play, no root) they always appear during the capture window.\nThe notification text reads \u0026quot;This device is protected by Cerberus\u0026quot;. It does not name the camera, microphone, location, or screen-recording activity that the foreground service is performing. With lockScreenVisibility = VISIBILITY_SECRET, the notification does not appear while the device is locked.\nThe FORCED_UNLOCK lure. LockScreenReceiver.onReceive posts notification ID 54321 on every SCREEN_OFF broadcast when w7.k4.e() (the rules-helper \u0026ldquo;is enabled\u0026rdquo; flag) returns true. The notification:\nChannel: Forced unlock — IMPORTANCE_HIGH, lock-screen public, heads-up banner Title: w7.k4.g() — operator-controlled text pulled from the rule configuration on the operator dashboard. The abuser writes whatever message they want. Tap intent: PendingIntent.getService(...) → ForcedUnlockService When the victim taps the notification, ForcedUnlockService.onStartCommand is a one-line passthrough that builds an Intent for SurebrecService with call=68 and starts it. SurebrecService case 68 logs the literal string \u0026quot;Event FORCED_UNLOCK received, executing actions in 15 seconds.\u0026quot; and schedules an action bundle 15 seconds out via setAndAllowWhileIdle, with action flags per the rule (location, picture, emergency, tasker).\nThe 15-second delay means the capture fires after the user has typically finished whatever lock-screen interaction they were performing — unlocking the phone, reading the notification, looking at the screen. The dispatched action bundle records a photo from the front camera, logs the GPS coordinates, and fires whatever Tasker macro the operator scripted.\nThe combination — operator-controlled lock-screen text on a high-importance public-visibility channel, tap-handler that schedules a delayed action bundle including silent camera capture — is reachable on every Cerberus install where the operator has enabled the feature on their dashboard. In an anti-theft framing the same code is the \u0026ldquo;thief sees notification, taps to interact, is photographed\u0026rdquo; flow. The mechanism is the same regardless of who installed Cerberus on whose phone; what the operator-configured text says, and to whom, depends entirely on the operator.\nCrypto weaknesses Password hashing U1.a(): MessageDigest.getInstance(\u0026quot;SHA-1\u0026quot;) with ISO-8859-1 encoding. No salt. The operator\u0026rsquo;s password hash is stored in SharedPreferences (hash key), synced to device-protected storage (accessible before first unlock), and transmitted to the C2 server during config restore. Trivially crackable.\nSMS command encryption Key: SMS keyword padded to 16 bytes with '0', reversed. IV: 16 null bytes. Algorithm: AES/CBC/PKCS5PADDING. Static null IV means identical commands produce identical ciphertext. Key derived from a user-chosen word.\nConfig restore encryption Same AES/CBC with device ID as key (padded/truncated to 16 bytes, reversed) and a literal 16-byte null IV. The encrypted config blob from /comm/restoreconf2.php contains the password hash, authorized SIM serials, phone numbers, and all feature flags.\nThe 16-byte \u0026ldquo;key\u0026rdquo; is the device identifier itself, padded with '0' to 16 characters when shorter, truncated to the first 16 characters when longer, then reversed and used as raw bytes. The decompiled body of the derivation function is unambiguous on both branches: String.format(\u0026quot;%-16s\u0026quot;, id).replace(' ', '0') followed by new StringBuilder(s).reverse().toString().getBytes().\nThe effective keyspace depends on which device-identifier source is reached. The two paths in U2.U1.i(Context, TelephonyManager) produce different shapes:\nIMEI path (when READ_PHONE_STATE is granted): the device ID is 15 decimal digits. Padded to 16 and reversed, the key consists of 16 ASCII bytes each with 10 possible values ('0'–'9' plus the appended '0'). Effective keyspace: 10¹⁶ ≈ 2⁵³. ANDROID_ID path (otherwise): the device ID is 16 hexadecimal characters. Reversed, the key consists of 16 ASCII bytes each with 16 possible values ('0'–'9', 'a'–'f'). Effective keyspace: 16¹⁶ = 2⁶⁴. The label \u0026ldquo;AES-128\u0026rdquo; suggests a 128-bit key. The actual entropy is 53 to 64 bits depending on which identifier source is reached, with the remaining bits non-random — they are the constraint that the key bytes happen to spell decimal digits or hex characters.\nThe 53–64-bit figure describes what a network-positioned attacker — without device-side access — faces; on a 2026-vintage GPU, even the 2^64^ end of that range completes in a small number of hours. That isn\u0026rsquo;t the threat model that matters for a stalkerware deployment. Anyone with realistic motivation to decrypt the config — a forensic investigator examining a victim\u0026rsquo;s device, a co-installed app, a researcher — has device-side access and can read the device identifier directly: IMEI from *#06# or the SIM tray, ANDROID_ID from Settings or any zero-permission co-installed app or Cerberus\u0026rsquo;s own exported IdProvider ContentProvider. With the device ID in hand, decrypting Cerberus\u0026rsquo;s encrypted config is O(1): read the ID, derive the key, decrypt.\nThere are three independent failures in the same code path: (1) deterministic CBC — the IV is a literal sixteen zero bytes, so identical plaintexts under the same key produce identical ciphertexts and a passive observer can distinguish repeated commands by ciphertext-equality alone, without any cryptanalysis. (2) 64-bit effective keyspace as derived above. (3) No MAC — the receiver cannot detect tampering, so the same padding-oracle that recovers plaintext also produces a forgery oracle for new ciphertexts. The plaintext-equality leak from (1) is the most serious of the three because it leaks protocol state — what command is being sent — without breaking the cipher at all.\nLock passcode derivation com.surebrec.LockActivity.onCreate renders a four-digit numeric unlock pad. On submit, the entered digits are compared against reverse(deviceID).substring(length - 4) — the first 4 characters of the device ID in reverse order. The device ID is what U2.U1.i(Context, TelephonyManager) returns; reading the decompiled body of that method shows the actual derivation: if the app holds READ_PHONE_STATE it returns TelephonyManager.getDeviceId() (the raw IMEI), otherwise it falls back to Settings.Secure.ANDROID_ID, with cosmetic stripping of -, _, ., and space characters. There is no hashing. The \u0026ldquo;device ID\u0026rdquo; used by the lock screen is the raw IMEI or the raw ANDROID_ID.\nWhen the device ID resolves to IMEI, the first 4 digits of the IMEI are the leading digits of the TAC (Type Allocation Code), which identifies the phone\u0026rsquo;s make and model. The TAC is public information — databases like imei.info list TACs by manufacturer and model. A thief who steals the phone, reads the brand off the casing, looks up the model\u0026rsquo;s TAC, reverses the first 4 digits, and tries the result will unlock in at most a handful of attempts (one per TAC variant of the model). No installed-on-device access required, no IMEI knowledge, no operator credentials. Cerberus\u0026rsquo;s \u0026ldquo;anti-theft lock screen\u0026rdquo; is unlocked by knowing what kind of phone it is.\nWhen the device ID resolves to Settings.Secure.ANDROID_ID, the first 4 hex characters carry roughly 16 bits of entropy in the abstract — but ANDROID_ID is exposed to any zero-permission co-installed app via Settings.Secure.getString(), and additionally exposed via Cerberus\u0026rsquo;s own com.surebrec.IdProvider (an exported ContentProvider declared with no android:permission). A zero-permission co-installed app recovers the unlock code by running content query --uri content://com.surebrec.IdProvider and taking the first four characters of the returned id column.\nThe same LockActivity displays the full device ID as a static line labeled \u0026ldquo;Device ID:\u0026rdquo; on the lock screen itself, so anyone holding the phone while it is locked can read the ID directly off the screen, take the first four digits, and unlock the device in one attempt. The \u0026ldquo;10,000 possible 4-digit codes\u0026rdquo; search space is theoretical; the code is recoverable in O(1) from public information about the phone, from the screen, from any other app on the device, or from Cerberus\u0026rsquo;s own unauthenticated ContentProvider.\nOperator-gated uninstall escape hatch LockActivity contains an android.widget.Button referred to in the decompiled layout binder as L, declared with android:visibility=\u0026quot;gone\u0026quot;. The button\u0026rsquo;s OnClickListener invokes the same Runtime.exec(\u0026quot;su\u0026quot;) → app_process → SuCommands allow_uninstall chain documented in the reflection-chain section, which calls IPackageManager.setBlockUninstallForUser(..., false) and then launches the system uninstall intent. The button\u0026rsquo;s visibility is toggled via a server-delivered configuration flag inside the restoreconf2.php blob. The operator controls, per-install, whether the victim is ever shown the \u0026ldquo;Allow uninstall\u0026rdquo; option from the lock screen.\nUpstream request authentication The Order header on every outbound C2 request is a 24-character lowercase alphanumeric nonce drawn from java.util.Random (not SecureRandom) — the non-cryptographic PRNG whose 48-bit internal seed is recoverable from three consecutive outputs in under a second on commodity hardware (the standard reference is Reeds 1977). The auth field on license-check requests is a reversed timestamp concatenated with random padding. Neither primitive authenticates anything: a network-positioned attacker who has observed a handful of requests can forge plausibly-ordered new ones, and the server has no HMAC or signature to reject them with. There is no upstream request authentication in the Play Store version beyond this cosmetic nonce.\nLock Screen Protector module com.lsdroid.lsp (v3.6) is a separate Play Store app labeled \u0026ldquo;Lock Screen Protector.\u0026rdquo; Its role:\nBlock power-off: Accessibility service monitors for the system power dialog, locks the device, and dismisses the dialog Block status bar: Prevents pulling down the notification shade (blocks airplane mode toggle) Screenshot on shutdown attempt: Captures screenshot and sends JPEG bytes via broadcast to the main app Device admin: force-lock policy, prevents casual uninstallation directBootAware: All components start before first unlock IPC between LSP and main app:\nLSP → main: broadcast com.lsdroid.shutdownattempt with screenshot bytes Main → LSP: broadcast com.surebrec.SHUT_STARTED Main queries LSP: content://com.lsdroid.lsp (exported, no permission) to check powerblock status Detecting the power menu across OEMs LSP\u0026rsquo;s com.lsdroid.lsp.AccessibilityService.onAccessibilityEvent inspects every accessibility event and checks the current window\u0026rsquo;s class name against two literals: com.android.systemui.globalactions.GlobalActionsDialog (the AOSP power menu) and com.oplus.systemui.shutdown (the OPPO ColorOS power menu). LSDroid has OPPO-specific targeting in the code. The COUIVerticalSeekBar and COUISeekBar literals in the same method are OPPO ColorOS widget class names — LSP has manufacturer-specific fingerprints for ColorOS power-menu layouts, which implies an OPPO customer base large enough to justify the code.\nScreenshot capture and the shutdown race When LSP detects the power menu, it calls this.takeScreenshot(DEFAULT_DISPLAY, mainExecutor, new I2.c(this)) — the Android 11+ accessibility screenshot API. The callback I2.c.onSuccess(ScreenshotResult) wraps the hardware buffer as a bitmap, compresses it to JPEG quality 70, falls back to quality 50 if the result exceeds 500 KB (just under Android\u0026rsquo;s Binder transaction limit), and broadcasts the JPEG bytes via sendBroadcast() with an explicit ComponentName pointing at com.surebrec.ShutdownAttemptReceiver, action string com.lsdroid.shutdownattempt, JPEG in the \u0026quot;s\u0026quot; extra.\nJPEG quality 70 preserves the legibility of text screens (passwords, 2FA codes, banking apps, messaging apps); busy images get retried at quality 50, and oversize screens above the 500 KB threshold silently drop.\nPower-off interception is a timing race, not a hard block. After capturing the screenshot, LSP broadcasts android.intent.action.CLOSE_SYSTEM_DIALOGS to dismiss the power menu. The victim sees the menu flash briefly and vanish. On slow devices under CPU load the race can fail and the shutdown can succeed. On a fast phone it\u0026rsquo;s nearly always successful; on an older or loaded phone, occasional shutdowns do go through.\nThe IPC is bidirectional. LSP also registers a runtime BroadcastReceiver (on API \u0026lt; 33) for action com.surebrec.SHUT_STARTED, which the main Cerberus app fires at LSP from the other direction to request a capture out-of-band. There are eight sendBroadcast() call sites in LSP\u0026rsquo;s AccessibilityService alone — the JPEG handoff is one of eight.\nShutdownAttemptReceiver has two modes The main-app side of the IPC is com.surebrec.ShutdownAttemptReceiver.onReceive. The receiver checks whether the Intent has an \u0026quot;s\u0026quot; extra. If yes, it writes the JPEG bytes to /data/data/com.ssurebrec/cache/scr.jpg (a fixed path with no randomization — each new screenshot overwrites the last one) and spawns an upload thread that POSTs the file to /comm/sendpicture.php.\nIf the Intent has no \u0026quot;s\u0026quot; extra — just the action com.lsdroid.shutdownattempt — the receiver runs a parallel action chain gated on operator-configured boolean flags: a 1.5-second debounce, then a fake shutdown dialog, then location capture (SurebrecService opcode 31 — a command not reachable via the FCM dispatcher, only through this path), then silent picture capture, then silent video capture, then a 90-second deferred emergency capture via AlarmManager.setAndAllowWhileIdle, then a Tasker macro if one is configured, then a push notification to the abuser\u0026rsquo;s paired smartwatch via com.google.android.gms.wearable.Wearable. The handler schedules the emergency capture to fire 90 seconds in the future. The recording does not start immediately on power press; it starts 90 seconds later.\nBecause the receiver is exported=\u0026quot;true\u0026quot; without a permission guard, and because the action string and the receiver component are both publicly known, any third-party app on a victim\u0026rsquo;s phone can fire the broadcast and trigger all eight reactions, subject to the operator\u0026rsquo;s flag configuration.\nLateral movement via Tasker Tasker is a well-known Android automation app (500k+ active installs on Play) that lets users script arbitrary device actions — launch apps, toggle settings, read sensors, send HTTP requests, manipulate files — in response to arbitrary events. It exposes a public Intent API: another app with the net.dinglisch.android.tasker.PERMISSION_RUN_TASKS permission can ask Tasker to run a named task by broadcasting an Intent.\nCerberus requests that permission in its manifest. com.surebrec.util.TaskerIntent, decompiled, contains five distinct error codes, dual package detection (net.dinglisch.android.tasker for the free version, net.dinglisch.android.taskerm for the paid Play Store version), and direct ContentProvider reads against Tasker\u0026rsquo;s internal settings at content://net.dinglisch.android.tasker/prefs to verify enabled=true and ext_access=true before dispatching — it queries Tasker\u0026rsquo;s own configuration through Tasker\u0026rsquo;s exported provider to check that the broadcast will be received before sending it.\nCross-referencing the three \u0026ldquo;Task not executed: …\u0026rdquo; log strings (which are only emitted in the Tasker error path) against every class in the decompiled source yields seven distinct event classes that dispatch Tasker macros:\n# Class / method Triggering event 1 com.surebrec.ShutdownAttemptReceiver.onReceive Power button press 2 com.surebrec.AdminReceiver.onPasswordFailed(Context, Intent) Wrong device-admin password typed 3 com.surebrec.GeofenceBroadcastReceiver.onReceive Entering or leaving a geofenced area 4 w7.n.onTrigger(TriggerEvent) Android low-power sensor trigger (significant-motion) 5 com.surebrec.BackgroundService.onStartCommand Background service lifecycle 6 com.surebrec.SurebrecService.onStartCommand Main service lifecycle 7 w7.p.onReceive(Context, Intent) An additional minified broadcast receiver Three of the seven trigger categories go beyond power-press / boot / service-lifecycle events. A wrong device-admin password triggers a Tasker macro: the abuser can configure \u0026ldquo;when the victim fails the lock-screen password, run task X.\u0026rdquo; A geofence crossing triggers a Tasker macro: the abuser can draw a polygon on the dashboard and schedule arbitrary automation on entry or exit. An Android significant-motion sensor event triggers a Tasker macro: \u0026ldquo;when the victim picks up the phone, run task X.\u0026rdquo; The dispatch code is in the binary; the operator configures the task name and the trigger via the dashboard.\nA rule-matching helper class, Lw7/k4;, exposes seven distinct event-dispatch methods (l, m, n, o, p, q, r), each with a different typed-parameter shape that corresponds to a different event category:\nl(Boolean, Boolean, Boolean, Boolean, String, Boolean, Boolean) — 4 boolean state flags + name + 2 booleans m(Boolean, Boolean, Boolean, Boolean, Boolean, String, Boolean, Boolean) — 5 boolean state flags + name + 2 booleans n(Boolean, Boolean, Boolean, Boolean, Boolean, String, Boolean, Boolean, Boolean) — 5 booleans + name + 3 booleans o(Boolean, String, Boolean, String) — simpler 4-parameter event p(String, Double, Double, Float, Boolean, Boolean, Boolean, Boolean, Boolean, Boolean, String) — (name, latitude, longitude, accuracy, ...flags..., extra) — the location event q(Boolean, Boolean, Boolean, Boolean, String, Boolean, Boolean) — 7-parameter event r(Integer, Boolean, Boolean, Boolean, Boolean, String, Boolean, Boolean) — integer payload (battery level) + 4 booleans + name + 2 booleans The seven methods correspond to the seven event triggers in the table above — power-button (ShutdownAttemptReceiver), wrong-admin-password (AdminReceiver), geofence-cross (GeofenceBroadcastReceiver), motion-sensor (w7.n.onTrigger), location update with lat/lon/accuracy (p), background-service lifecycle, and the Lw7/p; multi-event handler. Lw7/p; itself fans out across six system events (BATTERY_LOW, BATTERY_CHANGED, ACTION_POWER_CONNECTED, ACTION_POWER_DISCONNECTED, ACTION_SHUTDOWN, and Cerberus\u0026rsquo;s internal DEVICE_STARTED), each routable to a Tasker macro through one of the dispatch methods; the class alone contains 42 TaskerIntent broadcasts.\nThe typed parameters are used internally for rule matching — geofence-distance computation, battery-level threshold checks, foreground-package matching — not for parametrizing the Tasker macro. A bytecode-level scan across all 12,168 LSDroid methods in com.ssurebrec finds zero Intent.putExtra(\u0026quot;%var_name\u0026quot;, ...) calls with Tasker-style %-prefixed keys. The TaskerIntent constructor sets only task_name, version_number, and a random id extra; Cerberus fires the macro by name and passes no parameters. The operator\u0026rsquo;s Tasker macros must read device state through Tasker\u0026rsquo;s built-in state-query actions (e.g., %BATT, %LOC, %CONN); Cerberus\u0026rsquo;s contribution is \u0026ldquo;trigger this macro now,\u0026rdquo; not \u0026ldquo;trigger this macro with these values.\u0026rdquo; The seven typed-parameter shapes describe the events Cerberus can hook, not data it ships into Tasker.\nTasker is not sandboxed. Its permissions on a power user\u0026rsquo;s phone are typically broader than Cerberus\u0026rsquo;s own. A Tasker task can send HTTP requests to arbitrary URLs, read files, send SMS, place calls, control media, manipulate notifications, and run shell commands. Every capability Tasker has on the victim\u0026rsquo;s device is reachable through Cerberus via seven different event triggers. If the victim has Tasker installed, Cerberus\u0026rsquo;s reachable capability set extends to whatever the user\u0026rsquo;s Tasker macros are configured to do — without an APK update and without new permissions.\nThe C2 surface is 44 FCM commands plus a root shell plus everything the victim\u0026rsquo;s own Tasker install is configured to do.\nWhat a victim can do For someone who suspects Cerberus is on their device:\nIf you are in an abusive situation: Contact the National Domestic Violence Hotline (US: 1-800-799-7233) or the Coalition Against Stalkerware before doing anything on the device — even checking can alert the abuser, and the Kids app notifies the operator of permission changes in real time. Removal can also destroy forensic evidence. Consider contacting from a different device or a trusted person\u0026rsquo;s device; the compromised phone is itself a risk surface. Cornell Tech\u0026rsquo;s Clinic to End Tech Abuse (CETA), the NNEDV Safety Net Project, Operation Safe Escape, and WESNET (Australia) are DV-aware technologists who can plan a removal with the survivor. Outside the US: regional helplines. Safety planning first.\nDetection Check for these package names in Settings → Apps (show system apps): com.ssurebrec, com.surebrec, com.lsdroid.lsp, com.lsdroid.cerberus.kids, com.lsdroid.cerberus.persona2, com.lsdroid.cerberus.enterprise. The disguised version appears as \u0026ldquo;System Framework\u0026rdquo; with a generic Android icon. The LSP module appears as \u0026ldquo;Lock Screen Protector.\u0026rdquo; The Kids app appears as \u0026ldquo;Kids.\u0026rdquo;\nCheck accessibility services: Settings → Accessibility. Look for \u0026ldquo;System Framework,\u0026rdquo; \u0026ldquo;Lock Screen Protector,\u0026rdquo; or any service from the packages above with BIND_ACCESSIBILITY_SERVICE permission.\nCheck device admin: Settings → Security → Device admin apps. Cerberus registers as device admin to prevent uninstallation and enable lockNow().\nQuery the exported content providers from any app or ADB:\nadb shell content query --uri content://com.ssurebrec adb shell content query --uri content://com.surebrec adb shell content query --uri content://com.lsdroid.lsp If any returns data, the corresponding app is installed. The first two return the Cerberus device ID — the same ID used across the entire ecosystem.\nRemoval Revoke device admin: Settings → Security → Device admin apps → disable for Cerberus Disable accessibility services: Settings → Accessibility → disable \u0026ldquo;System Framework\u0026rdquo; / \u0026ldquo;Lock Screen Protector\u0026rdquo; / any Cerberus Kids service Uninstall any Cerberus apps found in Detection: Settings → Apps → Uninstall com.ssurebrec, com.surebrec, com.lsdroid.lsp, com.lsdroid.cerberus.kids, com.lsdroid.cerberus.persona2, com.lsdroid.cerberus.enterprise. Each app must be uninstalled separately — removing the main com.ssurebrec does not remove com.lsdroid.cerberus.kids or com.lsdroid.cerberus.enterprise. The Kids app\u0026rsquo;s notification listener and accessibility service persist independently; Enterprise is a parallel product that doesn\u0026rsquo;t depend on main. On rooted devices where block_uninstall was used, removal requires root access or factory reset. Check /system/etc/cerberus_conf.xml — if present, the config survives factory reset.\nDetection artifacts Package names com.ssurebrec # Play Store main app com.surebrec # Direct download main + WearOS com.lsdroid.lsp # Lock Screen Protector com.lsdroid.cerberus # Package alias (same code as com.surebrec) com.lsdroid.cerberuss # Package alias (same code as com.surebrec) com.lsdroid.cerberus.enterprise # Enterprise (marketed as MDM) com.lsdroid.cerberus.kids # Kids / \u0026#34;Child Safety\u0026#34; com.lsdroid.cerberus.persona2 # Persona / \u0026#34;Personal security — Women safe\u0026#34; Network indicators www.cerberusapp.com # Primary C2 (all apps except Enterprise) enterprise.cerberusapp.com # Enterprise C2 pippo.cerberusapp.com # Outbound mail relay wss://www.cerberusapp.com:8443 # WebSocket shell (v3.8.0) User-Agent: Cerberus \u0026lt;product\u0026gt; \u0026lt;version\u0026gt; - Android ... # all C2 traffic User-Agent: nokeepalive Cerberus ... # keepalive-disabled requests Filesystem artifacts /system/etc/cerberus_conf.xml /system/etc/cerberus_at.db {data}/shared_prefs/conf.xml {data}/databases/AutoTaskRules.db SharedPreferences indicators Key \u0026#34;hidden\u0026#34; = true → app is concealed from launcher Key \u0026#34;smskeyword\u0026#34; → SMS C2 trigger word Key \u0026#34;emergencymode\u0026#34; = true → continuous surveillance active Key \u0026#34;preventusbdebug\u0026#34; = true → anti-forensics active Key \u0026#34;protectadmin\u0026#34; = true → device admin removal blocked Broadcast actions com.lsdroid.shutdownattempt com.surebrec.SHUT_STARTED com.surebrec.DAILY_PING com.surebrec.CONN_DELAY shutshut # bare-name internal fake-shutdown signal (no reverse-domain namespace — distinctive) The companion apps Three more LSDroid apps ship from the same Play developer account alongside main Cerberus and LSP. The architectural picture is not uniform — the three companions split cleanly into two categories:\nAdd-ons (Kids, Persona2). Both layer additional surveillance capabilities on top of main, both carry their own Firebase project and (for Kids) their own FCM command channel, and both hard-depend on main being installed: each reads main\u0026rsquo;s device ID at launch via the unauthenticated IdProvider ContentProvider, and each refuses to function if main isn\u0026rsquo;t there. Their data joins main\u0026rsquo;s data stream on LSDroid\u0026rsquo;s backend under one device record. They are capability extensions on top of the main stalkerware, not standalone products.\nParallel product (Enterprise). Architecturally distinct from the add-ons: does not read main\u0026rsquo;s ContentProvider, does not share main\u0026rsquo;s device ID, does not depend on main being installed. Enterprise uses its own backend subdomain (enterprise.cerberusapp.com), its own server-assigned identity, and its own Firebase project. Enterprise is a separate product on LSDroid\u0026rsquo;s infrastructure that shares a developer account.\nThe companions are not orthogonal capability modules. They overlap heavily across surveillance categories — four of the five apps independently track GPS, three implement geofencing, three track app-inventory changes, two run their own accessibility services, two run their own NotificationListeners. Each app reimplements capability categories the others already cover:\nCapability Main LSP Kids Persona2 Enterprise GPS tracking yes — yes yes yes Geofencing yes — yes — yes App-inventory tracking yes — yes — yes NotificationListener yes — yes — — Accessibility service (uses LSP) yes yes (own) — — FCM command channel yes (44) — yes (17) — yes (5) What\u0026rsquo;s distinctive per app — the capabilities no other app in the suite covers:\nLSP: power-button intercept, status-bar overlay, screen-scrape via accessibility. Kids: continuous app-usage telemetry with operator alerts on installs, plus a NotificationListener configured for arbitrary-app suppression. The combined main + Kids + LSP deployment on a child\u0026rsquo;s phone runs three accessibility services, three FCM channels, three Firebase projects, and two NotificationListeners simultaneously. Persona2: lock-screen-active widget + per-victim shareable tracking URL that surfaces location data without requiring operator-login authentication. Persona2 is the only Cerberus app the victim can be persuaded to install themselves under its personal-safety-for-women positioning, which then forces installing main as a prerequisite. Enterprise: single-APK standalone deployment with no main-app dependency, plus marketing-as-MDM branding. Main: camera/microphone capture, screen recording, root shell (v3.8.0), SMS C2 channel (v3.8.0), and the full operator command surface (44 FCM commands). The redundancy is consistent with each product being independently built — different teams or different timelines, different Gradle workspaces, different obfuscator pipelines, different Firebase projects — with no shared LSDroid platform to consolidate the duplication. The suite is parallel implementations of overlapping surveillance capabilities with narrow distinctive add-ons per app, not clean modular extensions of a single core.\nThe bridge: IdProvider com.surebrec.IdProvider is a ContentProvider exported with android:exported=\u0026quot;true\u0026quot; and no android:permission attribute. Its query() method returns a single-column, single-row cursor containing the Cerberus device ID — the same identifier used in every upstream message to cerberusapp.com.\nquery() is seven lines:\npublic Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) { String[] cols = { \u0026#34;id\u0026#34; }; MatrixCursor cursor = new MatrixCursor(cols, 1); // R8-renamed U2.U1.i — IMEI if READ_PHONE_STATE, else ANDROID_ID cursor.newRow().add(w7.l5.d(getContext())); return cursor; } The other ContentProvider methods (delete, insert, update, getType, onCreate) are stubbed; IdProvider is read-only by design. The query() method ignores all five of its parameters — whatever a caller asks, the response is one column named id with one row containing the canonical device ID. There is no checkCallingPermission, no calling-UID check, no URI authority validation. The class as written has nowhere to put an authentication path.\nAny app on the device can query content://com.ssurebrec (Play Store) or content://com.surebrec (direct) and receive this ID without authentication. Two of the three companion apps do exactly this on launch. The device ID is the key that lets the server correlate data from all apps into a single victim profile.\nKids: \u0026ldquo;Cerberus Child Safety\u0026rdquo; (com.lsdroid.cerberus.kids v1.2.9) Installed on: the child\u0026rsquo;s device (CHILD role) and the parent\u0026rsquo;s device (PARENT role). Same APK, role chosen at setup.\nThe Kids app declares isMonitoringTool=child_monitoring in its manifest — Google\u0026rsquo;s required metadata for approved monitoring apps. It requires the main Cerberus app to be installed first. On launch, StartActivity queries the ContentProvider. If neither content://com.ssurebrec nor content://com.surebrec responds, the app shows: \u0026ldquo;Cerberus Anti-theft must be installed and configured on the phone, for this app to work\u0026rdquo; and redirects to the Play Store.\nThe Kids app is not a standalone product. LSDroid markets it as an add-on to the main Cerberus subscription — and every piece of evidence in the APK itself is consistent with that: the purchase URL routes through cerberusapp.com/buy.php?app=kids, the Kids app queries the main app\u0026rsquo;s com.surebrec.IdProvider for its device ID, both apps POST to the same cerberusapp.com/comm/* upstream endpoint family, and the Kids app refuses to run if the main app isn\u0026rsquo;t installed. A single-subscription model is the only architecture those dependencies fit.\nThe child\u0026rsquo;s phone runs two FCM command channels simultaneously. Kids has its own Firebase project (cerberus-kids) and its own FCM service (com.lsdroid.cerberus.kids.MyFirebaseMessagingService) that dispatches 17 short bidirectional command tokens (rsu, rau, rlu, rsr, asr, apn, opn, gt, i, ers, rn, t, s, gd, nan, lpn, lsn, asn). Some are upstream (CHILD-role: send stats, send location, send installed apps); some are downstream (PARENT-role: receive geofence-crossing notifications, receive new-app-installed alerts). Same APK, role chosen at install via ChildRoleDisclaimerActivity and stored in SharedPreferences.\nBecause Kids requires the main com.ssurebrec to be installed on the same device, every working Kids deployment is a co-deployment: the child\u0026rsquo;s phone runs Kids\u0026rsquo; 17-token FCM channel on cerberus-kids AND the main app\u0026rsquo;s 44-command FCM surveillance surface on api-project-999803017449. Two Firebase projects, two FCM channels, one device. Every Kids install on a child\u0026rsquo;s phone is also a main-app install on the same device.\nWhat the CHILD role collects:\nComplete app inventory every 30 minutes (InstalledAppsWorker), including app icons scraped from Google Play Store pages Per-app screen time at four granularities: hourly, daily, weekly, monthly (StatisticsWorker, MyAccessibilityService) GPS location every 30 minutes with on-demand high-accuracy fixes Geofence enter/exit events with exact crossing coordinates, speed, bearing, and accuracy Notification metadata — the NotificationListener can cancel or snooze notifications from restricted apps Permission status changes — if the child disables the accessibility service, location access, or notification listener, the operator receives an immediate alert All data sent to www.cerberusapp.com/comm/send_upstream_message.php — the same server infrastructure that handles camera captures, root shell sessions, and SMS exfiltration from the main stalkerware.\nWhat the accessibility service declares vs. what it does:\nThe accessibility service configuration requests canRetrieveWindowContent=\u0026quot;true\u0026quot; — the system grants permission to read every text element on screen. The user-facing description (displayed when enabling the service) states: \u0026ldquo;No data is collected or sent.\u0026rdquo;\nThe InstalledAppsWorker sends app inventories to cerberusapp.com every 30 minutes. The StatisticsWorker sends usage statistics every 4 hours. Permission changes trigger immediate upstream messages. The description is false.\nApp-block enforcement and OEM coverage. Kids\u0026rsquo; MyAccessibilityService enforces parental \u0026ldquo;block this app\u0026rdquo; rules by intercepting app-launch events. To avoid blocking the operating system itself, it consults a 48-entry static whitelist (Lcom/lsdroid/cerberus/kids/x;) of packages that are exempt from enforcement: AOSP system services, Google Play Services, package installer, settings, and 17 OEM launcher packages. The launcher-package list — com.miui.home (Xiaomi), com.huawei.android.launcher (Huawei), com.oppo.launcher (OPPO), net.oneplus.launcher (OnePlus), com.lenovo.launcher (Lenovo), com.lge.launcher2 and com.lge.launcher3 (LG), com.motorola.launcher3 (Motorola), com.sonyericsson.home (Sony), com.tct.tablet.launcher (TCT), com.zte.mifavor.launcher (ZTE), com.bbk.launcher2 (Vivo\u0026rsquo;s BBK Electronics), com.cyanogenmod.trebuchet (deprecated), org.lineageos.trebuchet, plus AOSP and Pixel — covers the OEMs Kids recognizes.\nThe Samsung launcher (com.sec.android.app.launcher) is not in the list. Samsung\u0026rsquo;s setup wizard (com.sec.android.app.SecSetupWizard) is, suggesting the launcher omission is unintentional. The consequence on Samsung phones: every interaction with the home screen is logged by StatisticsWorker as \u0026ldquo;child used app Launcher\u0026rdquo; rather than filtered out, and MyAccessibilityService posts an enforcement-decision log line and notification each time the child returns to the launcher under a \u0026ldquo;block all\u0026rdquo; policy. The phone is not bricked (the UsageLimitActivity overlay\u0026rsquo;s Home button uses an Android system intent that resolves to whatever the default launcher is), but the parent dashboard receives redundant launcher-as-app usage entries and enforcement notifications. Samsung is the largest Android OEM globally.\nWhat protections exist for the child\u0026rsquo;s data:\nNone specific to children. No certificate pinning. No network_security_config.xml. No local database encryption — data.db (Room) is unencrypted SQLite. No age verification. No verifiable parental consent — a checkbox saying \u0026ldquo;I\u0026rsquo;m a parent or legal guardian\u0026rdquo; with no identity verification. No privacy policy link in the app. Ad tracking enabled via com.google.android.gms.permission.AD_ID with allowAllToAccess=\u0026quot;true\u0026quot; in the ad services config and no tagForChildDirectedTreatment flag — Google\u0026rsquo;s ad network profiles the child without child-directed treatment protections.\nThe StartActivity contains a hardcoded blocklist of 22+ device identifiers (emulator IMEIs, DEFACE, NULL, known analysis rig serials) that prevent the app from running. The same blocklist appears in the main v3.8.0 app but not in the Play Store main app.\nWho legally owns the signing chain. The Kids APK is signed with a 2009 self-issued RSA-1024, SHA-1 certificate in the name of Luca Sagaria, with all identity fields other than the common name literally set to Unknown (CN=Luca Sagaria, OU=Unknown, O=Unknown, L=Unknown, ST=Unknown, C=Unknown). The cert was minted on 2009-03-13 — four years before the 2013 Verge profile first named Cerberus, years before LSDroid SRL was incorporated — and expires 2036-07-29. Everything else in the Cerberus Play ecosystem is either signed by Google under Play App Signing (LSP, Enterprise, Persona2) or by an LSDroid SRL corporate cert generated in June 2023 (main, disguised). Only the child-monitoring APK still ships under the founder\u0026rsquo;s personal developer identity with no corporate linkage in its signing chain. The entity that the Play listing, the billing relationship, and every GDPR data-controller analysis names as responsible for children\u0026rsquo;s data — LSDroid SRL — is not the entity whose name appears in the signing certificate of the software actually running on the child\u0026rsquo;s phone.\nPersona2: \u0026ldquo;Personal security — Women safe\u0026rdquo; (com.lsdroid.cerberus.persona2 v1.8) Installed on: the victim\u0026rsquo;s device. The victim installs it voluntarily — it is marketed as a personal-safety app for women.\nLSDroid\u0026rsquo;s Google Play listing title for com.lsdroid.cerberus.persona2 is literally \u0026ldquo;Personal security — Women safe\u0026rdquo;. The internal resource strings use \u0026ldquo;Cerberus Personal Safety\u0026rdquo; and \u0026ldquo;Cerberus Persona\u0026rdquo;; the customer-facing brand on the store page targets women specifically. The same developer account sells this \u0026ldquo;women safe\u0026rdquo; product and the covert-stalkerware main app (com.ssurebrec) — same C2 domain, shared device ID via the main app\u0026rsquo;s ContentProvider, and Persona2 refuses to register with the C2 if the main app isn\u0026rsquo;t installed.\nPersona2 does not declare isMonitoringTool in its manifest. It frames itself as the user\u0026rsquo;s own safety tool: \u0026ldquo;Persona can help in dangerous situations.\u0026rdquo; It sends emergency SMS with a cerberusapp.com/persona-{token} tracking URL to the victim\u0026rsquo;s contacts.\nOn launch, it queries the same ContentProvider. If neither authority responds, the method returns null. At current version (versionCode 21), there is no fallback — verified at the smali bytecode level. Persona2 cannot register with the C2 server without the main Cerberus app installed. It is a hard dependency, not an optional companion.\nThe hard dependency appears to be a retrofit. Decompiling Persona2\u0026rsquo;s identity-resolution path (I5/O0;-\u0026gt;p(Context)) shows a versionCode \u0026lt;= 10 branch that falls back to Settings.Secure.ANDROID_ID when the main app isn\u0026rsquo;t installed. That fallback is dead code at the current versionCode 21 — the gating check forces every fresh install to require main. Early Persona2 versions could run standalone with their own device-identity namespace; current Persona2 is bound to main. Somewhere between vc10 and vc21, LSDroid removed the standalone deployment path in favor of unified identity with the main stalkerware. The change is consistent with a product-strategy move toward consolidating customer identity across the suite.\nThe app actively directs users to install the main Cerberus app: \u0026quot;This app requires Cerberus Anti-theft.\u0026quot; with a link to market://details?id=com.ssurebrec.\nThe WidgetActivity declares setShowWhenLocked(true) and setTurnScreenOn(true) — the widget can activate from the lock screen without unlocking.\nGPS telemetry (latitude, longitude, accuracy, speed, bearing, battery level) is sent to cerberusapp.com/comm/persona_sendlocation.php. No certificate pinning.\nEnterprise: \u0026ldquo;Cerberus Enterprise\u0026rdquo; (com.lsdroid.cerberus.enterprise v1.7) Installed on: the employee\u0026rsquo;s device. Despite the \u0026ldquo;Enterprise MDM\u0026rdquo; product positioning, the APK has zero mobile-device-management capability. Architecturally, Enterprise is not a companion to the main Cerberus stalkerware — it doesn\u0026rsquo;t read main\u0026rsquo;s ContentProvider, doesn\u0026rsquo;t share main\u0026rsquo;s device ID, doesn\u0026rsquo;t depend on main being installed. It uses its own backend subdomain (enterprise.cerberusapp.com), its own Firebase project (cerberus-enterprise), and a server-assigned device identity. Enterprise is a parallel product on LSDroid\u0026rsquo;s infrastructure, not an add-on.\ncom.lsdroid.cerberus.enterprise v1.7 declares 13 permissions — all of them location, FCM, foreground-service, boot-completed, and notification. It does not declare BIND_DEVICE_ADMIN, it has no DeviceAdminReceiver in its manifest, and its AndroidManifest contains no Work Profile provisioning metadata. Every DevicePolicyManager reference in the decompiled binary resolves to a vendored Google library — Firebase Crashlytics\u0026rsquo; installer-trust helper (n8.j(Context, String) calling isDeviceOwnerApp / isProfileOwnerApp), and Google\u0026rsquo;s Android Management API client (com.google.android.gms.internal.amapi.*). LSDroid\u0026rsquo;s own 12 classes contain no device-policy code at all. A surface-level scanner that greps for DevicePolicyManager sees those library strings and assumes MDM capability; the APK has none.\nThe actual shape of com.lsdroid.cerberus.enterprise is twelve LSDroid classes — MainActivity, FCMService and its worker, RealTimeLocationService, LocationUpdatesBroadcastReceiver and worker, GeofenceSyncWorker, GeofenceTransitionBroadcastReceiver, BootBroadcastReceiver, PackageUpdateBroadcastReceiver, UpstreamResendWorker, a top-level utility class, and a Room database. The FCM command set is 5 commands — rt_location_start_request, rt_location_stop_request, geofence_sync_required (with policy_template_id, target_version, reason params), device_data (stores device_id to SharedPreferences), geolocation_enabled — versus the consumer app\u0026rsquo;s 44. The C2 surface is a new subdomain, enterprise.cerberusapp.com, with two endpoints: /api/send_upstream_message.php (upstream JSON POST, FCM token as the only device identifier) and /api/Geofences_DeviceSync.php (geofence-definition pull). Same authentication gap as the consumer side: no HMAC, no Authorization header, no certificate pinning, no custom network-security config.\nThe transparency of the foreground-service notification is real — \u0026ldquo;Your enterprise is tracking this device\u0026rsquo;s location in real-time.\u0026rdquo; What is not transparent is the product category. An IT admin who purchases \u0026ldquo;Cerberus Enterprise\u0026rdquo; under an MDM assumption is not buying managed-device software. They are buying an employee-location tracker with geofencing and an installed-app inventory, labelled Enterprise.\nThe distinction is not cosmetic. Under EU labour law, MDM software and employee-monitoring software sit in different compliance regimes. MDM has a well-trodden legitimate-interest + DPIA + works-council-notification framework. Employee-monitoring software without MDM capability is stricter: under Italy\u0026rsquo;s Statuto dei Lavoratori Article 4 — LSDroid\u0026rsquo;s home jurisdiction — remote monitoring of workers requires either a union agreement or prior authorization from the Ispettorato Nazionale del Lavoro. Germany\u0026rsquo;s Betriebsverfassungsgesetz requires works-council co-determination. The marketing label does not change the regulatory category. An Italian employer deploying Cerberus Enterprise to staff phones on the belief that MDM precedent applies is in the wrong regime.\nOne subscription, one customer The Cerberus website routes all product purchases through a single cerberusapp.com/buy.php?app=\u0026lt;name\u0026gt; checkout. The main app, the Kids app, and Persona2 all query the same shared device ID via com.surebrec.IdProvider / com.ssurebrec ContentProviders, all talk to the same cerberusapp.com C2, and each companion refuses to function without the main app installed. The architecture is consistent with a single-subscription model — one account, one backend, capability unlocked by product selection at install time.\nThere is no observed account tier that provides parental controls without surveillance capabilities. There is no data segregation between the anti-theft C2 and the child monitoring backend. The server receives GPS coordinates from the main app\u0026rsquo;s sendlocation.php and from the Kids app\u0026rsquo;s send_upstream_message.php with the same device ID. The child\u0026rsquo;s data and the stalkerware victim\u0026rsquo;s data sit in the same infrastructure.\nWhat the \u0026ldquo;anti-theft\u0026rdquo; defense doesn\u0026rsquo;t cover Cerberus markets itself as an anti-theft tool. Some of its features — GPS tracking, remote ring, remote lock, remote wipe — have plausible anti-theft use. But every one of those capabilities has been a free, built-in Android platform feature since August 2013, when Google launched Android Device Manager (now Find Hub). Every Android phone already ships with real-time device location, remote ring, remote lock, remote wipe, and — since April 2024 — offline Bluetooth-beacon tracking, all at no cost and with no third-party app installed. Apple\u0026rsquo;s equivalent predates Google\u0026rsquo;s by several years. The legitimate anti-theft customer is already served by a platform feature they didn\u0026rsquo;t have to install.\nWhat Cerberus adds beyond Find Hub\u0026rsquo;s feature set is the Cerberus product. The SMS exfiltration, call-log harvesting, contact exfiltration, microphone-only recording, interactive root shell, keylogging declaration, notification suppression, launcher-icon concealment, dial-code re-entry, Wi-Fi BSSID proximity tracking of specific networks, fake shutdown, and silent Intent-extras login serve no anti-theft purpose. Legitimate anti-theft apps (Google Find Hub, Samsung SmartThings Find, Apple Find My) do not bundle these capabilities, because the use cases for them are not anti-theft use cases.\nThe Kids app sits inside the same subscription as the main app, requires the main app as a dependency, and reads its device ID via an unauthenticated ContentProvider. None of the platform anti-theft tools (Find Hub, Apple Find My, Samsung SmartThings Find) have child-monitoring companions with these dependencies. The Kids ↔ main coupling is a single-vendor surveillance-suite architecture, not an anti-theft one.\nThe Kids app\u0026rsquo;s accessibility service description — \u0026ldquo;No data is collected or sent\u0026rdquo; — is a false statement in a Google-reviewed metadata field, while the app sends data to cerberusapp.com every 30 minutes. The app collects children\u0026rsquo;s precise GPS, complete app inventories, per-app screen time, and behavioral data with no certificate pinning, no local encryption, no age verification, no GDPR-compliant consent mechanism, and ad tracking enabled without child-directed treatment flags. This data flows through infrastructure shared with a rootkit that provides interactive shell access to victim devices.\nLSDroid is an Italian company. GDPR Article 8 requires consent from a parent or guardian for processing a child\u0026rsquo;s personal data, with \u0026ldquo;reasonable efforts to verify.\u0026rdquo; A checkbox is not verification. The Italian Garante has fined companies for less. Google\u0026rsquo;s Play Families Policy requires apps targeting children to comply with applicable children\u0026rsquo;s privacy laws, not serve uncertified ads, and provide a privacy policy. The Kids app provides no privacy policy link.\nThe public record Cerberus\u0026rsquo;s path through Google\u0026rsquo;s infrastructure is documented across nine years of public artifacts.\nNovember 2017. Google emailed LSDroid citing a Malicious Behavior Policy violation. LSDroid posted the email publicly.\nMay 2018. Chatterjee et al. published \u0026ldquo;The Spyware Used in Intimate Partner Violence\u0026rdquo; at IEEE S\u0026amp;P, naming Cerberus as IPV stalkerware and reporting the apps to Google.\nLate 2018 or early 2019. Google removed com.lsdroid.cerberus from Play, citing — by LSDroid\u0026rsquo;s own archived statement — the off-store-distribution policy. LSDroid continued distributing the same app from cerberusapp.com.\nSeptember 16, 2020 / effective October 1, 2020. Google published a dedicated Stalkerware Policy — a category of policy that did not exist when Cerberus was removed in 2018. The policy defines stalkerware as \u0026ldquo;code that collects and/or transmits personal or sensitive user data from a device without adequate notice or consent and doesn\u0026rsquo;t display a persistent notification that this is happening,\u0026rdquo; names a single carve-out for parental and enterprise monitoring apps, and explicitly excludes apps used to track \u0026ldquo;anyone else (a spouse, for example) even with their knowledge and permission.\u0026rdquo;\nOctober 2023. Google approved com.ssurebrec v1.0 for return to Play Store — three years and four days after the Stalkerware Policy went into effect. The launch capability set: 44 FCM commands including silent camera and microphone capture, fake shutdown, continuous location tracking, app-hiding, plus the org.lsposed.hiddenapibypass library.\n2024–2026. LSDroid expands the product line: Persona2 enrolls in Play App Signing on 2024-02-29. Five Cerberus apps distribute on Play. Google AdMob is the named buyer of ad impressions served inside the stalkerware (publisher pub-9848961826628138, listed in Google\u0026rsquo;s own sellers.json as LSDroid SRL). Google Firebase hosts the FCM command channels and the operator-state Realtime Database described in this writeup.\nPolicy violations on the current Play build Each citation is pinned to specific code in com.ssurebrec v1.4.9, verifiable from the live Play Store APK without LSDroid\u0026rsquo;s cooperation:\nNon-SDK interface access via HiddenApiBypass. Three Cerberus Play APKs ship org.lsposed.hiddenapibypass: com.ssurebrec v1.4.9, com.lsdroid.lsp v3.6, and com.lsdroid.cerberus.kids v1.2.9. Verification: unzip any one of the APKs, grep for org/lsposed/hiddenapibypass/. The library\u0026rsquo;s name is \u0026ldquo;Hidden API Bypass.\u0026rdquo; Google Play\u0026rsquo;s Malicious Behavior policy bans non-SDK interface access; the 2017 Google email to LSDroid cited that same policy area. Each of the three APKs is an independent ship and an independent policy citation.\nSilent runtime-permission grant of 16 dangerous permissions. com.surebrec.SuCommands.main(String[]) contains the full 16-permission grant loop as string literals plus the reflected call to IPackageManager.grantRuntimePermission.\nUninstall blocking via reflected IPackageManager. Same method: setBlockUninstallForUser string literal plus the reflected call via IPackageManager$Stub.\nLauncher-icon and recents-screen concealment. FCM command HIDE (opcode 9) in com.surebrec.FCMMessagingService.c(RemoteMessage). com.surebrec.Login.onCreate calls setTaskDescription(...) with null label and icon to remove the app from Android recents.\nPower-off interception and fake shutdown. com.lsdroid.lsp.AccessibilityService.onAccessibilityEvent plus the I2.c.onSuccess callback plus com.surebrec.ShutdownAttemptReceiver.onReceive.\nAd impressions served inside stalkerware via AdMob. Publisher ID pub-9848961826628138. Attribution verifiable via curl https://realtimebidding.google.com/sellers.json | jq '.sellers[] | select(.seller_id == \u0026quot;pub-9848961826628138\u0026quot;)' — returns LSDroid SRL.\nThe Stalkerware Policy that applies Google\u0026rsquo;s Stalkerware Policy took effect October 1, 2020 — three years before com.ssurebrec was admitted to the Play Store. It opens with the definition of the prohibited category:\nCode that collects and/or transmits personal or sensitive user data from a device without adequate notice or consent and doesn\u0026rsquo;t display a persistent notification that this is happening.\nIt then names the only carve-out:\nApps exclusively designed and marketed for parents to track their children or enterprise management, provided they fully comply with the requirements described below are the only acceptable surveillance apps. These apps cannot be used to track anyone else (a spouse, for example) even with their knowledge and permission, regardless if persistent notification is displayed.\nFor monitoring apps that qualify for the parental/enterprise carve-out, the policy lists four compliance requirements:\nApps must not present themselves as a spying or secret surveillance solution. Apps must not hide or cloak tracking behavior or attempt to mislead users about such functionality. Apps must present users with a persistent notification at all times when the app is running and a unique icon that clearly identifies the app. Apps and app listings on Google Play must not provide any means to activate or access functionality that violate these terms, such as linking to a non-compliant APK hosted outside Google Play.\nEach clause maps to a specific feature in the live com.ssurebrec v1.4.9 build. The mappings are pinned to source classes and methods so that a Play enforcement engineer can verify each one against the live APK without LSDroid cooperation:\nPolicy clause Cerberus implementation \u0026ldquo;Persistent notification at all times when the app is running\u0026rdquo; The foreground-service notification body reads \u0026quot;This device is protected by Cerberus\u0026quot; on a channel configured IMPORTANCE_LOW with lockScreenVisibility = VISIBILITY_SECRET — invisible on the locked screen, where the phone spends most of the day. The notification does not name the camera, microphone, location, or screen-recording activity occurring while it is posted. Source: SurebrecApplication.onCreate; BackgroundService.startForeground. \u0026ldquo;Unique icon that clearly identifies the app\u0026rdquo; The \u0026ldquo;disguised\u0026rdquo; build\u0026rsquo;s manifest declares android:label=\u0026quot;System Framework\u0026quot; and android:icon=\u0026quot;@android:drawable/sym_def_app_icon\u0026quot; — the generic stock Android icon. The HIDE command (FCM opcode 9) calls PackageManager.setComponentEnabledSetting() to disable the launcher activity entirely; setTaskDescription(null, null, color) removes the app from Android recents. Source: disguised build manifest; com.surebrec.FCMMessagingService.c; com.surebrec.Login.onCreate. \u0026ldquo;Apps must not present themselves as a spying or secret surveillance solution\u0026rdquo; LSDroid\u0026rsquo;s own promotional video, hardcoded inside the LSP APK at com.lsdroid.lsp.c line 27 and opened from within the LSP setup UI, describes the fake-shutdown feature as showing a fake power menu so that the person holding the phone believes it has been switched off while the device continues to track, record, and report. The shipped implementation matches the marketing description. Source: com.lsdroid.lsp.c:27; ShutdownDialogActivity. \u0026ldquo;Apps must not hide or cloak tracking behavior or attempt to mislead users about such functionality\u0026rdquo; HIDE command (launcher-icon concealment); dial-code re-entry (23723787 = T9 of CERBERUS, configurable per-install); NotificationListener cancels notifications within ±5 seconds of incoming command-SMS; fragmented string concatenation hides the C2 domain from static dependency scans. Source: opcode 9; StartReceiver.onReceive; NotificationListener; U2.U1. \u0026ldquo;Apps and app listings on Google Play must not provide any means to activate or access functionality that violate these terms, such as linking to a non-compliant APK hosted outside Google Play\u0026rdquo; Cerberus constructs the URL https://cerberusapp.com/download/version from runtime string concatenation (\u0026quot;https://cerb\u0026quot; + \u0026quot;erusapp.com\u0026quot; + \u0026quot;/download/version\u0026quot;) and uses it as a sideload self-update path. The string is reconstructed at runtime to evade the static-link prohibition. Source: U2.U1.java:2982. \u0026ldquo;Apps exclusively designed and marketed for parents to track their children\u0026rdquo; (the parental carve-out) Kids reads its device identity from the main stalkerware via the unauthenticated IdProvider ContentProvider (com.surebrec.IdProvider); Persona2 does the same. Both refuse to register with the C2 if the main app isn\u0026rsquo;t installed. Both feed LSDroid\u0026rsquo;s backend with the same device ID as the main stalkerware. Even if Kids alone qualified for the parental carve-out, its operational dependency on com.ssurebrec defeats the \u0026ldquo;exclusive\u0026rdquo; qualifier the policy requires. Source: IdProvider; Persona2 I5/O0;-\u0026gt;p(Context); Kids identity-resolution path. \u0026ldquo;These apps cannot be used to track anyone else (a spouse, for example) even with their knowledge and permission\u0026rdquo; The main com.ssurebrec is not designed for parental or enterprise use — operator dashboard, fake shutdown, dial-code re-entry, and Wi-Fi BSSID radar are advertised IPV-relevant features. Persona2 is marketed on its Google Play store page as \u0026ldquo;Personal security — Women safe\u0026rdquo; — explicitly outside the parental/enterprise carve-out. Source: Play Store listing for com.lsdroid.cerberus.persona2; LSDroid\u0026rsquo;s own marketing materials for the main app. Every clause of the policy maps to a specific implementation in the bytecode of the apps Google admitted in October 2023 and continues to host. The policy was published in 2020 to block exactly this category of app.\nAvailable enforcement surfaces Three Google product lines independently distribute, monetise, or operate parts of the Cerberus stack. Each is independently actionable on public evidence, without a court order:\nGoogle Play can remove com.ssurebrec and the four other LSDroid-distributed Cerberus apps under the Stalkerware Policy (every clause violated; see the clause-by-clause table above) and the Malicious Behavior Policy (HiddenApiBypass non-SDK-interface access in three of the five Play apps). A developer-account-level action against LSDroid SRL\u0026rsquo;s Play account clears all five Play listings simultaneously. Google AdMob can suspend publisher pub-9848961826628138. The AdMob account is independently actionable from the Play Store listing. Google Firebase / GCP can suspend the five Firebase projects (api-project-999803017449, cerberus-lock-screen-protector, cerberus-enterprise, cerberus-kids, cerberus-persona) that route the surveillance commands and persist operator-state data. If the five projects share a single GCP customer record, one account-level action takes them all offline; if not, per-project suspensions remain individually actionable. Each action reaches a different layer of Cerberus\u0026rsquo;s deployment.\nCerberus in commercial-stalkerware history Cerberus sits within a documented industry timeline:\nVendor Event Outcome mSpy 2015 + 2018 + 2024 customer-data breaches Continues to operate FlexiSpy 2017 customer-data leak Continues Retina-X Studios (MobileSpy / PhoneSheriff / TeenShield) 2019 FTC settlement — first stalkerware-vendor enforcement Required to demonstrate purchaser-consent verification before resuming sales; voluntarily shut down after 2018 data breach SpyFone (Support King) September 2021 FTC settlement under Section 5 of the FTC Act Banned from the surveillance industry; required to delete illegally harvested data; first FTC ban of a stalkerware vendor; followed 2018 data breach TheTruthSpy network (incl. Cocospy / Spyic / Spyzie) 2022 EFF letter to FTC urging investigation Amazon stonewalls breach notification, hosts victim data (Mar 2025); apps offline, Amazon deletes identified buckets after further reporting (May 2025); separate Cocospy repository public a year later (Apr 2026) Spyhide 2023 takedown after researcher disclosure Backend offline Of the FTC actions, only SpyFone faced a regulator-imposed industry ban; Retina-X faced a consent-verification requirement and chose to shut down rather than comply. Cerberus has been on Google Play continuously since October 4, 2023 despite meeting both FTC settlements\u0026rsquo; fact patterns: covert installation, monitoring without victim consent, marketing aimed at people who would surveil intimate partners, and design choices (the silent Intent-extras login, the hidden launcher icon, fake shutdown) that defeat any meaningful consent-of-the-monitored party. Despite EFF urging the FTC to investigate similar networks like TheTruthSpy in 2022, no public FTC action followed; the network went offline in May 2025 only after a data breach forced it.\nDisclosure The Cerberus apps documented here were reported to Google ahead of publication through Google Play\u0026rsquo;s reporting channel and Firebase abuse reporting.\nGoogle AdMob does not surface a public abuse-reporting channel for stalkerware monetization that this researcher could locate, despite academic research (Gibson et al., PoPETs 2022) finding that 99% of ad-monetized stalkerware uses AdMob and MIT Technology Review\u0026rsquo;s 2022 reporting documenting Google\u0026rsquo;s failure to enforce its existing ban on stalkerware ads. The publisher attribution (pub-9848961826628138, named as LSDroid SRL in Google\u0026rsquo;s own sellers.json) was therefore not separately reported and remains active at publication.\nLSDroid SRL was not given advance notice. Standard practice for commercial-stalkerware research is platform-side notification only: vendor advance-notice would create the opportunity to scrub indicators from infrastructure, migrate the command-and-control channel, or push a Play-Store update that disables the license-expiration self-reveal documented above — outcomes adverse to victim safety. The Coalition Against Stalkerware\u0026rsquo;s research conventions reflect the same reasoning.\nThe indicators of compromise from this research were submitted upstream to the stalkerware-indicators IOC database in commit fa248462 on 2026-04-10, ahead of publication. Every consumer of the Echap IOC feed (MVT, Quad9, AdGuard, TinyCheck, MISP) has the package names and the disguised-build display names available for end-user protection regardless of platform-side action.\nIf you are in an abusive situation: Contact the National Domestic Violence Hotline (US: 1-800-799-7233) or the Coalition Against Stalkerware before doing anything on the device — even checking can alert the abuser, and the Kids app notifies the operator of permission changes in real time. Removal can also destroy forensic evidence. Consider contacting from a different device or a trusted person\u0026rsquo;s device; the compromised phone is itself a risk surface. Cornell Tech\u0026rsquo;s Clinic to End Tech Abuse (CETA), the NNEDV Safety Net Project, Operation Safe Escape, and WESNET (Australia) are DV-aware technologists who can plan a removal with the survivor. Outside the US: regional helplines. Safety planning first.\n","date":"2026-04-30","description":"Decompiling Cerberus Stalkerware (LSDroid SRL): a static reverse engineering of 9 APKs across 8 packages, two distribution channels, three companion apps on Google Play. Documents the 44 FCM commands, the operator-state Realtime Database, the HiddenApiBypass dependency that gated the 2023 Play return, and the v3.8.0 reflected-binder-stub root toolkit. Includes attribution data, a chronology, and per-class indicators of compromise.","readingTime":84,"title":"Cerberus Anti-theft is stalkerware: a reverse engineering","url":"https://hexproof.dev/datagrams/cerberus-stalkerware-re/","wordCount":17769},{"author":"Mark Esler","content":"Every AI coding tool solves the same fundamental problem: fitting the right information into a fixed-size context window so an LLM can write correct code. Claude Code (v2.1.88, source maps), Aider (v0.86.3, Apache 2.0), Cursor (leaked prompts, v1.0-2.0), Windsurf (leaked prompts, Waves 1-11), and GitHub Copilot (vscode-copilot-chat v0.43.0, MIT) solve it five completely different ways.\nTobi Lutke named the discipline \u0026ldquo;context engineering\u0026rdquo; \u0026ndash; \u0026ldquo;the art of providing all the context for the task to be plausibly solvable by the LLM.\u0026rdquo; Mitchell Hashimoto pushed further to \u0026ldquo;harness engineering\u0026rdquo; \u0026ndash; \u0026ldquo;anytime you find an agent makes a mistake, you take the time to engineer a solution such that the agent never makes that mistake again.\u0026rdquo; Each architecture is a fossil record of the constraint its team built around. Claude Code: cost. Aider: model portability. Cursor: edit speed. Windsurf: autonomy. Copilot: model plurality. What follows is how each team engineered their harness, examined through source code, leaked prompts, and open-source repositories.\n1. Prompt Assembly The system prompt is the first thing the model sees. How it gets built determines everything downstream.\nDimension Claude Code Aider Cursor Windsurf Copilot Structure 20-section compiled pipeline 8-chunk ChatChunks dataclass ~17 XML sections Monolithic blob, XML sections 7+ per-model variant files Tool definitions Separate JSON schemas None Inline TypeScript types Inline TypeScript namespace JSON schemas, separate Build artifact? Yes (dead-code elimination) No (runtime template) No No Partial (per-model files) Approx. size ~12 KB static + dynamic ~2-8 KB ~19 KB (pruned from 39 KB) ~25 KB ~15-20 KB per variant x7+ Claude Code\u0026rsquo;s prompt is a build artifact. getSystemPrompt() assembles a string[] through a two-phase pipeline: static sections first (identity, system rules, coding guidelines, actions safety, tool preferences, tone, output efficiency), then dynamic sections after the SYSTEM_PROMPT_DYNAMIC_BOUNDARY marker (session guidance, memory, environment info, MCP instructions, compaction config). The build step evaluates process.env.USER_TYPE === 'ant' at compile time \u0026ndash; if false, the bundler strips internal-only code entirely. The public npm package never contains it. The prompt is compiled, not interpreted.\nAider\u0026rsquo;s prompt is the simplest. format_chat_chunks() returns a ChatChunks dataclass with eight ordered segments: system, examples, readonly_files, repo, done, chat_files, cur, reminder. Each coder type overrides the *_prompts.py fields. Dynamic elements are Python format strings substituted at runtime. No compilation, no build-time elimination, no markup. The prompt is a Python string.\nCursor went through a dramatic trajectory: v1.0 (9 KB) to v2.0 (39 KB, 4x expansion as features bolted on) to a later revision (19 KB, 2x reduction as stronger models needed less hand-holding). The \u0026lt;non_compliance\u0026gt; section is unique \u0026ndash; it instructs the model to detect and correct its own protocol violations mid-response.\nWindsurf ships everything in one blob. Every version follows the same pattern: identity preamble, XML-tagged behavioral sections, tool definitions as TypeScript namespace. The \u0026lt;EPHEMERAL_MESSAGE\u0026gt; mechanism silently injects mid-conversation directives from the IDE that the user never sees. Wave 11 added model identity injection: \u0026ldquo;if asked about what your underlying model is, respond with GPT 4.1\u0026rdquo; \u0026ndash; regardless of which model is actually serving the request. No other tool examined here hardcodes a model identity response.\nCopilot is unique in maintaining distinct system prompt variants per model. The open-source extension reveals 16 registered prompt resolvers via PromptRegistry \u0026ndash; far more than the 7 leaked prompt files suggested. GPT-5 through GPT-5.4 each have separate resolvers. Several model families are identified only by SHA256 hash of their family string, obscuring unreleased or partner models. The prompt assembly itself uses Microsoft\u0026rsquo;s prompt-tsx framework \u0026ndash; a JSX component tree where each section declares its priority and how much space it can grow into, and a renderer that allocates tokens the way CSS flex-grow allocates pixels. This is declarative prompt assembly with automatic budget management \u0026ndash; fundamentally different from Claude Code\u0026rsquo;s imperative function chain or Aider\u0026rsquo;s Python template strings. Nobody else does this.\n2. Caching Strategy Prompt caching is where harness engineering becomes economics. Anthropic\u0026rsquo;s API caches the static prefix of each request \u0026ndash; subsequent turns that match the prefix pay 10% of input cost. Any change to the prefix (model switch, new tool, flipped feature flag) busts the cache and charges full price. Every API user benefits from caching, but Claude Code\u0026rsquo;s architecture is designed to maximize hits: the static/dynamic prompt boundary, the system-reminder tags, the 14-dimension monitoring all exist to keep the prefix stable. Anthropic engineer Thariq Shihipar (February 2026) estimated that a long Claude Opus session at 100 turns costs Anthropic $50-100 without caching and $10-19 with.\nDimension Claude Code Aider Cursor Windsurf Copilot Default state ON (architecture depends on it) OFF Implicit (provider-native) None documented Annotations in prompt Boundary engineering SYSTEM_PROMPT_DYNAMIC_BOUNDARY, cacheScope annotations 3 optional breakpoints Relies on provider None copilot_cache_control markers Monitoring 14-dimension tracking, SEV on \u0026gt;5% miss increase None None None None Economic impact $50-100 → $10-19 per long session Optional savings Unknown Unknown Unknown Claude Code\u0026rsquo;s cache architecture is documented in promptCacheBreakDetection.ts. The system tracks 14 dimensions between API calls. It detects breaks by monitoring drops greater than 5% in cache_read_tokens. Two section types exist: systemPromptSection (computed once, cached) and DANGEROUS_uncachedSystemPromptSection (recomputes every turn, requires a justification string explaining why). The DANGEROUS_ qualifier means expensive, not insecure. Anthropic declares SEVs when cache hit rates drop. Stale information is sent as \u0026lt;system-reminder\u0026gt; tags in user messages rather than updating the system prompt, specifically to avoid breaking the cache.\nAider\u0026rsquo;s caching is opt-in via --cache-prompts. Three breakpoints when enabled. Keepalive pings every 4:55 (5-minute cache TTL). No monitoring, no break detection. The cache saves money when it works and silently costs full price when it doesn\u0026rsquo;t.\nCopilot\u0026rsquo;s source reveals a more sophisticated cache strategy than the leaked prompts showed. A 4-slot cache breakpoint allocation algorithm walks messages in reverse, placing breakpoints on tool results and user messages. A separate Anthropic-specific layer adds cache_control: ephemeral to the last tool definition and last system block. OpenAI\u0026rsquo;s Responses API uses prompt_cache_key = \u0026quot;{conversationId}:{model.family}\u0026quot; for cross-turn hits. This is provider-polymorphic caching \u0026ndash; different strategies per backend \u0026ndash; but still no monitoring infrastructure comparable to Claude Code\u0026rsquo;s 14-dimension tracking.\nCursor and Windsurf show no evidence of cache boundary engineering.\nOnly Claude Code treats caching as architecture. Everyone else treats it as a feature toggle or ignores it entirely.\n3. Codebase Awareness Three fundamentally different approaches: no index, structural index, and semantic index.\nDimension Claude Code Aider Cursor Windsurf Copilot Approach On-demand tools (Grep/Glob/Read) PageRank repo map (tree-sitter) Merkle tree + server embeddings M-Query RAG (proprietary) Semantic search tool Token cost/turn Variable (0 when not exploring) 1024-8192 always 0 (on invocation) 0 (on invocation) 0 (on invocation) Code leaves machine No No Yes Yes Yes Index without network Yes Yes No (server-side) No (server-side) No Claude Code navigates blind. The agent has no precomputed map. It uses Grep, Glob, and Read as a flashlight, building understanding incrementally through tool calls. Simplest, most flexible, most private. The cost is tokens for exploration.\nAider\u0026rsquo;s repo map is architecturally distinct. repomap.py parses every file with tree-sitter, builds a directed graph, runs PageRank with heavy weighting toward files in the current chat (50x), mentioned identifiers (10x), and downweighting private names (0.1x), then trims the map to fit the token budget. The model always knows the codebase topology without tool calls. The tradeoff: 1024-8192 tokens per turn regardless of whether the model needs codebase awareness, versus Claude Code\u0026rsquo;s on-demand exploration that costs nothing when the model already knows where it is. For sessions that involve heavy navigation, Aider\u0026rsquo;s approach is more token-efficient; for sessions focused on a known file, Claude Code\u0026rsquo;s is.\nCursor\u0026rsquo;s Merkle tree sync is the heaviest infrastructure. Client-side hash tree, server-side embedding, SimHash for team index reuse. Best recall for semantic queries. Your codebase is transmitted to and indexed on Cursor\u0026rsquo;s AWS infrastructure, re-embedded and persistently stored.\nWindsurf uses M-Query (proprietary, undocumented beyond marketing claims). Local protobuf index (~40 MB).\nCopilot uses semantic_search for embedding-based retrieval. For the cloud agent, code already lives on GitHub \u0026ndash; but the cloud agent runs in an ephemeral VM with network access, which is a different trust boundary than a git remote. The VM can execute arbitrary tool calls against a clone of the repo, and the firewalling/branch restrictions are the only guardrails.\n4. Edit Mechanism Each tool translates model intent into file changes differently.\nTool Model writes Harness finds the target by When it fails Claude Code Old string + new string Exact text match in file String not found in file Aider SEARCH/REPLACE text blocks Exact match, then fuzzy (5 levels) Wrong match from fuzzy approximation Cursor Sketch with // ... existing code ... gaps A second model merges sketch against original Merge model misreads the intent Windsurf Old chunk + new chunk Line ranges in the file Wrong lines matched Copilot Unified diff with class/function names Class and function names, not line numbers Named context not found The spectrum: exact match (Claude Code) → fuzzy match (Aider) → semantic sketch (Cursor) → semantic addressing (Copilot).\nCursor\u0026rsquo;s two-phase edit is the most architecturally novel. The primary model writes a sketch with // ... existing code ... placeholders. A fine-tuned 70B Llama on Fireworks takes the sketch plus the original file and produces the merged output at 1000+ tokens/second via speculative decoding. Neither Claude Code nor Aider does anything like this.\nCopilot\u0026rsquo;s apply_patch uses class/function names instead of line numbers:\n*** Begin Patch *** Update File: src/auth.py @@@ class AuthManager @@@ def validate_token - if token.expired: - return False + if token.expired: + logger.warning(\u0026#34;Token expired\u0026#34;) + return False *** End Patch Models are better at remembering \u0026ldquo;this is in validate_token of AuthManager\u0026rdquo; than \u0026ldquo;this starts at line 47.\u0026rdquo; Semantic addressing survives line-number shifts from concurrent edits.\nCopilot also maintains per-model edit formats \u0026ndash; apply_patch for GPT-4.1/GPT-5, replace_string_in_file for GPT-4o/Gemini, multi_replace_string_in_file for Claude \u0026ndash; because different models handle different formats with different reliability. The source reveals cross-pollination between competitors: the apply_patch parser is copyright OpenAI (Apache 2.0, from their cookbook), while the edit healing system is copyright Google LLC (Apache 2.0, adapted from Gemini CLI\u0026rsquo;s editCorrector.ts). Three competitors\u0026rsquo; code in one Microsoft codebase. For unknown bring-your-own-key models, an EditToolLearningService tracks success/failure per edit tool and dynamically selects the best-performing format \u0026ndash; a machine learning approach to edit format selection that no other tool has.\n5. Memory and Persistence In these tools, visibility into memory mechanisms tracks with resistance to poisoning. Cursor\u0026rsquo;s internals are not observable from leaked prompts alone; the table reflects what is externally visible.\nDimension Claude Code Aider Cursor Windsurf Copilot Mechanism 3-layer files None .cursorrules only create_memory (auto-fires) 3-scope virtual FS (user/session/repo) + instructions file Version-controlled Yes N/A Yes No (hidden dir) Yes Approval required Yes (file changes) N/A N/A No Varies Poisoning risk Low (file changes require approval) None (no memory) Moderate (repo file, no approval) Critical (SpAIware) Low (local); cloud agent has different trust boundary Claude Code\u0026rsquo;s memory is files on disk. CLAUDE.md in the repo, reviewed in PRs. MEMORY.md auto-persisted with frontmatter entries. autoDream runs background consolidation modeled on REM sleep (orient, gather, consolidate, prune). Everything is plaintext, version-controllable, inspectable.\nAider has no memory. Every session starts fresh. A feature, not a bug, for its use case.\nWindsurf\u0026rsquo;s memory is the worst security posture of the five. create_memory auto-fires without approval. Memories are stored at ~/.codeium/windsurf/memories/ but never surfaced in the UI. The prompt says: \u0026ldquo;You DO NOT need USER permission to create a memory.\u0026rdquo; This is the root cause of SpAIware: a single prompt injection permanently compromises all future sessions.\nThe pattern: visible file-based memory (Claude Code, Copilot) is auditable and resistant to poisoning. Hidden database-style memory (Windsurf) is convenient but creates a context gap the user cannot inspect \u0026ndash; the model\u0026rsquo;s behavior is shaped by memories the user never sees and cannot correct.\n6. Orchestration Dimension Claude Code Aider Cursor Windsurf Copilot Agent types 7 1 1 2 (planner + executor) 4 CLI modes + cloud agent Parallelism Fork subagents (cache-shared) + teammates (separate processes) None Concurrent tool calls (single agent) Concurrent tool calls (single agent) Fleet (separate processes) Tool restriction Primary behavioral control N/A (no tools) None None Mode-dependent Planning Extended thinking Architect mode (two-model) None (no dedicated planner) Background planner Plan mode (user-approved) Figure 1: Claude Code subsystem coupling. Three subsystems (Cache, Compaction, Agent) connected through the conversation driver loop and API client. The mutual recursion between query.ts and runAgent is how agents achieve multi-turn capability. Both Compaction and Agent converge on forkedAgent for cache-safe API calls. Blue dashed edges show cache feedback notifications after compaction events. (dot) Claude Code has the most sophisticated agent framework. Seven types with tool restriction as the primary behavioral control. Fork subagents share the parent\u0026rsquo;s cache prefix \u0026ndash; each additional fork pays only cache-read cost (10% of input) on the shared prefix, so five parallel forks cost roughly 1.4x one sequential agent, not 5x. Teammates are heavier \u0026ndash; separate processes that coordinate by writing files to disk.\nAider\u0026rsquo;s ceiling is architect mode: main model plans, editor model executes. Clean, simple, no subagents.\nWindsurf\u0026rsquo;s dual-agent planning (background planner + execution agent) is the most autonomous. Combined with 29 tools and the agentic loop, Cascade runs for extended periods without human checkpoints.\nCopilot\u0026rsquo;s Fleet mode is the closest to a distributed agent system. Each subagent is a separate process with its own model context. The cloud agent adds an ephemeral VM with network firewalling and branch restrictions.\n7. Compaction Dimension Claude Code Aider Cursor Windsurf Copilot Strategies 5 + autoDream 1 (recursive head summarization) Not documented Not documented 5 parallel systems Trigger Multiple thresholds Token budget exceeded Not documented Not documented Dual-threshold (80% background, 95% blocking) Instruction reinforcement Cached static prefix system_reminder repetition \u0026lt;non_compliance\u0026gt; self-correction \u0026lt;EPHEMERAL_MESSAGE\u0026gt; injection \u0026lt;reminderInstructions\u0026gt; tail Claude Code has five compaction strategies plus autoDream. A production bug illustrates the stakes: a comment in autoCompact.ts documents that 1,279 sessions experienced 50+ consecutive compaction failures, wasting 250,000 API calls/day. Fix: MAX_CONSECUTIVE_AUTOCOMPACT_FAILURES = 3.\nCopilot routes to multiple model providers (GPT, Claude, Gemini, Grok), and each provider compacts differently. The result is five parallel compaction systems \u0026ndash; the most complex compaction story of the five. A client-side background summarizer triggers at 80% context usage (non-blocking) and blocks at 95%. On the Anthropic backend: context editing clears thinking blocks and old tool uses server-side at 100K tokens. On the OpenAI backend: the Responses API handles compaction via encrypted content blobs at 90% threshold. Manual /compact and prompt-tsx budget truncation round out the set. The key architectural difference: Copilot delegates compaction to each provider\u0026rsquo;s native mechanism, while Claude Code owns it entirely. Post-compaction, Copilot saves the full pre-compaction transcript to disk and injects a hint: \u0026ldquo;use read_file to look up the full uncompacted conversation at {path}.\u0026rdquo; This transcript preservation is unique among the five.\nThe instruction reinforcement problem is universal: as context grows, behavioral adherence degrades. Morph/Chroma research tested 18 frontier models and found every one gets worse as input length increases. Each tool addresses this differently. Claude Code puts instructions in the cached static prefix (maximum attention weight). Copilot wraps rules in \u0026lt;reminderInstructions\u0026gt; at the end (primacy-recency effect). Aider repeats system_reminder. Cursor uses self-correction. Windsurf injects mid-conversation directives.\n8. Opacity Two mechanisms deserve mention. Windsurf\u0026rsquo;s \u0026lt;EPHEMERAL_MESSAGE\u0026gt; injects IDE directives into the conversation that the user never sees in the chat UI \u0026ndash; the model receives instructions the user cannot review. Copilot\u0026rsquo;s SHA256-hashed model family identifiers obscure which models are available or in use, making it impossible for users to audit what model processes their code without reverse-engineering the hash. Both are design choices that reduce user visibility into the system\u0026rsquo;s behavior. The security consequences of reduced visibility are measurable: AIShellJack found prompt injection success rates of 83.4% in Cursor\u0026rsquo;s auto mode versus 41-52% in Copilot, with tool-specific attack surfaces shaped by exactly these opacity decisions.\n9. What Converges, What Diverges Convergences XML-ish prompt sections. Tool-based interaction. Agentic loops (\u0026ldquo;keep going until done\u0026rdquo;). Per-file context injection. Project-level instruction files. Instruction repetition near the end of context.\nDivergences Dimension The Spectrum Caching Architecture (Claude Code) \u0026hellip; feature toggle (Aider) \u0026hellip; absent (Windsurf) Codebase awareness No index (Claude Code) \u0026hellip; structural (Aider) \u0026hellip; semantic (Cursor, Windsurf, Copilot) Code edit mechanism Exact match (Claude Code) \u0026hellip; fuzzy (Aider) \u0026hellip; sketch (Cursor) \u0026hellip; semantic (Copilot) Memory Visible files (Claude Code) \u0026hellip; hidden database (Windsurf) \u0026hellip; nothing (Aider) Orchestration 7 agent types (Claude Code) \u0026hellip; 5 surfaces (Copilot) \u0026hellip; dual-agent (Windsurf) \u0026hellip; single (Cursor) Prompt variants Per-model (Copilot) \u0026hellip; per-coder-type (Aider) \u0026hellip; single (Claude Code, Cursor, Windsurf) 10. The Fossil Record Each architecture is a fossil record of the constraint its team built around.\nClaude Code built around cost. Anthropic runs the model and pays per-token. The entire architecture \u0026ndash; static/dynamic boundary, global cache scope, cache-break monitoring, SEV alerting, fork subagent cache sharing, \u0026lt;system-reminder\u0026gt; tags to avoid prompt mutations \u0026ndash; exists to minimize per-session cost. Every other design decision is downstream of cache economics.\nAider built around model portability. The tool supports any model through any provider via LiteLLM. Function calling was tried then deliberately abandoned. Edits are text-prompted and regex-parsed, requiring 14 coder types. The three-model architecture (main, weak, editor) exists because cheap models can handle summarization. The entire architecture serves model portability.\nCursor built around edit speed. The two-phase sketch-plus-apply-model pattern exists because speculative decoding at 1000+ tokens/second on a fine-tuned 70B Llama makes edits feel instant. The Shadow Workspace (a hidden Electron window with gRPC/protobuf IPC providing lint feedback before presenting changes) addresses the second-order problem of fast edits being wrong edits. Speed created the architecture; correctness created the Shadow Workspace.\nWindsurf built around autonomy. The trajectory from Dec 2024 (11 tools, conservative pair programmer) through Wave 11 (29 tools, dual-agent planning, browser automation, deployment, persistent memory) is unidirectional: more capability, more tools, less human-in-the-loop. create_memory fires without approval because autonomous agents need persistence. read_url_content fires without approval because autonomous agents need web access. The same design philosophy that makes Windsurf the most capable tool makes it the most vulnerable.\nCopilot built around model plurality. GitHub supports GPT-4.1, GPT-5 through GPT-5.4, Claude Sonnet 4, Gemini, Grok, plus SHA256-hashed unreleased partners. Each model responds differently to prompting strategies. So Copilot maintains 16 prompt resolvers, 4 edit strategies with per-model selection, provider-specific compaction (Anthropic context editing, OpenAI Responses API, client-side summarization), and an edit tool learning system that dynamically discovers what works for unknown bring-your-own-key models. The apply_patch parser is OpenAI\u0026rsquo;s code. The edit healing is Google\u0026rsquo;s code. Three competitors\u0026rsquo; IP in one Microsoft codebase — the architecture is a treaty as much as it is engineering. The five execution surfaces exist because GitHub serves enterprises that need VM isolation AND individual developers who want IDE convenience. Architecture follows the customer base.\nThe harness is the fossil record. The problem is the same for everyone: build the system around the model so it can do useful work, and engineer it so mistakes don\u0026rsquo;t recur. The architecture reveals which constraint dominated.\n","date":"2026-04-08","description":"How Claude Code, Aider, Cursor, Windsurf, and Copilot engineer the systems around their models — and what the architecture reveals about each team's priorities. From source code and leaked prompts.","readingTime":15,"title":"The Fossil Record of Harness Engineering","url":"https://hexproof.dev/datagrams/fossil-record-harness-engineering/","wordCount":3129}],"description":"Security and systems performance research","feeds":{"json":"/index.json","rss":"/datagrams/index.xml"},"llms":"/llms.txt","site":"hexproof"}