相关工具:IDA、OD、python
由程序图标得知这是一个MFC程序。打开程序,随便输入sn,弹出失败对话框。
用OD打开CM,bp MessageBoxW,运行,输入sn,点击确定,触发MessageBoxW断点。栈回溯确定调用源0x407245,随即找到按钮处理函数0x4071fd。

主函数逻辑很简单,判断sub_406fc3的返回值。0弹出“失败”,1弹出“成功”,2弹出“你就差一点啦”
跟进0x406fc3函数,在正常流程中发现int 2d指令

int 2d反调试原理很简单,正常运行时int 2d触发异常,进入程序的异常处理函数。而当调试运行时,OD会处理该异常,将eip+1继续运行。
下图是CM设置的异常处理函数

patch掉int 2d,修改为jmp 0x40717d即可正常调试。
异常处理函数中有3个关键判断



判断1是CM最重要的部分,下面我会详细分析判断1。
作者设置判断2是为了防止程序多解。事实上,如果在分析理清了作者的算法思路,那么逆推得出的sn会自然的pass掉判断2。

为了便于描述,设var_d9f4开始,大小为0x30的字节流为BLOCK1。var_d934开始,大小为0x30的字节流为BLOCK2。
成功条件就是:BLOCK1和BLOCK2全等。

在0x40718b处下断,调试发现BLOCK1和BLOCK2都被初始化过。BLOCK1被初始化为0x00-0x2F依次递增排列,BLOCK2被初始化为0x00-0x2F的乱序排列。
往前翻代码,发现如下代码:

调试发现,sub_4084A3就是初始化BLOCK1和BLOCK2的函数。初始化时,sub_4084A3被调用(传入了字符串参数”KanXueCrackMe2017“,传入了BLOCK1的地址)(事实上,BLOCK1和BLOCK2在内存中是紧挨着的,实际上做变换的是BLOCK2),判断1前,sub_4084A3再次被调用(传入了我们的sn字符串,传入了BLOCK1的地址)。
为了使判断1成功,要让BLOCK2和BLOCK1全等。调试发现,sub_4084A3并不会打乱BLOCK1,只会打乱BLOCK2。(BLOCK1一直为0x00-0x2F的递增排列)
所以我们要找的sn可以说是字符串”KanXueCrackMe2017“在函数sub_4084A3作用下的”逆“。
”KanXueCrackMe2017“ 负责打乱递增排列的BLOCK2,而我们的SN则负责还原BLOCK2。
在打乱BLOCK之前,SN会经历4步变换
进入sub_4084A3

进入sub_40829c

对sn中的每一个char做判断:
在'0'~'9' 之间则减去'0'。结果范围[0,9]
在'A'~'Z'之间则减去'7'。结果范围[10,35]
在'a'~'z'之间则减去'='。结果范围[36,61]
变换1:由字符向[0,61]区间做1对1映射。
同样是在sub_40829c中

第一个call sub_403641作用是初始化变量。
第二个call sub_4038E1读取向[0,61]区间映射过的sn,按位权重乘以相应个数的62。
最后两个call将这些按62位权重的数求和。
变换2:转换为62进制数字

变换3:62进制转换为18进制

[注意]看雪招聘,专注安全领域的专业人才平台!