Kernel_pwn SringsIPC(P1)
从任意地址读写到提权的三种方法:
1.修改cred结构体
2.改写vdso映射函数+反弹shell
3.劫持prctl执行过程的hp->hook指针
这道题来自csaw-2015-ctf也是一道经典的内核题了,花了蛮久的时间去复现🤦
这题只给了程序的源代码main.c,需要我们自己搭建环境。我是懒狗,直接用了p4nda师傅的镜像和文件系统(十分感谢!😍)。
根据题目的名字猜测可能是实现了进程间通信过程中的的信箱机制(正好最近在做操作系统的IPC实验),但实际上是我想多了。
分析源码发现程序实现了对一片内存区域的申请,释放,读写,扩容,查寻操作。
漏洞点在扩容操作(realloc)函数中:
1 | static int realloc_ipc_channel ( struct ipc_state *state, int id, size_t size, int grow ) |
krealloc函数new_size是由channel->buf_size-size得到的,这两个值都可控,从而可以使得new_size+1==0。根据krealloc函数源码:
1 | /** |
如果传入的size为0,会返回ZERO_SIZE_PTR(#define ZERO_SIZE_PTR ((void *)16)
),即为0x10(而不是0!!!),此时有:
1 | channel->buf_size = new_size == 0x10 |
如果将channel->index赋值为target_addr-0x10,根据read/write的功能可以绕过size的限制从而实现任意地址读写🙌。
利用方法
可以任意地址读写了,要如何提权呢?共学到了三种姿势
1.修改进程cred结构体
做过了babydriver那题就比较容易想到的要修改cred结构体了,但要怎么找到cred的地址呢?虽然我们已经可以任意地址读了,但要读哪里才能读到cred呢?头大。。。
看了raycp师傅的博客学到了寻找cred结构体的方法tql!。
首先线程由于要使用所属进程的资源,在其thread_info结构块中有一个struct task_struct类型的结构体,其内容为:
1 | struct task_struct { |
可以看到我们所关心的conststruct cred __rcu *cred; conststruct cred __rcu *real_cred;
,还有一个重要的地方在于char comm[TASK_COMM_LEN];
,这个字符数组保存了进程的名字,也为我们寻找cred提供了方法。
首先,task_struct所需的内存是动态分配得到的,我们知道通过kmem_cache_alloc_node
函数申请的空间是在内核的动态分配区域。通过下面的内核映射空间可以确定爆破的范围在0xffff880000000000~0xffffc80000000000。
1 | 0xffffffffffffffff ---+-----------+-----------------------------------------------+-------------+ |
还有这里介绍一个关键的函数int prctl( int option,unsigned long arg2,unsigned long arg3,unsigned long arg4,unsigned long arg5 ),这是一个系统函数,是为进程制定而设计的。内核对应的处理函数为SYSCALL_DEFINE5()。
其中第一个参数option指定操作类型,如指定PR_SET_NAME,即设置进程名,之后的参数即为补充参数。
因此我们可以通过执行prctl(PR_SET_NAME,target)即可完成对进程名的设定。
因此利用方法就很明确了:
1.prctl函数设置进程明。
2.利用ioctl(fd,CSAW_SHRINK_CHANNEL,&shrink_channel)触发漏洞(channel->date=0x10 channel->buf_size=0xffffffffffffffff)。
3.利用任意地址读在0xffff880000000000~0xffffc80000000000范围内利用memmem()函数寻找进程名字符串char comm[TASK_COMM_LEN]
。
4.找到cred地址后利用任意地址写修改uidgid为0
触发漏洞:
call krealloc时寄存器状态:
1 | $rax : 0xffffffffffffffff |
返回值$rax : 0x0000000000000010 → 0x0000000000000010
赋值时:
1 | → 0xffffffffc00001b0 <realloc_ipc_channel.isra+80> mov QWORD PTR [rbx+0x8], rax |
寻找cred:
1 | for(;addr < 0xffffc80000000000;addr += 0x1000){ |
调试:
1 | gef➤ x/40gx 0xffff8800002034c0-0x30 //task_struct |
改写cred:
1 | for(int i=0;i<44;i++){ |
第一种的解法还算常规,第二三种解法就很骚了,看着p4nda和raycp两位大师傅的博客学的,师傅们tql!🤩,另外两种会单独再写两篇post。周一还有考试,赶紧去复习了😭。