【文章标题】: XP序列号算法分析,给像我这样的迷漫的初学者!
【文章作者】: NOIR
【作者邮箱】: [email]noir0080@21cn.com[/email]
【软件名称】: ProduKey.exe
【软件大小】: 52.5K
【下载地址】: 自己搜索下载
【编写语言】: VC6
【使用工具】: OLLYDGB
【操作平台】: XP
【软件介绍】: 这是一个算微软产品序列号的软件。
【作者声明】: 只是感兴趣,没有其他目的。失误之处敬请诸位大侠赐教!
--------------------------------------------------------------------------------
【详细过程】
本人也是菜鸟,用了几天研究了一下,这里特意把自己的心得记录下来,这篇文章完全是给像我这样的新手看的,在高手面前就献丑了,里面我会尽量把一些细节的东西都交代清楚,目的是务必使大家少走弯路。
好了,不说废话了,让我们先大致看一下这个软件吧。
用peid查壳,发现使UPX的一个老版本的壳,我上网直接找了个脱UPX壳的软件,直接脱了(没办法,菜鸟,不会手动脱壳啊)。脱壳之后,我们先运行一下这个软件。我们看到软件运行之后没有什么提示,直接显示出来的就是一个列表,列表内容就是在当前机器上所安装了的所有微软产品的CD-KEY。恩,不错,看上去还是挺简单的,不搞多余动作了,我们马上开始吧。
熟悉WIN32的朋友都应该知道。(什么!?你不知道?那我还是建议你快点先去恶补一下WIN32编程吧,推荐去看一下这个帖子http://bbs.pediy.com/showthread.php?s=&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);
}
大家结合着我给出的代码理解一下吧,我这个代码有很多地方是可以简化的,很多变量其实是重复的,不过我故意不简化,因为这样我个人觉得更加清楚,更加有针对性一些。
最后再次谢谢高手能花时间读我的陋文,同时很希望你们能指出我文中的错误。同时如果有什么问题,可以提出来,我们一起讨论一下,有觉得我这篇文章写的好,对你有帮助的朋友请小小的顶一下,以便让更多的需要帮助的人能看到。谢谢。
------------------------------------------------------------------------
【版权声明】本文纯属技术交流, 转载请注明作者信息并保持文章的完整, 谢谢!
[注意]传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!