Kernel Exploit에 필요한 지식의 대부분은 LK01에서 이미 설명했으므로, 이제부터는 커널 공간 특유의 공격 기법이나 Linux 커널에 탑재된 기능에 대한 공격 등 세부적인 내용을 다룹니다.
LK02(Angus)에서는 커널 공간에서의 NULL Pointer Dereference 악용 방법에 대해 배웁니다. 먼저 연습 문제 LK02 파일을 다운로드해 주세요.
이번 장에서 다루는 취약점에 대해
LK02의 qemu 실행 옵션을 보면 알 수 있듯이, 이번 공격 대상 머신에서는 SMAP가 비활성화되어 있습니다. 이번 장에서 다루는 NULL Pointer Dereference는 SMAP가 비활성화되어 있지 않으면 exploit 할 수 없습니다.
또한, 이번 커널을 부팅하고 다음 명령어를 입력해 보세요.
1 | $ cat /proc/sys/vm/mmap_min_addr |
mmap_min_addr는 Linux 커널 변수로, 이름처럼 사용자 랜드에서 mmap으로 매핑할 수 있는 가장 작은 주소를 제한합니다. 기본적으로는 0이 아닌 값이지만, 이번 공격 대상에서는 0으로 설정되어 있는 점에 주의하세요. 이 변수는 이번에 다루는 NULL Pointer Dereference에 대한 완화책(mitigation)으로서 Linux 커널 버전 2.6.23부터 도입되었습니다.
이처럼 이번 장의 내용은 SMAP나 mmap의 mitigation을 우회할 수 있다는 전제하에 공격하는 것이므로, 최신 Linux에서 사용할 수 있는 기법에만 관심이 있는 분은 건너뛰어도 좋습니다.
취약점 확인
먼저 LK02의 소스 코드를 읽어봅시다. 소스 코드는 src/angus.c에 작성되어 있습니다.
ioctl
LK01과 크게 다른 점은 read, write가 구현되어 있지 않은 대신 ioctl이라는 시스템 호출 핸들러가 기술되어 있다는 점입니다. 파일 디스크립터에 대해 ioctl을 호출함으로써, 해당하는 커널이나 드라이버의 ioctl 핸들러가 호출됩니다.
ioctl은 파일 디스크립터 이외에 request, argp라는 두 개의 인자를 받습니다.
1 | ioctl(fd, request, argp); |
request에는 그 디바이스를 조작하는 요청 코드를 전달합니다. 요청 코드는 드라이버가 각자 정의한 값이므로, 소스 코드를 읽고 어떤 요청을 보낼 수 있는지를 파악해야 합니다.
argp는 그 디바이스에 전달할 데이터를 넣습니다. 일반적으로 여기에는 사용자 공간 데이터의 포인터가 들어가며, 커널 모듈 측에서 copy_from_user를 사용해 요청 내용을 읽어옵니다.
이번 커널 모듈에서도 request_t라는 구조체를 사용자 공간에서 전달하는 사양으로 되어 있습니다.
1 | typedef struct { |
또한 request 코드에 따라 처리를 분기하는 모습도 확인할 수 있습니다.
1 | switch (cmd) { |
ioctl 핸들러의 구현을 읽기 전에, 이번에 사용되고 있는 private_data에 대해 설명합니다.
file 구조체
사용자 공간에서 드라이버 등을 조작할 때 파일 디스크립터를 사용하지만, 커널 측에서는 file 구조체로 받습니다.
file 구조체에는 예를 들면 lseek로 설정된 커서의 위치[1] 등 파일 고유의 정보가 있지만, 커널 모듈이 자유롭게 사용해도 되는 멤버로서 private_data가 있습니다.
1 | struct file { |
private_data에는 어떤 데이터를 두어도 상관없지만, 데이터의 할당이나 해제는 당연히 모듈 측이 올바르게 구현해야 합니다. 이번 드라이버에서는 XorCipher라는 독자적인 구조체를 저장하기 위해 사용하고 있습니다.
1 | static int module_open(struct inode *inode, struct file *filp) { |

LK01-4 (Holstein v4)에서도 여기에 데이터를 저장하면 레이스 컨디션이 발생하지 않았지.
프로그램 개요
이 프로그램은 데이터를 XOR 암호로 암호화·복호화할 수 있는 커널 모듈입니다.
이 모듈은 ioctl로 조작할 수 있으며, 요청 코드는 다음 5가지가 준비되어 있습니다.
1 |
먼저 CMD_INIT으로 호출하면 private_data에 XorCipher 구조체가 저장됩니다.
1 | typedef struct { |
XorCipher 구조체는 키 key와 그 길이 keylen, 데이터 data와 그 길이 datalen을 가집니다.
다음으로 CMD_SETKEY로 호출하면, argp로 전달된 데이터를 키로서 복사합니다. 이미 키가 등록되어 있는 경우에는 먼저 기존 키를 해제합니다.
1 | case CMD_SETKEY: |
마찬가지로 CMD_SETDATA에서는 사용자 공간에서 암호화·복호화하고 싶은 데이터를 복사합니다.
1 | case CMD_SETDATA: |
암호화·복호화된 데이터는 CMD_GETDATA를 사용해 사용자 공간으로 복사할 수 있습니다.
1 | case CMD_GETDATA: |
마지막으로 CMD_ENCRYPT와 CMD_DECRYPT에서는 xor 함수를 호출합니다. (XOR 암호이므로 암호화도 복호화도 같은 알고리즘입니다.) 데이터나 키가 설정되어 있지 않은 경우는 에러가 됩니다.
1 | long xor(XorCipher *ctx) { |
취약점 조사
이번 드라이버에는 버퍼 오버플로우나 Use-after-Free와 같은 취약점은 없습니다. 눈치채기 어려울 수도 있지만, 잘 읽어보면 암호화·복호화 처리에 NULL Pointer Dereference가 존재합니다.
먼저 ioctl의 처음에 private_data의 포인터를 XorCipher로서 가져옵니다.
1 | ctx = (XorCipher*)filp->private_data; |
CMD_SETKEY 등에서는 private_data가 초기화되었는지 검사하고 있습니다.
1 | if (!ctx) return -EINVAL; |
하지만, CMD_GETDATA, CMD_ENCRYPT, CMD_DECRYPT에는 이 검사가 없습니다.
1 | long xor(XorCipher *ctx) { |
따라서 데이터의 취득이나 암호화·복호화 시에 초기화되지 않은 XorCipher(즉, NULL 포인터)를 참조해 버릴 가능성이 있습니다.
취약점 확인
먼저 올바른 사용법으로 이 모듈을 호출해 보겠습니다. 각 요청 코드에 대응하는 함수를 만들면 편리합니다.
1 | int angus_init(void) { |
예를 들어, "Hello, World!"를 "ABC123"이라는 키로 암호화·복호화해 봅시다.
1 | int main() { |
데이터가 암호화·복호화되었다면 성공입니다.
다음으로 XorCipher를 초기화하지 않고 암호화해 봅시다.
1 | int main() { |
이것을 실행하면 다음과 같이 커널 패닉에 빠질 것입니다.
BUG 항목을 보면 "kernel NULL pointer dereference, address: 0000000000000008"라고 되어 있어, 분석한 대로 NULL 포인터를 참조하려고 해서 크래시가 발생하고 있음을 알 수 있습니다.
NULL 포인터 참조는 사용자 공간의 프로그램에서도 종종 발생하지만, 이 버그는 일반적으로 exploitable 하지 않습니다. 그럼 이번에는 어떻게 이 버그를 사용해 권한 상승을 할까요?
가상 메모리와 mmap_min_addr
Linux의 사양으로서, 가상 메모리는 주소에 따라 사용 용도가 다릅니다. 예를 들어 0000000000000000부터 00007fffffffffff까지는 사용자 공간이 자유롭게 사용할 수 있습니다. 또한, ffffffff80000000부터 ffffffff9fffffff까지는 커널 데이터 영역으로, 물리 주소 0에 매핑되어 있습니다.

Linux에서는 48비트 주소를 64비트로 부호 확장해. 그래서 0x800000000000부터 0xffff7fffffffffff까지는 주소로서 유효하지 않고, non-canonical이라고 불려.
0000000000000000부터 00007fffffffffff까지는 사용자 공간이 사용할 수 있습니다. 즉, 주소 0이 매핑되어 있을 때, NULL 포인터 참조는 Segmentation Fault를 일으키지 않고 데이터를 읽고 쓸 수 있습니다. 커널 공간의 NULL 포인터 참조에서는, SMAP가 비활성화일 때 사용자 공간의 데이터를 읽을 수 있으므로, 공격자가 의도적으로 주소 0에 준비한 데이터를 사용하게 되는 것입니다.
mmap에서는 보통 첫 번째 인수가 0(NULL)일 때는, 어느 주소에 매핑할지를 커널에 맡깁니다. 하지만 MAP_FIXED 플래그를 붙여서 매핑하면 반드시 그 주소에 매핑하도록(하거나 실패하도록) 되며, 주소 0에 메모리를 할당할 수 있습니다. (KPTI가 활성화되어 있으므로 MAP_POPULATE도 잊지 않도록 합시다.)
1 | mmap(0, 0x1000, PROT_READ|PROT_WRITE, |
이번 공격 대상 머신에서도 이 방법으로 주소 0에 메모리를 할당할 수 있지만, 여러분이 평소 사용하는 Linux 머신에서는 위 코드가 실패할 것입니다.
Linux에는 NULL pointer dereference에 대한 mitigation으로서 mmap_min_addr라는 변수가 있습니다.
1 | $ cat /proc/sys/vm/mmap_min_addr |
사용자 공간에서 이 주소보다 작은 주소에 메모리를 매핑할 수는 없습니다. 그 때문에 일반적으로 NULL pointer dereference는 unexploitable 하지만, 이번 공격 대상에서는 이 값이 0으로 설정되어 있기 때문에 공격 가능합니다.
권한 상승
XorCipher 구조체를 NULL 포인터 참조해 버리므로, 공격자는 주소 0에 가짜 XorCipher 구조체를 준비합니다.
1 | typedef struct { |
data 포인터와 datalen을 조작하면, CMD_GETDATA로 임의 주소에서 데이터를 읽어낼 수 있음을 알 수 있습니다. 또한 data 포인터와 datalen, 그리고 key와 keylen을 적절히 설정하면, 임의 주소의 데이터를 다시 쓸 수 있습니다.
따라서 이번 취약점에서는 AAR/AAW라는 매우 강력한 primitive를 만들 수 있습니다. CMD_GETDATA에서는 copy_to_user를 사용하여 커널 공간에서 사용자 공간으로 데이터를 전송합니다.
1 | if (copy_to_user(req.ptr, ctx->data, req.len)) return -EINVAL; |
copy_to_user나 copy_from_user와 같은 함수는 실수로 매핑되지 않은 주소가 전달되어도 크래시하지 않고 실패하도록 설계되어 있습니다. 따라서 KASLR가 활성화된 경우라도, 적당히 주소를 정해 무차별 대입적으로 데이터를 읽어가다 보면, 언젠가 copy_to_user가 성공합니다.
어쨌든 AAR/AAW를 만들어서, 사용자 공간의 데이터를 읽고 쓰는 것으로 구현을 확인해 봅시다.
1 | XorCipher *nullptr = NULL; |
AAR/AAW가 성공했습니다!
이제 커널의 베이스 주소를 찾거나, cred 구조체를 찾는 등 자유로운 기법으로 권한 상승을 시도해 보세요. 샘플 exploit 코드는 여기에서 다운로드할 수 있습니다.
cred構造体を見つける方法、カーネルのベースアドレスを見つける方法などを試し、どの手法が平均的に最も速く終わるかを調べましょう。また、それぞれの手法の利点と欠点は何でしょうか。
당연히
lseek핸들러도 커널 모듈 측이 올바르게 구현해야 합니다. ↩︎