Operation FlutterBridge: The FlutterShell macOS Backdoor
Identified through macOS endpoint monitoring, the CL-CRI-1089 cluster, delivered under the publicly reported Operation FlutterBridge campaign, demonstrates a deliberate misuse of the Flutter framework for macOS malware delivery. Rather than re-documenting the campaign itself, this report treats the recovered FlutterShell artifacts as a technical detection case study.
The analysis focuses on what can be learned directly from ten Mach-O samples across three generations between December 2025 and March 2026: cross-generation binary fingerprints, Dart AOT evolution, Gen 3 symbol obfuscation, sandbox timeout behavior, and code-level evidence such as the FlutterInvoke JavaScript (JS) bridge.
These findings highlight durable detection surfaces that remain useful even as domains, certificates, command names, and visible strings change. At its core, FlutterShell separates the static binary from the command payload: a WebView loads attacker-controlled content at runtime, which can issue commands to a native Dart handler through a JavaScript message channel. Without a live, responding C2 server, no bridge-command activity was observed in the reviewed sandbox telemetry, consistent with a C2-conditional execution model.
-
Cluster: CL-CRI-1089
-
Campaign: Operation FlutterBridge
-
Malware Family: FlutterShell (OSX/FlutterShell.A/B)
-
Platform: macOS · x86_64 · arm64
-
Motivation: Financial, browser search hijack
-
Activity Window: December 2025 to March 2026
-
Report Date: June 2026
Methodology
All primary findings derive from independent static analysis of ten Mach-O binaries recovered through macOS-targeted monitoring. This report does not reproduce or summarize any prior published account; where findings align with published research, this is noted explicitly. No controlled live execution was performed by the research team; automated detonation telemetry was reviewed.
|
Method |
What it reveals |
Coverage |
|
Signature metadata review |
File type, CPU architecture, code signing status, Team ID, bundle ID, revocation tags |
All 10 artifacts |
|
Mach-O load command parsing |
Segment layout, linked libraries, build version, export structure |
All 10 artifacts |
|
Fuzzy hash comparison |
Binary similarity and obfuscation boundary detection |
All 6 payload dylibs |
|
Exported symbol fingerprinting |
Symbol table identity across generations |
All 6 payload dylibs |
Key Behavioral Finding: Sandbox Timeouts
A "timeout" in behavioral environments indicates the binary successfully launched but produced zero behavioral output (no process activity, network traffic, or file writes).
-
Gen 1: Five independent environments timed out.
-
Gen 2: One environment timed out.
This provides strong supporting evidence that the binary can launch successfully but remains behaviorally dormant without a live C2 response. [Observed]
The static detection signatures that fired on Gen 1 and Gen 2 were all heuristic or pattern-based, keyed on Dart internal symbol strings, file structure, or known-bad certificate metadata, not on observed runtime behavior. Gen 3's low initial static visibility reflects successful rotation of the certificate and Dart symbol table: the static patterns no longer matched, and no behavioral signals were produced to trigger heuristics.
Why No Behavioral Output
The malware implements a C2-conditional execution model. The binary initializes a WKWebView and loads the attacker-controlled landing page. The actual command payload is JavaScript delivered by the C2 server at runtime, and it is never present in the static binary. No flutterInvoke.postMessage() activity was observed in the reviewed detonation telemetry; no bridge command executed. The binary produced a functioning, benign application UI in every environment that ran it.
What to Monitor in a Live Environment
Because behavioral sandboxes cannot replicate a live, responding C2, endpoint-level telemetry is the only reliable detection surface. The following runtime signals are expected based on binary string analysis and behavioral research:
|
Signal |
Observable |
Basis |
|
WKWebView outbound HTTPS |
Outbound connection to atsheisdomestic.org, etoftheappyrince.org, or healightejustb.org from a non-browser process |
Observed |
|
Hardware fingerprinting |
ioreg -rd1 -c IOPlatformExpertDevice | grep IOPlatformUUID spawned as child of non-system app |
Assessed |
|
Chrome profile modification |
Write to ~/Library/Application Support/Google/Chrome/Secure Preferences by non-Chrome process |
Assessed |
|
Chrome crash-bubble suppression |
killall "Google Chrome" followed by relaunch with --hide-crash-restore-bubble |
Assessed |
|
Sparkle silent replacement |
File activity under ~/Library/Caches/com.app.*/org.sparkle-project.Sparkle/Installation/ |
Assessed |
|
LaunchAgent installation |
New plist in ~/Library/LaunchAgents/ referencing com.app.podcastsLounge, com.app.pdfBrain, or com.pdfninja.app |
Assessed |
Threat Intelligence Confirmation
The three recovered C2 domains are externally associated with CL-CRI-1089 / Operation FlutterBridge.
Findings
1. Symhash Invariance Across All Generations
All six payload dylibs, across three generations, two architectures, and two different Apple Developer certificates, share an identical exported-symbol fingerprint (605169623267c4eb73693b22b811dc7a). This property enables a single, generation-agnostic detection rule that will match future variants as long as the Flutter framework base is reused. [Observed]
2. Architecture-Keyed Structural Clustering
Structural hashing reveals two distinct clusters: all x86_64 dylibs share structural hash ffd773f157df70291f0910a45a1d8d9a; all arm64 dylibs share b4aa7255af4b016586090a5b451300fa. This confirms the same Flutter framework base was carried across all three generations with only the Dart AOT snapshot modified. [Observed]
3. Binary Growth Profile: Private-Only Expansion
The Gen 1 → Gen 2 size increase of +3,448,528 bytes (+48.7%) is confined entirely to private, non-exported Dart AOT code. Because the exported-symbol fingerprint is invariant, zero new exported entry points were introduced. This pattern is consistent with obfuscation metadata expansion and the addition of new internal command handlers. [Observed]
4. TLSH Divergence at Obfuscation Boundary
TLSH fuzzy hashing shows Gen 1 and Gen 2 x86_64 dylibs at low distance; code structure largely preserved. Gen 3 diverges significantly, confirming that enabling full Dart obfuscation fundamentally redistributes byte content and breaks TLSH-based clustering. This is directly relevant to threat hunting infrastructure that relies on fuzzy similarity. [Observed]
5. Cross-Generation Detection Sequence Quantified
The detection trajectory across generations correlates precisely with specific evasion changes: Gen 1 (moderate initial profile) → Gen 2 (elevated, widest detection window) → Gen 3 (low initial static visibility following certificate rotation and full Dart obfuscation adoption). This report assembles the sequence from the analyzed artifact set. [Observed]
Threat Actor Attribution
|
Attribute |
Value/Confidence |
|
Designation |
CL-CRI-1089 [Observed] |
|
Campaign Name |
Operation FlutterBridge [Assessed] |
|
Motivation |
Financial, browser search hijack [Assessed] |
|
Activity Window |
Dec. 1, 2025, to Mar. 31, 2026 [Observed] |
Certificate Rotation Timeline
The rotation pattern is consistent with preemptive identity procurement, the next certificate was enrolled before the previous was revoked, not after. The shortest observed gap from revocation to next-generation artifact first-seen is 31 days.
|
Event |
Date |
Evidence |
|
Gen 0 fat binary first seen |
Dec. 2, 2025 |
Observed |
|
Gen 1 thin binaries first seen |
Dec. 12, 2025 |
Observed |
|
UBZDAAV97Y revoked by Apple |
Dec. 31, 2025 |
Observed |
|
Gen 2 first seen |
Jan. 12, 2026 |
Observed |
|
FW9NHQ8922 revoked by Apple |
Jan. 31, 2026 |
Observed |
|
Gen 3 self-signed artifacts first seen |
Mar. 10, 2026 |
Observed |
Dedicated to hunting and eradicating the world's most challenging threats.
Binary Architecture
Two-Component Deployment Model
Every FlutterShell deployment follows a consistent two-component architecture. A thin Mach-O stub launcher loads a large dynamically linked payload library containing the full Dart runtime and attacker-controlled logic.
|
Component |
Role |
Identifier |
Arch |
Size |
Profile |
|
Stub launcher |
Initializes Flutter runtime |
com.app.* |
x86/arm64 |
150 KB |
Low |
|
Payload dylib |
Flutter framework + Dart AOT |
io.flutter.flutter.app |
x86/arm64 |
10 MB |
Moderate |
All six payload dylibs share an identical load command ordering and segment layout across all three generations and both architectures. Key markers include:
- Single external dependency: Each payload dylib links only against /usr/lib/libSystem.B.dylib. No Foundation, AppKit, WebKit, or other macOS framework imports are present. [Observed]
- No Objective-C sections: The absence of __OBJC and __DATA_CONST segments, present in all native Cocoa binaries, is a directly observable marker distinguishing Flutter-based payloads from legitimate macOS applications. [Observed]
- LC_ID_DYLIB identifier: io.flutter.flutter.app, self-identifies as a Flutter runtime dylib. [Observed]
- Segment layout: __TEXT (Dart AOT code, dominant section)· __DATA (4 KB, BSS only)· __LINKEDIT (~113 KB). [Observed]
Certificate Generation Analysis
|
Generation |
Identity |
Masquerade |
First Seen |
C2 Domain |
Profile |
|
Gen 0 (fat) |
UBZDAAV97Y |
PodcastsLounge |
Dec. 2, 2025 |
atsheisdomestic.org |
Moderate |
|
Gen 1 (thin) |
UBZDAAV97Y |
PodcastsLounge |
Dec. 12, 2025 |
atsheisdomestic.org |
Moderate |
|
Gen 2 |
FW9NHQ8922 |
PDF-Brain |
Jan. 12, 2026 |
etoftheappyrince.org |
Elevated |
|
Gen 3 |
Self-signed |
PDF-Ninja |
Mar. 10, 2026 |
healightejustb.org |
Low |
Evasion Significance of the Rotation
Certificate rotation is not merely operational hygiene for this actor; it is a primary evasion mechanism. Apple notarization of Gens 1 and 2 meant Gatekeeper quarantine passed at distribution time with no user warning. Certificate-based blocklists were only actionable after Apple applied revocation tags, by which point the actor had already moved to the next identity. Gen 3's shift to self-signed artifacts trades Gatekeeper clearance for complete independence from Apple CA infrastructure.
Binary Evolution and Capability Growth
Size Progression (x86_64 dylib)
|
Generation |
File Size |
Delta |
Interpretation |
|
Gen 1 (Dec. 2025) |
7,077,984 bytes (6.75 MB) |
, |
Baseline. Partial Dart obfuscation; readable source paths in binary strings. |
|
Gen 2 (Jan. 2026) |
10,526,512 bytes (10.04 MB) |
+3,448,528 B (+48.7%) |
Growth entirely in private Dart AOT code. Zero new exported symbols. Consistent with obfuscation metadata expansion and addition of PDF processing library. (Syncfusion). ). [Observed] |
|
Gen 3 (Mar. 2026) |
8,569,680 bytes (8.17 MB) |
−1,956,832 B vs Gen 2 |
Smaller despite retained or expanded obfuscation. We assess that this reflects improved Dart tree-shaking removing dead-code and reflection metadata introduced in Gen 2. Size reduction does not imply capability reduction. [Assessed] |
Private-Only Growth Confirmed
The exported-symbol fingerprint is identical across all three x86_64 generations and both arm64 generations (see Appendix A for hash values). Zero new exported entry points were added across the entire campaign lifecycle. All binary growth was internal, consistent with private function additions and obfuscation metadata expansion, not architectural changes.
Counting Dart runtime symbols matching the obfuscated pattern (_[Name]@[hash].method) directly measures the effect of enabling Dart compilation with obfuscation:
|
Generation |
Obfuscated Dart Symbols |
Status |
|
Gen 1 |
92 |
Partial, normal Dart AOT mangling; class names readable in source paths |
|
Gen 2 |
91 |
Comparable to Gen 1; hash suffixes changed due to snapshot rebuild, not obfuscation |
|
Gen 3 |
707 (+669%) |
Full Dart obfuscation confirmed: all internal class names replaced with 2- to3-character random identifiers (e.g., _Eae@403490068, _Xa@0150898) [Observed] |
The JavaScript-to-native bridge command names changed with each generation, defeating detection rules anchored to specific command strings [Assessed]:
|
Generation |
App |
Bridge Command |
Likely Rationale |
|
Gen 1 |
PodcastsLounge |
exec_sync |
Initial deployment; generic name; drew detection attention |
|
Gen 2 |
PDF-Brain |
pdf_sync |
Renamed after exec_sync strings associated with detection signatures |
|
Gen 3 |
PDF-Ninja |
renderPDF |
Camouflaged as expected behavior for a PDF application |
Capability Delta: Added Dependencies per Generation
Internal Dart package paths leaked from the AOT snapshot reveal which libraries were added or removed per generation [Observed]:
|
Package |
Gen 1 |
Gen 2 |
Gen 3 |
Purpose |
|
sqflite |
✓ |
, |
, |
SQLite local data storage |
|
just_audio_platform_interface |
✓ |
, |
, |
Audio playback (podcast lure) |
|
syncfusion_pdfviewer_platform_interface |
, |
✓ |
, |
PDF rendering (PDF-Brain lure) |
|
window_size |
, |
✓ |
, |
Window dimension control |
|
url_launcher |
, |
✓ |
✓ |
External URL opening |
|
webview_flutter_wkwebview |
✓ |
✓ |
✓ |
Core C2 delivery mechanism, present all generations |
|
uuid |
✓ |
✓ |
✓ |
UUID generation for device fingerprinting |
Gen 2 binary strings still contain the internal Dart package name podcasts_lounge and the original bundle ID com.app.podcastsLounge despite external rebranding to PDF-Brain. This confirms the actor recompiled from the same Dart source tree without renaming the package, establishing direct code lineage between Gen 1 and Gen 2. [Observed]
Technical Analysis of the FlutterInvoke Bridge
FlutterShell's architecture separates the command dispatcher from the commands delivered by the C2 at runtime. The application registers a named JavaScript message channel (flutterInvoke) within a WKWebView. The C2 server delivers JavaScript templates, confirmed via binary string extraction, which allows commands to be updated server-side without binary modification. [Assessed, Observed]
Extracted JavaScript Bridge Template
// Extracted from Gen 3 binary data"if (typeof flutterInvoke !== 'undefined') {"" flutterInvoke.postMessage(JSON.stringify(request));""} else if (window.flutterInvoke) {"" window.flutterInvoke.postMessage(JSON.stringify(request));""}""<script src=\"welcome_page.js\"></script>"
Bridge Command and Capability Literals
renderPDF // bridge command namesetSparkleDelay // Sparkle update delay setterFailed to summarize PDF // AI exfil error pathrenderPDF not available in this build // error guard
Plugin Fingerprint Strings, All Generations
Four plugin registration strings appear across generations. The typo path_providerr (double 'r') is present in Gen 1 and Gen 3 and constitutes a durable binary fingerprint:
|
String |
Gen 1 Offset |
Gen 2 Offset |
Gen 3 |
|
plugins.flutter.io/webview |
0x556320 |
0x7de770 |
Present |
|
plugins.flutter.io/shared_preferences |
0x5dcf30 |
0x8e5710 |
, |
|
plugins.flutter.io/path_providerr (typo) |
0x5f8ce0 |
, |
Present |
|
plugins.flutter.io/url_launcher |
, |
0x8eec60 |
Present |
Gen 2 Debug Strings, Unstripped from Release Build
Three logging strings were found in the Gen 2 binary that were not stripped before the release build. They confirm the C2 base URL and the presence of explicit HTTP error handling:
// Found in bf90fb31 binary, developer logging left in release build
-
Base URL: https://etoftheappyrince.org
-
Endpoint not found (404). Is the server running? URL: https://etoftheappyrince.org/...
-
Request URL: https://etoftheappyrince.org/api/pdfs
Dart Source Path Leakage, Gen 1 and Gen 2
Dart AOT snapshots retain Dart package URI paths from the source tree. These confirm the internal application structure and cross-generation code lineage:
// Gen 1 (134517796178), package paths retained in AOT snapshotpackage:podcasts_lounge/services/sparkle_update_service.dartpackage:podcasts_lounge/screens/settings_screen.dartpackage:podcasts_lounge/widgets/episode_item.dart
// Gen 2 (bf90fb31), same package name despite external rebranding to PDF-Brainpackage:podcasts_lounge/services/pdf_service.dartcom.app.podcastsLounge // original bundle ID still present in Gen 2 binary
Dart Symbol Obfuscation, Gen 1 vs Gen 3
The difference between Gen 1 readable symbols and Gen 3 obfuscated identifiers directly shows the effect of enabling full Dart obfuscation at compilation:
|
Generation |
Symbol Pattern |
Examples |
|
Gen 1, partial obfuscation |
Readable class name + numeric hash |
_Image@17065589 · _Capability@1026248 |
|
Gen 3, full obfuscation enabled |
2- to3-char random identifier + hash |
_Eae@403490068 · _Oxf@884196681 · _Xa@0150898 · _ZU@44287047.LRbr |
Gen 1 contained 92 symbols in the partial-obfuscation pattern. Gen 3 contained 707, a +669% increase confirming the compiler flag change. Observed, counted directly from binary string extraction.
Attack Chain
Each step is labeled with its evidence basis. Behavioral steps corroborated by external research are marked “[Assessed]”.
1
T1566/T1185
Initial Access, Malvertising [Assessed]
Google/YouTube ad placement targeting keywords such as "podcast app for Mac" and "free PDF converter." Shell company ad accounts procured per generation. User clicks ad; redirected to typosquatted distribution domain; downloads a signed app bundle.
2
T1204.002
Deliver , Signed, Notarized App [Observed]
App bundle carries a valid Apple Developer certificate at distribution time. Gatekeeper quarantine passes, no "unidentified developer" prompt for the user. Revocation tag applied retroactively after discovery.
3
T1204.002
Execution, Flutter Runtime Initialization Observed (structure)
Stub launcher initializes Flutter/Dart runtime; payload dylib loaded via dynamic linking. App presents a functional UI, podcast browser or PDF viewer. Static load command profile is consistent with a standard Flutter app bundle; no anomalous library imports.
4
T1071.001/T1102
C2 Establishment, WebView Beacon [Assessed]
WKWebView loads the attacker domain and fetches /getConfig or the landing page endpoint. C2 URLs confirmed in binary strings; no live C2 session was observed. Without a live responding server, execution stops here, no malicious behavior follows.
5
T1082
Discovery, Hardware UUID Fingerprinting [Assessed]
ioreg -rd1 -c IOPlatformExpertDevice | grep IOPlatformUUID executed as a child process. Hardware UUID exfiltrated for persistent victim tracking.
6
T1176
Impact, Chrome Search Hijack [Assessed]
Modifies ~/Library/Application Support/Google/Chrome/Secure Preferences to inject sinterfumesco[.]com as default search provider. Kills Chrome; relaunches with --hide-crash-restore-bubble --disable-session-crashed-bubble to suppress browser warnings.
7
T1543.001
Persistence, Silent Sparkle Update [Assessed]
On Sparkle update cycle completion, a replacement bundle is staged in ~/Library/Caches/com.app.*/org.sparkle-project.Sparkle/Installation/ and silently opened. Enables payload rotation without user interaction.
Evasion Profile
|
# |
Layer |
Mechanism |
Evidence Basis |
Effective Against |
|
1 |
Notarization abuse |
Valid Apple notarization at distribution time, no static malicious content detectable |
Observed, valid cert tag; revocation applied post-distribution |
Gatekeeper, quarantine checks |
|
2 |
Flutter framework camouflage |
Structural profile identical to legitimate Flutter app bundle; sole dependency on libSystem |
Observed, load commands, segments, single dylib dependency |
Structure-based static detection |
|
3 |
C2-conditional payload |
No malicious behavior without live C2, no process trees, no network contacts in isolation |
Observed, zero detonation results across all 10 artifacts |
Dynamic sandbox analysis |
|
4 |
Dart symbol obfuscation |
Full obfuscation in Gen 3: all internal class names randomized; 707 obfuscated symbols confirmed |
Observed, symbol count from binary string extraction |
String-based signature matching |
|
5 |
Bridge command renaming |
exec_sync → pdf_sync → renderPDF across generations |
Assessed, command names extracted from binary strings and embedded JS |
Command-name string detection |
|
6 |
Certificate rotation |
New Apple Developer identity per generation; average gap <60 days from revocation to next cert |
Observed, first-seen date deltas: 31d (Gen 1→2), 57d (Gen 2→3) |
Certificate-based blocklisting |
|
7 |
Symhash preservation (unintended signal) |
Actor reuses Flutter framework base, exported-symbol fingerprint is invariant and constitutes an inadvertent detection surface |
Observed, symhash identical across all 6 dylibs |
N/A, this creates a detection opportunity, not an evasion |
Defensive Implications
We recommend a tiered detection approach. Tier A behavioral rules are durable across all three generations, effective regardless of certificate rotation, binary obfuscation, or command renaming. Tier B static detection rules add coverage for Gen 1 and Gen 2 but require updating for Gen 3. Tier C documents remaining gaps.
Tier A, Behavioral Rules (Generation-Agnostic)
These signals are not produced by any legitimate application in a managed macOS environment.
The combination of sinterfumesco[.]com and crash-suppression flags in a Chrome relaunch is campaign-specific tradecraft. No legitimate application produces this pattern.
title: FlutterBridge Chrome Hijack Relaunch (CL-CRI-1089)status: experimentaldetection:selection:event.process.name: 'Google Chrome'event.process.command_line|contains|all:- 'sinterfumesco'- '--hide-crash-restore-bubble'- '--disable-session-crashed-bubble'condition: selectionfalsepositives:- None expected for this exact combination
-
A2, Hardware UUID Fingerprinting via Child Process
Legitimate applications call ioreg via API, not child processes. A non-system application spawning an ioreg child to harvest IOPlatformUUID is anomalous.
title: FlutterBridge Hardware UUID Fingerprint Child Processdetection:selection:event.process.name: 'ioreg'event.process.command_line|contains|all:- 'IOPlatformExpertDevice'- 'IOPlatformUUID'event.process.parent.executable|re: '^/(?!System|usr/bin|usr/sbin)'filter_system:event.process.parent.name|startswith: ['mdmclient', 'jamf', 'launchd']condition: selection and not filter_system
title: FlutterBridge Sparkle Silent Bundle Replacementdetection:selection:event.process.name: 'open'event.process.command_line|contains|all:- 'org.sparkle-project.Sparkle'- 'Installation'event.process.command_line|contains:- 'com.app.podcastsLounge'- 'com.app.pdfBrain'- 'com.pdfninja.app'
Tier B, Static Detection Logic
Two static detection rules are recommended. The first is generation-agnostic and anchored to the invariant exported-symbol fingerprint. The second covers the cross-generation plugin typo fingerprint.
Full rule bodies are in Appendix C. Summary:
|
Rule |
Anchor |
Coverage |
Durability |
|
Cluster pivot rule |
Exported-symbol fingerprint + io.flutter.flutter.app identifier + file size range |
All 6 dylibs, all generations |
Durable, survives C2 rotation, cert rotation, command renaming. Breaks only if Flutter framework is replaced. |
|
C2 domain strings rule |
Network and hijack domain strings rule - Three C2 domains plus one browser hijack target |
Gens 1 to 3 |
Retire by 2026-09; rotate if new domain confirmed |
|
Plugin typo rule |
plugins.flutter.io/path_providerr (double 'r') + webview plugin string |
Gens 1 and Gen 3 confirmed; Gen 2 partial |
Durable as long as same source tree is reused |
|
Gap |
Why It’s Not Covered |
Our Recommendation |
|
Network-level C2 detection |
C2 domains rotate per generation; no live C2 was observed |
Sinkhole known domains; monitor registrar cluster for new short-lived .org domains matching the naming pattern |
|
Gen 3 self-signed detection at install |
Self-signed binaries are blocked by default on macOS without user override |
Alert on self-signed apps with io.flutter.flutter.app dylib identifier launched in user context |
|
Production B73CHZ24Y8 samples |
Not in this artifact set |
Monitor for signed Mach-O apps with bundle ID com.pdfninja.app or Team ID B73CHZ24Y8 |
|
Delivery-stage interception |
No telemetry at browser ad-click level |
Submit distribution domains to Safe Browsing; report ad accounts to Google |
Appendices
Supporting reference material, artifact tables, binary offset data, detection rule logic, and full IOC reference.
C2 Endpoint Strings, Binary Offsets
All URLs extracted directly from binary string data with confirmed offsets. C2 URLs were recovered from binary strings; no live C2 session was observed. [Observed]
|
Gen |
URL |
Offset |
Role |
|
1 |
https://atsheisdomestic.org/update-thanks.html |
0x5879e0 |
WebView landing page |
|
1 |
https://atsheisdomestic.org/api/update-delay |
0x5d0c60 |
Update timing config |
|
1 |
https://atsheisdomestic.org/api/subscribe |
0x574380 |
C2 registration |
|
1 |
https://atsheisdomestic.org/api/podcasts |
0x5b2a20 |
Lure content endpoint |
|
2 |
https://etoftheappyrince.org/update-thanks.html |
0x8eba10 |
WebView landing page |
|
2 |
https://etoftheappyrince.org/summarize-text |
0x894670 |
AI text summarization / exfil |
|
2 |
https://etoftheappyrince.org/api/update-delay |
0x86dbc0 |
Update timing config |
|
2 |
https://etoftheappyrince.org/api/pdfs |
0x7fa456 |
PDF content endpoint |
|
3 |
https://healightejustb.org/welcome_page.html |
0x7905f0 |
Landing page (renamed) |
|
3 |
https://healightejustb.org/welcome_page.js |
0x80b4a0 |
Explicit JS payload file |
|
3 |
https://healightejustb.org/api/central-config |
0x756d10 |
Config endpoint (renamed) |
|
3 |
https://healightejustb.org/summarize-text |
0x80adf0 |
AI summarization, retained from Gen 2 |
|
3 |
https://healightejustb.org/checkForNewVersion |
0x7f2f50 |
Sparkle version check |
Gen 2 debug string leakage: "Base URL: https://etoftheappyrince.org" at offset 0x8a3256, an unstripped developer logging line confirming the hardcoded base URL. Additionally: "Endpoint not found (404). Is the server running? URL: ...", HTTP error handler confirming explicit C2 error handling was compiled into the release build. [Observed]
|
Plugin String |
Gen 1 |
Gen 2 |
Gen 3 |
|
plugins.flutter.io/webview |
✓ 0x556320 |
✓ 0x7de770 |
✓ |
|
plugins.flutter.io/shared_preferences |
✓ 0x5dcf30 |
✓ 0x8e5710 |
, |
|
plugins.flutter.io/path_providerr (typo , double r) |
✓ 0x5f8ce0 |
, |
✓ |
|
plugins.flutter.io/url_launcher |
, |
✓ 0x8eec60 |
✓ |
Appendix C
All rules were validated against the analyzed artifacts. Full match rates: Gen 1, 20/20 strings; Gen 2, 17/17 strings; Gen 3, 23/23 strings.
C1, Cluster Pivot Rule (Generation-Agnostic)
rule MAL_FlutterShell_CL_CRI_1089_Dylib_Cluster { meta: description = "FlutterShell payload dylib , all generations , symhash cluster" cluster = "CL-CRI-1089" symhash = "605169623267c4eb73693b22b811dc7a" evidence_basis= "Observed: identical symhash across 6/6 dylibs" gen1_x86 = "134517796178a150a1585672be134169d6877082b598d840baa3f37b0222be26" gen2_x86 = "bf90fb31e6024d7e6616f5acd0e8aa28738a9095a508c1a986e1e974cb9e79a0" gen3_x86 = "2c5bc9e95e1e9b73e3ba8870a008802899866a2c0e2e10112aefddf7a96af04e" condition: /* Mach-O thin dylib, 64-bit */uint32(0) == 0xFEEDFACF and uint32(12) == 6 and /* Size range spans all known generations */filesize >= 6MB and filesize <= 12MB and /* LC_ID_DYLIB self-identifier */"io.flutter.flutter.app" in (0..500) and /* Single external dependency */"/usr/lib/libSystem.B.dylib" in (0..1000)}
C2, C2 Domain IOC Rule (retire 2026-09)
rule Network_And_Hijack_Domains { meta: retire_date = "2026-09" cluster = "CL-CRI-1089" strings: $g1 = "atsheisdomestic.org" nocase ascii wide $g2 = "etoftheappyrince.org" nocase ascii wide $g3 = "healightejustb.org" nocase ascii wide $hijack = "sinterfumesco.com" nocase ascii wide condition: uint32(0) == 0xFEEDFACF and 1 of them}
C3, Plugin Typo Cross-Generation Rule
rule MAL_FlutterShell_CL_CRI_1089_Plugin_Typo { meta: description = "Detects FlutterShell via persistent plugin name typo , durable across generations" cluster = "CL-CRI-1089" strings: $typo = "plugins.flutter.io/path_providerr" ascii wide // double 'r' , confirmed in Gen 1 + Gen 3 $wv = "plugins.flutter.io/webview" ascii wide $sp = "plugins.flutter.io/shared_preferences" ascii widecondition: $typo and 1 of ($wv, $sp)}
Appendix D
|
Category |
Indicator/Value |
Context/Description |
|
Network Domain |
atsheisdomestic.org |
Gen 1 C2 |
|
Network Domain |
etoftheappyrince.org |
Gen 2 C2 |
|
Network Domain |
healightejustb.org |
Gen 3 C2 |
|
Network Domain |
sinterfumesco.com |
Hijack Target |
|
SHA-256 |
363923500ce942bf1a953e8a4e943fbf1fb1b5ed6e5d247964c345b3ad5bfc34 |
Stub Gen 0 |
|
SHA-256 |
6c3f61d46d4de26b9cb16808bf17c33ae69f651a4b879e7b5612ff7f548e2a82 |
Stub Gen 1 x86 |
|
SHA-256 |
fc091ddb4d845280aeb7745cfdb6b7cb0013abc35db9e634f055b8e8fb0b5b1e |
Stub Gen 1 arm64 |
|
SHA-256 |
134517796178a150a1585672be134169d6877082b598d840baa3f37b0222be26 |
Dylib Gen 1 x86 |
|
SHA-256 |
cc4f048e66c5ab3c0f1d767bb8fc464d082641f4888ea3cd14ea3775077c4bf2 |
Dylib Gen 1 arm64 |
|
SHA-256 |
bf90fb31e6024d7e6616f5acd0e8aa28738a9095a508c1a986e1e974cb9e79a0 |
Dylib Gen 2 x86 |
|
SHA-256 |
32da1437a2734224406c7e5e8d756f0c0cd58c0c959478571cbfc0cd564d018a |
Dylib Gen 2 arm64 |
|
SHA-256 |
2c5bc9e95e1e9b73e3ba8870a008802899866a2c0e2e10112aefddf7a96af04e |
Dylib Gen 3 x86 |
|
SHA-256 |
f544bfab72d380cc20692d8ec9d31ea666785fe225dccd55beab29a3c0fdfad2 |
Dylib Gen 3 arm64 |
|
Team ID |
UBZDAAV97Y |
Revoked, Gen 0-1 |
|
Team ID |
FW9NHQ8922 |
Revoked, Gen 2 |
|
Team ID |
B73CHZ24Y8 |
External Attribution |
|
Behavioral |
Chrome Relaunch Hijack |
Use of --hide-crash-restore-bubble flags |
|
Behavioral |
Hardware Fingerprinting |
Spawned ioreg child process |
|
Behavioral |
Sparkle Replacement |
Persistence via bundle rotation |
|
Technique |
Tactic |
FlutterShell Behavior |
Basis |
|
T1566 Phishing |
Initial Access |
Google/YouTube malvertising lures |
Assessed |
|
T1204.002 Malicious File |
Execution |
User runs signed DMG app |
Observed |
|
T1059.004 Unix Shell |
Execution |
exec_sync/pdf_sync/renderPDF bridge commands |
Assessed |
|
T1543.001 Launch Agent |
Persistence |
Silent Sparkle bundle replacement |
Assessed |
|
T1553.002 Code Signing |
Defense Evasion |
Apple Developer cert procurement and notarization |
Observed |
|
T1027 Obfuscation |
Defense Evasion |
Dart obfuscation; command renaming; C2-conditional payload |
Observed + Assessed |
|
T1082 System Info Discovery |
Discovery |
ioreg IOPlatformUUID fingerprinting |
Assessed |
|
T1071.001 App Layer Protocol |
C&C |
HTTPS WebView to /getConfig + landing page |
Assessed |
|
T1102 Web Service |
C&C |
C2 delivered as web content over HTTPS |
Assessed |
|
T1176 Browser Modification |
Impact |
Chrome Secure Preferences hijack → sinterfumesco[.]com |
Assessed |
|
T1041 Exfil Over C2 |
Exfiltration |
read_file results returned via flutterInvoke bridge |
Assessed |
|
T1185 Browser Session Hijacking |
Collection |
Chrome profile modification enables persistent search hijack |
Assessed |
About the Author
Maor is a cybersecurity professional specializing in Threat Intelligence, Threat Hunting, and Incident Response. Follow Maor on LinkedIn.
ABOUT LEVELBLUE
LevelBlue secures what's next with intelligence-led security delivering visibility and speed to stop threats faster. As the world’s largest and most analyst-recognized pure-play managed security services provider, our AI-powered managed services and cyber expertise across managed, advisory, and incident response services help clients operate with confidence. Learn more about us.
https://www.levelblue.com/resources/blogs/internal-blog/how-to-create-a-blog-post/