CLion註冊碼算法逆向分析實錄(純研究)

聲明

CLion程序版權爲jetBrains所有、註冊碼授權爲jetBrains及其付費用戶所有,本篇只從興趣出發,研究其註冊碼生成算法。

  • 不會釋出任何完整的源代碼.
  • 網上查了下,已有註冊機,所以想要key的同學不要找我:p

背景

打算學習cocos2dx,奈何vim只會ggvG,被jetBrains慣壞了,找到了CLion,試了下,果然神器,我等菜鳥正好可以拿來愉快地學習書寫c++了。

但是,試用版有30天的限制,又沒有學生授權,懶得折騰,看下它的註冊算法吧。

本篇用到的主要工具和命令:

  • jd-gui 1.2
  • jdb
  • zip/unzip
  • jar

找出註冊算法的代碼段

先用jd-gui看下反編譯的效果,大概找了下,看到這個MainImpl類:
這裏寫圖片描述

反編譯的效果還是可以,不過注意到一些類和方法的名稱被混淆了,如上圖的a.a().a(new a.a_())....

嘗試直接建立java文件,發現很多類缺失,比較難補,遂放棄。

嘗試用java attachagent方式來dump運行時的class,比較難偷,遂放棄。

換換思路

CLion啓動後,如果未輸入正確的註冊碼,或者沒有選擇試用,會打開一個對話框,提示輸入註冊碼。

如下圖:
這裏寫圖片描述

如果選擇試用,先進入主界面,打開關於對話框,可以看到部分授權信息。

如下圖:
這裏寫圖片描述

好,從這兩個點出發,大概找找相關代碼,縮小分析範圍:

分析MainImpl啓動前後的類加載信息

修改$CLION_HOME/bin下的clion.sh
這裏寫圖片描述

在啓動參數中,加入-verbose.

重新啓動CLion,收集到MainImpl加載前後的一些信息,如下圖:

這裏寫圖片描述

繼續翻看,又發現如下一些信息:

這裏寫圖片描述

發現 com.intellij.ide.a包和com.intellij.a包嚴重混淆,並且在MainImpl加載前後出現.
可以初步判定這些包是分析重點.

分析AboutPopup類的信息

打開關於/About對話框,觀察比對代碼,發現如下信息:
這裏寫圖片描述

在粗略分析相關代碼後,跟蹤到這裏:

這裏寫圖片描述

LicensingFacade是個抽象類,跟一些實現,發現只有一個子類:

這裏寫圖片描述

接着跟,又發現一個抽象類bb

這裏寫圖片描述

這個類在com.intellij.ide.a.g包下,其中一段關鍵代碼:

public void a(q.a_ parama_)
  {
    boolean bool = GraphicsEnvironment.isHeadless();

    f();
    a(this.f);
    /// 這裏的u()是關鍵
    for (s locals : u())
    {
      if (locals.d() == null) {
        locals.c();
      }
      com.intellij.a.b.e locale = locals.d();
      if (locale != null)
      {
        g_ localg_ = a(parama_, locals);
        if (localg_ != g_.SKIP)
        {
          UpdateChecker.addUpdateRequestParameter("license", a(locale));
          if (localg_ != g_.OK) {
            break;
          }
          int i1 = (!bool) && (a(locale, locale instanceof l)) ? 1 : 0;
          if (i1 == 0)
          {
            a(parama_, locals, locale);
            return;
          }
          break;
        }
      }
    }
    if (bool)
    {
      b.error("No valid license found");
      System.exit(-1);
    }
    else
    {
      b(parama_);
    }
  }

接着看com.intellij.ide.a.g.bb.u()方法:

private s[] u()
  {
    s[] arrayOfs = v();
    Object localObject1 = null;
    for (Object localObject3 : arrayOfs)
    {
      try
      {
        ((s)localObject3).c();
        if (((s)localObject3).d() != null) {

          /// 這裏的b()是關鍵
          ((s)localObject3).b();
        } else {
          continue;
        }
      }
      catch (Exception localException)
      {
        continue;
      }
      localObject1 = localObject3;
      break;
    }
    if (localObject1 != null)
    {
      ??? = new ArrayList(Arrays.asList(arrayOfs));
      ((ArrayList)???).remove(localObject1);
      ((ArrayList)???).add(0, localObject1);
      return (s[])((ArrayList)???).toArray(new s[((ArrayList)???).size()]);
    }
    return arrayOfs;
  }

中間的過程比較繁瑣,考驗的是耐心、推測、和筆記功夫。
通過相關代碼的類型分析、方法分析、參數分析、變量分析,逐步縮小關鍵代碼段的分析範圍,同時增加對混淆代碼的熟悉。


分析註冊碼算法解密驗證的重點包

結合上面的對於MainImpl加載前後的類信息分析,重點關注com.intellij.a.g這個包.

最終跟到這樣一個類com.intellij.a.g.c

package com.intellij.a.g;

import java.math.BigInteger;

public class c
{

/// 這個a是關鍵,通過後面的實驗,這是RSA加密算法的公鑰部分的n .
  private static BigInteger a = new BigInteger("8eeb1420b7d8b90aa3b95c7ff73628e46e12c19dc91531be5f517a54b042e99c17445ce7b23834a3ec80d2691b463231be43aab7e897cc334bc9b8bb9f0d55f5", 16);
  private static BigInteger b = new BigInteger("10001", 16);

  /// 這個方法是關鍵
  /// 通過推測和實驗,可以確定paramString1和paramString2是用戶名和代碼,或者相反。
  public static h a(String paramString1, String paramString2)
    throws a
  {
    // 這個h類型是關鍵,記錄了註冊碼類型、產品類型、用戶名、過期時間、維護到期時間、主版本號、小版本號等關鍵信息
    h localh = new h();

    // 這裏m.a(,,,)方法完成第一次解密
    byte[] arrayOfByte = m.a(paramString1, localh, b, a);
    // m.a(,,,)方法驗證註冊碼解密後的字節信息,從中提取關鍵屬性
    m.a(arrayOfByte, localh, paramString2, 14);
    // 繼續驗證
    m.b(arrayOfByte, localh, paramString2, -1);
    return localh;
  }
}

爲了確定靜態分析過程的正確性,使用jdb來看一下簡單的調用棧:

Idea Main Thread 1.0.4#CL-141.874.66, eap:false[1] wherei 0x6f4

  /// 可以確定,每次加載MainImpl,調用其start方法,都會來下面這個方法中驗證註冊碼,這是迄今爲止最爲關鍵的發現
  [1] com.intellij.a.g.c.a (c.java:18), pc = 13

  [2] com.intellij.ide.a.b.b$0.b (b$0.java:71), pc = 2
  [3] com.intellij.ide.a.b.b$0.a (b$0.java:67), pc = 3
  [4] com.intellij.a.b.d.x (d.java:34), pc = 12
  [5] com.intellij.a.b.d.t (d.java:13), pc = 1
  [6] com.intellij.a.b.b.s (b.java:40), pc = 10
  [7] com.intellij.a.b.b.c (b.java:33), pc = 1
  [8] com.intellij.ide.a.g.t.h (t.java:108), pc = 41
  [9] com.intellij.ide.a.g.t.a (t.java:27), pc = 8
  [10] com.intellij.ide.a.g.m.b (m.java:29), pc = 27

  // bb類是個很重要的類,後續分析
  [11] com.intellij.ide.a.g.bb.u (bb.java:152), pc = 48
  [12] com.intellij.ide.a.g.bb.a (bb.java:78), pc = 18
  [13] com.intellij.idea.MainImpl$1.start (MainImpl.java:45), pc = 19
  [14] com.intellij.idea.StartupUtil.prepareAndStart (StartupUtil.java:117), pc = 115
  [15] com.intellij.idea.MainImpl.start (MainImpl.java:40), pc = 9
  [16] sun.reflect.NativeMethodAccessorImpl.invoke0 (native method)
  [17] sun.reflect.NativeMethodAccessorImpl.invoke (NativeMethodAccessorImpl.java:62), pc = 100
  [18] sun.reflect.DelegatingMethodAccessorImpl.invoke (DelegatingMethodAccessorImpl.java:43), pc = 6
  [19] java.lang.reflect.Method.invoke (Method.java:497), pc = 56
  [20] com.intellij.ide.plugins.PluginManager$2.run (PluginManager.java:91), pc = 53
  [21] java.lang.Thread.run (Thread.java:745), pc = 11

分析註冊碼字節數組


/// 我們重點跟一下com.intellij.a.g.c.a()方法:

package com.intellij.a.g;

import java.math.BigInteger;

public class c
{
  private static BigInteger a = new BigInteger("8eeb1420b7d8b90aa3b95c7ff73628e46e12c19dc91531be5f517a54b042e99c17445ce7b23834a3ec80d2691b463231be43aab7e897cc334bc9b8bb9f0d55f5", 16);
  private static BigInteger b = new BigInteger("10001", 16);

  /// 重點跟這個方法
  public static h a(String paramString1, String paramString2)
    throws a
  {
    h localh = new h();

    byte[] arrayOfByte = m.a(paramString1, localh, b, a);
    m.a(arrayOfByte, localh, paramString2, 14);
    m.b(arrayOfByte, localh, paramString2, -1);
    return localh;
  }
}

在上面的方法中,類型h很關鍵,這裏的字符串沒有混淆,給我們推測註冊碼結構提供了極大的便利 O^. ^O.

    /// 這裏的一些字段和變量的意思,已經給出來了
    public String toString() {
        StringBuffer var1 = new StringBuffer();
        var1.append("\n");
        var1.append("user name:" + this.b + "\n");
        var1.append("customer id:" + this.c + "\n");
        var1.append("product id:" + j.a(this.d) + "\n");
        var1.append("license type:" + i.a(this.e) + "\n");
        var1.append("major version:" + this.f + "\n");
        var1.append("minor version:" + this.g + "\n");
        var1.append("generationDate:" + this.h + "\n");
        var1.append("expirationDate:" + this.i + "\n");
        return var1.toString();
    }

    static {
        a.b = "Fake";
        a.d = 0;
        a.e = 1;
        a.f = 1;
        a.g = 0;
        a.h = d.a(1995, 1, 1);
        a.i = d.a(1995, 1, 2);
    }

跟這個方法:byte[] arrayOfByte = m.a(paramString1, localh, b, a);

protected static byte[] a(String var0, h var1, BigInteger var2, BigInteger var3) throws a {
        Matcher var4 = c.matcher(var0);
        var0 = var4.replaceAll("").trim();

        // 10是ASCII碼中的"\n"換行符
        // 13是ASCII碼中的"\r"回車符
        // 下面的代碼告訴我們一個重要信息:
        // 加密的註冊碼至少包含1個\n(linux)或者\r(windox|mac)
        int var5 = var0.indexOf(10);
        if(var5 < 0) {
            var5 = var0.indexOf(13);
        }

        if(var5 < 0) {
            throw new a();
        } else {
            // 45是ASCII碼中的"-"
            // 重要信息:加密的註冊碼至少包含一個"-",且出現在"\n"或"\r"之前
            String var6 = var0.substring(0, var5);
            //"-"
            int var7 = var6.indexOf(45);
            if(var7 > 0) {
                try {

                    // 從[xx-??\n???]中提取xx,並賦值給var1.c
                    // 根據上面的h類型的信息,這裏的xx是cutomerId,所以我們有了:
                    // license : customerId-??\n???
                    var1.c = Integer.parseInt(var6.substring(0, var7));
                } catch (NumberFormatException var9) {
                    throw new a();
                }
            } else {
                var1.c = -1;
            }

            // 這裏可以確定傳入的var2是公鑰的n,var3是公鑰的e
            // 同時根據RSADecoder我們也找到了對應的RSAEncoder,後續直接拷貝RSAEncoder過來,作註冊碼加密用^^
            return (new RSADecoder(var2, var3, 64)).decode(var0.substring(var5 + 1));
        }
    }

跟這個方法:m.a(arrayOfByte, localh, paramString2, 14);

static void a(byte[] var0, h var1, String var2, int var3) throws a {
        //這裏,我們根據傳入的var3 = 14,得知:
        //license的長度爲14個字節
        if(var0.length != var3) {
            byte[] var4;
            if(var0.length == var3 + 1) {
                if(var0[0] != 0) {
                    throw new a();
                }

                var4 = new byte[var3];
                System.arraycopy(var0, 1, var4, 0, var3);
                var0 = var4;
            } else {
                if(var0.length >= var3) {
                    throw new a();
                }

                var4 = new byte[var3];
                System.arraycopy(var0, 0, var4, var3 - var0.length, var0.length);
                var0 = var4;
            }
        }

    // 這裏我們根據分析,得到:
        //var2 : userName , var1.c : customerId , var3 : 14

        // 假設lic是個14字節長的字節數組,我們有:
        //lic[12] = customerId & 0xFF;
        //lic[13] = customerId >> 8 & 0xFF;

        if(var2 != null) {
        /* 
            下面的o.a(,,)方法的實現:
            可以直接把這段代碼copy到我們做註冊碼的地方^^

        public static short a(String var0, int var1, byte[] var2) {
            CRC32 var3 = new CRC32();
            int var4;
            if(var0 != null) {
                for(var4 = 0; var4 < var0.length(); ++var4) {
                    char var5 = var0.charAt(var4);
                    var3.update(var5);
                }
            }

            var3.update(var1);
            var3.update(var1 >> 8);
            var3.update(var1 >> 16);
            var3.update(var1 >> 24);

            for(var4 = 0; var4 < var2.length - 2; ++var4) {
                byte var6 = var2[var4];
                var3.update(var6);
            }

            return (short)((int)var3.getValue());
        }
         */

            short var5 = o.a(var2, var1.c, var0);
        // 這裏得到:
        // lic[12] = crc32 & 0xFF
        // lic[13] = crc32 >> 8 & 0xFF
            if(var0[var3 - 2] != (byte)(var5 & 255)) {
                throw new a();
            }

            if(var0[var3 - 1] != (byte)(var5 >> 8 & 255)) {
                throw new a();
            }
        }

努力,繼續跟這個方法:m.b(arrayOfByte, localh, paramString2, -1);

 static void b(byte[] var0, h var1, String var2, int var3) throws a {
        try {
       /** 對比
               var1.append("user name:" + this.b + "\n");
           var1.append("customer id:" + this.c + "\n");
           var1.append("product id:" + j.a(this.d) + "\n");
           var1.append("license type:" + i.a(this.e) + "\n");
           var1.append("major version:" + this.f + "\n");
           var1.append("minor version:" + this.g + "\n");
           var1.append("generationDate:" + this.h + "\n");
           var1.append("expirationDate:" + this.i + "\n");
       */

       // licTypeId是註冊碼類型的Id,prodId是產品類型的Id
           /**
           * lic[0] = licTypeId << 4 + (prodId & 0xFF);
       */

            var1.b = var2;
            var1.d = var0[0] & 15;
            var1.e = var0[0] >> 4;
            var1.g = var0[1] >> 4;

        //這裏得到generationDate信息,也就是now了,以毫秒錶示,並帶符號右移16位,然後按4字節存儲
            /**
            * long now = System.currentTimeMillis() >> 16;
            * lic[2] = now & 0xFF;
            * lic[3] = now >> 8 & 0xFF;
            * lic[4] = now >> 16 & 0xFF;
            * lic[5] = now >> 24 & 0xFF;
            */

            long var4 = ((long)var0[2] & 255L) + (((long)var0[3] & 255L) << 8) + (((long)var0[4] & 255L) << 16) + (((long)var0[5] & 255L) << 24) << 16;
            var1.h = new Date(var4);

            /*
            * 注意到上面有句 :var1.g = var0[1] >> 4;
            * 結合下面的代碼,我們有:
            * lic[1] = minorVer >> 4 + (majorVer & 0xFF);
            */

            var1.f = var0[1] & 15;

            /**
            * lic[6] = delta & 0xFF;
            * lic[7] = delta >> 8 & 0xFF;
            */

            int var6 = (var0[6] & 255) + ((var0[7] & 255) << 8);
            if(var6 != 0) {
                //expire date
                var1.i = new Date(var4 + (long)var6 * 24L * 60L * 60L * 1000L);
            }

            //var3:-1 
            //lic[10] = any
            //lic[11] = any
            int var7 = var3 > -1?var3:(var0[10] & 255) + ((var0[11] & 255) << 8);
            if(var6 != 0) {
                var7 = var6;
            }

        //var1.l = maintenanceDueDate ,維護到期時間
            var1.l = new Date(var4 + (long)var7 * 24L * 60L * 60L * 1000L);
        } catch (ArrayIndexOutOfBoundsException var8) {
            throw new a();
        }
    }

得到產品信息和註冊碼類型信息

現在我們基本拿到了註冊碼的字節信息,還差一點,產品信息和註冊碼類型信息這裏處理的是Id,那麼實際信息在哪裏?

還記得我們前面說過一個bb類吧,對它的子類進行分析,可以得到很多類似這樣的代碼:

    static a a(com.intellij.ide.a.r var0, q var1) {
        return new a(var0, "AppCode", 8, Products.APPCODE, 3, 30, "6", var1, new g() {
            public h b(String var1, String var2) throws com.intellij.a.g.a {
                return com.intellij.a.g.b.a(var1, var2);
            }
        });
    }

看來jetBrains的很多產品的加密算法都是差不多,真是比較懶啊.

通過分析bb類所在的包com.intellij.ide.a.a,以及com.intellij.a.g包,我們得到如下有用信息:
這裏寫圖片描述
以及:
這裏寫圖片描述


寫出註冊碼生成算法

  • 關鍵字節數組,14字節長:

這裏寫圖片描述

  • CRC校驗:

package rsa;

import java.util.zip.CRC32;

public class GroupUtil {
    public static final int a = 12;
    public static final int b = 14;
    public GroupUtil() {
    }

    public static short computeCRC32(String userName, int customerId, byte[] licBytes) {
        CRC32 crc32 = new CRC32();
        if(userName != null) {
            for(int i = 0; i < userName.length(); ++i) {
                char var5 = userName.charAt(i);
                crc32.update(var5);
            }
        }

        crc32.update(customerId);
        crc32.update(customerId >> 8);
        crc32.update(customerId >> 16);
        crc32.update(customerId >> 24);

        for(int i = 0; i < licBytes.length - 2; i++) {
            byte var6 = licBytes[i];
            crc32.update(var6);
        }

        return (short)((int)crc32.getValue());
    }
  • RSA加密,可直接拷貝反編譯代碼
  • 註冊碼生成
    這裏寫圖片描述

測試註冊碼

目前的問題是,我們沒有私鑰,怎麼搞?
可以隨意生成一對RSA的公鑰/私鑰,然後用我們生成的公鑰的n,替代class文件中的n,這裏的nmodulus.

看看clion.jar中的com.intellij.a.g.c.class的信息:
這裏寫圖片描述

注意右邊ASCII字符的部分,上文有提到這個類的n,我們可以替換之。

怎麼替換呢?
方法很多,jar、zip、sed,編程方式(javassist、asm、zip)都可以搞定,這個留給讀者吧:]


最終效果:

這裏寫圖片描述

clion_crack_succ

發佈了44 篇原創文章 · 獲贊 12 · 訪問量 12萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章