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
...
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;
중요 필드들은 다음과 같다
- usage
cred 참조 카운터로 하나의 cred 구조체는 여러 개의 프로세스에서 동시에 사용될 수 있다. cred 구조체는 아마 공유자원 인듯?
- uid
현 프로세스를 소유하고 있는 사용자의 ID를 저장한다. id=0 이면 root이다.
- euid
실제 명령을 수행하는 주체의 id를 가리킨다. 사용자가 처음 user1에 로그인을 하면 uid, euid가 동일하지만, su로 user2로 전환하면 uid는 user1이지만, euid는 user2를 가리킨다.
- 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 소스코드는 아래 사이트에서 확인 할수 있다.
https://elixir.bootlin.com/linux/v5.4.53/source/kernel/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;
}
함수 인자로 전달된 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,
};
init_cred 구조체는 초기 root 권한의 자격증명을 가지고 있는 것을 확인할 수 있다. 만약 daemon이 NULL이라면 kmem_cache_alloc() 함수를 통해 할당받은 new에 old 값을 복사하고, new를 리턴한다.
prepare_kernel_cred() 함수의 전체 로직을 한번 정리해보자.
- kmem_cache_alloc()에 의해 new 변수에 객체를 할당한다
- daemon 값이 NULL이 아니면 get_task_cred() 함수를 호출하여 전달된 프로세스의 자격증명(권한으로 보면될듯)을 old 변수에 저장한다
- daemon이 NULL이면 init_cred의 자격증명을 old 변수에 저장한다. init_cred 자격증명은 root이다
- validate_creds() 함수를 호출하여 전달된 자격증명(old)의 유효성을 검사한다
- atomic_set()을 통해 new→usage 필드에 1을 세팅한다
- set_cred_subscriber()을 통해 cred→subscribers 필드에 0을 세팅한다
- get_uid(), get_user_ns(), get_group_info() 함수를 통해 현재 new에 복사된 자격증명의 uid, user namespace, group info를 조회한다
- security_prepare_creds() 함수를 이용하여 현재 프로세스의 자격증명을 갱신한다.
- put_cred()함수를 이용하여 현재 프로세스가 이전에 참조한 자격증명을 해제한다
- 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);
commit_creds() 함수의 주요 흐름은 다음과 같다.
- current 변수에 담겨있는 현재 프로세스의 정보를 task_struct 구조체 변수인 task에 저장한다
- task 구조체를 이용해서 현재 프로세스가 사용중인 자격증명 정보를 old 변수에 저장한다.
- BUG_ON() 함수를 이용해서
- task→cred 와 old 가 같은지 확인한다
- new→usage에 저장된 값이 1보다 작은지 확인한다
- get_gred() 함수를 이용하여 new 변수에 저장된 자격증명에 참조되있는 정보를 가져온다
- uid_eq(), gid_eq() 함수를 이용하여 euid, egid 정보를 new, old 변수들에 담겨있는 값과 비교를 한다
- cred_cap_issubset() 함수를 이용하여 두 자격증명(old,new)이 동일한 사용자 namespace에 있는지 확인한다
- 다시 uid_eq(), gid_eq() 함수를 이용하여 다음의 값을 확인한다
- new→fsuid, old→fsuid
- new→fsgid, old→fsgid
두 값이 다를 경우 key_fsuid_chaned(), 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() 함수를 이용하여 이전에 사용된 자격증명을 모두 해제한다.
결론적으로 prepare_kernel_cred()
함수로 root 권한의 자격증명을 얻고, 이를 인자로 하여 commit_creds()
함수를 호출하면 root로의 권한상승을 일으킬수 있다
commit_creds(prepare_kernel_cred(NULL));
4. LPE TEST with Linux Kernel Module
위에서 공부한 이론을 바탕으로 직접 LPE 테스트를 해보자. 소스코드는 ioctl 설명에서 사용한 코드를 이용할것이다.
#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);
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)
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);
}
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
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)
#
lpe.ko를 생성하고 insmod로 커널에 등록한다. 그후 생성된 디바이스 파일(chardev0)를 open한뒤, ioctl() 함수를 호출한 결과이다. 처음에는 나의 uid이지만 test를 실행시키면 root로 권한상승이 일어난걸 볼 수 있다.
5. 참조
'컴퓨터 관련 과목 > 운영체제 & 커널' 카테고리의 다른 글
Linux kernel protection (0) | 2020.12.06 |
---|---|
리눅스 커널 디버깅하기 (8) | 2020.12.01 |
ioctl 이란? (0) | 2020.11.18 |
linux Character Device Drivers 만들기 (4) | 2020.11.16 |
linux kernel module 만들기 (0) | 2020.11.16 |
Uploaded by Notion2Tistory v1.0.0