[5월호] Kernel Use-After-Free
1. 들어가며
5월호의 주제는 Use-After-Free(UAF) 취약점이다. 해당 취약점을 이용한 CTF 문제 풀이 과정을 다룬다. UAF 취약점은 메모리 관리의 부주의로 인해 발생하는 보안 취약점 중 하나로, 해커가 악용할 경우 시스템 권한을 탈취당하거나 악성코드가 실행될 수 있다. 이러한 취약점은 특히 커널과 같은 중요한 시스템 컴포넌트에서 발생할 때 매우 치명적일 수 있다.
해당 글은 CTF 문제를 풀 때 필요한 전반적인 커널 지식과 문제 해결을 위한 접근 방법과 구체적 단계들을 설명한다. 이를 통해 UAF 취약점이 어떻게 악용될 수 있는지, 그리고 이를 방어하기 위한 기법들이 무엇이 될 수 있는지 생각하고 이해할 수 있다.
2. kernel 지식
2.1. Memory Corruption
- 메모리를 참조하는 부분에서 오류가 발생하는 취약점이다.
- 공격자는 메모리가 corruption 되는 부분을 이용하여 공격자가 원하는 명령어를 참조하도록 수행이 가능하다.
UAF는 Heap 영역에서 일어나는 취약점이며, Heap은 메모리 영역 중 하나이기 때문에 메모리 구조와 Heap에 대해서 알아야 한다.
2.2. 메모리 구조
Code 영역: 프로그램의 (컴파일된 기계어) 코드가 올라가는 곳이다.
Data 영역: global variable과 static bariable이 할당되는 곳이다.
Stack 영역: local variable과 parameter가 저장되는 곳이다.
함수가 시작되면 해당 함수의 local variable이 stack에 저장되었다가 함수가 종료되면 해당 영역을 해제시킨다.
Heap 영역: 빈 공간으로, 필요에 따라 동적으로 메모리를 할당하기도 하고 해제하기도 한다.
Heap 영역은 Stack에서 관리하는 이외의 데이터 형태를 사용하며, 컴파일 시 크기를 알지 못하다가 프로그램이 실행되면 크기가 결정되는 동적 할당 메모리를 받는 데 사용된다.
2.3. Heap
데이터에서 최댓값과 최솟값을 빠르게 찾기 위해 고안된 완전 이진트리이다.
https://velog.io/@jsbryan/%ED%9E%99-%EC%B5%9C%EC%86%8C-%ED%9E%99-%EC%B5%9C%EB%8C%80-%ED%9E%99
부모 노드의 인덱스는 1, 왼쪽 자식 노드부터 2, 3 순서이다.
Heap 구조는 위에서 말한대로 컴퓨터 안의 메모리 구조 중 사용자가 임의로 사용하는 메모리 공간으로, 대표적으로 malloc()
함수를 이용해 선언해주고, free()
함수를 통해 해제를 해준다.
2.4. First-Fit Algorithm
UAF에 대해 알기 전 또 알아야 할 것이 바로 First-Fit 알고리즘이다.
First-Fit 알고리즘은 메모리 할당 알고리즘들 중 하나로, 사용 가능한 공간들 중에서 첫번째로 찾아낸 공간을 할당하는 방식이다.
Linux OS는 해당 알고리즘을 채택하고 있다.
이외에도 Best-Fit, Worst-Fit 알고리즘도 존재한다.
1 | char*a = malloc(20); // 0xe4b010 |
위의 코드는 예시 코드다.
코드를 보면, 각각 20 바이트씩 Heap 영역에 4개의 영역을 할당해 주었고, free를 해준 후 다시 4개의 영역을 할당 받고 있다.
방식은 LIFO(Last In First Out)이다. 과정을 조금 더 설명해보겠다.
1 | <free> |
3. UAF 취약점
Heap 영역에서 할당된 (malloc) 공간을 free로 영역을 해제하고, 메모리를 다시 할당 시 같은 공간을 재사용 하면서 생기는 취약점이다.
1 |
|
예시 코드를 돌려보면 아래와 같은 결과가 나온다.
heap1
을 할당하고free
를 해주고, 다른 변수(heap2
)에 같은 크기로 할당하면 다시 그 공간을 재사용하게 된다.free
를 해주어도 그 전에 사용했던 공간에 할당이 가능하다.- 사이즈를 다르게 입력하면 다른 공간에 할당한다.
4. UAF 문제 분석 및 풀이
CISCN CTF 2017에 출제된 babydriver
문제로 UAF가 어떻게 발생하는지 알아보자.
4.1. 디바이스 드라이버
먼저 본격적으로 코드 분석에 앞서, 사전지식으로 알아야 할 게 있다. 바로 디바이스 드라이버라는 것이다.
1. 디바이스 드라이버
키보드, 하드디스크 같은 디바이스들을 컨트롤하는 것이 바로 디바이스 드라이버이다.
디바이스 드라이버란 실제 장치 부분을 추상화시켜 사용자 프로그램이 정형화된 인터페이스를 통해 디바이스를 접근할 수 있도록 하는 프로그램이다. 리눅스에서는 모든 것을 파일로 간주하는데, 이러한 디바이스 드라이버도 파일로 관리된다.
/dev/
아래에 들어있는 파일들이 바로 디바이스 드라이버 인터페이스고, 하드웨어와는 독립적으로 응용 프로그램이 파일 open
, read
같은 함수로 접근이 가능하다.
https://jeongzero.oopy.io/c5c9c223-d17f-4bbc-b054-4d9fa1faffd1
Real Device가 실제 물리적인 하드웨어이고, Device Driver를 통해 실제 디바이스를 컨트롤하게 된다. 그러나 장치 별로 제공되는 디바이스 드라이버가 다르기 때문에 리눅스에서는 VFS라는 파일 시스템 기능을 지원한다.
또한 모든 디바이스 드라이버는 /dev
하위에 파일로 취급되고, 위에서 말한 open
, read
, write
등의 연산을 통해 디바이스를 컨트롤할 수 있다. 이러한 디바이스 파일에게는 고유한 번호와 이름이 할당되어 있다.
2. 디바이스 드라이버 종류
https://jeongzero.oopy.io/c5c9c223-d17f-4bbc-b054-4d9fa1faffd1
디바이스 드라이버는 크게 3가지로 나뉜다.
종류 | 설명 | 등록 함수 | 특징 |
---|---|---|---|
Char device driver | device를 파일처럼 접근하여 직접 read/write 수행 | register_chrdev() |
버퍼 캐시 사용 X, 자료의 순차성을 지님, 터미널, 키보드 사운드 카드, 프린터 등 |
block device | disk와 같은 filesystem을 기반으로 block 단위로 data를 read/write | register_blkdev() |
블록 단위의 입출력 가능, 버퍼 캐시에 의한 내부 장치 표현, 파일 시스템에 의해 마운트되어 관리됨, 하드디스크 등 |
network device driver | network의 physical layer와 frame 단위의 data 송수신 | register_netdev() |
네트워크 스택과 네트워크 하드웨어 사이에 위치해 데이터 송수신 담당, 이더넷, 네트워크 인터페이스 카드 등 |
4.2. setting
babydriver 문제 파일에는 bzImage
, rootfs.cpio
, boot.sh
파일들이 존재한다.
문제 분석을 위해 다음과 같이 환경을 세팅해주었다.
4.2.1. vmlinux 생성
bzImage
를 이용해 vmlinux
를 생성한다.extract-vmlinux
스크립트를 이용하면 vmlinux
를 쉽게 추출할 수 있다.
extract-vmlinux 스크립트는 아래 사이트는 참고하였다.
https://raw.githubusercontent.com/torvalds/linux/master/scripts/extract-vmlinux
1 | ./extract-vmlinux bzImage > ./vmlinux |
4.2.2. rootfs.cpio 압축 해제
rootfs.cpio
는 압축된 파일 시스템이므로 압축을 풀어서 파일 시스템을 추출한다.
rootfs.gz
로 확장자를 변경한 뒤 gzip -d rootfs.gz
로 압축을 풀고, cpio -id -v < rootfs.cpio
명령어를 통해 파일 시스템을 추출하였다.
lib/modules/4.4.72
에 분석할 커널 모듈인 babydriver.ko
가 존재한다.
4.2.3. qemu 설치
boot.sh
를 실행시키기 위해 qemu
를 설치한다.
분석을 진행한 환경이 x86-64
이므로 아래의 명령어를 이용해 qemu
를 설치해주었다.
1 | apt-get install qemu-system-x86 |
4.3. 문제 풀이 및 분석
4.3.1. boot.sh 확인
우선 boot.sh
를 살펴보자.
1 |
|
해당 코드를 살펴보면, smep
이 걸려있는 걸 알 수 있다. 이는 user 공간에서 kernel 영역의 함수를 실행하지 못하게 하는 보호 기법이다.
kaslr
은 걸려있지 않다는 것 또한 확인할 수 있었다.
4.3.2. 문제 풀이 아이디어
boot.sh
로 부팅을 하면 ctf 권한을 가진 상태로 부팅이 된다.
rootfs/etc
폴더 아래에는 group
, init.d
, passwd
가 존재한다.
그 중 passwd
파일을 살펴보면 root 권한이 존재하는 것을 확인할 수 있었다.
1 | root:x:0:0:root:/root:/bin/sh |
ctf 권한을 root 권한으로 LPE(권한 상승) 하는 것이 문제의 목적이다.
이제 커널 모듈을 분석해보자.
4.3.3. babydriver.ko 분석
1 | int __cdecl babydriver_init() |
babydriver 커널 모듈을 초기화하는 과정으로, 커널에서 모듈을 불러올 때 가장 먼저 실행되는 함수이다.
alloc_chrdev_region()
함수로 Char Device의 번호를 할당한다.
1 | void __cdecl babydriver_exit() |
모듈이 종료될 때 호출되는 함수이다.
init()
함수에서 할당한 디바이스를 제거하는 함수다.
1 | int __fastcall babyopen(inode *inode, file *filp) |
open
을 하면, 커널에서 동적 할당을 통해 64바이트 만큼 babydev_struct
구조체의 device_buf
에 힙을 할당한다.
그리고 babydev_struct
구조체의 device_buf_len
필드에 크기 64를 buf_len
필드에 저장한다.
kmem_cache_alloc_trace()
함수가 바로 동적 할당 함수이다.
babydev 구조체는 아래와 같다.
1 | struct babydevice_t |
1 | ssize_t __fastcall babyread(file *filp, char *buffer, size_t length, loff_t *offset) |
babyread()
함수는 device_buf
필드가 null
이면 -1
을 반환한다.length(v4)
와 buf_len
을 비교해 buf_len
이 더 크면 copy_to_user()
함수를 호출하여 kernel 영역의 테이블을 user 공간인 buffer
로 복사한다.
1 | ssize_t __fastcall babywrite(file *filp, const char *buffer, size_t length, loff_t *offset) |
read
함수와 마찬가지로 device_buf
가 null
이면 -1
을 반환한다.device_buf_len
이 v4
보다 크면 copy_from_user()
함수를 호출해 user 영역인 buffer 테이블을 kernel 영역으로 복사한다.
1 | __int64 __fastcall babyioctl(file *filp, unsigned int command, unsigned __int64 arg) |
2번째 인자 command
가 0x10001(65537)
이면 device_buf
를 해제(free)하고 3번째 인자(arg(v4)
)만큼 힙을 재할당하여 초기화한다. 이때 힙 영역은 open
시에 할당 받은 힙 영역이다. buf_len
도 마찬가지다.
1 | int __fastcall babyrelease(inode *inode, file *filp) |
release()
함수는 close()
함수가 호출될 때 호출된다. device_buf
를 해제하지만 해제하면서 초기화 작업을 하지는 않는다.
해제 후 초기화를 안 하기 때문에 UAF가 발생하게 된다.
즉, Dangling Pointer(해제된 메모리 영역을 가리키는 포인터)가 된다.
4.3.4. struct cred 이용
초기 open()
→ ioctl()
시에 cred 구조체 사이즈인 168 바이트를 인자로 전달하여 해당 크기의 청크를 할당받게 한다. 그리고 close()를 하면 168 바이트 힙 청크를 가리키는 전역변수에 들어있는 값은 Dangling Pointer가 된다.
그리고 fork()
함수를 호출하여 fork()
내부에서 힙 할당을 하는 로직을 이용하여 free된 청크(현재 dangling pointer 주소인 청크)를 재할당받는다.
fork에서 힙을 할당받는 로직은 바로 부모의 pcb 정보 중 cred 구조체 필드들을 복사하기 위함이다.
- danling pointer 만들기
- fork 통해 dangling pointer를 할당받게 하고 (이는 cred 구조체 영역)
- 저 영역은
babywrite()
를 통해 user 공간의 값을 커널영역으로 복사할 수 있음 - 즉, fork에서 할당받은 cred 구조체 영역을 수정 가능하단 소리 → uid부분을 0으로 만들 수 있음
- 그 다음 user 영역에서 시스템 함수를 그냥 실행시키면 LPE가 일어남. (단, 이는 fork한 자식 프로세스 내에서 실행해야 함)
4.3.5. 함수 호출
1 |
|
권한 상승을 위해 fork()
함수를 호출해야 하는데 이를 호출하면 시스템 콜 clone()
함수를 호출하여 사용 처리가 된다.
clone()
내부에서는 do_fork()
함수를 호출한다.
1 | long _do_fork(unsigned long clone_flags, |
이는 do_fork()
함수이다. 이 함수에서는 copy_process()
함수를 호출한다.
1 |
|
copy_process()
함수이다.
task_struct
를 인자로 copy_creds()
함수를 호출한다.
1 | int copy_creds(struct task_struct *p, unsigned long clone_flags) |
copy_creds()
함수이다.
이 함수 내부에서는 prepare_creds()
함수를 호출한다.
1 | struct cred *prepare_creds(void) |
prepare_creds()
함수 내부에서는 kmem_Cache_alloc()
함수를 호출한다. 위에서부터 함수 내부로 계속해서 들어왔더니 아래의 순서대로 호출된다.
fork()
→ clone()
→ do_fork()
→ copy_process()
→ copy_creds()
→ prepare_creds()
→ kmem_cache_alloc()
`
4.3.6. Exploit
fork()
함수를 호출하면 기존 cred
를 복사할 공간이 필요하다.
cred
구조체의 크기는 168 바이트이다.
open()
함수를 2번 호출하여 힙을 할당babyioctl()
함수를 호출하여 168 바이트 만큼 재할당close()
함수를 호출하여 힙을 해제(free
)fork()
함수 호출write()
함수를 호출하여struct cred
값 변경
3번까지 진행하면 전역변수의 값은 Dangling Pointer가 된다.
그 후 4번을 진행하면 해제 한 힙 영역에 struct cred
가 할당되고, Dangling Pointer는 할당된 168 바이트 힙 영역을 가리키게 된다.
5번이 진행되면 write()
함수를 호출하여 struct cred
를 0으로 수정 후 system(”/bin/sh”)
함수를 호출하면 root 권한의 쉘을 획득하게 된다.
4.3.7. Exploit code
exploit.c
는 다음과 같다.
1 |
|
익스의 성공 여부를 확인하기 위해 boot.sh
파일을 아래와 같이 수정해주었다.
1 |
|
./boot.sh를
실행한 뒤, ./exploit
을 실행하면 LPE가 성공한 것을 확인할 수 있다.
5. 마무리하며
이번 CTF 문제를 해결하면서 커널에서 발생하는 Use-After-Free (UAF) 취약점의 심각성과 이를 악용하는 방법에 대해 이해할 수 있었다. 문제 해결 과정에서 메모리 할당 및 해제 관리가 얼마나 중요한지, 그리고 작은 부주의가 어떻게 심각한 보안 위협으로 이어질 수 있는지를 확인할 수 있었다.
앞으로 커널 보안에 대한 깊이 있는 이해를 위해 커널 구조와 메모리 관리, 동기화 메커니즘 등을 심도 있게 공부하며 리눅스 커널 소스 코드를 분석해보는 것도 좋은 방법이 될 수 있다.
6. 참고문헌
[1] [Heap Exploit] UAF(Use After Free) 기법 이론설명. (N.d.).
https://dokhakdubini.tistory.com/35
[2] Use After Free (UAF) 너 뭐임?. (N.d.).
https://seclab614.tistory.com/3
[3] Kernel (6) - CISCN 2017 babydriver write-up (Linux kernel UAF). (N.d.).
https://sunrinjuntae.tistory.com/130
[4] 까망눈 연구소. (N.d.).
https://jeongzero.oopy.io/
[5] [Linux] QEMU 가상머신에 OS 설치하기. (N.d.).
https://stackframe.tistory.com/28
[6] [CISCN CTF 2017] babydriver (kernel exploit, kUAF). (N.d.).
https://koharinn.tistory.com/561#google_vignette
[7] [Linux Kernel] CISCN 2017 babydriver. (N.d.).
https://applemasterz17.tistory.com/229
[8] [CISCN CTF 2017] babydriver. (N.d.).
http://ipwn.kr/index.php/2020/04/05/ciscn-ctf-2017-babydriver/