기본적으로 커널 익스플로잇은 권한 상승(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이 위 함수를 호출하는 것을 목표로 한다고 한다..