『深圳腾讯2010安全技术竞赛』第二阶段第一题是一道虚拟机相关的题目。由于自己之前只接触过一些概念相关的知识,最多看过VMP虚拟化单句代码的效果,再复杂就搞不来了,所以想借此机会加深下对虚拟机的理解。在此把自己的分析过程整理出来,望高手指教。

废话结束,下面进入正题。

刚看了题目要求之后,并不知道程序中有虚拟机。挂上OD断到程序映射文件完毕,开始分析。

代码:
004010FE   .  837D D0 00    cmp     dword ptr [ebp-30], 0
00401102   .  0F84 B3000000 je      004011BB
00401108   .  8B4D E4       mov     ecx, dword ptr [ebp-1C]
0040110B   .  8139 54455354 cmp     dword ptr [ecx], 54534554
00401111   .  0F85 A4000000 jnz     004011BB
这里是判断文件的长度是否为0,和文件的前四个字节是否为"TEST"。

紧接着call到 00401F10,这个函数最后使用了如下代码调用到00401310这个函数。
代码:
00401F44  |.  B8 10134000   mov     eax, 00401310                 
00401F49  |.  870424        xchg    dword ptr [esp], eax
00401F4C  \.  C3            retn
相当于 push + retn的调用方法。

这个函数OD刚开始没有分析出代码,以为是数据,我们让OD将它当做代码分析代码如下
代码:
00401310      60            pushad
00401311      9C            pushfd
00401312      8B5424 24     mov     edx, dword ptr [esp+24]
00401316      8BC2          mov     eax, edx
00401318      05 94000000   add     eax, 94
0040131D      8038 01       cmp     byte ptr [eax], 1
00401320      74 21         je      short 00401343
00401322      8B5424 24     mov     edx, dword ptr [esp+24]
00401326      8BC2          mov     eax, edx
00401328      05 A4000000   add     eax, 0A4
0040132D      50            push    eax
0040132E      83E8 0C       sub     eax, 0C
00401331      8B00          mov     eax, dword ptr [eax]
00401333      FFD0          call    eax
00401335      8B5424 24     mov     edx, dword ptr [esp+24]
00401339      8BC2          mov     eax, edx
0040133B      05 94000000   add     eax, 94
00401340      C600 01       mov     byte ptr [eax], 1
00401343      8B5424 24     mov     edx, dword ptr [esp+24]
00401347      8BC2          mov     eax, edx
00401349      05 A4000000   add     eax, 0A4
0040134E      50            push    eax
0040134F      83E8 08       sub     eax, 8
00401352      8B00          mov     eax, dword ptr [eax]
00401354      FFD0          call    eax
00401356      8BF4          mov     esi, esp
00401358      B9 09000000   mov     ecx, 9
0040135D      8B7C24 24     mov     edi, dword ptr [esp+24]
00401361      F3:A5         rep     movs dword ptr es:[edi], dword p>
00401363      9D            popfd
00401364      61            popad
00401365      58            pop     eax
00401366      8BC8          mov     ecx, eax
00401368      05 C4000000   add     eax, 0C4
0040136D      8B00          mov     eax, dword ptr [eax]
0040136F      8BE0          mov     esp, eax
00401371      51            push    ecx
00401372      E8 E9FEFFFF   call    00401260
简单跟下,发现函数中调用了InitializeCriticalSection函数初始化了一个临界区,然后调用
RtlEnterCriticalSection函数进入临界区。之后将前面用pushad和pushfd压入堆栈的8个通用寄存器和标志寄存器复制到了另外一个地址,最后将另外一个地址赋给esp,调用00401260。

保存所有寄存器,将esp指向堆中的地址,我们感觉到了什么? 没错,这里是虚拟机保护外部环境并初始化内部环境的动作。到这里可以肯定这道题考的就是虚拟机,那我们就到这个虚拟机里看看。

进入函数00401260之后,先大概跟了下,发现程序会进入到一个循环之中,凭感觉这里应该就是虚拟机中取指令并执行的过程了。我们验证下。我们先不进循环中call,跟一下循环。
代码:
004012E0  |>  8B46 68       /mov     eax, dword ptr [esi+68]
004012E3  |. |0FB608        |movzx   ecx, byte ptr [eax]
004012E6  |. |8B148D D0D840>|mov     edx, dword ptr [ecx*4+40D8D0]
004012ED  |. |56            |push    esi
004012EE  |. |FFD2          |call    edx
004012F0  |. |8346 68 10    |add     dword ptr [esi+68], 10
004012F4  |. |397E 68       |cmp     dword ptr [esi+68], edi
004012F7  |.^\72 E7         \jb      short 004012E0
跟了几圈后发现,第一句代码从esi+0x68处取了一个指针,然后取出指针中的第一个字节,用这个字节作为偏移从一张表中取到一个地址,然后调用这个地址,之后将esi+0x68处的指针加0x10个字节,循环。

从这些动作中,我们可以猜想这个循环就是循环取虚拟机指令然后执行的过程,但是我们要验证,先看一下那个函数地址表。
代码:
0040D8D0  80 13 40 00 C0 13 40 00 D0 13 40 00 E0 13 40 00  @.?@.?@.?@.
0040D8E0  30 14 40 00 80 14 40 00 D0 14 40 00 20 15 40 00  0@.@.?@. @.
0040D8F0  80 15 40 00 E0 15 40 00 F0 15 40 00 00 16 40 00  @.?@.?@..@.
0040D900  40 16 40 00 90 16 40 00 20 17 40 00 80 17 40 00  @@.?@. @.@.
0040D910  F0 17 40 00 60 18 40 00 20 19 40 00 E0 19 40 00  ?@.`@. @.?@.
0040D920  40 1A 40 00 F0 1A 40 00 A0 1B 40 00 50 1C 40 00  @@.?@.?@.P@.
0040D930  F0 1C 40 00 A0 1D 40 00 50 1E 40 00 50 1E 40 00  ?@.?@.P@.P@.
0040D940  00 1F 40 00 00 1F 40 00                          .@..@.
可以看到这个表中总共有30个地址,我们依次看下,发现都是一些比较短的函数。我们再验证下我们猜想的存放虚拟机指令的地址。
代码:
0040CDD0  01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ...............
0040CDE0  04 00 00 00 00 00 00 00 04 00 00 00 00 00 00 00  ..............
0040CDF0  06 00 00 00 01 00 00 00 FC FF FF FF 00 00 00 00  ......?....
0040CE00  09 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
0040CE10  03 00 00 00 04 00 00 00 00 00 00 00 00 00 00 00  ..............
0040CE20  04 00 00 00 01 00 00 00 02 00 00 00 00 00 00 00  .............
0040CE30  07 00 00 00 00 00 00 00 01 00 00 00 00 00 00 00  ..............
0040CE40  04 00 00 00 00 00 00 00 06 00 00 00 00 00 00 00  ..............
发现和我们猜想的基本差不多,可以看出来每个指令的长度为16字节,根据循环取第一个字节作为调用函数的偏移可以猜想指令的第一个字节就是指令的功能号,后面的数据我们先不管,我们跟进第一条指令的解释函数看看。

第一条指令为
0040CDD0  01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ...............

解释函数为表中的第二个函数,代码如下:
代码:
004013C0  /.  55            push    ebp                              ;  Verify.004120D0
004013C1  |.  8BEC          mov     ebp, esp
004013C3  |.  8B45 08       mov     eax, dword ptr [ebp+8]
004013C6  |.  8B08          mov     ecx, dword ptr [eax]
004013C8  |.  8948 60       mov     dword ptr [eax+60], ecx
004013CB  |.  5D            pop     ebp
004013CC  \.  C2 0400       retn    4
可以看到这个函数很简单,上面忘了说了,每次调用解释函数都会传递一个固定的参数,参数是一个指针,这里取出这个指针的第一个DWORD,然后保存在另外一个地方。

在虚拟机的概念中,虚拟机会保存外部的环境,包括通用寄存器和标志寄存器,然后会在虚拟机内部操作这些数据,那传进来的这个参数会不会就是保存外部环境的地址呢。

回头想想前面在初始化虚拟机环境之前有一个将所有寄存器值拷贝到一个地址的操作。如果细心点我们就会发现,传递进来的这个参数就是前面拷贝寄存器的指针,我们进数据中验证下。

0040DEB8  46 03 00 00 78 00 65 00 00 00 00 00 6C FF 13 00  F..x.e.....l.
0040DEC8  28 FF 13 00 00 F0 FD 7F 14 E5 92 7C 00 00 3F 00  (..瘕|..?.
0040DED8  00 00 3F 00 00 00 00 00 00 00 00 00 00 00 00 00  ..?.............

看到了什么?第一个是标志寄存器的值,之后分别是EIP-EAX 8个通用寄存器的值。这样就很好理解了,esi中保存的是程序外部环境的指针。这句虚拟机指令的作用就是取出标志寄存器,然后放到另外一个地方。

而esi+0x68保存的就是当前解释的虚拟机指令的地址,也就是EIP。现在我们去了解下负责解释虚拟机指令的30个函数的功能。这是一个令人痛苦的过程,了解了所有的函数的功能,我们就可以花费一定的时间去读懂那些“天书”的意思。这里我将自己分析出来的结果写出来。我们按照虚拟机指令的第一个字节给函数命名,比如说第一个字节是0我们就称之为"0号函数"。

这里先将虚拟机指令的结构说下,有助于理解下面的指令。
每条指令长度为16字节,第一个字节为功能号,也就是要执行的动作的编号,第二个字节无意义,第三四个字节是要执行的功能是按字节 字 还是双字。0表示双字,1表示字,2表示字节。第二个DWORD和第三个DWORD是两个操作数。第四个DWORD无意义。
寄存器的编号为1-8,但是第一个是eip,最后一个才是eax。

代码:
0号函数      负责还原外部环境,也就是退出虚拟机,每个虚拟机指令块的最后一句都是一串0

1号函数      取出标志寄存器,相当于pushfd

2号函数      存入标志寄存器,相当于popfd

3号函数      将一个数据按字节 字或者双字存入通用寄存器,相当于 mov指令,第一个操作数为要从入寄存器的编号

4号函数      将一个数据按字节 字或者双字从寄存器取出到其他地址,相当于mov指令,第二个操作数为要取出寄存器的编号

5号函数      将一个数据按字节 字或者双字从esi+0x40地址取到esi+0x40的另外一个地址,两个操作数分别是相对于esi+0x40的偏移。
             在esi保存的环境中,除了保存的寄存器和eip之外,其他的数据我没有确定是什么,比如那里是esp ebp什么的,所以描述起来                   
             可能不准确,希望包涵。

6号函数      将一个数据按字节 字或者双字放入esi+0x40,第一个操作数是偏移,第二个操作数是要放入的数据,相当于mov 立即数。

7号函数      相当于按字节 字或者双字 将第二个操作数中的数据放入第一个操作数中的地址,这里的操作数都是相对于esi+0x40的偏移。

8号函数      也是一个mov指令,但是相当于基址加偏移寻址。
 
9号函数      加法操作,相当于add,无操作数,操作数在esi+0x40和sei+0x44中

10号函数     减法操作,相当于sub,操作数同9号函数

11号函数     乘法操作,相当于mul,操作数同9号函数

12号函数     除法操作,相当于div,操作数同9号函数

13号函数     ...........................
累了,不写了


分析完虚拟机之后,我们分析题目。

题目一:

题目1中,要求找出一个文件校验错误的BUG,也就是1.dat这个文件本来应该是校验正确的,结果返回了错误,要找出原因。

我们先分析下这个文件的结构,这个文件结构很简单,如下:
代码:
  四字节标志 + 四字节文件头长度 + 四字节数据长度 + 四字节校验值 +  数据
    TEST                   16                  数据长度          校验值       数据
可以猜测,程序中的校验过程存在BUG,导致最后校验数据之后的结果和文件中保存的校验值不同。

首先我们我们猜测下校验算法,最常用的生成4字节校验值的校验算法是什么?没错是CRC32!

我们来验证下,首先我们在读入的文件的数据部分下个硬件断点,看看断在什么地方。

程序断在了这个位置:
代码:
00401598  |.  8B50 08       mov     edx, dword ptr [eax+8]           ;  Case 2 of switch 0040158D
0040159B  |.  8B5491 40     mov     edx, dword ptr [ecx+edx*4+40]
0040159F  |.  8B40 04       mov     eax, dword ptr [eax+4]
004015A2  |.  8A12          mov     dl, byte ptr [edx]
这个是第8号函数,是一个基址加偏移寻址的取数据指令,我们看看这个时候虚拟机指令的eip在那里,在这个位置:

0040D4F0  08 00 02 00 00 00 00 00 00 00 00 00 00 00 00 00  ..............   取一字节数据

既然程序开始取数据了,说明已经开始要进行校验算法了,就是这里,我们一步一步跟下去,看它在干什么:
代码:
0040D470  04 00 00 00 00 00 00 00 08 00 00 00 00 00 00 00  ..............  下面跳转到这里  取出41FFFFFF放入eax
0040D480  03 00 00 00 06 00 00 00 00 00 00 00 00 00 00 00  ..............   放入edx
0040D490  04 00 00 00 00 00 00 00 05 00 00 00 00 00 00 00  ..............
0040D4A0  04 00 00 00 01 00 00 00 05 00 00 00 00 00 00 00  .............
0040D4B0  15 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ...............
0040D4C0  02 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ...............
0040D4D0  03 00 00 00 05 00 00 00 00 00 00 00 00 00 00 00  ..............
0040D4E0  04 00 00 00 00 00 00 00 07 00 00 00 00 00 00 00  ..............
0040D4F0  08 00 02 00 00 00 00 00 00 00 00 00 00 00 00 00  ..............   取一字节数据
0040D500  03 00 02 00 05 00 00 00 00 00 00 00 00 00 00 00  .............   将上面取到的一字节放入ebx
0040D510  04 00 00 00 00 00 00 00 06 00 00 00 00 00 00 00  ..............   取出ecx
0040D520  06 00 00 00 01 00 00 00 FF 00 00 00 00 00 00 00  .............   写入FF到缓冲区
0040D530  14 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ...............
0040D540  03 00 00 00 06 00 00 00 00 00 00 00 00 00 00 00  ..............   edx=0xFF
0040D550  04 00 00 00 00 00 00 00 06 00 00 00 00 00 00 00  ..............
0040D560  04 00 00 00 01 00 00 00 05 00 00 00 00 00 00 00  .............  
0040D570  15 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ...............   取到的第一个字节和FF异或 得到0xAB
0040D580  02 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ...............   载入标志寄存器
0040D590  03 00 00 00 06 00 00 00 00 00 00 00 00 00 00 00  ..............   将0xAB放入edx
0040D5A0  04 00 00 00 00 00 00 00 08 00 00 00 00 00 00 00  ..............
0040D5B0  06 00 00 00 01 00 00 00 08 00 00 00 00 00 00 00  .............
0040D5C0  18 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ...............
0040D5D0  03 00 00 00 08 00 00 00 00 00 00 00 00 00 00 00  ..............    0xFFFFFF放入eax
0040D5E0  04 00 00 00 00 00 00 00 06 00 00 00 00 00 00 00  ..............    取出edx放到缓冲区
0040D5F0  06 00 00 00 01 00 00 00 04 00 00 00 00 00 00 00  .............
0040D600  0B 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ...............    0xAB乘以4
0040D610  06 00 00 00 01 00 00 00 E0 EC 40 00 00 00 00 00  ......囔@.....
0040D620  09 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................    乘积加上 0040ECE0
0040D630  08 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ...............    查表得 4102CA60  表的地址为 0040EF8C 这个表就是上面0040ECE0加上乘积的值
0040D640  03 00 00 00 06 00 00 00 00 00 00 00 00 00 00 00  ..............    将 4102CA60 写入edx
0040D650  04 00 00 00 00 00 00 00 08 00 00 00 00 00 00 00  ..............
0040D660  04 00 00 00 01 00 00 00 06 00 00 00 00 00 00 00  .............
0040D670  16 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ...............    将 4102CA60和00FFFFFF或运算
0040D680  02 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ...............
0040D690  03 00 00 00 08 00 00 00 00 00 00 00 00 00 00 00  ..............    将或运算结果41FFFFFF放入eax
0040D6A0  04 00 00 00 00 00 00 00 07 00 00 00 00 00 00 00  ..............    取到字符串指针
0040D6B0  06 00 00 00 01 00 00 00 01 00 00 00 00 00 00 00  ............. 
0040D6C0  0F 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ...............    将字符串指针加1
0040D6D0  03 00 00 00 07 00 00 00 00 00 00 00 00 00 00 00  ..............    将字符串指针放入ecx
0040D6E0  04 00 00 00 00 00 00 00 02 00 00 00 00 00 00 00  ..............
0040D6F0  06 00 00 00 01 00 00 00 01 00 00 00 00 00 00 00  .............
0040D700  10 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ...............
0040D710  03 00 00 00 02 00 00 00 00 00 00 00 00 00 00 00  ..............
0040D720  0D 00 00 00 02 00 00 00 00 00 00 00 00 00 00 00  ...............
0040D730  0E 00 00 00 D3 FF FF FF 00 00 00 00 00 00 00 00  ...?........   跳转到0040D470  取完字符之后就不会跳转
0040D740  04 00 00 00 00 00 00 00 04 00 00 00 00 00 00 00  ..............    取完字符之后运行到这里
这里是一个循环取字符然后运算的过程,我们前面猜测它是CRC32算法,我们验证下它是不是呢。
看看上面,很明显,不是吗,一个查表法的CRC32运算。现在我们确定算法了,我们看看它为什么会出错呢。

我们再在读入文件的校验值位置下一个硬件断点,看看断在那里。

断下来之后,我们看看eip 的位置,在这里

0040DC08  08 00 00 00 01 00 00 00 00 00 00 00 00 00 00 00  ..............  取文件中的校验值
0040DC18  04 00 00 00 00 00 00 00 08 00 00 00 00 00 00 00  ..............
0040DC28  10 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ...............  比较文件校验值和计算的结果

取到校验值之后,就要和前面计算出来的校验值比较了,但是可以看到计算出来的校验值是9E908000。很明显,校验算法有BUG导致校验值出了错。
我们现在仔细看看前面的校验算法,看看BUG在那里。

找啊找啊,找的好辛苦,突然!

0040D670  16 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ...............    将 4102CA60和00FFFFFF或运算

你泥吗什么玩意?或或或或以或或,对照着纯正的CRC算法一看,这里不是应该是异或运算吗?

我们验证下BUG是不是在这里,根据前面我们辛辛苦苦跟出来的虚拟机指令,我们知道或运算的哥哥异或运算就在它上面,15号函数。

于是我们把指令改为:
0040D670  15 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ...............  
保存文件,忐忑的输入Verify.exe 1.dat 居然成功了!

证明了BUG就在这里,因为弟弟抢了哥哥的工作导致算法错误,只要我们主持下正义就可以了。到此第一个问题分析完毕。


题目二:

题目二是因为Crash.dat会导致程序崩溃,要求找出BUG并修复,我们先看一看程序崩溃的地址:
00401598  |.  8B50 08       mov     edx, dword ptr [eax+8]           ;  Case 2 of switch 0040158D
0040159B  |.  8B5491 40     mov     edx, dword ptr [ecx+edx*4+40]
0040159F  |.  8B40 04       mov     eax, dword ptr [eax+4]
004015A2  |.  8A12          mov     dl, byte ptr [edx]

崩溃的地址是按字节读取文件的地方,题目说这是一个人为构造的文件,我们看看它是怎么构造的。


打开Crash.dat,原因就在眼前,文件中本该保存数据长度的地方,躺了一个大大的0x1000016,但是真正的数据只有0x16字节。我们只要把数据长度改为正确程序就不会崩溃了。


但是如果修正这个BUG呢?楼主陷入了沉思。


这是一个人为构造的文件,那程序的BUG在那里呢? 严格来说这个文件是一个格式错误的文件,那如果程序遇到这个文件该如何处理呢?楼主认为这样的不合格的文件应该坚决丢弃,
不能让它打入我们内部。所以楼主在程序打开文件的地方做了一个小小的补丁,判断文件中记录的长度如果不等于文件本身的长度减去16字节,就认为这是一个错误的文件,直接返回错误。
但是我感觉这样的处理方法有点简单,和第一个题的难度相差有点大,所以我不确定这种方法是否是别人认同的方法。只有等最后结果了。

至此全部分析完成, 由于第一次分析虚拟机,所以分析过程难免会有错误和遗漏的地方,还请大家多多指教。

搞的人一惊一乍的