运行abex’crackme #2
下载地址:https://pan.baidu.com/s/1qXhyt8C
运行程序:
输入符合要求的name与serial,点击check:
弹出wrong serial消息框。
Visual Basic文件的特征
abes’ crackme #2文件由Visual Basic编写而成。调试前首先了解Visual Basic文件特征。
VB专用引擎
VB文件使用名为MSVBVM60.dll的VB专用引擎,比如:显示消息框时,VB代码中要调用MsgBox()函数。VB编辑器真正调用的时MSVBVM60.dll中的rtcBox函数,在该函数内部通过调用user32.dll里的MessageBoxW()函数(Win32 API)来工作。
本地代码与伪代码
根据使用编译选项的不同,VB文件可以编译为本地代码(N code)和伪代码(P code)。本地代码一般使用易于调试解析的IA-32指令;伪代码是一种解释器语言,若要准确解析,就必须分析VB引擎并实现模拟器。
事件处理程序
VB主要用来编写GUI程序。VB采用Windows操作系统的事件驱动方式工作,在main()或Winmain()中不存在用户代码,用户代码存在于各个事件程序中。
如本程序中,用户代码在点击Check按钮时触发的事件处理程序内。
未文档化的结构体
VB中使用的各种信息(Dialog,Control,Form,Modul,Function)以结构体的形式保存在文件内部。微软未公开,故调试VB文件会困难一些。
开始调试
运行OD查看程序反汇编代码
上示3行代码即为VB文件的全部启动代码。
push 401E14
将RT_MainStruct结构体的地址入栈。
call 401232
调用401232地址处的jmp 4010A0
指令,跳转到VB引擎主函数ThunRTMain(),起前面入栈的401E14作为函数的参数。
间接调用
40123D地址处的call 401232
命令用于调用ThunRTMain()函数,这里并未直接跳转到MSVBVM60.dll里的ThunRTMain()函数,而是通过中间401232地址处的jmp命令跳转。
这是VC++,VB编译器中常用的间接调用法。
RT_MainStruct结构体
ThunRTMain()函数的参数时RT_MainStruct结构体,它存放在401E14处。查看:
RT_MainStruct结构体的成员是其他结构体的地址。也就是说,VB引擎通过参数传递过来的RT_MainStruct结构体获取程序运行需要的所有信息。
ThunRTMain()函数
该函数属于VB引擎代码。
分析crackme(1)——分析程序执行流程
检索字符串
all referenced text strings检索字符串:
双击跳转到该字符串处:
可以看到消息框的标题、内容、实际调用消息框函数的代码(4034A6)均显示出来了。
按照编程逻辑,用户序列号与字符串后,代码分为TRUE和FALSE两部分。换言之,上述代码前后存在字符串比较代码与条件转移语句:
可以看到调用403329地址的_vbaVarTstEq()函数,TEST比较返回值AX后,由403332地的条件转移指令决定执行TRUE代码还是FALSE代码。
查找字符串地址
403329地址的_vbaVarTstEq()函数为字符串比较函数,其上方的2个PUSH指令为比较函数的参数,即比较的字符串。
调试至403329地址处,输入如下,输入序列号为1234:
可以看到入栈的地址分别为12F458与12F468。
查看栈中存放数据:
很容易看到12F468中存放的是用户输入的serial值。12F458中存放的是实际的serial值。
进入字符串所在地址查看实际字符串:
运行程序,用户名不变,序列号输入为D6C9DAC9:
可以看到找到了正确的序列号并破解成功。
但如果改变name值,序列号不变:
显示错误信息,这说明程序使用了“以name字符串为基础随时生成Serial”的算法。
分析crackme(2)——分析生成Serial的算法
前面提到的条件转移代码显然属于某个函数,该函数应该就是check按钮的事件处理程序。因为选择check按钮后,该函数会被调用执行,且含有用户代码来弹出成功/失败消息框。向上查找
可以看到
push ebp
move ebp,esp
这是典型的栈帧代码,开始执行函数时就会形成栈帧,由此可知该位置是函数开始部分,即check按钮的事件处理程序。
预测代码
根据编程习惯,预测生成序列号的方法。若为Win32 API程序:
(1)读取name字符串,使用GetWindowText、GetDlagItem Text等API
(2)启动循环,对字符加密,XOR、ADD、SUB等
VB文件原理类似。若预测正确,从前面的事件处理程序其实代码开始调试,查找到Name字符串后,紧接着就会出现加密循环。
注意:调试前先预测代码的实现是个好习惯。预测正确可以节约大量调试时间。
读取name字符串的代码
接下来以call指令为主进行调试。开始调试后遇到的第四条call指令:
lea edx,dword ptr ss:[ebp-88]
返回间接操作数的偏移地址,EDX=12F414
push edx
地址12F414被传递给函数作为参数,要重点关注该地址
push esi
esi=01ACFFF4
mov ecx,dword ptr ds:[esi]
ecx=01AC3628,即地址ds:[esi]处存放内容
call dword ptr ds:[ecx+A0]
调用函数获取name
执行完call命令后,查看12F414:
可以看到217AAC处存放着name字符串,即name字符串以字符串对象的形式存储到[EBP-88]地址。
加密循环
调试发现循环从403197处test eax,eax
开始,直到4032A0处jmp 403197
结束,循环4次。
这两条语句等同于:if(AX==0) goto 4043A5
。
显然这里的vbaVarForInit与vbaForNext均类似于指针,作用是获取字符串下一字符。loop count使其按指定次数循环。
这里仅接收name字符串中的前4个字符。在代码内检查字符串的长度,少于4个字符弹出错误消息框。
加密方法
push eax
eax=228B84,查看该地址处可以看到存放的是获取的name的一个字符。
继续执行call dword ptr ds:[40101C]
将UNICODE变为ASCII。
该函数返回值为r的ASCII码值72h。存放在EAX中。
继续调试:
调试到到此处看一下栈:
查看各内存地址:
运行函数:
可以看到函数执行后ECX指向的缓冲区中0012F408变为D6(计算结果D6=72+64)。是前面正确序列号的前两个字字符。
继续调试,下面代码将数字D6 转换为字符串D6(UNICODE):
执行函数后,查看EAX所指缓冲区0012F400,可以看到生成了D6字符串:
查看实际字符串的地址。可以看到UNICODE字符串D6:
下面代码实现了字符串的拼接:_vbaVarCat()函数,这一部分在下一次循环时再关注:
第二次循环同上:
可以看到C9(C9=65(‘e’)+64)以字符串形式存储起来了。
字符串的拼接部分
_vbaVarCat()函数执行前:
查看栈:
ECX:
EDX:
执行后:
查看栈:
EAX:
可以看到EAX指向区域存放的时拼接后的结果,ECX指向区域处存放的时拼接前的字符串,EDX指向区域存放的是新读取字符加密后的字符串。
执行至最后一次循环:
执行前:
EDX存放新的字符串:
ECX存放拼接前的字符串:
执行后:
EAX存放拼接后的结果:
循环结束后跳转至4032A5处继续执行,继续执行跳转至4032E9,继续执行,就可以看到前面提到的403332分支指令处,分支跳转至403408,即可看到弹出错误序列号窗口的代码,如下:
输入正确的系列号,调试到403329处查看:
可以看到不发生跳转,顺序执行直接弹出正确的窗口:
总结
函数加密方法的整理:
- 从给定的Name字符串中逐个读取字符(4次)
- 将字符转换为数字(ASCII)
- 向变换后的数字加64
- 再次将数字转换为字符
- 连接变换后的字符
整个函数执行流程:
- 输入Name与序列号,点击check,触发事件处理程序
- 根据输入Name加密生成正确的序列号
- 输入序列号与正确序列号比较,判断是否正确
- 条件转移,若错误弹出错误窗口,正确弹出正确窗口。
总结前文调试的过程:
- 通过字符串检索查找到弹出错误消息框的代码位置。
- 向上查找定位到条件转移语句的位置:该处进行序列号的比较,此处可以在内存中定位到两个序列号的存储位置。
- 继续向上查找,找到栈帧代码
push ebp;mov ebp,esp;
开始执行函数就会形成栈帧,由此定位函数开始部分,即check按钮的事件处理程序。 - 预测代码接下来将要执行的是:读取name字符串、启动循环对字符加密。
- 向下查找call函数,多次调试定位到向后第4个call函数读取name字符串。同时定位到字符串存储位置。
- 继续调试,遇到一系列循环语句,发现一共循环四次。进一步分析循环部分,即加密部分执行的代码,找到加密方法。
- 调试发现加密部分执行的操作是:依此读取name字符串字符,将其转化为ASCII,与密钥64相加,结果再次转换为UNICODE,并将其与之前的加密结果拼接。**调试过程中发现栈被分为三个区域:ECX是用于保存结果的缓冲区,EAX存储密钥,EDX存放Name字母的ASCII码。**后续拼接时,原本的ECX值赋给EAX,新的EAX存放着最终拼接的结果。EDX存放着每次加密生成的新的字符,后面紧跟着以前拼接的结果。