【文章标题】: XP序列号算法分析,给像我这样的迷漫的初学者!
【文章作者】: NOIR
【作者邮箱】: noir0080@21cn.com
【软件名称】: ProduKey.exe
【软件大小】: 52.5K
【下载地址】: 下载
【编写语言】: VC6
【使用工具】: OLLYDGB
【操作平台】: XP
【软件介绍】: 这是一个算微软产品序列号的软件。
【作者声明】: 只是感兴趣,没有其他目的。失误之处敬请诸位大侠赐教!
--------------------------------------------------------------------------------
【详细过程】

    本人也是菜鸟,用了几天研究了一下,这里特意把自己的心得记录下来,这篇文章完全是给像我这样的新手看的,在高手面前就献丑了,里面我会尽量把一些细节的东西都交代清楚,目的是务必使大家少走弯路。

    好了,不说废话了,让我们先大致看一下这个软件吧。

    用peid查壳,发现使UPX的一个老版本的壳,我上网直接找了个脱UPX壳的软件,直接脱了(没办法,菜鸟,不会手动脱壳啊)。脱壳之后,我们先运行一下这个软件。我们看到软件运行之后没有什么提示,直接显示出来的就是一个列表,列表内容就是在当前机器上所安装了的所有微软产品的CD-KEY。恩,不错,看上去还是挺简单的,不搞多余动作了,我们马上开始吧。

熟悉WIN32的朋友都应该知道。(什么!?你不知道?那我还是建议你快点先去恶补一下WIN32编程吧,推荐去看一下这个帖子http://bbs.pediy.com/showthread.php...;threadid=14164 ) 前面说过,这个软件,界面上有一个应该是属于List控件一类的东西,来显示内容。这里,我先告诉大家,在windows编程中,如果要想在List控件中显示出东西,就一定要往List控件中先插入一些内容(当然啦,你都没把内容给人家,人家怎么知道要显示什么?) 这个插入内容,用API函数的形式表示,就是调用InsertItem() ,用消息的形式表示,就是 LVM_INSERTITEM 消息。(关于具体了解列表视图,我推荐大家google 一下,这里我给大家找到了一篇 http://211.90.241.130:22366/view.asp?file=107 )。好了,之所以扯了这么多,是希望先把必须的基础知识告诉大家,因为等下我们下断点的时候,要用到这些知识,下面请继续看文章:

    现在我们正式开始调试程序咯!(激动吧,我也很激动) 我们开始下断点,(我断,我断,我断断断)关于断点,我还想扯一下,在OLLYDBG中,通常的断点,有几大下法(类似 字串参考 ,函数参考,内存断点,消息断点及 RUN 跟踪 等,具体可以去看一下 CCDebuger 写的<<OllyDBG 入门系列>>,在看雪论坛搜索一下关键字"OllyDBG 入门系列"就有了,我也是看他的这个系列入门的。)因为我们要分析XP序列号的算法,那么我们自然希望断点能在程序具体实现算XP序列号的函数附近。前面通过观察程序,我猜,应该是在程序把算出的序列号插入到List控件的时候断下来,就是最接近算序列号函数的地方了。(为什么,你想想,如果是你写程序,序列号算出来了,不直接赋值给控件,你还留着那值做什么?)说白了就是断我前面说到的两个地方,InsertItem() 函数,或者是 LVM_INSERTITEM 消息。呵呵,好咯,思路也有了,我们开始吧!用OLLYDBG装载这个程序。按ALT+N 我们先试试断InsertItem() ,我找啊找,找啊找,我都找了三遍了,咋就没发现这个程序里面有个函数叫InsertItem()呢?(真是莫名其妙啊,是不是他在搞笑?)好,算,怕你,没有InsertItem()给我断,我断 LVM_INSERTITEM  还不行吗?,随后我们又按照 <<OllyDBG 入门系列(五)-消息断点及 RUN 跟踪>> 一文的方法,去找 LVM_INSERTITEM ,可是发现,消息列表里面根本没有我们要的消息 ( 苍天啊!大地啊!) 这可怎么办呢?

    别急,这个时候要定,冷静下来再看看。(请你相信,这个软件真的不难),我反复看,反复看,终于,被我发现一个地方,请大家注意看,算出来的序列号都是 XXXXX-XXXXX-XXXXX-XXXXX-XXXXX 这样的形式的,看到中间用来分隔的"-"这个符号了么?这个肯定是通过类似 strcat( dest , "-" ); 这样的语句插入到字符串里面去的。(什么!?,你不能理解!?恩,兄弟,我很负责任的告诉你,你真的该好好去补习一下C语言了,推荐谭浩强老师的C语言程序设计)。"-"的ASCII码是2D,现在我们豁然开朗了,这个就是线索。现在我们打开任何一款静态反编译软件,我用的是IDA。用IDA加载了程序之后,我们点那个"查找字节序列" 的按钮,(为什么不用"查找文本"!?兄弟,醒醒,汇编里面不可能用MOV XXX, "-" 的形式插入一个字符啊,最起码也得用 MOV XXX, 2D ;恩,明白?)我们输入2D,然后查找,(结果出来了,伟大的2D!他继承了cracker的光荣的传统。kanxue老大,CCDebuger ,blowfish, cnbragon 在这一刻灵魂附体,2D他一个字节他代表了cracker悠久的历史和传统,这一刻他不是一个人在战斗,他不是一个人!...)
呵呵,结果出来了,我笑了,结果里面只有一个地方用到了2Dh
00431B3B   mov     byte ptr [edi], 2Dh
呵呵,这么明显,不是他还能是谁?所以,别客气,断之。

我们马上打开OLLYDBG, ctrl+G 到 00431B3B 处,F2断之。F9运行之。终于断下来了,我们上下看看。经过我反复得F8观察之后,我很负责任的告诉大家,我现在断下来的这个CALL就是算XP序列号的CALL没错。呵呵,真麻烦啊。这里扯一下,有兴趣的朋友可以一步步的通过这个CALL返回上去,这样可以了解这个程序大概的流程和函数之间的关系。我就是通过这样走过之后,发现这个程序不管是算哪个产品的序列号,最终的算法都是来调用我们现在断下的这个CALL的,呵呵,这下方便了。只要搞懂这个CALL的算法就行了,下面我们马上开始重头戏,分析算法:

在开始讲算法之前,我想让大家了解一个基本知识:

在破解的时候,经常可以看见一个标准的函数起始代码: 
push ebp ;保存当前ebp 
mov ebp,esp ;EBP设为当前堆栈指针 
sub esp, xxx ;预留xxx字节给函数临时变量. 
... 

这样一来,EBP 构成了该函数的一个框架, 在EBP上方分别是原来的EBP, 返回地址和参数. EBP下方则是临时变量. 函数返回时作 mov  
esp,ebp/pop ebp/ret 即可. ESP 专门用作堆栈指针. 

假设一个子程序入口处(xxxxxxxx),堆栈的状态是这样的: 

03000000 (push 压入的参数) 
02000000 (push 压入的参数) 
yyyyyyyy <--ESP 指向返回地址 

    前面讲过,子程序的标准起始代码是这样的: 

push ebp ;保存原先的ebp 
mov ebp, esp;建立框架指针 
sub esp, XXX;给临时变量预留空间 
..... 

    执行push ebp之后,堆栈如下: 

03000000 
02000000 
yyyyyyyy 
old ebp <---- esp 指向原来的ebp 

    执行mov ebp,esp之后,ebp 和esp 都指向原来的ebp. 然后sub esp, xxx 给临时变量留空间。这里,假设只有一个临时变量temp,是一个长整数,需要4个字节,所以xxx=4。这样就建立了这个子程序的框架: 

03000000  (第二个参数 n )
02000000 (第一个参数 m)
yyyyyyyy 
old ebp <---- 当前ebp指向这里 
temp 

    所以子程序可以用[ebp+8]取得第一参数(m),用[ebp+C]来取得第二参数(n),以此类推。临时变量则都在ebp下面,如这里的temp就对应于
[ebp-4]. 

子程序执行到最后,要返回temp的值: 

mov eax,[ebp-04] 
然后执行相反的操作以撤销框架: 

mov esp,ebp ;这时esp 和ebp都指向old ebp,临时变量已经被撤销 
pop ebp ;撤销框架指针,恢复原ebp. 

这是esp指向返回地址。紧接的retn指令返回主程序: 
retn 4

呵呵,所以,以后大家要是看到类似 [ebp-?] , [ebp+?] 就记得是什么东西咯,前者一般是函数内的临时变量,后者一般是调用这个函数的时候传入的参数,我不知道我这么说对不对。我的理解就是这样子,关于这个问题,我很希望再去请教一下高手,也希望高手能指教一下我,谢谢。

我这里把代码都贴出来,一步步讲解:

00401AE3  /$  55            push    ebp                              ;  具体算XP序列号的CALL
00401AE4  |.  8BEC          mov     ebp, esp                         ;  保存最初的ESP到ebp中
00401AE6  |.  83EC 30       sub     esp, 30                          ;  为临时变量分配30h大小的空间
00401AE9  |.  53            push    ebx
00401AEA  |.  56            push    esi
00401AEB  |.  57            push    edi
00401AEC  |.  6A 06         push    6
00401AEE  |.  59            pop     ecx
00401AEF  |.  BE 28C14000   mov     esi, 0040C128                    ;  ASCII "BCDFGHJKMPQRTVWXY2346789"
00401AF4  |.  8D7D D0       lea     edi, [ebp-30]                    ;  将ebp-30的内存地址移入edi目的操作数中,以便下面的操作
00401AF7  |.  6A 20         push    20                               ; /n = 20 (32.)
00401AF9  |.  F3:A5         rep     movs dword ptr es:[edi], dword p>; |copy密匙KEY散列到堆栈
00401AFB  |.  A4            movs    byte ptr es:[edi], byte ptr [esi>; |copyKEY结束,插入00结束符
00401AFC  |.  8B75 08       mov     esi, [ebp+8]                     ; |第一个参数(一个字符串的地址)放入esi,目的操作数,[]内 
指定的不是立即数,而是偏移量
00401AFF  |.  6A 00         push    0                                ; |c = 00
00401B01  |.  56            push    esi                              ; |s
00401B02  |.  E8 FB770000   call    <jmp.&MSVCRT.memset>             ; \memset,好像是将第一个参数清0,初始化esi指向的地址
00401B07  |.  6A 0F         push    0F                               ; /n = F (15.)
00401B09  |.  8D45 EC       lea     eax, [ebp-14]                    ; |ebp-14,注意,下面要用到
00401B0C  |.  FF75 0C       push    dword ptr [ebp+C]                ; |src
00401B0F  |.  50            push    eax                              ; |dest
00401B10  |.  E8 E7770000   call    <jmp.&MSVCRT.memcpy>             ; \mencpy;把参数ebp+c的值copy到ebp-14的临时变量中
00401B15  |.  C745 08 01000>mov     dword ptr [ebp+8], 1             ;  第一个参数赋值1
00401B1C  |.  83C4 18       add     esp, 18                          ;  堆栈指针向后18h,指向Product ID
00401B1F  |.  2975 08       sub     [ebp+8], esi                     ;  第一个参数减esi所指向的值;/1-X
00401B22  |.  8D7E 1C       lea     edi, [esi+1C]                    ;  esi往后1C(28)位的地址,给EDI,作为目的字符串的开始地址
00401B25  |.  C745 FC 1D000>mov     dword ptr [ebp-4], 1D            ;  1D==29,就是序列号的字符长度(临时变量赋1D)
00401B2C  |>  8B45 08       /mov     eax, [ebp+8]                    ;  第一个参数赋值给eax
00401B2F  |.  6A 06         |push    6
00401B31  |.  03C7          |add     eax, edi
00401B33  |.  59            |pop     ecx                             ;  ecx赋值6//count=6
00401B34  |.  99            |cdq                                     ;  cdq:EDX清零,类型转换指令,双字转换为4字
00401B35  |.  F7F9          |idiv    ecx                             ;  EAX除以ECX,商放在EAX,余数在EDX
00401B37  |.  85D2          |test    edx, edx                        ;  测试EDX是否为0,这里是用来看是否已经计算了5位,而决定是 
否插入"-"
00401B39  |.  75 05         |jnz     short 00401B40                  ;  !=0则跳
00401B3B  |.  C607 2D       |mov     byte ptr [edi], 2D              ;  插入"-"
00401B3E  |.  EB 40         |jmp     short 00401B80
00401B40  |>  C745 0C 0E000>|mov     dword ptr [ebp+C], 0E           ;  第二个参数置0E
00401B47  |.  33D2          |xor     edx, edx                        ;  edx清0
00401B49  |>  8B45 0C       |/mov     eax, [ebp+C]                   ;  第二个参数自减后再赋值给eax ;//i = num2 ;//num2--;
00401B4C  |.  8BCA          ||mov     ecx, edx                       ;  余数给ecx
00401B4E  |.  C1E1 08       ||shl     ecx, 8                         ;  edx余数扩大256倍;ecx左移8位,低位补0
00401B51  |.  8D7405 EC     ||lea     esi, [ebp+eax-14]              ;  ebp-14+eax处的变量的下一位字符
00401B55  |.  6A 18         ||push    18                             ;  入18,十进制24
00401B57  |.  33D2          ||xor     edx, edx                       ;  edx=0
00401B59  |.  5B            ||pop     ebx                            ;  ebx=18
00401B5A  |.  0FB606        ||movzx   eax, byte ptr [esi]            ;  零扩展传送,格式--MOVZX DST,SRC,表示将源操作送给目的 
操作数,目的操作数空出的部分用0填补。
00401B5D  |.  0BC8          ||or      ecx, eax
00401B5F  |.  53            ||push    ebx
00401B60  |.  8BC1          ||mov     eax, ecx
00401B62  |.  F7F3          ||div     ebx                            ;  除法,结果在AX中
00401B64  |.  33D2          ||xor     edx, edx                       ;  edx清0
00401B66  |.  8806          ||mov     [esi], al                      ;  商al给ESI指向的值
00401B68  |.  8BC1          ||mov     eax, ecx
00401B6A  |.  5E            ||pop     esi
00401B6B  |.  F7F6          ||div     esi                            ;  除法,结果在AX中
00401B6D  |.  FF4D 0C       ||dec     dword ptr [ebp+C]              ;  自减1
00401B70  |.^ 79 D7         |\jns     short 00401B49                 ;  结果为正则跳,C语言中表达式应为>=0
00401B72  |.  53            |push    ebx
00401B73  |.  8BC1          |mov     eax, ecx                        ;  ecx是上面[ebp+eax-14] | ecx扩大256的值
00401B75  |.  33D2          |xor     edx, edx                        ;  edx=0
00401B77  |.  59            |pop     ecx                             ;  count=18
00401B78  |.  F7F1          |div     ecx                             ;  除法,结果在AX中
00401B7A  |.  8A4415 D0     |mov     al, [ebp+edx-30]                ;  这里要注意,检索的是低位的值
00401B7E  |.  8807          |mov     [edi], al                       ;  AL移入结果字符串[edi]
00401B80  |>  4F            |dec     edi                             ;  -1;用来存储结果的字符串指针向后1位,用来准备装载下一位,C语言中要注意,这个字符串数组应该式由高位向低位倒着长的
00401B81  |.  FF4D FC       |dec     dword ptr [ebp-4]               ;  [ebp-4]=序列号长度,//自减1
00401B84  |.^ 75 A6         \jnz     short 00401B2C
00401B86  |.  5F            pop     edi
00401B87  |.  5E            pop     esi
00401B88  |.  5B            pop     ebx
00401B89  |.  C9            leave
00401B8A  \.  C3            retn                                     ;  序列号计算完成,函数返回

呵呵,基本上,我每句都加了注释了,应该能看懂吧,注释的都是我自己片面的理解,如果有错误的地方,还请高手们多指正啊。谢谢。

这里对上面的反汇编代码几个需要注意的地方特别提出来一下。

一个是
00401B70  |.^ 79 D7         |\jns     short 00401B49                 ;  结果为正则跳,C语言中表达式应为>=0
这里,一定要注意,我刚开始的时候,看到mov     dword ptr [ebp+C], 0E ,OE=14,就把C代码写成 int i = 14 ; i >0 ; i – 其实正确的应该是 i>=0 为什么?因为程序一开始没有判断I的时候就走了一次,总共应该是走了15次。 呵呵,这点经验,高手看了一定笑话我了吧,不过,新手真的要注意哦

还有一个地方就是最后那部分
00401B80  |>  4F            |dec     edi                             ;  -1;用来存储结果的字符串指针向后1位,用来准备装载下一位,C语言中要注意,这个字符串数组应该式由高位向低位倒着长的

这里要注意看,他这个字符串在内存中,实际是倒着长的,我说怎么我写的算法算出来的序列号跟正确的始终的反的呢,原来我是直接 strcat() 的,所以我的就是正着长的,当然就跟正确的真好相反啦!呵呵。。。

最后,给处一个流程和算法:

程序的流程就是通过查找注册表特定的产品下的某个键值,得到一个产品ID (productID)然后通过这个产品ID 来进行一定的运算,把他的结果,匹配 "BCDFGHJKMPQRTVWXY2346789" 字符串中的某个值。这个值就是序列号的一位。

具体的算法是:
把余数扩大256倍,然后把这个结果跟产品ID的某一位做或运算,再把结果除24,再把除之后的结果产品ID的相对应的那一位,同时,把除24的时候的余数,再扩大256,再重复上述步骤,一共重复15次,循环结束后,把最后的余数的结果作为参数,在"BCDFGHJKMPQRTVWXY2346789" 字符串中取相应的位置,拷贝到结果字符串中去。

说了这么多,我是晕了,不知道你晕了没有,可能是我的语言表述有问题。不过算法这种东西,真的不是靠嘴巴就能说清楚的。所以这里给出我的实现代码


void XP() 
{
  HKEY hKey = 0;
  LONG hKey0 = RegOpenKey(HKEY_LOCAL_MACHINE, "SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion", &hKey);
  BYTE cKey[256];
  memset(cKey, 0, 256);
  DWORD dwLen = 164,
      dwType = REG_BINARY;
  RegQueryValueEx(hKey, "DigitalProductId", NULL, &dwType, cKey, &dwLen);

  BYTE pKey[15];
  memcpy(pKey, cKey + 52, sizeof(pKey));

  char pMask[] = {"BCDFGHJKMPQRTVWXY2346789"};

  char szKey[36];
  ZeroMemory( szKey , 36 );

  int count = 0;

  int i = 14 ;
  int yu = 0 ;

  int temp = 0;
  int temp2 = 0;

  int shang = 0;
  int yu2 = 0;

  for( count = 29 ; count > 0 ; count-- )
  {
    if( count % 6 == 0 && count !=0 )
    {
      strcat( szKey , "-" );
      continue;
    }

    for( i = 14 , yu = 0 ; i >= 0 ; i-- )
    {
      temp = yu * 256;
      temp2 = pKey[i] | temp;

      shang = temp2 / 24;
      pKey[i] = shang;

      yu = temp2 % 24;
    }

    yu2 = temp2 % 24;

    char szTemp[2];
    sprintf(szTemp,"%c",pMask[yu2]);
    strcat( szKey , szTemp );
  }

  MessageBox(szKey, NULL, MB_OK);
}

大家结合着我给出的代码理解一下吧,我这个代码有很多地方是可以简化的,很多变量其实是重复的,不过我故意不简化,因为这样我个人觉得更加清楚,更加有针对性一些。

最后再次谢谢高手能花时间读我的陋文,同时很希望你们能指出我文中的错误。同时如果有什么问题,可以提出来,我们一起讨论一下,有觉得我这篇文章写的好,对你有帮助的朋友请小小的顶一下,以便让更多的需要帮助的人能看到。谢谢。

------------------------------------------------------------------------
【版权声明】本文纯属技术交流, 转载请注明作者信息并保持文章的完整, 谢谢!