上次更新添加了算法分析功能,可以反出类似高级语言的表达式,现在分析简单的程序应该没什么问题了,很多人说不太会用,这里给一个实例分析,讲解一下用法。这篇文章只讲插件的应用和一般的分析方法,还简单介绍了一下VMP加壳的原理和脱壳方法,其他内容比如插件分析原理和虚拟机代码还原方法等请看VMP分析插件的帖子。
http://bbs.pediy.com/showthread.php?t=154621
先随便输入一些内容,用户名zdhysd,注册码qwertyuiop,点确定弹出注册码错误的提示。在MessageBox设个断点看看哪里来的,再点确定直接出错,看来有断点检查,在最后的retn设断点试试。这回断下来了,返回到
004CBAE8 9C PUSHFD
004CBAE9 C70424 AFF5A7F9 MOV DWORD PTR SS:[ESP],F9A7F5AF
004CBAF0 E8 1F41F7FF CALL VMPCrack.0043FC14
004CBAF5 9C PUSHFD
004CBAF6 90 NOP
004CBAF7 9C PUSHFD
... 不像正常代码,应该是虚拟机中调用的,在这里点右键,菜单选择分析虚拟机,分析成功,找到5个虚拟机。先不用分析虚拟程序,因为函数返回时在虚拟程序的中间,可以先选中进入虚拟机时中断,运行,切换到虚拟机调试模式了,Ctrl+F9运行到返回,返回到
00402990 8B4424 08 MOV EAX,DWORD PTR SS:[ESP+8]
00402994 2D 10010000 SUB EAX,110
00402999 74 1A JE SHORT VMPCrack.004029B5
0040299B 48 DEC EAX
0040299C 75 3A JNZ SHORT VMPCrack.004029D8
0040299E 8B4424 0C MOV EAX,DWORD PTR SS:[ESP+C]
004029A2 66:3D 0100 CMP AX,1
004029A6 75 15 JNZ SHORT VMPCrack.004029BD
004029A8 8B4424 04 MOV EAX,DWORD PTR SS:[ESP+4]
004029AC 50 PUSH EAX
004029AD E8 5EFEFFFF CALL VMPCrack.00402810 ***这个函数被加密了
004029B2 83C4 04 ADD ESP,4 ***返回到这里
004029B5 B8 01000000 MOV EAX,1
004029BA C2 1000 RETN 10 这里已经是正常代码了,跟进CALL VMPCrack.00402810,上来就是一个JMP,应该是进入虚拟机的,在这里分析虚拟程序。先选上反汇编后自动分析和分析选项里的分析虚拟机内调用,选中反汇编后自动分析会在每个虚拟程序反汇编后自动做数据和算法分析,选中分析虚拟机内调用会自动分析虚拟机内调用的函数。分析选项中除了化简无效数据以外也都选中,这样生成的代码比较简单,一般情况下无效数据都是没有用的,不化简可以节省很多时间。分析用了70多秒,一共分析出了19个虚拟程序,16万多行代码,看看记录,有几个未知的vESP改变和多个来源vESP不同,可能是调用引起的,先不管他。还有一个未知指令,一般没有什么影响,插件会自动分析指令相关的信息继续反汇编,遇到时再说吧。因为有虚拟机内调用,虚拟机内调用要在当前程序分析完成后才会分析,这是有些信息无法得到,最好在被调用函数分析完成后再分析一遍,可以在反汇编窗口右键菜单点分析-分析虚拟程序(全部)重新分析一次,这时会自动添加一些相关的分析提示,但不一定准确,调试时最好自己检查一下。
看看代码,大段大段的灰色指令,估计超过80%,而且有很多连接转移,应该是加变形了。点击反汇编栏标题把显示模式切换到有效指令,这样就不会显示垃圾指令了。当然也可以直接看最终操作,这样更简单,但是算法分析后的结果已经和虚拟机没有什么关系了,这里为了了解一下虚拟机的原理先不使用这个功能,看看VMP是什么样的。由于不使用算法分析的结果,指令没有化简,代码量很大,最好从一些关键的部分比如转移等看起。
从头跟踪一下,先把进入虚拟机时中断去掉,否则调用时退出虚拟机后再返回也会中断,在第一条指令设个断点,输入注册码点确定,中断下来了。第一个转移是JT,检查进入虚拟机时是否设了单步标志,最大保护下也用来检查是否被脱壳,不用管它。往下是一堆连接转移,每个指令块中的代码很少,垃圾指令都被清除掉了,大部分是计算转移目标用的,这是加变形后代码乱序的结果,没有什么用。走到
004C3918 |. /83 vJmp_00425E0A ; 虚拟机内调用 VMPCrack.004B77D0; 重新进入虚拟机 VMPCrack.004D6C58; VMPCrack.004B77D0
有一个虚拟机内调用,跟进去看看,代码很长,而且有很多vCall,一般程序中不会出现这个指令,只有壳本身使用,有可能是SDK代码。实际上这个函数就是SDK中的检测调试器,看看其中的几个联机指令就知道了(POP SS、INT3等),因为和壳的入口使用的方法几乎相同,这里不多作讲解了,留到后面脱壳时再讲,给他加个标签。再往下看,有一个条件转移
00465EBE |. /05 vJmp_00412430 ; JNZ VMPCrack.0046A6C8
看看判断的条件是什么,先点击反汇编窗口注释栏标题切换到引用数据,信息栏切换到数据。看看信息栏,vJmp引用了两个数据,一个是重定位(0),另一个是转移目标。转移目标是根据要检查的标志选择的,先生成条件转移的两个目标,在堆栈中连续保存,再根据标志计算出0或4,加上保存目标的地址,就可以选择出一个目标,所以要找判断条件,应该先找到标志是哪个计算产生的。在信息窗口的转移目标上选菜单中的转到数据来源或直接按Enter键,一直向上查找数据来源,由于转移目标是计算产生的,类型为变量,如果有多个引用数据的话跟随类型为变量的数据。一直跟随到地址为变量的vReadMemSs4,这个vReadMemSs4是读取转移目标用的,地址由标志计算得到,用来选择一个目标。下面是跟踪过程,标*的是经过的指令,有多个引用数据的话只跟踪第一个。
00465EF0 |. 55 vReadMemSs4 (变量); 堆栈-0B4:(变量) ***读取解密前的目标
00465EEF |. 38 vPopReg4 vR8 堆栈-0B4:(变量) ***
00465EEE |. 00 000000 AddVEsp 8 (常量)8; (vESP)-0B0
00465EEC |. 3C vPushReg4 vR8 vR8:(变量) ***
00465EEB |. 68 vPopReg4 vR13 堆栈-0AC:(变量) ***下面开始解密,异或AA2B8C58
00465EEA |. 6C vPushReg4 vR13 vR13:(变量)
00465EE9 |. 49 vPushVEsp (vESP)-0AC
00465EE8 |. 34 vReadMemSs4 堆栈-0AC:(变量); 堆栈-0B0:(vESP)-0AC
00465EE7 |. 20 vNand4 堆栈-0B0:(变量); 堆栈-0AC:(变量)
00465EE6 |. 00 000000 AddVEsp 4 (常量)4; (vESP)-0B0
00465EE5 |. 65 A773D455 vPushImm4 55D473A7 (常量)55D473A7
00465EE0 |. 20 vNand4 堆栈-0B0:(常量)55D473A7; 堆栈-0AC:(变量)
00465EDF |. 00 000000 AddVEsp 4 (常量)4; (vESP)-0B0
00465EDE |. DE 588C2BAA vPushImm4 0AA2B8C58 (常量)0AA2B8C58
00465ED9 |. 6C vPushReg4 vR13 vR13:(变量) ***
00465ED8 |. 20 vNand4 堆栈-0B4:(变量); 堆栈-0B0:(常量)0AA2B8C58 ***
00465ED7 |. 00 000000 AddVEsp 4 (常量)4; (vESP)-0B4
00465ED6 |. 20 vNand4 堆栈-0B0:(变量); 堆栈-0AC:(变量) ***
00465ED5 |. 00 000000 AddVEsp 4 (常量)4; (vESP)-0B0
00465ED4 |. 98 vPopReg4 vR2 堆栈-0AC:(变量) ***这是解密后的目标
00465ED3 |. 00 000000 AddVEsp 0FFFFFFF4 (常量)0FFFFFFF4; (vESP)-0A8
00465ED0 |. 5C vPushReg4 vR14 vR14:(寄存器)EBP
00465ECF |. CC vPushReg4 vR7 vR7:(寄存器)EBX
00465ECE |. 1C vPushReg4 vR10 vR10:(寄存器)ECX
00465ECD |. 00 000000 AddVEsp 0FFFFFFFC (常量)0FFFFFFFC; (vESP)-0C0
00465ECC |. AC vPushReg4 vR1 vR1:(寄存器)EAX
00465ECB |. 0C vPushReg4 vR11 vR11:(寄存器)EDX
00465ECA |. 7C vPushReg4 vR12 vR12:(寄存器)EDI
00465EC9 |. FC vPushReg4 vR4 vR4:(寄存器)ESI
00465EC2 |. 00 000000 vPushImm4 0EF8B1AAC (常量)0EF8B1AAC
00465EC0 |. EC vPushReg4 vR5 vR5:(常量)0
00465EBF |. 9C vPushReg4 vR2 vR2:(变量) ***
00465EBE |. 05 vJmp_00412430 堆栈-0E0:(变量); 堆栈-0DC:(常量)0 *** 然后开始跟踪vReadMemSs4的地址,就是第二个引用数据,地址为堆栈-0B4的那个。跟踪一次到
00465EF2 |. 50 vAdd4 堆栈-0B8:(标志); 堆栈-0B4:(vESP)-0B0
这就是把标志计算出来的结果(0或4)加上保存目标的地址,现在开始跟踪类型为标志的数据。跟踪过程就不写了,想了解详细的处理流程的话可以使用数据流图功能,在vJmp选择菜单里的图表-数据流图(指令相关),可以显示所有相关数据的来源,只能在显示全部指令时用,可能有些计算常量的指令也包括进来了。一直跟随到
0045E7A4 |. C0 vNand4 堆栈-0C0:(变量); 堆栈-0BC:(变量)
两个引用数据都是变量,说明标志是这里产生的,看看做的什么计算
0045E7AD |. BC vPushReg4 vR0 vR0:(寄存器)EAX
0045E7AC |. 17 vPushVEsp (vESP)-0BC
0045E7AB |. 34 vReadMemSs4 堆栈-0BC:(寄存器)EAX; 堆栈-0C0:(vESP)-0BC
0045E7AA |. 20 vNand4 堆栈-0C0:(寄存器)EAX; 堆栈-0BC:(寄存器)EAX //EAX ~& EAX = ~EAX
0045E7A9 |. 00 000000 AddVEsp 4 (常量)4; (vESP)-0C0
0045E7A8 |. BC vPushReg4 vR0 vR0:(寄存器)EAX
0045E7A7 |. BC vPushReg4 vR0 vR0:(寄存器)EAX
0045E7A6 |. 27 vNand4 堆栈-0C4:(寄存器)EAX; 堆栈-0C0:(寄存器)EAX //EAX ~& EAX = ~EAX
0045E7A5 |. 00 000000 AddVEsp 4 (常量)4; (vESP)-0C4
0045E7A4 |. C0 vNand4 堆栈-0C0:(变量); 堆栈-0BC:(变量) //~EAX ~& ~EAX = EAX & EAX 计算的是(EAX ~& EAX) ~& (EAX ~& EAX),也就是EAX & EAX,所以条件转移对应的汇编代码应该是
TEST EAX,EAX
JNZ VMPCrack.0046A6C8
由于前面有一个虚拟机内调用,很可能改变了EAX,这个转移应该是检查调用的返回值是否为0。可以看出即使给出了数据相关的信息和清除垃圾指令后,分析虚拟机代码还是很麻烦的,一个条件转移就要看这么多内容。下面还是看算法分析的结果吧,把反汇编栏切换到最终操作,注释栏切换到表达式。现在代码量就很少了,这里大部分都是转移和调用相关的指令。由于加了变形,代码乱序用的垃圾转移很多,插件为了调试方便会保留每个指令块第一条和最后一条指令,可能有不少没用的内容,但是垃圾转移的指令块中没有其他代码,一下就能看出来,不管他就行了。调用相关的指令也都保留了,比如调用退出虚拟机时真实寄存器的值,退出后仍然可以引用的数据(标记为EXIT)等,还有最大保护下的加密寄存器、检查虚拟机完整性,如果调用很多的话这些内容也会占用一部分代码,都可以不用管,后面给出的代码中为了节省空间把这些去掉了。
既然说到最大保护下的调用,就讲一下为什么刚才在MessageBox设断点会出错,详细的说明请看VMP分析插件帖子中的内容,这里只简单说一下检测INT3断点。随便找个调用看看
0048FA81 |. 53 vMul2 WORD v0 = GetBytes(9 * (0 : (5A86 ^ GetBytes(Rdtsc(), 0, 2)) % 1B5), 0, 2)
0048FA2F |. AD vAdd4 DWORD v1 = 428F55 + v0
0048F9DF |. 5A vReadMemDs1 BYTE m0 = BYTE DS:[4 + v1]
0048F9DD |. 08 vReadMemDs4 DWORD m1 = DWORD DS:[v1]
0048F8B3 |. 08 vReadMemDs4 DWORD m2 = DWORD DS:[5 + v1]
0048F8B0 |. 0B vAdd4 DWORD v2 = m2 + Check(0 + (400000 + (862FE2A3 + ((708F098F ^ m1) + 1 - 1))), 0 : (0 : ByteToWord(m0))) + (((40 & AddFlag(34, 8B)) >> 1) + 77D307EA)
0048F8A3 \. 86 vRet return v2; user32.MessageBoxA 可以看出返回地址是计算出来的,其中有一部分是随机检查一块内存是否被修改,这里就不讲了。看看其中的((40 & AddFlag(34, 8B)) >> 1),8B是MessageBoxA的第一个字节内容,插件认为是常量直接显示了。VMP会读出被调用函数的第一个字节,加上34,取加法的ZF标志随机移位加到返回地址上。INT3代码是CC,CC+34=100,ZF为1,如果设了断点加到返回地址的数就不是0了,会返回到一个随机的位置,所以出错。
现在再来看刚才的内容就容易多了
004917A6 /$ D8 vPopReg4 vR6
0049136C |. F9 vReadMemDs4 DWORD m0 = DWORD DS:[164B38]
0049121C |. 27 vNand4 DWORD v0 = 0 - ((m0 >>< 1B) + 1)
00490F4D |. 34 vReadMemSs4 DWORD m1 = DWORD SS:[Je(SubFlag((RolFlag(v0, 17) & 100) + (0 - (((164AD8 ^ (CpuidEax(1) & 0FFFFFFF0 ^ 588B4548) + (CpuidEbx(1) & 0FFFFFF ^ 3F598CC3)) >>< 1B) + 1) <<> 17), v0 <<> 17)) + 18]
00490F1C |. F1 vJmp_00412430 if (unpacked) goto VMPCrack.00476AA5 //检查是否被脱壳
0042E2DE |. 23 vPopfd EFL = 0FFFFF700 & EFL
00451055 |. BF vWriteMemSs4 EXIT DWORD v1 = EBP
00450F95 |. 82 vNand4 EXIT DWORD v2 = 118D7BDF
00464AF1 |. BF vWriteMemSs4 EXIT DWORD v3 = EBX
004DB3A5 |. BF vWriteMemSs4 EXIT DWORD v4 = ESI
004C19DC |. 32 vWriteMemSs4 EXIT DWORD v5 = EDI
004C193D |. E4 vWriteMemSs4 ARG1 EXIT DWORD v6 = 1 //调用的参数,TRUE,检测用户+内核调试器
004C3918 |. 83 vJmp_00425E0A callVM //调用SDK,检测调试器
00465EF0 |. 55 vReadMemSs4 DWORD m2 = DWORD SS:[Je(AndFlag("发现调试器", "发现调试器")) + 0FFFFFF50]
00465ECC |. AC vPushReg4 vR1 DWORD v15 = "发现调试器"
00465EBE |. 05 vJmp_00412430 if (entryVMEax_4D6C58 != 0) goto VMPCrack.0046A6C8 一开始是检查脱壳,壳的入口在初始化时会根据CPUID计算出一个数,每个虚拟程序开始时取CPUID计算后和这个数比较,如果被脱壳,入口的初始化代码不会被执行,直接DUMP出来的话这个值是固定的,到其他电脑上就会被检查到,返回到一个错误的地址,脱壳时要考虑这个问题。然后是保存一些寄存器,对应汇编中函数开始时的PUSH REG,由于加了变形,很多操作都被垃圾转移分隔了。再往下就是刚才那个条件转移,现在就很明显了,判断调用SDK检测调试器是否返回0。entryVMEax_4D6C58表示在4D6C58进入虚拟机时的EAX,由于插件不会自动分析被调用函数,这样有些需要的信息得不到,所以遇到调用时最好添加分析提示。一般调用需要的信息有ESP改变、引用和修改的数据等,虚拟机内调用还需要真实寄存器在初始堆栈中的位置,插件会自动填写一些,但是不一定准确,最好自己检查一下。由于这个虚拟机内调用没有填写初始寄存器提示,分析时不能把调用前后的真实寄存器联系起来,所以后面用entryVMxxx来表示被调用函数执行后重新进入虚拟机时的真实寄存器值。看看被调用函数第一条指令的注释/*esp(8); arg(1); regModify(eax)*/,插件已经自动填写了一些:ESP改变为8,有一个参数,改变寄存器EAX,这里可以设置一下参数名和返回值,改成arg(1,"检测内核调试器"); regModify(eax, "发现调试器"),显示时会自动标出这些内容。现在来检查一下是否正确,计算ESP改变最简单的方法是调试观察,先把注释栏切换到堆栈,看看分析结果
004C3918 |. /83 vJmp_00425E0A vESP=-0E8; ESP=-0E4; 堆栈长度=0038
004D6C58 |> \68 vPopReg4 vR13 vESP=-0DC; ESP=-0DC; 堆栈长度=0034
执行vJmp时vESP是-0E8,调用返回后是-0DC,加了0C。调试一下,再记录调用前后的vESP,执行vJmp时是13F960,调用返回后是0013F96C,也是加了0C,说明是对的。现在填写初始寄存器提示,有两种方法判断寄存器的位置,一种是根据调用前压栈的值计算,另一种是根据被调用函数返回时恢复的寄存器计算。刚才看记录发现这个函数中有未知vESP改变和来源堆栈信息不同,分析结果有可能不正确,跟进函数看看确实是这样,退出时所有寄存器都被关联到了同一个变量,所以现在选择根据调用前压栈的值计算。其实这是由于其中有联机指令INT3,正常情况下会走到异常处理,但是插件不能自动分析异常处理,添加分析提示设置一下联机指令的返回地址就能解决,这里先不用管,等后面讲脱壳时再看这些反调试。先把注释栏切换到产生数据,显示模式切换到全部指令,因为分析时无法关联调用前后的寄存器,这些内容都作为无效数据处理了,看看调用前压栈的值
004C392D |. BC vPushReg4 vR0 堆栈-0B4:(标志)
004C392C |. 6C vPushReg4 vR13 堆栈-0B8:(寄存器)ESI
004C392B |. 4C vPushReg4 vR15 堆栈-0BC:(寄存器)ECX
004C392A |. 1C vPushReg4 vR10 堆栈-0C0:(标志)
004C3929 |. 8C vPushReg4 vR3 堆栈-0C4:(寄存器)EAX
004C3928 |. 2C vPushReg4 vR9 堆栈-0C8:(寄存器)EBX
004C3927 |. 6C vPushReg4 vR13 堆栈-0CC:(寄存器)ESI
004C3926 |. 9C vPushReg4 vR2 堆栈-0D0:(寄存器)EDI
004C3925 |. 3C vPushReg4 vR8 堆栈-0D4:(vESP)30
004C3924 |. FC vPushReg4 vR4 堆栈-0D8:(常量)4B77D0
004C3923 |. DC vPushReg4 vR6 堆栈-0DC:(寄存器)EDX
004C3922 |. EC vPushReg4 vR5 堆栈-0E0:(常量)164AD8
004C3921 |. DE D4CF74EF vPushImm4 0EF74CFD4 堆栈-0E4:(常量)0EF74CFD4
004C391C |. 10 vAdd4 堆栈-0E0:(常量)0EF8B1AAC; 堆栈-0E4:(标志)
004C391B |. 78 vPopReg4 vR12 vR12:(标志)
004C391A |. CC vPushReg4 vR7 堆栈-0E4:(常量)0
004C3919 |. FC vPushReg4 vR4 堆栈-0E8:(常量)4B77D0
004C3918 |. 83 vJmp_00425E0A 大部分寄存器都和虚拟程序开始时相同,没有被改变,先拿EAX计算一下,EAX被写入堆栈的地址是-0C4,vJmp执行前vESP是-0E8,执行后会把栈顶的转移目标弹出,变成-0E4,这就是被调用函数执行时的vESP,-0C4 - -0E4 = 20,说明EAX在初始堆栈中的位置是20,添加提示initReg(eax,20),其他寄存器按相同方法计算。寄存器中缺少EBP,说明EBP的值在前面被改变了,但是有一个(vESP)30,EBP一般用来做堆栈指针,很可能就是它。ESI有两个分别计算一下,看看后面怎么使用,其中位置为2C的后面没有使用,是无效数据,应该是另一个。全部提示为/*initReg(eax,20);initReg(ecx,28);initReg(edx,8);initReg(ebx,1C);initReg(ebp,10);initReg(edi,14);initReg(esi,18)*/,注意一定要包含在/**/中才有效,可以添加到插件自动生成的提示里,用;分隔。添加提示后需要重新分析才会生效,按Ctrl+A重新分析当前函数(不是被调用函数),结果条件转移前的那一堆entryVMxxx都没有了,调用前标出了寄存器值。以后遇到调用最好都填写一下,分析会更准确,这个程序虚拟机内调用比较多,填写提示可能有些麻烦。以后给出的代码都是添加提示后的结果,如果没什么特殊情况就不多说明了。
再往下又是一个调用,里面也有vCall,应该是SDK代码,其中包括联机指令STR WORD PTR SS:[ESP]等,可能是检测虚拟机,留到脱壳时再讲。先填上分析提示,这回换一种找寄存器位置的方法,从被调用函数中找一个vRet,这里用第一个联机指令,因为退出虚拟机时会恢复真实寄存器,查看执行vRet前压栈的值就可以找到对应的寄存器。
0041EC9D |. 61 vPushReg4 vR6 ECX DWORD v8 = unknownInit8
0041EC9E |. 01 vPushReg4 vR0 EFL DWORD v9 = SubFlag(0C, 800)
0041EC9F |. 81 vPushReg4 vR8 EDX DWORD v10 = unknownInit7
0041ECA1 |. E1 vPushReg4 vR14 ESI DWORD v11 = unknownInit4
0041ECA2 |. 91 vPushReg4 vR9 EAX DWORD v12 = unknownInit2
0041ECA3 |. 71 vPushReg4 vR7 EDI DWORD v13 = unknownInit10
0041ECA4 |. 51 vPushReg4 vR5 EBP DWORD v14 = 0C
0041ECA5 |. A1 vPushReg4 vR10 EBX DWORD v15 = unknownInit3
0041ECA8 |. 00 vRet online 4CF851; VMPCrack.004CF851; STR WORD PTR SS:[ESP] 未知的初始数据显示为unknownInit,先随便找一个寄存器,比如ECX对应unknownInit8,把显示模式切换到全部指令,往前到虚拟程序开始的位置,有
0041EC54 |. 62 vPopReg4 vR6 DWORD _t31 = unknownInit8
这个指令引用的堆栈是20,这就是ECX的位置。EBP在退出前已经被改变了,还是用以前的方法,在调用的vJmp前压栈的数据中找。上面介绍的两种方法都是在寄存器没有被改变的情况下才能使用,如果被改变就要自己分析了,一般情况下即使不填这个提示也没有太大影响。填写完后分析代码如下
0046783B |. 83 vJmp_00425E0A callVM
00465908 |. 94 vReadMemSs4 DWORD m3 = DWORD SS:[Je(AndFlag("发现虚拟机", "发现虚拟机")) + 0FFFFFF50]
004658E8 |. 2C vPushReg4 vR9 DWORD v17 = 30
004658E4 |. DC vPushReg4 vR6 DWORD v18 = "发现虚拟机"
004658D6 |. 05 vJmp_00412430 if ("发现虚拟机" != 0) goto VMPCrack.0046A6C8 下面还是调用,有两个循环,里面都用到了vCheck,应该是SDK的检查文件改变,因为一般情况下vCheck都是在计算返回地址时用的。
0049FC7D |. 57 vJmp_00425E0A callVM
004A9FBD |. 34 vReadMemSs4 DWORD m4 = DWORD SS:[Je(AndFlag("文件没有改变", "文件没有改变")) + 0FFFFFF50]
004A9F99 |. 7C vPushReg4 vR12 DWORD v15 = "文件没有改变"
004A9F8B |. F1 vJmp_00412430 if ("文件没有改变" == 0) goto VMPCrack.0046A6C8 SDK中只有这3种检查,到这里应该检查完了,但是后面还有很多调用,不知道是干什么的。因为插件不能自动分析被调用函数,遇到调用最好先处理一下再看代码,这样分析结果会更准确。先跟进最近的一个调用看看
004417D4 /$ 80 vPopReg4 vR0
00441666 |. 2F vPopfd EFL = 0FFFFF700 & unknownInit5
004414D8 |. 12 vMul2 WORD v0 = GetBytes(9 * (0 : (30B4 ^ GetBytes(Rdtsc(), 0, 2)) % 1B5), 0, 2)
0044147F |. 41 vAdd4 DWORD v1 = 428F55 + v0
0044143E |. B4 vReadMemDs1 BYTE m0 = BYTE DS:[4 + v1]
0044143C |. 24 vReadMemDs4 DWORD m1 = DWORD DS:[v1]
0044133B |. 24 vReadMemDs4 DWORD m2 = DWORD DS:[5 + v1]
00441338 |. 0B vAdd4 DWORD v2 = m2 + Check(0 + (400000 + (862FE2A3 + ((708F098F ^ m1) + 1 - 1))), 0 : (0 : ByteToWord(m0))) + (((40 & AddFlag(34, 8B)) >> 2) + 77D0436E)
00441336 |. FE vPushReg4 vR2 EBP DWORD v3 = unknownInit2
00441335 |. F1 vPushReg4 vR15 EDX DWORD v4 = unknownInit4
00441334 |. F2 vPushReg4 vR14 EAX DWORD v5 = unknownInit10
00441333 |. FF vPushReg4 vR1 EBX DWORD v6 = unknownInit9
00441331 |. F3 vPushReg4 vR13 ESI DWORD v7 = unknownInit6
00441330 |. F8 vPushReg4 vR8 EFL DWORD v8 = unknownInit5
0044132F |. F4 vPushReg4 vR12 ECX DWORD v9 = unknownInit8
0044132E |. FD vPushReg4 vR3 EDI DWORD v10 = unknownInit3
0044132B \. 86 vRet return v2; user32.GetDlgItem 这很明显是调用API,调用目标已经分析出来了,是user32.GetDlgItem。VMP在选择加密输入表后所有输入函数地址都是加密的,一般的函数是加上一个常量,但是虚拟机中调用的函数会用更复杂的计算,这里因为结果是常量所以分析时自动计算了。虚拟机中每次调用输入函数都会单独生成一个虚拟程序来解密地址和调用,插件只能自动分析普通的解密输入表代码,所以这段代码没有被标记为解密输入表。看看自动添加的分析提示/*esp(0); regModify(eax)*/,插件认为这个函数的ESP改变为0,它本身的改变确实为0,但是退出时是返回到API,相当于一个API调用,插件不知道这一点,所以要修正一下,其他API调用也要这样处理。计算ESP改变还用前面说过的方法,看看分析得到的堆栈信息
0046540A |. /D9 vJmp_00411C30 vESP=-0EC; ESP=-0E8; 堆栈长度=0038
0049C3CE |> \58 vPopReg4 vR14 vESP=-0E8; ESP=-0E8; 堆栈长度=0034
在调试记录一下调用前后的vESP,分别为0013F95C和0013F96C,分析结果是4,调试得到的结果是10,所以应该填写10-4=0C,就是/*esp(0C)*/。实际上确实是这样,GetDlgItem有两个参数,返回时弹出两个参数和一个返回地址。由于插件不知道ESP改变,所以没有自动添加参数提示,这里也加上arg(1,hWnd);arg(2,ControlID),再加个标签。GetDlgItem也有对其他寄存器的改变,比如ECX等,应该填regModify(ecx)的,但是普通的函数返回后一般不会使用除EAX(返回值)以外的寄存器,所以这些不填也没关系。后面所有API都这样处理,就不多做说明了,加密输入表后虚拟机中调用的API比较多的话还是有些麻烦,其实分析提示只填ESP改变就可以,加其他内容只是为了代码更好看。参数提示最好也填上,因为这个提示会告诉插件对应的数据时有效的,不应该被作为垃圾指令,否则函数返回后把参数弹出而且这之前没有对参数的引用的话,插件可能会认为参数对应的数据是没有用的。
这里开始准备取输入的用户名,调用前那一堆带EXIT的都是真实寄存器,只有退出虚拟机的调用才会标出寄存器名,插件为了调试方便把调用相关的代码比如恢复寄存器等都保留了,如果不需要的话不管他就行了,这里贴出的代码为了节省空间直接去掉了。
0049DD33 |. 34 vReadMemSs4 DWORD s0 = Stack(vESP + 38, ESP + 4, 4)
004C7830 |. 32 vWriteMemSs4 ControlID EXIT DWORD v32 = 3E8 //用户名输入框ID
00486CDA |. BF vWriteMemSs4 hWnd EXIT DWORD v33 = s0 //主窗口句柄,ESP + 4说明是第一个参数传进来的
0046540A |. D9 vJmp_00411C30 callVM //取用户名输入框句柄
004B5E69 |. 32 vWriteMemSs4 hWnd EXIT DWORD v42 = "用户名hwnd"
0048B7F5 |. 09 vJmp_00411C30 callVM //取输入的用户名长度
0048324C |. 34 vReadMemSs4 DWORD m5 = DWORD SS:[Je(AndFlag("用户名长度", "用户名长度")) + 0FFFFFF44]
00483229 |. FC vPushReg4 vR4 DWORD v51 = "用户名长度"
00483227 |. 1C vPushReg4 vR10 DWORD v52 = "用户名长度"
0048321A |. F1 vJmp_00412430 if ("用户名长度" != 0) goto VMPCrack.0044E3A0//检查输入的用户名长度是否为0
004565AC |. DC vPushReg4 vR6 Style EXIT DWORD v53 = v52
0043E13E |. 32 vWriteMemSs4 Title EXIT DWORD v54 = 4074EC //标题"VMPCrackMe"
00456CC4 |. E4 vWriteMemSs4 Text EXIT DWORD v55 = 4074DC //内容"请输入用户名"
00483041 |. E4 vWriteMemSs4 hOwner EXIT DWORD v56 = v38
004B6338 |. D9 vJmp_00411C30 callVM //弹出消息框
0042F277 |. 34 vReadMemSs4 DWORD s1 = Stack(vESP + 34, ESP + 0, 4)
...
0042EF25 |. 44 vRet return v67 //函数结束 上面是检查用户名是否为空,为空的话弹出消息"请输入用户名"并结束。
004482FA |. 50 vAdd4 DWORD v76 = SubFlag(v51, 20)
00479AB2 |. 34 vReadMemSs4 DWORD m10 = DWORD SS:[Ja(v76) + 0FFFFFF44]
00479A81 |. 05 vJmp_00412430 if (v51 <= 20) goto VMPCrack.004D0510 //v51 = "用户名长度"
0043EDAB |. 34 vReadMemSs4 DWORD m11 = DWORD SS:[Jpo(v76) + 0FFFFFF44]
0043ED7A |. 05 vJmp_00412430 if (Jpe(v76)) goto VMPCrack.004D4ABA 这里有一个比较if (v51 <= 20) goto VMPCrack.004D0510,在信息窗口看看,v51的值是"用户名长度",说明用户名长度不能超过0x20(32)个字符。后面的那个if (Jpe(v76))不知道是什么意思,v76等于SubFlag(v51, 20),正常代码中不太可能有这种东西,除非是转移类型分析错了。如果加了变形的话可能是垃圾分支,这种情况下两个分支的代码是相同的,等于把代码复制了一份。往下看看确实是这样,执行的代码都是弹出消息框然后退出,肯定是垃圾分支了,加个分析提示清除掉。添加提示/*jmp(4D4ABA)*/,告诉插件作为只有一个目标的转移处理,因为转移在反汇编阶段处理,必须重新反汇编才能生效,可以在分析点窗口中选择重新分析或直接按空格键。重新反汇编前一定要注意删除或禁用所有断点,因为普通断点会在代码中插入vRet,这样反出来的代码是错误的,到插入的vRet时就会停止。
重新反汇编后代码变成了
0043ED7A |. /05 vJmp_00412430 goto m11 ^ 6F67D83E
00440CB7 |. 50 vAdd4 Style EXIT DWORD v83 = 0
004DA716 |. BF vWriteMemSs4 Title EXIT DWORD v84 = 4074EC //标题"VMPCrackMe"
004346AA |. 32 vWriteMemSs4 Text EXIT DWORD v85 = 4074C4 //内容"用户名不能超过32个字符"
00439EB1 |. 32 vWriteMemSs4 hOwner EXIT DWORD v86 = v38
00439DA2 |. 09 vJmp_00411C30 callVM //弹出消息框
004D90C8 |. 94 vReadMemSs4 DWORD s2 = Stack(vESP + 34, ESP + 0, 4)
...
004D8D23 |. 16 vRet return v96 //函数结束 再往下看有一个调用,也是只有一个指令块,像是调用API,但最后不是调用,而是返回了
00450450 /$ 7E vPopReg4 vR2
004502F0 |. 0C vPopfd EFL = 0FFFFF700 & unknownInit6
004502ED |. 4C vReadMemSs4 DWORD s0 = Stack(vESP + 34, ESP + 0, 4)
004502EC |. 20 vReadMemDs1 BYTE m0 = BYTE DS:[s0]
0045018F |. 10 vMul2 WORD v0 = GetBytes(9 * (0 : (2595 ^ GetBytes(Rdtsc(), 0, 2)) % 1B5), 0, 2)
0045013C |. 8C vAdd4 DWORD v1 = 428F55 + v0
0045010A |. 5A vReadMemDs1 BYTE m1 = BYTE DS:[4 + v1]
00450108 |. 24 vReadMemDs4 DWORD m2 = DWORD DS:[v1]
00450005 |. 2D vReadMemDs4 DWORD m3 = DWORD DS:[5 + v1]
00450002 |. 0B vAdd4 DWORD v2 = m3 + Check(0 + (400000 + (862FE2A3 + ((708F098F ^ m2) + 1 - 1))), 0 : (0 : ByteToWord(m1))) + (((40 & AddFlag(34, m0)) >> 3) + Stack(34, 4))
00450000 |. F6 vPushReg4 vR10 EBP DWORD v3 = unknownInit8
0044FFFF |. FB vPushReg4 vR5 EDX DWORD v4 = unknownInit9
0044FFFE |. F7 vPushReg4 vR9 EAX DWORD v5 = unknownInit5
0044FFFD |. FF vPushReg4 vR1 EBX DWORD v6 = 77D1216B
0044FFFB |. 00 vPushReg4 vR0 ESI DWORD v7 = unknownInit4
0044FFFA |. F9 vPushReg4 vR7 EFL DWORD v8 = unknownInit6
0044FFF9 |. F5 vPushReg4 vR11 ECX DWORD v9 = unknownInit7
0044FFF8 |. F3 vPushReg4 vR13 EDI DWORD v10 = unknownInit10
0044FFF5 \. 86 vRet return v2 这里除EBX以外的寄存器都没有改变,EBX变成了77D1216B,看看地址,是GetWindowTextA,应该也是解密输入表,对应的汇编代码类似MOV EBX,[],再往后可能会有像CALL EBX这样的代码,往下看看果然是这样,有一个
0048EA90 |. B5 vRet call v120
就是调用GetWindowTextA,但是插件不知道,可以加一个提示/*call(77D1216B,4D7656)*/,表示调用目标为77D1216B(GetWindowTextA),返回到4D7656,返回到的地址是被调用函数返回时的地址,不是虚拟机中的地址,注意不同的系统API地址可能不同。
004337E4 |. E4 vWriteMemSs4 EXIT DWORD v105 = 21 //参数Count
00493ACB |. E4 vWriteMemSs4 EXIT DWORD v106 = 0FFFFFFE8 //参数Buffer
00493A68 |. 32 vWriteMemSs4 EXIT DWORD v107 = v51 //参数hWnd,v51 = "用户名hwnd"
0049398D |. EB vJmp_00411C30 callVM VMPCrack.00450450 //解密GetWindowTextA的地址,保存到EBX
...
0048EA90 |. B5 vRet call v120; user32.GetWindowTextA 由于加了最大保护,调用前的那些vNand是加密寄存器,先返回到解密寄存器的代码,解密后再调用,还有检查虚拟机完整性,会用vCheck随机检查一段内存和返回地址计算,所以内容有点长,这里没贴出来,不用管这些。由于参数压栈后有一个虚拟机内调用,然后才是API,所以没有自动标出,参数Buffer因为是堆栈地址,在分析时以vESP偏移的形式保存,FFFFFFE8表示vESP+18。
往下又是3个调用
0043365C |. BF vWriteMemSs4 "MD5对象" EXIT DWORD v129 = 0CC75CF87
...
004886EF |. 16 vRet call v135;
0043D766 |. 32 vWriteMemSs4 "长度" EXIT DWORD v144 = v141
004AD7AB |. BF vWriteMemSs4 "内容" EXIT DWORD v145 = 235A22B8
004B18A6 |. BF vWriteMemSs4 "MD5对象" EXIT DWORD v146 = 235A2234
...
004BCE2E |. B2 vRet call v152;
004B2E32 |. 32 vWriteMemSs4 "MD5对象" EXIT DWORD v161 = 15CC7877
00496BA8 |. BF vWriteMemSs4 "保存MD5值" EXIT DWORD v162 = 15CC7933
...
004963CC |. B5 vRet call v168; 这些代码中都把检查虚拟机完整性和加密寄存器的部分去掉了,太占地方,而且一眼就能看出来,不用管它就行了。先跟进第一个调用看看,那些常量很熟悉吧,就是MD5初始化。由于这几个函数都是调用方弹出参数,被调用函数只弹出返回地址,所以没有自动加参数提示,自己添加一下。但是分析出来的参数好像不对,比如要初始化的MD5对象是0CC75CF87,这显然不像是一个有效的地址,往前跟踪数据来源看看怎么回事。在信息窗口中一直跟随引用数据,最后跟到了调用GetWindowTextA时的EBP,由于EBP这时是常量,而且最大保护下调用函数前要加密寄存器,就是异或一个常量,结果还是常量,分析时自动计算了。但是调用函数时要先解密,插件没有处理,认为调用后还是加密的值,所以错了。找到原因就好,可以在返回后添加一个常量提示,返回后弹出EBP是在
0045B289 |. 08 vPopReg4 vR11 DWORD _t22128 = 0CC75D053
添加提示/*const(1,20)*/,表示第一个操作数的值是常量20,也就是加密前的EBP,后面的调用也做相同处理,其实不填写也可以,只要自己能看明白就行。后面的两个函数猜都能猜出来是什么了,初始化后肯定是计算、取MD5值,跟进去看看确实是这样,顺便加上标签和参数提示。
再往下看
00430681 |. C0 vNand4 ControlID EXIT DWORD v175 = 3E9 //注册码输入框ID
004305A8 |. 32 vWriteMemSs4 hWnd EXIT DWORD v176 = v173 //主窗口句柄
004A9744 |. 09 vJmp_00411C30 callVM //取注册码输入框句柄
004462EC |. 32 vWriteMemSs4 hWnd EXIT DWORD v185 = "注册码hwnd"
00498016 |. 09 vJmp_00411C30 callVM //取输入的注册码长度
00455B49 |. 55 vReadMemSs4 DWORD m41 = DWORD SS:[Jnz(AndFlag("注册码长度", "注册码长度")) + 0FFFFFF50]
00455B26 |. CC vPushReg4 vR7 DWORD v193 = "注册码长度"
00455B17 |. F1 vJmp_00412430 if ("注册码长度" == 0) goto VMPCrack.00466E05//检查是否输入了注册码,转到弹出提示"请输入注册码"
004DF80B |. 10 vAdd4 DWORD v194 = SubFlag(v193, 28)
00467664 |. 34 vReadMemSs4 DWORD m42 = DWORD SS:[Je(v194) + 0FFFFFF50]
00467632 |. F1 vJmp_00412430 if (v193 != 28) goto VMPCrack.00441B20//v193 = "注册码长度",检查注册码是否为28位,转到弹出提示"注册码错误。"
0048A6D2 |. 32 vWriteMemSs4 Count EXIT DWORD v195 = 29 //长度
0048A665 |. 32 vWriteMemSs4 Buffer EXIT DWORD v196 = 0FFFFFFAC //保存注册码
004812BF |. 32 vWriteMemSs4 hWnd EXIT DWORD v197 = v190 //v190 = "注册码hwnd"
...
004B2243 |. 16 vRet call v203; user32.GetWindowTextA 这回是取输入的注册码,调用GetWindowTextA的地址还是上面得到的那个,也加个分析提示/*call(77D1216B,4C80FC)*/,这里可以看出来注册码必须是0x28(40)位。
00496DC7 |. 32 vWriteMemSs4 "数组长度" EXIT DWORD v212 = 14
00496D91 |. 32 vWriteMemSs4 "数组" EXIT DWORD v213 = 0FFFFFFFC
004D2528 |. 32 vWriteMemSs4 "字符串长度" EXIT DWORD v214 = 28
004938F7 |. BF vWriteMemSs4 "字符串" EXIT DWORD v215 = 0FFFFFFAC//输入的注册码
...
0044C1AA |. B5 vRet call v221; //把输入的注册码字符串转成数组
00486E37 |. 94 vReadMemSs4 DWORD m56 = DWORD SS:[Je(AndFlag("结果长度", "结果长度")) + 0FFFFFF50]
00486E04 |. F1 vJmp_00412430 if ("结果长度" == 0) goto VMPCrack.00441B20//结果长度为0表示转换失败,注册码不是16进制字符串,转到弹出提示"注册码错误。" 这里调用了一个函数4024F0,是把16进制字符串转成数组,说明注册码必须是16进制字符串,注册码输入1234567812345678123456781234567812345678再往下跟。
0042E566 |. B9 vJmp_00411670 callVM //调用验证函数
004BE669 |. 34 vReadMemSs4 DWORD m57 = DWORD SS:[Jnz(AndFlag(GetBytes("验证成功", 0, 1), GetBytes("验证成功", 0, 1))) + 0FFFFFF50]
004BE637 |. 05 vJmp_00412430 if (GetBytes("验证成功", 0, 1) == 0) goto VMPCrack.00441B20//返回0为验证失败,转到弹出提示"注册码错误。"
004B66A0 |. BF vWriteMemSs4 Style EXIT DWORD v231 = 0
00454109 |. 32 vWriteMemSs4 Title EXIT DWORD v232 = 4074EC //标题"VMPCrackMe"
00491BBF |. 32 vWriteMemSs4 Text EXIT DWORD v233 = 1 //内容,地址为1?
00468648 |. E4 vWriteMemSs4 hOwner EXIT DWORD v234 = v182
004D80F4 |. 09 vJmp_00411C30 callVM //弹出消息框
...
0046DC11 |. 16 vRet return v246 42E566的调用像是关键的验证函数,因为他返回0就转到验证失败了,试试能不能在if (GetBytes("验证成功", 0, 1) == 0)爆破。可以执行到vJmp然后修改堆栈里的目标,或者先执行到验证失败的转移目标,再用菜单里的 此处为新vEIP 功能设置到正确的目标。执行后出错了,77d08944引用内存1,好像是调用MessageBoxA时出的,回去看看参数,内容的地址果然是1。那怎么显示消息?先不管他,跟进验证函数里看看。
先试试最简单的方法,看能不能在验证函数里爆破。因为知道了返回0是失败,在函数里找出所有返回,可以查找vRet(不包括用作调用的),一共找到6个,看看返回值EAX。
00475768 |. ED vPushReg4 vR0 EAX DWORD v84 = GetBytes(v80, 1, 3) : 0
...
0047575D |. 2E vRet return v83
//
004C8E1B |. F5 vPushReg4 vR2 EAX DWORD v95 = GetBytes(s43, 1, 3) : 0
...
004C8E10 |. 2E vRet return v94
//
0049DE80 |. ED vPushReg4 vR0 EAX DWORD v125 = GetBytes(s46, 1, 3) : 0
...
0049DE75 |. 2E vRet return v124
//
004A22AC |. 05 vPushReg4 vR6 EAX DWORD v140 = GetBytes(entryVMEax_4689BB, 1, 3) : 1
...
004A22A1 |. 2E vRet return v139
//
0048704D |. 11 vPushReg4 vR9 EAX DWORD v151 = GetBytes(entryVMEax_45CED7, 1, 3) : 0
...
00487042 |. 37 vRet return v150
//
004376C3 |. FD vPushReg4 vR4 EAX DWORD v162 = GetBytes(v120, 1, 3) : 0
...
004376B8 |. 2E vRet return v161 其中5个把EAX的低BYTE(AL)设为0,另一个设为1,应该就是验证成功的地方。找一找转移到这些返回的分支,可以在包含返回的指令块开始用右键菜单 转到-转移来自,看看是哪里跳过来的,最后找到下面这些。
004425EA |. /06 vJmp_00411670 if (v79 == v80) goto VMPCrack.0047E2E8//跳
0047DD76 |. /06 vJmp_00411670 if (v40 == s43) goto VMPCrack.004318DE//跳
0048D8F3 |. /06 vJmp_00411670 if (Cross(nonentity, v111) == v121) goto VMPCrack.004A927A//跳
0047F698 |. /94 vJmp_00411670 if (entryVMEax_45CED7 != entryVMEsi_45CED7) goto VMPCrack.00467F03//不跳
004CAA21 |. /06 vJmp_00411670 if (v119 != v120) goto VMPCrack.00437E53//不跳 跳到返回0的必须不跳才能能验证成功,跳过返回0的必须跳。先调试一下,跟踪时用设置vEIP功能设置到正确的目标,这回弹出了"注册码正确,验证完成。",看来是可以爆破的。但是为什么刚才内容地址是1呢,再回去看看那个1是哪里来的,向上跟踪数据来源,最后找到了
004DFC32 |. 00 000000 vPushImm4 407470 (常量)407470
004DFC30 |. 6F vReadMemDs4 内存00407470:(常量)1; 堆栈-0B4:(常量)407470 是从地址407470里读出来的,插件认为内存是常量,所以直接显示了,再看看407470的值,已经变成了3420000,应该是注册成功后写入的。到返回1的那个vRet往前看看,前面有
00446EE6 |. /1B vJmp_00411C30 callVM VMPCrack.0046BB01
004B16AB |. 8F vWriteMemDs4 DWORD DS:[407470] = v136; EXIT DWORD v136 = entryVMEax_4689BB 字符串地址是一个调用返回的,跟进那个调用
0046BB01 /$ 7B vPopReg4 vR5
0046B9AB |. 4C vReadMemSs4 DWORD m0 = DWORD SS:[Je(AndFlag(0, 0)) + 28]
0046B987 |. F4 vPushReg4 vR12 DWORD v0 = 0
0046B979 |. 2B vJmp_00411C30 if (0 != 0) goto VMPCrack.004B8F91
004B9438 |. 71 vPopReg4 vR15
004B93CD |. 0B vAdd4 ARG4 DWORD v1 = 4
004B939A |. 2C vNand4 ARG3 DWORD v2 = 3000
004B9378 |. 66 vNand4 ARG2 DWORD v3 = 18
004B9340 |. 41 vAdd4 ARG1 DWORD v4 = 0
004B92C2 |. 17 04 vCall 4 v5 = Call(495AD3); VMPCrack.00495AD3 //VirtualAlloc
004B9250 |. CE vWriteMemDs4 DWORD DS:[v5] = v6; DWORD v6 = 0E1B2A2D7 //产生字符串
004B91EA |. 16 vWriteMemDs4 DWORD DS:[v5 + 4] = v7; DWORD v7 = 0FDD5EBC2
004B913E |. E2 vWriteMemDs4 DWORD DS:[v5 + 8] = v8; DWORD v8 = 0ACA3B7C8
004B90AA |. 16 vWriteMemDs4 DWORD DS:[v5 + 0C] = v9; DWORD v9 = 0A4D6E9D1
004B9057 |. 16 vWriteMemDs4 DWORD DS:[v5 + 10] = v10; DWORD v10 = 0C9B3EACD
004B902C |. 16 vWriteMemDs4 DWORD DS:[v5 + 14] = v11; DWORD v11 = 0A3A1
004B8FE2 |. E2 vWriteMemDs4 DWORD DS:[491D42] = v12; EXIT DWORD v12 = v5
004B8F9E |. F8 vPushReg4 vR8 DWORD v0 = v5
...
004B8C46 \. 86 vRet return v15 有vCall,还是SDK,应该是解密字符串。上面有一个if (0 != 0) goto VMPCrack.004B8F91,0和0比较,一般不会这么做,最有可能的是其中一个是内存中读出来的,被插件当作常量了,往上看看,有
0046BA92 |. 00 000000 vPushImm4 491D42 (常量)491D42
0046BA90 |. 08 vReadMemDs4 内存00491D42:(常量)0; 堆栈24:(常量)491D42 确实是内存里读出来的,加个分析提示告诉插件这里不是常量,/*memConst(0,491D42,4)*/表示491D42开始的4字节不是常量,一定要加到内存块开始,就是40A000。再分析一下,上面的比较变成了
0046BA90 |. 08 vReadMemDs4 DWORD m0 = DWORD DS:[491D42]
0046B9AB |. 4C vReadMemSs4 DWORD m1 = DWORD SS:[Je(AndFlag(m0, m0)) + 28]
0046B987 |. F4 vPushReg4 vR12 DWORD v0 = m0
0046B979 |. 2B vJmp_00411C30 if (m0 != 0) goto VMPCrack.004B8F91 检查是否已经解密过字符串,如果解密过直接使用,否则调用VirtualAlloc分配内存,产生字符串保存起来。
现在可以爆破了,把那5个条件转移改掉就行,因为加了壳,这部分代码是压缩的,只能在解压后修改。你如果觉得脱壳更简单那就脱吧,我不会脱壳,连最简单的压缩壳都没有脱过,一直都是带壳分析带壳修改,这次为了测试插件才研究了一下VMP脱壳,后面会讲一下。先把补丁做好,方法很多,能改转移目标就行,比如把两个解密前的目标改成相同的值、在靠近vJmp的位置插入代码修改目标、在验证失败的指令块开始添加vJmp转到正确的目标等。因为加了最大保护,解密前的目标不好修改,这里先讲一下使用插入指令的方法。随便找一个分支,比如
0047DD76 |. /33 vJmp_00411670 if (v40 == s43) goto VMPCrack.004318DE
这个分支必须跳,由于插入指令至少需要8字节的空间来添加一些附加的代码,所以不能直接在vJmp插入,应该向前找几个指令,最近的是
0047DD80 |. 63 15CDF8FA vPushImm4 0EF74CFD4 DWORD _t51643 = 0EF74CFD4
在这里选择插入指令,会弹出一个窗口,建好了插入指令的框架,内容是
vPopReg4 vR14 ;弹出重定位
;在这里添加指令
vPushImm4 0EF74CFD4 ;被覆盖的指令
vPushReg4 vR3 ;被覆盖的指令
vAdd4 ;被覆盖的指令
vPopReg4 vR5 ;被覆盖的指令
vPushReg4 vR14 ;重定位
vPushImm4 0047DD79 ;目标地址
vJmp_00411670 ;转到原来的位置 在上面标出的位置添加指令,由于是想修改转移目标,看看保存在哪个寄存器中。执行vJmp时栈顶的值是返回地址,一般是离vJmp最近的那个vPushReg4压入的,这个寄存器是vR9,所以添加代码
vPushImm4 4318DE
vPopReg4 vR9
这样就可以改变转移目标了,由于VMP中没有MOV指令,只能通过这种方法来赋值。然后找个地方来保存插入的代码,因为要带壳修改,最好放到不用解压的段里,一般找包含模块入口点的段就可以了,这个段在解压前就会执行。这个程序里是.vmp1,在后面找一块空的地方。选择保存位置时要注意是否为反向指令块,这时代码是从后向前保存的,这次要修改的代码就在反向指令块中,所以直接放在最后就行了,地址设置为5C0FFF。
插入指令会把原来的指令块拆分成两个,添加一个vJmp到插入的指令,执行插入代码后再执行被覆盖的指令,最后跳回原来的位置,还会自动处理重定位等。这是比较通用的方法,可以用于任何修改,而且有些修改由于会覆盖原来的指令,只能用这种方法,但缺点是生成的代码比较多。如果只是想改转移目标的话还有更简单的方法,就是在验证失败的指令块开始添加vJmp转到正确的目标,还是刚才那个分支,验证失败时转到004C9665,可以在这里修改代码,转到正确的目标4318DE
vPushImm4 4318DE
vJmp_00411670
只修改这两条指令就可以了,但是因为VMP使用类似流加密的方法加密指令,即使只修改一个指令,也要重新编译从修改位置到指令块结束的所有代码,这些插件都自动做了。由于是在指令块开始修改,整个指令块都重新编译了,修改范围太大,最大保护下很容易碰到验证完整性的代码,还要处理验证。但是这里修改后的指令是vJmp,后面的指令不会执行了,打补丁时只写入这两条指令就可以。把其他几个转移也修改一下,下面是修改后的代码和对应的内存内容
004400A8 8A 1914727C vPushImm4 47E2E8
004400A3 64 vJmp_00411670
004400A2 64 19 14 72 7C 8A
//
004C9665 47 C8CD0790 vPushImm4 4318DE
004C9660 23 vJmp_00411670
004C965F 23 C8 CD 07 90 47
//
004C9B00 22 95AD0CE9 vPushImm4 4A927A
004C9AFB A2 vJmp_00411670
004C9AFA A2 95 AD 0C E9 22
//
00467F03 21 14AEF0B4 vPushImm4 43C25B
00467EFE 82 vJmp_00411670
00467EFD 82 14 AE F0 B4 21
//
00437E53 71 E3CBEF76 vPushImm4 461C3F
00437E4E B6 vJmp_00411670
00437E4D B6 E3 CB EF 76 71 注意修改的这些指令块都是反向的,内存里的代码指令顺序相反。还有压栈的转移目标是重定位前的,如果修改DLL的话要注意这一点。现在在运行一下结果出错了,因为修改范围太大被查到了。重启程序,不用插件的汇编功能,只写入修改过的代码,随便输入40位16进制数就注册成功了,长度限制就不改了,否则输入超过40位可能会溢出。现在的问题是要修改的部分是解压出来的,不能直接修改文件,只能在解压后再写入。这也有很多方法,关键是解压后转到我们的代码,最简单的方法就是拦截程序真正的入口,走到这里肯定是解压完成的。但是这可能有一个问题,VMP在解压后会把段的内存属性设置成和加壳前相同,一般程序的代码段都是不可写的,这时可以自己修改内存属性或在VMP修改属性前写入补丁。因为这次只修改虚拟机代码,这些代码在VMP自己的段中,这个段始终是可写的,可以不考虑这个问题,等讲完脱壳后你会发现更好的拦截点。下面是找入口,VMP加壳后程序的入口很好找,先重启程序停在模块入口点然后分析,完成后看看记录,会发现这样的提示
开始反汇编虚拟程序...
指令块44DB51没有初始化,可能已加壳还没有解压完成,请解压完成后重新分析
反汇编完成,75个指令块,7892个指令
因为真正的入口代码是压缩的,分析的时候还没有解压,插件不能继续分析,所以会给出这个提示,这里的44DB51就是真正的入口。只有入口代码也被虚拟化后才这样,如果没有被虚拟化的话就是一个vRet退出虚拟机返回到入口。到44DB51看看
004E68CD |. BD vJmp_005BED37 ; 连接 VMPCrack.0044DB51; VMPCrack.0044DB51
0044DB51 |> 00 DB 00 ; 指令块没有被初始化 等壳的入口执行完后就会通过这个转移转到真正的入口,可以在这里插入一段代码写入补丁。如果不想出虚拟机的话可以使用vWriteMemDs来写入,但是直接写虚拟指令的话有点麻烦,如果补丁较长要写大量代码,可以用vCall调用一段汇编。vCall指令好像不是所有版本都有的,如果遇到没有的版本就只能通过vRet来调用了,但是要自己处理真实寄存器的恢复、返回时的虚拟程序初始化等,比较麻烦,还不如直接写虚拟指令了。先在.vmp1段里找一块空位,这个段是不需要解压的,写入补丁代码
005C0900 C705 A2004400 64191472 MOV DWORD PTR DS:[4400A2],72141964
005C090A 66:C705 A6004400 7C8A MOV WORD PTR DS:[4400A6],8A7C
005C0913 C705 5F964C00 23C8CD07 MOV DWORD PTR DS:[4C965F],7CDC823
005C091D 66:C705 63964C00 9047 MOV WORD PTR DS:[4C9663],4790
005C0926 C705 FA9A4C00 A295AD0C MOV DWORD PTR DS:[4C9AFA],0CAD95A2
005C0930 66:C705 FE9A4C00 E922 MOV WORD PTR DS:[4C9AFE],22E9
005C0939 C705 FD7E4600 8214AEF0 MOV DWORD PTR DS:[467EFD],F0AE1482
005C0943 66:C705 017F4600 B421 MOV WORD PTR DS:[467F01],21B4
005C094C C705 4D7E4300 B6E3CBEF MOV DWORD PTR DS:[437E4D],EFCBE3B6
005C0956 66:C705 517E4300 7671 MOV WORD PTR DS:[437E51],7176
005C095F C3 RETN 注意保护寄存器,vCall指令不会保护任何寄存器,要自己来做,这里没有用到寄存器所以不用处理。然后在那个转到入口的vJmp前插入指令
005C0FFF 97 vPopReg4 vR12 ; 弹出重定位
005C0FFE C6 49EC81D9 vPushImm4 5C0900 //补丁代码地址
005C0FF9 FC 4F vCall 0 //调用补丁代码,0个参数
005C0FF7 EF vPopReg4 vR10 //弹出返回值
005C0FF6 BE 802FF1B7 vPushImm4 0EF74CFD4 ; 被覆盖的指令
005C0FF1 C2 vAdd4 ; 被覆盖的指令
005C0FF0 D1 vPopReg4 vR10 ; 被覆盖的指令
005C0FEF CC vPushReg4 vR12 ; 被覆盖的指令
005C0FEE D1 vPushReg4 vR12 ; 重定位
005C0FED 80 5893A8AA vPushImm4 4E68CF ; 目标地址
005C0FE8 51 vJmp_005BED37 ; 转到原来的位置 先不保存,直接运行试试,提示文件损坏,看来有内存检查。重启一下,先把那段汇编代码填进去,没有问题,可能VMP不检查空位,只检查有效代码。再把虚拟指令的修改加上,这回查到了,设个硬件访问断点试试,断不到,可能被清了,换内存访问断点试试,又提示发现调试器,没办法了,只能看代码了。这些都是壳的入口里检查,还是留到脱壳时再讲吧,先直接给一个地址
005B15C3 |. /06 ||vJmp_005BED37 if (GetBytes(v331, 0, 1) != 0) goto
让他不跳就可以了,这回用插入指令的方法修改吧,因为有很多转移转到这个目标。注意现在直接反汇编入口的话看不到这个指令,因为中间有异常处理,插件不能自动分析,以后再讲。把所有添加的部分(包括插入指令时修改的虚拟机代码)选择复制到可执行文件然后保存,启动保存的文件又提示文件损坏,内存检查不是已经去掉了吗?保存文件后才被查到,那就可能是文件检查,还是看代码吧,改下面的地址
005A9B1F |. /6A ||vJmp_005BED37 if (GetBytes(v313, 0, 1) != 0) goto
让他不跳,其他的以后再讲。这回再保存文件,可以正常运行了,但是输入注册码后点确定进程结束,退出代码是DEADC0DE,肯定又被查到了。检查注册码时不是调了SDK吗,把它也改掉,由于这也是压缩的代码,放到上面的补丁里吧。
爆破完成,下面看看能不能找出算法,跟进验证函数里看看,这里贴出的代码还是去掉调用相关代码和垃圾转移后的。
0045B998 /$ C8 vPopReg4 vR14
0045B8A4 |. CA vPopVEsp vESP = 0FFFFFFFC
0045B722 |. F5 vReadMemSs4 DWORD s0 = Stack(vESP + 38, ESP + 4, 4)
00485989 |. 98 vWriteMemSs4 EXIT DWORD v0 = unknownInit2
004858DE |. 95 vWriteMemSs4 EXIT DWORD v1 = unknownInit6
004BBC36 |. 37 vWriteMemSs4 EXIT DWORD v2 = unknownInit5
00456E9A |. A1 vWriteMemSs4 "长度" EXIT DWORD v3 = 10
00484B5C |. 5A vWriteMemSs4 "结果" EXIT DWORD v4 = 0FFFFFFFC
004A64A2 |. 39 vWriteMemSs4 "内容" EXIT DWORD v5 = s0 //用户名MD5
004A63DD |. 12 vWriteMemSs4 "密码" EXIT DWORD v6 = 407498 //QWERTYUI
004B865E |. 63 vWriteMemSs4 "加解密" EXIT DWORD v7 = 1 //解密
...
00484180 |. 2A vRet call v13; 开始就是一个调用,跟进去看看,好像内容很多,没准是通用的算法。用peid打开进程,Krypto ANALyzer插件显示有base64、crc32、des、md5,md5刚才看到了,crc32和base64又不像,那是des?我对算法不是很熟悉,看看能不能找到什么特征,des应该有好几个表。跟进第一个调用里就有
0040122C |> /8A86 D0604000 /MOV AL,BYTE PTR DS:[ESI+4060D0]
4060D0的内容是
004060D0 01 01 02 02 02 02 02 02 01 02 02 02 02 02 02 01
找个des代码看看,确实有这个,就是其中的bitDisplace。肯定是des了,加个标签,再把参数提示填上,因为这是调用方弹出参数,插件不会自动添加。遇到这种情况最好添加提示,否则插件不知道函数引用这些参数,可能把他们当成没用的而不显示。也可以在调用的vRet添加/*exitValid(1)*/,这样会认为退出虚拟机后可以引用的数据都是有效的,但不会在代码中标出参数。函数还会把加解密的结果保存到堆栈里,看看参数,地址是0FFFFFFFC就是-4,在vRet添加一个提示/*vStackModify(-4,10,MD5)*/,表示函数把堆栈-4开始10字节内容变成了MD5,以后用到时会自动标出来。参数加解密为1是解密,要解密的内容是从ESP+4读出来的,应该是第一个参数,就是用户名的MD5,密码是QWERTYUI。
0042AAF1 |. D0 vReadMemSs4 DWORD m6 = DWORD SS:[Jnz(~v17 | AddFlag(LoWord(v21), 0FFFF)) + 0FFFFFFD4]
0042AABF |. 55 vJmp_00411670 if (Jnz(~v17 | AddFlag(LoWord(v21), 0FFFF))) goto VMPCrack.004DD64A
0047F53A |. 68 vReadMemSs4 DWORD s1 = Stack(vESP + 3C, ESP + 8, 4)
0048656B |. 1E vWriteMemSs4 EXIT DWORD v22 = 10
00486460 |. 93 vPopVEsp vESP = 0FFFFFFD8
004862BF |. 30 vReadMemSs4 DWORD m7 = DWORD SS:[Jle(v17) + 0FFFFFFD0]
00486298 |. 92 vPushReg4 vR0 DWORD v23 = s1
0048628D |. 1D vJmp_00411670 if (Jg(SubFlag(30, 34) ^ 8C4)) goto VMPCrack.004DBBDF 又有两个奇怪的比较,看看分支的代码都很像,可能还是垃圾分支,加提示清除掉。由于第二个垃圾分支在第一个后面,可以直接让第一个跳过去,添加/*jmp(4DD64A)*/后重新反汇编。这里再次提醒重新反汇编前要禁用或删除所有断点,否则断点插入到程序中的vRet会对反汇编造成影响,我就经常忘记,结果反出来很奇怪的代码。现在变成了
0042AABF |. /55 vJmp_00411670 goto 0ACA0F192 ^ m6
004DD5E1 |. 88 vReadMemSs4 DWORD s1 = Stack(vESP + 3C, ESP + 8, 4)
004D7808 |. 2D vWriteMemSs4 EXIT DWORD v22 = 10 //参数"长度"
004778EA |. 9E vWriteMemSs4 EXIT DWORD v23 = 20 //参数"结果"
0046732B |. 54 vWriteMemSs4 EXIT DWORD v24 = 20 //参数"内容",输入的注册码
00467233 |. DC vWriteMemSs4 EXIT DWORD v25 = 40748C //参数"密码",ASDFGHJK
00438232 |. CA vWriteMemSs4 EXIT DWORD v26 = 1 //参数"加解密",解密
004380AF |. 79 vReadMemSs4 DWORD m7 = DWORD SS:[Je(AddFlag(5, 0)) + 0FFFFFF78]
0043808B |. 97 vPushReg4 vR2 DWORD v27 = 20
00438089 |. 9F vPushReg4 vR9 DWORD v28 = 5
00438088 |. B6 vPushReg4 vR15 DWORD v29 = s1
0043807D |. 43 vJmp_00411670 if (5 + 0 == 0) goto VMPCrack.0046964E
0043807C |> 10 /vPopReg4 vR13
0043804F |. F1 |vReadMemDs4 DWORD m8 = DWORD DS:[v29]
0043804D |. FA |vWriteMemEs4 DWORD ES:[v27] = v30; DWORD v30 = m8
00437FB4 |. BA |vAdd4 DWORD v31 = 0FFFFFFFC + ((0FFFFFBFF ~& v17) >> 7)
00437EA2 |. 78 |vReadMemSs4 DWORD m9 = DWORD SS:[Jnz(AddFlag(v28, 0FFFFFFFF)) + 0FFFFFF78]
00437E7E |. F8 |vPushReg4 vR7 DWORD v27 = v27 + v31
00437E7C |. 04 |vPushReg4 vR0 DWORD v28 = v28 + 0FFFFFFFF
00437E7B |. 1D |vPushReg4 vR11 DWORD v29 = v29 + v31
00437E70 |.^ B4 \vJmp_00411670 if (v28 != 0) goto VMPCrack.0043807C
0046964E |> 26 vPopReg4 vR12
...
004E0003 |. 43 vRet call v37; 有一个循环,前面还有一个判断if (5 + 0 == 0),常量和常量比较?看看是干什么的。循环里有vWriteMemEs4,一般很少用到ES段,可能是MOVS之类的指令。地址每次循环增加0FFFFFFFC + ((0FFFFFBFF ~& v17) >> 7),很奇怪的计算,往前看看v17的值是SubFlag(30, 34) ^ 8C4,用标志来计算地址?既然是MOVS之类的指令前面很可能有REP,0FFFFFBFF二进制是11111111111111111111101111111111,第10位为0,对应DF(方向标志),和标志与非再右移7等于取出DF取反并设置到第3位,结果是0或8,再加上0FFFFFFFC(-4)就是-4或4。这就明白了,这个循环就是REP MOVS,根据方向标志每次对地址加4或减4,因为长度已知(5个DWORD),所以前面的判断显示为常量。
因为循环中写入的地址不是常量,插件不会把对应的数据添加到已知数据中,应该在写入的位置添加一个提示/*vStackModify(20,14,"注册码")*/,表示这个指令把堆栈20开始的14字节设为注册码。如果有写入地址不是常量的内存而你又知道地址的话,最好填上vStackModify或vMemModify提示,否则后面分析到这个地址的时候可能不知道数据已经改变。如果改变前数据没有初始化还好,直接生成一个读堆栈或读内存操作,否则分析时会认为数据还是初始化的内容,得到错误的结果,尤其是有循环的时候。比如
int a[4] = {0};//a初始化为0
int b;
for (int i = 0; i < 4; i++)
{
a[i] = 1;//把a中所有的值设为1,由于这里i不是常量,不会把这个赋值添加到已知数据中
}
b = a[0];//这里会认为a中的值还是0 遇到这种情况只要在对a赋值的地方填上vStackModify提示就可以了,因为循环4次,每次写4字节,应该填写vStackModify(a的地址,10,写入内容(可不填))。
后面又是des,因为参数压栈和调用隔着一个循环,没有自动标出,插件只能标出和调用在同一个指令块中的参数。地址是20,就是刚才那个循环复制的内容,ESP+8读出来的,应该是第二个参数,看看内容是1234567812345678123456781234567812345678,是我们输入的注册码,这里是要把输入的注册码解密。
0048DF12 |. 4A vWriteMemSs1 BYTE v46 = GetBytes(MD5, 8, 1) + GetBytes(MD5, 0C, 1) + GetBytes(MD5, 4, 1) + GetBytes(MD5, 0, 1) ^ 12
004C1464 |. B5 vReadMemSs4 DWORD v47 = GetBytes(MD5, 4, 4)
00487AF7 |. 67 vWriteMemSs1 BYTE v48 = GetBytes(MD5, 1, 1) - GetBytes(MD5, 0D, 1) - GetBytes(MD5, 9, 1) - GetBytes(MD5, 5, 1) ^ 34
00483EB5 |. 79 vWriteMemSs1 BYTE v49 = LoByte(ERROR("")) + 56
00454C74 |. 68 vWriteMemSs1 BYTE v50 = (GetBytes(MD5, 0F, 1) ^ GetBytes(MD5, 0B, 1) ^ GetBytes(MD5, 7, 1) ^ GetBytes(MD5, 3, 1)) - 78
004D37D0 |. D6 vWriteMemSs4 EXIT DWORD v51 = (GetBytes(MD5, 0, 4) ^ v50 : (v49 : (v48 : v46))) + (GetBytes(MD5, 8, 4) ^ v47) + (GetBytes(MD5, 0C, 4) ^ 98765432)
004C6383 |. 08 vWriteMemSs4 "结果" EXIT DWORD v52 = 0C
004C5160 |. A4 vWriteMemSs4 "密码" EXIT DWORD v53 = 0FFFFFFFC //解密后的MD5
004C4FD9 |. 0C vWriteMemSs4 "内容" EXIT DWORD v54 = 0C //上面计算的内容
0045E205 |. 09 vJmp_00425E0A callVM
004606F0 |. F8 vReadMemSs4 DWORD v57 = v50 : (v49 : (v48 : v46))
00460667 |. F1 vReadMemSs4 DWORD v58 = Cross(GetBytes("注册码", 0, 4), nonentity)
0044261C |. EF vReadMemSs4 DWORD m16 = DWORD SS:[Jnz(SubFlag(v57, v58)) + 0FFFFFFE8]
004425EA |. 31 vJmp_00411670 if (v57 == v58) goto VMPCrack.0047E2E8 //检查注册码第一部分
00475ACE |. 2E vReadMemSs4 DWORD s2 = Stack(vESP + 34, ESP + 0, 4)
00475768 |. 4B vPushReg4 vR0 EAX DWORD v62 = GetBytes(v58, 1, 3) : 0
...
0047575D |. 95 vRet return v61
0047E2E8 |> B8 vPopReg4 vR14
0047E00E |. 25 vReadMemSs4 DWORD v70 = Cross(GetBytes("注册码", 4, 4), nonentity)
0047DDA8 |. B4 vReadMemSs4 DWORD m21 = DWORD SS:[Jnz(SubFlag(v51, v70)) + 0FFFFFFE8]
0047DD76 |. 33 vJmp_00411670 if (v51 == v70) goto VMPCrack.004318DE //检查注册码第二部分
004C9144 |. 82 vReadMemSs4 DWORD s3 = Stack(vESP + 34, ESP + 0, 4)
004C8E1B |. 9A vPushReg4 vR2 EAX DWORD v74 = GetBytes(v70, 1, 3) : 0
...
004C8E10 |. 8C vRet return v73 上面那几个计算应该不用讲了吧,因为前面添加了提示,连内容都标出来了。有一个LoByte(ERROR("")) + 56,这可能是未知指令造成的,未知指令自动分析得到的信息会把操作表达式设为错误。往前看看那个错误表达式是哪里来的,找到
0047FE58 |. A7 未知指令 WORD _t13010 = ERROR(""); DWORD _t13011 = ERROR("")
确实是未知指令,从主菜单里打开虚拟指令窗口,找到这个指令
0040CF8A /MOV DL,BYTE PTR SS:[EBP] ; 未知指令
0040CF8F |MOV AL,BYTE PTR SS:[EBP+2]
0040CF9A |SUB EBP,2
0040CFA0 |IMUL DL
0040CFA8 |MOV WORD PTR SS:[EBP+4],AX
0040CFB6 |PUSHFD
0040CFC1 \POP DWORD PTR SS:[EBP] 有IMUL DL,应该是1字节有符号乘法,指令信息里没有这个指令,添加一下。特征用
IMUL DL
MOV WORD PTR [EBP+4],AX
PUSHFD
把指令信息中的分析虚拟机时自动获取选上,这样会自动分析指令信息。填写完后保存指令信息文件然后重启OD,因为分析时使用指令索引,如果在中间添加指令的话后面的索引都会改变,导致分析结果中的指令错位,已经开始分析后只能在最后添加指令,否则需要重启OD。重启完后先分析虚拟机,这回没有未知指令了,看看指令信息,已经分析出来了。有两个写操作数,填上操作表达式,分别为op1 ** op2和ImulFlag(op1, op2),记得点保存。然后再分析虚拟程序,变成了
00483EB5 |. 38 vWriteMemSs1 BYTE v49 = LoByte(LoByte(LoByte(GetBytes(MD5, 0E, 1) ** GetBytes(MD5, 0A, 1)) ** GetBytes(MD5, 6, 1)) ** GetBytes(MD5, 2, 1)) + 56
有一个调用,跟进去看看
0045E54B /$ D2 vPopReg4 vR13
0045E619 |. 0A vPopVEsp vESP = 28
00436936 |. 3D vReadMemSs4 DWORD s0 = Stack(vESP + 3C, ESP + 8, 4) //参数2
004DC3B7 |. 06 vReadMemDs4 DWORD m0 = DWORD DS:[s0]
004DC53F |. 06 vReadMemDs4 DWORD m1 = DWORD DS:[s0 + 4]
004A12F0 |. 03 vWriteMemSs4 EXIT DWORD v0 = m1
004A130B |. 06 vReadMemDs4 DWORD m2 = DWORD DS:[s0 + 8]
004A1419 |. 49 vReadMemDs4 DWORD m3 = DWORD DS:[s0 + 0C]
004956C2 |. BD vReadMemSs4 DWORD s1 = Stack(vESP + 38, ESP + 4, 4) //参数1
004D8325 |. 06 vReadMemDs4 DWORD m4 = DWORD DS:[s1]
004D843F |. 06 vReadMemDs4 DWORD m5 = DWORD DS:[s1 + 4]
004D87DE |. 0A vPopVEsp vESP = 18
004D8823 |. 41 vPushReg4 vR4 DWORD v1 = m5 ^ 2B3C4D5E
004D8824 |. B1 vPushReg4 vR11 DWORD v2 = 10 //循环次数,0x10(16)
004D8827 |. 91 vPushReg4 vR9 DWORD v3 = 0
004D8828 |. 01 vPushReg4 vR0 DWORD v4 = m4 ^ 1A2B3C4D
004D8832 |> 62 /vPopReg4 vR6
004BBE28 |. B3 |vNand4 DWORD v5 = v3 - 61C88647
004C2D6D |. C8 |vAdd4 DWORD v6 = (v1 >> 5) + v0
004C2F17 |. 60 |vShl4 DWORD v7 = v1 << 4
004B0A40 |. 14 |vNand4 DWORD v8 = m0 + v7 ^ v6
004B0AF2 |. C3 |vAdd4 DWORD v9 = v1 + v5
00492E61 |. 14 |vNand4 DWORD v10 = v8 ^ v9
0043585B |. C8 |vAdd4 DWORD v11 = v4 + v10
00434A13 |. 3D |vReadMemSs4 DWORD m6 = DWORD SS:[Jnz(DecFlag(v2)) + 10]
00434A35 |. 61 |vPushReg4 vR6 DWORD v1 = v1 + ((v11 >> 5) + m3 ^ (v11 << 4) + m2 ^ v11 + v5)
00434A36 |. 91 |vPushReg4 vR9 DWORD v2 = v2 + 0FFFFFFFF
00434A39 |. F1 |vPushReg4 vR15 DWORD v3 = v5
00434A3A |. B1 |vPushReg4 vR11 DWORD v4 = v11
00434A45 |.^ 7A \vJmp_00425E0A if (Jnz(DecFlag(v2))) goto VMPCrack.004D8832
00492A56 |. B7 vNand4 DWORD v12 = v4 ^ 4D3C2B1A
00492B05 |. 3D vReadMemSs4 DWORD s2 = Stack(vESP + 40, ESP + 0C, 4) //参数3
004CA064 |. 53 vNand4 DWORD v13 = v1 ^ 5E4D3C2B
00478252 |. EE vWriteMemDs4 DWORD DS:[s2] = v14; DWORD v14 = v12 //保存结果
004782E1 |. EE vWriteMemDs4 DWORD DS:[s2 + 4] = v15; DWORD v15 = v13 //保存结果
004CBEC2 |. 3D vReadMemSs4 DWORD s3 = Stack(vESP + 34, ESP + 0, 4)
...
004CC1F3 \. AE vRet return v18 有3个参数,都是指针,参数1两个DWORD,参数2四个DWORD,经过一个0x10次的循环计算出两个DWORD后保存到参数3中。由于加了变形,垃圾转移分隔了一部分操作,循环中的计算连起来是
v5 = v5 - 61C88647
v11 = v11 + ((v1 >> 5) + m1 ^ (v1 << 4) + m0 ^ v1 + v5)
v1 = v1 + ((v11 >> 5) + m3 ^ (v11 << 4) + m2 ^ v11 + v5) 看起来很熟悉,尤其是那个常量,对,就是tea加密的代码,参数1是内容,参数2是密码,参数3是保存结果。但是循环前后好像多了一些东西,加密前和加密后都异或了一个常量,可能是修改过的tea,填上标签和参数提示再回去看刚才的代码。现在参数已经标出来了,密码地址是0FFFFFFFC,就是第一次des的结果,用户名md5。要加密的内容地址是0C,好像没看到过这个地址,往前看看,前面那几个计算,就是变量v46、v48、v49、v50、v51,都保存在0C开始的8字节以内,这就是要加密的内容。后面是两个比较,第一个是if (v57 == v58),看看变量内容,是检查 v50 : (v49 : (v48 : v46)) == Cross(GetBytes("注册码", 0, 4), nonentity),就是注册码的第一个DWORD。因为注册码是在最开始的那个循环里复制过来的,但是前面有一个分支跳过循环,也就是说这个循环可能不会执行(其实不是,因为比较是常量,但插件不知道),这个路径中注册码没有定义,所以产生了一个交汇,就是说这里可能是"注册码"或nonentity(不存在的变量)。第二个比较是if (v51 == v70),检查注册码的第二个DWORD。
下面有一个垃圾分支
0043505F |. /06 vJmp_00411670 if (0FFFFFF9C + 50 >= 0) goto VMPCrack.00482F6A
添加提示/*jmp(482F6A)*/清除掉,重新反汇编后再看
0043183F |. 93 vReadMemSs4 DWORD v82 = GetBytes(MD5, 0, 4)
004DA066 |. A4 vReadMemSs4 DWORD v83 = GetBytes(MD5, 0C, 4)
004D9FDD |. 2B vReadMemSs4 DWORD v84 = GetBytes(MD5, 8, 4)
0042B0C3 |. 49 vWriteMemSs1 EXIT BYTE v85 = (GetBytes(v51, 0, 1) ^ v46) + (GetBytes(MD5, 0, 1) ^ 0AA)
0042AF7A |. 2B vReadMemSs4 DWORD v86 = GetBytes(MD5, 4, 4)
004AB52C |. D3 vWriteMemSs1 EXIT BYTE v87 = (GetBytes(v51, 1, 1) ^ v48) + (GetBytes(MD5, 5, 1) ^ 0BB)
004B20AF |. 38 vWriteMemSs1 EXIT BYTE v88 = (v49 ^ GetBytes(v51, 2, 1)) + (GetBytes(MD5, 0A, 1) ^ 0CC)
00482E81 |. 38 vWriteMemSs1 EXIT BYTE v89 = (GetBytes(MD5, 0F, 1) ^ 0DD) + (GetBytes(v51, 3, 1) ^ v50)
0045F1B8 |. E1 vDiv4 DWORD v90 = 0 : (v82 ^ (v83 ^ v84 ^ v86)) % 5
004D277E |. 2B vReadMemSs4 DWORD m27 = DWORD SS:[Ja(SubFlag(v90, 4)) + 0FFFFFFE8]
004D275E |. 19 vPushReg4 vR11 DWORD v91 = v84
004D274B |. 94 vJmp_00411670 if (v90 > 4) goto VMPCrack.00470963
0047C94E |. 8C vReadMemDs4 DWORD m28 = DWORD DS:[(v90 << 2) + 40609C]
0047C920 |.- 06 vJmp_00411670 switch (v90)
004DD0F5 |> F3 vPopReg4 vR2 //switch 0
00437358 |. 31 vWriteMemSs4 EXIT DWORD v92 = (v82 ^ 11223344) + (v86 ^ 22334455)
004B3409 |> 03 vPopReg4 vR6 //switch 1
004B31D6 |. C7 vNand4 DWORD v93 = v91 ^ 44556677
0048CF6D |. 31 vWriteMemSs4 EXIT DWORD v92 = v93 + (v86 ^ 33445566)
0048CD9F |. 1D vPushReg4 vR12 DWORD v91 = v93
0047C627 |> FB vPopReg4 vR4 //switch 2
0049ED8F |. 00 vNand4 DWORD v94 = v91 ^ 55667788
004E19BC |. 31 vWriteMemSs4 EXIT DWORD v92 = v94 + (v83 ^ 66778899)
004E18D6 |. F1 vPushReg4 vR1 DWORD v91 = v94
004A9743 |> EF vPopReg4 vR1 //switch 3
00472BED |. 31 vWriteMemSs4 EXIT DWORD v92 = (v83 ^ 778899AA) + (v82 ^ 8899AABB)
0045E0BA |> 0B vPopReg4 vR8 //switch 4
0045DF80 |. 00 vNand4 DWORD v95 = v91 ^ 0AABBCCDD
0045DE90 |. 00 vNand4 DWORD v96 = v82 ^ 99AABBCC
00470B2F |. 48 vAdd4 DWORD v97 = v96 + v95
004709E8 |. BA vWriteMemSs4 EXIT DWORD v92 = v97
0047098C |. FD vPushReg4 vR4 DWORD v91 = v97
00470963 |> 1B vPopReg4 vR12
0047881C |. 31 vWriteMemSs4 "结果" EXIT DWORD v98 = 14
0047CAB2 |. 31 vWriteMemSs4 "密码" EXIT DWORD v99 = 0FFFFFFFC //解密后的MD5
00434420 |. BA vWriteMemSs4 "内容" EXIT DWORD v100 = 14 //上面计算的内容
00481A39 |. 11 vPushReg4 vR9 EXIT DWORD v101 = v91
00481A32 |. 6F vAdd4 EXIT DWORD v102 = 0EF8B1AAC
00481A2E |. 52 vJmp_00412430 callVM
004B4216 |. 2B vReadMemSs4 DWORD v103 = Cross(GetBytes("注册码", 8, 4), nonentity)
004B4154 |. A4 vReadMemSs4 DWORD v104 = v89 : (v88 : (v87 : v85))
004B3F46 |. 00 vNand4 DWORD v105 = v103 ^ 13579BDF
004CAA54 |. CE vReadMemSs4 DWORD m29 = DWORD SS:[Je(SubFlag(v104, v105)) + 0FFFFFFE8]
004CAA21 |. 06 vJmp_00411670 if (v104 != v105) goto VMPCrack.00437E53 //检查注册码第三部分
0046198A |. A4 vReadMemSs4 DWORD v106 = Cross(GetBytes("注册码", 0C, 4), nonentity)
004A8CA3 |. 73 vNand4 DWORD v107 = v106 ^ 0FDB97531
0048D926 |. 2B vReadMemSs4 DWORD m30 = DWORD SS:[Jnz(SubFlag(Cross(nonentity, v92), v107)) + 0FFFFFFE8]
0048D8F3 |. 06 vJmp_00411670 if (Cross(nonentity, v92) == v107) goto VMPCrack.004A927A//检查注册码第四部分
0049E1D7 |. 2B vReadMemSs4 DWORD s4 = Stack(vESP + 34, ESP + 0, 4)
...
0049DE75 |. 2E vRet return v110 这部分因为有一个switch,可能乱一点。一开始是把MD5的4个DWORD做异或,然后除以5取余数作为switch索引选择一个分支,每个分支有不同的算法。索引对应的分支可以看看switch的注释,第一个目标是0,第二个是1,依此类推。后面又是一个调用,是tea解密,代码和加密很像,也是前后加了异或,就不贴出来了。下面是两个比较,分别检查注册码的第三和第四个DWORD,算法应该很容易看出来了。这次不是直接和解密后的注册码比较,而是先把注册码异或了一个常量。第二个比较有一个Cross(nonentity, v92),也是因为switch前有一个分支跳了过去,这个路径没有变量定义。
004C04FE |. BA vWriteMemSs4 "长度" EXIT DWORD v119 = 14
004574ED |. 31 vWriteMemSs4 "内容" EXIT DWORD v120 = 20 //输入的注册码
00446FD7 |. 15 vPushReg4 vR10 EXIT DWORD v121 = XorFlag(Cross(GetBytes("注册码", 4, 4), nonentity) + Cross(GetBytes("注册码", 0, 4), nonentity), v103 + v106)
00446FD0 |. 6F vAdd4 EXIT DWORD v122 = 0EF8B1AAC
00446FCC |. 45 vJmp_00411C30 callVM
0047F6CA |. CE vReadMemSs4 DWORD m35 = DWORD SS:[Je(SubFlag(entryVMEax_45CED7, entryVMEsi_45CED7)) + 0FFFFFFE8]
0047F698 |. 94 vJmp_00411670 if (entryVMEax_45CED7 != entryVMEsi_45CED7) goto VMPCrack.00467F03
00446EEA |. AD vAdd4 EXIT DWORD v123 = 0EF8B1AAC
00446EE6 |. 1B vJmp_00411C30 callVM VMPCrack.0046BB01 //调用SDK,解密字符串
004B16AB |. 8F vWriteMemDs4 DWORD DS:[407470] = v124; EXIT DWORD v124 = entryVMEax_4689BB
004A259F |. 93 vReadMemSs4 DWORD s5 = Stack(vESP + 34, ESP + 0, 4)
...
004A22A1 |. 2E vRet return v127 开始是一个调用,跟进去看看,第一个转移是垃圾分支
0042C409 |. /2B vJmp_00411C30 if (Ja(1 ^ unknownInit2)) goto VMPCrack.004C9F2B
添加提示/*jmp(4C9F2B)*/清除掉,重新反汇编
0042C5FB /$ 7A vPopReg4 vR6
004C9CCE |. 19 vReadMemSs4 DWORD s0 = Stack(vESP + 3C, ESP + 8, 4) //参数2
004C83CD |. 19 vReadMemSs4 DWORD m1 = DWORD SS:[Jle(AndFlag(s0, s0)) + 28]
004C83AB |. F2 vPushReg4 vR14 DWORD v0 = s0
004C83AA |. F4 vPushReg4 vR12 DWORD v1 = unknownInit10
004C83A9 |. F5 vPushReg4 vR11 DWORD v2 = unknownInit4
004C83A7 |. FC vPushReg4 vR4 DWORD v3 = unknownInit9 | 0FFFFFFFF
004C83A5 |. F7 vPushReg4 vR9 DWORD v4 = 0
004C839A |. 2B vJmp_00411C30 if (s0 <= 0) goto VMPCrack.0046D138
004ACA12 |. B6 vReadMemSs4 DWORD s1 = Stack(vESP + 38, ESP + 4, 4) //参数1
004AC8C9 |. F3 vPushReg4 vR13 DWORD v5 = s1
004AC8BD |> 77 /vPopReg4 vR9
004AC6D5 |. FE |vPushReg4 vR2 DWORD v6 = 0 : ((GetBytes(v3, 1, 1) ^ 0) + 1)
0048F74F |. 20 |vReadMemDs1 BYTE m2 = BYTE DS:[v5 + v4]
004751ED |. AF |vReadMemDs4 DWORD m3 = DWORD DS:[((v6 : m2 ^ v3 & 0FF) << 2) + 407030]
004A7C18 |. 4C |vReadMemSs4 DWORD m4 = DWORD SS:[Jge(SubFlag(v4 + 1, v0)) + 20]
004A7BF8 |. F9 |vPushReg4 vR7 DWORD v3 = m3 ^ v3 >> 8
004A7BF7 |. FA |vPushReg4 vR6 DWORD v4 = v4 + 1
004A7BF4 |. F6 |vPushReg4 vR10 DWORD v1 = m3
004A7BE5 |.^ 1B \vJmp_00411C30 if (v4 < v0) goto VMPCrack.004AC8BD
0046D138 |> 7F vPopReg4 vR1
0046CBCC |. 19 vReadMemSs4 DWORD s2 = Stack(vESP + 34, ESP + 0, 4)
...
0046C844 |. F3 vPushReg4 vR13 EAX DWORD v12 = v3 ^ 6789ABCD
...
0046C83B \. 86 vRet return v9 先检查第二个参数是否大于0,然后是一个循环,次数由第二个参数控制,第一个参数是指针,这里可以看出来第二个参数就是内容长度。大部分计算都很明显,但是有一个
DWORD DS:[((v6 : m2 ^ v3 & 0FF) << 2) + 407030],v6长度是4字节,又连接上一个1字节的m2是5字节?v6的高3字节是0,我觉得可能是0:m2,这里的分析有点问题。循环里的代码连起来是
DWORD DS:[((BYTE DS:[v5 + v4] ^ v3 & 0FF) << 2) + 407030] ^ v3 >> 8
好像是crc,看看407030的内容
00407030 00 00 00 00 96 30 07 77 2C 61 0E EE BA 51 09 99
很明显是crc使用的表了,刚才peid的Krypto ANALyzer插件显示的crc就是这个地址,但还是有点不一样,crc结果最后应该取反,这里是异或6789ABCD,可能又是修改过的算法,异或6789ABCD相当于取反后再异或98765432。填上标签和参数提示再回去看刚才的代码。
然后是一个比较if (entryVMEax_45CED7 != entryVMEsi_45CED7),eax是调用crc的返回值,esi是什么?给crc函数加上初始寄存器提示吧,/*initReg(esi,0C)*/。调用前还有一个
v121 = XorFlag(Cross(GetBytes("注册码", 4, 4), nonentity) + Cross(GetBytes("注册码", 0, 4), nonentity), v103 + v106)
这是异或的标志,但是前面没见过这个计算,调用前计算的,很可能是调用需要的内容,没准是要计算crc的部分,但是插件不能自动分析被调用函数,不知道他引用了这个数据,所以认为是无效的了。给调用crc的vJmp加上一个/*exitValid(1)*/提示,表示调用退出虚拟机时可访问的数据都是有效的,再重新分析一下试试,这回变成了
004A8FBE |. 2B vReadMemSs4 DWORD v120 = Cross(GetBytes("注册码", 0, 4), nonentity)
00485759 |. 48 vAdd4 DWORD v121 = v104 + v107 //v104 = GetBytes("注册码", 8, 4),v107 = GetBytes("注册码", 0C, 4)
004355E7 |. 6F vAdd4 DWORD v122 = Cross(GetBytes("注册码", 4, 4), nonentity) + v120
004C04FE |. BA vWriteMemSs4 "长度" EXIT DWORD v123 = 14
004C02B5 |. BA vWriteMemSs4 EXIT DWORD v124 = v122 ^ v121
004574ED |. 31 vWriteMemSs4 "内容" EXIT DWORD v125 = 20 //输入的注册码,第5个DWORD替换成了v124
...
00446FD8 |. 29 vPushReg4 vR15 EXIT DWORD v136 = Cross(GetBytes("注册码", 10, 4), v0)
00446FD7 |. 15 vPushReg4 vR10 EXIT DWORD v137 = XorFlag(v122, v121)
00446FD0 |. 6F vAdd4 EXIT DWORD v138 = 0EF8B1AAC
00446FCC |. 45 vJmp_00411C30 callVM
0047F6CA |. CE vReadMemSs4 DWORD m35 = DWORD SS:[Je(SubFlag(entryVMEax_45CED7, v136)) + 0FFFFFFE8]
0047F698 |. 94 vJmp_00411670 if (entryVMEax_45CED7 != v136) goto VMPCrack.00467F03//检查注册码第五部分
00446EEA |. AD vAdd4 EXIT DWORD v139 = 0EF8B1AAC
00446EE6 |. 1B vJmp_00411C30 callVM VMPCrack.0046BB01 //调用SDK,解密字符串
004B16AB |. 8F vWriteMemDs4 DWORD DS:[407470] = v140; EXIT DWORD v140 = entryVMEax_4689BB
004A259F |. 93 vReadMemSs4 DWORD s5 = Stack(vESP + 34, ESP + 0, 4)
...
004A22A1 |. 2E vRet return v143 现在出来了,原来是把输入的注册码第5个DWORD替换成了GetBytes("注册码", 0, 4)+GetBytes("注册码", 4, 4) ^ GetBytes("注册码", 8, 4)+GetBytes("注册码", 0C, 4)。由于后面没有引用这个数据,插件认为是无效的,所以没有显示。如果被调用函数引用了堆栈或内存的话,最好填上stackRef或memRef提示,也可以更简单直接在调用的地方填exitValid(1),否则函数引用的数据可能被作为无效的而不显示。这是最后一个比较了,先取出输入的注册码的第5个DWORD,替换成上面的内容,再计算crc,和刚才取出的值比较,如果相等的话就解密要显示的字符串并返回成功。
现在验证过程已经知道了,反过来就是注册码的计算方法,注册码一共5个DWORD,计算方法为
md5=DES解密(MD5(用户名), QWERTYUI)
注册码1.1=(md51.1+md52.1+md53.1+md54.1)^12
注册码1.2=(md51.2-md52.2-md53.2-md54.2)^34
注册码1.3=(md51.3*md52.3*md53.3*md54.3)+56
注册码1.4=(md51.4^md52.4^md53.4^md54.4)-78
注册码2=(注册码1^md51) + (md52^md53) + (md54^98765432)
注册码1:2=TEA加密(注册码1:2 ^ 1A2B3C4D:2B3C4D5E, md5) ^ 4D3C2B1A:5E4D3C2B
注册码3.1=(注册码1.1^注册码2.1) + (md51.1^AA)
注册码3.2=(注册码1.2^注册码2.2) + (md52.2^BB)
注册码3.3=(注册码1.3^注册码2.3) + (md53.3^CC)
注册码3.4=(注册码1.4^注册码2.4) + (md54.4^DD)
注册码4=switch((md51^md52^md53^md54) % 5)
{
case 0
(md51^11223344) + (md52^22334455)
case 1
(md52^33445566) + (md53^44556677)
case 2
(md53^55667788) + (md54^66778899)
case 3
(md54^778899AA) + (md51^8899AABB)
case 4
(md51^99AABBCC) + (md53^AABBCCDD)
}
注册码3:4=TEA解密(注册码3:4 ^ A1B2C3D4:B2C3D4E5, md5) ^ D4C3B2A1:E5D4C3B2
注册码3=注册码3^13579BDF
注册码4=注册码4^FDB97531
注册码5=(注册码1+注册码2) ^ (注册码3+注册码4)
注册码5=CRC(注册码1:2
4:5) ^ 98765432
注册码1:2
4=DES加密(注册码1:2
4, ASDFGHJK)
下面讲一下VMP的脱壳吧,这是我脱的第一个壳,原来一直感觉脱壳没什么用,带壳分析带壳修改对大部分程序都可以,这次想测试插件才研究了一下VMP脱壳。我不会脱壳,方法都是自己想的,可能不是很好。在插件的帮助下很快就把VMP加壳的原理和各种检测看懂了,感觉还不是太难,只是一些修复比较麻烦。先从入口跟踪一下,看看VMP都做了什么。
005BB9EE /$ 13 vPopReg4 vR10
005BB992 |. 2B vReadMemSs4 DWORD m0 = DWORD SS:[Je(SubFlag(0, 0)) + 28]
005BB961 |. 94 vJmp_005BED37 if (0 != 0) goto VMPCrack.005B820C 第一个比较是if (0 != 0),常量和常量比较一般是其中的变量值在编译时已知,或者是在内存中而且插件认为内存是常量。由于VMP中经常读取虚拟机的一部分来计算常量,现在认为内存段中有虚拟机代码或属性为只读、可执行,或包含常量内容比如代码、输入表等,这个段中的内容都是常量,这可能把一些变量当作常量,出现上面的情况。向上跟踪一下数据来源,找到
005BB9CF |. 33 vReadMemDs1 内存005ABAAF:(常量)0; 堆栈2A:(常量)5ABAAF
[注意]看雪招聘,专注安全领域的专业人才平台!
上传的附件: