"유저랜드 pwn은 대충 공부했지만, 커널부터는 어려울 것 같아서 손을 못 대겠다"는 분들이 많을 것입니다. 하지만 사실 커널 exploit은 경우에 따라 공격이 매우 간단하기도 합니다.
이 절에서는 유저랜드 exploit과 커널 exploit의 차이, 환경 구축 등에 대해 설명합니다.
커널 exploit의 특징
먼저 유저랜드와 비교해서 커널 공간의 취약점에는 어떤 특징이 있는지 알아둡시다.
공격 대상
유저랜드 exploit과 커널 exploit의 가장 큰 차이는 그 목적에 있습니다.
지금까지 설명한 유저랜드 exploit에서는 많은 경우 '임의 명령 실행’이라는 목표를 향해 exploit을 작성했습니다. 반면, 커널 exploit에서는 일반적으로 '권한 상승’을 위해 exploit을 작성합니다. 어떤 수단으로 공격 대상 머신에 침입할 수 있었다고 가정하고, 공격자는 커널 exploit을 이용해 추가로 root 권한을 획득합니다[1]. 이처럼 로컬 머신에서의 권한 상승을 LPE(Local Privilege Escalation)라고 부릅니다.
물론 유저랜드에서 권한 상승할 수 있는 취약점도 있지만, 그것은 공격 대상 프로그램이 특권 유저로 동작하고 있기 때문입니다. 커널 exploit의 경우, 공격 대상은 주로 다음 두 가지가 있습니다.
- Linux 커널
- 커널 모듈
Linux 커널 내의 코드(시스템 콜이나 파일 시스템)는 root 권한으로 동작하고 있기 때문에, 커널 자체에 버그가 있는 경우 LPE로 이어질 가능성이 있습니다.
다른 하나는 디바이스 드라이버 등의 커널 모듈에 포함된 취약점입니다. 디바이스 드라이버는 유저 공간에서 주로 외부 기기(프린터 등)와의 통신을 쉽게 하기 위한 인터페이스입니다. 디바이스 드라이버도 반드시 root 권한으로 동작하고 있으므로[2], 버그가 있는 경우 LPE로 이어집니다.
공격 방법
유저랜드 exploit의 경우, 일반적으로 공격 대상 서비스에 입력을 줌으로써 exploit했습니다. 그래서 Python 등의 언어로 exploit을 작성하는 것이 주를 이룹니다.
반면, 커널 exploit의 경우는 대상이 OS나 드라이버입니다. 이러한 조작은 낮은 레이어이기 때문에, C 언어 등으로 exploit을 작성하는 것이 주를 이룹니다. 물론 Python 등으로도 쓸 수 있지만, 애초에 공격 대상 머신(특히 CTF나 실험 환경에서 준비되는 작은 Linux) 위에 Python이 존재하는 경우가 적기 때문에 작동하지 않을 가능성이 높습니다.
이 사이트에서도 exploit을 C 언어로 기술합니다. 자세한 이야기는 다른 절에서 나오겠지만, musl-gcc라는 컴파일러를 사용합니다.
리소스 공유
커널 exploit의 또 다른 특징은 리소스가 공유되고 있다는 점입니다.
유저랜드에서는 보통 공격 대상 프로세스가 하나 존재하고, 그 프로세스를 exploit함으로써 쉘을 따내는 등의 공격을 했습니다. 반면, Linux 커널이나 디바이스 드라이버 같은 프로그램은 OS를 이용하는 모든 프로세스에 공유됩니다. 시스템 콜은 누구나 자유로운 타이밍에 사용할 수 있고, 디바이스 드라이버도 누가 언제 조작할지 알 수 없습니다. 즉, 커널 공간에서 동작하는 코드를 작성할 때는 항상 멀티스레드라는 생각으로 프로그래밍하지 않으면 쉽게 취약점을 심게 됩니다.

전역 변수처럼 경합할 가능성이 있는 데이터를 사용할 때는 락을 걸어야 한다는 거네.
커널 공간 프로그래밍은 힘들다~.
힙 영역의 공유
게다가 커널의 힙 영역은 모든 드라이버와 커널에서 공유된다는 큰 특징이 있습니다.
지금까지 유저랜드 exploit에서는 프로그램마다 힙이 있기 때문에, 그 프로그램에서 Heap Overflow가 일어났다고 해서 exploitable한지는 프로그램 의존적이었습니다. 하지만, 예를 들어 디바이스 드라이버에서 한 번 힙 오버플로우가 일어나면, 다른 디바이스 드라이버나 Linux 커널이 힙에 확보한 주변 데이터까지 오염되어 버립니다.
공격자의 관점에서 보면, 이 특징에는 장점과 단점이 있습니다. 장점은 힙 주변의 작은 취약점이라도 LPE로 이어질 가능성이 매우 높다는 점입니다. 예를 들어 함수 포인터를 가진 객체는 Linux 커널에 많이 존재하므로, 그중 하나를 이용해 RIP를 쉽게 얻을 수 있는 것입니다. 단점은 모든 프로그램의 영향을 받기 때문에 힙 상태를 예측할 수 없다는 점입니다. 유저랜드 프로그램은 간단한 것이라면 입력에 대해 힙 상태가 결정적이었기 때문에 복잡한 힙 exploit(흔히 말하는 House of XXX 등)이 가능했습니다. 반면, 커널에서는 Heap Overflow가 발생하는 청크 뒤에 어떤 데이터가 존재하는지나, Use-after-Free로 해제된 후에 누가 그 주소를 사용할지는 알 수 없습니다.

그렇다는 건, 커널 exploit에서는 Heap Spray가 중요하다는 거네.
취약점 자체는 유저랜드의 것과 크게 다르지 않습니다. Stack Overflow나 Use-after-Free 등은 커널 랜드에도 존재할 수 있습니다. 또한, 디바이스 드라이버의 스택에도 보안 기법으로 Stack Canary를 둘 수 있습니다. 다만, 커널 공간 특유의 취약점이라는 것도 있으므로, 그것은 뒤쪽 절에서 등장합니다.
qemu 이용
Linux 커널 exploit을 작성할 때는, 디버깅을 위해 에뮬레이터 위에서 커널을 구동합니다. VM이라면 무엇이든 상관없지만, qemu가 일반적이므로 이 사이트에서도 qemu를 이용합니다.
독자 여러분은 자신의 환경에 맞춰 qemu-system을 설치해 두어 주세요.
1 | # apt install qemu-system |
디스크 이미지
qemu로 머신을 기동할 때, Linux 커널과는 별도로 루트 디렉토리로서 마운트되는 디스크 이미지가 필요합니다.
디스크 이미지는 일반적으로 ext 등의 파일 시스템 원시 바이너리나, cpio라고 불리는 형식으로 작성·배포됩니다.
파일 시스템의 경우는 mount 명령어로 마운트하면 안의 파일을 편집할 수 있습니다.
1 | # mkdir root |
이 사이트에서 다루는 연습에서는 CTF에서 일반적이고 가벼운 cpio 형식을 사용합니다.
cpio 명령어를 사용하여 다음과 같이 파일을 전개합니다.
1 | # mkdir root |
파일을 추가하거나 편집했다면, 다음과 같이 다시 cpio 파일로 묶습니다.
1 | # find . -print0 | cpio -o --format=newc --null > ../rootfs_updated.cpio |
cpio가 추가로 gz로 압축되어 있는 경우도 있으니, 그럴 때는 적절히 전개·재압축해 주세요.
또한 cpio는 권한 정보도 부여하므로, 파일 시스템 편집에서는 적절히 파일 소유자를 root로 할당할 필요가 있습니다. 위 명령어는 모두 root 권한으로 실행하고 있으므로 문제없지만, 번거로운 경우는 --owner=root 옵션을 주어 팩해도 됩니다.
1 | $ mkdir root |
(1)
run.sh를 실행하여 Linux가 실행되는 것을 확인해 주세요.(2) 기동 시 쉘이 root 권한이 되도록
rootfs.cpio를 편집해 주세요. (힌트: 기동 시 메시지를 표시하고 있는 스크립트를 찾아봅시다.)