软编码寻找序列号(三)

使用工具

  • OllyDbg 1.10原版,简称OD
  • OD 汉化插件均来自互联网;
  • CrackMe来自互联网,仅供学习使用;
  • 文中特殊数字均是HEX,为了书写方便采用DEC

逆向思路

  1. 首先,打开软件到处点点,逆向一个软件起码要会用吧,不会用就没必要说逆向了:

    • 这是一个类似于蜘蛛纸牌的程序,需要安装,安装完成打开之后会提示输入注册码:

      开始

    • 输入用户名和 6 位数字的注册码之后,发现Ok按钮无法点击:

      按钮

      有点意思,看来只有输入正确的序列号按钮才能点击,至于为啥注册码是 6 位数字,它不是已经说明了吗?

  2. 倒入OD开始分析:

    • 首先按下Ctrl + AOD分析一下代码;

    • 猜测一下它的工作原理,如果在输入正确的注册码之后按钮可以点击,那就说明它会实时获取我们输入的内容并进行判断,这样的话,有用的应该就是内存断点了;

    • 说干就干,既然要去内存中找我们输入的内容,那输入就一定得个性一点,起码不是内存中常见的,不然一找一大堆就无从下手了:

      输入

    • 然后去内存窗口按下Ctrl + B或者右键选择查找,输入我们的序列号点击确定开始查找,记得勾选区分大小写

      查找

    • 输入的还算奇葩,只找到了一个:

      查找结果

    • 设置内存访问断点,既然它都告诉我们是 6 位了,那就设置 6 个字节:

      设置断点

    • 然后去程序界面再输入一个字符后,发现程序中断了,回到OD看看,嘿,它居然在转移数据,在数据窗口跟随一下EDI,果然是我们输入的假序列号:

      转移数据

    • 既然数据都挪窝儿了,那之前的断点也就没用了,选择EDI的数据右键断点 >> 内存访问重新设置内存访问断点

      新断点

    • 断点设置好之后,F9继续运行程序,程序再次中断,发现又在挪窝儿?怎么回事儿,挪窝儿这么快乐吗?

      又一个断点

      重新设置内存访问断点,然后继续运行程序;

    • 程序再次中断,并且有比较有跳转,这儿可能就是重点了,分析一下:

      • 第 1 行是:EAX的前 4 字节数据拷贝到EDXF7单步执行,然后查看一下是什么东西:

        查看EAX

        原来是我们输入的假序列号,前面还多了个04,应该是长度吧;

      • 第 2 行是:EDX的前 4 字节数据拷贝到EBXF7单步执行,然后查看一下是什么东西:

        查看EDX

        嘿嘿,一串可疑的数字,而且是 6 位,和序列号的规则很吻合,而且前面多了个06,应该也是长度;

      • 接着往后分析,逻辑很常规,比较完前四个字节比较长度,都不相等就返回了:

        返回

    • 结局没有悬念,用户名对应的序列号就是那串数字,不过,序列号是如何计算出来的呢?我们接着分析:

      • 我们都知道RETN返回的是调用位置的下一行,那跟着RETN去看看:

        RETN

        按照程序的流程,既然这里是判断,那上面的就是获取和处理了,上面CALL不少,哪一个才是我们想要的那个呢,按照先获取后处理的流程来看,离我们最近的那个CALL应该是处理用的,但CALL上面那行代码看着也不像是给它传递参数,那就在这个CALL上设置断点,进去看看它是干嘛的;

      • 设置好断点,重载程序并运行程序之后,程序都没有加载完成就中断了,既然都没有输入,那肯定不行啦,F9运行程序,起码运行到我们能输入内容;

      • 然后,我们发现,输入用户名的时候,每输入一个字符都会中断一次,那就说明断点有效,虽然不知道这个CALL具体是干嘛的,但肯定和用户名有关,既然每次都中断,那就输入一次运行一次呗,直到用户名输入完成,还没有完,为了防止它没有完全获取用户名(鬼知道它是怎么获取的,万一是运行之后获取呢?),在输入完用户名最后一个字符之后,程序会再次中断,我们再运行程序一次,在序列号一栏中,输入一个字符,发现它中断了;

      • 管它呢,反正用户名输入完了,也在序列号一栏输入字符了,能保证它获取了完整用户名就行,至于它是干啥的,F7进去看看不就知道了:

        • 刚分析了几行,发现它用同样的方法转移用户名,既然要知道序列号是怎么计算的,那肯定要跟着用户名喽,给EDI中的用户名数据设置内存访问断点

          用户名

        • 接着,F9运行程序,程序跳转到了这里,果然,这里就是用来生成序列号的:

          小循环

          这里是一个完整的小循环,用来运算用户名的每一个字符,并且把结果累加,至于运算规则:

          • 字节下标从 1 开始;

          • 字节机器码和下标相乘;

          • IMUL DWORD PTR DS:[EDX*4+4EB5D8]这行代码中的所有特殊字符:

            12345678910
            D96358223E93F0083462
            11121314151617181920
            1BBFD7B96F4A5AB28424

            从上面的表格可以看出,用户名最大不能超过 16 进制的 14 位,也就是 10 进制的 20 位,代码中也一直和 14 做比较,当然,这是我的猜测,有兴趣的同学可以试试;

          • 然后字节机器码和下标相乘的结果,和表格中对应下标的字符相乘;

          • 接着,把每个字符运算的结果累加;

          • 就这?这么简单?想多了;

        • 接着往下看:

          余数

          在上一步,也就是那个小循环中,按照特定的规则运算了用户名的每一个字节,并把每个字节的运算结果进行累加,然后才是这一步,规则如下:

          • 上一步累加的最终结果和A 的 6 次方进行求余运算;

          • 如果余数大于A 的 5 次方,进行下一步,也就是开始计算序列号,至于为什么要有这么个规定,如果你还记得序列号的规则,那就不会有什么疑惑了,因为只有余数大于A 的 5 次方,才能计算出 6 位数的序列号,看到这句话,是不是已经想到如何计算序列号了,追随你的心,是的,就是那样计算的;

          • 如果余数小于A 的 5 次方,那么就重复执行上一步中的小循环,只不过,这一次的计算结果会累加在上一次计算的最终结果上,也就是,你只需要计算一个完整小循环,如果计算的结果求余A 的 6 次方小于A 的 5 次方,只需要把你计算的最终结果乘以 2,就是下次循环的计算结果,如果乘以 2 的结果求余A 的 6 次方还是小于A 的 5 次方,那就乘以 3 呗,以此类推;

          • 不要急,下面就开始计算序列号了;

        • 接着,就到了计算序列号的CALL:

          计算序列号

          • 这一步很简单,用累加结果求余A 的 6 次方,然后就是计算序列号的函数了;
          • 到重点了,当然是F7进去看看喽;
        • CALL之后,你会很惊讶,因为真正的计算就简简单单几行代码:

          计算

          • 用累加结果求余A 的 6 次方后的余数对A进行求余运算;
          • 然后判断商是否为 0,不是 0 ,继续对A进行求余运算,直到商为 0;
          • 把每次对A求余运算的余数加上 30 存起来;
        • 还没有结束,最后一步就是把上一步的结果逆序,然后就是真正的序列号了:

          剧终

    • 啰哩啰嗦终于分析完了,总结一下注册码的生成规则:

      • 用户名必须小于 10 进制的 20 位,我也不确定啊,没试;

      • 用户名字节的下标从 1 开始;

      • 取用户名每个字节和它对应的下标进行乘法运算;

      • 用每个字节运算的结果乘以对应下标的给定字符,给定字符如下,序号就是下标:

        1234567891011121314151617181920
        D96358223E93F00834621BBFD7B96F4A5AB28424
      • 然后把每个字节乘以给定字符的结果相加;

      • 用相加的结果求余A 的 6 次方,如果余数大于A 的 5 次方,向下执行;如果余数小于A 的 5 次方,用累加的最终结果乘以它的自然数倍数,如 2 倍或者 3 倍,直到最终结果求余A 的 6 次方的余数大于A 的 5 次方

      • 然后用上一步中的余数,也就是求余A 的 6 次方后的余数,对A进行求余运算,直到对A的求余运算商为 0;

      • 把每次对A求余运算的余数加 30;

      • 把余数加 30 后的数字逆序排列;

      • 转为 10 进制就是用户名对应的序列号;

    • 说人话:

      • 16 进制的A是 10 进制的10;
      • A 的 6 次方1000000,也就是一百万;
      • A 的 5 次方100000,也就是十万;
      • 也就是说,用户名累加的结果大于一百万的时候,这条规则才真正有意义,否则,对A 的 6 次方求余的余数永远是它自己;
      • 至于为啥要大于A 的 5 次方,十万不就是最小的 6 位数吗?你不大于A 的 5 次方也就是不够 6 位,肯定不行喽;
      • 至于对 10 求余的结果是什么,不用说了吧?
      • 16 进制的30是什么,是 ASCII 码的0,所以,求余后加 0 的意义是什么?
      • 总结:如果你输入的用户名运算的最终结果,转换为 10 进制是 6 位数字,那这个 6 位的数字就是注册码;如果小于 6 位,就用最终结果乘以 2 或 3 或 N,以此类推,直到是 6 位或更多位,当然,6 位最好,因为更多位需要求余 😊;
    • 无图无真相:

      • 例如:crackme的机器码是63 72 61 63 6B 6D 65

      • 每个字节和它对应的下标进行乘法运算:

        63 * 172 * 261 * 363 * 46B * 56D * 665 * 7
        63E412318C21728E2C3
      • 用每个字节运算的结果乘以对应下标的给定字符:

        1234567891011121314151617181920
        D96358223E93F00834621BBFD7B96F4A5AB28424
        63 * D9E4 * 63123 * 5818C * 22217 * 3E28E * 932C3 * F0
        53EB582C6408349881921778A296D0
      • 把每个字节乘以给定字符的结果相加:53EB + 582C + 6408 + 3498 + 8192 + 1778A + 296D0 >> 5D4A3

      • 对 10 求余再加 0 之类的操作就没必要了吧;

      • 5D4A3转换成 10 进制是382115

      • 输入用户名和注册码:

        无图无真相