欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  网络运营

MD5算法应用之“迷魂阵”

程序员文章站 2022-04-12 08:31:18
文/图 TC-XB=================================== 我们已经见识很多种MD5 算法在软件注册中的应用,同时也得到了一个结论:一味生搬硬*成的算法而不加改...

文/图 TC-XB
===================================
我们已经见识很多种MD5 算法在软件注册中的应用,同时也得到了一个结论:一味生搬硬*成的算法而不加改变,即使算法再强大在破解者面前也是“不堪一击”的。所以,我们就见识到了各种各样的奇思妙想,将MD5算法使用得淋漓尽致。但是,算法上做足了功夫却没有注意注册码比较的过程,导致了注册码明码在内存中出现,结果同样会导致“功亏一篑”。所以,必须在算法的使用和注册验证这两个方面同时下手,才可以让一个优秀的算法发挥出它最大的作用。
今天,我们选择的这个MD5算法应用的实例,却从另一个角度启发我们究竟什么才叫做灵活应用。在注册过程中,真正的注册码很容易就被发现了,但想要成功注册?呵呵,门都没有!在稍后分析的过程中,大家一定会看着注册码干着急,明明就是正确注册码了,却怎么也用不了。这究竟是为什么呢?让我们带着这个问题开始下面的分析之路吧!
这次的实例是一个CrackMe,用PEiD检查,确定这个小程序没有加壳,接下来我们就可以搬出OD了。用OD打开这个CrackMe,载入之后打开“字符串查找功能”,很容易就发现了关键的注册提示信息,如图1所示,双击找到的注册提示,就可以来到对应的代码处了,如图2所示。
 
图1
 
图2
   现在我们看到的是“注册提示”也就是来到了告诉我们注册成功与否的地方,注册码计算和注册验证的代码应该就在前面了,一路向上来到以下代码处。

00401C05 . 56 push esi  ;在这里设置断点
00401C06 . 8BD8 mov  ebx, eax
00401C08 . FFD7 call edi  ;取输入的注册码
00401C0A . 8D4424 0C  lea  eax, [esp+C]
;将注册码保存在EAX中
00401C0E . 8D50 01  lea  edx, [eax+1]
;对注册码作相同的检查
00401C11 > 8A08 mov  cl, [eax]
;取注册码的每一位
00401C13 . 40 inc  eax
00401C14 . 84C9 test cl, cl
00401C16 .^ 75 F9  jnz  short 00401C11 ;循环计算
00401C18 . 2BC2 sub  eax, edx
;得到输入的注册码的长度
00401C1A . 8BD0 mov  edx, eax
;将注册码的长度保存在EDX中
00401C1C . 0F84 C5000000 je 00401CE7

程序首先将我们输入的注册码进行了一个预处理,并把注册码的长度记录下来。

00401C22 . 85DB test ebx, ebx
00401C24 . 0F84 BD000000 je 00401CE7
00401C2A . 33C9 xor  ecx, ecx
00401C2C . 85D2 test edx, edx
;开始检验注册码的范围
00401C2E . 7E 29  jle  short 00401C59
00401C30 > 8A440C 0C  mov  al, [esp+ecx+C]
;取注册码的每一位
00401C34 . 3C 30  cmp  al, 30
00401C36 . 7C 04  jl short 00401C3C
00401C38 . 3C 39  cmp  al, 39
00401C3A . 7E 18  jle  short 00401C54
00401C3C > 3C 41  cmp  al, 41
00401C3E . 7C 0C  jl short 00401C4C
00401C40 . 3C 47  cmp  al, 47
00401C42 . 7F 08  jg short 00401C4C
00401C44 . 04 20  add  al, 20
00401C46 . 88440C 0C  mov  [esp+ecx+C], al
00401C4A . EB 08  jmp  short 00401C54
00401C4C > 3C 61  cmp  al, 61
00401C4E . 7C 65  jl short 00401CB5
00401C50 . 3C 67  cmp  al, 67
00401C52 . 7F 61  jg short 00401CB5
00401C54 > 41 inc  ecx
00401C55 . 3BCA cmp  ecx, edx
00401C57 .^ 7C D7  jl short 00401C30
00401C59 > 33C0 xor  eax, eax
00401C5B . 85DB test ebx, ebx
00401C5D . 7E 0D  jle  short 00401C6C
  
紧接着就是对注册码的每一位进行验证,主要目的就是检验注册码的每一位是否符合要求。简单的说,就是注册码的每一位必须在数字和字母这个范围内,超出这个范围就会出错了。大家可以注意到,上面的大段代码都是用来检验注册码的,看来我们输入的注册码虽然是“假码”,但这个CrackMe却对这个“假码”很感兴趣,一而再再而三地进行检验。下面又会对这个假码进行怎样的处理呢?我们继续分析。

00401C5F . 8A4C14 0B  mov  cl, [esp+edx+B]
;取注册码的最后一位
00401C63 > 304C04 40  xor  [esp+eax+40], cl
;与注册名的每一位运算
00401C67 . 40  inc  eax
;每计算一次EAX加1
00401C68 . 3BC3  cmp  eax, ebx
;计算完了吗
00401C6A .^ 7C F7  jl short 00401C63 ;循环计算
00401C6C > 8D4424 40  lea  eax, [esp+40]
;保存计算的结果str1
00401C70 . 50  push eax ;参数入栈

这里程序将我们输入的注册码的最后一位单独取了出来,并用它和注册名一一计算。下面把这段代码的详细计算过程给大家说明一下。
第一步:取注册码的最后一位的HEX值;
第二步:取注册名每一位的ASCII码;
第三步:将HEX值与注册名每一位的ASCII码作XOR(异或)运算;
第四步:将计算的结果转换成对应的字母。
完整的过程就是这样的,但感觉还是有些含糊,我们举例来说明吧。在这里我输入的注册名是tcxb,注册码是12345,那么在这段代码中的计算过程就是这样的。
第一步:取注册码的最后一位,也就是这里的5,计算对应的HEX值结果是35;
第二步:取注册名每一位的ASCII码,tcxb这四个字母对应的ASCII码分别是:74、63、78、62;
第三步:将35分别与74、63、78、62进行异或运算,XOR(35.74)=41,XOR(35.63)=56,XOR(35.78)=4D,XOR(35.62)=57;
第四步:因为在ASCII码表中,字母A对应的值就是41,所以注册名第一位计算得到的最终结果就是A。
按照ASCII码表中的对应关系一一计算,可以得到每一个结果,分别是A、V、M、W。最后将这四个字母合并组成一个字符串,也就是我们所说的str1=AVMW。得到了这个字符串之后又要干什么呢?我们接着向下分析。
00401C71 . C64414 0F 00 mov  byte ptr [esp+edx+F], 0
00401C76 . E8 75FEFFFF call 00401AF0
;将str1做MD5计算
00401C7B . 83C4 04  add  esp, 4
00401C7E . 8D4C24 0C  lea  ecx, [esp+C]
;取输入的注册码
00401C82 . 51  push ecx  ; /String2
00401C83 . 50  push eax  ; |String1
00401C84 . FF15 00704000 call [<&KERNEL32.lstrcmpA>]; lstrcmpA
00401C8A . 85C0  test eax, eax  ;将两者比较
00401C8C . 6A 00  push 0
00401C8E . 75 33  jnz  short 00401CC3
00401C90 . 68 98714000 push 00407198 ;succeed!
00401C95 . 68 88714000 push 00407188 ;great,注册成功!
00401C9A > 8B15 40974000 mov  edx, [409740]; |
00401CA0 . 52  push edx  ;|hOwner => NULL
00401CA1 . FF15 DC704000 call [<&USER32.MessageBoxA>]; MessageBoxA

这段代码就简单多了,主要作用是将上一步得到的str1即AVMW进行MD5计算,将计算得到的结果与我们输入的注册码相比较,如果相等就注册成功。通过各种方法很容易就可以计算出来MD5(AVMW)=c37985c0c4a3a9e4600d67c5d18af450。按照我们分析的结果,这个MD5值如果没有问题就是注册码了。
现在我们重新运行程序依次输入注册名tcxb,注册码c37985c0c4a3a9e4600d67c5d18af450,很自信地按下“注册”按钮,什么?居然提示“注册码”不正确!怎么回事?我们分析的过程没有错呀!再一次打开OD重新分析一遍,输入注册名tcxb和注册码12345,跟踪到最后,依然是上一步得到的那个计算结果。注册码就在眼前,可为什么不能成功注册呢?
用OD分析的过程是不可能出错的,问题是不是在我们,是不是分析的时候漏掉了什么?整理一下思路,这个CrackMe的计算过程很简单,主要就是把一个字符串用MD5进行了一个加密处理。一般来说,MD5加密的都是注册名或是注册名加固定字符串等这样一些东西,这里也没有什么特别的地方。那么注册码验证的地方就更不用怀疑了,很明显是真假注册码比较,而且还是明码比较,但为什么结果就是不对呢?
大家注意,我们所接触的类似这样将某一个字符串用MD5加密,然后将加密结果作为注册码的情况,被加密的字符串都是由注册名直接变换得出的,比如加上一个固定字符串或者是和某一个数字做计算等,简单的说就是被加密的信息与注册码之间是没有关系的,从头到尾都是对注册名的计算。其实问题就是出在这里了。在这个CrackMe中,大家回忆一下被加密的那个str1是怎样得来的?是用注册码的最后一位与注册名的每一位分别计算后得出的,这样在被加密的信息和输入的注册码之间就有了一个动态关系了,只要输入的注册码发生变化,就直接导致被加密的信息发生变化,被加密的信息都发生变化了,最后的结果肯定会出问题的。
还是举一个例子来说吧。依然是tcxb这个注册名和12345这个注册码,经过分析我们可以了解到,现在的重点就在最后的那个数字5上面,因为被加密的字符串str1是由这个5和注册名直接计算而来的。接着我们在OD中找到了所谓的注册码c37985c0c4a3a9e4600d67c5d18af450,这些都没有问题。然而,当我们试图用c37985c0c4a3a9e4600d67c5d18af450这个字符串作为注册码与tcxb进行注册的时候,问题出现了,注册名没有变化,但是注册码的最后一位已不再是5了,而是0了,用5和0这两个不同的数与相同的一个对象作相同的计算,怎么可能得出相同的结果呢?