微信号:pojie_52

介绍:吾爱破解论坛致力于软件安全与病毒分析的前沿,丰富的技术版块交相辉映,由无数热衷于软件加密解密及反病毒爱好者共同维护,留给世界一抹值得百年回眸的惊艳,沉淀百年来计算机应用之精华与优雅,任岁月流转,低调...

水杉4注册码算法分析以及注册机编写

2016-08-12 10:52 cdj68765

一年前的时候, 我暴力破解了水杉4(http://www.52pojie.cn/thread-368311-1-1.html),一年后的今天,不甘心的我,尝试对水杉4的注册呀验证算法过程进行逆向破解(好吧,其实是在公司没事干,无聊了而已..)

这次破解的我主要用的还是x64dbg,
x64obg 虽然在脱壳等,针对反汇编有防护功能的程序上面会有些无力,但是在分析汇编上面我觉得比od更大强大
尤其是近期更新以后,增加了类似于IDA的F5功能,而且某些方面X64上面的伪代码生成比IDA上面的那个插件更好,可以分析出结构体,而且在代码上面已经完全是C++的风格了
不过毕竟是新功能,所以在伪代码生成结果上面,还是出现了很多问题,嘛,用来算法分析差不多够了

所以这次
使用X64DBG做动态调试
IDA的伪代码做辅助分析
IDE用VS2010(单位电脑上面只能装XP..)

之前帖子上分析的时候,就知道水杉4是使用C++编写的,而且不带壳,更重要的地方是,程序自带
 
pdb标识文件,导致破解过程变成了Debug过程,难度降低了不知道多少,所以没啥技术含量可言,也不知道软件发布公司是怎么想的

嘛,废话不多说,开始正题吧

根据之前破解的经验,我得知,验证密钥的过程,是在名为MQRegister::CheckRegistration(int &,int &)>的函数里面执行的,这个函数名由同名pdb标识文件提供,
并且在之前的破解中得知,这个函数会优先从注册表获得注册信息,然后对密码解密以后与用户名进行对比来获得注册结果,
 
不过这里我走了弯路,我之前以为,输入到注册表里面的用户名和密码,就是原本用户输入的用户名和密码,结果在这次分析到一半的时候,才发现,注册表里面的是加密后的密钥,而用户输入的密钥,在经过之前提到的MQRegister::UncryptPassword(int &,int &)后,才会得到,
所以这里我们可以不用管这个函数做了什么,因为这个函数输出的数据,反而是我们要的

之前的都是回顾,现在开始正式的注册码破解过程

首先,注册码和密钥对我们来说,目前就是个黑箱,我们并不知道注册码有几位和格式是怎么样的,因为,我们只能输入一个随机的
 
比如这样,挺直观的,并且我发现,输入的字符自动变成了大写,所以我们知道线索之一,注册码和密钥都是大写字母和其他东西的组合(比如数字或者符号)然后我们给注册码验证函数下断
 
至于这个为啥给这个函数下断,看我之前的文章就知道了
然后点OK
 
没断下来,并且出现了验证码错误的字,说明在这个函数之前,就有个粗略的验证过程,估计是验证字符长度,我们给调用这个子函数的父函数头下断。
果不其然,断下来了,然后我们单步分析
发现
 
在验证注册码函数之前,有个跳转,通过上图就能知道,这个跳转分析的,就是我输入的用户名,
最终用户名长度和 cmp eax,13  | eax:L"123456789ABCDEF"和16进制的13进行对比,我们把13转换成10进制
 
X64自带的进制计算器,简直赞,好了,我们知道了,用户名是用19个字符组成的,因此我们把注册码改成1234567890123456789
再次运行下断,很好,断在呢验证函数的门口,说明用户名的长度就是19位
我们F7跟进去看看
 
这里再次体现了X64方便调试分析的地方,看注释那里,详细给出了该调指令调用的寄存器里面存着什么参数
同时在下面那个红框上面,我看到了跟之前验证用户名一样的结构,而且跳转分析的对象正好显示的是密钥,
所以cmp eax,7   | eax:L"123456" 我认为,这里就是判断密码长度的地方,密码长度就是7

好,接下来我们就可以直接按F5了,
 
好,我们看到,X64的伪代码生成功能,生成了直观的C代码,
而且使用的参数,完全是
 
结构体访问的方式
 
而且,还给出了结构体完整的结构
因此,这个要比IDA生成的伪代码,在可读性上面,增强了很多,甚至,我可以拿生成的伪代码,稍加修改直接在IDE里面编译
而事实上,我就是这么做的

do {

           eax32 = static_cast<uint32_t>(*(uint16_t*)((int32_t)edi12 + edx31 * 2));//获得用户名第7位,并转换成ascii码表

           esi33 = eax32 - 48;//该位ascii码表所对应的数值减去48h

           if (*(uint16_t*)&esi33 > 9) {

               esi34 = eax32 - 65;

               if (*(uint16_t*)&esi34 > 7) {

                   esi35 = eax32 - 74;

                   if (*(uint16_t*)&esi35 > 4) {

                       if (eax32 == 80) {

                           continue;

                       } else {

                           esi36 = eax32 - 82;

                           if (*(uint16_t*)&esi36 > 8) 

                               goto addr_0x12921a5_4;

                           eax37 = eax32 - 57;

                       }

                   } else {

                       eax37 = eax32 + 0xffffffc9;

                   }

               } else {

                   eax37 = eax32  -55;

               }

           } else {

               eax37 = eax32 - 47;

           }

           if ((int32_t)eax37 <= (int32_t)0) 

               goto addr_0x12921a5_4;

           ++edx31;//计数+1

       } while (edx31 <= 12);//计数小于等于12就循环

以上的这种类型的循环代码,进行了好几次分别对7到12位,14到19位,1到6位,以及密码的1到3位,5到7位进行了同样的操作,而goto addr_0x12921a5_4;,其实就是失败,也就是goto return
因此,判断,这里的这道算法,就是对输入的字符进行转换,如果不符合,那就直接失败,而为啥不直接把所有的字符一次性验证完,而分开验证呢,不用想我们也可以猜到,密码和用户名常用的字符“-”字符,估计是不能通过验证的,所以专门为了这个字符而分开验证。。。我只能说,这估计是为了迷惑破解者才弄得吧,
通过以上,我得到了用户名格式是:XXXXXX-XXXXXX-XXXXX,而密码应该就是XXX-XXX;当然我现在是后日谈,所以很多东西都是一笔带过,而真正猜出这条结论的时候,还是通过动态调试,分析了很多次
而哪些字符可以通过验证呢?我复制了伪代码,稍加修改,写成了C#代码

输出的结果是
 
0到9,大写的A到Z之间,跳过I,O,P这三个大写字母的字符,都是可以使用的字符

验证完后,出现了一条很长的判断
  if (edi12->f12 == 45 && (edi12->f26 == 45 && (ecx5->f6 == 45 && ((eax59 = static_cast<uint32_t>(edi12->f10), v60 = eax59, eax59 == 84) || (eax59 == 82 || (eax59 == 85 || (eax59 == 83 || eax59 == 69))))))) {.....}

判断了eax59这个变量是否是84,82,85,83,69,之间的某个数,而且eax59是从edi
12-》f10来的,而edi12正好是存用户名的一个结构体f10是用户名的第5位,因此,我判断,第5位字符是用来判断哪个版本的,至于为什么我可以断定是用来判断版本的了,请看我之前文章里面提到的版本选择那一块,跟这里的对比一模一样,好吧,我瞎猜的,233,其实破解注册码的过程,也是一个猜测可能性并且验证的过程不是?
根据之前的经验,水杉最高级的版本是当69的时候跳转,那也就是说,注册码第5位。应该固定是字符E(ASK码69)
同时我们还看到对f12,f23,f6是否是45进行验证,而45对应的正好是"-"字符,由此可见,我之前猜测的用户名格式是正确的
哦对了,你们可能会问,为啥我会判断f10是用户名第几位云云
其实
 
根本不用分析,X64生成的伪代码,把该函数用到的结构体也分析了,你看s0里面的结构,每一位是否就对应的用户名的每一位?而s1也就对应的密钥的每一位,这也是我认为X64的伪代码生成比IDA强大的地方
以上都是验证用户名密钥格式以及字符正确性的地方,接下来,就是验证用户名密钥关联性正确的了
 
往下翻阅的时候,我们看到,都是千篇一律的获得用户名某位的字符,进行一定的判断转化
写成C#就是类似于这样


int v136;

           if ((UInt16)(v19 - 48) > 9u)

           {

               if ((UInt16)(v19 - 65) > 7u)

               {

                   if ((UInt16)(v19 - 74) > 4u)

                   {

                       if (v19 == 80)

                           v136 = 24;

                       else

                           v136 = (UInt16)(v19 - 82) > 8u ? 0 : v19 - 57;

                   }

                   else

                   {

                       v136 = v19 - 55;

                   }

               }

               else

               {

                   v136 = v19 - 54;

               }

           }

           else

           {

               v136 = v19 - 47;

           }

           return v136;

比如我输入C,返回的就是13,输入0返回的就是1,由此可见,这算法,就是把输入的字符,映射成1到33之间对应的数值,
之后,程序对17位字符(不包括第5位和-)进行相同的操作之后
终于对每个获得的字符串进行一次整合性的运算


edx174 = (int32_t)(eax116 * v109 + ebx102 * v95 + v89 * v82 + v74 * v67 + ecx173 * esi165 + edx158 * v152 + v145 * v138 + v130 * v123 + 17) % 33 + 1;


 
整合运算以后,进行一次类似之前的操作之后,第一次将用户名与密码进行了一次对比,这里是跟密码第6位进行对比,
而且是直接明文对比,也就是说,我之前的用户名只要符合规则,即便随便乱写,这里也会给我正确的一位密码,
分析到这里后,事情就变得简单多了后面都是相同模式的验证方法,只不过是前面一环套后面一环的验证,虽然很复杂,但是由于是明文验证,所以知道根据伪代码,知道验证的是哪一位,即便是傻子都能得到正确的密钥,哎,想我去年,还以为密钥破解有多少困难了,没想到都是唬人的

之后的事情就简单了,随机生成一个符合规则的用户名,只要满足0到9,A到Z之间不是I,O,P,并且第5位一定是E的话,只要跟着伪代码写,伪代码里对比密钥的就是,就是密钥的正确字符
然后全部代码就是(没办法,我熟悉的只有C#)


Dic t = new Dic(id);

int v39 = (t.Num[7] * t.Num[12] + t.Num[10] * t.Num[11] + t.Num[9] * t.Num[8] + t.Num[0] * t.Num[14] + t.Num[2] * t.Num[18] + t.Num[1] * t.Num[15] + t.Num[3] * t.Num[17] + t.Num[4] * t.Num[16] + 17) % 33 + 1;

char[] password = new char[7] { '0', '0', '0', '-', '0', '0', Convert.ToChar(AnyAddNum(v39)) };

int v46 = 73 * t.Num[1];//v41 + 2,219

int v51 = 12 * t.Num[5] + 512 + v46 + 17693;//v50-v122-v17-a2 + 10,18604,48AC

int v55 = 267 * t.Num[12] + 5112 + v51;//v54-v121-v31-a2 + 24,26920

int v59 = v55 + ((t.Num[16] + 8) << 6);//v58-v117-a2 + 32,28392

int v63 = 47 * t.Num[3] + 5842 + v59;//v62-v112-a2 + 6,34469

int v67 = v63 + 9477 * t.Num[9] + 687;//v66-v120-a2 + 18,120449

int v72 = 4798 - 94 * t.Num[8] + v67;//v70-v123-v127 + 16,124495

int v75 = v72 + 10784 - 94 * t.Num[2];//v74-v125-a2 + 4,134903,20EF7

int v78 = 874 - 15 * t.Num[17];//v77-a2 + 34,634

int v79 = 14 * t.Num[18] + 3543; //v76-v118-a2 + 36,3781.EC5

int v47 = 1241 - 71 * t.Num[11];//v42-v127 + 22,460,1CC

int v52 = 7 * 121 + v47 + 37926;//v48-v41 + 38,39114-<39233-<9941

int v56 = v52 + 7 * t.Num[14] + 187;//v111-v19-a2 + 28,39392

int v60 = 13 * t.Num[0] + v56 + 6897;//v115-v18-a2,46315

int v64 = v60 + 13 * t.Num[10] + 764;//v119-v32-a2 + 20,47209

int v68 = 13 * t.Num[4] + 7604 + v64;//v113-v28-a2 + 8,54891

int v71 = 987 - 32 * t.Num[7] + v68;//v124-v30-a2 + 14,55654

int v125 = v71 + 9653 - 16 * t.Num[15];//v114-v23-a2 + 30,65083

int v80 = ((v75 * v78 - v125 * v79) % 64102);//-38913

int v82 = Math.Abs(v80) / 43 % 33 + 1;

password[2] = Convert.ToChar(AnyAddNum(v82));

int v126 = v71 + 9653 - 16 * t.Num[15];// v115.dwHighDateTime = v23;FEB2

int v87 = 17 * t.Num[17] + 148;//420

int v81 = (Int32)(v126 * v78 + v75 * v79) % 15627;//7676,1DCO

int v86 = 453 - 7 * t.Num[0];//v85-v116-v18-a2,439,1B7

int v88 = v80 * v86;//-17620582,FEF3219A

int v90 = Math.Abs((v88 - v81 * v87) % 316306);//259412,0003F553

int v91 = (v90 + 692351) % 33 + 1;//11,B

password[5] = Convert.ToChar(AnyAddNum2(v90, v91));

int v89 = (v81 * v86 + v87 * v80) % 47891;

int v94 = Math.Abs(v89);

int v95 = (v94 + 14632) % 33 + 1;

password[0] = Convert.ToChar(AnyAddNum2(v94, v95));

int v100 = (t.Num[16] + AnyNumeToChar(AnyAddNum2(v90, v91)) * t.Num[10]) % 33 + 1;

password[1] = Convert.ToChar(AnyAddNum(v100));

int v102 = AnyAddNUm4(Convert.ToChar(t.t[7]));

int v103 = AnyAddNUm4(Convert.ToChar(password[2]));

int v104 = AnyAddNUm4(Convert.ToChar(t.t[18]));

int v105 = (v102 + v103 * v104) % 33 + 1;

int v106;

if ((uint)((v102 + v103 * v104) % 33) > 9)

{

    if ((uint)((v102 + v103 * v104) % 33 - 10) > 7)

    {

        if ((uint)((v102 + v103 * v104) % 33 - 18) > 4)

        {

            if (v105 == 24)

                v106 = 80;

            else

                v106 = (uint)((v102 + v103 * v104) % 33 - 24) > 8 ? 42 : v105 + 57;

        }

        else

        {

            v106 = v105 + 55;

        }

    }

    else

    {

        v106 = v105 + 54;

    }

}

else

{

    v106 = v105 + 47;

}

password[4] = Convert.ToChar(v106);


然后么
 
根据代码写了个注册机
生成后
 
不错吧,233,终于从注册码层面告破了

嘛,别看我写的这么轻松,其实我还是花了一周时间的,毕竟技术不到家,虽然这文章里面全程没有看到我在动态调试,不过实际上我还是边对比伪代码边调试的,这里全部省略了而已。

还有 ,虽然很抱歉,但我还是不会放出注册机的,毕竟还是人家卖钱的东西,而且卖的挺贵,EX版本 大概要RMB 1000了,而且,代码都在上面了,真想要注册码,拿我上面贴出来的程序,然后自己再根据我说的分析下就能得到正确的注册码了
(上面的程式没问题,我没给出“AnyAddNum”具体的实现过程,所以并不能直接生成,不过我可以告诉你AnyAddNum你们的方法就是类似代码最后面那种跳转获得数值的方法而已,很容易就可以找到)
就这样吧,有问题我再改

--官方论坛

www.52pojie.cn

--推荐给朋友

公众微信号:吾爱破解论坛

或搜微信号:pojie_52

 
吾爱破解论坛 更多文章 【清理未活跃会员】清理2016年暑假开放注册未活跃会员公告 吾爱破解论坛动画大赛2016(火热开赛中ing~) 吾爱破解论坛动画大赛2016(明日开赛) Android逆向实例笔记—同步家教王及其升级版的破解 【Android 原创】单机游戏SB69——unity逆向简记
猜您喜欢 ❲阮一峰❳ Git、GitHub、GitLab协作流程详解 HBase高可用原理与实践 数字化企业云平台下的移动平台建设 AOP中的一些术语解析 Swift之贪婪的UIButton-应该是关于UIButton的全部了