커널의 기동 방법, 디버깅 방법, 그리고 보안 등 Kernel Exploit을 시작하는 데 필요한 지식은 완벽하게 습득했습니다. 이제부터는 실제로 exploit을 어떻게 작성해 나갈지와, 작성한 exploit을 어떻게 qemu 상에서 구동할지를 배웁니다.

qemu 상에서의 실행

qemu 위에서 exploit을 작성하고 빌드, 실행하면 커널이 크래시될 때마다 다시 작업해야 하므로 힘듭니다. 따라서 C 언어로 작성한 exploit을 로컬에서 빌드한 후, 그것을 qemu로 보낼 필요가 있습니다.
이 흐름을 매번 명령어로 입력하는 것은 번거로우므로, 쉘 스크립트 등으로 템플릿을 준비해 둡시다. 예를 들어 다음과 같은 transfer.sh를 준비해 봅니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#!/bin/sh
gcc exploit.c -o exploit
mv exploit root
cd root; find . -print0 | cpio -o --null --format=newc > ../debugfs.cpio
cd ../

qemu-system-x86_64 \
-m 64M \
-nographic \
-kernel bzImage \
-append "console=ttyS0 loglevel=3 oops=panic panic=-1 nopti nokaslr" \
-no-reboot \
-cpu qemu64 \
-gdb tcp::12345 \
-smp 1 \
-monitor /dev/null \
-initrd debugfs.cpio \
-net nic,model=virtio \
-net user

설명할 필요도 없겠지만, 단순히 GCC로 exploit.c를 컴파일하여 cpio에 추가하고, qemu를 기동하는 것뿐입니다. 원래의 rootfs.cpio를 훼손하지 않도록 debugfs.cpio라는 이름의 디스크를 사용하고 있습니다만, 취향에 따라 변경해도 상관없습니다.
또한, cpio를 만들 때는 root 권한이 아니면 파일 권한이 바뀌므로, transfer.sh는 root 권한으로 실행하도록 주의해 주세요.

자, exploit.c에 다음과 같은 코드를 넣고 transfer.sh를 실행해 봅시다.

1
2
3
4
5
6
#include <stdio.h>

int main() {
puts("Hello, World!");
return 0;
}

그러면 다음과 같이 에러가 발생합니다. 왜일까요?

GCC로 컴파일한 exploit이 동작하지 않음

실은 이번에 배포한 이미지는 일반적인 libc가 아니라, uClibc라는 콤팩트한 라이브러리를 사용하고 있습니다. 당연히 exploit을 컴파일한 여러분의 환경에서는 GCC, 즉 libc를 사용하고 있으므로, 동적 링크에 실패하여 exploit이 동작하지 않습니다.
따라서 qemu 상에서 exploit을 구동할 때는 static 링크하도록 주의합시다.

1
gcc exploit.c -o exploit -static

이렇게 변경해서 실행하면 프로그램이 동작할 것입니다.

static 링크하면 exploit이 동작함

원격 머신에서의 실행: musl-gcc 사용

여기까지 무사히 exploit을 qemu 상에서 실행할 수 있었습니다. 이번에 배포한 환경은 네트워크 접속이 가능하도록 설정되어 있기 때문에, 원격에서 실행하고 싶은 경우는 qemu 상에서 wget 명령어 등을 이용해 exploit을 전송할 수 있습니다.
하지만 CTF 등 일부 작은 환경에서는 네트워크를 이용할 수 없습니다. 이런 경우, busybox에 존재하는 명령어를 이용하여 원격으로 바이너리를 전송해야 합니다. 일반적으로는 base64가 사용되지만, GCC로 빌드한 파일은 수백 KB에서 수십 MB나 되기 때문에 전송에 매우 오랜 시간이 걸립니다. 크기가 커지는 것은 외부 라이브러리(libc)의 함수를 static 링크하고 있는 것이 원인입니다.
GCC로 크기를 줄이고 싶다면, libc를 사용하지 않도록 하고, read나 write 등은 시스템 콜(인라인 어셈블리)을 사용해 직접 정의해야 합니다. 물론 이것은 매우 힘든 일입니다.
그래서 많은 CTFer들은 Kernel Exploit을 목적으로 musl-gcc라고 불리는 C 컴파일러를 이용하고 있습니다. 아래 링크에서 다운로드하고, 빌드하여 설치를 완료해 주세요.

https://www.musl-libc.org/

설치가 완료되면, 다음과 같이 transfer.sh의 컴파일 부분을 고쳐 써 봅시다. musl-gcc의 경로는 각자 설치한 디렉토리를 지정해 주세요.

1
/usr/local/musl/bin/musl-gcc exploit.c -o exploit -static

저자의 환경에서는 앞서 만든 Hello, World 프로그램이 gcc의 경우 851KB, musl-gcc의 경우 18KB였습니다. 더욱 줄이고 싶은 경우는 strip 등으로 디버그 심볼을 삭제해도 됩니다.

늑대군

일부 헤더 파일(Linux 커널 계열)은 musl-gcc에는 없으니까, 인클루드 경로를 설정하거나 gcc로 컴파일할 필요가 있어. 그럴 때는 일단 어셈블리를 거쳐서 빌드하면, gcc의 기능을 쓰면서 파일 사이즈를 억제할 수 있지.
$ gcc -S sample.c -o sample.S
$ musl-gcc sample.S -o sample.elf

여기까지 완료했다면, 원격에(nc 경유로) base64를 사용해 바이너리를 전송하는 스크립트를 작성합시다. 이 업로더는 CTF의 경우 매번 사용하게 되므로, 템플릿으로 자신만의 것을 만들어 두는 것을 추천합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
from ptrlib import *
import time
import base64
import os

def run(cmd):
sock.sendlineafter("$ ", cmd)
sock.recvline()

with open("./root/exploit", "rb") as f:
payload = bytes2str(base64.b64encode(f.read()))

#sock = Socket("HOST", PORT) # remote
sock = Process("./run.sh")

run('cd /tmp')

logger.info("Uploading...")
for i in range(0, len(payload), 512):
print(f"Uploading... {i:x} / {len(payload):x}")
run('echo "{}" >> b64exp'.format(payload[i:i+512]))
run('base64 -d b64exp > exploit')
run('rm b64exp')
run('chmod +x exploit')

sock.interactive()

실행하고 잠시 후면 다음과 같이 업로드가 완료될 것입니다.

upload.py의 실행 결과

이 사이트에서는 여러분이 로컬에서 테스트할 뿐이라 업로드는 불필요하지만, CTF 등에서 실전할 때는 이것을 떠올려 사용합시다.