<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="https://xiaosan-re.github.io/feed.xml" rel="self" type="application/atom+xml" /><link href="https://xiaosan-re.github.io/" rel="alternate" type="text/html" /><updated>2026-10-30T16:19:34+08:00</updated><id>https://xiaosan-re.github.io/feed.xml</id><title type="html">我是小三</title><subtitle>我是小三的个人博客，记录 逆向相关。</subtitle><author><name>我是小三</name></author><entry><title type="html">反 AI 逆向方法论：从智能逆向原理到 LLVM Pass 落地</title><link href="https://xiaosan-re.github.io/2026/10/30/anti-ai-reversing-llvm-pass/" rel="alternate" type="text/html" title="反 AI 逆向方法论：从智能逆向原理到 LLVM Pass 落地" /><published>2026-10-30T00:00:00+08:00</published><updated>2026-10-30T00:00:00+08:00</updated><id>https://xiaosan-re.github.io/2026/10/30/anti-ai-reversing-llvm-pass</id><content type="html" xml:base="https://xiaosan-re.github.io/2026/10/30/anti-ai-reversing-llvm-pass/"><![CDATA[<p>Android Native 逆向里，经常能看到这样的保护思路：隐藏符号、加一层控制流平坦化，再配上反调试检测。攻击者拿到 SO 后，需要找入口、追调用、还原表达式，最后写代码验证。保护的价值，很大一部分来自这些步骤消耗的时间。</p>

<p>MCP 接口和 REA 等工具进入工作流后，这些操作开始被智能代理串起来：给出目标，代理查询函数与交叉引用，编写解码脚本；反编译走不通，就读机器码；静态推导代价太高，就用真机 trace 或在环境里仿真分析。最终交付的也可以是一份能运行的重建代码。</p>

<p>这并不意味着任意保护都能被瞬间破解。真正发生变化的是：<strong>逆向不再完全依赖分析师逐个完成操作，一部分专业工具的使用与验证过程可以被持续自动化。</strong> 对保护方而言，藏掉函数名、把控制流图变复杂，或引入 VMP（代码虚拟化），已经不足以回答“业务是否更难恢复”。</p>

<p>本文研究的对象是一个明确的转换链：<strong>如何将 BLA 的分析预算干扰、证据干扰和运行时守卫适配到 Android Native，再通过 LLVM Pass 改写真实业务的数据流与执行表示？</strong></p>

<p>攻击方持有发布后的 ELF，可进行静态解码；动态轨还允许在自有副本上仿真、patch 和读取状态。研究目标是提高独立功能重建的成本。对攻击者可控制的客户端执行环境，不假设本地检测返回值或混淆种子具有不可提取性。</p>

<p>下面先拆解智能逆向的工作链，再实现发布证据干扰、入口观察、表达式切片和整函数虚拟化。每种机制都说明它改变了哪一层表示、为什么可能有效，以及攻击者还能从哪里继续。最后用同一业务函数的隔离盲测检验这些判断。</p>

<h2 id="section-1">一、背景：MCP、REA 与智能逆向的出现</h2>

<h3 id="section-1-1">1.1 从“模型解释代码”到“代理使用工具”</h3>

<p>单独的语言模型只能根据收到的内容推断。它看不到二进制中的全部代码，也不会因为读了一段伪代码就自动拥有调试器的能力。</p>

<p><a href="https://github.com/modelcontextprotocol/docs/blob/main/introduction.mdx">MCP（Model Context Protocol）</a>提供了连接 AI 应用与外部工具、数据的标准接口。接入相应服务后，模型可以发出结构化调用，请工具执行操作，再根据返回结果继续决策。MCP 本身不是逆向算法，具体能力取决于服务暴露了什么工具。</p>

<p><a href="https://github.com/morluto/rea">REA</a>将二进制、应用和运行行为分析组织成可供代理使用的工具入口。反编译、函数查询、字符串和引用分析可以成为代理的一步操作，而不是需要人工复制粘贴的一段上下文。</p>

<p>当工具入口与脚本环境结合，工作流就从“一问一答”变成了循环：</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>提出分析目标
    ↓
选择工具 → 获取证据 → 推导假设 → 编写脚本验证
    ↑                                ↓
    └──────── 失败后换路径 ────────────┘
                                     ↓
                            提交解释或独立实现
</code></pre></div></div>

<p>手机阅读时，代码块和宽表格可左右滑动；架构图可点击打开 SVG 原图，再用浏览器缩放查看。实测部分另提供逐例对照卡片。</p>

<p><a href="/images/posts/android/anti-ai-llvm/01-agent-workflow.svg" class="anti-ai-figure"><img src="/images/posts/android/anti-ai-llvm/01-agent-workflow.svg" alt="智能逆向代理的工具与验证闭环" /></a></p>

<p><em>图 1：模型负责选择和组合操作，分析引擎提供证据，脚本与执行环境用来验证假设。</em></p>

<h3 id="section-1-2">1.2 为什么核心逻辑更容易被持续分析</h3>

<p>变化不只在模型“会读代码”，还在它能重复做过去昂贵、琐碎的操作：</p>

<ul>
  <li><strong>扩展证据范围</strong>：从一个导出函数追到辅助函数、常量表和调用者。</li>
  <li><strong>切换表示方式</strong>：反编译失败后，继续读原始字节、解码指令或提升为中间表示。</li>
  <li><strong>批量试验</strong>：生成输入、记录返回值、比较分支，逐步缩小算法假设。</li>
  <li><strong>跨过局部障碍</strong>：静态分析代价太高时，尝试执行、仿真或修改自己的目标副本。</li>
  <li><strong>生成替代实现</strong>：交付可运行程序，而不只是对伪代码的自然语言解释。</li>
</ul>

<p>这些能力降低了部分分析工作的操作门槛，也使同一类保护被反复尝试的成本下降。但是否完整恢复，仍然受目标结构、工具能力、输入样本、预算和验证质量影响。</p>

<h3 id="section-1-3">1.3 保护目标需要重新定义</h3>

<p>对 Android SO，至少要区分三件事：</p>

<table>
  <thead>
    <tr>
      <th>目标</th>
      <th>应检查什么</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>隐藏实现细节</td>
      <td>算法、控制流、关键常量是否容易被解释或提取</td>
    </tr>
    <tr>
      <td>阻碍功能克隆</td>
      <td>是否能生成不依赖原 SO 的独立实现</td>
    </tr>
    <tr>
      <td>保持正常业务</td>
      <td>加固后是否仍符合原有输入输出与环境约束</td>
    </tr>
  </tbody>
</table>

<p>模型没有解释清楚算法，不代表它无法克隆行为；模型给出流畅报告，也不代表它的实现是正确的。本文把“独立重建通过隐藏输入测试”作为主要实验指标，同时单独观察关键材料泄露和执行成本。</p>

<h2 id="section-2">二、原理：沿着智能逆向链路寻找防护位置</h2>

<h3 id="section-2-1">2.1 分析器、模型与执行环境各自承担什么</h3>

<p>一条典型链路可以拆成三部分：</p>

<table>
  <thead>
    <tr>
      <th>部分</th>
      <th>提供的能力</th>
      <th>保护方可以影响的位置</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>格式与分析引擎</td>
      <td>识别文件、发现函数、提取字符串、反编译与交叉引用</td>
      <td>证据数量、结构特征、函数边界与表示复杂度</td>
    </tr>
    <tr>
      <td>模型推导</td>
      <td>解释证据、提出假设、决定下一步工具调用</td>
      <td>证据的一致性、真假路径、跨函数数据依赖</td>
    </tr>
    <tr>
      <td>脚本与执行环境</td>
      <td>批量计算、仿真、跟踪、patch 和差分验证</td>
      <td>运行时观察面、数据出现的位置与执行路径</td>
    </tr>
  </tbody>
</table>

<p>三者之间不是单向流水线。某个分析器返回 <code class="language-plaintext highlighter-rouge">limited</code> 后，代理仍可能换工具；某条伪代码太难理解，它可以把片段提升成脚本；遇到本地检测，它也可能直接修改返回值。</p>

<p>因此，“让一个入口失效”和“让任务无法完成”之间，还有很长的距离。</p>

<h3 id="section-2-2">2.2 反 AI 防护的核心是改变完成任务的成本</h3>

<p>保护不应该只增加代码长度，而要增加攻击者取得可信结果所需的工作：</p>

<ol>
  <li>让便宜的证据入口不再直接暴露关键关系。</li>
  <li>增加真假路径判断与跨片段组合的成本。</li>
  <li>降低单个 hook 点、解码器或纯函数接口的取证价值。</li>
  <li>让独立重建必须处理真正的业务状态，而不只是绕过一个 wrapper。</li>
</ol>

<p>这里既包含工具层面的干扰，也包含传统混淆的数据流、控制流和表示变换。它们对人和自动化代理的作用有重叠，不能仅因为目标是 AI，就认为旧的编译器语义问题不再重要。</p>

<h3 id="section-2-3">2.3 正确性、攻击结果与成本必须分开</h3>

<p><strong>语义等价</strong>检查的是原始与保护后的产物；<strong>恢复正确率</strong>检查的是攻击者提交的重建；<strong>成本</strong>记录双方为此付出的资源。三者不能互相替代。</p>

<p>例如，保护后的 SO 在测试中与原版完全一致，但代理仍可能完整恢复；代理消耗更多 token 后提交错误程序，也不能证明它在剩余预算内绝无可能修正。评估必须保留这些区别。</p>

<h3 id="section-2-4">2.4 形式化目标：正常路径语义保持与独立重建</h3>

<p>设原业务函数为 <code class="language-plaintext highlighter-rouge">F(x)</code>，其中 x 是四个 i32 参数；整函数转换后的真实业务为 <code class="language-plaintext highlighter-rouge">V(x)</code>，影子业务为 <code class="language-plaintext highlighter-rouge">H(x)</code>，入口观察结果为 <code class="language-plaintext highlighter-rouge">g(e,c)</code>。e 表示观察到的环境信息，c 表示调用计数与缓存状态。最终产物的抽象行为为：</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>P(x,e,c) = H(x),  if g(e,c) != 0
           V(x),  otherwise

正常路径正确性目标：V(x) = F(x)，对受支持源 IR 的有定义执行成立。
带守卫的行为对照：g(e,c) = 0 时，P(x,e,c) = F(x)。
</code></pre></div></div>

<p>因此，“等价校验通过”需要说明环境前提。守卫命中后返回影子结果，是设计中的另一条行为，不属于正常路径等价承诺。当前有限测试为这些关系提供回归证据，没有构成全输入域的形式化证明。</p>

<p>攻击目标则是提交独立程序 <code class="language-plaintext highlighter-rouge">R(x)</code>，在冻结的隐藏测试集 D 上满足 <code class="language-plaintext highlighter-rouge">R(x)=F(x)</code>，并且不加载、委托原目标。代理可在多种路线中选择：反编译、字节解码、静态 lifting、运行时跟踪或复用目标解码器。单独提高一条路线的代价，不能推导出整体恢复成本的下界；只要另一条可行路线仍便宜，任务就可能完成。</p>

<h2 id="section-3">三、防御理论：四个作用面与各自边界</h2>

<p>这里将 <a href="https://github.com/qybaihe/bla/blob/main/docs/readme/README_zh.md">BLA 的三层防御</a>与本文实现逐项对应。本文仅适配适用于 Native 的机制，不声称完整移植，也不引用 BLA 的跨格式基准作为本实验成绩。</p>

<table>
  <thead>
    <tr>
      <th>BLA 层</th>
      <th>Native 实现与代码位置</th>
      <th>迁移边界</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Exhaustion</td>
      <td><code class="language-plaintext highlighter-rouge">generate_metadata.py</code> 保留 68 条不同 Rust 风格路径，放入合法 <code class="language-plaintext highlighter-rouge">.rodata.bla</code>，研究扫描上限</td>
      <td>没有迁移 JS getter、alias 扩展或 Electron Fuse 哨兵阈值</td>
    </tr>
    <tr>
      <td>Deception</td>
      <td><code class="language-plaintext highlighter-rouge">shadow.c</code> 的强连通调用簇、有界影子报价，与 48 份诱饵记录</td>
      <td>没有接入 SourceMap；元数据不代表真实 TEE 能力</td>
    </tr>
    <tr>
      <td>Runtime Guard</td>
      <td><code class="language-plaintext highlighter-rouge">runtime.c</code> 读取观察信号，<code class="language-plaintext highlighter-rouge">BlaNativeLayers.cpp</code> 插桩真实／影子入口</td>
      <td>使用 Android 观察面，不迁移 Electron Fuse 或 Node Inspector 机制</td>
    </tr>
    <tr>
      <td>本文增加的业务变换</td>
      <td><code class="language-plaintext highlighter-rouge">AnalysisComplexity.cpp</code> 做表达式切片；<code class="language-plaintext highlighter-rouge">BusinessVM.cpp</code> 生成整函数编码 VM</td>
      <td>由本文实验实现，不归为 BLA 原有 LLVM 算法</td>
    </tr>
  </tbody>
</table>

<p>前三项分别影响分析入口、语义归因和运行时路径；最后一项改变正常路径的真实计算表示。下文同时讨论各层的成本来源与可绕过边界。</p>

<h3 id="section-3-1">3.1 预算干扰：影响证据提取的便宜入口</h3>

<p>分析器通常会限制候选数量、递归深度和输出规模。研究这些限制，可以找到特定工具版本的低成本证据入口，并观察某种产物布局是否会让该入口降级。</p>

<p>一个具体例子是本地 REA 6.8.0 的 Rust 扫描模块：它包含 <code class="language-plaintext highlighter-rouge">MAX_RUSTC_HASHES = 64</code>。实验保留了 68 条不同的 Rust 风格构建路径，用来观察该入口的阈值行为。数据放在合法 ELF 只读节中，不靠损坏文件格式制造解析崩溃。</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">from</span> <span class="nn">hashlib</span> <span class="kn">import</span> <span class="n">sha256</span>

<span class="c1"># 研究特定扫描入口的受控元数据；不是业务秘密。
</span><span class="k">for</span> <span class="n">index</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="mi">68</span><span class="p">):</span>
    <span class="n">digest</span> <span class="o">=</span> <span class="n">sha256</span><span class="p">(</span><span class="sa">f</span><span class="s">"omvll-bla-demo-rust-</span><span class="si">{</span><span class="n">index</span><span class="si">}</span><span class="s">"</span><span class="p">.</span><span class="n">encode</span><span class="p">()).</span><span class="n">hexdigest</span><span class="p">()[:</span><span class="mi">40</span><span class="p">]</span>
    <span class="n">path</span> <span class="o">=</span> <span class="sa">f</span><span class="s">"/rustc/</span><span class="si">{</span><span class="n">digest</span><span class="si">}</span><span class="s">/library/core/src/quote_engine.rs"</span>
</code></pre></div></div>

<p>该扫描器先合并只读节与 ELF 只读、非执行载入区的文件范围，再提取、去重 rustc 摘要；已有 64 项时，遇到第 65 个不同摘要即抛出资源上限异常。生成器为每条路径使用不同的 40 字符十六进制摘要，使其满足识别形态且不被简单去重合并；68 条不同路径为超过 64 项的限制提供候选，前提是它们位于该入口实际扫描的范围内。<code class="language-plaintext highlighter-rouge">used</code> 与 ELF 的 retain／链接保留规则负责让这些数据进入最终产物。</p>

<p>这项变换未增加认证公式的代数复杂度，而是争取让特定 provider 提前降级。某个 provider 返回 <code class="language-plaintext highlighter-rouge">limited</code> 后，代理仍可查询导出、读取字节或改用其他分析器。触发阈值与阻止恢复是两个需要分别测量的事件。</p>

<h3 id="section-3-2">3.2 证据干扰与影子业务：增加真假路径的判断成本</h3>

<p>字符串、符号、模块名和调用图，常常是代理建立初始假设的依据。可以构造一组内部自洽、但不代表真实业务的证据：虚构模块记录、诱饵调用簇，以及输入输出形态合理的影子状态机。</p>

<p>在 <a href="/assets/downloads/anti-ai-llvm/source/o-mvll/runtime/bla-native/shadow.c"><code class="language-plaintext highlighter-rouge">shadow.c</code></a> 中，<code class="language-plaintext highlighter-rouge">stage_0…stage_4</code> 的潜在调用边构成强连通簇，在 depth 非零时，按更新后状态的最低位选择两个后继之一：</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>q_i = (s_i XOR K_stage) * 0x9e3779b9  mod 2^32
s_(i+1) = q_i XOR (q_i &gt;&gt; 13)
d_(i+1) = d_i - 1
</code></pre></div></div>

<p>每个 stage 先限制 depth、计算 q；depth 为 0 时返回 q，否则将下一状态与 depth−1 传给后继。入口 depth=4，因而最多经过 5 个 stage，再执行固定 6 步影子报价 switch。这是“静态调用关系复杂、实际执行有界”的结构。48 份诱饵记录则提供模块、接口和策略字段，与这条路径共同构成可解释的假业务。</p>

<p>这些都是本地数据与代码，不表示产物真的使用了 TEE、RSA 或 enclave。强连通结构也不意味着已经触发某个调用图预算；本次没有逐层消融来量化它的独立效果。</p>

<p>诱饵元数据影响的是“这个模块可能做什么”的初始判断；影子状态机则让错误路径确实能够运行，返回形态合理的结果。两者的作用不同：前者增加证据筛选成本，后者增加执行路径识别成本。它们都没有改变正常路径上的业务公式。</p>

<p>这类设计的收益取决于代理是否采信这些证据。一旦代理通过入口分支、正常输入或差分观察识别出真实路径，诱饵就可能被整体排除。后文的实验确实出现了这种绕过，因此不能把诱饵数量当作保护强度。</p>

<h3 id="section-3-3">3.3 运行时守卫：增加观察成本，避免单点依赖</h3>

<p>运行时层可以读取本地环境信号，例如 <code class="language-plaintext highlighter-rouge">TracerPid</code> 和 <code class="language-plaintext highlighter-rouge">/proc/self/maps</code> 中的部分工具特征，再决定是否走影子路径。当前实验在 Android ARM64 上用本地 syscall 读取这些信息，缓存结果，每 64 次调用重新观察一次。</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// 插桩行为示意，具体类型和控制流由 LLVM Pass 生成。</span>
<span class="k">if</span> <span class="p">(</span><span class="n">observer_detected</span><span class="p">())</span>
    <span class="k">return</span> <span class="nf">shadow_business</span><span class="p">(</span><span class="n">user</span><span class="p">,</span> <span class="n">quantity</span><span class="p">,</span> <span class="n">tier</span><span class="p">,</span> <span class="n">token</span><span class="p">);</span>
<span class="k">return</span> <span class="nf">protected_business</span><span class="p">(</span><span class="n">user</span><span class="p">,</span> <span class="n">quantity</span><span class="p">,</span> <span class="n">tier</span><span class="p">,</span> <span class="n">token</span><span class="p">);</span>
</code></pre></div></div>

<p>本地信号不构成信任根。允许仿真和 patch 的攻击者可以替换检测返回值、调整夹具，或者跳过分支。若后面的真实计算仍然很容易孤立求解，入口守卫只能增加一次定位和绕过的工作。</p>

<p>这里还有时序语义：原子计数器负责分配调用序号，<code class="language-plaintext highlighter-rouge">ticket &amp; 63</code> 非零时读取缓存，否则刷新观察。relaxed 原子避免普通共享变量的数据竞争，但不保证所有并发调用都使用同一时刻的新环境快照。某个观测读取失败不会自行置位，但其他观测已发现的标记仍可保留。因此“返回 0”只能解释为当前探针未报告异常。</p>

<p>缓存会降低系统调用频率，同时产生观察窗口；真机上的误报、并发行为和窗口内的漏报都需要另测。本文没有用仿真结果替代这些验证。</p>

<h3 id="section-3-4">3.4 业务语义混淆：保护要进入真实计算</h3>

<p>只增加不可达分支或假函数，未必改变真实算法的恢复难度。保护还需要作用于认证关系、条件判断、常量传播和结果计算本身。</p>

<p>本文对照两类做法：</p>

<table>
  <thead>
    <tr>
      <th>机制</th>
      <th>如何改变真实业务</th>
      <th>主要风险</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>表达式切片</td>
      <td>把比较、算术依赖拆成编码的小函数，再组合结果</td>
      <td>小而纯的函数边界也可能方便逐个恢复</td>
    </tr>
    <tr>
      <td>整函数编码虚拟化</td>
      <td>把业务转换为自定义指令和编码寄存器状态</td>
      <td>通用解释器可能形成集中解码点，或被独立提升</td>
    </tr>
  </tbody>
</table>

<p>编码、控制流平坦化与虚拟化都是表示变换。其安全收益要由攻击成本和恢复结果说明，不能由使用了多少种变换来推定。</p>

<h3 id="section-3-5">3.5 不透明谓词必须尊重机器语义</h3>

<p>例如 <code class="language-plaintext highlighter-rouge">x² mod 8 == 3</code> 在数学上恒假；平方的模 8 剩余只有 0、1、4。采用无符号 i32 回绕时，低三位关系仍然成立，可以用它保留一条正常情况下不会选择的路径。但简单关系也可能被代理直接证明，并不天然构成高强度障碍。</p>

<p>更要小心把整数证明直接搬进 LLVM。若先用 i32 计算 <code class="language-plaintext highlighter-rouge">7*x*x-1</code>，再求有符号余数，乘法已经可能回绕。例如 <code class="language-plaintext highlighter-rouge">x=25,000</code> 时，回绕值为 <code class="language-plaintext highlighter-rouge">80,032,703</code>，对 7 的余数为 2；整数域中“7 的倍数减 1”的证明不能原样套用。</p>

<p>Pass 还必须处理位宽、符号扩展、PHI 的并行语义，以及溢出标志和副作用摘要。语义保持首先是编译器正确性问题，不能用“死分支里不会执行”代替证明。</p>

<h2 id="section-4">四、LLVM 落地：把分层保护接入 Android Native</h2>

<h3 id="section-4-1">4.1 为什么选择 LLVM IR</h3>

<p>LLVM IR 保留了算术、比较、分支、调用与数据依赖，适合在生成机器码之前做结构变换。以 <a href="https://github.com/open-obfuscator/o-mvll">O-MVLL</a> 为基础，可以把已有控制流混淆与自定义 Module Pass 组合，再生成 Android AArch64 ELF。</p>

<p>需要特别区分：本机运行的是 LLVM 21 插件，Android 产物使用 NDK 29 的 sysroot 与 linker。插件能否加载，取决于 Clang/LLVM 的 ABI 兼容性，不能假定任意 NDK Clang 都接受同一个插件二进制。</p>

<p>本文的具体机制、分层实现和源码关系如下：</p>

<table>
  <thead>
    <tr>
      <th>层</th>
      <th>实现</th>
      <th>作用</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>控制流</td>
      <td>O-MVLL flatten 配置</td>
      <td>改变真实函数的分派和基本块关系</td>
    </tr>
    <tr>
      <td>发布证据</td>
      <td>受控元数据生成器</td>
      <td>生成阈值研究数据和诱饵记录</td>
    </tr>
    <tr>
      <td>观察／影子</td>
      <td>入口插桩与 native runtime</td>
      <td>根据观察状态选择真实或合成业务</td>
    </tr>
    <tr>
      <td>数据流</td>
      <td>纯算术表达式切片</td>
      <td>外提并编码真实比较与计算</td>
    </tr>
    <tr>
      <td>整函数表示</td>
      <td>BusinessVM 实验 Pass</td>
      <td>将整个受支持业务函数替换为编码程序与解释器</td>
    </tr>
  </tbody>
</table>

<p>前面三层可以与后面两种业务表示方案分别组合，实验中保持这些外围层相同。下文的“切片分层保护”指 flatten、表达式切片、元数据、影子和守卫的组合；“整函数虚拟化保护”将其中的表达式切片替换为编码 VM。它们是机制名称，不代表某个商业保护产品的全部能力。</p>

<p><a href="/images/posts/android/anti-ai-llvm/02-layered-protection.svg" class="anti-ai-figure"><img src="/images/posts/android/anti-ai-llvm/02-layered-protection.svg" alt="分层保护与真实业务执行路径" /></a></p>

<p><em>图 2：外围证据与影子路径提供干扰，真实业务本身还需要数据流或整函数表示变换。</em></p>

<h3 id="section-4-2">4.2 选择与验证：先确定能转换，再改函数体</h3>

<p>这次保护的业务入口为四参数纯整数函数。整函数方案用显式注解选择目标，避免误处理 JNI 桥接、系统库和解释器自己的 runtime：</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cp">#include</span> <span class="cpf">&lt;stdint.h&gt;</span><span class="cp">
</span>
<span class="n">__attribute__</span><span class="p">((</span><span class="n">annotate</span><span class="p">(</span><span class="s">"omvll.business-vm"</span><span class="p">)))</span>
<span class="n">__attribute__</span><span class="p">((</span><span class="n">annotate</span><span class="p">(</span><span class="s">"omvll.bla.shadow"</span><span class="p">)))</span>
<span class="n">__attribute__</span><span class="p">((</span><span class="n">annotate</span><span class="p">(</span><span class="s">"omvll.bla.runtime"</span><span class="p">)))</span>
<span class="kt">uint32_t</span> <span class="nf">demo_evaluate</span><span class="p">(</span><span class="kt">uint32_t</span> <span class="n">user</span><span class="p">,</span> <span class="kt">uint32_t</span> <span class="n">quantity</span><span class="p">,</span>
                       <span class="kt">uint32_t</span> <span class="n">tier</span><span class="p">,</span> <span class="kt">uint32_t</span> <span class="n">token</span><span class="p">);</span>
</code></pre></div></div>

<p>这三个字符串分别对应整函数转换、影子路径和运行时观察。切片配置将第一个标记换成 <code class="language-plaintext highlighter-rouge">omvll.analysis-complexity</code>；两个业务表示方案不会同时叠在同一个业务函数上。注解由 Clang 写入 <code class="language-plaintext highlighter-rouge">llvm.global.annotations</code>，Module Pass 读取其中的函数引用与字符串，再建立待处理列表。</p>

<p>当前整函数 Pass 只接受 <code class="language-plaintext highlighter-rouge">i32(i32,i32,i32,i32)</code> ABI、最多 128 个基本块，以及 1—32 位整数 SSA/CFG。白名单包括算术、位运算、比较、select、整数转换、PHI、分支与返回；源 IR 中的内存访问、函数调用、浮点、宽整数和 switch 均拒绝。</p>

<p>这里的限制发生在<strong>源业务 IR</strong>。Pass 随后生成的解释器本身会使用栈内存和 switch，两者并不矛盾。输入需要在优化后满足白名单，例如可被内联和消去的源码辅助调用，不一定在最终输入 IR 中仍然存在。</p>

<p><a href="/assets/downloads/anti-ai-llvm/source/passes/business-vm/BusinessVM.cpp"><code class="language-plaintext highlighter-rouge">BusinessVM.cpp</code></a> 的入口可以按下面的顺序阅读。代码仅整理排版、略去 seed 更新与已处理标记判断，保留“全部验证后再变换”的关键控制流：</p>

<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// fs 已由注解筛选；seed 为本次构建种子。</span>
<span class="k">for</span> <span class="p">(</span><span class="n">Function</span> <span class="o">*</span><span class="n">F</span> <span class="o">:</span> <span class="n">fs</span><span class="p">)</span> <span class="p">{</span>
    <span class="n">Lower</span> <span class="n">Probe</span><span class="p">(</span><span class="o">*</span><span class="n">F</span><span class="p">,</span> <span class="n">seed</span><span class="p">);</span>
    <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="n">Probe</span><span class="p">.</span><span class="n">validate</span><span class="p">())</span>
        <span class="n">report_fatal_error</span><span class="p">(</span><span class="s">"business-vm: unsupported selected function; "</span>
                           <span class="s">"refusing plaintext fallback"</span><span class="p">);</span>
<span class="p">}</span>
<span class="k">for</span> <span class="p">(</span><span class="n">Function</span> <span class="o">*</span><span class="n">F</span> <span class="o">:</span> <span class="n">fs</span><span class="p">)</span> <span class="p">{</span>
    <span class="n">Lower</span> <span class="n">L</span><span class="p">(</span><span class="o">*</span><span class="n">F</span><span class="p">,</span> <span class="n">seed</span><span class="p">);</span>
    <span class="n">L</span><span class="p">.</span><span class="n">compile</span><span class="p">();</span>      <span class="c1">// SSA/CFG → 虚拟寄存器与指令记录</span>
    <span class="n">L</span><span class="p">.</span><span class="n">materialize</span><span class="p">();</span>  <span class="c1">// 编码程序 + 新解释器函数体</span>
<span class="p">}</span>
</code></pre></div></div>

<p>先验证整个选择集，是为了避免处理到一半才发现不支持，却留下部分函数仍为明文。这个行为是整函数 Pass 的约束。表达式切片采用有上限的候选选择，未选中的表达式可能保留，不能给它同样的“整个函数已转换”保证。</p>

<p><strong>贯穿示例：从认证判断到折扣计算。</strong></p>

<p>后面统一使用下面这个公开教学函数。它与实测样例具有“认证后计算报价”的相似结构，但常量和算法特意简化，<strong>不是隐藏测试目标，也不作为新的防护成绩</strong>。所有计算使用 <code class="language-plaintext highlighter-rouge">uint32_t</code> 回绕；这个认证关系仅用来说明转换，不是安全的认证设计。</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">uint32_t</span> <span class="nf">quote_demo</span><span class="p">(</span><span class="kt">uint32_t</span> <span class="n">user</span><span class="p">,</span> <span class="kt">uint32_t</span> <span class="n">quantity</span><span class="p">,</span>
                    <span class="kt">uint32_t</span> <span class="n">tier</span><span class="p">,</span> <span class="kt">uint32_t</span> <span class="n">token</span><span class="p">)</span> <span class="p">{</span>
    <span class="kt">uint32_t</span> <span class="n">expected</span> <span class="o">=</span> <span class="p">(</span><span class="n">user</span> <span class="o">*</span> <span class="mi">17u</span> <span class="o">+</span> <span class="mi">9u</span><span class="p">)</span> <span class="o">^</span> <span class="mh">0x13579bdfu</span><span class="p">;</span> <span class="c1">// E1</span>
    <span class="k">if</span> <span class="p">(</span><span class="n">token</span> <span class="o">!=</span> <span class="n">expected</span><span class="p">)</span> <span class="k">return</span> <span class="mi">0</span><span class="p">;</span>                    <span class="c1">// E2</span>
    <span class="kt">uint32_t</span> <span class="n">gross</span> <span class="o">=</span> <span class="n">quantity</span> <span class="o">*</span> <span class="p">(</span><span class="mi">100u</span> <span class="o">+</span> <span class="n">tier</span> <span class="o">*</span> <span class="mi">5u</span><span class="p">);</span>      <span class="c1">// E3</span>
    <span class="kt">uint32_t</span> <span class="n">discount</span> <span class="o">=</span> <span class="n">quantity</span> <span class="o">&gt;=</span> <span class="mi">10u</span> <span class="o">?</span> <span class="n">gross</span> <span class="o">/</span> <span class="mi">10u</span> <span class="o">:</span> <span class="mi">0u</span><span class="p">;</span>
    <span class="k">return</span> <span class="n">gross</span> <span class="o">-</span> <span class="n">discount</span><span class="p">;</span>                           <span class="c1">// E4</span>
<span class="p">}</span>
</code></pre></div></div>

<p>E1 生成认证关系，E2 决定是否进入真实报价，E3 计算原价，E4 计算折扣并返回。四个位置也标在图 3 中：外围观察控制能否进入真实函数，切片与整函数 VM 则是两种替代的业务表示。</p>

<p><a href="/images/posts/android/anti-ai-llvm/09-quote-walkthrough.svg" class="anti-ai-figure"><img src="/images/posts/android/anti-ai-llvm/09-quote-walkthrough.svg" alt="公开报价函数的贯穿转换示例" /></a></p>

<p><em>图 3：同一段 E1—E4 业务经过 C、LLVM IR 和两种保护表示；最终都应保持公开示例的输入输出。</em></p>

<p>用 LLVM 21 的 <code class="language-plaintext highlighter-rouge">clang -O1 -S -emit-llvm</code> 编译后，得到下面的实际指令结构。为便于对照，仅重命名 SSA 值和基本块、略去属性；完整文件见 <a href="/assets/downloads/anti-ai-llvm/source/examples/quote-walkthrough/quote.c">quote.c</a> 与 <a href="/assets/downloads/anti-ai-llvm/source/examples/quote-walkthrough/quote.ll">quote.ll</a>。</p>

<div class="language-llvm highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">define</span> <span class="kt">i32</span> <span class="vg">@quote_demo</span><span class="p">(</span><span class="kt">i32</span> <span class="nv">%user</span><span class="p">,</span> <span class="kt">i32</span> <span class="nv">%quantity</span><span class="p">,</span> <span class="kt">i32</span> <span class="nv">%tier</span><span class="p">,</span> <span class="kt">i32</span> <span class="nv">%token</span><span class="p">)</span> <span class="p">{</span>
<span class="nl">entry:</span>
  <span class="nv">%scaled</span> <span class="p">=</span> <span class="k">mul</span> <span class="kt">i32</span> <span class="nv">%user</span><span class="p">,</span> <span class="m">17</span>
  <span class="nv">%sum</span> <span class="p">=</span> <span class="k">add</span> <span class="kt">i32</span> <span class="nv">%scaled</span><span class="p">,</span> <span class="m">9</span>
  <span class="nv">%mixed</span> <span class="p">=</span> <span class="p">x</span><span class="k">or</span> <span class="kt">i32</span> <span class="nv">%token</span><span class="p">,</span> <span class="nv">%sum</span>
  <span class="nv">%ok</span> <span class="p">=</span> <span class="k">icmp</span> <span class="k">eq</span> <span class="kt">i32</span> <span class="nv">%mixed</span><span class="p">,</span> <span class="m">324508639</span> <span class="c1">; E1/E2: 0x13579bdf</span>
  <span class="k">br</span> <span class="kt">i1</span> <span class="nv">%ok</span><span class="p">,</span> <span class="kt">label</span> <span class="nv">%price</span><span class="p">,</span> <span class="kt">label</span> <span class="nv">%exit</span>
<span class="nl">price:</span>
  <span class="nv">%level</span> <span class="p">=</span> <span class="k">mul</span> <span class="kt">i32</span> <span class="nv">%tier</span><span class="p">,</span> <span class="m">5</span>
  <span class="nv">%unit</span> <span class="p">=</span> <span class="k">add</span> <span class="kt">i32</span> <span class="nv">%level</span><span class="p">,</span> <span class="m">100</span>
  <span class="nv">%gross</span> <span class="p">=</span> <span class="k">mul</span> <span class="kt">i32</span> <span class="nv">%unit</span><span class="p">,</span> <span class="nv">%quantity</span>   <span class="c1">; E3</span>
  <span class="nv">%bulk</span> <span class="p">=</span> <span class="k">icmp</span> <span class="k">ugt</span> <span class="kt">i32</span> <span class="nv">%quantity</span><span class="p">,</span> <span class="m">9</span>
  <span class="nv">%tenth</span> <span class="p">=</span> <span class="k">udiv</span> <span class="kt">i32</span> <span class="nv">%gross</span><span class="p">,</span> <span class="m">10</span>
  <span class="nv">%discount</span> <span class="p">=</span> <span class="k">select</span> <span class="kt">i1</span> <span class="nv">%bulk</span><span class="p">,</span> <span class="kt">i32</span> <span class="nv">%tenth</span><span class="p">,</span> <span class="kt">i32</span> <span class="m">0</span>
  <span class="nv">%net</span> <span class="p">=</span> <span class="k">sub</span> <span class="kt">i32</span> <span class="nv">%gross</span><span class="p">,</span> <span class="nv">%discount</span>   <span class="c1">; E4</span>
  <span class="k">br</span> <span class="kt">label</span> <span class="nv">%exit</span>
<span class="nl">exit:</span>
  <span class="nv">%result</span> <span class="p">=</span> <span class="k">phi</span> <span class="kt">i32</span> <span class="p">[</span> <span class="nv">%net</span><span class="p">,</span> <span class="nv">%price</span> <span class="p">],</span> <span class="p">[</span> <span class="m">0</span><span class="p">,</span> <span class="nv">%entry</span> <span class="p">]</span>
  <span class="k">ret</span> <span class="kt">i32</span> <span class="nv">%result</span>
<span class="p">}</span>
</code></pre></div></div>

<p>这里已经有一次值得注意的变化：编译器将 <code class="language-plaintext highlighter-rouge">token == (sum XOR K)</code> 改成了 <code class="language-plaintext highlighter-rouge">(token XOR sum) == K</code>，并将数量判断改成 <code class="language-plaintext highlighter-rouge">ugt 9</code>。Pass 面对的是优化后的 IR，不能依赖源码表达式的固定写法。</p>

<p>例如 <code class="language-plaintext highlighter-rouge">user=7, quantity=12, tier=2, token=0x13579b5f</code> 时，认证通过，<code class="language-plaintext highlighter-rouge">unit=110</code>、<code class="language-plaintext highlighter-rouge">gross=1320</code>、<code class="language-plaintext highlighter-rouge">discount=132</code>，返回 <code class="language-plaintext highlighter-rouge">1188</code>；仅把 token 改成 <code class="language-plaintext highlighter-rouge">0</code>，返回 <code class="language-plaintext highlighter-rouge">0</code>。这组公开数值贯穿后面的指令执行说明。</p>

<h3 id="section-4-3">4.3 表达式切片：从真实依赖图中外提计算</h3>

<p><strong>① 找根节点，再向操作数回溯。</strong></p>

<p>切片优先选择控制业务判断的 <code class="language-plaintext highlighter-rouge">icmp</code>，随后处理结果跨到不受支持操作或其他基本块的表达式边界。<code class="language-plaintext highlighter-rouge">Slice::collect()</code> 只在同一基本块里递归收集受支持的整数运算，先访问操作数、再把当前指令放入列表，因此克隆时依赖已经建立。</p>

<p>遇到参数、常量、其他基本块的值，或不适合搬移的指令，就把它当作叶子输入。load、store、call、PHI 都不随切片迁移。原型限制每函数最多 16 个切片位置、模块最多 128 个 helper，单片最多 24 个节点、32 个叶子，递归收集深度小于 16，以控制代码膨胀。</p>

<p><strong>② 在调用边界编码，在 helper 内恢复原类型。</strong></p>

<p>沿用 E1/E2。以 <code class="language-plaintext highlighter-rouge">%ok</code> 为根，切片会收集同一入口块里的乘、加、异或和比较；<code class="language-plaintext highlighter-rouge">user</code>、<code class="language-plaintext highlighter-rouge">token</code> 与常量成为叶子。转换后的意义可用下面的教学 C 表示。为突出调用边界，这里只画两个变量叶子；当前实现也为收集到的常量叶子建立编码参数，不能据此推断 helper 的固定 ABI。</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// 示意，不是原 Pass 的逐字输出；掩码为公开教学值。</span>
<span class="k">static</span> <span class="kt">uint32_t</span> <span class="nf">auth_slice</span><span class="p">(</span><span class="kt">uint32_t</span> <span class="n">eu</span><span class="p">,</span> <span class="kt">uint32_t</span> <span class="n">et</span><span class="p">)</span> <span class="p">{</span>
    <span class="kt">uint32_t</span> <span class="n">u</span> <span class="o">=</span> <span class="n">eu</span> <span class="o">^</span> <span class="mh">0x11111111u</span><span class="p">;</span>
    <span class="kt">uint32_t</span> <span class="n">t</span> <span class="o">=</span> <span class="n">et</span> <span class="o">^</span> <span class="mh">0x22222223u</span><span class="p">;</span>
    <span class="kt">uint32_t</span> <span class="n">ok</span> <span class="o">=</span> <span class="p">((</span><span class="n">t</span> <span class="o">^</span> <span class="p">(</span><span class="n">u</span> <span class="o">*</span> <span class="mi">17u</span> <span class="o">+</span> <span class="mi">9u</span><span class="p">))</span> <span class="o">==</span> <span class="mh">0x13579bdfu</span><span class="p">);</span>
    <span class="k">return</span> <span class="n">ok</span> <span class="o">^</span> <span class="mh">0x33333333u</span><span class="p">;</span>
<span class="p">}</span>
<span class="kt">uint32_t</span> <span class="n">ok</span> <span class="o">=</span> <span class="n">auth_slice</span><span class="p">(</span><span class="n">user</span> <span class="o">^</span> <span class="mh">0x11111111u</span><span class="p">,</span> <span class="n">token</span> <span class="o">^</span> <span class="mh">0x22222223u</span><span class="p">)</span>
              <span class="o">^</span> <span class="mh">0x33333333u</span><span class="p">;</span>
<span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="n">ok</span><span class="p">)</span> <span class="k">return</span> <span class="mi">0</span><span class="p">;</span> <span class="c1">// E2；通过后仍进入 E3/E4 的报价块</span>
</code></pre></div></div>

<p>原 <code class="language-plaintext highlighter-rouge">%ok</code> 是 i1；调用边界使用 i32，返回解码后必须截回 i1，再供原 <code class="language-plaintext highlighter-rouge">br</code> 使用。同理，E3 的乘法和 E4 的结果边界可以形成其他切片，但跨块的返回 PHI 不会被搬进这个 helper。</p>

<p><a href="/images/posts/android/anti-ai-llvm/07-expression-slice.svg" class="anti-ai-figure"><img src="/images/posts/android/anti-ai-llvm/07-expression-slice.svg" alt="表达式切片的依赖收集与替换流程" /></a></p>

<p><em>图 4：A 收集依赖，B 建立编码边界并克隆原运算，C 替换原使用点；旁边的真实业务分支继续使用恢复后的结果。</em></p>

<p>在实际 LLVM 实现中，输入统一转为 i32，解码后再截回叶子的原位宽。下面三个步骤对应图 4 的 A、B、C；摘取自 <a href="/assets/downloads/anti-ai-llvm/source/o-mvll/src/passes/analysis-complexity/AnalysisComplexity.cpp"><code class="language-plaintext highlighter-rouge">outlineSlice()</code></a>，省略函数创建与循环上下文：</p>

<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// A：收集同块纯整数依赖；Nodes 按依赖顺序保存。</span>
<span class="n">S</span><span class="p">.</span><span class="n">collect</span><span class="p">(</span><span class="n">Root</span><span class="p">);</span>

<span class="c1">// B：调用方编码；helper 解码并恢复叶子原类型。</span>
<span class="n">Args</span><span class="p">.</span><span class="n">push_back</span><span class="p">(</span><span class="n">CB</span><span class="p">.</span><span class="n">CreateXor</span><span class="p">(</span><span class="n">CB</span><span class="p">.</span><span class="n">CreateZExtOrTrunc</span><span class="p">(</span><span class="n">Leaf</span><span class="p">,</span> <span class="n">I32</span><span class="p">),</span>
                           <span class="n">CB</span><span class="p">.</span><span class="n">getInt32</span><span class="p">(</span><span class="n">Key</span><span class="p">)));</span>
<span class="n">Value</span> <span class="o">*</span><span class="n">Decoded</span> <span class="o">=</span> <span class="n">HB</span><span class="p">.</span><span class="n">CreateXor</span><span class="p">(</span><span class="n">H</span><span class="o">-&gt;</span><span class="n">getArg</span><span class="p">(</span><span class="n">I</span><span class="p">),</span> <span class="n">HB</span><span class="p">.</span><span class="n">getInt32</span><span class="p">(</span><span class="n">Key</span><span class="p">));</span>
<span class="n">Values</span><span class="p">[</span><span class="n">Leaf</span><span class="p">]</span> <span class="o">=</span> <span class="n">HB</span><span class="p">.</span><span class="n">CreateTruncOrBitCast</span><span class="p">(</span><span class="n">Decoded</span><span class="p">,</span> <span class="n">Leaf</span><span class="o">-&gt;</span><span class="n">getType</span><span class="p">());</span>

<span class="c1">// 对每个 Nodes 中的指令 clone()，用 Values 映射重接操作数。</span>
<span class="c1">// clone 保留原指令的运算和相关标志。</span>
<span class="n">Value</span> <span class="o">*</span><span class="n">Result</span> <span class="o">=</span> <span class="n">HB</span><span class="p">.</span><span class="n">CreateZExtOrTrunc</span><span class="p">(</span><span class="n">Values</span><span class="p">.</span><span class="n">lookup</span><span class="p">(</span><span class="n">Root</span><span class="p">),</span> <span class="n">I32</span><span class="p">);</span>
<span class="n">HB</span><span class="p">.</span><span class="n">CreateRet</span><span class="p">(</span><span class="n">HB</span><span class="p">.</span><span class="n">CreateXor</span><span class="p">(</span><span class="n">Result</span><span class="p">,</span> <span class="n">HB</span><span class="p">.</span><span class="n">getInt32</span><span class="p">(</span><span class="n">OutKey</span><span class="p">)));</span>

<span class="c1">// C：替换原表达式的使用；只删除已无使用者的旧计算。</span>
<span class="n">Value</span> <span class="o">*</span><span class="n">Call</span> <span class="o">=</span> <span class="n">CB</span><span class="p">.</span><span class="n">CreateCall</span><span class="p">(</span><span class="n">H</span><span class="p">,</span> <span class="n">Args</span><span class="p">);</span>
<span class="n">Value</span> <span class="o">*</span><span class="n">Plain</span> <span class="o">=</span> <span class="n">CB</span><span class="p">.</span><span class="n">CreateXor</span><span class="p">(</span><span class="n">Call</span><span class="p">,</span> <span class="n">CB</span><span class="p">.</span><span class="n">getInt32</span><span class="p">(</span><span class="n">OutKey</span><span class="p">));</span>
<span class="n">Value</span> <span class="o">*</span><span class="n">Replacement</span> <span class="o">=</span> <span class="n">CB</span><span class="p">.</span><span class="n">CreateTruncOrBitCast</span><span class="p">(</span><span class="n">Plain</span><span class="p">,</span> <span class="n">Root</span><span class="o">-&gt;</span><span class="n">getType</span><span class="p">());</span>
<span class="n">Root</span><span class="o">-&gt;</span><span class="n">replaceAllUsesWith</span><span class="p">(</span><span class="n">Replacement</span><span class="p">);</span>
<span class="n">RecursivelyDeleteTriviallyDeadInstructions</span><span class="p">(</span><span class="n">Root</span><span class="p">);</span>
</code></pre></div></div>

<p><strong>③ 切片替换的不变量。</strong></p>

<p>令原根表达式为 <code class="language-plaintext highlighter-rouge">r=f(l0,…,ln)</code>，叶子位宽为 wi、结果位宽为 wr。实现将每个叶子的原位模式零扩展到 i32，再做异或编码；helper 解码后截回 wi。返回端同样先扩展、编码，再由调用方解码并截回 wr：</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>输入：arg_i = zext32(l_i) XOR k_i
恢复：l'_i  = trunc_wi(arg_i XOR k_i) = l_i
结果：r'    = trunc_wr((zext32(f(l'_0,…,l'_n)) XOR k_out) XOR k_out)
            = f(l_0,…,l_n)
</code></pre></div></div>

<p>这个关系依赖于三项实现条件：克隆保留原运算及其适用语义；叶子在新调用位置可用；被搬移的节点没有内存或其他副作用。同块回溯与白名单用于缩小需要证明的范围。边界上的 load 或 PHI 作为已计算的叶子传入，并不等于把它们重新执行到 helper 内。</p>

<p><code class="language-plaintext highlighter-rouge">replaceAllUsesWith()</code> 只负责重接使用点；<code class="language-plaintext highlighter-rouge">RecursivelyDeleteTriviallyDeadInstructions()</code> 才清理无用旧表达式。共享子表达式仍被其他使用者需要时不能删除。因此需要核对最终 IR 的使用关系，不能把新增 helper 数量当作覆盖率。</p>

<p><strong>④ 保留边界，并核对它是否值得保留。</strong></p>

<p>helper 设置 <code class="language-plaintext highlighter-rouge">noinline</code> 与 <code class="language-plaintext highlighter-rouge">optnone</code>，保留调用边界并限制对 helper 函数体的常规优化；它只计算纯表达式，因此标记 <code class="language-plaintext highlighter-rouge">memory(none)</code>。这些属性不会阻止外部分析器做代数消元，也不保证最终每条指令与未优化 IR 一一对应。</p>

<p>这种转换把业务关系拆到多个调用里，增加跨函数重组步骤。但固定 XOR 满足 <code class="language-plaintext highlighter-rouge">(x XOR k) XOR k = x</code>，helper 又没有外部状态，静态展开可以消去掩码，动态观察可以直接记录参数与返回值。它改变了数据流的组织方式，同时也产生了可单独求解的小接口。失守复盘里会看到这条边界如何被利用。</p>

<h3 id="section-4-4">4.4 入口观察与影子分支：插桩后必须更新属性</h3>

<p>图 2 的“入口观察与影子选择”由 <a href="/assets/downloads/anti-ai-llvm/source/o-mvll/src/passes/bla-native-layers/BlaNativeLayers.cpp"><code class="language-plaintext highlighter-rouge">BlaNativeLayers.cpp</code></a> 完成。它在原入口前增加 <code class="language-plaintext highlighter-rouge">bla.gate</code>，保留原业务入口，再新增 <code class="language-plaintext highlighter-rouge">bla.synthetic</code>：</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>bla.gate
  hit = ((user * user) &amp; 7) == 3
  hit |= runtime_probe() != 0
  ├─ hit：bla.synthetic → shadow_eval(原四参数) → return
  └─ 否：原业务入口 → 切片业务或编码 VM → return
</code></pre></div></div>

<p>对应的 IRBuilder 核心操作如下。省略注解位判断与基本块创建，变量名与实现一致：</p>

<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">Value</span> <span class="o">*</span><span class="n">Square</span> <span class="o">=</span> <span class="n">B</span><span class="p">.</span><span class="n">CreateMul</span><span class="p">(</span><span class="n">F</span><span class="o">-&gt;</span><span class="n">getArg</span><span class="p">(</span><span class="mi">0</span><span class="p">),</span> <span class="n">F</span><span class="o">-&gt;</span><span class="n">getArg</span><span class="p">(</span><span class="mi">0</span><span class="p">));</span>
<span class="n">Value</span> <span class="o">*</span><span class="n">Hit</span> <span class="o">=</span> <span class="n">B</span><span class="p">.</span><span class="n">CreateICmpEQ</span><span class="p">(</span><span class="n">B</span><span class="p">.</span><span class="n">CreateAnd</span><span class="p">(</span><span class="n">Square</span><span class="p">,</span> <span class="n">B</span><span class="p">.</span><span class="n">getInt32</span><span class="p">(</span><span class="mi">7</span><span class="p">)),</span>
                          <span class="n">B</span><span class="p">.</span><span class="n">getInt32</span><span class="p">(</span><span class="mi">3</span><span class="p">));</span>
<span class="n">Value</span> <span class="o">*</span><span class="n">Reason</span> <span class="o">=</span> <span class="n">B</span><span class="p">.</span><span class="n">CreateCall</span><span class="p">(</span><span class="n">Runtime</span><span class="p">);</span>
<span class="n">Hit</span> <span class="o">=</span> <span class="n">B</span><span class="p">.</span><span class="n">CreateOr</span><span class="p">(</span><span class="n">Hit</span><span class="p">,</span> <span class="n">B</span><span class="p">.</span><span class="n">CreateICmpNE</span><span class="p">(</span><span class="n">Reason</span><span class="p">,</span> <span class="n">B</span><span class="p">.</span><span class="n">getInt32</span><span class="p">(</span><span class="mi">0</span><span class="p">)));</span>
<span class="n">B</span><span class="p">.</span><span class="n">CreateCondBr</span><span class="p">(</span><span class="n">Hit</span><span class="p">,</span> <span class="n">Synthetic</span><span class="p">,</span> <span class="n">Original</span><span class="p">);</span>
</code></pre></div></div>

<p>第一个谓词基于无符号回绕后仍成立的低三位关系，正常不会命中；真正可能改变执行路径的是 runtime 返回的观察结果。<code class="language-plaintext highlighter-rouge">runtime.c</code> 用位标记区分非零 <code class="language-plaintext highlighter-rouge">TracerPid</code> 与特定 maps 字符串，首次观察后每 64 次调用刷新一次，其他调用读取缓存。读取失败不会自行置异常位；若其他信号已命中，仍可走影子路径。缓存时序见第 3.3 节。</p>

<p>更容易漏掉的是<strong>插桩后的副作用摘要</strong>。原业务可能被优化器推断为 <code class="language-plaintext highlighter-rouge">memory(none)</code>、<code class="language-plaintext highlighter-rouge">willreturn</code>，调用者还可能保存返回值范围。加入读取 <code class="language-plaintext highlighter-rouge">/proc</code> 的调用和另一种返回路径后，这些结论需要重新审视。实现保守移除了相关函数、调用点上的 memory、返回 range 等推断，并清理 speculatable、nosync、nofree、willreturn 等属性。</p>

<p>如果只插入 <code class="language-plaintext highlighter-rouge">call</code> 而保留旧摘要，后续优化就可能基于过期前提重排或简化程序。这里要解决的是编译正确性，不能靠“反编译看起来有检测”判断完成。</p>

<h3 id="section-4-5">4.5 整函数虚拟化：用一套指令承载完整业务</h3>

<p>切片保留了小而纯的业务接口。整函数方案尝试移除这个接口：将输入校验、认证、价格与折扣全部降低成同一套虚拟程序，保持外部 ABI，删除原计算体，再生成解释器。</p>

<p><a href="/images/posts/android/anti-ai-llvm/03-business-vm.svg" class="anti-ai-figure"><img src="/images/posts/android/anti-ai-llvm/03-business-vm.svg" alt="整函数编码虚拟化的构建与执行架构" /></a></p>

<p><em>图 5：上半部分为构建期的 <code class="language-plaintext highlighter-rouge">validate → compile → materialize</code>，下半部分为生成函数的执行循环；字段和操作数的明文在分派块中出现。</em></p>

<p><strong>① 从 SSA 值到虚拟寄存器。</strong></p>

<p>LLVM 的 SSA 表示中，每个指令结果只定义一次；CFG 描述基本块之间的控制转移。<code class="language-plaintext highlighter-rouge">Lower::compile()</code> 为参数、指令结果和整数常量建立寄存器编号。常量用 <code class="language-plaintext highlighter-rouge">MOVI</code> 指令物化；算术生成对应 opcode，比较保存有符号／无符号谓词，分支转为虚拟跳转。构造 <code class="language-plaintext highlighter-rouge">Lower</code> 时生成随机 opcode 编号；<code class="language-plaintext highlighter-rouge">compile()</code> 随机化基本块／边程序布局，完成指令与后继解析后再置换完整寄存器空间。</p>

<p>一条指令固定包含九个 32 位字段，字段位置与源码 <code class="language-plaintext highlighter-rouge">Ins::f</code> 一致：</p>

<table>
  <thead>
    <tr>
      <th>字段</th>
      <th>含义</th>
      <th>例子</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">f[0]</code></td>
      <td>opcode</td>
      <td>ADD、CMP、BR、RET 的构建期随机编号</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">f[1]</code></td>
      <td>目标寄存器</td>
      <td>算术结果的写回位置</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">f[2..4]</code></td>
      <td>三个源寄存器</td>
      <td>算术输入、select 条件与两个候选值</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">f[5]</code></td>
      <td>立即数或附加信息</td>
      <td>常量、比较谓词、转换的源位宽</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">f[6]</code></td>
      <td>主要运算位宽</td>
      <td>算术结果位宽；CMP 使用被比较操作数位宽</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">f[7..8]</code></td>
      <td>下一 PC／备用 PC</td>
      <td>顺序执行、条件分支后继</td>
    </tr>
  </tbody>
</table>

<p>不是每种 opcode 都使用全部字段。固定布局便于构建，也意味着格式一旦恢复，就能批量读取整份程序。</p>

<p>沿用 E3 的 <code class="language-plaintext highlighter-rouge">%gross = mul i32 %unit, %quantity</code>。在取消编号随机化的教学记法下，它对应：</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>p: [ OP_MUL, rGross, rUnit, rQuantity, rUnused, 0, 32, p+1, 0 ]
     f[0]    f[1]    f[2]   f[3]       f[4]    f[5..8]
</code></pre></div></div>

<p>这是一条<strong>字段对应示意</strong>，不是实际产物的 opcode 或寄存器编号。真实构建会重新编号，<code class="language-plaintext highlighter-rouge">p+1</code> 表示本例顺序后继。九字段里的常量字段也会编码；<code class="language-plaintext highlighter-rouge">unit</code> 的来源是前面的 MOVI、MUL 与 ADD，MUL 的源字段保存寄存器编号而非立即数。</p>

<p><strong>② PHI 要拆成边程序，且必须并行复制。</strong></p>

<p>PHI 的输入取决于来自哪个前驱块，所以它不是普通的顺序赋值。假设循环某条回边要交换两个值：</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>原语义：   a_next = b_old；b_next = a_old
错误复制： a = b；b = a          # 第二步读到的已是新 a
正确复制： t0 = b；t1 = a；a = t0；b = t1
</code></pre></div></div>

<p>实现为每条前驱→后继边生成程序，先把所有 incoming 值读到新临时寄存器，再写入所有 PHI 目标。下面是源码中的两阶段复制，排版略作展开：</p>

<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">std</span><span class="o">::</span><span class="n">vector</span><span class="o">&lt;</span><span class="n">std</span><span class="o">::</span><span class="n">pair</span><span class="o">&lt;</span><span class="n">PHINode</span> <span class="o">*</span><span class="p">,</span> <span class="kt">unsigned</span><span class="o">&gt;&gt;</span> <span class="n">copies</span><span class="p">;</span>
<span class="k">for</span> <span class="p">(</span><span class="k">auto</span> <span class="o">&amp;</span><span class="n">I</span> <span class="o">:</span> <span class="o">*</span><span class="n">to</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">auto</span> <span class="o">*</span><span class="n">P</span> <span class="o">=</span> <span class="n">dyn_cast</span><span class="o">&lt;</span><span class="n">PHINode</span><span class="o">&gt;</span><span class="p">(</span><span class="o">&amp;</span><span class="n">I</span><span class="p">);</span>
    <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="n">P</span><span class="p">)</span> <span class="k">break</span><span class="p">;</span>
    <span class="kt">unsigned</span> <span class="n">temp</span> <span class="o">=</span> <span class="n">regs</span><span class="o">++</span><span class="p">;</span>
    <span class="n">emit</span><span class="p">(</span><span class="n">MOV</span><span class="p">,</span> <span class="n">temp</span><span class="p">,</span> <span class="n">reg</span><span class="p">(</span><span class="n">P</span><span class="o">-&gt;</span><span class="n">getIncomingValueForBlock</span><span class="p">(</span><span class="n">from</span><span class="p">)),</span>
         <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="n">bits</span><span class="p">(</span><span class="n">P</span><span class="o">-&gt;</span><span class="n">getType</span><span class="p">()));</span>
    <span class="n">copies</span><span class="p">.</span><span class="n">emplace_back</span><span class="p">(</span><span class="n">P</span><span class="p">,</span> <span class="n">temp</span><span class="p">);</span>
<span class="p">}</span>
<span class="k">for</span> <span class="p">(</span><span class="k">auto</span> <span class="p">[</span><span class="n">P</span><span class="p">,</span> <span class="n">temp</span><span class="p">]</span> <span class="o">:</span> <span class="n">copies</span><span class="p">)</span>
    <span class="n">emit</span><span class="p">(</span><span class="n">MOV</span><span class="p">,</span> <span class="n">reg</span><span class="p">(</span><span class="n">P</span><span class="p">),</span> <span class="n">temp</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="n">bits</span><span class="p">(</span><span class="n">P</span><span class="o">-&gt;</span><span class="n">getType</span><span class="p">()));</span>
<span class="n">emit</span><span class="p">(</span><span class="n">JMP</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">32</span><span class="p">,</span> <span class="n">labels</span><span class="p">[</span><span class="n">to</span><span class="p">]);</span>
</code></pre></div></div>

<p><a href="/images/posts/android/anti-ai-llvm/08-vm-lowering.svg" class="anti-ai-figure"><img src="/images/posts/android/anti-ai-llvm/08-vm-lowering.svg" alt="SSA 与 PHI 降低到编码解释器的流程" /></a></p>

<p><em>图 6：A 保留源语义，B 建立编码程序，C 生成执行循环。PHI 临时寄存器也纳入后续编号随机化。</em></p>

<p><strong>③ 程序编码与状态编码分开处理。</strong></p>

<p>程序的每个字段按构建 seed、虚拟 PC 和字段序号异或编码：</p>

<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// materialize() 中的程序字段编码；p 为虚拟指令序号。</span>
<span class="n">code</span><span class="p">[</span><span class="n">p</span><span class="p">].</span><span class="n">f</span><span class="p">[</span><span class="n">k</span><span class="p">]</span> <span class="o">^</span> <span class="n">mix</span><span class="p">(</span><span class="n">seed</span> <span class="o">^</span> <span class="p">(</span><span class="n">p</span> <span class="o">*</span> <span class="mh">0x9e3779b9u</span><span class="p">)</span> <span class="o">^</span> <span class="p">(</span><span class="n">k</span> <span class="o">*</span> <span class="mh">0x85ebca6bu</span><span class="p">))</span>
</code></pre></div></div>

<p>它改变发布产物中的指令表示。寄存器帧则采用另一套按寄存器和本次调用变化的可逆编码；以下公式对应源码 <code class="language-plaintext highlighter-rouge">encode</code>／<code class="language-plaintext highlighter-rouge">decode</code>，全部算术在 i32 下进行：</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>m       = mix(register_id XOR seed_constant) | 1
add     = register_id * 0x85ebca6b + session
key     = (register_id * 0xc2b2ae35) XOR session
encoded = ((value XOR key) * m + add) mod 2^32
value   = ((encoded - add) * inverse(m)) XOR key
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">session</code> 由第一个、第四个参数与构建常量混合得来。两种编码分别作用于程序记录与跨指令保存的值；帧编码中的 session 不参与程序字段的掩码派生。</p>

<p>为什么能够精确逆变换？在环 <code class="language-plaintext highlighter-rouge">Z/(2^32)</code> 中，奇数 m 与 <code class="language-plaintext highlighter-rouge">2^32</code> 互素，因此存在唯一乘法逆元。实现以 <code class="language-plaintext highlighter-rouge">inv=m</code> 起步，重复五次 <code class="language-plaintext highlighter-rouge">inv = inv*(2-m*inv)</code>。若当前 <code class="language-plaintext highlighter-rouge">m*inv = 1 mod 2^k</code>，下一次误差满足：</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>1 - m*inv_next = (1 - m*inv)^2
</code></pre></div></div>

<p>每次迭代将已正确的低位数至少加倍；所有中间计算按 i32 回绕。加法、乘法与 XOR 分别被逆序消去，得到 <code class="language-plaintext highlighter-rouge">decode_r(encode_r(v))=v</code>。这里证明的是表示可逆，恰恰不是保密性。</p>

<p>该 session 在一次调用内保持不变，既没有随指令推进，也没有认证指令序列。对相同 PC、字段 k，程序字段的解码掩码固定；对相同寄存器、同一次调用，帧编码关系也固定。解释器包含恢复它们所需的计算，所以攻击者仍可跟踪、提升或复用解码过程。</p>

<p><strong>④ 生成解释器，并保留位宽语义。</strong></p>

<p><code class="language-plaintext highlighter-rouge">materialize()</code> 将编码数组放进私有全局变量，<code class="language-plaintext highlighter-rouge">F.deleteBody()</code> 删除原计算体，创建 <code class="language-plaintext highlighter-rouge">vm.entry</code>、<code class="language-plaintext highlighter-rouge">vm.dispatch</code>、<code class="language-plaintext highlighter-rouge">vm.store</code> 和 handler。图 5、图 6 的循环对应：</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>vm.entry：初始化编码帧、参数、session、PC
vm.dispatch：取 9 个字段 → 解码字段 → 解码 a/b/c → switch(opcode)
handler：计算结果，或分支／返回
vm.store：结果按位宽掩码 → 编码写回 → 更新 PC → vm.dispatch
</code></pre></div></div>

<p>可以用状态映射检查生成循环与源计算的对应。令源 SSA 值环境为 ρ，逻辑寄存器映射为 id(v)，生成函数状态为 <code class="language-plaintext highlighter-rouge">(PC, frame, session)</code>。在与源操作完成或后继块入口对应的位置，对当前动态执行中已定义且需要保持的源值，要求：</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>D_(id(v),session)(frame[id(v)]) = bitpattern(ρ(v))
</code></pre></div></div>

<p>算术 handler 读取映射值、执行原操作，再按原位宽截断并编码到目标寄存器；分支选择边程序；边程序完成两阶段复制后，才建立 PHI 的后继环境。复制进行中不要求全部 PHI 目标同时对应后继环境，临时寄存器负责保存仍需读取的旧值。RET 返回分派块中已解码的操作数。参数初始化建立起始关系，以上步骤用于说明如何逐步保持它。这是实现应满足的不变量，当前代码没有提供机器检查的完整模拟关系证明。</p>

<p>注意操作数解码实际在 <code class="language-plaintext highlighter-rouge">vm.dispatch</code> 中、进入 opcode handler <strong>之前</strong>完成。写回时限制到原位宽；有符号比较和算术右移先按源位宽符号扩展，<code class="language-plaintext highlighter-rouge">sext</code> 也保留源位宽。实现还为除数零与有符号除法溢出设置防护，避免解释器引入额外的危险求值；等价主张只覆盖受支持源 IR 的有定义执行。</p>

<p>现在可以把 E3 的一条指令走完：</p>

<table>
  <thead>
    <tr>
      <th>执行位置</th>
      <th>使用的数据</th>
      <th>本例发生的事</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">vm.dispatch</code> 取字段</td>
      <td><code class="language-plaintext highlighter-rouge">words[p*9+k]</code>、seed、p、k</td>
      <td>还原 MUL、三个寄存器编号、位宽与后继</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">vm.dispatch</code> 解码帧</td>
      <td><code class="language-plaintext highlighter-rouge">frame[rUnit]</code>、<code class="language-plaintext highlighter-rouge">frame[rQuantity]</code></td>
      <td>用本次 session 与各自寄存器关系恢复 <code class="language-plaintext highlighter-rouge">a=110, b=12</code></td>
    </tr>
    <tr>
      <td>MUL handler</td>
      <td>明文 a、b</td>
      <td><code class="language-plaintext highlighter-rouge">CreateMul(a,b)</code> 得到 <code class="language-plaintext highlighter-rouge">1320</code></td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">vm.store</code></td>
      <td>结果、<code class="language-plaintext highlighter-rouge">f[1]</code>、<code class="language-plaintext highlighter-rouge">f[6]</code>、<code class="language-plaintext highlighter-rouge">f[7]</code></td>
      <td>保留低 32 位，以 rGross 的关系编码保存，PC 前进</td>
    </tr>
    <tr>
      <td>后续 E4 指令</td>
      <td>rGross 与数量条件</td>
      <td>计算 132 的折扣，最后返回 1188</td>
    </tr>
  </tbody>
</table>

<p>E1/E2 也进入同一程序，认证失败则经对应边程序把 0 送到返回寄存器。这里的边程序正是返回 PHI 语义所需的复制，而不是另外保留一份明文报价函数。</p>

<p>帧读写设为 volatile，函数设为 noinline、optnone，用于保留原型的表示结构。volatile 不提供保密性；实际产物中的频繁帧访问与稳定解码现场也会成为观察点。<strong>编码帧并不等于执行时没有明文。</strong> 后文的动态恢复正是从这段分派状态入手。</p>

<p>公开教学示例的 <a href="/assets/downloads/anti-ai-llvm/source/examples/quote-walkthrough/verify.py">验证脚本</a>、<a href="/assets/downloads/anti-ai-llvm/source/examples/quote-walkthrough/reproduce.sh">本地复现入口</a> 与 <a href="/assets/downloads/anti-ai-llvm/source/examples/quote-walkthrough/verification.json">转换验证记录</a> 单独保存。用实际 BusinessVM Pass 转换后，macOS ARM64 本地对照通过 2,090 项（2,000 项随机、90 项边界），公开数值样例返回 1188；这只检验示例转换，不并入 18 次盲测，也不是 Android 真机验证。完整源码与构建入口见 <a href="/assets/downloads/anti-ai-llvm/source/passes/business-vm/BusinessVM.cpp">BusinessVM.cpp</a>、<a href="/assets/downloads/anti-ai-llvm/source/passes/business-vm/build.sh">build.sh</a>。其输入白名单不覆盖真实 JNI/C++ 业务中的对象、异常、内存别名和外部调用。</p>

<h3 id="section-4-6">4.6 构建顺序与验证：从 IR 一直核对到最终 SO</h3>

<p>整函数转换的输入必须仍是受支持的纯业务 IR。如果先插入 runtime 调用，白名单会拒绝；如果在普通优化中随意安排回调，又可能无法确定转换与平坦化的先后。因此本实验显式分成两阶段：</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>带注解的业务 C
  → Clang -O2 生成 Android IR
  → opt 显式执行整函数 Pass
  → O-MVLL 的控制流处理与入口插桩
  → 链接 metadata、runtime、shadow 对象
  → Android AArch64 SO
</code></pre></div></div>

<p>图 5 将这三件事分开画出：<code class="language-plaintext highlighter-rouge">compile()</code> 生成未编码的逻辑指令；<code class="language-plaintext highlighter-rouge">materialize()</code> 编码字段、创建全局数组并替换函数体；主插件处理与最终链接随后执行。</p>

<p>本项目主插件在 <code class="language-plaintext highlighter-rouge">OptimizerLast</code> 回调中先添加配置指定的 Last 阶段混淆，再执行 <code class="language-plaintext highlighter-rouge">AnalysisComplexity</code>，最后执行 <code class="language-plaintext highlighter-rouge">BlaNativeLayers</code>。因此两种方案的具体顺序不同：</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>切片：配置的控制流混淆 → 表达式切片 → 入口插桩 → 链接
整函数：显式 VM 转换 → 配置的控制流混淆 → 入口插桩 → 链接
</code></pre></div></div>

<p>整函数目标不带切片标记，切片 Pass 不再处理它。图 2 展示机制组合，不表示各 Pass 按图中节点顺序执行。</p>

<p>下面是源码 <a href="/assets/downloads/anti-ai-llvm/source/passes/business-vm/README.rst">实现说明</a> 中两阶段命令的整理。它说明实际调用关系；Android target、sysroot、配置文件、runtime 对象和链接选项仍需使用工程里的完整构建参数：</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># 两个 clang 步骤使用一致的 Android target 与 sysroot。</span>
clang <span class="nt">-O2</span> <span class="nt">-emit-llvm</span> <span class="nt">-c</span> annotated-core.c <span class="nt">-o</span> core.bc
<span class="nv">OMVLL_VM_SEED</span><span class="o">=</span><span class="s2">"</span><span class="nv">$BUILD_SEED</span><span class="s2">"</span> opt <span class="nt">-load-pass-plugin</span><span class="o">=</span>BusinessVM.dylib <span class="se">\</span>
  <span class="nt">-passes</span><span class="o">=</span>v4-virtualize core.bc <span class="nt">-o</span> core-vm.bc
<span class="nv">OMVLL_CONFIG</span><span class="o">=</span><span class="s2">"</span><span class="nv">$FLATTEN_CONFIG</span><span class="s2">"</span> clang <span class="nt">-O2</span> <span class="se">\</span>
  <span class="nt">-fpass-plugin</span><span class="o">=</span>libOMVLL.dylib <span class="nt">-c</span> core-vm.bc <span class="nt">-o</span> core.o
<span class="c"># 再与 metadata、runtime、shadow 对象链接生成 SO。</span>
</code></pre></div></div>

<p>命令中的 <code class="language-plaintext highlighter-rouge">v4-virtualize</code> 是源码实际注册的工具名，仅为复现保留。它对应本文的整函数虚拟化机制，不要求读者了解内部迭代历史。<code class="language-plaintext highlighter-rouge">BUILD_SEED</code> 要由构建环境提供；种子用于可复现多态，不应作为不可提取的秘密承诺。</p>

<p>此外，<code class="language-plaintext highlighter-rouge">used</code> 等编译侧保留标记与链接侧 retain／节回收规则要配合使用，核对合法 <code class="language-plaintext highlighter-rouge">.rodata.bla</code> 元数据最终确实存在。光在源文件写了字符串，或看到插件加载日志，都不足以说明最终保护生效。</p>

<p>本次工程验证覆盖了以下层次：</p>

<table>
  <thead>
    <tr>
      <th>验证层</th>
      <th>检查内容</th>
      <th>能说明什么</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>结构</td>
      <td>IR verifier；原业务体替换、VM switch 与外层 flatten 共存；重复转换幂等</td>
      <td>转换执行且生成结构合法</td>
    </tr>
    <tr>
      <td>语义</td>
      <td>正常环境下 8,592 项输入输出对照</td>
      <td>当前夹具与样本集上的行为保持</td>
    </tr>
    <tr>
      <td>边界</td>
      <td>10,000 项 PHI 循环、10,000 项窄整数／转换；不支持 IR 拒绝构建</td>
      <td>针对高风险编译语义的回归证据</td>
    </tr>
    <tr>
      <td>产物</td>
      <td>最终 ELF 中的代码、节与 helper 链接检查</td>
      <td>构建和链接没有丢失预期组件</td>
    </tr>
    <tr>
      <td>对抗</td>
      <td>独立代理提交重建，再由隔离裁判执行</td>
      <td>固定预算内实际恢复到什么程度</td>
    </tr>
  </tbody>
</table>

<p>还有两类编译细节不能由 IR verifier 兜底：</p>

<ul>
  <li><strong>源操作的定义域。</strong> i32 普通算术的回绕、带 <code class="language-plaintext highlighter-rouge">nsw/nuw</code> 的溢出约束、有符号除法、过宽移位和 poison 不能混为一谈。切片克隆保留原指令标志；VM 用自己的算术实现模拟有定义源执行，不能声称为原本未定义的输入建立了等价语义。</li>
  <li><strong>分析结果失效。</strong> CFG、调用和内存行为变化后，旧 memory/range 等推断可能失效。清理 IR 属性与返回 <code class="language-plaintext highlighter-rouge">PreservedAnalyses::none()</code> 解决的是不同层面的过期信息：前者改 IR 中的承诺，后者通知 Pass Manager 不复用旧分析结果。</li>
</ul>

<p>前四项为保护强度测试建立前提，不能代替第五项。IR verifier 检查结构与类型规则，有限输入对照检查具体行为；二者都不等于全输入域的等价证明。</p>

<h2 id="section-5">五、实测：用独立重建检验保护效果</h2>

<h3 id="section-5-1">5.1 先定义实验任务，再看分析报告</h3>

<p>样例实现一个离线报价策略：数量范围、档位、认证关系、基础价格、数量折扣与用户优惠。它是自建窄函数族，不是真实 SDK，也不是安全授权设计。</p>

<p>实验比较四种明确的构建配置：</p>

<table>
  <thead>
    <tr>
      <th>配置名称</th>
      <th>本文含义</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>未保护</td>
      <td>优化编译的原始业务</td>
    </tr>
    <tr>
      <td>控制流平坦化</td>
      <td>仅启用本项目选定的 O-MVLL flatten 配置</td>
    </tr>
    <tr>
      <td>切片分层保护</td>
      <td>平坦化、表达式切片、元数据、影子与运行时守卫</td>
    </tr>
    <tr>
      <td>整函数虚拟化保护</td>
      <td>相同外围层，以整函数编码 VM 替代表达式切片</td>
    </tr>
  </tbody>
</table>

<p>后两项名称指整个组合，不能把差异精确归因于某个单独保护层。实验没有做完整消融，也没有测试 O-MVLL 所有功能组合。</p>

<table>
  <thead>
    <tr>
      <th>项目</th>
      <th>配置</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>分析入口</td>
      <td>REA 6.8.0 + 固定 IDA headless 后端</td>
    </tr>
    <tr>
      <td>模型</td>
      <td><code class="language-plaintext highlighter-rouge">gpt-6.1-sol</code>，high reasoning，独立上下文</td>
    </tr>
    <tr>
      <td>预算</td>
      <td>每轮 900 秒、100 次工具调用，固定脚本资源限制</td>
    </tr>
    <tr>
      <td>静态轨</td>
      <td>允许检查、反编译、字节解码、静态 lifting 与推导计算；不运行原目标或抽取的 VM 程序</td>
    </tr>
    <tr>
      <td>动态轨</td>
      <td>在静态能力上，允许原目标仿真、执行与修改自有副本</td>
    </tr>
    <tr>
      <td>提交</td>
      <td>不依赖原 SO 的可移植 C；允许独立 IR 或字节码重建，禁止加载原 ELF 或委托原目标求值</td>
    </tr>
    <tr>
      <td>隐藏测试</td>
      <td>8,192 项随机输入 + 400 项边界输入，包含能通过认证的真实成功路径</td>
    </tr>
  </tbody>
</table>

<p>模型名称按本次编辑更正。历史运行记录与实测总览图仍使用 <code class="language-plaintext highlighter-rouge">gpt-5.6-sol</code> 标签，模型元数据存在差异；以下成绩沿用原有实验，未因名称更正重新运行。</p>

<p>本实验共 18 次方案对照：三个保护配置、三组新秘密、两轨；再补 6 次同秘密未保护基线。后者在主实验完成后执行，保持最大并行数不变。每组四个产物均经过正常环境等价预检。</p>

<h3 id="section-5-2">5.2 隔离比“另开一个聊天窗口”更重要</h3>

<p>源码、答案、旧重建和裁判都在研究者工作区里。攻击代理若能读到这些材料，得到正确结果也无法证明突破了保护。</p>

<p>本实验将模型客户端放在外层 OS 沙箱，用私有通信通道连接固定目标 broker；脚本在另一套受限工作进程中执行。REA 后端每轮独立，候选 C 在单独的沙箱编译与运行，保存 oracle 的裁判不加载候选代码。先用合成控制文件验证真实访问结果，再启动盲测。</p>

<p><a href="/images/posts/android/anti-ai-llvm/04-evaluation-isolation.svg" class="anti-ai-figure"><img src="/images/posts/android/anti-ai-llvm/04-evaluation-isolation.svg" alt="隔离盲测与独立裁判架构" /></a></p>

<p><em>图 7：候选程序与答案分离，脚本无源码／裁判访问，隐藏评分不反馈代理。</em></p>

<p>这种做法吸收了 <a href="https://www.elastic.co/security-labs/threat-command/llm-reversing-vs-llm-obfuscation">Elastic</a> 对结果、时间与成本的多维观察，以及 <a href="https://blog.quarkslab.com/defeating-ai-assisted-reverse-engineering-or-at-least-trying-to.html">Quarkslab</a> 对答案旁路和错误自述的复盘。本文使用自己的固定预算与评分协议，不把外部实验的成绩当作本文数据。</p>

<h3 id="section-5-3">5.3 合规与正确性分别判定</h3>

<p>“完整恢复”指独立 C 通过全部 8,592 项隐藏输入，并符合对应轨道。漂亮的业务公式和低层 IR 功能克隆都可计入，不能事后只接受某种提交形式。</p>

<p>一轮静态代理对解码后的目标 VM 运行了自写解释器，验证约 50,000 组输入，超出静态规则。因此该轮保留原始观测，但不计有效静态成功或失败。它没有源码／答案访问，问题在轨道合规，而非隔离泄漏。</p>

<h3 id="section-5-4">5.4 方案对照结果</h3>

<table>
  <thead>
    <tr>
      <th>保护配置</th>
      <th style="text-align: right">静态完整恢复 / 有效轮数</th>
      <th style="text-align: right">动态完整恢复 / 有效轮数</th>
      <th style="text-align: right">合计</th>
      <th style="text-align: right">排除</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>控制流平坦化</td>
      <td style="text-align: right">2/3</td>
      <td style="text-align: right">3/3</td>
      <td style="text-align: right">5/6</td>
      <td style="text-align: right">0</td>
    </tr>
    <tr>
      <td>切片分层保护</td>
      <td style="text-align: right">1/3</td>
      <td style="text-align: right">2/3</td>
      <td style="text-align: right">3/6</td>
      <td style="text-align: right">0</td>
    </tr>
    <tr>
      <td>整函数虚拟化保护</td>
      <td style="text-align: right">2/2</td>
      <td style="text-align: right">2/3</td>
      <td style="text-align: right">4/5</td>
      <td style="text-align: right">1 轮静态违规</td>
    </tr>
    <tr>
      <td>未保护补充基线</td>
      <td style="text-align: right">3/3</td>
      <td style="text-align: right">3/3</td>
      <td style="text-align: right">6/6</td>
      <td style="text-align: right">0</td>
    </tr>
  </tbody>
</table>

<p>表中数字是<strong>攻击者完整恢复的次数</strong>，不是保护得分。静态有效分母不等、每个配置只有少量样本，不能据此声称统计上显著更强或更弱。可以确定的是：两种分层方案的动态完整恢复数相同；整函数虚拟化仍出现了两次合规静态功能克隆。</p>

<p>把三组秘密逐一配对，比较才不会被合计数字掩盖：</p>

<table>
  <thead>
    <tr>
      <th>动态实例</th>
      <th>切片分层保护被完整恢复</th>
      <th>整函数虚拟化被完整恢复</th>
      <th>对保护方的变化</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>0</td>
      <td>否</td>
      <td>是</td>
      <td>变差</td>
    </tr>
    <tr>
      <td>1</td>
      <td>是</td>
      <td>是</td>
      <td>未变</td>
    </tr>
    <tr>
      <td>2</td>
      <td>是</td>
      <td>否</td>
      <td>变好</td>
    </tr>
  </tbody>
</table>

<p>一组改善、一组退步、一组不变，合计均为 2/3。因此，本轮没有观察到整函数虚拟化相对切片方案的动态恢复率收益，也不能由这三个样本证明二者的真实成功概率相同。</p>

<p><strong>这些“未完整恢复”应如何理解？</strong> 主实验各轮均在 900 秒内主动结束，没有预算耗尽或基础设施失败。它们表示最终提交未通过全部隐藏输入，不能写成“防御拖满预算”。整函数方案未通过的动态轮尤其典型：非法输入 5,697/5,697 正确，合法业务 0/2,895，合计仍有 5,697/8,592。它主要恢复了拒绝行为，不能把约 66.3% 的总正确率解释为恢复了同等比例的业务算法。</p>

<h3 id="section-5-5">5.5 保护前后对照：恢复结果与分析成本</h3>

<p>下表将同一秘密、同一轨道的未保护产物与整函数虚拟化产物对齐。六行来自同一个函数族，不能理解成六个不同 App。桌面端总览图中，灰蓝色对应未保护、蓝色对应保护后，橙色标出违规观测，文本表用箭头表示配对；手机端按实例展示左右对照卡片。违规行不计算有效 token 倍数。</p>

<div class="benchmark-mobile" aria-label="保护前后逐例数据">
  <p><strong>手机端逐例对照</strong>：左列未保护，右列整函数虚拟化；以下数据与总览图一致。</p>
  <p><a href="/images/posts/android/anti-ai-llvm/05-before-after-benchmark.svg">查看完整总览图（可缩放）</a></p>
  
  
  
  
  <section class="benchmark-case benchmark-invalid">
    <p class="benchmark-case-title">实例 0 · 静态轨</p>
    <div class="benchmark-columns" aria-hidden="true"><span>指标</span><span>未保护</span><span>保护后</span></div>
    <dl class="benchmark-rows">
      <div class="benchmark-row"><dt>重建正确数</dt><dd class="benchmark-before" aria-label="未保护">8592/8592</dd><dd class="benchmark-after" aria-label="保护后">6057/8592*</dd></div>
      <div class="benchmark-row"><dt>会话秒数</dt><dd class="benchmark-before" aria-label="未保护">45.0</dd><dd class="benchmark-after" aria-label="保护后">496.7</dd></div>
      <div class="benchmark-row"><dt>工具调用</dt><dd class="benchmark-before" aria-label="未保护">4</dd><dd class="benchmark-after" aria-label="保护后">48</dd></div>
      <div class="benchmark-row"><dt>累计 token</dt><dd class="benchmark-before" aria-label="未保护">54724</dd><dd class="benchmark-after" aria-label="保护后">2545504</dd></div>
      <div class="benchmark-row"><dt>未缓存输入</dt><dd class="benchmark-before" aria-label="未保护">12679</dd><dd class="benchmark-after" aria-label="保护后">111540</dd></div>
      <div class="benchmark-row"><dt>SO 等价</dt><dd class="benchmark-before" aria-label="未保护">8592/8592</dd><dd class="benchmark-after" aria-label="保护后">8592/8592</dd></div>
    </dl>
    <p class="benchmark-case-note">累计 token 倍率：<strong>N/A</strong>。* 静态轨实际执行目标 VM，原始观测保留，不计有效恢复率或配对倍率。</p>
  </section>
  
  
  
  
  <section class="benchmark-case">
    <p class="benchmark-case-title">实例 0 · 动态轨</p>
    <div class="benchmark-columns" aria-hidden="true"><span>指标</span><span>未保护</span><span>保护后</span></div>
    <dl class="benchmark-rows">
      <div class="benchmark-row"><dt>重建正确数</dt><dd class="benchmark-before" aria-label="未保护">8592/8592</dd><dd class="benchmark-after" aria-label="保护后">8592/8592</dd></div>
      <div class="benchmark-row"><dt>会话秒数</dt><dd class="benchmark-before" aria-label="未保护">67.2</dd><dd class="benchmark-after" aria-label="保护后">550.4</dd></div>
      <div class="benchmark-row"><dt>工具调用</dt><dd class="benchmark-before" aria-label="未保护">8</dd><dd class="benchmark-after" aria-label="保护后">58</dd></div>
      <div class="benchmark-row"><dt>累计 token</dt><dd class="benchmark-before" aria-label="未保护">123950</dd><dd class="benchmark-after" aria-label="保护后">5309354</dd></div>
      <div class="benchmark-row"><dt>未缓存输入</dt><dd class="benchmark-before" aria-label="未保护">12572</dd><dd class="benchmark-after" aria-label="保护后">162968</dd></div>
      <div class="benchmark-row"><dt>SO 等价</dt><dd class="benchmark-before" aria-label="未保护">8592/8592</dd><dd class="benchmark-after" aria-label="保护后">8592/8592</dd></div>
    </dl>
    <p class="benchmark-case-note">累计 token 倍率：<strong>42.8×</strong>。</p>
  </section>
  
  
  
  
  <section class="benchmark-case">
    <p class="benchmark-case-title">实例 1 · 静态轨</p>
    <div class="benchmark-columns" aria-hidden="true"><span>指标</span><span>未保护</span><span>保护后</span></div>
    <dl class="benchmark-rows">
      <div class="benchmark-row"><dt>重建正确数</dt><dd class="benchmark-before" aria-label="未保护">8592/8592</dd><dd class="benchmark-after" aria-label="保护后">8592/8592</dd></div>
      <div class="benchmark-row"><dt>会话秒数</dt><dd class="benchmark-before" aria-label="未保护">36.1</dd><dd class="benchmark-after" aria-label="保护后">678.9</dd></div>
      <div class="benchmark-row"><dt>工具调用</dt><dd class="benchmark-before" aria-label="未保护">4</dd><dd class="benchmark-after" aria-label="保护后">57</dd></div>
      <div class="benchmark-row"><dt>累计 token</dt><dd class="benchmark-before" aria-label="未保护">54344</dd><dd class="benchmark-after" aria-label="保护后">4464072</dd></div>
      <div class="benchmark-row"><dt>未缓存输入</dt><dd class="benchmark-before" aria-label="未保护">16892</dd><dd class="benchmark-after" aria-label="保护后">145304</dd></div>
      <div class="benchmark-row"><dt>SO 等价</dt><dd class="benchmark-before" aria-label="未保护">8592/8592</dd><dd class="benchmark-after" aria-label="保护后">8592/8592</dd></div>
    </dl>
    <p class="benchmark-case-note">累计 token 倍率：<strong>82.1×</strong>。</p>
  </section>
  
  
  
  
  <section class="benchmark-case">
    <p class="benchmark-case-title">实例 1 · 动态轨</p>
    <div class="benchmark-columns" aria-hidden="true"><span>指标</span><span>未保护</span><span>保护后</span></div>
    <dl class="benchmark-rows">
      <div class="benchmark-row"><dt>重建正确数</dt><dd class="benchmark-before" aria-label="未保护">8592/8592</dd><dd class="benchmark-after" aria-label="保护后">8592/8592</dd></div>
      <div class="benchmark-row"><dt>会话秒数</dt><dd class="benchmark-before" aria-label="未保护">53.6</dd><dd class="benchmark-after" aria-label="保护后">443.2</dd></div>
      <div class="benchmark-row"><dt>工具调用</dt><dd class="benchmark-before" aria-label="未保护">4</dd><dd class="benchmark-after" aria-label="保护后">40</dd></div>
      <div class="benchmark-row"><dt>累计 token</dt><dd class="benchmark-before" aria-label="未保护">55126</dd><dd class="benchmark-after" aria-label="保护后">2737362</dd></div>
      <div class="benchmark-row"><dt>未缓存输入</dt><dd class="benchmark-before" aria-label="未保护">11394</dd><dd class="benchmark-after" aria-label="保护后">142663</dd></div>
      <div class="benchmark-row"><dt>SO 等价</dt><dd class="benchmark-before" aria-label="未保护">8592/8592</dd><dd class="benchmark-after" aria-label="保护后">8592/8592</dd></div>
    </dl>
    <p class="benchmark-case-note">累计 token 倍率：<strong>49.7×</strong>。</p>
  </section>
  
  
  
  
  <section class="benchmark-case">
    <p class="benchmark-case-title">实例 2 · 静态轨</p>
    <div class="benchmark-columns" aria-hidden="true"><span>指标</span><span>未保护</span><span>保护后</span></div>
    <dl class="benchmark-rows">
      <div class="benchmark-row"><dt>重建正确数</dt><dd class="benchmark-before" aria-label="未保护">8592/8592</dd><dd class="benchmark-after" aria-label="保护后">8592/8592</dd></div>
      <div class="benchmark-row"><dt>会话秒数</dt><dd class="benchmark-before" aria-label="未保护">39.8</dd><dd class="benchmark-after" aria-label="保护后">487.2</dd></div>
      <div class="benchmark-row"><dt>工具调用</dt><dd class="benchmark-before" aria-label="未保护">5</dd><dd class="benchmark-after" aria-label="保护后">33</dd></div>
      <div class="benchmark-row"><dt>累计 token</dt><dd class="benchmark-before" aria-label="未保护">74594</dd><dd class="benchmark-after" aria-label="保护后">2234561</dd></div>
      <div class="benchmark-row"><dt>未缓存输入</dt><dd class="benchmark-before" aria-label="未保护">13535</dd><dd class="benchmark-after" aria-label="保护后">117126</dd></div>
      <div class="benchmark-row"><dt>SO 等价</dt><dd class="benchmark-before" aria-label="未保护">8592/8592</dd><dd class="benchmark-after" aria-label="保护后">8592/8592</dd></div>
    </dl>
    <p class="benchmark-case-note">累计 token 倍率：<strong>30.0×</strong>。</p>
  </section>
  
  
  
  
  <section class="benchmark-case">
    <p class="benchmark-case-title">实例 2 · 动态轨</p>
    <div class="benchmark-columns" aria-hidden="true"><span>指标</span><span>未保护</span><span>保护后</span></div>
    <dl class="benchmark-rows">
      <div class="benchmark-row"><dt>重建正确数</dt><dd class="benchmark-before" aria-label="未保护">8592/8592</dd><dd class="benchmark-after" aria-label="保护后">5697/8592</dd></div>
      <div class="benchmark-row"><dt>会话秒数</dt><dd class="benchmark-before" aria-label="未保护">30.1</dd><dd class="benchmark-after" aria-label="保护后">330.2</dd></div>
      <div class="benchmark-row"><dt>工具调用</dt><dd class="benchmark-before" aria-label="未保护">4</dd><dd class="benchmark-after" aria-label="保护后">25</dd></div>
      <div class="benchmark-row"><dt>累计 token</dt><dd class="benchmark-before" aria-label="未保护">54247</dd><dd class="benchmark-after" aria-label="保护后">681749</dd></div>
      <div class="benchmark-row"><dt>未缓存输入</dt><dd class="benchmark-before" aria-label="未保护">11305</dd><dd class="benchmark-after" aria-label="保护后">44651</dd></div>
      <div class="benchmark-row"><dt>SO 等价</dt><dd class="benchmark-before" aria-label="未保护">8592/8592</dd><dd class="benchmark-after" aria-label="保护后">8592/8592</dd></div>
    </dl>
    <p class="benchmark-case-note">累计 token 倍率：<strong>12.6×</strong>。</p>
  </section>
  
</div>

<div class="benchmark-desktop">

  <p><a href="/images/posts/android/anti-ai-llvm/05-before-after-benchmark.svg" class="anti-ai-figure"><img src="/images/posts/android/anti-ai-llvm/05-before-after-benchmark.svg" alt="Android SO 保护前后实测表" /></a></p>

  <p><em>图 8：行为重建得分与原始二进制等价校验分开统计，所有数值来自实际运行记录。</em></p>

  <table>
    <thead>
      <tr>
        <th>实例 / 轨道</th>
        <th style="text-align: right">隐藏测试正确数：未保护 → 保护后</th>
        <th style="text-align: right">会话秒数</th>
        <th style="text-align: right">累计 tokens</th>
        <th style="text-align: right">token 倍数</th>
      </tr>
    </thead>
    <tbody>
      <tr>
        <td>0 / 静态</td>
        <td style="text-align: right">8,592 → 6,057*</td>
        <td style="text-align: right">45.0 → 496.7</td>
        <td style="text-align: right">54,724 → 2,545,504</td>
        <td style="text-align: right">N/A（违规）</td>
      </tr>
      <tr>
        <td>0 / 动态</td>
        <td style="text-align: right">8,592 → 8,592</td>
        <td style="text-align: right">67.2 → 550.4</td>
        <td style="text-align: right">123,950 → 5,309,354</td>
        <td style="text-align: right">42.8×</td>
      </tr>
      <tr>
        <td>1 / 静态</td>
        <td style="text-align: right">8,592 → 8,592</td>
        <td style="text-align: right">36.1 → 678.9</td>
        <td style="text-align: right">54,344 → 4,464,072</td>
        <td style="text-align: right">82.1×</td>
      </tr>
      <tr>
        <td>1 / 动态</td>
        <td style="text-align: right">8,592 → 8,592</td>
        <td style="text-align: right">53.6 → 443.2</td>
        <td style="text-align: right">55,126 → 2,737,362</td>
        <td style="text-align: right">49.7×</td>
      </tr>
      <tr>
        <td>2 / 静态</td>
        <td style="text-align: right">8,592 → 8,592</td>
        <td style="text-align: right">39.8 → 487.2</td>
        <td style="text-align: right">74,594 → 2,234,561</td>
        <td style="text-align: right">30.0×</td>
      </tr>
      <tr>
        <td>2 / 动态</td>
        <td style="text-align: right">8,592 → 5,697</td>
        <td style="text-align: right">30.1 → 330.2</td>
        <td style="text-align: right">54,247 → 681,749</td>
        <td style="text-align: right">12.6×</td>
      </tr>
    </tbody>
  </table>

</div>

<p><code class="language-plaintext highlighter-rouge">*</code> 为静态轨实际运行目标 VM 的无效观测，不计恢复率或配对倍率。原始未保护／保护 SO 在正常夹具下各通过 8,592/8,592 项等价检查；这与 AI 重建的正确数是不同指标。</p>

<p>以实例 0 的动态配对为例：两份重建都通过 8,592 项，保护后的会话从 67.2 秒增到 550.4 秒，工具调用从 8 次增到 58 次，累计 token 从 123,950 增到 5,309,354。它能说明该次恢复走了更长的分析路线，不能说明保护阻止了恢复。</p>

<p>累计 input token 还会重复计入多次请求里携带的上下文。这个量反映的是 CLI 报告的会话用量，并不等于独立阅读了这么多新字节。会话结束时间也不等于首次正确提交的时间；裁判验证只在研究者侧进行，代理不会得到隐藏测试反馈。</p>

<p>Token 使用 CLI 报告的累计 <code class="language-plaintext highlighter-rouge">input_tokens + output_tokens</code>，缓存输入已包含在 input 内，不重复加 cached，也不另加 reasoning 字段。图中单列未缓存输入，避免隐藏缓存结构。12.6×—82.1× 是已报告用量的倍率，不是费用倍率，也不是完成恢复的必需成本。实际逐轮实付美元费用不可取得，因此不填估算账单。</p>

<h3 id="section-5-6">5.6 保护侧代价：不能只计算攻击者变慢多少</h3>

<p>对三组秘密的实际产物，每实例使用同一组 40 项合法输入，每配置合计 120 次。守卫探针在测量前预热，40 次窗口未触及下一次刷新；记录中的测量段系统调用数为 0，所有输出与 oracle 一致。</p>

<table>
  <thead>
    <tr>
      <th>配置</th>
      <th style="text-align: right">SO 大小</th>
      <th style="text-align: right">ARM64 仿真指令中位数</th>
      <th style="text-align: right">相对未保护中位数</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>未保护</td>
      <td style="text-align: right">2,272 字节</td>
      <td style="text-align: right">48</td>
      <td style="text-align: right">1.00×</td>
    </tr>
    <tr>
      <td>控制流平坦化</td>
      <td style="text-align: right">3,216 字节</td>
      <td style="text-align: right">262</td>
      <td style="text-align: right">5.46×</td>
    </tr>
    <tr>
      <td>切片分层保护</td>
      <td style="text-align: right">22,608 字节</td>
      <td style="text-align: right">696</td>
      <td style="text-align: right">14.50×</td>
    </tr>
    <tr>
      <td>整函数虚拟化保护</td>
      <td style="text-align: right">30,512 字节</td>
      <td style="text-align: right">36,731</td>
      <td style="text-align: right">765.23×</td>
    </tr>
  </tbody>
</table>

<p>整函数虚拟化与切片方案的中位数之比为 <strong>52.77×</strong>。这是 Unicorn 的执行指令计数，不是 Android 真机延迟、周期、耗电或用户体验的倍率，也不覆盖所有冷启动与检测成本。</p>

<p>这是采样指令中位数之比，不是逐输入倍率的中位数。结合第 5.4 节相同的动态完整恢复次数，当前数据未显示足以支持这项执行代价的恢复率收益。</p>

<h3 id="section-5-7">5.7 能从结果中得出什么，哪些问题还没回答</h3>

<table>
  <thead>
    <tr>
      <th>观察</th>
      <th>可以支持的结论</th>
      <th>仍不能支持的结论</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>未保护 6/6 被完整重建</td>
      <td>任务在当前模型、工具和预算内可完成</td>
      <td>所有未保护 SO 都同样容易恢复</td>
    </tr>
    <tr>
      <td>分层方案部分轮次未完整恢复</td>
      <td>本轮固定预算内出现阻碍，值得保留失败产物分析</td>
      <td>未恢复的算法或秘密不可提取</td>
    </tr>
    <tr>
      <td>整函数方案动态仍为 2/3 完整恢复</td>
      <td>本轮未显示优于切片方案的动态恢复率</td>
      <td>两者真实成功概率完全相等</td>
    </tr>
    <tr>
      <td>token 用量增多、执行指令大幅增加</td>
      <td>分析与运行两侧都有代价</td>
      <td>同等美元成本、真机延迟或跨模型强度</td>
    </tr>
  </tbody>
</table>

<p>实验只覆盖一个纯整数函数族，三个实例更换秘密，但没有扩展到三个独立算法族；每配置每轨仅三次，且一次静态违规被排除。没有第二模型、完整消融和随机化交错的基线时序，不能把表里的小样本差异归因为普遍保护优势。</p>

<p>测试环境是 macOS Seatbelt 隔离、固定 REA／IDA 后端与 Unicorn ARM64 仿真。它能够检验该协议下的代码恢复，尚未覆盖完整 REA 工具集合、真实 Android 内核行为、App 调用上下文、冷启动性能、误报与多线程兼容性。8,592 项全通过是本协议的“完整恢复”，不是对函数全输入域的数学证明。</p>

<h2 id="section-6">六、失守复盘与未来发展预测</h2>

<h3 id="section-6-1">6.1 案例一：跟随真实执行，从分派栈快照恢复报价</h3>

<p>本例对应表中实例 0 的整函数方案动态轮。下面的顺序来自保存的工具调用与脚本记录；地址只属于该样本，不能当作通用偏移。教学函数用于解释数据如何流动，<strong>下面的成绩来自原盲测业务</strong>，不是教学函数的结果。</p>

<p><strong>① 定位入口，先分清正常与影子路径。</strong></p>

<p>代理从导出和指令分析进入目标，仿真观察入口守卫，校正检测返回值与分支极性的关系，进入正常 VM 路径。这一步对应第 4.4 节的 <code class="language-plaintext highlighter-rouge">bla.gate → Original</code>。能返回一个看似合理的报价，并不足以确认正在分析真实业务；必须先分清分支。</p>

<p><strong>② 在解码完成的位置取证。</strong></p>

<p>调用 31—39 中，代理在样本分派点 <code class="language-plaintext highlighter-rouge">0x530c</code> 逐次采集栈状态，识别 PC、opcode、操作数和立即数。对应第 4.5 节，保存帧虽然经过编码，但 <code class="language-plaintext highlighter-rouge">vm.dispatch</code> 已将字段与 a/b/c 解码，随后 handler 才能计算。该样本采用 <code class="language-plaintext highlighter-rouge">optnone</code> 的代码生成保留了相对稳定的明文栈槽，形成集中观察点。本次没有切换该属性的消融实验，不能将 spill 完全归因于这一属性。</p>

<p>公开 E3 的例子能解释这个差别：直接读帧得到的是编码值，读解码后的操作数却能看到 <code class="language-plaintext highlighter-rouge">110</code> 和 <code class="language-plaintext highlighter-rouge">12</code>。实际攻击恢复的是更复杂的认证与优惠关系，不能把这两个教学数字当作攻击日志。</p>

<p><strong>③ 从轨迹推导关系，再覆盖成功业务分支。</strong></p>

<p>调用 41—47 恢复 mix 与有效 token 的关系，调用 48—56 处理 coupon 条件与整数取整顺序。认证关系解决的是“怎样进入成功路径”，价格与折扣数据流解决的是“进入后怎样计算”；恢复前者并不自动恢复后者。</p>

<p><strong>④ 自检与独立裁判分开。</strong></p>

<p>调用 57 进行了 1,000 组合法／非法输入自检，调用 58 提交 1,026 字节独立纯算术 C。最终会话约 550.4 秒，58 次调用；隔离裁判判定 8,592/8,592，其中合法业务 2,784/2,784、非法输入 5,808/5,808。重建不加载原 SO，也没有复制原 VM。</p>

<p><a href="/images/posts/android/anti-ai-llvm/06-decoder-observation.svg" class="anti-ai-figure"><img src="/images/posts/android/anti-ai-llvm/06-decoder-observation.svg" alt="从实际分派状态到独立业务实现" /></a></p>

<p><em>图 9：本例按正常执行顺序收集明文状态，再归纳业务关系；调用编号可与保存轨迹对应。</em></p>

<p>这条路径突破的是“编码保存”和“明文计算”之间的边界。入口守卫增加了路径识别工作，编码帧增加了直接读状态的难度，但最终恢复依赖的明文仍在一个稳定位置出现。</p>

<h3 id="section-6-2">6.2 案例二：主动选择虚拟 PC，让原解释器解码程序</h3>

<p>本例对应实例 1 的整函数方案动态轮。它同样绕过入口观察，但没有一直跟随某条真实业务轨迹，而是主动扩大可观察的指令范围。</p>

<p><strong>① 先建立可控的解码现场。</strong></p>

<p>代理定位目标、令观察检测返回正常状态，在调用 29 确定 handler 语义。随后不仅选择虚拟 PC，还恢复外层平坦化所需的分派状态，使目标到达相应取指与解码位置。这不是简单修改函数入口参数，也不是直接调用隐藏裁判。</p>

<p><strong>② 借原目标读出字段。</strong></p>

<p>调用 30—36 中，它主动指定虚拟 PC 为 <code class="language-plaintext highlighter-rouge">0…91</code>，采集 opcode、操作数寄存器、立即数和后继。第 4.5 节的字段编码关系解释了为什么这条路成立：</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>decoded_field = words[PC*9+k]
                XOR mix(seed XOR PC*C1 XOR k*C2)
</code></pre></div></div>

<p>某个 PC 的字段只依赖 seed、PC 和字段序号。寄存器帧的 session 与调用输入有关，但它并没有把程序字段绑定到“之前必须合法执行过哪些指令”。代理能够复用目标的解码逻辑，便不必先独立求出全部掩码。</p>

<p><a href="/images/posts/android/anti-ai-llvm/10-chosen-pc-recovery.svg" class="anti-ai-figure"><img src="/images/posts/android/anti-ai-llvm/10-chosen-pc-recovery.svg" alt="主动选择虚拟 PC 的完整恢复路线" /></a></p>

<p><em>图 10：选择 PC、恢复外层状态、借目标解码，再独立实现；这是动态轨允许的目标仿真行为。</em></p>

<p><strong>③ 从记录还原计算，提交时去掉原目标依赖。</strong></p>

<p>得到指令记录后，代理恢复认证与价格公式，完成 500 组差分自检，最后提交 762 字节纯算术 C。会话约 443.2 秒，40 次工具调用；隔离裁判判定 8,592/8,592，其中合法业务 2,848/2,848、非法输入 5,744/5,744。提交中没有 VM、字节码或原 SO 委托。</p>

<p>两条路线的差别可以概括为：第一条问“这次执行解码出了什么”，第二条问“指定指令位置会解码出什么”。它们最终都把原解释器当成取证工具，而不是最终重建的依赖。</p>

<table>
  <thead>
    <tr>
      <th>对照位置</th>
      <th>设计原本增加的成本</th>
      <th>记录中实际利用的边界</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>入口守卫</td>
      <td>区分正常与影子路径</td>
      <td>动态轨允许修改自有目标，检测结果可替换</td>
    </tr>
    <tr>
      <td>编码寄存器帧</td>
      <td>增加直接读帧的解释成本</td>
      <td>分派时操作数已恢复，部分值稳定落栈</td>
    </tr>
    <tr>
      <td>编码指令记录</td>
      <td>隐藏直接可读的指令字段</td>
      <td>可选择 PC，复用目标的字段解码</td>
    </tr>
    <tr>
      <td>删除明文业务体</td>
      <td>避免直接读回原函数</td>
      <td>轨迹或指令记录仍足以重建算术关系</td>
    </tr>
  </tbody>
</table>

<p>两例分别利用两种不同表示上的关系：<code class="language-plaintext highlighter-rouge">frame → 明文操作数</code> 的集中解码，以及 <code class="language-plaintext highlighter-rouge">(PC,k) → 明文字段</code> 的可重复解码。前者与寄存器帧编码有关，后者与程序编码有关；只增强其中一项不能自动消除另一条恢复路径。它们说明当前实现移动了观察边界，却没有封住这两种具体取证方式。减少明文 spill、拆散通用解码点或让字段依赖执行历史，都仍需新的实现与盲测验证；本轮没有这些效果数据。</p>

<h3 id="section-6-3">6.3 静态恢复边界：语义 lifting 可以绕过高层解释</h3>

<p>切片方案还有另一类边界：固定 XOR 可被代数消去，纯 helper 可以逐个展开或记录参数／返回值。因此拆分真实计算同时也提供了小型求解接口。</p>

<p>整函数方案的合规静态代理还走出了两条路线：把必要 AArch64 语义静态提升成新的 C IR 执行器；或者解码虚拟指令记录，重建独立状态机。分析阶段没有运行目标，重建由隔离裁判执行。</p>

<p>这说明，要求代理必须解释出漂亮公式，会低估实际威胁。只要独立产物能够复现业务，功能克隆目标就已经达成；可读性保护与行为保护需要分别讨论。</p>

<h3 id="section-6-4">6.4 泄露指标：功能恢复、材料提取与算法理解分开统计</h3>

<p>一份错误重建可能已经恢复认证 key 与优惠 salt，只在价格数据流上拼错。<a href="/assets/downloads/anti-ai-llvm/artifacts/v4-blind-eval-20261030/materials-public.json">材料审计</a>按提交 C 与代理派生脚本做事后核对：两种分层配置各自的三个秘密实例，都至少在一次合规攻击中观察到与 oracle 匹配且具有相应业务用途的 key、salt。该审计是次要泄露指标，不替换隐藏测试评分。</p>

<p>反过来，没有观察到显式常量提取，也不能推定其未泄露：低层 IR 克隆可以正确运行，而不把高层 key 单独命名。关键材料提取、功能等价和自然语言理解是不同观察量。</p>

<h3 id="section-6-5">6.5 预测一：工具切换、仿真与 lifting 会更常态化</h3>

<p><strong>以下是基于本次观察的推断，不是已验证的未来事实。</strong></p>

<p>当一种入口失效后，代理可以继续换工具、读字节或提升代码。随着这些方法被封装成可复用工具，仅针对某个解析器阈值或某种反编译显示的保护，可能更容易被路由绕过。</p>

<p>防护评测也需要覆盖替代路径。只看默认工具的一次输出，将越来越难代表代理完成任务的实际能力。</p>

<h3 id="section-6-6">6.6 预测二：固定模板会更容易被复用的分析程序识别</h3>

<p>统一解释器、固定解码栈槽和重复的纯函数模板，能帮助保护方实现与维护，也能给攻击者提供稳定特征。一次成功分析若能沉淀为通用解码器，后续样本的成本可能下降。</p>

<p>构建多态、路径相关状态与 handler 融合可以作为研究方向，但本实验没有验证它们的收益。多态应增加语义恢复成本，而不能只是改一批容易重新映射的 opcode 编号。</p>

<h3 id="section-6-7">6.7 预测三：代码保护会更重视最小暴露与成本分配</h3>

<p>对于能在本地独立产生结果的业务，观察或复制其执行路径始终是重要威胁。未来设计可能更倾向于减少客户端必须长期持有的关键材料、缩小可独立复用的业务单元，并在条件允许时把最终授权决策移到受控服务端。</p>

<p>软件混淆仍有成本放大价值，但需要与频率、性能和兼容性一起设计。高频路径和低频关键函数不适合不加区分地应用同一套重型转换。</p>

<h3 id="section-6-8">6.8 预测四：防护效果会成为持续回归指标</h3>

<p>保护层会变，分析器也会变。比一次“护盾成功”报告更有价值的，是每次发布都检查行为等价、多个攻击路径、新秘密与新代理，以及执行成本。</p>

<p>下一轮评测应优先扩展算法族与模型，并补充真机兼容性、冷启动开销及各保护层的消融，而不是仅更换 opcode 编号后重复当前小样本。</p>

<h3 id="section-6-9">6.9 这次实验说明了什么</h3>

<p>MCP 与 REA 让核心代码更容易进入持续、可编程的分析闭环。应对这种变化，需要同时关注证据入口、真实数据流、运行时观察面和独立重建能力。</p>

<p>本实验表明，分层保护可以增加分析步骤和已报告 token 用量，却没有因为加入整函数编码虚拟机就获得更好的动态恢复率，还产生了显著执行代价。对保护方案的判断，应落在可复核的攻击产物、语义保持和成本上。</p>

<h3 id="source-materials">源码与实验材料</h3>

<p>两例对应的 <a href="/assets/downloads/anti-ai-llvm/artifacts/v4-blind-eval-20261030/primary/trials/f9d0696a64d0db5c/tools.jsonl">案例一调用轨迹</a> 与 <a href="/assets/downloads/anti-ai-llvm/artifacts/v4-blind-eval-20261030/primary/trials/8674d65e676be161/tools.jsonl">案例二调用轨迹</a> 保留了调用顺序；脚本、提交与审计见完整证据包。</p>

<ul>
  <li><a href="/assets/downloads/anti-ai-llvm/source/o-mvll/src/passes/analysis-complexity/AnalysisComplexity.cpp">表达式切片实现</a>、<a href="/assets/downloads/anti-ai-llvm/source/o-mvll/src/passes/bla-native-layers/BlaNativeLayers.cpp">入口与影子选择插桩</a>、<a href="/assets/downloads/anti-ai-llvm/source/o-mvll/runtime/bla-native/generate_metadata.py">元数据生成器</a>、<a href="/assets/downloads/anti-ai-llvm/source/o-mvll/runtime/bla-native/runtime.c">运行时观察</a>、<a href="/assets/downloads/anti-ai-llvm/source/o-mvll/runtime/bla-native/shadow.c">影子状态机</a>。</li>
  <li><a href="/assets/downloads/anti-ai-llvm/source/passes/business-vm/BusinessVM.cpp">整函数虚拟化源码</a>、<a href="/assets/downloads/anti-ai-llvm/source/passes/business-vm/build.sh">构建入口</a>和<a href="/assets/downloads/anti-ai-llvm/source/passes/business-vm/README.rst">支持范围说明</a>。</li>
  <li><a href="/assets/downloads/anti-ai-llvm/artifacts/v4-blind-eval-20261030/results.json">逐轮评分与审计</a>、<a href="/assets/downloads/anti-ai-llvm/images/methodology/before-after-metrics.json">保护前后原始指标</a>、<a href="/assets/downloads/anti-ai-llvm/artifacts/v4-blind-eval-20261030/matched-instruction-cost.json">指令开销记录</a>、<a href="/assets/downloads/anti-ai-llvm/artifacts/v4-blind-eval-20261030.zip">完整证据包</a>。</li>
</ul>

<p>原始逐轮材料保留了实施时的目录与工具标识。正文按机制组织，源码和记录用于核对每一步实现与实验结论。</p>]]></content><author><name>我是小三</name></author><category term="Android" /><category term="Android" /><category term="Native" /><category term="LLVM" /><category term="O-MVLL" /><category term="AI逆向" /><category term="MCP" /><category term="REA" /><category term="代码保护" /><category term="虚拟化" /><summary type="html"><![CDATA[MCP 与 REA 让模型能够持续调用反编译、脚本和仿真工具，核心代码的分析与功能克隆更容易形成自动化闭环。本文从这条攻击链出发，讨论预算干扰、证据欺骗、运行时守卫和业务语义混淆，给出 Android Native 的 LLVM Pass 实现，并用独立盲测说明收益、代价与失守边界。]]></summary></entry><entry><title type="html">Android 无痕 Hook：基于 APatch KPM 与 ARM Debug 寄存器的硬件断点实战</title><link href="https://xiaosan-re.github.io/2026/07/02/android-hardware-breakpoint-hook/" rel="alternate" type="text/html" title="Android 无痕 Hook：基于 APatch KPM 与 ARM Debug 寄存器的硬件断点实战" /><published>2026-07-02T00:00:00+08:00</published><updated>2026-07-02T00:00:00+08:00</updated><id>https://xiaosan-re.github.io/2026/07/02/android-hardware-breakpoint-hook</id><content type="html" xml:base="https://xiaosan-re.github.io/2026/07/02/android-hardware-breakpoint-hook/"><![CDATA[<h2 id="一背景--场景">一、背景 &amp; 场景</h2>

<h3 id="11-环境">1.1 环境</h3>

<p>设备型号：Android arm64 真机、已刷 APatch / KernelPatch</p>

<p>内核模块：<code class="language-plaintext highlighter-rouge">khwbp.kpm</code></p>

<p>手机端控制器：<code class="language-plaintext highlighter-rouge">khwbpctl</code></p>

<p>分析工具：IDA Pro、adb、dmesg、APatch supercall</p>

<h3 id="12-引言">1.2 引言</h3>

<p>Android 逆向里，hook 也是绕不开的基本功。常见方案——Frida、Dobby、xhook、inline trampoline——大多数时候很好用，但碰到风控 SDK / 加固壳 / 反调试组合拳，就开始变得吵：</p>

<ul>
  <li>inline hook 改了目标函数开头几条指令，<code class="language-plaintext highlighter-rouge">__text</code> 校验一跑就露馅；</li>
  <li>Frida / gadget / gum-js-loop 这类特征太显眼，很多 SDK 已经按套餐扫；</li>
  <li><code class="language-plaintext highlighter-rouge">mprotect + RWX</code>、可疑匿名可执行页、跳板区、PLT/GOT 改写，都能被归到“环境异常”；</li>
  <li>部分目标会扫线程、端口、maps、符号、指令序言，甚至在关键函数前后自检。</li>
</ul>

<p>有没有办法不碰目标函数一字节代码，只靠 CPU 自己的地址比较器来“截住”执行？有，答案就是 <strong>ARM64 硬件断点</strong>。</p>

<p>Android 用户态不能直接写 EL1 Debug 寄存器，常规 ptrace/perf 路径又太显眼。要做得稳，最直接的路是进内核：用 APatch / KernelPatch 的 KPM，在内核侧写 <code class="language-plaintext highlighter-rouge">DBGBVR/DBGBCR</code>，hook <code class="language-plaintext highlighter-rouge">do_debug_exception</code>，命中后记录寄存器、读参数内存，甚至在异常返回前直接改目标进程数据。</p>

<p>本文记录的就是这套 <code class="language-plaintext highlighter-rouge">khwbp</code>：一个基于 APatch KPM 的 Android arm64 硬件断点 hook 小引擎。它支持按包名 + SO 文件偏移下断、App 重启后自动重挂、打印入口参数/返回值/指针内存，以及命中后自动写数据。文中目标 App、路径、指纹、密钥、明文等均做脱敏处理。</p>

<h3 id="13-对抗场景安全-sdk-的-hook-检测">1.3 对抗场景：安全 SDK 的 hook 检测</h3>

<p>这次动机同样来自实战。目标 App 内置商业安全 SDK，native 层有设备信息采集、签名、加密、环境检测等逻辑。直接 Frida stalk 或 inline hook，有几个明显问题：</p>

<ul>
  <li><strong>函数头改写会被扫</strong>：关键 native 函数入口如果变成 <code class="language-plaintext highlighter-rouge">B xxx</code>、<code class="language-plaintext highlighter-rouge">LDR/BR</code> 跳板，非常容易被发现；</li>
  <li><strong>maps 特征明显</strong>：Frida、gadget、匿名可执行页、注入 so 都是高频检测项；</li>
  <li><strong>代码段完整性校验</strong>：SDK 对自己的 so 做 CRC / hash，任何 patch 都可能导致结果不可信；</li>
  <li><strong>ptrace / perf / uprobes 等外部调试面</strong>：容易和反调试逻辑撞车。</li>
</ul>

<p>硬件断点的优势在这里很直接：</p>

<blockquote>
  <p>目标指令不变、函数头不变、method 表不变、GOT/PLT 不变；命中靠 CPU Debug 寄存器比较，异常由内核消费。检测面从“代码被改/进程被注入”收敛到“Debug 寄存器被设置”和“内核异常路径被接管”。</p>
</blockquote>

<h3 id="14-研究动机与目标">1.4 研究动机与目标</h3>

<ul>
  <li>搞清楚 Android arm64 上硬件断点从 <code class="language-plaintext highlighter-rouge">DBG*</code> 寄存器到 <code class="language-plaintext highlighter-rouge">do_debug_exception</code> 的完整路径；</li>
  <li>用 APatch KPM 做一套<strong>内核侧无痕 hook</strong>，不改目标 App 代码；</li>
  <li>支持真实逆向中最常用的工作流：按包名、按 SO 偏移、App 重启自动重挂；</li>
  <li>在真实加固 App 上验证参数、返回值、内存预览、命中后改数据是否可用。</li>
</ul>

<h2 id="二硬件断点原理">二、硬件断点原理</h2>

<h3 id="21-三层机制">2.1 三层机制</h3>

<p>Android arm64 上的硬件断点，本质也是 <strong>ARMv8 Debug 架构</strong> + <strong>Linux debug exception</strong> + <strong>KPM 接管异常处理</strong>。可以拆成三层：</p>

<p><strong>① 底层：ARM64 Debug 寄存器</strong></p>

<p>硬件断点不是 <code class="language-plaintext highlighter-rouge">BRK</code>，也不是改函数头，而是 CPU 里专门的比较器：</p>

<ul>
  <li>指令断点：<code class="language-plaintext highlighter-rouge">DBGBVR&lt;n&gt;_EL1</code>（地址）+ <code class="language-plaintext highlighter-rouge">DBGBCR&lt;n&gt;_EL1</code>（控制）</li>
  <li>数据观察点：<code class="language-plaintext highlighter-rouge">DBGWVR&lt;n&gt;_EL1</code> + <code class="language-plaintext highlighter-rouge">DBGWCR&lt;n&gt;_EL1</code></li>
  <li>数量由 <code class="language-plaintext highlighter-rouge">ID_AA64DFR0_EL1</code> 报告，实测设备常见是 <strong>6 个指令断点 + 4 个观察点</strong></li>
</ul>

<p><code class="language-plaintext highlighter-rouge">DBGBCR/DBGWCR</code> 的几个关键位：</p>

<table>
  <thead>
    <tr>
      <th>位</th>
      <th>名称</th>
      <th>含义</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>bit0</td>
      <td>E</td>
      <td>使能</td>
    </tr>
    <tr>
      <td>bits[2:1]</td>
      <td>PMC/PAC</td>
      <td>匹配 EL0 用户态访问</td>
    </tr>
    <tr>
      <td>bits[12:5]</td>
      <td>BAS</td>
      <td>Byte Address Select，选择命中字节</td>
    </tr>
    <tr>
      <td>bits[4:3]</td>
      <td>LSC</td>
      <td>watchpoint 读/写/读写匹配</td>
    </tr>
  </tbody>
</table>

<p><strong>② 内核层：EL0 不能直接写 Debug 寄存器</strong></p>

<p>App 在 EL0，没有权限执行 <code class="language-plaintext highlighter-rouge">MSR DBGBVR0_EL1, x0</code>。常规调试器靠内核的 perf/hw_breakpoint 或 ptrace 间接设置，但这些路径都太“正规”，也更容易被观察到。</p>

<p><code class="language-plaintext highlighter-rouge">khwbp</code> 选择绕过 perf 框架：KPM 运行在内核侧，直接用 <code class="language-plaintext highlighter-rouge">mrs/msr</code> 写 <code class="language-plaintext highlighter-rouge">DBGBVR/DBGBCR/DBGWVR/DBGWCR</code>。Debug 寄存器是 per-CPU 的，所以 slot 表更新后用 <code class="language-plaintext highlighter-rouge">on_each_cpu()</code> 同步到每个 CPU。</p>

<p><strong>③ 异常分发：hook <code class="language-plaintext highlighter-rouge">do_debug_exception</code></strong></p>

<p>当 EL0 执行到被下断地址，CPU 产生同步 Debug 异常，Linux/Android 内核进入 <code class="language-plaintext highlighter-rouge">do_debug_exception</code>。如果这是 perf/ptrace 认识的事件，内核会按正常调试流程处理；但我们直接写寄存器，原 handler 并不知道这些断点是谁的。</p>

<p>所以 KPM 必须 hook <code class="language-plaintext highlighter-rouge">do_debug_exception</code>：</p>

<ul>
  <li>如果 PC/FAR 命中 <code class="language-plaintext highlighter-rouge">khwbp</code> 的 slot，就记录现场并消费异常；</li>
  <li>如果不是我们的事件，就放回原 handler；</li>
  <li>消费后还要负责“踏过断点”，否则异常返回会立刻再次命中，目标线程原地打转。</li>
</ul>

<h3 id="22-为什么叫无痕">2.2 为什么叫”无痕”</h3>

<ol>
  <li><strong>目标代码段不改</strong>：不写 <code class="language-plaintext highlighter-rouge">B</code> 跳板，不破坏函数序言，过大多数 <code class="language-plaintext highlighter-rouge">__text</code> hash；</li>
  <li><strong>不依赖 Frida / inline trampoline</strong>：没有 gadget、gum、匿名跳板区等典型特征；</li>
  <li><strong>按 pid/包名过滤</strong>：同一个 so 偏移只作用于目标进程；</li>
  <li><strong>命中后可直接读/改寄存器现场对应的数据</strong>：入口参数、返回值、结构体字段都能在不插桩的情况下观察。</li>
</ol>

<p>代价也明确：硬件槽位很少，异常处理写错会把目标线程卡死，内核态代码一旦出错就是重启级别的反馈。</p>

<h2 id="三引擎实现">三、引擎实现</h2>

<p>整体流程分成<strong>控制期</strong>和<strong>命中期</strong>。控制期由 <code class="language-plaintext highlighter-rouge">khwbpctl</code> 解析包名、pid、SO 文件偏移，再通过 APatch supercall 把命令送进 KPM；命中期由 KPM 在 debug exception 里记录现场、执行 patch、恢复断点。</p>

<p><img src="/images/posts/android/khwbp/flow.png" alt="Android 硬件断点 hook 整体流程" /></p>

<p><img src="/images/posts/android/khwbp/flow-detail.png" alt="Android 硬件断点 hook 详细流程" /></p>

<h3 id="31-下断点">3.1 下断点</h3>

<p>KPM 初始化时先探测硬件槽位数量：</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">static</span> <span class="kr">inline</span> <span class="n">u32</span> <span class="nf">khwbp_num_brps</span><span class="p">(</span><span class="kt">void</span><span class="p">)</span> <span class="p">{</span>
    <span class="n">u64</span> <span class="n">dfr0</span><span class="p">;</span>
    <span class="n">asm</span> <span class="k">volatile</span><span class="p">(</span><span class="s">"mrs %0, id_aa64dfr0_el1"</span> <span class="o">:</span> <span class="s">"=r"</span><span class="p">(</span><span class="n">dfr0</span><span class="p">));</span>
    <span class="k">return</span> <span class="p">((</span><span class="n">u32</span><span class="p">)((</span><span class="n">dfr0</span> <span class="o">&gt;&gt;</span> <span class="mi">12</span><span class="p">)</span> <span class="o">&amp;</span> <span class="mh">0xf</span><span class="p">))</span> <span class="o">+</span> <span class="mi">1</span><span class="p">;</span>
<span class="p">}</span>

<span class="k">static</span> <span class="kr">inline</span> <span class="n">u32</span> <span class="nf">khwbp_num_wrps</span><span class="p">(</span><span class="kt">void</span><span class="p">)</span> <span class="p">{</span>
    <span class="n">u64</span> <span class="n">dfr0</span><span class="p">;</span>
    <span class="n">asm</span> <span class="k">volatile</span><span class="p">(</span><span class="s">"mrs %0, id_aa64dfr0_el1"</span> <span class="o">:</span> <span class="s">"=r"</span><span class="p">(</span><span class="n">dfr0</span><span class="p">));</span>
    <span class="k">return</span> <span class="p">((</span><span class="n">u32</span><span class="p">)((</span><span class="n">dfr0</span> <span class="o">&gt;&gt;</span> <span class="mi">20</span><span class="p">)</span> <span class="o">&amp;</span> <span class="mh">0xf</span><span class="p">))</span> <span class="o">+</span> <span class="mi">1</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

<p>指令断点控制位按 EL0 匹配来填：</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cp">#define DBG_CR_E        (1u &lt;&lt; 0)
#define DBG_CR_PRIV_EL0 (0b10u &lt;&lt; 1)
#define DBG_CR_SSC_NS   (0b00u &lt;&lt; 14)
#define DBG_CR_BAS(mask) (((u32)(mask) &amp; 0xff) &lt;&lt; 5)
</span>
<span class="k">static</span> <span class="kr">inline</span> <span class="kt">void</span> <span class="nf">khwbp_calc_bp</span><span class="p">(</span><span class="n">u64</span> <span class="n">addr</span><span class="p">,</span> <span class="n">u64</span> <span class="o">*</span><span class="n">out_val</span><span class="p">,</span> <span class="n">u32</span> <span class="o">*</span><span class="n">out_ctrl</span><span class="p">)</span> <span class="p">{</span>
    <span class="o">*</span><span class="n">out_val</span>  <span class="o">=</span> <span class="n">addr</span> <span class="o">&amp;</span> <span class="o">~</span><span class="mh">0x3ULL</span><span class="p">;</span>
    <span class="o">*</span><span class="n">out_ctrl</span> <span class="o">=</span> <span class="n">DBG_CR_E</span> <span class="o">|</span> <span class="n">DBG_CR_PRIV_EL0</span> <span class="o">|</span> <span class="n">DBG_CR_SSC_NS</span> <span class="o">|</span> <span class="n">DBG_CR_BAS</span><span class="p">(</span><span class="mh">0xf</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>

<p>写寄存器不能用变量拼名字，<code class="language-plaintext highlighter-rouge">DBGBVR0_EL1</code> 到 <code class="language-plaintext highlighter-rouge">DBGBVR15_EL1</code> 必须展开成 switch：</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cp">#define KHWBP_WR_CASE(n, reg) \
    case n: asm volatile("msr " #reg #n "_el1, %0" :: "r"(val)); break
</span>
<span class="k">static</span> <span class="kr">inline</span> <span class="kt">void</span> <span class="nf">dbg_write_reg</span><span class="p">(</span><span class="k">enum</span> <span class="n">dbg_reg_file</span> <span class="n">file</span><span class="p">,</span> <span class="kt">int</span> <span class="n">n</span><span class="p">,</span> <span class="n">u64</span> <span class="n">val</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">switch</span> <span class="p">(</span><span class="n">file</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">case</span> <span class="n">DBG_REG_BVR</span><span class="p">:</span>
        <span class="k">switch</span> <span class="p">(</span><span class="n">n</span><span class="p">)</span> <span class="p">{</span> <span class="n">KHWBP_WR_CASE</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="n">dbgbvr</span><span class="p">);</span> <span class="n">KHWBP_WR_CASE</span><span class="p">(</span><span class="mi">1</span><span class="p">,</span> <span class="n">dbgbvr</span><span class="p">);</span> <span class="cm">/* ... */</span> <span class="p">}</span>
        <span class="k">break</span><span class="p">;</span>
    <span class="k">case</span> <span class="n">DBG_REG_BCR</span><span class="p">:</span>
        <span class="k">switch</span> <span class="p">(</span><span class="n">n</span><span class="p">)</span> <span class="p">{</span> <span class="n">KHWBP_WR_CASE</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="n">dbgbcr</span><span class="p">);</span> <span class="n">KHWBP_WR_CASE</span><span class="p">(</span><span class="mi">1</span><span class="p">,</span> <span class="n">dbgbcr</span><span class="p">);</span> <span class="cm">/* ... */</span> <span class="p">}</span>
        <span class="k">break</span><span class="p">;</span>
    <span class="p">}</span>
    <span class="n">asm</span> <span class="k">volatile</span><span class="p">(</span><span class="s">"isb"</span> <span class="o">:::</span> <span class="s">"memory"</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>

<p>更新 slot 后同步所有 CPU：</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">static</span> <span class="kt">void</span> <span class="nf">hwbp_apply_cpu</span><span class="p">(</span><span class="kt">void</span> <span class="o">*</span><span class="n">unused</span><span class="p">)</span> <span class="p">{</span>
    <span class="n">khwbp_unlock_os_lock</span><span class="p">();</span>

    <span class="k">for</span> <span class="p">(</span><span class="kt">int</span> <span class="n">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">i</span> <span class="o">&lt;</span> <span class="n">n_brps</span><span class="p">;</span> <span class="n">i</span><span class="o">++</span><span class="p">)</span>
        <span class="n">dbg_write_reg</span><span class="p">(</span><span class="n">DBG_REG_BCR</span><span class="p">,</span> <span class="n">i</span><span class="p">,</span> <span class="mi">0</span><span class="p">);</span>

    <span class="k">for</span> <span class="p">(</span><span class="kt">int</span> <span class="n">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">,</span> <span class="n">bp</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">i</span> <span class="o">&lt;</span> <span class="n">KHWBP_MAX_SLOTS</span><span class="p">;</span> <span class="n">i</span><span class="o">++</span><span class="p">)</span> <span class="p">{</span>
        <span class="k">struct</span> <span class="n">slot</span> <span class="o">*</span><span class="n">s</span> <span class="o">=</span> <span class="o">&amp;</span><span class="n">slots</span><span class="p">[</span><span class="n">i</span><span class="p">];</span>
        <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="n">s</span><span class="o">-&gt;</span><span class="n">used</span> <span class="o">||</span> <span class="n">s</span><span class="o">-&gt;</span><span class="n">kind</span> <span class="o">!=</span> <span class="n">KHWBP_KIND_EXEC</span><span class="p">)</span>
            <span class="k">continue</span><span class="p">;</span>
        <span class="n">dbg_write_reg</span><span class="p">(</span><span class="n">DBG_REG_BVR</span><span class="p">,</span> <span class="n">bp</span><span class="p">,</span> <span class="n">s</span><span class="o">-&gt;</span><span class="n">val</span><span class="p">);</span>
        <span class="n">dbg_write_reg</span><span class="p">(</span><span class="n">DBG_REG_BCR</span><span class="p">,</span> <span class="n">bp</span><span class="p">,</span> <span class="n">s</span><span class="o">-&gt;</span><span class="n">ctrl</span><span class="p">);</span>
        <span class="n">bp</span><span class="o">++</span><span class="p">;</span>
    <span class="p">}</span>

    <span class="n">write_mdscr</span><span class="p">(</span><span class="n">read_mdscr</span><span class="p">()</span> <span class="o">|</span> <span class="n">MDSCR_EL1_MDE</span> <span class="o">|</span> <span class="n">MDSCR_EL1_KDE</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>

<h3 id="32-接管-do_debug_exception">3.2 接管 <code class="language-plaintext highlighter-rouge">do_debug_exception</code></h3>

<p>APatch / KernelPatch KPM 不能静态链接内核符号，初始化时用 <code class="language-plaintext highlighter-rouge">kallsyms_lookup_name</code> 找需要的函数：</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">R</span><span class="p">(</span><span class="n">printk</span><span class="p">,</span> <span class="s">"printk"</span><span class="p">,</span> <span class="s">"_printk"</span><span class="p">);</span>
<span class="n">R</span><span class="p">(</span><span class="n">on_each_cpu</span><span class="p">,</span> <span class="s">"on_each_cpu"</span><span class="p">);</span>
<span class="n">R</span><span class="p">(</span><span class="n">do_debug_exception</span><span class="p">,</span> <span class="s">"do_debug_exception"</span><span class="p">);</span>
<span class="n">R</span><span class="p">(</span><span class="n">migrate_disable</span><span class="p">,</span> <span class="s">"migrate_disable"</span><span class="p">);</span>
<span class="n">R</span><span class="p">(</span><span class="n">migrate_enable</span><span class="p">,</span> <span class="s">"migrate_enable"</span><span class="p">);</span>
<span class="n">R</span><span class="p">(</span><span class="n">send_sig</span><span class="p">,</span> <span class="s">"send_sig"</span><span class="p">);</span>
</code></pre></div></div>

<p>hook 入口里拿到 <code class="language-plaintext highlighter-rouge">far/esr/regs</code>，按 ESR 的 <code class="language-plaintext highlighter-rouge">DBG_ESR_EVT</code> 区分事件：</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">int</span> <span class="nf">hwbp_handle_debug</span><span class="p">(</span><span class="n">u64</span> <span class="n">far</span><span class="p">,</span> <span class="n">u64</span> <span class="n">esr</span><span class="p">,</span> <span class="k">struct</span> <span class="n">pt_regs_min</span> <span class="o">*</span><span class="n">regs</span><span class="p">)</span> <span class="p">{</span>
    <span class="n">u32</span> <span class="n">evt</span> <span class="o">=</span> <span class="p">(</span><span class="n">u32</span><span class="p">)((</span><span class="n">esr</span> <span class="o">&gt;&gt;</span> <span class="mi">27</span><span class="p">)</span> <span class="o">&amp;</span> <span class="mh">0x7</span><span class="p">);</span>

    <span class="k">if</span> <span class="p">(</span><span class="n">evt</span> <span class="o">==</span> <span class="mh">0x1</span><span class="p">)</span> <span class="p">{</span>
        <span class="c1">// software step 完成，复原断点</span>
    <span class="p">}</span>

    <span class="k">if</span> <span class="p">(</span><span class="n">evt</span> <span class="o">==</span> <span class="mh">0x0</span><span class="p">)</span> <span class="p">{</span>
        <span class="c1">// instruction breakpoint，匹配 PC</span>
    <span class="p">}</span> <span class="k">else</span> <span class="k">if</span> <span class="p">(</span><span class="n">evt</span> <span class="o">==</span> <span class="mh">0x2</span><span class="p">)</span> <span class="p">{</span>
        <span class="c1">// watchpoint，匹配 FAR</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>这里有一个坑：我们绕过了 perf/hw_breakpoint，原内核并不知道这些 Debug 异常是谁的。如果命中了我们的寄存器却返回给原 handler，轻则 SIGTRAP，重则内核把它当 spurious debug exception 处理。所以只要确认是自己的事件，就必须完整消费。</p>

<h3 id="33-踏过断点从软件单步改成-pc4-临时断点">3.3 踏过断点：从软件单步改成 PC+4 临时断点</h3>

<p>命中后最容易踩的坑是：<strong>怎么继续执行而不死循环</strong>？</p>

<p>第一版走经典方案：禁用当前断点，打开 <code class="language-plaintext highlighter-rouge">MDSCR_EL1.SS + PSTATE.SS</code>，单步执行一条后复原。watchpoint 用这个方案没问题；但 exec 断点在部分 Android 内核上会出现 <code class="language-plaintext highlighter-rouge">PSTATE</code>/debug mask 组合微妙、单步事件不稳定、head 疯涨、app 卡住等问题。</p>

<p>最终 exec 断点改成更稳的方案：<strong>把当前硬件断点临时改到 <code class="language-plaintext highlighter-rouge">PC+4</code></strong>。</p>

<p>流程：</p>

<ol>
  <li>入口 <code class="language-plaintext highlighter-rouge">PC</code> 命中；</li>
  <li>不关 slot，只把当前 CPU 的 <code class="language-plaintext highlighter-rouge">DBGBVR[n]</code> 改成 <code class="language-plaintext highlighter-rouge">PC+4</code>；</li>
  <li>异常返回后目标线程执行原指令；</li>
  <li>执行到下一条 <code class="language-plaintext highlighter-rouge">PC+4</code> 再命中一次；</li>
  <li>在这次“临时命中”里把 <code class="language-plaintext highlighter-rouge">DBGBVR[n]</code> 改回原地址。</li>
</ol>

<p>核心代码：</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">static</span> <span class="kt">int</span> <span class="nf">step_over_exec_next</span><span class="p">(</span><span class="kt">int</span> <span class="n">matched</span><span class="p">,</span> <span class="k">struct</span> <span class="n">slot</span> <span class="o">*</span><span class="n">s</span><span class="p">,</span>
                               <span class="k">struct</span> <span class="n">pt_regs_min</span> <span class="o">*</span><span class="n">regs</span><span class="p">)</span> <span class="p">{</span>
    <span class="kt">int</span> <span class="n">hw</span> <span class="o">=</span> <span class="n">slot_hw_index</span><span class="p">(</span><span class="n">matched</span><span class="p">);</span>
    <span class="n">u64</span> <span class="n">next_pc</span> <span class="o">=</span> <span class="p">(</span><span class="n">regs</span><span class="o">-&gt;</span><span class="n">pc</span> <span class="o">&amp;</span> <span class="o">~</span><span class="mh">0x3ULL</span><span class="p">)</span> <span class="o">+</span> <span class="mi">4</span><span class="p">;</span>

    <span class="k">if</span> <span class="p">(</span><span class="n">kfn</span><span class="p">.</span><span class="n">migrate_disable</span><span class="p">)</span>
        <span class="n">kfn</span><span class="p">.</span><span class="n">migrate_disable</span><span class="p">();</span>

    <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="n">step_claim</span><span class="p">(</span><span class="n">khwbp_current</span><span class="p">(),</span> <span class="n">matched</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="n">next_pc</span><span class="p">))</span>
        <span class="k">return</span> <span class="mi">1</span><span class="p">;</span>

    <span class="n">dbg_write_reg</span><span class="p">(</span><span class="n">DBG_REG_BVR</span><span class="p">,</span> <span class="n">hw</span><span class="p">,</span> <span class="n">next_pc</span><span class="p">);</span>
    <span class="n">dbg_write_reg</span><span class="p">(</span><span class="n">DBG_REG_BCR</span><span class="p">,</span> <span class="n">hw</span><span class="p">,</span> <span class="n">s</span><span class="o">-&gt;</span><span class="n">ctrl</span><span class="p">);</span>
    <span class="n">stat_temp_arm</span><span class="o">++</span><span class="p">;</span>
    <span class="k">return</span> <span class="mi">0</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

<p>临时命中时复原：</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">if</span> <span class="p">(</span><span class="n">evt</span> <span class="o">==</span> <span class="mh">0x0</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">struct</span> <span class="n">step_ent</span> <span class="o">*</span><span class="n">te</span> <span class="o">=</span> <span class="n">step_find</span><span class="p">(</span><span class="n">task</span><span class="p">);</span>
    <span class="k">if</span> <span class="p">(</span><span class="n">te</span> <span class="o">&amp;&amp;</span> <span class="n">te</span><span class="o">-&gt;</span><span class="n">temp_exec</span> <span class="o">&amp;&amp;</span> <span class="p">((</span><span class="n">regs</span><span class="o">-&gt;</span><span class="n">pc</span> <span class="o">&amp;</span> <span class="o">~</span><span class="mi">3ULL</span><span class="p">)</span> <span class="o">==</span> <span class="p">(</span><span class="n">te</span><span class="o">-&gt;</span><span class="n">pc</span> <span class="o">&amp;</span> <span class="o">~</span><span class="mi">3ULL</span><span class="p">)))</span> <span class="p">{</span>
        <span class="k">struct</span> <span class="n">slot</span> <span class="o">*</span><span class="n">ts</span> <span class="o">=</span> <span class="o">&amp;</span><span class="n">slots</span><span class="p">[</span><span class="n">te</span><span class="o">-&gt;</span><span class="n">slot</span><span class="p">];</span>
        <span class="kt">int</span> <span class="n">hw</span> <span class="o">=</span> <span class="n">slot_hw_index</span><span class="p">(</span><span class="n">te</span><span class="o">-&gt;</span><span class="n">slot</span><span class="p">);</span>
        <span class="n">dbg_write_reg</span><span class="p">(</span><span class="n">DBG_REG_BVR</span><span class="p">,</span> <span class="n">hw</span><span class="p">,</span> <span class="n">ts</span><span class="o">-&gt;</span><span class="n">val</span><span class="p">);</span>
        <span class="n">dbg_write_reg</span><span class="p">(</span><span class="n">DBG_REG_BCR</span><span class="p">,</span> <span class="n">hw</span><span class="p">,</span> <span class="n">ts</span><span class="o">-&gt;</span><span class="n">ctrl</span><span class="p">);</span>
        <span class="n">step_release</span><span class="p">(</span><span class="n">te</span><span class="p">);</span>
        <span class="n">migrate_enable</span><span class="p">();</span>
        <span class="k">return</span> <span class="mi">1</span><span class="p">;</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">dump</code> 里健康状态应该类似：</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>stats: bp=75 wp=0 arm=0 ss=0 found=0 miss=0 claimfail=0 temparm=75 temphit=75
</code></pre></div></div>

<p>也就是 exec 断点不再依赖软件单步，<code class="language-plaintext highlighter-rouge">temparm</code> 和 <code class="language-plaintext highlighter-rouge">temphit</code> 基本成对。</p>

<h3 id="34-pid-过滤btf-不稳就用户态解析">3.4 pid 过滤：BTF 不稳就用户态解析</h3>

<p>硬件 Debug 寄存器是 CPU 级资源，同一个地址如果别的进程也映射了相同 VA，理论上也会触发。实际要按 pid 过滤。</p>

<p>KPM 在异常里能拿到 <code class="language-plaintext highlighter-rouge">current</code>，但 <code class="language-plaintext highlighter-rouge">task_struct.pid</code> 偏移随内核版本变化。第一版尝试内核侧解析 BTF；后面发现部分设备上大 BTF blob 通过 control 上传不稳，于是改成：</p>

<ol>
  <li><code class="language-plaintext highlighter-rouge">khwbpctl btf</code> 在用户态读取 <code class="language-plaintext highlighter-rouge">/sys/kernel/btf/vmlinux</code>；</li>
  <li>用户态解析 <code class="language-plaintext highlighter-rouge">task_struct.pid</code> 偏移；</li>
  <li>通过 <code class="language-plaintext highlighter-rouge">pidoff &lt;off&gt;</code> 发给 KPM；</li>
  <li>KPM 异常里用 <code class="language-plaintext highlighter-rouge">*(int *)((char *)current + pid_off)</code> 读 pid。</li>
</ol>

<p>成功输出：</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>btf: task_struct.pid off=1584 (ok)
</code></pre></div></div>

<p>这一步很关键。没有 pid 偏移时，断点仍能命中，但 <code class="language-plaintext highlighter-rouge">pid=</code> 过滤和按包名追踪会变得不可靠。</p>

<h2 id="四封装成-apatch-kpm--控制器">四、封装成 APatch KPM + 控制器</h2>

<p>用户层和内核层的通信路径如下：</p>

<p><img src="/images/posts/android/khwbp/user-kernel-flow.png" alt="khwbp 用户层与内核通信交互" /></p>

<p>整体分两个产物：</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>kpm/khwbp.kpm      <span class="c"># APatch / KernelPatch 内核模块</span>
cli/khwbpctl       <span class="c"># Android arm64 静态控制器</span>
</code></pre></div></div>

<p>构建：</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">KP_DIR</span><span class="o">=</span>/path/to/KernelPatch <span class="se">\</span>
<span class="nv">NDK</span><span class="o">=</span>/path/to/android-ndk <span class="se">\</span>
./build.sh
</code></pre></div></div>

<p>加载：</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>adb push kpm/khwbp.kpm /sdcard/DCIM/khwbp.kpm
adb push cli/khwbpctl /data/local/tmp/khwbpctl

adb shell
su
<span class="nb">cd</span> /data/local/tmp
<span class="nb">chmod </span>755 khwbpctl

/system/bin/truncate &lt;key&gt; module load /sdcard/DCIM/khwbp.kpm
./khwbpctl &lt;key&gt; btf
./khwbpctl &lt;key&gt; selftest
</code></pre></div></div>

<p>如果 <code class="language-plaintext highlighter-rouge">apd</code> 没有 <code class="language-plaintext highlighter-rouge">kpm</code> 子命令，直接用 APatch 的 supercall 入口：</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>/system/bin/truncate &lt;key&gt; module unload khwbp
/system/bin/truncate &lt;key&gt; module load /sdcard/DCIM/khwbp.kpm
</code></pre></div></div>

<h3 id="41-按包名--so-文件偏移下断">4.1 按包名 + SO 文件偏移下断</h3>

<p>实战里 IDA 给你的通常是 <code class="language-plaintext highlighter-rouge">libxxx.so + file offset</code>，而不是运行时 VA。<code class="language-plaintext highlighter-rouge">khwbpctl</code> 会扫 <code class="language-plaintext highlighter-rouge">/proc/&lt;pid&gt;/maps</code>：</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">static</span> <span class="kt">int</span> <span class="nf">resolve_file_offset</span><span class="p">(</span><span class="kt">int</span> <span class="n">pid</span><span class="p">,</span> <span class="k">const</span> <span class="kt">char</span> <span class="o">*</span><span class="n">needle</span><span class="p">,</span>
                               <span class="kt">uint64_t</span> <span class="n">file_off</span><span class="p">,</span> <span class="kt">uint64_t</span> <span class="o">*</span><span class="n">out_va</span><span class="p">)</span> <span class="p">{</span>
    <span class="c1">// 解析 /proc/&lt;pid&gt;/maps:</span>
    <span class="c1">// start-end perms map_off dev inode path</span>
    <span class="c1">// 如果 file_off 落在 [map_off, map_off + size)，VA = start + file_off - map_off</span>
<span class="p">}</span>
</code></pre></div></div>

<p>一次性按包名下断：</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>./khwbpctl &lt;key&gt; addpkg x com.target.app libtarget.so 0x60FF18
</code></pre></div></div>

<p>App 会重启、pid 会变、ASLR 基址会变，所以更常用的是 watcher：</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>./khwbpctl &lt;key&gt; watchpkg x com.target.app libtarget.so 0x60FF18
</code></pre></div></div>

<p>输出类似：</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>watching x libtarget.so+0x60ff18 for package com.target.app
waiting for package com.target.app...
slot 0
armed package com.target.app -&gt; pid 21397 (com.target.app)
resolved libtarget.so+0x60ff18 -&gt; 0x6e69ba9f18 (.../lib/arm64/libtarget.so)
</code></pre></div></div>

<h3 id="42-tracepkg参数返回值内存预览">4.2 tracepkg：参数、返回值、内存预览</h3>

<p><code class="language-plaintext highlighter-rouge">tracepkg</code> 是日常分析最舒服的命令：</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>./khwbpctl &lt;key&gt; tracepkg com.target.app libtarget.so 0x60FF18 0x100
</code></pre></div></div>

<p>命中入口打印：</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>== call pc=0x6e69ba9f18 target=0x6e69ba9f18 tid=21397
   x0=0xb40000703b029450 x1=0x000000005d922f68 x2=0x0000007fc5558ab8 x3=0x62
   x4=0x0000000000000000 x5=0xb4000070db032448 x6=0x2cd0 x7=0x5da9b390
   sp=0x0000007fc55589c0 lr=0x0000006ff65a3e34
</code></pre></div></div>

<p>返回值靠临时给 LR 再下一个 exec 断点，并用 <code class="language-plaintext highlighter-rouge">LR + SP</code> 配对入口：</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>== ret  pc=0x6ff65a3e34 target=0x6ff65a3e34 tid=21397 call_sp=0x0000007fc5558f40 x0=0x0
   call x0=0xb40000703b029450 x1=0x000000005d922f68 x2=0x0000007fc5559038 x3=0x0
</code></pre></div></div>

<p>如果参数像指针，还会从 <code class="language-plaintext highlighter-rouge">/proc/&lt;pid&gt;/mem</code> 读一段预览：</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>mem x2=0x0000007fc5559038 len=256
  0000: 50 e9 c5 f6 6f 00 00 00 ... |P...o...|
</code></pre></div></div>

<p>这样不需要 Frida、不需要注入 JavaScript，就能先把函数形态、参数角色、返回值行为摸清楚。</p>

<h3 id="43-命中后自动改数据">4.3 命中后自动改数据</h3>

<p>观察只是第一步，有时还需要“命中后改一个参数 / 改一个结构体字段，然后继续执行”。这也是硬件断点 hook 最像 hook 的地方。</p>

<p>KPM slot 上可以挂一个 patch action：</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">struct</span> <span class="n">slot</span> <span class="p">{</span>
    <span class="kt">int</span> <span class="n">patch_enabled</span><span class="p">;</span>
    <span class="kt">int</span> <span class="n">patch_base_reg</span><span class="p">;</span>   <span class="c1">// 0..30=x0..x30, 31=sp, -1=abs, -2=far</span>
    <span class="n">u64</span> <span class="n">patch_off</span><span class="p">;</span>
    <span class="n">u32</span> <span class="n">patch_len</span><span class="p">;</span>
    <span class="n">u8</span>  <span class="n">patch_bytes</span><span class="p">[</span><span class="mi">32</span><span class="p">];</span>
<span class="p">};</span>
</code></pre></div></div>

<p>命中后在异常处理里计算目标地址并写用户态内存：</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">static</span> <span class="kt">int</span> <span class="nf">apply_patch_action</span><span class="p">(</span><span class="k">struct</span> <span class="n">slot</span> <span class="o">*</span><span class="n">s</span><span class="p">,</span> <span class="n">u64</span> <span class="n">far</span><span class="p">,</span>
                              <span class="k">struct</span> <span class="n">pt_regs_min</span> <span class="o">*</span><span class="n">regs</span><span class="p">)</span> <span class="p">{</span>
    <span class="n">u64</span> <span class="n">base</span><span class="p">,</span> <span class="n">dst</span><span class="p">;</span>

    <span class="k">if</span> <span class="p">(</span><span class="n">s</span><span class="o">-&gt;</span><span class="n">patch_base_reg</span> <span class="o">&gt;=</span> <span class="mi">0</span> <span class="o">&amp;&amp;</span> <span class="n">s</span><span class="o">-&gt;</span><span class="n">patch_base_reg</span> <span class="o">&lt;=</span> <span class="mi">30</span><span class="p">)</span>
        <span class="n">base</span> <span class="o">=</span> <span class="n">regs</span><span class="o">-&gt;</span><span class="n">regs</span><span class="p">[</span><span class="n">s</span><span class="o">-&gt;</span><span class="n">patch_base_reg</span><span class="p">];</span>
    <span class="k">else</span> <span class="k">if</span> <span class="p">(</span><span class="n">s</span><span class="o">-&gt;</span><span class="n">patch_base_reg</span> <span class="o">==</span> <span class="mi">31</span><span class="p">)</span>
        <span class="n">base</span> <span class="o">=</span> <span class="n">regs</span><span class="o">-&gt;</span><span class="n">sp</span><span class="p">;</span>
    <span class="k">else</span> <span class="k">if</span> <span class="p">(</span><span class="n">s</span><span class="o">-&gt;</span><span class="n">patch_base_reg</span> <span class="o">==</span> <span class="o">-</span><span class="mi">2</span><span class="p">)</span>
        <span class="n">base</span> <span class="o">=</span> <span class="n">far</span><span class="p">;</span>
    <span class="k">else</span>
        <span class="n">base</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>

    <span class="n">dst</span> <span class="o">=</span> <span class="n">untag_user_ptr</span><span class="p">(</span><span class="n">base</span> <span class="o">+</span> <span class="n">s</span><span class="o">-&gt;</span><span class="n">patch_off</span><span class="p">);</span>
    <span class="k">return</span> <span class="n">kfn</span><span class="p">.</span><span class="n">copy_to_user</span><span class="p">((</span><span class="kt">void</span> <span class="o">*</span><span class="p">)</span><span class="n">dst</span><span class="p">,</span> <span class="n">s</span><span class="o">-&gt;</span><span class="n">patch_bytes</span><span class="p">,</span> <span class="n">s</span><span class="o">-&gt;</span><span class="n">patch_len</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>

<p>命令：</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># 命中 libtarget.so+0x60FF18 时，把 x2 指向内存前 4 字节改成 01 02 03 04</span>
./khwbpctl &lt;key&gt; watchpatchpkg com.target.app libtarget.so 0x60FF18 x2 0 01020304

<span class="c"># 命中时写 x0+0x10，小端写入 0x12345678</span>
./khwbpctl &lt;key&gt; watchpatchpkg com.target.app libtarget.so 0x60FF18 x0 0x10 78563412

<span class="c"># 命中时写固定地址</span>
./khwbpctl &lt;key&gt; watchpatchpkg com.target.app libtarget.so 0x60FF18 abs 0x7fc5558ab8 01000000
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">dump</code> 看写入是否成功：</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>patchok=12 patchfail=0 patch_addr=0x7fc5558ab8
</code></pre></div></div>

<p>这类写法适合改堆、栈、参数指针、返回结构体字段。写只读代码页不一定成功，因为 <code class="language-plaintext highlighter-rouge">copy_to_user</code> 仍然尊重页权限；要改代码段，最好提前通过 <code class="language-plaintext highlighter-rouge">/proc/&lt;pid&gt;/mem</code> 或别的方式处理映射权限。</p>

<h2 id="五实战追踪某加固-app-的-native-风控函数">五、实战：追踪某加固 App 的 native 风控函数</h2>

<h3 id="51-选目标">5.1 选目标</h3>

<p>IDA 里定位到目标 so 的一个 native 函数，偏移记为 <code class="language-plaintext highlighter-rouge">libtarget.so+0x60FF18</code>。这个函数在设备信息采集链路里被频繁调用，入口参数里有几类东西：</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">x0/x1</code>：疑似上下文对象或输入 buffer；</li>
  <li><code class="language-plaintext highlighter-rouge">x2</code>：栈上/堆上的输出结构指针；</li>
  <li><code class="language-plaintext highlighter-rouge">x3</code>：长度、类型或状态值；</li>
  <li><code class="language-plaintext highlighter-rouge">lr</code>：调用点稳定，可以顺手追返回值。</li>
</ul>

<p>函数体做了控制流混淆和大量内联逻辑，但我们只在入口和返回地址看寄存器，不需要理解每个 basic block。</p>

<p>先加载模块并初始化：</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>/system/bin/truncate &lt;key&gt; module load /sdcard/DCIM/khwbp.kpm
./khwbpctl &lt;key&gt; btf
./khwbpctl &lt;key&gt; selftest
</code></pre></div></div>

<h3 id="52-入口--返回追踪">5.2 入口 + 返回追踪</h3>

<p>启动追踪：</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>./khwbpctl &lt;key&gt; tracepkg com.target.app libtarget.so 0x60FF18 0x100
</code></pre></div></div>

<p>命中后能看到入口参数：</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>== call pc=0x6e69ba9f18 target=0x6e69ba9f18 tid=21397
   x0=0xb40000703b029450 x1=0x000000005d922f68 x2=0x0000007fc5558ab8 x3=0x62
   x4=0x0000000000000000 x5=0xb4000070db032448 x6=0x2cd0 x7=0x5da9b390
   sp=0x0000007fc55589c0 lr=0x0000006ff65a3e34
   mem x2=0x0000007fc5558ab8 len=256
     0000: 00 00 00 00 90 24 03 db 70 00 00 b4 ... |.....$..p...|
</code></pre></div></div>

<p>返回时：</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>== ret  pc=0x6ff65a3e34 target=0x6ff65a3e34 tid=21397 call_sp=0x0000007fc5558f40 x0=0x0
   call x0=0xb40000703b029450 x1=0x000000005d922f68 x2=0x0000007fc5559038 x3=0x0
   mem call.x2=0x0000007fc5559038 len=256
     0000: 01 00 00 00 40 00 00 00 ... |....@...|
</code></pre></div></div>

<p>这里有两个实战判断：</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">ret x0=0</code> 不一定代表失败，很多 native 函数返回状态码，真正输出写在 <code class="language-plaintext highlighter-rouge">x2</code> 指向的结构里；</li>
  <li>TBI tag 要去掉，<code class="language-plaintext highlighter-rouge">0xb4000070...</code> 这种指针读内存时要按低 56 位地址处理。</li>
</ul>

<h3 id="53-命中后改字段">5.3 命中后改字段</h3>

<p>确认 <code class="language-plaintext highlighter-rouge">x2</code> 是输出结构后，就可以在入口命中时先把某个字段改掉，让原函数继续跑：</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>./khwbpctl &lt;key&gt; watchpatchpkg com.target.app libtarget.so 0x60FF18 x2 0x10 01000000
</code></pre></div></div>

<p>检查状态：</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>./khwbpctl &lt;key&gt; list
./khwbpctl &lt;key&gt; dump
</code></pre></div></div>

<p>输出类似：</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>slot 0 exec @0x6e69ba9f18 len=4 pid=21397 patch=x2+0x10:01000000
stats: bp=28 ... temparm=28 temphit=28 patchok=28 patchfail=0 patch_addr=0x7fc5559048
</code></pre></div></div>

<p>这样就实现了一个很实用的效果：不改函数头、不插 trampoline，函数每次执行到入口时自动改参数/结构体字段，然后继续执行。</p>

<h2 id="六坑与优化">六、坑与优化</h2>

<p>这套东西能跑起来不难，长期挂在真实 App 上才会露出问题。几个坑都比较硬。</p>

<h3 id="61-apatch-cli-不支持-apd-kpm">6.1 APatch CLI 不支持 <code class="language-plaintext highlighter-rouge">apd kpm</code></h3>

<p>有些设备上：</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>/data/adb/ap/bin/apd kpm load ...
error: unrecognized subcommand 'kpm'
</code></pre></div></div>

<p>直接用 supercall：</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>/system/bin/truncate &lt;key&gt; module load /sdcard/DCIM/khwbp.kpm
/system/bin/truncate &lt;key&gt; module unload khwbp
</code></pre></div></div>

<p>注意别把 key 写成空变量，否则会变成系统自带 <code class="language-plaintext highlighter-rouge">truncate</code> 的普通参数解析，报：</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>truncate: Needs -s
</code></pre></div></div>

<h3 id="62-btf-err大-btf-不适合直接塞-control">6.2 <code class="language-plaintext highlighter-rouge">btf: err</code>：大 BTF 不适合直接塞 control</h3>

<p>早期让 KPM 从 control 通道拿 BTF blob，部分机器直接失败。后面改成用户态解析 <code class="language-plaintext highlighter-rouge">/sys/kernel/btf/vmlinux</code>，只把 <code class="language-plaintext highlighter-rouge">task_struct.pid</code> 偏移发给内核：</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>./khwbpctl &lt;key&gt; btf
</code></pre></div></div>

<p>成功：</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>btf: task_struct.pid off=1584 (ok)
</code></pre></div></div>

<p>这比在内核里吞几 MB BTF 稳很多。</p>

<h3 id="63-exec-断点不要迷信软件单步">6.3 exec 断点不要迷信软件单步</h3>

<p>理论上“禁用断点 + 单步 + 复原”最正统，但实测某些 Android 内核组合下 exec 断点会卡在单步恢复。症状：</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>head 疯涨
hookstat consumed 很高
last_pc 一直是目标 PC
App 卡住
</code></pre></div></div>

<p>改成 <code class="language-plaintext highlighter-rouge">PC+4</code> 临时断点后，健康状态变成：</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>temparm=N temphit=N ss=0
</code></pre></div></div>

<p>这个方案对固定 4 字节 AArch64 指令非常顺手。</p>

<h3 id="64-tid-和-pid-不是一回事">6.4 TID 和 PID 不是一回事</h3>

<p>异常发生在线程上，记录到的 <code class="language-plaintext highlighter-rouge">pid</code> 字段实际更接近当前 task 的 pid/tid。目标 App 主进程 pid 和工作线程 tid 不一定相同。早期用户态错误地用 <code class="language-plaintext highlighter-rouge">h-&gt;pid != pid</code> 过滤，导致明明命中了 ring buffer，却 <code class="language-plaintext highlighter-rouge">tracepkg</code> 不打印。</p>

<p>修复方式：KPM 里 pid 过滤只用于确认进程归属；用户态 trace 不再把线程 tid 当主 pid 强过滤。</p>

<h3 id="65-返回值追踪会吃硬件槽">6.5 返回值追踪会吃硬件槽</h3>

<p><code class="language-plaintext highlighter-rouge">tracepkg</code> 为了抓返回值，会在入口命中后给 LR 再下一个临时 exec 断点。设备如果只有 6 个 breakpoint，入口占 1 个，返回地址再占若干个；并发调用太多时，部分返回值可能会跳过，但入口参数仍然可靠。</p>

<h3 id="66-命中后写数据只适合用户态可写页">6.6 命中后写数据只适合用户态可写页</h3>

<p><code class="language-plaintext highlighter-rouge">watchpatchpkg</code> 在异常处理里用 <code class="language-plaintext highlighter-rouge">copy_to_user</code> 写目标进程地址。它适合：</p>

<ul>
  <li>改 <code class="language-plaintext highlighter-rouge">x0/x1/x2</code> 指向的 buffer；</li>
  <li>改栈上参数；</li>
  <li>改输出结构字段；</li>
  <li>改返回结构里的 flag。</li>
</ul>

<p>但它不适合直接写只读代码页。写代码段如果页权限不允许，<code class="language-plaintext highlighter-rouge">patchfail</code> 会涨。这个限制反而是好事：异常上下文里少做危险操作，稳定性比“什么都能写”更重要。</p>

<h2 id="七总结">七、总结</h2>

<table>
  <thead>
    <tr>
      <th> </th>
      <th>inline hook / Frida</th>
      <th>Android KPM 硬件断点 hook</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>改目标代码</td>
      <td>✅ 通常要改函数头或插 trampoline</td>
      <td>❌ 不改目标指令</td>
    </tr>
    <tr>
      <td>过代码完整性</td>
      <td>❌ 容易被 CRC/hash 发现</td>
      <td>✅ 目标 text 不变</td>
    </tr>
    <tr>
      <td>注入特征</td>
      <td>明显，maps/线程/符号都可能暴露</td>
      <td>控制面在 KPM，用户态痕迹少</td>
    </tr>
    <tr>
      <td>数量限制</td>
      <td>基本无限</td>
      <td>硬件槽很少，常见 6 个 exec</td>
    </tr>
    <tr>
      <td>核心机制</td>
      <td>inline patch / JS runtime / PLT/GOT</td>
      <td><code class="language-plaintext highlighter-rouge">DBG*</code> 寄存器 + <code class="language-plaintext highlighter-rouge">do_debug_exception</code></td>
    </tr>
    <tr>
      <td>额外能力</td>
      <td>成熟生态，脚本方便</td>
      <td>可在命中异常里读寄存器、改用户态数据</td>
    </tr>
  </tbody>
</table>

<p>硬件断点 hook 不是 Frida/Dobby 的替代品，而是一个很适合“安静观察”的补充工具：当目标做了代码完整性校验、反 inline hook、反注入，或者你只想在关键 native 函数入口拿一眼参数，它非常好用。</p>

<p>Android 这条路比较硬：要进 KPM、写 Debug 寄存器、接管 <code class="language-plaintext highlighter-rouge">do_debug_exception</code>，还得自己处理 pid 过滤、单步恢复、App 重启重挂。但一旦跑通，收益也很直接：目标 App 代码一字节不动，入口参数、返回值、内存预览、命中后改数据都能做。</p>

<p>实战里，<code class="language-plaintext highlighter-rouge">khwbp</code> 已经能完成一条完整链路：</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>/system/bin/truncate &lt;key&gt; module load /sdcard/DCIM/khwbp.kpm
./khwbpctl &lt;key&gt; btf
./khwbpctl &lt;key&gt; tracepkg com.target.app libtarget.so 0x60FF18 0x100
./khwbpctl &lt;key&gt; watchpatchpkg com.target.app libtarget.so 0x60FF18 x2 0x10 01000000
</code></pre></div></div>

<p>剩下可以继续优化的方向也很明确：多 slot 调度、按调用栈过滤、返回点批量复用、watchpoint 的读写条件细化，以及把命中日志输出成更适合 IDA/Ghidra 对照的格式。</p>

<blockquote>
  <p>本文涉及的逆向均在自有设备、用于安全研究与对抗学习，请勿用于非法用途。</p>
</blockquote>]]></content><author><name>我是小三</name></author><category term="Android" /><category term="Android" /><category term="Hook" /><category term="硬件断点" /><category term="逆向" /><category term="ARM64" /><category term="APatch" /><category term="KernelPatch" /><category term="KPM" /><summary type="html"><![CDATA[从 ARM64 Debug 寄存器出发，在 Android 内核侧用 APatch / KernelPatch KPM 接管硬件断点异常，实现不改目标代码的参数追踪、返回值捕获、命中后自动改数据，并实战观察某加固 App 的 native 风控函数。]]></summary></entry><entry><title type="html">逆向某 AI App：设备注册与 AI 对话的完整协议分析</title><link href="https://xiaosan-re.github.io/2026/06/11/ai-app-protocol/" rel="alternate" type="text/html" title="逆向某 AI App：设备注册与 AI 对话的完整协议分析" /><published>2026-06-11T00:00:00+08:00</published><updated>2026-06-11T00:00:00+08:00</updated><id>https://xiaosan-re.github.io/2026/06/11/ai-app-protocol</id><content type="html" xml:base="https://xiaosan-re.github.io/2026/06/11/ai-app-protocol/"><![CDATA[<h2 id="一背景--场景">一、背景 &amp; 场景</h2>

<h3 id="11-环境">1.1 环境</h3>

<p>设备：iPhone 14 / iOS 16.4.1 / 越狱（Dopamine）
工具链：Frida 16.1.4、IDA Pro 9.3、基于 <a href="https://github.com/unicorn-engine/unicorn">Unicorn</a> 自建的 iOS 模拟执行环境、Charles
目标：某 AI 对话 App，iOS 端</p>

<blockquote>
  <p>为避免针对具体业务，下文统一称“某 App”，主机名、bot_id、device_id 等做了脱敏，不影响技术结论。</p>
</blockquote>

<h3 id="12-引言">1.2 引言</h3>

<p>逆向一个 App 的网络协议，要把整条链拆清楚，绕不开三道关：</p>

<ol>
  <li><strong>设备指纹采集</strong>——一个请求 body 里塞了几十个设备字段，得厘清每个从哪来；</li>
  <li><strong>请求体加密</strong>（TTEncrypt）——body 是二进制密文；</li>
  <li><strong>请求签名</strong>（metasec / Argus 套件）——<code class="language-plaintext highlighter-rouge">X-Gorgon</code> 等一堆头，算法藏在重度混淆的 native 代码里。</li>
</ol>

<p>本文围绕两个核心接口——<code class="language-plaintext highlighter-rouge">device_register</code>（设备注册）和 <code class="language-plaintext highlighter-rouge">chat/completion</code>（AI 对话）——逐层拆。前两关可以静态还原；签名因 OLLVM + 国密混淆，静态逆向性价比极低。这里有个分析上的取舍：<strong>签名算法静态难以推进时，与其耗在静态分析上，不如把真实二进制放进受控模拟环境执行、直接观测它产出的签名头</strong>——「读懂算法」暂时换成「动态观测产物」，先把协议这条链验证通，再回头深挖算法本身（第六节）。</p>

<h3 id="13-研究动机与目标">1.3 研究动机与目标</h3>

<ul>
  <li>设备注册的 body 里那几十个字段，<strong>分别从哪采集、怎么组合、怎么加密</strong>？</li>
  <li>纯协议生成的 <code class="language-plaintext highlighter-rouge">device_id</code>，服务器<strong>认不认、能不能用</strong>？</li>
  <li>那一堆 <code class="language-plaintext highlighter-rouge">X-Gorgon / X-Argus</code> 签名头，<strong>哪些接口真校验</strong>？签名算法 OLLVM 混淆到读不动时，有没有<strong>不逆算法也能拿到正确签名</strong>的办法？</li>
</ul>

<h3 id="14-整体流程与框架">1.4 整体流程与框架</h3>

<p>两条最终跑通的链路，整体架构如下：</p>

<p><img src="/images/posts/ios/ai-app-protocol/01-architecture.png" alt="整体架构：设备注册与 AI 对话两条链路" />
<em>图 1：整体架构——设备注册与 AI 对话两条链路</em></p>

<p>一个反直觉的结论先抛出来：<strong><code class="language-plaintext highlighter-rouge">device_register</code> 服务端不校验签名，<code class="language-plaintext highlighter-rouge">chat/completion</code> 校验。</strong> 这决定了模拟执行产出的签名，在前者“可选”、在后者“必需”。</p>

<hr />

<h2 id="二设备注册采集--组合--加密--请求">二、设备注册：采集 → 组合 → 加密 → 请求</h2>

<h3 id="21-整体流程">2.1 整体流程</h3>

<p>设备注册不是“填个表发出去”，而是一条流水线。先看全貌，再逐段拆：</p>

<p><img src="/images/posts/ios/ai-app-protocol/02-register-pipeline.png" alt="设备注册流水线：采集 → 组合 → gzip+加密 → 拼请求" />
<em>图 2：设备注册流水线——采集 → 组合 → gzip+加密 → 拼请求</em></p>

<h3 id="22-采集设备字段从哪来">2.2 采集：设备字段从哪来</h3>

<p>body 的 <code class="language-plaintext highlighter-rouge">header{}</code> 由 BDInstall（RangersAppLog）逐项采集。关键入口 <code class="language-plaintext highlighter-rouge">+[BDInstallNetworkUtility commonURLParameters]</code> / <code class="language-plaintext highlighter-rouge">onTheFlyParameter</code>，外加一层“混淆采集” <code class="language-plaintext highlighter-rouge">+[BDInstallExtraParams extraParams]</code>（字段名用 base64 藏起来，搜索不到字符串）。</p>

<table>
  <thead>
    <tr>
      <th>body 字段</th>
      <th>采集来源</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">device_model</code></td>
      <td><code class="language-plaintext highlighter-rouge">sysctl hw.machine</code>（如 iPhone14,7）</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">hardware_model</code></td>
      <td><code class="language-plaintext highlighter-rouge">sysctl hw.model</code>（如 D27AP）——base64 key 混淆采集</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">resolution</code></td>
      <td><code class="language-plaintext highlighter-rouge">UIScreen.nativeBounds × scale</code>（1170*2532）</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">vendor_id</code></td>
      <td>IDFV，<code class="language-plaintext highlighter-rouge">UIDevice.identifierForVendor</code></td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">idfa</code></td>
      <td>ATT <code class="language-plaintext highlighter-rouge">trackingIdentifier</code>（未授权返回全 0）</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">cdid</code></td>
      <td>本地持久化的 UUID（首启生成、长期不变）</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">os_version</code></td>
      <td><code class="language-plaintext highlighter-rouge">UIDevice.systemVersion</code></td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">mcc_mnc</code></td>
      <td>CoreTelephony 运营商</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">access</code> / <code class="language-plaintext highlighter-rouge">ac</code></td>
      <td>网络类型（WIFI…）</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">timezone</code> / <code class="language-plaintext highlighter-rouge">tz_name</code></td>
      <td><code class="language-plaintext highlighter-rouge">NSTimeZone</code></td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">mb_time</code> / <code class="language-plaintext highlighter-rouge">device_init_time</code></td>
      <td>SDK 首启 / 设备初始化时戳——base64 key 混淆采集</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">aid</code> / <code class="language-plaintext highlighter-rouge">channel</code> / <code class="language-plaintext highlighter-rouge">appName</code></td>
      <td>全局配置</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">device_id</code> / <code class="language-plaintext highlighter-rouge">install_id</code> / <code class="language-plaintext highlighter-rouge">device_token</code></td>
      <td>文件 / Keychain；<strong>首次注册为空，由服务器分配</strong></td>
    </tr>
  </tbody>
</table>

<p>带“base64-key 混淆采集”的那几个，反编译里长这样——字段名藏在 base64，值现场取：</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// +[BDInstallExtraParams extraParams]</span>
<span class="n">v8</span>  <span class="o">=</span> <span class="o">+</span><span class="p">[</span><span class="n">BDInstallAxtraLT</span>      <span class="n">getMB0</span><span class="p">];</span>   <span class="c1">// mb_time（SDK 首启时戳）</span>
<span class="n">v12</span> <span class="o">=</span> <span class="o">+</span><span class="p">[</span><span class="n">BDInstallAxtraModUtils</span> <span class="n">getMO0</span><span class="p">];</span>   <span class="c1">// hardware_model</span>
<span class="n">v20</span> <span class="o">=</span> <span class="o">+</span><span class="p">[</span><span class="n">BDInstallAxtraLT</span>      <span class="n">getDI0</span><span class="p">];</span>   <span class="c1">// device_init_time</span>
</code></pre></div></div>

<p>而 <code class="language-plaintext highlighter-rouge">getMO0</code> 往下就是一次 <code class="language-plaintext highlighter-rouge">sysctlbyname</code>，交叉引用看得很直白：</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>[api 'hw.model']    &lt;- +[BDInstallAxtraModUtils getMO0]      →  "D27AP"      (hardware_model)
[api 'hw.machine']  &lt;- +[BDInstallNetworkUtility platform]    →  "iPhone14,7" (device_model)
</code></pre></div></div>

<blockquote>
  <p>关键洞察：真正决定“这是哪台设备”的，是 <code class="language-plaintext highlighter-rouge">vendor_id</code>(IDFV) + <code class="language-plaintext highlighter-rouge">cdid</code> 这组<strong>客户端生成的稳定标识</strong>。服务器据此判断新老设备。把这两个换成全新 UUID、<code class="language-plaintext highlighter-rouge">device_id</code> 留空，就等于“一台没见过的新手机”。</p>
</blockquote>

<h3 id="23-组合拼成请求-body">2.3 组合：拼成请求 body</h3>

<p>采集完，BDInstall 序列化层把字段拼成一个固定结构的 JSON：</p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w"> </span><span class="nl">"magic_tag"</span><span class="p">:</span><span class="w"> </span><span class="s2">"ss_app_log"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"header"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nl">"device_id"</span><span class="p">:</span><span class="s2">""</span><span class="p">,</span><span class="w"> </span><span class="nl">"vendor_id"</span><span class="p">:</span><span class="s2">"&lt;IDFV&gt;"</span><span class="p">,</span><span class="w"> </span><span class="nl">"cdid"</span><span class="p">:</span><span class="s2">"&lt;UUID&gt;"</span><span class="p">,</span><span class="w">
              </span><span class="nl">"device_model"</span><span class="p">:</span><span class="s2">"iPhone14,7"</span><span class="p">,</span><span class="w"> </span><span class="nl">"hardware_model"</span><span class="p">:</span><span class="s2">"D27AP"</span><span class="p">,</span><span class="w">
              </span><span class="nl">"resolution"</span><span class="p">:</span><span class="s2">"1170*2532"</span><span class="p">,</span><span class="w"> </span><span class="nl">"os_version"</span><span class="p">:</span><span class="s2">"16.4.1"</span><span class="p">,</span><span class="w"> </span><span class="nl">"region"</span><span class="p">:</span><span class="s2">"TW"</span><span class="p">,</span><span class="w">
              </span><span class="nl">"custom"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nl">"mcc_mnc"</span><span class="p">:</span><span class="s2">"..."</span><span class="p">,</span><span class="w"> </span><span class="nl">"flow_app_variant"</span><span class="p">:</span><span class="s2">"..."</span><span class="p">,</span><span class="w"> </span><span class="err">...</span><span class="w"> </span><span class="p">},</span><span class="w">
              </span><span class="err">...</span><span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p>组合链：</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>-[BDInstallRequestSerializer commonHandleWithRequest:params:]   @0x10d53f428
  params → btd_safeJsonStringEncoded（紧凑 JSON，无空格）
         → dataByGZipCompressingWithError（gzip = P）
         → bd_dataByDecorated → bd_dataByRandom（@0x10071562c）
         → 核心 sub_10D48009C（@0x10d48009c）
</code></pre></div></div>

<h3 id="24-加密ttencrypt">2.4 加密：TTEncrypt</h3>

<p>抓包里 body 是这样的：</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Content-Type: application/octet-stream;tt-data=a
body:  74 63 05 10 00 00 | &lt;32 字节 R&gt; | &lt;AES 密文&gt;
</code></pre></div></div>

<p>头 4 字节 <code class="language-plaintext highlighter-rouge">74 63 05 10</code>（小端 magic <code class="language-plaintext highlighter-rouge">0x10056374</code>）是 TTEncrypt 帧头。逐个 hook 核心里的原生原语 dump 出 <code class="language-plaintext highlighter-rouge">R / key / iv / inner / out</code>，算法就清晰了：</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>R   = 32 随机字节                              # 任意 32 字节都行
key = SHA512( SHA512(R) || CONST64 )[0:16]     # AES-128 key
iv  = SHA512( SHA512(R) || CONST64 )[16:32]    # iv
inner = SHA512(P) || P                          # P = gzip(紧凑JSON)
body  = AES-128-CBC-PKCS7(key, iv, inner)
out   = 74 63 05 10 00 00 || R || body
</code></pre></div></div>

<p><img src="/images/posts/ios/ai-app-protocol/03-ttencrypt.png" alt="TTEncrypt 加密流程：key/iv 派生、inner 组装与输出帧布局" />
<em>图 3：TTEncrypt 加密流程——key/iv 派生、inner 组装与输出帧布局</em></p>

<p>派生函数 <code class="language-plaintext highlighter-rouge">sub_100AB6AEC</code> 的反编译把这套说得很清楚——SHA512(R) 先算一遍，紧接着把 CONST64 拼上去（两张表逐 16 字节异或、运行时算出），再 SHA512 一遍：</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// sub_100AB6AEC：key/iv = SHA512( SHA512(R) || CONST64 )</span>
<span class="n">v12</span> <span class="o">=</span> <span class="n">malloc</span><span class="p">(</span><span class="mh">0x80</span><span class="p">);</span>
<span class="n">sub_100AB7164</span><span class="p">(</span><span class="n">R</span><span class="p">,</span> <span class="n">len</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">v20</span><span class="p">);</span>          <span class="c1">// v20..v23 = SHA512(R)（前 64 字节）</span>
<span class="o">*</span><span class="n">v12</span> <span class="o">=</span> <span class="n">SHA512</span><span class="p">(</span><span class="n">R</span><span class="p">);</span>                      <span class="c1">// 拷到 v12[0..3]</span>
<span class="k">do</span> <span class="p">{</span>                                   <span class="c1">// 后 64 字节 = CONST64，运行时算出：</span>
    <span class="n">v12</span><span class="p">[</span><span class="n">i</span><span class="o">+</span><span class="mi">4</span><span class="p">]</span> <span class="o">=</span> <span class="n">veorq_s8</span><span class="p">(</span> <span class="n">unk_117408288</span><span class="p">[</span><span class="n">i</span><span class="p">],</span> <span class="n">unk_117408248</span><span class="p">[</span><span class="n">i</span><span class="p">]</span> <span class="p">);</span>   <span class="c1">// 两表逐 16B 异或</span>
<span class="p">}</span> <span class="k">while</span> <span class="p">(</span> <span class="o">++</span><span class="n">i</span> <span class="o">&lt;</span> <span class="mi">4</span> <span class="p">);</span>
<span class="n">sub_100AB7164</span><span class="p">(</span><span class="n">v12</span><span class="p">,</span> <span class="mh">0x80</span><span class="p">,</span> <span class="n">out</span><span class="p">);</span>         <span class="c1">// out = SHA512( SHA512(R) || CONST64 )</span>
<span class="c1">// key = out[0:16],  iv = out[16:32]</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">sub_100AB7164</code> 就是标准 SHA-512（IV <code class="language-plaintext highlighter-rouge">xmmword_117408550</code> = SHA-512 初始向量）。而两张表里 <code class="language-plaintext highlighter-rouge">unk_117408248</code> 正是 <strong>AES 逆 S-box</strong>——<code class="language-plaintext highlighter-rouge">CONST64</code> = 逆 S-box 两半异或，纯静态可取：</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>CONST64 = 4dd4c2e6b83162090e52b3c7a6733ba4 1cb2462b829ab58a196b39db57177524
          f49baf7f08e8d68d26a72e37c1a95a2f 1f05a51892aef2949732b62a38aadd58
</code></pre></div></div>

<p>对应到 Python，干净得多：</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">tt_encrypt</span><span class="p">(</span><span class="n">P</span><span class="p">,</span> <span class="n">R</span><span class="o">=</span><span class="bp">None</span><span class="p">):</span>
    <span class="n">R</span> <span class="o">=</span> <span class="n">R</span> <span class="ow">or</span> <span class="n">os</span><span class="p">.</span><span class="n">urandom</span><span class="p">(</span><span class="mi">32</span><span class="p">)</span>
    <span class="n">d2</span> <span class="o">=</span> <span class="n">hashlib</span><span class="p">.</span><span class="n">sha512</span><span class="p">(</span><span class="n">hashlib</span><span class="p">.</span><span class="n">sha512</span><span class="p">(</span><span class="n">R</span><span class="p">).</span><span class="n">digest</span><span class="p">()</span> <span class="o">+</span> <span class="n">CONST64</span><span class="p">).</span><span class="n">digest</span><span class="p">()</span>
    <span class="n">key</span><span class="p">,</span> <span class="n">iv</span> <span class="o">=</span> <span class="n">d2</span><span class="p">[:</span><span class="mi">16</span><span class="p">],</span> <span class="n">d2</span><span class="p">[</span><span class="mi">16</span><span class="p">:</span><span class="mi">32</span><span class="p">]</span>
    <span class="n">inner</span> <span class="o">=</span> <span class="n">hashlib</span><span class="p">.</span><span class="n">sha512</span><span class="p">(</span><span class="n">P</span><span class="p">).</span><span class="n">digest</span><span class="p">()</span> <span class="o">+</span> <span class="n">P</span>
    <span class="n">body</span>  <span class="o">=</span> <span class="n">AES</span><span class="p">.</span><span class="n">new</span><span class="p">(</span><span class="n">key</span><span class="p">,</span> <span class="n">AES</span><span class="p">.</span><span class="n">MODE_CBC</span><span class="p">,</span> <span class="n">iv</span><span class="p">).</span><span class="n">encrypt</span><span class="p">(</span><span class="n">pad</span><span class="p">(</span><span class="n">inner</span><span class="p">,</span> <span class="mi">16</span><span class="p">))</span>
    <span class="k">return</span> <span class="n">MAGIC</span> <span class="o">+</span> <span class="n">R</span> <span class="o">+</span> <span class="n">body</span>
</code></pre></div></div>

<blockquote>
  <p>R 内嵌在密文里（不需密钥协商），这套是<strong>完全离线、对称自包含</strong>的：能加密发包，也能直接解开抓到的任意 octet-stream 包体。多组真机向量验证，派生 key/iv 与整体密文跟真机<strong>逐字节一致</strong>。</p>
</blockquote>

<h3 id="25-请求与真机验证">2.5 请求与真机验证</h3>

<p>把 url（明文设备公参 query）+ body（TTEncrypt）+ 头拼起来。第一发探路，用<strong>最小头、不带任何签名</strong>：</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">headers</span> <span class="o">=</span> <span class="p">{</span>
    <span class="s">"Content-Type"</span><span class="p">:</span> <span class="s">"application/octet-stream;tt-data=a"</span><span class="p">,</span>
    <span class="s">"User-Agent"</span><span class="p">:</span>   <span class="s">"&lt;App UA&gt; Cronet"</span><span class="p">,</span>
    <span class="s">"x-ss-stub"</span><span class="p">:</span>    <span class="n">md5</span><span class="p">(</span><span class="n">body</span><span class="p">).</span><span class="n">hexdigest</span><span class="p">(),</span>      <span class="c1"># body 的 MD5
</span>    <span class="s">"x-khronos"</span><span class="p">:</span>    <span class="nb">str</span><span class="p">(</span><span class="nb">int</span><span class="p">(</span><span class="n">time</span><span class="p">.</span><span class="n">time</span><span class="p">())),</span>       <span class="c1"># 时间戳
</span><span class="p">}</span>
<span class="n">r</span> <span class="o">=</span> <span class="n">requests</span><span class="p">.</span><span class="n">post</span><span class="p">(</span><span class="n">URL</span><span class="p">,</span> <span class="n">data</span><span class="o">=</span><span class="n">body</span><span class="p">,</span> <span class="n">headers</span><span class="o">=</span><span class="n">headers</span><span class="p">)</span>
</code></pre></div></div>

<p>直接 <strong>HTTP 200</strong>，返回完整注册响应：</p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="nl">"server_time"</span><span class="p">:</span><span class="mi">1781160581</span><span class="p">,</span><span class="w"> </span><span class="nl">"device_id"</span><span class="p">:</span><span class="mi">120218806</span><span class="err">******</span><span class="mi">24</span><span class="p">,</span><span class="w"> </span><span class="nl">"install_id"</span><span class="p">:</span><span class="mi">120218806</span><span class="err">******</span><span class="mi">20</span><span class="p">,</span><span class="w">
 </span><span class="nl">"device_token"</span><span class="p">:</span><span class="s2">"AAAXMHR62KT3..."</span><span class="p">,</span><span class="w"> </span><span class="nl">"dtrait_pk1"</span><span class="p">:</span><span class="s2">"..."</span><span class="p">,</span><span class="w"> </span><span class="nl">"new_user"</span><span class="p">:</span><span class="mi">0</span><span class="p">,</span><span class="w"> </span><span class="err">...</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<blockquote>
  <p>前面那一整套要逆的签名，<strong>对 device_register 根本不需要</strong>——服务端不校验签名头。真正关键的是 body 加密（已完成）+ 正确的字段组合。</p>
</blockquote>

<p><strong>注册一台全新设备：</strong> body 里 <code class="language-plaintext highlighter-rouge">device_id / old_did / device_token</code> 清空，<code class="language-plaintext highlighter-rouge">vendor_id</code>(IDFV)、<code class="language-plaintext highlighter-rouge">cdid</code>、<code class="language-plaintext highlighter-rouge">mb_time</code> 全部重新生成 → 服务器当新设备处理：</p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="nl">"device_id"</span><span class="p">:</span><span class="mi">429841463</span><span class="err">******</span><span class="mi">51</span><span class="p">,</span><span class="w"> </span><span class="nl">"install_id"</span><span class="p">:</span><span class="mi">429841463</span><span class="err">******</span><span class="mi">47</span><span class="p">,</span><span class="w"> </span><span class="nl">"new_user"</span><span class="p">:</span><span class="mi">1</span><span class="p">,</span><span class="w"> </span><span class="err">...</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">new_user:1</code> —— 纯协议成功注册出一台全新设备，后续验证是否可以正常使用。</p>

<p><img src="/images/posts/ios/ai-app-protocol/04-regdeviceid.png" alt="device_register 实跑：凭空注册一台全新设备" />
<em>图 4：<code class="language-plaintext highlighter-rouge">device_register</code> 实跑——重新生成 <code class="language-plaintext highlighter-rouge">vendor_id</code>(IDFV)/<code class="language-plaintext highlighter-rouge">cdid</code> 后注册，服务端判为新设备（<code class="language-plaintext highlighter-rouge">new_user=1</code>），下发 <code class="language-plaintext highlighter-rouge">device_id</code>/<code class="language-plaintext highlighter-rouge">install_id</code>/<code class="language-plaintext highlighter-rouge">device_token</code>（HTTP 200，已脱敏）</em></p>

<p><strong>device_id 真能用吗？</strong> 拿它 + cookie 调一个需要有效设备的鉴权接口 <code class="language-plaintext highlighter-rouge">/im/conversation/main</code>：</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>HTTP 200  status_code 0   →  返回真实主会话 conversation_id=384299...
</code></pre></div></div>

<p>服务器为这个纯协议注册的设备分配了真实会话 → <strong>device_id 有效可用</strong>。</p>

<h3 id="26-服务端下发配置动态指纹采集gfdtrait">2.6 服务端下发配置：动态指纹采集（GF/dtrait）</h3>

<p>上面那些是写死在 body 里的“静态字段”。风控更深的一层，是<strong>服务端下发配置、客户端按配置动态采集</strong>的设备指纹（字节叫 GF / dtrait）。</p>

<p>线索在 device_register 的响应里就埋了——返回的 <code class="language-plaintext highlighter-rouge">dtrait_pk1 / dtrait_pk2</code> 是两把 <strong>RSA-2048 公钥</strong>，专门用来加密后续上报的 secDtrait。真正的动态采集走另一个接口：</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>POST /service/3/device_sdk/stats_collect/?...&amp;fetch_config=true
</code></pre></div></div>

<p>它是<strong>三层套娃</strong>：<code class="language-plaintext highlighter-rouge">TTEncrypt → gzip → base64 → bd_pack</code>（bd_pack 是字节另一套对称编码，也已离线破解：AES-256-CBC，key/iv 同样走 <code class="language-plaintext highlighter-rouge">SHA512(SHA512(R)||GF_CONST)</code> 派生）。这个接口的<strong>响应</strong>下发采集配置：</p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w"> </span><span class="nl">"fields"</span><span class="p">:</span><span class="w">        </span><span class="p">[</span><span class="s2">"d_c0"</span><span class="p">,</span><span class="s2">"d_r1"</span><span class="p">,</span><span class="s2">"d_m1"</span><span class="p">,</span><span class="s2">"d_r2"</span><span class="p">,</span><span class="s2">"d_o0"</span><span class="p">,</span><span class="s2">"d_o1"</span><span class="p">],</span><span class="w">
  </span><span class="nl">"dtrait_fields"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">"d_c0"</span><span class="p">,</span><span class="s2">"d_o0"</span><span class="p">,</span><span class="s2">"d_o1"</span><span class="p">,</span><span class="s2">"d_m1"</span><span class="p">,</span><span class="s2">"d_b0"</span><span class="p">],</span><span class="w">
  </span><span class="nl">"version"</span><span class="p">:</span><span class="w"> </span><span class="mi">3</span><span class="p">,</span><span class="w"> </span><span class="nl">"delay_sec"</span><span class="p">:</span><span class="w"> </span><span class="mi">10</span><span class="p">,</span><span class="w"> </span><span class="nl">"dtrait_mem_ttl_sec"</span><span class="p">:</span><span class="w"> </span><span class="mi">3600</span><span class="p">,</span><span class="w">
  </span><span class="nl">"configDO0Path"</span><span class="p">:</span><span class="w"> </span><span class="s2">"/var/mobile/Library/Carrier Bundles/Overlay"</span><span class="p">,</span><span class="w"> </span><span class="err">...</span><span class="w"> </span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p><strong>关键点：要采哪些字段、每个字段对应哪个系统文件路径，都是服务端下发的，不写死。</strong> 客户端 <code class="language-plaintext highlighter-rouge">+[BDInstallGFManager startStatusCollectWithFetchConfig:]</code> → <code class="language-plaintext highlighter-rouge">generateGFCollectBody</code> → 一堆 <code class="language-plaintext highlighter-rouge">collectDxx:path:</code> 按配置去取。frida 跟下来各 <code class="language-plaintext highlighter-rouge">d_*</code> 的真身：</p>

<table>
  <thead>
    <tr>
      <th>字段</th>
      <th>采集内容（真机 frida 实测）</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">d_c0</code></td>
      <td><code class="language-plaintext highlighter-rouge">/var/mobile/Library/Carrier Bundles</code> 的创建时间</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">d_o0</code></td>
      <td><code class="language-plaintext highlighter-rouge">…/Carrier Bundles/Overlay</code> 的时间</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">d_o1</code></td>
      <td>另一运营商目录时间（路径服务端下发）</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">d_m1</code></td>
      <td>SDK 首启时戳</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">d_b0</code></td>
      <td>系统启动时间</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">d_r2</code></td>
      <td><strong>App Store 收据</strong>（Apple WWDR 签名的 PKCS#7，内含 bundleid，约 4.8KB DER）</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">d_r1</code></td>
      <td>空桩（<code class="language-plaintext highlighter-rouge">collectDR1</code> 直接返回空——代码即空，非采集失败）</td>
    </tr>
  </tbody>
</table>

<blockquote>
  <p>这套为什么难伪造：它不是客户端随手能编的字段，而是<strong>一组真实系统文件的时间戳 + Apple 签名的收据</strong>。Carrier Bundles 的时间随设备激活/运营商更新固定、跨重装稳定，是强设备指纹；<code class="language-plaintext highlighter-rouge">d_r2</code> 那张收据是 Apple 私钥签的，伪不出来。再加上“采哪些文件”由服务端动态下发，连个固定靶子都不给你。</p>
</blockquote>

<p><img src="/images/posts/ios/ai-app-protocol/05-gf-dtrait.png" alt="GF/dtrait 动态指纹采集：服务端下发配置 + 三层套娃上报" />
<em>图 5：GF/dtrait 动态指纹采集——服务端下发配置 + 三层套娃上报</em></p>

<p>一个容易踩的坑：<strong>下发配置用的 bd_pack 常量和上报的设备数据不是同一个</strong>——配置用 <code class="language-plaintext highlighter-rouge">GF_CONST1</code>（派生分支 <code class="language-plaintext highlighter-rouge">a7=1</code>），设备数据用 <code class="language-plaintext highlighter-rouge">GF_CONST</code>（<code class="language-plaintext highlighter-rouge">a7=0</code>）。解码器两个都得试。</p>

<p>对协议分析的影响：<code class="language-plaintext highlighter-rouge">device_register</code> 本身<strong>不依赖</strong>这套（前面已经注册成功了），但若关心“注册的设备能否长期用、会不会被风控盯上”，GF/dtrait 这层动态指纹才是真正的硬骨头——它把“伪造一台设备”从“拼对几十个字段”，升级成了“伪造一组真实文件时间戳 + 一张 Apple 签名收据”。</p>

<hr />

<h2 id="三请求签名metasec--x-gorgon">三、请求签名：metasec / X-Gorgon</h2>

<h3 id="31-签名头有哪些">3.1 签名头有哪些</h3>

<p>这版 TTNet 是 Rust 写的，发包前调 metasec 加签，一次产出一整套：</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>X-Argus / X-Gorgon / X-Helios / X-Khronos / X-Ladon / X-Medusa / X-Neptune / X-SS-STUB
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">X-SS-STUB = MD5(body)</code>、<code class="language-plaintext highlighter-rouge">X-Khronos = 时间戳</code> 简单。难的是 <code class="language-plaintext highlighter-rouge">X-Gorgon</code>，格式形如 <code class="language-plaintext highlighter-rouge">8404 | &lt;2字节 nonce&gt; | 0000 | &lt;20字节摘要&gt;</code>。</p>

<h3 id="32-静态逆向碰壁ollvm">3.2 静态逆向碰壁（OLLVM）</h3>

<p>硬件断点（绕过反插桩）抓到活的签名入口 <code class="language-plaintext highlighter-rouge">sub_111E08FF8(url, headers)</code>——它一次产出全部 X 头。但往里走就是噩梦：</p>

<ul>
  <li><strong>控制流平坦化</strong>：核心 <code class="language-plaintext highlighter-rouge">sub_111E0A0C4</code> 是标准 OLLVM dispatcher，状态变量 + 魔数常量 + 不透明谓词 + 花指令；</li>
  <li><strong>国密体系</strong>：全镜像扫密码常量，命中 SM3 / SM4 / SHA1 / MD5 / AES 混合，没现成标准库可对；</li>
  <li><strong>真算法靠运行时函数指针到达</strong>，静态 call-graph 解析不出。</li>
</ul>

<p>动态路也都堵死：</p>

<table>
  <thead>
    <tr>
      <th>方案</th>
      <th>结果</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>IDA 远程单步</td>
      <td>太慢；<code class="language-plaintext highlighter-rouge">LDAXR/STLXR</code> 原子在单步下活锁（独占监视器每步被清）</td>
    </tr>
    <tr>
      <td>Frida Stalker 跟进</td>
      <td>一进 metasec 就崩（return-地址 gadget，见第七节）</td>
    </tr>
    <tr>
      <td>Frida Interceptor hook</td>
      <td>碰一下就 <code class="language-plaintext highlighter-rouge">Process terminated</code>（反篡改，约 25s 杀进程）</td>
    </tr>
  </tbody>
</table>

<blockquote>
  <p>这套反插桩怎么实现的、metasec 背后还在采哪些设备风险信号，单开一节细拆（见第七节）。这里先记住结论：<strong>动态分析这条路对 metasec 本体是堵死的。</strong></p>
</blockquote>

<h3 id="33-思路转变不逆算法模拟调用">3.3 思路转变：不逆算法，模拟调用</h3>

<p>签名函数的 I/O 是确定的：<code class="language-plaintext highlighter-rouge">sub_111E08FF8(url, headers) → 全部 X 头</code>。既然<strong>内部算法静态难以推进，就把这个函数原封不动地“跑起来”</strong>——脱离手机、脱离反调试，在可控的模拟器里加载真实二进制直接调。</p>

<p>用的是<strong>基于 Unicorn 自建的 iOS 模拟执行环境</strong>：Unicorn 跑 arm64 指令，外面补上 ObjC 运行时 / Foundation / 常用 libc。这类「模拟执行真实二进制」的思路，分析字节系的 X-Gorgon/X-Argus 时很常见。</p>

<hr />

<h2 id="四把签名函数放进受控模拟环境执行">四、把签名函数放进受控模拟环境执行</h2>

<h3 id="41-为什么用模拟执行">4.1 为什么用模拟执行</h3>

<ul>
  <li>几百万指令/秒，没有 USB 往返；</li>
  <li><strong>没有反调试、没有 LL/SC 活锁</strong>（单线程模拟，独占监视器是对的）——之前真机单步卡死的那段 Arc 引用计数自增，这里根本不是问题；</li>
  <li>跟单步一样是<strong>动态执行，能穿透 OLLVM 平坦化</strong>（只执行真实那条路径）。</li>
</ul>

<h3 id="42-三个关键修复缺一不可">4.2 三个关键修复（缺一不可）</h3>

<p>直接调签名函数是跑不起来的。545MB 主二进制加载只要 13 秒，但要让函数真正跑通，得过三关：</p>

<p><strong>① 必须全初始化。</strong> 用默认参数加载（跑 <code class="language-plaintext highlighter-rouge">init_array</code> + <code class="language-plaintext highlighter-rouge">objc_init</code>）。关掉 objc_init，<code class="language-plaintext highlighter-rouge">_objc_msgSend</code> 的 GOT 槽是 0，Rust 代码调它直接 <code class="language-plaintext highlighter-rouge">br</code> 到 0 崩溃。</p>

<p><strong>② hook <code class="language-plaintext highlighter-rouge">__tlv_bootstrap</code>（TLS 引导）。</strong> 签名器访问 thread-local，经 libdyld 的 <code class="language-plaintext highlighter-rouge">__tlv_bootstrap</code>，模拟里跑不通撞 <code class="language-plaintext highlighter-rouge">BRK</code>。自己实现：按 descriptor 的 key 分配清零块返回。</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">tlv_boot</span><span class="p">(</span><span class="n">uc</span><span class="p">,</span> <span class="n">address</span><span class="p">,</span> <span class="n">size</span><span class="p">,</span> <span class="n">ud</span><span class="p">):</span>
    <span class="n">desc</span> <span class="o">=</span> <span class="n">emu</span><span class="p">.</span><span class="n">get_arg</span><span class="p">(</span><span class="mi">0</span><span class="p">)</span>
    <span class="n">key</span><span class="p">,</span> <span class="n">off</span> <span class="o">=</span> <span class="n">emu</span><span class="p">.</span><span class="n">read_u64</span><span class="p">(</span><span class="n">desc</span> <span class="o">+</span> <span class="mi">8</span><span class="p">),</span> <span class="n">emu</span><span class="p">.</span><span class="n">read_u64</span><span class="p">(</span><span class="n">desc</span> <span class="o">+</span> <span class="mh">0x10</span><span class="p">)</span>
    <span class="k">if</span> <span class="n">key</span> <span class="ow">not</span> <span class="ow">in</span> <span class="n">arenas</span><span class="p">:</span>
        <span class="n">a</span> <span class="o">=</span> <span class="n">emu</span><span class="p">.</span><span class="n">create_buffer</span><span class="p">(</span><span class="mh">0x80000</span><span class="p">);</span> <span class="n">emu</span><span class="p">.</span><span class="n">write_bytes</span><span class="p">(</span><span class="n">a</span><span class="p">,</span> <span class="sa">b</span><span class="s">"</span><span class="se">\x00</span><span class="s">"</span> <span class="o">*</span> <span class="mh">0x80000</span><span class="p">)</span>
        <span class="n">arenas</span><span class="p">[</span><span class="n">key</span><span class="p">]</span> <span class="o">=</span> <span class="n">a</span>
    <span class="k">return</span> <span class="n">arenas</span><span class="p">[</span><span class="n">key</span><span class="p">]</span> <span class="o">+</span> <span class="n">off</span>
<span class="n">emu</span><span class="p">.</span><span class="n">add_interceptor</span><span class="p">(</span><span class="s">"__tlv_bootstrap"</span><span class="p">,</span> <span class="n">tlv_boot</span><span class="p">)</span>
</code></pre></div></div>

<p><strong>③ patch <code class="language-plaintext highlighter-rouge">semwait</code> 返回成功。</strong> 模拟环境默认让信号量等待永远超时（ETIMEDOUT），导致 Rust 的 <code class="language-plaintext highlighter-rouge">Once</code> 误判失败 → panic。改成返回 0：</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">IosSyscallHandler</span><span class="p">.</span><span class="n">_handle_sys_semwait_signal</span> <span class="o">=</span> <span class="k">lambda</span> <span class="bp">self</span><span class="p">:</span> <span class="mi">0</span>
</code></pre></div></div>

<p>（另：objc init 会找主程序 <code class="language-plaintext highlighter-rouge">Info.plist</code>，从 ipa 抽一份放到二进制旁边即可。）</p>

<h3 id="43-执行一次观测产出">4.3 执行一次，观测产出</h3>

<p>三关一过，签名函数 2 秒跑完（550 万条指令）。给它一组 <code class="language-plaintext highlighter-rouge">(url, headers)</code>，把它写回的那块响应头读出来，就是真实的全套 X 头：</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>输入  url + {x-ss-stub, Content-Type, ...}
输出  X-Gorgon:'8404a094...' / X-Khronos / X-Argus / X-Ladon
      / X-Helios / X-Medusa / X-Neptune
</code></pre></div></div>

<p>这一步的价值是<strong>把「黑盒签名头」变成可观测对象</strong>：同一组输入跑出确定结果，就能拿它做后续分析——比如对指令轨迹做密码扫描确认 <code class="language-plaintext highlighter-rouge">X-Gorgon</code> 的 20 字节摘要，命中 SM3（主）+ SHA1 + MD5，摘要是分字段打包（query 哈希→字节 0-3，时间/nonce→中段，末字节校验）。</p>

<blockquote>
  <p>注意这里只是<strong>让签名函数在受控环境里执行、观测它的输入输出</strong>，并没有「读懂」算法——签名内部到底怎么算、能不能闭式还原，是第六节的事（详细VM分析）。</p>
</blockquote>

<hr />

<h2 id="五对话流程从拿会话到-ai-回复">五、对话流程：从拿会话到 AI 回复</h2>

<h3 id="51-整体流程">5.1 整体流程</h3>

<p><img src="/images/posts/ios/ai-app-protocol/06-chat-flow.png" alt="对话流程：注册 → 拿会话 → 进会话 → chat → SSE 回复" />
<em>图 6：对话流程——注册 → 拿会话 → 进会话 → chat → SSE 回复</em></p>

<h3 id="52-对话接口结构">5.2 对话接口结构</h3>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>POST https://api5-normal-hl.&lt;host&gt;/chat/completion?...
Content-Type: application/json; encoding=utf-8
</code></pre></div></div>

<p>惊喜：<strong>body 是纯 JSON 明文，不加密！</strong></p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w"> </span><span class="nl">"client_meta"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="nl">"bot_id"</span><span class="p">:</span><span class="s2">"&lt;默认助手&gt;"</span><span class="p">,</span><span class="w"> </span><span class="nl">"conversation_id"</span><span class="p">:</span><span class="s2">"..."</span><span class="p">,</span><span class="w"> </span><span class="err">...</span><span class="p">},</span><span class="w">
  </span><span class="nl">"option"</span><span class="p">:</span><span class="w">      </span><span class="p">{</span><span class="nl">"need_create_conversation"</span><span class="p">:</span><span class="kc">false</span><span class="p">,</span><span class="w"> </span><span class="nl">"send_message_scene"</span><span class="p">:</span><span class="s2">"keyboard"</span><span class="p">,</span><span class="w"> </span><span class="err">...</span><span class="p">},</span><span class="w">
  </span><span class="nl">"messages"</span><span class="p">:</span><span class="w">    </span><span class="p">[{</span><span class="nl">"content_block"</span><span class="p">:[{</span><span class="nl">"content"</span><span class="p">:{</span><span class="nl">"text_block"</span><span class="p">:{</span><span class="nl">"text"</span><span class="p">:</span><span class="s2">"用户的问题"</span><span class="p">}},</span><span class="w"> </span><span class="nl">"block_type"</span><span class="p">:</span><span class="mi">10000</span><span class="p">}]}]</span><span class="w"> </span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<ul>
  <li>鉴权 = device_register 下发的 cookie（<code class="language-plaintext highlighter-rouge">odin_tt</code> / <code class="language-plaintext highlighter-rouge">install_id</code> / <code class="language-plaintext highlighter-rouge">ttreq</code>，Domain 跨 host 通用，<code class="language-plaintext highlighter-rouge">requests.Session</code> 自动带）；</li>
  <li>响应 = <strong>SSE 流式</strong>（<code class="language-plaintext highlighter-rouge">event: CHUNK_DELTA / STREAM_CHUNK / SSE_REPLY_END</code>），AI 回复分块流回。</li>
</ul>

<h3 id="53-会话从哪来">5.3 会话从哪来</h3>

<p>新设备没有会话，body 里 <code class="language-plaintext highlighter-rouge">conversation_id</code> 留空报 <code class="language-plaintext highlighter-rouge">710020202 invalid param</code>。会话是<strong>设备绑定的 guest 主会话</strong>，App 本地持久化，抓包看不到创建那一刻。</p>

<p>试了下：全新设备调 <code class="language-plaintext highlighter-rouge">/im/conversation/main</code>（body <code class="language-plaintext highlighter-rouge">{"ext":{}}</code>）——服务器直接返回一个<strong>真实主会话 id</strong>：</p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nl">"conversation_info"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="nl">"conversation_id"</span><span class="p">:</span><span class="s2">"384300..."</span><span class="p">,</span><span class="w"> </span><span class="nl">"name"</span><span class="p">:</span><span class="s2">"&lt;App 名&gt;"</span><span class="p">,</span><span class="w"> </span><span class="nl">"conversation_type"</span><span class="p">:</span><span class="mi">3</span><span class="p">,</span><span class="w"> </span><span class="err">...</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p>每个新 device 都分到自己的 guest 主会话。拿到后再 <code class="language-plaintext highlighter-rouge">/im/conversation/in_out</code>（cmd 4210）进会话。</p>

<h3 id="54-卡在-710022002chat-验签名">5.4 卡在 710022002：chat 验签名</h3>

<p>万事俱备，chat 始终返回：</p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="nl">"error_code"</span><span class="p">:</span><span class="mi">710022002</span><span class="p">,</span><span class="w"> </span><span class="nl">"error_msg"</span><span class="p">:</span><span class="s2">"当前服务访问频繁，请稍后重试"</span><span class="p">,</span><span class="w"> </span><span class="nl">"extra"</span><span class="p">:{</span><span class="nl">"ack"</span><span class="p">:</span><span class="s2">"1"</span><span class="p">}}</span><span class="w">
</span></code></pre></div></div>

<p>错误像限流，于是冷却 15 分钟单发——<strong>仍然 710022002</strong>。排除限流后，回头看一个被误判的结论：</p>

<blockquote>
  <p>早期“chat 不验签名”的测试，是在<strong>无效会话</strong>上做的——会话错误在签名校验<strong>之前</strong>就返回了，从没真正走到验签名那一步。</p>
</blockquote>

<p>补上模拟执行产出的 <code class="language-plaintext highlighter-rouge">X-Gorgon</code> 等头再发：</p>

<h3 id="55-端到端验证整条链跑通一次">5.5 端到端验证：整条链跑通一次</h3>

<p>把前面逆清的每一环串起来，给这一次 chat 请求补上模拟执行产出的签名头，发出去：</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>明文 JSON body
  + X-SS-STUB = MD5(body)
  + 模拟执行产出的 X-Gorgon / X-Khronos / X-Argus / …
→ POST chat/completion
</code></pre></div></div>

<p>SSE 流里终于出现真实文本：</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>event: CHUNK_DELTA   data: {"text":"我是 XX 自研的"}
event: CHUNK_DELTA   data: {"text":"AI 助手，乐于为你解答问题…"}
event: SSE_REPLY_END data: {"end_type":1, ...}
</code></pre></div></div>

<p>把上面整条链封进一个交互式客户端，就能直接对话——签名头由模拟执行实时产出，SSE 边收边解析：</p>

<p><img src="/images/posts/ios/ai-app-protocol/07-chat-result.png" alt="chat/completion 实跑：交互式对话的流式回复" />
<em>图 7：<code class="language-plaintext highlighter-rouge">chat/completion</code> 实跑——纯协议注册的设备进入交互式对话，问答经签名校验放行，AI 回复以 SSE 流式逐字返回（已脱敏）</em></p>

<blockquote>
  <p><strong>chat 确实验签名（与 device_register 不同）。</strong> 前面对签名函数的模拟执行，到这里正好验证了价值——整条链（采集 → 加密 → 拿会话 → 签名 → 发包 → SSE 回复）端到端跑通，metasec 这一路的分析在这一步闭环。这是对「逆向是否到位」的一次完整验证，而非一个可复用的发号工具。</p>
</blockquote>

<hr />

<h2 id="六深入签名算法vm中计算签名">六、深入签名算法：VM中计算签名</h2>

<p>签名头能在模拟环境里观测、协议也端到端跑通了，但有个问题一直没答案：那 20 字节的 <code class="language-plaintext highlighter-rouge">X-Gorgon</code> 摘要、那串更长的 <code class="language-plaintext highlighter-rouge">X-Medusa</code>，<strong>里面到底算了什么</strong>？前面只是「观测产物」，没「读懂算法」，里面是否有隐藏的风险是未知的。这一节就回头把签名器拆开——结果发现它远不止「OLLVM 混淆的 native 函数」那么简单，而是<strong>VMP</strong>。</p>

<h3 id="61-不是普通-ollvm是-vm">6.1 不是普通 OLLVM，是 VM</h3>

<p>签名入口 <code class="language-plaintext highlighter-rouge">sub_111E08FF8</code> 往里走，不是常规函数调用链，而是进了一个<strong>解释器</strong>：</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>sub_111E08FF8(url, headers)
   └─ sub_1002D85AC: vm_run(vm_code, &amp;args, ...)
        └─ sub_1002B3EDC          ← 字节码解释器
             dispatcher 0x1002B4114 (fetch → decode → br x8)
             handler 区 0x1002B4000 – 0x1002BDFFF
</code></pre></div></div>

<p>指令编码也摸清了：每条 32 位字，<code class="language-plaintext highlighter-rouge">OP = word[6:11]</code>（6 位），操作数 <code class="language-plaintext highlighter-rouge">f0..3 = word[12:17]/[17:22]/[22:27]/[27:32]</code>（各 5 位，都是虚拟寄存器下标，<code class="language-plaintext highlighter-rouge">vreg[i] = [x20 + i*8]</code>）。真机 trace 实测只有 26 个真实 opcode 在跑，全是纯算术：<code class="language-plaintext highlighter-rouge">XOR / ADD / SUB / AND / OR / NOT / NEG / ROR / SHL / LSR / MUL</code>，外加 <code class="language-plaintext highlighter-rouge">LOAD</code>（读输入）和 <code class="language-plaintext highlighter-rouge">CALL</code>（调外部，实测只有 memmove/memset）。<strong>没有 SM3/SM4/AES 硬件指令</strong> —— 国密全是这些 opcode 指令拼出来的。</p>

<p>最难缠的是<strong>三层保护</strong>，每一层都是「改代码就崩」的被动自毁，比主动检测更难绕：</p>

<table>
  <thead>
    <tr>
      <th>层</th>
      <th>机制</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>① opcode 滚动解密</td>
      <td>每个 handler 执行完，把「下一条」opcode 字 XOR 一个 handler 专属常量、就地写回。静态看全是密文，且<strong>路径相关</strong>（没走到的分支永远是密文）</td>
    </tr>
    <tr>
      <td>② 滚动密钥 <code class="language-plaintext highlighter-rouge">var_60</code></td>
      <td>逐指令 <code class="language-plaintext highlighter-rouge">EOR #const</code> 更新，参与 handler 地址解码 —— <strong>指令顺序错一条，后面全乱</strong></td>
    </tr>
    <tr>
      <td>③ 自地址完整性</td>
      <td>每个 handler 顶部 <code class="language-plaintext highlighter-rouge">ADRL x8, 解释器; ...位运算...; CMP; B.CC</code>，用<strong>函数自身地址的比特</strong>当派发密钥 X5/X6。patch 代码 → 地址派生变 → 派发跳飞</td>
    </tr>
  </tbody>
</table>

<p>这也解释了为什么前面 Frida Stalker/Interceptor 一碰就崩：它们要搬代码/打补丁，而 ①③ 让任何代码改动都破坏密钥链。模拟环境原地执行、保留真实寄存器，反而免疫。</p>

<p>把签名器内部画出来，就是一台带三层护甲的虚拟机：</p>

<p><img src="/images/posts/ios/ai-app-protocol/08-signer-vm.png" alt="签名器内部：字节码 VM + 三层自毁保护" />
<em>图 8：签名器内部——字节码 VM + 三层自毁保护</em></p>

<h3 id="62-x-medusa-拆开明文-protobuf-提取">6.2 X-Medusa 拆开：明文 protobuf 提取</h3>

<p><code class="language-plaintext highlighter-rouge">X-Medusa</code> 是这套里最重的头，结构是 <code class="language-plaintext highlighter-rouge">base64( [4字节 nonce] + 流密码加密(protobuf) )</code>。每次签名 nonce 都变，所以密文每次不同 —— 这其实是个<strong>逐次 nonce 的流密码</strong>（差分扫描印证：明文⊕密文=keystream，nonce 喂 counter）。</p>

<p>要看明文 protobuf 得过一关：<strong>明文是「原地加密」的瞬态</strong>，签完内存里那块已经被密文覆盖了，事后 dump 只剩密文。解法是 <strong>「首读值」法</strong> —— 在模拟环境里钩内存<strong>读</strong>，对每个地址只记<strong>第一次被读到的值</strong>（即被覆盖前的明文），配合把时间/熵全部固定让结果确定：</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">mem</span> <span class="o">=</span> <span class="p">{}</span>
<span class="k">def</span> <span class="nf">on_read</span><span class="p">(</span><span class="n">uc</span><span class="p">,</span> <span class="n">access</span><span class="p">,</span> <span class="n">addr</span><span class="p">,</span> <span class="n">size</span><span class="p">,</span> <span class="n">value</span><span class="p">,</span> <span class="n">ud</span><span class="p">):</span>
    <span class="k">if</span> <span class="ow">not</span> <span class="p">(</span><span class="n">HEAP_LO</span> <span class="o">&lt;=</span> <span class="n">addr</span> <span class="o">&lt;</span> <span class="n">HEAP_HI</span><span class="p">):</span> <span class="k">return</span>
    <span class="n">data</span> <span class="o">=</span> <span class="n">uc</span><span class="p">.</span><span class="n">mem_read</span><span class="p">(</span><span class="n">addr</span><span class="p">,</span> <span class="n">size</span><span class="p">)</span>
    <span class="k">for</span> <span class="n">i</span><span class="p">,</span> <span class="n">b</span> <span class="ow">in</span> <span class="nb">enumerate</span><span class="p">(</span><span class="n">data</span><span class="p">):</span>
        <span class="n">mem</span><span class="p">.</span><span class="n">setdefault</span><span class="p">(</span><span class="n">addr</span> <span class="o">+</span> <span class="n">i</span><span class="p">,</span> <span class="n">b</span><span class="p">)</span>        <span class="c1"># 只记首读 = 加密前明文
</span><span class="n">emu</span><span class="p">.</span><span class="n">uc</span><span class="p">.</span><span class="n">hook_add</span><span class="p">(</span><span class="n">UC_HOOK_MEM_READ</span><span class="p">,</span> <span class="n">on_read</span><span class="p">)</span>
</code></pre></div></div>

<p>这样就把完整 flat protobuf 提取了出来，任意设备全字段可解：</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>field7  "13.6.0"                 # app 版本
field8  "v04.10.00-ml-iOS"       # 安全 SDK 版本
field13 &lt;20 字节&gt;                # 设备哈希  ← 本节主角
field14 &lt;6 字节&gt;                 # 请求摘要
field15 / field20 / ...
</code></pre></div></div>

<h3 id="63-一个能闭式还原的字段field14">6.3 一个能闭式还原的字段：field14</h3>

<p><code class="language-plaintext highlighter-rouge">field14</code> 运气好，一层就到底。在 VM 里追 <code class="language-plaintext highlighter-rouge">LOAD</code> opcode 读的输入指针，还原出被哈希的明文串（注意 VM 加载时有个半字节交换 <code class="language-plaintext highlighter-rouge">[1,0,3,2]</code>，<code class="language-plaintext highlighter-rouge">ediv→devi</code>），拼出来正是请求 query：</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># 双设备验证通过
</span><span class="n">field14</span> <span class="o">=</span> <span class="n">SM3</span><span class="p">(</span><span class="s">"device_id=&lt;did&gt;&amp;iid=&lt;iid&gt;&amp;aid=&lt;aid&gt;"</span><span class="p">)[:</span><span class="mi">6</span><span class="p">]</span>
</code></pre></div></div>

<p>干净的闭式，可移植。这说明签名里<strong>有</strong>能纯算法还原的部分。</p>

<h3 id="64-难以还原的字段field13--键控-sm3设备指纹">6.4 难以还原的字段：field13 = 键控 SM3(设备指纹)</h3>

<p><code class="language-plaintext highlighter-rouge">field13</code> 是另一个故事。它 20 字节、随设备变，但<strong>不是</strong> query 的任何哈希 —— sha1/sm3/md5/sha256、各种哈希链、key×query 全试否。它的输入是 VM 内部拼出来的二进制，不落明文。</p>

<p>于是上<strong>完整 trace + 反向数据流切片</strong>：在模拟环境里全量记录 VM 区每条内存读写（带值），逮住 <code class="language-plaintext highlighter-rouge">field13</code> 摘要写出的那一刻，再从输出点反推。落点很明确 —— <code class="language-plaintext highlighter-rouge">field13</code> 的首字节在地址 <code class="language-plaintext highlighter-rouge">0x1002b64bc</code> 写出，那是 VM 的 <code class="language-plaintext highlighter-rouge">XOR</code> opcode（<code class="language-plaintext highlighter-rouge">vreg[f2] = vreg[f1] ^ vreg[f0]</code>），正好是 <strong>SM3 的 feed-forward 终结</strong>（<code class="language-plaintext highlighter-rouge">V = ABCDEFGH ^ V</code>）。配上 trace 里那张 <code class="language-plaintext highlighter-rouge">0x79cc4519</code> 轮常量表 —— <strong>坐实 field13 = SM3 输出</strong>。</p>

<p>再数压缩块（轮常量读次数 / 64）：<strong>约 9 块 ≈ 512 字节消息</strong>。而 trace 窗口里出现的字符串碎片是关键证据：</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>common_key / sign_key          # mssdk 的两把 base64 密钥（license protobuf 里）
…ers/Bundle/Application/…       # App 沙盒路径
mssdk / Carrier / App 收据片段
</code></pre></div></div>

<p>也就是说：</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>field13 = SM3( mssdk_key , 设备指纹 blob )[:20]      # 键控 SM3
</code></pre></div></div>

<p>输入是<strong>整块设备指纹</strong>（就是第 2.6 节那套 GF/dtrait：Carrier Bundles 时间戳、App Store 收据、路径……），不是闭式公式 —— <strong>绑设备，换设备就变</strong>。</p>

<p>那能不能把这 512 字节消息从 trace 里复原出来？我写了<strong>SM3 感知提取器</strong>，用消息扩展公式反查 W 数组：</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>W[j] = P1(W[j-16] ^ W[j-9] ^ ROL(W[j-3],15)) ^ ROL(W[j-13],7) ^ W[j-6]
P1(x) = x ^ ROL(x,15) ^ ROL(x,23)
</code></pre></div></div>

<p>思路是：哪个内存 buffer 的值序列满足这个扩展，它的前 16 词就是消息块。四种方法都试了 —— 值导向后向切片、连续地址验扩展、每块 buffer 抽取、指针表解引用 —— <strong>全部证伪</strong>：扩展公式在 flat 内存里怎么都不命中。最后跟到那张「W 数组指针表」，<code class="language-plaintext highlighter-rouge">[0]</code> 存的赫然是 OLLVM 不透明谓词常量。</p>

<p>结论铁证：<strong>W 数组完全 vreg 驻留 + 指针间接 + OLLVM 谓词交织，消息词不存在于任何连续 buffer。</strong> 要字节级还原 <code class="language-plaintext highlighter-rouge">field13</code>，只剩 IDA GUI + 微码去混淆（d810）反虚拟化那段 VM SM3 是另一个量级的工程。</p>

<p><img src="/images/posts/ios/ai-app-protocol/09-field13-devirt.png" alt="field13 反虚拟化：从定位到四法证伪，最终只剩 d810" />
<em>图 9：field13 反虚拟化——从定位到四法证伪，最终只剩 d810</em></p>

<h3 id="65-vm-字节码反汇编与还原高级代码">6.5 VM 字节码反汇编与还原高级代码</h3>

<p>前面几节都在用 VM 的执行结果，这里单独把方法说清：<strong>怎么把这套字节码反汇编出来、再往高级代码还原</strong>。</p>

<p><img src="/images/posts/ios/ai-app-protocol/10-vm-lift.png" alt="VM 反汇编到还原高级代码的流水线" />
<em>图 10：VM 反汇编到还原高级代码的流水线</em></p>

<p><strong>反汇编：只能动态，不能静态。</strong> 直接静态反汇编这套字节码会得到一堆乱码，原因有三：</p>

<ul>
  <li><strong>滚动自解密</strong>：每条 opcode 执行时才被 XOR 解出（6.1 的 ① 层），静态看全是密文；没走到的分支永远不解密。</li>
  <li><strong>变长 + 数据交织</strong>：字节码里夹着常量数据，按固定长度线性扫描会错位。</li>
  <li><strong>路径相关</strong>：同一段字节随执行路径解出不同指令。</li>
</ul>

<p>所以反汇编得<strong>从动态执行里取</strong>：在派发点（<code class="language-plaintext highlighter-rouge">fetch → decode → br x8</code>）插一下，每条 VM 指令记下它的 <code class="language-plaintext highlighter-rouge">OP</code>、操作数 <code class="language-plaintext highlighter-rouge">f0..3</code>、以及读写的虚拟寄存器值，得到一条<strong>已解密、可读</strong>的 VM 指令级 trace：</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>OP=0x06 XOR  r[f2] = r[f1] ^ r[f0]    ; r[f1]=0x.. r[f0]=0x..
OP=0x24 ADD  r[f1] = r[f1] + r[f3]
OP=0x16 ROR  r[f1] = ror(r[f0], r[f1])
</code></pre></div></div>

<p>实测只有 <strong>26 个真实 opcode</strong> 在跑，全是标量算术（XOR/ADD/SUB/AND/OR/NOT/NEG/ROR/SHL/LSR/MUL）+ LOAD/CALL。国密哈希就是这些 opcode 软件拼出来的。</p>

<p>反汇编器本身很短，核心就是「解码一条指令字 + 一张语义表 + 在派发点抓 vreg 值」：</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># ① 解码：每条 VM 指令是一个 32 位字
</span><span class="k">def</span> <span class="nf">decode</span><span class="p">(</span><span class="n">word</span><span class="p">):</span>
    <span class="n">op</span> <span class="o">=</span> <span class="p">(</span><span class="n">word</span> <span class="o">&gt;&gt;</span> <span class="mi">6</span><span class="p">)</span> <span class="o">&amp;</span> <span class="mh">0x3f</span>                          <span class="c1"># 操作码（6 位）
</span>    <span class="n">f</span>  <span class="o">=</span> <span class="p">[(</span><span class="n">word</span> <span class="o">&gt;&gt;</span> <span class="mi">12</span><span class="p">)</span> <span class="o">&amp;</span> <span class="mh">0x1f</span><span class="p">,</span> <span class="p">(</span><span class="n">word</span> <span class="o">&gt;&gt;</span> <span class="mi">17</span><span class="p">)</span> <span class="o">&amp;</span> <span class="mh">0x1f</span><span class="p">,</span>  <span class="c1"># 4 个操作数
</span>          <span class="p">(</span><span class="n">word</span> <span class="o">&gt;&gt;</span> <span class="mi">22</span><span class="p">)</span> <span class="o">&amp;</span> <span class="mh">0x1f</span><span class="p">,</span> <span class="p">(</span><span class="n">word</span> <span class="o">&gt;&gt;</span> <span class="mi">27</span><span class="p">)</span> <span class="o">&amp;</span> <span class="mh">0x1f</span><span class="p">]</span>  <span class="c1">#   = 虚拟寄存器下标
</span>    <span class="k">return</span> <span class="n">op</span><span class="p">,</span> <span class="n">f</span>

<span class="c1"># ② OP → 语义（实测在跑的 26 个，全是标量运算）
</span><span class="n">SEM</span> <span class="o">=</span> <span class="p">{</span>
    <span class="mh">0x06</span><span class="p">:</span> <span class="p">(</span><span class="s">'XOR'</span><span class="p">,</span>  <span class="s">'r[f0] = r[f0] ^ r[f3]'</span><span class="p">),</span>
    <span class="mh">0x24</span><span class="p">:</span> <span class="p">(</span><span class="s">'ADD'</span><span class="p">,</span>  <span class="s">'r[f1] = r[f1] + r[f3]'</span><span class="p">),</span>
    <span class="mh">0x16</span><span class="p">:</span> <span class="p">(</span><span class="s">'ROR'</span><span class="p">,</span>  <span class="s">'r[f1] = ror(r[f0], r[f1])'</span><span class="p">),</span>
    <span class="mh">0x3f</span><span class="p">:</span> <span class="p">(</span><span class="s">'AND'</span><span class="p">,</span>  <span class="s">'r[f2] = r[f1] &amp; r[f3]'</span><span class="p">),</span>
    <span class="mh">0x2e</span><span class="p">:</span> <span class="p">(</span><span class="s">'OR'</span><span class="p">,</span>   <span class="s">'r[f3] = r[f1] | r[f2]'</span><span class="p">),</span>
    <span class="mh">0x15</span><span class="p">:</span> <span class="p">(</span><span class="s">'LOAD'</span><span class="p">,</span> <span class="s">'读输入字节'</span><span class="p">),</span>
    <span class="mh">0x13</span><span class="p">:</span> <span class="p">(</span><span class="s">'CALL'</span><span class="p">,</span> <span class="s">'调外部函数'</span><span class="p">),</span>
    <span class="c1"># … NOT / NEG / SHL / ADDR 等
</span><span class="p">}</span>

<span class="c1"># ③ 在派发点插桩，按指令边界切，把每条的 vreg 读写值抓下来
</span><span class="n">FETCH</span>  <span class="o">=</span> <span class="mh">0x1002B4118</span>   <span class="c1"># ldr  w24,[x8]       取 opcode 字 -&gt; word
</span><span class="n">DECODE</span> <span class="o">=</span> <span class="mh">0x1002B411C</span>   <span class="c1"># ubfx x8,x24,#6,#6   OP = (word &gt;&gt; 6) &amp; 0x3f
</span><span class="n">DISP</span>   <span class="o">=</span> <span class="mh">0x1002B414C</span>   <span class="c1"># br   x8             派发到 handler
# 虚拟寄存器：基址 = vPC 槽地址 + 8，r[i] 存于 [base + i*8]
</span><span class="k">def</span> <span class="nf">resolve_vregs</span><span class="p">(</span><span class="n">base</span><span class="p">,</span> <span class="n">mem_ops</span><span class="p">):</span>                    <span class="c1"># mem_ops: 该指令窗口的访存
</span>    <span class="k">return</span> <span class="p">{(</span><span class="n">a</span> <span class="o">-</span> <span class="n">base</span><span class="p">)</span><span class="o">//</span><span class="mi">8</span><span class="p">:</span> <span class="n">v</span> <span class="k">for</span> <span class="n">rw</span><span class="p">,</span> <span class="n">a</span><span class="p">,</span> <span class="n">v</span><span class="p">,</span> <span class="n">sz</span> <span class="ow">in</span> <span class="n">mem_ops</span>
            <span class="k">if</span> <span class="n">base</span> <span class="o">&lt;=</span> <span class="n">a</span> <span class="o">&lt;</span> <span class="n">base</span> <span class="o">+</span> <span class="mi">55</span><span class="o">*</span><span class="mi">8</span> <span class="ow">and</span> <span class="p">(</span><span class="n">a</span> <span class="o">-</span> <span class="n">base</span><span class="p">)</span> <span class="o">%</span> <span class="mi">8</span> <span class="o">==</span> <span class="mi">0</span><span class="p">}</span>
</code></pre></div></div>

<p>把 <code class="language-plaintext highlighter-rouge">decode()</code> 的输出套上 <code class="language-plaintext highlighter-rouge">SEM</code> 表、再带上 <code class="language-plaintext highlighter-rouge">resolve_vregs()</code> 解出的实参值，就是上面那段可读 trace。这几十行就是「反汇编一台未知 VM」的全部脚手架——难的不是解码，是先在派发循环里认出 <code class="language-plaintext highlighter-rouge">fetch/decode/br x8</code> 这三个点。</p>

<p><strong>识别语义：从轮常量认出 SM3。</strong> 有了可读 trace，下一步把「一堆标量运算」认成「已知算法」。最直接的线索是<strong>密码常量</strong>：寄存器流里出现 <code class="language-plaintext highlighter-rouge">0x79cc4519</code>（SM3 轮常量 Tj）、SHA1 的 <code class="language-plaintext highlighter-rouge">0x5a827999</code>、MD5 的 <code class="language-plaintext highlighter-rouge">0x67452301</code>——看到它们就知道这段在算哪个哈希。这一步是<strong>模式识别</strong>，trace 不会自动给你，得靠经验比对。</p>

<p><strong>还原高级代码：本质是数据流回溯。</strong> 「还原成高级代码」不是把每条 VM 指令翻成一行 C，而是把<strong>百万条标量运算压回「<code class="language-plaintext highlighter-rouge">field14 = SM3(query)[:6]</code>」这种一句话</strong>。做法是数据流回溯：</p>

<ol>
  <li>从输出（某个签名字段）<strong>反向切片</strong>：它的值由哪条指令算出、那条指令的操作数又来自哪——值导向地一层层往回追。</li>
  <li>追到一段满足 SM3 轮结构的运算 → <strong>整段折叠成 <code class="language-plaintext highlighter-rouge">SM3(消息)</code></strong>。</li>
  <li>再找那个消息的来源（被哈希的输入）。<code class="language-plaintext highlighter-rouge">field14</code> 就是这么还原的：回溯到一个明文 query 串，一层到底。</li>
</ol>

<p>举个具体的「前后对比」。下面是 VM 指令级 trace 里一轮 SM3 压缩的代表性片段（mnemonic、operand 编码、轮常量都是 trace 里实抓的）：</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>OP=0x16 ROR  r[t]   = ror(r[A], 20)            ; A &lt;&lt;&lt; 12
OP=0x24 ADD  r[t]   = r[t] + r[E]
OP=0x24 ADD  r[t]   = r[t] + 0x79cc4519        ; ← SM3 轮常量 Tj（认出它=认出 SM3）
OP=0x16 ROR  r[ss1] = ror(r[t], 25)            ; &lt;&lt;&lt; 7
OP=0x06 XOR  r[ss2] = r[ss1] ^ r[a12]
OP=0x06 XOR  ...     (FF/GG 布尔函数 + TT1/TT2，每轮 ~70 条标量 op)
...
OP=0x06 XOR  r[v0]  = r[a'] ^ r[v0]            ; ← feed-forward：V = ABCDEFGH ^ V_init
</code></pre></div></div>

<p>把这 64 轮、几千条标量 op <strong>折叠回去</strong>，就是一个标准 SM3 压缩函数；再往上一层，就是一句话：</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// 上面一整轮 ≈</span>
<span class="n">SS1</span> <span class="o">=</span> <span class="n">ROL</span><span class="p">(</span><span class="n">ROL</span><span class="p">(</span><span class="n">A</span><span class="p">,</span><span class="mi">12</span><span class="p">)</span> <span class="o">+</span> <span class="n">E</span> <span class="o">+</span> <span class="n">ROL</span><span class="p">(</span><span class="n">Tj</span><span class="p">,</span> <span class="n">j</span><span class="p">),</span> <span class="mi">7</span><span class="p">);</span>
<span class="n">SS2</span> <span class="o">=</span> <span class="n">SS1</span> <span class="o">^</span> <span class="n">ROL</span><span class="p">(</span><span class="n">A</span><span class="p">,</span><span class="mi">12</span><span class="p">);</span>
<span class="n">TT1</span> <span class="o">=</span> <span class="n">FF</span><span class="p">(</span><span class="n">A</span><span class="p">,</span><span class="n">B</span><span class="p">,</span><span class="n">C</span><span class="p">)</span> <span class="o">+</span> <span class="n">D</span> <span class="o">+</span> <span class="n">SS2</span> <span class="o">+</span> <span class="p">(</span><span class="n">Wj</span> <span class="o">^</span> <span class="n">Wj4</span><span class="p">);</span>
<span class="n">TT2</span> <span class="o">=</span> <span class="n">GG</span><span class="p">(</span><span class="n">E</span><span class="p">,</span><span class="n">F</span><span class="p">,</span><span class="n">G</span><span class="p">)</span> <span class="o">+</span> <span class="n">H</span> <span class="o">+</span> <span class="n">SS1</span> <span class="o">+</span> <span class="n">Wj</span><span class="p">;</span>
<span class="c1">// 64 轮 + feed-forward 折叠完：</span>
<span class="n">field14</span> <span class="o">=</span> <span class="n">SM3</span><span class="p">(</span><span class="s">"device_id=&lt;did&gt;&amp;iid=&lt;iid&gt;&amp;aid=&lt;aid&gt;"</span><span class="p">)[</span><span class="o">:</span><span class="mi">6</span><span class="p">];</span>
</code></pre></div></div>

<p>左边几千条 <code class="language-plaintext highlighter-rouge">ROR/ADD/XOR</code>，右边一行 —— 这就是「还原成高级代码」要做的压缩。认出 <code class="language-plaintext highlighter-rouge">0x79cc4519</code> 和结尾的 feed-forward XOR，是把这堆标量运算锚定成 SM3 的关键。</p>

<p><strong>还原的边界。</strong> 不是所有字段都能一句话还原。<code class="language-plaintext highlighter-rouge">field14</code> 运气好（消息是明文串）；<code class="language-plaintext highlighter-rouge">field13</code> 的 SM3 消息<strong>在 vreg 里经指针间接拼装、和 OLLVM 谓词交织</strong>，反向切片追到的是 SM3 中间态而非干净消息（6.4）。这类要么继续做<strong>指针感知的符号执行</strong>把 vreg 间接解开，要么上 <strong>IDA + 微码去混淆（d810）</strong> 把那段 handler 反编译出来——后者是「还原成高级代码」的重武器，以后有时间再继续。</p>

<blockquote>
  <p>小结：<strong>VM 反汇编 = 动态抓已解密的执行路径</strong>（可靠、可读）；<strong>还原高级代码 = 数据流回溯 + 算法识别</strong>（对干净输入能到「一句话」，对绑设备 / 指针间接的部分需符号执行或微码去混淆）。和「读懂一段加壳 native 函数」的差别在于：你面对的是一台自带指令集的虚拟机，得先摸清它的 ISA，再在它的指令层做数据流分析。</p>
</blockquote>

<h3 id="66-这轮深入分析印证了什么">6.6 这轮深入分析印证了什么</h3>

<ul>
  <li><strong>签名器是「字节码 VM + 国密 + 设备指纹」三件套</strong>，比「OLLVM native 函数」深一层。当初判断「静态性价比极低」是对的，而且低估了。</li>
  <li><strong>同一套签名里，字段难度天差地别</strong>：<code class="language-plaintext highlighter-rouge">field14</code> 是 query 的闭式 SM3，一层到底；<code class="language-plaintext highlighter-rouge">field13</code> 是设备指纹的键控 SM3，绑设备、藏在 VM 指针间接里，纯算法成本极高。</li>
  <li><strong>最关键的是</strong>：<code class="language-plaintext highlighter-rouge">field13</code> 即便复原出来也<strong>不可移植</strong>（它哈希的是真实设备指纹）。这说明对这类「键控 + 绑设备」的签名，纯算法还原的边际价值很低——逆清它的<strong>结构与性质</strong>（是什么、绑什么、为什么不可移植）才是分析的落点，而不是非要求出一份字节级闭式公式。</li>
</ul>

<blockquote>
  <p>一句话收口：能闭式的（<code class="language-plaintext highlighter-rouge">field14</code>）就闭式，绑设备 + VM 藏起来的（<code class="language-plaintext highlighter-rouge">field13</code>）就交给模拟器跑。<strong>知道哪些值得逆、哪些不值得逆，本身就是逆向的一部分。</strong></p>
</blockquote>

<hr />

<h2 id="七设备风险检测与反-frida">七、设备风险检测与反 frida</h2>

<p>签名只是 metasec 的一半活。另一半是<strong>给这台设备打风险分</strong>——它一边采指纹算签名，一边在背后探「是否调试、是不是越狱、有没有注入框架」，把结论编进签名里。这一节拆这套防御机制，也补上第三节「动态路全堵死」背后到底是怎么堵的。</p>

<h3 id="71-它在采什么风险信号">7.1 它在采什么风险信号</h3>

<p>用最朴素的办法看 metasec 探什么：hook 它会用到的底层 syscall——文件系统（<code class="language-plaintext highlighter-rouge">stat/lstat/access/open/fopen</code>）、进程（<code class="language-plaintext highlighter-rouge">ptrace/sysctl</code>）、环境（<code class="language-plaintext highlighter-rouge">getenv</code>），命中时打印<strong>调用方地址</strong>（<code class="language-plaintext highlighter-rouge">__builtin_return_address(0)</code>，过滤出 metasec 区的调用）。一跑就看清它的探针面：</p>

<table>
  <thead>
    <tr>
      <th>探测面</th>
      <th>手段</th>
      <th>探什么</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>调试器</td>
      <td><code class="language-plaintext highlighter-rouge">sysctl({CTL_KERN,KERN_PROC,KERN_PROC_PID,pid})</code> 查 <code class="language-plaintext highlighter-rouge">p_flag &amp; P_TRACED</code></td>
      <td>是否被 ptrace 附加（debugger / frida-server）</td>
    </tr>
    <tr>
      <td>越狱</td>
      <td><code class="language-plaintext highlighter-rouge">stat / access</code> 一串路径</td>
      <td>越狱文件、目录是否存在</td>
    </tr>
    <tr>
      <td>注入</td>
      <td>镜像枚举 / <code class="language-plaintext highlighter-rouge">fopen</code></td>
      <td>是否加载了 Substrate / frida 等</td>
    </tr>
    <tr>
      <td>环境</td>
      <td><code class="language-plaintext highlighter-rouge">getenv</code></td>
      <td><code class="language-plaintext highlighter-rouge">DYLD_INSERT_LIBRARIES</code> 等注入痕迹</td>
    </tr>
    <tr>
      <td>完整性</td>
      <td>自身代码/签名校验</td>
      <td>代码是否被 patch（见第六节三层保护）</td>
    </tr>
  </tbody>
</table>

<p>这些结论不单独上报，而是<strong>揉进签名</strong>：<code class="language-plaintext highlighter-rouge">X-Medusa</code> 明文 protobuf 里有个 <code class="language-plaintext highlighter-rouge">field24</code> 是风险 JSON，形如 <code class="language-plaintext highlighter-rouge">{"sts": ...}</code>（status，风险态），init 期就开始采。环境一旦「脏」，这个值就变——服务端拿它做风控。</p>

<h3 id="72-反-frida--反-trace为什么工具一碰就崩">7.2 反 frida / 反 trace：为什么工具一碰就崩</h3>

<p>第三节那张「动态路全堵死」的表，背后是一套组合拳。除了第六节的三层 VM 自毁保护（改代码就跳飞），还有专门针对插桩工具的：</p>

<p><strong>① 反调试</strong>：上面 <code class="language-plaintext highlighter-rouge">sysctl(KERN_PROC)</code> 查 <code class="language-plaintext highlighter-rouge">P_TRACED</code>，frida-server 用 ptrace 附加就中招。</p>

<p><strong>② 碰一下就杀</strong>：Interceptor hook 签名管理类的方法，甚至只是<strong>枚举一下这个类的方法列表</strong>，App 就 <code class="language-plaintext highlighter-rouge">Process terminated</code>——懒守卫，碰类即触发反 frida 检测，约 25s 杀进程。</p>

<p><strong>③ Stalker 跟进必崩——而且不是「检测到 Stalker」</strong>：这是最有意思的一点。反编译实锤，崩因是 <strong>return-地址 gadget</strong>：</p>

<pre><code class="language-asm">; 0x100109848（被调 257 次）
mov  x3, x30        ; 取返回地址
add  x3, x3, x0     ; 对它做算术
mov  x30, x3        ; 写回 LR
ret                 ; 跳到「算出来的」地址
</code></pre>

<p>正常执行时 x30 是真实返回地址，加完跳对地方。但 <strong>Stalker 会把代码搬到 scratch 区执行、借用 x30 放自己的蹦床</strong>——gadget 对蹦床地址做算术，结果跳进 scratch 乱区，崩。compute 区有 60+ 个这种 <code class="language-plaintext highlighter-rouge">ret</code>、5 个 <code class="language-plaintext highlighter-rouge">adr</code> 假 ret。<strong>所以不是「检测到 frida 才崩」，是控制流本身依赖真实 LR，Stalker 一搬就散架。</strong> 硬件断点 / QBDI 原地执行、保留真 x30，反而免疫。</p>

<p><strong>④ 没有固定 hook 点</strong>：这版 TTNet 是 Rust 直调 metasec native C，<strong>不走 ObjC 方法</strong>——按 selector 过滤 <code class="language-plaintext highlighter-rouge">objc_msgSend</code> 0 命中。签名计算又全在 VM 里 <code class="language-plaintext highlighter-rouge">br x8</code> 计算派发，没有 <code class="language-plaintext highlighter-rouge">bl 某函数</code> 这种固定调用点可下钩。</p>

<p><img src="/images/posts/ios/ai-app-protocol/11-antifrida.png" alt="反 frida / 反 trace：三条动态路各自的崩因" />
<em>图 11：反 frida / 反 trace——三条动态路各自的崩因</em></p>

<p>唯一安全的姿势：hook <strong>非 metasec 的 ttnet 胶水层</strong>（请求编排、<code class="language-plaintext highlighter-rouge">MD5(body)</code> 那些），从边界读 I/O、反推，但碰不到签名内部。</p>

<h3 id="73-风险态怎么反馈进签名">7.3 风险态怎么反馈进签名</h3>

<p>这套检测不是摆设，会<strong>直接改签名输出</strong>。在被插桩的真机上，抓到的 <code class="language-plaintext highlighter-rouge">X-Argus</code> 解出来是退化的占位值（base64 的递增 khronos），风险态 <code class="language-plaintext highlighter-rouge">sts</code> 也高——metasec 发现环境脏，干脆不给真签名。</p>

<blockquote>
  <p>这恰好是模拟执行能成的一个隐藏原因：<strong>模拟环境是「干净」的</strong>——没有 ptrace、没有注入库、没有越狱文件、代码没被 patch。风险检测全部通过，metasec 给的是<strong>正常签名</strong>，而不是真机被插桩时那种退化值。换句话说，模拟执行不光绕过了反调试，还顺带骗过了风险画像。</p>
</blockquote>

<h3 id="74-对抗这套的思路">7.4 对抗这套的思路</h3>

<ul>
  <li><strong>别 hook metasec 本体</strong>：碰类即死。要 trace 它，只有硬件断点（无痕、不改代码、保留真 LR）或 QBDI / Unicorn（原地执行）。</li>
  <li><strong>想读签名 I/O</strong>：hook 它外面的 ttnet 胶水层（参数进 / 结果出的边界）。</li>
  <li><strong>想要真签名</strong>：要么真机不插桩（但你就 trace 不了），要么用模拟执行——干净环境里原样跑函数，既绕反调试又过风险画像，一举两得。</li>
</ul>

<p>把第六、七节连起来看，metasec 的设计哲学就是三道咬合的防御，而它们共享同一个盲点：</p>

<p><img src="/images/posts/ios/ai-app-protocol/12-defense.png" alt="三道咬合防御 vs 模拟执行的干净环境" />
<em>图 12：三道咬合防御 vs 模拟执行的干净环境</em></p>

<blockquote>
  <p><strong>签名 = 设备指纹（绑死硬件）× 国密 VM（算法藏起来）× 风险画像（环境脏就退化）</strong>，三者层层咬合，任何一环单独绕不够。但它最大的盲点是「假设自己跑在真实、脏的设备上」——模拟执行给它一台<strong>干净的虚拟设备、原样跑函数</strong>，反调试、反插桩、风险画像同时落空。</p>
</blockquote>

<hr />

<h2 id="八安全设计评析">八、安全设计评析</h2>

<p>把前面拆开的东西换个视角——站在<strong>防御方</strong>看，这套 metasec 哪些设计是真有效、哪些是纸老虎。</p>

<h3 id="81-设计得好的地方">8.1 设计得好的地方</h3>

<table>
  <thead>
    <tr>
      <th>设计</th>
      <th>为什么有效</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><strong>纵深咬合</strong></td>
      <td>签名 = 设备指纹 × 国密 VM × 风险画像，三者层层依赖，单点绕过不够</td>
    </tr>
    <tr>
      <td><strong>绑硬件的指纹</strong></td>
      <td>GF/dtrait = 真实系统文件时间戳 + Apple 签名收据，伪不出来；且「采哪些」服务端动态下发，不给固定靶子</td>
    </tr>
    <tr>
      <td><strong>VM + 国密 + 三层自毁</strong></td>
      <td>静态反编译出空壳、动态插桩即自毁，把逆向成本抬到「另一个量级」</td>
    </tr>
    <tr>
      <td><strong>反插桩靠结构而非检测</strong></td>
      <td>return-gadget 依赖真实 LR、自地址派生密钥——绕过某个「检测点」没用，得不破坏整条密钥链。比 <code class="language-plaintext highlighter-rouge">if (frida) crash</code> 难绕得多</td>
    </tr>
    <tr>
      <td><strong>风险检测内嵌进签名</strong></td>
      <td>调试器/越狱/注入的检测结论不单独上报，而是编进签名 protobuf 的 <code class="language-plaintext highlighter-rouge">field24</code>（<code class="language-plaintext highlighter-rouge">{"sts":..}</code>）——<strong>每个签名请求都附一份难伪造的设备风险自评</strong>。服务端不必自己探测：环境脏 → 字段变 + 签名退化，双重信号一起到后端</td>
    </tr>
    <tr>
      <td><strong>键控 + 绑设备字段</strong></td>
      <td><code class="language-plaintext highlighter-rouge">field13</code> 即便逆出算法也不可移植（哈希真实设备指纹），跨设备复用无门</td>
    </tr>
  </tbody>
</table>

<h3 id="82-被绕过--可改进的地方">8.2 被绕过 / 可改进的地方</h3>

<table>
  <thead>
    <tr>
      <th>弱点</th>
      <th>说明</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><strong>核心假设是单点盲区</strong></td>
      <td>所有自毁保护都假设「对手在真实设备上、会改代码 / 搬代码」。模拟执行——干净虚拟设备 + 原地不改码——<strong>一次性绕过反调试、反插桩、风险画像三层</strong>；连带那份内嵌签名的 <code class="language-plaintext highlighter-rouge">field24</code> 风险自评，也照常自报「干净」。最强的护甲，挡的是它预设的那种攻击</td>
    </tr>
    <tr>
      <td><strong>风险自评只是客户端自述</strong></td>
      <td><code class="language-plaintext highlighter-rouge">field24</code> 是设备<strong>给自己</strong>打的分——签名只保证它没被改，不保证它说的是真话。在干净模拟环境里，它如实报告一个干净环境，于是「难篡改」的风险遥测反而成了「难篡改的假报告」</td>
    </tr>
    <tr>
      <td><strong>TTEncrypt 对称自包含</strong></td>
      <td>密钥派生常量内嵌（逆 S-box 两半异或）、<code class="language-plaintext highlighter-rouge">R</code> 内嵌密文、不与服务器协商 → 可完全离线复现 + 解密任意抓包，没有真正的「端密钥」</td>
    </tr>
    <tr>
      <td><strong>校验边界比预期小</strong></td>
      <td><code class="language-plaintext highlighter-rouge">device_register</code> 服务端<strong>不验签名</strong>，纯协议即可注册可用设备；签名实际拦截范围远小于它「看起来」覆盖的范围</td>
    </tr>
    <tr>
      <td><strong>端点强度不一致</strong></td>
      <td>同一份 <code class="language-plaintext highlighter-rouge">X-Gorgon</code>，chat 拦、注册 / 会话放行 → 弱端点成突破口</td>
    </tr>
    <tr>
      <td><strong>并非字段字段都绑死</strong></td>
      <td><code class="language-plaintext highlighter-rouge">field14</code> = query 的闭式 <code class="language-plaintext highlighter-rouge">SM3</code>，可纯算法复现；防护强度在字段间并不均匀</td>
    </tr>
  </tbody>
</table>

<h3 id="83-给攻防双方的启示">8.3 给攻防双方的启示</h3>

<ul>
  <li><strong>分析侧</strong>：先用最小请求探出真实校验边界，往往比预期窄；算法逆不动就模拟执行、观测产物；判断「哪些值得逆」本身是关键。</li>
  <li><strong>防御侧</strong>：最大盲点是「假设对手一定在真实设备、且会动代码」。该补的是<strong>把信任根下沉到服务端</strong>——真正的密钥协商而非内嵌常量、收据 / 证明在<strong>服务端</strong>验签而非仅本地、关键校验扩到所有端点。对模拟执行环境做检测当然也行，但那是又一轮军备竞赛，治标不治本。</li>
</ul>

<blockquote>
  <p>一句话：这套防御工程做得很扎实，<strong>强在「绑死硬件 + 抬高逆向成本 + 环境感知」</strong>；但它和绝大多数客户端防护一样，根子上的软肋是——<strong>安全逻辑跑在你不信任的设备上</strong>。一旦对手能在一个「干净、不改代码」的环境里把它原样执行，分层护甲就一起失效。真正的护城河，终究是服务端验证 + 绑服务器的密钥。</p>
</blockquote>

<hr />

<h2 id="九总结">九、总结</h2>

<table>
  <thead>
    <tr>
      <th>阶段</th>
      <th>手段</th>
      <th>结论</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>字段采集</td>
      <td>静态定位采集点</td>
      <td>IDFV+cdid 是稳定标识，决定新老设备；部分字段 base64-key 混淆采集</td>
    </tr>
    <tr>
      <td>body 组合</td>
      <td>BDInstall 序列化</td>
      <td><code class="language-plaintext highlighter-rouge">{magic_tag, header{...}}</code> → 紧凑 JSON → gzip</td>
    </tr>
    <tr>
      <td>请求体加密</td>
      <td>静态还原 TTEncrypt</td>
      <td>SHA512×2 派生 + AES-128-CBC，纯 Python 复现、真机逐字节一致</td>
    </tr>
    <tr>
      <td>动态指纹采集</td>
      <td>服务端下发配置</td>
      <td>GF/dtrait：服务器下发“采哪些系统文件时间戳”+ App Store 收据，难伪造；device_register 不依赖</td>
    </tr>
    <tr>
      <td>metasec 签名</td>
      <td><strong>模拟执行 + 观测</strong></td>
      <td>OLLVM 静态难以推进，在受控 Unicorn 环境执行签名函数、观测其产出</td>
    </tr>
    <tr>
      <td>签名算法（深入分析）</td>
      <td>trace + 数据流切片</td>
      <td>签名器是<strong>字节码 VM</strong>（三层自毁保护）；<code class="language-plaintext highlighter-rouge">X-Medusa</code> 拆出明文 protobuf；<code class="language-plaintext highlighter-rouge">field14</code>=<code class="language-plaintext highlighter-rouge">SM3(query)[:6]</code> 闭式，<code class="language-plaintext highlighter-rouge">field13</code>=键控 SM3(设备指纹) 绑设备不可移植</td>
    </tr>
    <tr>
      <td>风险检测 / 反插桩</td>
      <td>ReconAF 探针 + 反编译</td>
      <td>探调试器/越狱/注入/环境，结论编进 <code class="language-plaintext highlighter-rouge">field24</code> 风险 JSON；反 Stalker 是 return-gadget 依赖真实 LR（非检测）；被插桩则签名退化。模拟执行的干净环境同时绕反调试 + 过风险画像</td>
    </tr>
    <tr>
      <td>device_register</td>
      <td>协议拼装</td>
      <td>服务端<strong>不验签名</strong>，纯 body+stub+khronos 即可注册可用设备</td>
    </tr>
    <tr>
      <td>chat/completion</td>
      <td>协议拼装 + 签名</td>
      <td>body 明文 JSON，但<strong>验签名</strong>，靠模拟执行产出的 X-Gorgon 才放行</td>
    </tr>
  </tbody>
</table>

<p>各接口 / 数据的加密、编码与签名一览：</p>

<table>
  <thead>
    <tr>
      <th>接口 / 数据</th>
      <th>编码·加密</th>
      <th>签名</th>
      <th>说明</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">device_register</code> body</td>
      <td>gzip → TTEncrypt(AES-128-CBC)</td>
      <td>不验</td>
      <td>设备信息注册</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">chat/completion</code> body</td>
      <td>明文 JSON</td>
      <td><strong>验 X-Gorgon</strong></td>
      <td>AI 对话，SSE 流式响应</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">/im/conversation/*</code></td>
      <td>明文 JSON</td>
      <td>不验</td>
      <td>会话管理（main 拿会话 / in_out 进会话）</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">stats_collect</code> 上报</td>
      <td>TTEncrypt → gzip → base64 → bd_pack(AES-256)</td>
      <td>不验</td>
      <td>GF/dtrait 指纹上报（三层套娃）</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">stats_collect</code> 下发配置</td>
      <td>同上（bd_pack <code class="language-plaintext highlighter-rouge">a7=1</code> 常量）</td>
      <td>—</td>
      <td>动态采集配置（采哪些文件）</td>
    </tr>
  </tbody>
</table>

<p>几点心得：</p>

<ol>
  <li><strong>逆向不止“读懂算法”一条路。</strong> 当目标被 OLLVM / VMP / 国密堆到静态性价比极低时，“把那段代码放进受控环境原样执行、观测它的输入输出”往往更快。基于 Unicorn 这类 CPU 模拟器自建的执行环境，本质是把 App 的一个函数变成<strong>可观测、可复跑的对象</strong>——用来取样和验证，而不是替你读懂它。</li>
  <li><strong>先发一个最小请求探路，再决定逆什么。</strong> 没先用最小头试 device_register，我会一直以为必须先还原 X-Gorgon——结果它并不需要。服务器真实校验的范围，往往比你以为的小。</li>
  <li><strong>同一套签名，不同端点校验强度不同。</strong> device_register 放行、chat 拦截，是同一份 <code class="language-plaintext highlighter-rouge">X-Gorgon</code> 在不同后端的不同待遇。别用一个接口的结论套另一个。</li>
  <li><strong>采集这块最容易被忽视但最关键。</strong> 协议跑通后“能不能持续用、会不会被风控”，很大程度取决于设备字段采得真不真、组合得对不对。</li>
  <li><strong>知道哪些值得逆，本身就是逆向的一部分。</strong> 回头深入签名（第六节）才看清：它是字节码 VM + 国密 + 设备指纹键控。能闭式的字段（<code class="language-plaintext highlighter-rouge">field14 = SM3(query)[:6]</code>）就闭式还原；绑设备、藏在 VM 指针间接里的字段（<code class="language-plaintext highlighter-rouge">field13</code> = 键控 SM3(设备指纹)）即便复原出来也不可移植——对这类签名，逆清结构与性质比求一份字节级公式更有价值。判断「不值得纯算法逆」和真把它逆出来，同样重要。</li>
  <li><strong>模拟执行顺带绕过了风险画像。</strong> metasec 不只算签名，还探调试器/越狱/注入/完整性，环境不干净就让签名退化（第七节）。模拟执行给它一台<strong>干净虚拟设备、原样跑函数</strong>——反调试、反插桩、风险检测三道防御同时落空。逆向防御机制时，先想清楚它「假设自己跑在什么环境里」，往往就找到了那个盲点。</li>
</ol>

<p>最终成果：从设备注册到 AI 对话，整条协议链逐层拆清，并以一次端到端跑通验证到位；签名头通过模拟执行观测取样。再回头拆开签名算法，逆出了能闭式的字段、证明了绑设备字段不可移植——<strong>协议链分析与签名算法逆向，两头都落到了实处</strong>。</p>

<blockquote>
  <p>本文仅用于安全研究与协议分析学习，相关标识已脱敏。</p>
</blockquote>]]></content><author><name>我是小三</name></author><category term="iOS" /><category term="iOS" /><category term="逆向" /><category term="协议分析" /><category term="metasec" /><category term="TTEncrypt" /><category term="设备指纹" /><category term="VM" /><category term="反虚拟化" /><summary type="html"><![CDATA[完整拆解某 AI App（字节系）iOS 端的设备注册与 AI 对话链路——设备字段采集、请求体组合与 TTEncrypt 加密、metasec 签名与设备风险检测；签名因 OLLVM + 国密混淆静态难以推进，转而在受控的 Unicorn 模拟环境里执行签名函数、观测其产出；再深入拆开签名算法，发现它是字节码 VM + 国密 SM3 + 设备指纹键控，逆出可闭式的字段、证明绑设备字段不可移植。文末以一次端到端协议跑通验证整条链。]]></summary></entry><entry><title type="html">博客搭建完成</title><link href="https://xiaosan-re.github.io/2026/06/05/hello-world/" rel="alternate" type="text/html" title="博客搭建完成" /><published>2026-06-05T00:00:00+08:00</published><updated>2026-06-05T00:00:00+08:00</updated><id>https://xiaosan-re.github.io/2026/06/05/hello-world</id><content type="html" xml:base="https://xiaosan-re.github.io/2026/06/05/hello-world/"><![CDATA[<p>这是博客的第一篇文章。以后会在这里记录 Android native 逆向相关的内容：</p>

<ul>
  <li>so 加固脱壳</li>
  <li>OLLVM / 控制流平坦化反混淆</li>
  <li>Frida hook 与 trace 实战</li>
  <li>IDA / capstone 使用笔记</li>
</ul>

<h2 id="怎么写新文章">怎么写新文章</h2>

<p>在 <code class="language-plaintext highlighter-rouge">_posts/</code> 目录下新建文件，命名格式为 <code class="language-plaintext highlighter-rouge">年-月-日-英文标题.md</code>，开头带上 front matter：</p>

<div class="language-markdown highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nn">---</span>
<span class="na">title</span><span class="pi">:</span> <span class="s">文章标题</span>
<span class="na">categories</span><span class="pi">:</span> <span class="s">Android</span>
<span class="na">tags</span><span class="pi">:</span> <span class="pi">[</span><span class="nv">Frida</span><span class="pi">,</span> <span class="nv">Android</span><span class="pi">]</span>
<span class="nn">---</span>

正文用 Markdown 写……
</code></pre></div></div>

<p>提交推送后，GitHub 会自动重新构建并更新网站。</p>

<h2 id="代码高亮示例">代码高亮示例</h2>

<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">base</span> <span class="o">=</span> <span class="nx">Module</span><span class="p">.</span><span class="nx">findBaseAddress</span><span class="p">(</span><span class="dl">"</span><span class="s2">libfoo.so</span><span class="dl">"</span><span class="p">);</span>
<span class="nx">Interceptor</span><span class="p">.</span><span class="nx">attach</span><span class="p">(</span><span class="nx">base</span><span class="p">.</span><span class="nx">add</span><span class="p">(</span><span class="mh">0x1234</span><span class="p">),</span> <span class="p">{</span>
  <span class="nx">onEnter</span><span class="p">(</span><span class="nx">args</span><span class="p">)</span> <span class="p">{</span>
    <span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="dl">"</span><span class="s2">arg0 =</span><span class="dl">"</span><span class="p">,</span> <span class="nx">args</span><span class="p">[</span><span class="mi">0</span><span class="p">]);</span>
  <span class="p">},</span>
<span class="p">});</span>
</code></pre></div></div>]]></content><author><name>我是小三</name></author><category term="杂记" /><category term="杂记" /><summary type="html"><![CDATA[博客的第一篇文章，介绍这里以后会写些什么。]]></summary></entry><entry><title type="html">iOS DeviceCheck：苹果设备身份链与黑灰产对抗的攻防博弈</title><link href="https://xiaosan-re.github.io/2026/06/05/ios-devicecheck/" rel="alternate" type="text/html" title="iOS DeviceCheck：苹果设备身份链与黑灰产对抗的攻防博弈" /><published>2026-06-05T00:00:00+08:00</published><updated>2026-06-05T00:00:00+08:00</updated><id>https://xiaosan-re.github.io/2026/06/05/ios-devicecheck</id><content type="html" xml:base="https://xiaosan-re.github.io/2026/06/05/ios-devicecheck/"><![CDATA[<h2 id="一背景--场景">一、背景 &amp; 场景</h2>

<h3 id="11-环境">1.1 环境</h3>

<p>设备型号:iphone 14、iOS 版本:16.4.1、越狱方案:Dopamine</p>

<p>工具链：Frida16.1.4、IDA pro 9.2</p>

<h3 id="12-引言">1.2 引言</h3>

<p>对于大多数互联网风控攻防业务来说，真正棘手的往往不是单个请求本身，而是背后那一整套“批量造设备、批量刷行为”的自动化体系。正是在这样的背景下，从最初的简单模拟请求，到如今的设备指纹伪造、群控刷量，攻防对抗从未停止。在这一背景下，Apple 推出的 <strong>DeviceCheck</strong>（以及后续的 <strong>App Attest</strong>）成为了 iOS 平台上反作弊与反爬虫的重要防线。</p>

<p>DeviceCheck 的核心目标是<strong>在保护用户隐私的前提下，为开发者提供一种持久化的设备标记能力</strong>。即使 App 被卸载重装，甚至设备被恢复出厂设置（在某些条件下），DeviceCheck 存储的 2-bit 状态依然可以被保留。这使得它成为了对抗“新用户薅羊毛”、“设备封禁”等场景的利器。</p>

<p>然而，对于安全研究人员和黑灰产从业者而言，DeviceCheck 也是一个必须攻克的堡垒。本文将基于逆向工程视角，简单剖析 DeviceCheck 的底层实现，揭示其加密流程，并探讨在越狱环境下进行攻防对抗的可行性。</p>

<h3 id="13研究动机与目标">1.3、研究动机与目标</h3>

<ul>
  <li>
    <p>验证通过改机（UniqueChipID / ECID/SerialNumber等）为什么不能激活设备？</p>
  </li>
  <li>
    <p>想搞清楚：苹果到底在检验什么？哪些字段是“硬绑定”的？</p>
  </li>
  <li>
    <p>从攻击与防护视角学习如何应用好DeviceCheck?</p>
  </li>
</ul>

<h3 id="14整体流程与框架">1.4、整体流程与框架</h3>

<ul>
  <li>激活流程</li>
</ul>

<p><img src="/images/posts/ios/devicecheck/img-01.png" alt="" /></p>

<ul>
  <li>Device Token生成流程</li>
</ul>

<p><img src="/images/posts/ios/devicecheck/img-02.png" alt="" /></p>

<h2 id="二mobileactivationd-激活流程分析">二、mobileactivationd** 激活流程分析**</h2>

<h3 id="21什么时候会激活">2.1、<strong>什么时候会激活？</strong></h3>

<p>这里说的激活，就是由 <strong>mobileactivationd + Apple 服务器</strong> 参与的那种“和苹果聊一聊，我是谁、能不能用”的流程，而不是普通 App 级别的登录。</p>

<h3 id="首次开箱--刷机之后第一次开机"><strong>首次开箱 / 刷机之后第一次开机</strong></h3>

<ul>
  <li>
    <p>全新 iPhone、刚刷完原版 IPSW、DFU 恢复之后第一次进开机向导。</p>
  </li>
  <li>
    <p>这时候本地没任何“激活凭据”，系统必须：</p>
  </li>
  <li>
    <p>收集硬件指纹（ECID、ChipID、BoardId、SerialNumber…）</p>
  </li>
  <li>
    <p>跟 Apple 服务器谈一轮，生成 / 刷新 uik.pem / ucrt.pem、设备证书等。</p>
  </li>
  <li>
    <p>你能看到的表现就是：</p>
  </li>
</ul>

<blockquote>
  <ul>
    <li>屏幕上显示  “正在激活 iPhone，需要几分钟”</li>
  </ul>

</blockquote>

<h3 id="恢复出厂设置--抹掉所有内容之后"><strong>恢复出厂设置 / 抹掉所有内容之后</strong></h3>

<ul>
  <li>
    <p>在<strong>设置 → 通用 → 传输或还原 iPhone → 抹掉所有内容和设置</strong>，或者用 Finder/iTunes 恢复。</p>
  </li>
  <li>
    <p>抹掉后再次启动，和第一次开箱本质一样，也要重新激活。</p>
  </li>
  <li>
    <p>这时 mobileactivationd 会：</p>
  </li>
  <li>
    <p>检查旧的激活票据是否还有效</p>
  </li>
  <li>
    <p>一般还是会重新向服务器请求新的激活证书（你抓到的 RKCertification 就是这类东西）。</p>
  </li>
</ul>

<h3 id="硬件变化比较大时换本板--基带--改机"><strong>硬件变化比较大时（换本板 / 基带 / 改机）</strong></h3>

<ul>
  <li>
    <p>主板更换、严重“硬改机”、某些基带相关硬件变化，会导致：</p>
  </li>
  <li>
    <p>UniqueChipID（ECID）、ChipID、BoardId、SecurityDomain 组合发生改变。</p>
  </li>
  <li>
    <p>Apple 服务器视角：这是“另一台设备”，必须重新审查：</p>
  </li>
  <li>
    <p>有没有被报失</p>
  </li>
  <li>
    <p>有没有被运营商锁 / 黑名单</p>
  </li>
  <li>
    <p>所以一旦检测到这种变化，系统会强制重新走激活流程。</p>
  </li>
  <li>
    <p>你的 hook 里如果把 UniqueChipID 换成别的机器的 ECID，就会看到服务器直接回：</p>
  </li>
</ul>

<blockquote>
  <ul>
    <li>
      <p>ECC signature verification failed</p>
    </li>
    <li>
      <p>说明<strong>硬件指纹和之前那套证书对不上</strong>，激活不通过。</p>
    </li>
  </ul>

</blockquote>

<h3 id="某些大版本升级--本地激活证书过期异常"><strong>某些大版本升级 / 本地激活证书过期异常</strong></h3>

<ul>
  <li>
    <p>iOS 大版本升级、系统内部结构变动，有时会触发：</p>
  </li>
  <li>
    <p>重签发部分证书</p>
  </li>
  <li>
    <p>重新校验激活状态</p>
  </li>
  <li>
    <p>如果本地激活缓存损坏（文件坏了、权限错了，或者你乱动 system volume），系统也可能判定无效，要求重新激活。</p>
  </li>
</ul>

<h3 id="和运营商--区域相关的情况"><strong>和运营商 / 区域相关的情况</strong></h3>

<ul>
  <li>
    <p>有运营商锁（有锁机）、合约机、某些地区政策机型（比如中国/欧盟特定版本），激活时会把：</p>
  </li>
  <li>
    <p>运营商信息</p>
  </li>
  <li>
    <p>区域/国家代码</p>
  </li>
  <li>
    <p>一起发给 Apple，</p>
  </li>
  <li>
    <p>服务器会根据政策决定：</p>
  </li>
  <li>
    <p>是否允许在当前运营商/区域激活</p>
  </li>
  <li>
    <p>是否切换锁状态（从未激活变成“锁到某运营商”）</p>
  </li>
</ul>

<blockquote>
  <p>小结一句：</p>

  <p><strong>只要 Apple 觉得“我有理由重新确认你这台机器的身份/状态”，就会触发 mobileactivationd 跑激活。</strong></p>

</blockquote>

<h3 id="22为什么要激活">2.2、<strong>为什么要激活？</strong></h3>

<p>从苹果的角度，这一整套“激活”设计，主要是为了解决三大问题：<strong>防伪造、防偷盗、统一发证</strong>。DeviceCheck / AppAttest 其实都是站在这套基础设施上实现的。</p>

<h3 id="确认这是一台真机而不是山寨模拟设备"><strong>确认“这是一台真机，而不是山寨/模拟设备”</strong></h3>

<ul>
  <li>
    <p>激活时上传的 RKCertification 里，包含：</p>
  </li>
  <li>
    <p>从 Secure Enclave 拿到的<strong>真·硬件密钥的 attestation</strong></p>
  </li>
  <li>
    <p>ECID / ChipID / BoardId / SerialNumber / SecurityDomain 等指纹</p>
  </li>
  <li>
    <p>苹果在服务器上用自己的 CA 体系校验证书链：</p>
  </li>
  <li>
    <p>证书是不是 Apple 自己签出来的</p>
  </li>
  <li>
    <p>硬件签名是不是由安全芯片里那把私钥算出来的</p>
  </li>
  <li>
    <p>结论：</p>

    <ul>
      <li>
        <p>你光在软件层面“改 MGCopyAnswer 值”是骗不过的；</p>
      </li>
      <li>
        <p>没有 SE + 正规证书链，服务器不会把你当成一台合法 iPhone。</p>
      </li>
    </ul>
  </li>
</ul>

<h3 id="防丢失防盗机控制设备生命周期"><strong>防丢失、防盗机、控制设备生命周期</strong></h3>

<p>激活是苹果“设备生命周期管理”的关键环节：</p>

<ul>
  <li>
    <p><strong>防盗 / 查找我的 iPhone</strong>：</p>
  </li>
  <li>
    <p>被“查找”绑定过的机器，如果被抹掉再激活，Apple 会要求输入原 Apple ID 才能继续。</p>
  </li>
  <li>
    <p><strong>黑名单 / 封禁</strong>：</p>
  </li>
  <li>
    <p>被判定为重大违规（盗刷、欺诈）的 ECID/序列号，可以在服务器侧直接标记，下一次激活就直接拒绝。</p>
  </li>
  <li>
    <p><strong>区域控制</strong>：</p>
  </li>
  <li>
    <p>比如某些功能/频段只在特定地区开放，也可以通过激活阶段写入策略。</p>
  </li>
</ul>

<h3 id="为上层安全服务铺路你现在在玩的部分"><strong>为上层安全服务“铺路”（你现在在玩的部分）</strong></h3>

<p>激活完成后，mobileactivationd 会产出一套“设备根身份”：</p>

<ul>
  <li>
    <p>系统组文件：uik.pem / ucrt.pem</p>
  </li>
  <li>
    <p>Keychain 里的：</p>

    <ul>
      <li>
        <p>2bit-identity-rk</p>
      </li>
      <li>
        <p>2bit-identity-leaf / intermediate / combined</p>
      </li>
      <li>
        <p>2bit-identity-monotonic-clock / rtc-reset-count / server-timestamp 等</p>
      </li>
    </ul>
  </li>
  <li>
    <p>这些东西后面会被谁用？</p>

    <ul>
      <li>
        <p>devicecheckd 生成 DeviceCheck Token</p>
      </li>
      <li>
        <p>AppAttest 做应用级 attestation</p>
      </li>
      <li>
        <p>各种安全相关 daemon（FairPlay、Apple Pay、Find My…）</p>
      </li>
    </ul>
  </li>
</ul>

<h3 id="23激活流程逆向分析">2.3、激活流程逆向分析</h3>

<ul>
  <li>采集设备信息</li>
</ul>

<p>主要通过copyAnswer:方法采集设备唯一性相关信息</p>

<pre><code class="language-JSON">UniqueChipID:    "6871544955xxxxxx", 
ChipID:          "33040",
BoardId:         "24",
SecurityDomain:  "1",
SerialNumber:    "F6X439xxxxxx",
BuildVersion:    "20E252",
UniqueDeviceID:  "00008110-303C30C91A0xxxxxx"
</code></pre>

<ul>
  <li>代码逻辑</li>
</ul>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>// 获取设备信息
void __fastcall create_baa_info_sub_1000EEDCC(__SecKey *a1, id a2, _QWORD *a3)
{
  id v4; // x25
  id v5; // x20
  void *v6; // x21
  id v7; // x20
  int v8; // w5
  int v9; // w6
  int v10; // w7
  id v11; // x19
  NSArray *v12; // x21
  void *v13; // x27
  NSNumber *v14; // x0
  int v15; // w5
  int v16; // w6
  int v17; // w7
  NSNumber *v18; // x24
  void *v19; // x22
  id v20; // x23
  void *v21; // x27
  void *v22; // x22
  char *v23; // x0
  char *v24; // x23
  __int64 v25; // x25
  char *i; // x28
  __SecKey *v27; // x24
  int v28; // w5
  int v29; // w6
  int v30; // w7
  const void *v31; // x9
  void *v32; // x27
  void *v33; // x26
  void *v34; // x23
  void *v35; // x19
  _BOOL4 v36; // w9
  __SecKey *v37; // x20
  void *v38; // x23
  _BOOL4 v39; // w24
  _BOOL4 v40; // w19
  NSNumber *v41; // x26
  id v42; // x22
  int v43; // w1
  __SecKey *v44; // x21
  int v45; // w5
  int v46; // w6
  int v47; // w7
  id v48; // x20
  __SecKey *v49; // x0
  int v50; // w5
  int v51; // w6
  int v52; // w7
  CFDataRef v53; // x21
  int v54; // w5
  int v55; // w6
  int v56; // w7
  __int64 v57; // x0
  int v58; // w5
  int v59; // w6
  int v60; // w7
  char v61; // w22
  int v62; // w24
  int v63; // w5
  int v64; // w6
  int v65; // w7
  __SecKey *v66; // x19
  int v67; // w5
  int v68; // w6
  int v69; // w7
  unsigned int v70; // w28
  void *v71; // x21
  __SecKey *v72; // x22
  int v73; // w5
  int v74; // w6
  int v75; // w7
  int v76; // w5
  int v77; // w6
  int v78; // w7
  id v79; // x19
  int v80; // w5
  int v81; // w6
  int v82; // w7
  void *v83; // x21
  id v84; // x22
  NSNumber *v85; // x25
  NSNumber *v86; // x19
  NSNumber *v87; // x21
  unsigned __int8 v88; // w22
  int v89; // w5
  int v90; // w6
  int v91; // w7
  NSNumber *v92; // x19
  unsigned int v93; // w20
  int v94; // w5
  int v95; // w6
  int v96; // w7
  const __CFString *v97; // x4
  int v98; // w1
  int v99; // w2
  id v100; // x20
  int v101; // w5
  int v102; // w6
  int v103; // w7
  __SecKey *v104; // x0
  int v105; // w5
  int v106; // w6
  int v107; // w7
  NSFileManager *v108; // x20
  NSFileManager *v109; // x21
  void *v110; // x22
  unsigned __int8 v111; // w23
  int v112; // w5
  int v113; // w6
  int v114; // w7
  id v115; // x20
  id v116; // x20
  int v117; // w5
  int v118; // w6
  int v119; // w7
  id v120; // x20
  void *v121; // x21
  id v122; // x20
  int v123; // w5
  int v124; // w6
  int v125; // w7
  id v126; // x20
  void *v127; // x21
  id v128; // x20
  int v129; // w5
  int v130; // w6
  int v131; // w7
  id v132; // x20
  __SecKey *v133; // x21
  id v134; // x20
  int v135; // w5
  int v136; // w6
  int v137; // w7
  id v138; // x20
  __SecKey *v139; // x21
  id v140; // x20
  int v141; // w5
  int v142; // w6
  int v143; // w7
  id v144; // x21
  void *v145; // x26
  int v146; // w5
  int v147; // w6
  int v148; // w7
  __SecKey *v149; // x0
  int v150; // w5
  int v151; // w6
  int v152; // w7
  CFDataRef v153; // x0
  int v154; // w5
  int v155; // w6
  int v156; // w7
  CFDataRef v157; // x24
  int v158; // w1
  id v159; // x26
  id v160; // x21
  int v161; // w5
  int v162; // w6
  int v163; // w7
  __SecKey *v164; // x19
  __CFString **v165; // x8
  CFDataRef v166; // x26
  CFDataRef v167; // x2
  void *v168; // x20
  id v169; // x21
  void *v170; // x20
  void *v171; // x21
  void *v172; // x0
  void *v173; // x23
  int v174; // w5
  int v175; // w6
  int v176; // w7
  void *v177; // x20
  int v178; // w5
  int v179; // w6
  int v180; // w7
  void *v181; // x20
  void *v182; // x21
  void *v183; // x0
  void *v184; // x20
  int v185; // w5
  int v186; // w6
  int v187; // w7
  void *v188; // x20
  int v189; // w5
  int v190; // w6
  int v191; // w7
  void *v192; // x20
  void *v193; // x21
  void *v194; // x0
  void *v195; // x20
  int v196; // w5
  int v197; // w6
  int v198; // w7
  int v199; // w5
  int v200; // w6
  int v201; // w7
  int v202; // w5
  int v203; // w6
  int v204; // w7
  __SecKey *v205; // x0
  void *v206; // x20
  id v207; // x21
  void *v208; // x20
  void *v209; // x21
  id v210; // x22
  void *v211; // x21
  unsigned int v212; // w19
  void *v213; // x22
  id v214; // x23
  void *v215; // x21
  void *v216; // x22
  id v217; // x23
  void *v218; // x21
  void *v219; // x22
  id v220; // x23
  void *v221; // x21
  void *v222; // x22
  id v223; // x23
  void *v224; // x23
  int v225; // w5
  int v226; // w6
  int v227; // w7
  void *v228; // x22
  id v229; // x23
  __SecKey *v230; // x21
  __SecKey *v231; // x24
  void *v232; // x28
  id v233; // x27
  void *v234; // x25
  void *v235; // x26
  __SecKey *v236; // x23
  void *v237; // x22
  id v238; // x19
  const void *v239; // x20
  __SecKey *v240; // x21
  __SecKey *v241; // x0
  id v242; // x19
  NSDictionary *v243; // x22
  void *v244; // x24
  __CFString **v245; // x8
  void *v246; // x0
  id v247; // x0
  void *v248; // x22
  char v249; // w23
  id v250; // x19
  id v251; // x23
  int v252; // w5
  int v253; // w6
  int v254; // w7
  id v255; // x19
  id v256; // x24
  int v257; // w5
  int v258; // w6
  int v259; // w7
  char is_darwinos; // w0
  id v261; // x19
  id v262; // x21
  int v263; // w5
  int v264; // w6
  int v265; // w7
  id v266; // x24
  NSData *v267; // x24
  int v268; // w5
  int v269; // w6
  int v270; // w7
  CFDataRef v271; // x2
  int v272; // w5
  int v273; // w6
  int v274; // w7
  char v275; // [xsp+0h] [xbp-3B0h]
  char v276; // [xsp+0h] [xbp-3B0h]
  void *v277; // [xsp+8h] [xbp-3A8h]
  __SecKey *v278; // [xsp+18h] [xbp-398h]
  __SecKey *v279; // [xsp+20h] [xbp-390h]
  void *v280; // [xsp+28h] [xbp-388h]
  void *v281; // [xsp+30h] [xbp-380h]
  void *v282; // [xsp+38h] [xbp-378h]
  __SecKey *v283; // [xsp+40h] [xbp-370h]
  __SecKey *v284; // [xsp+48h] [xbp-368h]
  __SecKey *cf; // [xsp+50h] [xbp-360h]
  id v286; // [xsp+58h] [xbp-358h]
  id v287; // [xsp+58h] [xbp-358h]
  char v288[8]; // [xsp+60h] [xbp-350h]
  __SecKey *v289; // [xsp+68h] [xbp-348h]
  __SecKey *v290; // [xsp+70h] [xbp-340h]
  const void *v291; // [xsp+78h] [xbp-338h]
  id v292; // [xsp+80h] [xbp-330h]
  id v293; // [xsp+80h] [xbp-330h]
  __SecKey *v294; // [xsp+88h] [xbp-328h]
  __SecKey *v295; // [xsp+88h] [xbp-328h]
  CFDataRef v296; // [xsp+90h] [xbp-320h]
  unsigned int v297; // [xsp+98h] [xbp-318h]
  NSData *v298; // [xsp+98h] [xbp-318h]
  CFDataRef v299; // [xsp+A0h] [xbp-310h]
  id v300; // [xsp+A8h] [xbp-308h]
  id v301; // [xsp+B0h] [xbp-300h]
  id v302; // [xsp+B8h] [xbp-2F8h]
  _BOOL4 v303; // [xsp+C0h] [xbp-2F0h]
  _BOOL4 v304; // [xsp+C0h] [xbp-2F0h]
  id v305; // [xsp+C0h] [xbp-2F0h]
  id v306; // [xsp+C8h] [xbp-2E8h]
  id v307; // [xsp+D0h] [xbp-2E0h]
  NSNumber *v308; // [xsp+D8h] [xbp-2D8h]
  NSNumber *v309; // [xsp+E0h] [xbp-2D0h]
  id v310; // [xsp+E8h] [xbp-2C8h]
  id v311; // [xsp+F0h] [xbp-2C0h]
  id v312; // [xsp+F8h] [xbp-2B8h]
  NSNumber *v313; // [xsp+100h] [xbp-2B0h]
  id v314; // [xsp+108h] [xbp-2A8h]
  id v315; // [xsp+110h] [xbp-2A0h]
  __SecKey *v316; // [xsp+118h] [xbp-298h]
  unsigned int v317; // [xsp+118h] [xbp-298h]
  id v318; // [xsp+120h] [xbp-290h]
  id v319; // [xsp+128h] [xbp-288h]
  CFDataRef v320; // [xsp+130h] [xbp-280h]
  id v321; // [xsp+138h] [xbp-278h]
  NSNumber *v322; // [xsp+140h] [xbp-270h]
  __SecKey *v323; // [xsp+148h] [xbp-268h]
  id v324; // [xsp+148h] [xbp-268h]
  id v325; // [xsp+148h] [xbp-268h]
  id v327; // [xsp+158h] [xbp-258h] BYREF
  id v328; // [xsp+160h] [xbp-250h] BYREF
  id v329; // [xsp+168h] [xbp-248h] BYREF
  id v330[2]; // [xsp+170h] [xbp-240h] BYREF
  id v331; // [xsp+180h] [xbp-230h] BYREF
  id v332; // [xsp+188h] [xbp-228h] BYREF
  id v333; // [xsp+190h] [xbp-220h] BYREF
  id v334[3]; // [xsp+198h] [xbp-218h] BYREF
  id v335[2]; // [xsp+1B0h] [xbp-200h] BYREF
  id v336[2]; // [xsp+1C0h] [xbp-1F0h]
  id v337[2]; // [xsp+1D0h] [xbp-1E0h]
  id v338[2]; // [xsp+1E0h] [xbp-1D0h]
  CFErrorRef error; // [xsp+1F0h] [xbp-1C0h] BYREF
  id v340[12]; // [xsp+1F8h] [xbp-1B8h] BYREF
  _QWORD v341[12]; // [xsp+258h] [xbp-158h] BYREF
  char v342[8]; // [xsp+2B8h] [xbp-F8h] BYREF
  _QWORD v343[3]; // [xsp+338h] [xbp-78h] BYREF
  __int64 vars8; // [xsp+3B8h] [xbp+8h]

  v4 = objc_retain(a2);
  error = 0;
  v5 = objc_retainAutoreleasedReturnValue(+[GestaltHlpr getSharedInstance](&amp;OBJC_CLASS___GestaltHlpr, "getSharedInstance"));
  v6 = objc_msgSend(v5, "copyAnswer:", CFSTR("s7nuHoZIYNoOHCqT9iyZkQ"));
  objc_release(v5);
  v319 = v6;
  v7 = objc_retainAutoreleasedReturnValue((id)isKindOfClass_0(v6));
  objc_release(v7);
  if ( !v7 )
  {
    v322 = 0;
    v324 = objc_retainAutoreleasedReturnValue((id)com_apple_MobileActivation_ErrorDomain(
                                                    (int)"create_baa_info",
                                                    244,
                                                    -1,
                                                    0,
                                                    (int)CFSTR("Failed to retrieve %@."),
                                                    v8,
                                                    v9,
                                                    v10,
                                                    (char)CFSTR("s7nuHoZIYNoOHCqT9iyZkQ")));
    v302 = 0;
    v31 = 0;
    v306 = 0;
    v307 = 0;
    v315 = 0;
    v299 = 0;
    v300 = 0;
    v310 = 0;
    v311 = 0;
    v313 = 0;
    v308 = 0;
    v309 = 0;
    v318 = 0;
    v321 = 0;
    v27 = 0;
    a1 = 0;
    v32 = 0;
    v33 = 0;
    v34 = 0;
    v35 = 0;
LABEL_18:
    v320 = 0;
    v316 = 0;
LABEL_46:
    v296 = 0;
    v298 = 0;
    v312 = 0;
    v314 = 0;
    v44 = 0;
LABEL_47:
    v72 = 0;
    v301 = 0;
    v305 = 0;
    goto LABEL_180;
  }
  v11 = objc_alloc((Class)&amp;OBJC_CLASS___NSMutableArray);
  v343[0] = CFSTR("1.2.840.113635.100.8.4");
  v343[1] = CFSTR("1.2.840.113635.100.8.5");
  v343[2] = CFSTR("1.2.840.113635.100.8.7");
  v12 = objc_retainAutoreleasedReturnValue(+[NSArray arrayWithObjects:count:](&amp;OBJC_CLASS___NSArray, "arrayWithObjects:count:", v343, 3));
  v13 = objc_msgSend(v11, "initWithArray:", v12);
  objc_release(v12);
  v14 = objc_retainAutoreleasedReturnValue(+[NSNumber numberWithUnsignedInt:](&amp;OBJC_CLASS___NSNumber, "numberWithUnsignedInt:", 0));
  v322 = v14;
  if ( !v4 )
  {
    v36 = 0;
    v297 = 0;
    v37 = 0;
    v320 = 0;
    v38 = 0;
    v310 = 0;
    v311 = 0;
    v300 = 0;
    v315 = 0;
    v306 = 0;
    v39 = 0;
    v40 = 1;
    v41 = (NSNumber *)&amp;off_100E66CB8;
    v308 = (NSNumber *)&amp;off_100E66CB8;
    v309 = (NSNumber *)&amp;off_100E66CA0;
    goto LABEL_20;
  }
  v18 = v14;
  v323 = a1;
  v19 = objc_retainAutoreleasedReturnValue(objc_msgSend(v4, "objectForKeyedSubscript:", CFSTR("OIDSToInclude")));
  v20 = objc_retainAutoreleasedReturnValue((id)isKindOfClass());
  objc_release(v20);
  objc_release(v19);
  if ( v20 )
  {
    v321 = v13;
    *(_OWORD *)v337 = 0u;
    *(_OWORD *)v338 = 0u;
    *(_OWORD *)v335 = 0u;
    *(_OWORD *)v336 = 0u;
    v21 = v4;
    v22 = objc_retainAutoreleasedReturnValue(objc_msgSend(v4, "objectForKeyedSubscript:", CFSTR("OIDSToInclude")));
    v23 = (char *)objc_msgSend(v22, "countByEnumeratingWithState:objects:count:", v335, v342, 16);
    if ( v23 )
    {
      v24 = v23;
      v25 = *(_QWORD *)v336[0];
      while ( 2 )
      {
        for ( i = 0; i != v24; ++i )
        {
          if ( *(_QWORD *)v336[0] != v25 )
            objc_enumerationMutation(v22);
          v27 = (__SecKey *)objc_retainAutoreleasedReturnValue((id)isKindOfClass_1(*((_QWORD *)v335[1] + (_QWORD)i)));
          objc_release(v27);
          if ( !v27 )
          {
            v4 = v21;
            v277 = objc_retainAutoreleasedReturnValue(objc_msgSend(v21, "objectForKeyedSubscript:", CFSTR("OIDSToInclude")));
            v324 = objc_retainAutoreleasedReturnValue((id)com_apple_MobileActivation_ErrorDomain(
                                                            (int)"create_baa_info",
                                                            263,
                                                            -2,
                                                            0,
                                                            (int)CFSTR("Invalid options (%@): %@"),
                                                            v76,
                                                            v77,
                                                            v78,
                                                            (char)CFSTR("OIDSToInclude")));
            objc_release(v277);
            objc_release(v22);
            v302 = 0;
            v31 = 0;
            v306 = 0;
            v307 = 0;
            v315 = 0;
            v300 = 0;
            v310 = 0;
            v311 = 0;
            v318 = 0;
            goto LABEL_93;
          }
        }
        v24 = (char *)objc_msgSend(v22, "countByEnumeratingWithState:objects:count:", v335, v342, 16);
        if ( v24 )
          continue;
        break;
      }
    }
    objc_release(v22);
    v4 = v21;
    v13 = objc_retainAutoreleasedReturnValue(objc_msgSend(v21, "objectForKeyedSubscript:", CFSTR("OIDSToInclude")));
    objc_release(v321);
    v18 = v322;
  }
  v321 = v13;
  if ( (unsigned int)objc_msgSend(v13, "containsObject:", CFSTR("1.2.840.113635.100.8.1"))
    &amp;&amp; ((sub_100C87320() &amp; 1) != 0 || (unsigned int)sub_100C87330()) )
  {
    v324 = objc_retainAutoreleasedReturnValue((id)com_apple_MobileActivation_ErrorDomain(
                                                    (int)"create_baa_info",
                                                    276,
                                                    -2,
                                                    0,
                                                    (int)CFSTR("Boot manifest not available in rOS or diagnosticsOS."),
                                                    v28,
                                                    v29,
                                                    v30,
                                                    v275));
    v302 = 0;
    v31 = 0;
    v306 = 0;
    v315 = 0;
    v300 = 0;
    v310 = 0;
    v311 = 0;
    v318 = 0;
    goto LABEL_92;
  }
  v304 = ((unsigned int)objc_msgSend(v13, "containsObject:", CFSTR("1.2.840.113635.100.8.1")) &amp; 1) != 0
      || ((unsigned int)objc_msgSend(v13, "containsObject:", CFSTR("1.2.840.113635.100.8.7")) &amp; 1) != 0
      || ((unsigned int)objc_msgSend(v13, "containsObject:", CFSTR("1.2.840.113635.100.8.10.1")) &amp; 1) != 0
      || ((unsigned int)objc_msgSend(v13, "containsObject:", CFSTR("1.2.840.113635.100.8.10.2")) &amp; 1) != 0
      || (unsigned int)objc_msgSend(v13, "containsObject:", CFSTR("1.2.840.113635.100.8.10.3"));
  v70 = (unsigned int)objc_msgSend(v13, "containsObject:", CFSTR("1.2.840.113635.100.8.7"));
  if ( ((unsigned int)objc_msgSend(v13, "containsObject:", CFSTR("1.2.840.113635.100.8.2")) &amp; 1) != 0
    || (unsigned int)objc_msgSend(v13, "containsObject:", CFSTR("1.2.840.113635.100.8.11.1")) )
  {
    v71 = objc_retainAutoreleasedReturnValue(objc_msgSend(v4, "objectForKeyedSubscript:", CFSTR("nonce")));
    v72 = (__SecKey *)objc_retainAutoreleasedReturnValue((id)isKindOfClass_2());
    objc_release(v72);
    objc_release(v71);
    if ( !v72 )
    {
      v324 = objc_retainAutoreleasedReturnValue((id)com_apple_MobileActivation_ErrorDomain(
                                                      (int)"create_baa_info",
                                                      295,
                                                      -2,
                                                      0,
                                                      (int)CFSTR("Missing required option: %@"),
                                                      v73,
                                                      v74,
                                                      v75,
                                                      (char)CFSTR("nonce")));
      v302 = 0;
      v31 = 0;
      v306 = 0;
      v307 = 0;
      v315 = 0;
      v316 = 0;
      v299 = 0;
      v300 = 0;
      v310 = 0;
      v311 = 0;
      v318 = 0;
      v27 = 0;
      a1 = 0;
      v32 = 0;
      v33 = 0;
      v34 = 0;
      v35 = 0;
      v7 = 0;
      v320 = 0;
      v296 = 0;
      v298 = 0;
      v312 = 0;
      v314 = 0;
      v44 = 0;
LABEL_96:
      v301 = 0;
      v305 = 0;
      v313 = (NSNumber *)&amp;off_100E66CB8;
      v308 = (NSNumber *)&amp;off_100E66CB8;
      v309 = (NSNumber *)&amp;off_100E66CA0;
      goto LABEL_180;
    }
    v318 = objc_retainAutoreleasedReturnValue(objc_msgSend(v4, "objectForKeyedSubscript:", CFSTR("nonce")));
  }
  else
  {
    v318 = 0;
  }
  v83 = objc_retainAutoreleasedReturnValue(objc_msgSend(v4, "objectForKeyedSubscript:", CFSTR("CertType")));
  v84 = objc_retainAutoreleasedReturnValue((id)isKindOfClass_0(v83));
  objc_release(v84);
  objc_release(v83);
  v293 = v4;
  if ( v84 )
  {
    v85 = objc_retainAutoreleasedReturnValue(objc_msgSend(v4, "objectForKeyedSubscript:", CFSTR("CertType")));
    objc_release(v18);
    v86 = objc_retainAutoreleasedReturnValue(+[NSNumber numberWithUnsignedInt:](&amp;OBJC_CLASS___NSNumber, "numberWithUnsignedInt:", 0));
    if ( -[NSNumber isEqualToNumber:](v85, "isEqualToNumber:", v86) )
    {
      objc_release(v86);
      v18 = v85;
    }
    else
    {
      v87 = objc_retainAutoreleasedReturnValue(+[NSNumber numberWithUnsignedInt:](&amp;OBJC_CLASS___NSNumber, "numberWithUnsignedInt:", 1));
      v88 = -[NSNumber isEqualToNumber:](v85, "isEqualToNumber:", v87);
      objc_release(v87);
      objc_release(v86);
      v18 = v85;
      if ( (v88 &amp; 1) == 0 )
      {
        v324 = objc_retainAutoreleasedReturnValue((id)com_apple_MobileActivation_ErrorDomain(
                                                        (int)"create_baa_info",
                                                        306,
                                                        -2,
                                                        0,
                                                        (int)CFSTR("Invalid value for option (%@): %@"),
                                                        v89,
                                                        v90,
                                                        v91,
                                                        (char)CFSTR("CertType")));
        v302 = 0;
        v31 = 0;
        v306 = 0;
        v307 = 0;
        v315 = 0;
        v316 = 0;
        v299 = 0;
        v300 = 0;
        v310 = 0;
        v311 = 0;
        v27 = 0;
        a1 = 0;
        v32 = 0;
        v33 = 0;
        v34 = 0;
        v35 = 0;
        v7 = 0;
        v320 = 0;
        v296 = 0;
        v298 = 0;
        v314 = 0;
        v44 = 0;
        v72 = 0;
        v301 = 0;
        v305 = 0;
        v322 = v85;
        v312 = 0;
        v313 = (NSNumber *)&amp;off_100E66CB8;
        v308 = (NSNumber *)&amp;off_100E66CB8;
        v309 = (NSNumber *)&amp;off_100E66CA0;
        v4 = v293;
        goto LABEL_180;
      }
    }
  }
  v92 = objc_retainAutoreleasedReturnValue(+[NSNumber numberWithUnsignedInt:](&amp;OBJC_CLASS___NSNumber, "numberWithUnsignedInt:", 1));
  v93 = -[NSNumber isEqualToNumber:](v18, "isEqualToNumber:", v92);
  objc_release(v92);
  v322 = v18;
  if ( v93 )
  {
    v4 = v293;
    if ( ((unsigned int)objc_msgSend(v319, "boolValue") &amp; 1) == 0 )
    {
      v97 = CFSTR("Certificate type not supported on this platform: %@");
      v276 = (char)v18;
      v98 = 313;
      v99 = -3;
      goto LABEL_89;
    }
    if ( ((unsigned int)objc_msgSend(v13, "containsObject:", CFSTR("1.2.840.113635.100.6.71.1")) &amp; 1) == 0 )
    {
      v97 = CFSTR("Missing required OID for certificate type (%@): %@");
      v276 = (char)v18;
      v98 = 318;
LABEL_66:
      v99 = -2;
LABEL_89:
      v324 = objc_retainAutoreleasedReturnValue((id)com_apple_MobileActivation_ErrorDomain(
                                                      (int)"create_baa_info",
                                                      v98,
                                                      v99,
                                                      0,
                                                      (int)v97,
                                                      v94,
                                                      v95,
                                                      v96,
                                                      v276));
LABEL_90:
      v302 = 0;
      v31 = 0;
      v306 = 0;
      v315 = 0;
      v300 = 0;
      v311 = 0;
LABEL_91:
      v310 = 0;
LABEL_92:
      v307 = 0;
      v27 = 0;
LABEL_93:
      a1 = 0;
      v32 = 0;
      v33 = 0;
      v34 = 0;
      v299 = 0;
      v35 = 0;
      goto LABEL_94;
    }
  }
  else
  {
    v4 = v293;
    if ( ((unsigned int)objc_msgSend(v13, "containsObject:", CFSTR("1.2.840.113635.100.6.71.1")) &amp; 1) != 0
      || ((unsigned int)objc_msgSend(v13, "containsObject:", CFSTR("1.2.840.113635.100.6.71.2")) &amp; 1) != 0
      || (unsigned int)objc_msgSend(v13, "containsObject:", CFSTR("1.2.840.113635.100.6.71.3")) )
    {
      v97 = CFSTR("Invalid OID(s) for requested certificate type: %@");
      v276 = (char)v18;
      v98 = 325;
      goto LABEL_66;
    }
  }
  if ( (unsigned int)objc_msgSend(v13, "containsObject:", CFSTR("1.2.840.113635.100.6.71.1"))
    &amp;&amp; (v168 = objc_retainAutoreleasedReturnValue(objc_msgSend(v4, "objectForKeyedSubscript:", CFSTR("MFiProperties"))),
        v169 = objc_retainAutoreleasedReturnValue((id)isKindOfClass_2()),
        objc_release(v169),
        objc_release(v168),
        v169) )
  {
    v170 = objc_retainAutoreleasedReturnValue(objc_msgSend(v4, "objectForKeyedSubscript:", CFSTR("MFiProperties")));
    v171 = objc_msgSend(v170, "length");
    objc_release(v170);
    v172 = objc_retainAutoreleasedReturnValue(objc_msgSend(v4, "objectForKeyedSubscript:", CFSTR("MFiProperties")));
    v173 = v172;
    if ( v171 != (void *)32 )
    {
      objc_msgSend(v172, "length");
      v324 = objc_retainAutoreleasedReturnValue((id)com_apple_MobileActivation_ErrorDomain(
                                                      (int)"create_baa_info",
                                                      333,
                                                      -2,
                                                      0,
                                                      (int)CFSTR("Invalid value for option (%@): unexpected size (%lu)"),
                                                      v174,
                                                      v175,
                                                      v176,
                                                      (char)CFSTR("MFiProperties")));
      objc_release(v173);
      goto LABEL_90;
    }
  }
  else
  {
    v173 = 0;
  }
  v311 = v173;
  if ( (unsigned int)objc_msgSend(v13, "containsObject:", CFSTR("1.2.840.113635.100.6.71.2")) )
  {
    v177 = objc_retainAutoreleasedReturnValue(objc_msgSend(v4, "objectForKeyedSubscript:", CFSTR("MFiPPUID")));
    v44 = (__SecKey *)objc_retainAutoreleasedReturnValue((id)isKindOfClass_1(v177));
    objc_release(v44);
    objc_release(v177);
    if ( !v44 )
    {
      v324 = objc_retainAutoreleasedReturnValue((id)com_apple_MobileActivation_ErrorDomain(
                                                      (int)"create_baa_info",
                                                      343,
                                                      -2,
                                                      0,
                                                      (int)CFSTR("Missing required option: %@"),
                                                      v178,
                                                      v179,
                                                      v180,
                                                      (char)CFSTR("MFiPPUID")));
      v302 = 0;
      v31 = 0;
      v306 = 0;
      v315 = 0;
      v300 = 0;
LABEL_135:
      v310 = 0;
      v307 = 0;
      v27 = 0;
      a1 = 0;
      v32 = 0;
      v33 = 0;
      v34 = 0;
      v298 = 0;
      v299 = 0;
      v35 = 0;
      v7 = 0;
      v320 = 0;
      v316 = 0;
      v296 = 0;
      v312 = 0;
      v314 = 0;
      goto LABEL_95;
    }
    v181 = objc_retainAutoreleasedReturnValue(objc_msgSend(v4, "objectForKeyedSubscript:", CFSTR("MFiPPUID")));
    v182 = objc_msgSend(v181, "length");
    objc_release(v181);
    v183 = objc_retainAutoreleasedReturnValue(objc_msgSend(v4, "objectForKeyedSubscript:", CFSTR("MFiPPUID")));
    v184 = v183;
    if ( (unsigned __int64)v182 &gt;= 0x25 )
    {
      objc_msgSend(v183, "length");
      v324 = objc_retainAutoreleasedReturnValue((id)com_apple_MobileActivation_ErrorDomain(
                                                      (int)"create_baa_info",
                                                      348,
                                                      -2,
                                                      0,
                                                      (int)CFSTR("Invalid value for option (%@): unexpected size (%lu)"),
                                                      v185,
                                                      v186,
                                                      v187,
                                                      (char)CFSTR("MFiPPUID")));
      objc_release(v184);
      v302 = 0;
      v31 = 0;
      v306 = 0;
      v315 = 0;
      v300 = 0;
      goto LABEL_91;
    }
    v35 = objc_retainAutoreleasedReturnValue(objc_msgSend(v183, "stringByPaddingToLength:withString:startingAtIndex:", 36, &amp;stru_100E61E50, 0));
    objc_release(v184);
    if ( !v35 )
    {
      v324 = objc_retainAutoreleasedReturnValue((id)com_apple_MobileActivation_ErrorDomain(
                                                      (int)"create_baa_info",
                                                      357,
                                                      -2,
                                                      0,
                                                      (int)CFSTR("Failed to pad string."),
                                                      v199,
                                                      v200,
                                                      v201,
                                                      v275));
      v302 = 0;
      v31 = 0;
      v306 = 0;
      v307 = 0;
      v315 = 0;
      v299 = 0;
      v300 = 0;
      v310 = 0;
      v27 = 0;
      a1 = 0;
      v32 = 0;
      v33 = 0;
      v34 = 0;
LABEL_94:
      v7 = 0;
      v320 = 0;
      v316 = 0;
      v296 = 0;
      v298 = 0;
      v312 = 0;
      v314 = 0;
      v44 = 0;
LABEL_95:
      v72 = 0;
      goto LABEL_96;
    }
    v315 = objc_retainAutoreleasedReturnValue(objc_msgSend(v35, "dataUsingEncoding:", 4));
    if ( !v315 )
    {
      v300 = v35;
      v324 = objc_retainAutoreleasedReturnValue((id)com_apple_MobileActivation_ErrorDomain(
                                                      (int)"create_baa_info",
                                                      363,
                                                      -2,
                                                      0,
                                                      (int)CFSTR("Failed to convert string to data."),
                                                      v202,
                                                      v203,
                                                      v204,
                                                      v275));
      v302 = 0;
      v31 = 0;
      v306 = 0;
      v315 = 0;
      goto LABEL_91;
    }
  }
  else
  {
    v35 = 0;
    v315 = 0;
  }
  v300 = v35;
  if ( !(unsigned int)objc_msgSend(v13, "containsObject:", CFSTR("1.2.840.113635.100.6.71.3")) )
  {
    v306 = 0;
    goto LABEL_137;
  }
  v188 = objc_retainAutoreleasedReturnValue(objc_msgSend(v4, "objectForKeyedSubscript:", CFSTR("MFiData")));
  v44 = (__SecKey *)objc_retainAutoreleasedReturnValue((id)isKindOfClass_2());
  objc_release(v44);
  objc_release(v188);
  if ( !v44 )
  {
    v324 = objc_retainAutoreleasedReturnValue((id)com_apple_MobileActivation_ErrorDomain(
                                                    (int)"create_baa_info",
                                                    370,
                                                    -2,
                                                    0,
                                                    (int)CFSTR("Missing required option: %@"),
                                                    v189,
                                                    v190,
                                                    v191,
                                                    (char)CFSTR("MFiData")));
    v302 = 0;
    v31 = 0;
    v306 = 0;
    goto LABEL_135;
  }
  v192 = objc_retainAutoreleasedReturnValue(objc_msgSend(v4, "objectForKeyedSubscript:", CFSTR("MFiData")));
  v193 = objc_msgSend(v192, "length");
  objc_release(v192);
  v194 = objc_retainAutoreleasedReturnValue(objc_msgSend(v4, "objectForKeyedSubscript:", CFSTR("MFiData")));
  v195 = v194;
  if ( (unsigned __int64)v193 &gt;= 0x4B0 )
  {
    objc_msgSend(v194, "length");
    v324 = objc_retainAutoreleasedReturnValue((id)com_apple_MobileActivation_ErrorDomain(
                                                    (int)"create_baa_info",
                                                    375,
                                                    -2,
                                                    0,
                                                    (int)CFSTR("Invalid value for option (%@): unexpected size (%lu)"),
                                                    v196,
                                                    v197,
                                                    v198,
                                                    (char)CFSTR("MFiData")));
    objc_release(v195);
    v302 = 0;
    v31 = 0;
    v306 = 0;
    goto LABEL_91;
  }
  v306 = v194;
LABEL_137:
  v206 = objc_retainAutoreleasedReturnValue(objc_msgSend(v4, "objectForKeyedSubscript:", CFSTR("UseRKSigningInterface")));
  v207 = objc_retainAutoreleasedReturnValue((id)isKindOfClass_0(v206));
  objc_release(v207);
  objc_release(v206);
  if ( v207 )
  {
    v208 = objc_retainAutoreleasedReturnValue(objc_msgSend(v4, "objectForKeyedSubscript:", CFSTR("UseRKSigningInterface")));
    v317 = (unsigned int)objc_msgSend(v208, "boolValue");
    objc_release(v208);
  }
  else
  {
    v317 = 0;
  }
  v209 = objc_retainAutoreleasedReturnValue(objc_msgSend(v4, "objectForKeyedSubscript:", CFSTR("UseSoftwareGeneratedKey")));
  v210 = objc_retainAutoreleasedReturnValue((id)isKindOfClass_0(v209));
  objc_release(v210);
  objc_release(v209);
  if ( v210 )
  {
    v211 = objc_retainAutoreleasedReturnValue(objc_msgSend(v4, "objectForKeyedSubscript:", CFSTR("UseSoftwareGeneratedKey")));
    v212 = (unsigned int)objc_msgSend(v211, "boolValue");
    objc_release(v211);
  }
  else
  {
    v212 = 1;
  }
  v213 = objc_retainAutoreleasedReturnValue(objc_msgSend(v4, "objectForKeyedSubscript:", CFSTR("Validity")));
  v214 = objc_retainAutoreleasedReturnValue((id)isKindOfClass_0(v213));
  objc_release(v214);
  objc_release(v213);
  if ( v214 )
  {
    v215 = objc_retainAutoreleasedReturnValue(objc_msgSend(v4, "objectForKeyedSubscript:", CFSTR("Validity")));
    v309 = objc_retainAutoreleasedReturnValue(
             +[NSNumber numberWithUnsignedInteger:](
               &amp;OBJC_CLASS___NSNumber,
               "numberWithUnsignedInteger:",
               objc_msgSend(v215, "unsignedIntegerValue")));
    objc_release(v215);
  }
  else
  {
    v309 = (NSNumber *)&amp;off_100E66CA0;
  }
  v216 = objc_retainAutoreleasedReturnValue(objc_msgSend(v4, "objectForKeyedSubscript:", CFSTR("CACert")));
  v217 = objc_retainAutoreleasedReturnValue((id)isKindOfClass_0(v216));
  objc_release(v217);
  objc_release(v216);
  if ( v217 )
  {
    v218 = objc_retainAutoreleasedReturnValue(objc_msgSend(v4, "objectForKeyedSubscript:", CFSTR("CACert")));
    v308 = objc_retainAutoreleasedReturnValue(
             +[NSNumber numberWithUnsignedInteger:](
               &amp;OBJC_CLASS___NSNumber,
               "numberWithUnsignedInteger:",
               objc_msgSend(v218, "unsignedIntegerValue")));
    objc_release(v218);
  }
  else
  {
    v308 = (NSNumber *)&amp;off_100E66CB8;
  }
  v219 = objc_retainAutoreleasedReturnValue(objc_msgSend(v4, "objectForKeyedSubscript:", CFSTR("scrtAttestation")));
  v220 = objc_retainAutoreleasedReturnValue((id)isKindOfClass_0(v219));
  objc_release(v220);
  objc_release(v219);
  if ( v220 )
  {
    v221 = objc_retainAutoreleasedReturnValue(objc_msgSend(v4, "objectForKeyedSubscript:", CFSTR("scrtAttestation")));
    v41 = objc_retainAutoreleasedReturnValue(
            +[NSNumber numberWithUnsignedInteger:](
              &amp;OBJC_CLASS___NSNumber,
              "numberWithUnsignedInteger:",
              objc_msgSend(v221, "unsignedIntegerValue")));
    objc_release(v221);
  }
  else
  {
    v41 = (NSNumber *)&amp;off_100E66CB8;
  }
  v222 = objc_retainAutoreleasedReturnValue(objc_msgSend(v4, "objectForKeyedSubscript:", CFSTR("ClientAttestationData")));
  v223 = objc_retainAutoreleasedReturnValue((id)isKindOfClass_2());
  objc_release(v223);
  objc_release(v222);
  if ( !v223 )
  {
    v37 = 0;
    v320 = 0;
    goto LABEL_156;
  }
  v224 = objc_retainAutoreleasedReturnValue(objc_msgSend(v4, "objectForKeyedSubscript:", CFSTR("ClientAttestationPublicKey")));
  v27 = (__SecKey *)objc_retainAutoreleasedReturnValue((id)isKindOfClass_2());
  objc_release(v27);
  objc_release(v224);
  if ( !v27 )
  {
    v313 = v41;
    v324 = objc_retainAutoreleasedReturnValue((id)com_apple_MobileActivation_ErrorDomain(
                                                    (int)"create_baa_info",
                                                    412,
                                                    -2,
                                                    0,
                                                    (int)CFSTR("Missing required option for %@."),
                                                    v225,
                                                    v226,
                                                    v227,
                                                    (char)CFSTR("ClientAttestationPublicKey")));
    v302 = 0;
    v31 = 0;
    v310 = 0;
    v307 = 0;
    a1 = 0;
    v32 = 0;
    v33 = 0;
    v34 = 0;
    v299 = 0;
    v35 = 0;
    v7 = 0;
    goto LABEL_18;
  }
  v37 = objc_retainAutoreleasedReturnValue(objc_msgSend(v4, "objectForKeyedSubscript:", CFSTR("ClientAttestationData")));
  v320 = objc_retainAutoreleasedReturnValue(objc_msgSend(v4, "objectForKeyedSubscript:", CFSTR("ClientAttestationPublicKey")));
LABEL_156:
  v297 = v70;
  v228 = objc_retainAutoreleasedReturnValue(objc_msgSend(v4, "objectForKeyedSubscript:", CFSTR("AppID")));
  v229 = objc_retainAutoreleasedReturnValue((id)isKindOfClass_1(v228));
  objc_release(v229);
  objc_release(v228);
  v39 = v317 != 0;
  v40 = v212 != 0;
  if ( v229 )
    v310 = objc_retainAutoreleasedReturnValue(objc_msgSend(v4, "objectForKeyedSubscript:", CFSTR("AppID")));
  else
    v310 = 0;
  v38 = v318;
  a1 = v323;
  v36 = v304;
LABEL_20:
  v321 = v13;
  v318 = v38;
  v313 = v41;
  if ( !a1 )
  {
    v316 = v37;
    v324 = objc_retainAutoreleasedReturnValue((id)com_apple_MobileActivation_ErrorDomain(
                                                    (int)"create_baa_info",
                                                    467,
                                                    -2,
                                                    0,
                                                    (int)CFSTR("Invalid input."),
                                                    v15,
                                                    v16,
                                                    v17,
                                                    v275));
    v302 = 0;
    v31 = 0;
    v307 = 0;
    v27 = 0;
LABEL_45:
    v32 = 0;
    v33 = 0;
    v34 = 0;
    v299 = 0;
    v35 = 0;
    v7 = 0;
    goto LABEL_46;
  }
  v42 = 0;
  v303 = v36;
  if ( !v39 || v37 )
  {
    v291 = 0;
    v289 = 0;
    v294 = 0;
LABEL_49:
    v316 = v37;
    v333 = v42;
    v334[0] = 0;
    v27 = security_copy_system_key(0, v334, &amp;v333);
    v7 = objc_retain(v334[0]);
    v79 = objc_retain(v333);
    objc_release(v42);
    if ( !v27 )
    {
      v324 = objc_retainAutoreleasedReturnValue((id)com_apple_MobileActivation_ErrorDomain(
                                                      (int)"create_baa_info",
                                                      519,
                                                      -1,
                                                      error,
                                                      (int)CFSTR("Failed to copy SIK attestation key."),
                                                      v80,
                                                      v81,
                                                      v82,
                                                      v275));
      objc_release(v79);
      v302 = 0;
      v305 = 0;
      v301 = 0;
      v72 = 0;
      v44 = 0;
      v314 = 0;
      v296 = 0;
      v298 = 0;
      v312 = 0;
      v35 = 0;
      v299 = 0;
      v34 = 0;
      v33 = 0;
      v32 = 0;
      a1 = 0;
      v307 = 0;
      v290 = 0;
      goto LABEL_173;
    }
    *(_QWORD *)v288 = v7;
    if ( -[NSNumber boolValue](v41, "boolValue") )
    {
      v292 = v4;
      v286 = 0;
      v284 = 0;
    }
    else
    {
      v331 = v79;
      v332 = 0;
      v72 = security_copy_system_key(2, &amp;v332, &amp;v331);
      v286 = objc_retain(v332);
      v100 = objc_retain(v331);
      objc_release(v79);
      if ( !v72 )
      {
        v324 = objc_retainAutoreleasedReturnValue((id)com_apple_MobileActivation_ErrorDomain(
                                                        (int)"create_baa_info",
                                                        528,
                                                        -1,
                                                        v100,
                                                        (int)CFSTR("Failed to copy UIK attestation key."),
                                                        v101,
                                                        v102,
                                                        v103,
                                                        v275));
        objc_release(v100);
        CFRelease(v27);
        v302 = 0;
        v305 = 0;
        v301 = 0;
        v44 = 0;
        v314 = 0;
        v296 = 0;
        v298 = 0;
        v312 = 0;
        v299 = 0;
        v34 = 0;
        v33 = 0;
        v32 = 0;
        a1 = 0;
        v27 = 0;
        v307 = 0;
        v290 = 0;
        v35 = v286;
        v7 = *(id *)v288;
        goto LABEL_173;
      }
      v284 = v72;
      v292 = v4;
      v79 = v100;
      v7 = *(id *)v288;
    }
    cf = v27;
    v104 = (__SecKey *)objc_alloc_init((Class)&amp;OBJC_CLASS___NSMutableDictionary);
    v72 = v104;
    if ( !v104 )
    {
      v324 = objc_retainAutoreleasedReturnValue((id)com_apple_MobileActivation_ErrorDomain(
                                                      (int)"create_baa_info",
                                                      536,
                                                      -1,
                                                      0,
                                                      (int)CFSTR("Failed to allocate dictionary."),
                                                      v105,
                                                      v106,
                                                      v107,
                                                      v275));
      objc_release(v79);
      v302 = 0;
      v305 = 0;
      v301 = 0;
      v44 = 0;
      v314 = 0;
      v296 = 0;
      v298 = 0;
      v312 = 0;
      v299 = 0;
      v34 = 0;
      v33 = 0;
      v32 = 0;
      a1 = 0;
      v27 = 0;
      v307 = 0;
      v290 = 0;
      v4 = v292;
      v35 = v286;
      goto LABEL_171;
    }
    v283 = v104;
    if ( -[NSNumber boolValue](v41, "boolValue")
      || (v108 = objc_retainAutoreleasedReturnValue(+[NSFileManager defaultManager](&amp;OBJC_CLASS___NSFileManager, "defaultManager")),
          v109 = objc_retainAutoreleasedReturnValue(objc_retainAutoreleaseReturnValue(v108)),
          v110 = objc_retainAutoreleasedReturnValue(
                   -[NSFileManager stringByAppendingPathComponent:](
                     v109,
                     "stringByAppendingPathComponent:",
                     CFSTR("ucrt.pem"))),
          v111 = -[NSFileManager fileExistsAtPath:](v108, "fileExistsAtPath:", v110),
          objc_release(v110),
          objc_release(v109),
          objc_release(v108),
          (v111 &amp; 1) != 0) )
    {
      v115 = objc_retainAutoreleasedReturnValue(+[GestaltHlpr getSharedInstance](&amp;OBJC_CLASS___GestaltHlpr, "getSharedInstance"));
      v34 = objc_msgSend(v115, "copyAnswer:", CFSTR("UniqueChipID"));
      objc_release(v115);
      v116 = objc_retainAutoreleasedReturnValue((id)isKindOfClass_0(v34));
      objc_release(v116);
      if ( v116 )
      {
        v281 = v34;
        v120 = objc_retainAutoreleasedReturnValue(+[GestaltHlpr getSharedInstance](&amp;OBJC_CLASS___GestaltHlpr, "getSharedInstance"));
        v121 = objc_msgSend(v120, "copyAnswer:", CFSTR("ChipID"));
        objc_release(v120);
        v282 = v121;
        v122 = objc_retainAutoreleasedReturnValue((id)isKindOfClass_0(v121));
        objc_release(v122);
        if ( !v122 )
        {
          v324 = objc_retainAutoreleasedReturnValue((id)com_apple_MobileActivation_ErrorDomain(
                                                          (int)"create_baa_info",
                                                          588,
                                                          -1,
                                                          0,
                                                          (int)CFSTR("Failed to retrieve %@."),
                                                          v123,
                                                          v124,
                                                          v125,
                                                          (char)CFSTR("ChipID")));
          objc_release(v79);
          v302 = 0;
          v305 = 0;
          v301 = 0;
          v44 = 0;
          v314 = 0;
          v296 = 0;
          v298 = 0;
          v312 = 0;
          v299 = 0;
          v32 = 0;
          a1 = 0;
          v27 = 0;
          v307 = 0;
          v290 = 0;
          v4 = v292;
          v35 = v286;
          v7 = *(id *)v288;
          v33 = v282;
          v72 = v283;
          goto LABEL_171;
        }
        v126 = objc_retainAutoreleasedReturnValue(+[GestaltHlpr getSharedInstance](&amp;OBJC_CLASS___GestaltHlpr, "getSharedInstance"));
        v127 = objc_msgSend(v126, "copyAnswer:", CFSTR("BoardId"));
        objc_release(v126);
        v280 = v127;
        v128 = objc_retainAutoreleasedReturnValue((id)isKindOfClass_0(v127));
        objc_release(v128);
        if ( !v128 )
        {
          v324 = objc_retainAutoreleasedReturnValue((id)com_apple_MobileActivation_ErrorDomain(
                                                          (int)"create_baa_info",
                                                          594,
                                                          -1,
                                                          0,
                                                          (int)CFSTR("Failed to retrieve %@."),
                                                          v129,
                                                          v130,
                                                          v131,
                                                          (char)CFSTR("BoardId")));
          objc_release(v79);
          v302 = 0;
          v305 = 0;
          v301 = 0;
          v44 = 0;
          v314 = 0;
          v296 = 0;
          v298 = 0;
          v312 = 0;
          v299 = 0;
          a1 = 0;
          v27 = 0;
          v307 = 0;
          v290 = 0;
          v4 = v292;
          v35 = v286;
          v7 = *(id *)v288;
          v33 = v282;
          v72 = v283;
          v32 = v280;
          goto LABEL_171;
        }
        v132 = objc_retainAutoreleasedReturnValue(+[GestaltHlpr getSharedInstance](&amp;OBJC_CLASS___GestaltHlpr, "getSharedInstance"));
        v133 = (__SecKey *)objc_msgSend(v132, "copyAnswer:", CFSTR("SecurityDomain"));
        objc_release(v132);
        v279 = v133;
        v134 = objc_retainAutoreleasedReturnValue((id)isKindOfClass_0(v133));
        objc_release(v134);
        if ( !v134 )
        {
          v324 = objc_retainAutoreleasedReturnValue((id)com_apple_MobileActivation_ErrorDomain(
                                                          (int)"create_baa_info",
                                                          600,
                                                          -1,
                                                          0,
                                                          (int)CFSTR("Failed to retrieve %@."),
                                                          v135,
                                                          v136,
                                                          v137,
                                                          (char)CFSTR("SecurityDomain")));
          objc_release(v79);
          v302 = 0;
          v305 = 0;
          v301 = 0;
          v44 = 0;
          v314 = 0;
          v296 = 0;
          v298 = 0;
          v312 = 0;
          v299 = 0;
          v27 = 0;
          v307 = 0;
          v290 = 0;
          v4 = v292;
          v35 = v286;
          v7 = *(id *)v288;
          v33 = v282;
          v72 = v283;
          v32 = v280;
LABEL_133:
          a1 = v279;
          goto LABEL_171;
        }
        v138 = objc_retainAutoreleasedReturnValue(+[GestaltHlpr getSharedInstance](&amp;OBJC_CLASS___GestaltHlpr, "getSharedInstance"));
        v139 = (__SecKey *)objc_msgSend(v138, "copyAnswer:", CFSTR("SerialNumber"));
        objc_release(v138);
        v278 = v139;
        v140 = objc_retainAutoreleasedReturnValue((id)isKindOfClass_1(v139));
        objc_release(v140);
        if ( v140 )
        {
          v144 = objc_retainAutoreleasedReturnValue(+[GestaltHlpr getSharedInstance](&amp;OBJC_CLASS___GestaltHlpr, "getSharedInstance"));
          v145 = objc_msgSend(v144, "copyAnswer:", CFSTR("BuildVersion"));
          objc_release(v144);
          v307 = v145;
          v44 = (__SecKey *)objc_retainAutoreleasedReturnValue((id)isKindOfClass_1(v145));
          objc_release(v44);
          if ( v44 )
          {
            v149 = SecKeyCopyPublicKey(a1);
            if ( v149 )
            {
              v290 = v149;
              v153 = SecKeyCopyExternalRepresentation(v149, &amp;error);
              v7 = *(id *)v288;
              if ( v153 )
              {
                v157 = v153;
                if ( -[NSNumber boolValue](v313, "boolValue") )
                  v158 = 0;
                else
                  v158 = 2;
                v330[1] = v79;
                v159 = objc_retainAutoreleasedReturnValue((id)security_create_system_key_attestation(
                                                                (int)a1,
                                                                v158,
                                                                v318));
                v160 = objc_retain(v79);
                objc_release(v79);
                v312 = v159;
                v299 = v157;
                if ( v159 )
                {
                  v325 = v160;
                  if ( v316 )
                  {
                    v164 = v283;
                    -[__SecKey setObject:forKeyedSubscript:](
                      v283,
                      "setObject:forKeyedSubscript:",
                      v316,
                      CFSTR("RKCertification"));
                    -[__SecKey setObject:forKeyedSubscript:](
                      v283,
                      "setObject:forKeyedSubscript:",
                      v159,
                      CFSTR("RKSigning"));
                    v165 = off_100E52138;
                    v166 = v157;
                    v167 = v157;
                  }
                  else
                  {
                    v165 = off_100E51E10;
                    v167 = (CFDataRef)v159;
                    v164 = v283;
                    v166 = v157;
                  }
                  -[__SecKey setObject:forKeyedSubscript:](v164, "setObject:forKeyedSubscript:", v167, *v165);
                  v242 = objc_alloc((Class)&amp;OBJC_CLASS___NSMutableDictionary);
                  v340[0] = CFSTR("UniqueChipID");
                  v340[1] = CFSTR("ChipID");
                  v341[0] = v34;
                  v341[1] = v282;
                  v340[2] = CFSTR("BoardId");
                  v340[3] = CFSTR("SecurityDomain");
                  v341[2] = v280;
                  v341[3] = v279;
                  v340[4] = CFSTR("SerialNumber");
                  v340[5] = CFSTR("OsBuildVersion");
                  v341[4] = v278;
                  v341[5] = v307;
                  v340[6] = CFSTR("scrtAttestation");
                  v340[7] = CFSTR("CertType");
                  v341[6] = v313;
                  v341[7] = v322;
                  v340[8] = CFSTR("Validity");
                  v340[9] = CFSTR("CACert");
                  v341[8] = v309;
                  v341[9] = v308;
                  v340[10] = CFSTR("OIDSToInclude");
                  v340[11] = CFSTR("SIKPub");
                  v341[10] = v13;
                  v341[11] = *(_QWORD *)v288;
                  v243 = objc_retainAutoreleasedReturnValue(
                           +[NSDictionary dictionaryWithObjects:forKeys:count:](
                             &amp;OBJC_CLASS___NSDictionary,
                             "dictionaryWithObjects:forKeys:count:",
                             v341,
                             v340,
                             12));
                  v244 = objc_msgSend(v242, "initWithDictionary:", v243);
                  objc_release(v243);
                  v245 = off_100E52130;
                  if ( v316 )
                  {
                    objc_msgSend(v244, "setObject:forKeyedSubscript:", v320, CFSTR("RKCertificationPub"));
                    v245 = off_100E52138;
                  }
                  v35 = v286;
                  v246 = objc_msgSend(v244, "setObject:forKeyedSubscript:", v166, *v245);
                  if ( v286 )
                    v246 = objc_msgSend(v244, "setObject:forKeyedSubscript:", v286, CFSTR("UIKPub"));
                  v247 = objc_retainAutoreleasedReturnValue((id)IORegistryEntry_sub_10052290C(v246));
                  v314 = v244;
                  if ( !v247 )
                    goto LABEL_201;
                  v248 = v247;
                  if ( (sub_100C87330() &amp; 1) != 0 )
                  {
                    objc_release(v248);
LABEL_201:
                    v302 = 0;
LABEL_207:
                    if ( v303 &amp;&amp; (sub_100C87320() &amp; 1) == 0 )
                    {
                      if ( (sub_100C87330() &amp; 1) != 0 )
                      {
                        v301 = 0;
                      }
                      else
                      {
                        v329 = v325;
                        v255 = objc_retainAutoreleasedReturnValue((id)sub_100C75830(2, &amp;v329));
                        v256 = objc_retain(v329);
                        objc_release(v325);
                        v301 = v255;
                        if ( !v255 )
                        {
                          v324 = objc_retainAutoreleasedReturnValue((id)com_apple_MobileActivation_ErrorDomain(
                                                                          (int)"create_baa_info",
                                                                          735,
                                                                          -1,
                                                                          v256,
                                                                          (int)CFSTR("Failed to load boot manifest."),
                                                                          v257,
                                                                          v258,
                                                                          v259,
                                                                          v275));
                          objc_release(v256);
                          v305 = 0;
                          v301 = 0;
LABEL_240:
                          v44 = 0;
                          v296 = 0;
                          v298 = 0;
                          goto LABEL_168;
                        }
                        objc_msgSend(v314, "setObject:forKeyedSubscript:", v255, CFSTR("Image4Manifest"));
                        v325 = v256;
                      }
                      v35 = v286;
                      v7 = *(id *)v288;
                      v34 = v281;
                    }
                    else
                    {
                      v301 = 0;
                    }
                    if ( (sub_100C87330() &amp; 1) != 0 )
                    {
                      v305 = 0;
LABEL_221:
                      v4 = v292;
                      v266 = v306;
                      v72 = v283;
LABEL_222:
                      v33 = v282;
                      v262 = v325;
                      goto LABEL_223;
                    }
                    is_darwinos = os_variant_is_darwinos(objc_msgSend(CFSTR("com.apple.mobileactivationd"), "UTF8String"));
                    v305 = 0;
                    if ( !v297 )
                      goto LABEL_221;
                    if ( (is_darwinos &amp; 1) != 0 )
                    {
                      v4 = v292;
                      v266 = v306;
                      v35 = v286;
                      v7 = *(id *)v288;
                      v72 = v283;
                      v34 = v281;
                      goto LABEL_222;
                    }
                    v328 = v325;
                    v261 = objc_retainAutoreleasedReturnValue((id)sub_100C75830(10, &amp;v328));
                    v262 = objc_retain(v328);
                    objc_release(v325);
                    v305 = v261;
                    if ( v261 )
                    {
                      objc_msgSend(v314, "setObject:forKeyedSubscript:", v261, CFSTR("Cryptex1Image4Manifest"));
                      v4 = v292;
                      v266 = v306;
                      v35 = v286;
                      v7 = *(id *)v288;
                      v33 = v282;
                      v72 = v283;
                      v34 = v281;
LABEL_223:
                      v32 = v280;
                      if ( v310 )
                        objc_msgSend(v314, "setObject:forKeyedSubscript:", v310, CFSTR("AppID"));
                      if ( v311 )
                        objc_msgSend(v314, "setObject:forKeyedSubscript:", v311, CFSTR("MFiProperties"));
                      if ( v315 )
                        objc_msgSend(v314, "setObject:forKeyedSubscript:", v315, CFSTR("MFiPPUID"));
                      if ( v266 )
                        objc_msgSend(v314, "setObject:forKeyedSubscript:", v266, CFSTR("MFiData"));
                      v327 = 0;
                      v267 = objc_retainAutoreleasedReturnValue(
                               +[NSPropertyListSerialization dataWithPropertyList:format:options:error:](
                                 &amp;OBJC_CLASS___NSPropertyListSerialization,
                                 "dataWithPropertyList:format:options:error:",
                                 v314,
                                 100,
                                 0,
                                 &amp;v327));
                      v287 = objc_retain(v327);
                      objc_release(v262);
                      if ( v267 )
                      {
                        -[__SecKey setObject:forKeyedSubscript:](
                          v72,
                          "setObject:forKeyedSubscript:",
                          v267,
                          CFSTR("RKProperties"));
                        v298 = v267;
                        v271 = SecKeyCreateSignature(
                                 a1,
                                 kSecKeyAlgorithmECDSASignatureMessageX962SHA256,
                                 (CFDataRef)v267,
                                 &amp;error);
                        v27 = v278;
                        if ( v271 )
                        {
                          v296 = v271;
                          -[__SecKey setObject:forKeyedSubscript:](
                            v72,
                            "setObject:forKeyedSubscript:",
                            v271,
                            CFSTR("RKPropertiesSignature"));
                          v44 = objc_retain(v72);
                          v324 = v287;
                        }
                        else
                        {
                          v324 = objc_retainAutoreleasedReturnValue((id)com_apple_MobileActivation_ErrorDomain(
                                                                          (int)"create_baa_info",
                                                                          810,
                                                                          -1,
                                                                          error,
                                                                          (int)CFSTR("Failed to sign data with ref key."),
                                                                          v272,
                                                                          v273,
                                                                          v274,
                                                                          v275));
                          objc_release(v287);
                          v44 = 0;
                          v296 = 0;
                        }
                        goto LABEL_133;
                      }
                      v324 = objc_retainAutoreleasedReturnValue((id)com_apple_MobileActivation_ErrorDomain(
                                                                      (int)"create_baa_info",
                                                                      799,
                                                                      -1,
                                                                      v287,
                                                                      (int)CFSTR("Could not convert dictionary to xml data."),
                                                                      v268,
                                                                      v269,
                                                                      v270,
                                                                      v275));
                      objc_release(v287);
                      v44 = 0;
                      v296 = 0;
                      v298 = 0;
LABEL_170:
                      v27 = v278;
                      a1 = v279;
LABEL_171:
                      CFRelease(cf);
                      if ( v284 )
                        CFRelease(v284);
LABEL_173:
                      if ( v289 )
                        CFRelease(v289);
                      v205 = v294;
                      if ( !v294 )
                        goto LABEL_177;
                      goto LABEL_176;
                    }
                    v324 = objc_retainAutoreleasedReturnValue((id)com_apple_MobileActivation_ErrorDomain(
                                                                    (int)"create_baa_info",
                                                                    770,
                                                                    -1,
                                                                    v262,
                                                                    (int)CFSTR("Failed to load cryptex1 manifest."),
                                                                    v263,
                                                                    v264,
                                                                    v265,
                                                                    v275));
                    objc_release(v262);
                    v305 = 0;
                    goto LABEL_240;
                  }
                  v249 = os_variant_is_darwinos(objc_msgSend(CFSTR("com.apple.mobileactivationd"), "UTF8String"));
                  objc_release(v248);
                  if ( (v249 &amp; 1) != 0 )
                  {
                    v302 = 0;
LABEL_206:
                    v34 = v281;
                    goto LABEL_207;
                  }
                  v330[0] = v160;
                  v250 = objc_retainAutoreleasedReturnValue((id)sub_100C75830(9, v330));
                  v251 = objc_retain(v330[0]);
                  objc_release(v160);
                  v302 = v250;
                  if ( v250 )
                  {
                    objc_msgSend(v244, "setObject:forKeyedSubscript:", v250, CFSTR("LocalPolicy"));
                    v325 = v251;
                    v35 = v286;
                    v7 = *(id *)v288;
                    goto LABEL_206;
                  }
                  v324 = objc_retainAutoreleasedReturnValue((id)com_apple_MobileActivation_ErrorDomain(
                                                                  (int)"create_baa_info",
                                                                  719,
                                                                  -1,
                                                                  v251,
                                                                  (int)CFSTR("Failed to load cryptex1 local policy."),
                                                                  v252,
                                                                  v253,
                                                                  v254,
                                                                  v275));
                  objc_release(v251);
                  v305 = 0;
                  v301 = 0;
                  v44 = 0;
                  v296 = 0;
                  v298 = 0;
LABEL_167:
                  v302 = 0;
LABEL_168:
                  v4 = v292;
                  v7 = *(id *)v288;
LABEL_169:
                  v35 = v286;
                  v33 = v282;
                  v72 = v283;
                  v32 = v280;
                  v34 = v281;
                  goto LABEL_170;
                }
                v324 = objc_retainAutoreleasedReturnValue((id)com_apple_MobileActivation_ErrorDomain(
                                                                (int)"create_baa_info",
                                                                637,
                                                                -1,
                                                                v160,
                                                                (int)CFSTR("Failed to create reference key attestation (nonce: %@)."),
                                                                v161,
                                                                v162,
                                                                v163,
                                                                (char)v318));
                objc_release(v160);
                v305 = 0;
                v301 = 0;
                v44 = 0;
                v314 = 0;
                v296 = 0;
                v298 = 0;
                v312 = 0;
              }
              else
              {
                v324 = objc_retainAutoreleasedReturnValue((id)com_apple_MobileActivation_ErrorDomain(
                                                                (int)"create_baa_info",
                                                                628,
                                                                -1,
                                                                0,
                                                                (int)CFSTR("Failed to encode RK public key as data."),
                                                                v154,
                                                                v155,
                                                                v156,
                                                                v275));
                objc_release(v79);
                v305 = 0;
                v301 = 0;
                v44 = 0;
                v314 = 0;
                v296 = 0;
                v298 = 0;
                v312 = 0;
                v299 = 0;
              }
              v302 = 0;
              v4 = v292;
              goto LABEL_169;
            }
            v324 = objc_retainAutoreleasedReturnValue((id)com_apple_MobileActivation_ErrorDomain(
                                                            (int)"create_baa_info",
                                                            622,
                                                            -1,
                                                            0,
                                                            (int)CFSTR("Failed to copy RK public key."),
                                                            v150,
                                                            v151,
                                                            v152,
                                                            v275));
            objc_release(v79);
            v305 = 0;
            v301 = 0;
            v44 = 0;
          }
          else
          {
            v324 = objc_retainAutoreleasedReturnValue((id)com_apple_MobileActivation_ErrorDomain(
                                                            (int)"create_baa_info",
                                                            612,
                                                            -1,
                                                            v79,
                                                            (int)CFSTR("Failed to retrieve %@."),
                                                            v146,
                                                            v147,
                                                            v148,
                                                            (char)CFSTR("BuildVersion")));
            objc_release(v79);
            v305 = 0;
            v301 = 0;
          }
          v314 = 0;
          v296 = 0;
          v298 = 0;
          v312 = 0;
          v299 = 0;
        }
        else
        {
          v324 = objc_retainAutoreleasedReturnValue((id)com_apple_MobileActivation_ErrorDomain(
                                                          (int)"create_baa_info",
                                                          606,
                                                          -1,
                                                          0,
                                                          (int)CFSTR("Failed to retrieve %@."),
                                                          v141,
                                                          v142,
                                                          v143,
                                                          (char)CFSTR("SerialNumber")));
          objc_release(v79);
          v305 = 0;
          v301 = 0;
          v44 = 0;
          v314 = 0;
          v296 = 0;
          v298 = 0;
          v312 = 0;
          v299 = 0;
          v307 = 0;
        }
        v290 = 0;
        goto LABEL_167;
      }
      v324 = objc_retainAutoreleasedReturnValue((id)com_apple_MobileActivation_ErrorDomain(
                                                      (int)"create_baa_info",
                                                      582,
                                                      -1,
                                                      0,
                                                      (int)CFSTR("Failed to retrieve %@."),
                                                      v117,
                                                      v118,
                                                      v119,
                                                      (char)CFSTR("UniqueChipID")));
      objc_release(v79);
      v305 = 0;
      v301 = 0;
      v44 = 0;
      v314 = 0;
      v296 = 0;
      v298 = 0;
      v312 = 0;
      v299 = 0;
    }
    else
    {
      v324 = objc_retainAutoreleasedReturnValue((id)com_apple_MobileActivation_ErrorDomain(
                                                      (int)"create_baa_info",
                                                      543,
                                                      -4,
                                                      0,
                                                      (int)CFSTR("UCRT is unavailable."),
                                                      v112,
                                                      v113,
                                                      v114,
                                                      v275));
      objc_release(v79);
      v305 = 0;
      v301 = 0;
      v44 = 0;
      v314 = 0;
      v296 = 0;
      v298 = 0;
      v312 = 0;
      v299 = 0;
      v34 = 0;
    }
    v33 = 0;
    v32 = 0;
    a1 = 0;
    v27 = 0;
    v307 = 0;
    v290 = 0;
    v302 = 0;
    v4 = v292;
    v35 = v286;
    v7 = *(id *)v288;
    v72 = v283;
    goto LABEL_171;
  }
  if ( -[NSNumber boolValue](v41, "boolValue") )
    v43 = 0;
  else
    v43 = 2;
  v334[2] = 0;
  v44 = (__SecKey *)objc_retainAutoreleasedReturnValue((id)security_create_system_key_attestation((int)a1, v43, v38));
  v48 = objc_retain(0);
  if ( !v44 )
  {
    v324 = objc_retainAutoreleasedReturnValue((id)com_apple_MobileActivation_ErrorDomain(
                                                    (int)"create_baa_info",
                                                    477,
                                                    -1,
                                                    v48,
                                                    (int)CFSTR("Failed to create reference key attestation."),
                                                    v45,
                                                    v46,
                                                    v47,
                                                    v275));
    objc_release(v48);
    v302 = 0;
    v31 = 0;
    v307 = 0;
    v27 = 0;
    a1 = 0;
    v32 = 0;
    v33 = 0;
    v34 = 0;
    v298 = 0;
    v299 = 0;
    v35 = 0;
    v7 = 0;
    v316 = 0;
    v296 = 0;
    v312 = 0;
    v314 = 0;
    goto LABEL_47;
  }
  v316 = v44;
  v49 = SecKeyCopyPublicKey(a1);
  if ( !v49 )
  {
    v324 = objc_retainAutoreleasedReturnValue((id)com_apple_MobileActivation_ErrorDomain(
                                                    (int)"create_baa_info",
                                                    483,
                                                    -1,
                                                    0,
                                                    (int)CFSTR("Failed to copy RK public key."),
                                                    v50,
                                                    v51,
                                                    v52,
                                                    v275));
    objc_release(v48);
    v302 = 0;
    v31 = 0;
    v307 = 0;
    v27 = 0;
    a1 = 0;
    goto LABEL_45;
  }
  v294 = v49;
  v53 = SecKeyCopyExternalRepresentation(v49, &amp;error);
  objc_release(v320);
  if ( v53 )
  {
    v57 = SecAccessControlCreate(0, &amp;error);
    if ( v57 )
    {
      v61 = kSecAttrAccessibleUntilReboot;
      v62 = v57;
      v291 = (const void *)v57;
      if ( (SecAccessControlSetProtection(v57, kSecAttrAccessibleUntilReboot, &amp;error) &amp; 1) != 0 )
      {
        v334[1] = v48;
        v66 = (__SecKey *)sub_100C6E934(v62, v40, v4);
        v42 = objc_retain(v48);
        objc_release(v48);
        if ( v66 )
        {
          v320 = v53;
          a1 = v66;
          v289 = v66;
          v37 = v316;
          goto LABEL_49;
        }
        v324 = objc_retainAutoreleasedReturnValue((id)com_apple_MobileActivation_ErrorDomain(
                                                        (int)"create_baa_info",
                                                        508,
                                                        -1,
                                                        v42,
                                                        (int)CFSTR("Failed to create reference key."),
                                                        v67,
                                                        v68,
                                                        v69,
                                                        v275));
        v48 = v42;
      }
      else
      {
        v324 = objc_retainAutoreleasedReturnValue((id)com_apple_MobileActivation_ErrorDomain(
                                                        (int)"create_baa_info",
                                                        502,
                                                        -1,
                                                        error,
                                                        (int)CFSTR("Failed to set ACL protection to %@."),
                                                        v63,
                                                        v64,
                                                        v65,
                                                        v61));
      }
    }
    else
    {
      v324 = objc_retainAutoreleasedReturnValue((id)com_apple_MobileActivation_ErrorDomain(
                                                      (int)"create_baa_info",
                                                      497,
                                                      -1,
                                                      error,
                                                      (int)CFSTR("Failed to create access control."),
                                                      v58,
                                                      v59,
                                                      v60,
                                                      v275));
      v291 = 0;
    }
    v320 = v53;
  }
  else
  {
    v324 = objc_retainAutoreleasedReturnValue((id)com_apple_MobileActivation_ErrorDomain(
                                                    (int)"create_baa_info",
                                                    489,
                                                    -1,
                                                    error,
                                                    (int)CFSTR("Failed to encode RK public key as data."),
                                                    v54,
                                                    v55,
                                                    v56,
                                                    v275));
    v320 = 0;
    v291 = 0;
  }
  objc_release(v48);
  v302 = 0;
  v305 = 0;
  v301 = 0;
  v72 = 0;
  v44 = 0;
  v314 = 0;
  v296 = 0;
  v298 = 0;
  v312 = 0;
  v7 = 0;
  v35 = 0;
  v299 = 0;
  v34 = 0;
  v33 = 0;
  v32 = 0;
  a1 = 0;
  v27 = 0;
  v307 = 0;
  v290 = 0;
  v205 = v294;
LABEL_176:
  CFRelease(v205);
LABEL_177:
  if ( v290 )
    CFRelease(v290);
  v31 = v291;
LABEL_180:
  if ( error )
  {
    v295 = v44;
    v230 = v27;
    v231 = a1;
    v232 = v32;
    v233 = v4;
    v234 = v33;
    v235 = v34;
    v236 = v72;
    v237 = v35;
    v238 = v7;
    v239 = v31;
    CFRelease(error);
    v31 = v239;
    v7 = v238;
    v35 = v237;
    v72 = v236;
    v34 = v235;
    v33 = v234;
    v4 = v233;
    v32 = v232;
    a1 = v231;
    v27 = v230;
    v44 = v295;
  }
  error = 0;
  if ( v31 )
    CFRelease(v31);
  if ( a3 &amp;&amp; !v44 )
    *a3 = objc_retainAutorelease(v324);
  v240 = objc_retain(v44);
  objc_release(v302);
  objc_release(v324);
  objc_release(v306);
  objc_release(v315);
  objc_release(v300);
  objc_release(v311);
  objc_release(v310);
  objc_release(v322);
  objc_release(v313);
  objc_release(v308);
  objc_release(v309);
  objc_release(v318);
  objc_release(v321);
  objc_release(v307);
  objc_release(v27);
  objc_release(a1);
  objc_release(v32);
  objc_release(v33);
  objc_release(v34);
  objc_release(v299);
  objc_release(v35);
  objc_release(v7);
  objc_release(v320);
  objc_release(v316);
  objc_release(v296);
  objc_release(v312);
  objc_release(v298);
  objc_release(v314);
  objc_release(v240);
  objc_release(v72);
  objc_release(v301);
  objc_release(v319);
  objc_release(v305);
  objc_release(v4);
  if ( ((vars8 ^ (2 * vars8)) &amp; 0x4000000000000000LL) != 0 )
    __break(0xC471u);
  v241 = objc_autoreleaseReturnValue(v240);
}
</code></pre></div></div>

<ul>
  <li>采集验签证书</li>
</ul>

<p>用 SE 里封装的密钥 + 苹果预置的证书链，生成了一份 attestation（证明）blob，这个 blob 里面带着证书和签名，对“某些设备硬件字段 + 公钥”做了绑定。</p>

<p>这个“证书/attestation blob”是 依赖安全硬件（Secure Enclave / Apple KeyStore）生成的，而且签名覆盖了与这台硬件绑定的一些信息。</p>

<p>格式如下：</p>

<pre><code class="language-JSON">SEQUENCE
  INTEGER 02
  SEQUENCE
    OCTETSTRING xxxxdeca90b4fd52da1c016730a81dcb281f6c4dbb973e6b8438e6827530a177
    OCTETSTRING xxxx980ea0e8b0211cc342dc42067f1b..(total 65bytes)..2c8c482afb33b9e84221b49ffd1d81fa
    OCTETSTRING xxxx730da8259089a9b9b02bd4a455fd
    OCTETSTRING xxxxce59b8232b317ff32beb21237051
    OCTETSTRING xxxx56c0b70138d40071067ee100013f..(total 1074bytes)..4b70f5283c1085f6a8d25107d6b9a94b

</code></pre>

<ul>
  <li>上报激活信息</li>
</ul>

<p>将采集的硬件信息与证书签名信信息组合成 plist格式数据上报到服务器端</p>

<ul>
  <li>Headers</li>
</ul>

<pre><code class="language-YAML">[*] URL     : https://humb.apple.com/humbug/baa
[*] Method  : POST
        0 : User-Agent = {contents = "iOS Device Activator (MobileActivation-834.100.27) - devicecheckd"}
        3 : Content-Type = {contents = "application/xml"}
        4 : Content-Length = 9822
        6 : x-jmet-deviceid = {contents = "00008110-303C30C91A06484E"}
}
</code></pre>

<ul>
  <li>body</li>
</ul>

<pre><code class="language-XML">&lt;?xml version="1.0" encoding="UTF-8"?&gt;
&lt;!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"&gt;
&lt;plist version="1.0"&gt;
&lt;dict&gt;
        &lt;key&gt;RKCertification&lt;/key&gt;
        &lt;data&gt;
        MIIExQIBAjxxxxxxxxP4C3sqQtP1S2hwBZzCoHcsoH2xNu5c+a4Q45oJ1MKF3BEEEiTxl
        3JiRx7vW49DhREeRVU04QQBLnB7Lm7nI8+D2ayS/PMZ7rcmhC89px3xHSlAvDl2MQopT
        SHXxnC/0Bs/6KgQQ1SciqWRdHp3U/vxi/S1O/wQQNpOm/5v25ojsX99XACzgpwSCBDHO
        l9izvodcLU0pQSlaNXHlIp7nMLqh/QkyEI+a/YlGFaqTR/PgZbq4o37Rw6SCZQ+MjU2F
        uzuq9O6sTaKVBcw4aDW0oPDmZgH5m1ZlDwvUqQFAy+OvNrO8L4D4zSo2lW8BRqR89jkx
        WAixq12iERj/4+orIFAFGG86P/PnRRUsC8uR1tkqM4NpjXiLNUlO/X7VF1iY7g3nf5JS
        uxAqGMNZ8eEXXHJ9I5XuPDC/B1BvpoM/Pk65CPRbaw5ojMr+McZ+IsuWQBb21dxMldAy
        MMg5sFEt5+uIqWFaVyjFMfFCqYTfSofsyFQO8woaPegrmqFBVJRTGbbPkEDEVaWgLprd
        VuoE8hkSvecWaBwsyNH0UmTLMOwrMHF12RLVPc6qChr4ki0FkIP9crLHhQwxVYeXgRpA
        ClcA6uNFFa412RdhRhv5T88NBVRNu9ewCm3gWBnhMF10PltNVGJ1ZRjhZS0ghaVnqHpj
        DA+2pnObaocLzgC9bOl0RExDwVwxLp7/hD/0jqZ+GqGSPgXYGY3FxnIB9n1e35KNjWaV
        ur1WgHps5TEdi9LHxur2iqv5IkmxXNojxUoWprRc36RCBkurDLJiCM6Es8/63z1mZQ9h
        a4iA5+u0PStVpCSjOJ/0gn01pIxxxeppomCsT6980+l1HYi7zsD/tBkOznpFwGS5Ki9h
        RErTaCc2/9YGjgWVOBv1w5dEnBnwb1P59Qgpmnx3k9eyczaxt3OXOnKVwzYKtowI0OeD
        F9jM1yN9KZVtAqQJM4H8N+W3f0w8x+PwYHUf/0AHdAK/WyUHObDkoPzZOY+N7aFvkSf8
        aVcxC5MMCM6YFGpeDN9yoroti2mxxxelwDYx7Q8wuIeBBDiJfHRMWXxQEbej2XoYxGs4
        tz8Qh0z5p0kL5bb5MERKHPkaIg+slVxEpoCAxCnNHiTHjK3gEXhgyxkiBmLO4ENSGDTp
        +5PX64sez9Sb95ua6PhFFJYRXFgnp2IqffOoxx9Pxtxf7aKvpmlhPu+YGfbpz+mF+BSQ
        Y0a/ybU0xySRCKfTlepT8J+3HGNGl6HmjKPMDgj1SIloAoNR+VATnMs0THe7ZZTYLI+m
        bghSAHqDcjZCOWbRp19gSbUz71WU/sDAbN477biemxOYszVzS6cckGmmoDSLuav8RCSH
        9h254xTGB5VzEjT5lqpsWJ0u+IJUW7gfCbkq1IGer5PdmVrzAluNTS4G85iHnXi3EKJB
        wJ1upCRMjwlQRFlKP/SDo1TTQUa1kX0x+o1bFjvroyNKq+wL56PRQex22FzN00gsursF
        dstdB+WPHde2jfeVmielZ+wxw59wEuE2CLeS5D1CLIEGYgsTyoLxl6QQ80UjaphhAuVA
        A7HqcmXNe/6AOGdL42LHUhF23K5Fd55i8yaJP0sMwzY1e3nPbeunZmIiL1KiAFcKLzpm
        cg==
        &lt;/data&gt;
        &lt;key&gt;RKProperties&lt;/key&gt;
        &lt;data&gt;
        ff94bfwgfmVyc2lvbjxxxxxwIiBlbmNvZGluZz0iVVRGLTgiPz4KPCFET0NUWVBFIHBs
        aXN0IFBVQkxJQyAiLSxxxxxwbGUvL0RURCBQTElTVCAxLjAvL0VOIiAiaHR0cDovL3d3
        dy5hcHBsZS5jb20vRFREcy9Qcm9wZXJ0eUxpc3QtMS4wLmR0ZCI+CjxwbGlzdCB2ZXJz
        aW9uPSIxLjAiPgo8ZGljdD4KCTxrZXk+QXBwSUQ8L2tleT4KCTxzdHJpbmc+Y29tLmFw
        cGxlLmRldmljZWNoZWNrZDwvf3RyaW5nPgoJPGtleT5Cb2FyZElkPC9rZXk+Cgk8aW50
        ZWdlcj4yNDwvaW50ZWdlcj4KffxrZXk+QfFfZXffPC9rZXk+Cgk8aW50ZWdlcj4wPC9p
        bnRlZ2VyPgoJPGtleT5DZXJ0VHlwZTwva2V5PgoJPGludGVnZXI+MDwvaW50ZWdlcj4K
        CTxrZXk+Q2hpcElEPC9rZXk+Cgk8aW50ZWdlcj4zMzA0MDwvaW50ZWdlcj4KCTxrZXk+
        TG9jYWxQb2xpY3k8L2tleT4KCTxkYXRhPgoJTUlJTVFCWUVTVTFITkRBVUZnUkpUVFJR
        RmdSc2NHOXNGZ014TGpBRUFRQ2dnZfdfTUfJfUhCWUVTVTAwVFFJQkFER0MKCUFpUC9o
        T3FGbkVLQ0Fob3dnZ0lXRmdSTlFxxUNNWUlDRFArRTZvV2NVSUlCbnpDQ0Fac1dCRTFC
        VGxBeGdnR1IvNFNTCgl2YVJFQ3pBSkZnUkNUMUpFQWdFWS80U2FsYUJQQ3pBSkZnUkRS
        VkJQQWdFQi80U2FvWkpRRFRBTEZnUkRTRWxRQWdNQQoJZ1JEL2hKckJwRThMTUFrV0JF
        TlFVazhCQWYvL2hKck5pa01MTUFrV0JFTlRxVU1CQfWYvL2hLcU5ra1FSTUE4V0JFVkQK
        CVNVUUNCeGhwb2p3Z0lCNy9oWnFSbmswTE1Ba1dCRk5FVDAwQ0FRSC9odU85eEc4TE1B
        a1dCR3h2WW04QkFmLy9odlBOCgkwbWc2TURnV0JHNXphV2dFTUlQc0gwYlAybmtaaDNo
        NVF3U2RXUU1CL0NoekZrWlMyczE0SxlWxnJTdDZwR2RlaDIvSgoJd3VnR2V4dFU5cWNm
        Uy8rSGs3M2NhQ293S0JZRWNtOXVhQVFnU2srUmVVVnd1QWlCeEw0cWM5WmlZZUFFQ3Y1
        OVN2dmwKCXk4SWhTM2ZaK2k3L2g1dkIwbWc2TURnV0JITndhV2dFTUo3V1ZSSWhWNU13
        ZXlXa2VHdVo5STVSbUFSNGQycm9aZnFQCglmOE92aXljRkFjUEs0cHJDUXVTN3BSa3lB
        WDQxVi8rSG04bnNiaDR3SEJZRWMzSjJiZ1FVRzIrWkp3aXFselVHSUkzQQoJVmZMcklJ
        L3RkMGYvaDdQVjBtUWFNQmdXQkhaMWFXUUVFQUFBQUFBQUFBQUFBQUFBQUFBQUFBRC9o
        dVBCM214ZE1Gc1cKCUJHeHdiMnd4VS8rRW9wMm1WRG93T0JZRVJFZFRWQVF3MFFGVU9N
        U29rWEtqQkkxZXJyeXkzbVYzZGNacStHaVJhcWVXCglHUUk5Z29haFJoREhKZVNSem1m
        MERMMVl0M2h5LzRTcXJZcFpDekFKRmdSRlMwVlpBUUgvQklJQ0FFRmdqK1krQkpheAoJ
        MU1jS3dGQkdFMHpvcElKZkJWY2dKcG5MeDV2MFA2L1VsbG1jWmZYcnVYK2t1K2xRYm9H
        UXBkQ3ZZcnp6K3l4bXVjS00KCWtDOXlJdlpzMndjMldrMzdDeHl4a1lXZ2UvckZmSERC
        WFpQS3hTc1hIRFpkVXVjanFQbGlqVkJGV1pXekJjc0hQd2M3CgkyOFJUcTNWQU9aeG0x
        RXo3RVFlVHRmUDFocDN0WEdic0xaMlZ0aVcyWG12VDFDeCsvajNCbnhnNGNtQy9EUjJW
        SmpZaQoJUUx2V0dJSTNSVUY5cUEwL3BBeGcvQ3JCeXI4Zkx3Uk5QV2haOGMwRW04eVhi
        U2MxZ1h2K0NlUnJJcVBNZXFWU1o3cGoKCXdGamlhTWlwRXEwRUJhQkcyOG9xNkNCZE9E
        K1ZDUWxYSzBNOEdqUXZXc3FpcS9UVDRlQkVGV0NQTzRKLzU2bzE4WkxpCgk5OFdZaTQy
        RVBLb0RHM0ZyTFhreWNWR0E0QTdnVlhIczBnS25TeWJEKzZmYlk0SGF5cWtpUEM4ZHl1
        QWlUNXF5WThwUwoJdVp5TTY5aEs4dEJleFpiWDdiOFdEd29HVElzd3RST0d1SGZJbEpS
        bXBIVXF2YnRUcWs4Qk42cFBwaHlNMlJqb1p4QmgKCXlBdzdEQ0M5Y0tuTkpnamhQTE91
        aFRaUlU0aCtqMXo5WnY2UmdpSU44L21wTkxZV2pNMmtFWEw1eVRKbVhYVnBOcThICgll
        UGx5VUF4TnF6dkFLVFpZQjMrODROOFF4ekF4bVk4eFFBcUpVb1ROWWVZSjZnSWZXcnFM
        VktsRFdDcy9kUE5kc2xndwoJNHN4ci9xS09kcDI1cm5ST1hyK21Kc080Qm4rNkI2Vmhm
        WndWWnpmT2VkN0toZ3dMaGxrSXRYY25UbEN0TUlJSDVEQ0MKCUIrQXdnZ1hJb0FNQ0FR
        SUNFRDM5ajV5cU5WOXZVQ0xzTjAyRWRCOHdEUVlKS29aSWh2Y05BUUVNQlFBd1N6RW5N
        Q1VHCglBMVVFQXd3ZVFYQndiR1VnVTJWamRYSmxJRUp2YjNRZ1VtOXZkQ0JEUVNBdElF
        YzJNUk13RVFZRFZRUUtEQXBCY0hCcwoJWlNCSmJtTXVNUXN3Q1FZRFZRUUdFd0pWVXpB
        ZUZ3MHlNREEzTWpJeE9EQTJORGhhRncwek1EQTNNak14T0RBMk5EaGEKCU1GSXhMakFz
        QmdOVkJBTU1KVlE0TVRFd0xWTkVUMDB4TFZKbFkyOTJaWEo1UW05dmRDMVNaWFpCTFVa
        aFkzUnZjbmt4CglFekFSQmdOVkJBb01Da0Z3Y0d4bElFbHVZeTR4Q3pBSkJnTlZCQVlU
        QWxWVE1JSUNJakFOQmdrcWhraUc5dzBCQVFFRgoJQUFPQ0FnOEFNSUlDQ2dLQ0FnRUF5
        aVFHMk1leXZJeFlCQThJdis1bVd0cG83SHJ6ZUdvdU1sSlR1SHFqbmNUYzdDVEYKCUlp
        WlUvT1RMaVRoT2FsdS85L29rQnBqY2MyY0hVdXV6NFlMeUhRYkNFYzllam5MdTFhY2lY
        Q3MvVHZaZmoyTFpScnZsCgk4Tk1xaTBrNUE5RHlUVGxzYnZWRDhXQmdIZ3VUN2MySjh2
        N0tpUnM5MkZUWjZ6OGROK0ZPa1VjM3dTUnRiOFBaQ2QrWgoJREdXblpDYzZyZFlkOFRJ
        WDhNK0dCYzhGeUVEaXFhUmNONmpNNzJkRkRDL0NCQjZxcmFKZDlEQ1VzRTdDVXJ5Zk02
        VWYKCXpqbVNyTGNnSkZYSjhZK3k0N2VjaGcrY2RDTytOQThBYjQzL2E1N0xJaTBtT0JI
        V2FIUytHTkExR3c3WkpqdHdNQXcvCgllbW9heXVNYXB6d1RlRjFNTW1qKzNTa0h2WEdT
        T0JtcU5SYVBoT2hRd1p3bkpsaEhpSkNzaVl1dWJ3N3Bxa0MycytiYgoJekx3MkxNakEy
        UUppQWdDOHJmU3VYb2RUT2EzRkxhK1RmSlJ4TjhoL3p2aEU5Wmk0RDNBeC8xQ2dBS1Qx
        bTNXT3JtQ1AKCURVZDZKOXZ5c3plTWdlZ3RTWldHdU9oL1o1OGFDR1lROTFoMVdpYSt5
        VHdvU1J4c1lYdTR5ZldkT0ZlelpYQjlzMmFqCglWUEFRKzI2eE8xWlRsR29CWXVHcU1y
        TGEvaUV5S1J1UHpzZDVzcC9uU29wdE43dDQwS0h5Uy9KL1luT2RoZTltREdqUwoJMTZE
        aFJBSjRWdHBubDFmeHhubFdNcXpJeUNTMWxtTDdWeWxMdkF1YkpKZTF6MGtaTm9CNWln
        SFB5Q3V2ZW5FUVpSVkQKCUlBcFJ5YlpqSk9Pc1FoVUY3dGFYVUQwQ0F3RUFBYU9DQXJj
        d2dnS3pNQXdHQTFVZEV3RUIvd1FDTUFBd0h3WURWUjBqCglCQmd3Rm9BVVU3ZUV0TEhv
        T0R5Vzc2UldMdm96M1BFUWtPQXdIUVlxxlIwT0JCWUVGRlhYTkNqTWlaMzlYVFlGU3B0
        TgoJTXFNRlNwcWNNQTRHQTFVZER3RUIvd1FFQXdJSGdEQ0NBbEVHQ2lxR1NJYjNZMlFH
        QVE4QkFmOEVnZ0krTVlJQ092K0UKCTZvV2NVSUlCd1RDQ0FiMFdCRTFCVGxBeGdnR3ov
        NFNTdVlaSUREQUtGZ1JDVGtOSW9RSUZBUCtFa3Iya1JBd3dDaFlFCglRazlTUktBQ0JR
        RC9oSnFWb0U4TE1Ba1dCRU5GVUU4Q0FRSC9oSnFoa2xBTk1Bc1dCRU5JU1ZBQ0F3Q0JF
        UCtFbXNHawoJVHd3d0NoWUVRMUJTVDZBQ0JxRCxxSnJxxWtNTU1Bb1dCRU5UUlVPZ0Fn
        VUEvNFNxalpKRUREQUtGZ1JGUTBsRW9BSUYKCUFQK0ZtcEdlVFFzd0NSWUVVMFJQVFFJ
        QkFmK0dpOVh3YVF3d0NoWUVZWFY0YWFFQ0JRRC9ob3ZWOEhBTU1Bb1dCR0YxCgllSENo
        QWdVQS80Ymp2Y1J2Q3pBSkZnUnNiMkp2QVFILy80Ymp3ZHhvRERBS0ZnUnNjRzVvb1FJ
        RkFQK0c4ODNTYUF3dwoJQ2hZRWJxxxBxS0FDQlFEL2g1TzkzR2dNTUFvV0JISnZibWln
        QWdVQS80ZVR3ZHhvRERBS0ZnUnljRzVvb1FJRkFQK0gKCW02WGdNQXd3Q2hZRWMybHdN
        S0VDQlFEL2g1dWw0REVNTUFvV0JITnBjREdoQWdVQS80ZWJwZUF5RERBS0ZnUnphWEF5
        CglvUUlGQVArSG02WGdNd3d3Q2hZRWMybHdNNkVDQlFEL2g1dTF4REFNTUFvV0JITnRZ
        akNoQWdVQS80ZWJ0Y1F4RERBSwoJRmdSemJXSXhvUUlGQVArSG03WEVNZ3d3Q2hZRWMy
        MWlNcUVDQlFEL2g3UFYwbVFNTUFvV0JIWjFhV1NnQWdVQS80VDYKCWlaUlFhVEJuRmdS
        UFFrcFFNVi8vaEtLZHBsUU1NQW9XQkVSSFUxU2dBZ1VBLzRTaXdhUlBEREFLRmdSRVVG
        SlBvUUlGCglBUCtFb3MyS1F3d3dDaFlFUkZORlE2RUNCUUQvaEtyQnBFOE1NQW9XQkVW
        UVVrK2hBZ1VBLzRTcXpZcEREREFLRmdSRgoJVTBWRG9RSUZBREFOQmdrcWhraUc5dzBC
        QVF3RkFBT0NBZ0VBalRUWGVyeE9pMnRWbkNUdXBYR3ZJZW5FQWRLNnZKZ28KCVJaTFNo
        TFo0NGFqNnYySS93V1NKblE1T1VBdGJaNEdiL1RSblZIOUV1dEtZZks0cC9GVlJOZVUv
        c0w2UjRHZEI2TGV0CgkwbWNKWkpSN2xtaUtUQmtMaFBYcnZsaU9qM2hQRlMxWHhCNDBT
        YzU2SlJ6L3REMlpjM1VPSmFNZDZza2YxR3FRd3BUaQoJUzgxVU5zS3JNVmtpc0R5YXYv
        VWxTQmpKM1hyazBCMmN1YVNpVjRaUXhZaSs0ZjRaSXUyZDQ2Y3lBMEJ3YXhBd0UwNFkK
        CU1OdDZVUXl2Y0R4VUxrMU5oUzhUbEtvMVJLczM3UWJxY2k4bUdMOU52M2N4NThjN3B4
        czBNT2VKMVY1UmNtRGxxZWVLCgliTEtTc3BnUTRLNkFHaTJwL3VsckI0YWsxU3lZZGky
        UVlMUUw5TmU3dHhlMXE3c0Zlcmw0RnluOVZGZ2F1aVIreGlvQgoJaXpMcFZWblNPa1RR
        SGgzVEtNbVpEUS9MUDdscUNrcnVCOHZPRGYxV3dITzNOQityVTRXZlBZeERyazJqOWln
        a1pDdzEKCStjVFdWMmozU2lIekk1WUQ5WGRWZmNpZklOTktiNnhhckhuODdpQkh2Rmgv
        M3hKekVqUzZTclI2WEpRTi9RU0FTTXpMCglnVkZRUXFxaHM0N1ZRNmlOS0NjZnZpR21U
        aXFoOE5vWVh1ZXUrME1Rdm5kb3NtcjUzelQzMEFTVWt0TTJnNFBKeDBuawoJUHN1dTVy
        SVVaK3pKeWd6YjVmKzlhSXRHYkNUZEdwajM4YldYR0U3aWRXYTJuQlh6bnBVbDgyd3Fh
        MlpWMHE1OFp2N20KCTBXY09ZWlRxcnVOSTcxRTVXd1duMWFCdzZQRlFOeG0xQlBOcFhS
        MD0KCTwvZGF0YT4KCTxrZXk+T0lEU1RvSW5jbHVkZTwva2V5PgoJPGFycmF5PgoJCTxz
        dHJpbmc+MS4yLjg0MC4xMTM2MzUuMTAwLjEwLjE8L3N0cmluZz4KCTwvYXJyYXk+Cgk8
        a2V5Pk9zQnVpbGRWZXJzaW9uPC9rZXk+Cgk8c3RyaW5nPjIwRTI1Mjwvc3RyaW5nPgoJ
        PGtleT5SS0NlcnRpZmljYXRpb25QdWI8L2tleT4KCTxkYXRhPgoJQkgvL3VxQ1FzNEE0
        VElCdm1ZTWNJQkhiaGo1d0ZURkJaaDU3d2JkL0hTU25aSi9ZeE5YandIZ1FhZzlkandM
        Vk84VjkKCS9DUHk2dDhmbURvbXM1TFpmYVE9Cgk8L2RhdGE+Cgk8a2V5PlNJS1B1Yjwv
        a2V5PgoJPGRhdGE+CglCUHpXTnFSZk1MYmZCNTE0ZXorMWdXNG5vRzZxWGRMa3VySTYz
        Y0NXSlRaall5cHVQVWIvUVZnNmJySUg5M2VUREd3QwoJMFJHTERuYjF3VjlPOHhSWkxM
        ODVSNGdZMVJMeGw0ZnlmYkxLbzUrVHNoQk01bkYyckRlU0hMOG43cEFVOGc9PQoJPC9k
        YXRhPgoJPGtleT5TZWN1cml0eURvbWFpbjwva2V5PgoJPGludGVnZXI+MTwvaW50ZWdl
        cj4KCTxrZXk+U2VyaWFsTnVtYmVyPC9rZXk+Cgk8c3RyaW5nPkY2WDQzOU5IRjk8L3N0
        cmluZz4KCTxrZXk+VUlLUHViPC9rZXk+Cgk8ZGF0YT4KCUJEOHVOckcvaWJvL1RzbVg5
        a3JlVHZvS1N5WW1pZ2Z4UTJzTndTODVSb20rNy90TDBUM2toMlByTVlhWithZTlidWdq
        CglNVWxFTkxFOHBGUTFaTjdmSkprPQoJPC9kYXRhPgoJPGtleT5VbmlxdWVDaGlwSUQ8
        L2tleT4KCTxpbnRlZ2VyPjY4NzE1NDQ5NTU0MTI1MTA8L2ludGVnZXI+Cgk8a2V5PlZh
        bGlkaXR5PC9rZXk+Cgk8aW50ZWdlcj40NDExODY8L2ludGVnZXI+Cgk8a2V5PnNjcnRB
        dHRlc3RhdGlvbjwva2V5PgoJPGludGVnZXI+MDwvaW50ZWdlcj4KPC9kaWN0Pgo8L3Bs
        aXN0Pgo=
        &lt;/data&gt;
        &lt;key&gt;RKPropertiesSignature&lt;/key&gt;
        &lt;data&gt;
        MEYCIQD6alE5at+gOBxxxxxxYAIVZ6QwFJb7Q5lWdv/s95rSZAIhALu+7s0YPJu8LtN9
        O19Svcb46Vhw2CKF9Txv7GNheg+6
        &lt;/data&gt;
&lt;/dict&gt;
&lt;/plist&gt;
</code></pre>

<p>即使hook伪造 MGCopyAnswer获取的的设备 ID，也不能伪造成另一台真机，因为底层 attestation 必须由那台真机的 SE 自签。单纯在软件层面篡改 UniqueChipID 时，服务器端会发现 “你上报的设备 ID 和 attestation 里的（或服务器已有记录里的）设备硬件身份对不上”。</p>

<ul>
  <li>返回激活证书</li>
</ul>

<p>服务验签没问题后会返回正常的激活两本证书</p>

<pre><code class="language-Bash">-----BEGIN CERTIFICATE-----
MIIDQTCCAuagAwIBAgIGAZrdcv7/MAoGCCqGSM49BAMCMFMxJzAlBgNVBAMMHkJh
c2ljIEF0dGVzdGF0aW9uIFVzZXIgU3ViIENBMTETMBEGA1UECgwKQXBwbGUgSW5j
LjETMBEGA1UECAwKQ2FsaWZvcm5pYTAeFw0yNTEyMDEwNTA0NTJaFw0yNjA3MzEx
MTMzNTJaMIGRMUkwRwYDVQQDDEA4YmQzNmIxNWZkNGViODgxZjZlYzNlYzE1MjJh
NjlhNzE2ZGY1YTIxNzAxZmZlNTc1NWUwMmQ0MmVjZWRhYTVjMRowGAYDVQQLDBFC
QUEgQ2VydGlmaWNhdGlvbjETMBEGx1xECgwKQXBwbGUgSW5jLjETMBEGA1UECAwK
Q2FsaWZvcm5pYTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABAZRvBj8pkr4rDSb
r4x137hYn/JvpfSXJpuvcTL0XXwhvK5s/TDrhpp07/n3Vo/1GDz3DyDjGbOdIci2
6U4h54GjggFlMIIBYTAMBgNVHRMBAf8EAxxAMA4GA1UdDwEB/wQEAwIE8DCCAT8G
CSqGSIb3Y2QKAQSCATAEggEsMYIBKP+EmqGsUA0wCxYEQ0hJUAIDAIEQ/4SqjZJE
ETAPFgRFQ0lEAgcYaaI8ICAe/4SSvaRECzAJFgRCT1JEAgEY/4aTtcJjGzAZFgRi
bWFjBBFhYzoxNjoxNTo2ZDphNToxZf+Gy7XKaRkwFxYEaW1laQQPMzU2MTI0NzE1
MDY4NTU1/4ebydxtFDASFgRzcm5tBApGNlg0MzlOSEY5/4erkdJkIzAhFgR1ZGlk
BBkwMDAwODExMC0wMDE4NjlBMjNDMjAyMDFF/4e7tcJjGzAZFgR3bWFjBBFhYzox
NjoxNTo3OTo4ZTpmZf+Hm5XSZDowOBYEc2VpZAQwMDQyRDI3QzU4QTE2OTAwMjIz
MzIwMTMxNzI0MDUyMjRGRDE3MkZCRUQxM0VFQzdCMAoGCCqGSM49BAMCA0kAMEYC
IQCkVmdn5P6/mipbxpLOX7j9tTZ0ob4x2GzYsMeNF7mnwAIhAJgrskCv3xq9xR/Q
CaBYl+b8Ju0i2Jk5xxxxxxxxxx=
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
MIICIzCCAaigAwIBAgIIeNjhG9tnDGgwCxxIKxZIzj0EAwIwUzEnMCUGA1UEAwwe
QmFzaWMgQXR0ZXN0YXRpb24gVXNlciBSb290IENBMRMwEQYDVQQKDApBcHBsZSBJ
bmMuMRMwEQYDVQQIDApDYWxpZm9ybmlhMB4XDTE3MDQyMDAwNDIwMFoXDTMyMDMy
MjAwMDAwMFowUzEnMCUGA1UEAwweQmFzaWMgQXR0ZXN0YXRpb24gVXNlciBTdWIg
Q0ExMRMwEQYDVQQKDApBcHBsZSBJbmMuMRMwEQYDVQQIDApDYWxpZm9ybmlhMFkw
EwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEoSZ/1t9eBAEVp5a8PrXacmbGb8zNC1X3
StLI9YO6Y0CL7blHmSGmjGWTwD4Q+i0J2BY3+bPHTGRyA9jGB3MSbaNmMGQwEgYD
VR0TAQH/BAgwBgEB/wIBADAfBgNVHSMEGDAWgBSD5aMhnrB0w/lhkP2XTiMQdqSj
8jAdBgNVHQ4EFgQU5mWf1DYLTXUdQ9xmOH/uqeNSD80wDgYDVR0PAQH/BAQDAgEG
MAoGCCqGSM49BAMCA2kAMGYCMQC3M360LLtJS60Z9q3vVjJxMgMcFQ1roGTUcKqv
W+4hJ4CeJjySXTgq6IEHn/yWab4CMQCm5NnK6SOSK+AqWum9lL87W3E6AA1f2TvJ
/hgok/34jr93nhS87tOQNdxDS8xxxx=
-----END CERTIFICATE-----
</code></pre>

<p>这两本证书就是后面用于计算DeviceCheck Token的，格式解析如下：</p>

<p><img src="/images/posts/ios/devicecheck/img-03.png" alt="" /></p>

<p>验证证书有效后缓存在本地keychain，后面计算DeviceCheck Token时会有用到。</p>

<h3 id="24修改设备硬件信息激活验证测试">2.4、修改设备硬件信息激活验证测试</h3>

<ul>
  <li>修改全部设备信息</li>
</ul>

<pre><code class="language-JavaScript">// ========= 0. Hook MGCopyAnswer =========

    var g_mgCopyAnswerHooked = false;
    var mgJsCache = {
        UniqueChipID:    null,
        ChipID:          null,
        BoardId:         null,
        SecurityDomain:  null,
        SerialNumber:    null,
        BuildVersion:    null,
        UniqueDeviceID:  null
    };

    function hookGestatcopyAnswer() {
        try {
            if (g_mgCopyAnswerHooked) {
                console.log("[*] MGCopyAnswer already hooked");
                return;
            }

            var mgCopyAnswer = Module.findExportByName(null, "MGCopyAnswer");
            if (!mgCopyAnswer) {
                console.error("[-] MGCopyAnswer not found");
                return;
            }

            console.log("[+] MGCopyAnswer =", mgCopyAnswer);

            Interceptor.attach(mgCopyAnswer, {
                onEnter: function (args) {
                    this.keyStr = null;
                    try {
                        var keyObj = new ObjC.Object(args[0]);
                        var ks = keyObj.toString();
                        this.keyStr = ks;
                        console.log("[MGCopyAnswer] key =", ks);
                    } catch (e) {
                        console.log("[MGCopyAnswer] key (raw) =", args[0], "err:", e);
                    }
                },
                onLeave: function (retval) {
                    var keyStr = this.keyStr;
                    var origDesc;

                    try {
                        var origObj = new ObjC.Object(retval);
                        origDesc = origObj.toString();
                    } catch (e) {
                        origDesc = "&lt;raw&gt; " + retval;
                    }

                    console.log("[MGCopyAnswer] orig =&gt;", origDesc);

                    if (!CONFIG.patchMGCopyAnswer || !keyStr) {
                        return;
                    }

                    try {
                        var NSString = ObjC.classes.NSString;
                        var NSNumber = ObjC.classes.NSNumber;
                        var NSUUID   = ObjC.classes.NSUUID;
                        var fakeObj  = null;

                        var fixed    = CONFIG.fixedMG || {};
                        var useFixed = CONFIG.useFixedMGValues === true;

                        switch (keyStr) {
                            case "UniqueChipID":
                            case "Uniquechipid": {
                                var v;
                                if (useFixed &amp;&amp; fixed.UniqueChipID !== undefined) {
                                    v = Number(fixed.UniqueChipID);
                                } else {
                                    if (mgJsCache.UniqueChipID === null) {
                                        mgJsCache.UniqueChipID = (Math.floor(Math.random() * 0xFFFFFFFF) &gt;&gt;&gt; 0);
                                    }
                                    v = mgJsCache.UniqueChipID;
                                }
                                fakeObj = NSNumber.numberWithLongLong_(v);
                                fakeObj.retain();
                                console.log("[*] override UniqueChipID =&gt;", fakeObj.toString());
                                retval.replace(fakeObj.handle);
                                break;
                            }

                            case "ChipID":
                            case "Chipid": {
                                var cVal;
                                if (useFixed &amp;&amp; fixed.ChipID !== undefined) {
                                    cVal = Number(fixed.ChipID);
                                } else {
                                    if (mgJsCache.ChipID === null) {
                                        mgJsCache.ChipID = (0x2000 + Math.floor(Math.random() * 0x1000)) &gt;&gt;&gt; 0;
                                    }
                                    cVal = mgJsCache.ChipID;
                                }
                                fakeObj = NSNumber.numberWithUnsignedInt_(cVal &gt;&gt;&gt; 0);
                                fakeObj.retain();
                                console.log("[*] override ChipID =&gt;", fakeObj.toString());
                                retval.replace(fakeObj.handle);
                                break;
                            }

                            case "BoardId":
                            case "BoardID": {
                                var bVal;
                                if (useFixed &amp;&amp; fixed.BoardId !== undefined) {
                                    bVal = Number(fixed.BoardId);
                                } else {
                                    if (mgJsCache.BoardId === null) {
                                        mgJsCache.BoardId = 10 + Math.floor(Math.random() * 20);
                                    }
                                    bVal = mgJsCache.BoardId;
                                }
                                fakeObj = NSNumber.numberWithUnsignedInt_(bVal &gt;&gt;&gt; 0);
                                fakeObj.retain();
                                console.log("[*] override BoardId =&gt;", fakeObj.toString());
                                retval.replace(fakeObj.handle);
                                break;
                            }

                            case "SecurityDomain":
                            case "Securitydomain": {
                                var sVal;
                                if (useFixed &amp;&amp; fixed.SecurityDomain !== undefined) {
                                    sVal = Number(fixed.SecurityDomain);
                                } else {
                                    if (mgJsCache.SecurityDomain === null) {
                                        mgJsCache.SecurityDomain = 1;
                                    }
                                    sVal = mgJsCache.SecurityDomain;
                                }
                                fakeObj = NSNumber.numberWithUnsignedInt_(sVal &gt;&gt;&gt; 0);
                                fakeObj.retain();
                                console.log("[*] override SecurityDomain =&gt;", fakeObj.toString());
                                retval.replace(fakeObj.handle);
                                break;
                            }

                            case "SerialNumber":
                            case "Serialnumber": {
                                var sn;
                                if (useFixed &amp;&amp; fixed.SerialNumber) {
                                    sn = fixed.SerialNumber.toString();
                                } else {
                                    if (mgJsCache.SerialNumber === null) {
                                        mgJsCache.SerialNumber = "F" + randHex(11);
                                    }
                                    sn = mgJsCache.SerialNumber;
                                }
                                var snCStr = Memory.allocUtf8String(sn);
                                fakeObj = NSString.stringWithUTF8String_(snCStr);
                                fakeObj.retain();
                                console.log("[*] override SerialNumber =&gt;", fakeObj.toString());
                                retval.replace(fakeObj.handle);
                                break;
                            }

                            case "BuildVersion":
                            case "Buildversion": {
                                var bv;
                                if (useFixed &amp;&amp; fixed.BuildVersion) {
                                    bv = fixed.BuildVersion.toString();
                                } else {
                                    if (mgJsCache.BuildVersion === null) {
                                        mgJsCache.BuildVersion = "20" + randHex(4);
                                    }
                                    bv = mgJsCache.BuildVersion;
                                }
                                var bvCStr = Memory.allocUtf8String(bv);
                                fakeObj = NSString.stringWithUTF8String_(bvCStr);
                                fakeObj.retain();
                                console.log("[*] override BuildVersion =&gt;", fakeObj.toString());
                                retval.replace(fakeObj.handle);
                                break;
                            }

                            case "UniqueDeviceID": {
                                var udid;
                                if (useFixed &amp;&amp; fixed.UniqueDeviceID) {
                                    udid = fixed.UniqueDeviceID.toString();
                                } else {
                                    if (mgJsCache.UniqueDeviceID === null) {
                                        var uuidObj = NSUUID.UUID();
                                        mgJsCache.UniqueDeviceID = uuidObj.UUIDString().toString();
                                    }
                                    udid = mgJsCache.UniqueDeviceID;
                                }
                                var udidCStr = Memory.allocUtf8String(udid);
                                fakeObj = NSString.stringWithUTF8String_(udidCStr);
                                fakeObj.retain();
                                console.log("[*] override UniqueDeviceID =&gt;", fakeObj.toString());
                                retval.replace(fakeObj.handle);
                                break;
                            }

                            default:
                                break;
                        }
                    } catch (e) {
                        console.log("[-] MGCopyAnswer patch error:", e);
                    }
                }
            });

            g_mgCopyAnswerHooked = true;
            console.log("[+] Hooked MGCopyAnswer (patch =", CONFIG.patchMGCopyAnswer,
                        ", useFixed =", CONFIG.useFixedMGValues, ")");
        } catch (e) {
            console.error("[-] hookGestatcopyAnswer failed:", e.stack || e);
        }
    }
</code></pre>

<p>全部更改设备信息服务器会返回错误</p>

<pre><code class="language-XML">-----BEGIN ERROR-----
93:UCRT SCRT SEAL from tabasco is null.
-----END ERROR-----
</code></pre>

<p>站在苹果服务器的角度，它收到这些东西之后，大概会做这样几步检测：</p>

<pre><code class="language-Markdown">1. 验证 SE attestation（由 Apple 根证书/中间证书链签的）：
   - 检查证书链是否正确、未过期、签名有效
   - 从 attestation blob 里取出：
       * 这个 key 所在的“安全硬件身份”：比如 Secure Enclave 的某个 ID
       * 跟这一块硬件绑定的标识：UniqueChipID/ChipID/SecurityDomain/...（以某种形式存在）

2. 验证 RKPropertiesSignature：
   - 用 attestation 里证明的那把 refKey 的 “公钥” 去验签 RKPropertiesSignature
   - 确认：RKProperties 确实是 **同一个 SE 里那把 refKey** 签出来的，
           而不是你在用户态自己随便弄的 key。

3. 一致性检查：
   - 从 attestation / 后端数据库里得到的“真实硬件 ID” 记为 H = {U_real, ChipID_real, ...}
   - 从 RKProperties 里解析出你上报的 ID 记为 S = {U_reported, ChipID_reported, ...}

   然后做类似：
       if U_reported != U_real or ChipID_reported != ChipID_real ...
           =&gt; 认为这是“篡改 / 不一致”，直接拒绝，后面的 UCRT/SCRT/SEAL 都不下发
</code></pre>

<ul>
  <li>修改部分真实设备信息</li>
</ul>

<p>其它字段随机、只保留UniqueChipID、ChipID不改，正常返回证书；</p>

<ul>
  <li>A手机信息替换为B手机</li>
</ul>

<p>将A手机的UniqueChipID、ChipID替换到B手机，返回如下：</p>

<pre><code class="language-XML">-----BEGIN ERROR-----
29:ECC signature verification failed: signature incorrect.
-----END ERROR-----
</code></pre>

<ul>
  <li>整体流程与修改信息验证流程</li>
</ul>

<p><img src="/images/posts/ios/devicecheck/img-04.png" alt="" /></p>

<ul>
  <li>修改设备信息验证流程</li>
</ul>

<p><img src="/images/posts/ios/devicecheck/img-05.png" alt="" /></p>

<ul>
  <li>结论</li>
</ul>

<p>从上面验证情况来看，硬ID与SE 里的证书是绑定的。SE 里的“根密钥”和“设备证书”是在出厂生产 / 校准阶段由 Apple 写入硬件安全区的，属于“出厂级”的东西。</p>

<p>我改的 MobileGestalt 层（UniqueChipID/ChipID）只是“软件视角的 ID”，并不会改变 SE 里真实的密钥和它生成的签名，所以软改实现激活大概率是不可行的。</p>

<h2 id="三devicecheck-架构与正向流程">三、DeviceCheck 架构与正向流程</h2>

<p>DeviceCheck 的工作流程涉及三方交互：客户端 App、系统守护进程 <code class="language-plaintext highlighter-rouge">devicecheckd</code> 以及 Apple 服务器。整体流程如下：</p>

<p><img src="/images/posts/ios/devicecheck/img-06.png" alt="" /></p>

<h3 id="31-核心组件">3.1 核心组件</h3>

<ul>
  <li>
    <p><strong>客户端 (App)</strong>：通过 <code class="language-plaintext highlighter-rouge">DCDevice</code> 类发起 Token 生成请求。</p>
  </li>
  <li>
    <p><strong>守护进程 (devicecheckd)</strong>：iOS 系统内置的守护进程，负责与硬件安全模块（Secure Enclave）通信，执行核心加密操作。</p>
  </li>
  <li>
    <p><strong>Apple Server</strong>：接收 Token，验证其合法性，并根据 Token 更新或查询设备的 2-bit 状态。</p>
  </li>
</ul>

<h3 id="32-bit-状态存储机制">3.2 bit 状态存储机制</h3>

<p>DeviceCheck 允许开发者为每个设备-App 组合存储两个二进制位（00, 01, 10, 11）。</p>

<ul>
  <li>
    <p><strong>Bit 0</strong>：通常用于标记“是否已领取新人优惠”。</p>
  </li>
  <li>
    <p><strong>Bit 1</strong>：通常用于标记“是否为违规设备”或“是否被封禁”。 由于这两个位是存储在 Apple 服务器上的，且与硬件强绑定，因此单纯的重置 IDFA 或卸载 App 无法清除这些标记。</p>
  </li>
</ul>

<h3 id="33-正向调用流程">3.3 正向调用流程</h3>

<ul>
  <li>
    <p>App 调用 <code class="language-plaintext highlighter-rouge">[DCDevice generateTokenWithCompletionHandler:]</code>。</p>
  </li>
  <li>
    <p>请求通过 XPC（跨进程通信）被转发给系统的 <code class="language-plaintext highlighter-rouge">com.apple.devicecheckd</code> 服务。</p>
  </li>
  <li>
    <p><code class="language-plaintext highlighter-rouge">devicecheckd</code> 调用底层硬件接口生成加密的 Base64 Token。</p>
  </li>
  <li>
    <p>App 将 Token 发送给开发者的业务服务器。</p>
  </li>
  <li>
    <p>业务服务器调用 Apple API（<code class="language-plaintext highlighter-rouge">update_two_bits</code> 或 <code class="language-plaintext highlighter-rouge">query_two_bits</code>），验证 Token 并读写状态。</p>
  </li>
</ul>

<h2 id="四逆向分析揭秘-devicecheckd">四、逆向分析：揭秘 devicecheckd</h2>

<p>通过对 <code class="language-plaintext highlighter-rouge">devicecheckd</code> 二进制文件的静态分析与动态调试，分析其核心加密流程。</p>

<h3 id="41-关键函数定位">4.1 关键函数定位</h3>

<ul>
  <li>API 定位：</li>
</ul>

<p>在 App 二进制中搜索客户端获取DeviceCheckenToken调用的api，定位到 <code class="language-plaintext highlighter-rouge">[DCDevice generateTokenWithCompletionHandler:]</code></p>

<p>然后再一步调试到关键方法，但这种方式比较慢，还有一种更方便的方式，根据日志输出字符串定位，下面是生成token过程中输出的日志：</p>

<pre><code class="language-XML">Generating certificate...
Fetched remote public key...
Encrypting data...
Certificate issued, processing..
Using synced timestamp
Returning cached certificates
-[MAAssetQuery getResultsFromMessage:]: Got the query meta data reply for: com.apple.MobileAsset.DeviceCheck, response: 0
Falling back to locally cached key... Asset fetch failed: Error Domain=com.apple.twobit.fetcherror Code=-3100
</code></pre>

<ul>
  <li>搜索“Generating certificate…”定位到获取公钥的方法,这个密钥后面加密时会用到。</li>
</ul>

<pre><code class="language-C++">void __fastcall __37__DCCryptoProxyImpl__fetchPublicKey___block_invoke(__int64 a1, void *a2, void *a3)
{
  id v5; // x19
  id v6; // x20
  void *v7; // x23
  __int64 v8; // x0
  __int64 v9; // x0
  NSObject *v10; // x22
  _BOOL4 v11; // w0
  __int64 v12; // x22
  __int64 v13; // x0
  void *v14; // x21
  int v15; // [xsp+0h] [xbp-50h] BYREF
  id v16; // [xsp+4h] [xbp-4Ch]

  v5 = objc_retain(a2);
  v6 = objc_retain(a3);
  v7 = (void *)objc_claimAutoreleasedReturnValue_1330(objc_msgSend(v5, "publicKey"));
  objc_release(v7);
  v9 = _DCLogSystem(v8);
  v10 = (NSObject *)objc_claimAutoreleasedReturnValue_1330(v9);
  v11 = os_log_type_enabled_1518(v10, OS_LOG_TYPE_DEFAULT);
  if ( v7 )
  {
    if ( v11 )
    {
      LOWORD(v15) = 0;
      _os_log_impl_1398(&amp;dword_446A2E000, v10, OS_LOG_TYPE_DEFAULT, "Fetched remote public key...", (uint8_t *)&amp;v15, 2u);
    }
    objc_release(v10);
    v12 = *(_QWORD *)(a1 + 32);
    v13 = objc_claimAutoreleasedReturnValue_1330(objc_msgSend(v5, "publicKey"));
  }
  else
  {
    if ( v11 )
    {
      v15 = 138412290;
      v16 = v6;
      _os_log_impl_1398(
        &amp;dword_446A2E000,
        v10,
        OS_LOG_TYPE_DEFAULT,
        "Falling back to locally cached key... Asset fetch failed: %@",
        (uint8_t *)&amp;v15,
        0xCu);
    }
    objc_release(v10);
    v12 = *(_QWORD *)(a1 + 32);
    v13 = objc_claimAutoreleasedReturnValue_1330(objc_msgSend(&amp;OBJC_CLASS___NSData, "dataWithBytes:length:", &amp;fallback_server_pubkey, 65));
  }
  v14 = (void *)v13;
  (*(void (__fastcall **)(__int64, __int64))(v12 + 16))(v12, v13);
  objc_release(v14);
  objc_release(v6);
  objc_release(v5);
}
</code></pre>

<p>尝试用 DCAssetFetcher/MobileAsset 拉最新的远程公钥，如果成功就用它；失败就用代码中默认密钥fallback_server_pubkey 常量，然后把选出来的公钥交回后续逻辑（DCCertificateGenerator 那边去做 ECDH）。</p>

<pre><code class="language-Objective-C">pub key
0000000A9700B010              04 50 D9 34  FA 67 BC F6 F2 DF BF 96
0000000A9700B020  62 9E 0A 72 38 E9 20 5D  75 F2 8C FC D8 4F 35 A6 
0000000A9700B030  59 2B BE 05 8A 9C 0F 8E  DB CA 2A CB 67 EF B7 74 
0000000A9700B040  97 1C A4 5F 7D 85 6A 69  4F B1 B9 C4 0B 94 FB 2E 
0000000A9700B050  7A 5A 94 98 B0
</code></pre>

<ul>
  <li>准备读取本地缓存的激活证书</li>
</ul>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
**这张证书属于哪一类身份 &amp; 存在哪个抽屉**
KeychainLabel = "2bit-identity"
KeychainAccessGroup = "2bit-identity"
//生成配置信息
id +[DCCryptoUtilities identityCertificateOptions]()
{
  void *v0; // x19
  __int64 v1; // x0
  NSObject *v2; // x20
  void *v3; // x22
  void *v4; // x23
  void *v5; // x24
  void *v6; // x21
  void *v7; // x22
  __CFString *v9; // [xsp+0h] [xbp-A0h] BYREF
  _QWORD v10[6]; // [xsp+8h] [xbp-98h] BYREF
  _QWORD v11[6]; // [xsp+38h] [xbp-68h] BYREF
  __int64 vars8; // [xsp+A8h] [xbp+8h]

  v0 = objc_msgSend(&amp;OBJC_CLASS___DCCryptoUtilities, "generateTTL");
  v1 = _DCLogSystem(v0);
  v2 = (NSObject *)objc_claimAutoreleasedReturnValue_1330(v1);
  if ( os_log_type_enabled_1518(v2, OS_LOG_TYPE_DEBUG) )
    +[DCCryptoUtilities identityCertificateOptions].cold.1(v0, v2);
  objc_release(v2);
  v10[0] = CFSTR("Validity");
  v3 = (void *)objc_claimAutoreleasedReturnValue_1330(objc_msgSend(&amp;OBJC_CLASS___NSNumber, "numberWithUnsignedInt:", v0));
  v11[0] = v3;
  v11[1] = &amp;__kCFBooleanFalse;
  v10[1] = CFSTR("scrtAttestation");
  v10[2] = CFSTR("KeychainLabel");
  v11[2] = CFSTR("2bit-identity");
  v11[3] = CFSTR("2bit-identity");
  v10[3] = CFSTR("KeychainAccessGroup");
  v10[4] = CFSTR("IgnoreExistingKeychainItems");
  v11[4] = &amp;__kCFBooleanFalse;
  v10[5] = CFSTR("OIDSToInclude");
  v9 = CFSTR("1.2.840.113635.100.10.1");
  v4 = (void *)objc_claimAutoreleasedReturnValue_1330(objc_msgSend(&amp;OBJC_CLASS___NSArray, "arrayWithObjects:count:", &amp;v9, 1));
  v11[5] = v4;
  v5 = (void *)objc_claimAutoreleasedReturnValue_1330(objc_msgSend(&amp;OBJC_CLASS___NSDictionary, "dictionaryWithObjects:forKeys:count:", v11, v10, 6));
  v6 = (void *)objc_claimAutoreleasedReturnValue_1330(objc_msgSend(&amp;OBJC_CLASS___NSMutableDictionary, "dictionaryWithDictionary:", v5));
  objc_release(v5);
  objc_release(v4);
  objc_release(v3);
  if ( (unsigned int)os_variant_allows_internal_security_policies_55(0) )
  {
    v7 = objc_msgSend(
           objc_alloc((Class)&amp;OBJC_CLASS___NSUserDefaults),
           "initWithSuiteName:",
           CFSTR("com.apple.DeviceCheck"));
    if ( (unsigned int)objc_msgSend(v7, "boolForKey:", CFSTR("DCIgnoreExistingKeychainItems")) )
      objc_msgSend(v6, "setObject:forKeyedSubscript:", &amp;__kCFBooleanTrue, CFSTR("IgnoreExistingKeychainItems"));
    objc_release(v7);
  }
  if ( (DeviceIdentityUCRTAttestationSupported_1() &amp; 1) == 0 )
    objc_msgSend(v6, "setObject:forKeyedSubscript:", &amp;__kCFBooleanTrue, CFSTR("scrtAttestation"));
  objc_msgSend(v6, "setObject:forKeyedSubscript:", &amp;__kCFBooleanTrue, CFSTR("ReturnReferenceDate"));
  if ( ((vars8 ^ (2 * vars8)) &amp; 0x4000000000000000LL) != 0 )
    __break(0xC471u);
  return objc_autoreleaseReturnValue(v6);
}
//开始读取
void __fastcall __DeviceIdentityIssueClientCertificateWithCompletion_block_invoke(__int64 a1)
{
  __int64 v2; // x5
  __int64 v3; // x6
  __int64 v4; // x7
  NSObject *v5; // x20
  dispatch_queue_global_t global_queue_364; // x0
  __int64 v7; // x19
  __int64 v8; // x5
  __int64 v9; // x6
  __int64 v10; // x7
  __SecTask *v11; // x0
  __int64 v12; // x5
  __int64 v13; // x6
  __int64 v14; // x7
  __SecTask *v15; // x23
  __int64 v16; // x5
  __int64 v17; // x6
  __int64 v18; // x7
  __int64 v19; // x0
  void *v20; // x21
  unsigned __int8 v21; // w22
  void *v22; // x0
  id *v23; // x28
  void *v24; // x21
  __int64 v25; // x0
  void *v26; // x22
  void *v27; // x21
  void *v28; // x22
  __int64 v29; // x0
  void *v30; // x22
  unsigned __int8 v31; // w25
  id MobileActivationError_0; // x0
  __int64 v33; // x0
  id v34; // x0
  id v35; // x21
  CFDictionaryRef v36; // x25
  CFDataRef v37; // x28
  void *v38; // x22
  void *v39; // x27
  __int64 v40; // x8
  void *v41; // x26
  void *v42; // x9
  __int64 *v43; // x8
  const void *v44; // x0
  __int64 *v45; // x8
  const void *v46; // x0
  __int64 *v47; // x8
  const void *v48; // x0
  __int64 *v49; // x8
  const void *v50; // x0
  __int64 *v51; // x8
  const void *v52; // x0
  id v53; // x26
  id *v54; // x19
  id v55; // x0
  CFErrorRef v56; // x21
  __int64 v57; // x5
  __int64 v58; // x6
  __int64 v59; // x7
  id v60; // x0
  __int64 v61; // x0
  __int64 v62; // x8
  void *v63; // x9
  id v64; // x0
  bool v65; // w25
  __int64 v66; // x5
  __int64 v67; // x6
  __int64 v68; // x7
  char v69; // w26
  __int64 v70; // x0
  __int64 v71; // x5
  __int64 v72; // x6
  __int64 v73; // x7
  __int64 v74; // x0
  __int64 v75; // x5
  __int64 v76; // x6
  __int64 v77; // x7
  __int64 v78; // x21
  __int64 v79; // x0
  __int64 v80; // x5
  __int64 v81; // x6
  __int64 v82; // x7
  const __CFData *v83; // x21
  SecCertificateRef v84; // x0
  __int64 v85; // x5
  __int64 v86; // x6
  __int64 v87; // x7
  const __CFData *v88; // x21
  SecCertificateRef v89; // x0
  __int64 v90; // x5
  __int64 v91; // x6
  __int64 v92; // x7
  id v93; // x0
  CFErrorRef v94; // x19
  __int64 v95; // x5
  __int64 v96; // x6
  __int64 v97; // x7
  id v98; // x0
  __int64 v99; // x0
  __int64 v100; // x8
  void *v101; // x9
  __int64 v102; // x19
  __int64 v103; // x5
  __int64 v104; // x6
  __int64 v105; // x7
  __int64 v106; // x19
  void *v107; // x3
  id *v108; // x19
  void *v109; // t1
  id v110; // x0
  double v111; // d8
  __int64 v112; // x5
  __int64 v113; // x6
  __int64 v114; // x7
  __int64 v115; // x19
  void *v116; // x3
  id *v117; // x19
  void *v118; // t1
  id v119; // x0
  void *v120; // x25
  unsigned __int8 v121; // w26
  void *v122; // x21
  void *v123; // x22
  void *v124; // x25
  void *v125; // x26
  void *v126; // x27
  __int64 v127; // x5
  __int64 v128; // x6
  __int64 v129; // x7
  id v130; // x0
  int v131; // w21
  void *v132; // x3
  id v133; // x0
  id v134; // x0
  id v135; // x0
  id v136; // x0
  __int64 v137; // x5
  __int64 v138; // x6
  __int64 v139; // x7
  __int64 v140; // x5
  __int64 v141; // x6
  __int64 v142; // x7
  id v143; // x0
  void *v144; // x9
  __int64 v145; // x5
  __int64 v146; // x6
  __int64 v147; // x7
  __int64 v148; // x5
  __int64 v149; // x6
  __int64 v150; // x7
  CFDataRef v151; // x21
  __int64 v152; // x5
  __int64 v153; // x6
  __int64 v154; // x7
  __int64 v155; // x0
  __int64 v156; // x21
  char v157; // w22
  __int64 v158; // x5
  __int64 v159; // x6
  __int64 v160; // x7
  id v161; // x0
  void *v162; // x22
  __int64 v163; // x0
  void *v164; // x21
  __int64 v165; // x22
  void *v166; // x21
  __int64 v167; // x0
  void *v168; // x19
  void *v169; // x22
  __int64 v170; // x5
  __int64 v171; // x6
  __int64 v172; // x7
  id v173; // x0
  __int64 v174; // x0
  __int64 v175; // x0
  void *v176; // x9
  __int64 v177; // x5
  __int64 v178; // x6
  __int64 v179; // x7
  void *v180; // x0
  __int64 v181; // x0
  __int64 v182; // x0
  void *v183; // x9
  __int64 v184; // x5
  __int64 v185; // x6
  __int64 v186; // x7
  void *v187; // x0
  __int64 v188; // x0
  __int64 v189; // x0
  void *v190; // x9
  void *v191; // x0
  id v192; // x0
  void *v193; // x0
  void *v194; // x9
  id v195; // x0
  id v196; // x0
  id v197; // x0
  id v198; // x0
  id v199; // x0
  void *v200; // x21
  __int64 v201; // x0
  void *v202; // x22
  __int64 v203; // x5
  __int64 v204; // x6
  __int64 v205; // x7
  id v206; // x0
  void *v207; // x21
  __int64 v208; // x0
  void *v209; // x22
  __int64 v210; // x0
  __CFString *v211; // x0
  void *v212; // x21
  __int64 v213; // x0
  void *v214; // x22
  void *v215; // x21
  unsigned int v216; // w19
  void *v217; // x22
  __int64 v218; // x5
  __int64 v219; // x6
  __int64 v220; // x7
  id v221; // x0
  void *v222; // x22
  __int64 v223; // x0
  void *v224; // x25
  __int64 v225; // x5
  __int64 v226; // x6
  __int64 v227; // x7
  id v228; // x0
  void *v229; // x25
  __int64 v230; // x0
  void *v231; // x26
  __int64 v232; // x5
  __int64 v233; // x6
  __int64 v234; // x7
  id v235; // x0
  void *v236; // x22
  CFTypeID v237; // x25
  CFTypeID TypeID_4; // x26
  void *v239; // x22
  __int64 v240; // x0
  void *v241; // x25
  void *v242; // x21
  void *v243; // x22
  __int64 v244; // x0
  void *v245; // x25
  void *v246; // x21
  void *v247; // x25
  __int64 v248; // x0
  void *v249; // x26
  void *v250; // x21
  void *v251; // x21
  __int64 v252; // x0
  void *v253; // x26
  void *v254; // x21
  __int64 v255; // x0
  void *v256; // x26
  __int64 v257; // x5
  __int64 v258; // x6
  __int64 v259; // x7
  void *v260; // x25
  __int64 v261; // x0
  void *v262; // x26
  void *v263; // x21
  unsigned int v264; // w27
  void *v265; // x25
  __int64 v266; // x0
  void *v267; // x26
  void *v268; // x21
  unsigned int v269; // w22
  void *v270; // x25
  __int64 v271; // x0
  void *v272; // x26
  void *v273; // x19
  __int64 v274; // x26
  __int64 v275; // x27
  __int64 v276; // x22
  __int64 v277; // x19
  __int64 v278; // x0
  id v279; // x0
  void *v280; // x21
  __int64 v281; // x0
  __int64 v282; // x5
  __int64 v283; // x6
  __int64 v284; // x7
  __int64 v285; // x5
  __int64 v286; // x6
  __int64 v287; // x7
  id v288; // x0
  id v289; // x0
  id v290; // x0
  id v291; // x0
  __int64 v292; // x0
  __int64 v293; // x8
  void *v294; // x9
  __int64 v295; // x0
  __int64 v296; // x25
  __int64 v297; // x21
  int valid; // w28
  __int64 v299; // x0
  __int64 v300; // x21
  int v301; // w28
  __int64 v302; // x21
  char v303; // w28
  id v304; // x0
  __int64 v305; // x0
  __int64 v306; // x8
  void *v307; // x9
  id v308; // x0
  void *v309; // x8
  __int64 *v310; // x8
  const void *v311; // x0
  __int64 *v312; // x8
  const void *v313; // x0
  __int64 *v314; // x8
  const void *v315; // x0
  __int64 v316; // x8
  void *v317; // x9
  double v318; // d0
  void *v319; // x0
  __int64 v320; // x0
  char v321; // w19
  void *v322; // x8
  __int64 v323; // x0
  __int64 v324; // x21
  __int64 v325; // x21
  double v326; // d8
  double v327; // d9
  double v328; // d0
  int v329; // w25
  int v330; // w8
  id v331; // x0
  __int64 v332; // x0
  const void *v333; // x0
  __SecKey *v334; // x25
  __int64 v335; // x2
  __int64 v336; // x26
  __int64 v337; // x5
  __int64 v338; // x6
  __int64 v339; // x7
  __SecKey *v340; // x0
  __int64 v341; // x5
  __int64 v342; // x6
  __int64 v343; // x7
  __int64 v344; // x5
  __int64 v345; // x6
  __int64 v346; // x7
  CFDictionaryRef v347; // x0
  __int64 v348; // x5
  __int64 v349; // x6
  __int64 v350; // x7
  __int64 v351; // x5
  __int64 v352; // x6
  __int64 v353; // x7
  void *v354; // x26
  dispatch_queue_global_t v355; // x0
  __CFString *v356; // x0
  __int64 v357; // x8
  id v358; // x0
  id v359; // x0
  id v360; // x0
  id v361; // x0
  __int64 v362; // x9
  void *v363; // x8
  const void *v364; // x0
  CFTypeRef v365; // x0
  id v366; // x0
  void *v367; // x0
  void *v368; // x0
  void *v369; // x0
  __int64 v370; // x19
  void *v371; // x0
  id v372; // x0
  __int64 v373; // x8
  id v374; // x19
  id v375; // x0
  id v376; // x0
  id v377; // x0
  id v378; // x0
  id v379; // x0
  id v380; // x0
  __int64 v381; // x9
  __int64 *v382; // x8
  __int64 *v383; // x9
  char v384; // [xsp+0h] [xbp-680h]
  void *v385; // [xsp+18h] [xbp-668h]
  id v386; // [xsp+20h] [xbp-660h]
  void *v387; // [xsp+28h] [xbp-658h]
  id v388; // [xsp+30h] [xbp-650h]
  __int64 v389; // [xsp+38h] [xbp-648h]
  void *v390; // [xsp+38h] [xbp-648h]
  int v391; // [xsp+40h] [xbp-640h]
  unsigned int v392; // [xsp+40h] [xbp-640h]
  __int64 v393; // [xsp+40h] [xbp-640h]
  void *v394; // [xsp+40h] [xbp-640h]
  _BOOL4 v395; // [xsp+48h] [xbp-638h]
  void *v396; // [xsp+48h] [xbp-638h]
  _BOOL4 v397; // [xsp+50h] [xbp-630h]
  void *v398; // [xsp+50h] [xbp-630h]
  _BOOL4 v399; // [xsp+58h] [xbp-628h]
  id v400; // [xsp+58h] [xbp-628h]
  void *queue; // [xsp+60h] [xbp-620h]
  _BOOL4 queuea; // [xsp+60h] [xbp-620h]
  unsigned int queueb; // [xsp+60h] [xbp-620h]
  NSObject *queuec; // [xsp+60h] [xbp-620h]
  id queued; // [xsp+60h] [xbp-620h]
  id v406; // [xsp+68h] [xbp-618h]
  void *v407; // [xsp+70h] [xbp-610h]
  id v408; // [xsp+78h] [xbp-608h]
  void *v409; // [xsp+80h] [xbp-600h]
  id v410; // [xsp+88h] [xbp-5F8h]
  id v411; // [xsp+90h] [xbp-5F0h]
  void *v412; // [xsp+98h] [xbp-5E8h]
  id v413; // [xsp+A0h] [xbp-5E0h]
  CFDataRef v414; // [xsp+A8h] [xbp-5D8h]
  unsigned int v415; // [xsp+A8h] [xbp-5D8h]
  unsigned int v416; // [xsp+A8h] [xbp-5D8h]
  id v417; // [xsp+B0h] [xbp-5D0h]
  id v418; // [xsp+B8h] [xbp-5C8h]
  id v419; // [xsp+C0h] [xbp-5C0h]
  unsigned __int8 v420; // [xsp+C0h] [xbp-5C0h]
  void *v421; // [xsp+C8h] [xbp-5B8h]
  __CFString *v422; // [xsp+D0h] [xbp-5B0h]
  void *cf; // [xsp+D8h] [xbp-5A8h]
  void *cfa; // [xsp+D8h] [xbp-5A8h]
  id v425; // [xsp+E0h] [xbp-5A0h]
  _QWORD block[4]; // [xsp+E8h] [xbp-598h] BYREF
  _QWORD v427[4]; // [xsp+108h] [xbp-578h] BYREF
  _QWORD v428[4]; // [xsp+128h] [xbp-558h] BYREF
  id v429; // [xsp+148h] [xbp-538h] BYREF
  __CFString *v430; // [xsp+150h] [xbp-530h]
  id v431; // [xsp+158h] [xbp-528h]
  id v432; // [xsp+160h] [xbp-520h]
  id v433; // [xsp+168h] [xbp-518h]
  id v434; // [xsp+170h] [xbp-510h]
  id v435; // [xsp+178h] [xbp-508h]
  id v436; // [xsp+180h] [xbp-500h]
  id v437; // [xsp+188h] [xbp-4F8h]
  id v438; // [xsp+190h] [xbp-4F0h]
  id v439; // [xsp+198h] [xbp-4E8h]
  id v440; // [xsp+1A0h] [xbp-4E0h]
  id v441; // [xsp+1A8h] [xbp-4D8h]
  NSObject *v442; // [xsp+1B0h] [xbp-4D0h]
  id v443; // [xsp+1B8h] [xbp-4C8h]
  __int64 v444; // [xsp+1C0h] [xbp-4C0h]
  _QWORD *v445; // [xsp+1C8h] [xbp-4B8h]
  __int64 *v446; // [xsp+1D0h] [xbp-4B0h]
  __int64 *v447; // [xsp+1D8h] [xbp-4A8h]
  __int64 *v448; // [xsp+1E0h] [xbp-4A0h]
  __int64 *v449; // [xsp+1E8h] [xbp-498h]
  __int64 *v450; // [xsp+1F0h] [xbp-490h]
  __int64 *v451; // [xsp+1F8h] [xbp-488h]
  __int64 *v452; // [xsp+200h] [xbp-480h]
  __int64 *v453; // [xsp+208h] [xbp-478h]
  __int64 *v454; // [xsp+210h] [xbp-470h]
  __int64 *v455; // [xsp+218h] [xbp-468h]
  __int64 *v456; // [xsp+220h] [xbp-460h]
  __int64 *v457; // [xsp+228h] [xbp-458h]
  __int64 *v458; // [xsp+230h] [xbp-450h]
  __int64 *v459; // [xsp+238h] [xbp-448h]
  __int64 *v460; // [xsp+240h] [xbp-440h]
  __int64 *v461; // [xsp+248h] [xbp-438h]
  bool v462; // [xsp+250h] [xbp-430h]
  id v463; // [xsp+258h] [xbp-428h] BYREF
  id v464; // [xsp+260h] [xbp-420h]
  id v465; // [xsp+268h] [xbp-418h] BYREF
  id v466; // [xsp+270h] [xbp-410h] BYREF
  id v467; // [xsp+278h] [xbp-408h] BYREF
  id obj; // [xsp+280h] [xbp-400h] BYREF
  id v469; // [xsp+288h] [xbp-3F8h] BYREF
  id v470; // [xsp+290h] [xbp-3F0h] BYREF
  id v471; // [xsp+298h] [xbp-3E8h] BYREF
  __int64 v472; // [xsp+2A0h] [xbp-3E0h] BYREF
  double *v473; // [xsp+2A8h] [xbp-3D8h]
  __int64 v474; // [xsp+2B0h] [xbp-3D0h]
  __int64 v475; // [xsp+2B8h] [xbp-3C8h]
  __int64 v476; // [xsp+2C0h] [xbp-3C0h] BYREF
  __int64 *v477; // [xsp+2C8h] [xbp-3B8h]
  __int64 v478; // [xsp+2D0h] [xbp-3B0h]
  int v479; // [xsp+2D8h] [xbp-3A8h]
  int v480; // [xsp+2E4h] [xbp-39Ch] BYREF
  double v481; // [xsp+2E8h] [xbp-398h] BYREF
  __int64 v482; // [xsp+2F0h] [xbp-390h] BYREF
  double *v483; // [xsp+2F8h] [xbp-388h]
  __int64 v484; // [xsp+300h] [xbp-380h]
  __int64 v485; // [xsp+308h] [xbp-378h]
  __int64 v486; // [xsp+310h] [xbp-370h] BYREF
  __int64 *v487; // [xsp+318h] [xbp-368h]
  __int64 v488; // [xsp+320h] [xbp-360h]
  __int64 (__fastcall *v489)(); // [xsp+328h] [xbp-358h]
  __int64 (__fastcall *v490)(); // [xsp+330h] [xbp-350h]
  id v491; // [xsp+338h] [xbp-348h]
  __int64 v492; // [xsp+340h] [xbp-340h] BYREF
  __int64 *v493; // [xsp+348h] [xbp-338h]
  __int64 v494; // [xsp+350h] [xbp-330h]
  __int64 (__fastcall *v495)(); // [xsp+358h] [xbp-328h]
  __int64 (__fastcall *v496)(); // [xsp+360h] [xbp-320h]
  id v497; // [xsp+368h] [xbp-318h]
  __int64 v498; // [xsp+370h] [xbp-310h] BYREF
  __int64 *v499; // [xsp+378h] [xbp-308h]
  __int64 v500; // [xsp+380h] [xbp-300h]
  __int64 (__fastcall *v501)(); // [xsp+388h] [xbp-2F8h]
  __int64 (__fastcall *v502)(); // [xsp+390h] [xbp-2F0h]
  id v503; // [xsp+398h] [xbp-2E8h]
  __int64 v504; // [xsp+3A0h] [xbp-2E0h] BYREF
  __int64 *v505; // [xsp+3A8h] [xbp-2D8h]
  __int64 v506; // [xsp+3B0h] [xbp-2D0h]
  __int64 (__fastcall *v507)(); // [xsp+3B8h] [xbp-2C8h]
  __int64 (__fastcall *v508)(); // [xsp+3C0h] [xbp-2C0h]
  id v509; // [xsp+3C8h] [xbp-2B8h]
  __int64 v510; // [xsp+3D0h] [xbp-2B0h] BYREF
  __int64 *v511; // [xsp+3D8h] [xbp-2A8h]
  __int64 v512; // [xsp+3E0h] [xbp-2A0h]
  __int64 (__fastcall *v513)(); // [xsp+3E8h] [xbp-298h]
  __int64 (__fastcall *v514)(); // [xsp+3F0h] [xbp-290h]
  id v515; // [xsp+3F8h] [xbp-288h]
  __int64 v516; // [xsp+400h] [xbp-280h] BYREF
  __int64 *v517; // [xsp+408h] [xbp-278h]
  __int64 v518; // [xsp+410h] [xbp-270h]
  __int64 (__fastcall *v519)(); // [xsp+418h] [xbp-268h]
  __int64 (__fastcall *v520)(); // [xsp+420h] [xbp-260h]
  id v521; // [xsp+428h] [xbp-258h]
  int v522; // [xsp+434h] [xbp-24Ch] BYREF
  __int64 v523; // [xsp+438h] [xbp-248h] BYREF
  __int64 *v524; // [xsp+440h] [xbp-240h]
  __int64 v525; // [xsp+448h] [xbp-238h]
  __int64 v526; // [xsp+450h] [xbp-230h]
  __int64 v527; // [xsp+458h] [xbp-228h] BYREF
  __int64 *v528; // [xsp+460h] [xbp-220h]
  __int64 v529; // [xsp+468h] [xbp-218h]
  __int64 v530; // [xsp+470h] [xbp-210h]
  CFErrorRef error; // [xsp+478h] [xbp-208h] BYREF
  __int64 v532; // [xsp+480h] [xbp-200h] BYREF
  __int64 *v533; // [xsp+488h] [xbp-1F8h]
  __int64 v534; // [xsp+490h] [xbp-1F0h]
  __int64 v535; // [xsp+498h] [xbp-1E8h]
  __int64 v536; // [xsp+4A0h] [xbp-1E0h] BYREF
  __int64 *v537; // [xsp+4A8h] [xbp-1D8h]
  __int64 v538; // [xsp+4B0h] [xbp-1D0h]
  __int64 v539; // [xsp+4B8h] [xbp-1C8h]
  __int64 v540; // [xsp+4C0h] [xbp-1C0h] BYREF
  __int64 *v541; // [xsp+4C8h] [xbp-1B8h]
  __int64 v542; // [xsp+4D0h] [xbp-1B0h]
  __int64 v543; // [xsp+4D8h] [xbp-1A8h]
  __int64 v544; // [xsp+4E0h] [xbp-1A0h] BYREF
  __int64 *v545; // [xsp+4E8h] [xbp-198h]
  __int64 v546; // [xsp+4F0h] [xbp-190h]
  __int64 v547; // [xsp+4F8h] [xbp-188h]
  __int64 v548; // [xsp+500h] [xbp-180h] BYREF
  id *v549; // [xsp+508h] [xbp-178h]
  __int64 v550; // [xsp+510h] [xbp-170h]
  __int64 (__fastcall *v551)(); // [xsp+518h] [xbp-168h]
  __int64 (__fastcall *v552)(); // [xsp+520h] [xbp-160h]
  id v553; // [xsp+528h] [xbp-158h]
  _QWORD v554[5]; // [xsp+530h] [xbp-150h] BYREF
  id v555; // [xsp+558h] [xbp-128h]
  uint8_t buf[4]; // [xsp+560h] [xbp-120h] BYREF
  void *v557; // [xsp+564h] [xbp-11Ch]
  __int16 v558; // [xsp+56Ch] [xbp-114h]
  id v559; // [xsp+56Eh] [xbp-112h]
  __int16 v560; // [xsp+576h] [xbp-10Ah]
  id v561; // [xsp+578h] [xbp-108h]
  __int16 v562; // [xsp+580h] [xbp-100h]
  void *v563; // [xsp+582h] [xbp-FEh]
  __int16 v564; // [xsp+58Ah] [xbp-F6h]
  id v565; // [xsp+58Ch] [xbp-F4h]
  __int16 v566; // [xsp+594h] [xbp-ECh]
  id v567; // [xsp+596h] [xbp-EAh]
  __int16 v568; // [xsp+59Eh] [xbp-E2h]
  __int64 v569; // [xsp+5A0h] [xbp-E0h]
  __int64 v570; // [xsp+5B0h] [xbp-D0h]
  _QWORD v571[3]; // [xsp+5B8h] [xbp-C8h] BYREF
  __int64 v572; // [xsp+5D0h] [xbp-B0h] BYREF
  __CFString *v573; // [xsp+5D8h] [xbp-A8h] BYREF
  void *v574; // [xsp+5E0h] [xbp-A0h] BYREF
  __CFString *v575; // [xsp+5E8h] [xbp-98h] BYREF
  void *v576; // [xsp+5F0h] [xbp-90h] BYREF

  v554[0] = 0;
  v554[1] = v554;
  v554[2] = 0x3032000000LL;
  v554[3] = __Block_byref_object_copy__4684;
  v554[4] = __Block_byref_object_dispose__4684;
  v555 = 0;
  v548 = 0;
  v549 = (id *)&amp;v548;
  v550 = 0x3032000000LL;
  v551 = __Block_byref_object_copy__4684;
  v552 = __Block_byref_object_dispose__4684;
  v553 = 0;
  v544 = 0;
  v545 = &amp;v544;
  v546 = 0x2020000000LL;
  v547 = 0;
  v540 = 0;
  v541 = &amp;v540;
  v542 = 0x2020000000LL;
  v543 = 0;
  v536 = 0;
  v537 = &amp;v536;
  v538 = 0x2020000000LL;
  v539 = 0;
  v532 = 0;
  v533 = &amp;v532;
  v534 = 0x2020000000LL;
  v535 = 0;
  error = 0;
  v527 = 0;
  v528 = &amp;v527;
  v529 = 0x2020000000LL;
  v530 = 0;
  v523 = 0;
  v524 = &amp;v523;
  v525 = 0x2020000000LL;
  v526 = 0;
  v522 = -1;
  v516 = 0;
  v517 = &amp;v516;
  v518 = 0x3032000000LL;
  v519 = __Block_byref_object_copy__4684;
  v520 = __Block_byref_object_dispose__4684;
  v521 = 0;
  v510 = 0;
  v511 = &amp;v510;
  v512 = 0x3032000000LL;
  v513 = __Block_byref_object_copy__4684;
  v514 = __Block_byref_object_dispose__4684;
  v515 = 0;
  v504 = 0;
  v505 = &amp;v504;
  v506 = 0x3032000000LL;
  v507 = __Block_byref_object_copy__4684;
  v508 = __Block_byref_object_dispose__4684;
  v509 = 0;
  v498 = 0;
  v499 = &amp;v498;
  v500 = 0x3032000000LL;
  v501 = __Block_byref_object_copy__4684;
  v502 = __Block_byref_object_dispose__4684;
  v503 = 0;
  v492 = 0;
  v493 = &amp;v492;
  v494 = 0x3032000000LL;
  v495 = __Block_byref_object_copy__4684;
  v496 = __Block_byref_object_dispose__4684;
  v497 = 0;
  v486 = 0;
  v487 = &amp;v486;
  v488 = 0x3032000000LL;
  v489 = __Block_byref_object_copy__4684;
  v490 = __Block_byref_object_dispose__4684;
  v491 = 0;
  v482 = 0;
  v483 = (double *)&amp;v482;
  v484 = 0x2020000000LL;
  v485 = 0;
  v481 = 0.0;
  v480 = 0;
  v476 = 0;
  v477 = &amp;v476;
  v478 = 0x2020000000LL;
  v479 = 0;
  v472 = 0;
  v473 = (double *)&amp;v472;
  v474 = 0x2020000000LL;
  v475 = 0;
  v5 = (NSObject *)objc_retain(*(id *)(a1 + 32));
  if ( !v5 )
  {
    global_queue_364 = j__dispatch_get_global_queue_364(0, 0);
    v7 = objc_claimAutoreleasedReturnValue_556(global_queue_364);
    objc_release(0);
    v5 = (NSObject *)v7;
  }
  if ( !*(_QWORD *)(a1 + 48) )
  {
    MobileActivationError_0 = createMobileActivationError_0(
                                (__int64)"DeviceIdentityIssueClientCertificateWithCompletion_block_invoke",
                                989,
                                -1,
                                0,
                                CFSTR("Invalid input."),
                                v2,
                                v3,
                                v4,
                                v384);
    v33 = objc_claimAutoreleasedReturnValue_556(MobileActivationError_0);
LABEL_20:
    v425 = 0;
LABEL_21:
    v35 = 0;
    v421 = 0;
    v422 = 0;
    v36 = 0;
    v37 = 0;
    v38 = 0;
    v39 = 0;
    v414 = 0;
    v417 = 0;
    v408 = 0;
    v409 = 0;
    v410 = 0;
    v411 = 0;
    v412 = 0;
    v413 = 0;
    v418 = 0;
    v419 = 0;
    cf = 0;
    v15 = 0;
LABEL_22:
    v406 = 0;
    v407 = 0;
LABEL_23:
    v40 = *(_QWORD *)(*(_QWORD *)(a1 + 56) + 8LL);
    v41 = *(void **)(v40 + 40);
    *(_QWORD *)(v40 + 40) = v33;
    goto LABEL_24;
  }
  v425 = objc_alloc_init((Class)off_41CEDE910);
  if ( !v425 )
  {
    v34 = createMobileActivationError_0(
            (__int64)"DeviceIdentityIssueClientCertificateWithCompletion_block_invoke",
            995,
            -1,
            0,
            CFSTR("Failed to allocate array."),
            v8,
            v9,
            v10,
            v384);
    v33 = objc_claimAutoreleasedReturnValue_556(v34);
    goto LABEL_20;
  }
  v11 = SecTaskCreateFromSelf_87(0);
  v15 = v11;
  if ( !v11 )
  {
    v55 = createMobileActivationError_0(
            (__int64)"DeviceIdentityIssueClientCertificateWithCompletion_block_invoke",
            1003,
            -1,
            0,
            CFSTR("Failed to create task."),
            v12,
            v13,
            v14,
            v384);
    v33 = objc_claimAutoreleasedReturnValue_556(v55);
    goto LABEL_21;
  }
  v422 = (__CFString *)SecTaskCopySigningIdentifier_25(v11, &amp;error);
  if ( !v422 )
  {
    v64 = createMobileActivationError_0(
            (__int64)"DeviceIdentityIssueClientCertificateWithCompletion_block_invoke",
            1009,
            -1,
            error,
            CFSTR("Failed to query code signing identifier."),
            v16,
            v17,
            v18,
            v384);
    v33 = objc_claimAutoreleasedReturnValue_556(v64);
    v35 = 0;
    v421 = 0;
    v422 = 0;
LABEL_47:
    v36 = 0;
    v37 = 0;
    v38 = 0;
    v39 = 0;
    v414 = 0;
    v417 = 0;
    v408 = 0;
    v409 = 0;
    v410 = 0;
    v411 = 0;
    v412 = 0;
    v413 = 0;
    v418 = 0;
    v419 = 0;
LABEL_48:
    cf = 0;
    goto LABEL_22;
  }
  v421 = (void *)SecTaskCopyValueForEntitlement_98(v15, CFSTR("com.apple.mobileactivationd.spi"), &amp;error);
  v19 = isNSNumber_0(v421);
  v20 = (void *)objc_claimAutoreleasedReturnValue_556(v19);
  if ( !v20 || (v21 = (unsigned __int8)objc_msgSend(v421, "boolValue"), objc_release(v20), (v21 &amp; 1) == 0) )
  {
    v56 = error;
    v575 = CFSTR("com.apple.mobileactivationd.spi");
    v576 = &amp;__kCFBooleanTrue;
    v41 = (void *)objc_claimAutoreleasedReturnValue_556(objc_msgSend(off_41CEDE908, "dictionaryWithObjects:forKeys:count:", &amp;v576, &amp;v575, 1));
    v60 = createMobileActivationError_0(
            (__int64)"DeviceIdentityIssueClientCertificateWithCompletion_block_invoke",
            1015,
            -7,
            v56,
            CFSTR("Missing required entitlement: %@"),
            v57,
            v58,
            v59,
            (char)v41);
    v61 = objc_claimAutoreleasedReturnValue_556(v60);
LABEL_45:
    v62 = *(_QWORD *)(*(_QWORD *)(a1 + 56) + 8LL);
    v63 = *(void **)(v62 + 40);
    *(_QWORD *)(v62 + 40) = v61;
    objc_release(v63);
    v35 = 0;
    v36 = 0;
    v37 = 0;
    v38 = 0;
    v39 = 0;
    v414 = 0;
    v417 = 0;
    v408 = 0;
    v409 = 0;
    v410 = 0;
    v411 = 0;
    v412 = 0;
    v413 = 0;
    v418 = 0;
    v419 = 0;
    cf = 0;
    v406 = 0;
    v407 = 0;
    goto LABEL_24;
  }
  v23 = (id *)(a1 + 40);
  v22 = *(void **)(a1 + 40);
  if ( !v22 )
  {
    queuea = 0;
    v395 = 0;
    v418 = 0;
    v419 = 0;
    v65 = 0;
    v399 = 0;
    v406 = 0;
    v407 = 0;
    v412 = 0;
    v413 = 0;
    v409 = 0;
    v410 = 0;
    v417 = 0;
    v411 = 0;
    v408 = 0;
    cf = 0;
    v397 = 1;
    goto LABEL_50;
  }
  v24 = (void *)objc_claimAutoreleasedReturnValue_556(objc_msgSend(v22, "objectForKeyedSubscript:", CFSTR("OIDSToInclude")));
  v25 = isNSArray();
  v26 = (void *)objc_claimAutoreleasedReturnValue_556(v25);
  objc_release(v26);
  objc_release(v24);
  if ( v26 )
  {
    v27 = (void *)objc_claimAutoreleasedReturnValue_556(objc_msgSend(*v23, "objectForKeyedSubscript:", CFSTR("OIDSToInclude")));
    if ( ((unsigned int)objc_msgSend(v27, "containsObject:", CFSTR("1.2.840.113635.100.10.1")) &amp; 1) == 0 )
    {
      v28 = (void *)objc_claimAutoreleasedReturnValue_556(objc_msgSend(*v23, "objectForKeyedSubscript:", CFSTR("OIDSToInclude")));
      if ( !(unsigned int)objc_msgSend(v28, "containsObject:", CFSTR("1.2.840.113635.100.8.1")) )
      {
        v120 = (void *)objc_claimAutoreleasedReturnValue_556(objc_msgSend(*v23, "objectForKeyedSubscript:", CFSTR("OIDSToInclude")));
        v121 = (unsigned __int8)objc_msgSend(v120, "containsObject:", CFSTR("1.2.840.113635.100.8.3"));
        objc_release(v120);
        objc_release(v28);
        objc_release(v27);
        if ( (v121 &amp; 1) == 0 )
          goto LABEL_71;
        goto LABEL_15;
      }
      objc_release(v28);
    }
    objc_release(v27);
LABEL_15:
    queue = (void *)SecTaskCopyValueForEntitlement_98(
                      v15,
                      CFSTR("com.apple.mobileactivationd.device-identifiers"),
                      &amp;error);
    objc_release(v421);
    v29 = isNSNumber_0(queue);
    v30 = (void *)objc_claimAutoreleasedReturnValue_556(v29);
    if ( !v30 || (v31 = (unsigned __int8)objc_msgSend(queue, "boolValue"), objc_release(v30), (v31 &amp; 1) == 0) )
    {
      v94 = error;
      v573 = CFSTR("com.apple.mobileactivationd.device-identifiers");
      v574 = &amp;__kCFBooleanTrue;
      v41 = (void *)objc_claimAutoreleasedReturnValue_556(objc_msgSend(off_41CEDE908, "dictionaryWithObjects:forKeys:count:", &amp;v574, &amp;v573, 1));
      v98 = createMobileActivationError_0(
              (__int64)"DeviceIdentityIssueClientCertificateWithCompletion_block_invoke",
              1028,
              -7,
              v94,
              CFSTR("Missing required entitlement: %@"),
              v95,
              v96,
              v97,
              (char)v41);
      v99 = objc_claimAutoreleasedReturnValue_556(v98);
      v100 = *(_QWORD *)(*(_QWORD *)(a1 + 56) + 8LL);
      v101 = *(void **)(v100 + 40);
      *(_QWORD *)(v100 + 40) = v99;
      objc_release(v101);
      v35 = 0;
      v36 = 0;
      v37 = 0;
      v38 = 0;
      v39 = 0;
      v414 = 0;
      v417 = 0;
      v408 = 0;
      v409 = 0;
      v410 = 0;
      v411 = 0;
      v412 = 0;
      v413 = 0;
      v418 = 0;
      v419 = 0;
      cf = 0;
      v406 = 0;
      v407 = 0;
      v421 = queue;
      goto LABEL_24;
    }
    v421 = queue;
LABEL_71:
    v122 = (void *)objc_claimAutoreleasedReturnValue_556(objc_msgSend(*v23, "objectForKeyedSubscript:", CFSTR("OIDSToInclude")));
    if ( ((unsigned int)objc_msgSend(v122, "containsObject:", CFSTR("1.2.840.113635.100.8.9.1")) &amp; 1) != 0 )
    {
LABEL_80:
      objc_release(v122);
LABEL_81:
      v41 = (void *)objc_claimAutoreleasedReturnValue_556(objc_msgSend(*v23, "objectForKeyedSubscript:", CFSTR("OIDSToInclude")));
      v130 = createMobileActivationError_0(
               (__int64)"DeviceIdentityIssueClientCertificateWithCompletion_block_invoke",
               1039,
               -2,
               0,
               CFSTR("This API does not support Enterprise Device Attestation OIDs: %@"),
               v127,
               v128,
               v129,
               (char)v41);
      v61 = objc_claimAutoreleasedReturnValue_556(v130);
      goto LABEL_45;
    }
    v123 = (void *)objc_claimAutoreleasedReturnValue_556(objc_msgSend(*v23, "objectForKeyedSubscript:", CFSTR("OIDSToInclude")));
    if ( ((unsigned int)objc_msgSend(v123, "containsObject:", CFSTR("1.2.840.113635.100.8.9.2")) &amp; 1) != 0 )
    {
LABEL_79:
      objc_release(v123);
      goto LABEL_80;
    }
    v124 = (void *)objc_claimAutoreleasedReturnValue_556(objc_msgSend(*v23, "objectForKeyedSubscript:", CFSTR("OIDSToInclude")));
    if ( ((unsigned int)objc_msgSend(v124, "containsObject:", CFSTR("1.2.840.113635.100.8.10.1")) &amp; 1) != 0 )
    {
LABEL_78:
      objc_release(v124);
      goto LABEL_79;
    }
    v125 = (void *)objc_claimAutoreleasedReturnValue_556(objc_msgSend(*v23, "objectForKeyedSubscript:", CFSTR("OIDSToInclude")));
    if ( ((unsigned int)objc_msgSend(v125, "containsObject:", CFSTR("1.2.840.113635.100.8.10.2")) &amp; 1) != 0 )
    {
LABEL_77:
      objc_release(v125);
      goto LABEL_78;
    }
    v126 = (void *)objc_claimAutoreleasedReturnValue_556(objc_msgSend(*v23, "objectForKeyedSubscript:", CFSTR("OIDSToInclude")));
    if ( ((unsigned int)objc_msgSend(v126, "containsObject:", CFSTR("1.2.840.113635.100.8.10.3")) &amp; 1) != 0 )
    {
      objc_release(v126);
      goto LABEL_77;
    }
    cfa = (void *)objc_claimAutoreleasedReturnValue_556(objc_msgSend(*v23, "objectForKeyedSubscript:", CFSTR("OIDSToInclude")));
    v420 = (unsigned __int8)objc_msgSend(cfa, "containsObject:", CFSTR("1.2.840.113635.100.8.11.1"));
    objc_release(cfa);
    objc_release(v126);
    objc_release(v125);
    objc_release(v124);
    objc_release(v123);
    objc_release(v122);
    if ( (v420 &amp; 1) != 0 )
      goto LABEL_81;
    v168 = (void *)objc_claimAutoreleasedReturnValue_556(objc_msgSend(*v23, "objectForKeyedSubscript:", CFSTR("OIDSToInclude")));
    if ( ((unsigned int)objc_msgSend(v168, "containsObject:", CFSTR("1.2.840.113635.100.8.6")) &amp; 1) != 0 )
    {
      v169 = (void *)objc_claimAutoreleasedReturnValue_556(objc_msgSend(*v23, "objectForKeyedSubscript:", CFSTR("AccessControls")));
      objc_release(v169);
      objc_release(v168);
      if ( !v169 )
      {
        v173 = createMobileActivationError_0(
                 (__int64)"DeviceIdentityIssueClientCertificateWithCompletion_block_invoke",
                 1044,
                 -2,
                 0,
                 CFSTR("Missing required option: %@"),
                 v170,
                 v171,
                 v172,
                 (char)CFSTR("AccessControls"));
        v33 = objc_claimAutoreleasedReturnValue_556(v173);
LABEL_139:
        v35 = 0;
        goto LABEL_47;
      }
    }
    else
    {
      objc_release(v168);
    }
  }
  v200 = (void *)objc_claimAutoreleasedReturnValue_556(objc_msgSend(*v23, "objectForKeyedSubscript:", CFSTR("KeychainLabel")));
  v201 = isNSString_0();
  v202 = (void *)objc_claimAutoreleasedReturnValue_556(v201);
  objc_release(v202);
  objc_release(v200);
  if ( v202 )
  {
    if ( (isRunningInRootLaunchdContext() &amp; 1) != 0
      || (isRunningInRecovery() &amp; 1) != 0
      || (unsigned int)isRunningInDiagnosticsMode() )
    {
      v206 = createMobileActivationError_0(
               (__int64)"DeviceIdentityIssueClientCertificateWithCompletion_block_invoke",
               1058,
               -2,
               0,
               CFSTR("Option (%@) not allowed for processes running in the system context or diagnostics mode."),
               v203,
               v204,
               v205,
               (char)CFSTR("KeychainLabel"));
      v33 = objc_claimAutoreleasedReturnValue_556(v206);
      goto LABEL_139;
    }
    v222 = (void *)objc_claimAutoreleasedReturnValue_556(objc_msgSend(*v23, "objectForKeyedSubscript:", CFSTR("ClientAttestationData")));
    v223 = isNSData_0();
    v224 = (void *)objc_claimAutoreleasedReturnValue_556(v223);
    objc_release(v224);
    objc_release(v222);
    if ( v224 )
    {
      v228 = createMobileActivationError_0(
               (__int64)"DeviceIdentityIssueClientCertificateWithCompletion_block_invoke",
               1063,
               -2,
               0,
               CFSTR("Keychain (%@) not supported with %@."),
               v225,
               v226,
               v227,
               (char)CFSTR("KeychainLabel"));
      v33 = objc_claimAutoreleasedReturnValue_556(v228);
      goto LABEL_139;
    }
    v418 = (id)objc_claimAutoreleasedReturnValue_556(objc_msgSend(*v23, "objectForKeyedSubscript:", CFSTR("KeychainLabel")));
    v417 = (id)objc_claimAutoreleasedReturnValue_556(objc_msgSend(off_41CEDE948, "stringWithFormat:", CFSTR("%@-rk"), v418));
    v412 = (void *)objc_claimAutoreleasedReturnValue_556(objc_msgSend(off_41CEDE948, "stringWithFormat:", CFSTR("%@-leaf"), v418));
    v409 = (void *)objc_claimAutoreleasedReturnValue_556(objc_msgSend(off_41CEDE948, "stringWithFormat:", CFSTR("%@-intermediate"), v418));
    v413 = (id)objc_claimAutoreleasedReturnValue_556(objc_msgSend(off_41CEDE948, "stringWithFormat:", CFSTR("%@-combined"), v418));
    v411 = (id)objc_claimAutoreleasedReturnValue_556(objc_msgSend(off_41CEDE948, "stringWithFormat:", CFSTR("%@-monotonic-clock"), v418));
    v408 = (id)objc_claimAutoreleasedReturnValue_556(objc_msgSend(off_41CEDE948, "stringWithFormat:", CFSTR("%@-server-timestamp"), v418));
    v384 = (char)v418;
    v410 = (id)objc_claimAutoreleasedReturnValue_556(objc_msgSend(off_41CEDE948, "stringWithFormat:", CFSTR("%@-rtc-reset-count")));
  }
  else
  {
    v417 = 0;
    v418 = 0;
    v412 = 0;
    v413 = 0;
    v409 = 0;
    v410 = 0;
    v411 = 0;
    v408 = 0;
  }
  v207 = (void *)objc_claimAutoreleasedReturnValue_556(objc_msgSend(*v23, "objectForKeyedSubscript:", CFSTR("KeychainAccessGroup")));
  v208 = isNSString_0();
  v209 = (void *)objc_claimAutoreleasedReturnValue_556(v208);
  objc_release(v209);
  objc_release(v207);
  if ( v209 )
    v210 = objc_claimAutoreleasedReturnValue_556(objc_msgSend(*v23, "objectForKeyedSubscript:", CFSTR("KeychainAccessGroup")));
  else
    v210 = 0;
  if ( v418 &amp;&amp; !v210 )
  {
    v211 = objc_retain(v422);
    v210 = (__int64)v422;
  }
  v419 = (id)v210;
  v212 = (void *)objc_claimAutoreleasedReturnValue_556(objc_msgSend(*v23, "objectForKeyedSubscript:", CFSTR("IgnoreExistingKeychainItems")));
  v213 = isNSNumber_0(v212);
  v214 = (void *)objc_claimAutoreleasedReturnValue_556(v213);
  objc_release(v214);
  objc_release(v212);
  if ( v214 )
  {
    v215 = (void *)objc_claimAutoreleasedReturnValue_556(objc_msgSend(*v23, "objectForKeyedSubscript:", CFSTR("IgnoreExistingKeychainItems")));
    v216 = (unsigned int)objc_msgSend(v215, "boolValue");
    objc_release(v215);
  }
  else
  {
    v216 = 0;
  }
  v217 = (void *)objc_claimAutoreleasedReturnValue_556(objc_msgSend(*v23, "objectForKeyedSubscript:", CFSTR("AccessControls")));
  objc_release(v217);
  if ( !v217 )
    goto LABEL_163;
  if ( (isRunningInRootLaunchdContext() &amp; 1) != 0
    || (isRunningInRecovery() &amp; 1) != 0
    || (unsigned int)isRunningInDiagnosticsMode() )
  {
    v221 = createMobileActivationError_0(
             (__int64)"DeviceIdentityIssueClientCertificateWithCompletion_block_invoke",
             1092,
             -2,
             0,
             CFSTR("Option (%@) not allowed for processes running in the system context or diagnostics mode."),
             v218,
             v219,
             v220,
             (char)CFSTR("AccessControls"));
    v33 = objc_claimAutoreleasedReturnValue_556(v221);
LABEL_155:
    v35 = 0;
    v36 = 0;
    v37 = 0;
    v38 = 0;
    v39 = 0;
    v414 = 0;
    goto LABEL_48;
  }
  v229 = (void *)objc_claimAutoreleasedReturnValue_556(objc_msgSend(*v23, "objectForKeyedSubscript:", CFSTR("ClientAttestationData")));
  v230 = isNSData_0();
  v231 = (void *)objc_claimAutoreleasedReturnValue_556(v230);
  objc_release(v231);
  objc_release(v229);
  if ( v231 )
  {
    v235 = createMobileActivationError_0(
             (__int64)"DeviceIdentityIssueClientCertificateWithCompletion_block_invoke",
             1097,
             -2,
             0,
             CFSTR("ACLs (%@) not supported with %@."),
             v232,
             v233,
             v234,
             (char)CFSTR("AccessControls"));
    v33 = objc_claimAutoreleasedReturnValue_556(v235);
    goto LABEL_155;
  }
  v236 = (void *)objc_claimAutoreleasedReturnValue_556(objc_msgSend(*v23, "objectForKeyedSubscript:", CFSTR("AccessControls")));
  v237 = CFGetTypeID_254(v236);
  TypeID_4 = SecAccessControlGetTypeID_4();
  objc_release(v236);
  if ( v237 == TypeID_4 )
  {
    cf = (void *)objc_claimAutoreleasedReturnValue_556(objc_msgSend(*v23, "objectForKeyedSubscript:", CFSTR("AccessControls")));
    objc_release(cf);
    CFRetain_262(cf);
  }
  else
  {
LABEL_163:
    cf = 0;
  }
  v239 = (void *)objc_claimAutoreleasedReturnValue_556(objc_msgSend(*v23, "objectForKeyedSubscript:", CFSTR("reuseExistingKey")));
  v240 = isNSNumber_0(v239);
  v241 = (void *)objc_claimAutoreleasedReturnValue_556(v240);
  objc_release(v241);
  objc_release(v239);
  if ( v241 )
  {
    v242 = (void *)objc_claimAutoreleasedReturnValue_556(objc_msgSend(*v23, "objectForKeyedSubscript:", CFSTR("reuseExistingKey")));
    v415 = (unsigned int)objc_msgSend(v242, "boolValue");
    objc_release(v242);
  }
  else
  {
    v415 = 0;
  }
  v243 = (void *)objc_claimAutoreleasedReturnValue_556(objc_msgSend(*v23, "objectForKeyedSubscript:", CFSTR("UseSoftwareGeneratedKey")));
  v244 = isNSNumber_0(v243);
  v245 = (void *)objc_claimAutoreleasedReturnValue_556(v244);
  objc_release(v245);
  objc_release(v243);
  if ( v245 )
  {
    v246 = (void *)objc_claimAutoreleasedReturnValue_556(objc_msgSend(*v23, "objectForKeyedSubscript:", CFSTR("UseSoftwareGeneratedKey")));
    queueb = (unsigned int)objc_msgSend(v246, "boolValue");
    objc_release(v246);
  }
  else
  {
    queueb = 1;
  }
  v247 = (void *)objc_claimAutoreleasedReturnValue_556(objc_msgSend(*v23, "objectForKeyedSubscript:", CFSTR("scrtAttestation")));
  v248 = isNSNumber_0(v247);
  v249 = (void *)objc_claimAutoreleasedReturnValue_556(v248);
  objc_release(v249);
  objc_release(v247);
  if ( v249 )
  {
    v250 = (void *)objc_claimAutoreleasedReturnValue_556(objc_msgSend(*v23, "objectForKeyedSubscript:", CFSTR("scrtAttestation")));
    v392 = (unsigned int)objc_msgSend(v250, "boolValue");
    objc_release(v250);
  }
  else
  {
    v392 = 0;
  }
  v251 = (void *)objc_claimAutoreleasedReturnValue_556(objc_msgSend(*v23, "objectForKeyedSubscript:", CFSTR("ClientAttestationData")));
  v252 = isNSData_0();
  v253 = (void *)objc_claimAutoreleasedReturnValue_556(v252);
  objc_release(v253);
  objc_release(v251);
  if ( v253 )
  {
    v254 = (void *)objc_claimAutoreleasedReturnValue_556(objc_msgSend(*v23, "objectForKeyedSubscript:", CFSTR("ClientAttestationPublicKey")));
    v255 = isNSData_0();
    v256 = (void *)objc_claimAutoreleasedReturnValue_556(v255);
    objc_release(v256);
    objc_release(v254);
    if ( !v256 )
    {
      v279 = createMobileActivationError_0(
               (__int64)"DeviceIdentityIssueClientCertificateWithCompletion_block_invoke",
               1121,
               -2,
               0,
               CFSTR("Missing required option for %@."),
               v257,
               v258,
               v259,
               (char)CFSTR("ClientAttestationPublicKey"));
      v33 = objc_claimAutoreleasedReturnValue_556(v279);
      v35 = 0;
      v36 = 0;
      v37 = 0;
      v38 = 0;
      v39 = 0;
      v414 = 0;
      goto LABEL_22;
    }
    v407 = (void *)objc_claimAutoreleasedReturnValue_556(objc_msgSend(*v23, "objectForKeyedSubscript:", CFSTR("ClientAttestationData")));
    v406 = (id)objc_claimAutoreleasedReturnValue_556(objc_msgSend(*v23, "objectForKeyedSubscript:", CFSTR("ClientAttestationPublicKey")));
  }
  else
  {
    v406 = 0;
    v407 = 0;
  }
  v260 = (void *)objc_claimAutoreleasedReturnValue_556(objc_msgSend(*v23, "objectForKeyedSubscript:", CFSTR("ReturnReferenceDate")));
  v261 = isNSNumber_0(v260);
  v262 = (void *)objc_claimAutoreleasedReturnValue_556(v261);
  objc_release(v262);
  objc_release(v260);
  if ( v262 )
  {
    v263 = (void *)objc_claimAutoreleasedReturnValue_556(objc_msgSend(*v23, "objectForKeyedSubscript:", CFSTR("ReturnReferenceDate")));
    v264 = (unsigned int)objc_msgSend(v263, "boolValue");
    objc_release(v263);
  }
  else
  {
    v264 = 0;
  }
  v265 = (void *)objc_claimAutoreleasedReturnValue_556(objc_msgSend(*v23, "objectForKeyedSubscript:", CFSTR("SkipNetworkRequest")));
  v266 = isNSNumber_0(v265);
  v267 = (void *)objc_claimAutoreleasedReturnValue_556(v266);
  objc_release(v267);
  objc_release(v265);
  if ( v267 )
  {
    v268 = (void *)objc_claimAutoreleasedReturnValue_556(objc_msgSend(*v23, "objectForKeyedSubscript:", CFSTR("SkipNetworkRequest")));
    v269 = (unsigned int)objc_msgSend(v268, "boolValue");
    objc_release(v268);
  }
  else
  {
    v269 = 0;
  }
  v270 = (void *)objc_claimAutoreleasedReturnValue_556(objc_msgSend(*v23, "objectForKeyedSubscript:", CFSTR("DeleteExistingKeysAndCerts")));
  v271 = isNSNumber_0(v270);
  v272 = (void *)objc_claimAutoreleasedReturnValue_556(v271);
  objc_release(v272);
  objc_release(v270);
  v399 = v415 != 0;
  v65 = v216 != 0;
  v397 = queueb != 0;
  v395 = v264 != 0;
  queuea = v269 != 0;
  if ( v272 )
  {
    v273 = (void *)objc_claimAutoreleasedReturnValue_556(objc_msgSend(*v23, "objectForKeyedSubscript:", CFSTR("DeleteExistingKeysAndCerts")));
    v416 = (unsigned int)objc_msgSend(v273, "boolValue");
    objc_release(v273);
    if ( !v417 )
    {
      v417 = 0;
      if ( !v416 )
        goto LABEL_199;
LABEL_194:
      v35 = 0;
      if ( v418 &amp;&amp; v419 )
      {
        delete_keychain_data(v419, v411, 0);
        delete_keychain_data(v419, v410, 0);
        delete_keychain_data(v419, v408, 0);
        delete_keychain_data(v419, v413, 0);
        delete_keychain_item(v419, v417, 0);
        delete_certificate(v419, v412, 0);
        delete_certificate(v419, v409, 0);
        v35 = 0;
        v36 = 0;
        v37 = 0;
        v38 = 0;
        v39 = 0;
        v414 = 0;
      }
      else
      {
        v36 = 0;
        v37 = 0;
        v38 = 0;
        v39 = 0;
        v414 = 0;
      }
      goto LABEL_25;
    }
  }
  else
  {
    if ( !v417 )
    {
      v417 = 0;
      goto LABEL_199;
    }
    LOBYTE(v416) = 0;
  }
  if ( v392 )
  {
    v393 = objc_claimAutoreleasedReturnValue_556(objc_msgSend(v417, "stringByAppendingString:", CFSTR("-scrt")));
    objc_release(v417);
    v389 = objc_claimAutoreleasedReturnValue_556(objc_msgSend(v412, "stringByAppendingString:", CFSTR("-scrt")));
    objc_release(v412);
    v274 = objc_claimAutoreleasedReturnValue_556(objc_msgSend(v409, "stringByAppendingString:", CFSTR("-scrt")));
    objc_release(v409);
    v275 = objc_claimAutoreleasedReturnValue_556(objc_msgSend(v413, "stringByAppendingString:", CFSTR("-scrt")));
    objc_release(v413);
    v276 = objc_claimAutoreleasedReturnValue_556(objc_msgSend(v411, "stringByAppendingString:", CFSTR("-scrt")));
    objc_release(v411);
    v277 = objc_claimAutoreleasedReturnValue_556(objc_msgSend(v408, "stringByAppendingString:", CFSTR("-scrt")));
    objc_release(v408);
    v278 = objc_claimAutoreleasedReturnValue_556(objc_msgSend(v410, "stringByAppendingString:", CFSTR("-scrt")));
  }
  else
  {
    v393 = objc_claimAutoreleasedReturnValue_556(objc_msgSend(v417, "stringByAppendingString:", CFSTR("-ucrt")));
    objc_release(v417);
    v389 = objc_claimAutoreleasedReturnValue_556(objc_msgSend(v412, "stringByAppendingString:", CFSTR("-ucrt")));
    objc_release(v412);
    v274 = objc_claimAutoreleasedReturnValue_556(objc_msgSend(v409, "stringByAppendingString:", CFSTR("-ucrt")));
    objc_release(v409);
    v275 = objc_claimAutoreleasedReturnValue_556(objc_msgSend(v413, "stringByAppendingString:", CFSTR("-ucrt")));
    objc_release(v413);
    v276 = objc_claimAutoreleasedReturnValue_556(objc_msgSend(v411, "stringByAppendingString:", CFSTR("-ucrt")));
    objc_release(v411);
    v277 = objc_claimAutoreleasedReturnValue_556(objc_msgSend(v408, "stringByAppendingString:", CFSTR("-ucrt")));
    objc_release(v408);
    v278 = objc_claimAutoreleasedReturnValue_556(objc_msgSend(v410, "stringByAppendingString:", CFSTR("-ucrt")));
  }
  v280 = (void *)v278;
  objc_release(v410);
  v408 = (id)v277;
  v409 = (void *)v274;
  v410 = v280;
  v411 = (id)v276;
  v417 = (id)v393;
  v412 = (void *)v389;
  v413 = (id)v275;
  if ( (v416 &amp; 1) != 0 )
    goto LABEL_194;
LABEL_199:
  if ( v407 )
  {
    if ( cf )
      CFRelease_438(cf);
    v281 = SecAccessControlCreate_3(0, &amp;error);
    cf = (void *)v281;
    if ( !v281 )
    {
      v289 = createMobileActivationError_0(
               (__int64)"DeviceIdentityIssueClientCertificateWithCompletion_block_invoke",
               1195,
               -1,
               error,
               CFSTR("Failed to create access control."),
               v282,
               v283,
               v284,
               v384);
      v33 = objc_claimAutoreleasedReturnValue_556(v289);
      v35 = 0;
      v36 = 0;
      v37 = 0;
      v38 = 0;
      v39 = 0;
      v414 = 0;
      cf = 0;
      goto LABEL_23;
    }
    if ( (SecAccessControlSetProtection_3(v281, CFSTR("f"), &amp;error) &amp; 1) == 0 )
    {
      v288 = createMobileActivationError_0(
               (__int64)"DeviceIdentityIssueClientCertificateWithCompletion_block_invoke",
               1200,
               -1,
               error,
               CFSTR("Failed to set ACL protection to %@."),
               v285,
               v286,
               v287,
               (char)CFSTR("f"));
      v33 = objc_claimAutoreleasedReturnValue_556(v288);
      goto LABEL_86;
    }
  }
  else
  {
    v407 = 0;
  }
LABEL_50:
  if ( (isRunningInRecovery() &amp; 1) != 0 )
  {
    v69 = 0;
  }
  else
  {
    v102 = *(_QWORD *)(*(_QWORD *)(a1 + 56) + 8LL);
    v471 = *(id *)(v102 + 40);
    v69 = isAutomaticTimeEnabledWithError(&amp;v471);
    j__objc_storeStrong_550((id *)(v102 + 40), v471);
    v106 = *(_QWORD *)(*(_QWORD *)(a1 + 56) + 8LL);
    v109 = *(void **)(v106 + 40);
    v108 = (id *)(v106 + 40);
    v107 = v109;
    if ( (v69 &amp; 1) == 0 &amp;&amp; v107 )
    {
      v110 = createMobileActivationError_0(
               (__int64)"DeviceIdentityIssueClientCertificateWithCompletion_block_invoke",
               1212,
               -1,
               v107,
               CFSTR("Failed to query automatic time state."),
               v103,
               v104,
               v105,
               v384);
      v33 = objc_claimAutoreleasedReturnValue_556(v110);
      goto LABEL_86;
    }
    v470 = v107;
    v111 = copyMonotonicClock(&amp;v470);
    j__objc_storeStrong_550(v108, v470);
    v483[3] = v111;
    v115 = *(_QWORD *)(*(_QWORD *)(a1 + 56) + 8LL);
    v118 = *(void **)(v115 + 40);
    v117 = (id *)(v115 + 40);
    v116 = v118;
    if ( v111 == 0.0 )
    {
      v119 = createMobileActivationError_0(
               (__int64)"DeviceIdentityIssueClientCertificateWithCompletion_block_invoke",
               1218,
               -1,
               v116,
               CFSTR("Failed to query monotonic clock."),
               v112,
               v113,
               v114,
               v384);
      v33 = objc_claimAutoreleasedReturnValue_556(v119);
      goto LABEL_86;
    }
    v469 = v116;
    v131 = copyRTCResetCountWithError(&amp;v469);
    j__objc_storeStrong_550(v117, v469);
    *((_DWORD *)v477 + 6) = v131;
    if ( !v131 )
    {
      v132 = *(void **)(*(_QWORD *)(*(_QWORD *)(a1 + 56) + 8LL) + 40LL);
      if ( v132 )
      {
        v133 = createMobileActivationError_0(
                 (__int64)"DeviceIdentityIssueClientCertificateWithCompletion_block_invoke",
                 1224,
                 -1,
                 v132,
                 CFSTR("Failed to query RTC reset count."),
                 v66,
                 v67,
                 v68,
                 v384);
        v33 = objc_claimAutoreleasedReturnValue_556(v133);
        goto LABEL_86;
      }
    }
  }
  v35 = 0;
  v414 = 0;
  v39 = 0;
  v38 = 0;
  if ( v65 || !v418 )
    goto LABEL_248;
  v70 = copy_keychain_item(v419, v417, *v23, &amp;v522, 0);
  v533[3] = v70;
  if ( v522 != -25300 &amp;&amp; v522 )
  {
    v134 = createMobileActivationError_0(
             (__int64)"DeviceIdentityIssueClientCertificateWithCompletion_block_invoke",
             1234,
             -1,
             *(void **)(*(_QWORD *)(*(_QWORD *)(a1 + 56) + 8LL) + 40LL),
             CFSTR("Failed to query existing reference key (%@/%@): %d"),
             v71,
             v72,
             v73,
             (char)v419);
    v33 = objc_claimAutoreleasedReturnValue_556(v134);
    goto LABEL_86;
  }
  v74 = copy_keychain_data(v419, v413, &amp;v522, 0);
  v414 = (CFDataRef)objc_claimAutoreleasedReturnValue_556(v74);
  if ( v522 != -25300 &amp;&amp; v522 )
  {
    v135 = createMobileActivationError_0(
             (__int64)"DeviceIdentityIssueClientCertificateWithCompletion_block_invoke",
             1240,
             -1,
             *(void **)(*(_QWORD *)(*(_QWORD *)(a1 + 56) + 8LL) + 40LL),
             CFSTR("Failed to query existing leaf/intermediate certificates (%@/%@): %d"),
             v75,
             v76,
             v77,
             (char)v419);
    v33 = objc_claimAutoreleasedReturnValue_556(v135);
    goto LABEL_88;
  }
  if ( !v414 )
  {
    load_certificate(v528 + 3, v419, v412, &amp;v522, 0);
    if ( v522 != -25300 &amp;&amp; v522 )
    {
      v161 = createMobileActivationError_0(
               (__int64)"DeviceIdentityIssueClientCertificateWithCompletion_block_invoke",
               1272,
               -1,
               *(void **)(*(_QWORD *)(*(_QWORD *)(a1 + 56) + 8LL) + 40LL),
               CFSTR("Failed to query existing leaf certificate (%@/%@): %d"),
               v137,
               v138,
               v139,
               (char)v419);
      v33 = objc_claimAutoreleasedReturnValue_556(v161);
    }
    else
    {
      load_certificate(v524 + 3, v419, v409, &amp;v522, 0);
      if ( v522 != -25300 &amp;&amp; v522 )
      {
        v196 = createMobileActivationError_0(
                 (__int64)"DeviceIdentityIssueClientCertificateWithCompletion_block_invoke",
                 1278,
                 -1,
                 *(void **)(*(_QWORD *)(*(_QWORD *)(a1 + 56) + 8LL) + 40LL),
                 CFSTR("Failed to query existing intermediate certificate (%@/%@): %d"),
                 v140,
                 v141,
                 v142,
                 (char)v419);
        v33 = objc_claimAutoreleasedReturnValue_556(v196);
      }
      else
      {
        if ( !v528[3] || !v524[3] )
        {
          v414 = 0;
          goto LABEL_104;
        }
        v143 = objc_alloc_init((Class)off_41CEDE9A8);
        v144 = (void *)v517[5];
        v517[5] = (__int64)v143;
        objc_release(v144);
        if ( v517[5] )
        {
          v151 = SecCertificateCopyData_23((SecCertificateRef)v528[3]);
          if ( !v151 )
          {
            v377 = createMobileActivationError_0(
                     (__int64)"DeviceIdentityIssueClientCertificateWithCompletion_block_invoke",
                     1291,
                     -1,
                     0,
                     CFSTR("Failed to copy certificate data."),
                     v148,
                     v149,
                     v150,
                     v384);
            v33 = objc_claimAutoreleasedReturnValue_556(v377);
            goto LABEL_86;
          }
          objc_msgSend((id)v517[5], "appendData:", v151);
          v414 = SecCertificateCopyData_23((SecCertificateRef)v524[3]);
          objc_release(v151);
          if ( !v414 )
          {
            v379 = createMobileActivationError_0(
                     (__int64)"DeviceIdentityIssueClientCertificateWithCompletion_block_invoke",
                     1299,
                     -1,
                     0,
                     CFSTR("Failed to copy certificate data."),
                     v152,
                     v153,
                     v154,
                     v384);
            v33 = objc_claimAutoreleasedReturnValue_556(v379);
            goto LABEL_86;
          }
          objc_msgSend((id)v517[5], "appendData:", v414);
          v155 = v517[5];
          v156 = *(_QWORD *)(*(_QWORD *)(a1 + 56) + 8LL);
          v467 = *(id *)(v156 + 40);
          v157 = store_keychain_data(v155, v419, v413, &amp;v467);
          j__objc_storeStrong_550((id *)(v156 + 40), v467);
          if ( (v157 &amp; 1) != 0 )
          {
            delete_certificate(v419, v412, 0);
            delete_certificate(v419, v409, 0);
LABEL_104:
            v39 = 0;
            goto LABEL_105;
          }
          v380 = createMobileActivationError_0(
                   (__int64)"DeviceIdentityIssueClientCertificateWithCompletion_block_invoke",
                   1306,
                   -1,
                   *(void **)(*(_QWORD *)(*(_QWORD *)(a1 + 56) + 8LL) + 40LL),
                   CFSTR("Failed to store leaf/intermediate certificates."),
                   v158,
                   v159,
                   v160,
                   v384);
          v33 = objc_claimAutoreleasedReturnValue_556(v380);
LABEL_88:
          v35 = 0;
          v36 = 0;
          v37 = 0;
          v38 = 0;
          v39 = 0;
          goto LABEL_23;
        }
        v199 = createMobileActivationError_0(
                 (__int64)"DeviceIdentityIssueClientCertificateWithCompletion_block_invoke",
                 1285,
                 -1,
                 0,
                 CFSTR("Failed to allocate data."),
                 v145,
                 v146,
                 v147,
                 v384);
        v33 = objc_claimAutoreleasedReturnValue_556(v199);
      }
    }
LABEL_86:
    v35 = 0;
    v36 = 0;
    v37 = 0;
    v38 = 0;
    v39 = 0;
    v414 = 0;
    goto LABEL_23;
  }
  v78 = *(_QWORD *)(*(_QWORD *)(a1 + 56) + 8LL);
  obj = *(id *)(v78 + 40);
  v79 = parseDERCertificatesFromChain_0(v414, &amp;obj);
  v39 = (void *)objc_claimAutoreleasedReturnValue_556(v79);
  j__objc_storeStrong_550((id *)(v78 + 40), obj);
  if ( !v39 || objc_msgSend(v39, "count") != (void *)2 )
  {
    v136 = createMobileActivationError_0(
             (__int64)"DeviceIdentityIssueClientCertificateWithCompletion_block_invoke",
             1251,
             -1,
             *(void **)(*(_QWORD *)(*(_QWORD *)(a1 + 56) + 8LL) + 40LL),
             CFSTR("Failed to parse DER certificate chain."),
             v80,
             v81,
             v82,
             v384);
    v33 = objc_claimAutoreleasedReturnValue_556(v136);
    goto LABEL_90;
  }
  v83 = (const __CFData *)objc_claimAutoreleasedReturnValue_556(objc_msgSend(v39, "objectAtIndexedSubscript:", 0));
  v84 = SecCertificateCreateWithData_33(0, v83);
  v528[3] = (__int64)v84;
  objc_release(v83);
  if ( !v528[3] )
  {
    v197 = createMobileActivationError_0(
             (__int64)"DeviceIdentityIssueClientCertificateWithCompletion_block_invoke",
             1257,
             -1,
             0,
             CFSTR("Failed to create certificate from DER data."),
             v85,
             v86,
             v87,
             v384);
    v33 = objc_claimAutoreleasedReturnValue_556(v197);
    goto LABEL_90;
  }
  v88 = (const __CFData *)objc_claimAutoreleasedReturnValue_556(objc_msgSend(v39, "objectAtIndexedSubscript:", 1));
  v89 = SecCertificateCreateWithData_33(0, v88);
  v524[3] = (__int64)v89;
  objc_release(v88);
  if ( !v524[3] )
  {
    v93 = createMobileActivationError_0(
            (__int64)"DeviceIdentityIssueClientCertificateWithCompletion_block_invoke",
            1263,
            -1,
            0,
            CFSTR("Failed to create certificate from DER data."),
            v90,
            v91,
            v92,
            v384);
    v33 = objc_claimAutoreleasedReturnValue_556(v93);
LABEL_90:
    v35 = 0;
    v36 = 0;
    v37 = 0;
    v38 = 0;
    goto LABEL_23;
  }
LABEL_105:
  if ( (isRunningInRecovery() &amp; 1) == 0 )
  {
    v174 = copy_keychain_data(v419, v411, &amp;v522, 0);
    v175 = objc_claimAutoreleasedReturnValue_556(v174);
    v176 = (void *)v487[5];
    v487[5] = v175;
    objc_release(v176);
    if ( v522 != -25300 &amp;&amp; v522 )
    {
      v198 = createMobileActivationError_0(
               (__int64)"DeviceIdentityIssueClientCertificateWithCompletion_block_invoke",
               1318,
               -1,
               *(void **)(*(_QWORD *)(*(_QWORD *)(a1 + 56) + 8LL) + 40LL),
               CFSTR("Failed to query existing motononic time (%@/%@): %d"),
               v177,
               v178,
               v179,
               (char)v419);
      v33 = objc_claimAutoreleasedReturnValue_556(v198);
      goto LABEL_90;
    }
    v180 = (void *)v487[5];
    if ( v180 )
      objc_msgSend(v180, "getBytes:length:", &amp;v481, 8);
    v181 = copy_keychain_data(v419, v410, &amp;v522, 0);
    v182 = objc_claimAutoreleasedReturnValue_556(v181);
    v183 = (void *)v511[5];
    v511[5] = v182;
    objc_release(v183);
    if ( v522 != -25300 &amp;&amp; v522 )
    {
      v290 = createMobileActivationError_0(
               (__int64)"DeviceIdentityIssueClientCertificateWithCompletion_block_invoke",
               1328,
               -1,
               *(void **)(*(_QWORD *)(*(_QWORD *)(a1 + 56) + 8LL) + 40LL),
               CFSTR("Failed to query existing RTC reset count (%@/%@): %d"),
               v184,
               v185,
               v186,
               (char)v419);
      v33 = objc_claimAutoreleasedReturnValue_556(v290);
      goto LABEL_90;
    }
    v187 = (void *)v511[5];
    if ( v187 )
      objc_msgSend(v187, "getBytes:length:", &amp;v480, 4);
    v188 = copy_keychain_data(v419, v408, &amp;v522, 0);
    v189 = objc_claimAutoreleasedReturnValue_556(v188);
    v190 = (void *)v505[5];
    v505[5] = v189;
    objc_release(v190);
    if ( v522 != -25300 &amp;&amp; v522 )
    {
      v378 = createMobileActivationError_0(
               (__int64)"DeviceIdentityIssueClientCertificateWithCompletion_block_invoke",
               1338,
               -1,
               *(void **)(*(_QWORD *)(*(_QWORD *)(a1 + 56) + 8LL) + 40LL),
               CFSTR("Failed to query existing server timestamp (%@/%@): %d"),
               v66,
               v67,
               v68,
               (char)v419);
      v33 = objc_claimAutoreleasedReturnValue_556(v378);
      goto LABEL_90;
    }
    v191 = (void *)v505[5];
    if ( v191 )
    {
      objc_msgSend(v191, "getBytes:length:", v473 + 3, 8);
      v192 = objc_alloc((Class)off_41CEDE958);
      v193 = objc_msgSend(v192, "initWithTimeIntervalSinceReferenceDate:", v473[3]);
      v194 = (void *)v493[5];
      v493[5] = (__int64)v193;
      objc_release(v194);
      if ( !v493[5] )
      {
        v195 = createMobileActivationError_0(
                 (__int64)"DeviceIdentityIssueClientCertificateWithCompletion_block_invoke",
                 1347,
                 -1,
                 0,
                 CFSTR("Failed to create date from server timestamp."),
                 v66,
                 v67,
                 v68,
                 v384);
        v33 = objc_claimAutoreleasedReturnValue_556(v195);
        goto LABEL_90;
      }
    }
  }
  if ( !v533[3] || !v528[3] )
  {
    v38 = 0;
LABEL_130:
    v391 = 0;
    goto LABEL_211;
  }
  v162 = (void *)objc_claimAutoreleasedReturnValue_556(objc_msgSend(*v23, "objectForKeyedSubscript:", CFSTR("OIDSToInclude")));
  v163 = isNSArray();
  v164 = (void *)objc_claimAutoreleasedReturnValue_556(v163);
  objc_release(v164);
  objc_release(v162);
  if ( v164 )
  {
    v165 = v528[3];
    v166 = (void *)objc_claimAutoreleasedReturnValue_556(objc_msgSend(*v23, "objectForKeyedSubscript:", CFSTR("OIDSToInclude")));
    v167 = copyCertificateOIDsThatDiffer(v165, v166);
    v38 = (void *)objc_claimAutoreleasedReturnValue_556(v167);
    objc_release(v166);
  }
  else
  {
    v38 = 0;
  }
  if ( !objc_msgSend(v38, "count") )
    goto LABEL_130;
  v291 = createMobileActivationError_0(
           (__int64)"DeviceIdentityIssueClientCertificateWithCompletion_block_invoke",
           1365,
           -1,
           0,
           CFSTR("Mismatch in requested OIDs and existing certificate's OIDs (%@)."),
           v66,
           v67,
           v68,
           (char)v38);
  v292 = objc_claimAutoreleasedReturnValue_556(v291);
  v293 = *(_QWORD *)(*(_QWORD *)(a1 + 56) + 8LL);
  v294 = *(void **)(v293 + 40);
  *(_QWORD *)(v293 + 40) = v292;
  objc_release(v294);
  v391 = 1;
LABEL_211:
  if ( v533[3] )
  {
    if ( (unsigned int)isFactoryMFiCertificate(v419, v528[3]) )
    {
      v295 = v533[3];
      v296 = a1 + 56;
      v297 = *(_QWORD *)(*(_QWORD *)(a1 + 56) + 8LL);
      v466 = *(id *)(v297 + 40);
      valid = validSecureEnclaveReferenceKey(v295, 1, 0, &amp;v466);
      j__objc_storeStrong_550((id *)(v297 + 40), v466);
      if ( !valid )
        goto LABEL_221;
    }
  }
  v299 = v533[3];
  if ( !v299 )
    goto LABEL_219;
  v296 = a1 + 56;
  v300 = *(_QWORD *)(*(_QWORD *)(a1 + 56) + 8LL);
  v465 = *(id *)(v300 + 40);
  v301 = validSecureEnclaveReferenceKey(v299, 0, 0, &amp;v465);
  j__objc_storeStrong_550((id *)(v300 + 40), v465);
  if ( !v301
    || v533[3]
    &amp;&amp; v528[3]
    &amp;&amp; (v302 = *(_QWORD *)(*(_QWORD *)v296 + 8LL),
        v464 = *(id *)(v302 + 40),
        v303 = certificatePublicKeyMatchesBIKPublicKey(),
        j__objc_storeStrong_550((id *)(v302 + 40), v464),
        (v303 &amp; 1) == 0) )
  {
LABEL_221:
    v304 = createMobileActivationError_0(
             (__int64)"DeviceIdentityIssueClientCertificateWithCompletion_block_invoke",
             1385,
             -2,
             *(void **)(*(_QWORD *)(*(_QWORD *)v296 + 8LL) + 40LL),
             CFSTR("Invalid reference key."),
             v66,
             v67,
             v68,
             v384);
    v305 = objc_claimAutoreleasedReturnValue_556(v304);
    v306 = *(_QWORD *)(*(_QWORD *)v296 + 8LL);
    v307 = *(void **)(v306 + 40);
    *(_QWORD *)(v306 + 40) = v305;
    objc_release(v307);
  }
  else
  {
LABEL_219:
    if ( !v391 )
      goto LABEL_231;
  }
  v308 = objc_retain(&amp;_os_log_default_0);
  if ( os_log_type_enabled_632((os_log_t)&amp;_os_log_default_0, OS_LOG_TYPE_DEFAULT) )
  {
    v309 = *(void **)(*(_QWORD *)(*(_QWORD *)(a1 + 56) + 8LL) + 40LL);
    *(_DWORD *)buf = 138412290;
    v557 = v309;
    _os_log_impl_600(
      &amp;dword_40E4FC000,
      (os_log_t)&amp;_os_log_default_0,
      OS_LOG_TYPE_DEFAULT,
      "Deleting invalid keys/certificates: %@",
      buf,
      0xCu);
  }
  objc_release(&amp;_os_log_default_0);
  delete_keychain_data(v419, v411, 0);
  delete_keychain_data(v419, v410, 0);
  delete_keychain_data(v419, v408, 0);
  delete_keychain_data(v419, v413, 0);
  delete_keychain_item(v419, v417, 0);
  delete_certificate(v419, v412, 0);
  delete_certificate(v419, v409, 0);
  v310 = v533;
  v311 = (const void *)v533[3];
  if ( v311 )
  {
    CFRelease_438(v311);
    v310 = v533;
  }
  v310[3] = 0;
  v312 = v528;
  v313 = (const void *)v528[3];
  if ( v313 )
  {
    CFRelease_438(v313);
    v312 = v528;
  }
  v312[3] = 0;
  v314 = v524;
  v315 = (const void *)v524[3];
  if ( v315 )
  {
    CFRelease_438(v315);
    v314 = v524;
  }
  v314[3] = 0;
  v316 = *(_QWORD *)(*(_QWORD *)(a1 + 56) + 8LL);
  v317 = *(void **)(v316 + 40);
  *(_QWORD *)(v316 + 40) = 0;
  objc_release(v317);
LABEL_231:
  if ( !v533[3] || !v528[3] || !v524[3] )
    goto LABEL_247;
  if ( (isRunningInRecovery() &amp; 1) != 0 || (v69 &amp; 1) != 0 || (v318 = v481, v481 == 0.0) || (v319 = (void *)v493[5]) == 0 )
  {
    v323 = objc_claimAutoreleasedReturnValue_556(objc_msgSend(off_41CEDE958, "date", v318));
    v322 = (void *)v499[5];
    v499[5] = v323;
    v321 = 1;
  }
  else
  {
    v320 = objc_claimAutoreleasedReturnValue_556(objc_msgSend(v319, "dateByAddingTimeInterval:", v483[3] - v481));
    v321 = 0;
    v322 = (void *)v499[5];
    v499[5] = v320;
  }
  objc_release(v322);
  v324 = v528[3];
  objc_msgSend((id)v499[5], "timeIntervalSinceReferenceDate");
  if ( !(unsigned int)SecCertificateIsValid_2(v324)
    || (v325 = v524[3],
        objc_msgSend((id)v499[5], "timeIntervalSinceReferenceDate"),
        !(unsigned int)SecCertificateIsValid_2(v325)) )
  {
    v330 = 0;
    v329 = 0;
    if ( (v321 &amp; 1) == 0 )
      goto LABEL_276;
    goto LABEL_246;
  }
  v326 = SecCertificateNotValidAfter_8(v528[3]);
  v327 = SecCertificateNotValidBefore_7(v528[3]);
  objc_msgSend((id)v499[5], "timeIntervalSinceReferenceDate");
  if ( v328 - SecCertificateNotValidBefore_7(v528[3]) &gt;= (v326 - v327) * 0.9 )
  {
    v330 = 1;
    v329 = 1;
    if ( (v321 &amp; 1) == 0 )
      goto LABEL_276;
LABEL_246:
    if ( v330 )
      goto LABEL_277;
LABEL_247:
    v35 = 0;
    goto LABEL_248;
  }
  v329 = 0;
  v330 = 1;
  if ( (v321 &amp; 1) != 0 )
    goto LABEL_246;
LABEL_276:
  if ( ((unsigned __int8)v330 &amp; (*((_DWORD *)v477 + 6) == v480)) == 0 )
    goto LABEL_247;
LABEL_277:
  if ( v395 )
  {
    v381 = v524[3];
    v571[1] = v528[3];
    v571[2] = v381;
    v382 = &amp;v572;
    v383 = v499 + 5;
  }
  else
  {
    v570 = v528[3];
    v382 = v571;
    v383 = v524 + 3;
  }
  *v382 = *v383;
  v35 = (id)objc_claimAutoreleasedReturnValue_556(objc_msgSend(off_41CEDE918, "arrayWithObjects:count:"));
  if ( v329 )
  {
LABEL_248:
    if ( queuea )
    {
      v331 = createMobileActivationError_0(
               (__int64)"DeviceIdentityIssueClientCertificateWithCompletion_block_invoke",
               1470,
               -1,
               0,
               CFSTR("Missing or expired certificates, and network request explictly not attempted."),
               v66,
               v67,
               v68,
               v384);
      v332 = objc_claimAutoreleasedReturnValue_556(v331);
    }
    else
    {
      if ( v399 &amp;&amp; (v333 = (const void *)v533[3]) != 0 )
      {
        v334 = (__SecKey *)CFRetain_262(v333);
        v541[3] = (__int64)v334;
      }
      else
      {
        v335 = *(_QWORD *)(a1 + 40);
        v336 = *(_QWORD *)(*(_QWORD *)(a1 + 56) + 8LL);
        v463 = *(id *)(v336 + 40);
        v334 = (__SecKey *)createReferenceKeyBlob(cf, v397, v335, &amp;v463);
        j__objc_storeStrong_550((id *)(v336 + 40), v463);
        v541[3] = (__int64)v334;
        if ( !v334 )
        {
          v360 = createMobileActivationError_0(
                   (__int64)"DeviceIdentityIssueClientCertificateWithCompletion_block_invoke",
                   1481,
                   -1,
                   *(void **)(*(_QWORD *)(*(_QWORD *)(a1 + 56) + 8LL) + 40LL),
                   CFSTR("Failed to create reference key."),
                   v337,
                   v338,
                   v339,
                   v384);
          v332 = objc_claimAutoreleasedReturnValue_556(v360);
          goto LABEL_262;
        }
      }
      v340 = SecKeyCopyPublicKey_11(v334);
      v537[3] = (__int64)v340;
      if ( v340 )
      {
        v37 = SecKeyCopyExternalRepresentation_13(v340, &amp;error);
        if ( v37 )
        {
          v347 = SecKeyCopyAttributes_8((SecKeyRef)v541[3]);
          v36 = v347;
          if ( v347 )
          {
            v354 = (void *)objc_claimAutoreleasedReturnValue_556(-[__CFDictionary objectForKeyedSubscript:](v347, "objectForKeyedSubscript:", CFSTR("toid")));
            if ( v354 )
            {
              v355 = j__dispatch_get_global_queue_364(0, 0);
              queuec = (NSObject *)objc_claimAutoreleasedReturnValue_556(v355);
              v428[0] = &amp;_NSConcreteStackBlock_0;
              v428[1] = 3221225472LL;
              v428[2] = __DeviceIdentityIssueClientCertificateWithCompletion_block_invoke_2;
              v428[3] = &amp;unk_41974FD50;
              v54 = &amp;v429;
              v429 = objc_retain(*(id *)(a1 + 40));
              v356 = objc_retain(v422);
              v357 = *(_QWORD *)(a1 + 56);
              v430 = v356;
              v444 = v357;
              v53 = objc_retain(v354);
              v431 = v53;
              v445 = v554;
              v446 = &amp;v498;
              v358 = objc_retain(v425);
              v462 = v395;
              v425 = v358;
              v432 = v358;
              v447 = &amp;v516;
              v448 = &amp;v540;
              v406 = objc_retain(v406);
              v433 = v406;
              v418 = objc_retain(v418);
              v434 = v418;
              v419 = objc_retain(v419);
              v435 = v419;
              v413 = objc_retain(v413);
              v436 = v413;
              v417 = objc_retain(v417);
              v437 = v417;
              v411 = objc_retain(v411);
              v438 = v411;
              v410 = objc_retain(v410);
              v439 = v410;
              v408 = objc_retain(v408);
              v440 = v408;
              v449 = &amp;v486;
              v450 = &amp;v482;
              v451 = &amp;v510;
              v452 = &amp;v476;
              v453 = &amp;v472;
              v454 = &amp;v504;
              v455 = &amp;v544;
              v456 = &amp;v548;
              v457 = &amp;v532;
              v458 = &amp;v527;
              v459 = &amp;v523;
              v35 = objc_retain(v35);
              v441 = v35;
              v460 = &amp;v492;
              v442 = objc_retain(v5);
              v443 = objc_retain(*(id *)(a1 + 48));
              v461 = &amp;v536;
              j__dispatch_async_488(queuec, v428);
              objc_release(queuec);
              objc_release(v443);
              objc_release(v442);
              objc_release(v441);
              objc_release(v440);
              objc_release(v439);
              objc_release(v438);
              objc_release(v437);
              objc_release(v436);
              objc_release(v435);
              objc_release(v434);
              objc_release(v433);
              objc_release(v432);
              objc_release(v431);
              objc_release(v430);
              goto LABEL_36;
            }
            v376 = createMobileActivationError_0(
                     (__int64)"DeviceIdentityIssueClientCertificateWithCompletion_block_invoke",
                     1532,
                     -1,
                     error,
                     CFSTR("Failed to encode RK as data."),
                     v351,
                     v352,
                     v353,
                     v384);
            v332 = objc_claimAutoreleasedReturnValue_556(v376);
            goto LABEL_264;
          }
          v375 = createMobileActivationError_0(
                   (__int64)"DeviceIdentityIssueClientCertificateWithCompletion_block_invoke",
                   1526,
                   -1,
                   error,
                   CFSTR("Failed to copy RK attributes."),
                   v348,
                   v349,
                   v350,
                   v384);
          v332 = objc_claimAutoreleasedReturnValue_556(v375);
LABEL_263:
          v36 = 0;
LABEL_264:
          v362 = *(_QWORD *)(*(_QWORD *)(a1 + 56) + 8LL);
          v363 = *(void **)(v362 + 40);
          *(_QWORD *)(v362 + 40) = v332;
          objc_release(v363);
          goto LABEL_265;
        }
        v361 = createMobileActivationError_0(
                 (__int64)"DeviceIdentityIssueClientCertificateWithCompletion_block_invoke",
                 1494,
                 -1,
                 error,
                 CFSTR("Failed to encode RK public key as data."),
                 v344,
                 v345,
                 v346,
                 v384);
        v332 = objc_claimAutoreleasedReturnValue_556(v361);
      }
      else
      {
        v359 = createMobileActivationError_0(
                 (__int64)"DeviceIdentityIssueClientCertificateWithCompletion_block_invoke",
                 1488,
                 -1,
                 0,
                 CFSTR("Failed to copy RK public key."),
                 v341,
                 v342,
                 v343,
                 v384);
        v332 = objc_claimAutoreleasedReturnValue_556(v359);
      }
    }
LABEL_262:
    v37 = 0;
    goto LABEL_263;
  }
  v37 = 0;
  v36 = 0;
LABEL_265:
  v364 = (const void *)v533[3];
  if ( !v364 || !v35 )
    goto LABEL_25;
  v365 = CFRetain_262(v364);
  v545[3] = (__int64)v365;
  j__objc_storeStrong_550(v549 + 5, v35);
  v41 = &amp;_os_log_default_0;
  v366 = objc_retain(&amp;_os_log_default_0);
  if ( os_log_type_enabled_632((os_log_t)&amp;_os_log_default_0, OS_LOG_TYPE_DEFAULT) )
  {
    v394 = (void *)objc_claimAutoreleasedReturnValue_556(objc_msgSend(v549[5], "objectAtIndexedSubscript:", 0));
    v398 = (void *)objc_claimAutoreleasedReturnValue_556(objc_msgSend(v549[5], "objectAtIndexedSubscript:", 0));
    v367 = objc_msgSend(off_41CEDE958, "dateWithTimeIntervalSinceReferenceDate:", SecCertificateNotValidBefore_7(v398));
    v388 = objc_retain((id)objc_claimAutoreleasedReturnValue_556(v367));
    v396 = (void *)objc_claimAutoreleasedReturnValue_556(objc_msgSend(v549[5], "objectAtIndexedSubscript:", 0));
    v368 = objc_msgSend(off_41CEDE958, "dateWithTimeIntervalSinceReferenceDate:", SecCertificateNotValidAfter_8(v396));
    queued = objc_retain((id)objc_claimAutoreleasedReturnValue_556(v368));
    v387 = (void *)objc_claimAutoreleasedReturnValue_556(objc_msgSend(v549[5], "objectAtIndexedSubscript:", 1));
    v390 = (void *)objc_claimAutoreleasedReturnValue_556(objc_msgSend(v549[5], "objectAtIndexedSubscript:", 1));
    v369 = objc_msgSend(off_41CEDE958, "dateWithTimeIntervalSinceReferenceDate:", SecCertificateNotValidBefore_7(v390));
    v400 = objc_retain((id)objc_claimAutoreleasedReturnValue_556(v369));
    v370 = objc_claimAutoreleasedReturnValue_556(objc_msgSend(v549[5], "objectAtIndexedSubscript:", 1));
    v371 = objc_msgSend(off_41CEDE958, "dateWithTimeIntervalSinceReferenceDate:", SecCertificateNotValidAfter_8(v370));
    v372 = objc_retain((id)objc_claimAutoreleasedReturnValue_556(v371));
    v373 = v493[5];
    *(_DWORD *)buf = 138544898;
    v557 = v394;
    v558 = 2114;
    v559 = v388;
    v560 = 2114;
    v561 = queued;
    v562 = 2114;
    v385 = (void *)v370;
    v386 = v372;
    v563 = v387;
    v564 = 2114;
    v565 = v400;
    v566 = 2114;
    v567 = v372;
    v374 = v372;
    v568 = 2114;
    v569 = v373;
    v41 = &amp;_os_log_default_0;
    _os_log_impl_600(
      &amp;dword_40E4FC000,
      (os_log_t)&amp;_os_log_default_0,
      OS_LOG_TYPE_DEFAULT,
      "Returning cached certificates:\n"
      "* %{public}@\n"
      "    Not Valid Before: %{public}@\n"
      "  Not Valid After: %{public}@\n"
      "* %{public}@\n"
      "    Not Valid Before: %{public}@\n"
      "  Not Valid After: %{public}@\n"
      "* Server Timestamp: %{public}@\n",
      buf,
      0x48u);
    objc_release(v374);
    objc_release(v385);
    objc_release(v400);
    objc_release(v390);
    objc_release(v387);
    objc_release(queued);
    objc_release(v396);
    objc_release(v388);
    objc_release(v398);
    objc_release(v394);
    objc_release(v386);
    objc_release(v400);
    objc_release(queued);
    objc_release(v388);
  }
LABEL_24:
  objc_release(v41);
LABEL_25:
  block[0] = &amp;_NSConcreteStackBlock_0;
  block[1] = 3221225472LL;
  block[2] = __DeviceIdentityIssueClientCertificateWithCompletion_block_invoke_309;
  block[3] = &amp;unk_41974FD00;
  v42 = *(void **)(a1 + 48);
  v427[1] = *(_QWORD *)(a1 + 56);
  v427[0] = objc_retain(v42);
  v427[2] = &amp;v544;
  v427[3] = &amp;v548;
  j__dispatch_async_488(v5, block);
  v43 = v541;
  v44 = (const void *)v541[3];
  if ( v44 )
  {
    CFRelease_438(v44);
    v43 = v541;
  }
  v43[3] = 0;
  v45 = v537;
  v46 = (const void *)v537[3];
  if ( v46 )
  {
    CFRelease_438(v46);
    v45 = v537;
  }
  v45[3] = 0;
  v47 = v533;
  v48 = (const void *)v533[3];
  if ( v48 )
  {
    CFRelease_438(v48);
    v47 = v533;
  }
  v47[3] = 0;
  v49 = v528;
  v50 = (const void *)v528[3];
  if ( v50 )
  {
    CFRelease_438(v50);
    v49 = v528;
  }
  v49[3] = 0;
  v51 = v524;
  v52 = (const void *)v524[3];
  if ( v52 )
  {
    CFRelease_438(v52);
    v51 = v524;
  }
  v53 = 0;
  v54 = (id *)v427;
  v51[3] = 0;
LABEL_36:
  objc_release(*v54);
  if ( v15 )
    CFRelease_438(v15);
  if ( error )
    CFRelease_438(error);
  error = 0;
  if ( cf )
    CFRelease_438(cf);
  _Block_object_dispose_549(&amp;v472, 8);
  _Block_object_dispose_549(&amp;v476, 8);
  _Block_object_dispose_549(&amp;v482, 8);
  objc_release(v5);
  objc_release(v38);
  objc_release(v422);
  _Block_object_dispose_549(&amp;v486, 8);
  objc_release(v491);
  _Block_object_dispose_549(&amp;v492, 8);
  objc_release(v497);
  _Block_object_dispose_549(&amp;v498, 8);
  objc_release(v503);
  _Block_object_dispose_549(&amp;v504, 8);
  objc_release(v509);
  _Block_object_dispose_549(&amp;v510, 8);
  objc_release(v515);
  _Block_object_dispose_549(&amp;v516, 8);
  objc_release(v521);
  objc_release(v39);
  objc_release(v414);
  objc_release(v408);
  objc_release(v411);
  objc_release(v410);
  objc_release(v417);
  objc_release(v409);
  objc_release(v412);
  objc_release(v413);
  objc_release(v419);
  objc_release(v418);
  _Block_object_dispose_549(&amp;v523, 8);
  _Block_object_dispose_549(&amp;v527, 8);
  _Block_object_dispose_549(&amp;v532, 8);
  _Block_object_dispose_549(&amp;v536, 8);
  _Block_object_dispose_549(&amp;v540, 8);
  _Block_object_dispose_549(&amp;v544, 8);
  objc_release(v406);
  objc_release(v407);
  objc_release(v37);
  objc_release(v36);
  objc_release(v53);
  objc_release(v421);
  _Block_object_dispose_549(&amp;v548, 8);
  objc_release(v553);
  objc_release(v35);
  objc_release(v425);
  _Block_object_dispose_549(v554, 8);
  objc_release(v555);
}

</code></pre></div></div>

<p>identityCertificateOptions 用来构造“我想要一张什么样的 identity 证书”；</p>

<p>DeviceIdentityIssueClientCertificateWithCompletion 则负责根据这些参数检查调用方权限、复用或重新签发 attestation 证书链，并在 keychain 中维护一整套“证书 + SE key + 时间防回拨信息”的档案。流程如下：</p>

<p><img src="/images/posts/ios/devicecheck/img-07.png" alt="" /></p>

<p>读取激活证书后准备加密生成token。</p>

<ul>
  <li>加密证书</li>
</ul>

<p>组合参数</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>_SecCertificateCopyData
id __fastcall __66__DCCertificateGenerator__generateCertificateChainWithCompletion___block_invoke(
        __int64 a1,
        __int64 a2)
{
  void *v2; // x27
  __int64 v3; // x0
  __int64 v4; // x0
  __int64 v5; // x21
  __int64 v6; // x0
  __int64 v7; // x0
  __int64 v8; // x23
  unsigned __int64 v9; // x0
  void *v10; // x28
  char v11; // w9
  __int64 v12; // x2
  char v13; // w8
  char v14; // w19
  char v15; // w26
  __int64 v16; // x20
  __int64 v17; // x0
  void *v18; // x21
  void *v19; // x26
  void *v20; // x24
  char *v21; // x22
  int v22; // w28
  int v23; // w27
  char *v24; // x25
  char *v25; // x0
  char *v26; // x20
  char *v27; // x28
  void *v28; // x25
  NSError *v29; // x0
  NSError *v30; // x0
  NSError *v31; // x0
  __int64 v32; // x0
  __int64 v33; // x0
  __int64 v34; // x0
  void *v35; // x0
  __int64 v36; // x21
  int v37; // w19
  __int64 v38; // x0
  __int64 v39; // x0
  __int64 v40; // x0
  __int64 v41; // x20
  __int64 v42; // x0
  void *v43; // x0
  __int64 v44; // x0
  __int64 v45; // x0
  __int64 v46; // x0
  __int64 v47; // x0
  __int64 v48; // x0
  __int64 v49; // x0
  __int64 v50; // x0
  __int64 v51; // x0
  id result; // x0
  NSError *v53; // x0
  __int64 v54; // x0
  unsigned int v55; // w20
  __int64 v56; // x20
  __int64 v57; // x0
  __int64 v58; // x0
  __int64 v59; // x22
  __int64 v60; // x0
  __int64 v61; // x0
  __int64 v62; // x20
  __int64 v63; // x0
  NSError *v64; // x0
  __int64 v65; // x20
  __int64 v66; // x0
  DCCertificateGenerator *v67; // x0
  SEL v68; // x1
  id v69; // x2
  id v70; // x3
  id *v71; // x4
  __int64 v72; // [xsp+0h] [xbp-180h]
  unsigned __int64 v73; // [xsp+10h] [xbp-170h]
  void *v75; // [xsp+20h] [xbp-160h]
  void *v77; // [xsp+30h] [xbp-150h]
  void *v78; // [xsp+40h] [xbp-140h]
  char v79; // [xsp+48h] [xbp-138h]
  char v80; // [xsp+4Ch] [xbp-134h]
  __int64 v81; // [xsp+50h] [xbp-130h] BYREF
  __CFString *v82; // [xsp+58h] [xbp-128h] BYREF
  __int64 v83; // [xsp+60h] [xbp-120h] BYREF
  __CFString *v84; // [xsp+68h] [xbp-118h] BYREF
  __int64 v85; // [xsp+70h] [xbp-110h] BYREF
  __CFString *v86; // [xsp+78h] [xbp-108h] BYREF
  char __src[16]; // [xsp+80h] [xbp-100h] BYREF
  __int128 v88; // [xsp+90h] [xbp-F0h]
  __int128 v89; // [xsp+A0h] [xbp-E0h]
  __int128 v90; // [xsp+B0h] [xbp-D0h]
  __int128 v91; // [xsp+C0h] [xbp-C0h]
  char __str[16]; // [xsp+D0h] [xbp-B0h] BYREF
  __int128 v93; // [xsp+E0h] [xbp-A0h]
  __int128 v94; // [xsp+F0h] [xbp-90h]
  __int128 v95; // [xsp+100h] [xbp-80h]
  __int128 v96; // [xsp+110h] [xbp-70h]
  unsigned __int64 v97; // [xsp+120h] [xbp-60h]

  v97 = 0x8FCA7F6773E200E4LL;
  v2 = (void *)objc_retain_x2_1310();
  v3 = ((__int64 (*)(void))MEMORY[0x243ECC210])();
  v4 = _DCLogSystem(v3);
  v5 = ((__int64 (__fastcall *)(__int64))MEMORY[0x243ECC0A0])(v4);
  v6 = ((__int64 (__fastcall *)(__int64, _QWORD))MEMORY[0x243ECC2A0])(v5, 0);
  if ( (_DWORD)v6 )
  {
    *(_WORD *)__str = 0;
    v6 = ((__int64 (__fastcall *)(int *, __int64, _QWORD, const char *, char *, __int64))MEMORY[0x243ECBE60])(
           &amp;dword_24150F000,
           v5,
           0,
           "Certificate issued, processing..",
           __str,
           2);
  }
  v7 = ((__int64 (__fastcall *)(__int64))MEMORY[0x243ECC130])(v6);
  v8 = ((__int64 (__fastcall *)(__int64))MEMORY[0x243ECC210])(v7);
  v75 = (void *)((__int64 (__fastcall *)(void *))MEMORY[0x243ECC0A0])(+[NSDate date](&amp;OBJC_CLASS___NSDate, "date"));
  if ( v2 &amp;&amp; (v9 = (unsigned __int64)objc_msgSend(v2, "count"), (v9 &amp; 0xFFFFFFFFFFFFFFFELL) == 2) )
  {
    v73 = v9;
    v79 = 0;
    v10 = 0;
    v11 = 0;
    v12 = 0;
    v13 = 1;
    v77 = v2;
    while ( 1 )
    {
      v14 = v13;
      v15 = v11;
      v16 = ((__int64 (__fastcall *)(void *))MEMORY[0x243ECC0A0])(objc_msgSend(v2, "objectAtIndexedSubscript:", v12));
      MEMORY[0x243ECC120]();
      v17 = ((__int64 (__fastcall *)(__int64))unk_243ECBDF0)(v16);
      v18 = (void *)((__int64 (__fastcall *)(__int64))MEMORY[0x243ECC1B0])(v17);
      v95 = 0u;
      v96 = 0u;
      v93 = 0u;
      v94 = 0u;
      *(_OWORD *)__str = 0u;
      v90 = 0u;
      v91 = 0u;
      v88 = 0u;
      v89 = 0u;
      *(_OWORD *)__src = 0u;
      if ( v18 )
      {
        v80 = v15;
        v19 = (void *)((__int64 (*)(void))MEMORY[0x243ECC080])();
        v20 = (void *)((__int64 (__fastcall *)(void *))MEMORY[0x243ECC0A0])(objc_msgSend(v18, "base64EncodedDataWithOptions:", 1));
        v21 = (char *)objc_msgSend(v20, "length");
        if ( v21 )
        {
          v78 = v10;
          v22 = snprintf(__str, 0x50u, "-----BEGIN %s-----\n", "CERTIFICATE");
          v23 = snprintf(__src, 0x50u, "\n-----END %s-----", "CERTIFICATE");
          v24 = &amp;v21[v22 + v23];
          v25 = (char *)malloc((size_t)(v24 + 1));
          if ( v25 )
          {
            v26 = v25;
            v27 = &amp;strncpy(v25, __str, v22)[v22];
            objc_msgSend(v20, "getBytes:range:", v27, 0, v21);
            strncpy(&amp;v21[(_QWORD)v27], __src, v23);
            v28 = (void *)((__int64 (__fastcall *)(NSData *))MEMORY[0x243ECC0A0])(
                            +[NSData(NSData) dataWithBytesNoCopy:length:](
                              &amp;OBJC_CLASS___NSData,
                              "dataWithBytesNoCopy:length:",
                              v26,
                              v24));
            if ( !v28 )
            {
              v85 = 0x21AEAF128LL;
              v86 = CFSTR("Failed to create pem data.");
              v31 = +[NSError errorWithDomain:code:userInfo:](
                      &amp;OBJC_CLASS___NSError,
                      "errorWithDomain:code:userInfo:",
                      CFSTR("com.apple.devicecheck.cryptoerror"),
                      0,
                      ((__int64 (__fastcall *)(NSDictionary *))MEMORY[0x243ECC0A0])(
                        +[NSDictionary dictionaryWithObjects:forKeys:count:](
                          &amp;OBJC_CLASS___NSDictionary,
                          "dictionaryWithObjects:forKeys:count:",
                          &amp;v86,
                          &amp;v85,
                          1)));
              ((void (__fastcall *)(NSError *))MEMORY[0x243ECC0A0])(v31);
              ((void (*)(void))unk_243ECC180)();
              free(v26);
            }
          }
          else
          {
            v85 = 0x21AEAF128LL;
            v86 = CFSTR("Failed to allocate buffer.");
            v30 = +[NSError errorWithDomain:code:userInfo:](
                    &amp;OBJC_CLASS___NSError,
                    "errorWithDomain:code:userInfo:",
                    CFSTR("com.apple.devicecheck.cryptoerror"),
                    0,
                    ((__int64 (__fastcall *)(NSDictionary *))MEMORY[0x243ECC0A0])(
                      +[NSDictionary dictionaryWithObjects:forKeys:count:](
                        &amp;OBJC_CLASS___NSDictionary,
                        "dictionaryWithObjects:forKeys:count:",
                        &amp;v86,
                        &amp;v85,
                        1)));
            ((void (__fastcall *)(NSError *))MEMORY[0x243ECC0A0])(v30);
            ((void (*)(void))unk_243ECC170)();
            v28 = 0;
          }
          v2 = v77;
          v10 = v78;
        }
        else
        {
          v28 = 0;
        }
        ((void (*)(void))MEMORY[0x243ECC160])();
        objc_autoreleasePoolPop(v19);
        v15 = v80;
      }
      else
      {
        v85 = 0x21AEAF128LL;
        v86 = CFSTR("Invalid inputs.");
        v29 = +[NSError errorWithDomain:code:userInfo:](
                &amp;OBJC_CLASS___NSError,
                "errorWithDomain:code:userInfo:",
                CFSTR("com.apple.devicecheck.cryptoerror"),
                0,
                ((__int64 (__fastcall *)(NSDictionary *))MEMORY[0x243ECC0A0])(
                  +[NSDictionary dictionaryWithObjects:forKeys:count:](
                    &amp;OBJC_CLASS___NSDictionary,
                    "dictionaryWithObjects:forKeys:count:",
                    &amp;v86,
                    &amp;v85,
                    1)));
        ((void (__fastcall *)(NSError *))MEMORY[0x243ECC0A0])(v29);
        ((void (*)(void))MEMORY[0x243ECC160])();
        v28 = 0;
      }
      v32 = ((__int64 (*)(void))MEMORY[0x243ECC140])();
      v33 = ((__int64 (__fastcall *)(__int64))MEMORY[0x243ECC130])(v32);
      v34 = ((__int64 (__fastcall *)(__int64))MEMORY[0x243ECC150])(v33);
      ((void (__fastcall *)(__int64))MEMORY[0x243ECC130])(v34);
      if ( !v28 )
        break;
      if ( v10 )
        objc_msgSend(v10, "appendBytes:length:", "\n", 1);
      else
        v10 = objc_msgSend(
                (id)((__int64 (__fastcall *)(void *))MEMORY[0x243ECC040])(&amp;OBJC_CLASS___NSMutableData),
                "initWithCapacity:",
                objc_msgSend(v28, "length"));
      v35 = objc_msgSend(v10, "appendData:", v28);
      v13 = 0;
      v11 = 1;
      v79 = v15;
      v12 = 1;
      if ( (v14 &amp; 1) == 0 )
      {
        v36 = v8;
        goto LABEL_36;
      }
    }
    v83 = 0x21AEAF128LL;
    v84 = CFSTR("Failed to create PEM data from cert.");
    v53 = +[NSError errorWithDomain:code:userInfo:](
            &amp;OBJC_CLASS___NSError,
            "errorWithDomain:code:userInfo:",
            CFSTR("com.apple.devicecheck.cryptoerror"),
            0,
            ((__int64 (__fastcall *)(NSDictionary *))MEMORY[0x243ECC0A0])(
              +[NSDictionary dictionaryWithObjects:forKeys:count:](
                &amp;OBJC_CLASS___NSDictionary,
                "dictionaryWithObjects:forKeys:count:",
                &amp;v84,
                &amp;v83,
                1)));
    v36 = ((__int64 (__fastcall *)(NSError *))MEMORY[0x243ECC0A0])(v53);
    v54 = ((__int64 (*)(void))MEMORY[0x243ECC150])();
    v35 = (void *)((__int64 (__fastcall *)(__int64))MEMORY[0x243ECC140])(v54);
    v15 = v79;
LABEL_36:
    if ( v73 == 3 )
    {
      v55 = (unsigned int)objc_msgSend(
                            *(id *)(a1 + 32),
                            "_isNSDate:",
                            ((__int64 (__fastcall *)(void *))MEMORY[0x243ECC0A0])(objc_msgSend(v2, "objectAtIndexedSubscript:", 2)));
      ((void (*)(void))MEMORY[0x243ECC140])();
      if ( v55 )
      {
        v56 = ((__int64 (__fastcall *)(void *))MEMORY[0x243ECC0A0])(objc_msgSend(v2, "objectAtIndexedSubscript:", 2));
        v57 = MEMORY[0x243ECC1A0]();
        v58 = _DCLogSystem(v57);
        v59 = ((__int64 (__fastcall *)(__int64))MEMORY[0x243ECC0A0])(v58);
        v60 = ((__int64 (__fastcall *)(__int64, _QWORD))MEMORY[0x243ECC2A0])(v59, 0);
        if ( (_DWORD)v60 )
        {
          *(_WORD *)__str = 0;
          v60 = ((__int64 (__fastcall *)(int *, __int64, _QWORD, const char *, char *, __int64))MEMORY[0x243ECBE60])(
                  &amp;dword_24150F000,
                  v59,
                  0,
                  "Using synced timestamp...",
                  __str,
                  2);
        }
        ((void (__fastcall *)(__int64))MEMORY[0x243ECC140])(v60);
        v37 = v15 &amp; 1;
        v75 = (void *)v56;
      }
      else
      {
        v81 = 0x21AEAF128LL;
        v82 = CFSTR("Expected date field, failing...");
        v64 = +[NSError errorWithDomain:code:userInfo:](
                &amp;OBJC_CLASS___NSError,
                "errorWithDomain:code:userInfo:",
                CFSTR("com.apple.devicecheck.cryptoerror"),
                0,
                ((__int64 (__fastcall *)(NSDictionary *))MEMORY[0x243ECC0A0])(
                  +[NSDictionary dictionaryWithObjects:forKeys:count:](
                    &amp;OBJC_CLASS___NSDictionary,
                    "dictionaryWithObjects:forKeys:count:",
                    &amp;v82,
                    &amp;v81,
                    1)));
        v65 = ((__int64 (__fastcall *)(NSError *))MEMORY[0x243ECC0A0])(v64);
        v66 = MEMORY[0x243ECC130]();
        ((void (__fastcall *)(__int64))MEMORY[0x243ECC140])(v66);
        v37 = 0;
        v36 = v65;
      }
    }
    else
    {
      v61 = _DCLogSystem(v35);
      v62 = ((__int64 (__fastcall *)(__int64))MEMORY[0x243ECC0A0])(v61);
      v63 = ((__int64 (__fastcall *)(__int64, _QWORD))MEMORY[0x243ECC2A0])(v62, 0);
      if ( (_DWORD)v63 )
      {
        *(_WORD *)__str = 0;
        v63 = ((__int64 (__fastcall *)(int *, __int64, _QWORD, const char *, char *, __int64))MEMORY[0x243ECBE60])(
                &amp;dword_24150F000,
                v62,
                0,
                "Using device timestamp...",
                __str,
                2);
      }
      ((void (__fastcall *)(__int64))MEMORY[0x243ECC120])(v63);
      v37 = v15 &amp; 1;
    }
  }
  else
  {
    v10 = 0;
    v37 = 0;
    v36 = v8;
  }
  v38 = a2;
  if ( a2 )
    v38 = ((__int64 (*)(void))unk_243ECBD30)();
  v39 = _DCLogSystem(v38);
  v40 = ((__int64 (__fastcall *)(__int64))MEMORY[0x243ECC0A0])(v39);
  v41 = v40;
  if ( v37 )
  {
    v42 = ((__int64 (__fastcall *)(__int64, _QWORD))MEMORY[0x243ECC2A0])(v40, 0);
    if ( (_DWORD)v42 )
    {
      v43 = objc_msgSend(v10, "length");
      *(_DWORD *)__str = 134217984;
      *(_QWORD *)&amp;__str[4] = v43;
      v42 = ((__int64 (*)(int *, __int64, _QWORD, const char *, ...))MEMORY[0x243ECBE60])(
              &amp;dword_24150F000,
              v41,
              0,
              "Certificate processed... (%lu)",
              v72);
    }
    ((void (__fastcall *)(__int64))MEMORY[0x243ECC120])(v42);
    v44 = (*(__int64 (__fastcall **)(_QWORD, void *, void *))(*(_QWORD *)(a1 + 40) + 16LL))(
            *(_QWORD *)(a1 + 40),
            objc_msgSend(v10, "copy"),
            objc_msgSend(v75, "copy"));
    v45 = ((__int64 (__fastcall *)(__int64))MEMORY[0x243ECC160])(v44);
  }
  else
  {
    if ( (unsigned int)((__int64 (__fastcall *)(__int64, __int64))MEMORY[0x243ECC2A0])(v40, 16) )
      __66__DCCertificateGenerator__generateCertificateChainWithCompletion___block_invoke_cold_1(v36, v41);
    MEMORY[0x243ECC120]();
    v45 = (*(__int64 (__fastcall **)(_QWORD, _QWORD, void *))(*(_QWORD *)(a1 + 40) + 16LL))(
            *(_QWORD *)(a1 + 40),
            0,
            objc_msgSend(v75, "copy"));
  }
  v46 = ((__int64 (__fastcall *)(__int64))MEMORY[0x243ECC120])(v45);
  v47 = ((__int64 (__fastcall *)(__int64))MEMORY[0x243ECC110])(v46);
  v48 = ((__int64 (__fastcall *)(__int64))unk_243ECC190)(v47);
  v49 = ((__int64 (__fastcall *)(__int64))unk_243ECC170)(v48);
  v50 = ((__int64 (__fastcall *)(__int64))MEMORY[0x243ECC130])(v49);
  v51 = ((__int64 (__fastcall *)(__int64))MEMORY[0x243ECC150])(v50);
  result = (id)((__int64 (__fastcall *)(__int64))unk_243ECC180)(v51);
  if ( v97 != 0x8FCA7F6773E200E4LL )
  {
    v67 = (DCCertificateGenerator *)((__int64 (__fastcall *)(id))unk_243ECBE30)(result);
    return -[DCCertificateGenerator _encryptData:serverSyncedDate:error:](v67, v68, v69, v70, v71);
  }
  return result;
}
</code></pre></div></div>

<p>将组合后的两个激活证传给加密方法</p>

<ul>
  <li>搜索“Encrypting data…”定位关键加密算法。</li>
</ul>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>id __fastcall -[DCCertificateGenerator _encryptData:serverSyncedDate:error:](id *a1, __int64 a2, void *a3, void *a4)
{
  id v6; // x21
  __int64 v7; // x0
  NSObject *v8; // x19
  void *v9; // x19
  void *v10; // x20
  unsigned int v11; // w27
  const void *v12; // x28
  unsigned int v13; // w25
  id v14; // x21
  const void *v15; // x19
  __int64 v16; // x23
  size_t v17; // x24
  _DWORD *v18; // x0
  _DWORD *v19; // x22
  char *v20; // x23
  void *v21; // x27
  void *v22; // x0
  __int64 v23; // d0
  __int64 v24; // x0
  NSObject *v25; // x19
  __int64 v26; // x8
  __int64 v27; // x0
  __int64 v28; // x19
  __int64 v29; // x0
  NSObject *v30; // x24
  __int64 v31; // x0
  void *v32; // x25
  _OWORD *public_key_1; // x0
  __int128 v34; // q0
  __int128 v35; // q1
  __int128 v36; // q2
  __int64 i; // x20
  __int64 v38; // x0
  __int64 v39; // x19
  __int64 v40; // x0
  __int64 v41; // x0
  char *v43; // x19
  __int64 v44; // x20
  unsigned __int8 *v45; // x25
  int v46; // t1
  __int64 v47; // x0
  __int64 v48; // x19
  __int64 v49; // x0
  __int64 v50; // x0
  __int64 v51; // x25
  __int64 v52; // x0
  __int64 v53; // x0
  __int64 v54; // [xsp+28h] [xbp-D8h]
  id v55; // [xsp+30h] [xbp-D0h]
  id v56; // [xsp+38h] [xbp-C8h]
  size_t __n; // [xsp+40h] [xbp-C0h] BYREF
  __int64 v58; // [xsp+48h] [xbp-B8h] BYREF
  void *v59; // [xsp+50h] [xbp-B0h] BYREF
  __int64 v60; // [xsp+58h] [xbp-A8h] BYREF
  uint8_t v61[4]; // [xsp+60h] [xbp-A0h] BYREF
  __int64 time; // [xsp+64h] [xbp-9Ch]
  uint8_t buf[16]; // [xsp+70h] [xbp-90h] BYREF
  _DWORD v64[8]; // [xsp+80h] [xbp-80h] BYREF
  __int64 vars8; // [xsp+108h] [xbp+8h]

  v6 = objc_retain(a3);
  v56 = objc_retain(a4);
  v7 = _DCLogSystem(v56);
  v8 = (NSObject *)objc_claimAutoreleasedReturnValue_1330(v7);
  if ( os_log_type_enabled_1518(v8, OS_LOG_TYPE_DEFAULT) )
  {
    *(_WORD *)buf = 0;
    _os_log_impl_1398(&amp;dword_446A2E000, v8, OS_LOG_TYPE_DEFAULT, "Encrypting data...", buf, 2u);
  }
  objc_release(v8);
  v9 = (void *)objc_claimAutoreleasedReturnValue_1330(objc_msgSend(a1[2], "clientAppID"));
  v10 = (void *)objc_claimAutoreleasedReturnValue_1330(objc_msgSend(v9, "dataUsingEncoding:", 4));
  objc_release(v9);
  v11 = (unsigned int)objc_msgSend(v10, "length");
  v55 = objc_retainAutorelease(v10);
  v12 = objc_msgSend(v55, "bytes");
  v13 = (unsigned int)objc_msgSend(v6, "length");
  v14 = objc_retainAutorelease(v6);
  v15 = objc_msgSend(v14, "bytes");
  v59 = 0;
  v60 = 0;
  v58 = 0;
  v16 = ccaes_gcm_encrypt_mode_18();
  *(_OWORD *)buf = 0u;
  memset(v64, 0, 28);
  v17 = v13 + v11 + 235LL;
  v18 = j__calloc_516(1u, v17);
  v19 = v18;
  if ( v18 )
  {
    v54 = v16;
    *(_DWORD *)((char *)v18 + 150) = v13 + v11 + 81;
    *v18 = 2;
    j__memcpy_1002(v18 + 5, objc_msgSend(a1[1], "bytes"), (size_t)objc_msgSend(a1[1], "length"));
    v20 = (char *)j__calloc_516(1u, *(unsigned int *)((char *)v19 + 150));
    *(_DWORD *)(v20 + 73) = v13;
    j__memcpy_1002(v20 + 81, v15, v13);
    *(_DWORD *)(v20 + 77) = v11;
    j__memcpy_1002(&amp;v20[*(unsigned int *)(v20 + 73) + 81], v12, v11);
    v21 = v56;
    v22 = objc_msgSend(v56, "timeIntervalSince1970");
    *(_QWORD *)(v20 + 65) = v23;
    v24 = _DCLogSystem(v22);
    v25 = (NSObject *)objc_claimAutoreleasedReturnValue_1330(v24);
    if ( os_log_type_enabled_1518(v25, OS_LOG_TYPE_DEFAULT) )
    {
      v26 = *(_QWORD *)(v20 + 65);
      *(_DWORD *)v61 = 134217984;
      time = v26;
      _os_log_impl_1398(&amp;dword_446A2E000, v25, OS_LOG_TYPE_DEFAULT, "Token timestamp: %f", v61, 0xCu);
    }
    objc_release(v25);
    v27 = aks_ref_key_create_1(objc_msgSend(a1, "keybagHandle"), 11, 4, 0, 0, &amp;v60);
    if ( (_DWORD)v27 )
    {
      v28 = v27;
      v29 = _DCLogSystem(v27);
      v30 = (NSObject *)objc_claimAutoreleasedReturnValue_1330(v29);
      if ( os_log_type_enabled_1518(v30, OS_LOG_TYPE_ERROR) )
        -[DCCertificateGenerator _encryptData:serverSyncedDate:error:].cold.6(v28, v30);
LABEL_20:
      v32 = 0;
      goto LABEL_21;
    }
    __n = 0;
    public_key_1 = (_OWORD *)aks_ref_key_get_public_key_1(v60, &amp;__n);
    if ( __n != 65 )
    {
      v41 = _DCLogSystem(public_key_1);
      v30 = (NSObject *)objc_claimAutoreleasedReturnValue_1330(v41);
      if ( os_log_type_enabled_1518(v30, OS_LOG_TYPE_ERROR) )
        -[DCCertificateGenerator _encryptData:serverSyncedDate:error:].cold.5(&amp;__n, v30);
      goto LABEL_20;
    }
    *(_OWORD *)((char *)v19 + 85) = *public_key_1;
    v34 = public_key_1[1];
    v35 = public_key_1[2];
    v36 = public_key_1[3];
    *((_BYTE *)v19 + 149) = *((_BYTE *)public_key_1 + 64);
    *(_OWORD *)((char *)v19 + 133) = v36;
    *(_OWORD *)((char *)v19 + 117) = v35;
    *(_OWORD *)((char *)v19 + 101) = v34;
    j__memcpy_1002(v20, public_key_1, __n);
    j__printf_200("%-25.25s = ", "random_pubkey");
    for ( i = 85; i != 150; ++i )
      j__printf_200("%02x", *((unsigned __int8 *)v19 + i));
    putchar_75(10);
    v38 = aks_ref_key_compute_key_0(v60, 0, 0, objc_msgSend(a1[1], "bytes"), objc_msgSend(a1[1], "length"), &amp;v59, &amp;v58);
    if ( (_DWORD)v38 )
    {
      v39 = v38;
      v40 = _DCLogSystem(v38);
      v30 = (NSObject *)objc_claimAutoreleasedReturnValue_1330(v40);
      if ( os_log_type_enabled_1518(v30, OS_LOG_TYPE_ERROR) )
        -[DCCertificateGenerator _encryptData:serverSyncedDate:error:].cold.4(v39, v30);
      goto LABEL_20;
    }
    v43 = (char *)v59;
    v44 = v58 - 2;
    j__printf_200("%-25.25s = ", "ECDH shared key");
    if ( v44 )
    {
      v45 = (unsigned __int8 *)(v43 + 2);
      do
      {
        v46 = *v45++;
        j__printf_200("%02x", v46);
        --v44;
      }
      while ( v44 );
    }
    putchar_75(10);
    v47 = cchkdf_19(&amp;ccsha256_ltc_di, v58 - 2, (char *)v59 + 2, 0, 0, 0, 0, 44, buf);
    if ( (_DWORD)v47 )
    {
      v48 = v47;
      v49 = _DCLogSystem(v47);
      v30 = (NSObject *)objc_claimAutoreleasedReturnValue_1330(v49);
      if ( os_log_type_enabled_1518(v30, OS_LOG_TYPE_ERROR) )
        -[DCCertificateGenerator _encryptData:serverSyncedDate:error:].cold.3(v48, v30);
      goto LABEL_20;
    }
    hex_2((std::ios_base *)"HKDF derived key");
    hex_2((std::ios_base *)"HKDF derived iv");
    v50 = ccgcm_one_shot_10(
            v54,
            32,
            buf,
            12,
            &amp;v64[4],
            0,
            0,
            *(unsigned int *)((char *)v19 + 150),
            v20,
            (char *)v19 + 154,
            16,
            v19 + 1);
    if ( (_DWORD)v50 )
    {
      v51 = v50;
      v52 = _DCLogSystem(v50);
      v30 = (NSObject *)objc_claimAutoreleasedReturnValue_1330(v52);
      if ( os_log_type_enabled_1518(v30, OS_LOG_TYPE_ERROR) )
        -[DCCertificateGenerator _encryptData:serverSyncedDate:error:].cold.2(v51, v30);
      goto LABEL_20;
    }
    hex_2((std::ios_base *)"tag");
    j__fprintf_285((FILE *)__stderrp_0, "encrypted_data_len: %d\n", *(_DWORD *)((char *)v19 + 150));
    v32 = (void *)objc_claimAutoreleasedReturnValue_1330(objc_msgSend(&amp;OBJC_CLASS___NSData, "dataWithBytes:length:", v19, v17));
    v53 = _DCLogSystem(v32);
    v30 = (NSObject *)objc_claimAutoreleasedReturnValue_1330(v53);
    _DCLogDebugBinary_0(v30, CFSTR("Token"), v32);
  }
  else
  {
    v31 = _DCLogSystem(0);
    v30 = (NSObject *)objc_claimAutoreleasedReturnValue_1330(v31);
    if ( os_log_type_enabled_1518(v30, OS_LOG_TYPE_ERROR) )
      -[DCCertificateGenerator _encryptData:serverSyncedDate:error:].cold.1(v30);
    v20 = 0;
    v32 = 0;
    v21 = v56;
  }
LABEL_21:
  objc_release(v30);
  if ( v60 )
    aks_ref_key_free_1(&amp;v60);
  if ( v19 )
    j__free_1273(v19);
  if ( v20 )
    j__free_1273(v20);
  if ( v59 )
    j__free_1273(v59);
  objc_release(v55);
  objc_release(v21);
  objc_release(v14);
  if ( ((vars8 ^ (2 * vars8)) &amp; 0x4000000000000000LL) != 0 )
    __break(0xC471u);
  return objc_autoreleaseReturnValue(v32);
}
</code></pre></div></div>

<p>这个方法用 Secure Enclave 生成一个临时 EC 密钥 → 和服务器公钥做 ECDH → HKDF 派生 AES-256-GCM key+IV →把「[ephemeral 公钥 + 时间戳 + 激活证书 + appID]」打包加密 → 和各种公钥 / 长度 / tag 一起封装成 Token。流程如下：</p>

<p><img src="/images/posts/ios/devicecheck/img-08.png" alt="" /></p>

<h3 id="42-加密算法流程">4.2 加密算法流程</h3>

<p>整个加密流程是一个变种的 <strong>ECIES</strong> (Elliptic** Curve Integrated Encryption Scheme)**，具体步骤如下：</p>

<p><strong>Payload 构建</strong>： 系统将 <strong>App ID</strong> (如 <code class="language-plaintext highlighter-rouge">com.example.app</code>)、<strong>设备证书链</strong>（包含 App Attest 证书和 Device CA 证书）以及 <strong>时间戳</strong> 拼接成一段二进制数据。</p>

<p><strong>硬件密钥生成 (AKS)</strong>： 调用私有 API <code class="language-plaintext highlighter-rouge">aks_ref_key_create</code>。这一步是在 <strong>Secure Enclave (SE)</strong> 安全芯片内部生成一个临时的 P-256 椭圆曲线密钥对。</p>

<ul>
  <li><strong>关键点</strong>：私钥 (Private Key) 永远停留在硬件安全区内，外部无法读取，只能通过句柄引用。</li>
</ul>

<p><strong>ECDH 密钥协商</strong>： 调用 <code class="language-plaintext highlighter-rouge">aks_ref_key_compute_key</code>。系统使用刚才生成的<strong>硬件临时私钥</strong>，与代码中硬编码的 <strong>Apple 服务器公钥</strong> 进行 ECDH (Elliptic-curve Diffie–Hellman) 运算，计算出一个 <strong>共享密钥 (Shared Secret)</strong>。</p>

<p><strong>HKDF 密钥派生</strong>： 调用 <code class="language-plaintext highlighter-rouge">cchkdf</code> (基于 HMAC 的密钥派生函数)。使用共享密钥作为输入，派生出用于后续对称加密的 <strong>AES 密钥</strong> 和 <strong>IV (初始化向量)</strong>。</p>

<p><strong>AES-GCM 加密</strong>： 调用 <code class="language-plaintext highlighter-rouge">ccgcm_one_shot</code>。使用派生出的 AES 密钥，采用 <strong>AES-GCM</strong> 模式对第一步构建的 Payload 进行加密。GCM 模式不仅加密数据，还生成一个 <strong>Tag (认证标签)</strong>，用于防止数据被篡改。</p>

<p><strong>最终封装</strong>： 最终的 Token 结构大致为：</p>

<pre><code class="language-Objective-C">#pragma pack(push, 1)
typedef struct {
    uint8_t  ec_pubkey[65];   // 04 + X(32) + Y(32)
    double   timestamp;       // 小端 double
    uint32_t cert_len;        // 激活证书pme
    uint32_t bundle_len;      // app包名长度
    // 下面是可变部分：
    uint8_t cert_pem[cert_len];
    char    bundle[bundle_len]; // "xxC6HB646H.com.beeasy.xxopee.xx"
} TokenPlainHeader;
#pragma pack(pop)


</code></pre>

<h3 id="43-算法还原及为什么不建议生成纯算法">4.3 算法还原及为什么不建议生成纯算法？</h3>

<ul>
  <li>还原算法</li>
</ul>

<pre><code class="language-Python">import base64
import time
import struct
from typing import Tuple

from cryptography.hazmat.primitives.ciphers.aead import AESGCM
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.kdf.hkdf import HKDF


class DeviceCheckTokenBuilder:
    *"""*
*    - server_pub: 固定“服务器公钥”（65 字节，0x04 开头）*
*    - ephemeral_pub: 固定“设备临时公钥”（65 字节，0x04 开头）*
*    - ikm: 固定的“ECDH shared secret”*
*    - AES key / IV 通过 HKDF(SHA256, L=44) 从 IKM 导出*
*    """*

*    *def __init__(self):
        # ==== 1. 固定服务器公钥 // fallback_server_pubkey ====
        self.server_pub = bytes.fromhex(
            "04 50 D9 34 FA 67 BC F6 F2 DF BF 96 62 9E 0A 72"
            " 38 E9 20 5D 75 F2 8C FC D8 4F 35 A6 59 2B BE 05"
            " 8A 9C 0F 8E DB CA 2A CB 67 EF B7 74 97 1C A4 5F"
            " 7D 85 6A 69 4F B1 B9 C4 0B 94 FB 2E 7A 5A 94 98"
            " B0"
        )

        # ==== 2. 固定设备临时公钥 _aks_ref_key_get_public_key_1 ====
        self.ephemeral_pub = bytes.fromhex(
            "04 79 5B C4 54 FF 9D 34 1E D8 11 CE CC"
            " 73 C4 AF 32 1E 81 27 6D A0 BE F0 BF F6 B5 D6 3C"
            " 5B AC 9F 09 2D 45 90 1B 6C A1 DA BF D9 8B 63 EF"
            " 7E 3B 9D 14 3D 14 4D FE 70 85 7C 0A 4E 07 88 40"
            " 4F 92 87 D0"
        )

        # ==== 3. 固定 IKM（ECDH shared secret）====
        self.ikm = bytes.fromhex(
            "fe3cb482384079c224225f476244d7c7"
            "4031a41bf09ea60e27f9ede0861bf42e"
        )

        # 通过 HKDF 推导 AES key &amp; IV（仿 cchkdf_19）
        self.aes_key, self.aes_iv = self._hkdf_derive_key_iv(self.ikm)

    # ------------------ 核心加密相关 ------------------ #

    @staticmethod
    def _hkdf_derive_key_iv(ikm: bytes) -&gt; Tuple[bytes, bytes]:
        *"""*
*        从 IKM 导出 44 字节 OKM，前 32 字节做 AES key，后 12 字节做 GCM IV*
*        相当于：*
*          cchkdf_19(sha256, ikm, outLen=44, salt=NULL, info=NULL)*
*        """*
*        *hkdf = HKDF(
            algorithm=hashes.SHA256(),
            length=44,
            salt=None,
            info=None,
        )
        okm = hkdf.derive(ikm)
        key = okm[:32]
        iv = okm[32:44]
        return key, iv

    @staticmethod
    def _gcm_encrypt(plaintext: bytes, key: bytes, iv: bytes) -&gt; Tuple[bytes, bytes]:
        *"""*
*        AES-GCM 加密：*
*        返回 (ciphertext_without_tag, tag_16B)*
*        """*
*        *aesgcm = AESGCM(key)
        ct_with_tag = aesgcm.encrypt(iv, plaintext, None)
        tag = ct_with_tag[-16:]
        ciphertext_only = ct_with_tag[:-16]
        return ciphertext_only, tag


    def build_dc_token(
        self,
        appname: str = None,
        appcert: str = None,
        devicecert: str = None,
    ) -&gt; str:
        *"""*

*        Token Layout:*
*          u32  version = 2*
*          u8   tag[16]*
*          u8   server_pub[65]*
*          u8   ephemeral_pub[65]*
*          u32  ciphertext_len*
*          u8   ciphertext[ciphertext_len]*
*        """*

*        *# ---- 1. 默认 appname / 证书 ---- #
        if appname is None:
            appname = "T7C6HB646H.com.beeasy.shoppe.tw"

        if appcert is None:
            appcert = """-----BEGIN CERTIFICATE-----
MIIDJjCCAsugAwIBAgIGAZrJqz2xMAoGCCqGSM49BAMCMFMxJzAlBgNVBAMMHkJh
c2ljIEF0dGVzdGF0aW9uIFVzZXIgU3ViIENBMTETMBEGA1UECgwKQXBwbGUgSW5j
LjETMBEGA1UECAwKQ2FsaWZvcm5pYTAeFw0yNTExMjcwODUzNTRaFw0yNjEwMTQx
ODIwNTRaMIGRMUkwRwYDVQQDDEBiNjdjMGY3ZWM4OWJiOTZlZmU3ZGY5OTIzNDVl
NDIxNjdiMjU3ODQyZGQyZWM5MjdkZjE5MWVjYmQwYzM0YmI5MRowGAYDVQQLDBFC
QUEgQ2VydGlmaWNhdGlvbjETMBEGA1UECgwKQXBwbGUgSW5jLjETMBEGA1UECAwK
Q2FsaWZvcm5pYTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABKoJsFk1GR/rs/Jj
+3iFqwYNnMZCB/0/s40hCTNSr5iooEkoAg8Apue/liWQrcVLYvknsfpRDZ74cKnp
cRI/8+ejggFKMIIBRjAMBgNVHRMBAf8EAjAAMA4GA1UdDwEB/wQEAwIE8DCCASQG
CSqGSIb3Y2QKAQSCARUEggERMYIBDf+EmqGSUA0wCxYEQ0hJUAIDAIEQ/4SqjZJE
ETAPFgRFQ0lEAgcYaaI8ICAe/4SSvaRECzAJFgRCT1JEAgEY/4aTtcJjGzAZFgRi
bWFjBBFhYzoxNjoxNTo2ZDphNToxZf+Gy7XKaRkwFxYEaW1laQQPMzU2MTI0NzE1
MDY4NTU1/4erkdJkIzAhFgR1ZGlkBBkwMDAwODExMC0wMDE4NjlBMjNDMjAyMDFF
/4e7tcJjGzAZFgR3bWFjBBFhYzoxNjoxNTo3OTo4ZTpmZf+Hm5XSZDowOBYEc2Vp
ZAQwMDQyRDI3QzU4QTE2OTAwMjIzMzIwMTMxNzI0MDUyMjRGRDE3MkZCRUQxM0VF
QzdCMAoGCCqGSM49BAMCA0kAMEYCIQCytP7jpU3xbaRHvXyvqCcum2F/kkDBSv1x
/RFAUXLJQwIhALeFnzR9R9BFlTjLS+nql3Cb6FqxywwJfCvf9v+JJTJl
-----END CERTIFICATE-----"""

        if devicecert is None:
            devicecert = """-----BEGIN CERTIFICATE-----
MIICIzCCAaigAwIBAgIIeNjhG9tnDGgwCgYIKoZIzj0EAwIwUzEnMCUGA1UEAwwe
QmFzaWMgQXR0ZXN0YXRpb24gVXNlciBSb290IENBMRMwEQYDVQQKDApBcHBsZSBJ
bmMuMRMwEQYDVQQIDApDYWxpZm9ybmlhMB4XDTE3MDQyMDAwNDIwMFoXDTMyMDMy
MjAwMDAwMFowUzEnMCUGA1UEAwweQmFzaWMgQXR0ZXN0YXRpb24gVXNlciBTdWIg
Q0ExMRMwEQYDVQQKDApBcHBsZSBJbmMuMRMwEQYDVQQIDApDYWxpZm9ybmlhMFkw
EwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEoSZ/1t9eBAEVp5a8PrXacmbGb8zNC1X3
StLI9YO6Y0CL7blHmSGmjGWTwD4Q+i0J2BY3+bPHTGRyA9jGB3MSbaNmMGQwEgYD
VR0TAQH/BAgwBgEB/wIBADAfBgNVHSMEGDAWgBSD5aMhnrB0w/lhkP2XTiMQdqSj
8jAdBgNVHQ4EFgQU5mWf1DYLTXUdQ9xmOH/uqeNSD80wDgYDVR0PAQH/BAQDAgEG
MAoGCCqGSM49BAMCA2kAMGYCMQC3M360LLtJS60Z9q3vVjJxMgMcFQ1roGTUcKqv
W+4hJ4CeJjySXTgq6IEHn/yWab4CMQCm5NnK6SOSK+AqWum9lL87W3E6AA1f2TvJ
/hgok/34jr93nhS87tOQNdxDS8zyiqw=
-----END CERTIFICATE-----"""

        # ---- 2. 拼接证书 + appname ---- #
        cert_bytes = bytearray()
        cert_bytes.extend(appcert.encode("utf-8"))
        cert_bytes.append(0x0A)  # 额外换行
        cert_bytes.extend(devicecert.encode("utf-8"))

        app_bytes = appname.encode("utf-8")

        cert_len = len(cert_bytes)
        app_len = len(app_bytes)

        # ---- 3. 构造明文 ---- #

        # ---- 4. AES-GCM 加密（key/iv 来自 HKDF） ---- #


        # ---- 5. 组装最终 Token ---- #
        result = bytearray()
        result.extend(struct.pack("&lt;I", 2))      # version = 2 (小端 u32)
        result.extend(tag)                       # 16B tag
        result.extend(self.server_pub)           # 服务器公钥
        result.extend(self.ephemeral_pub)        # 设备临时公钥
        result.extend(struct.pack("&lt;I", len(ciphertext)))
        result.extend(ciphertext)

        # Base64 输出
        return base64.b64encode(bytes(result)).decode("ascii")


if __name__ == "__main__":
    builder = DeviceCheckTokenBuilder()
    token = builder.build_dc_token()
    print("Demo DC Token (base64):")
    print(token)
</code></pre>

<ul>
  <li>不建议生成纯算法原因在于：<strong>私钥不可导出</strong>。 我们在外部代码中只能看到 Apple 的公钥，但无法获取在那一瞬间硬件生成的临时私钥。没有私钥，就无法在外部环境中计算出与硬件一致的 ECDH 共享密钥，也就无法生成能被 Apple 服务器解密的 Token，写死一份是有风险的，不确定Apple服务器端是否有检测。RPC调用可能真实性会更高一些。</li>
</ul>

<h3 id="44-证书替换的可行性分析">4.4 证书替换的可行性分析</h3>

<p>除了伪造 ID，我们还可以通过 Hook <code class="language-plaintext highlighter-rouge">_encryptData</code> 的入参来<strong>伪造设备信息生成我们想要的token</strong>。</p>

<ul>
  <li>
    <p><strong>伪造证书</strong>：</p>

    <ul>
      <li>
        <p>逆向分析发现 <code class="language-plaintext highlighter-rouge">_encryptData</code> 的 <code class="language-plaintext highlighter-rouge">data</code> 参数本质上是 PEM 格式证书的 Base64 编码。</p>
      </li>
      <li>
        <p>可以预先收集一批“白名单设备”的证书。</p>
      </li>
      <li>
        <p>在 Hook 点将当前的“黑名单证书”替换为“白名单证书”。frida脚本如下：</p>
      </li>
    </ul>

    <pre><code class="language-JavaScript">  (function () {
      if (!ObjC.available) {
          console.error("[-] ObjC runtime not available");
          return;
      }
    
      const NULL = ptr(0);
    
      // ========= 通用符号解析工具 =========
      function resolveSymbol(moduleHint, name) {
          var addr = null;
    
          try { addr = Module.findExportByName(moduleHint, name); } catch (e) {}
          if (!addr) {
              try { addr = Module.findExportByName(null, name); } catch (e) {}
          }
    
          if (addr) {
              console.log("[*] Resolved " + name + " @ " + addr);
          } else {
              console.error("[-] Failed to resolve symbol: " + name);
          }
          return addr;
      }
    
      // ====== 导出 C 函数（允许部分缺失） ======
      const func_create   = resolveSymbol("Security",       "SecCertificateCreateWithData");
      const func_summary  = resolveSymbol("Security",       "SecCertificateCopySubjectSummary");
      const func_copyData = resolveSymbol("Security",       "SecCertificateCopyData");
      const func_copyVals = resolveSymbol("Security",       "SecCertificateCopyValues");
      const func_release  = resolveSymbol("CoreFoundation", "CFRelease");
    
      // 这三个必须要有，否则没法干活
      if (!func_create || !func_copyData || !func_release) {
          console.error("[-] Critical symbols missing, abort.");
          return;
      }
    
      const SecCertificateCreateWithData = new NativeFunction(func_create, 'pointer', ['pointer', 'pointer']);
      const SecCertificateCopyData       = new NativeFunction(func_copyData, 'pointer', ['pointer']);
      const CFRelease                    = new NativeFunction(func_release, 'void',    ['pointer']);
    
      var SecCertificateCopySubjectSummary = null;
      if (func_summary) {
          SecCertificateCopySubjectSummary = new NativeFunction(func_summary, 'pointer', ['pointer']);
      }
    
      var SecCertificateCopyValues = null;
      if (func_copyVals) {
          SecCertificateCopyValues = new NativeFunction(func_copyVals, 'pointer', ['pointer', 'pointer', 'pointer']);
      }
    
      // ====== 小工具函数 ======
      function cfToObj(ref) {
          if (!ref || ref.isNull()) return null;
          try { return new ObjC.Object(ref); }
          catch (e) { return null; }
      }
    
      // 按标准 PEM 格式打印证书（每行 64 字符）
      function printPemFromDerNSData(derNSData) {
          if (!derNSData) return;
          var b64 = derNSData.base64EncodedStringWithOptions_(0).toString();
    
          console.log("-----BEGIN CERTIFICATE-----");
          for (var i = 0; i &lt; b64.length; i += 64) {
              console.log(b64.substr(i, 64));
          }
          console.log("-----END CERTIFICATE-----");
      }
    
      // 打印 NSData （返回 token 用）
      function dumpNSData(nsdata, label, maxHexBytes) {
          if (!nsdata) {
              console.log("[" + label + "] &lt;NSData is null&gt;");
              return;
          }
    
          var len = nsdata.length();
          console.log("[" + label + "] length: " + len + " bytes");
    
          // 1) Base64（全部）
          var b64 = nsdata.base64EncodedStringWithOptions_(0).toString();
          console.log("[" + label + "] base64:");
          for (var i = 0; i &lt; b64.length; i += 64) {
              console.log("    " + b64.substr(i, 64));
          }
    
          // 2) Hex（前 maxHexBytes）
          var limit = Math.min(len, maxHexBytes || 256);
          var bytesPtr = nsdata.bytes();
          var hex = "";
          for (var j = 0; j &lt; limit; j++) {
              var b = Memory.readU8(bytesPtr.add(j));
              var h = ("0" + b.toString(16)).slice(-2);
              hex += h + ((j + 1) % 16 === 0 ? "\n    " : " ");
          }
          console.log("[" + label + "] hex (first " + limit + " bytes):\n    " + hex);
      }
    
      function printCertDetail(certRef, label) {
          if (!certRef || certRef.isNull()) {
              console.log("[" + label + "] ❌ CertRef is NULL");
              return;
          }
    
          console.log("\n========== [" + label + "] ==========");
    
          // 1. Subject Summary（如果符号存在）
          if (SecCertificateCopySubjectSummary) {
              var summaryRef = SecCertificateCopySubjectSummary(certRef);
              var summaryObj = cfToObj(summaryRef);
              if (summaryObj) {
                  console.log("SubjectSummary : " + summaryObj.toString());
              } else {
                  console.log("SubjectSummary : &lt;null&gt;");
              }
              if (summaryRef &amp;&amp; !summaryRef.isNull()) CFRelease(summaryRef);
          } else {
              console.log("SubjectSummary : &lt;unavailable&gt;");
          }
    
          // 2. DER -&gt; PEM
          var derRef = SecCertificateCopyData(certRef);
          var derObj = cfToObj(derRef);
          if (derObj) {
              printPemFromDerNSData(derObj);
          } else {
              console.log("[!] SecCertificateCopyData failed");
          }
          if (derRef &amp;&amp; !derRef.isNull()) CFRelease(derRef);
    
          // 3. 详细 values（如果符号存在）
          if (SecCertificateCopyValues) {
              var valuesRef = SecCertificateCopyValues(certRef, NULL, NULL);
              var valuesObj = cfToObj(valuesRef);
              if (valuesObj) {
                  console.log("Values (raw NSDictionary):");
                  console.log("  " + valuesObj.toString());
              } else {
                  console.log("[!] SecCertificateCopyValues returned NULL");
              }
              if (valuesRef &amp;&amp; !valuesRef.isNull()) CFRelease(valuesRef);
          } else {
              console.log("Values : &lt;SecCertificateCopyValues unavailable&gt;");
          }
    
          console.log("========== [END] ==========\n");
      }
    
      // ====== Hook DCCertificateGenerator ======
      console.log("\n[*] Script Loaded. Waiting...");
    
      try {
          var className  = "DCCertificateGenerator";
          var methodName = "- _encryptData:serverSyncedDate:error:";
    
          if (!ObjC.classes[className]) {
              console.error("[-] Class " + className + " not found");
              return;
          }
          if (!ObjC.classes[className][methodName]) {
              console.error("[-] Method " + methodName + " not found on " + className);
              return;
          }
    
          var hook = ObjC.classes[className][methodName];
    
          Interceptor.attach(hook.implementation, {
              onEnter: function (args) {
                  console.warn("\n[+] 🟢 Intercepted Encryption Request");
    
                  // args[2] = _encryptData:  (NSData*)
                  var dataObj = cfToObj(args[2]);
                  if (!dataObj) {
                      console.log("  [Payload Info] dataObj is NULL or invalid");
                      return;
                  }
    
                  var len = dataObj.length();
                  console.log("  [Payload Info] Raw Length: " + len + " bytes");
    
                  // 先直接当 DER 证书试
                  var certRef = SecCertificateCreateWithData(NULL, dataObj.handle);
    
                  if (!certRef.isNull()) {
                      printCertDetail(certRef, "Original (DER)");
                      CFRelease(certRef);
                  } else {
                      console.log("  [Info] SecCertificateCreateWithData failed, trying PEM path...");
    
                      var pemStrObj = ObjC.classes.NSString
                          .alloc()
                          .initWithData_encoding_(dataObj, 4); // UTF8
    
                      if (pemStrObj) {
                          var strContent = pemStrObj.toString();
    
                          if (strContent.indexOf("BEGIN CERTIFICATE") !== -1) {
                              console.log("  [Original Cert] Format: PEM (Base64 Text)");
    
                              var matches = strContent.match(/-----BEGIN CERTIFICATE-----([\s\S]+?)-----END CERTIFICATE-----/g);
                              if (matches) {
                                  for (var i = 0; i &lt; matches.length; i++) {
                                      var block = matches[i];
                                      var cleanBase64 = block
                                          .replace(/-----BEGIN CERTIFICATE-----/g, "")
                                          .replace(/-----END CERTIFICATE-----/g, "")
                                          .replace(/[\r\n]/g, "");
    
                                      var derData = ObjC.classes.NSData
                                          .alloc()
                                          .initWithBase64EncodedString_options_(cleanBase64, 0);
    
                                      if (derData) {
                                          var certRef2 = SecCertificateCreateWithData(NULL, derData.handle);
                                          if (!certRef2.isNull()) {
                                              printCertDetail(certRef2, "Parsed PEM Cert #" + (i + 1));
                                              CFRelease(certRef2);
                                          } else {
                                              console.log("  [Parsed PEM Cert #" + (i + 1) + "] ❌ SecCertificateCreateWithData failed");
                                          }
                                      } else {
                                          console.log("  [Parsed PEM Cert #" + (i + 1) + "] ❌ initWithBase64EncodedString failed");
                                      }
                                  }
                              } else {
                                  console.log("  [Original Cert] PEM markers found but no blocks matched");
                              }
                          } else {
                              console.log("  [Original Cert] Format: Unknown Binary / Non-PEM text");
                          }
                      } else {
                          console.log("  [Original Cert] Cannot decode as UTF-8 string");
                      }
                  }
              },
    
              onLeave: function (retval) {
                  if (retval.isNull()) {
                      console.log("[+] Return value is NULL");
                      return;
                  }
    
                  try {
                      var retObj = new ObjC.Object(retval);
                      var clsName = retObj.$className || "&lt;unknown&gt;";
                      console.log("[+] Return ObjC class: " + clsName);
    
                      if (retObj.isKindOfClass_(ObjC.classes.NSData)) {
                          // 返回 NSData（大概率是加密后的 token）
                          dumpNSData(retObj, "Return NSData", 256);
                      } else if (retObj.isKindOfClass_(ObjC.classes.NSString)) {
                          console.log("[+] Return NSString:");
                          console.log("    " + retObj.toString());
                      } else {
                          console.log("[+] Return object description:");
                          console.log("    " + retObj.toString());
                      }
                  } catch (e) {
                      console.log("[!] onLeave: cannot wrap retval as ObjC.Object -&gt; " + e.message);
                  }
              }
          });
    
          console.log("[*] Hook Success");
      } catch (e) {
          console.error("[!] Hook Error: " + e.message);
      }
  })();
    
  // frida -U -n devicecheckd -l devicecheckd1.js
</code></pre>
  </li>
</ul>

<h2 id="五在爬虫--群控场景中的实战应用"><strong>五、在爬虫 / 群控场景中的实战应用</strong></h2>

<p>在理解了底层加密原理和 基本流程后，我们需要将视角拉回到业务对抗的一线。在黑灰产（爬虫方）与风控（防守方）的博弈中，DeviceCheck 不仅仅是一个 API，而是判定设备“生死”的判官。</p>

<h3 id="51爬虫方怎么用真机--devicecheck-规避风控">5.1、<strong>爬虫方怎么用真机 + DeviceCheck 规避风控”</strong></h3>

<p>由于 DeviceCheck 的核心密钥（ECDH 协商用的临时私钥）被锁在 Secure Enclave 硬件中，纯算法还原（脱机算法）的路径已被彻底堵死。目前的黑灰产已全面转向<strong>“真机群控”</strong>与<strong>“底层 Hook”</strong>相结合的模式。</p>

<h3 id="借尸还魂证书与身份的批量伪造">“借尸还魂”：证书与身份的批量伪造</h3>

<p>攻击者建立一个庞大的“白名单设备库”，这些设备是真实存在的、干净的 iPhone。</p>

<ul>
  <li>
    <p><strong>采集阶段</strong>：使用越狱设备或硬改工具，批量提取白名单设备的 <code class="language-plaintext highlighter-rouge">Device Certificate</code> (PEM) 和 <code class="language-plaintext highlighter-rouge">UniqueChipID</code> 等硬件指纹。</p>
  </li>
  <li>
    <p><strong>攻击阶段</strong>：在被封禁的设备（或专门用于攻击的“脏”设备）上运行 Frida 脚本（如我们之前编写的脚本）。</p>
  </li>
  <li>
    <p><strong>运行时篡改</strong>：</p>

    <ul>
      <li>
        <p>Hook <code class="language-plaintext highlighter-rouge">devicecheckd</code> 进程。</p>
      </li>
      <li>
        <p>在生成 Token 前，将内存中的“脏证书”替换为采集来的“白证书”。</p>
      </li>
      <li>
        <p><strong>核心突破</strong>：虽然硬件签名（SE）使用的是脏设备的私钥，但只要 Hook 了 <code class="language-plaintext highlighter-rouge">DCContext</code> 伪造 AppID，生成的 Token 在逻辑格式上是合法的。</p>
      </li>
    </ul>

    <p>### “RPC 签名节点”：群控架构的演进</p>

    <p>为了规模化作业，攻击者不再在每台手机上跑业务逻辑，而是将其改造为<strong>签名节点</strong>。</p>

    <ul>
      <li>
        <p><strong>架构</strong>：</p>

        <ul>
          <li>
            <p><strong>服务器端爬虫</strong>：负责跑业务逻辑（HTTP 请求），遇到需要 DeviceToken 的接口时，向手机发送 RPC 请求。</p>
          </li>
          <li>
            <p><strong>手机端（Agent）</strong>：运行在后台，接收 RPC 指令，调用 <code class="language-plaintext highlighter-rouge">DCDevice</code> 生成 Token（配合上述的 Hook 伪造策略），然后将 Token 返回给<strong>服务器端</strong>。</p>
          </li>
        </ul>
      </li>
      <li>
        <p><strong>优势</strong>：一台真机可以为十几个线程提供签名服务，极大地降低了硬件成本。</p>
      </li>
    </ul>
  </li>
</ul>

<h3 id="52防守方怎么用-devicecheck-收拾真机爬虫"><strong>5.2、防守方怎么用 DeviceCheck 收拾真机爬虫”</strong></h3>

<p>面对上述攻击，防守方并非束手无策。正确使用 DeviceCheck 的服务端能力，可以对真机群控造成降维打击。</p>

<h3 id="强制开启双重验证">强制开启“双重验证”</h3>

<p>绝不能仅仅依赖“Token 能否被解码”来判断请求合法性。必须调用 Apple 的 Query/Update API 并检查返回值。</p>

<ul>
  <li><strong>检查报错</strong>：如果 Apple校验token API 返回错误，直接封禁该请求。这说明攻击者试图在 A 设备上使用 B 设备的证书（移花接木攻击），这是目前 Hook 方案无法解决的死穴。</li>
</ul>

<h3 id="善用-2-bit-状态标记持久化封禁">善用 2-bit 状态标记（持久化封禁）</h3>

<p>DeviceCheck 最强大的地方在于<strong>状态的硬件绑定性</strong>。</p>

<ul>
  <li>
    <p><strong>策略</strong>：当风控系统检测到某账号存在异常行为（如高频请求、脚本操作）时，不要仅仅封禁账号（UID），而是立即调用 DeviceCheck 的 <code class="language-plaintext highlighter-rouge">update_two_bits</code> 接口，将该设备的 <strong>Bit 1 置为 1</strong>（标记为“黑名单设备”）。</p>
  </li>
  <li>
    <p><strong>效果</strong>：</p>

    <ul>
      <li>
        <p>攻击者即使卸载 App、重装 App，Bit 1 的状态依然是 1。</p>
      </li>
      <li>
        <p>攻击者即使抹掉 iPhone 恢复出厂设置（在未更换主板的情况下），Apple 服务器上的 Bit 1 状态依然保留。</p>
      </li>
      <li>
        <p><strong>击杀点</strong>：这迫使攻击者必须更换物理硬件（硬改）/或更激活证书才能清洗设备状态，极大地拉高了攻击成本。</p>
      </li>
    </ul>
  </li>
</ul>

<h3 id="时间戳与频率风控">时间戳与频率风控</h3>

<ul>
  <li>
    <p><strong>时间窗口</strong>：严格校验 Token 内部解密出的 <code class="language-plaintext highlighter-rouge">timestamp</code>。如果时间戳是 2 天前的，或者与当前服务器时间偏差超过 5 分钟，直接拒绝。</p>
  </li>
  <li>
    <p><strong>频率限制</strong>：对同一个 Device Token（即使它是合法的）的提交频率做限制。正常的真机用户不会在 1 秒内生成 10 次 Token。</p>
  </li>
</ul>

<h2 id="六总结">六、总结</h2>

<p>iOS DeviceCheck（及 App Attest）代表了移动安全领域的一个重要转折点：<strong>从“软件对抗”走向“硬件对抗”</strong>。</p>

<p>通过本次逆向分析，我们得出以下核心结论：</p>

<ol>
  <li>
    <p><strong>硬件是信任的基石</strong>：DeviceCheck 的不可伪造性完全依赖于 Secure Enclave。私钥不出安全区，使得纯软件层面的算法还原（如 Python 脚本生成 Token）在理论上和工程上都成为不可能。</p>
  </li>
  <li>
    <p><strong>攻防的不对称性</strong>：</p>

    <ul>
      <li>
        <p><strong>攻击者</strong>不得不通过昂贵的真机群控、复杂的系统级 Hook（注入 <code class="language-plaintext highlighter-rouge">devicecheckd</code>）以及硬改（更换基带/闪存芯片/激活证书）来绕过防护，成本呈指数级上升。</p>
      </li>
      <li>
        <p><strong>防守方</strong>只需在服务端接入 Apple 的验证 API，并制定合理的 2-bit 状态标记策略，即可获得一个难以被清洗的设备指纹。</p>
      </li>
    </ul>
  </li>
  <li>
    <p><strong>未来的趋势</strong>：随着 Apple 在 iOS 14+ 引入 <code class="language-plaintext highlighter-rouge">DCAppAttestService</code>，将密钥的绑定粒度从“设备”细化到了“设备 + App 实例”，并且支持对关键业务数据（Assertion）进行签名。这意味着未来的风控将更加精准，留给黑灰产“撞库”和“伪造”的空间将进一步被压缩。</p>
  </li>
</ol>

<p><strong>对于安全从业者而言</strong>，理解 DeviceCheck 的底层逻辑，不仅有助于在红蓝对抗中制定更有效的策略，也提醒我们：在零信任架构下，基于硬件的可信证明（Hardware-backed Attestation）将是移动端身份认证的终极解决方案。</p>

<p>参考资料：</p>

<p>https://bbs.kanxue.com/thread-281819.htm</p>

<p>https://iosre.com/t/%E8%BD%AC%E8%BD%BD%E6%9F%90dd%E6%9F%90%E9%9F%B3-ios-devicecheck-token-%E6%AD%A3%E5%90%91%E7%A0%94%E5%8F%91%E4%B8%8E%E9%80%86%E5%90%91%E5%88%86%E6%9E%90%E4%B9%8B%E8%B7%AF%E6%80%9D%E8%80%83/24569/1</p>

<p>致谢: <a href="https://bbs.kanxue.com/user-home-757881.htm"><strong>yuzhouheike</strong></a>**  addhaloka 感谢两位大佬提供的指导与帮助**</p>]]></content><author><name>我是小三</name></author><category term="iOS" /><category term="iOS" /><category term="DeviceCheck" /><category term="逆向" /><category term="风控" /><summary type="html"><![CDATA[从逆向视角剖析 iOS DeviceCheck / App Attest 的设备身份链与加密流程，以及越狱环境下的攻防对抗。]]></summary></entry><entry><title type="html">iOS 无痕 Hook：基于硬件断点（ARM Debug 寄存器）的实现与实战</title><link href="https://xiaosan-re.github.io/2026/06/05/ios-hardware-breakpoint-hook/" rel="alternate" type="text/html" title="iOS 无痕 Hook：基于硬件断点（ARM Debug 寄存器）的实现与实战" /><published>2026-06-05T00:00:00+08:00</published><updated>2026-06-05T00:00:00+08:00</updated><id>https://xiaosan-re.github.io/2026/06/05/ios-hardware-breakpoint-hook</id><content type="html" xml:base="https://xiaosan-re.github.io/2026/06/05/ios-hardware-breakpoint-hook/"><![CDATA[<h2 id="一背景--场景">一、背景 &amp; 场景</h2>

<h3 id="11-环境">1.1 环境</h3>

<p>设备型号：iPhone 14、iOS 版本：16.x、越狱方案：Dopamine（rootless，<code class="language-plaintext highlighter-rouge">/var/jb</code>）</p>

<p>工具链：Theos、IDA Pro 9.2、ellekit</p>

<h3 id="12-引言">1.2 引言</h3>

<p>做 iOS 逆向，hook 是绕不开的基本功。主流方案——Substrate / Dobby / fishhook——本质都是 <strong>inline hook</strong>：改写目标函数开头几条指令，跳到自己的 trampoline。这套在大多数场景够用，但碰到<strong>带完整性校验</strong>的目标就难受了：</p>

<ul>
  <li>改了 <code class="language-plaintext highlighter-rouge">__text</code> 段，App 自己做的 CRC / 段哈希一算就对不上；</li>
  <li>iOS 的 W^X 和代码签名让”把跳板写进代码段”本身就要折腾 <code class="language-plaintext highlighter-rouge">MAP_JIT</code>；</li>
  <li>函数头的 <code class="language-plaintext highlighter-rouge">B</code> / <code class="language-plaintext highlighter-rouge">LDR PC</code> 跳板有明显特征，反 hook 扫描一眼就能发现。</li>
</ul>

<p>有没有一种 hook <strong>完全不碰目标内存</strong>？有——<strong>硬件断点</strong>。它是 CPU 里专门的地址比较器，命中后产生调试异常，全程不改一个字节的指令。代价是数量有限（arm64 通常 6 个指令断点）、且强依赖异常机制。</p>

<p>本文从 <strong>lldb debugserver 的寄存器填法</strong>出发，自己撸一套 iOS arm64(e) 进程内硬件断点 hook 框架，封装成越狱插件，最后实战 hook 某加固 App 的设备信息加密函数，抓出明文 JSON 和每次调用的密钥。（文中目标 App 名、专有符号、采集到的指纹/密钥/明文等均已脱敏。）</p>

<h3 id="13-对抗场景安全-sdk-的-hook-检测">1.3 对抗场景：安全 SDK 的 hook 检测</h3>

<p>直接的动机来自实战：目标 App 集成了一套商业安全 SDK（做设备风控 / 反作弊），它<strong>自带 hook 检测</strong>。一旦发现自己被 hook，轻则在上报里打一个”环境异常”标记把你拉黑，重则直接返回脏数据、让你逆出来的算法对不上。常见的检测手段有：</p>

<ul>
  <li><strong>代码完整性校验</strong>：对自己的 <code class="language-plaintext highlighter-rouge">__text</code> 段做 CRC / 哈希，inline hook 改了开头几条指令 → 校验失败；</li>
  <li><strong>函数序言扫描</strong>：检查关键函数开头是不是 <code class="language-plaintext highlighter-rouge">B</code> / <code class="language-plaintext highlighter-rouge">LDR PC</code> / <code class="language-plaintext highlighter-rouge">BR Xn</code> 这类跳板特征；</li>
  <li><strong>Frida / 注入特征</strong>：扫 <code class="language-plaintext highlighter-rouge">frida-gum</code>、<code class="language-plaintext highlighter-rouge">gadget</code>、可疑 <code class="language-plaintext highlighter-rouge">DYLD_INSERT_LIBRARIES</code>、异常的内存可执行区、监听端口等；</li>
  <li><strong>符号/IMP 校验</strong>：对比 ObjC method 的 IMP 是否指向自己模块之外（抓 method swizzle）。</li>
</ul>

<p>可以看到，这些检测<strong>全部针对”代码/内存被改”或”有外来注入特征”</strong>。于是思路很自然：</p>

<blockquote>
  <p>用一种<strong>完全不改目标代码、不留 trampoline、不依赖 Frida</strong> 的 hook，绕过上面所有针对”改内存 / 注入特征”的检测——这正是<strong>硬件断点</strong>的主场。它的命中靠 CPU 寄存器比较，目标的指令、method 表一个字节都不动，前 4 类检测全部失效。残留的检测面只剩”调试寄存器是否被设置”和”异常端口是否被占”，对抗收敛到这两点（见第六节）。</p>
</blockquote>

<h3 id="14-研究动机与目标">1.4 研究动机与目标</h3>

<ul>
  <li>搞清楚 iOS 上硬件断点到底<strong>怎么落到寄存器、怎么经内核分发到用户态</strong>；</li>
  <li>自己实现一套<strong>无痕 hook</strong>，绕过安全 SDK 针对 inline-hook / 注入特征的检测；</li>
  <li>在真实加固 App（带反插桩与 hook 检测的商业安全 SDK）上验证可用性与稳定性。</li>
</ul>

<h2 id="二硬件断点原理">二、硬件断点原理</h2>

<h3 id="21-三层机制">2.1 三层机制</h3>

<p>iOS（arm64）上的硬件断点，本质是 <strong>ARMv8 Debug 架构的断点寄存器</strong> + <strong>XNU 把它封装成 thread state + Mach 异常</strong>。分三层：</p>

<p><strong>① 底层：CoreSight / ARMv8 调试寄存器</strong></p>

<p>硬件断点不是软件断点那种改指令写 <code class="language-plaintext highlighter-rouge">BRK</code>，而是 CPU 里专门的比较器：</p>

<ul>
  <li>指令断点：<code class="language-plaintext highlighter-rouge">DBGBVR&lt;n&gt;_EL1</code>（存地址）+ <code class="language-plaintext highlighter-rouge">DBGBCR&lt;n&gt;_EL1</code>（控制/使能）</li>
  <li>数据观察点：<code class="language-plaintext highlighter-rouge">DBGWVR&lt;n&gt;_EL1</code> + <code class="language-plaintext highlighter-rouge">DBGWCR&lt;n&gt;_EL1</code></li>
</ul>

<p>数量由 <code class="language-plaintext highlighter-rouge">ID_AA64DFR0_EL1</code> 报告，苹果 A 系列大核典型是 <strong>6 个指令断点 + 4 个观察点</strong>。命中后 CPU 产生一个<strong>同步 Debug 异常</strong>。</p>

<p><code class="language-plaintext highlighter-rouge">DBGBCR</code> 关键位：</p>

<table>
  <thead>
    <tr>
      <th>位</th>
      <th>名称</th>
      <th>含义</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>bit0</td>
      <td>E</td>
      <td>使能</td>
    </tr>
    <tr>
      <td>bits[2:1]</td>
      <td>PMC</td>
      <td>特权级匹配，EL0（用户态）填 <code class="language-plaintext highlighter-rouge">0b10</code></td>
    </tr>
    <tr>
      <td>bits[8:5]</td>
      <td>BAS</td>
      <td>Byte Address Select，AArch64 下填 <code class="language-plaintext highlighter-rouge">0b1111</code></td>
    </tr>
  </tbody>
</table>

<p><strong>② 内核层：XNU 不让用户态直接写 EL1 寄存器</strong></p>

<p>这些是 EL1 特权寄存器，App（EL0）不能直接 <code class="language-plaintext highlighter-rouge">MSR</code>。XNU 用 <code class="language-plaintext highlighter-rouge">arm_debug_state64_t</code> 做中转：</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">struct</span> <span class="n">arm_debug_state64</span> <span class="p">{</span>
    <span class="n">__uint64_t</span> <span class="n">bvr</span><span class="p">[</span><span class="mi">16</span><span class="p">];</span>   <span class="c1">// 断点地址</span>
    <span class="n">__uint32_t</span> <span class="n">bcr</span><span class="p">[</span><span class="mi">16</span><span class="p">];</span>   <span class="c1">// 断点控制</span>
    <span class="n">__uint64_t</span> <span class="n">wvr</span><span class="p">[</span><span class="mi">16</span><span class="p">];</span>   <span class="c1">// 观察点地址</span>
    <span class="n">__uint32_t</span> <span class="n">wcr</span><span class="p">[</span><span class="mi">16</span><span class="p">];</span>   <span class="c1">// 观察点控制</span>
    <span class="n">__uint64_t</span> <span class="n">mdscr_el1</span><span class="p">;</span> <span class="c1">// 单步等</span>
<span class="p">};</span>
</code></pre></div></div>

<p>通过 <code class="language-plaintext highlighter-rouge">thread_set_state(thread, ARM_DEBUG_STATE64, ...)</code> 写进去，线程被调度回 CPU 时内核把这些值真正 load 进 <code class="language-plaintext highlighter-rouge">DBGBVR/DBGBCR</code>。</p>

<p><strong>③ 异常分发：走 Mach 异常，不是信号</strong></p>

<p>这是 iOS 和 Linux 最大的区别。命中后 XNU 把 Debug 异常转成 <strong>Mach 异常 <code class="language-plaintext highlighter-rouge">EXC_BREAKPOINT</code></strong>，投递给注册的异常端口：</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">thread_set_exception_ports</span><span class="p">(</span><span class="kr">thread</span><span class="p">,</span> <span class="n">EXC_MASK_BREAKPOINT</span><span class="p">,</span> <span class="n">exc_port</span><span class="p">,</span>
        <span class="n">EXCEPTION_STATE_IDENTITY</span> <span class="o">|</span> <span class="n">MACH_EXCEPTION_CODES</span><span class="p">,</span> <span class="n">ARM_THREAD_STATE64</span><span class="p">);</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">ptrace</code> 在 iOS 上残废，真正的调试控制全靠 <strong>Mach 异常 + thread state</strong>。所以硬件断点完全不经过 <code class="language-plaintext highlighter-rouge">ptrace</code>。</p>

<h3 id="22-为什么叫无痕">2.2 为什么叫”无痕”</h3>

<ol>
  <li><strong>目标代码段一个字节都不改</strong> → 过 <code class="language-plaintext highlighter-rouge">__text</code> CRC、段哈希、代码签名、W^X；</li>
  <li><strong>不依赖 trampoline</strong> → 没有可被扫描的 <code class="language-plaintext highlighter-rouge">B</code> / <code class="language-plaintext highlighter-rouge">LDR PC</code> 跳板特征；</li>
  <li>检测面只剩两个：<strong>调试寄存器本身</strong>（对方读 <code class="language-plaintext highlighter-rouge">bcr</code> 能发现非零）和<strong>异常端口被占</strong>（<code class="language-plaintext highlighter-rouge">thread_get_exception_ports</code> 能看到）。对抗就发生在这两点。</li>
</ol>

<h2 id="三引擎实现">三、引擎实现</h2>

<p>整体流程分<strong>注入期</strong>（把断点和异常处理铺好）和<strong>运行期</strong>（命中后处理并踏过断点）两段，全程不改目标一个字节的指令：</p>

<p><img src="/images/posts/ios/hbhook/flow.png" alt="硬件断点 hook 整体流程" /></p>

<p>文字版同样的流程：</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>═══════════════ 注入期 (越狱框架 dlopen 进目标进程 → %ctor) ═══════════════

  ① task_set_exception_ports(EXC_MASK_BREAKPOINT, 我们的端口)   接管断点异常
  ② pthread_create ─► 异常服务器线程 (跑 mach_msg 接收循环)
  ③ 对所有线程 thread_set_state(ARM_DEBUG_STATE64):
        DBGBVR[n] = 目标函数地址 (strip PAC, 4字节对齐)
        DBGBCR[n] = BAS | PMC_EL0 | ENABLE
     + watcher 线程: 每 200ms 给「新出现的线程」补下断 (硬件断点 per-thread)

═══════════════════════════ 运行期 ═══════════════════════════

   目标线程执行到被下断的地址:  PC == DBGBVR[n]
            │  CPU 地址比较器命中 (指令不变,不触发任何完整性/反hook检测)
            ▼
     产生同步 Debug 异常  ──►  XNU 转成 Mach 异常 EXC_BREAKPOINT
            │  (目标线程挂起,等待异常处理回复)         │
            │                                          ▼
            │                          投递到我们注册的异常端口
            ▼                                          │
   ┌──────────────────────── 异常服务器线程 ────────────────────────┐
   │  mach_msg 收到消息 (携带 ARM_THREAD_STATE64 即寄存器现场)       │
   │     │                                                          │
   │     ├─ 这是「单步」产生的异常? ─是─► 复原断点 + 关单步 ──┐     │
   │     │                                                    │     │
   │     └─ 否,是断点命中                                      │     │
   │          │                                               │     │
   │          ▼  调用回调 cb(state): 读/改 x0~x30 / pc / sp    │     │
   │          │     (vm_read_overwrite 安全 dump 参数,不崩)    │     │
   │          ▼                                               │     │
   │      回调把 PC 改走了? ─是─► 直接放行 (无需单步) ────────┤     │
   │          │ 否                                            │     │
   │          ▼  踏过断点: 禁用本断点 E 位 + 置 mdscr.SS+cpsr.SS│     │
   └──────────│──────────────────────────────────────────────│─────┘
              │  把(可能改过的)寄存器回填 new_state            │
              ▼  内核据此恢复目标线程                          │
   目标线程单步执行命中处那一条指令 ──► 再次触发 EXC_BREAKPOINT │
              │                                                │
              └────────────────────────────────────────────────┘
                     (回到服务器,走上面「单步异常」分支:复原断点、关单步)
              │
              ▼
        目标线程继续正常执行,App 毫无察觉
</code></pre></div></div>

<p>寄存器位怎么填、单步怎么搞，参考 lldb 的 <code class="language-plaintext highlighter-rouge">DNBArchImplARM64.cpp</code>（<code class="language-plaintext highlighter-rouge">EnableHardwareBreakpoint</code> / 单步逻辑）。</p>

<h3 id="31-下断点">3.1 下断点</h3>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// arm64 调试架构常量(参考 lldb DNBArchImplARM64)</span>
<span class="cp">#define BCR_ENABLE   (1u &lt;&lt; 0)        // E:  使能
#define BCR_PMC_EL0  (0b10u &lt;&lt; 1)     // PMC: 在 EL0(用户态)匹配
#define BCR_BAS_ALL  (0b1111u &lt;&lt; 5)   // BAS: AArch64 下全 4 字节
#define MDSCR_SS     (1u &lt;&lt; 0)        // MDSCR_EL1.SS: 软件单步使能
#define PSTATE_SS    (1u &lt;&lt; 21)       // CPSR/PSTATE.SS
</span>
<span class="k">static</span> <span class="kt">void</span> <span class="nf">apply_bps_to_thread</span><span class="p">(</span><span class="n">mach_port_t</span> <span class="n">th</span><span class="p">)</span> <span class="p">{</span>
    <span class="n">arm_debug_state64_t</span> <span class="n">d</span><span class="p">;</span>
    <span class="n">mach_msg_type_number_t</span> <span class="n">c</span> <span class="o">=</span> <span class="n">ARM_DEBUG_STATE64_COUNT</span><span class="p">;</span>
    <span class="n">thread_get_state</span><span class="p">(</span><span class="n">th</span><span class="p">,</span> <span class="n">ARM_DEBUG_STATE64</span><span class="p">,</span> <span class="p">(</span><span class="n">thread_state_t</span><span class="p">)</span><span class="o">&amp;</span><span class="n">d</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">c</span><span class="p">);</span>
    <span class="k">for</span> <span class="p">(</span><span class="kt">int</span> <span class="n">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">i</span> <span class="o">&lt;</span> <span class="n">MAX_HW_BP</span><span class="p">;</span> <span class="n">i</span><span class="o">++</span><span class="p">)</span> <span class="p">{</span>
        <span class="k">if</span> <span class="p">(</span><span class="n">g_bps</span><span class="p">[</span><span class="n">i</span><span class="p">].</span><span class="n">used</span><span class="p">)</span> <span class="p">{</span>
            <span class="n">d</span><span class="p">.</span><span class="n">__bvr</span><span class="p">[</span><span class="n">i</span><span class="p">]</span> <span class="o">=</span> <span class="n">g_bps</span><span class="p">[</span><span class="n">i</span><span class="p">].</span><span class="n">addr</span> <span class="o">&amp;</span> <span class="o">~</span><span class="mi">3ULL</span><span class="p">;</span>            <span class="c1">// 4 字节对齐</span>
            <span class="n">d</span><span class="p">.</span><span class="n">__bcr</span><span class="p">[</span><span class="n">i</span><span class="p">]</span> <span class="o">=</span> <span class="n">BCR_BAS_ALL</span> <span class="o">|</span> <span class="n">BCR_PMC_EL0</span> <span class="o">|</span> <span class="n">BCR_ENABLE</span><span class="p">;</span>
        <span class="p">}</span>
    <span class="p">}</span>
    <span class="n">thread_set_state</span><span class="p">(</span><span class="n">th</span><span class="p">,</span> <span class="n">ARM_DEBUG_STATE64</span><span class="p">,</span> <span class="p">(</span><span class="n">thread_state_t</span><span class="p">)</span><span class="o">&amp;</span><span class="n">d</span><span class="p">,</span> <span class="n">ARM_DEBUG_STATE64_COUNT</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>

<blockquote>
  <p>注意 arm64e 上函数指针带 PAC，下断前要 <code class="language-plaintext highlighter-rouge">ptrauth_strip</code>：</p>
  <div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">static</span> <span class="kt">uint64_t</span> <span class="nf">strip_pac</span><span class="p">(</span><span class="kt">uint64_t</span> <span class="n">p</span><span class="p">)</span> <span class="p">{</span>
<span class="cp">#if __has_feature(ptrauth_calls)
</span>    <span class="k">return</span> <span class="p">(</span><span class="kt">uint64_t</span><span class="p">)</span><span class="n">ptrauth_strip</span><span class="p">((</span><span class="kt">void</span> <span class="o">*</span><span class="p">)</span><span class="n">p</span><span class="p">,</span> <span class="n">ptrauth_key_function_pointer</span><span class="p">);</span>
<span class="cp">#else
</span>    <span class="k">return</span> <span class="n">p</span><span class="p">;</span>
<span class="cp">#endif
</span><span class="p">}</span>
</code></pre></div>  </div>
</blockquote>

<h3 id="32-mach-异常服务器手写消息免-mig">3.2 Mach 异常服务器（手写消息，免 MIG）</h3>

<p>注册异常端口，起一个线程跑 <code class="language-plaintext highlighter-rouge">mach_msg</code> 接收循环。为了单文件自包含，这里<strong>不用 mig</strong>，直接手写 <code class="language-plaintext highlighter-rouge">mach_exception_raise_state_identity</code>（id=2407）的请求/应答结构：</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cp">#pragma pack(4)
</span><span class="k">typedef</span> <span class="k">struct</span> <span class="p">{</span>
    <span class="n">mach_msg_header_t</span>          <span class="n">Head</span><span class="p">;</span>
    <span class="n">mach_msg_body_t</span>            <span class="n">msgh_body</span><span class="p">;</span>
    <span class="n">mach_msg_port_descriptor_t</span> <span class="kr">thread</span><span class="p">;</span>
    <span class="n">mach_msg_port_descriptor_t</span> <span class="n">task</span><span class="p">;</span>
    <span class="n">NDR_record_t</span>               <span class="n">NDR</span><span class="p">;</span>
    <span class="n">exception_type_t</span>           <span class="n">exception</span><span class="p">;</span>
    <span class="n">mach_msg_type_number_t</span>     <span class="n">codeCnt</span><span class="p">;</span>
    <span class="kt">int64_t</span>                    <span class="n">code</span><span class="p">[</span><span class="mi">2</span><span class="p">];</span>
    <span class="kt">int</span>                        <span class="n">flavor</span><span class="p">;</span>
    <span class="n">mach_msg_type_number_t</span>     <span class="n">old_stateCnt</span><span class="p">;</span>
    <span class="n">natural_t</span>                  <span class="n">old_state</span><span class="p">[</span><span class="n">THREAD_STATE_MAX</span><span class="p">];</span>
<span class="p">}</span> <span class="n">exc_request_t</span><span class="p">;</span>

<span class="k">typedef</span> <span class="k">struct</span> <span class="p">{</span>
    <span class="n">mach_msg_header_t</span>       <span class="n">Head</span><span class="p">;</span>
    <span class="n">NDR_record_t</span>           <span class="n">NDR</span><span class="p">;</span>
    <span class="n">kern_return_t</span>          <span class="n">RetCode</span><span class="p">;</span>
    <span class="kt">int</span>                    <span class="n">flavor</span><span class="p">;</span>
    <span class="n">mach_msg_type_number_t</span> <span class="n">new_stateCnt</span><span class="p">;</span>
    <span class="n">natural_t</span>              <span class="n">new_state</span><span class="p">[</span><span class="n">THREAD_STATE_MAX</span><span class="p">];</span>
<span class="p">}</span> <span class="n">exc_reply_t</span><span class="p">;</span>
<span class="cp">#pragma pack()
</span></code></pre></div></div>

<p>用 <code class="language-plaintext highlighter-rouge">EXCEPTION_STATE_IDENTITY</code>，内核会把 <strong>thread state 直接放进消息</strong>，我们改完 <code class="language-plaintext highlighter-rouge">old_state→new_state</code> 内核就用它恢复线程——这是无痕的关键，全程不碰目标内存：</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">static</span> <span class="kt">void</span> <span class="o">*</span><span class="nf">exc_server_thread</span><span class="p">(</span><span class="kt">void</span> <span class="o">*</span><span class="n">arg</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">for</span> <span class="p">(;;)</span> <span class="p">{</span>
        <span class="n">exc_request_t</span> <span class="n">req</span><span class="p">;</span> <span class="n">exc_reply_t</span> <span class="n">rep</span><span class="p">;</span>
        <span class="n">memset</span><span class="p">(</span><span class="o">&amp;</span><span class="n">req</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="k">sizeof</span><span class="p">(</span><span class="n">req</span><span class="p">));</span>
        <span class="n">mach_msg</span><span class="p">(</span><span class="o">&amp;</span><span class="n">req</span><span class="p">.</span><span class="n">Head</span><span class="p">,</span> <span class="n">MACH_RCV_MSG</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="k">sizeof</span><span class="p">(</span><span class="n">req</span><span class="p">),</span>
                 <span class="n">g_exc_port</span><span class="p">,</span> <span class="n">MACH_MSG_TIMEOUT_NONE</span><span class="p">,</span> <span class="n">MACH_PORT_NULL</span><span class="p">);</span>

        <span class="n">mach_port_t</span> <span class="kr">thread</span> <span class="o">=</span> <span class="n">req</span><span class="p">.</span><span class="kr">thread</span><span class="p">.</span><span class="n">name</span><span class="p">;</span>
        <span class="n">arm_thread_state64_t</span> <span class="o">*</span><span class="n">st</span> <span class="o">=</span> <span class="p">(</span><span class="n">arm_thread_state64_t</span> <span class="o">*</span><span class="p">)</span><span class="n">req</span><span class="p">.</span><span class="n">old_state</span><span class="p">;</span>
        <span class="n">kern_return_t</span> <span class="n">rc</span> <span class="o">=</span> <span class="n">handle_exception</span><span class="p">(</span><span class="kr">thread</span><span class="p">,</span> <span class="n">req</span><span class="p">.</span><span class="n">exception</span><span class="p">,</span> <span class="n">st</span><span class="p">);</span>

        <span class="n">memset</span><span class="p">(</span><span class="o">&amp;</span><span class="n">rep</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="k">sizeof</span><span class="p">(</span><span class="n">rep</span><span class="p">));</span>
        <span class="n">rep</span><span class="p">.</span><span class="n">Head</span><span class="p">.</span><span class="n">msgh_bits</span> <span class="o">=</span> <span class="n">MACH_MSGH_BITS</span><span class="p">(</span><span class="n">MACH_MSGH_BITS_REMOTE</span><span class="p">(</span><span class="n">req</span><span class="p">.</span><span class="n">Head</span><span class="p">.</span><span class="n">msgh_bits</span><span class="p">),</span> <span class="mi">0</span><span class="p">);</span>
        <span class="n">rep</span><span class="p">.</span><span class="n">Head</span><span class="p">.</span><span class="n">msgh_remote_port</span> <span class="o">=</span> <span class="n">req</span><span class="p">.</span><span class="n">Head</span><span class="p">.</span><span class="n">msgh_remote_port</span><span class="p">;</span>
        <span class="n">rep</span><span class="p">.</span><span class="n">Head</span><span class="p">.</span><span class="n">msgh_id</span> <span class="o">=</span> <span class="n">req</span><span class="p">.</span><span class="n">Head</span><span class="p">.</span><span class="n">msgh_id</span> <span class="o">+</span> <span class="mi">100</span><span class="p">;</span>   <span class="c1">// 2407 -&gt; 2507</span>
        <span class="n">rep</span><span class="p">.</span><span class="n">NDR</span> <span class="o">=</span> <span class="n">req</span><span class="p">.</span><span class="n">NDR</span><span class="p">;</span> <span class="n">rep</span><span class="p">.</span><span class="n">RetCode</span> <span class="o">=</span> <span class="n">rc</span><span class="p">;</span> <span class="n">rep</span><span class="p">.</span><span class="n">flavor</span> <span class="o">=</span> <span class="n">req</span><span class="p">.</span><span class="n">flavor</span><span class="p">;</span>
        <span class="k">if</span> <span class="p">(</span><span class="n">rc</span> <span class="o">==</span> <span class="n">KERN_SUCCESS</span><span class="p">)</span> <span class="p">{</span>
            <span class="n">rep</span><span class="p">.</span><span class="n">new_stateCnt</span> <span class="o">=</span> <span class="n">req</span><span class="p">.</span><span class="n">old_stateCnt</span><span class="p">;</span>
            <span class="n">memcpy</span><span class="p">(</span><span class="n">rep</span><span class="p">.</span><span class="n">new_state</span><span class="p">,</span> <span class="n">req</span><span class="p">.</span><span class="n">old_state</span><span class="p">,</span> <span class="n">req</span><span class="p">.</span><span class="n">old_stateCnt</span> <span class="o">*</span> <span class="k">sizeof</span><span class="p">(</span><span class="n">natural_t</span><span class="p">));</span>
            <span class="n">rep</span><span class="p">.</span><span class="n">Head</span><span class="p">.</span><span class="n">msgh_size</span> <span class="o">=</span> <span class="n">offsetof</span><span class="p">(</span><span class="n">exc_reply_t</span><span class="p">,</span> <span class="n">new_state</span><span class="p">)</span>
                               <span class="o">+</span> <span class="n">req</span><span class="p">.</span><span class="n">old_stateCnt</span> <span class="o">*</span> <span class="k">sizeof</span><span class="p">(</span><span class="n">natural_t</span><span class="p">);</span>
        <span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
            <span class="n">rep</span><span class="p">.</span><span class="n">Head</span><span class="p">.</span><span class="n">msgh_size</span> <span class="o">=</span> <span class="n">offsetof</span><span class="p">(</span><span class="n">exc_reply_t</span><span class="p">,</span> <span class="n">flavor</span><span class="p">);</span>
        <span class="p">}</span>
        <span class="n">mach_msg</span><span class="p">(</span><span class="o">&amp;</span><span class="n">rep</span><span class="p">.</span><span class="n">Head</span><span class="p">,</span> <span class="n">MACH_SEND_MSG</span><span class="p">,</span> <span class="n">rep</span><span class="p">.</span><span class="n">Head</span><span class="p">.</span><span class="n">msgh_size</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span>
                 <span class="n">MACH_PORT_NULL</span><span class="p">,</span> <span class="n">MACH_MSG_TIMEOUT_NONE</span><span class="p">,</span> <span class="n">MACH_PORT_NULL</span><span class="p">);</span>
        <span class="n">mach_port_deallocate</span><span class="p">(</span><span class="n">mach_task_self</span><span class="p">(),</span> <span class="n">req</span><span class="p">.</span><span class="kr">thread</span><span class="p">.</span><span class="n">name</span><span class="p">);</span>
        <span class="n">mach_port_deallocate</span><span class="p">(</span><span class="n">mach_task_self</span><span class="p">(),</span> <span class="n">req</span><span class="p">.</span><span class="n">task</span><span class="p">.</span><span class="n">name</span><span class="p">);</span>
    <span class="p">}</span>
    <span class="k">return</span> <span class="nb">NULL</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

<h3 id="33-踏过断点本框架最核心的难点">3.3 踏过断点：本框架最核心的难点</h3>

<p>命中后<strong>怎么继续执行而不死循环</strong>？因为硬件断点还挂在那个地址，异常返回后会立刻再次命中。解法是经典的「<strong>禁用断点 + 硬件单步 + 复原</strong>」：</p>

<ol>
  <li>命中 → 跑用户回调；</li>
  <li>如果回调没改 PC（还要执行原指令）→ 临时关掉本断点的 <code class="language-plaintext highlighter-rouge">bcr.E</code>，同时打开单步（<code class="language-plaintext highlighter-rouge">mdscr_el1.SS</code> + <code class="language-plaintext highlighter-rouge">cpsr.SS</code>）；</li>
  <li>CPU 单步执行完命中那条指令后，再产生一个<strong>单步异常</strong>（也走 <code class="language-plaintext highlighter-rouge">EXC_BREAKPOINT</code>）；</li>
  <li>在这次异常里把断点重新打开、关掉单步。</li>
</ol>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">static</span> <span class="n">kern_return_t</span> <span class="nf">handle_exception</span><span class="p">(</span><span class="n">mach_port_t</span> <span class="kr">thread</span><span class="p">,</span> <span class="n">exception_type_t</span> <span class="n">exc</span><span class="p">,</span>
                                      <span class="n">arm_thread_state64_t</span> <span class="o">*</span><span class="n">st</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">if</span> <span class="p">(</span><span class="n">exc</span> <span class="o">!=</span> <span class="n">EXC_BREAKPOINT</span><span class="p">)</span> <span class="k">return</span> <span class="n">KERN_FAILURE</span><span class="p">;</span>

    <span class="kt">uint64_t</span> <span class="n">tid</span> <span class="o">=</span> <span class="n">tid_of</span><span class="p">(</span><span class="kr">thread</span><span class="p">);</span>
    <span class="n">pthread_mutex_lock</span><span class="p">(</span><span class="o">&amp;</span><span class="n">g_lock</span><span class="p">);</span>
    <span class="n">step_ctx_t</span> <span class="o">*</span><span class="n">sc</span> <span class="o">=</span> <span class="n">step_get</span><span class="p">(</span><span class="n">tid</span><span class="p">);</span>

    <span class="c1">// 情况 1:这是我们刚才发起的「单步」产生的异常 —— 复原断点,关单步,放行</span>
    <span class="k">if</span> <span class="p">(</span><span class="n">sc</span> <span class="o">&amp;&amp;</span> <span class="n">sc</span><span class="o">-&gt;</span><span class="n">slot</span> <span class="o">&gt;=</span> <span class="mi">0</span><span class="p">)</span> <span class="p">{</span>
        <span class="n">arm_debug_state64_t</span> <span class="n">d</span><span class="p">;</span>
        <span class="k">if</span> <span class="p">(</span><span class="n">get_dbg</span><span class="p">(</span><span class="kr">thread</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">d</span><span class="p">)</span> <span class="o">==</span> <span class="n">KERN_SUCCESS</span><span class="p">)</span> <span class="p">{</span>
            <span class="n">d</span><span class="p">.</span><span class="n">__bcr</span><span class="p">[</span><span class="n">sc</span><span class="o">-&gt;</span><span class="n">slot</span><span class="p">]</span> <span class="o">|=</span> <span class="n">BCR_ENABLE</span><span class="p">;</span>   <span class="c1">// 重新挂上之前禁用的断点</span>
            <span class="n">d</span><span class="p">.</span><span class="n">__mdscr_el1</span>     <span class="o">&amp;=</span> <span class="o">~</span><span class="n">MDSCR_SS</span><span class="p">;</span>     <span class="c1">// 关单步</span>
            <span class="n">set_dbg</span><span class="p">(</span><span class="kr">thread</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">d</span><span class="p">);</span>
        <span class="p">}</span>
        <span class="n">st</span><span class="o">-&gt;</span><span class="n">__cpsr</span> <span class="o">&amp;=</span> <span class="o">~</span><span class="n">PSTATE_SS</span><span class="p">;</span>
        <span class="n">sc</span><span class="o">-&gt;</span><span class="n">slot</span> <span class="o">=</span> <span class="o">-</span><span class="mi">1</span><span class="p">;</span> <span class="n">sc</span><span class="o">-&gt;</span><span class="n">tid</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>            <span class="c1">// 释放槽位</span>
        <span class="n">pthread_mutex_unlock</span><span class="p">(</span><span class="o">&amp;</span><span class="n">g_lock</span><span class="p">);</span>
        <span class="k">return</span> <span class="n">KERN_SUCCESS</span><span class="p">;</span>
    <span class="p">}</span>

    <span class="c1">// 情况 2:断点命中 —— 找到是哪个槽位</span>
    <span class="kt">uint64_t</span> <span class="n">pc</span> <span class="o">=</span> <span class="n">strip_pac</span><span class="p">((</span><span class="kt">uint64_t</span><span class="p">)</span><span class="n">arm_thread_state64_get_pc</span><span class="p">(</span><span class="o">*</span><span class="n">st</span><span class="p">));</span>
    <span class="n">hb_entry_t</span> <span class="o">*</span><span class="n">hit</span> <span class="o">=</span> <span class="nb">NULL</span><span class="p">;</span>
    <span class="k">for</span> <span class="p">(</span><span class="kt">int</span> <span class="n">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">i</span> <span class="o">&lt;</span> <span class="n">MAX_HW_BP</span><span class="p">;</span> <span class="n">i</span><span class="o">++</span><span class="p">)</span>
        <span class="k">if</span> <span class="p">(</span><span class="n">g_bps</span><span class="p">[</span><span class="n">i</span><span class="p">].</span><span class="n">used</span> <span class="o">&amp;&amp;</span> <span class="p">(</span><span class="n">g_bps</span><span class="p">[</span><span class="n">i</span><span class="p">].</span><span class="n">addr</span> <span class="o">&amp;</span> <span class="o">~</span><span class="mi">3ULL</span><span class="p">)</span> <span class="o">==</span> <span class="p">(</span><span class="n">pc</span> <span class="o">&amp;</span> <span class="o">~</span><span class="mi">3ULL</span><span class="p">))</span> <span class="p">{</span> <span class="n">hit</span> <span class="o">=</span> <span class="o">&amp;</span><span class="n">g_bps</span><span class="p">[</span><span class="n">i</span><span class="p">];</span> <span class="k">break</span><span class="p">;</span> <span class="p">}</span>
    <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="n">hit</span><span class="p">)</span> <span class="p">{</span> <span class="n">pthread_mutex_unlock</span><span class="p">(</span><span class="o">&amp;</span><span class="n">g_lock</span><span class="p">);</span> <span class="k">return</span> <span class="n">KERN_FAILURE</span><span class="p">;</span> <span class="p">}</span> <span class="c1">// 不是我们的,转发出去</span>

    <span class="n">hb_callback_t</span> <span class="n">cb</span> <span class="o">=</span> <span class="n">hit</span><span class="o">-&gt;</span><span class="n">cb</span><span class="p">;</span> <span class="kt">void</span> <span class="o">*</span><span class="n">user</span> <span class="o">=</span> <span class="n">hit</span><span class="o">-&gt;</span><span class="n">user</span><span class="p">;</span> <span class="kt">int</span> <span class="n">slot</span> <span class="o">=</span> <span class="n">hit</span><span class="o">-&gt;</span><span class="n">slot</span><span class="p">;</span>
    <span class="n">pthread_mutex_unlock</span><span class="p">(</span><span class="o">&amp;</span><span class="n">g_lock</span><span class="p">);</span>

    <span class="k">if</span> <span class="p">(</span><span class="n">cb</span><span class="p">)</span> <span class="n">cb</span><span class="p">(</span><span class="n">st</span><span class="p">,</span> <span class="n">user</span><span class="p">);</span>   <span class="c1">// ===== 用户 hook 逻辑 =====</span>

    <span class="c1">// 用户改了 PC 就不会再撞断点,直接放行,无需单步</span>
    <span class="kt">uint64_t</span> <span class="n">newpc</span> <span class="o">=</span> <span class="n">strip_pac</span><span class="p">((</span><span class="kt">uint64_t</span><span class="p">)</span><span class="n">arm_thread_state64_get_pc</span><span class="p">(</span><span class="o">*</span><span class="n">st</span><span class="p">));</span>
    <span class="k">if</span> <span class="p">((</span><span class="n">newpc</span> <span class="o">&amp;</span> <span class="o">~</span><span class="mi">3ULL</span><span class="p">)</span> <span class="o">!=</span> <span class="p">(</span><span class="n">pc</span> <span class="o">&amp;</span> <span class="o">~</span><span class="mi">3ULL</span><span class="p">))</span> <span class="k">return</span> <span class="n">KERN_SUCCESS</span><span class="p">;</span>

    <span class="c1">// PC 没动 —— 必须「踏过去」</span>
    <span class="n">pthread_mutex_lock</span><span class="p">(</span><span class="o">&amp;</span><span class="n">g_lock</span><span class="p">);</span>
    <span class="n">arm_debug_state64_t</span> <span class="n">d</span><span class="p">;</span>
    <span class="k">if</span> <span class="p">(</span><span class="n">get_dbg</span><span class="p">(</span><span class="kr">thread</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">d</span><span class="p">)</span> <span class="o">==</span> <span class="n">KERN_SUCCESS</span><span class="p">)</span> <span class="p">{</span>
        <span class="n">d</span><span class="p">.</span><span class="n">__bcr</span><span class="p">[</span><span class="n">slot</span><span class="p">]</span> <span class="o">&amp;=</span> <span class="o">~</span><span class="n">BCR_ENABLE</span><span class="p">;</span>
        <span class="n">d</span><span class="p">.</span><span class="n">__mdscr_el1</span> <span class="o">|=</span> <span class="n">MDSCR_SS</span><span class="p">;</span>
        <span class="n">set_dbg</span><span class="p">(</span><span class="kr">thread</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">d</span><span class="p">);</span>
    <span class="p">}</span>
    <span class="n">st</span><span class="o">-&gt;</span><span class="n">__cpsr</span> <span class="o">|=</span> <span class="n">PSTATE_SS</span><span class="p">;</span>
    <span class="n">step_ctx_t</span> <span class="o">*</span><span class="n">ns</span> <span class="o">=</span> <span class="n">step_find_or_alloc</span><span class="p">(</span><span class="n">tid</span><span class="p">);</span>
    <span class="k">if</span> <span class="p">(</span><span class="n">ns</span><span class="p">)</span> <span class="n">ns</span><span class="o">-&gt;</span><span class="n">slot</span> <span class="o">=</span> <span class="n">slot</span><span class="p">;</span>
    <span class="n">pthread_mutex_unlock</span><span class="p">(</span><span class="o">&amp;</span><span class="n">g_lock</span><span class="p">);</span>
    <span class="k">return</span> <span class="n">KERN_SUCCESS</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

<blockquote>
  <p><code class="language-plaintext highlighter-rouge">mdscr_el1.SS</code> 和 <code class="language-plaintext highlighter-rouge">cpsr.SS</code> 必须<strong>配合置位</strong>,arm64 软件单步才真正生效。</p>
</blockquote>

<h3 id="34-per-thread-难题">3.4 per-thread 难题</h3>

<p>这是硬件断点相对 inline hook 最大的弱点：<strong>调试寄存器是 per-thread 的</strong>。<code class="language-plaintext highlighter-rouge">thread_set_state</code> 只对一个线程生效，新建的线程不会自动带上断点。如果目标函数跑在 hook 之后才创建的线程上，永远不命中。</p>

<p>第一版用一次性 <code class="language-plaintext highlighter-rouge">hb_install_all_threads()</code>（遍历 <code class="language-plaintext highlighter-rouge">task_threads</code> 给所有现存线程下断），但这只覆盖”下断时已存在的线程”。后面会讲怎么用一个后台 watcher 彻底解决。</p>

<h2 id="四封装成越狱插件">四、封装成越狱插件</h2>

<p>引擎是自包含的 C，不依赖 Substrate。注入交给越狱框架（ellekit）按 bundle id 把 dylib <code class="language-plaintext highlighter-rouge">dlopen</code> 进目标进程，<code class="language-plaintext highlighter-rouge">%ctor</code> 里起服务器 + 下断。</p>

<p><code class="language-plaintext highlighter-rouge">Tweak.xm</code> 的目标定位支持两种方式：</p>

<div class="language-objc highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// 方式 A:hook 某 ObjC 实例方法的 IMP —— 拦方法但不 swizzle,method 表/指令都不动</span>
<span class="k">static</span> <span class="k">const</span> <span class="kt">char</span> <span class="o">*</span><span class="n">TARGET_CLASS</span> <span class="o">=</span> <span class="nb">NULL</span><span class="p">;</span>
<span class="k">static</span> <span class="k">const</span> <span class="kt">char</span> <span class="o">*</span><span class="n">TARGET_SEL</span>   <span class="o">=</span> <span class="nb">NULL</span><span class="p">;</span>

<span class="c1">// 方式 B:hook native 函数。IMAGE=mach-o 名后缀, OFFSET = IDA 地址 - IDA image base</span>
<span class="k">static</span> <span class="k">const</span> <span class="kt">char</span> <span class="o">*</span><span class="n">TARGET_IMAGE</span>  <span class="o">=</span> <span class="s">"TargetApp"</span><span class="p">;</span>
<span class="k">static</span> <span class="k">const</span> <span class="kt">uint64_t</span> <span class="n">TARGET_OFFSET</span> <span class="o">=</span> <span class="mh">0x9</span><span class="n">XXXXX</span><span class="p">;</span>   <span class="c1">// 目标函数偏移,按自己 IDA 实测填</span>
</code></pre></div></div>

<p>运行期基址（含 ASLR slide）这样取：</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">static</span> <span class="kt">uintptr_t</span> <span class="nf">image_base_by_suffix</span><span class="p">(</span><span class="k">const</span> <span class="kt">char</span> <span class="o">*</span><span class="n">suffix</span><span class="p">)</span> <span class="p">{</span>
    <span class="kt">uint32_t</span> <span class="n">cnt</span> <span class="o">=</span> <span class="n">_dyld_image_count</span><span class="p">();</span>
    <span class="k">for</span> <span class="p">(</span><span class="kt">uint32_t</span> <span class="n">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">i</span> <span class="o">&lt;</span> <span class="n">cnt</span><span class="p">;</span> <span class="n">i</span><span class="o">++</span><span class="p">)</span> <span class="p">{</span>
        <span class="k">const</span> <span class="kt">char</span> <span class="o">*</span><span class="n">name</span> <span class="o">=</span> <span class="n">_dyld_get_image_name</span><span class="p">(</span><span class="n">i</span><span class="p">);</span>
        <span class="kt">size_t</span> <span class="n">nl</span> <span class="o">=</span> <span class="n">strlen</span><span class="p">(</span><span class="n">name</span><span class="p">),</span> <span class="n">sl</span> <span class="o">=</span> <span class="n">strlen</span><span class="p">(</span><span class="n">suffix</span><span class="p">);</span>
        <span class="k">if</span> <span class="p">(</span><span class="n">nl</span> <span class="o">&gt;=</span> <span class="n">sl</span> <span class="o">&amp;&amp;</span> <span class="n">strcmp</span><span class="p">(</span><span class="n">name</span> <span class="o">+</span> <span class="n">nl</span> <span class="o">-</span> <span class="n">sl</span><span class="p">,</span> <span class="n">suffix</span><span class="p">)</span> <span class="o">==</span> <span class="mi">0</span><span class="p">)</span>
            <span class="k">return</span> <span class="p">(</span><span class="kt">uintptr_t</span><span class="p">)</span><span class="n">_dyld_get_image_header</span><span class="p">(</span><span class="n">i</span><span class="p">);</span>
    <span class="p">}</span>
    <span class="k">return</span> <span class="mi">0</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">%ctor</code> 里下断 + 开 watcher：</p>

<div class="language-objc highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="o">%</span><span class="n">ctor</span> <span class="p">{</span>
    <span class="k">@autoreleasepool</span> <span class="p">{</span>
        <span class="n">install_hooks</span><span class="p">();</span>
        <span class="n">hb_enable_thread_watcher</span><span class="p">();</span>   <span class="c1">// 自动给新线程补断点(见第六节)</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<blockquote>
  <p>非越狱也能用：把 <code class="language-plaintext highlighter-rouge">hbhook.c/.h</code> 直接拖进自己的 Xcode 工程，Debug 包自带 <code class="language-plaintext highlighter-rouge">get-task-allow</code>，进程就有权给自己设异常端口和调试寄存器。适合自调试 / 自保护 / PoC。</p>
</blockquote>

<h2 id="五实战hook-某加固-app-的设备信息加密">五、实战：hook 某加固 App 的设备信息加密</h2>

<h3 id="51-选目标">5.1 选目标</h3>

<p>IDA 里目标主程序某个 native 函数（命名 <code class="language-plaintext highlighter-rouge">getdeviceinfo_sub</code>），被一个形如 <code class="language-plaintext highlighter-rouge">+[UIDevice xxx_riskInfoCryptInner:andLength:andGen:andKey:…andAppendData:]</code> 的 ObjC 方法调用——典型的设备信息采集 + 加密内核。函数体是 OLLVM 控制流平坦化（一个取 opcode 的派发循环 + 大量内联 EOR 串解密），但<strong>只 hook 入口、不进 VM</strong>，完全不受混淆影响。</p>

<p>序言保存的寄存器给出参数布局（地址用相对偏移示意）：</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>func+0x20  MOV  X22, X7        ; x7
func+0x28  STUR X6, [...]      ; x6
func+0x30  STUR W5, [...]      ; w5(32位)
func+0x38  MOV  X20, X3        ; x3
func+0x3C  MOV  X21, X2        ; x2
func+0x44  STUR W1, [...]      ; w1(32位,length)
func+0x4C  STUR X0, [...]      ; x0
</code></pre></div></div>

<h3 id="52-回调安全读参数">5.2 回调：安全读参数</h3>

<p>第一版回调直接把参数当 OC 对象 <code class="language-plaintext highlighter-rouge">isKindOfClass</code>，结果 <code class="language-plaintext highlighter-rouge">EXC_BAD_ACCESS</code> 闪退——<strong><code class="language-plaintext highlighter-rouge">@try</code> 抓不住 BAD_ACCESS</strong>（那是 Mach 异常不是 NSException）。教训：逆向场景别盲目发消息，用 <code class="language-plaintext highlighter-rouge">vm_read_overwrite</code> 安全读，地址非法只返回错误码、绝不崩：</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">static</span> <span class="kt">void</span> <span class="nf">dump_mem</span><span class="p">(</span><span class="k">const</span> <span class="kt">char</span> <span class="o">*</span><span class="n">tag</span><span class="p">,</span> <span class="kt">uint64_t</span> <span class="n">addr</span><span class="p">,</span> <span class="kt">size_t</span> <span class="n">len</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="n">addr</span><span class="p">)</span> <span class="p">{</span> <span class="n">NSLog</span><span class="p">(</span><span class="err">@</span><span class="s">"  %-8s = (null)"</span><span class="p">,</span> <span class="n">tag</span><span class="p">);</span> <span class="k">return</span><span class="p">;</span> <span class="p">}</span>
    <span class="k">if</span> <span class="p">(</span><span class="n">len</span> <span class="o">==</span> <span class="mi">0</span><span class="p">)</span> <span class="n">len</span> <span class="o">=</span> <span class="mi">32</span><span class="p">;</span> <span class="k">if</span> <span class="p">(</span><span class="n">len</span> <span class="o">&gt;</span> <span class="mi">4096</span><span class="p">)</span> <span class="n">len</span> <span class="o">=</span> <span class="mi">4096</span><span class="p">;</span>
    <span class="k">static</span> <span class="kt">uint8_t</span> <span class="n">buf</span><span class="p">[</span><span class="mi">4096</span><span class="p">];</span> <span class="n">vm_size_t</span> <span class="n">out</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
    <span class="n">kern_return_t</span> <span class="n">kr</span> <span class="o">=</span> <span class="n">vm_read_overwrite</span><span class="p">(</span><span class="n">mach_task_self</span><span class="p">(),</span>
                          <span class="p">(</span><span class="n">vm_address_t</span><span class="p">)</span><span class="n">addr</span><span class="p">,</span> <span class="n">len</span><span class="p">,</span> <span class="p">(</span><span class="n">vm_address_t</span><span class="p">)</span><span class="n">buf</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">out</span><span class="p">);</span>
    <span class="k">if</span> <span class="p">(</span><span class="n">kr</span> <span class="o">!=</span> <span class="n">KERN_SUCCESS</span><span class="p">)</span> <span class="p">{</span>
        <span class="n">NSLog</span><span class="p">(</span><span class="err">@</span><span class="s">"  %-8s @0x%llx 不可读(可能是标量, kr=%d)"</span><span class="p">,</span> <span class="n">tag</span><span class="p">,</span> <span class="p">(</span><span class="kt">unsigned</span> <span class="kt">long</span> <span class="kt">long</span><span class="p">)</span><span class="n">addr</span><span class="p">,</span> <span class="n">kr</span><span class="p">);</span>
        <span class="k">return</span><span class="p">;</span>
    <span class="p">}</span>
    <span class="n">NSMutableString</span> <span class="o">*</span><span class="n">h</span> <span class="o">=</span> <span class="p">[</span><span class="n">NSMutableString</span> <span class="n">string</span><span class="p">],</span> <span class="o">*</span><span class="n">a</span> <span class="o">=</span> <span class="p">[</span><span class="n">NSMutableString</span> <span class="n">string</span><span class="p">];</span>
    <span class="k">for</span> <span class="p">(</span><span class="n">vm_size_t</span> <span class="n">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">i</span> <span class="o">&lt;</span> <span class="n">out</span><span class="p">;</span> <span class="n">i</span><span class="o">++</span><span class="p">)</span> <span class="p">{</span>
        <span class="p">[</span><span class="n">h</span> <span class="n">appendFormat</span><span class="o">:</span><span class="err">@</span><span class="s">"%02x"</span><span class="p">,</span> <span class="n">buf</span><span class="p">[</span><span class="n">i</span><span class="p">]];</span>
        <span class="p">[</span><span class="n">a</span> <span class="n">appendFormat</span><span class="o">:</span><span class="err">@</span><span class="s">"%c"</span><span class="p">,</span> <span class="p">(</span><span class="n">buf</span><span class="p">[</span><span class="n">i</span><span class="p">]</span> <span class="o">&gt;=</span> <span class="mh">0x20</span> <span class="o">&amp;&amp;</span> <span class="n">buf</span><span class="p">[</span><span class="n">i</span><span class="p">]</span> <span class="o">&lt;</span> <span class="mh">0x7f</span><span class="p">)</span> <span class="o">?</span> <span class="n">buf</span><span class="p">[</span><span class="n">i</span><span class="p">]</span> <span class="o">:</span> <span class="sc">'.'</span><span class="p">];</span>
    <span class="p">}</span>
    <span class="n">NSLog</span><span class="p">(</span><span class="err">@</span><span class="s">"  %-8s @0x%llx (%lu): %@ | %@"</span><span class="p">,</span> <span class="n">tag</span><span class="p">,</span> <span class="p">(</span><span class="kt">unsigned</span> <span class="kt">long</span> <span class="kt">long</span><span class="p">)</span><span class="n">addr</span><span class="p">,</span> <span class="p">(</span><span class="kt">unsigned</span> <span class="kt">long</span><span class="p">)</span><span class="n">out</span><span class="p">,</span> <span class="n">h</span><span class="p">,</span> <span class="n">a</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>

<h3 id="53-命中结果">5.3 命中结果</h3>

<p>触发一次风控操作，命中（注意是<strong>后台线程</strong>）。以下日志中的指纹、密钥、明文等敏感字节均已脱敏，仅保留结构：</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>[hbhook] crypt-inner hit  thread=0x16xxxxxxx
  raw x0..x7 = 1xxxxxxxx 100 1xxxxxxxx 2xxxxxxxx 1 3e9 0 0
  w1 len = 256   stk append = 0x2xxxxxxxx
  x0 data @0x1xxxxxxxx (256): 7a65……（256B 高熵，已脱敏）
  x3 obj  @0x2xxxxxxxx (64): 4158bf1b02000003 20000000 00000000
                              ……（紧随 16 字节对象头的 32B 高熵，已脱敏）
  append  @0x2xxxxxxxx: &lt;对象头16B&gt; 357b0a……（"xxxxdf" 字段的 UTF8,已脱敏）……
                        | 5{  "xxxxdf" : "AC45****……****C251"\n}
</code></pre></div></div>

<p>解码三个关键参数：</p>

<ul>
  <li><strong><code class="language-plaintext highlighter-rouge">append</code> = 单字段明文 JSON</strong>：对象布局 <code class="language-plaintext highlighter-rouge">isa(8) + len32 + flags32 + [1字节长度前缀][UTF8]</code>，内容形如 <code class="language-plaintext highlighter-rouge">{"xxxxdf":"AC45****……****C251"}</code>（指纹值已脱敏）。该字段是稳定设备指纹（多次调用不变），这就是上层安全 SDK 加密<strong>前的 body 明文</strong>。</li>
  <li><strong><code class="language-plaintext highlighter-rouge">x3</code> = 32 字节 per-call 高熵</strong>：两次调用完全不同 → 每次随机的密钥 / nonce / salt，AEAD key 的有力候选（值已脱敏）。</li>
  <li><strong><code class="language-plaintext highlighter-rouge">x0</code> = 256 字节裸 buffer</strong>：每次变，可能是密文输出、keystream 或 RC4-style 256 字节状态。</li>
</ul>

<blockquote>
  <p>一个细节：<code class="language-plaintext highlighter-rouge">x0</code> 在普通堆是<strong>裸 buffer</strong>；<code class="language-plaintext highlighter-rouge">x3</code>/<code class="language-plaintext highlighter-rouge">append</code> 在 nano 堆区（<code class="language-plaintext highlighter-rouge">0x2xx……</code>）、带 isa 头是 <strong>OC/CF 对象</strong>。这就是为什么盲目对 <code class="language-plaintext highlighter-rouge">x0</code> 发消息会崩。</p>
</blockquote>

<p>至此，用一个不改任何指令的硬件断点，就把加固 App 的设备信息明文和动态密钥抓了出来。</p>

<h2 id="六坑与优化">六、坑与优化</h2>

<p>把它长期挂在目标 App（线程多、命中频繁）上，暴露出几个会<strong>实际咬人</strong>的问题：</p>

<h3 id="61-单步状态表只增不减--跑久了卡死">6.1 单步状态表只增不减 → 跑久了卡死</h3>

<p>踏断点的 per-thread 状态用一张小表记录。第一版单步完成只把 <code class="language-plaintext highlighter-rouge">slot=-1</code>，<strong>没释放线程项</strong>，死线程把表占满后 → 分配失败 → 踏不过断点 → 死循环。修复：单步完成时连 key 一起清掉。</p>

<h3 id="62-用-mach-端口名做-key-不可靠">6.2 用 Mach 端口名做 key 不可靠</h3>

<p><code class="language-plaintext highlighter-rouge">g_step</code> 一开始用 <code class="language-plaintext highlighter-rouge">mach_port_t</code> 当 key，但端口名会跨消息回收：<strong>入口命中</strong>和<strong>单步完成</strong>是两条独立 Mach 消息，中间端口名被 <code class="language-plaintext highlighter-rouge">deallocate</code> 了，一旦不复用，单步那条消息匹配不上 → 该断点永久卡死。正解是用稳定线程 id：</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">static</span> <span class="kt">uint64_t</span> <span class="nf">tid_of</span><span class="p">(</span><span class="n">mach_port_t</span> <span class="n">th</span><span class="p">)</span> <span class="p">{</span>
    <span class="n">thread_identifier_info_data_t</span> <span class="n">ii</span><span class="p">;</span>
    <span class="n">mach_msg_type_number_t</span> <span class="n">c</span> <span class="o">=</span> <span class="n">THREAD_IDENTIFIER_INFO_COUNT</span><span class="p">;</span>
    <span class="k">if</span> <span class="p">(</span><span class="n">thread_info</span><span class="p">(</span><span class="n">th</span><span class="p">,</span> <span class="n">THREAD_IDENTIFIER_INFO</span><span class="p">,</span> <span class="p">(</span><span class="n">thread_info_t</span><span class="p">)</span><span class="o">&amp;</span><span class="n">ii</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">c</span><span class="p">)</span> <span class="o">==</span> <span class="n">KERN_SUCCESS</span><span class="p">)</span>
        <span class="k">return</span> <span class="n">ii</span><span class="p">.</span><span class="n">thread_id</span><span class="p">;</span>
    <span class="k">return</span> <span class="mi">0</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">g_step</code> 改存 <code class="language-plaintext highlighter-rouge">tid</code>，端口名就能放心 deallocate。</p>

<h3 id="63-per-thread-的终极解法后台-watcher">6.3 per-thread 的终极解法：后台 watcher</h3>

<p>用一个后台线程，<strong>只给新出现的线程补断点</strong>（按 tid 记忆，死线程自动剔除），并每 ~5s 全量重下一次——顺带兜底”被反调试擦寄存器”的情况：</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">static</span> <span class="kt">void</span> <span class="o">*</span><span class="nf">watcher_thread</span><span class="p">(</span><span class="kt">void</span> <span class="o">*</span><span class="n">arg</span><span class="p">)</span> <span class="p">{</span>
    <span class="n">g_watch_tid</span> <span class="o">=</span> <span class="n">tid_of</span><span class="p">(</span><span class="n">pthread_mach_thread_np</span><span class="p">(</span><span class="n">pthread_self</span><span class="p">()));</span>
    <span class="k">static</span> <span class="kt">uint64_t</span> <span class="n">seen</span><span class="p">[</span><span class="mi">1024</span><span class="p">];</span> <span class="k">static</span> <span class="kt">int</span> <span class="n">nseen</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
    <span class="kt">int</span> <span class="n">tick</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
    <span class="k">while</span> <span class="p">(</span><span class="n">g_watch_run</span><span class="p">)</span> <span class="p">{</span>
        <span class="k">if</span> <span class="p">(</span><span class="o">++</span><span class="n">tick</span> <span class="o">&gt;=</span> <span class="mi">25</span><span class="p">)</span> <span class="p">{</span> <span class="n">nseen</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">tick</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="p">}</span>   <span class="c1">// 每 ~5s 全量重下(兜底)</span>
        <span class="n">thread_act_array_t</span> <span class="n">threads</span><span class="p">;</span> <span class="n">mach_msg_type_number_t</span> <span class="n">n</span><span class="p">;</span>
        <span class="k">if</span> <span class="p">(</span><span class="n">task_threads</span><span class="p">(</span><span class="n">mach_task_self</span><span class="p">(),</span> <span class="o">&amp;</span><span class="n">threads</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">n</span><span class="p">)</span> <span class="o">==</span> <span class="n">KERN_SUCCESS</span><span class="p">)</span> <span class="p">{</span>
            <span class="kt">uint64_t</span> <span class="n">now</span><span class="p">[</span><span class="mi">1024</span><span class="p">];</span> <span class="kt">int</span> <span class="n">nnow</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
            <span class="n">pthread_mutex_lock</span><span class="p">(</span><span class="o">&amp;</span><span class="n">g_lock</span><span class="p">);</span>
            <span class="k">for</span> <span class="p">(</span><span class="kt">unsigned</span> <span class="n">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">i</span> <span class="o">&lt;</span> <span class="n">n</span><span class="p">;</span> <span class="n">i</span><span class="o">++</span><span class="p">)</span> <span class="p">{</span>
                <span class="n">mach_port_t</span> <span class="n">th</span> <span class="o">=</span> <span class="n">threads</span><span class="p">[</span><span class="n">i</span><span class="p">];</span>
                <span class="kt">uint64_t</span> <span class="n">tid</span> <span class="o">=</span> <span class="n">tid_of</span><span class="p">(</span><span class="n">th</span><span class="p">);</span>
                <span class="k">if</span> <span class="p">(</span><span class="n">tid</span> <span class="o">&amp;&amp;</span> <span class="n">tid</span> <span class="o">!=</span> <span class="n">g_server_tid</span> <span class="o">&amp;&amp;</span> <span class="n">tid</span> <span class="o">!=</span> <span class="n">g_watch_tid</span><span class="p">)</span> <span class="p">{</span>
                    <span class="k">if</span> <span class="p">(</span><span class="n">nnow</span> <span class="o">&lt;</span> <span class="mi">1024</span><span class="p">)</span> <span class="n">now</span><span class="p">[</span><span class="n">nnow</span><span class="o">++</span><span class="p">]</span> <span class="o">=</span> <span class="n">tid</span><span class="p">;</span>
                    <span class="n">bool</span> <span class="n">was</span> <span class="o">=</span> <span class="nb">false</span><span class="p">;</span>
                    <span class="k">for</span> <span class="p">(</span><span class="kt">int</span> <span class="n">k</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">k</span> <span class="o">&lt;</span> <span class="n">nseen</span><span class="p">;</span> <span class="n">k</span><span class="o">++</span><span class="p">)</span> <span class="k">if</span> <span class="p">(</span><span class="n">seen</span><span class="p">[</span><span class="n">k</span><span class="p">]</span> <span class="o">==</span> <span class="n">tid</span><span class="p">)</span> <span class="p">{</span> <span class="n">was</span> <span class="o">=</span> <span class="nb">true</span><span class="p">;</span> <span class="k">break</span><span class="p">;</span> <span class="p">}</span>
                    <span class="n">step_ctx_t</span> <span class="o">*</span><span class="n">sc</span> <span class="o">=</span> <span class="n">step_get</span><span class="p">(</span><span class="n">tid</span><span class="p">);</span>
                    <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="n">was</span> <span class="o">&amp;&amp;</span> <span class="o">!</span><span class="p">(</span><span class="n">sc</span> <span class="o">&amp;&amp;</span> <span class="n">sc</span><span class="o">-&gt;</span><span class="n">slot</span> <span class="o">&gt;=</span> <span class="mi">0</span><span class="p">))</span> <span class="n">apply_bps_to_thread</span><span class="p">(</span><span class="n">th</span><span class="p">);</span>
                <span class="p">}</span>
                <span class="n">mach_port_deallocate</span><span class="p">(</span><span class="n">mach_task_self</span><span class="p">(),</span> <span class="n">th</span><span class="p">);</span>
            <span class="p">}</span>
            <span class="n">nseen</span> <span class="o">=</span> <span class="n">nnow</span><span class="p">;</span>
            <span class="k">for</span> <span class="p">(</span><span class="kt">int</span> <span class="n">k</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">k</span> <span class="o">&lt;</span> <span class="n">nnow</span><span class="p">;</span> <span class="n">k</span><span class="o">++</span><span class="p">)</span> <span class="n">seen</span><span class="p">[</span><span class="n">k</span><span class="p">]</span> <span class="o">=</span> <span class="n">now</span><span class="p">[</span><span class="n">k</span><span class="p">];</span>
            <span class="n">pthread_mutex_unlock</span><span class="p">(</span><span class="o">&amp;</span><span class="n">g_lock</span><span class="p">);</span>
            <span class="n">vm_deallocate</span><span class="p">(</span><span class="n">mach_task_self</span><span class="p">(),</span> <span class="p">(</span><span class="n">vm_address_t</span><span class="p">)</span><span class="n">threads</span><span class="p">,</span> <span class="n">n</span> <span class="o">*</span> <span class="k">sizeof</span><span class="p">(</span><span class="n">thread_act_t</span><span class="p">));</span>
        <span class="p">}</span>
        <span class="n">usleep</span><span class="p">(</span><span class="mi">200</span> <span class="o">*</span> <span class="mi">1000</span><span class="p">);</span>
    <span class="p">}</span>
    <span class="k">return</span> <span class="nb">NULL</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

<p>稳态开销从”每秒数百 syscall”降到”基本只在新线程出现时动一下”。</p>

<h3 id="64-还没做但要知道的">6.4 还没做但要知道的</h3>

<ul>
  <li><strong>异常端口转发</strong>：<code class="language-plaintext highlighter-rouge">task_set_exception_ports</code> 会顶掉 App 原来的 <code class="language-plaintext highlighter-rouge">EXC_BREAKPOINT</code> 处理者（crash 上报 SDK、反调试）。要和它们<strong>长期共存</strong>，得 <code class="language-plaintext highlighter-rouge">task_get_exception_ports</code> 存旧端口，遇到非我方异常时 <code class="language-plaintext highlighter-rouge">mach_msg</code> 转发回去。否则可能互相打架，或者你被对方顶掉（表现为命中突然停止）。</li>
  <li><strong>只有 6 个断点槽</strong>，多 hook 点要复用 + 动态换。</li>
</ul>

<h2 id="七总结">七、总结</h2>

<table>
  <thead>
    <tr>
      <th> </th>
      <th>inline hook（Dobby 等）</th>
      <th>硬件断点 hook</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>改目标内存</td>
      <td>✅ 改前 N 条指令</td>
      <td>❌ 一字节不改</td>
    </tr>
    <tr>
      <td>过 CRC / 代码签名</td>
      <td>❌ 会被挡</td>
      <td>✅ 无痕</td>
    </tr>
    <tr>
      <td>数量限制</td>
      <td>无</td>
      <td>只有 6 个</td>
    </tr>
    <tr>
      <td>依赖</td>
      <td>trampoline + 指令重定位</td>
      <td><code class="language-plaintext highlighter-rouge">arm_debug_state64</code> + Mach 异常端口</td>
    </tr>
    <tr>
      <td>检测面</td>
      <td>函数头跳板特征</td>
      <td>调试寄存器非零、异常端口被占</td>
    </tr>
  </tbody>
</table>

<p>硬件断点 hook 不是 inline hook 的替代品，而是它的补充：当目标做了完整性校验、反 inline-hook，或者你只想<strong>安静地观察</strong>而不留痕时，它就是趁手的武器。核心三件套——<code class="language-plaintext highlighter-rouge">arm_debug_state64</code> 下断、Mach 异常服务器拿 state、单步踏过断点——配齐了就是一套能在真实加固 App 上跑的无痕 hook。</p>

<p>实战里它帮我们用零内存改动，从一个 OLLVM 平坦化的加密函数入口抓出了设备信息明文和动态密钥。剩下的——抓输出密文凑齐 <code class="language-plaintext highlighter-rouge">(明文, key, 密文)</code> 三元组定位算法——只需要在返回地址（LR）再下一个断点，留给下一篇。</p>

<blockquote>
  <p>本文涉及的逆向均在自有设备、用于安全研究与对抗学习，请勿用于非法用途。</p>
</blockquote>]]></content><author><name>我是小三</name></author><category term="iOS" /><category term="iOS" /><category term="Hook" /><category term="硬件断点" /><category term="逆向" /><category term="ARM64" /><category term="Mach" /><summary type="html"><![CDATA[从 lldb 的寄存器填法出发，自己实现一套 iOS arm64(e) 进程内硬件断点 hook 框架，封装成越狱插件，并实战 hook 某加固 App 的设备信息加密函数抓取明文与密钥。]]></summary></entry><entry><title type="html">汽车 App 加固分析：网易易盾壳的自定义 linker、SMC 与脱壳实战</title><link href="https://xiaosan-re.github.io/2023/02/02/yidun-shell-unpack/" rel="alternate" type="text/html" title="汽车 App 加固分析：网易易盾壳的自定义 linker、SMC 与脱壳实战" /><published>2023-02-02T00:00:00+08:00</published><updated>2023-02-02T00:00:00+08:00</updated><id>https://xiaosan-re.github.io/2023/02/02/yidun-shell-unpack</id><content type="html" xml:base="https://xiaosan-re.github.io/2023/02/02/yidun-shell-unpack/"><![CDATA[<h2 id="一背景--场景">一、背景 &amp; 场景</h2>

<h3 id="11-环境">1.1 环境</h3>

<p>样本：某汽车 App（包名 <code class="language-plaintext highlighter-rouge">com.xiaopeng.mycarinfo</code>），网易易盾（<code class="language-plaintext highlighter-rouge">libnesec.so</code> + <code class="language-plaintext highlighter-rouge">com.netease.nis.wrapper</code>）加固，Android arm64</p>

<p>工具链：IDA Pro 9.2、调试器（linker / art 下断）、JADX</p>

<h3 id="12-加固的本质与不同视角">1.2 加固的本质，与不同视角</h3>

<p>聊脱壳之前，先说点务虚的——加固到底在保护什么。</p>

<p><strong>代码安全只是表面</strong>，核心是帮客户满足”业务不被阻断、关键数据资产不被窃取”的安全需求。加固自身不创造价值，它的价值必须挂在业务上间接体现：通过安全体系为业务保驾护航，增加收益、降低资损率。</p>

<p>换个立场看 App 加固，每一面看到的东西都不一样：</p>

<ul>
  <li><strong>用户视角</strong>：只关注功能和交互，对 C 端用户而言加固几乎无感；</li>
  <li><strong>产品视角</strong>：关注需求、方案、价值；</li>
  <li><strong>技术视角</strong>：关注架构、实现、可扩展性与安全性，能感知加固的重要性，但更多从技术实现出发；</li>
  <li><strong>业务视角</strong>：保障业务正常运营，看到的是成本和收益。</li>
</ul>

<p>视角不同，决策不同，安全程度也不同。有了这层铺垫，下面这套”看起来有点绕”的加固方案，你大概就不会觉得奇怪了——<strong>它是计划的一部分</strong>。</p>

<p>本文以一个汽车 App 的易盾壳为样本，完整走一遍脱壳：壳的 Java 层 → SO 静态分析（入口怎么藏的）→ SO 动态分析（自定义 linker、SMC、抹 ELF 头）→ DEX 解密与脱壳点 → dump 二次打包。文中涉及的包名、密钥等均为逆向实测结果，仅供安全研究学习。</p>

<blockquote>
  <p>本文仅限学习交流与防御研究，请勿用于非法及商业用途。</p>
</blockquote>

<h2 id="二加固整体架构">二、加固整体架构</h2>

<p>整套壳是经典的 <strong>dex 整体加固 + Native 自定义 linker 保护</strong> 结构：</p>

<p><img src="/images/posts/android/yidun-unpack/architecture.jpg" alt="加固整体架构" /></p>

<p>分三层看：</p>

<ul>
  <li><strong>Java 层</strong>：替换 App 入口、读取壳 SO、判断目标平台、加载壳 SO；</li>
  <li><strong>Native 层（<code class="language-plaintext highlighter-rouge">libnesec.so</code>）</strong>：壳入口（<code class="language-plaintext highlighter-rouge">init_array</code> / 解密代码 / 自定义 linker 加载 SO）、初始化（<code class="language-plaintext highlighter-rouge">JNI_OnLoad</code> / SMC 执行代码 / 注册 native）、反调试（IDA 端口 / 脱壳工具 / 进程状态 / root·模拟器）；</li>
  <li><strong>Load Dex</strong>：解密 dex、加载 dex、还原原 App 入口。</li>
</ul>

<h3 id="解壳过程">解壳过程</h3>

<p>壳跑起来后，干的就是”解密原始 dex + 启动加载原 App Application”这一串活：</p>

<ol>
  <li><strong>解密原 apk 的 dex 集合</strong>——用加密时对应的算法逐个解密 dex；</li>
  <li><strong>把解密后的 dex 加进 <code class="language-plaintext highlighter-rouge">dexElements</code> 数组</strong>——通过反射塞进去；</li>
  <li><strong>动态加载原 apk 的 Application</strong>——加密时原 Application 被替换成了壳的 Application，加载过程中要把这步还原回去。</li>
</ol>

<h2 id="三壳-java-层分析">三、壳 Java 层分析</h2>

<h3 id="31-attachbasecontext">3.1 attachBaseContext</h3>

<p>SO 的释放与加载发生在进程创建、加载 Application <strong>之前</strong>：在 <code class="language-plaintext highlighter-rouge">attachBaseContext</code> 里解压释放 SO 到 lib 目录，<code class="language-plaintext highlighter-rouge">System.loadLibrary("nesec")</code> 加载进内存，并在 <code class="language-plaintext highlighter-rouge">MyJni</code> 类里注册了几个 native 方法：</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="kd">static</span> <span class="kd">native</span> <span class="kt">void</span> <span class="nf">cp</span><span class="o">();</span>
<span class="kd">public</span> <span class="kd">static</span> <span class="kd">native</span> <span class="kt">void</span> <span class="nf">d</span><span class="o">(</span><span class="nc">String</span> <span class="n">arg0</span><span class="o">);</span>
<span class="kd">public</span> <span class="kd">static</span> <span class="kd">native</span> <span class="kt">void</span> <span class="nf">e</span><span class="o">(</span><span class="nc">String</span> <span class="n">arg0</span><span class="o">);</span>
<span class="kd">public</span> <span class="kd">static</span> <span class="kd">native</span> <span class="kt">boolean</span> <span class="nf">load</span><span class="o">(</span><span class="nc">Application</span> <span class="n">arg0</span><span class="o">,</span> <span class="nc">String</span> <span class="n">arg1</span><span class="o">);</span>
<span class="kd">public</span> <span class="kd">static</span> <span class="kd">native</span> <span class="kt">boolean</span> <span class="nf">load2</span><span class="o">(</span><span class="nc">Application</span> <span class="n">arg0</span><span class="o">,</span> <span class="nc">String</span> <span class="n">arg1</span><span class="o">);</span>
<span class="kd">public</span> <span class="kd">static</span> <span class="kd">native</span> <span class="kt">boolean</span> <span class="nf">run</span><span class="o">(</span><span class="nc">Context</span> <span class="n">arg0</span><span class="o">,</span> <span class="nc">Application</span> <span class="n">arg1</span><span class="o">);</span>
</code></pre></div></div>

<h3 id="32-native-load-加载-dex">3.2 native load 加载 dex</h3>

<p>加载完 SO 后调用 <code class="language-plaintext highlighter-rouge">load</code>：解密 dex 并加载到内存——dex 文件解密 + 把解密后的 dex 集合塞进 <code class="language-plaintext highlighter-rouge">dexElements</code> 数组。</p>

<h3 id="33-还原原-application">3.3 还原原 Application</h3>

<p>dex 加载好后，要从<strong>原 App 的 Application</strong> 跑起来。壳里存了原 Application 的类名，反射创建并执行它：</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">static</span> <span class="o">{</span>
    <span class="nc">MyApplication</span><span class="o">.</span><span class="na">strAppName</span> <span class="o">=</span> <span class="s">"com.netease.nis.wrapper.MyApplication"</span><span class="o">;</span>
    <span class="o">...</span>
<span class="o">}</span>
<span class="kd">private</span> <span class="kd">static</span> <span class="nc">Application</span> <span class="nf">a</span><span class="o">(</span><span class="nc">Context</span> <span class="n">ctx</span><span class="o">)</span> <span class="o">{</span>
    <span class="nc">ClassLoader</span> <span class="n">cl</span> <span class="o">=</span> <span class="n">ctx</span><span class="o">.</span><span class="na">getClassLoader</span><span class="o">();</span>
    <span class="nc">Class</span> <span class="n">c</span> <span class="o">=</span> <span class="n">cl</span><span class="o">.</span><span class="na">loadClass</span><span class="o">(</span><span class="nc">MyApplication</span><span class="o">.</span><span class="na">strAppName</span><span class="o">);</span>   <span class="c1">// 加载原 Application</span>
    <span class="nc">MyApplication</span><span class="o">.</span><span class="na">newApp</span> <span class="o">=</span> <span class="o">(</span><span class="nc">Application</span><span class="o">)</span> <span class="n">c</span><span class="o">.</span><span class="na">newInstance</span><span class="o">();</span> <span class="c1">// 创建实例</span>
    <span class="k">return</span> <span class="nc">MyApplication</span><span class="o">.</span><span class="na">newApp</span><span class="o">;</span>
<span class="o">}</span>
</code></pre></div></div>

<p>具体流程：</p>

<ol>
  <li><code class="language-plaintext highlighter-rouge">Class.newInstance()</code> 创建一个 Application 实例；</li>
  <li>反射拿到 <code class="language-plaintext highlighter-rouge">Application.attach(Context)</code> 并 <code class="language-plaintext highlighter-rouge">invoke</code> 执行：</li>
</ol>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nc">Application</span> <span class="n">app</span> <span class="o">=</span> <span class="nc">MyApplication</span><span class="o">.</span><span class="na">a</span><span class="o">(</span><span class="n">ctx</span><span class="o">);</span>
<span class="nc">Method</span> <span class="n">attach</span> <span class="o">=</span> <span class="nc">Application</span><span class="o">.</span><span class="na">class</span><span class="o">.</span><span class="na">getDeclaredMethod</span><span class="o">(</span><span class="s">"attach"</span><span class="o">,</span> <span class="nc">Context</span><span class="o">.</span><span class="na">class</span><span class="o">);</span>
<span class="n">attach</span><span class="o">.</span><span class="na">setAccessible</span><span class="o">(</span><span class="kc">true</span><span class="o">);</span>
<span class="n">attach</span><span class="o">.</span><span class="na">invoke</span><span class="o">(</span><span class="n">app</span><span class="o">,</span> <span class="n">ctx</span><span class="o">);</span>   <span class="c1">// 让原 App 真正"接管"</span>
</code></pre></div></div>

<p>到这里壳就把控制权交还给了原 App，对上层完全透明。</p>

<h2 id="四壳-so-静态分析">四、壳 SO 静态分析</h2>

<p>按惯例，定位到壳 SO 后先用 IDA 静态收集信息（字符串、入口、导出方法）。但这个壳一上来就给你三连击：</p>

<p><strong>① 加载直接报错</strong>——节信息被处理过，专门用来反静态反编译：</p>

<p><img src="/images/posts/android/yidun-unpack/ida-warning.jpg" alt="IDA 加载报错" /></p>

<p><strong>② 导出函数一堆乱码</strong>——导出表被加密了：</p>

<p><img src="/images/posts/android/yidun-unpack/export-garbled.jpg" alt="导出函数乱码" /></p>

<p><strong>③ 找不到 <code class="language-plaintext highlighter-rouge">init_array</code> 节</strong>——连入口节都被抹了：</p>

<p><img src="/images/posts/android/yidun-unpack/no-initarray.jpg" alt="缺失 init_array 节" /></p>

<p>静态这条路基本被堵死。结论很简单：<strong>静态不好定位就动态定位</strong>——内存里再怎么藏，也跳不出系统的加载机制。</p>

<h2 id="五壳-so-动态分析">五、壳 SO 动态分析</h2>

<h3 id="51-壳入口定位">5.1 壳入口定位</h3>

<p>按 linker 加载 SO 的流程，有两个点可以当壳入口：<code class="language-plaintext highlighter-rouge">init</code> 和 <code class="language-plaintext highlighter-rouge">init_array</code> 是 SO 代码能执行的最早时机，之后才轮到 <code class="language-plaintext highlighter-rouge">JNI_OnLoad</code>。只要在 <strong>linker 执行 <code class="language-plaintext highlighter-rouge">init</code> 的地方下断</strong>，就能逮到壳入口：</p>

<p><img src="/images/posts/android/yidun-unpack/linker-init-bp.jpg" alt="在 linker init 处下断" /></p>

<p>定位到的 <code class="language-plaintext highlighter-rouge">init_array</code> 里有几个关键点，其中 <code class="language-plaintext highlighter-rouge">init_array[3]</code> 负责<strong>解密 <code class="language-plaintext highlighter-rouge">JNI_OnLoad</code> 的代码</strong>：</p>

<p><img src="/images/posts/android/yidun-unpack/decrypt-jnionload.jpg" alt="init_array 解密 JNI_OnLoad" /></p>

<p>解密例程是个标准的 <strong>RC4 风格 KSA/PRGA 变体</strong>（256 字节 S 盒交换 + 取流 + 一点魔改）：</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>dec_sub:
loop:
    ADD   W6, W6, #1 ; AND W6, W6, #0xFF      ; i = (i+1) &amp; 0xFF
    LDRB  W3, [X0,X4]                          ; S[i]
    ADD   W5, W3, W5 ; AND W5, W5, #0xFF       ; j = (j+S[i]) &amp; 0xFF
    LDRB  W8, [X0,X7]                          ; S[j]
    STRB  W8, [X0,X4] ; STRB W3, [X0,X7]       ; swap S[i],S[j]
    ADD   W4, W3, W4 ; UXTB W4, W4             ; t = (S[i]+S[j]) &amp; 0xFF
    LDRB  W3, [X0,X4]                          ; k = S[t]
    LSR   W4, W3, #2 ; ORR W3, W4, W3,LSL#6    ; 魔改：循环右移
    ADD   W3, W3, #0x3A                        ; +0x3A
    EOR   W3, W3, W7                            ; 与明文异或
    STRB  W3, [X1],#1
    CMP   X1, X2 ; B.NE loop
</code></pre></div></div>

<h3 id="52-运行时-smc执行时解密执行后加密">5.2 运行时 SMC：执行时解密、执行后加密</h3>

<p>壳大量使用 <strong>SMC（自修改代码）</strong>：一段代码只在<strong>即将执行时</strong>才解密，<strong>执行完立刻加密回去</strong>。这样无论你静态分析还是内存 dump，看到的都是密文。解密用 NEON 一次处理 16 字节，单字节 key 异或：</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>decCode:
    ...
    DUP   V1.16B, W23           ; 用 W23 这个 key 字节铺满 16 字节
loop16:
    LDR   Q0, [X3]
    EOR   V0.16B, V0.16B, V1.16B ; 16 字节一把 XOR 解密
    STR   Q0, [X3],#0x10
    ...
tail:                          ; 不足 16 字节的尾部逐字节
    LDRB  W1, [X19,X2]
    EOR   W1, W23, W1
    STRB  W1, [X19,X2]
    ...
</code></pre></div></div>

<blockquote>
  <p>SMC 是这个壳最硬的一块：你在 IDA 里看到的永远是加密态，必须<strong>动态跑到那条指令解密的瞬间</strong>才能看清，且看完它马上又加密回去。</p>
</blockquote>

<h3 id="53-自定义-linker解密--填充--抹-elf-头">5.3 自定义 linker：解密 + 填充 + 抹 ELF 头</h3>

<p>壳没用系统 linker，而是<strong>自己实现了一套 SO 加载</strong>，全程在 <code class="language-plaintext highlighter-rouge">init_array</code> 里完成。三步走：</p>

<p><strong>① 解密字符串表</strong>（简单异或，带递增 key）：</p>

<p><img src="/images/posts/android/yidun-unpack/decrypt-strtab.jpg" alt="解密后的字符串表" /></p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>DecSecString:
    MOV   W7, #0x2342 ; MOVK W7, #0x5631,LSL#16   ; key = 0x56312342
loop:
    LDR   W6, [X3,X5]
    EOR   W6, W6, W7                               ; 异或解密
    ADD   W7, W7, W5                               ; key 随偏移递增
    STR   W6, [X3,X5]
    ADD   X5, X5, #4 ; CMP W4,W5 ; B.HI loop
</code></pre></div></div>

<p><strong>② 解密指令</strong>（前面 5.1 那个 RC4 变体）。</p>

<p><strong>③ 填充指令 + 抹 ELF 头</strong>：解析 ELF 头拿到各 LOAD 段，<code class="language-plaintext highlighter-rouge">mmap</code> 映射进内存，<code class="language-plaintext highlighter-rouge">mprotect</code> 改读写权限，最后<strong>用 <code class="language-plaintext highlighter-rouge">0xBB</code> 把 ELF 头整个 <code class="language-plaintext highlighter-rouge">memset</code> 掉</strong>：</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>mmap_so_memcpy_code:
    ...
    BLR   X8            ; mprotect(对齐后的段, RW)
    MOV   W1, #0xBB
    BL    memset_0      ; 用 0xBB 填充/抹掉 ELF 头
    ...
    BLR   X4            ; mprotect(段, 按 p_flags 设置最终权限)
</code></pre></div></div>

<blockquote>
  <p>抹 ELF 头这一手专治”从内存直接反 ELF / dump SO”——你 dump 出来的是一坨没有头、没有节表的残体，想完整还原 SO，得<strong>按 ELF 格式手工重组</strong>一个出来。这是这个壳防 dump 的核心价值所在。</p>
</blockquote>

<p>至此 <code class="language-plaintext highlighter-rouge">init_array</code> 阶段结束，接着才执行 <code class="language-plaintext highlighter-rouge">JNI_OnLoad</code>。</p>

<h3 id="54-jni_onload-定位">5.4 JNI_OnLoad 定位</h3>

<p>代码虽然解密、填充好了，但导出函数还是看不到，所以仍要借系统机制定位 <code class="language-plaintext highlighter-rouge">JNI_OnLoad</code>。跟 <code class="language-plaintext highlighter-rouge">LoadNativeLibrary</code> 流程：</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>art::JavaVMExt::LoadNativeLibrary(...)
  └─ art::SharedLibrary::FindSymbolWithoutNativeBridge("JNI_OnLoad")
         → 直接返回 JNI_OnLoad 地址
</code></pre></div></div>

<p>在 <code class="language-plaintext highlighter-rouge">FindSymbolWithoutNativeBridge</code> 返回处即可拿到真实 <code class="language-plaintext highlighter-rouge">JNI_OnLoad</code> 地址。</p>

<h3 id="55-反调试">5.5 反调试</h3>

<p><code class="language-plaintext highlighter-rouge">JNI_OnLoad</code> 里铺了几道反调试，常规手法：</p>

<ul>
  <li><strong>扫调试器</strong>：<code class="language-plaintext highlighter-rouge">opendir("/proc/")</code> + <code class="language-plaintext highlighter-rouge">readdir</code> 遍历进程，查是否有 <code class="language-plaintext highlighter-rouge">android_server</code>（IDA 远程调试服务）在跑；</li>
  <li><strong><code class="language-plaintext highlighter-rouge">fork</code> 子进程反调试</strong>：<code class="language-plaintext highlighter-rouge">fork</code> 出子进程，<code class="language-plaintext highlighter-rouge">pthread_create</code> 起反调试线程（典型的 <code class="language-plaintext highlighter-rouge">ptrace</code> 互锁 / 自附加思路）。</li>
</ul>

<h3 id="56-registernatives-与-dex-解密">5.6 RegisterNatives 与 DEX 解密</h3>

<p><code class="language-plaintext highlighter-rouge">JNI_OnLoad</code> 里 <code class="language-plaintext highlighter-rouge">RegisterNatives</code> 动态注册 native 方法（<code class="language-plaintext highlighter-rouge">X2</code> 寄存器即 <code class="language-plaintext highlighter-rouge">JNINativeMethod</code> 数组）：</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">typedef</span> <span class="k">struct</span> <span class="p">{</span>
    <span class="k">const</span> <span class="kt">char</span><span class="o">*</span> <span class="n">name</span><span class="p">;</span>       <span class="c1">// Java 层 native 方法名</span>
    <span class="k">const</span> <span class="kt">char</span><span class="o">*</span> <span class="n">signature</span><span class="p">;</span>  <span class="c1">// 函数签名</span>
    <span class="kt">void</span><span class="o">*</span>       <span class="n">fnPtr</span><span class="p">;</span>      <span class="c1">// native 实现地址</span>
<span class="p">}</span> <span class="n">JNINativeMethod</span><span class="p">;</span>
</code></pre></div></div>

<p><strong>解密 dex 前先查脱壳器</strong>——<code class="language-plaintext highlighter-rouge">access</code> 探测一票已知脱壳工具 / 模拟器特征：</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>/data/dexname
/data/local/tmp/unpacker.config
/data/fart
/sdcard/fart
/data/local/tmp/libFupk3.so
libFupk3.so
libblackdex.so
libhoudini.so      ; 模拟器（houdini = x86 上跑 ARM）
</code></pre></div></div>

<p>然后从壳 dex 里拷出一段<strong>配置密文</strong>解密，里面是包名、原 Application、各种 key 和开关：</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>c : 5FD6EB80B07CB412704C5A46D899C63B
p : com.xiaopeng.mycarinfo
a : com.xiaopeng.mycarinfo.application.tinker.CarApplication   // 原 App 入口
r : 047AEA142BC032C4
d : 4A7651EA24600BAA                                           // 解密 dex 的 key
m : 1   z : 0   u : 1   ...（一长串行为开关）
</code></pre></div></div>

<p>拿到 dex key 后，解析壳 dex 格式取出加密存放的 DEX，<strong>循环解密 6 个 DEX</strong> 还原出明文：</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>DecDex_sub:
    ...
    BL  memcpy_2              ; 拷贝 DEX 密文
    BL  DecKEY_sub           ; 解密 key（4A7651EA24600BAA）
    BL  initkey_sub          ; 初始化解密器
    BL  Dec_DexData_sub      ; 解出明文 DEX
    ...
</code></pre></div></div>

<blockquote>
  <p><strong>脱壳点 1</strong>：这里解密完，内存里就是<strong>原始明文 DEX</strong>，直接 dump 即可。</p>
</blockquote>

<h3 id="57-加载-dex">5.7 加载 DEX</h3>

<p>Android 10 是个分水岭：10 之前 <code class="language-plaintext highlighter-rouge">DexClassLoader</code> 加载的 dex 默认会走 <code class="language-plaintext highlighter-rouge">dex2oat</code> 优化；10 之后系统<strong>不再</strong>对 <code class="language-plaintext highlighter-rouge">DexClassLoader</code> 加载的 dex 做 <code class="language-plaintext highlighter-rouge">dex2oat</code>，运行时只接受系统生成的 OAT。所以壳直接调 art 内部接口加载明文 DEX：</p>

<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">std</span><span class="o">::</span><span class="n">unique_ptr</span><span class="o">&lt;</span><span class="n">DexFile</span><span class="o">&gt;</span> <span class="n">DexFile</span><span class="o">::</span><span class="n">OpenCommon</span><span class="p">(</span>
        <span class="k">const</span> <span class="kt">uint8_t</span><span class="o">*</span> <span class="n">base</span><span class="p">,</span> <span class="kt">size_t</span> <span class="n">size</span><span class="p">,</span>
        <span class="k">const</span> <span class="n">std</span><span class="o">::</span><span class="n">string</span><span class="o">&amp;</span> <span class="n">location</span><span class="p">,</span> <span class="kt">uint32_t</span> <span class="n">location_checksum</span><span class="p">,</span> <span class="p">...);</span>
</code></pre></div></div>

<blockquote>
  <p><strong>脱壳点 2</strong>：<code class="language-plaintext highlighter-rouge">DexFile::OpenCommon</code> 是个非常舒服的脱壳点——hook 它，<code class="language-plaintext highlighter-rouge">base</code>/<code class="language-plaintext highlighter-rouge">size</code> 就是完整 DEX，直接拿。之后壳构造 <code class="language-plaintext highlighter-rouge">PathDexList</code>，用其成员 <code class="language-plaintext highlighter-rouge">Element[] dexElements</code> 指向这些 DEX，DEX 就被挂进了 ClassLoader。</p>
</blockquote>

<h2 id="六脱壳二次打包">六、脱壳二次打包</h2>

<p>经过上面的分析，<strong>dump 点其实很多</strong>：内存里解密完那一刻、<code class="language-plaintext highlighter-rouge">DexFile::OpenCommon</code> 加载时，都能拿到明文 DEX。</p>

<p>把 dump 出来的 6 个 DEX 重新打包进 apk，<strong>入口类改成上面解出来的原 App 入口</strong> <code class="language-plaintext highlighter-rouge">com.xiaopeng.mycarinfo.application.tinker.CarApplication</code>，加载完所有 DEX 后反射调用原始入口，App 就能正常跑起来：</p>

<p><img src="/images/posts/android/yidun-unpack/repack-entry.jpg" alt="重打包反射调用原始入口" /></p>

<h2 id="七总结">七、总结</h2>

<p><strong>亮点</strong>：Native 层的 SO 保护比”整体压缩加密”那种壳上了一个台阶——<strong>SMC（运行时自修改）+ 自定义 linker 加载 SO + 抹掉 ELF 头/节信息</strong>，能很好地防止从内存直接 dump ELF。想完整脱出 SO，得按 ELF 格式重新拼一个完整文件出来，防脱壳的安全度确实是有的。</p>

<p><strong>不足</strong>：Native 层堆了那么多反调试、反脱壳，但<strong>最终 DEX 还是要在内存里以完整明文出现</strong>，这一步绕不过去——只要 DEX 落地成明文，各种脱壳机就很难真正防住。这也是 dex 整体加固的通病：壳保护得再狠，被保护的 DEX 总有”现明文”的一刻。</p>

<p>最后用一张表对比一下这个壳”防得住”和”防不住”的：</p>

<table>
  <thead>
    <tr>
      <th>防护手段</th>
      <th>目的</th>
      <th>实际效果</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>节信息处理 / 导出加密 / 抹 init_array</td>
      <td>反静态分析</td>
      <td>✅ 静态基本看不了</td>
    </tr>
    <tr>
      <td>SMC 运行时加解密</td>
      <td>反内存 dump 代码</td>
      <td>✅ 看到的永远是密文</td>
    </tr>
    <tr>
      <td>自定义 linker + 抹 ELF 头</td>
      <td>防 dump 完整 SO</td>
      <td>✅ dump 出来是残体，要手工重组</td>
    </tr>
    <tr>
      <td>多重反调试（android_server / fork ptrace）</td>
      <td>拦调试器</td>
      <td>⚠️ 常规手法，可对抗</td>
    </tr>
    <tr>
      <td>脱壳器特征检测（fart/blackdex/Fupk3）</td>
      <td>拦已知脱壳机</td>
      <td>⚠️ 黑名单式，能绕</td>
    </tr>
    <tr>
      <td>DEX 整体加密</td>
      <td>保护业务代码</td>
      <td>❌ 内存必现明文 → 可 dump</td>
    </tr>
  </tbody>
</table>

<p>一句话收尾：<strong>这个壳真正的功夫全在 SO 的自我保护上，DEX 本身反而是软肋</strong>。对 dex 壳来说，SO 加固做得再厚，也挡不住”内存里那份明文 DEX”——找准 <code class="language-plaintext highlighter-rouge">DexFile::OpenCommon</code> 这类系统加载点，脱壳就是水到渠成的事。</p>

<p>借这次分析，也顺带把高版本系统的 DEX 加载流程和自定义 linker 的玩法摸了一遍。</p>

<blockquote>
  <p>本文涉及的逆向均用于安全研究与防御对抗学习，请勿用于非法用途。</p>
</blockquote>]]></content><author><name>我是小三</name></author><category term="Android" /><category term="Android" /><category term="逆向" /><category term="脱壳" /><category term="加固" /><category term="易盾" /><category term="自定义linker" /><category term="SMC" /><category term="DEX" /><category term="ELF" /><summary type="html"><![CDATA[以某汽车 App（网易易盾加固）为样本，完整走一遍 dex 壳的逆向脱壳：从壳的 Java 层 attachBaseContext，到 SO 的入口隐藏、自定义 linker 加载、运行时 SMC 加解密、抹除 ELF 头防 dump，再到 JNI_OnLoad 定位、反调试、DEX 解密与 DexFile::OpenCommon 脱壳点，最后 dump + 二次打包跑起来。]]></summary></entry><entry><title type="html">某老牌反作弊产品分析：从 VM 加固到一个能被中间人攻击的加密漏洞</title><link href="https://xiaosan-re.github.io/2022/06/18/anticheat-sdk-crypto-mitm/" rel="alternate" type="text/html" title="某老牌反作弊产品分析：从 VM 加固到一个能被中间人攻击的加密漏洞" /><published>2022-06-18T00:00:00+08:00</published><updated>2022-06-18T00:00:00+08:00</updated><id>https://xiaosan-re.github.io/2022/06/18/anticheat-sdk-crypto-mitm</id><content type="html" xml:base="https://xiaosan-re.github.io/2022/06/18/anticheat-sdk-crypto-mitm/"><![CDATA[<h2 id="一背景--场景">一、背景 &amp; 场景</h2>

<h3 id="11-环境">1.1 环境</h3>

<p>样本：某老牌反作弊 SDK（Android arm64，含 <code class="language-plaintext highlighter-rouge">libtongdun.so</code> + <code class="language-plaintext highlighter-rouge">libtdvm.so</code> 双模块）</p>

<p>工具链：IDA Pro 9.2、Frida、JADX、JDK（用标准 <code class="language-plaintext highlighter-rouge">KeyFactory</code>/<code class="language-plaintext highlighter-rouge">Cipher</code> 还原算法）</p>

<h3 id="12-引言">1.2 引言</h3>

<p>做风控对抗，绕不开”设备指纹”这块硬骨头。厂商在 App 里塞一个 SDK，启动时采集一大堆设备信息、跑一遍环境检测，加密后上报服务器，换回一个 <code class="language-plaintext highlighter-rouge">blackbox</code> 字符串作为这台设备的唯一身份。我们这边想做的，无非是把这条链路看穿：<strong>采了什么、怎么加密、能不能伪造</strong>。</p>

<p>本文分析的是一款做了多年的老牌反作弊产品。它该有的对抗都有——字符串加密、关键算法塞进自研 VM、AB 双模块拆分逻辑、全套环境检测（多开 / Xposed / Magisk / 模拟器 / 云手机 / 自动点击）。整体看下来加固水平不低。</p>

<p>但最后落到加密协议本身时，它犯了一个<strong>致命的设计错误</strong>：用 <strong>RSA 私钥</strong> 去加密会话用的 AES KEY/IV，而这把私钥<strong>直接内置在 SO 里</strong>，并且是标准 <strong>PKCS#8</strong> 编码。PKCS#8 的私钥结构里同时带着 <code class="language-plaintext highlighter-rouge">n / e / d / p / q</code>——也就是说，<strong>拿到私钥就等于拿到了公钥</strong>。于是任何人都能从上报报文里把 AES KEY/IV 解出来，再解出全部明文设备数据。传输层的机密性直接归零，构成一个可被中间人攻击的加密漏洞。</p>

<p>下文按”先看清加固、再戳穿加密”的顺序展开：产品框架 → JNI 初始化 → VM 引擎 → 环境检测与信息采集 → 加密流程 → 漏洞还原与 MITM。文中私钥、密钥、指纹明文等敏感数据均做了部分脱敏处理，仅保留结构。</p>

<blockquote>
  <p>本文仅限学习交流与防御研究，请勿用于非法及商业用途。</p>
</blockquote>

<h2 id="二产品整体框架">二、产品整体框架</h2>

<p>逆向还原出来，整套 SDK 是 <strong>Java 接入层 + 两个 Native 模块</strong> 的结构，职责拆得很清楚：</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>┌──────────────────────── Java 接入层 ────────────────────────┐
│  FMAgent.initWithCallback(ctx, ENV, callback)               │
│     └─ 异步采集 → 加密 → 上报 → 回调 onEvent(blackbox)       │
└────────────────────────────┬───────────────────────────────┘
                             │ JNI
        ┌────────────────────┴────────────────────┐
        ▼                                          ▼
┌─────────────────────┐                ┌───────────────────────┐
│   libtongdun.so      │  调用 VM 解析  │      libtdvm.so        │
│  （业务逻辑 / A 模块）│ ─────────────► │  （VM 引擎 / B 模块）   │
│  环境检测、信息采集   │ ◄───────────── │  解压解密 + 执行字节码  │
│  加密流程编排         │   handle 回调  │  关键算法藏在字节码里   │
└─────────────────────┘                └───────────────────────┘
</code></pre></div></div>

<p>关键的对抗设计在于：<strong>A 模块（<code class="language-plaintext highlighter-rouge">libtongdun.so</code>）里大量关键方法并不直接实现算法，而是把一段 VM 字节码丢给 B 模块（<code class="language-plaintext highlighter-rouge">libtdvm.so</code>）解析执行</strong>。想静态看懂某个算法，就得先把 VM 的 handle 一个个还原出来。这是它把逆向成本抬高的主要手段。</p>

<p>逆向还原出的完整产品架构如下：</p>

<p><img src="/images/posts/android/anticheat-mitm/architecture.png" alt="产品整体架构" /></p>

<p>接入方使用上很简单：选对应平台 SDK → 前后端联调 → 运行 App 拿到 <code class="language-plaintext highlighter-rouge">blackbox</code> 设备指纹。整体对接的数据流程时序如下：</p>

<p><img src="/images/posts/android/anticheat-mitm/seq-diagram.png" alt="整体对接数据流程时序图" /></p>

<h2 id="三java-与-jni-初始化">三、JAVA 与 JNI 初始化</h2>

<h3 id="31-入口">3.1 入口</h3>

<p>接入方在首页 Activity 的 <code class="language-plaintext highlighter-rouge">onCreate</code> 里调用初始化，传环境标志和回调：</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// FMAgent.ENV_SANDBOX     沙盒环境</span>
<span class="c1">// FMAgent.ENV_PRODUCTION  生产环境</span>
<span class="nc">FMAgent</span><span class="o">.</span><span class="na">initWithCallback</span><span class="o">(</span><span class="k">this</span><span class="o">,</span> <span class="nc">FMAgent</span><span class="o">.</span><span class="na">ENV_PRODUCTION</span><span class="o">,</span> <span class="k">new</span> <span class="nc">FMCallback</span><span class="o">()</span> <span class="o">{</span>
    <span class="nd">@Override</span>
    <span class="kd">public</span> <span class="kt">void</span> <span class="nf">onEvent</span><span class="o">(</span><span class="nc">String</span> <span class="n">s</span><span class="o">)</span> <span class="o">{</span>
        <span class="n">tdBlackbox</span> <span class="o">=</span> <span class="n">s</span><span class="o">;</span>                 <span class="c1">// 拿到 blackbox 设备指纹</span>
        <span class="nc">Log</span><span class="o">.</span><span class="na">e</span><span class="o">(</span><span class="no">TAG</span><span class="o">,</span> <span class="s">"blackbox:"</span> <span class="o">+</span> <span class="n">tdBlackbox</span><span class="o">);</span>
    <span class="o">}</span>
<span class="o">});</span>
</code></pre></div></div>

<h3 id="32-加载-so">3.2 加载 SO</h3>

<p>初始化里按版本加载两个 SO，先 VM 引擎后业务模块：</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">if</span> <span class="o">(</span><span class="nc">Build</span><span class="o">.</span><span class="na">VERSION</span><span class="o">.</span><span class="na">SDK_INT</span> <span class="o">&gt;=</span> <span class="mi">17</span><span class="o">)</span> <span class="o">{</span>
    <span class="nc">System</span><span class="o">.</span><span class="na">loadLibrary</span><span class="o">(</span><span class="s">"tdvm"</span><span class="o">);</span>      <span class="c1">// VM 引擎</span>
<span class="o">}</span>
<span class="nc">System</span><span class="o">.</span><span class="na">loadLibrary</span><span class="o">(</span><span class="s">"tongdun"</span><span class="o">);</span>       <span class="c1">// 业务逻辑</span>
</code></pre></div></div>

<h3 id="33-jni_onload-动态注册">3.3 JNI_OnLoad 动态注册</h3>

<p>Native 方法走 <code class="language-plaintext highlighter-rouge">RegisterNatives</code> 动态注册——这是反作弊 SO 的标配，目的是让你没法直接靠导出符号定位 native 实现。注册逻辑本身被一层 trampoline 包了一下：</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>RegisterNatives_sub_786D163E4C
    ...
    BLR  X8            ; 实际调用 JNIEnv-&gt;RegisterNatives
    RET
</code></pre></div></div>

<p>还原出注册表里的 native 方法签名：</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>getData2        (Ljava/lang/String;)[B
tongdun         (Landroid/content/Context;)V
tongdun2        (Landroid/content/Context;)V
XOnEvent        (Landroid/content/Context;)Ljava/lang/String;
onSensorChanged (Landroid/hardware/SensorManager;Lcn/.../s;Landroid/hardware/SensorEvent;)V
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">XOnEvent</code> 就是采集 + 加密 + 返回 blackbox 的主入口，是后面所有分析的锚点。整个 Java/JNI 初始化流程如下：</p>

<p><img src="/images/posts/android/anticheat-mitm/jni-init-flow.png" alt="JAVA 与 JNI 初始化整体流程" /></p>

<h2 id="四vm-虚拟机基本逻辑">四、VM 虚拟机基本逻辑</h2>

<h3 id="41-两个导出函数">4.1 两个导出函数</h3>

<p>VM 逻辑全在 <code class="language-plaintext highlighter-rouge">libtdvm.so</code>，模块只导出两个方法：</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>td_eea7e05642c04e240c51   // 解压解密 VM 字节码（VMBycode）
td_b13d6928ba611f6a6e37   // 解析执行 VM 字节码
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">libtongdun.so</code> 里绝大多数关键方法，最终都会把一段字节码传给这两个函数解析执行。</p>

<h3 id="42-取指--派发--执行">4.2 取指 → 派发 → 执行</h3>

<p>进入 VM 后是一个非常典型的 <strong>取指 / 解码 / 派发</strong> 解释器循环。先取出解压后的字节码：</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>EnterVM_sub_709EA954F0
    ...
    LDR   X28, [X19,#0x28]      ; 解压后字节码基址
    LDR   X23, [X19]
    ADRP  X26, jump_table       ; 派发跳转表
    ...
    B     dsp_loc                ; 取下一条 VMBycode
</code></pre></div></div>

<p>派发处把指令字段抽出来当 switch 的 case 索引，跳到对应 handle：</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>dsp_loc:
    LDR    W21, [X8,X28]        ; 取一条 VMBycode
    UBFX   W8, W21, #0x19, #4   ; 抽出 opcode 字段
    SUB    W8, W8, #4           ; 归一化成 switch index
    CMP    W8, #0xB
    B.HI   default
    LDRSW  X8, [X26,X8,LSL#2]   ; 查跳转表
    ADD    X8, X8, X26
    BR     X8                   ; switch 跳转
</code></pre></div></div>

<h3 id="43-常见-handle">4.3 常见 Handle</h3>

<p>算术类 handle 都是教科书式的”取两个操作数、算、写回”，几条指令一个：</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ADD_sub:  ADD W8, W3, W2 ; STR W8, [X1] ; RET
SUB_sub:  SUB W8, W2, W3 ; STR W8, [X1] ; RET
AND_sub:  AND W8, W3, W2 ; STR W8, [X1] ; RET
EOR_sub:  EOR W8, W3, W2 ; STR W8, [X1] ; RET
MUL_sub:  MUL W8, W3, W2 ; STR W8, [X1] ; RET
</code></pre></div></div>

<p>加上一个取 VM 内部数据栈值的 <code class="language-plaintext highlighter-rouge">getdatabas</code> handle（按编号 <code class="language-plaintext highlighter-rouge">0x1D/0x1E/0x1F...</code> 取不同槽位的基址），加密时这些算术 + 取值 handle 来回组合，就把算法”摊”进了字节码里。</p>

<h3 id="44-跳出-vm-调其它模块的-handle">4.4 跳出 VM 调其它模块的 Handle</h3>

<p>最关键的是这个<strong>回调 handle</strong>——它负责从 VM 里跳回 <code class="language-plaintext highlighter-rouge">libtongdun.so</code> 执行 native 逻辑：</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>call_loc_709EA936C0
    SUB   SP, SP, #0x80
    BLR   X9            ; 调用 tongdun.so 中的 native 函数
    ADD   SP, SP, #0x80
    ...
</code></pre></div></div>

<blockquote>
  <p><strong>分析技巧</strong>：如果只想理清整体流程、不打算还原算法，调试时盯死这一个 handle 就够了——所有”VM → 业务模块”的调用都从这里出去，等于拿到了 VM 的对外调用日志。真要还原算法，才需要逐个 handle 啃。</p>
</blockquote>

<h2 id="五环境检测与设备信息采集">五、环境检测与设备信息采集</h2>

<h3 id="51-随机-idaes-加密--钉子文件">5.1 随机 ID：AES 加密 + 钉子文件</h3>

<p>首次运行时如果本地没有随机数 ID，就生成一个，用 AES 加密后<strong>写到三个位置</strong>互为备份（俗称”钉子文件”，删一个还能从另两个恢复），而且<strong>三处用不同的 AES KEY</strong>：</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>SharedPreferences  td-client-id-3
/data/user/0/&lt;包名&gt;/files/.td-3
/storage/emulated/0/.td-3

# 三处分别用的 KEY
bs3ggr0ismnzmdwxkacrq88xs9uj3l06
ykj314o0nd8423k2cimo5fvx0k234sc5
phx7ryl7sjppatga3nfl1caircw6ct79
</code></pre></div></div>

<p>AES 这一步是 native 里反射调用 Java 的 <code class="language-plaintext highlighter-rouge">Cipher</code> 实现的（<code class="language-plaintext highlighter-rouge">AES/ECB/PKCS5Padding</code>），native 侧负责把 key 字节逐个 <code class="language-plaintext highlighter-rouge">STRB</code> 拼出来再 <code class="language-plaintext highlighter-rouge">NewStringUTF</code> 传给 Java——典型的”算法在 Java、调度在 native，字符串全程不落地”写法。</p>

<h3 id="52-环境检测一览">5.2 环境检测一览</h3>

<p>检测部分覆盖得很全，手法基本是”特征字符串 + <code class="language-plaintext highlighter-rouge">access</code>/读 <code class="language-plaintext highlighter-rouge">maps</code>/<code class="language-plaintext highlighter-rouge">__system_property_get</code>/反射 <code class="language-plaintext highlighter-rouge">loadClass</code>“几板斧。整理成表：</p>

<table>
  <thead>
    <tr>
      <th>检测项</th>
      <th>手法</th>
      <th>部分特征</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><strong>多开</strong></td>
      <td>读 <code class="language-plaintext highlighter-rouge">/proc/self/maps</code> 找包名特征</td>
      <td><code class="language-plaintext highlighter-rouge">com.lody.virtual</code>、<code class="language-plaintext highlighter-rouge">com.lbe.parallel</code>、<code class="language-plaintext highlighter-rouge">com.qihoo.magic</code>、<code class="language-plaintext highlighter-rouge">io.va.exposed</code>、<code class="language-plaintext highlighter-rouge">com.excelliance.dualaid</code> …</td>
    </tr>
    <tr>
      <td><strong>Xposed</strong></td>
      <td>反射 <code class="language-plaintext highlighter-rouge">getSystemClassLoader().loadClass</code> + 扫进程</td>
      <td><code class="language-plaintext highlighter-rouge">de/robv/android/xposed/XposedBridge</code>、<code class="language-plaintext highlighter-rouge">libxposed_art.so</code>、<code class="language-plaintext highlighter-rouge">com.saurik.substrate</code></td>
    </tr>
    <tr>
      <td><strong>Magisk</strong></td>
      <td><code class="language-plaintext highlighter-rouge">df/mount/ps</code> + <code class="language-plaintext highlighter-rouge">strstr</code> 找特征</td>
      <td><code class="language-plaintext highlighter-rouge">/sbin/.magisk</code></td>
    </tr>
    <tr>
      <td><strong>自动点击</strong></td>
      <td><code class="language-plaintext highlighter-rouge">access</code> 探测路径</td>
      <td><code class="language-plaintext highlighter-rouge">net.aisence.Touchelper</code>、<code class="language-plaintext highlighter-rouge">com.cyjh.mobileanjian</code>、<code class="language-plaintext highlighter-rouge">com.touchsprite.android</code> …</td>
    </tr>
    <tr>
      <td><strong>模拟器</strong></td>
      <td><code class="language-plaintext highlighter-rouge">access</code> 文件 + 比包名 + <code class="language-plaintext highlighter-rouge">__system_property_get</code></td>
      <td><code class="language-plaintext highlighter-rouge">/dev/qemu_pipe</code>、<code class="language-plaintext highlighter-rouge">/dev/socket/genyd</code>、<code class="language-plaintext highlighter-rouge">/system/bin/nox-props</code>、<code class="language-plaintext highlighter-rouge">init.svc.droid4x</code>、<code class="language-plaintext highlighter-rouge">qemu.sf.lcd_density</code>、<code class="language-plaintext highlighter-rouge">bluestacks</code> 系列包名 …</td>
    </tr>
    <tr>
      <td><strong>云手机</strong></td>
      <td><code class="language-plaintext highlighter-rouge">access</code> + <code class="language-plaintext highlighter-rouge">getPackageInfo</code> 比包名</td>
      <td><code class="language-plaintext highlighter-rouge">com.haimawan.cloudappstore</code>、<code class="language-plaintext highlighter-rouge">com.baidu.mtc.ysera</code>、<code class="language-plaintext highlighter-rouge">cn.testin.itestin</code> …</td>
    </tr>
  </tbody>
</table>

<p>值得一提的是多开检测那段被做了<strong>控制流混淆 + 字符串加密</strong>：包名特征是运行时一个字节一个字节 <code class="language-plaintext highlighter-rouge">STRB</code> 拼出来的，再逐字符做半字节交换（<code class="language-plaintext highlighter-rouge">LSR #4</code> + <code class="language-plaintext highlighter-rouge">BFI</code>）解密，最后 <code class="language-plaintext highlighter-rouge">strstr</code> 去 <code class="language-plaintext highlighter-rouge">maps</code> 里匹配。静态看是一坨散乱的常量比较，动态下断点才看得清。</p>

<h3 id="53-设备信息采集">5.3 设备信息采集</h3>

<p>采集走的是一张<strong>“类名 + 方法名”表</strong>，native 里用 <code class="language-plaintext highlighter-rouge">forName / getDeclaredMethod / invoke</code> 双重反射逐项调用。表的内存布局很规整（每项固定槽位、<code class="language-plaintext highlighter-rouge">类名\0...方法名\0</code>）：</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>android.telephony.TelephonyManager  -&gt;  getDeviceId
                                         getVoiceMailNumber
                                         getSimSerialNumber
                                         getNetworkCountryIso
                                         getNetworkOperatorName
                                         getSimOperatorName
                                         getPhoneType
                                         getNetworkType
                                         getCellLocation
                                         getDeviceSoftwareVersion
android.net.wifi.WifiInfo            -&gt;  getMacAddress / getIpAddress / getSSID / getBSSID
android.net.wifi.WifiManager         -&gt;  getConnectionInfo / getDhcpInfo / getScanResults
java.net.NetworkInterface            -&gt;  getNetworkInterfaces
android.net.Proxy                    -&gt;  getHost
...
</code></pre></div></div>

<p>双重反射的核心调用链（已脱去 trampoline）：</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>GetStaticMethodID(... "forName" ...)        ; Class.forName(类名)
NewStringUTF(方法名)
GetMethodID(... "getDeclaredMethod" ...)     ; clazz.getDeclaredMethod(方法名)
CallObjectMethod(...)                        ; method.invoke() 取设备信息
</code></pre></div></div>

<blockquote>
  <p>要补全”还有哪些设备信息”，不用逐行读汇编——在 4.4 那个”跳出 VM”的 handle 上下断点，把每次回调的类名/方法名打出来即可，几次运行就能拉全清单。</p>
</blockquote>

<h3 id="54-每采一项vm-加密一次">5.4 每采一项，VM 加密一次</h3>

<p>采集不是攒齐再加密，而是<strong>每取到一项设备信息就进 VM 跑一次对应 handle 加密</strong>。这也是为什么前面要先把 VM 引擎看明白——加密散落在字节码里，逐项进行。</p>

<h2 id="六加密流程分析">六、加密流程分析</h2>

<p>把采集到的设备数据变成最终上报的报文，整条加密链是这样：</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>设备信息(JSON) ──► ① MurmurHash2 校验 + 组合 ──► ② 压缩
                                                      │
        ┌─────────────────────────────────────────────┘
        ▼
   ③ 随机生成 AES KEY/IV ──► ④ AES 加密压缩后的设备数据 ─┐
        │                                               │
        └─► ⑤ 用内置 RSA【私钥】加密 KEY/IV ─┐           │
                                            ▼           ▼
                              ⑥ 组合 [RSA(KEY/IV)] + [AES(设备数据)] ──► POST 上报
</code></pre></div></div>

<h3 id="61-压缩前的校验与组合">6.1 压缩前的校验与组合</h3>

<p>压缩前先算一道校验值跟设备数据组合。文中标的是”CRC”，但看常量 <code class="language-plaintext highlighter-rouge">0x5BD1E995</code> 就知道这其实是 <strong>MurmurHash2</strong>（这个乘子是 MurmurHash2 的标志性 <code class="language-plaintext highlighter-rouge">m</code>）：</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>EncData_sub:
    MOV   W8, #0x5BD1E995     ; MurmurHash2 magic
    EOR   W9, W2, W1
    ...
    MUL   W9, W9, W8
    EOR   W14, W14, W14,LSR#24
    ...
</code></pre></div></div>

<h3 id="62-随机生成-aes-keyiv">6.2 随机生成 AES KEY/IV</h3>

<p>KEY/IV 来自 <code class="language-plaintext highlighter-rouge">gettimeofday</code> 播种的 <code class="language-plaintext highlighter-rouge">srand</code> + <code class="language-plaintext highlighter-rouge">sprintf</code> 拼接，是<strong>每次会话动态生成</strong>的：</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>gettimeofday → srand → sprintf

// 本次抓到的明文 KEY/IV（脱敏保留结构）
KEY = fda958f6-07e5-47
IV  = e4ae2f7b-96b5-4a
</code></pre></div></div>

<h3 id="63-用-rsa-私钥加密-aes-keyiv问题就在这">6.3 用 RSA 私钥加密 AES KEY/IV（问题就在这）</h3>

<p>把 KEY 和 IV 拼成一个字符串 <code class="language-plaintext highlighter-rouge">fda958f6-07e5-47e4ae2f7b-96b5-4a</code>，然后——<strong>用 RSA 私钥对它加密</strong>。这把私钥<strong>硬编码在 SO 里</strong>，PKCS#8 base64（已隐去部分）：</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>MIICeAIBADANBgkqhkiG9w0BAQEFAASCAmIwggJeAgEAAoGBALE2OfQ8BYg9Lq4n
KGTamXyia6raCc1adzCOsFrnk/VN0s2W8yQJfYdq+QUNRHv0zANW0Uafh7nHCWeBn
... （中间略） ...
8POECQQDPcNOysMNbrLh1mGe6ydUsojSbheAIOZPQ/lhUbhzPXAXTYaPkTq7uty6S
YZOMtWLxIFZ1eA9HHm3tJOCgC888
</code></pre></div></div>

<p>加密同样是反射调 Java：<code class="language-plaintext highlighter-rouge">getInstance</code> → <code class="language-plaintext highlighter-rouge">generatePrivate</code> → <code class="language-plaintext highlighter-rouge">doFinal</code>。加密后的 KEY/IV 密文（128 字节，RSA-1024）：</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>98 93 1B 85 66 82 76 26  88 2B 09 13 AA 22 4E 76
9B 3F 47 93 8B A7 CD D7  A6 48 3D C9 70 55 29 6A
57 B7 65 AE F4 3E 2C CB  5C E1 CD 6B 57 B5 86 2F
1D 81 FC A3 56 27 64 13  27 42 A0 84 C3 23 CD 0D
05 D1 0D B0 22 36 FE 36  B5 17 61 6F 19 14 1D B1
67 A0 1F F4 F2 09 83 CA  C1 9A C4 64 14 F4 54 7D
DA ...
</code></pre></div></div>

<blockquote>
  <p>这里就埋下了第七节要戳穿的雷：<strong>“用私钥加密”在密码学上等价于签名</strong>，它保证的是”来源可信”，而<strong>不是机密性</strong>——因为对应的公钥按定义是公开可得的。把会话密钥用私钥加密，等于用一把人人能拿到的钥匙去锁机密数据。</p>
</blockquote>

<h3 id="64-aes-加密压缩后的设备数据">6.4 AES 加密压缩后的设备数据</h3>

<p>再用 6.2 那把随机 AES KEY 加密压缩后的设备数据。AES 实现是 native 自带的标准实现（key 扩展、查表都在），反编译出来就是教科书 AES：</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// X0:key, X1:位长(128/192/256), X2:输出轮密钥</span>
<span class="n">__int64</span> <span class="nf">AES_initkey</span><span class="p">(</span><span class="kt">unsigned</span> <span class="kt">int</span> <span class="o">*</span><span class="n">key</span><span class="p">,</span> <span class="kt">int</span> <span class="n">bits</span><span class="p">,</span> <span class="kt">unsigned</span> <span class="kt">int</span> <span class="o">*</span><span class="n">rk</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">if</span> <span class="p">(</span><span class="n">bits</span> <span class="o">!=</span> <span class="mi">128</span> <span class="o">&amp;&amp;</span> <span class="n">bits</span> <span class="o">!=</span> <span class="mi">256</span> <span class="o">&amp;&amp;</span> <span class="n">bits</span> <span class="o">!=</span> <span class="mi">192</span><span class="p">)</span> <span class="k">return</span> <span class="o">-</span><span class="mi">2</span><span class="p">;</span>
    <span class="n">rk</span><span class="p">[</span><span class="mi">60</span><span class="p">]</span> <span class="o">=</span> <span class="p">(</span><span class="n">bits</span><span class="o">==</span><span class="mi">128</span><span class="p">)</span><span class="o">?</span><span class="mi">10</span> <span class="o">:</span> <span class="p">(</span><span class="n">bits</span><span class="o">==</span><span class="mi">192</span><span class="p">)</span><span class="o">?</span><span class="mi">12</span> <span class="o">:</span> <span class="mi">14</span><span class="p">;</span>   <span class="c1">// 轮数</span>
    <span class="n">rk</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span> <span class="o">=</span> <span class="n">__builtin_bswap32</span><span class="p">(</span><span class="n">key</span><span class="p">[</span><span class="mi">0</span><span class="p">]);</span>
    <span class="n">rk</span><span class="p">[</span><span class="mi">1</span><span class="p">]</span> <span class="o">=</span> <span class="n">__builtin_bswap32</span><span class="p">(</span><span class="n">key</span><span class="p">[</span><span class="mi">1</span><span class="p">]);</span>
    <span class="n">rk</span><span class="p">[</span><span class="mi">2</span><span class="p">]</span> <span class="o">=</span> <span class="n">__builtin_bswap32</span><span class="p">(</span><span class="n">key</span><span class="p">[</span><span class="mi">2</span><span class="p">]);</span>
    <span class="n">rk</span><span class="p">[</span><span class="mi">3</span><span class="p">]</span> <span class="o">=</span> <span class="n">__builtin_bswap32</span><span class="p">(</span><span class="n">key</span><span class="p">[</span><span class="mi">3</span><span class="p">]);</span>
    <span class="c1">// ... 标准 Rijndael key expansion（Te/Rcon 查表）...</span>
<span class="p">}</span>
</code></pre></div></div>

<h3 id="65-组合上报">6.5 组合上报</h3>

<p>最后把两段拼起来——<code class="language-plaintext highlighter-rouge">[RSA 私钥加密的 AES KEY/IV] + [AES 加密的设备数据]</code>——POST 给服务器。组合后的报文格式如下（前 128 字节高亮部分是 RSA 加密的 AES KEY/IV，其后是 AES 加密的设备数据）：</p>

<p><img src="/images/posts/android/anticheat-mitm/report-format.jpg" alt="上报报文组合格式" /></p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>url: https://fp.fraudmetrix.cn/android3_5/profile.json?partner=xxx&amp;version=3.6.7&amp;clientSeqId=1654331726915998700
</code></pre></div></div>

<p>上报走反射调用 <code class="language-plaintext highlighter-rouge">cn/tongdun/android/shell/common/HttpHelper</code>，可选 <code class="language-plaintext highlighter-rouge">trustSSL</code> / 自定义 <code class="language-plaintext highlighter-rouge">HostnameVerifier</code>，并从响应 <code class="language-plaintext highlighter-rouge">Set-Cookie</code> 里抠出 <code class="language-plaintext highlighter-rouge">XXID</code> 存为会话 id：</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">private</span> <span class="kd">static</span> <span class="nc">String</span> <span class="nf">connect</span><span class="o">(</span><span class="no">URL</span> <span class="n">url</span><span class="o">,</span> <span class="kt">byte</span><span class="o">[]</span> <span class="n">body</span><span class="o">,</span> <span class="o">...)</span> <span class="o">{</span>
    <span class="nc">HttpsURLConnection</span> <span class="n">conn</span> <span class="o">=</span> <span class="o">(</span><span class="nc">HttpsURLConnection</span><span class="o">)</span> <span class="n">url</span><span class="o">.</span><span class="na">openConnection</span><span class="o">(</span><span class="nc">Proxy</span><span class="o">.</span><span class="na">NO_PROXY</span><span class="o">);</span>
    <span class="c1">// arg12==1: trustSSL  arg12==2: 自定义 HostnameVerifier</span>
    <span class="n">conn</span><span class="o">.</span><span class="na">setRequestMethod</span><span class="o">(</span><span class="s">"POST"</span><span class="o">);</span>
    <span class="n">conn</span><span class="o">.</span><span class="na">getOutputStream</span><span class="o">().</span><span class="na">write</span><span class="o">(</span><span class="n">body</span><span class="o">);</span>
    <span class="c1">// ... 从 Set-Cookie 解析 XXID → FMAgent.xxid ...</span>
    <span class="c1">// 读取响应体（blackbox）</span>
<span class="o">}</span>
</code></pre></div></div>

<p>服务器成功后返回 blackbox：</p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="nl">"code"</span><span class="p">:</span><span class="s2">"000"</span><span class="p">,</span><span class="nl">"desc"</span><span class="p">:</span><span class="s2">"k9OCtUBncUi1/r3N84z30FFW3AwxnmZnJfuKa2bhCcS/s9mKZAuBFnJ6BYRDDpUkz+fxJhWvD+bbun3eUbCyiw=="</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p>这个值由硬件 ID、OAID、文件 ID 等生成，就是这台设备最终的指纹。</p>

<h2 id="七加密漏洞还原与中间人攻击">七、加密漏洞还原与中间人攻击</h2>

<p>铺垫完整条加密链，现在戳穿它。目标很明确：<strong>不依赖任何运行时 hook，仅凭 SO 里内置的那把私钥 + 一份抓到的上报报文，把全部明文设备数据解出来</strong>。能做到，就证明这套传输加密形同虚设、可被中间人完全解读乃至伪造。</p>

<h3 id="71-从私钥反推公钥">7.1 从私钥反推公钥</h3>

<p>直觉上”只有私钥（d, n）很难推出公钥”——但前提是私钥只含 <code class="language-plaintext highlighter-rouge">d</code> 和 <code class="language-plaintext highlighter-rouge">n</code>。本 SDK 的私钥是 <strong>PKCS#8 / PKCS#1 <code class="language-plaintext highlighter-rouge">RSAPrivateKey</code></strong> 结构，它<strong>完整携带了密钥对的全部参数</strong>：</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>RSAPrivateKey ::= SEQUENCE {
    version           Version,
    modulus           INTEGER,  -- n
    publicExponent    INTEGER,  -- e   ← 公钥指数就在里面！
    privateExponent   INTEGER,  -- d
    prime1            INTEGER,  -- p
    prime2            INTEGER,  -- q
    exponent1         INTEGER,  -- d mod (p-1)
    exponent2         INTEGER,  -- d mod (q-1)
    coefficient       INTEGER   -- (q^-1) mod p
}
</code></pre></div></div>

<p>公钥就是 <code class="language-plaintext highlighter-rouge">(n, e)</code>，而 <code class="language-plaintext highlighter-rouge">n</code> 和 <code class="language-plaintext highlighter-rouge">e</code>(通常 <code class="language-plaintext highlighter-rouge">0x10001</code>) 都明明白白躺在私钥结构里。所以<strong>只要私钥泄露，公钥就是免费送的</strong>。用标准 JCE 加载私钥、取 <code class="language-plaintext highlighter-rouge">modulus</code>、配上 <code class="language-plaintext highlighter-rouge">e=010001</code> 重建公钥：</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// 1. 加载内置私钥（PKCS#8）</span>
<span class="kt">byte</span><span class="o">[]</span> <span class="n">buffer</span> <span class="o">=</span> <span class="nc">Base64</span><span class="o">.</span><span class="na">getDecoder</span><span class="o">().</span><span class="na">decode</span><span class="o">(</span><span class="n">strPrivateKey</span><span class="o">);</span>
<span class="nc">PKCS8EncodedKeySpec</span> <span class="n">keySpec</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">PKCS8EncodedKeySpec</span><span class="o">(</span><span class="n">buffer</span><span class="o">);</span>
<span class="nc">RSAPrivateKey</span> <span class="n">privateKey</span> <span class="o">=</span> <span class="o">(</span><span class="nc">RSAPrivateKey</span><span class="o">)</span>
        <span class="nc">KeyFactory</span><span class="o">.</span><span class="na">getInstance</span><span class="o">(</span><span class="s">"RSA"</span><span class="o">).</span><span class="na">generatePrivate</span><span class="o">(</span><span class="n">keySpec</span><span class="o">);</span>

<span class="c1">// 2. 从私钥里取 modulus，配 e=0x10001 重建公钥</span>
<span class="nc">BigInteger</span> <span class="n">modulus</span> <span class="o">=</span> <span class="n">privateKey</span><span class="o">.</span><span class="na">getModulus</span><span class="o">();</span>
<span class="nc">String</span> <span class="n">modulusHex</span> <span class="o">=</span> <span class="n">bytesToHex</span><span class="o">(</span><span class="n">modulus</span><span class="o">.</span><span class="na">toByteArray</span><span class="o">());</span>
<span class="nc">RSAPublicKeySpec</span> <span class="n">pubSpec</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">RSAPublicKeySpec</span><span class="o">(</span>
        <span class="k">new</span> <span class="nf">BigInteger</span><span class="o">(</span><span class="n">modulusHex</span><span class="o">,</span> <span class="mi">16</span><span class="o">),</span>
        <span class="k">new</span> <span class="nf">BigInteger</span><span class="o">(</span><span class="s">"010001"</span><span class="o">,</span> <span class="mi">16</span><span class="o">));</span>
<span class="nc">RSAPublicKey</span> <span class="n">publicKey</span> <span class="o">=</span> <span class="o">(</span><span class="nc">RSAPublicKey</span><span class="o">)</span>
        <span class="nc">KeyFactory</span><span class="o">.</span><span class="na">getInstance</span><span class="o">(</span><span class="s">"RSA"</span><span class="o">).</span><span class="na">generatePublic</span><span class="o">(</span><span class="n">pubSpec</span><span class="o">);</span>
<span class="nc">String</span> <span class="n">publicKeyString</span> <span class="o">=</span> <span class="nc">Base64</span><span class="o">.</span><span class="na">encode</span><span class="o">(</span><span class="n">publicKey</span><span class="o">.</span><span class="na">getEncoded</span><span class="o">());</span>
</code></pre></div></div>

<h3 id="72-用公钥解出-aes-keyiv">7.2 用公钥解出 AES KEY/IV</h3>

<p>私钥加密 ↔ 公钥解密。既然公钥已经到手，就拿它去解上报报文里那 128 字节的 RSA 密文（6.3 抓到的那段）：</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// keydata = 6.3 抓到的 128 字节 RSA 密文</span>
<span class="nc">String</span> <span class="n">aeskey</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">String</span><span class="o">(</span><span class="nc">RSAUtils</span><span class="o">.</span><span class="na">publicKeyDecrypt</span><span class="o">(</span><span class="n">publicKeyString</span><span class="o">,</span> <span class="n">keydata</span><span class="o">));</span>
<span class="nc">System</span><span class="o">.</span><span class="na">out</span><span class="o">.</span><span class="na">println</span><span class="o">(</span><span class="s">"aeskey: "</span> <span class="o">+</span> <span class="n">aeskey</span><span class="o">);</span>

<span class="c1">// 解出：前 16 字节是 AES KEY，后 16 字节是 IV</span>
<span class="c1">// =&gt; fda958f6-07e5-47e4ae2f7b-96b5-4a</span>
</code></pre></div></div>

<p>解出来的正是 6.2 那把随机 KEY/IV——<strong>整个会话密钥协商被攻破</strong>。</p>

<h3 id="73-用-aes-解出设备数据">7.3 用 AES 解出设备数据</h3>

<p>有了 KEY/IV，最后一步就是标准 AES 解密上报报文里的设备数据段：</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// 前 16 字节是 KEY，后 16 字节是 IV</span>
<span class="nc">String</span> <span class="n">key</span> <span class="o">=</span> <span class="s">"fda958f6-07e5-47"</span><span class="o">;</span>
<span class="nc">String</span> <span class="n">iv</span>  <span class="o">=</span> <span class="s">"e4ae2f7b-96b5-4a"</span><span class="o">;</span>

<span class="kd">public</span> <span class="kd">static</span> <span class="kt">byte</span><span class="o">[]</span> <span class="nf">aesDecrypt</span><span class="o">(</span><span class="nc">String</span> <span class="n">filePath</span><span class="o">,</span> <span class="nc">String</span> <span class="n">key</span><span class="o">,</span> <span class="nc">String</span> <span class="n">iv</span><span class="o">)</span> <span class="o">{</span>
    <span class="kt">byte</span><span class="o">[]</span> <span class="n">enc</span> <span class="o">=</span> <span class="nc">FileUtils</span><span class="o">.</span><span class="na">getContent</span><span class="o">(</span><span class="n">filePath</span><span class="o">);</span>          <span class="c1">// 上报报文中的 AES 段</span>
    <span class="nc">Cipher</span> <span class="n">cipher</span> <span class="o">=</span> <span class="nc">Cipher</span><span class="o">.</span><span class="na">getInstance</span><span class="o">(</span><span class="no">ALGORITHMS</span><span class="o">);</span>        <span class="c1">// AES/CBC/...</span>
    <span class="n">cipher</span><span class="o">.</span><span class="na">init</span><span class="o">(</span><span class="nc">Cipher</span><span class="o">.</span><span class="na">DECRYPT_MODE</span><span class="o">,</span>
            <span class="k">new</span> <span class="nf">SecretKeySpec</span><span class="o">(</span><span class="n">key</span><span class="o">.</span><span class="na">getBytes</span><span class="o">(),</span> <span class="s">"AES"</span><span class="o">),</span>
            <span class="k">new</span> <span class="nf">IvParameterSpec</span><span class="o">(</span><span class="n">iv</span><span class="o">.</span><span class="na">getBytes</span><span class="o">(</span><span class="nc">StandardCharsets</span><span class="o">.</span><span class="na">UTF_8</span><span class="o">)));</span>
    <span class="k">return</span> <span class="n">cipher</span><span class="o">.</span><span class="na">doFinal</span><span class="o">(</span><span class="n">enc</span><span class="o">);</span>                            <span class="c1">// 解出压缩后的设备数据</span>
<span class="o">}</span>
</code></pre></div></div>

<p>到这里，<strong>仅凭一把内置私钥 + 一份抓包，无需在目标进程里 hook 任何东西，就把上报的设备数据完整解了出来</strong>。这意味着位于网络中间的攻击者可以读取、甚至篡改/伪造任意设备的指纹上报——这就是标题里说的”可被中间人攻击的加密漏洞”。</p>

<blockquote>
  <p>AES 解出来的还是<strong>压缩后</strong>的数据，再往里还有一层 VM 内的处理需要还原大部分 handle。好在 VM 字节码没做强混淆，分析起来不难——这部分留个坑，给想深入的同学。</p>
</blockquote>

<h2 id="八总结">八、总结</h2>

<p>把这款产品拆完，几点观感：</p>

<p><strong>业务</strong>：老牌厂商，近几年从营销 / 渠道反作弊转向金融安全。产品包体偏大、模块偏多，体验、稳定性、易用性上能感觉到历史包袱。</p>

<p><strong>代码</strong>：架构不够精简，代码冗余明显；很多空数据加密时不做判空，空数据也照样进 VM 引擎跑一遍，白白吃性能。把部分关键算法塞进 VM 这一手，对抗逆向确实有效。</p>

<p><strong>安全</strong>：整体加固能力是在线的——字符串加密、AB 双模块拆分、关键逻辑下沉到 VM，逆向成本被实实在在抬高了。但<strong>一个致命短板抵消了大半努力：用内置的 RSA 私钥去加密会话密钥</strong>。私钥本就不该出现在客户端，更何况它是带全参数的 PKCS#8、公钥可直接反推。结果就是采集、检测、VM 加密一整套重武器堆下来，传输层却被一行 <code class="language-plaintext highlighter-rouge">getModulus()</code> 撕开口子。</p>

<p>最后用一张表对比一下”它以为的防护”和”实际的防护”：</p>

<table>
  <thead>
    <tr>
      <th>环节</th>
      <th>设计意图</th>
      <th>实际效果</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>关键算法塞进 VM</td>
      <td>抬高静态逆向成本</td>
      <td>✅ 有效，确实费劲</td>
    </tr>
    <tr>
      <td>字符串加密 / 多开 / 模拟器检测</td>
      <td>拦截改机、群控、自动化</td>
      <td>✅ 覆盖全面</td>
    </tr>
    <tr>
      <td>AB 双模块拆分</td>
      <td>打断分析链路</td>
      <td>✅ 增加跳转复杂度</td>
    </tr>
    <tr>
      <td>RSA【私钥】加密 AES KEY/IV</td>
      <td>保护传输机密性</td>
      <td>❌ 私钥内置 + PKCS#8 → 公钥可推 → 全量可解</td>
    </tr>
  </tbody>
</table>

<p>一句话收尾：<strong>加固做得再厚，密钥学基础错了就是地基塌方</strong>。对客户端来说，凡是”想保密的东西却把解密能力一并打包进 App”，本质上都等于没加密——RSA 用私钥加密、对称密钥硬编码、都是同一类错误的不同写法。</p>

<blockquote>
  <p>本文涉及的逆向均用于安全研究与防御对抗学习，相关私钥 / 密钥 / 指纹明文均已脱敏，请勿用于非法用途。</p>
</blockquote>]]></content><author><name>我是小三</name></author><category term="Android" /><category term="Android" /><category term="逆向" /><category term="反作弊" /><category term="设备指纹" /><category term="RSA" /><category term="AES" /><category term="VMP" /><category term="中间人攻击" /><summary type="html"><![CDATA[逆向某老牌反作弊 SDK 的设备指纹（blackbox）采集与加密链路：Java/JNI 初始化、自研 VM 引擎、环境检测、设备信息采集，最后还原出它"用 RSA 私钥加密 AES KEY/IV"的设计缺陷——私钥内置且为 PKCS#8，可反推公钥，进而解出全部上报数据，构成可被中间人攻击的加密漏洞。]]></summary></entry></feed>