『深圳腾讯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
紧接着call到 00401F10,这个函数最后使用了如下代码调用到00401310这个函数。
代码:
00401F44 |. B8 10134000 mov eax, 00401310 00401F49 |. 870424 xchg dword ptr [esp], eax 00401F4C \. C3 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
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
从这些动作中,我们可以猜想这个循环就是循环取虚拟机指令然后执行的过程,但是我们要验证,先看一下那个函数地址表。
代码:
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 .@..@.
代码:
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 ..............
第一条指令为
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
在虚拟机的概念中,虚拟机会保存外部的环境,包括通用寄存器和标志寄存器,然后会在虚拟机内部操作这些数据,那传进来的这个参数会不会就是保存外部环境的地址呢。
回头想想前面在初始化虚拟机环境之前有一个将所有寄存器值拷贝到一个地址的操作。如果细心点我们就会发现,传递进来的这个参数就是前面拷贝寄存器的指针,我们进数据中验证下。
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 数据长度 校验值 数据
首先我们我们猜测下校验算法,最常用的生成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]
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运算。现在我们确定算法了,我们看看它为什么会出错呢。
我们再在读入文件的校验值位置下一个硬件断点,看看断在那里。
断下来之后,我们看看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字节,就认为这是一个错误的文件,直接返回错误。
但是我感觉这样的处理方法有点简单,和第一个题的难度相差有点大,所以我不确定这种方法是否是别人认同的方法。只有等最后结果了。
至此全部分析完成, 由于第一次分析虚拟机,所以分析过程难免会有错误和遗漏的地方,还请大家多多指教。
搞的人一惊一乍的