<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>nullze</title><link>https://nullze.net/</link><description>Here you can find my researches on EDRs and Malware Development</description><language>en-us</language><generator>Hugo</generator><lastBuildDate>Fri, 04 Sep 2026 13:20:00 +0200</lastBuildDate><atom:link href="https://nullze.net/index.xml" rel="self" type="application/rss+xml"/><item><title>Peeling the Sentinel: A Market-Leading EDR Comes Apart With Undergraduate Tools</title><link>https://nullze.net/posts/peeling-the-sentinel/</link><guid isPermaLink="true">https://nullze.net/posts/peeling-the-sentinel/</guid><pubDate>Fri, 04 Sep 2026 13:20:00 +0200</pubDate><author>nullze</author><category>reverse-engineering</category><category>edr</category><category>security</category><category>sentinelone</category><description>A reverse-engineering teardown of SentinelOne Agent 26.1.2.177. The alarming part isn’t any single detection rule it’s how little skill, and how little time, it took to pull the whole detection stack apart on a workbench.</description><content:encoded><![CDATA[<blockquote><p><strong>A note on the code in this post.</strong> The snippets are <strong>redacted</strong> excerpts from the analysis tooling. Secret keys are masked and local paths are placeholders. The <em>mechanisms</em> are shown deliberately, because the mechanism is the finding: none of this required breaking real cryptography, and none of it required an exploit. This is a companion to a longer architectural write-up of the same agent; where that report stays neutral, this one is meant to ring a bell.</p></blockquote><h2 id="the-thesis-up-front">The thesis, up front<a class="anchor" href="#the-thesis-up-front" aria-label="Permalink to this section">&sect;</a></h2><p>EDR vendors expect their customers to think about their agents as of hard controls residing in the most privileged environment. As part of the promise made to the customer, there is an expectation of security in terms of confidentiality: an attacker should not be able to get inside the head of the agent and figure out how the agent works and which decisions are taken on the ground.</p>
<p>That is far from being the case for SentinelOne Agent 26.1.2.177 on Windows. All of the local detection policy corpus, the rules, the machine learning models, the trust/allow-lists were extracted and read in plaintext <strong>offline</strong> using Ghidra and a few dozens lines of Python code. None of the cryptography was broken. There were no zero-days o strange behaviours by the agent. Techniques applied are standard for introductory reverse-engineering/cryptography class.</p>
<p>This is the threat. Not that all EDR can be reverse-engineered (they do), but that this particular one gives away the information to a beginner analyst in an afternoon.</p>
<p>All the conclusions stated below are based on the analysis of the <strong>extracted</strong> files only. When a certain fact is inferred and not proven, that is stated explicitly.</p>
<h2 id="how-little-this-took">How little this took<a class="anchor" href="#how-little-this-took" aria-label="Permalink to this section">&sect;</a></h2><p>Prior to the discovery, the entry barrier, which is the point:</p>
<ul>
<li><strong>Environment</strong>: one agent running on disk, files from <code>C:\Program Files\SentinelOne\</code> and <code>C:\ProgramData\Sentinel\</code>. Offline only.</li>
<li><strong>Tools</strong>: Ghidra on the binaries, stock Python (<code>struct</code>, <code>base64</code>, plus one standard RC4 loop) on all formats.</li>
<li><strong>Cryptography</strong>: none. The configuration layer is XOR against a key repeated periodically; the key is retrieved using textbook known-plaintext attacks. The rules are RC4 encrypted with a key that is embedded <strong>into the product itself</strong>. The compiled YARA is XOR-<code>0xFF</code>.</li>
<li><strong>Exploitation</strong>: none. No bypasses were discovered at run-time; the files are just decrypted.</li>
<li><strong>Source code</strong>: each decryptor/parser listed below is about 10–60 lines.</li>
</ul>
<p>If PoC fits into a tweet, &quot;hard to inspect&quot; is not an actual feature of the product.</p>
<h2 id="the-architecture-in-one-breath">The architecture in one breath<a class="anchor" href="#the-architecture-in-one-breath" aria-label="Permalink to this section">&sect;</a></h2><p>To be fair to the design, the detection <em>architecture</em> is genuinely layered. Confirmed subsystems:</p>
<table>
  <thead>
      <tr>
          <th>Subsystem</th>
          <th>What it is</th>
          <th>Confirmed scale</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><strong>Static AI Engine</strong></td>
          <td>Pre-execution file classifier (<code>SentinelStaticAI.dll</code>, a generic YARA + Tree-sitter engine)</td>
          <td>~2,500 rules across 26 file-type categories</td>
      </tr>
      <tr>
          <td><strong>Behavioral ML Model</strong></td>
          <td>Runtime process classifier (<code>primaryBehavioralModel.bin</code>)</td>
          <td>65-condition decision chain, 5 features used</td>
      </tr>
      <tr>
          <td><strong>Discovery AI Model</strong></td>
          <td>Second model (<code>discovery_ai.json</code>)</td>
          <td>9,289-tree DAG forest, 162 thresholds</td>
      </tr>
      <tr>
          <td><strong>Lua Behavioral Rules</strong></td>
          <td>Scripted detections on telemetry (<code>Win-LUA-Behavioral.tar</code>)</td>
          <td>307+ rules, decrypted to source</td>
      </tr>
      <tr>
          <td><strong>Lunar Engine</strong></td>
          <td>Behavioral correlation framework (<code>LunarEngineWin.tar</code>)</td>
          <td>764 rule definitions, 202 behavioral indicators</td>
      </tr>
      <tr>
          <td><strong>Driver Rules</strong></td>
          <td>Kernel driver blocklist (<code>DriverRules.json</code>)</td>
          <td>1.4 MB, decrypted</td>
      </tr>
      <tr>
          <td><strong>Memory Scanner</strong></td>
          <td>In-memory YARA sets</td>
          <td>Per-file-type rules</td>
      </tr>
  </tbody>
</table>
<p>Beneath these sit kernel drivers (<code>SentinelMonitor.sys</code>, <code>SentinelNetworkMonitor.sys</code>, <code>SentinelDeviceControl.sys</code>, boot-start <code>SentinelELAM.sys</code>) and user-mode injection/hooking (<code>InProcessClient*.dll</code>, <code>Shadow32/64.dll</code>, AMSI and .NET/Java instrumentation). The ambition is not the problem. The problem is that the <em>protective shell</em> around all of this local content is ornamental.</p>
<h2 id="finding-1-the-encryption-is-obfuscation-not-protection">Finding #1 The “encryption” is obfuscation, not protection<a class="anchor" href="#finding-1-the-encryption-is-obfuscation-not-protection" aria-label="Permalink to this section">&sect;</a></h2><h3 id="config-and-policy-one-static-xor-key-for-everything">Config and policy: one static XOR key for everything<a class="anchor" href="#config-and-policy-one-static-xor-key-for-everything" aria-label="Permalink to this section">&sect;</a></h3><p>Each and every sensitive document that the agent keeps including <code>Policy.json</code>, <code>DriverRules.json</code>, <code>StaticRuleWin.json</code>, <code>SpecialImageAdjustments.json</code>, both the Lua tarballs, and so on has a magic <code>0B F5 CA ED</code> followed by 4-byte category. The rest of the file is XORed with a <strong>11-byte key which is common to all the files</strong>. As proven by decrypting all of them with one key:</p>
<figure class="code">
  <div class="code__well">
    <button class="copy" type="button" aria-label="Copy code to clipboard">copy</button>
    <div class="chroma-wrap"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="c1"># decrypt_all.py</span>
</span></span><span class="line"><span class="cl"><span class="kn">import</span> <span class="nn">struct</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># 11-byte static XOR key, identical for every encrypted file. Value redacted.</span>
</span></span><span class="line"><span class="cl"><span class="n">XOR_KEY</span> <span class="o">=</span> <span class="nb">bytes</span><span class="p">([</span><span class="mh">0x82</span><span class="p">,</span> <span class="mh">0x68</span><span class="p">,</span> <span class="mh">0x12</span><span class="p">,</span> <span class="mh">0xAF</span><span class="p">,</span> <span class="mh">0x1D</span><span class="p">,</span> <span class="mh">0xF6</span><span class="p">,</span> <span class="mh">0xA3</span><span class="p">,</span> <span class="mh">0x94</span><span class="p">,</span> <span class="mh">0x7F</span><span class="p">,</span> <span class="mh">0x6C</span><span class="p">,</span> <span class="mh">0x13</span><span class="p">])</span>
</span></span><span class="line"><span class="cl"><span class="c1">#        &lt;-- actual bytes masked; there is exactly one key and it never changes</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">data</span>  <span class="o">=</span> <span class="nb">open</span><span class="p">(</span><span class="n">filepath</span><span class="p">,</span> <span class="s1">&#39;rb&#39;</span><span class="p">)</span><span class="o">.</span><span class="n">read</span><span class="p">()</span>
</span></span><span class="line"><span class="cl"><span class="n">magic</span>  <span class="o">=</span> <span class="n">struct</span><span class="o">.</span><span class="n">unpack</span><span class="p">(</span><span class="s1">&#39;&lt;I&#39;</span><span class="p">,</span> <span class="n">data</span><span class="p">[</span><span class="mi">0</span><span class="p">:</span><span class="mi">4</span><span class="p">])[</span><span class="mi">0</span><span class="p">]</span>  <span class="c1"># 0x EDCAF50B (0B F5 CA ED, little-endian)</span>
</span></span><span class="line"><span class="cl"><span class="n">type_id</span> <span class="o">=</span> <span class="n">struct</span><span class="o">.</span><span class="n">unpack</span><span class="p">(</span><span class="s1">&#39;&lt;I&#39;</span><span class="p">,</span> <span class="n">data</span><span class="p">[</span><span class="mi">4</span><span class="p">:</span><span class="mi">8</span><span class="p">])[</span><span class="mi">0</span><span class="p">]</span>  <span class="c1"># a CATEGORY label confirmed NOT a key</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">payload</span> <span class="o">=</span> <span class="n">data</span><span class="p">[</span><span class="mi">8</span><span class="p">:]</span>               <span class="c1"># skip 4-byte magic + 4-byte category id</span>
</span></span><span class="line"><span class="cl"><span class="n">decoded</span> <span class="o">=</span> <span class="nb">bytes</span><span class="p">(</span><span class="n">payload</span><span class="p">[</span><span class="n">i</span><span class="p">]</span> <span class="o">^</span> <span class="n">XOR_KEY</span><span class="p">[</span><span class="n">i</span> <span class="o">%</span> <span class="mi">11</span><span class="p">]</span> <span class="k">for</span> <span class="n">i</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="nb">len</span><span class="p">(</span><span class="n">payload</span><span class="p">)))</span></span></span></code></pre></div>
  </div>
</figure>
<p>Confirmed facts: no key-derivation function, no machine binding, no DPAPI, no AES or RC4 at this layer. The 4-byte category field does not change the key the same 11 bytes decrypt everything.</p>
<p><strong>Recovering the key required no secret.</strong> Tar archives are null-padded; XOR of known-zero plaintext against the ciphertext leaks the keystream, and since the key repeats every 11 bytes, the keystream <em>is</em> the key:</p>
<figure class="code">
  <div class="code__well">
    <button class="copy" type="button" aria-label="Copy code to clipboard">copy</button>
    <div class="chroma-wrap"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="c1"># keystream_test.py</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Tar filename fields are null-padded, so ciphertext over those nulls == raw keystream.</span>
</span></span><span class="line"><span class="cl"><span class="n">payload</span> <span class="o">=</span> <span class="nb">open</span><span class="p">(</span><span class="n">lunar_tar</span><span class="p">,</span> <span class="s1">&#39;rb&#39;</span><span class="p">)</span><span class="o">.</span><span class="n">read</span><span class="p">()[</span><span class="mi">8</span><span class="p">:]</span>
</span></span><span class="line"><span class="cl"><span class="n">key11</span>  <span class="o">=</span> <span class="nb">bytes</span><span class="p">(</span><span class="n">payload</span><span class="p">[</span><span class="mi">10</span> <span class="o">+</span> <span class="n">i</span><span class="p">]</span> <span class="k">for</span> <span class="n">i</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="mi">11</span><span class="p">))</span>  <span class="c1"># 11 bytes lifted from the padding region</span>
</span></span><span class="line"><span class="cl"><span class="n">decoded</span> <span class="o">=</span> <span class="nb">bytes</span><span class="p">(</span><span class="n">payload</span><span class="p">[</span><span class="n">i</span><span class="p">]</span> <span class="o">^</span> <span class="n">key11</span><span class="p">[</span><span class="n">i</span> <span class="o">%</span> <span class="mi">11</span><span class="p">]</span> <span class="k">for</span> <span class="n">i</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="nb">len</span><span class="p">(</span><span class="n">payload</span><span class="p">)))</span>
</span></span><span class="line"><span class="cl"><span class="c1"># -&gt; a valid &#39;ustar&#39; tar header appears. Confirmed.</span></span></span></code></pre></div>
  </div>
</figure>
<p>That is a known-plaintext attack on a repeating-key XOR a classic textbook exercise, applied to a shipping enterprise security product.</p>
<h4 id="proof-hey-share-your-dotfiles">Proof: Hey share your dotfiles<a class="anchor" href="#proof-hey-share-your-dotfiles" aria-label="Permalink to this section">&sect;</a></h4><figure class="fig"><a href="https://nullze.net/posts/peeling-the-sentinel/dotfiles.png" data-zoom><img src="https://nullze.net/posts/peeling-the-sentinel/dotfiles.png" srcset="/posts/peeling-the-sentinel/dotfiles_hu_13dec55667097ff7.webp 480w" sizes="(min-width: 46em) 660px, 100vw" width="620" height="624" alt="SentinelOne Dotfiles XD" loading="lazy" decoding="async"></a>
  <figcaption>SentinelOne Dotfiles XD</figcaption>
</figure>
<figure class="fig u-wide"><a href="https://nullze.net/posts/peeling-the-sentinel/lines-50k.png" data-zoom><img src="https://nullze.net/posts/peeling-the-sentinel/lines-50k_hu_fcc7e67007178be0.webp" srcset="/posts/peeling-the-sentinel/lines-50k_hu_6919c7cd4e29849f.webp 480w, /posts/peeling-the-sentinel/lines-50k_hu_195a17902706626f.webp 768w, /posts/peeling-the-sentinel/lines-50k_hu_fcc7e67007178be0.webp 1024w" sizes="(min-width: 46em) 800px, 100vw" width="1024" height="944" alt="Total Lines are ~50k" loading="lazy" decoding="async"></a>
  <figcaption>Total Lines are ~50k</figcaption>
</figure>
<p>With only is possible to filter out what drivers are instantly flagged by the EDR.</p>
<h3 id="rules-rc4-with-a-key-that-ships-in-the-box">Rules: RC4 with a key that ships in the box<a class="anchor" href="#rules-rc4-with-a-key-that-ships-in-the-box" aria-label="Permalink to this section">&sect;</a></h3><p>The 307 Lua rules for detecting malware are RC4 encrypted and base64 encoded within the stubs <code>return { &quot;...&quot; }</code>. The RC4 encryption key was obtained in base64 encoding <strong>from the <code>lunar_loader.lua</code> of the very agent itself</strong> the product utilizes this key to protect itself:</p>
<figure class="code">
  <div class="code__well">
    <button class="copy" type="button" aria-label="Copy code to clipboard">copy</button>
    <div class="chroma-wrap"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="c1"># batch_decrypt_rules.py (redacted excerpt)</span>
</span></span><span class="line"><span class="cl"><span class="kn">import</span> <span class="nn">base64</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">rc4_crypt</span><span class="p">(</span><span class="n">data</span><span class="p">,</span> <span class="n">key</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">  <span class="n">S</span> <span class="o">=</span> <span class="nb">list</span><span class="p">(</span><span class="nb">range</span><span class="p">(</span><span class="mi">256</span><span class="p">));</span> <span class="n">j</span> <span class="o">=</span> <span class="mi">0</span>
</span></span><span class="line"><span class="cl">  <span class="k">for</span> <span class="n">i</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="mi">256</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">    <span class="n">j</span> <span class="o">=</span> <span class="p">(</span><span class="n">j</span> <span class="o">+</span> <span class="n">S</span><span class="p">[</span><span class="n">i</span><span class="p">]</span> <span class="o">+</span> <span class="n">key</span><span class="p">[</span><span class="n">i</span> <span class="o">%</span> <span class="nb">len</span><span class="p">(</span><span class="n">key</span><span class="p">)])</span> <span class="o">%</span> <span class="mi">256</span>
</span></span><span class="line"><span class="cl">    <span class="n">S</span><span class="p">[</span><span class="n">i</span><span class="p">],</span> <span class="n">S</span><span class="p">[</span><span class="n">j</span><span class="p">]</span> <span class="o">=</span> <span class="n">S</span><span class="p">[</span><span class="n">j</span><span class="p">],</span> <span class="n">S</span><span class="p">[</span><span class="n">i</span><span class="p">]</span>
</span></span><span class="line"><span class="cl">  <span class="n">i</span> <span class="o">=</span> <span class="n">j</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">out</span> <span class="o">=</span> <span class="nb">bytearray</span><span class="p">(</span><span class="nb">len</span><span class="p">(</span><span class="n">data</span><span class="p">))</span>
</span></span><span class="line"><span class="cl">  <span class="k">for</span> <span class="n">k</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="nb">len</span><span class="p">(</span><span class="n">data</span><span class="p">)):</span>
</span></span><span class="line"><span class="cl">    <span class="n">i</span> <span class="o">=</span> <span class="p">(</span><span class="n">i</span> <span class="o">+</span> <span class="mi">1</span><span class="p">)</span> <span class="o">%</span> <span class="mi">256</span><span class="p">;</span> <span class="n">j</span> <span class="o">=</span> <span class="p">(</span><span class="n">j</span> <span class="o">+</span> <span class="n">S</span><span class="p">[</span><span class="n">i</span><span class="p">])</span> <span class="o">%</span> <span class="mi">256</span>
</span></span><span class="line"><span class="cl">    <span class="n">S</span><span class="p">[</span><span class="n">i</span><span class="p">],</span> <span class="n">S</span><span class="p">[</span><span class="n">j</span><span class="p">]</span> <span class="o">=</span> <span class="n">S</span><span class="p">[</span><span class="n">j</span><span class="p">],</span> <span class="n">S</span><span class="p">[</span><span class="n">i</span><span class="p">]</span>
</span></span><span class="line"><span class="cl">    <span class="n">out</span><span class="p">[</span><span class="n">k</span><span class="p">]</span> <span class="o">=</span> <span class="n">data</span><span class="p">[</span><span class="n">k</span><span class="p">]</span> <span class="o">^</span> <span class="n">S</span><span class="p">[(</span><span class="n">S</span><span class="p">[</span><span class="n">i</span><span class="p">]</span> <span class="o">+</span> <span class="n">S</span><span class="p">[</span><span class="n">j</span><span class="p">])</span> <span class="o">%</span> <span class="mi">256</span><span class="p">]</span>
</span></span><span class="line"><span class="cl">  <span class="k">return</span> <span class="nb">bytes</span><span class="p">(</span><span class="n">out</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">key</span> <span class="o">=</span> <span class="sa">b</span><span class="s1">&#39;NeverGonna…redacted…&#39;</span>     <span class="c1"># shipped in lunar_loader.lua; you can guess the rest</span>
</span></span><span class="line"><span class="cl"><span class="n">plaintext</span> <span class="o">=</span> <span class="n">rc4_crypt</span><span class="p">(</span><span class="n">base64</span><span class="o">.</span><span class="n">b64decode</span><span class="p">(</span><span class="n">extract_first_quoted_string</span><span class="p">(</span><span class="n">content</span><span class="p">)),</span> <span class="n">key</span><span class="p">)</span></span></span></code></pre></div>
  </div>
</figure>
<p>Confirmed: with that one key, all 307 rules and the container (202 behavioral-indicator IDs, rule metadata, event maps) decrypt to readable Lua.</p>
<h4 id="proof-yet-another-place-where-lua-is-used">Proof: Yet another place where LUA is used<a class="anchor" href="#proof-yet-another-place-where-lua-is-used" aria-label="Permalink to this section">&sect;</a></h4><figure class="fig"><a href="https://nullze.net/posts/peeling-the-sentinel/lua-rules.png" data-zoom><img src="https://nullze.net/posts/peeling-the-sentinel/lua-rules.png" srcset="/posts/peeling-the-sentinel/lua-rules_hu_d93ec8831a6e9900.webp 480w" sizes="(min-width: 46em) 660px, 100vw" width="648" height="1252" alt="The Mystical LUA rules!!!!!" loading="lazy" decoding="async"></a>
  <figcaption>The Mystical LUA rules!!!!!</figcaption>
</figure>
<figure class="fig u-wide"><a href="https://nullze.net/posts/peeling-the-sentinel/lua-helpers.png" data-zoom><img src="https://nullze.net/posts/peeling-the-sentinel/lua-helpers_hu_c8349be2de061a1a.webp" srcset="/posts/peeling-the-sentinel/lua-helpers_hu_b0b52b70ca11f547.webp 480w, /posts/peeling-the-sentinel/lua-helpers_hu_56134c773e370ad4.webp 768w, /posts/peeling-the-sentinel/lua-helpers_hu_c8349be2de061a1a.webp 1024w" sizes="(min-width: 46em) 800px, 100vw" width="1024" height="652" alt="Some helper functions" loading="lazy" decoding="async"></a>
  <figcaption>Some helper functions</figcaption>
</figure>
<h3 id="yara-xor-0xff">YARA: XOR <code>0xFF</code><a class="anchor" href="#yara-xor-0xff" aria-label="Permalink to this section">&sect;</a></h3><p>The compiled YARA corpus uses a custom container with magic <code>A6 BE AD BE</code> literally <code>YARA</code> with every bit flipped:</p>
<figure class="code">
  <div class="code__well">
    <button class="copy" type="button" aria-label="Copy code to clipboard">copy</button>
    <div class="chroma-wrap"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="c1"># yara_rule_extractor.py (redacted excerpt)</span>
</span></span><span class="line"><span class="cl"><span class="n">MAGIC_XOR</span> <span class="o">=</span> <span class="sa">b</span><span class="s1">&#39;</span><span class="se">\xA6\xBE\xAD\xBE</span><span class="s1">&#39;</span>   <span class="c1"># &#39;YARA&#39; inverted</span>
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">deobfuscate</span><span class="p">(</span><span class="n">data</span><span class="p">):</span> <span class="k">return</span> <span class="nb">bytes</span><span class="p">(</span><span class="n">b</span> <span class="o">^</span> <span class="mh">0xFF</span> <span class="k">for</span> <span class="n">b</span> <span class="ow">in</span> <span class="n">data</span><span class="p">)</span>  <span class="c1"># A6^FF=&#39;Y&#39;, BE^FF=&#39;A&#39;, ...</span></span></span></code></pre></div>
  </div>
</figure>
<h3 id="why-this-is-the-finding-that-matters">Why this is the finding that matters<a class="anchor" href="#why-this-is-the-finding-that-matters" aria-label="Permalink to this section">&sect;</a></h3><p>Poor obfuscation of the rules is very common and even survivable, and by itself, analysts will always get at least the rules eventually. The identified issue is one of <strong>complete and total lack of cost associated with getting at the confidential data</strong>. Recovery of one XOR key for null-padding, one RC4 key which shipped inside the binary, and a one-line YARA transform reveals everything the product knows <em>locally</em>, including all policies, all thresholds, and all trust rules to any person who can read the files. There is no incremental cost difference between &quot;install the agent&quot; and &quot;read everything the agent knows.&quot;</p>
<p>This product, which is entirely predicated on the attacker's inability to predict or mitigate the detections of the product, does not merely have a hardening problem in this area – there is simply nothing keeping this data from leaking out. (Note scope: This is just a confidentiality vulnerability – an attacker reading the information in the file system. Files are ACL protected and the agent has full privileges to access them.)</p>
<h4 id="proof-this-was-too-easy-to-get">Proof: This was too easy to get<a class="anchor" href="#proof-this-was-too-easy-to-get" aria-label="Permalink to this section">&sect;</a></h4><figure class="fig"><a href="https://nullze.net/posts/peeling-the-sentinel/yara.png" data-zoom><img src="https://nullze.net/posts/peeling-the-sentinel/yara.png" srcset="/posts/peeling-the-sentinel/yara_hu_80d590ae85d378b2.webp 480w" sizes="(min-width: 46em) 660px, 100vw" width="608" height="741" alt="Ya….Ra" loading="lazy" decoding="async"></a>
  <figcaption>Ya….Ra</figcaption>
</figure>
<figure class="fig u-wide"><a href="https://nullze.net/posts/peeling-the-sentinel/yara-rules.png" data-zoom><img src="https://nullze.net/posts/peeling-the-sentinel/yara-rules_hu_d48f440d8948dda3.webp" srcset="/posts/peeling-the-sentinel/yara-rules_hu_cbecfbe8d559276b.webp 480w, /posts/peeling-the-sentinel/yara-rules_hu_676a3b2fdc6257b9.webp 768w, /posts/peeling-the-sentinel/yara-rules_hu_d48f440d8948dda3.webp 1024w" sizes="(min-width: 46em) 800px, 100vw" width="1024" height="948" alt="Pretty standard yara rules" loading="lazy" decoding="async"></a>
  <figcaption>Pretty standard yara rules</figcaption>
</figure>
<h2 id="finding-2-the-behavioral-ml-model-is-a-readable-decision-chain-gated-on-one-feature">Finding #2 The behavioral ML model is a readable decision chain gated on one feature<a class="anchor" href="#finding-2-the-behavioral-ml-model-is-a-readable-decision-chain-gated-on-one-feature" aria-label="Permalink to this section">&sect;</a></h2><p><code>primaryBehavioralModel.bin</code> (~500 KB) is marketed as machine learning. Confirmed by parsing: it is a <strong>65-condition sequential decision chain</strong> a flat pool of 4-byte values reinterpreted as thresholds, leaf scores, or packed decision nodes. The parser is short enough to read whole:</p>
<figure class="code">
  <div class="code__well">
    <button class="copy" type="button" aria-label="Copy code to clipboard">copy</button>
    <div class="chroma-wrap"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="c1"># behavioral_model_harness.py (redacted excerpt)</span>
</span></span><span class="line"><span class="cl"><span class="n">FEATURE_BYTES</span> <span class="o">=</span> <span class="p">{</span><span class="mh">0x44</span><span class="p">,</span> <span class="mh">0x45</span><span class="p">,</span> <span class="mh">0x46</span><span class="p">,</span> <span class="mh">0x47</span><span class="p">,</span> <span class="mh">0x61</span><span class="p">}</span>  <span class="c1"># the only feature IDs present: 68,69,70,71,97</span>
</span></span><span class="line"><span class="cl"><span class="n">pool</span> <span class="o">=</span> <span class="p">[</span><span class="n">u32</span><span class="p">(</span><span class="n">raw</span><span class="p">,</span> <span class="mi">12</span> <span class="o">+</span> <span class="n">i</span><span class="o">*</span><span class="mi">4</span><span class="p">)</span> <span class="k">for</span> <span class="n">i</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">((</span><span class="nb">len</span><span class="p">(</span><span class="n">raw</span><span class="p">)</span> <span class="o">-</span> <span class="mi">12</span><span class="p">)</span><span class="o">//</span><span class="mi">4</span><span class="p">)]</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">internals</span> <span class="o">=</span> <span class="p">{}</span>
</span></span><span class="line"><span class="cl"><span class="k">for</span> <span class="n">i</span><span class="p">,</span> <span class="n">v</span> <span class="ow">in</span> <span class="nb">enumerate</span><span class="p">(</span><span class="n">pool</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">  <span class="n">feat</span> <span class="o">=</span> <span class="p">(</span><span class="n">v</span> <span class="o">&gt;&gt;</span> <span class="mi">24</span><span class="p">)</span> <span class="o">&amp;</span> <span class="mh">0xFF</span>
</span></span><span class="line"><span class="cl">  <span class="k">if</span> <span class="n">feat</span> <span class="ow">in</span> <span class="n">FEATURE_BYTES</span><span class="p">:</span>         <span class="c1"># this slot is a decision node</span>
</span></span><span class="line"><span class="cl">    <span class="n">thresh_idx</span> <span class="o">=</span> <span class="p">(</span><span class="n">v</span> <span class="o">&gt;&gt;</span> <span class="mi">16</span><span class="p">)</span> <span class="o">&amp;</span> <span class="mh">0xFF</span>    <span class="c1"># -&gt; threshold float in the pool</span>
</span></span><span class="line"><span class="cl">    <span class="n">right_child</span> <span class="o">=</span> <span class="n">v</span> <span class="o">&amp;</span> <span class="mh">0xFFFF</span>       <span class="c1"># -&gt; leaf score (the &#34;detected&#34; branch)</span>
</span></span><span class="line"><span class="cl">    <span class="n">internals</span><span class="p">[</span><span class="n">i</span><span class="p">]</span> <span class="o">=</span> <span class="p">(</span><span class="n">feat</span><span class="p">,</span> <span class="n">thresh_idx</span><span class="p">,</span> <span class="n">right_child</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="c1"># left child is implicit: i + 1 (fall through to the next condition)</span></span></span></code></pre></div>
  </div>
</figure>
<p>Two confirmed structural facts:</p>
<p><strong>Only five features are used</strong> <code>{68, 69, 70, 71, 97}</code>. Everything else the pipeline can collect is unused by the shipped model.</p>
<p><strong>A particular feature sits at the head of the chain.</strong> Feature 68 is evaluated in the first <strong>43 out of 65 conditions</strong>, and its initial threshold is <code>2.0</code>. The evaluator exits on meeting the threshold for a feature, and therefore fails otherwise. Thus, the model checks whether &quot;is feature 68 &gt;= 2?&quot; and fails to check features 69, 70, 71, and 97 in the remaining 43 conditions of the same feature.</p>
<p>This was confirmed empirically with a binary search over each feature in isolation:</p>
<figure class="code">
  <div class="code__well">
    <button class="copy" type="button" aria-label="Copy code to clipboard">copy</button>
    <div class="chroma-wrap"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="c1"># evasion_analysis.py (redacted excerpt)</span>
</span></span><span class="line"><span class="cl"><span class="k">for</span> <span class="n">feat</span> <span class="ow">in</span> <span class="n">all_features</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">  <span class="n">lo</span><span class="p">,</span> <span class="n">hi</span><span class="p">,</span> <span class="n">trigger</span> <span class="o">=</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">10000</span><span class="p">,</span> <span class="kc">None</span>
</span></span><span class="line"><span class="cl">  <span class="k">while</span> <span class="n">lo</span> <span class="o">&lt;=</span> <span class="n">hi</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="n">mid</span> <span class="o">=</span> <span class="p">(</span><span class="n">lo</span> <span class="o">+</span> <span class="n">hi</span><span class="p">)</span> <span class="o">//</span> <span class="mi">2</span>
</span></span><span class="line"><span class="cl">    <span class="n">features</span> <span class="o">=</span> <span class="p">{</span><span class="n">f</span><span class="p">:</span> <span class="mf">0.0</span> <span class="k">for</span> <span class="n">f</span> <span class="ow">in</span> <span class="n">all_features</span><span class="p">}</span>  <span class="c1"># every other feature = 0</span>
</span></span><span class="line"><span class="cl">    <span class="n">features</span><span class="p">[</span><span class="n">feat</span><span class="p">]</span> <span class="o">=</span> <span class="nb">float</span><span class="p">(</span><span class="n">mid</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="n">score_path</span><span class="p">(</span><span class="n">pool</span><span class="p">,</span> <span class="n">chain</span><span class="p">,</span> <span class="n">features</span><span class="p">)[</span><span class="mi">0</span><span class="p">]</span> <span class="o">&gt;=</span> <span class="mf">0.5</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">      <span class="n">trigger</span> <span class="o">=</span> <span class="n">mid</span><span class="p">;</span> <span class="n">hi</span> <span class="o">=</span> <span class="n">mid</span> <span class="o">-</span> <span class="mi">1</span>
</span></span><span class="line"><span class="cl">    <span class="k">else</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">      <span class="n">lo</span> <span class="o">=</span> <span class="n">mid</span> <span class="o">+</span> <span class="mi">1</span>
</span></span><span class="line"><span class="cl">  <span class="c1"># Result: only feature 68 ever flips the verdict from a cold start.</span>
</span></span><span class="line"><span class="cl">  <span class="c1"># With feature 68 = 0, features 69/70/71/97 swept 0..10000 never trigger.</span></span></span></code></pre></div>
  </div>
</figure>
<p>Also verified: some later conditions have <strong>negative thresholds</strong> (−0.6 to −0.9) against counters that can never be negative because of construction conditions that can never trigger. Dead code.</p>
<p>The confirmed conclusion is specific yet significant: this model’s output boils down to one feature. If feature 68 is below 2, then the model will always output “clean” regardless of the rest of the input. (<em>Inference, not confirmation:</em> It looks like SentinelOne’s API hooks are cross-referenced and feature 68 corresponds to inter-process memory accesses, although the exact meaning of features 68 through 97 lives in native code, not in the model file, and was not completely confirmed.)</p>
<p>The second model, called “Discovery AI,” is far more serious, a 9,289 tree DAG forest containing 162 thresholds, and the features themselves live in native code, not in the model file. Verified: it is controlled by a single configuration flag, <code>discoveryAIConfig.telemetriesEnabledGroup</code>, located in the trivially-decrypted <code>InstallationConfig.json</code> from Finding #1.</p>
<h4 id="proof-a-machine-that-learns">Proof: A Machine That Learns<a class="anchor" href="#proof-a-machine-that-learns" aria-label="Permalink to this section">&sect;</a></h4><figure class="fig u-wide"><a href="https://nullze.net/posts/peeling-the-sentinel/behavioral-model.png" data-zoom><img src="https://nullze.net/posts/peeling-the-sentinel/behavioral-model_hu_df6eee6c5c12ca09.webp" srcset="/posts/peeling-the-sentinel/behavioral-model_hu_1f017573f5133d61.webp 480w, /posts/peeling-the-sentinel/behavioral-model_hu_396296effaa2975d.webp 768w, /posts/peeling-the-sentinel/behavioral-model_hu_df6eee6c5c12ca09.webp 1024w" sizes="(min-width: 46em) 800px, 100vw" width="1024" height="952" alt="Light Weight!!" loading="lazy" decoding="async"></a>
  <figcaption>Light Weight!!</figcaption>
</figure>
<h2 id="finding-3-the-entire-trust-model-is-human-readable">Finding #3 The entire trust model is human-readable<a class="anchor" href="#finding-3-the-entire-trust-model-is-human-readable" aria-label="Permalink to this section">&sect;</a></h2><p>Decrypting the Lua tarball yields <code>Matchers.lua</code>: <strong>2,147 lines of plaintext Lua defining 215 named matchers</strong> the allow-lists that exempt trusted software from specific detections. Each entry is <code>{ path, publisher, description, signingType }</code>, where <code>L(...)</code> is a case-insensitive wildcard:</p>
<figure class="code">
  <div class="code__well">
    <button class="copy" type="button" aria-label="Copy code to clipboard">copy</button>
    <div class="chroma-wrap"><pre tabindex="0" class="chroma"><code class="language-lua" data-lang="lua"><span class="line"><span class="cl"><span class="c1">-- Matchers.lua (verbatim excerpt public-name binaries only)</span>
</span></span><span class="line"><span class="cl"><span class="n">Matchers.svchostMatchers</span> <span class="o">=</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="p">{</span><span class="n">L</span><span class="p">(</span><span class="s1">&#39;%SystemRoot%</span><span class="se">\\</span><span class="s1">System32</span><span class="se">\\</span><span class="s1">svchost.exe&#39;</span><span class="p">),</span> <span class="n">L</span><span class="p">(</span><span class="s1">&#39;&#39;</span><span class="p">),</span> <span class="n">L</span><span class="p">(</span><span class="s1">&#39;Host Process for Windows Services&#39;</span><span class="p">),</span> <span class="s1">&#39;SystemEKU&#39;</span><span class="p">},</span>
</span></span><span class="line"><span class="cl">  <span class="p">{</span><span class="n">L</span><span class="p">(</span><span class="s1">&#39;%SystemRoot%</span><span class="se">\\</span><span class="s1">SysWOW64</span><span class="se">\\</span><span class="s1">svchost.exe&#39;</span><span class="p">),</span> <span class="n">L</span><span class="p">(</span><span class="s1">&#39;&#39;</span><span class="p">),</span> <span class="n">L</span><span class="p">(</span><span class="s1">&#39;Host Process for Windows Services&#39;</span><span class="p">),</span> <span class="s1">&#39;SystemEKU&#39;</span><span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span></span></span></code></pre></div>
  </div>
</figure>
<p>The problem isn't the existence of the allow-list, all EDRs have one, but rather that the <strong>entire trust model is shared with the endpoint in essentially plain text</strong>, so any analyst can simply parse the file and figure out what is allowed and what is blocked. Confirmed contents include:</p>
<ul>
<li><strong>27 commercial attack simulators</strong> named explicitly (Cymulate, Picus, AttackIQ, SafeBreach).</li>
<li><strong>28 AV/security vendors</strong> matched with the <code>?ALL_PATHS?</code> wildcard path is irrelevant; the signature is the only gate.</li>
<li><strong>A global Microsoft exemption</strong> (<code>msMatchers</code>) granting <code>?ALL_PATHS?</code> to Microsoft-published binaries.</li>
<li><strong>50+ backup products</strong> (Veeam, Veritas, Acronis, CommVault, Datto, Arcserve, …).</li>
<li><strong>8 forensic memory-acquisition drivers</strong> whitelisted as <code>memoryAccessDrivers</code> (WinPMEM, DumpIt, FTK Imager, Redline, EnCase, Memoryze, …).</li>
<li>Named <strong>LSASS-access</strong> exemptions for several EDR/DLP products, <strong>direct-syscall</strong> exemptions scoped to Office, and a single <strong><code>RobloxPlayerBeta.exe</code></strong> entry that whitelists an LDR callback-trap bypass.</li>
</ul>
<p>The importance of the <code>signingType</code> is confirmed by the following facts: the value <code>SystemEKU</code> mandates the use of a particular Windows component EKU certificate in the chain (an actual limitation), while <code>NoEKUs</code> accepts any code signing certificate and the empty value disables the whole signature validation. The fact that the defender has access to this table is helpful; the fact that everyone with access to the agent does is alarming.</p>
<h4 id="proof-this-is-not-good-to-know">Proof: This is not good to know…<a class="anchor" href="#proof-this-is-not-good-to-know" aria-label="Permalink to this section">&sect;</a></h4><figure class="fig u-wide"><a href="https://nullze.net/posts/peeling-the-sentinel/trust-matchers.png" data-zoom><img src="https://nullze.net/posts/peeling-the-sentinel/trust-matchers_hu_799cf989afc19ea8.webp" srcset="/posts/peeling-the-sentinel/trust-matchers_hu_9853454fb1c12295.webp 480w, /posts/peeling-the-sentinel/trust-matchers_hu_a69e9740332dd32f.webp 768w, /posts/peeling-the-sentinel/trust-matchers_hu_799cf989afc19ea8.webp 1024w" sizes="(min-width: 46em) 800px, 100vw" width="1024" height="541" alt="Hmmm….Maybe this shouldn’t be here…" loading="lazy" decoding="async"></a>
  <figcaption>Hmmm….Maybe this shouldn’t be here…</figcaption>
</figure>
<h2 id="finding-4-detection-logic-is-fully-legible-including-its-own-gaps">Finding #4 Detection logic is fully legible, including its own gaps<a class="anchor" href="#finding-4-detection-logic-is-fully-legible-including-its-own-gaps" aria-label="Permalink to this section">&sect;</a></h2><p>Because the rules decrypt to source, the agent’s detection <em>decisions</em> can be read directly. A grep across the decrypted corpus enumerates the primitives; reading individual rules confirms how they decide:</p>
<figure class="code">
  <div class="code__well">
    <button class="copy" type="button" aria-label="Copy code to clipboard">copy</button>
    <div class="chroma-wrap"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="c1"># batch_decrypt_rules.py (redacted excerpt) after RC4-decrypting all 307 rules:</span>
</span></span><span class="line"><span class="cl"><span class="n">patterns</span> <span class="o">=</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="s1">&#39;Injection&#39;</span><span class="p">:</span> <span class="p">[</span><span class="s1">&#39;CreateRemoteThread&#39;</span><span class="p">,</span> <span class="s1">&#39;WriteProcessMemory&#39;</span><span class="p">,</span> <span class="s1">&#39;VirtualAllocEx&#39;</span><span class="p">],</span>
</span></span><span class="line"><span class="cl">  <span class="s1">&#39;LSASS&#39;</span><span class="p">:</span>   <span class="p">[</span><span class="s1">&#39;lsass&#39;</span><span class="p">,</span> <span class="s1">&#39;Lsass&#39;</span><span class="p">,</span> <span class="s1">&#39;LSASS&#39;</span><span class="p">],</span>
</span></span><span class="line"><span class="cl">  <span class="s1">&#39;Syscall&#39;</span><span class="p">:</span>  <span class="p">[</span><span class="s1">&#39;NtCreate&#39;</span><span class="p">,</span> <span class="s1">&#39;NtOpen&#39;</span><span class="p">,</span> <span class="s1">&#39;NtWrite&#39;</span><span class="p">,</span> <span class="s1">&#39;ZwCreate&#39;</span><span class="p">],</span>
</span></span><span class="line"><span class="cl">  <span class="s1">&#39;ETW/AMSI&#39;</span><span class="p">:</span> <span class="p">[</span><span class="s1">&#39;etw&#39;</span><span class="p">,</span> <span class="s1">&#39;ETW&#39;</span><span class="p">,</span> <span class="s1">&#39;amsi&#39;</span><span class="p">,</span> <span class="s1">&#39;AMSI&#39;</span><span class="p">],</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span></span></span></code></pre></div>
  </div>
</figure>
<p>By reading the source it confirmed several desgin behaviors tha are simply the <em>how the code is written</em>, no exploitation involved:</p>
<ul>
<li><strong>Silent event drop on unresolved source UID.</strong> In <code>ows_model_interface.lua</code>, if <code>event.source.uid == nil</code>, the event is dropped; the code comment states it is “not possible to send a detection on them even if a rule matches.”</li>
<li><strong><code>OncePerProcess</code> / <code>OncePerGroup</code> indicators</strong> exist and, by definition, fire once per scope.</li>
<li><strong>Custom process blaming is disabled.</strong> <code>is_custom_blaming_on()</code> unconditionally returns <code>false</code>, guarded by a <code>WIN-76915</code> TODO. Blame always falls on the source process.</li>
<li><strong>Detection registration is version-gated.</strong> Large portions of rule logic register only on newer agents (e.g. registry dynamic rules &gt;= 25.2, behavioral-indicator filtering &gt;= 26.1); older agents log “did not register … because version is not above X.”</li>
<li><strong><code>Shadow32.dll</code> hooks exactly 6 user-mode APIs</strong> (five <code>Rtl*Heap</code> variants plus <code>CreateToolhelp32Snapshot</code>) across ntdll/kernel32, with <strong>no syscall-layer hooks</strong> confirmed by disassembly. It also carries documented, string-labeled off-switches for its own hooks (predicate failure, policy exclusion, DLL version mismatch, per-thread TLS state).</li>
</ul>
<p>These are stated here as confirmed properties of the shipped code. Turning any of them into an operational bypass is left unwritten the point of this post is the legibility, not a playbook.</p>
<h2 id="the-final-blow">The Final Blow<a class="anchor" href="#the-final-blow" aria-label="Permalink to this section">&sect;</a></h2><p>I think it’s time to show how could an attacked use the findings:</p>
<figure class="fig video">
  <video controls preload="metadata" playsinline poster="/posts/peeling-the-sentinel/final-blow.png" width="1802" height="934"><source src="https://nullze.net/posts/peeling-the-sentinel/final-blow.mp4" type="video/mp4">
    <p class="notice">Your browser can’t play this video.
       <a href="https://nullze.net/posts/peeling-the-sentinel/final-blow.mp4">Download it</a> instead.</p>
  </video><figcaption>samSung.bat triggering the WMI even when it's a SAM dump.</figcaption>
</figure>

<div class="callout callout--note" role="note">
    <strong class="callout__label">NOTE</strong>
    <p>The alert was generated but was miss-classified as a: <strong>samSung.bat - Enumeration using a WMI query detected</strong> This shows that rules have gaps that even the cloud can't resolve</p>
  </div><h2 id="why-this-is-grave">Why this is grave<a class="anchor" href="#why-this-is-grave" aria-label="Permalink to this section">&sect;</a></h2><p>This laundry list builds upon itself, and all of it stems from the same common problem:</p>
<ol>
<li>The obfuscation <strong>leaks the rules</strong>, rendering any detection, threshold, and policy readable offline.</li>
<li>The obfuscation <strong>leaks the configuration</strong>; the flags controlling entire subsystems are readable in plaintext.</li>
<li>The machine learning model <strong>reveals its decision to one countermeasure</strong>; its decision process is a short and readable sequence of operations.</li>
<li>The trust model <strong>reveals its weaknesses</strong>; the allow-list enumerates exactly what is trusted and how loosely.</li>
</ol>
<p>The threat model of the EDR assumes that the endpoint will fall into the hands of an attacker – that is the entire justification for this product. Given this assumption, protecting the detection logic against disclosure by using a fixed XOR key and in-box RC4 key means that a mediocre analyst quickly reaches parity with the vendor in terms of understanding what the agent searches for. Being able to identify exactly what is and is not detected is exactly the kind of reconnaissance necessary to turn a generic payload into a targeted attack.</p>
<p>Note that this is a limited consideration: cloud intelligence, server-side correlation and SOC analyst are not in the scope here, and they do matter. Quite a lot of silent verdicts also generate telemetry in the upstream side, and a good SOC can catch anything that is not stopped by the local decision-making logic. However, local confidentiality of the detection logic is the expected behavior of the agent, which is not provided in this build.</p>
<h2 id="what-defenders-should-take-from-this">What defenders should take from this<a class="anchor" href="#what-defenders-should-take-from-this" aria-label="Permalink to this section">&sect;</a></h2><ul>
<li><strong>Assume your adversary will be able to read the same rules and allow-lists as you can.</strong> Consider your detection content locally to be effectively available to all those who have access to the agent, and periodically verify that trusted paths, trusted publishers, and trusted descriptions from the matcher set cannot be trivially fulfilled by untrusted binaries.</li>
<li><strong>Rely on the cloud/SOC level.</strong> Local detection is one factor; central correlation is where hidden telemetry comes into play. Make sure it is really being correlated.</li>
<li><strong>Ask the right questions of your vendors:</strong> Is there something stronger than a static, in-product key that protects your local detection content? How does the model change its verdict if a single feature is disabled?</li>
</ul>
<h2 id="a-note-on-responsibility">A note on responsibility<a class="anchor" href="#a-note-on-responsibility" aria-label="Permalink to this section">&sect;</a></h2><p>This was offline analysis of a shipping build, for defensive research. Every code excerpt is redacted keys masked, personal paths replaced with placeholders and no operational bypass is spelled out. The most important fixes are entirely in the vendor’s hands: protect local detection content with keys that do not travel inside the product, and stop letting a single feature act as a master switch for a model marketed as ML.</p>
<p>The uncomfortable takeaway is not that this agent can be studied. It is that studying it required so little. Trust in a security product should rest on its design surviving scrutiny not on scrutiny being hard.</p>
<hr>
<p><em>Analyzed build: SentinelOne Agent 26.1.2.177 (Windows).</em></p>
<p><em>Confirmed via Ghidra plus ~10–60-line Python decryptors/parsers for the XOR config format, the RC4 rule format, the XOR-<code>0xFF</code> YARA container, both ML models, the RocksDB scan-results store (55,316 entries), and the behavioral-indicator mappings. Inferences are labeled as such; code excerpts are redacted for responsible publication.</em></p>
]]></content:encoded></item></channel></rss>