【  标题  】 小议ring 3虚拟机调试器
【  作者  】 linxer
【  声明  】 俺系初级选手,高手略过。失误之处敬请诸位大侠赐教!


其实虚拟机调试器已经并不是什么新鲜玩意了,估计很多高人都玩烂了,但一直没有看到这方面的文章,今天写代码写的郁闷,决定瞎掰篇文章发上来。

长假漫漫,远离家乡,不能回家,真的很羡慕那些能回家的人,也羡慕那些能趁着长假出去玩耍旅游的人……唉,一个人孤苦伶仃的窝在家里,除了睡觉就是写代码,终日与电脑为伴,研究了下ring 3的虚拟机调试器。原本计划现在应该可以支持调试多进程了,但梦想再度被现实强奸,目前只能勉强跑单任务程序。都怪自己有奢求完美主义的癖好,研究过程中想的东西太多。下面简单介绍下这个东西,高手就请飘过了。

虚拟机调试器,是虚拟机和调试器的结合,代码的执行权限在虚拟机和调试器之间来回切换,这样带来的好处就是程序基本上是步步可控的,程序的行为也一目了然,还有一个好处就是它已经摆脱了传统的下断点方法,只要代码是在虚拟机里面执行,一切断点都是逻辑的虚拟的,这样可以避开一些反调试手段(比如占据硬件断点,CRC效验,API的0xcc断点检,改写内存属性取消内存断点等),总之,它带来的好处是很可观的,但是缺点是速度慢,虚拟机的仿真能力也很重要,要考虑的东西比较多,毕竟虚拟CPU不是真实CPU,利用这个弱点就可以写出针对这种调试器的anti-debug,还有就是虚拟机和调试器的结合地方也可能是新的anti-debug的众矢之的。

下面大致说下这个东西的实现过程,由于水平有限,目前我做的也还不是太好,本可以做好了在写这个的,估计等我弄好了,我就没激情写这个了,我的文章一般都是激情 + 无知的时候写出来的,错误在所难免,凑合着看吧。

一.  X86虚拟机
关于这个,目前论坛上有几篇帖子在讲这个,以前是讲虚拟机脱壳,这个x86虚拟机应该比用来脱壳的简单一些,我们只要仿真基本的寄存器,x86指令识别,寻址系统,指令解析系统就可以了,其它的可以不管,不过为了程序很多时间的执行机会都在虚拟机上,虚拟机的执行系统最好仿真的多些,对那些没有仿真的执行,交给真实CPU去执行。这种执行权限的交替是很费时间的,这也势必要求我们指令解析系统比较强大。
另外虚拟机还必须支持异常的捕获,否则的话,要不会程序流程错乱,要不会虚拟机以后拿不到执行权限。

二.  传统调试器
唉,这个没有什么可讲的,我巨菜,在论坛混的兄弟,都是这方面高手,我就不说了,免得出丢人现眼。

三.  程序执行权限的更替
调试器建立调试进程,断在入口点或者TLS入口上,把真实CPU的状态告知虚拟机,唤醒虚拟机线程,虚拟机开始执行代码,这个时候系统中被调试的线程被调试器设置成阻塞。当发生异常或者要调用系统API(或者要进入内核),这个时候在合适的位置设置个断点,把虚拟机的状态同步到真实CPU上,虚拟机线程阻塞,唤醒被调试线程,让其进行异常处理或者进入内核去工作,工作返回后,会触发进入时设置的断点,这个时候,把断点及时取消,阻塞被调试线程,同步真实CPU状态到虚拟机中,唤醒虚拟机线程开始执行代码。就这样来回进行执行权限的更替。
当然以上是单线程情况下的权限更迭,对多线程多任务的调试,原理和上面一样,但是虚拟机线程基本上不会有休息的时候,它的休息情况由任务调度模块决定。

四.  异常处理
虚拟机中要支持ring 3能出现的所有异常捕获,否则一切都空谈。这也需要对win的异常处理机制比较熟悉,当捕获到异常时,按win的异常处理机制,在马上要执行的第一个异常处理回调函数入口下断点,把权限交给真实CPU,让它进行异常处理,这样当执行到虚拟机下的断点的时候,虚拟机又可以获得控制权了,取出这个异常处理回调函数的返回地址,这个要用来判定什么时候异常处理退出的,在退出的地方要把执行权限再次交给真实CPU的,在退出的地方,虚拟机也要获得下条用户空间代码的位置,设置一个断点,保证异常处理彻底结束时,虚拟机又可以拿到执行权限。当然,上面说的是最简单的情况,比如SEH的展开之类的,会比较复杂,但是处理过程是一样的,关键是要对异常处理流程熟悉。
内存属性导致的异常比较特殊,这里特说下,由于我是在虚拟机上引入了缓存,缓存以页为单位,把进程的内存属性要反映到这些缓存上,我们访问的时候,就会导致同样的异常,虚拟机调试器对这种异常进行捕获,然后它也就O了。

五.  API调用
这个地方一言难尽,说简单很简单,说复杂比有点,关键是虚拟机策略问题。要保证用户空间代码绝大多数都在虚拟机里执行,要看这个。如果一发现API调用,就把执行权限交给真实CPU,这个方法最省事,但是有时候会很无奈,API传回调函数进去了怎么办,像MFC这种恶心的东西咋办。还有一种方法就是对API也放入虚拟机里执行,直到其要进入内核,才让把执行权限交给真实CPU,虚拟机本来就慢,在这样搞,会死人的。
API调用时,如果采用第一种方法,涉及到CPU权限的更迭,如果采用第二种方法,某些API也涉及CPU权限的更迭。这种更迭还是依靠在返回处第一条执行上下断点。

六.  虚拟CPU没有仿真指令的执行问题
对虚拟机里目前搞不定的指令,一律交给真实CPU去执行,比如一些MMX指令,使用频率很低,加上指令仿真特别体力活,去仿真它们需要很大的勇气,还是让真实CPU解决它们吧,这里要注意一点就是,跳转指令还是要全部仿真的,否则执行权限的更替会比较复杂,不复杂的话会没有安全感,把那些乱蹦的指令仿真完了,我们就可以安心的在其紧挨的指令上下断点了,来实现虚拟机和真实CPU执行权限的交互。

七.  虚拟机缓存
这个东西搞的我不行了,如果不从效率考虑,它完全是多余的,没用它的话,每次执行指令,包括指令,操作数都要从被调试进程空间去,执行完后,如果更改了数据,在把数据写回去,系统调用是巨耗CPU时间的,为了不让频繁调用ReadProcessMemory/WriteProcessMemory,最好还是拿点空间出来cache吧,这里面有几个算法要实现,比如怎么cache,cache的边界问题,脏数据的避免等等都是要考虑的,唉,一言难尽,不说了。这几天我很多时间都在排除它的错误,调试虚拟机里运行的程序比较要人命。

八.  对多线程多任务的支持(这块我还没实现,纯属胡说八道)
调试器对进程/线程事件进行处理,当收到创建事件的时候,阻塞之,把它放入活动任务队列,等待虚拟机虚拟执行,被虚拟执行的线程在处理异常或者调用API的时候,这个时候可以选择新的线程进行执行,把原来的线程放入阻塞任务队列中,只要是工作良好的进程线程,按这种方式调度执行应该不会出现问题,因为同步互斥都是win的任务,与虚拟机无关。但是光用上面的方式任务切换是不行的。有些程序会用轮循方式来进行同步,这就要求虚拟调试器有自己的任务调度模块,对活动任务队列中的线程进行调度执行。

好了,就说到这里,吃饭去了。

BTW:伟大的致敬给***大哥,虽然只跟他聊过两次天,他说的很多东西我都听不明白。但是他对调试器的理解让我受益匪浅。当然要说明一点,虽然他跟国内某款虚拟机调试器有着一些关系,但是我们并没有说过这个东西,也没有谈论它的技术问题,他给我灌输的是更高级别的调试器思想。谢谢!