2、运行程序,了解大致的运行过程
3、开始调试
4、分析 crackme
4.1、检索字符串
4.2、查找字符串地址
4.3、生成Serial的算法(找到函数开始部分)
4.4、预测代码
4.5、读取Name字符串的代码
4.6、加密循环
4.7、加密方法
4.8、加密方法整理
5、Serial生成代码
1、分析环境
- 操作系统:Win10 1809 x64
- 调试工具:Ollydbg
2、运行程序,了解大致的运行过程
- 根据上图发现,该程序需要输入适合的Name和Serial并点击Check进行验证
- 通过输入Name发现,该程序Name至少需要4个字符
- 该程序是通过获取用户输入的Name字符串,再通过加密的方式加密Name字符生成相应的Serial值。这里可以明确一下目标,即逆向分析该加密算法具体是如何实现的。
3、开始调试
- 运行OllyDbg调试程序,查看反汇编代码
EP代码前面的代码为IAT区域,是一些调用VB引擎的函数,如ThunRTMain()。接着PUSH指令将RT_MainStruct结构体的地址作为ThunRTMain()函数的参数压入栈,再执行CALL指令即调用ThunRTMain()函数。这里用到的是间接调用的方法调用ThunRTMain()函数,是VC++和VB编译器中常用的间接调用方法。
- 查看一下RT_MainStruct结构体
RT_MainStruct结构体的成员是其他结构体成员的地址,即VB引擎通过参数传递过来的RT_MainStruct结构体获取程序运行需要的所有信息。
- 查看ThunRTMain()函数
由于这里是MSVBVM60.dll模块的地址区域,是VB引擎代码而不是程序代码,因而不用对其进行分析。
4、分析 crackme
4.1、检索字符串
- 根据程序运行时有弹窗提示,此时可以使用Ollydbg的字符串检索来定位到实际的代码处
- 通过观察,我们发现该程序的限制Name长度的弹窗字符串、错误弹窗字符串、还有一处应该是正确弹窗的字符串,这里我们点击其中一个正确弹窗字符串进入其地址处,同样可以通过错误的字符串去查看,两处相距不远
- 进入到正确字符串地址后向上滚动,发现条件转移语句的代码
- 调用__vbaVarTstEq()函数比较字符串后,执行TEST命令(TEST命令为逻辑比较,仅改变EFLAGS寄存器而不改变操作数的值)检测AX的值是否为0,再由JE指令决定执行那部分代码。
4.2、查找字符串地址
- 403329地址处的__vbaVarTstEq()函数为字符串比较的参数,上方2个PUSH是该函数的参数,即需要比较的两个字符串。
- 此时,我们可以在403329处设置断点,然后运行代码让其在该处中断下来,并查看栈
- 查看存储在栈中的内存地址
- 由于Dump窗口显示的是16进制数据,此时我们可以将其转换成long型地址,就可以观察到实际的字符串了
这里我们可以观察到VB使用的字符串是可变长度的字符串类型(Unicode)
通过观察,EDX为实际的Serial值,EAX是用户输入的Serial值
- 运行一个新的Crackme程序,输入Name=“ABex1234”,Serial=“A5A6C9DC”,会弹出成功的消息框
- 由此可知,当输入Name=“ABex1234”,Serial=“A5A6C9DC”时即可显示正确的信息。也就是说,程序是根据输入的Name来生成特定的Serial值。由此便需要查看生成Serial的算法。
4.3、生成Serial的算法(找到函数开始部分)
由上面分析发现,该函数可能是Check按钮的事件处理程序。此时我们找到该函数头(栈帧代码,该处上方有NOP指令填充)
4.4、预测代码
由上面猜测该程序是根据Name的输入得出Serial的,推测出如下特点:
- 读取Name字符串(使用GetWindowText、GetDlgItemText等API)
- 启动循环,对字符加密(XOR、ADD、SUB等)
4.5、读取Name字符串的代码
经过调试发现,[EBP - 0x88]处为保存Name字符串的字符串对象,402F98为获取Name字符串的函数
我们可以通过Dump窗口和栈窗口(修改相对于EBP)观察到该字符串
4.6、加密循环
继续调试,找到加密循环的关键位置,这里为__vbaVarForInit()和__vbaVarForNext()函数:
4.7、加密方法
输入的Name字符串为“ABex1234”
在上述循环中寻找如__vbaStrVarVal()等的字符串相关处理函数,在该函数处设置断点,运行完该指令后,查看那EAX寄存器保存的内容
可以看到,其为输入的Name字符串的第一个字符“A”。
接着rtcAnsiValueBstr()函数将“A”字符进行Unicode>ASCII的变换。已知“A”=53。
继续向下调试,我们观察到__vbaVarAdd()函数,应该是加法运算,并且该函数上方有3个PUSH,我们在PUSH下方设置断点运行到此处
执行完操作后,查看这3出寄存器所指的内存区域
继续运行代码到__vbaVarAdd()函数处,并观察3个寄存器的内存区域,发现其中,EAX的值发生改变,为0x64
此时我们得到如下推测:
- EAX = 64 (用于加法运算)
- ECX = 计算结果目标缓冲区(推测:该处未有数值初始化)
- EDX = 41(“A”)
执行完CALL后观察ECX寄存器的内存数据是否发生变化,41 + 64 = A5
接着下面的代码将ASCII数值转换为Unicode字符:
rtcHexVarFromVar()函数将相应ASCII数值转换为Unicode字符。上述代码将数字A5转换为字符“A5”(Unicode)。执行完该CALL指令后,查看EAX寄存器所指的缓冲区:
我们到该字符串实际的地址查看:
接下来是拼接字符串
最终循环4次,生成如下序列号:
Serial = old("A5A6C9") + add("DC") = "A5A6C9DC"
4.8、加密方法整理
- 从给定的Name字符串前端逐一读取字符(共4次)
- 将字符转换成数字(ASCII代码)
- 将变换后的数组加64
- 再次将数字转换为字符
- 连接变换后的字符
5、Serial生成代码
编程工具:VS2017
编程语言:C
#include <stdio.h>
#include <conio.h>
int main()
{
char str[ 20 ];
printf( "Name:" );
gets_s( str, 20 );
int Serial[ 4 ] = { 0 };
for (int i = 0;i<4;i++)
{
Serial[i] = str[ i ] + 0x64;
}
printf( "Serial:" );
printf( "%X%X%X%X\n", Serial[0], Serial[ 1 ], Serial[ 2 ], Serial[ 3 ] );
_getch();
return 0;
}