Kernel Exploit에 필요한 기초지식들을 한 번 정리해보려한다.
정확히 말하면 Kernel Exploit을 공부하면서 내가 잘 모르거나 중요하다고 느낀 것들을 정리해보려한다.
가좌~👍
우선 Kernel이 무엇인지부터 정리해보자👊
Kernel은 운영 체제의 핵심이 되는 프로그램으로, 시스템의 주요 기능들을 제어한다.
일반적인 리눅스 바이너리를 디버깅하는 것과는 달리, 커널을 디버깅하려면 디버거와 분리하여 실행시켜야한다.
즉, QEMU라는 가상 머신을 사용하여 디버그 옵션을 줘서 Kernel을 구동한 뒤 디버거로 붙으면 디버깅을 할 수 있다 ❗
apt install qemu-utils qemu-system-x86
qemu-system-x86_64 -version
위의 명령어를 통해 QEMU를 설치할 수 있고, 두 번째 커맨드를 통해 정상적으로 설치되었는지 확인할 수 있다.
CTF의 Kernel 문제에서는 보통 Kernel을 실행하기 위해 "boot.sh"라던가 "run.sh" 등의 Kenrel을 실행할 수 있는 스크립트 파일을 제공한다.
./run.sh -S -gdb tcp:localhost:1234
디버깅하는 방법은 간단하다. QEMU의 -S 옵션과 -gdb 옵션을 위와 같이 준다고 하면 gdb를 실행시키고 localhost:1234로 붙으면 디버깅할 수 있다!
또 다른 Command 창
gdb-pwndbg vmlinux
target remote localhost:1234
vmlinux 파일을 통해 gdb를 열고 target remote 커맨드를 통해 디버깅할 수 있다.
vmlinux 파일에는 Kernel의 Symbol 및 Debug Symbol이 있어서 디버깅 시 유용하다.
또한 vmlinux 파일을 통해 Kernel Gadget들을 구할 수 있다.
만약에 vmlinux 파일이 없다면, bzImage라는 빌드된 커널 이미지 파일을 통해 추출할 수 있다.
./extract-vmlinux bzImage > ./vmlinux
위의 커맨드를 통해 vmlinux 파일을 추출할 수 있다. 만약 Kernel 빌드 시 Symbol들을 다 날렸다면, 심볼정보는 알 수 없겠지만 Kerel ROP를 수행하기 위한 Gadget들은 구할 수 있다.
마지막으로 Kernel Exploit에서 자주 등장하는 코드가 있다. 해당 코드만 살펴보고 마무리하려고 한다.
commit_creds(prepare_kernel_cred(NULL));
우선 해당 코드를 살펴보기전 간단하게 task_struct와 struct cred도 짚어보자.
리눅스에는 "태스크(Task)"라는 개념이 있다. Task는 프로그램의 실행 단위를 나타낸다. 리눅스에서는 하나의 프로세스 or 스레드가 각각의 Task로 구성된다.
여기서 각 Task는 커널 메모리에 task_struct 구조체로 표현되는데 해당 구조체에는 태스크에 관련된 여러 정보가 저장되어 있다. 이 중 cred라는 사용자 신원 및 권한과 관련된 정보가 있는데 해당 부분을 조작하는 것이 위의 코드이다.
그러면 task_struct 구조체와 cred 구조체에 뭐가 있는지 간단하게 살펴보자.
task_struct에는 프로세스의 메모리 맵, 파일 디스크럽터, 프로세스의 권한 등 여러 정보가 저장되어 있다.
struct task_struct {
volatile long state;
struct list_head tasks;
struct mm_struct *mm;
/* Effective (overridable) subjective task credentials (COW): */
const struct cred __rcu *cred;
char comm[TASK_COMM_LEN];
/* Open file information: */
struct files_struct *files;
...
}
여기서 중요하게 살펴볼 부분이 cred이다. cred 구조체는 현재 태스크의 신원 정보를 기리키는 포인터이다.
바로 cred 구조체를 살펴보자
// Permal link: struct cred {
atomic_t usage;
...
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 */
...
}
cred 구조체는 "include/linux/sched.h"에 정의되어 있다.
참고로 cred 구조체는 여러 개의 프로세스에서 동시에 사용될 수 있는데, 이를 위해 참조 카운터인 usage 변수를 사용한다. 두둥❗
참고로 cred 구조체는 다른 프로세스들이 공유해서 사용되므로 권한 상승을 위해 사용하려면 복사를 한 후 태스크가 복사된 cred를 가리키도록 해야한다고 한다!
task_struct와 struct cred를 살펴봤으니 다시 본론으로 돌아오자.
우선 prepare_kernel_cred(NULL)부터 살펴보자.
/* 함수 원형 */
struct cred *prepare_kernel_cred(struct task_struct *daemon)
/* 함수 코드 일부분 */
if (daemon)
old = get_task_cred(daemon);
else
old = get_cred(&init_cred);
맨 위는 해당 함수의 원형이고, 아래는 함수의 일부분이다.
해당 함수는 먼저 daemon을 가져오고 get_task_cred 함수를 통해 cred 구조체를 가져온다.
만약 daemon이 NULL이라면 get_cred 함수를 통해 init_cred 구조체를 가져오는데, 해당 구조체는 ROOT의 UID 및 GID를 가지고 있다.
struct cred init_cred = {
.usage = ATOMIC_INIT(4),
#ifdef CONFIG_DEBUG_CREDENTIALS
.subscribers = ATOMIC_INIT(2),
.magic = CRED_MAGIC,
#endif
.uid = GLOBAL_ROOT_UID,
.gid = GLOBAL_ROOT_GID,
.suid = GLOBAL_ROOT_UID,
.sgid = GLOBAL_ROOT_GID,
.euid = GLOBAL_ROOT_UID,
.egid = GLOBAL_ROOT_GID,
.fsuid = GLOBAL_ROOT_UID,
.fsgid = GLOBAL_ROOT_GID,
.securebits = SECUREBITS_DEFAULT,
.cap_inheritable = CAP_EMPTY_SET,
.cap_permitted = CAP_FULL_SET,
.cap_effective = CAP_FULL_SET,
.cap_bset = CAP_FULL_SET,
.user = INIT_USER,
.user_ns = &init_user_ns,
.group_info = &init_groups,
};
즉, prepare_kernel_cred(NULL)을 호출하면 root 권한을 갖는 cred 구조체를 획득할 수 있다. 그러면 다음 함수인 commit_creds 함수를 살펴보자 ❗
/* 함수 원형 */
int commit_creds(struct cred *new)
/* 함수 코드 일부분 */
struct task_struct *task = current;
rcu_assign_pointer(task->real_cred, new);
rcu_assign_pointer(task->cred, new);
현재 테스크를 가리키는 task 포인터를 선언하고 new로 현재 테스크의 신원 정보를 교체한다.
즉, prepare_kernel_cred의 인자로 NULL을 전달하면 root 권한의 cred 구조체를 반환받고, commit_creds 함수의 인자로 전달되어 현재 Task의 권한을 root 권한으로 상승시킬 수 있다.
갑자기 이렇게 마무뤼~!
bye~😁
※ 참고
👉 https://ko.wikipedia.org/wiki/%EC%BB%A4%EB%84%90_(%EC%BB%B4%ED%93%A8%ED%8C%85)
👉 https://dreamhack.io/lecture/courses/61
👉 https://dreamhack.io/lecture/courses/56