<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="4.4.1">Jekyll</generator><link href="https://jtsylve.blog/feed.xml" rel="self" type="application/atom+xml" /><link href="https://jtsylve.blog/" rel="alternate" type="text/html" /><updated>2026-05-04T22:34:38+00:00</updated><id>https://jtsylve.blog/feed.xml</id><title type="html">Joe T. Sylve, Ph.D.</title><subtitle>Digital Forensic Researcher and Educator</subtitle><entry><title type="html">IDA-MCP Is Now RE-MCP With Ghidra Support</title><link href="https://jtsylve.blog/post/2026/05/04/ida-mcp-becomes-re-mcp" rel="alternate" type="text/html" title="IDA-MCP Is Now RE-MCP With Ghidra Support" /><published>2026-05-04T00:00:00+00:00</published><updated>2026-05-04T00:00:00+00:00</updated><id>https://jtsylve.blog/post/2026/05/04/ida-mcp-becomes-re-mcp</id><content type="html" xml:base="https://jtsylve.blog/post/2026/05/04/ida-mcp-becomes-re-mcp"><![CDATA[<p>When I started building ida-mcp, the goal was simple: give an LLM headless access to IDA Pro through MCP (Model Context Protocol). Open a binary, decompile functions, follow cross-references, rename symbols.</p>

<p>2.0 added a supervisor/worker architecture for analyzing multiple binaries simultaneously. 2.1 introduced progressive tool discovery so the LLM could find specialized tools on demand instead of loading ~195 schemas at startup. 2.2 added meta-tools that let the LLM write multi-step analysis scripts, issue bulk operations, and persist state across sessions through a daemon.</p>

<p>Each release solved a real friction point. But that progression revealed something about the interface itself. The tools the LLM actually calls (decompile this function, get cross-references to that address, rename this symbol, search for strings matching this pattern) described reverse engineering in the abstract, not IDA in particular. IDA was the engine behind those tools, but the tool surface itself was generic. An LLM asking to decompile <code class="language-plaintext highlighter-rouge">main</code> doesn’t care whether the answer comes from Hex-Rays or Ghidra’s decompiler. It cares about the pseudocode.</p>

<p>That realization is why ida-mcp is now <a href="https://github.com/jtsylve/re-mcp">re-mcp</a> (reverse engineering MCP). Version 3.0 ships with a full <a href="https://ghidra-sre.org/">Ghidra</a> backend alongside the existing IDA Pro backend, with a shared tool interface that makes LLM workflows portable across both.</p>

<h2 id="why-ghidra-matters-here">Why Ghidra matters here</h2>

<p>The most common response I heard after publishing ida-mcp was some variation of “this looks great, but I don’t have an IDA license.” IDA Pro is the industry standard for binary analysis, but it costs thousands of dollars per seat. For students, independent researchers, CTF players, and hobbyists, that puts LLM-driven reverse engineering out of reach before it even starts.</p>

<p>Ghidra, released by the NSA as open source in 2019, has become the primary free alternative. It supports dozens of processor architectures, its decompiler is capable, and it has an active community building extensions and loaders. By adding Ghidra as a backend, re-mcp makes everything from 2.0 through 2.2 (multi-database analysis, progressive tool discovery, <code class="language-plaintext highlighter-rouge">execute</code> scripts, <code class="language-plaintext highlighter-rouge">batch</code> operations) available to anyone willing to install a free tool and a JDK.</p>

<h2 id="getting-started-with-ghidra">Getting started with Ghidra</h2>

<p>The Ghidra backend requires Python 3.12+, <a href="https://ghidra-sre.org/">Ghidra 12+</a>, and JDK 21+. Ghidra’s install path is found automatically from the <code class="language-plaintext highlighter-rouge">GHIDRA_INSTALL_DIR</code> environment variable or platform-specific default locations.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>uv tool <span class="nb">install </span>re-mcp-ghidra
</code></pre></div></div>

<p>Then configure your MCP client:</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">"mcpServers"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nl">"ghidra"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
      </span><span class="nl">"command"</span><span class="p">:</span><span class="w"> </span><span class="s2">"uvx"</span><span class="p">,</span><span class="w">
      </span><span class="nl">"args"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">"re-mcp-ghidra"</span><span class="p">]</span><span class="w">
    </span><span class="p">}</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>From there, everything works the way it did with IDA. Open a binary, wait for analysis to complete, and start asking questions.</p>

<p>The meta-tools from 2.2 work on the Ghidra backend too. Here’s an <code class="language-plaintext highlighter-rouge">execute</code> script that finds functions referencing error strings and summarizes them:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">strings</span> <span class="o">=</span> <span class="k">await</span> <span class="nf">invoke</span><span class="p">(</span><span class="sh">"</span><span class="s">find_code_by_string</span><span class="sh">"</span><span class="p">,</span> <span class="p">{</span>
    <span class="sh">"</span><span class="s">pattern</span><span class="sh">"</span><span class="p">:</span> <span class="sh">"</span><span class="s">invalid|error|fail</span><span class="sh">"</span><span class="p">,</span> <span class="sh">"</span><span class="s">limit</span><span class="sh">"</span><span class="p">:</span> <span class="mi">50</span>
<span class="p">})</span>
<span class="n">seen</span> <span class="o">=</span> <span class="nf">set</span><span class="p">()</span>
<span class="n">results</span> <span class="o">=</span> <span class="p">[]</span>
<span class="k">for</span> <span class="n">hit</span> <span class="ow">in</span> <span class="n">strings</span><span class="p">[</span><span class="sh">"</span><span class="s">items</span><span class="sh">"</span><span class="p">]:</span>
    <span class="n">fn</span> <span class="o">=</span> <span class="n">hit</span><span class="p">.</span><span class="nf">get</span><span class="p">(</span><span class="sh">"</span><span class="s">function_name</span><span class="sh">"</span><span class="p">,</span> <span class="sh">""</span><span class="p">)</span>
    <span class="k">if</span> <span class="ow">not</span> <span class="n">fn</span> <span class="ow">or</span> <span class="n">fn</span> <span class="ow">in</span> <span class="n">seen</span><span class="p">:</span>
        <span class="k">continue</span>
    <span class="n">seen</span><span class="p">.</span><span class="nf">add</span><span class="p">(</span><span class="n">fn</span><span class="p">)</span>
    <span class="n">decomp</span> <span class="o">=</span> <span class="k">await</span> <span class="nf">invoke</span><span class="p">(</span><span class="sh">"</span><span class="s">decompile_function</span><span class="sh">"</span><span class="p">,</span> <span class="p">{</span>
        <span class="sh">"</span><span class="s">address</span><span class="sh">"</span><span class="p">:</span> <span class="n">hit</span><span class="p">[</span><span class="sh">"</span><span class="s">function_address</span><span class="sh">"</span><span class="p">]</span>
    <span class="p">})</span>
    <span class="n">results</span><span class="p">.</span><span class="nf">append</span><span class="p">({</span>
        <span class="sh">"</span><span class="s">function</span><span class="sh">"</span><span class="p">:</span> <span class="n">decomp</span><span class="p">[</span><span class="sh">"</span><span class="s">function_name</span><span class="sh">"</span><span class="p">],</span>
        <span class="sh">"</span><span class="s">address</span><span class="sh">"</span><span class="p">:</span> <span class="n">decomp</span><span class="p">[</span><span class="sh">"</span><span class="s">address</span><span class="sh">"</span><span class="p">],</span>
        <span class="sh">"</span><span class="s">matched_string</span><span class="sh">"</span><span class="p">:</span> <span class="n">hit</span><span class="p">[</span><span class="sh">"</span><span class="s">string_value</span><span class="sh">"</span><span class="p">],</span>
        <span class="sh">"</span><span class="s">lines</span><span class="sh">"</span><span class="p">:</span> <span class="nf">len</span><span class="p">(</span><span class="n">decomp</span><span class="p">[</span><span class="sh">"</span><span class="s">decompiled_code</span><span class="sh">"</span><span class="p">].</span><span class="nf">splitlines</span><span class="p">())</span>
    <span class="p">})</span>
<span class="k">return</span> <span class="p">{</span><span class="sh">"</span><span class="s">functions_with_error_strings</span><span class="sh">"</span><span class="p">:</span> <span class="n">results</span><span class="p">}</span>
</code></pre></div></div>

<p>One tool call. The LLM gets back every function that references an error string, with its decompiled size, ready for triage. The same workflow pattern from the <a href="/post/2026/04/21/ida-mcp-2.2">2.2 post</a> applies here (the only difference being response field names like <code class="language-plaintext highlighter-rouge">decompiled_code</code> vs. <code class="language-plaintext highlighter-rouge">pseudocode</code>).</p>

<h2 id="comparing-engines">Comparing engines</h2>

<p>There’s a practical reason to support both backends even if you already have an IDA license. IDA and Ghidra have different analysis engines, different heuristics for function boundary detection, different type propagation strategies. Running the same binary through both and comparing the output is a common practice in professional reverse engineering; each tool catches things the other misses.</p>

<p>With re-mcp, you configure both servers, and the LLM can open the same binary in each and compare function lists, decompiler output, and cross-references across the two.</p>

<h2 id="one-interface-two-engines">One interface, two engines</h2>

<p>Both backends implement the same core tool interface: identical tool names, identical parameters, and the same categories of information in responses (though individual field names in responses may differ slightly between engines). From a user’s perspective, it doesn’t matter which engine is running: the LLM issues the same tool calls and returns comparable results either way.</p>

<p>The shared surface covers the operations that define a reverse engineering session:</p>

<ul>
  <li><strong>Functions</strong>: list, decompile, disassemble, rename, set prototypes</li>
  <li><strong>Navigation</strong>: cross-references (to and from), imports, exports, entry points, names</li>
  <li><strong>Search</strong>: strings with regex filtering, byte patterns, immediate values</li>
  <li><strong>Types</strong>: local type libraries, structures, enums, type application</li>
  <li><strong>Annotation</strong>: comments, names, bookmarks</li>
  <li><strong>Patching</strong>: byte-level modification, segment operations</li>
  <li><strong>Meta-tools</strong>: <code class="language-plaintext highlighter-rouge">search_tools</code>, <code class="language-plaintext highlighter-rouge">get_schema</code>, <code class="language-plaintext highlighter-rouge">call</code>, <code class="language-plaintext highlighter-rouge">execute</code>, <code class="language-plaintext highlighter-rouge">batch</code></li>
</ul>

<p>An <code class="language-plaintext highlighter-rouge">execute</code> script that crawls error strings, decompiles referencing functions, and renames them follows the same logic on either engine; scripts only need to adjust for the field name differences noted above.</p>

<p>Each backend also retains capabilities specific to its engine. The IDA backend keeps everything from the 2.x releases: IDAPython scripting via <code class="language-plaintext highlighter-rouge">run_script</code>, file region mapping, executable rebuilding, IDC evaluation, and the eight guided prompts for structured analysis workflows. The Ghidra backend brings its own strengths: Function ID for automatic library function identification and data type archive support.</p>

<h2 id="architecture-and-transport">Architecture and transport</h2>

<p>re-mcp is a monorepo with three packages: <strong>re-mcp-core</strong> (supervisor, transport, meta-tools), <strong>re-mcp-ida</strong> (IDA Pro backend wrapping idalib), and <strong>re-mcp-ghidra</strong> (Ghidra backend wrapping <a href="https://github.com/NationalSecurityAgency/ghidra/tree/master/Ghidra/Features/PyGhidra">pyghidra</a>). The core package doesn’t depend on IDA or Ghidra. Backends are discovered through Python entry points, so you install only what you need:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># IDA users</span>
uv tool <span class="nb">install </span>re-mcp-ida

<span class="c"># Ghidra users</span>
uv tool <span class="nb">install </span>re-mcp-ghidra

<span class="c"># Both</span>
uv tool <span class="nb">install </span>re-mcp <span class="nt">--with</span> re-mcp-ida <span class="nt">--with</span> re-mcp-ghidra
</code></pre></div></div>

<p>Future backends (Binary Ninja, radare2, or something that doesn’t exist yet) would slot in as additional packages implementing the same worker interface, with no changes to the core or any existing backend.</p>

<p>re-mcp 3.0 switches the default transport to direct stdio: one session, workers terminate on disconnect. This is simpler to set up than the HTTP daemon that ida-mcp 2.2 defaulted to, and it works universally with every MCP client. For workflows that need persistence, the daemon is still available via <code class="language-plaintext highlighter-rouge">proxy</code> or <code class="language-plaintext highlighter-rouge">serve</code> subcommands (e.g., <code class="language-plaintext highlighter-rouge">re-mcp-ghidra serve</code>, <code class="language-plaintext highlighter-rouge">re-mcp-ida serve</code>). The transport mode is independent of the backend; all options work the same for <code class="language-plaintext highlighter-rouge">re-mcp-ida</code>, <code class="language-plaintext highlighter-rouge">re-mcp-ghidra</code>, and the unified <code class="language-plaintext highlighter-rouge">re-mcp --backend &lt;name&gt;</code> command.</p>

<h2 id="migrating-from-ida-mcp">Migrating from ida-mcp</h2>

<p>The legacy <code class="language-plaintext highlighter-rouge">ida-mcp</code> PyPI package now redirects to <code class="language-plaintext highlighter-rouge">re-mcp-ida</code>. Existing installations continue to work after upgrading:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>uv tool <span class="nb">install</span> <span class="nt">--upgrade</span> ida-mcp
<span class="c"># or install directly</span>
uv tool <span class="nb">install </span>re-mcp-ida
</code></pre></div></div>

<p>The MCP tool interface is backward compatible. Existing <code class="language-plaintext highlighter-rouge">execute</code> scripts, <code class="language-plaintext highlighter-rouge">batch</code> operations, and direct tool calls work without changes. Requirements are unchanged: IDA Pro 9+ with Python 3.12+. The main visible difference is the entry point name (<code class="language-plaintext highlighter-rouge">ida-mcp</code> becomes <code class="language-plaintext highlighter-rouge">re-mcp-ida</code>), though the old name continues to work as an alias.</p>

<p>Environment variables follow the same pattern as before, prefixed per backend. <code class="language-plaintext highlighter-rouge">IDA_MCP_</code> variables carry over unchanged for the IDA backend; the Ghidra backend uses <code class="language-plaintext highlighter-rouge">GHIDRA_MCP_</code> with the same suffixes.</p>

<h2 id="links">Links</h2>

<ul>
  <li><strong>Repository</strong>: <a href="https://github.com/jtsylve/re-mcp">github.com/jtsylve/re-mcp</a></li>
  <li><strong>PyPI</strong>: <a href="https://pypi.org/project/re-mcp-ida/">re-mcp-ida</a> · <a href="https://pypi.org/project/re-mcp-ghidra/">re-mcp-ghidra</a> · <a href="https://pypi.org/project/re-mcp/">re-mcp</a></li>
</ul>

<p>If you run into issues or have feature requests, please <a href="https://github.com/jtsylve/re-mcp/issues">open an issue</a> on GitHub.</p>

<hr />

<p><em>IDA Pro and Hex-Rays are trademarks of Hex-Rays SA. Ghidra is developed by the National Security Agency. re-mcp is an independent project and is not affiliated with or endorsed by Hex-Rays or the NSA.</em></p>]]></content><author><name></name></author><category term="reverse-engineering" /><category term="tools" /><category term="ida-pro" /><category term="ghidra" /><category term="mcp" /><category term="llm" /><category term="ai" /><category term="idalib" /><category term="pyghidra" /><category term="reverse-engineering" /><summary type="html"><![CDATA[When I started building ida-mcp, the goal was simple: give an LLM headless access to IDA Pro through MCP (Model Context Protocol). Open a binary, decompile functions, follow cross-references, rename symbols.]]></summary></entry><entry><title type="html">ida-mcp 2.2: From Tool Calls to Analysis Scripts</title><link href="https://jtsylve.blog/post/2026/04/21/ida-mcp-2.2" rel="alternate" type="text/html" title="ida-mcp 2.2: From Tool Calls to Analysis Scripts" /><published>2026-04-21T00:00:00+00:00</published><updated>2026-04-21T00:00:00+00:00</updated><id>https://jtsylve.blog/post/2026/04/21/ida-mcp-2.2</id><content type="html" xml:base="https://jtsylve.blog/post/2026/04/21/ida-mcp-2.2"><![CDATA[<p><a href="https://github.com/jtsylve/ida-mcp">ida-mcp 2.2.0</a> is out. This release removes the friction between what the LLM <em>wants</em> to do and what MCP lets it express in a single round trip.</p>

<p>In 2.1, each action was a discrete tool call: decompile this function, get cross-references to that address, rename this symbol. Every step was a full MCP round trip. Every intermediate result landed in the context window. An analysis workflow that a human would express as a ten-line IDAPython script became thirty sequential tool calls, each waiting for the previous one to return before the LLM could decide what to do next. The LLM knew what it wanted to do, but it couldn’t say it all at once.</p>

<p>2.2 introduces meta-tools that let the LLM operate at a higher level of abstraction: writing multi-step analysis scripts, issuing bulk operations, and calling tools it discovers at runtime. It also makes the server persistent, so analysis state survives across sessions. And for the first time, ida-mcp can analyze firmware and raw binaries directly.</p>

<h2 id="meta-tools">Meta-tools</h2>

<h3 id="execute-sandboxed-analysis-scripts"><code class="language-plaintext highlighter-rouge">execute</code>: sandboxed analysis scripts</h3>

<p><code class="language-plaintext highlighter-rouge">execute</code> accepts Python code that calls IDA tools through <code class="language-plaintext highlighter-rouge">await invoke(name, params)</code>, with full control flow: loops, conditionals, regex, <code class="language-plaintext highlighter-rouge">struct</code> unpacking, list comprehensions. Individual tools are still the right choice for simple operations, but for multi-step analysis, the LLM becomes a script writer.</p>

<p>Consider a common reverse engineering task: finding every function that references an error string and understanding how each one handles the error. In 2.1, this was a multi-step conversation:</p>

<ol>
  <li>Call <code class="language-plaintext highlighter-rouge">get_strings</code> with a filter → get back 40 matching strings</li>
  <li>Call <code class="language-plaintext highlighter-rouge">get_xrefs_to</code> for the first string address → get back 3 cross-references</li>
  <li>Call <code class="language-plaintext highlighter-rouge">decompile_function</code> for each referencing function → get back pseudocode</li>
  <li>Repeat steps 2–3 for each of the remaining 39 strings</li>
</ol>

<p>That’s potentially 160+ tool calls, each a full round trip, with the LLM holding intermediate addresses in context between calls. If the context window fills up mid-workflow, earlier results get compacted and the LLM loses track of where it was.</p>

<p>With <code class="language-plaintext highlighter-rouge">execute</code>, the same workflow is a single tool call:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">strings</span> <span class="o">=</span> <span class="k">await</span> <span class="nf">invoke</span><span class="p">(</span><span class="sh">"</span><span class="s">get_strings</span><span class="sh">"</span><span class="p">,</span> <span class="p">{</span><span class="sh">"</span><span class="s">filter</span><span class="sh">"</span><span class="p">:</span> <span class="sh">"</span><span class="s">error|fail|panic</span><span class="sh">"</span><span class="p">})</span>
<span class="n">results</span> <span class="o">=</span> <span class="p">[]</span>
<span class="k">for</span> <span class="n">s</span> <span class="ow">in</span> <span class="n">strings</span><span class="p">[</span><span class="sh">"</span><span class="s">strings</span><span class="sh">"</span><span class="p">]:</span>
    <span class="n">xrefs</span> <span class="o">=</span> <span class="k">await</span> <span class="nf">invoke</span><span class="p">(</span><span class="sh">"</span><span class="s">get_xrefs_to</span><span class="sh">"</span><span class="p">,</span> <span class="p">{</span><span class="sh">"</span><span class="s">address</span><span class="sh">"</span><span class="p">:</span> <span class="n">s</span><span class="p">[</span><span class="sh">"</span><span class="s">address</span><span class="sh">"</span><span class="p">]})</span>
    <span class="k">for</span> <span class="n">xref</span> <span class="ow">in</span> <span class="n">xrefs</span><span class="p">[</span><span class="sh">"</span><span class="s">xrefs</span><span class="sh">"</span><span class="p">]:</span>
        <span class="n">decomp</span> <span class="o">=</span> <span class="k">await</span> <span class="nf">invoke</span><span class="p">(</span><span class="sh">"</span><span class="s">decompile_function</span><span class="sh">"</span><span class="p">,</span> <span class="p">{</span><span class="sh">"</span><span class="s">address</span><span class="sh">"</span><span class="p">:</span> <span class="n">xref</span><span class="p">[</span><span class="sh">"</span><span class="s">from</span><span class="sh">"</span><span class="p">]})</span>
        <span class="n">results</span><span class="p">.</span><span class="nf">append</span><span class="p">({</span>
            <span class="sh">"</span><span class="s">string</span><span class="sh">"</span><span class="p">:</span> <span class="n">s</span><span class="p">[</span><span class="sh">"</span><span class="s">value</span><span class="sh">"</span><span class="p">],</span>
            <span class="sh">"</span><span class="s">function</span><span class="sh">"</span><span class="p">:</span> <span class="n">decomp</span><span class="p">[</span><span class="sh">"</span><span class="s">name</span><span class="sh">"</span><span class="p">],</span>
            <span class="sh">"</span><span class="s">pseudocode</span><span class="sh">"</span><span class="p">:</span> <span class="n">decomp</span><span class="p">[</span><span class="sh">"</span><span class="s">pseudocode</span><span class="sh">"</span><span class="p">]</span>
        <span class="p">})</span>
<span class="k">return</span> <span class="n">results</span>
</code></pre></div></div>

<p>One round trip. The LLM gets back a structured result containing every error-handling function with its decompiled pseudocode. No intermediate state to track, no context window spent on addresses it only needed temporarily. And if the LLM decides the approach is wrong, it’s only wasted one tool call finding out.</p>

<p>Any “get a list, then process each item” workflow collapses from O(n) tool calls to one. The bigger gain is for workflows that don’t reduce to sequential calls: conditional logic, data transformation, or cross-referencing between results.</p>

<p><strong>Automated renaming based on string references:</strong></p>

<p>A stripped binary might have thousands of <code class="language-plaintext highlighter-rouge">sub_*</code> functions with no meaningful names, but many of them reference string literals that hint at their purpose. A human analyst would scan through decompiled output, spot a string like <code class="language-plaintext highlighter-rouge">"failed to parse header"</code>, and rename the function accordingly. With <code class="language-plaintext highlighter-rouge">execute</code>, the LLM can do this systematically across the entire binary in a single tool call:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="n">re</span>

<span class="n">funcs</span> <span class="o">=</span> <span class="k">await</span> <span class="nf">invoke</span><span class="p">(</span><span class="sh">"</span><span class="s">list_functions</span><span class="sh">"</span><span class="p">,</span> <span class="p">{</span><span class="sh">"</span><span class="s">filter</span><span class="sh">"</span><span class="p">:</span> <span class="sh">"</span><span class="s">sub_</span><span class="sh">"</span><span class="p">})</span>
<span class="n">renamed</span> <span class="o">=</span> <span class="p">[]</span>
<span class="k">for</span> <span class="n">func</span> <span class="ow">in</span> <span class="n">funcs</span><span class="p">[</span><span class="sh">"</span><span class="s">functions</span><span class="sh">"</span><span class="p">]:</span>
    <span class="n">decomp</span> <span class="o">=</span> <span class="k">await</span> <span class="nf">invoke</span><span class="p">(</span><span class="sh">"</span><span class="s">decompile_function</span><span class="sh">"</span><span class="p">,</span> <span class="p">{</span><span class="sh">"</span><span class="s">address</span><span class="sh">"</span><span class="p">:</span> <span class="n">func</span><span class="p">[</span><span class="sh">"</span><span class="s">address</span><span class="sh">"</span><span class="p">]})</span>
    <span class="n">strings</span> <span class="o">=</span> <span class="n">re</span><span class="p">.</span><span class="nf">findall</span><span class="p">(</span><span class="sa">r</span><span class="sh">'"</span><span class="s">([^</span><span class="sh">"</span><span class="s">]{4,})</span><span class="sh">"'</span><span class="p">,</span> <span class="n">decomp</span><span class="p">[</span><span class="sh">"</span><span class="s">pseudocode</span><span class="sh">"</span><span class="p">])</span>
    <span class="k">if</span> <span class="n">strings</span><span class="p">:</span>
        <span class="n">candidate</span> <span class="o">=</span> <span class="n">re</span><span class="p">.</span><span class="nf">sub</span><span class="p">(</span><span class="sa">r</span><span class="sh">'</span><span class="s">[^a-zA-Z0-9_]</span><span class="sh">'</span><span class="p">,</span> <span class="sh">'</span><span class="s">_</span><span class="sh">'</span><span class="p">,</span> <span class="n">strings</span><span class="p">[</span><span class="mi">0</span><span class="p">])[:</span><span class="mi">40</span><span class="p">]</span>
        <span class="k">await</span> <span class="nf">invoke</span><span class="p">(</span><span class="sh">"</span><span class="s">rename_function</span><span class="sh">"</span><span class="p">,</span> <span class="p">{</span>
            <span class="sh">"</span><span class="s">address</span><span class="sh">"</span><span class="p">:</span> <span class="n">func</span><span class="p">[</span><span class="sh">"</span><span class="s">address</span><span class="sh">"</span><span class="p">],</span>
            <span class="sh">"</span><span class="s">new_name</span><span class="sh">"</span><span class="p">:</span> <span class="sa">f</span><span class="sh">"</span><span class="s">uses_</span><span class="si">{</span><span class="n">candidate</span><span class="si">}</span><span class="sh">"</span>
        <span class="p">})</span>
        <span class="n">renamed</span><span class="p">.</span><span class="nf">append</span><span class="p">({</span><span class="sh">"</span><span class="s">old</span><span class="sh">"</span><span class="p">:</span> <span class="n">func</span><span class="p">[</span><span class="sh">"</span><span class="s">name</span><span class="sh">"</span><span class="p">],</span> <span class="sh">"</span><span class="s">new</span><span class="sh">"</span><span class="p">:</span> <span class="sa">f</span><span class="sh">"</span><span class="s">uses_</span><span class="si">{</span><span class="n">candidate</span><span class="si">}</span><span class="sh">"</span><span class="p">})</span>
<span class="k">return</span> <span class="p">{</span><span class="sh">"</span><span class="s">renamed</span><span class="sh">"</span><span class="p">:</span> <span class="nf">len</span><span class="p">(</span><span class="n">renamed</span><span class="p">),</span> <span class="sh">"</span><span class="s">functions</span><span class="sh">"</span><span class="p">:</span> <span class="n">renamed</span><span class="p">}</span>
</code></pre></div></div>

<p>The names this generates are rough: a first pass rather than a final answer. But <code class="language-plaintext highlighter-rouge">uses_failed_to_parse_header</code> is vastly more useful than <code class="language-plaintext highlighter-rouge">sub_140001A30</code> when you’re trying to understand a binary’s structure, and the LLM can refine them in a second pass once it understands the broader architecture.</p>

<p><strong>Cross-database patch diffing:</strong></p>

<p>Patch analysis requires comparing function lists between two versions of a library, identifying what was added or removed, and diffing the implementations that exist in both. Without <code class="language-plaintext highlighter-rouge">execute</code>, the LLM would pull function lists from each database in separate tool calls, hold both in context, compute set differences itself, and decompile changed functions one at a time. Dozens of round trips, large intermediate results sitting in context.</p>

<p>With <code class="language-plaintext highlighter-rouge">execute</code>, the entire triage happens server-side:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">old_funcs</span> <span class="o">=</span> <span class="k">await</span> <span class="nf">invoke</span><span class="p">(</span><span class="sh">"</span><span class="s">list_functions</span><span class="sh">"</span><span class="p">,</span> <span class="p">{</span><span class="sh">"</span><span class="s">database</span><span class="sh">"</span><span class="p">:</span> <span class="sh">"</span><span class="s">libcrypto_1.1.1</span><span class="sh">"</span><span class="p">})</span>
<span class="n">new_funcs</span> <span class="o">=</span> <span class="k">await</span> <span class="nf">invoke</span><span class="p">(</span><span class="sh">"</span><span class="s">list_functions</span><span class="sh">"</span><span class="p">,</span> <span class="p">{</span><span class="sh">"</span><span class="s">database</span><span class="sh">"</span><span class="p">:</span> <span class="sh">"</span><span class="s">libcrypto_1.1.2</span><span class="sh">"</span><span class="p">})</span>

<span class="n">old_names</span> <span class="o">=</span> <span class="p">{</span><span class="n">f</span><span class="p">[</span><span class="sh">"</span><span class="s">name</span><span class="sh">"</span><span class="p">]</span> <span class="k">for</span> <span class="n">f</span> <span class="ow">in</span> <span class="n">old_funcs</span><span class="p">[</span><span class="sh">"</span><span class="s">functions</span><span class="sh">"</span><span class="p">]}</span>
<span class="n">new_names</span> <span class="o">=</span> <span class="p">{</span><span class="n">f</span><span class="p">[</span><span class="sh">"</span><span class="s">name</span><span class="sh">"</span><span class="p">]</span> <span class="k">for</span> <span class="n">f</span> <span class="ow">in</span> <span class="n">new_funcs</span><span class="p">[</span><span class="sh">"</span><span class="s">functions</span><span class="sh">"</span><span class="p">]}</span>

<span class="n">added</span> <span class="o">=</span> <span class="nf">sorted</span><span class="p">(</span><span class="n">new_names</span> <span class="o">-</span> <span class="n">old_names</span><span class="p">)</span>
<span class="n">removed</span> <span class="o">=</span> <span class="nf">sorted</span><span class="p">(</span><span class="n">old_names</span> <span class="o">-</span> <span class="n">new_names</span><span class="p">)</span>

<span class="c1"># Spot-check shared functions for implementation changes
</span><span class="n">changed</span> <span class="o">=</span> <span class="p">[]</span>
<span class="k">for</span> <span class="n">name</span> <span class="ow">in</span> <span class="nf">sorted</span><span class="p">(</span><span class="n">old_names</span> <span class="o">&amp;</span> <span class="n">new_names</span><span class="p">)[:</span><span class="mi">30</span><span class="p">]:</span>
    <span class="n">old_dec</span> <span class="o">=</span> <span class="k">await</span> <span class="nf">invoke</span><span class="p">(</span><span class="sh">"</span><span class="s">decompile_function</span><span class="sh">"</span><span class="p">,</span> <span class="p">{</span>
        <span class="sh">"</span><span class="s">address</span><span class="sh">"</span><span class="p">:</span> <span class="n">name</span><span class="p">,</span> <span class="sh">"</span><span class="s">database</span><span class="sh">"</span><span class="p">:</span> <span class="sh">"</span><span class="s">libcrypto_1.1.1</span><span class="sh">"</span>
    <span class="p">})</span>
    <span class="n">new_dec</span> <span class="o">=</span> <span class="k">await</span> <span class="nf">invoke</span><span class="p">(</span><span class="sh">"</span><span class="s">decompile_function</span><span class="sh">"</span><span class="p">,</span> <span class="p">{</span>
        <span class="sh">"</span><span class="s">address</span><span class="sh">"</span><span class="p">:</span> <span class="n">name</span><span class="p">,</span> <span class="sh">"</span><span class="s">database</span><span class="sh">"</span><span class="p">:</span> <span class="sh">"</span><span class="s">libcrypto_1.1.2</span><span class="sh">"</span>
    <span class="p">})</span>
    <span class="k">if</span> <span class="n">old_dec</span><span class="p">[</span><span class="sh">"</span><span class="s">pseudocode</span><span class="sh">"</span><span class="p">]</span> <span class="o">!=</span> <span class="n">new_dec</span><span class="p">[</span><span class="sh">"</span><span class="s">pseudocode</span><span class="sh">"</span><span class="p">]:</span>
        <span class="n">changed</span><span class="p">.</span><span class="nf">append</span><span class="p">(</span><span class="n">name</span><span class="p">)</span>

<span class="k">return</span> <span class="p">{</span>
    <span class="sh">"</span><span class="s">added</span><span class="sh">"</span><span class="p">:</span> <span class="n">added</span><span class="p">[:</span><span class="mi">50</span><span class="p">],</span>
    <span class="sh">"</span><span class="s">removed</span><span class="sh">"</span><span class="p">:</span> <span class="n">removed</span><span class="p">[:</span><span class="mi">50</span><span class="p">],</span>
    <span class="sh">"</span><span class="s">changed</span><span class="sh">"</span><span class="p">:</span> <span class="n">changed</span><span class="p">,</span>
    <span class="sh">"</span><span class="s">summary</span><span class="sh">"</span><span class="p">:</span> <span class="p">{</span>
        <span class="sh">"</span><span class="s">added</span><span class="sh">"</span><span class="p">:</span> <span class="nf">len</span><span class="p">(</span><span class="n">added</span><span class="p">),</span>
        <span class="sh">"</span><span class="s">removed</span><span class="sh">"</span><span class="p">:</span> <span class="nf">len</span><span class="p">(</span><span class="n">removed</span><span class="p">),</span>
        <span class="sh">"</span><span class="s">shared_checked</span><span class="sh">"</span><span class="p">:</span> <span class="nf">min</span><span class="p">(</span><span class="mi">30</span><span class="p">,</span> <span class="nf">len</span><span class="p">(</span><span class="n">old_names</span> <span class="o">&amp;</span> <span class="n">new_names</span><span class="p">)),</span>
        <span class="sh">"</span><span class="s">shared_changed</span><span class="sh">"</span><span class="p">:</span> <span class="nf">len</span><span class="p">(</span><span class="n">changed</span><span class="p">)</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>The <code class="language-plaintext highlighter-rouge">database</code> parameter override lets a single <code class="language-plaintext highlighter-rouge">execute</code> block work across multiple open databases. Each <code class="language-plaintext highlighter-rouge">invoke</code> call can target a different database by name. The LLM gets back a structured summary of what changed between versions, and can then drill into specific changed functions in follow-up calls. The set operations, sorting, and conditional comparison all happen server-side rather than burning context on intermediate data the LLM only needs to pass through.</p>

<h4 id="the-sandbox">The sandbox</h4>

<p>The code runs in a <a href="https://restrictedpython.readthedocs.io/">RestrictedPython</a> sandbox. The LLM can import <code class="language-plaintext highlighter-rouge">re</code>, <code class="language-plaintext highlighter-rouge">struct</code>, <code class="language-plaintext highlighter-rouge">json</code>, <code class="language-plaintext highlighter-rouge">math</code>, <code class="language-plaintext highlighter-rouge">collections</code>, <code class="language-plaintext highlighter-rouge">itertools</code>, <code class="language-plaintext highlighter-rouge">functools</code>, and a few other safe standard library modules. It cannot access the filesystem, open network connections, or spawn subprocesses. Attribute access to dunder names (<code class="language-plaintext highlighter-rouge">__class__</code>, <code class="language-plaintext highlighter-rouge">__globals__</code>, <code class="language-plaintext highlighter-rouge">__code__</code>) is blocked at the AST level, closing Python sandbox escape hatches. Print output is capped at ~1 MiB to prevent runaway loops from exhausting worker memory.</p>

<p>Database lifecycle tools (<code class="language-plaintext highlighter-rouge">open_database</code>, <code class="language-plaintext highlighter-rouge">close_database</code>, <code class="language-plaintext highlighter-rouge">wait_for_analysis</code>) are blocked inside the sandbox; an <code class="language-plaintext highlighter-rouge">execute</code> block shouldn’t be spawning or tearing down workers as a side effect. The meta-tools themselves (<code class="language-plaintext highlighter-rouge">execute</code>, <code class="language-plaintext highlighter-rouge">batch</code>, <code class="language-plaintext highlighter-rouge">call</code>) are also blocked to prevent recursion. Everything else (decompilation, disassembly, renaming, commenting, type manipulation, structure editing) is available through <code class="language-plaintext highlighter-rouge">await invoke()</code>.</p>

<p>A failed <code class="language-plaintext highlighter-rouge">invoke</code> call raises a Python exception that the script can catch with <code class="language-plaintext highlighter-rouge">try</code>/<code class="language-plaintext highlighter-rouge">except</code>, or that terminates the block with an error message if uncaught.</p>

<p>If the LLM writes an <code class="language-plaintext highlighter-rouge">execute</code> block that contains a single <code class="language-plaintext highlighter-rouge">invoke</code> call with no processing logic around it, the server detects this and returns a hint suggesting the simpler <code class="language-plaintext highlighter-rouge">call</code> meta-tool instead. Small nudges like this help the LLM learn the right tool for the job over the course of a session.</p>

<h3 id="batch-bulk-operations-without-scripting-overhead"><code class="language-plaintext highlighter-rouge">batch</code>: bulk operations without scripting overhead</h3>

<p>Not every multi-call workflow needs control flow. Sometimes it’s the same operation twenty times: decompile a list of functions, rename a set of symbols, add comments at known addresses. For these, <code class="language-plaintext highlighter-rouge">execute</code> is overkill: sandbox overhead just to loop over a list. <code class="language-plaintext highlighter-rouge">batch</code> handles this directly: a list of operations, run sequentially with per-item error handling.</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">"operations"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
    </span><span class="p">{</span><span class="nl">"tool"</span><span class="p">:</span><span class="w"> </span><span class="s2">"decompile_function"</span><span class="p">,</span><span class="w"> </span><span class="nl">"params"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="nl">"address"</span><span class="p">:</span><span class="w"> </span><span class="s2">"0x401000"</span><span class="p">}},</span><span class="w">
    </span><span class="p">{</span><span class="nl">"tool"</span><span class="p">:</span><span class="w"> </span><span class="s2">"decompile_function"</span><span class="p">,</span><span class="w"> </span><span class="nl">"params"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="nl">"address"</span><span class="p">:</span><span class="w"> </span><span class="s2">"0x401100"</span><span class="p">}},</span><span class="w">
    </span><span class="p">{</span><span class="nl">"tool"</span><span class="p">:</span><span class="w"> </span><span class="s2">"rename_function"</span><span class="p">,</span><span class="w"> </span><span class="nl">"params"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="nl">"address"</span><span class="p">:</span><span class="w"> </span><span class="s2">"0x401000"</span><span class="p">,</span><span class="w"> </span><span class="nl">"new_name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"parse_header"</span><span class="p">}},</span><span class="w">
    </span><span class="p">{</span><span class="nl">"tool"</span><span class="p">:</span><span class="w"> </span><span class="s2">"rename_function"</span><span class="p">,</span><span class="w"> </span><span class="nl">"params"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="nl">"address"</span><span class="p">:</span><span class="w"> </span><span class="s2">"0x401100"</span><span class="p">,</span><span class="w"> </span><span class="nl">"new_name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"validate_checksum"</span><span class="p">}},</span><span class="w">
    </span><span class="p">{</span><span class="nl">"tool"</span><span class="p">:</span><span class="w"> </span><span class="s2">"set_comment"</span><span class="p">,</span><span class="w"> </span><span class="nl">"params"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="nl">"address"</span><span class="p">:</span><span class="w"> </span><span class="s2">"0x401000"</span><span class="p">,</span><span class="w"> </span><span class="nl">"comment"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Entry point for packet parsing"</span><span class="p">}},</span><span class="w">
    </span><span class="p">{</span><span class="nl">"tool"</span><span class="p">:</span><span class="w"> </span><span class="s2">"set_comment"</span><span class="p">,</span><span class="w"> </span><span class="nl">"params"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="nl">"address"</span><span class="p">:</span><span class="w"> </span><span class="s2">"0x401100"</span><span class="p">,</span><span class="w"> </span><span class="nl">"comment"</span><span class="p">:</span><span class="w"> </span><span class="s2">"CRC-32 validation"</span><span class="p">}}</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>Up to 50 operations per call, mixing different tools freely. This example decompiles two functions, renames them, and annotates them: six operations that would have been six separate tool calls in 2.1, collapsed into one.</p>

<p>In 2.1, batching was baked into individual tools: <code class="language-plaintext highlighter-rouge">decompile_function</code> accepted up to 50 addresses, <code class="language-plaintext highlighter-rouge">get_xrefs_to</code> accepted up to 50, each with its own batch parameter format. The LLM had to remember which tools supported batching and how each one worked. The unified <code class="language-plaintext highlighter-rouge">batch</code> meta-tool replaces all of that: a list of <code class="language-plaintext highlighter-rouge">{tool, params}</code> objects. Any tool can be batched.</p>

<p><code class="language-plaintext highlighter-rouge">stop_on_error</code> controls whether the batch aborts on the first failure or continues collecting results. The default is to continue: if 30 functions are being renamed and one address is invalid, the other 29 still succeed. The response includes per-operation success/failure status, so the LLM can see exactly what failed and decide whether to retry or move on.</p>

<p>The split is straightforward: if there’s no data dependency between operations (the output of one doesn’t feed into another), the LLM uses <code class="language-plaintext highlighter-rouge">batch</code>. If the workflow chains outputs, filters intermediate results, or applies conditional logic, it writes an <code class="language-plaintext highlighter-rouge">execute</code> script.</p>

<h3 id="call-and-get_schema-the-discovery-layer"><code class="language-plaintext highlighter-rouge">call</code> and <code class="language-plaintext highlighter-rouge">get_schema</code>: the discovery layer</h3>

<p>2.1 introduced progressive tool discovery: ~20 core tools registered upfront, the rest discoverable via <code class="language-plaintext highlighter-rouge">search_tools</code> and callable through <code class="language-plaintext highlighter-rouge">call_tool</code>. 2.2 refines this into a cleaner surface:</p>

<ul>
  <li><strong><code class="language-plaintext highlighter-rouge">search_tools</code></strong>: regex search over tool names, descriptions, and tags. Returns compact signatures by default; pass <code class="language-plaintext highlighter-rouge">detail="detailed"</code> for descriptions or <code class="language-plaintext highlighter-rouge">detail="full"</code> for complete schemas.</li>
  <li><strong><code class="language-plaintext highlighter-rouge">get_schema</code></strong>: fetch the full parameter schema for a specific tool by name, skipping the search when the LLM already knows what it wants.</li>
  <li><strong><code class="language-plaintext highlighter-rouge">call</code></strong>: invoke any tool by name (renamed from <code class="language-plaintext highlighter-rouge">call_tool</code>), including hidden tools not in the client’s tool list.</li>
</ul>

<p>~25 tools are now pinned (up from ~20), and the total count is down to ~125 after 2.1’s resource consolidation. The remaining ~100 specialized tools are discoverable through <code class="language-plaintext highlighter-rouge">search_tools</code> and callable through <code class="language-plaintext highlighter-rouge">call</code>, <code class="language-plaintext highlighter-rouge">batch</code>, or <code class="language-plaintext highlighter-rouge">execute</code>.</p>

<p>Together, the five meta-tools form a hierarchy:</p>

<table>
  <thead>
    <tr>
      <th>Need</th>
      <th>Meta-tool</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Find a tool</td>
      <td><code class="language-plaintext highlighter-rouge">search_tools</code></td>
    </tr>
    <tr>
      <td>Check its parameters</td>
      <td><code class="language-plaintext highlighter-rouge">get_schema</code></td>
    </tr>
    <tr>
      <td>Call it once</td>
      <td><code class="language-plaintext highlighter-rouge">call</code> (or directly, if pinned)</td>
    </tr>
    <tr>
      <td>Call many tools independently</td>
      <td><code class="language-plaintext highlighter-rouge">batch</code></td>
    </tr>
    <tr>
      <td>Chain tool outputs with logic</td>
      <td><code class="language-plaintext highlighter-rouge">execute</code></td>
    </tr>
  </tbody>
</table>

<p>The LLM picks the right level without prompting. A quick rename uses a pinned tool directly. A bulk annotation uses <code class="language-plaintext highlighter-rouge">batch</code>. A multi-step investigation uses <code class="language-plaintext highlighter-rouge">execute</code>. When it needs something specialized (applying a calling convention, editing register variables), it searches, checks the schema, and calls through <code class="language-plaintext highlighter-rouge">call</code>.</p>

<h2 id="daemon-mode">Daemon mode</h2>

<p>The meta-tools only pay off if the server stays alive long enough to use them. In 2.1, ida-mcp ran as a stdio subprocess of the MCP client. When the client disconnected (closing an editor, cycling a session, restarting after a crash), the server process died and took all worker state with it. Every open database, every completed auto-analysis pass, every renamed function: gone. For quick, single-session analysis, this was acceptable. But reverse engineering work rarely fits in a single session. You open a binary, let auto-analysis run, rename a few hundred functions, apply types, and then come back the next day to continue. Or the session cycles for an unrelated reason and you lose everything.</p>

<p>The problem was worse in Claude Code, where subagents share a single MCP session. A subagent halfway through analyzing a firmware image (hundreds of functions renamed, types applied) loses everything when the session cycles. It reconnects, but has to reopen, re-analyze, and reconstruct its progress from whatever survived context compaction.</p>

<p>In 2.2, the server runs as a persistent HTTP daemon behind a lightweight stdio proxy:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>LLM Client  &lt;──stdio──&gt;  Proxy  &lt;──HTTP──&gt;  Daemon
                                             Workers + Databases
</code></pre></div></div>

<p>The first time an MCP client connects, the proxy spawns a daemon process and detaches it. Subsequent connections (including reconnections after a session cycle, from a different editor, or from a completely new conversation) reuse the running daemon. Workers and their databases persist across disconnects: renamed symbols, added comments, applied types all survive.</p>

<p>The daemon also supports collaboration across clients. If a human analyst has been annotating a binary through one MCP session, a second session connecting to the same daemon sees all those annotations immediately. The daemon doesn’t care who made the changes; it just maintains the databases.</p>

<p>The daemon listens on <code class="language-plaintext highlighter-rouge">127.0.0.1</code> with a per-instance 256-bit bearer token. The state file is written with <code class="language-plaintext highlighter-rouge">0600</code> permissions so only the spawning user can read the token. To stop the daemon:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ida-mcp stop
</code></pre></div></div>

<p>This is the default transport now. Existing MCP client configurations (<code class="language-plaintext highlighter-rouge">ida-mcp</code> as the command) work without changes. The proxy handles daemon lifecycle transparently.</p>

<h2 id="raw-binary-and-firmware-support">Raw binary and firmware support</h2>

<p>ida-mcp could already open ELF, PE, and Mach-O files, where IDA auto-detects the architecture and load address from file headers. But firmware analysis (bootloaders, ROM dumps, flash extractions) starts with a blob of bytes and no metadata. Previously, you had to preprocess the binary in IDA’s GUI or write a loader script before ida-mcp could work with it. In 2.2, <code class="language-plaintext highlighter-rouge">open_database</code> accepts three new parameters that give the LLM what it needs to bootstrap analysis on raw binaries:</p>

<ul>
  <li><strong><code class="language-plaintext highlighter-rouge">processor</code></strong>: the IDA processor module with an optional variant (e.g., <code class="language-plaintext highlighter-rouge">arm:ARMv7-M</code> for Cortex-M firmware, <code class="language-plaintext highlighter-rouge">metapc:80386p</code> for 32-bit x86, <code class="language-plaintext highlighter-rouge">mips:mipsl</code> for little-endian MIPS)</li>
  <li><strong><code class="language-plaintext highlighter-rouge">loader</code></strong>: explicit loader selection (e.g., <code class="language-plaintext highlighter-rouge">"Binary file"</code> for raw blobs)</li>
  <li><strong><code class="language-plaintext highlighter-rouge">base_address</code></strong>: the load address in hex or decimal (e.g., <code class="language-plaintext highlighter-rouge">"0x08000000"</code> for a typical STM32 flash base)</li>
</ul>

<p>For structured formats, these parameters are optional. IDA figures them out from the file headers. For raw binaries, the LLM needs to provide them. If the user says “analyze this Cortex-M firmware dump loaded at 0x08000000,” those three parameters map directly:</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">"file_path"</span><span class="p">:</span><span class="w"> </span><span class="s2">"/path/to/firmware.bin"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"processor"</span><span class="p">:</span><span class="w"> </span><span class="s2">"arm:ARMv7-M"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"loader"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Binary file"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"base_address"</span><span class="p">:</span><span class="w"> </span><span class="s2">"0x08000000"</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p>The server validates processor names and catches a subtle headless-mode pitfall: processor names like <code class="language-plaintext highlighter-rouge">arm</code>, <code class="language-plaintext highlighter-rouge">metapc</code>, and <code class="language-plaintext highlighter-rouge">mips</code> are ambiguous. In IDA’s GUI, selecting one of these pops up a dialog asking which variant you mean: ARM or AArch64? 32-bit or 64-bit x86? But headless <code class="language-plaintext highlighter-rouge">idalib</code> never shows that dialog. It silently picks a default, and the default is often wrong. A Cortex-M firmware blob opened with bare <code class="language-plaintext highlighter-rouge">arm</code> ends up disassembled as AArch64, producing nonsense.</p>

<p>The server rejects these bare names on raw binaries and returns the available variants with descriptions:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>"arm" is ambiguous for raw binaries. It defaults to AArch64 in headless mode.
Use a specific variant:
  arm:ARMv7-M    Cortex-M (32-bit Thumb-2)
  arm:ARMv7-A    32-bit A-profile
  arm:AArch64    64-bit (explicit)
</code></pre></div></div>

<p>The LLM can also call <code class="language-plaintext highlighter-rouge">list_targets</code> to enumerate all available processors and loaders, so it can match an unknown binary to the right target without guessing.</p>

<h2 id="fat-mach-o-support">Fat Mach-O support</h2>

<p>macOS universal binaries pack multiple architecture slices into a single file. In 2.1, opening one would silently pick whichever slice IDA defaulted to, usually arm64, even when the target was x86_64. Nothing indicated the wrong slice had been selected until the disassembly didn’t make sense.</p>

<p>In 2.2, the server parses the fat header, identifies the available slices, and requires the caller to choose explicitly:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>AmbiguousFatBinary: universal binary contains multiple architectures.
Available slices: arm64, arm64e, x86_64
Pass fat_arch="arm64" to select a slice.
</code></pre></div></div>

<p>Each slice gets its own <code class="language-plaintext highlighter-rouge">.i64</code> sidecar (<code class="language-plaintext highlighter-rouge">binary.arm64.i64</code>, <code class="language-plaintext highlighter-rouge">binary.x86_64.i64</code>), so multiple architectures can be opened simultaneously in separate workers. Combined with <code class="language-plaintext highlighter-rouge">execute</code>’s cross-database support, the LLM can decompile the same function in both the arm64 and x86_64 slices and diff the pseudocode. This helps when finding platform-specific behavior, verifying that a vulnerability affects all architectures, or understanding how the compiler optimized differently for each target.</p>

<p>The fat header parser also handles an edge case that has bitten other tools: Java <code class="language-plaintext highlighter-rouge">.class</code> files share the same magic bytes (<code class="language-plaintext highlighter-rouge">0xCAFEBABE</code>) as Mach-O fat binaries. The parser validates slice counts and CPU types to distinguish the two, so a directory full of Java classes won’t trigger false fat-binary detection.</p>

<h2 id="tuning-for-your-model-and-client">Tuning for your model and client</h2>

<p>Not every model writes good Python, and not every MCP client needs server-side tool discovery. The meta-tools are designed to be independently useful, so you can enable the ones that match your setup and disable the ones that don’t.</p>

<p>Three environment variables control which meta-tools are available:</p>

<ul>
  <li>
    <p><strong><code class="language-plaintext highlighter-rouge">IDA_MCP_DISABLE_EXECUTE</code></strong>: hides the <code class="language-plaintext highlighter-rouge">execute</code> meta-tool. Smaller models or those without strong code generation can produce unreliable Python in <code class="language-plaintext highlighter-rouge">execute</code> blocks: wrong parameter names, broken control flow, off-by-one iteration. For these models, discrete tool calls are more reliable: each call is independently validated, and errors are clear and localized. Disabling <code class="language-plaintext highlighter-rouge">execute</code> keeps <code class="language-plaintext highlighter-rouge">batch</code> for bulk operations and <code class="language-plaintext highlighter-rouge">call</code> for hidden tools.</p>
  </li>
  <li>
    <p><strong><code class="language-plaintext highlighter-rouge">IDA_MCP_DISABLE_BATCH</code></strong>: hides the <code class="language-plaintext highlighter-rouge">batch</code> meta-tool. Useful if your workflow routes all multi-step work through <code class="language-plaintext highlighter-rouge">execute</code> anyway, since having both visible can lead the LLM to pick the wrong one.</p>
  </li>
  <li>
    <p><strong><code class="language-plaintext highlighter-rouge">IDA_MCP_DISABLE_TOOL_SEARCH</code></strong>: disables server-side progressive disclosure entirely. All ~125 tools become directly visible in the client’s tool list, and <code class="language-plaintext highlighter-rouge">search_tools</code> and <code class="language-plaintext highlighter-rouge">get_schema</code> are removed. This is the right setting for clients like Claude Code that already implement their own tool deferral. Claude Code defers tool schemas and loads them on demand. If ida-mcp is <em>also</em> hiding tools behind <code class="language-plaintext highlighter-rouge">search_tools</code>, the LLM has to go through two layers of discovery to reach a specialized tool. Disabling the server-side layer removes the redundancy.</p>
  </li>
</ul>

<p>These are environment variables on the server process, so they apply to all sessions against that daemon. Set them in your MCP client configuration:</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">"command"</span><span class="p">:</span><span class="w"> </span><span class="s2">"ida-mcp"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"env"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nl">"IDA_MCP_DISABLE_TOOL_SEARCH"</span><span class="p">:</span><span class="w"> </span><span class="s2">"1"</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>As a starting point: if you’re using Claude (Opus or Sonnet) through Claude Code, disable tool search. If you’re using a smaller model or a client without native tool deferral, leave everything enabled and let server-side progressive disclosure handle it.</p>

<h2 id="other-improvements">Other improvements</h2>

<ul>
  <li><strong>Per-run log files</strong>: Each server run writes to its own timestamped log file, and <code class="language-plaintext highlighter-rouge">open_database</code> warnings (e.g., loader compatibility issues) are surfaced to the client instead of silently swallowed. When something goes wrong, you can find the relevant log without scrolling through a monolithic file.</li>
  <li><strong>Heartbeat progress reporting</strong>: <code class="language-plaintext highlighter-rouge">save_database</code> and <code class="language-plaintext highlighter-rouge">execute</code> blocks send progress notifications every 5 seconds to prevent client timeouts on large databases. Saving a database with millions of functions and extensive annotations can take minutes; without heartbeats, the MCP client would assume the server had hung and disconnect.</li>
  <li><strong>Database reopen fix</strong>: Reopening an existing <code class="language-plaintext highlighter-rouge">.i64</code> no longer passes stale loader options that caused <code class="language-plaintext highlighter-rouge">idalib</code> to <code class="language-plaintext highlighter-rouge">exit(1)</code> on format mismatch. This was annoying because the failure mode was a silent exit with no error message: the worker just disappeared.</li>
</ul>

<h2 id="upgrading">Upgrading</h2>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>uv tool <span class="nb">install</span> <span class="nt">--upgrade</span> ida-mcp
</code></pre></div></div>

<p>Or with pip:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>pip <span class="nb">install</span> <span class="nt">--upgrade</span> ida-mcp
</code></pre></div></div>

<p>The MCP interface is backward compatible. Existing client configurations work without changes. The daemon spawns automatically on first connection.</p>

<h2 id="links">Links</h2>

<ul>
  <li><strong>Repository</strong>: <a href="https://github.com/jtsylve/ida-mcp">github.com/jtsylve/ida-mcp</a></li>
  <li><strong>PyPI</strong>: <a href="https://pypi.org/project/ida-mcp/">pypi.org/project/ida-mcp</a></li>
  <li><strong>Previous post</strong>: <a href="/post/2026/04/07/ida-mcp-2.1">ida-mcp 2.1: Progressive Tool Discovery, Background Analysis, and Batch Operations</a></li>
</ul>

<p>If you run into issues or have feature requests, please <a href="https://github.com/jtsylve/ida-mcp/issues">open an issue</a> on GitHub.</p>

<hr />

<p><em>IDA Pro and Hex-Rays are trademarks of Hex-Rays SA. ida-mcp is an independent project and is not affiliated with or endorsed by Hex-Rays.</em></p>]]></content><author><name></name></author><category term="reverse-engineering" /><category term="tools" /><category term="ida-pro" /><category term="mcp" /><category term="llm" /><category term="ai" /><category term="idalib" /><category term="reverse-engineering" /><summary type="html"><![CDATA[ida-mcp 2.2.0 is out. This release removes the friction between what the LLM wants to do and what MCP lets it express in a single round trip.]]></summary></entry><entry><title type="html">ida-mcp 2.1: Progressive Tool Discovery, Background Analysis, and Batch Operations</title><link href="https://jtsylve.blog/post/2026/04/07/ida-mcp-2.1" rel="alternate" type="text/html" title="ida-mcp 2.1: Progressive Tool Discovery, Background Analysis, and Batch Operations" /><published>2026-04-07T00:00:00+00:00</published><updated>2026-04-07T00:00:00+00:00</updated><id>https://jtsylve.blog/post/2026/04/07/ida-mcp-2.1</id><content type="html" xml:base="https://jtsylve.blog/post/2026/04/07/ida-mcp-2.1"><![CDATA[<p><a href="https://github.com/jtsylve/ida-mcp">ida-mcp 2.1.0</a> is out. This release focuses on making the LLM a more efficient analyst: fewer wasted tool calls, less context window consumed by tool schemas, and better behavior when multiple subagents are working on the same set of binaries. The changes are individually small, but together they add up.</p>

<h2 id="what-changed">What changed</h2>

<h3 id="progressive-tool-discovery">Progressive tool discovery</h3>

<p>In 2.0, all ~195 tools were registered with the MCP client at startup. Every tool’s full schema (name, description, parameters, output type) was injected into the LLM’s context window before it had even opened a binary. That’s tokens spent describing tools the LLM may never use.</p>

<p>In 2.1, only ~20 core tools are registered directly: the database lifecycle tools, <code class="language-plaintext highlighter-rouge">decompile_function</code>, <code class="language-plaintext highlighter-rouge">list_functions</code>, <code class="language-plaintext highlighter-rouge">get_strings</code>, <code class="language-plaintext highlighter-rouge">get_xrefs_to</code>, <code class="language-plaintext highlighter-rouge">list_names</code>, and a handful of others that cover the most common analysis workflows. Everything else is discoverable through two meta-tools:</p>

<ul>
  <li><strong><code class="language-plaintext highlighter-rouge">search_tools</code></strong> — takes a keyword regex (e.g., <code class="language-plaintext highlighter-rouge">patch|assemble</code>, <code class="language-plaintext highlighter-rouge">snapshot</code>, <code class="language-plaintext highlighter-rouge">operand</code>) and returns matching tool names with descriptions</li>
  <li><strong><code class="language-plaintext highlighter-rouge">call_tool</code></strong> — invokes any tool by name, even if it wasn’t in the initial registration</li>
</ul>

<p>When the LLM needs something specialized — manipulating register variables, generating FLIRT signatures — it searches for the right tool and calls it through <code class="language-plaintext highlighter-rouge">call_tool</code>. The full schema for that tool is fetched on demand rather than sitting in context from the start.</p>

<h3 id="background-auto-analysis">Background auto-analysis</h3>

<p>Opening a binary in IDA triggers auto-analysis: the passes that identify functions, resolve cross-references, recognize library code, and build the initial database. For large binaries, this can take minutes. In 2.0, <code class="language-plaintext highlighter-rouge">open_database</code> blocked the calling agent until analysis completed, but a second subagent could attach to an already-open database and start querying before auto-analysis had finished.</p>

<p>In 2.1, <code class="language-plaintext highlighter-rouge">open_database</code> returns immediately. Analysis runs as a background task while the LLM moves on. The caller uses <code class="language-plaintext highlighter-rouge">wait_for_analysis</code> to block until analysis has fully completed for a specific database. When multiple databases are being opened in parallel, <code class="language-plaintext highlighter-rouge">wait_for_analysis</code> accepts a list and returns as soon as <em>any</em> of them finish, so the LLM can start on whichever is ready first.</p>

<p>This lets the LLM issue multiple <code class="language-plaintext highlighter-rouge">open_database</code> calls before blocking on any of them. While analysis is running, tool calls to that database return an error rather than silently operating on incomplete data. The state machine is explicit: open, wait, query.</p>

<h3 id="batch-operations">Batch operations</h3>

<p>Several tools now accept batched inputs to reduce round-trip overhead:</p>

<ul>
  <li><strong><code class="language-plaintext highlighter-rouge">decompile_function</code></strong> — up to 50 addresses in a single call</li>
  <li><strong><code class="language-plaintext highlighter-rouge">get_xrefs_to</code></strong> — up to 50 addresses with direction control</li>
  <li><strong><code class="language-plaintext highlighter-rouge">get_strings</code></strong> — up to 10 filter patterns</li>
</ul>

<p>Each tool call is a full MCP round trip, so batching 20 decompile calls into one eliminates 19 round trips.</p>

<h3 id="find_code_by_string"><code class="language-plaintext highlighter-rouge">find_code_by_string</code></h3>

<p>This new composite tool combines what used to be a multi-step workflow: search for a string literal, find cross-references to that string’s address, and resolve those references to their containing functions — all in one call. Previously the LLM had to chain <code class="language-plaintext highlighter-rouge">get_strings</code> → <code class="language-plaintext highlighter-rouge">get_xrefs_to</code> → <code class="language-plaintext highlighter-rouge">get_function</code> manually, requiring three tool calls and holding intermediate results in context. <code class="language-plaintext highlighter-rouge">find_code_by_string</code> does the full pipeline server-side.</p>

<h3 id="session-scoped-database-ownership">Session-scoped database ownership</h3>

<p>When multiple subagents share the same ida-mcp server, 2.0 had no concept of which agent “owned” which database. Any agent could close any database, potentially pulling the rug out from under a sibling.</p>

<p>In 2.1, workers track which MCP sessions (agents) are attached. <code class="language-plaintext highlighter-rouge">close_database</code> detaches the calling session and only terminates the worker when no sessions remain. <code class="language-plaintext highlighter-rouge">list_databases</code> now reports session counts and whether the calling agent is attached to each database. Subagents can now work concurrently on shared databases without interfering with each other.</p>

<h3 id="other-improvements">Other improvements</h3>

<ul>
  <li><strong>Main-thread dispatch</strong> — All IDA API calls are now routed through a <code class="language-plaintext highlighter-rouge">MainThreadExecutor</code> that enforces idalib’s thread affinity requirements. The MCP event loop runs on a background thread, avoiding the deadlock scenarios that could occur in 2.0 when analysis callbacks and tool calls competed for the main thread.</li>
  <li><strong>Structured Pydantic output</strong> — Every tool returns a typed Pydantic model with an <code class="language-plaintext highlighter-rouge">output_schema</code>, so MCP clients with structured output support can parse responses programmatically instead of parsing text.</li>
  <li><strong>MCP annotations</strong> — Tools declare <code class="language-plaintext highlighter-rouge">readOnlyHint</code>, <code class="language-plaintext highlighter-rouge">destructiveHint</code>, and <code class="language-plaintext highlighter-rouge">idempotentHint</code>, letting clients distinguish safe reads from mutations and prompt for confirmation on destructive operations.</li>
  <li><strong>Address resolution fix</strong> — <code class="language-plaintext highlighter-rouge">parse_address()</code> now resolves symbol names before falling back to bare hex interpretation. In 2.0, a function named <code class="language-plaintext highlighter-rouge">add</code> or <code class="language-plaintext highlighter-rouge">dead</code> would be interpreted as the hex value <code class="language-plaintext highlighter-rouge">0xadd</code> or <code class="language-plaintext highlighter-rouge">0xdead</code> instead of as a symbol lookup. This was subtle and maddening.</li>
  <li><strong>Resources consolidated into tools</strong> — Many resources that duplicated tool functionality (segments, types, structs, enums, per-entity lookups) were removed. The remaining resources cover genuinely static data: imports, exports, entry points, and aggregate statistics.</li>
  <li><strong>Progress reporting</strong> — Long-running operations report progress through MCP’s native progress notification mechanism.</li>
</ul>

<h2 id="comparison-20-vs-21-on-a-real-target">Comparison: 2.0 vs 2.1 on a real target</h2>

<p>I ran the same analysis task against both versions on a large, stripped macOS application bundle (multiple binaries, 200MB+ of ARM64 code). Same prompt, same binaries, same hardware, Claude Opus orchestrating parallel subagents.</p>

<p>The most visible difference was function discovery. The 2.0 run found roughly 4,000 functions in the main binary and 3,600 in the main framework, limited to the Objective-C methods with surviving selector names. The 2.1 run found <strong>1.26 million</strong> and <strong>570,000</strong> respectively. Same binaries.</p>

<p>In the 2.0 run, subagents started querying before auto-analysis had finished discovering functions in the stripped code. In 2.1, <code class="language-plaintext highlighter-rouge">wait_for_analysis</code> ensured analysis was complete before any queries ran. Those additional <code class="language-plaintext highlighter-rouge">sub_*</code> routines are where the actual implementation lives; without them, the LLM only sees the Objective-C dispatch layer and misses the C/C++ engine underneath.</p>

<p>The end results reflected the difference in visibility. The 2.0 run produced output derived mostly from string literals and Objective-C class names: what the code <em>talks about</em> but not what it <em>does</em>. The 2.1 run produced output derived from actual decompiled logic: specific function addresses, byte-level protocol details, and enum values extracted from the code itself.</p>

<p>On the efficiency side, the two runs made a comparable number of tool calls (2.0: 234, 2.1: 257), but 2.1’s batching and <code class="language-plaintext highlighter-rouge">find_code_by_string</code> meant each call covered more ground. The bigger gain was context window usage: 195 tool schemas registered upfront in 2.0 vs 20 in 2.1, with the rest discovered on demand. That frees up context for actual analysis.</p>

<h2 id="upgrading">Upgrading</h2>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>uv tool <span class="nb">install</span> <span class="nt">--upgrade</span> ida-mcp
</code></pre></div></div>

<p>Or with pip:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>pip <span class="nb">install</span> <span class="nt">--upgrade</span> ida-mcp
</code></pre></div></div>

<p>The MCP interface is backward compatible. Existing client configurations work without changes.</p>

<h2 id="links">Links</h2>

<ul>
  <li><strong>Repository</strong>: <a href="https://github.com/jtsylve/ida-mcp">github.com/jtsylve/ida-mcp</a></li>
  <li><strong>PyPI</strong>: <a href="https://pypi.org/project/ida-mcp/">pypi.org/project/ida-mcp</a></li>
  <li><strong>Previous post</strong>: <a href="/post/2026/03/25/Announcing-ida-mcp-2">Announcing ida-mcp 2.0</a></li>
</ul>

<p>If you run into issues or have feature requests, please <a href="https://github.com/jtsylve/ida-mcp/issues">open an issue</a> on GitHub.</p>

<hr />

<p><em>IDA Pro and Hex-Rays are trademarks of Hex-Rays SA. ida-mcp is an independent project and is not affiliated with or endorsed by Hex-Rays.</em></p>]]></content><author><name></name></author><category term="reverse-engineering" /><category term="tools" /><category term="ida-pro" /><category term="mcp" /><category term="llm" /><category term="ai" /><category term="idalib" /><category term="reverse-engineering" /><summary type="html"><![CDATA[ida-mcp 2.1.0 is out. This release focuses on making the LLM a more efficient analyst: fewer wasted tool calls, less context window consumed by tool schemas, and better behavior when multiple subagents are working on the same set of binaries. The changes are individually small, but together they add up.]]></summary></entry><entry><title type="html">Announcing ida-mcp 2.0: A Headless MCP Server for IDA Pro</title><link href="https://jtsylve.blog/post/2026/03/25/Announcing-ida-mcp-2" rel="alternate" type="text/html" title="Announcing ida-mcp 2.0: A Headless MCP Server for IDA Pro" /><published>2026-03-25T00:00:00+00:00</published><updated>2026-03-25T00:00:00+00:00</updated><id>https://jtsylve.blog/post/2026/03/25/Announcing%20ida-mcp%202</id><content type="html" xml:base="https://jtsylve.blog/post/2026/03/25/Announcing-ida-mcp-2"><![CDATA[<p>The <a href="https://modelcontextprotocol.io/">Model Context Protocol</a> (MCP) lets LLMs call external tools, and for reverse engineers the obvious application is connecting an LLM to IDA Pro — navigating binaries, reading disassembly, decompiling functions, and annotating databases. Several MCP servers for IDA already exist. Today I’m releasing <a href="https://github.com/jtsylve/ida-mcp">ida-mcp 2.0</a>, a headless server with ~190 tools, 36 resources, 8 prompts, and support for analyzing multiple binaries simultaneously.</p>

<h2 id="tool-coverage">Tool coverage</h2>

<p>ida-mcp is built on <a href="https://docs.hex-rays.com/release-notes/9_0#ida-as-a-library-idalib">idalib</a> and exposes ~190 tools covering:</p>

<ul>
  <li><strong>Analysis &amp; navigation</strong> — open binaries, list/query functions, decode instructions, walk basic blocks and CFG edges, follow cross-references, build call graphs</li>
  <li><strong>Decompilation</strong> — Hex-Rays pseudocode, microcode at any maturity level, ctree AST traversal and pattern matching, variable renaming and retyping</li>
  <li><strong>Type system</strong> — local type libraries, structure and enum creation/editing, C declaration parsing, type application at addresses</li>
  <li><strong>Annotation</strong> — comments (including appending with deduplication), names, bookmarks, colors, register variables, hidden ranges</li>
  <li><strong>Modification</strong> — patching bytes, combined assemble-and-patch, creating/deleting functions, data type definitions, operand display formatting</li>
  <li><strong>Batch operations</strong> — export all pseudocode or disassembly, generate output files (ASM, LST, MAP), rebuild executables from databases</li>
  <li><strong>Signatures</strong> — FLIRT signature application and generation, type library loading, IDS module loading</li>
  <li><strong>Advanced</strong> — segment register tracking, switch table analysis, fixups, exception handlers, undo/redo, snapshots, directory tree management</li>
</ul>

<p>Every tool accepts addresses in hex (<code class="language-plaintext highlighter-rouge">0x401000</code>), decimal, or as a symbol name, and list operations use <code class="language-plaintext highlighter-rouge">offset</code>/<code class="language-plaintext highlighter-rouge">limit</code> pagination.</p>

<p>All mutation tools return <code class="language-plaintext highlighter-rouge">old_*</code> fields showing the previous state — <code class="language-plaintext highlighter-rouge">old_comment</code>, <code class="language-plaintext highlighter-rouge">old_name</code>, <code class="language-plaintext highlighter-rouge">old_color</code>, <code class="language-plaintext highlighter-rouge">old_bytes</code>, etc. — so the LLM can see what changed without a separate read-back call.</p>

<p>For anything the built-in tools don’t cover, <code class="language-plaintext highlighter-rouge">run_script</code> allows arbitrary IDAPython execution (enabled by the <code class="language-plaintext highlighter-rouge">IDA_MCP_ALLOW_SCRIPTS</code> environment variable).</p>

<h2 id="whats-new-in-20">What’s new in 2.0</h2>

<h3 id="resources">Resources</h3>

<p>MCP defines three primitives: tools (actions), resources (read-only context), and prompts (guided workflows). Most IDA MCP servers only implement tools; ida-mcp 2.0 implements all three.</p>

<p>Resources are read-only endpoints that provide context without consuming a tool call. ida-mcp exposes 36 of them via <code class="language-plaintext highlighter-rouge">ida://</code> URIs, organized into four tiers:</p>

<p><strong>Core context</strong> — database metadata, file paths, processor info, segments, entry points, imports, exports, and a statistics summary. These give the LLM orientation when it first opens a binary.</p>

<p><strong>Structural reference</strong> — the local type catalog, individual type definitions, structure layouts with member offsets, enum definitions, and applied FLIRT/TIL signatures. These let the LLM inspect the type system without calling tools.</p>

<p><strong>Browsable collections</strong> — functions, strings, named locations, and bookmarks. Enough for the LLM to get a high-level picture of the binary.</p>

<p>Most collection resources also expose a <code class="language-plaintext highlighter-rouge">search/{pattern}</code> variant for filtering by name or address, so the LLM can narrow results without paging through large lists.</p>

<p><strong>Per-entity lookups</strong> — function metadata, stack frames, exception handlers, decompiled variables, and cross-references by address. These are parameterized URIs like <code class="language-plaintext highlighter-rouge">ida://functions/{addr}</code> and <code class="language-plaintext highlighter-rouge">ida://xrefs/to/{addr}</code>.</p>

<p>In multi-database mode, the supervisor proxies resource reads to the appropriate worker and exposes its own <code class="language-plaintext highlighter-rouge">ida://databases</code> resource listing all open databases with worker status.</p>

<h3 id="prompts">Prompts</h3>

<p>ida-mcp includes 8 prompts — structured analysis templates that guide the LLM through multi-step workflows:</p>

<p><strong>Analysis:</strong></p>
<ul>
  <li><code class="language-plaintext highlighter-rouge">survey_binary</code> — binary triage: identify the file type, architecture, key functions, strings of interest, and imports. Accepts an optional focus parameter to narrow the survey.</li>
  <li><code class="language-plaintext highlighter-rouge">analyze_function</code> — single-function deep dive with data flow analysis and security notes.</li>
  <li><code class="language-plaintext highlighter-rouge">diff_before_after</code> — preview how a rename or retype will affect the decompiler output before committing.</li>
  <li><code class="language-plaintext highlighter-rouge">classify_functions</code> — group functions by behavioral pattern (crypto, networking, string manipulation, etc.) to prioritize analysis effort.</li>
</ul>

<p><strong>Security:</strong></p>
<ul>
  <li><code class="language-plaintext highlighter-rouge">find_crypto_constants</code> — scan for known constants from AES, SHA-256, SHA-1, MD5, CRC-32, ChaCha20, RSA, and Blowfish.</li>
</ul>

<p><strong>Workflow:</strong></p>
<ul>
  <li><code class="language-plaintext highlighter-rouge">auto_rename_strings</code> — suggest function renames based on unique string references, without applying any changes.</li>
  <li><code class="language-plaintext highlighter-rouge">apply_abi</code> — apply type information for a known ABI (Linux syscalls, libc, Windows API, POSIX).</li>
  <li><code class="language-plaintext highlighter-rouge">export_idc_script</code> — generate a reproducible IDAPython script capturing all annotations made during the session.</li>
</ul>

<h3 id="multi-database-support">Multi-database support</h3>

<p>Reverse engineering rarely involves a single binary. You might need to cross-reference a DLL against its loader, compare two firmware versions, or analyze a malware dropper alongside its payload. With ida-mcp 1.x, you had to close one database before opening another. With ida-mcp 2.0, you can keep them all open at once.</p>

<p>ida-mcp runs a <strong>supervisor process</strong> that spawns <strong>worker subprocesses</strong> on demand. Each worker loads idalib independently and manages a single database. The supervisor proxies MCP tool calls to the appropriate worker based on a <code class="language-plaintext highlighter-rouge">database</code> parameter it injects into every tool’s schema.</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>MCP Client  ←—stdio—→  Supervisor (ProxyMCP)
                              │
                              ├——stdio——→  Worker 1  (binary_a.exe)
                              ├——stdio——→  Worker 2  (library.dll)
                              └——stdio——→  Worker 3  (firmware.bin)
</code></pre></div></div>

<p>This is a direct consequence of idalib’s threading model: all IDA API calls must happen on the thread that imported the <code class="language-plaintext highlighter-rouge">idapro</code> module, and global state is shared per-process. Rather than fighting that, each database gets its own process with complete isolation.</p>

<p>This means the LLM never pays a context-switch penalty. In a serial setup, switching from one binary to another means closing the current database and reopening the next one — a swap that flushes all in-memory state and can take seconds depending on database size. With per-database workers, the LLM just passes a different <code class="language-plaintext highlighter-rouge">database</code> parameter and gets an immediate response. All databases stay warm.</p>

<p>This matters most when the LLM is using subagents. An orchestrating agent can spawn parallel subagents — one reversing a loader, another analyzing the payload it drops, a third inspecting a shared library — and they all run concurrently against their own workers without blocking each other. No subagent has to wait for another to release the database.</p>

<p>To use it, pass <code class="language-plaintext highlighter-rouge">keep_open=True</code> when opening a database:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># First binary — opens normally
</span><span class="nf">open_database</span><span class="p">(</span><span class="sh">"</span><span class="s">/path/to/binary_a.exe</span><span class="sh">"</span><span class="p">,</span> <span class="n">keep_open</span><span class="o">=</span><span class="bp">True</span><span class="p">)</span>

<span class="c1"># Second binary — previous database stays open
</span><span class="nf">open_database</span><span class="p">(</span><span class="sh">"</span><span class="s">/path/to/library.dll</span><span class="sh">"</span><span class="p">,</span> <span class="n">keep_open</span><span class="o">=</span><span class="bp">True</span><span class="p">)</span>

<span class="c1"># Tools target a specific database
</span><span class="nf">decompile_function</span><span class="p">(</span><span class="sh">"</span><span class="s">main</span><span class="sh">"</span><span class="p">,</span> <span class="n">database</span><span class="o">=</span><span class="sh">"</span><span class="s">binary_a.exe</span><span class="sh">"</span><span class="p">)</span>
<span class="nf">get_xrefs_to</span><span class="p">(</span><span class="sh">"</span><span class="s">ImportantExport</span><span class="sh">"</span><span class="p">,</span> <span class="n">database</span><span class="o">=</span><span class="sh">"</span><span class="s">library.dll</span><span class="sh">"</span><span class="p">)</span>
</code></pre></div></div>

<p>Idle workers are cleaned up after a configurable timeout (default 30 minutes, controlled by <code class="language-plaintext highlighter-rouge">IDA_MCP_IDLE_TIMEOUT</code>), and the maximum number of concurrent workers can be capped with <code class="language-plaintext highlighter-rouge">IDA_MCP_MAX_WORKERS</code>.</p>

<p>If you don’t need multi-database support, the <code class="language-plaintext highlighter-rouge">ida-mcp-worker</code> entry point provides the same single-database behavior as 1.x.</p>

<h2 id="existing-ida-mcp-servers">Existing IDA MCP servers</h2>

<p>ida-mcp is not the only IDA MCP server. The existing servers fall into two categories: <strong>plugin-based</strong> servers that run inside a GUI session, and <strong>headless</strong> servers that run standalone without a GUI.</p>

<p>The plugin-based approach is the most common. The most popular is <a href="https://github.com/mrexodia/ida-pro-mcp">ida-pro-mcp</a> by mrexodia (of x64dbg fame), which runs as an IDA plugin communicating over SSE or stdio and exposes a large tool set. Others in this category include <a href="https://github.com/MeroZemory/ida-multi-mcp">ida-multi-mcp</a> (multi-instance routing through a single MCP endpoint), <a href="https://github.com/jelasin/IDA-MCP">IDA-MCP</a> (a gateway architecture supporting multiple IDA instances), and <a href="https://github.com/symgraph/IDAssistMCP">IDAssistMCP</a>. Plugin-based servers require a running GUI session, which ties the server’s lifecycle to a visible IDA window.</p>

<p>On the headless side:</p>

<p><strong><a href="https://github.com/mrexodia/ida-pro-mcp">ida-pro-mcp</a></strong> includes <code class="language-plaintext highlighter-rouge">idalib-mcp</code>, a headless mode built on the same idalib foundation as ida-mcp. It exposes ~76 tools (96 with the debugger extension) plus 11 MCP resources, serving over HTTP/SSE. Requirements are IDA 8.3+ and Python 3.11+. The multi-database mode works by swapping the active database in a single process — only one is loaded at a time.</p>

<p><strong><a href="https://github.com/blacktop/ida-mcp-rs">ida-mcp-rs</a></strong> links directly against IDA’s native libraries from Rust. It has first-class support for Apple’s <code class="language-plaintext highlighter-rouge">dyld_shared_cache</code>, useful if you work with macOS/iOS binaries. The tool surface is smaller (~11 tools) and focused on core analysis operations.</p>

<p><strong><a href="https://github.com/cnitlrt/headless-ida-mcp-server">headless-ida-mcp-server</a></strong> uses IDA’s headless executable (<code class="language-plaintext highlighter-rouge">idat</code>) rather than idalib, which avoids the idalib dependency but routes through a separate process for each API call.</p>

<p>ida-mcp shares the idalib foundation with <code class="language-plaintext highlighter-rouge">idalib-mcp</code> but takes a different approach: stdio transport instead of HTTP/SSE, per-database subprocess isolation instead of serial database swapping, and automatic idalib discovery instead of requiring a pip install. ida-mcp requires IDA Pro 9+ and Python 3.12+; <code class="language-plaintext highlighter-rouge">idalib-mcp</code> supports IDA 8.3+ and Python 3.11+ and includes debugger tools that ida-mcp does not have yet.</p>

<h2 id="getting-started">Getting started</h2>

<p>ida-mcp requires IDA Pro 9+ with a valid license and Python 3.12+. A Hex-Rays decompiler license is needed for decompilation tools but is not required for the rest.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Install from PyPI</span>
uv tool <span class="nb">install </span>ida-mcp
</code></pre></div></div>

<p>IDA Pro is found automatically from standard installation paths, or you can set <code class="language-plaintext highlighter-rouge">IDADIR</code> to point to your installation.</p>

<p>Then configure your MCP client to launch the server. If you prefer not to install globally, <code class="language-plaintext highlighter-rouge">uvx</code> can fetch and run it on demand:</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">"mcpServers"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nl">"ida"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
      </span><span class="nl">"command"</span><span class="p">:</span><span class="w"> </span><span class="s2">"uvx"</span><span class="p">,</span><span class="w">
      </span><span class="nl">"args"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">"ida-mcp"</span><span class="p">]</span><span class="w">
    </span><span class="p">}</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>No plugin files to copy, no ports to configure, no GUI to keep running.</p>

<h2 id="links">Links</h2>

<ul>
  <li><strong>Repository</strong>: <a href="https://github.com/jtsylve/ida-mcp">github.com/jtsylve/ida-mcp</a></li>
  <li><strong>PyPI</strong>: <a href="https://pypi.org/project/ida-mcp/">pypi.org/project/ida-mcp</a></li>
  <li><strong>License</strong>: MIT</li>
</ul>

<p>If you run into issues or have feature requests, please <a href="https://github.com/jtsylve/ida-mcp/issues">open an issue</a> on GitHub.</p>

<hr />

<p><em>IDA Pro and Hex-Rays are trademarks of Hex-Rays SA. ida-mcp is an independent project and is not affiliated with or endorsed by Hex-Rays.</em></p>]]></content><author><name></name></author><category term="reverse-engineering" /><category term="tools" /><category term="ida-pro" /><category term="mcp" /><category term="llm" /><category term="ai" /><category term="idalib" /><category term="reverse-engineering" /><summary type="html"><![CDATA[The Model Context Protocol (MCP) lets LLMs call external tools, and for reverse engineers the obvious application is connecting an LLM to IDA Pro — navigating binaries, reading disassembly, decompiling functions, and annotating databases. Several MCP servers for IDA already exist. Today I’m releasing ida-mcp 2.0, a headless server with ~190 tools, 36 resources, 8 prompts, and support for analyzing multiple binaries simultaneously.]]></summary></entry><entry><title type="html">A Copy-Paste Bug That Broke PSpice® AES-256 Encryption</title><link href="https://jtsylve.blog/post/2026/03/18/PSpice-Encryption-Weakness" rel="alternate" type="text/html" title="A Copy-Paste Bug That Broke PSpice® AES-256 Encryption" /><published>2026-03-18T00:00:00+00:00</published><updated>2026-03-18T00:00:00+00:00</updated><id>https://jtsylve.blog/post/2026/03/18/PSpice-Encryption-Weakness</id><content type="html" xml:base="https://jtsylve.blog/post/2026/03/18/PSpice-Encryption-Weakness"><![CDATA[<p>PSpice is a SPICE circuit simulator from Cadence Design Systems that encrypts proprietary semiconductor model files to protect vendor IP and prevent reuse in third-party SPICE simulators.  The encryption scheme is proprietary and undocumented.</p>

<p>Many third-party component vendors distribute SPICE models exclusively as PSpice-encrypted files, locking them to a single simulator and preventing their use in open-source and alternative tools such as <a href="https://ngspice.sourceforge.io/">NGSpice</a>, <a href="https://xyce.sandia.gov/">Xyce</a>, and <a href="https://github.com/PySpice-org/PySpice">PySpice</a>.  As part of research into these encryption schemes, I’ve released <a href="https://github.com/jtsylve/spice-crypt/">SpiceCrypt</a> — a Python library and CLI tool that decrypts encrypted SPICE model files, restoring interoperability so engineers can use lawfully obtained models in any simulator.</p>

<p>PSpice supports six encryption modes (0–5).  Modes 0–3 and 5 derive all key material from constants hardcoded in the binary; once those constants are extracted, files in these modes can be decrypted directly.  Mode 4 is the only mode that incorporates user-supplied key material: vendors provide a key string via a CSV file referenced by the <code class="language-plaintext highlighter-rouge">CDN_PSPICE_ENCKEYS</code> environment variable.  This key is XOR’d with the hardcoded base keys during derivation, so decryption requires the same key file.  A bug in key derivation reduces the effective keyspace to 2^32, making the user key recoverable by brute force in seconds.</p>

<h3 id="the-bug">The Bug</h3>

<p>Mode 4 uses AES-256 in ECB mode.  Key derivation starts from two base strings:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">g_desKey</code>: a 4-byte “short” base key (<code class="language-plaintext highlighter-rouge">"8gM2"</code>)</li>
  <li><code class="language-plaintext highlighter-rouge">g_aesKey</code>: a 27-byte “extended” base key (<code class="language-plaintext highlighter-rouge">"H41Mlwqaspj1nxasyhq8530nh1r"</code>)</li>
</ul>

<p>When a user provides a key via the <code class="language-plaintext highlighter-rouge">CDN_PSPICE_ENCKEYS</code> CSV file, user key bytes 0–3 are XOR’d into the short base, and bytes 4–30 are XOR’d into the extended base.  A version suffix (e.g., <code class="language-plaintext highlighter-rouge">"1002"</code>) is then appended to each base key.</p>

<p><code class="language-plaintext highlighter-rouge">PSpiceAESEncoder_setKey</code> receives only the short key (<code class="language-plaintext highlighter-rouge">g_desKey</code>), not the extended key (<code class="language-plaintext highlighter-rouge">g_aesKey</code>).  The 32-byte AES-256 key is constructed by zero-padding this null-terminated string:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Byte  0–3:  XOR("8gM2", user_key[0:4])   -- unknown (4 bytes)
Byte  4–7:  "1002"                       -- version suffix (atoi(version_string) + 999)
Byte  8:    0x00 (null terminator)       -- known
Byte  9–31: 0x00 (zero padding)          -- known
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">EncryptionContext_init</code> calls <code class="language-plaintext highlighter-rouge">initEncryptionKeys</code> to derive both keys, then passes only <code class="language-plaintext highlighter-rouge">g_desKey</code> to the cipher engine via a vtable call:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>lea     rdx, g_desKey           ; short key loaded as setKey argument
...
call    qword ptr [rax]         ; vtable[0]: setKey(&amp;g_desKey)
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">PSpiceAESEncoder_setKey</code> copies this null-terminated string into a zero-filled 32-byte local buffer and calls <code class="language-plaintext highlighter-rouge">AES_keyExpansion(self+8, keyBuf, 256)</code>.  <code class="language-plaintext highlighter-rouge">g_desKey</code> in mode 4 is 8 characters (4 XOR’d bytes + <code class="language-plaintext highlighter-rouge">"1002"</code>) followed by a null terminator, so bytes 9–31 of the AES key are always zero.</p>

<p>Since 28 of 32 key bytes are known, the effective keyspace shrinks from 2^256 to 2^32.</p>

<p>In practice the keyspace is even smaller: since user keys are stored in a CSV file, each byte is almost certainly printable ASCII (<code class="language-plaintext highlighter-rouge">0x20</code>–<code class="language-plaintext highlighter-rouge">0x7E</code>), reducing the search space to roughly 95^4 (~81 million candidates).  SpiceCrypt does not exploit this observation — exhausting the full 2^32 space is fast enough that filtering by character class would add complexity without meaningful benefit.</p>

<h3 id="brute-force-attack">Brute-Force Attack</h3>

<p>The first encrypted block after every <code class="language-plaintext highlighter-rouge">$CDNENCSTART</code> marker is a metadata header whose plaintext always begins with the fixed prefix <code class="language-plaintext highlighter-rouge">"0001.0000 "</code> (10 ASCII bytes).  This prefix falls entirely within the first 16-byte AES sub-block, providing a known-plaintext crib for validating candidate keys.</p>

<p>The attack:</p>

<ol>
  <li>Take the first 16 bytes of the header ciphertext block.</li>
  <li>For each of the 2^32 candidate 4-byte values, construct the full 32-byte key (4 candidate bytes + known suffix + zeros) and decrypt the sub-block.</li>
  <li>If the first 10 bytes of the decrypted sub-block equal <code class="language-plaintext highlighter-rouge">"0001.0000 "</code>, the candidate is correct.</li>
</ol>

<p>Exhaustive search of all 2^32 candidates takes seconds with AES-NI, or under 1 second on a GPU.</p>

<p>SpiceCrypt implements this attack with a hardware-accelerated Rust extension (AES-NI / ARM Crypto Extensions) for key recovery:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Brute-force recover the user key (~seconds on modern hardware)</span>
spice-crypt <span class="nt">--recover-key</span> encrypted_file.lib

<span class="c"># Decrypt with a known user key</span>
spice-crypt <span class="nt">--user-key</span> KEY encrypted_file.lib
</code></pre></div></div>

<h3 id="full-user-key-recovery">Full User Key Recovery</h3>

<p>Once the 4-byte brute-force attack succeeds, the full user key is recoverable.  The metadata header’s plaintext contains the derived <code class="language-plaintext highlighter-rouge">g_aesKey</code>: the extended base XOR’d with user key bytes, with the version suffix appended.</p>

<ol>
  <li>
    <p><strong>Short user key</strong> (bytes 0–3): XOR the recovered 4 bytes with the known base <code class="language-plaintext highlighter-rouge">"8gM2"</code>.</p>
  </li>
  <li>
    <p><strong>Extended user key</strong> (bytes 4–30): Decrypt the metadata header with the recovered AES key.  The embedded <code class="language-plaintext highlighter-rouge">g_aesKey</code> equals <code class="language-plaintext highlighter-rouge">XOR("H41Mlwqaspj1nxasyhq8530nh1r", user_key[4:31]) + "1002"</code>.  Strip the version suffix and XOR with the known base to recover the remaining 27 user key bytes.</p>
  </li>
</ol>

<p>The entire user key string from the CSV file is now known, and all files encrypted with that key are compromised.</p>

<h3 id="root-cause">Root Cause</h3>

<p>The names <code class="language-plaintext highlighter-rouge">g_desKey</code> and <code class="language-plaintext highlighter-rouge">g_aesKey</code> are reverse-engineered labels, not original source names.  The key sizes suggest the extended key was intended for AES and the short key for DES.  The short key is 8 bytes after derivation, matching a DES key size.  The extended key is 31 bytes plus a null terminator to fill 32 bytes, which is likely an off-by-one error since AES-256 requires 32 bytes of key material.  Passing the short key to the AES engine appears to be a copy-paste error from the DES code path.  Had the extended key been used, the effective keyspace would be 2^216, making a brute-force attack infeasible.</p>

<p>AES-256 encryption support was introduced in PSpice 16.6 (April 2014), alongside the existing DES-based modes.  The bug has presumably been present since that release.  Fixing it now would break compatibility with every encrypted model created in the twelve years since its introduction.</p>

<h3 id="spicecrypt">SpiceCrypt</h3>

<p><a href="https://github.com/jtsylve/spice-crypt/">SpiceCrypt</a> is a tool I’ve released that handles decryption of all PSpice encryption modes, as well as LTspice encryption formats.  It can be installed from <a href="https://pypi.org/project/spice-crypt/">PyPI</a>:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>pip <span class="nb">install </span>spice-crypt
</code></pre></div></div>

<p>All encryption formats are auto-detected:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Decrypt any encrypted SPICE model file</span>
spice-crypt encrypted_file.lib

<span class="c"># Decrypt to an output file</span>
spice-crypt <span class="nt">-o</span> decrypted.lib encrypted_file.lib
</code></pre></div></div>

<p>SpiceCrypt also provides a Python API for programmatic use:</p>

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

<span class="n">plaintext</span><span class="p">,</span> <span class="n">verification</span> <span class="o">=</span> <span class="nf">decrypt_stream</span><span class="p">(</span><span class="sh">"</span><span class="s">encrypted.lib</span><span class="sh">"</span><span class="p">)</span>
</code></pre></div></div>

<p>Beyond PSpice, SpiceCrypt supports LTspice’s text-based DES format and Binary File format.  Full details on all supported formats, the Python API, and the legal basis for this interoperability work are available in the <a href="https://github.com/jtsylve/spice-crypt/">project README</a>.</p>

<p><strong>Disclaimer:</strong> SpiceCrypt is intended solely for enabling simulator interoperability with lawfully obtained models.  Using it to violate intellectual property rights is immoral and is not an acceptable use of the tool.</p>

<hr />
<p>PSpice is a trademark of Cadence Design Systems, Inc.</p>]]></content><author><name></name></author><category term="security-research" /><category term="encryption" /><category term="pspice" /><category term="aes" /><category term="brute-force" /><category term="reverse-engineering" /><category term="vulnerability" /><summary type="html"><![CDATA[PSpice is a SPICE circuit simulator from Cadence Design Systems that encrypts proprietary semiconductor model files to protect vendor IP and prevent reuse in third-party SPICE simulators. The encryption scheme is proprietary and undocumented.]]></summary></entry><entry><title type="html">2022 APFS Advent Challenge - Retrospective</title><link href="https://jtsylve.blog/post/2022/12/30/Challenge-Retrospective" rel="alternate" type="text/html" title="2022 APFS Advent Challenge - Retrospective" /><published>2022-12-30T00:00:00+00:00</published><updated>2022-12-30T00:00:00+00:00</updated><id>https://jtsylve.blog/post/2022/12/30/Challenge%20Retrospective</id><content type="html" xml:base="https://jtsylve.blog/post/2022/12/30/Challenge-Retrospective"><![CDATA[<p>As 2022 ends, so does my APFS Advent Challenge. Deciding at the last minute to write this series of blogs turned out to be even more challenging than expected. Life tends to find a way to complicate things, and December was no exception for me this year. I am glad I stuck with the challenge and hope that the information provided in the series was of some value to you.</p>

<h3 id="donations">Donations</h3>

<p>To help keep me honest and support a worthy cause, I pledged to donate $100 to the <a href="https://www.gofundme.com/f/ukraine-humanitarian-fund">Ukraine Humanitarian Fund</a> for each day I failed to write a post.</p>

<p>Early on, I decided to change the challenge’s parameters from posting every day until Christmas to posting every weekday in December. Because that changed the maximum number of posts from 24 to 22, I donated <a href="/images/advent2022/donate1.png">$200 on December 3rd</a>.</p>

<p>I donated an additional $100 per day on days <a href="/images/advent2022/donate2.png">10</a> and <a href="/images/advent2022/donate3.png">19</a> when my recently diagnosed carpal tunnel syndrome symptoms were especially bothersome.</p>

<p>Because I like round numbers, support the cause, and I’m not sure if today’s post counts, I have <a href="/images/advent2022/donate4.png">donated an additional $100</a>, bringing my total contribution to the fund to $500 for this challenge.</p>

<p>If it is within your means, please donate to help the Ukrainian people.  Regardless of your politics, the civilians that have lost everything due to this senseless conflict are blameless and deserving of our support.</p>

<h3 id="what-happens-next">What happens next?</h3>

<p>I decided to start this blog as part of my resolution to write more in 2023 and share my research. The advent challenge was a good way of kick-starting that effort. I plan on continuing to post, albeit at a much less demanding pace. If there are any topics about APFS or anything else in digital forensics that you are interested in learning more about, please feel free to reach out to me. I’ve decided to sunset my Twitter account, but you’ll find me active on Mastodon <a href="https://infosec.exchange/@jtsylve">@jtsylve@infosec.exchange</a>.</p>]]></content><author><name></name></author><category term="meta" /><category term="advent-2022" /><category term="retrospective" /><summary type="html"><![CDATA[As 2022 ends, so does my APFS Advent Challenge. Deciding at the last minute to write this series of blogs turned out to be even more challenging than expected. Life tends to find a way to complicate things, and December was no exception for me this year. I am glad I stuck with the challenge and hope that the information provided in the series was of some value to you.]]></summary></entry><entry><title type="html">Fusion Containers</title><link href="https://jtsylve.blog/post/2022/12/29/APFS-Fusion-Containers" rel="alternate" type="text/html" title="Fusion Containers" /><published>2022-12-29T00:00:00+00:00</published><updated>2022-12-29T00:00:00+00:00</updated><id>https://jtsylve.blog/post/2022/12/29/APFS%20Fusion%20Containers</id><content type="html" xml:base="https://jtsylve.blog/post/2022/12/29/APFS-Fusion-Containers"><![CDATA[<p>As we discussed in <a href="/post/2022/12/05/APFS-Containers">an earlier post</a>, Apple’s <a href="https://en.wikipedia.org/wiki/Fusion_Drive">Fusion Drives</a> combine the storage capacity of a hard disk drive (HDD) with the faster access speed of a solid state drive (SSD). The HDD is the primary storage device, and the SSD acts as a cache for recently accessed data. However, the Fusion Drive does not have built-in caching logic, and the operating system treats the two drives as separate storage devices.   Apple created <a href="https://en.wikipedia.org/wiki/Core_Storage">Core Storage</a> to support the desired caching capabilities and the ability to pool the storage of each device into a single logical volume. APFS removes the need for Core Storage by having first-class support for this tiered storage model.  This post will go into more detail about APFS <em>Fusion Containers</em>.</p>

<h2 id="physical-stores">Physical Stores</h2>

<p>Both the SSD and HDD of a Fusion Drive appear to macOS as separate physical disk devices.  Both disks are <a href="https://en.wikipedia.org/wiki/GUID_Partition_Table">GPT</a> partitioned with a standard EFI partition and a second, larger partition, which takes up the bulk of the space on disk.  For example, running the command <code class="language-plaintext highlighter-rouge">diskutil list</code> may show the HDD as <code class="language-plaintext highlighter-rouge">/dev/disk0</code> with its primary partition as <code class="language-plaintext highlighter-rouge">/dev/disk0s2</code> and the SSD as <code class="language-plaintext highlighter-rouge">/dev/disk1</code> and <code class="language-plaintext highlighter-rouge">/dev/disk1s2</code>.  These two partitions make up the <em>physical stores</em> of the Fusion Container.</p>

<p>Each physical store is formatted separately in much the same way as any other APFS container.  Both will share the same <code class="language-plaintext highlighter-rouge">nx_uuid</code> in their <em>NX Superblocks</em> and have a separate, nearly-identical UUID in the <code class="language-plaintext highlighter-rouge">nx_fusion_uuid</code> field, with the <em>most significant bit</em> being cleared on the <code class="language-plaintext highlighter-rouge">tier1</code> SSD partition and set on the <code class="language-plaintext highlighter-rouge">tier2</code> HDD partition.  The combination of these UUIDs can be used to identify the physical storage tiers of the container.</p>

<h2 id="synthesized-container">Synthesized Container</h2>

<p>Both tiers are mapped together as a single “synthesized” container and are presented to macOS as a single logical block device (for example, <code class="language-plaintext highlighter-rouge">/dev/disk2</code>). The <code class="language-plaintext highlighter-rouge">tier1</code> blocks are mapped at logical byte offset zero, and the <code class="language-plaintext highlighter-rouge">tier2</code> blocks at 4 EiB. The offsets within the exabyte-scale gap between the two sets of blocks cannot be read.</p>

<p>APFS objects and blocks can be stored on either (or both) tiers, and their physical addresses will require some simple translation as follows:</p>

<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cp">#define FUSION_TIER2_DEVICE_BYTE_ADDR 0x4000000000000000ULL
</span><span class="k">const</span> <span class="n">paddr_t</span> <span class="n">first_tier2_block</span> <span class="o">=</span> <span class="n">FUSION_TIER2_DEVICE_BYTE_ADDR</span> <span class="o">/</span> <span class="n">nxsb</span><span class="o">-&gt;</span><span class="n">block_size</span><span class="p">;</span>

<span class="k">if</span> <span class="p">(</span><span class="n">paddr</span> <span class="o">&lt;</span> <span class="n">first_tier2_block</span><span class="p">)</span> <span class="p">{</span>
  <span class="n">tier1</span><span class="o">-&gt;</span><span class="n">read_block</span><span class="p">(</span><span class="n">paddr</span><span class="p">);</span> 
<span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
  <span class="n">tier2</span><span class="o">-&gt;</span><span class="n">read_block</span><span class="p">(</span><span class="n">paddr</span> <span class="err">–</span> <span class="n">first_tier2_block</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>

<p>The logically exabyte-scale gap separating the two tiers presents a unique problem during digital forensic imaging of Fusion Containers.  To preserve the logical offsets of the evidence without having to use a data center worth of storage, you must use an evidence storage format that supports <em>sparse</em> imaging.  As long as this is considered along with the additional physical address translation described above, analyzing Fusion Containers does not generally differ from analyzing other APFS containers.</p>]]></content><author><name></name></author><category term="file-systems" /><category term="apfs" /><category term="apfs" /><category term="fusion" /><category term="containers" /><summary type="html"><![CDATA[As we discussed in an earlier post, Apple’s Fusion Drives combine the storage capacity of a hard disk drive (HDD) with the faster access speed of a solid state drive (SSD). The HDD is the primary storage device, and the SSD acts as a cache for recently accessed data. However, the Fusion Drive does not have built-in caching logic, and the operating system treats the two drives as separate storage devices. Apple created Core Storage to support the desired caching capabilities and the ability to pool the storage of each device into a single logical volume. APFS removes the need for Core Storage by having first-class support for this tiered storage model. This post will go into more detail about APFS Fusion Containers.]]></summary></entry><entry><title type="html">Snapshot Metadata</title><link href="https://jtsylve.blog/post/2022/12/28/APFS-Snapshot-Metadata" rel="alternate" type="text/html" title="Snapshot Metadata" /><published>2022-12-28T00:00:00+00:00</published><updated>2022-12-28T00:00:00+00:00</updated><id>https://jtsylve.blog/post/2022/12/28/APFS%20Snapshot%20Metadata</id><content type="html" xml:base="https://jtsylve.blog/post/2022/12/28/APFS-Snapshot-Metadata"><![CDATA[<p>Our previous post covered how <a href="/post/2022/12/12/APFS-OMAP"><em>Object Maps</em></a> facilitate the implementation of point-in-time <em>Snapshots</em> of APFS file systems by preserving <a href="/post/2022/12/15/APFS-FSTrees"><em>File System Tree Nodes</em></a> from earlier transactions. In that discussion, I outlined the on-disk structure of the <em>Object Map Snapshot Tree</em> and how it can be used to enumerate the transaction identifiers of each Volume Snapshot. Today, we will briefly discuss two other sources of information that store additional metadata about each Snapshot.</p>

<h2 id="snapshot-metadata-tree">Snapshot Metadata Tree</h2>

<p>The <em>Snapshot Metadata Tree</em> is a <a href="/post/2022/12/08/APFS-BTrees">B-Tree</a> whose physical address can be located by reading the <code class="language-plaintext highlighter-rouge">apfs_snap_meta_tree_oid</code> field of the <a href="/post/2022/12/13/APFS-Volume-Superblock"><em>Volume Superblock</em></a>.  It stores two types of objects, structured as <a href="/post/2022/12/15/APFS-FSTrees"><em>File System Records</em></a>.</p>

<h3 id="snapshot-metadata-records">Snapshot Metadata Records</h3>

<p><em>Snapshot Metadata Records</em> store the bulk of metadata about Volume Snapshots.  The key-half is a <code class="language-plaintext highlighter-rouge">j_snap_metadata_key</code> structure with an encoded type of <code class="language-plaintext highlighter-rouge">APFS_TYPE_SNAP_METADATA</code>.</p>

<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">typedef</span> <span class="k">struct</span> <span class="nc">j_snap_metadata_key</span> <span class="p">{</span>
  <span class="n">j_key_t</span> <span class="n">hdr</span><span class="p">;</span>           <span class="c1">// 0x00</span>
<span class="p">}</span> <span class="n">j_snap_metadata_key_t</span><span class="p">;</span> <span class="c1">// 0x08</span>
</code></pre></div></div>
<ul>
  <li><code class="language-plaintext highlighter-rouge">hdr</code>: The record’s header.  The object identifier in the header is the snapshot’s transaction identifier.</li>
</ul>

<p>The value-half of the record is a <code class="language-plaintext highlighter-rouge">j_snap_metadata_val_t</code> structure and is immediately followed by the UTF-8 encoded name of the snapshot.</p>

<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">typedef</span> <span class="k">struct</span> <span class="nc">j_snap_metadata_val</span> <span class="p">{</span>
  <span class="n">oid_t</span> <span class="n">extentref_tree_oid</span><span class="p">;</span>       <span class="c1">// 0x00</span>
  <span class="n">oid_t</span> <span class="n">sblock_oid</span><span class="p">;</span>               <span class="c1">// 0x08</span>
  <span class="kt">uint64_t</span> <span class="n">create_time</span><span class="p">;</span>           <span class="c1">// 0x10</span>
  <span class="kt">uint64_t</span> <span class="n">change_time</span><span class="p">;</span>           <span class="c1">// 0x18</span>
  <span class="kt">uint64_t</span> <span class="n">inum</span><span class="p">;</span>                  <span class="c1">// 0x20</span>
  <span class="kt">uint32_t</span> <span class="n">extentref_tree_type</span><span class="p">;</span>   <span class="c1">// 0x28</span>
  <span class="kt">uint32_t</span> <span class="n">flags</span><span class="p">;</span>                 <span class="c1">// 0x2C</span>
  <span class="kt">uint16_t</span> <span class="n">name_len</span><span class="p">;</span>              <span class="c1">// 0x30</span>
  <span class="kt">uint8_t</span> <span class="n">name</span><span class="p">[</span><span class="mi">0</span><span class="p">];</span>                <span class="c1">// 0x32</span>
<span class="p">}</span> <span class="n">j_snap_metadata_val_t</span><span class="p">;</span>
</code></pre></div></div>
<ul>
  <li><code class="language-plaintext highlighter-rouge">extentref_tree_oid</code>: The <em>physical object identifier</em> of the B-Tree that stores extent references for the snapshot.</li>
  <li><code class="language-plaintext highlighter-rouge">sblock_oid</code>: The <em>physical object identifier</em> of a backup of the snapshot’s Volume Superblock</li>
  <li><code class="language-plaintext highlighter-rouge">create_time</code>: The time when the snapshot was created</li>
  <li><code class="language-plaintext highlighter-rouge">change_time</code>: The time that this snapshot was last modified</li>
  <li><code class="language-plaintext highlighter-rouge">inum</code>: <em>reserved</em></li>
  <li><code class="language-plaintext highlighter-rouge">extentref_tree_type</code>: The type of the <em>Extent Reference Tree</em></li>
  <li><code class="language-plaintext highlighter-rouge">flags</code>: A bit field that contains additional information about a snapshot metadata record</li>
  <li><code class="language-plaintext highlighter-rouge">name_len</code>: The length of the name that follows this structure (in bytes)</li>
</ul>

<h4 id="snapshot-metadata-record-flags">Snapshot Metadata Record Flags</h4>

<table style="margin-left: 0">
  <thead>
    <tr>
      <th>Name</th>
      <th>Value</th>
      <th>Description</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>SNAP_META_PENDING_DATALESS</td>
      <td>0x00000001</td>
      <td>This snapshot is <em>dataless</em>, meaning that it does not preserve the file extents</td>
    </tr>
    <tr>
      <td>SNAP_META_MERGE_IN_PROGRESS</td>
      <td>0x00000002</td>
      <td>The snapshot is in the process of being merged with another</td>
    </tr>
  </tbody>
</table>

<h3 id="snapshot-name-records">Snapshot Name Records</h3>

<p><em>Snapshot Name Records</em> are used to map snapshot names to their <em>transaction identifiers</em>.  The key-half of the record is a <code class="language-plaintext highlighter-rouge">j_snap_name_key_t</code> structure with an encoded type of <code class="language-plaintext highlighter-rouge">APFS_TYPE_SNAP_NAME</code>.  It is followed by the UTF-8 encoded name of the snapshot.</p>

<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">typedef</span> <span class="k">struct</span> <span class="nc">j_snap_name_key</span> <span class="p">{</span>
  <span class="n">j_key_t</span> <span class="n">hdr</span><span class="p">;</span>        <span class="c1">// 0x00</span>
  <span class="kt">uint16_t</span> <span class="n">name_len</span><span class="p">;</span>  <span class="c1">// 0x08</span>
  <span class="kt">uint8_t</span> <span class="n">name</span><span class="p">[</span><span class="mi">0</span><span class="p">];</span>    <span class="c1">// 0x0A</span>
<span class="p">}</span> <span class="n">j_snap_name_key_t</span><span class="p">;</span>
</code></pre></div></div>
<ul>
  <li><code class="language-plaintext highlighter-rouge">hdr</code>: The record’s header.  The object identifier can be ignored.</li>
  <li><code class="language-plaintext highlighter-rouge">name_len</code>: The length of the name (in bytes)</li>
  <li><code class="language-plaintext highlighter-rouge">name</code>: The start of the UTF-8 encoded name</li>
</ul>

<p>The value-half is a <code class="language-plaintext highlighter-rouge">j_snap_name_val_t</code> structure.</p>

<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">typedef</span> <span class="k">struct</span> <span class="nc">j_snap_name_val</span> <span class="p">{</span>
  <span class="n">xid_t</span> <span class="n">snap_xid</span><span class="p">;</span>    <span class="c1">// 0x00</span>
<span class="p">}</span> <span class="n">j_snap_name_val_t</span><span class="p">;</span> <span class="c1">// 0x08</span>
</code></pre></div></div>
<ul>
  <li><code class="language-plaintext highlighter-rouge">snap_xid</code>: The <em>transaction identifier</em> of the snapshot</li>
</ul>

<h2 id="snapshot-extended-metadata-object">Snapshot Extended Metadata Object</h2>

<p>Each snapshot has a <em>virtual</em> <em>Snapshot Extended Metadata Object</em> in the volume’s <em>Object Map</em>.  The <em>virtual object identifier</em> of this object is stored in the <code class="language-plaintext highlighter-rouge">apfs_snap_meta_ext_oid</code> field of the Volume Superblock.  There are multiple versions of this object whose <em>transaction identifiers</em> correspond to each snapshot.</p>

<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">typedef</span> <span class="k">struct</span> <span class="nc">snap_meta_ext_obj_phys</span> <span class="p">{</span>
  <span class="n">obj_phys_t</span> <span class="n">smeop_o</span><span class="p">;</span>        <span class="c1">// 0x00</span>
  <span class="n">snap_meta_ext_t</span> <span class="n">smeop_sme</span><span class="p">;</span> <span class="c1">// 0x20</span>
<span class="p">}</span> <span class="n">snap_meta_ext_obj_phys_t</span><span class="p">;</span>  <span class="c1">// 0x48</span>
</code></pre></div></div>
<ul>
  <li><code class="language-plaintext highlighter-rouge">smeop_o</code>: The object’s header</li>
  <li><code class="language-plaintext highlighter-rouge">smeop_sme</code>: The snapshot’s extended metadata</li>
</ul>

<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">typedef</span> <span class="k">struct</span> <span class="nc">snap_meta_ext</span> <span class="p">{</span>
  <span class="kt">uint32_t</span> <span class="n">sme_version</span><span class="p">;</span> <span class="c1">// 0x00</span>
  <span class="kt">uint32_t</span> <span class="n">sme_flags</span><span class="p">;</span>   <span class="c1">// 0x04</span>
  <span class="n">xid_t</span> <span class="n">sme_snap_xid</span><span class="p">;</span>   <span class="c1">// 0x08</span>
  <span class="n">uuid_t</span> <span class="n">sme_uuid</span><span class="p">;</span>      <span class="c1">// 0x10</span>
  <span class="kt">uint64_t</span> <span class="n">sme_token</span><span class="p">;</span>   <span class="c1">// 0x20</span>
<span class="p">}</span> <span class="n">snap_meta_ext_t</span><span class="p">;</span>      <span class="c1">// 0x28</span>
</code></pre></div></div>
<ul>
  <li><code class="language-plaintext highlighter-rouge">sme_version</code>: The version of this structure (currently 1)</li>
  <li><code class="language-plaintext highlighter-rouge">sme_flags</code>: A bitfield of flags (none are currently defined)</li>
  <li><code class="language-plaintext highlighter-rouge">sme_snap_xid</code>: The transaction identifier of the snapshot</li>
  <li><code class="language-plaintext highlighter-rouge">sme_uuid</code>: The unique identifier of the snapshot</li>
  <li><code class="language-plaintext highlighter-rouge">sme_token</code>: An opaque token (<em>reserved</em>)</li>
</ul>]]></content><author><name></name></author><category term="file-systems" /><category term="apfs" /><category term="apfs" /><category term="snapshots" /><summary type="html"><![CDATA[Our previous post covered how Object Maps facilitate the implementation of point-in-time Snapshots of APFS file systems by preserving File System Tree Nodes from earlier transactions. In that discussion, I outlined the on-disk structure of the Object Map Snapshot Tree and how it can be used to enumerate the transaction identifiers of each Volume Snapshot. Today, we will briefly discuss two other sources of information that store additional metadata about each Snapshot.]]></summary></entry><entry><title type="html">Decryption</title><link href="https://jtsylve.blog/post/2022/12/26/APFS-Decryption" rel="alternate" type="text/html" title="Decryption" /><published>2022-12-26T00:00:00+00:00</published><updated>2022-12-26T00:00:00+00:00</updated><id>https://jtsylve.blog/post/2022/12/26/APFS%20Decryption</id><content type="html" xml:base="https://jtsylve.blog/post/2022/12/26/APFS-Decryption"><![CDATA[<p>Now that we know how to parse the <a href="/post/2022/12/15/APFS-FSTrees">File System Tree</a>, <a href="/post/2022/12/21/APFS-Keybags">analyze keybags</a>, and <a href="/post/2022/12/22/APFS-Wrapped-Keys">unwrap decryption keys</a>, it’s time to put it all together and learn how to decrypt file system metadata and file data on encrypted volumes in APFS.</p>

<h2 id="tweaks">Tweaks</h2>

<p>All encryption in APFS is based on the <a href="https://en.wikipedia.org/wiki/Disk_encryption_theory#XEX-based_tweaked-codebook_mode_with_ciphertext_stealing_(XTS)">XTS-AES-128</a> cipher, which uses a 256-bit key and a 64-bit <a href="https://en.wikipedia.org/wiki/Block_cipher#Tweakable_block_ciphers">“tweak”</a> value.  This <em>tweak</em> value is position dependent.  It allows the same <em>plaintext</em> to be encrypted and stored in different locations on disk and have drastically different <em>ciphertext</em> while using the same AES key.  Every 512 bytes of encrypted data uses a tweak based on the container offset of the block’s initial storage.</p>

<p>Knowledge of the AES key alone is not always enough for successful decryption.  If the encrypted block is ever relocated on disk, the data is not guaranteed to be re-encrypted with a new tweak.  In these cases, the tweak can not be inferred based on the block’s on-disk location, so we must learn the original tweak value used for encryption.</p>

<h2 id="identifying-encrypted-blocks">Identifying Encrypted Blocks</h2>

<p>There are primarily two sets of data protected with the APFS <em>Volume Encryption Key</em>: <a href="/post/2022/12/15/APFS-FSTrees"><em>File System Tree Nodes</em></a> and <a href="/post/2022/12/19/APFS-Data-Streams"><em>File Extents</em></a>.  As we’ve discussed, <em>File System Tree Nodes</em> store the <em>File System Records</em> that contain the file system’s metadata, and <em>File Extents</em> contain the bulk of the data stored in a file’s <em>Data Streams</em>.</p>

<h3 id="encrypted-fs-tree-nodes">Encrypted FS-Tree Nodes</h3>

<p>A volume’s <em>Object Map</em> is never encrypted, but its referenced <em>virtual objects</em> may be, as is the case with FS-Tree Nodes on encrypted volumes.</p>

<p>Let’s revisit the value half of an <em>Object Map entry</em>.</p>

<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">typedef</span> <span class="k">struct</span> <span class="nc">omap_val</span> <span class="p">{</span>
  <span class="kt">uint32_t</span> <span class="n">ov_flags</span><span class="p">;</span> <span class="c1">// 0x00</span>
  <span class="kt">uint32_t</span> <span class="n">ov_size</span><span class="p">;</span>  <span class="c1">// 0x04</span>
  <span class="n">paddr_t</span> <span class="n">ov_paddr</span><span class="p">;</span>  <span class="c1">// 0x08</span>
<span class="p">}</span> <span class="n">omap_val_t</span><span class="p">;</span>        <span class="c1">// 0x10</span>
</code></pre></div></div>

<p>If the <code class="language-plaintext highlighter-rouge">ov_flags</code> bit-field member has the <code class="language-plaintext highlighter-rouge">OMAP_VAL_ENCRYPTED</code> flag set, then the virtual object located at <code class="language-plaintext highlighter-rouge">ov_paddr</code> is encrypted. These objects are never relocated without being re-encrypted, so the tweak of the first 512 bytes of data can be determined by the physical location of the data using the following logic, with the following tweak values incremented for each subsequent 512 bytes of data:</p>

<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">uint64_t</span> <span class="n">tweak0</span> <span class="o">=</span> <span class="p">(</span><span class="n">ov_paddr</span> <span class="o">*</span> <span class="n">block_size</span><span class="p">)</span> <span class="o">/</span> <span class="mi">512</span><span class="p">;</span>
</code></pre></div></div>

<h3 id="encrypted-extents">Encrypted Extents</h3>

<p>Extent data can be relocated on disk and is not guaranteed to be re-encrypted.  Due to this, the initial tweak value is stored in the <code class="language-plaintext highlighter-rouge">crypto_id</code> field of the <code class="language-plaintext highlighter-rouge">j_file_extent_val_t</code> file system record:</p>

<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">typedef</span> <span class="k">struct</span> <span class="nc">j_file_extent_val</span> <span class="p">{</span>
  <span class="kt">uint64_t</span> <span class="n">len_and_flags</span><span class="p">;</span>  <span class="c1">// 0x00</span>
  <span class="kt">uint64_t</span> <span class="n">phys_block_num</span><span class="p">;</span> <span class="c1">// 0x08</span>
  <span class="kt">uint64_t</span> <span class="n">crypto_id</span><span class="p">;</span>      <span class="c1">// 0x10</span>
<span class="p">}</span> <span class="n">j_file_extent_val_t</span><span class="p">;</span>     <span class="c1">// 0x18</span>
</code></pre></div></div>

<h2 id="conclusion">Conclusion</h2>

<p>We’ve now discussed all of the information needed to access data on software-encrypted APFS volumes.  This decryption requires the knowledge of the password of any user on the system or one of the various recovery keys.  While APFS hardware encryption works in largely the same manner, the encryption also depends on keys that are stored within the specific security chip on a given system.  There are currently no known methods of extracting these chip-specific keys; therefore, the data on hardware-encrypted devices must be decrypted at acquisition time on the device itself.  The only software that I am aware of that is capable of this is <a href="https://cellebrite.com/en/digital-collector/">Cellebrite’s Digital Collector</a>.</p>

<p><em>Full disclosure:  I currently work for Cellebrite and helped develop these capabilities.  I do not directly profit from the sales of Digital Collector but felt it appropriate to disclose my association when linking to a commercial product.  I am not trying to sell you anything.  Unfortunately, I am also not at liberty to discuss the methodology used to facilitate this decryption.</em></p>]]></content><author><name></name></author><category term="file-systems" /><category term="apfs" /><category term="apfs" /><category term="decryption" /><category term="encryption" /><summary type="html"><![CDATA[Now that we know how to parse the File System Tree, analyze keybags, and unwrap decryption keys, it’s time to put it all together and learn how to decrypt file system metadata and file data on encrypted volumes in APFS.]]></summary></entry><entry><title type="html">Update: Blazingly Fast-er SIMD Checksums</title><link href="https://jtsylve.blog/post/2022/12/24/Blazingly-Fast-er-SIMD-Checksums" rel="alternate" type="text/html" title="Update: Blazingly Fast-er SIMD Checksums" /><published>2022-12-24T00:00:00+00:00</published><updated>2022-12-24T00:00:00+00:00</updated><id>https://jtsylve.blog/post/2022/12/24/Blazingly%20Fast-er%20SIMD%20Checksums</id><content type="html" xml:base="https://jtsylve.blog/post/2022/12/24/Blazingly-Fast-er-SIMD-Checksums"><![CDATA[<p>This is a quick update to <a href="/post/2022/12/23/Blazingly-Fast-Checksums-with-SIMD">yesterday’s post</a> on using <a href="https://en.cppreference.com/w/cpp/experimental/simd/simd"><code class="language-plaintext highlighter-rouge">std::experimental::simd</code></a> to speed up APFS Fletcher-64 calculations.  It turns out that there were still some low-hanging optimizations that could be used to improve my code.  I got better performance from my code by using a simple <a href="https://en.wikipedia.org/wiki/Loop_unrolling">loop unrolling</a> technique.</p>

<p>Here’s the new version of the function.  Notice that the only difference is that I’m now calculating more data per iteration of the loop.  I’m using a lambda here to avoid code duplication, but the compiler will gladly inline the code.</p>

<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">static</span> <span class="kt">uint64_t</span> <span class="nf">fletcher64_simd</span><span class="p">(</span><span class="n">std</span><span class="o">::</span><span class="n">span</span><span class="o">&lt;</span><span class="k">const</span> <span class="kt">uint32_t</span><span class="p">,</span> <span class="mi">1024</span><span class="o">&gt;</span> <span class="n">words</span><span class="p">)</span> <span class="p">{</span>
  <span class="n">vu64</span> <span class="n">sum1</span><span class="p">{};</span>
  <span class="n">sum1</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span> <span class="o">=</span> <span class="o">-</span><span class="p">(</span><span class="k">static_cast</span><span class="o">&lt;</span><span class="kt">uint64_t</span><span class="o">&gt;</span><span class="p">(</span><span class="n">words</span><span class="p">[</span><span class="mi">0</span><span class="p">])</span> <span class="o">+</span> <span class="n">words</span><span class="p">[</span><span class="mi">1</span><span class="p">]);</span>

  <span class="n">vu64</span> <span class="n">sum2</span><span class="p">{};</span>
  <span class="n">sum2</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span> <span class="o">=</span> <span class="n">words</span><span class="p">[</span><span class="mi">1</span><span class="p">];</span>

  <span class="k">const</span> <span class="k">auto</span> <span class="n">calc</span> <span class="o">=</span> <span class="p">[</span><span class="o">&amp;</span><span class="p">](</span><span class="kt">size_t</span> <span class="n">n</span><span class="p">)</span> <span class="p">{</span>
    <span class="n">sum2</span> <span class="o">+=</span> <span class="n">vu32</span><span class="o">::</span><span class="n">size</span><span class="p">()</span> <span class="o">*</span> <span class="n">sum1</span><span class="p">;</span>

    <span class="k">const</span> <span class="n">vu64</span> <span class="n">all</span><span class="p">{</span><span class="k">reinterpret_cast</span><span class="o">&lt;</span><span class="k">const</span> <span class="kt">uint64_t</span><span class="o">*&gt;</span><span class="p">(</span><span class="n">std</span><span class="o">::</span><span class="n">addressof</span><span class="p">(</span><span class="n">words</span><span class="p">[</span><span class="n">n</span><span class="p">])),</span>
                  <span class="n">stdx</span><span class="o">::</span><span class="n">vector_aligned</span><span class="p">};</span>

    <span class="k">const</span> <span class="n">vu64</span> <span class="n">evens</span> <span class="o">=</span> <span class="n">all</span> <span class="o">&amp;</span> <span class="n">max32</span><span class="p">;</span>
    <span class="k">const</span> <span class="n">vu64</span> <span class="n">odds</span> <span class="o">=</span> <span class="n">all</span> <span class="o">&gt;&gt;</span> <span class="mi">32</span><span class="p">;</span>

    <span class="n">sum1</span> <span class="o">+=</span> <span class="n">evens</span> <span class="o">+</span> <span class="n">odds</span><span class="p">;</span>
    <span class="n">sum2</span> <span class="o">+=</span> <span class="n">evens</span> <span class="o">*</span> <span class="n">even_m</span> <span class="o">+</span> <span class="n">odds</span> <span class="o">*</span> <span class="n">odd_m</span><span class="p">;</span>
  <span class="p">};</span>

  <span class="k">for</span> <span class="p">(</span><span class="kt">size_t</span> <span class="n">n</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">n</span> <span class="o">&lt;</span> <span class="n">words</span><span class="p">.</span><span class="n">size</span><span class="p">();</span> <span class="n">n</span> <span class="o">+=</span> <span class="n">vu32</span><span class="o">::</span><span class="n">size</span><span class="p">())</span> <span class="p">{</span>
    <span class="n">calc</span><span class="p">(</span><span class="n">n</span><span class="p">);</span>
    <span class="n">calc</span><span class="p">(</span><span class="n">n</span> <span class="o">+=</span> <span class="n">vu32</span><span class="o">::</span><span class="n">size</span><span class="p">());</span>
    <span class="n">calc</span><span class="p">(</span><span class="n">n</span> <span class="o">+=</span> <span class="n">vu32</span><span class="o">::</span><span class="n">size</span><span class="p">());</span>
    <span class="n">calc</span><span class="p">(</span><span class="n">n</span> <span class="o">+=</span> <span class="n">vu32</span><span class="o">::</span><span class="n">size</span><span class="p">());</span>
    <span class="n">calc</span><span class="p">(</span><span class="n">n</span> <span class="o">+=</span> <span class="n">vu32</span><span class="o">::</span><span class="n">size</span><span class="p">());</span>
    <span class="n">calc</span><span class="p">(</span><span class="n">n</span> <span class="o">+=</span> <span class="n">vu32</span><span class="o">::</span><span class="n">size</span><span class="p">());</span>
    <span class="n">calc</span><span class="p">(</span><span class="n">n</span> <span class="o">+=</span> <span class="n">vu32</span><span class="o">::</span><span class="n">size</span><span class="p">());</span>
    <span class="n">calc</span><span class="p">(</span><span class="n">n</span> <span class="o">+=</span> <span class="n">vu32</span><span class="o">::</span><span class="n">size</span><span class="p">());</span>
  <span class="p">}</span>

  <span class="c1">// Fold the 64-bit overflow back into the 32-bit value</span>
  <span class="k">const</span> <span class="k">auto</span> <span class="n">fold</span> <span class="o">=</span> <span class="p">[</span><span class="o">&amp;</span><span class="p">](</span><span class="kt">uint64_t</span> <span class="n">x</span><span class="p">)</span> <span class="p">{</span>
    <span class="n">x</span> <span class="o">=</span> <span class="p">(</span><span class="n">x</span> <span class="o">&amp;</span> <span class="n">max32</span><span class="p">)</span> <span class="o">+</span> <span class="p">(</span><span class="n">x</span> <span class="o">&gt;&gt;</span> <span class="mi">32</span><span class="p">);</span>
    <span class="k">return</span> <span class="p">(</span><span class="n">x</span> <span class="o">==</span> <span class="n">max32</span><span class="p">)</span> <span class="o">?</span> <span class="mi">0</span> <span class="o">:</span> <span class="n">x</span><span class="p">;</span>
  <span class="p">};</span>

  <span class="k">const</span> <span class="kt">uint64_t</span> <span class="n">low</span> <span class="o">=</span> <span class="n">fold</span><span class="p">(</span><span class="n">stdx</span><span class="o">::</span><span class="n">reduce</span><span class="p">(</span><span class="n">sum1</span><span class="p">));</span>
  <span class="k">const</span> <span class="kt">uint64_t</span> <span class="n">high</span> <span class="o">=</span> <span class="n">fold</span><span class="p">(</span><span class="n">stdx</span><span class="o">::</span><span class="n">reduce</span><span class="p">(</span><span class="n">sum2</span><span class="p">));</span>

  <span class="k">const</span> <span class="kt">uint64_t</span> <span class="n">ck_low</span> <span class="o">=</span> <span class="n">max32</span> <span class="o">-</span> <span class="p">((</span><span class="n">low</span> <span class="o">+</span> <span class="n">high</span><span class="p">)</span> <span class="o">%</span> <span class="n">max32</span><span class="p">);</span>
  <span class="k">const</span> <span class="kt">uint64_t</span> <span class="n">ck_high</span> <span class="o">=</span> <span class="n">max32</span> <span class="o">-</span> <span class="p">((</span><span class="n">low</span> <span class="o">+</span> <span class="n">ck_low</span><span class="p">)</span> <span class="o">%</span> <span class="n">max32</span><span class="p">);</span>

  <span class="k">return</span> <span class="n">ck_low</span> <span class="o">|</span> <span class="n">ck_high</span> <span class="o">&lt;&lt;</span> <span class="mi">32</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

<h4 id="updated-results">Updated Results</h4>

<p>Here are the updated relative performance statistics with the updated code running on the same hardware as yesterday’s tests.  Amazing!</p>

<table style="margin-left: 0">
  <thead>
    <tr>
      <th>Target Architecture</th>
      <th>Time per Checksum</th>
      <th>Throughput</th>
      <th>Speedup</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>SSE</td>
      <td>217ns</td>
      <td>17.5543 GiB/s</td>
      <td>3.4x</td>
    </tr>
    <tr>
      <td>AVX2</td>
      <td>105ns</td>
      <td>36.2421 GiB/s</td>
      <td>7x</td>
    </tr>
    <tr>
      <td>AVX-512</td>
      <td>75ns</td>
      <td>50.7305 GiB/s</td>
      <td>9.7x</td>
    </tr>
    <tr>
      <td>NEON</td>
      <td>171ns</td>
      <td>22.273 GiB/s</td>
      <td>2.7x</td>
    </tr>
  </tbody>
</table>]]></content><author><name></name></author><category term="high-performance-computing" /><category term="apfs" /><category term="simd" /><category term="checksums" /><category term="performance" /><summary type="html"><![CDATA[This is a quick update to yesterday’s post on using std::experimental::simd to speed up APFS Fletcher-64 calculations. It turns out that there were still some low-hanging optimizations that could be used to improve my code. I got better performance from my code by using a simple loop unrolling technique.]]></summary></entry></feed>