ept hook一直是二进制安全领域特别有用的工具,特别是在windows内核引入patchguard之后。传统的ept hook一般使用影子页切换,但实践中发现存在代码自修改,多核同步,host环境容易被针对等问题。对此,程聪先生介绍了如何利用kvm,配合模拟器实现无影子页ept hook,巧妙的解决传统方法存在的问题。
在分析恶意样本时,常常会被反调试干扰,比如常见的软硬件断点检测。本议题介绍了如何利用kvm给现有的调试分析工具(ida,x64dbg…),在无需任何改造的情况下,增加隐藏软硬件断点等反调试能力,实现降维打击。
![2022 SDC 议题回顾 | 基于硬件虚拟化技术的新一代二进制分析利器 2022 SDC 议题回顾 | 基于硬件虚拟化技术的新一代二进制分析利器](https://www.ctfiot.com/wp-content/themes/onenav/images/t.png)
下面就让我们来回顾【看雪第六届安全开发者峰会】上《基于硬件虚拟化技术的新一代二进制分析利器》的精彩内容。
现就职于阿里云安全团队,曾就职于腾讯云虚拟化团队,拥有7年安全对抗经验,擅长内核安全、虚拟化安全、逆向分析、二进制攻防对抗。
大家好,我是程聪。很开心能来参加看雪安全开发者峰会,我今天给带来大家分享的议题是基于硬件虚拟化技术的新一代二进制分析利器。我现在就职于阿里云安全系统安全团队,主要的研究方向有病毒检测、主机安全、内核安全、虚拟化安全和二进制攻防。
我今天给大家分享的内容主要包含以下几个方面:第一部分是背景介绍,第二部分是qemukvm简介,第三部分是无影子页的ept hook,第四部分是虚拟化调试器,第五部分是内核级 trace,最后是总结部分。
我们先来看一下背景介绍,大家看一下这张图,如果是搞windows内核安全的,对这个图应该不陌生,在windows它在64位引入patchguard之后,我们如果对内核的一些敏感的部分进行patch,进行hook这种操作它都会触发一个蓝屏,它就是为了防止我们对内核进行胡乱的hook,但是有很多场景其实我们还是需要对它进行hook的,比如说做沙箱或者做一些端上的防御产品。
![2022 SDC 议题回顾 | 基于硬件虚拟化技术的新一代二进制分析利器 2022 SDC 议题回顾 | 基于硬件虚拟化技术的新一代二进制分析利器](https://www.ctfiot.com/wp-content/themes/onenav/images/t.png)
然后安全研究人员就发现可以借助硬件虚拟化的特性,实现了ept hook, ept hook他就可以来兼容我们 的patchguard,相当于对它进行一个绕过。
但是我在实践中发现传统的ept hook一般都使用影子页表来切换,实现欺骗,我们发现这种方法其实是存在一些问题的,它在某些case下,它其实是不工作的,然后我们今天会介绍一种新的方法,来巧妙的解决现有方法存在的问题。
我们要实现以ept hook,首先需要一个类似于下图这样一个虚拟化平台,然后我们在这个虚拟化平台上面再去实现我们自己想要的功能。
![2022 SDC 议题回顾 | 基于硬件虚拟化技术的新一代二进制分析利器 2022 SDC 议题回顾 | 基于硬件虚拟化技术的新一代二进制分析利器](https://www.ctfiot.com/wp-content/themes/onenav/images/t.png)
我们可以看到虚拟化平台包含了vcpu,设备,内存,中断,半虚等管理。从这个图中我们也能看出虚拟化平台可能包含的面比较多,所以我们要从头开发一个这样的虚拟化平台,工作量是很大的。
所以我们一般要实现 ept hook,我们一般会基于现有的一个虚拟化平台进行二次开发,但是我们一些专注于安全的这种开源的虚拟化平台,比如说比较出名的hyperplatform,它这种平台的话,它或多或少会存在各种各样的问题,一般我们也称之为玩具的vmm,比如说你写一些垃圾的msr,或者你对一些cr、dr寄存器进行操作的时候,它可能会崩溃,因为它不是一个商业化的,所以基于这个问题,我们最终是选择了qemukvm来做二次开发。
因为相对来说qemukvm,它的优势就是它一个是开源,一个是它有商业化落地的产品,我们云上一般用的比较多,我们这次分享就会基于 qemukvm来打造无影之页的ept hook,虚拟化调试器和内核级trace工具。
我们为什么要选择qemukvm,相比hyperplatform等虚拟化平台?它有以下优势,我就不一一读了。
![2022 SDC 议题回顾 | 基于硬件虚拟化技术的新一代二进制分析利器 2022 SDC 议题回顾 | 基于硬件虚拟化技术的新一代二进制分析利器](https://www.ctfiot.com/wp-content/themes/onenav/images/t.png)
我重点说一下它支持GPU透传,然后他还支持嵌套虚拟化,它可以在 guest里面再去运行一个我们的 vmm程序,然后其他的比如说他是开源的,它背靠Linux内核,我们在开发的时候可以用Linux内核的各种基础设施。介绍完它的优势,我们来对它做一个简单的介绍。
我们看这张图,我们从最下面开始看,它的整体架构的话,最下面就是硬件部分,它包含了CPU,内存设备等,然后Linux内核是运行在这个硬件之上的,然后 kvm作为一个ko,它是运行在内核做一个驱动程序运行的。
然后再之上就是我们的普通的应用程序,然后这里有个比较特殊的就是qemu,qemu其实它也作为一个普通的程序,但是它可以配合 kvm驱动,然后来进行一些虚拟化的操作,然后我们在 qemu上就可以运行windows、Linux这种guest os,然后在这之上,他们各自运行自己的应用程序。
介绍完 qemukvm的整体架构,我们来简单介绍一下我们今天可能涉及到的一些硬件虚拟化的基本原理。我们看这张图,我们可以看到左边这张图有两个虚拟机,一个是vm0,一个是vm1,然后虚拟机里面运行的guest os,分别是XP和Linux。
然后一般来说的话,如果是 XP还有Linux,它自己的guest里面的应用程序,在运行的时候不涉及到特权指令,比如说他要一个系统调用,这个时候我们更底层的vmm是不会去介入的,只有当 guest里面的操作系统它执行一些特权指令,比如说我们在 XP里面执行一个cpuid,这个时候vmm才会去介入,因为它执行了一个特权指令,或者说它发生了异常,我们的vmm才会去介入,它发生异常之后会产生一个vmexit,然后我们的 host,也就是我们的vmm处理完之后,再通过vmentry回到我们的guest里面,概括来说就是 vmm平常不会去关心我们的guest里面的操作,直到他访问一些特权指令或者产生异常时候,我们才关心,然后这就打造了一个 vcpu在 guest跟host之间的一个循环。
然后说完 CPU虚拟化,再来重点说一下内存虚拟化,因为今天的议题可能跟内存虚拟化关系更大一些。
我们先看这张图,这个是在没有虚拟化的场景下,我们的内存地址翻译其实比较简单,直接 va也就是虚拟成内存,直接翻译成物理内存,在x86上是通过cr3指向的页表来翻译的,有了虚拟化之后,这个情况变得复杂了一些,这个va和pa变成了guest的va,就是guest的里面的虚拟内存和guest的物理内存,这个翻译它发生在guest的内部,但是我们翻译到GPA之后, guest的并不是最终的物理内存,还需要通过一个ept页表才能翻译成我们最终的hpa,也就是我们host的物理内存,所以它比没有虚拟化的场景下多了一步。
它除了要从guest的虚拟内存翻译到guest的物理内存之外,它还要从guest的物理内存再翻译到最后的host的物理内存才完成了整个翻译。
同时我们还要注意到 qemu的 hva,就qemu的虚拟内存也会映射到host的物理内存,我为什么要提这一点?因为这个是在实践当中它有个坑,因为qemu可能把 hpa这个物理页换出去了之后,它也会导致 gpa映射的物理页失效。
然后这个ept的翻译,其实刚刚我们说到的 cr3的翻译是类似的,它也通过多级页表实现的,然后我们在这个里面就可以去,比如说手动的去掉某些权限,我们就可以达到一个监控的目的。
比如说我们把这读写权限去掉,当guest里面发生了对某个物理页的读写的时候,我们就可以对它进行一个监控,或者对它进行一个拦截。
然后它还有一个特点就是我们 ept相关的操作是发生在,我刚刚圈出来 Linux host 上的,对我们的guest是不可见的,就是 host里面对ept操作对guest是不可见的。
所以我们在进行一些攻防或者说进行对抗的时候,可以对 guest里面的一些动作的话进行降维打击的,比如说反调试之类的,我们可以进行降维打击进行绕过。
简单介绍完前面的qemukvm的架构,还有虚拟化的基本原理,我们来到今天的第一个主题就是无影子页的ept hook,说无影页的ept hook之前,我们先来说说什么是影子页ept hook?
![2022 SDC 议题回顾 | 基于硬件虚拟化技术的新一代二进制分析利器 2022 SDC 议题回顾 | 基于硬件虚拟化技术的新一代二进制分析利器](https://www.ctfiot.com/wp-content/themes/onenav/images/t.png)
假设在我们没有进行hook之前,没有进行hook之前,他是地址翻译就跟刚刚的内存简介里面说的一样,它是从 gva翻译到gpa,而gpa在最终通过ept翻译到最终的物理页,然后以我们假设我们需要进行hook的话,我们就需要创建一个影子页。
我们假设我们想进行一个hook,我们可以在这上面创建一个影子页,把这个影子页只保留执行权限,然后我们在这个职业上进行一个inline hook,这样子当然有 CPU执行到执行到影子页的时候,它就会触发一个hook逻辑,但是如果patchguard或者其他的程序过来读取页的时候,我们就会产生异常。因为我们这个影子业只有一个执行权限,它就会产生一个异常,产生异常就会vmexit到我们的host,host上就捕获到这个事件,发现你想读取一个执行页,我就给你切换了读写页,然后读写页,里面的内容是跟原始页一样的,没有进行修改,所以patchguard去扫描的时候发现页并没有修改,但是我们的hook逻辑是生效的,如果后续还有cpu再执行,我们切换到读写页的情况时,因为我们现在只有读写,所以他想执行的话,我们又会触发一个vmexit再给它切换回来。
概括的话他需要执行的时候我们就给他执行页,他需要读写的时候我们就给他读写页,然后我们的host负责来切换读写和执行页,然后他如果读取的时候,我们就给他最原始的内容,这样子他就发现我们并没有进行一个hook,这种方法存在什么问题?我们来看一下它存在的问题,右边这张图跟刚刚是类似的,只不过它多了一条指令,就假设在我们要进行hook页面上它多了一条指令是mov rcx,[rip],这条指令有什么特点,它会读取自身的页面。
因为我们刚才说它如果遇到执行页,我们想去读写执行页的时候,它就会切成读写页,如果是遇到执行读写页的时候,它就会切换回执行页,但是这个情况他会在执行的时候读写自身,那我们假设我们在影子页上执行,他想去执行的时候,他发现它会读取自身,它就会切到那个原始页面,但是那个原始页面。又没有执行权限,所以它又得切回来,它就会导致一个死循环的问题,然后就会进入一个死锁。
其实这种自己读取自己当前页面的情况是很常见的,我下面列了两个例子,一个就是如果看windows内核的反汇编比较多的话,可以看到windows内核里面有很多比较大的那种函数的话,它有switch case会生成一个jump table,这个jump table就在当前的代码页面里面,然后他去读取的时候就会产生这种情况。
还有一个比较常见的用户态的自修改,这种一般是在一些壳对抗代码中比较常见,存在这种问题,我们怎么对它进行改进,我们来看一下改进的方案。
因为刚刚的一个核心的原因,就是因为我们想既执行又去读写。
我们回到一开始的那个图的话,我们可以想象一下,我们如果从影子页切换到原始页的时候,我们不把执行权限给去掉,这样子从影子页切换到原始页的时候,它不就既能执行又能读写了吗?
但是这里还存在一个问题,假设我们从影子页切换到原始页的时候,然后我们没有权限再给它切回来,然后后面的hook逻辑,后面的CPU在执行这个页面的时候,hook逻辑就会失效了,所以我们得让他去切换过去之后,我们让他得执行一条指令之后再给它切换回来,这样保证后续的 cpu在执行这个页面的时候还是有hook逻辑的。
我们怎么给它切换回来?一般是使用MTF,MTF就是 x86提供了一个 flag,我们给guest设置这个flag之后,它会执行完一条指令之后,它又产生一个vmexit。
然后这个exit的理由就是mtf,我们就相当于我们假设我们在影子页上,执行到既执行又读写的指令的时候,我们单步的去切换到原始页,然后原始页执行完这一条指令之后,他再切换回来,这种方法就解决了刚刚前面所提到的自己读取自己修改的问题,改进之后还有什么问题,其实这里是有一个空隙,我们看一下这个空隙,我们刚才说它如果单步的从影子页切换到原始页,就是切换到我们原始页,就是可读可写可执行的页面的时候,我们不是最后会给它切回来吗?
![2022 SDC 议题回顾 | 基于硬件虚拟化技术的新一代二进制分析利器 2022 SDC 议题回顾 | 基于硬件虚拟化技术的新一代二进制分析利器](https://www.ctfiot.com/wp-content/themes/onenav/images/t.png)
但是有个问题就是我们在切过去执行单步执行这条指令的这一时刻,如果有一个其他核,我们看最右边?有个其他核它在执行这个页面,那么这个核的hook逻辑就会失效了。
这个问题的本质的原因是是因为什么?它本质的原因是因为我们刚刚说一个核它在切换页面权限的时候影响了另外一个核,假设下面是cpu0,cpu0从影职页切换到原始页的时候,就这个时刻,然后我们另外一个CPU去执行,相当于是我们当前CPU把页面权限切换的时候影响了另外核,然后一般出现这种情况我们怎么解决呢?
我们可以给每个盒都设置一套ept页表,它自己维护自己的权限,但这种存在一些问题,它一个是内存仓比较多,因为你有多套的ept页表,另外一个它的性能损耗也比较大,因为它涉及到一些同步,前面提到hyperplatform就是使用这种方案,但是我们使用kvm这种商业化落地的vmm,它是不使用这种每个核一个页表,它是共用一套页表的,我们在共用一套页表的情况下,我们又想解决这个问题,我们怎么做?我们来进行一些思考。
其实前面核心的点ept hook最核心的点就是页面切换,我们为什么要从刚刚说的执行页切换到读写页,就因为我们想欺骗cpu,让他读取到我们修改前的内容,这样子,他就以为我们没有再进行inline hook,我们有没有办法不去切换页面,同时可以欺骗CPU读写?我们这里的方案就是模拟执行。
我们看一下模拟执行的情况,使用了模拟执行的话,整个情况就变得非常简单了,我们不再需要去创建一个影子页,我们只需要把原始页面的读写权限给去掉就行了。
读写权限去掉,然后我们对这个页面只保留一个执行权限。
我们把原始页面的读写逻辑去掉之后只保留执行权限,然后我们去对它进行inline hook,可以看到我们假设这个页面是0x1000,然后我们尽量会把头部改成jmp,这样CPU执行的时候就会触发一个hook逻辑,但是假设现在patchguard,他来读取这个页面,或者我们右边有一个示例,就是我们CPU执行mov rax,[0x1000]来读取这个页面的时候,它就会产生一个异常,因为这个页面没有读写权限,这个时候我们就会产生一个vmexit,因为产生异常exit到我们的host上,然后我们host就通过一个模拟器去模拟,说你要读0x1000这个页面对吧?
然后我模拟的时候给你的值是修改前的,我还是修改前的sub rsp,0x88,这样子你去读取的时候读取到的最开始的内容,这样子我们就不需要用影子页来对cpu进行了一个欺骗。
总结来说就是涉及到非ept异常的,比如说我们最右边的里面有两条指令,一条mov rbx,[0x2000],一条是mov rax,[0x1000],如果我们因为我们这个例子里面只对0x1000页面设置了异常,所以我们0x2000是不会触发ept异常的,所以对于这种不会触发ept异常的时候,我们使用的就是真实执行,直接在CPU上执行的,但如果涉及到ept 异常,比如说0x1000这个页面的话,它就会触发一个模拟执行,总结来说就是模拟执行,然后通过模拟执行去欺骗cpu,有了前面模拟执行的方案,我们选择什么样的模拟器呢?
这有很多比较出名的模拟器,就是大家用的比较多,比如说unicorn、bochs,但是我们这里其实都不是选择这些模拟器,我们选择其实是kvm里面自带了一个x86的模拟器,它原本是为了去执行一些mmio的操作,他不想回到用户态,然后我们就可以直接复用它这里面的模拟器,具体代码在emulate.c里面,内核的emulate.c里面,然后我们需要对模拟器进行一些修改,一个是它最底层的那些读写函数进行修改,一个是它整体的一些结构进行修改,然后我们就可以在它读取指令的时候,我们用原始的指令去替换,这样子我们就使用了 ept加上模拟器,实现了一个没有影子页的ept hook,解决了刚刚前面提到的影子页,页面切换存在的一些问题。
有了这种软硬结合的思路,我们还发现就实现一些其他的比较硬核的工具,比如说虚拟化调试器、内核级trace工具也变得非常容易,我们来接下来具体看一下。
基于前面的思路,我们来看一下怎么实现虚拟化调试器。说到虚拟化调试器之前,我们先看一下普通的调试器,我们看这张图,一般的调试器它可能包含几个部分,一个就是断点机制,比如说软件断点硬断点,断点在出发的时候可能涉及到一个异常分发,异常分发,比如说在windows下面可能会涉及到一个debugport,这个时候它就会把这个事件分发到它对应的debugport里面,然后对应的程序里面去,分发到我们的调试管理程序,调试管理程序里面,可能它涉及到一些断点的管理,模块管理,符号管理。那什么是虚拟化调试器?
我们这里所说的虚拟化调试器,就是将这里面的一些比较容易被对抗的一个是断点机制,你在下断点的时候可能被检测。
另外一个就刚刚说的异常事件分发,可能涉及到debugport之类的,这块也是比较容易被检测,我们把这块的,比较容易被检测的这块,通过虚拟化的方法来实现,这样子那些反调试或者那些反分析就对我们完全无效了。
由于这块异常事件分发它涉及到的点可能会比较多,所以我们今天重点来介绍一下怎么使用虚拟化来实现我们断点机制断点。
我们知道一一般包括软件断点和硬件断点,然后我们先来看一下软件断点,我们先来简单介绍一下软件断点的一个基本原理,其实软件断点在x86里面也比较简单,就是int3指令,它的二进制代码就是0xcc,当我们下了软件断点之后,程序执行到 int3指令,它就会触发一个异常。
随后这个异常通过刚刚的异常事件分发,然后就会分发到我们的调试器,调试器介绍这个异常之后,它就能对这个断点进行处理了。
我们刚才也提到这个断点是需要写入一个int3,所以它是很容易被对抗的。一般常见对方法我们可以对函数算一个crc,他如果修改了,我们就发现,或者函数头部,我们就看他是不是刚刚 opcode,看看是不是0xcc这个指令。
然后我们看一个具体的对抗的实例,这个例子也比较简单,其实它会不停的去打开文件,打开一个a.txt文件,然后它不停的去操作,但是它随后他会对 CreateFile头部它检测一下是不是就下的软件断点,如果下的软件断点,它就会走一个异常,然后退出的流程。
假设我们想调试这个程序,但是我们又想在 createfile上面下软件断点,我们怎么隐藏这个软件断点呢?其实有了前面 ept hook的方案的话,隐藏软件断点变得非常简单,我们刚刚ept hook是需要对那个页面进行一个inline hook,这个时候其实我们同样的把原始页面的读写前面去掉,然后我们在它执行页上面头部插入一个软件断点int3,然后这个时候假设比如说刚刚那个程序它来去校验的时候,它就发生一个读写对这个页面,这读写的时候就会触发我们的 vmexit到host,host就根据模拟器,模拟器同样的给他一个假的指令,让他去读取到是sub rsp,0x88,这样子它就检测不到我们下的一个软件断点。
介绍完前面隐藏软件断点,我们应该怎么样来打造一个具有隐形隐藏软件断点的调试器呢。其实如果大家研究调试对抗这块,应该也知道就有类似于这种hyperdbg,它是从头开发一个调试器,借助于虚拟化的,但是我们研究发现这种从头开发存在一些问题,就一个是他工作量比较大,他要从虚拟化层面,然后从调试器的交互层面全部都要重写。
![2022 SDC 议题回顾 | 基于硬件虚拟化技术的新一代二进制分析利器 2022 SDC 议题回顾 | 基于硬件虚拟化技术的新一代二进制分析利器](https://www.ctfiot.com/wp-content/themes/onenav/images/t.png)
另外一个其实它交互整体是不如商业化产品,比如说ida类似的产品友好的,还有一个就是用户从别的调速器切换过来成本也比较高,所以我们编写虚拟化调试器方法是选择去加持现有的调试器器,比如说比较流行的x64dbg、Ida,这样一方面我们切换成本比较低,另外一方面它原本调试器是商业化产品也比较稳定,但是这有一个问题,就是x64这种产品它是开源的,所以我们很好的去可以去找到他什么地方下断点的流程,所以我们可以给它下断点流程进行修改,把它下的软件断点变成一个隐藏软件断点。
但是对于这种ida这样的闭源产品,我们怎么去修改?假设我们不在任何patch的情况下,怎么去给它添加一个隐藏软件断点,我们先看看左边这张图,其实就是假设我们没有虚拟化场景,ida去对一个软件下软件断点的过程,我们可以看到它主要的流程就是它会向被调试进程写入一个int3,然后接下来会调到 readprocessmemory去用系统的用户态API去执行写入操作,然后再到内核的话,它最终会掉mmcopyvirtualmemory。
然后我们现在有虚拟化之后,我们可以对 mmcopyvirtualmemory进行一个ept hook,我们进ept hook点之后,我们就可以在这里检测是不是ida在下软件断点,如果它是下软件断点,我们通过一个vmcall就会通知到我们底下的host,或者说我们这里是kvm,通知到kvm,让kvm把显式的软件断点转化成一个隐藏的软件断点,就根据我们前面的原理,然后我们看一下一个具体的隐藏软件断点的例子。
我们看最这个图的左上角,这个图的左上角的话,其实就是刚刚那个程序,刚刚检测软件断点的程序编译后的一个汇编,我们可以看到它其实就是不停的去操作文件,这里是不停的去操作文件,操作完文件之后他就sleep了一段,然后随后他就检测 createfile是不是被下断点,如果被下断点他就会走到下面这个流程,下面流程就是异常退出了,说明他发现了调试器,但是我们这张图片中,我们确实在那个地方下了断点,但它不会退出,为什么?
是因为我们箭头这条指令触发了模拟执行,然后模拟执行对它进行了欺骗。
然后我们看下面这张图是我从kvm中打印出来的日志,我们也可以看到模拟执行的 rip,0x7ff646411058,它跟我们箭头的 rip是能对应上的。我们来看一下我们一个具体的视频演示,这是我在一个ubuntu的物理机,然后里面运行了一个windows的 guest,然后我们的ubuntu内核是5.4.0,然后我们再看一下我们的 windows的里面的内核版本是18363的,然后我们首先来演示一下这个场景下是没有虚拟化加持的,我们看一下,虽然开了虚拟机,但是我们没有虚拟机加持的场景下,我们打开刚刚软件断点调试的软件断点检测的例子,我们可以看一下它是一个什么样的效果,没有虚拟化加持。
它同样就刚刚说的例子,它会不停的操作文件,随后去检测检测,看看有没有下软件端点,如果下软件端点它就会执行到一个异常流程。我们稍等一会看看运行起来是个什么状况,就这句指令去检测的,检测完了如果发生异常的话,它就会走下面的。然后我们接下来让它运行起来看一下,我们下两个断点给它运行起来,默认情况下,因为我们没有再CreateFile下断点,所以他肯定是能够一直不停的去执行的。
然后假设我们现在去createfile下一个软件断点,它就写入了一个int3,这样子。下面这个逻辑就是1058那句的逻辑,它就会检测到这个软件断点了,随后就会弹窗,然后退出了。我们看它那个箭头在闪,说明它会走下面这条逻辑,是一个异常逻辑。
我们运行来看它确实弹窗了,检测到你在调试它了,然后他就会异常退出了,这个是没有虚拟化加持的,然后我们怎么让他去有虚拟化加持能力,其实我们只要把 ida原封不动的拷到我们一个特定的目录下,这是我在内核里面写死的,就一个叫vt_hidden的目录下,我们只需要把 ida拷过来,他就有了虚拟化的加持,他对软件断点就进行了一个隐藏。
我们同样看看,看一下刚刚的那条软件断点检测的例子,这个程序是跟刚刚一样的,我们同样下断点让它运行起来,我们可以看到它运行起来,但我们没有在createfile下断点之前,他肯定跟刚刚是一样的,肯定可以一直不停的在运行的。
假设我们现在去createfile下个个,当年我们可以看到createfile的一个地址是7ffb251d2090,我们记住这个值,当然后面的日志里面也会显示出来,我们现在下的断点跟刚刚的区别就是我们除了写入一个int3之外,我们还把它的读写权限去掉, createfile所在页的读写权限我们给它去掉了,我们来看一下我们现在下点能不能触发,然后触发之后我们这个断点能不能被检测。
我们看一下我们运行了一下断点,现在触发了,确实能够执行到这段,也说明我们断点是下成功了。
我们再看一下他会不会走到一个异常退出的流程,就是1058的那句7ff6c78c1058这条指令,它能不能检测到我们在下断点呢,我们运行了一下,我们可以看到这次它不会再走,它闪烁的箭头是往上走的,说明它还是在一个原本的程序的流程里面继续去循环,而不会走到我们下面这个异常流程里面来。
我们稍等一下,可以看到它,他去检测那条语句,我们刚才说了它是模拟执行,我们可以看到它会留下来一条模拟执行的日志。然后我们可以看一下kvm里面的日志。
kvm里面他说模拟的 IP就是7ff6c78c1058,然后模拟的时候对他进行了欺骗,算是一个偷梁换柱。
然后我们可以我特意把 rax打出来了,rax就我刚才说的 createfile的它的虚拟内存,然后这里面 gpa其实就是他guest的虚拟内存对应的guest的物理内存,然后我们又运行了一遍,然后我们可以看到更多的日子,我可以断点还是继续触发了,说明我们断点还在生效。
我们又走到刚刚模拟执行的逻辑的,我们看看现在又多了一条日志,说明他一直在模拟的执行,如果我们现在给它断点去掉,这个时候它就不会再有模拟执行的一个逻辑了,相当于对它进行了一个绕过。
我来说说我实践过程中这个地方踩的坑,主要是包含几个问题,第一个 guest的内存可能会被交换到磁盘,比如说刚刚的createfile,它可能假如说他是一个换页内存,他可能会被交换到磁盘,这时候它 GVA跟GPA的一个映射关系就不稳定了,我们怎么解决?
我们其实解决方案也比较简单,我们就不要让他换页,用mdl,在windows下用 mdl去给他锁住,然后就可以防止它换页。
第二个问题 copy on write问题,比如说刚刚的 createfile,它在一个dll里面,然后系统为了节省内存的话,它会把所有进程里面的 dll映射到同一个物理内存,这样子他的GVA 跟GPA的映射,guest的虚拟内存核guest的物理内存映射就不是唯一的了。不唯一的话,假设我们下个断点的话,就下因为我们下的我们断点是需要改变GPA的权限,所以它就会影响到所有的进程。
我们怎么保证它唯一我们的方法就是向里面写入一段相同的内容,写入一段相同内容之后,它就会触发到 cow,触发 cow之后,我们 GVA到GPA的映射就唯一了。
第三个是qemu换页导致的一个ept异常,我们最开始在介绍内存虚拟化的时候,我们特意说了 qemu,它的 hva跟它的hpa的映射关系会影响到EPT的映射,然后qemu换页的话,就相当于他自己的 HPA跟他的HPA映射关系,就相当于被切换走了,所以我们这里的解决方案就是在它启动的时候加一个mlock,这样子相当于把它映射关系给锁定了。
然后说完这个软件断点,我们再来简单说说硬件断点,x86提供了8个调试寄存器,dr0到dr7用于硬件调试。其中前4个就是dr0到dr3是硬件断点寄存器,我们可以放入一些内存地址,然后或者io地址放进去,然后等到CPU执行到满足条件的时候,他就会停下来,一般我们可以用于监控数据读写,因此也叫数据断点。
硬件断点是很强大的,因为是它CPU自身提供的一个机制,但它缺点它只有4个,因为它只有dr0到dr3,同时它也比较容易被检测到。
我们来看一个具体的检测的例子,这个例子也比较简单,就是它不停的去我们有一个global变量,不停的去读取这个变量,随后把它打印出来,然后它打印完之后,它就会去检测一下这些dr寄存器有没有设置值,如果有设置值说明有人在下硬件断点,比如说假设我们在global变量里面下了一个硬件,这个时候dr0就有值了, dr0有值了之后,它就会异常退出,说明它检测到调试器在下硬件断点。
同样的问题,我们怎么对硬件断点的进行一个隐藏?其实有了前面的这种ept hook,隐藏软件断点的逻辑,隐藏硬件断点就变得非常简单了,他甚至都不需要patch内存,假设我们想监控一下谁去读写0x1000这个地址,我们只需要把这个页面它的读写里面去掉,当有人读写它的时候,它就会触发一个模拟器逻辑,然后模拟器逻辑就会去匹配当前的线程断点,然后如果匹配成功了,他就会向guest注入一个#DB异常,注入#DB异常其实比较简单,我们直接在KVM里面就有现成的函数,一个比较棘手的就是在 host中、kvm中怎么获取guest的当前线程,这个地方是有比较多的坑,我们来看一下我们怎么在kvm中获取guest当前线程。
我们先来看一下guest的自己是怎么获取当前线程。我们以 windows 64位内核为例,我们可以看到代码非常简单,它获取当前线程的方法就是取了一个gs,gs断寄存器base偏移188的一个位置取内存,我们直观能想到在kvm中的获取方法是什么呢?我们也执行这段代码,这个显然是不行的,因为这个时候GS不是guest的,而且cr3也从guest切换到host,所以内存是不可访问的。
我们使用另外一个方案,就是GS base就是说的是我们guest退出的时候的GS,这个GS是对的,但是我们测试发现它是需要修正的,它是有问题的,然后我们修正完之后,我们就得到正确的GS base之后,我们再用 kvm提供的一个函数,kvm _read_guest_virt去读取,这个函数的原理主要是它手动去解析cr3页表,然后去读取到 guest里面对应的最终物理内存,但是我们发现,它我们修正完GS我们使用这个函数去读取,它依然会失败,我们为什么需要修正?
然后为什么修正后还是读取失败,这个地方也比较坑,我们看一下失败的原因,失败的原因其实就是因为我们软件端点,硬件端点触发vmexit的时候,这个时机是在用户态,也就是R3,这个时候GS其实在windows下面是指向了teb,然后只有在R0的时候它才指向PCR,这个时候KeGetCurrentThread才会得到正确的索引。那什么时候它从teb切换到kpcr,我们看这个图,其实这个是一个windows系统调用的入口函数。
我们看到第一条指令执行的就是swapgs,这条指令就会把它从用户台的 teb切换到内核态的 kpcr,这条指令如果是研究过 CPU投机执行漏洞的,应该不会陌生,然后根据手册的话,这条指令会把 msr[0xc0000102]它里面的值放到 GS base里面,所以我们在kvm里面只需要取一个msr寄存器里面值,然后就能得到正确的GS base,同时配合 kvm_read_guest_virt这个函数,我们就可以去读取,但是我们发现这个函数本身是会check,也会check vcpu的cpl,所以我们在实践中不能用这个函数,需要用它更底层的一个函数,然后我们手动指定 cpl为0。然后硬件断点的视频演示,我这里先就不放了,跟软件断点是类似的,我们只需要把这个放到 vt_hidden的目录下,它就自动获得隐藏硬件断点的能力。
然后我们再来说一说基于前面的思路怎么实现一个内核级trace。说到内核级trace之前,我们先来说说什么是用户态的trace。这张图是我们基于intel pin,这么一个二进制插桩工具,实现了一个trace工具,它可以看到它确实出 ls的一个指令序列,这里我显示的指令序列不是很全,其实他指令序列里面应该还包含了那些其他寄存器的,这个指令序列在实践中是非常有用的,它可以在用在脱壳中,比如说我们想判断什么时候到达壳的入口点,或者说在去虚拟化,去vmp虚拟化当中它也能用,我们假设我们把指令序列配合我们的llvm的ir,配合符号执行,它就能得到一个提升,然后再进行优化,就能达到针对单分支的去虚拟化,这个效果是非常好的。
然后还有一个常见的二进制分析场景,因为这个里面的信息比较全面,我们也可以从里面看看他有没有恶意行为,这是个用户态的一个trace的例子,就是把所有的指令trace出来。
但是我们发现市面上intel pin还有等这种二进制插桩工具,它只支持用户态,它不支持内核态,我们怎么基于前面的思路,借助硬件虚拟化来实现一个内核态的trace,比如说我想对驱动它执行的所有指令,我给它打印出来,比如说驱动我想脱它的vmp壳,我想脱它的upx的壳,我们怎么做呢?
其实整体思路还是前面提到的真实执行加模拟执行,然后我们可以看看怎么将一个驱动程序来生成它的trace全过程。
假如我们有一个xxx.sys的驱动程序,然后我们注册了一个加载回调,这样的加载时候我们就感知到,感知到之后我们就用虚拟化的方法,ept把这个他的执行权限给去掉,执行权限给去掉,然后我们让他去执行到他自己的入口,因为他执行权限去掉,所以他不能执行,他会产生一个异常,这个时候就会退出到我们的kvm里面,kvm里面就用模拟器去模拟执行这条指令。
模拟这条执行这条指令之后,我们就相当于我们可以去单部的一条一条的去模拟,这样子就得到了一个trace日志。
同时假设我们在模拟当中还可以对它的寄存器进行修改,内存进行修改,所以我们还可以实现一个内核级的插桩功能。
我们这种方法的话相比于一些其他的,比如说我们用调试器也能生成trace,单部的去执行的话,它也能生成这种trace日志,或者说我们用纯qemu的模拟,全系统的模拟,但是纯qemu的模拟的话噪音比较大,然后我们噪音比较小,然后我们也不会像调试器那样会被检测到,这就介绍了一个我们怎么样通过软硬结合实现了一个内核级trace的工具。
我们主要介绍了如何基于硬件虚拟化特性配合模拟器,实现了无影子业的ept hook,解决了一些传统方法存在的问题,同时介绍了如何基于qemukvm打造我们的虚拟化调试器,内核级trace等工具,其实可能还有沙箱之类的,这里面不再赘述了。我的分享到此结束,谢谢大家。
注意:峰会议题PPT及回放视频已上传至【看雪课程】
https://www.kanxue.com/book-leaflet-153.htm
PPT及回放视频对【未购票者收费】;
对【已购票的参会人员免费】:我方已通过短信将“兑换码”发至手机,按提示兑换即可~(若没收到,请添加工作人员微信:kanxuecom,备注“2022 SDC”并发送参会订单截图)
![2022 SDC 议题回顾 | 基于硬件虚拟化技术的新一代二进制分析利器 2022 SDC 议题回顾 | 基于硬件虚拟化技术的新一代二进制分析利器](https://www.ctfiot.com/wp-content/themes/onenav/images/t.png)
《看雪2022 SDC》
https://www.kanxue.com/book-leaflet-153.htm
长按识别小程序参与抽奖
(1)公开转发本文(不可设置分组)
(2)兑奖时需出示朋友圈转发截图
![2022 SDC 议题回顾 | 基于硬件虚拟化技术的新一代二进制分析利器 2022 SDC 议题回顾 | 基于硬件虚拟化技术的新一代二进制分析利器](https://www.ctfiot.com/wp-content/themes/onenav/images/t.png)
原文始发于微信公众号(看雪学苑):2022 SDC 议题回顾 | 基于硬件虚拟化技术的新一代二进制分析利器