5) - Exploit Core Functions
Contents
prepare_kernel_cred()
commit_creds()
copy_to_user
copy_from_user
prepare_kernel_cred()
struct cred *prepare_kernel_cred(struct task_struct *daemon)
/* https://elixir.bootlin.com/linux/v4.18/source/kernel/cred.c#L595 */
/**
* prepare_creds - Prepare a new set of credentials for modification
*
* Prepare a new set of task credentials for modification. A task's creds
* shouldn't generally be modified directly, therefore this function is used to
* prepare a new copy, which the caller then modifies and then commits by
* calling commit_creds().
*
* Preparation involves making a copy of the objective creds for modification.
*
* Returns a pointer to the new creds-to-be if successful, NULL otherwise.
*
* Call commit_creds() or abort_creds() to clean up.
*/
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);
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;
new->security = NULL;
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);
일단 위에 주석처리된 부분을 번역해보겠다.
credentials
는 인증,권한의 의미를 가지고 modification
는 수정의 의미를 가진다.
즉, 위 주석에 나와있는 설명대로 prepare_creds - Prepare a new set of credentials for modification
는 자격 수정에 대한 새로운 세트를 준비한다라고 의역할 수 있다.
그리고 아랫 부분을 계속 읽어보면, commit_creds()
이라는 함수를 통하여 권한을 수정 한 다음, prepare_creds()
에서 사용하고 싶은 권한을 넣어주면 된다고 설명되어있다.
함수의 호출에 성공하면 새로운 권한(자격)에 대한 pointer
를, 아니면 NULL
을 반환한다.
코드를 보며 정리해보면, prepare_kernel_cred()
는 아래와 같이 진행된다.
new = kmem_cache_alloc(cred_jar, GFP_KERNEL);
으로 객체 할당if (daemon)
으로 인자의 값에 따라서 동작이 달라짐해당 조건문을 만족하면
get_task_cred()
함수를 호출하여 전달된 프로세스의 자격 증명(credentials)을 old 변수에 저장함.해당 조건문을 만족하지 않으면
get_cred()
함수를 호출하여init_cred
의 자격 증명(credentials)을 old 변수에 저장함.
validate_creds(old);
으로old(권한)
가 올바른지 확인함.atomic_set()
함수에 의해 "&new→usage" 영역에 1이 설정됨.set_cred_subscribers()
함수를 이용하여 "&cred→subscribers" 영역에 0이 설정됨.get_uid(), get_user_ns(), get_group_info()
새 자격 증명의 uid, user namespace, group info를 조회함.security_prepare_creds()
함수를 이용하여 현재 프로세스의 자격 증명을 변경.put_cred()
함수를 이용하여 현재 프로세스가 이전에 참조한 자격 증명을 해제함.validate_creds()
함수에 의해 전달된 자격 증명(new)의 유효성을 검사함.
여기서 자격 증명은 struct
cred init_cred
에 의해 관리된다.
struct cred init_cred = {
.usage = ATOMIC_INIT(4),
.subscribers = ATOMIC_INIT(2),
.magic = CRED_MAGIC,
.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,
};
typedef struct {
uid_t val;
} kuid_t;
typedef struct {
gid_t val;
} kgid_t;
이 구조체를 보면 GLOBAL_ROOT_UID
등의 코드를 통해 uid,gid,suid,sgid
의 권한이 root
로 설정 되어있음을 볼 수 있다.
따라서 prepare_kernel_cred()
함수 호출 시 인자값으로 root
를 의미하는 NULL(0)
을 전달하면 root
권한의 자격 증명을 준비할 수 있다.
최종적으로 정리하면, prepare_kernel_cred()
를 호출하면 new cred를 만드는데, 0을 인자로 주면 uid, gid 등이 0으로 설정된 cred가 생성이 된다. 즉 prepare_kernel_cred(0)
은 credential structure
를 0으로 초기화해서 생성해주는 역할을 한다.
commit_cred()
는 credential structure
를 인자로 받아서 해당 cred로 재설정을 해주는 역할을 하기에, 0으로 초기화된 cred를 넘겨주면 uid, gid 등이 결국 0으로 재설정되어 lpe가 가능한 것이다.
juntae@ubuntu:~$ id
uid=1000(juntae) gid=1000(juntae) groups=1000(juntae),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),113(lpadmin),128(sambashare),129(docker)
juntae@ubuntu:~$ sudo su
root@ubuntu:/home/juntae# id
uid=0(root) gid=0(root) groups=0(root)
추가로, 위와 같이 root
권한의 uid
등은 0임을 확인할 수 있다.
commit_creds()
int commit_creds(struct cred *new)
/**
* commit_creds - Install new credentials upon the current task
* @new: The credentials to be assigned
*
* Install a new set of credentials to the current task, using RCU to replace
* the old set. Both the objective and the subjective credentials pointers are
* updated. This function may not be called if the subjective credentials are
* in an overridden state.
*
* This function eats the caller's reference to the new credentials.
*
* Always returns 0 thus allowing this function to be tail-called at the end
* of, say, sys_setgid().
*/
int commit_creds(struct cred *new)
{
struct task_struct *task = current;
const struct cred *old = task->real_cred;
kdebug("commit_creds(%p{%d,%d})", new,
atomic_read(&new->usage),
read_cred_subscribers(new));
BUG_ON(task->cred != old);
BUG_ON(read_cred_subscribers(old) < 2);
validate_creds(old);
validate_creds(new);
BUG_ON(atomic_read(&new->usage) < 1);
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;
}
commit_cred
함수는 prepare_kernel_cred
함수에 의해 준비된 권한 증명을 실제로 적용하여 현재 프로세스에 새 자격 증명을 설치한다.
아래와 같이 동작한다.
current가 가지고 있는 현재 프로세스의 정보를 task에 저장한다.
task 구조체를 이용하여 현재 프로세스가 사용중인 자격 증명 정보를 old 변수에 저장한다.
BUG_ON()
을 이용하여 다음과 같은 사항을 확인한다."task->cred"과 "old"의 자격증명이 다른지 확인한다.
"&new→usage"에 저장된 값이 1 보다 작은지 확인한다.
위의
prepare_kernel_cred
함수에서&new->uasge
에 1이라는 값을 저장하였기 때문에, 해당 부분은 무시된다.
get_cred()
를 이용하여 new 변수에 저장된 자격 증명에 참조됨 정보를 가져온다.uid_eq()
,gid_eq()
를 이용하여 다음과 같은 구조체 내에 저장된 변수의 값을 확인한다.euid
,egid
는 유효 사용자 식별자(effective user ID,euid
)이라는 의미로 프로세스가 파일에 대해 가지는 권한을 뜻한다.fsuid
는 리눅스에는 파일 시스템 접근 제어 용도로 사용되는 파일 시스템 사용자 ID(file system user ID,fsuid
)를 뜻한다.old->euid, new→euid
old->egid, new→egid
old->fsuid, new→fsuid
old->fsgid, new→fsgid
cred_cap_issubset()
함수를 이용하여 두 자격 증명이 동일한 사용자공간에 있는지 확인한다.또다시 uid_eq(), gid_eq() 함수를 이용하여 다음과 같은 구조체 내에 저장된 변수의 값을 확인한다.
new→fsuid, old→fsuid
new→fsgid, old→fsgid
비교 값이 다를 경우
key_fsuid_changed()
,key_fsgid_changed()
를 이용하여 현재 프로세스의 fsuid, fsgid으로 값을 갱신한다.
alter_cred_subscribers()
를 이용하여 new 구조체에서 subscribers 변수에 2를 더한다.rcu_assign_pointer()
를 이용하여 현재 프로세스의 "task->real_cred", "task→cred" 영역에 새로운 자격 증명을 등록한다.alter_cred_subscribers()
를 이용하여 old 구조체에서 subscribers 변수에 -2를 더한다.put_cred() 함수를 이용하여 이전에 사용된 자격 증명을 모두(old obj and subj)해제 한다.
copy_from_user(), _copy_from_user()
/* include/linux/uaccess.h */
copy_from_user(void *to, const void __user *from, unsigned long n)
{
if (likely(check_copy_size(to, n, false)))
n = _copy_from_user(to, from, n);
return n;
}
user address(from)
에서 kernel address(to)
로 nbytes(n)
만큼 복사한다.
/* include/linux/asm/uaccess.h */
static inline unsigned long
_copy_from_user(void *to, const void __user *from, unsigned long n)
{
unsigned long res = n;
might_fault();
if (likely(access_ok(from, n))) {
kasan_check_write(to, n);
res = raw_copy_from_user(to, from, n);
}
if (unlikely(res))
memset(to + (n - res), 0, res);
return res;
}
user application
에서 사용될때는 INLINE
함수로 사용된다.
여기서 INLINE
함수란, 함수를 라이브러리를 통하여 가져오는게 아닌 코드를 그자리에서 즉시 실행한다는 점이 특징이다. 즉, 함수 호출과정이 없으므로 속도가 빠르다.
따라서 인라인함수는 자주 호출되면서 속도가 중요한 부분에 사용된다.
kernel application
에서 사용될 때에는 그냥 라이브러리 에서 가져온다.
return
: 복사되지 않은 바이트 수. 정상적으로 수행됐다면 0
을 return
한다.
copy_to_user(), _copy_to_user()
/* include/linux/asm/uaccess.h */
unsigned long copy_to_user(void __user* to, const void* from, unsigned long n)
static __always_inline unsigned long __must_check
copy_to_user(void __user *to, const void *from, unsigned long n)
{
if (likely(check_copy_size(from, n, true)))
n = _copy_to_user(to, from, n);
return n;
}
kernel address(from)
에서 user address(to)
에 nbytes
만큼 값을 써넣는다.
return
: 복사되지 않은 바이트 수. 정상적으로 수행됐다면 0
을 return
한다.
static inline unsigned long
_copy_to_user(void __user *to, const void *from, unsigned long n)
{
might_fault();
if (access_ok(VERIFY_WRITE, to, n)) {
kasan_check_read(from, n);
n = raw_copy_to_user(to, from, n);
}
return n;
}
마찬가지로 INLINE
함수가 존재한다.
Reference
https://wally0813.github.io/kernel%20exploit/2018/09/17/Kernel-Exploit-Basic/
https://elixir.bootlin.com/linux/v4.18/source/kernel/cred.c#L595
https://nonetype.github.io/2019-09-23/Linux-Kernel-Exploit-Development-lab3