先说下仅仅是一部分,从零开始分析一个vmprotect虚拟机。
Step1:偶然入手一个vmprotect的样本,我这里版本是vmprotect3.0.8,拖入ida,查看区段
啥也没看出来,估计真正入口在text段吧,暂时不管他。
Step2:尝试直接去入口分析
一看这种代码就很坑,慢慢来吧
Step3:尝试用ida画图看下,乱七八糟的
Step4:这么乱的流程图不是我们想要的,所以我们可以尝试写个程序清理出一块代码,基本来说,区分块就是用jmp edi和ret来截断了指令。看起来也很乱
Step5:由于并不清楚这个虚拟机的结构,于是尝试清理代码,去掉一些无用的花指令,先给第一个块去花。过程如下:
前面两个jmp是无效的,nop之
这种衔接cmp,无用,nop之
Step6:没意义的cmp与test,nop之……总是这些规律不难,需要注意的是xchg指令需要花点心思。直接给大家看下清理花指令以后的吧。
开头部分是一堆的push,可能是保存寄存器环境,最后的mov esi,[esp+28h]应该是取出一个key,继续往下分析
Step7:中间对esi和eax做了一系列的变化,
其中esi来源于一个key,eax也来源于一个const,然后一直变化
Step8:最后ret回去,来源于edi,而edi来源于esi和eax
Step9:根据前面的信息,我们大概就理清了一点思路,首先一堆push保存寄存器;然后ebp=esp以后esp就随意变换了。。尽管他也没使用过esp,也就是说,这时候的ebp相当于esp,就是个堆栈指针;最后根据2个key变换esi和eax,eax和esi仅仅用来计算edi,edi用来计算ret的地址。
Sep10:
Ebp:堆栈指针
Esi:数据源
Eax:临时变量
Edi:ret专用返回(跳转)地址
Step11:根据上面的结果,我们只是得到了一个大概,还需要继续分析block2(实际上,所有的block已经获取到),才能分清楚他的机构,以便后期的继续自动化。
Step12:看block2,也就是第二个块,仍旧有很多花指令
Step13:继续尝试根据上面的规律去掉这些花指令,去掉以后如下:
很显然,第二个block也是符合上面的规律的
ebp作为一个指针a,esp+eax作为一个指针b,eax作为一个临时变量可以不用管,edi作为衔接作用也暂时不用管,esi作为一个指向const区域的指针也可以不用管,这些相当于虚拟机的内部,我们都可以屏蔽,我们要做的,就是搞清楚这个block哪些部分是重要的。
Step14:继续观察代码,发现真正有用的代码,只有下图(之所以说其他没用,因为其他代码里面的eax,esi寄存器,都是用来计算下一个代码块的,这些代码对于我们分析虚拟机没什么作用)
Step15:没错,真正有效代码,就是上面这四句,其实就是做了一个从ebp到[esp+eax]位置的变量转移,也可以说说一个 x86寄存器=》vmp虚拟机寄存器的mov
。然后根据之前的分析结果,ebp是堆栈的话,那么这里的ebp+4相当于一个vmp的pop指令,esi作为指令指针继续指向下一条const,这句代码,可以简称为一个
“vmp_push reg”,之所以是push,是因为ebp+4了,而这里变动的堆栈,则是vmp的esp(听起来很绕),同时看出来,他的指令是倒着走的,vmp的eip会减1 ,至于这个reg对应哪个寄存器,因为我们这里仅仅是纯静态,所以很难确定,但是这里可以暂时猜测这一块代码,仅仅是做了一个操作,那就是:把x86的寄存器放入到vmp的区域,如果以后写一个伪指令调试器,则可以直接模拟一个push即可,其他的esi寄存器、ebp寄存器,eax寄存器,忘掉他吧。或者,直接写伪调试器的时候,不去写这几句,因为他们仅仅是在初始化vmp的堆栈,给他们一个初始值。
Step16:对,你没看错,block1是保存了所有的x86寄存器,block2是开始初始化vmp的第一个寄存器,至于block3我们可以大胆猜测,是初始化vmp的第二个寄存器。而这些,如果在做还原的时候,其实是最好忽略掉的,因为来回的堆栈改变会影响我们的分析(这句现在说的很啰嗦,但是后面就知道,这句话会很重要)。
Step17:现在可以先停下来,重新梳理一下思路,已知他会先把所有寄存器保存下来,然后用新的ptr指针代替原操作,那么我们需要做的是,搞清楚他哪些ptr对应哪些寄存器,做了哪些操作。到这里,需要我们获取所有的block了,正好把他的反调试处理一下。
Step1:偶然入手一个vmprotect的样本,我这里版本是vmprotect3.0.8,拖入ida,查看区段
啥也没看出来,估计真正入口在text段吧,暂时不管他。
Step2:尝试直接去入口分析
一看这种代码就很坑,慢慢来吧
Step3:尝试用ida画图看下,乱七八糟的
Step4:这么乱的流程图不是我们想要的,所以我们可以尝试写个程序清理出一块代码,基本来说,区分块就是用jmp edi和ret来截断了指令。看起来也很乱
Step5:由于并不清楚这个虚拟机的结构,于是尝试清理代码,去掉一些无用的花指令,先给第一个块去花。过程如下:
前面两个jmp是无效的,nop之
这种衔接cmp,无用,nop之
Step6:没意义的cmp与test,nop之……总是这些规律不难,需要注意的是xchg指令需要花点心思。直接给大家看下清理花指令以后的吧。
开头部分是一堆的push,可能是保存寄存器环境,最后的mov esi,[esp+28h]应该是取出一个key,继续往下分析
Step7:中间对esi和eax做了一系列的变化,
其中esi来源于一个key,eax也来源于一个const,然后一直变化
Step8:最后ret回去,来源于edi,而edi来源于esi和eax
Step9:根据前面的信息,我们大概就理清了一点思路,首先一堆push保存寄存器;然后ebp=esp以后esp就随意变换了。。尽管他也没使用过esp,也就是说,这时候的ebp相当于esp,就是个堆栈指针;最后根据2个key变换esi和eax,eax和esi仅仅用来计算edi,edi用来计算ret的地址。
Sep10:
Ebp:堆栈指针
Esi:数据源
Eax:临时变量
Edi:ret专用返回(跳转)地址
Step11:根据上面的结果,我们只是得到了一个大概,还需要继续分析block2(实际上,所有的block已经获取到),才能分清楚他的机构,以便后期的继续自动化。
Step12:看block2,也就是第二个块,仍旧有很多花指令
Step13:继续尝试根据上面的规律去掉这些花指令,去掉以后如下:
很显然,第二个block也是符合上面的规律的
ebp作为一个指针a,esp+eax作为一个指针b,eax作为一个临时变量可以不用管,edi作为衔接作用也暂时不用管,esi作为一个指向const区域的指针也可以不用管,这些相当于虚拟机的内部,我们都可以屏蔽,我们要做的,就是搞清楚这个block哪些部分是重要的。
Step14:继续观察代码,发现真正有用的代码,只有下图(之所以说其他没用,因为其他代码里面的eax,esi寄存器,都是用来计算下一个代码块的,这些代码对于我们分析虚拟机没什么作用)
Step15:没错,真正有效代码,就是上面这四句,其实就是做了一个从ebp到[esp+eax]位置的变量转移,也可以说说一个 x86寄存器=》vmp虚拟机寄存器的mov
。然后根据之前的分析结果,ebp是堆栈的话,那么这里的ebp+4相当于一个vmp的pop指令,esi作为指令指针继续指向下一条const,这句代码,可以简称为一个
“vmp_push reg”,之所以是push,是因为ebp+4了,而这里变动的堆栈,则是vmp的esp(听起来很绕),同时看出来,他的指令是倒着走的,vmp的eip会减1 ,至于这个reg对应哪个寄存器,因为我们这里仅仅是纯静态,所以很难确定,但是这里可以暂时猜测这一块代码,仅仅是做了一个操作,那就是:把x86的寄存器放入到vmp的区域,如果以后写一个伪指令调试器,则可以直接模拟一个push即可,其他的esi寄存器、ebp寄存器,eax寄存器,忘掉他吧。或者,直接写伪调试器的时候,不去写这几句,因为他们仅仅是在初始化vmp的堆栈,给他们一个初始值。
Step16:对,你没看错,block1是保存了所有的x86寄存器,block2是开始初始化vmp的第一个寄存器,至于block3我们可以大胆猜测,是初始化vmp的第二个寄存器。而这些,如果在做还原的时候,其实是最好忽略掉的,因为来回的堆栈改变会影响我们的分析(这句现在说的很啰嗦,但是后面就知道,这句话会很重要)。
Step17:现在可以先停下来,重新梳理一下思路,已知他会先把所有寄存器保存下来,然后用新的ptr指针代替原操作,那么我们需要做的是,搞清楚他哪些ptr对应哪些寄存器,做了哪些操作。到这里,需要我们获取所有的block了,正好把他的反调试处理一下。
Step18:刚才说到了反调试。现在就按照严格的顺序,按部就班的说一下反调试怎么处理(不要打开任何反反调试插件,要么就自己写,我们要的是纯手工)
IsDebuggerPresent下断点,这将使第一个反调试点,可以利用它回溯一下堆栈,会看到一句 call eax,这里将是vmprptect调用反调试函数的入口;
CheckRemoteDebuggerPresent,这也也很好处理;返回值改1
ZwQueryInformationProcess,这时的[esp+4]是0x1e,处理的方法是,把返回值eax改成0xC0000353,把[esp+8]的buffer改成0;
NtSetInformationThread 这个需要改他的传递参数,0x11改成别的;
NtQuerySystemInformation 调用号为0x23,这里需要改他的返回OUT参数为0;
NtQuerySystemInformation 调用号为0x0b,这里他会遍历系统模块,一些sys和dll等,这个处理。我么不能直接全部清0,需要和没调试状态的对比一份修改 ,可能有人说直接清零缓冲区就可以。但是在我的系统里面,这样依然会被检测到,也可能是我姿势不对吧,继续
Step19:好了,关于反调试本身现在暂时到此为止,我们要做的,并不满足于如何过掉他,而是看他如何检测和调用的,如果想要过掉他直接用个插件就可以了。回到正题,先以第一个IsDebuggerPresent为例;我们看看他是怎么调用过来的,这对于理解vmp虚拟机,是有好处的。那么根据之前总结的结果,我们知道,代码都是一块一块的,由0xc3和0xffe7截断,也就是一些ret和jmp edi,我们现在,可以获取目前为止的所有block了,先说下结果,经过ida trace,从入口到IsDebuggerPresent,一共调用了1022个block,我们要做的,就是把这1022个block,尽可能的读懂。好了,现在可以开始尝试读混淆过的虚拟机代码了。
Step20:这里可能会有人问如何获取这么多block,其实办法很多,run trace可以,但是最好还是静态解析,然后下断,再f9,这样中途可以截获一些指令,而且效率也比较高一些。好了,现在先贴一下这1022个block的块头
[招生]科锐逆向工程师培训(2024年11月15日实地,远程教学同时开班, 第51期)
最后于 2019-5-9 21:21
被白菜大哥编辑
,原因: