查壳
将CrackMe拖入PEiD进行查壳,可以看到程序是由汇编编写的,无壳。
程序测试
双击打开CrackMe,点击Rigster,输入Name和Serial,点击测试,弹出如下提示。那么我们便可以从这个错误提示字符串入手进行分析。
代码分析
将程序拖入OD开始分析,使用中文搜索引擎查找ASCII字符串,可以看到我们想要寻找的错误提示字符串"No luck there,mate!",如下图所示。然后我们双击进入。
然后,我们可以看到错误提示字符串所在的代码段,上面刚好是注册成功对应的代码。同时观察这两部分可以发现,在每部分的开头和结尾出现了push和retn指令,由此可以推断它们大致是分别被封装在一个子函数内,然后由条件判断进行跳转并call到对应的子函数。
果然,我们点击每部分开始的push指令,下面的提示框显示了函数调用的发生地址,我们循着这个地址开始向上查找。
在对应的地址处,我们找到了两个关键跳转和关键调用,跳转条件是eax和ebx的比较结果,如果相等那么跳转到注册成功的子函数,否则跳转到注册失败。那么程序的关键,就是分析eax和ebx内到底保存了什么。
我们在cmp处下一个断点,然后运行程序,输入Name和Serial,可以看到两个push处出现了我们输入的Name和Serial,但是我输入的Name为小写的zheyi,这里经过处理后变成了大写的ZHEYI,所以我们是否可以猜测它会将Name中的小写字母转换成大写字母。但是有一点可以确定是的,图中的两个call是我们需要寻找的关键call,它们分别对我们输入的Name和Serial中的字符进行了一定的算法处理,并分别保存在eax和ebx中,然后再进行比较,接下来就是对算法进行分析。
第一个关键call
我们在push Name的位置下一个断点,然后输入Name和Serial后进入第一个call。第一个call中,首先获得了传入Name字符串的首地址,接着将每个字符取出,并判断是否在65~90的范围内,即是否为一个小写字母,如果是小写字母,进入另一个call中,这个子函数将每个小写字母的ASCII值减去0x20,也就是转换为了大写字母,接着查看下一个字符。在将所有小写字母转换完成后,这里又调用了一个关键call,我们进入看看。
这里很简单,就是取出了转换后的字符串中的每个字符,将其ASCII值依次相加,然后返回主程序后,将相加得到的值再与0x5678进行异或操作,返回值保存在eax中,并push到栈中进行保存,方便后续取出进行比较。
这里比较有意思的是,如果我们的Name中存在其他字符(不是小写字母),那么程序就不会继续执行后续的字符转换操作,而是弹出一个错误提示框并返回,这里我们测试一下,果然如此。如图所示,我们输入的Name中只有数字的前半部分进行了转换,而后半部分没有进行转换,并弹出了错误提示框。
第二个关键call
接着,进入第二个关键call,这个call内,首先将Serial中的每个字符取出,然后依次减去0x30,然后与0xA相乘,再加上下一个字符,然后再乘以0xA,依次循环,直至处理完所有字符。最后,再将结果与0x1234进行异或操作,然后保存在ebx并返回。关于算法,我的大致理解为是,假如我们输入的Serial为"x1x2x3x4",每个字符减去0x30分别为y1y2y3y4,计算结果便为1000*y1+100*y2+10*y3+y4,由此,可以作为我们推断Serial的依据。
Serial推断
上面说到,我们输入的Name的计算过程是将小写字母进行转换,然后进行字符的ASCII值相加,然后将得到的值与0x5678进行异或,并保存在eax中,这里我们输入的"zheyi"经过计算后为0x57F1。那么,只要保证我们的密码经过计算后也为0x57F1即可。
上面说到,我们的Serial经过计算后,最后和0x1234相异或,然后再和eax比较。由异或的性质,比如A xor B = C,那么A = B xor C,所以经过计算后的Serial = 0x57F1 xor 0x1234 = 0x45C5 = 17861。由上面的性质,17861 = 17*1000+8*100+6*10+1,也就是减去0x30的字符串为"11 8 6 1",原字符串为"41 38 36 31",即"A861"。这就是我们需要的Serial。