[Linux Kernel Exploit] Introduction

dandb3·2024년 4월 22일
0

linux kernel

목록 보기
2/3

기본적으로 커널 익스플로잇은 권한 상승(Privilege Escalation)을 목표로 한다.
이 권한 상승을 하게 만드는 가장 중요한 함수가 prepare_kernel_cred(), commit_creds() 함수이다.

각 함수를 알아보기에 앞서, 몇 가지 구조체들에 대해 설명할 것이다.
이 포스팅은 리눅스 4.18.20을 기준으로 한다.

struct cred

struct cred {
	atomic_t	usage;
#ifdef CONFIG_DEBUG_CREDENTIALS
	atomic_t	subscribers;	/* number of processes subscribed */
	void		*put_addr;
	unsigned	magic;
#define CRED_MAGIC	0x43736564
#define CRED_MAGIC_DEAD	0x44656144
#endif
	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 */
#ifdef CONFIG_KEYS
	unsigned char	jit_keyring;	/* default keyring to attach requested
					 * keys to */
	struct key __rcu *session_keyring; /* keyring inherited over fork */
	struct key	*process_keyring; /* keyring private to this process */
	struct key	*thread_keyring; /* keyring private to this thread */
	struct key	*request_key_auth; /* assumed request_key authority */
#endif
#ifdef CONFIG_SECURITY
	void		*security;	/* subjective LSM security */
#endif
	struct user_struct *user;	/* real user ID subscription */
	struct user_namespace *user_ns; /* user_ns the caps and keyrings are relative to. */
	struct group_info *group_info;	/* supplementary groups for euid/fsgid */
	struct rcu_head	rcu;		/* RCU deletion hook */
} __randomize_layout;

권한과 관련된 구조체이다.
간단하게 몇 가지만 살펴보자면,
중간에 uid, gid와 관련된 여러 멤버변수가 존재하는 것을 알 수 있다.

struct task_struct

struct task_struct {
	...
	/* Process credentials: */

	/* Tracer's credentials at attach: */
	const struct cred __rcu		*ptracer_cred;

	/* Objective and real subjective task credentials (COW): */
	const struct cred __rcu		*real_cred;

	/* Effective (overridable) subjective task credentials (COW): */
	const struct cred __rcu		*cred;
    ...
}

필요한 부분만 가져왔다.
task_struct 구조체에서 cred의 포인터를 저장하고 있는 것을 확인할 수 있는데, 이 저장된 cred 포인터가 가리키는 정보가 현재 task가 실행되고 있는 권한을 의미한다.

여기서 알 수 있는 내용은,
struct task_struct가 가리키는 cred 구조체의 uid / gid 값을 root의 것으로 변경시킬 수 있다면 Privilege Escalation을 일으킬 수 있다.
라는 것이다.

하지만, struct cred를 보면 atomic_t usage; 라는 변수가 존재하는데, 이는 현재 cred 구조체를 참조하고 있는 수를 카운트 하는 것을 의미한다.

즉, 하나의 cred 구조체를 여러 곳에서 참조할 수 있다는 뜻이다.
그러므로 그냥 cred 구조체의 값을 바꾸어 버리면 race condition이 일어나서 비정상적인 동작을 일으킬 수 있다. 그래서 cred 구조체를 복사해서 가져오는 방식을 택하는데, 이 때 쓰이는 것이 바로 prepare_kernel_cred() 함수이다.
또한, commit_creds() 함수를 통해 현재 task의 cred에 인자로 들어온 cred 값을 설정해 줄 수 있다.

prepare_kernel_cred() 를 통해서 root 권한을 가진 cred 구조체를 만들고, 그 구조체를 commit_creds()를 통해 등록시키면 Privilege Escalation을 일으킬 수 있다.

prepare_kernel_cred()

새로운 cred를 만들어주는 함수이다.
인자로 들어온 struct task_struct *가 가리키고 있는 cred 구조체를 복사하여 리턴해 준다.
만약 인자로 NULL이 들어왔다면, init_cred를 복사하여 리턴해 준다.

struct cred *prepare_kernel_cred(struct task_struct *daemon)
{
	const struct cred *old;
	struct cred *new;

	new = kmem_cache_alloc(cred_jar, GFP_KERNEL);
	if (!new)
		return NULL;

	kdebug("prepare_kernel_cred() alloc %p", new);

	if (daemon)
		old = get_task_cred(daemon);
	else
		old = get_cred(&init_cred);

	validate_creds(old);

	*new = *old;
	atomic_set(&new->usage, 1);
	set_cred_subscribers(new, 0);
	get_uid(new->user);
	get_user_ns(new->user_ns);
	get_group_info(new->group_info);

#ifdef CONFIG_KEYS
	new->session_keyring = NULL;
	new->process_keyring = NULL;
	new->thread_keyring = NULL;
	new->request_key_auth = NULL;
	new->jit_keyring = KEY_REQKEY_DEFL_THREAD_KEYRING;
#endif

#ifdef CONFIG_SECURITY
	new->security = NULL;
#endif
	if (security_prepare_creds(new, old, GFP_KERNEL) < 0)
		goto error;

	put_cred(old);
	validate_creds(new);
	return new;

error:
	put_cred(new);
	put_cred(old);
	return NULL;
}
EXPORT_SYMBOL(prepare_kernel_cred);

new는 새로 만들어진 struct cred를 저장하는 변수이고,
old는 복사할 struct cred를 가리키는 변수이다.

코드를 보면,,
우선 new에다가 메모리 할당을 해 준다.
그 후, 인자가 NULL이 아니라면 인자로 들어온 task의 cred 값을,
그렇지 않으면 init_cred의 값을 old에다가 저장한다.

그 후에 old에 있던 내용을 new에다가 복사해 준다.
마지막에 new를 리턴하는 것으로 함수가 끝난다.
뒤의 상세내용은 스킵...

init_cred?

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,
};

init_cred이다.
uid와 gid 값이 모두 root의 것으로 설정되어 있다.
즉, prepare_kernel_cred(NULL)을 호출하면 루트의 권한을 가지는 struct cred가 반환된다.

commit_creds()

int commit_creds(struct cred *new)
{
	struct task_struct *task = current;
	const struct cred *old = task->real_cred;

	get_cred(new); /* we will require a ref for the subj creds too */

	/* dumpability changes */
	if (!uid_eq(old->euid, new->euid) ||
	    !gid_eq(old->egid, new->egid) ||
	    !uid_eq(old->fsuid, new->fsuid) ||
	    !gid_eq(old->fsgid, new->fsgid) ||
	    !cred_cap_issubset(old, new)) {
		if (task->mm)
			set_dumpable(task->mm, suid_dumpable);
		task->pdeath_signal = 0;
		smp_wmb();
	}

	/* alter the thread keyring */
	if (!uid_eq(new->fsuid, old->fsuid))
		key_fsuid_changed(task);
	if (!gid_eq(new->fsgid, old->fsgid))
		key_fsgid_changed(task);

	/* do it
	 * RLIMIT_NPROC limits on user->processes have already been checked
	 * in set_user().
	 */
	alter_cred_subscribers(new, 2);
	if (new->user != old->user)
		atomic_inc(&new->user->processes);
	rcu_assign_pointer(task->real_cred, new);
	rcu_assign_pointer(task->cred, new);
	if (new->user != old->user)
		atomic_dec(&old->user->processes);
	alter_cred_subscribers(old, -2);

	/* send notifications */
	if (!uid_eq(new->uid,   old->uid)  ||
	    !uid_eq(new->euid,  old->euid) ||
	    !uid_eq(new->suid,  old->suid) ||
	    !uid_eq(new->fsuid, old->fsuid))
		proc_id_connector(task, PROC_EVENT_UID);

	if (!gid_eq(new->gid,   old->gid)  ||
	    !gid_eq(new->egid,  old->egid) ||
	    !gid_eq(new->sgid,  old->sgid) ||
	    !gid_eq(new->fsgid, old->fsgid))
		proc_id_connector(task, PROC_EVENT_GID);

	/* release the old obj and subj refs both */
	put_cred(old);
	put_cred(old);
	return 0;
}
EXPORT_SYMBOL(commit_creds);

task 변수는 현재 실행되고 있는 task를 가리킨다.
중간 부분만 보자면,

	rcu_assign_pointer(task->real_cred, new);
	rcu_assign_pointer(task->cred, new);

를 통해 new가 가리키는 값을 task->read_cred, task->cred에 저장하는 것을 확인할 수 있다.

결론

결국, commit_creds(prepare_kernel_cred(NULL)); 를 호출해주게 되면 현재 프로세스의 권한이 root로 변경되어 exploit에 성공하게 된다.
대부분의 kernel exploit이 위 함수를 호출하는 것을 목표로 한다고 한다..

profile
공부 내용 저장소

0개의 댓글