FlutterShell — Operation FlutterBridge (macOS Backdoor)
Executive Summary
FlutterShell is a macOS backdoor distributed in a malvertising campaign Palo Alto Unit 42 codenamed Operation FlutterBridge (actor CL-CRI-1089, active since at least 2023; successor to the 2025 JSCoreRunner/FileRipple campaign). The crew buys Google and YouTube ads through a rotating set of Google-verified shell companies (e.g. AdsParkPro LTD, Advantage Web Marketing LLC, SOFT WE ART LIMITED → PACIFIC TRADE SOLUTIONS LTD), pushing fake desktop apps to macOS users in the US, Canada, Australia, France and Germany.
The malware is built with the Flutter framework and ships as notarized apps with valid Apple Developer IDs — masquerading as AI productivity tools: PodcastsLounge, PDF-Brain and PDF-Ninja. Its WebView architecture exposes a JavaScript-to-native bridge that grants arbitrary command execution, file-system manipulation, environment-variable exfiltration, system fingerprinting and browser-session theft. The malicious logic is pulled dynamically from attacker infrastructure, so the signed app body stays "clean" and the behavior can change without re-notarization. A core capability is Chrome traffic hijacking — the app rewrites Chrome configuration to route the victim's browsing through an attacker-controlled intermediary.
The strongest defensive angle on macOS is the post-install behavior: a freshly-installed "PDF/podcast" app modifying Google Chrome's preference files and spawning shells that read env vars and reach external infrastructure. Code-signing is no defense here — the apps are legitimately notarized — so hunt the behavior, not the signature.
Defender priority: Hunt macOS endpoints for the app bundles PodcastsLounge / PDF-Brain / PDF-Ninja, any non-Chrome process modifying Chrome's Preferences/Secure Preferences, and LaunchAgent persistence written by these apps. No public hashes/C2 were released — detection is behavior-led; fill §10 as Unit 42 publishes atomics.
Source Review & Web Hunter Notes
| Tier | Source | Key Finding | Carry Forward |
|---|---|---|---|
| 1 | Palo Alto Unit 42 — Operation FlutterBridge (via The Hacker News, 2026-06-04) | Actor CL-CRI-1089; Flutter/WebView JS-native bridge; notarized masquerading apps (PodcastsLounge, PDF-Brain, PDF-Ninja); Chrome hijack; dynamic external payload; shell-company ad fronts | Yes |
| 2 | Unit 42 JSCoreRunner / FileRipple reporting (Aug 2025) | Prior campaign lineage; same actor's malvertising-to-macOS-backdoor playbook | Partial |
No atomic IOCs (file hashes, C2 domains/IPs, exact bundle IDs) were published in the available reporting. This pack is behavior-led; §10 uses REPLACE_WITH_* placeholders so atomics drop in unmodified when Unit 42 releases its appendix. Nothing here is fabricated.
Hunt Brief & Attack Chain
Hunt Hypotheses (fidelity-ordered)
- H1 (high): An app bundle named
PodcastsLounge/PDF-Brain/PDF-Ninjaexecutes from/Applicationsor a user Downloads path. - H2 (high): A non-Chrome process rewrites Chrome
Preferences/Secure Preferences— the traffic-hijack step. - H3 (high): A LaunchAgent/LaunchDaemon plist is written referencing one of the masquerading apps (persistence).
- H4 (medium): A FlutterShell app spawns
/bin/sh/bashrunningcurl/env— arbitrary command exec + env-var exfil via the JS bridge. - H5 (medium): The app fetches its dynamic payload from external infra shortly after first launch.
- H6 (low): Quarantine attribute / first-run of a notarized "PDF/podcast" app downloaded via an ad click.
Attack Chain
| Step | Behavior | Telemetry | Hunt Angle |
|---|---|---|---|
| 1 · Malvertising | Google/YouTube ad via verified shell company → fake app site | DnsRequest, NetworkConnectIP4 | Q5 |
| 2 · Delivery | User downloads notarized FlutterShell app (PDF/podcast lure) | FileCreateInfo, ProcessRollup2 | Q1, Q6 |
| 3 · Execution | Flutter app launches; WebView JS-to-native bridge active | ProcessRollup2 | Q1 |
| 4 · Dynamic payload | App fetches malicious logic from attacker infra | NetworkConnectIP4, DnsRequest | Q5 |
| 5 · Browser hijack | Rewrites Chrome config to route traffic via intermediary | FileCreateInfo (Chrome prefs) | Q2 |
| 6 · Collection / exfil | Shell exec, env-var read, browser-session theft | ProcessRollup2 CommandLine | Q4 |
| 7 · Persistence | LaunchAgent/LaunchDaemon plist | FileCreateInfo (LaunchAgents) | Q3 |
Consolidated IOC Table
| Type | Value | Confidence | Action | Context |
|---|---|---|---|---|
| appname | PodcastsLounge | high | hunt | FlutterShell masquerade |
| appname | PDF-Brain | high | hunt | FlutterShell masquerade (AI PDF lure) |
| appname | PDF-Ninja | high | hunt | FlutterShell masquerade (AI PDF lure) |
| behavior | Non-Chrome process rewriting Chrome Preferences | high | hunt | Traffic-hijack step |
| ad-front | AdsParkPro LTD / Advantage Web Marketing LLC / SOFT WE ART LIMITED / PACIFIC TRADE SOLUTIONS LTD | medium | enrich | Google-verified ad shell companies |
| sha256 | REPLACE_WITH_FLUTTERSHELL_APP_HASH | pending | detect | Populate from Unit 42 appendix |
| domain | REPLACE_WITH_DYNAMIC_PAYLOAD_C2 | pending | detect | Dynamic payload / intermediary host |
Affected Surface & Telemetry Matrix
| Surface | Required Telemetry | Priority | Gap Risk |
|---|---|---|---|
| macOS endpoints (US/CA/AU/FR/DE users) | ProcessRollup2 (macOS sensor) | High | Medium — requires Falcon macOS coverage |
| Chrome profile config files | FileCreateInfo / file-write events | High | Medium — app-data writes not always logged |
| LaunchAgent/Daemon persistence | FileCreateInfo on plist paths | High | Low |
| Egress to dynamic-payload infra | NetworkConnectIP4, DnsRequest | Medium | High — no published infra to anchor |
| JS-to-native bridge command exec | ProcessRollup2 (child shells) | Medium | Medium — in-WebView logic opaque |
ATT&CK Mapping
| Tactic | Technique | Observed Behavior | Query / Control |
|---|---|---|---|
| Resource Development | T1583.008 Malvertising | Google/YouTube ads via verified shell companies | Q5 / web filtering |
| Initial Access / Execution | T1204.002 Malicious File | User installs notarized fake PDF/podcast app | Q1, Q6 |
| Execution | T1059.004 Unix Shell | JS-native bridge spawns /bin/sh | Q4 |
| Defense Evasion | T1036.005 Masquerading | Valid Developer ID + notarization; AI-tool branding | Q1 |
| Persistence | T1543.001 Launch Agent | LaunchAgent/Daemon plist | Q3 / M1018 |
| Collection | T1539 / T1552.001 Browser session & env-var theft | Reads Chrome session data, env vars | Q2, Q4 |
| Defense Evasion | T1539 Browser Hijack (config rewrite) | Rewrites Chrome Preferences to proxy traffic | Q2 |
| Command & Control | T1105 Ingress Tool Transfer | Dynamic payload pulled from attacker infra | Q5 |
Native Audit-Log Hunts
- macOS Unified Log / MDM: Inventory installed apps for
PodcastsLounge,PDF-Brain,PDF-Ninja(and any AI PDF/podcast app installed via ad click); flag those with recently-issued Developer IDs. - LaunchAgents/Daemons sweep: Enumerate
~/Library/LaunchAgents,/Library/LaunchAgents,/Library/LaunchDaemonsfor plists pointing at these app bundles. - Chrome config integrity: Diff
~/Library/Application Support/Google/Chrome/Default/PreferencesandSecure Preferencesagainst a known-good baseline; look for injected proxy / startup-URL entries. - Secure web gateway: Hunt ad click-throughs to non-official "PDF tool" download domains and alert on first-seen app-download domains advertised via Google/YouTube.
CrowdStrike LogScale CQL Hunt Queries
Looks for: Execution of the known FlutterShell app bundles on macOS endpoints.
// HUNT: FlutterShell masquerading app execution (Operation FlutterBridge) // MITRE: T1204.002, T1036.005 // CONF: high FP: low COST: low | REQUIRES: ProcessRollup2 (macOS) #event_simpleName=/ProcessRollup2|SyntheticProcessRollup2/ | ImageFileName=/\/(PodcastsLounge|PDF-Brain|PDF-Ninja)(\.app)?\//i | table([ComputerName, UserName, ImageFileName, CommandLine, SHA256HashData], limit=200)
Looks for: A process other than Chrome writing Chrome's Preferences/Secure Preferences — the traffic-hijack step. FP: some MDM/profile tools touch Chrome policy; exclude known management agents.
// HUNT: Chrome preference rewrite by non-Chrome process (FlutterShell hijack) // MITRE: T1539 // CONF: high FP: medium COST: low | REQUIRES: FileCreateInfo (macOS) // TUNING: exclude MDM/endpoint-mgmt agents that legitimately set Chrome policy #event_simpleName=/FileRename/ | TargetFileName=/\/Google\/Chrome\/.*(Secure Preferences|Preferences)$/i | ContextBaseFileName!=/^(Google Chrome|com\.google\.Chrome|chrome)$/i | table([ComputerName, UserName, ContextBaseFileName, TargetFileName], limit=200)
Looks for: A LaunchAgent/LaunchDaemon plist written for one of the FlutterShell apps. FP: legitimate apps add LaunchAgents; the app-name match keeps this tight.
// HUNT: FlutterShell LaunchAgent persistence (Operation FlutterBridge) // MITRE: T1543.001 // CONF: high FP: medium COST: low | REQUIRES: FileCreateInfo (macOS) // TUNING: broaden to any new plist if app name is obfuscated at install time #event_simpleName=/FileRename/ | TargetFileName=/\/Library\/Launch(Agents|Daemons)\/.*(PodcastsLounge|PDF-Brain|PDF-Ninja|flutter).*\.plist$/i | table([ComputerName, UserName, ContextBaseFileName, TargetFileName], limit=200)
Looks for: One of the FlutterShell apps spawning /bin/sh/bash running curl or reading env vars — JS-bridge command exec + exfil. FP: some legit apps shell out; the parent app-name anchor keeps FP low.
// HUNT: FlutterShell JS-bridge shell exec / env exfil (Operation FlutterBridge) // MITRE: T1059.004, T1552.001 // CONF: medium FP: medium COST: low | REQUIRES: ProcessRollup2 (macOS) // TUNING: scope to the masquerade app-name parents; exclude approved apps that legitimately shell out #event_simpleName=/ProcessRollup2|SyntheticProcessRollup2/ | ParentBaseFileName=/(PodcastsLounge|PDF-Brain|PDF-Ninja)/i | ImageFileName=/\/(sh|bash|zsh|curl)$/i | table([ComputerName, UserName, ParentBaseFileName, ImageFileName, CommandLine], limit=200)
Looks for: Network egress initiated by a FlutterShell app — the dynamic-payload pull / hijack proxy. FP: populate DomainName once Unit 42 releases infra; until then review any egress from these apps.
// HUNT: FlutterShell dynamic-payload egress (Operation FlutterBridge) // MITRE: T1105, T1583.008 // CONF: medium FP: medium COST: low | REQUIRES: NetworkConnectIP4 (macOS) // TUNING: add published C2 domains to DomainName when Unit 42 releases infra #event_simpleName=/NetworkConnectIP4/ | ContextBaseFileName=/(PodcastsLounge|PDF-Brain|PDF-Ninja)/i | table([ComputerName, ContextBaseFileName, RemoteAddressIP4, RemotePort], limit=200)
Looks for: A FlutterShell app first executing from a user Downloads/quarantine path (ad-driven install). FP: any legitimately downloaded app runs from Downloads once; pair with the app-name list.
// HUNT: First-run FlutterShell app from Downloads (Operation FlutterBridge) // MITRE: T1204.002 // CONF: medium FP: medium COST: low | REQUIRES: ProcessRollup2 (macOS) // TUNING: a legit downloaded app runs from Downloads once — gate on the masquerade app-name list to cut FP #event_simpleName=/ProcessRollup2|SyntheticProcessRollup2/ | ImageFileName=/\/(Downloads|Desktop)\/.*(PodcastsLounge|PDF-Brain|PDF-Ninja)/i | table([ComputerName, UserName, ImageFileName, ParentBaseFileName, SHA256HashData], limit=200)
CrowdStrike Custom IOA Recommendations
| IOA Name | Pattern | Exclusions | Action |
|---|---|---|---|
| FlutterShell — Masquerade App Exec | macOS process image matches PodcastsLounge|PDF-Brain|PDF-Ninja | None | Detect (high) |
| FlutterShell — Chrome Hijack | Non-Chrome process writes Chrome Secure Preferences | MDM/endpoint-mgmt agents | Detect (high) |
| FlutterShell — App-Parented Shell | Masquerade app spawns sh/bash/curl | None | Detect (medium) |
Q1 is the strongest IOA promotion candidate. Q2/Q4 promote after the MDM-agent exclusions are validated. Falcon macOS sensor coverage is a prerequisite for all of these.
Machine-Readable IOC Appendix
type,value,action,severity,expiration,description,tags sha256,REPLACE_WITH_FLUTTERSHELL_APP_HASH,prevent,critical,2026-12-24,FlutterShell macOS app,campaign:FlutterBridge sha256,REPLACE_WITH_FLUTTERSHELL_VARIANT_HASH,prevent,critical,2026-12-24,FlutterShell variant,campaign:FlutterBridge domain,REPLACE_WITH_DYNAMIC_PAYLOAD_C2,detect,high,2026-09-24,Dynamic payload / hijack proxy,campaign:FlutterBridge domain,REPLACE_WITH_FAKE_APP_DOWNLOAD_DOMAIN,detect,high,2026-09-24,Malvertising app download site,campaign:FlutterBridge
macOS process image ~ PodcastsLounge | PDF-Brain | PDF-Ninja non-Chrome process writes ~/Library/Application Support/Google/Chrome/.../Secure Preferences LaunchAgent/Daemon plist referencing a masquerade app masquerade app parents /bin/sh|bash|zsh|curl (env-var read / exfil) network egress whose initiating image is a masquerade app
Campaign: Operation FlutterBridge (Palo Alto Unit 42) Actor: CL-CRI-1089 (active since 2023; succeeds JSCoreRunner/FileRipple, 2025) Backdoor: FlutterShell (Flutter + WebView JS-to-native bridge) Masquerade: PodcastsLounge, PDF-Brain, PDF-Ninja (AI PDF/podcast tools) Signing: valid Apple Developer IDs + notarization (NOT a trust signal here) Delivery: Google/YouTube ads via verified shell companies Capability: arbitrary cmd exec, file ops, env-var exfil, Chrome traffic hijack, browser-session theft Payload: pulled dynamically from attacker infra (app body stays clean)
AdsParkPro LTD Advantage Web Marketing LLC SOFT WE ART LIMITED (renamed -> PACIFIC TRADE SOLUTIONS LTD) Note: Google-verified advertiser identities; links to Ukrainian individuals (Unit 42) Action: report fraudulent advertiser IDs to Google; block their ad-download domains at SWG
Detection Validation Gates
| Gate | Check | Pass Criteria |
|---|---|---|
| Telemetry ready | Falcon macOS sensor reporting ProcessRollup2 + FileCreateInfo | Both event types present from macOS hosts in last 24h |
| Benign baseline | Run Q2 over 7 days | MDM/policy agents that touch Chrome prefs identified and excluded |
| Positive test | Lab macOS: write a test file to Chrome's Default dir from a non-Chrome process; create a dummy app named PDF-Ninja | Q1 and Q2 fire |
| Promotion | Q1 promoted to Custom IOA | Zero FP across macOS pilot ring |
Hardening — Tiered & Deployable
- Block the masquerade apps — add
PodcastsLounge/PDF-Brain/PDF-Ninjabundle IDs to MDM block / Falcon prevent once hashes land (M1038). - SWG block the ad-front download domains and report the shell-company advertiser IDs to Google (M1021).
- Baseline Chrome preference integrity and alert on out-of-band rewrites (M1042).
- Gatekeeper to "App Store and identified developers" + restrict app install to MDM-approved sources — notarization alone is not sufficient here, so pair it with an allow-list (M1038).
- Managed Chrome policy via MDM (lock proxy / startup URLs / extension install) so a config rewrite is overridden by policy (M1042).
- User awareness on ad-driven "AI PDF/podcast tool" downloads; steer to vetted software portals (M1017).
- macOS application allow-listing (MDM + Falcon) so only approved apps run on managed fleet (M1038).
- Egress filtering / DNS security on macOS segments to surface dynamic-payload pulls and hijack-proxy traffic (M1037, M1031).
Deployable Playbooks
1 · Hunt the masquerade apps + LaunchAgents across a Mac (shell)
#!/bin/bash # masquerade app bundles mdfind "kMDItemKind == 'Application'" 2>/dev/null | grep -Ei 'PodcastsLounge|PDF-Brain|PDF-Ninja' ls -la /Applications | grep -Ei 'PodcastsLounge|PDF-Brain|PDF-Ninja' # persistence for d in ~/Library/LaunchAgents /Library/LaunchAgents /Library/LaunchDaemons; do grep -lEi 'PodcastsLounge|PDF-Brain|PDF-Ninja|flutter' "$d"/*.plist 2>/dev/null done
2 · Check Chrome preference integrity (shell)
PREF="$HOME/Library/Application Support/Google/Chrome/Default/Secure Preferences"
[ -f "$PREF" ] && /usr/bin/python3 -c 'import json,sys;d=json.load(open(sys.argv[1]));print(d.get("proxy"),d.get("session",{}).get("startup_urls"))' "$PREF"
3 · Remove a confirmed FlutterShell app + persistence (shell)
sudo rm -rf "/Applications/PDF-Ninja.app" "/Applications/PDF-Brain.app" "/Applications/PodcastsLounge.app" for d in ~/Library/LaunchAgents /Library/LaunchAgents /Library/LaunchDaemons; do grep -lEi 'PodcastsLounge|PDF-Brain|PDF-Ninja|flutter' "$d"/*.plist 2>/dev/null | xargs -r sudo rm -f done
Capture the .app bundle and its Info.plist (Developer ID / Team ID) for hashing and abuse-reporting to Apple before deletion — that Team ID is your tenant-local atomic IOC.
Containment Runbook
| Phase | Actions | Owner | Evidence |
|---|---|---|---|
| Isolate | Network-contain the Mac in Falcon; preserve the app bundle + Chrome profile | SOC L2 | Containment timestamp, bundle copy |
| Eradicate | Remove the masquerade app + LaunchAgent plists; reset Chrome (delete tampered Preferences/Secure Preferences) | SOC L2 | File hashes, plist export |
| Credentials | Rotate browser-saved creds, SSO sessions, and any secrets exposed via env vars on the host | IAM | Reset audit log |
| Recover | Reinstall Chrome; restore from clean baseline; re-enroll in MDM allow-list | Endpoint Eng | Rebuild ticket |
| Lessons | Report the abused Apple Developer/Team IDs and Google advertiser IDs; promote Q1 to IOA | Detection Eng / TI | This pack, updated |
Detection Coverage Map
| Technique | Behavior | CQL | IOA | Coverage |
|---|---|---|---|---|
| T1583.008 | Malvertising delivery | Q5 | — | Partial |
| T1204.002 / T1036.005 | Masquerade app install + exec | Q1, Q6 | Yes | Good |
| T1539 | Chrome traffic hijack (config rewrite) | Q2 | Yes | Good |
| T1543.001 | LaunchAgent persistence | Q3 | — | Good |
| T1059.004 / T1552.001 | Shell exec + env-var exfil | Q4 | Candidate | Partial |
| T1105 | Dynamic payload pull | Q5 | — | Gap (no infra) |
| — | In-WebView JS bridge logic | — | — | Gap |
Coverage is strong on the app-execution, Chrome-hijack and persistence anchors (Q1/Q2/Q3) and weaker on the in-WebView JS logic and the dynamic-payload C2 (no published infra). All queries assume Falcon's macOS sensor is deployed — without it this pack has no telemetry. Validation gates (§11) must pass before promoting Q1 to a Custom IOA.
Hunt Summary Ticket
TITLE: FlutterShell / Operation FlutterBridge (CL-CRI-1089) — macOS backdoor hunt SEVERITY: High — active in-the-wild macOS backdoor + stealer SCOPE: macOS endpoints (US/CA/AU/FR/DE users); requires Falcon macOS sensor HYPOTHESIS: Notarized fake PDF/podcast app (FlutterShell) hijacks Chrome + exfiltrates via JS-native bridge QUERIES: Q1 app exec · Q2 Chrome pref rewrite · Q3 LaunchAgent · Q4 app-parented shell · Q5 C2 egress · Q6 Downloads first-run DO FIRST: Run Q1, Q2 across macOS fleet; inventory PodcastsLounge/PDF-Brain/PDF-Ninja FINDINGS: ____________________________________________ GAPS: in-WebView JS bridge logic; dynamic-payload C2 (no published infra) ACTIONS: Block masquerade apps; managed Chrome policy; report Apple Team IDs + Google advertiser IDs; promote Q1 to IOA OWNER: ____________________ VERSION: v0.1 (2026-06-24)
Changelog
table() calls now carry an explicit row limit: the default is 200 and truncation is silent, so a capped result was indistinguishable from a complete one. Atomic-IOC sweeps (filters over 5+ hashes or C2 IPs) use limit=max so a wide infection is never silently under-scoped; behavioural hunts use limit=200, where exceeding the cap indicates the query needs tuning. Where present, event names that do not exist in the Falcon data model were corrected (e.g. ServiceInstalled is a Sysmon concept, not a Falcon event; ElfFileWritten is ELFFileWritten) — such queries could never return a row. No detection logic, fields, or IOCs changed.References
| Tier | Source | Used For | Access Date |
|---|---|---|---|
| 1 | The Hacker News — FlutterShell Backdoor Spreads to macOS via Malicious Google and YouTube Ads (Unit 42, 2026-06-04) | Operation FlutterBridge, CL-CRI-1089, masquerade apps, Flutter/WebView bridge, Chrome hijack, shell-company ad fronts, dynamic payload | 2026-06-24 |
| R | MITRE ATT&CK | Technique IDs and M-number mitigation mapping (macOS) | 2026-06-24 |