<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="https://mnsr.dev/feed.xml" rel="self" type="application/atom+xml" /><link href="https://mnsr.dev/" rel="alternate" type="text/html" /><updated>2026-10-20T15:13:33+00:00</updated><id>https://mnsr.dev/feed.xml</id><title type="html">mnsr.dev</title><subtitle>Security research, reverse engineering, malware, binary analysis, and hardware.</subtitle><author><name>{&quot;email&quot;=&gt;&quot;js@mnsr.dev&quot;}</name><email>js@mnsr.dev</email></author><entry><title type="html">I am Chroot! Host File Read in angr</title><link href="https://mnsr.dev/2026/10/18/angr-dir-read/" rel="alternate" type="text/html" title="I am Chroot! Host File Read in angr" /><published>2026-10-18T00:00:00+00:00</published><updated>2026-10-18T00:00:00+00:00</updated><id>https://mnsr.dev/2026/10/18/angr-dir-read</id><content type="html" xml:base="https://mnsr.dev/2026/10/18/angr-dir-read/"><![CDATA[<h1 id="introduction">Introduction</h1>

<p>Binary analysis tools, especially those like angr, typically come with an
expectation that system interaction will be emulated or symbolic and won’t
interact directly with host state unless explicitly permitted to do so.
If the input binary is untrusted, then an attacker could leverage host
interactivity issues to extract host information influence downstream
processing.
In this post, I will detail a bug which can demonstrate the former. The risk is
concrete host information which winds up in angr analysis results might be
leveraged by an attacker to gain insights into a host analysis platform.</p>

<h1 id="background">Background</h1>

<ul>
  <li><a href="https://man7.org/linux/man-pages/man2/chroot.2.html"><code class="language-plaintext highlighter-rouge">chroot</code></a>: This is a system call which allows a process with
sufficient permissions to change the apparent root directory path resolution
for itself and its children. You provide a real directory name as an argument
and that directory will become “/” in subsequent path based file operations.</li>
  <li><a href="https://docs.angr.io/en/latest/extending-angr/simprocedures.html"><code class="language-plaintext highlighter-rouge">SimProcedure</code></a>: This is a hook for symbolic execution which
allows the host to intercept and compute a response to a given function call.
This is often used to implement guest behavior for common library functions or
syscalls.</li>
  <li><a href="https://github.com/angr/angr/blob/v9.3.3/angr/simos/linux.py"><code class="language-plaintext highlighter-rouge">concrete_fs</code></a>: an argument in angr which enables the use of the
host filesystem for guest file operations.</li>
</ul>

<h1 id="host-file-read-in-angr">Host File Read in Angr</h1>

<p>A binary analyzed by angr using default project settings can use the
<code class="language-plaintext highlighter-rouge">chroot</code> system call to mount a host directory. Subsequent <code class="language-plaintext highlighter-rouge">open</code>/<code class="language-plaintext highlighter-rouge">read</code> calls
can then read host contents (that the angr process has permissions for) without host approval and no <code class="language-plaintext highlighter-rouge">concrete_fs</code> requirement.</p>

<p><em>Note: The symbolic execution engine must reach the <code class="language-plaintext highlighter-rouge">chroot</code> procedure. Just
having it in the binary is not sufficient.</em></p>

<p>First, let’s examine the key part of the
<a href="https://github.com/angr/angr/blob/v9.3.3/angr/procedures/posix/chroot.py"><code class="language-plaintext highlighter-rouge">chroot</code> SimProcedure in angr 9.3.3</a> (abridged):</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">chroot</span><span class="p">(</span><span class="n">angr</span><span class="p">.</span><span class="n">SimProcedure</span><span class="p">):</span>
<span class="p">...</span>
    <span class="k">def</span> <span class="nf">run</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">file_path</span><span class="p">):</span>
    <span class="p">...</span>
        <span class="k">if</span> <span class="ow">not</span> <span class="n">os</span><span class="p">.</span><span class="n">path</span><span class="p">.</span><span class="n">exists</span><span class="p">(</span><span class="n">newPath</span><span class="p">):</span>
            <span class="k">return</span> <span class="o">-</span><span class="mi">1</span>

        <span class="bp">self</span><span class="p">.</span><span class="n">state</span><span class="p">.</span><span class="n">fs</span><span class="p">.</span><span class="n">mount</span><span class="p">(</span><span class="s">"/"</span><span class="p">,</span> <span class="n">angr</span><span class="p">.</span><span class="n">SimHostFilesystem</span><span class="p">(</span><span class="n">newPath</span><span class="p">))</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">os.path.exists(newPath)</code> tests for the existence of a file on the <em>host</em>
filesystem. <code class="language-plaintext highlighter-rouge">angr.SimHostFilesystem(newPath)</code> creates a host filesystem object.
<code class="language-plaintext highlighter-rouge">state.fs.mount("/", ...)</code> mounts that host based filesystem object to the
simulated root path, making it accessible to guest symbolic execution paths.
No check for <code class="language-plaintext highlighter-rouge">concrete_fs</code> is made. This is the trust-boundary violation.</p>

<p>The following binary, when compiled, will exercise the vulnerability:</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cm">/*
 * Minimal x86-64 Linux guest for validating angr's chroot SimProcedure.
 *
 * Build from the angr checkout when the binaries repository is beside it:
 *
 * gcc -O0 -o ../binaries/tests/x86_64/chroot_read \
 *   tests/procedures/posix/chroot_read/chroot_read.c
 *
 * The host directory to chroot into is argv[1].
 */</span>

<span class="cp">#include</span> <span class="cpf">&lt;fcntl.h&gt;</span><span class="cp">
#include</span> <span class="cpf">&lt;unistd.h&gt;</span><span class="cp">
</span>
<span class="kt">int</span> <span class="nf">main</span><span class="p">(</span><span class="kt">int</span> <span class="n">argc</span><span class="p">,</span> <span class="kt">char</span> <span class="o">**</span><span class="n">argv</span><span class="p">)</span>
<span class="p">{</span>
    <span class="kt">char</span> <span class="n">buffer</span><span class="p">[</span><span class="mi">64</span><span class="p">];</span>
    <span class="kt">int</span> <span class="n">fd</span><span class="p">;</span>
    <span class="kt">ssize_t</span> <span class="n">size</span><span class="p">;</span>

    <span class="k">if</span> <span class="p">(</span><span class="n">chroot</span><span class="p">(</span><span class="n">argv</span><span class="p">[</span><span class="mi">1</span><span class="p">])</span> <span class="o">&lt;</span> <span class="mi">0</span><span class="p">)</span>
        <span class="k">return</span> <span class="mi">1</span><span class="p">;</span>

    <span class="n">fd</span> <span class="o">=</span> <span class="n">open</span><span class="p">(</span><span class="s">"/marker.txt"</span><span class="p">,</span> <span class="n">O_RDONLY</span><span class="p">);</span>
    <span class="n">size</span> <span class="o">=</span> <span class="n">read</span><span class="p">(</span><span class="n">fd</span><span class="p">,</span> <span class="n">buffer</span><span class="p">,</span> <span class="k">sizeof</span><span class="p">(</span><span class="n">buffer</span><span class="p">));</span>
    <span class="k">if</span> <span class="p">(</span><span class="n">size</span> <span class="o">&gt;</span> <span class="mi">0</span><span class="p">)</span>
        <span class="n">write</span><span class="p">(</span><span class="n">STDOUT_FILENO</span><span class="p">,</span> <span class="n">buffer</span><span class="p">,</span> <span class="n">size</span><span class="p">);</span>

    <span class="n">close</span><span class="p">(</span><span class="n">fd</span><span class="p">);</span>
    <span class="k">return</span> <span class="mi">0</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

<p>This binary simply calls <code class="language-plaintext highlighter-rouge">chroot</code> with an input argument. Assume <code class="language-plaintext highlighter-rouge">marker.txt</code>
is a private file on the host system and there is an angr analysis which
populates a report with the resolved <code class="language-plaintext highlighter-rouge">stdout</code> after symbolic execution. Then
that report would contain the content of <code class="language-plaintext highlighter-rouge">marker.txt</code>. An example of such an
analysis is as follows:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">test_chroot_read</span><span class="p">(</span><span class="n">binary</span><span class="p">:</span> <span class="nb">str</span><span class="p">,</span> <span class="n">expected</span><span class="p">:</span> <span class="nb">str</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="bp">None</span><span class="p">:</span>
    <span class="n">marker</span> <span class="o">=</span> <span class="sa">b</span><span class="s">"ANGR_CHROOT_HOST_READ_OK</span><span class="se">\n</span><span class="s">"</span>

    <span class="c1"># This directory and marker exist only on the analysis host. The Linux
</span>    <span class="c1"># guest is symbolically executed by angr; it is never run natively.
</span>    <span class="k">with</span> <span class="n">tempfile</span><span class="p">.</span><span class="n">TemporaryDirectory</span><span class="p">()</span> <span class="k">as</span> <span class="n">host_directory</span><span class="p">:</span>
        <span class="n">Path</span><span class="p">(</span><span class="n">host_directory</span><span class="p">,</span> <span class="s">"marker.txt"</span><span class="p">).</span><span class="n">write_bytes</span><span class="p">(</span><span class="n">marker</span><span class="p">)</span>

        <span class="n">project</span> <span class="o">=</span> <span class="n">angr</span><span class="p">.</span><span class="n">Project</span><span class="p">(</span><span class="n">binary</span><span class="p">,</span> <span class="n">auto_load_libs</span><span class="o">=</span><span class="bp">False</span><span class="p">)</span>
        <span class="n">state</span> <span class="o">=</span> <span class="n">project</span><span class="p">.</span><span class="n">factory</span><span class="p">.</span><span class="n">entry_state</span><span class="p">(</span>
            <span class="n">args</span><span class="o">=</span><span class="p">[</span><span class="n">binary</span><span class="p">,</span> <span class="n">Path</span><span class="p">(</span><span class="n">host_directory</span><span class="p">).</span><span class="n">resolve</span><span class="p">().</span><span class="n">as_posix</span><span class="p">()],</span>
            <span class="n">concrete_fs</span><span class="o">=</span><span class="bp">False</span><span class="p">,</span>
        <span class="p">)</span>
        <span class="n">simulation</span> <span class="o">=</span> <span class="n">project</span><span class="p">.</span><span class="n">factory</span><span class="p">.</span><span class="n">simgr</span><span class="p">(</span><span class="n">state</span><span class="p">)</span>
        <span class="n">simulation</span><span class="p">.</span><span class="n">run</span><span class="p">()</span>

        <span class="k">assert</span> <span class="ow">not</span> <span class="n">simulation</span><span class="p">.</span><span class="n">errored</span>
        <span class="k">assert</span> <span class="n">simulation</span><span class="p">.</span><span class="n">deadended</span>
        <span class="n">stdout</span> <span class="o">=</span> <span class="p">[</span><span class="n">state</span><span class="p">.</span><span class="n">posix</span><span class="p">.</span><span class="n">dumps</span><span class="p">(</span><span class="mi">1</span><span class="p">)</span> <span class="k">for</span> <span class="n">state</span> <span class="ow">in</span> <span class="n">simulation</span><span class="p">.</span><span class="n">deadended</span><span class="p">]</span>

    <span class="n">host_file_was_read</span> <span class="o">=</span> <span class="nb">any</span><span class="p">(</span><span class="n">marker</span> <span class="ow">in</span> <span class="n">output</span> <span class="k">for</span> <span class="n">output</span> <span class="ow">in</span> <span class="n">stdout</span><span class="p">)</span>
    <span class="k">print</span><span class="p">(</span><span class="sa">f</span><span class="s">"angr </span><span class="si">{</span><span class="n">angr</span><span class="p">.</span><span class="n">__version__</span><span class="si">}</span><span class="s">: stdout=</span><span class="si">{</span><span class="n">stdout</span><span class="si">!r}</span><span class="s">"</span><span class="p">)</span>

    <span class="k">if</span> <span class="n">expected</span> <span class="o">==</span> <span class="s">"affected"</span><span class="p">:</span>
        <span class="k">assert</span> <span class="n">host_file_was_read</span>
    <span class="k">elif</span> <span class="n">expected</span> <span class="o">==</span> <span class="s">"fixed"</span><span class="p">:</span>
        <span class="c1"># The simulated open may still succeed with symbolic file content.
</span>        <span class="k">assert</span> <span class="ow">not</span> <span class="n">host_file_was_read</span>
    <span class="k">else</span><span class="p">:</span>
        <span class="k">raise</span> <span class="nb">ValueError</span><span class="p">(</span><span class="s">"expected must be 'affected' or 'fixed'"</span><span class="p">)</span>
</code></pre></div></div>

<p>This issue was reported directly to the angr team via email, as recommended by
the <a href="https://github.com/angr/angr/blob/a5b6ee1/SECURITY.md">angr security policy</a>, on August 28, 2026. The versions of
angr affected: 9.2.12 through 9.3.3</p>

<h2 id="what-about-write">What About <code class="language-plaintext highlighter-rouge">write</code>?</h2>

<p>Upon finding this, my first question was if this could be turned into a write
operation.
Fortunately, host files are opened with <code class="language-plaintext highlighter-rouge">"rb"</code> permissions by default in the
<a href="https://github.com/angr/angr/blob/v9.3.3/angr/state_plugins/filesystem.py"><code class="language-plaintext highlighter-rouge">SimHostFilesystem</code> implementation</a>.
Guest operations only affect <a href="https://github.com/angr/angr/blob/v9.3.3/angr/storage/file.py">simulated storage</a>.</p>

<h2 id="resolution">Resolution</h2>

<p>The fix can be found in
<a href="https://github.com/angr/angr/pull/7017">angr/angr#7017: Implement chroot properly</a>. To summarize the angr team’s changes, they:</p>

<ul>
  <li>added <code class="language-plaintext highlighter-rouge">_root</code> to track, internally, the root directory of the simulated
filesystem</li>
  <li>changed <code class="language-plaintext highlighter-rouge">chroot</code> SimProcedure to use guest simulated fs relative to this root
rather than forcing the host mount</li>
  <li>added a warning to the user if a chroot request path resolves to an already
configured <code class="language-plaintext highlighter-rouge">SimHostFileSystem</code>, such as one created with <code class="language-plaintext highlighter-rouge">concrete_fs=True</code></li>
</ul>

<p>Thanks again to the angr team for the quick fix and turnaround!</p>

<h1 id="references">References</h1>

<p><a href="https://man7.org/linux/man-pages/man2/chroot.2.html">Linux manual: chroot(2)</a>: Native Linux behavior, permissions,
  and limitations.</p>

<p><a href="https://docs.angr.io/en/latest/extending-angr/simprocedures.html">angr documentation: Hooks and SimProcedures</a>: How Python
  procedures replace modeled guest behavior.</p>

<p><a href="https://github.com/angr/angr/blob/a5b6ee1/SECURITY.md">angr security policy at the fix commit</a>: The project’s
  security expectations and private reporting guidance.</p>

<p><a href="https://github.com/angr/angr/blob/v9.3.3/angr/simos/linux.py">angr 9.3.3: Linux state initialization and concrete_fs</a>: State
  construction and explicit host filesystem configuration.</p>

<p><a href="https://github.com/angr/angr/blob/v9.3.3/angr/procedures/posix/chroot.py">angr 9.3.3: Vulnerable chroot SimProcedure</a>: The procedure
  that creates a host-backed mount from the guest-supplied path.</p>

<p><a href="https://github.com/angr/angr/blob/v9.3.3/angr/state_plugins/filesystem.py">angr 9.3.3: Host-backed filesystem implementation</a>: Host-file
  loading and simulated filesystem caching.</p>

<p><a href="https://github.com/angr/angr/blob/v9.3.3/angr/storage/file.py">angr 9.3.3: Simulated file implementation</a>: File operations
  within simulated storage.</p>

<p><a href="https://github.com/angr/angr/pull/7017">angr/angr#7017: Implement chroot properly</a>: The fix and its
  regression tests.</p>]]></content><author><name>{&quot;email&quot;=&gt;&quot;js@mnsr.dev&quot;}</name><email>js@mnsr.dev</email></author><category term="reverse-engineering" /><category term="vulnerability-assessment" /><category term="angr" /><category term="linux" /><category term="static-analysis" /><category term="arbitrary-file-read" /><summary type="html"><![CDATA[A host-file read in angr's chroot SimProcedure, its impact on symbolic execution, and the fix to keep guest paths within the simulated filesystem.]]></summary></entry><entry><title type="html">Pickle Surprise: Pickle deserialization RCE in oauth2client</title><link href="https://mnsr.dev/2026/10/11/pickle-surprise/" rel="alternate" type="text/html" title="Pickle Surprise: Pickle deserialization RCE in oauth2client" /><published>2026-10-11T00:00:00+00:00</published><updated>2026-10-11T00:00:00+00:00</updated><id>https://mnsr.dev/2026/10/11/pickle-surprise</id><content type="html" xml:base="https://mnsr.dev/2026/10/11/pickle-surprise/"><![CDATA[<h1 id="introduction">Introduction</h1>

<p><code class="language-plaintext highlighter-rouge">pickle</code> is Python’s built-in serialization/deserialization library. It allows
users to capture Python state in a format that is transferable between
interpreters. It has long been a rich source of bugs for users who rely on it to
deserialize untrusted content. What makes pickle more dangerous than other
serialization/deserialization utilities, such as JSON, is an object’s ability to
override a special method called <code class="language-plaintext highlighter-rouge">__reduce__</code>, which embeds a callable into the
pickled object. Upon deserialization, the callable can execute arbitrary Python
code.</p>

<p><code class="language-plaintext highlighter-rouge">oauth2client</code> is a deprecated Google library that implements OAuth 2.0 and
integrates with Flask, a Python web framework. Its pickle deserialization is
reachable from attacker-controlled input. If an attacker can forge a session
cookie, that cookie can contain a payload that executes arbitrary code on the
host before authentication.</p>

<p>In this post, I explain the bug, demonstrate the exploit, and alert any
downstream users I may have missed.</p>

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

<p>There are two functions I will highlight to help explain the bug: the source of
the legitimate pickled OAuth 2.0 flow and the vulnerable destination of the
forged pickle payload.</p>

<ol>
  <li>
    <p>The <code class="language-plaintext highlighter-rouge">_make_flow()</code> function, which produces the serialized flow.</p>

    <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code> <span class="n">flow</span> <span class="o">=</span> <span class="n">client</span><span class="p">.</span><span class="n">OAuth2WebServerFlow</span><span class="p">(</span>
     <span class="n">client_id</span><span class="o">=</span><span class="bp">self</span><span class="p">.</span><span class="n">client_id</span><span class="p">,</span>
     <span class="n">client_secret</span><span class="o">=</span><span class="bp">self</span><span class="p">.</span><span class="n">client_secret</span><span class="p">,</span>
     <span class="n">scope</span><span class="o">=</span><span class="bp">self</span><span class="p">.</span><span class="n">scopes</span><span class="p">,</span>
     <span class="c1"># ...
</span> <span class="p">)</span>
 <span class="n">flow_key</span> <span class="o">=</span> <span class="n">_FLOW_KEY</span><span class="p">.</span><span class="nb">format</span><span class="p">(</span><span class="n">csrf_token</span><span class="p">)</span>
 <span class="n">session</span><span class="p">[</span><span class="n">flow_key</span><span class="p">]</span> <span class="o">=</span> <span class="n">pickle</span><span class="p">.</span><span class="n">dumps</span><span class="p">(</span><span class="n">flow</span><span class="p">)</span> <span class="c1"># ← pickled, stored in session
</span></code></pre></div>    </div>
  </li>
  <li>
    <p>The <code class="language-plaintext highlighter-rouge">_get_flow_for_token()</code> function, which deserializes the pickle.</p>

    <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code> <span class="n">flow_pickle</span> <span class="o">=</span> <span class="n">session</span><span class="p">.</span><span class="n">pop</span><span class="p">(</span>
     <span class="n">_FLOW_KEY</span><span class="p">.</span><span class="nb">format</span><span class="p">(</span><span class="n">csrf_token</span><span class="p">),</span> <span class="bp">None</span><span class="p">)</span>
 <span class="k">if</span> <span class="n">flow_pickle</span> <span class="ow">is</span> <span class="bp">None</span><span class="p">:</span>
     <span class="k">return</span> <span class="bp">None</span>
 <span class="k">else</span><span class="p">:</span>
     <span class="k">return</span> <span class="n">pickle</span><span class="p">.</span><span class="n">loads</span><span class="p">(</span><span class="n">flow_pickle</span><span class="p">)</span> <span class="c1"># ← deserialized from session
</span></code></pre></div>    </div>
  </li>
</ol>

<p>The cookie signature is validated before <code class="language-plaintext highlighter-rouge">_get_flow_for_token()</code> is called.</p>

<p>According to the project’s recommended default setup, the Flask app is
instantiated with the <code class="language-plaintext highlighter-rouge">oauth2client</code> library integration and initialized with a
<code class="language-plaintext highlighter-rouge">SECRET_KEY</code>. The key suggested by <a href="https://github.com/GoogleCloudPlatform/getting-started-python/blob/main/bookshelf/main.py">Google’s Bookshelf example</a> is
<code class="language-plaintext highlighter-rouge">'secret'</code>. The <code class="language-plaintext highlighter-rouge">oauth2client</code> docstring uses <code class="language-plaintext highlighter-rouge">'your-secret-key'</code>.
This secret key is used to sign a cookie using <a href="https://en.wikipedia.org/wiki/HMAC">HMAC</a>. The cookie
contains a pickled OAuth flow object.</p>

<p>The attacker must know or correctly guess the signing key. A wrong guess fails
Flask’s cookie signature check, so the forged session is not loaded. The
sequence of operations that leads to exploitation is as follows:</p>

<pre><code class="language-mermaid">sequenceDiagram
    participant Attacker
    participant App as Flask app

    Attacker-&gt;&gt;Attacker: Create new pickle from payload defining __reduce__
    Attacker-&gt;&gt;Attacker: Build session with new pickle and matching CSRF token
    Attacker-&gt;&gt;Attacker: Sign forged session cookie with candidate key
    Attacker-&gt;&gt;App: Send forged cookie with OAuth callback
    App-&gt;&gt;App: Validate cookie signature
    App-&gt;&gt;App: Validate callback state and matching CSRF token
    App-&gt;&gt;App: _get_flow_for_token() calls pickle.loads()
    Note over App: Pickle payload executes during deserialization
</code></pre>

<p>To validate the vulnerability, I created a simple proof of concept (PoC) that
writes a file to the target server’s filesystem. Here is an excerpt:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">RCEPayload</span><span class="p">:</span>
    <span class="k">def</span> <span class="nf">__reduce__</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
        <span class="k">return</span> <span class="p">(</span><span class="nb">eval</span><span class="p">,</span> <span class="p">(</span><span class="s">"open('poc_proof.txt', 'w').write('RCE achieved')"</span><span class="p">,))</span>
</code></pre></div></div>

<p>When <code class="language-plaintext highlighter-rouge">pickle.loads</code> is called, <code class="language-plaintext highlighter-rouge">eval</code> is run. When I ran the PoC in my test
environment with <code class="language-plaintext highlighter-rouge">oauth2client</code> 4.1.3, Flask 3.1.3, and Python 3.13.2, I saw
<code class="language-plaintext highlighter-rouge">poc_proof.txt</code> written to disk with the marker text.</p>

<h1 id="disclosure">Disclosure</h1>

<p>I disclosed this vulnerability and the exploit PoC to Google on February 22, 2026. The report was marked as <em>WONTFIX</em> due to the project’s archived state,
even though <a href="https://pypistats.org/packages/oauth2client">PyPI Stats</a> reported 24,545,418 downloads of
<code class="language-plaintext highlighter-rouge">oauth2client</code> in the last month when checked on October 11, 2026. These counts
measure downloads, not unique users or vulnerable deployments.
I am sharing these details without a fix strictly for educational purposes and
to help downstream users mitigate the issue.</p>

<p>If your app is using:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">from</span> <span class="nn">oauth2client.contrib.flask_util</span> <span class="kn">import</span> <span class="n">UserOAuth2</span>
<span class="n">oauth2</span> <span class="o">=</span> <span class="n">UserOAuth2</span><span class="p">(</span><span class="n">app</span><span class="p">)</span>
</code></pre></div></div>

<p>and you are using a weak secret key, then you may be vulnerable.</p>

<h2 id="brief-aside-cracking-hmac-signatures">Brief Aside: Cracking HMAC Signatures</h2>

<p>Assuming a weak secret, the HMAC-signed cookie can be brute-forced offline
without server interaction. You only need one cookie/signing key.
Hashcat 7.1.2 gives 2.53 billion guesses per second for mode 29100
(Flask Session Cookie) on my GPU (NVIDIA GeForce RTX 5080).</p>

<table>
  <thead>
    <tr>
      <th>Signing key choice</th>
      <th>Full search time</th>
      <th>Average time</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Random 8 digits</td>
      <td>&lt;1 second</td>
      <td>&lt;1 second</td>
    </tr>
    <tr>
      <td>Random 8 lowercase letters</td>
      <td>~83 seconds</td>
      <td>~41 seconds</td>
    </tr>
    <tr>
      <td>Random 10 lowercase letters</td>
      <td>~15.5 hours</td>
      <td>~7.7 hours</td>
    </tr>
    <tr>
      <td>Random 8 letters and digits</td>
      <td>~24 hours</td>
      <td>~12 hours</td>
    </tr>
    <tr>
      <td>Random 10 letters and digits</td>
      <td>~10.5 years</td>
      <td>~5.3 years</td>
    </tr>
    <tr>
      <td>Random 16 hexadecimal digits</td>
      <td>~231 years</td>
      <td>~115 years</td>
    </tr>
    <tr>
      <td>Random 32 hexadecimal digits</td>
      <td>~4 × 10²¹ years</td>
      <td>~2 × 10²¹ years</td>
    </tr>
  </tbody>
</table>

<h1 id="lessons-learned">Lessons Learned</h1>

<ul>
  <li>Probably avoid <code class="language-plaintext highlighter-rouge">pickle</code>. This OAuth flow could have been JSON.</li>
  <li>Use <code class="language-plaintext highlighter-rouge">google-auth</code> instead of the deprecated <code class="language-plaintext highlighter-rouge">oauth2client</code>.</li>
  <li>Don’t use a hard-coded, weak, or shared example <code class="language-plaintext highlighter-rouge">SECRET_KEY</code>.</li>
</ul>

<h1 id="references">References</h1>

<p><a href="https://github.com/googleapis/oauth2client/blob/v4.1.3/oauth2client/contrib/flask_util.py">oauth2client 4.1.3 Flask integration</a>: Source for
  <code class="language-plaintext highlighter-rouge">_make_flow()</code>, <code class="language-plaintext highlighter-rouge">_get_flow_for_token()</code>, and callback handling.</p>

<p><a href="https://github.com/GoogleCloudPlatform/getting-started-python/blob/main/bookshelf/main.py">Google’s Bookshelf example</a>: Example Flask application that sets
  <code class="language-plaintext highlighter-rouge">SECRET_KEY</code> to <code class="language-plaintext highlighter-rouge">'secret'</code>.</p>

<p><a href="https://docs.python.org/3.13/library/pickle.html">Python 3.13 pickle documentation</a>: Serialization behavior,
  <code class="language-plaintext highlighter-rouge">__reduce__</code>, and the warning against unpickling untrusted data.</p>

<p><a href="https://flask.palletsprojects.com/en/stable/config/#SECRET_KEY">Flask configuration documentation</a>: The role of <code class="language-plaintext highlighter-rouge">SECRET_KEY</code>
  in signing session cookies and guidance on choosing a secure value.</p>

<p><a href="https://en.wikipedia.org/wiki/HMAC">HMAC overview</a>: Background on keyed message authentication.</p>

<p><a href="https://pypistats.org/packages/oauth2client">oauth2client download statistics</a>: PyPI Stats reported
  24,545,418 downloads in the last month when checked on October 11, 2026.
  This is a download count, not a count of vulnerable deployments.</p>

<p><a href="https://hashcat.net/wiki/doku.php?id=example_hashes">Hashcat example hashes</a>: Supported hash modes and example
  hashes. The GPU throughput reported above is from my own measurement.</p>]]></content><author><name>{&quot;email&quot;=&gt;&quot;js@mnsr.dev&quot;}</name><email>js@mnsr.dev</email></author><category term="reverse-engineering" /><category term="vulnerability-assessment" /><category term="python" /><category term="pickle" /><category term="oauth2client" /><summary type="html"><![CDATA[A look at pickle deserialization in oauth2client's Flask integration, its reliance on session signing keys, and lessons for downstream users.]]></summary></entry><entry><title type="html">TLS Callback Evasion in angr</title><link href="https://mnsr.dev/2026/10/04/tls-callbacks/" rel="alternate" type="text/html" title="TLS Callback Evasion in angr" /><published>2026-10-04T00:00:00+00:00</published><updated>2026-10-04T00:00:00+00:00</updated><id>https://mnsr.dev/2026/10/04/tls-callbacks</id><content type="html" xml:base="https://mnsr.dev/2026/10/04/tls-callbacks/"><![CDATA[<h1 id="introduction">Introduction</h1>

<p>Lately, I have been on a quest to find and fix bugs in open-source software that
I use for my personal projects. In this post, I’ll highlight a bug that causes
the analysis tool <em>angr</em> to miss functionality on certain architectures and
could allow a malware author to evade static analysis.</p>

<h1 id="scenario">Scenario</h1>

<p>Assume you are given a Windows PE file and would like to determine whether it
is malicious.</p>

<p>You run your nifty angr-based malware analyzer on it, and your tool reports
that it is benign. You double-check the functions analyzed and note an entry
point and a few functions called from it.</p>

<p>You run the executable and observe something strange: it prints a message
whose strings appear in neither the entry-point function nor the functions
called from it. This message appears to hint at malicious behavior occurring
on the machine!</p>

<p>Contrary to what you might think, a lot happens before the entry point runs.</p>

<h1 id="background">Background</h1>

<h2 id="thread-local-storage-tls">Thread Local Storage (TLS)</h2>

<p>Microsoft’s PE format documentation defines TLS as follows:<sup id="fnref:pe-format" role="doc-noteref"><a href="#fn:pe-format" class="footnote" rel="footnote">1</a></sup></p>

<blockquote>
  <p>The .tls section provides direct PE and COFF support for static thread local
storage (TLS). TLS is a special storage class that Windows supports in which
a data object is not an automatic (stack) variable, yet is local to each
individual thread that runs the code. Thus, each thread can maintain a
different value for a variable declared by using TLS.</p>
</blockquote>

<p>TLS callbacks support TLS setup and teardown. During startup, they run in
sequence before the entry point.<sup id="fnref:phrack" role="doc-noteref"><a href="#fn:phrack" class="footnote" rel="footnote">2</a></sup> Typical uses include calling
constructors and destructors. This functionality can be abused to evade
analysis by detecting analyzers before the main entry point.<sup id="fnref:tls-dll" role="doc-noteref"><a href="#fn:tls-dll" class="footnote" rel="footnote">3</a></sup></p>

<h2 id="use-in-the-wild">Use in the wild</h2>

<p>I found a few examples of TLS callbacks being used for evasion:</p>

<ul>
  <li>The first public use of TLS callbacks to run code before the entry point was
W32.Shrug,<sup id="fnref:roy-g-biv" role="doc-noteref"><a href="#fn:roy-g-biv" class="footnote" rel="footnote">4</a></sup> credited to “roy g biv”.<sup id="fnref:chiton" role="doc-noteref"><a href="#fn:chiton" class="footnote" rel="footnote">5</a></sup> Chthon is a
descendant file infector.<sup id="fnref:chthon-source" role="doc-noteref"><a href="#fn:chthon-source" class="footnote" rel="footnote">6</a></sup></li>
  <li>An analysis of a Mustang Panda PlugX sample shows that its DLL loader finds
and calls the TLS callbacks of the target DLL.<sup id="fnref:plugx" role="doc-noteref"><a href="#fn:plugx" class="footnote" rel="footnote">7</a></sup></li>
  <li>Rhadamanthys uses static TLS callbacks.<sup id="fnref:rhadamanthys" role="doc-noteref"><a href="#fn:rhadamanthys" class="footnote" rel="footnote">8</a></sup></li>
</ul>

<p>Because attackers use TLS callbacks to hide functionality, these callbacks
are important targets for static analysis. But what if the analysis tool
fails to parse them correctly?</p>

<h1 id="the-tls-callback-bug-in-angr">The TLS Callback Bug in angr</h1>

<p><a href="https://github.com/angr">angr</a> is a symbolic execution analysis engine for
executables that supports a variety of custom analyzers.</p>

<p>Under the hood, angr uses <a href="https://github.com/angr/cle">CLE</a>, a library responsible for parsing and loading targets into a form
that angr can analyze. CLE parses PE files, including their TLS callbacks.</p>

<p>CLE’s TLS callback handling is implemented in <code class="language-plaintext highlighter-rouge">cle/backends/pe/pe.py</code>:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">_register_tls_callbacks</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">addr</span><span class="p">):</span>
    <span class="n">callbacks</span> <span class="o">=</span> <span class="p">[]</span>
    <span class="n">callback_rva</span> <span class="o">=</span> <span class="n">AT</span><span class="p">.</span><span class="n">from_lva</span><span class="p">(</span><span class="n">addr</span><span class="p">,</span> <span class="bp">self</span><span class="p">).</span><span class="n">to_rva</span><span class="p">()</span>
    <span class="n">callback</span> <span class="o">=</span> <span class="bp">self</span><span class="p">.</span><span class="n">_pe</span><span class="p">.</span><span class="n">get_dword_at_rva</span><span class="p">(</span><span class="n">callback_rva</span><span class="p">)</span>
    <span class="k">while</span> <span class="n">callback</span> <span class="o">!=</span> <span class="mi">0</span> <span class="ow">and</span> <span class="n">callback</span> <span class="ow">is</span> <span class="ow">not</span> <span class="bp">None</span><span class="p">:</span>
        <span class="n">callbacks</span><span class="p">.</span><span class="n">append</span><span class="p">(</span><span class="n">callback</span><span class="p">)</span>
        <span class="n">callback_rva</span> <span class="o">+=</span> <span class="mi">4</span>
        <span class="n">callback</span> <span class="o">=</span> <span class="bp">self</span><span class="p">.</span><span class="n">_pe</span><span class="p">.</span><span class="n">get_dword_at_rva</span><span class="p">(</span><span class="n">callback_rva</span><span class="p">)</span>
    <span class="k">return</span> <span class="n">callbacks</span>
</code></pre></div></div>

<p>Can you spot the bug?</p>

<p><strong>TLS callback addresses in 64-bit PE files are 8 bytes wide, not 4!</strong></p>

<p>For a 64-bit PE file with a TLS callback at VA <code class="language-plaintext highlighter-rouge">0x140001000</code>, the buggy parser
reports two TLS callback addresses: <code class="language-plaintext highlighter-rouge">0x40001000</code> and <code class="language-plaintext highlighter-rouge">0x1</code>, hiding the <em>actual</em>
address. This affects downstream angr analyses that use the enumerated TLS
callbacks.</p>

<p>Microsoft’s PE format documentation notes the architecture-dependent
sizes:<sup id="fnref:pe-format:1" role="doc-noteref"><a href="#fn:pe-format" class="footnote" rel="footnote">1</a></sup></p>

<blockquote>
  <p>Note: References to 32-bit offsets and index multipliers of 4 apply to systems
with 32-bit architectures. In a system based on 64-bit architectures, adjust
them as necessary.</p>
</blockquote>

<p>The issue was reported and tracked in
<a href="https://github.com/angr/cle/issues/648">CLE issue #648</a>. This bug dates back 
to at least CLE version 5.6.10.5. It affects angr/CLE versions up to and 
including 9.2.196. The downstream impact depends on how an analysis uses the 
parsed TLS callbacks.</p>

<h2 id="the-fix">The Fix</h2>

<p>I added a check of the target’s architecture to select the appropriate pointer
size and read function. The merged implementation is shown below:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">_register_tls_callbacks</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">addr</span><span class="p">):</span>
    <span class="s">"""
    TLS callbacks are stored as an array of virtual addresses to functions.
    The last entry is empty (NULL), which indicates the end of the table
    """</span>
    <span class="n">callbacks</span> <span class="o">=</span> <span class="p">[]</span>

    <span class="n">callback_rva</span> <span class="o">=</span> <span class="n">AT</span><span class="p">.</span><span class="n">from_lva</span><span class="p">(</span><span class="n">addr</span><span class="p">,</span> <span class="bp">self</span><span class="p">).</span><span class="n">to_rva</span><span class="p">()</span>
    <span class="n">is_64bit</span> <span class="o">=</span> <span class="bp">self</span><span class="p">.</span><span class="n">arch</span><span class="p">.</span><span class="n">bits</span> <span class="o">==</span> <span class="mi">64</span>
    <span class="n">ptr_size</span> <span class="o">=</span> <span class="mi">8</span> <span class="k">if</span> <span class="n">is_64bit</span> <span class="k">else</span> <span class="mi">4</span>
    <span class="n">get_ptr</span> <span class="o">=</span> <span class="p">(</span>
        <span class="bp">self</span><span class="p">.</span><span class="n">_pe</span><span class="p">.</span><span class="n">get_qword_at_rva</span> <span class="k">if</span> <span class="n">is_64bit</span> <span class="k">else</span> <span class="bp">self</span><span class="p">.</span><span class="n">_pe</span><span class="p">.</span><span class="n">get_dword_at_rva</span>
    <span class="p">)</span>

    <span class="n">callback</span> <span class="o">=</span> <span class="n">get_ptr</span><span class="p">(</span><span class="n">callback_rva</span><span class="p">)</span>
    <span class="k">while</span> <span class="n">callback</span> <span class="o">!=</span> <span class="mi">0</span> <span class="ow">and</span> <span class="n">callback</span> <span class="ow">is</span> <span class="ow">not</span> <span class="bp">None</span><span class="p">:</span>
        <span class="n">callbacks</span><span class="p">.</span><span class="n">append</span><span class="p">(</span><span class="n">callback</span><span class="p">)</span>
        <span class="n">callback_rva</span> <span class="o">+=</span> <span class="n">ptr_size</span>
        <span class="n">callback</span> <span class="o">=</span> <span class="n">get_ptr</span><span class="p">(</span><span class="n">callback_rva</span><span class="p">)</span>

    <span class="k">return</span> <span class="n">callbacks</span>
</code></pre></div></div>

<h2 id="validation">Validation</h2>

<p>To validate this fix, I added a unit test that checks a crafted executable with
a TLS callback address above 4 GiB, so the upper 32 bits are nonzero:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">test_tls_x64</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
    <span class="n">exe</span> <span class="o">=</span> <span class="n">os</span><span class="p">.</span><span class="n">path</span><span class="p">.</span><span class="n">join</span><span class="p">(</span><span class="n">TEST_BASE</span><span class="p">,</span> <span class="s">"tests"</span><span class="p">,</span> <span class="s">"x86_64"</span><span class="p">,</span> <span class="s">"windows"</span><span class="p">,</span> <span class="s">"TLS.exe"</span><span class="p">)</span>
    <span class="n">ld</span> <span class="o">=</span> <span class="n">cle</span><span class="p">.</span><span class="n">Loader</span><span class="p">(</span><span class="n">exe</span><span class="p">,</span> <span class="n">auto_load_libs</span><span class="o">=</span><span class="bp">False</span><span class="p">)</span>
    <span class="n">tls</span> <span class="o">=</span> <span class="n">ld</span><span class="p">.</span><span class="n">tls</span><span class="p">.</span><span class="n">new_thread</span><span class="p">()</span>

    <span class="k">assert</span> <span class="n">ld</span><span class="p">.</span><span class="n">main_object</span><span class="p">.</span><span class="n">tls_used</span>
    <span class="k">assert</span> <span class="n">ld</span><span class="p">.</span><span class="n">main_object</span><span class="p">.</span><span class="n">tls_callbacks</span> <span class="o">==</span> <span class="p">[</span><span class="mh">0x140001000</span><span class="p">]</span>

    <span class="k">assert</span> <span class="n">tls</span> <span class="ow">is</span> <span class="ow">not</span> <span class="bp">None</span>
    <span class="k">assert</span> <span class="nb">len</span><span class="p">(</span><span class="n">ld</span><span class="p">.</span><span class="n">tls</span><span class="p">.</span><span class="n">modules</span><span class="p">)</span> <span class="o">==</span> <span class="mi">1</span>
</code></pre></div></div>

<p>This checks that CLE recovers the full 64-bit callback address rather than
misreading it as two 32-bit entries.</p>

<p>You can examine the fix in <a href="https://github.com/angr/cle/pull/649">PR #649</a>.
I’d like to give a shout-out to the angr team for the prompt review and merge!</p>

<h1 id="lessons-learned">Lessons Learned</h1>

<ul>
  <li>Always check for TLS callbacks.</li>
  <li>Ensure your debugger is configured to break on TLS callbacks.</li>
  <li>MSVC doesn’t always use a <code class="language-plaintext highlighter-rouge">.tls</code> section for TLS callbacks. Use the TLS
data-directory entry to locate the TLS directory.</li>
</ul>

<h1 id="references">References</h1>

<div class="footnotes" role="doc-endnotes">
  <ol>
    <li id="fn:pe-format" role="doc-endnote">
      <p>Microsoft Learn, <a href="https://learn.microsoft.com/en-us/windows/win32/debug/pe-format#the-tls-section">PE Format: The .tls Section</a> and
<a href="https://learn.microsoft.com/en-us/windows/win32/debug/pe-format#tls-callback-functions">TLS Callback Functions</a>. <a href="#fnref:pe-format" class="reversefootnote" role="doc-backlink">&#8617;</a> <a href="#fnref:pe-format:1" class="reversefootnote" role="doc-backlink">&#8617;<sup>2</sup></a></p>
    </li>
    <li id="fn:phrack" role="doc-endnote">
      <p>Phrack #71,
<a href="https://phrack.org/issues/71/">A VX Adventure in Build Systems and Oldschool Techniques</a>.
Discusses TLS directory injection, callbacks before the main entry point,
and pitfalls such as calling APIs that spawn threads inside a TLS callback. <a href="#fnref:phrack" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:tls-dll" role="doc-endnote">
      <p>TLS callbacks also work inside DLLs and run before <code class="language-plaintext highlighter-rouge">DllMain</code> during
process attachement. See
<a href="https://isc.sans.edu/diary/32580">SANS Internet Storm Center’s diary entry 32580</a>. <a href="#fnref:tls-dll" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:roy-g-biv" role="doc-endnote">
      <p><a href="https://spthvx.github.io/ezines/eof2/interviews/intwrgb.html">Interview with roy g biv</a>. Discusses W32.Shrug and the
early use of TLS callbacks. <a href="#fnref:roy-g-biv" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:chiton" role="doc-endnote">
      <p>Peter Ferrie, <a href="https://pferrie.epizy.com/papers/chiton.pdf?i=1">Chiton paper (PDF)</a>. <a href="#fnref:chiton" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:chthon-source" role="doc-endnote">
      <p><a href="https://git.entan.gl/empathicqubit/MalwareSourceCode/src/commit/25ee5f4dc00857ea86204761e516c43a0832c869/Win32/Infector/Win32.Chthon.asm">Win32.Chthon.asm source</a>. <a href="#fnref:chthon-source" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:plugx" role="doc-endnote">
      <p>Kien Manowar,
<a href="https://kienmanowar.wordpress.com/2022/12/27/diving-into-a-plugx-sample-of-mustang-panda-group/">Diving into a PlugX sample of Mustang Panda group</a>. <a href="#fnref:plugx" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:rhadamanthys" role="doc-endnote">
      <p>Cybereason, <a href="https://www.cybereason.com/blog/rhadamanthys-stealer-europe">Rhadamanthys stealer analysis</a>. <a href="#fnref:rhadamanthys" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
  </ol>
</div>]]></content><author><name>{&quot;email&quot;=&gt;&quot;js@mnsr.dev&quot;}</name><email>js@mnsr.dev</email></author><category term="reverse-engineering" /><category term="malware-analysis" /><category term="angr" /><category term="cle" /><category term="windows" /><category term="pe" /><category term="tls-callbacks" /><category term="static-analysis" /><summary type="html"><![CDATA[A pointer-width bug in angr's CLE loader misread 64-bit TLS callback addresses. Here's the root cause, fix, and regression test.]]></summary></entry></feed>