ACTF2022 pwn mykvm题解
- 题目描述:KVM is funny, enjoy it!
- 题目附件:mykvm.zip
分析
先看一下main函数中的代码逻辑,
1 | puts("your code size: "); //待输入code的大小 |
做题之前并没有研究过KVM的机制,于是搜了一下跟kvm相关的ctf题,发现了这一篇:Confidence2020 CTF KVM。按照这个题目中的漏洞点,很快在mykvm中也找到了类似漏洞。如下,sub_400B92()函数中memory_size的值设置的相当大,于是我们能在kvm虚拟机中越界访问到host机的内存。
1 | v8.slot = 0; |
不过,unk_602100在bss段,在kvm虚拟机中我们只能往后访问,即可能访问到host进程的堆空间。同时,该题对kvm虚拟机的设置,要求我们的代码从实模式开始运行。因此,要么我们在实模式有限的访存空间(1M)下进行利用,要么想办法进入保护模式进行利用。
由于对实模式和保护模式的切换以及页表的设置不熟,且堆的地址范围在一定概率下会跟kvm能访问的空间(1M)有重合,于是我们就直接在实模式下完成了对该题的利用。
越界能做什么?
首先,在bss段的最后,有一个指针变量,是memcpy(dest, *(const void **)&nbytes[1], 0x20uLL);
的目的地址,也就是说我们能写host进程空间的任意地址,长度为0x20。
1 | .bss:0000000000602100 unk_602100 db ? ; ; DATA XREF: sub_400B92+8C↑o |
然后,再往后写就是host进程的堆空间了,这部分需要动态调试查看其内容,找到对我们有用的部分。
(调试过程省略,该环境使用的lib版本较低,是libc2.23,存在fastbin)
在堆空间中有一些libc的地址,利用这个可以在code中计算libc基址。
我发现,如果”guest name: “,”guest passwd: “和”host name: “都输入相同长度字符串”xxxx”时,三者分配的是同一个chunk。且chunk的后8个字节存着一个固定的libc地址。
错位写入
通过以上分析,我们具备了如下能力:
任意地址写(目的地址可控,写入内容可控,长度为0x20)
已知libc基址
所以,我们可以覆写host进程的got表,将puts("Bye!")
打印劫持成执行一段gadget。
但是,由于memcpy(dest, *(const void **)&nbytes[1], 0x20uLL);
中nbytes[1]的第一个字节一定会被我们的输入覆盖,所以如果将dest地址改成puts@got的话,无法写成目标gadget地址。幸好我们能写0x20大小的内容,因此通过错位写入,就能达到目的。
ps. 由于泄露的libc地址是保存在shellcode的执行环境中的,并未泄露给我们,因此需要在我们的shellcode中将gadget地址写到nbytes[1]对应的堆块中。这样也能达到hook got表项的目的。
其他
做题过程中发现的一些认为重要的点。
buf未初始化
main函数中的buf没有初始化,所以我们将"your code size:
“的大小设置为最大值4096时,栈中残留数据会被带入我们运行的kvm虚拟机可访问空间内。这里面存在libc相关的地址,也是泄露libc基址的一种方法。
1 | __int64 result; // rax |
readline是什么
sub_400F28()这个函数被调用了三次,两次在kvm执行代码前,一次在kvm执行代码后。这个函数中使用了readline,关于这个函数,简单解释如下。
readline() 的参数是一个字符串,调用函数的时候会在屏幕上输出,这个函数会读取一行输入,然后返回一个指向输入字符串的指针,readline 会为输入的字符串动态分配内存,所以使用完之后需要free掉。——来自readline库的简单使用
1 | void *__fastcall sub_400F28(__int64 a1) |
所以这里会使用堆,且堆的大小和内容,我们可控。
调试
注意点:
- host需要开启kvm支持
- docker container启动命令需要加
--privileged
,使docker中能调用到对应的功能
1 | docker container run --privileged --rm -p 8000:8888 -p 1234:1234 -d mykvm:latest |
利用
shellcode编写
汇编代码详细解释
1 | .code16gcc ; 指明编译的程序是运行在16位x86实模式下的 |
在写shellcode时,由于对x86实模式下的汇编规则不熟,遇到些坑点。如:
- eax用于访存和add指令相关的操作时无法正常工作
- ds寄存器的设置与取消
- edx中存放的值间隔几条指令后可能被清零,而ebx不会
exp
1 | from pwn import * |
精简版exp
1 | from pwn import * |
续:进入实模式的利用
官方的方法是通过进入保护模式,使shellcode能访问到更多的内存,然后泄露libc地址,最后写got表及dest(“/bin/sh”),达到执行system("/bin/sh")
的目的。(应该是出于gadget有environ的限制,所以未使用gadget直接get shell)
从实模式进入保护模式
关于如何从16位实模式进入32位保护模式,可以参考这篇文章—— [原创]16位实模式切换32位保护模式过程详解.
从实模式进入32位保护模式需要完成如下几件事情:
- 屏蔽中断
- 初始化全局描述符表(GDT)
- 将CR0寄存器最低位置1
- 执行远跳转
- 初始化段寄存器和栈指针
以下汇编代码便是完成上述功能的一个框架,可以根据需求在 ; your function code
处编写在保护模式下运行的代码。
1 | org 0 |
尝试写写
以本题为例,我们尝试写堆空间(超过1M访存),如下是功能代码:
1 | mov eax, [0x7100] |
编译命令:
1 | nasm test.asm -o test.bin |
脚本中加载读取test.bin的汇编内容
1 | from pwn import * |
结果如下,对应的堆地址处被写为52 52 52 52
,且hex(0x1c05010-0x603000) = 0x1602010
距离超过1M。说明此时在32位保护模式下,我们能直接访存的空间更大了。
1 | gef➤ heap chunks |
保护模式下的利用
进入保护模式后,由于访存能力更强,我们可以省略暴破的环节,同时不用栈中的libc地址了,而是直接取堆空间中存在的libc地址
1 | ; ...... |
将以上汇编合入框架中,编译成test.bin,攻击脚本如下:
1 | from pwn import * |
可成功获取shell。
汇编那一段,可以更简化,CS和DS段是必须的,其他段可以删除。见test.asm。
kvm跟host的交互
in/out指令,官方WP中利用这个指令泄露libc地址,我的方法中未使用
本题中,利用out
指令返回一个字符"W"
,脚本如下:
1 | from pwn import * |
debug信息证明在保护模式下成功执行了shellcode
1 | [DEBUG] Received 0x1e bytes: |
续:什么是实模式和保护模式
x86下,系统上电经过bios自检后进入实模式。
实模式下采用段寻址方式,可直接访问物理地址。此时的通用寄存器位宽只有16bit,借助段寄存器(cs,ds,ss,es)可将寻址能力扩展至1M范围。(linear address = segment << 4 + offset)
实模式显然无法满足计算机日渐增长的访存需求,于是出现了保护模式。不仅扩展了访存能力,同时也提高了安全性。
保护模式中引入分段和分页的概念。
逻辑地址 –[分段]–》 线性地址 –[分页]–》 物理地址
- 分段
画了一张表,理清了段选择子,GDTR,GDT之间的关系。(注意:这张图是针对x86 32位架构的。x86 64架构下,GDTR中基地址是64位)
- 分页
CR0寄存器的PG标识等于1时,表示启用分页机制
CR3 页目录基地址寄存器PDBR(Page-Directory Base address Register)