Skip to content

标题: Charles 4.5.1逆向工程(2)

创建: 2019-10-24 17:14 更新: 2019-10-25 17:50 链接: https://scz.617.cn/misc/201910241714.txt https://www.52pojie.cn/thread-1042815-1-1.html

续前文:

《Charles 4.5.1逆向工程》 https://scz.617.cn/misc/201910170937.txt https://www.52pojie.cn/thread-1039171-1-1.html

上次通过定位Register按钮处理代码定位与License强相关的YQUd.class,后来看到 另一种定位方案,参看:

《如何逆向破解抓包工具Charles》 https://blog.csdn.net/Kaitiren/article/details/83182108

他的思路是在JD-GUI中搜"This is a 30 day trial version",定位:

com.xk72.charles.gui.SplashWindow.class


import com.xk72.charles.YQUd;

public class SplashWindow extends JWindow { public void showSharewareStatus () { this.showStatus( "This is a 30 day trial version. If you continue using Charles you must\npurchase a license. Please see the Help menu for details." ); }

public void showRegistrationStatus ()
{
    /*
     * 为true时表示已注册
     */
    if ( YQUd.tEdg() )
    {
        /*
         * 形如"Anyting - Site License"
         */
        this.showStatus( "Registered to: " + YQUd.NCuT() );
    }
    else
    {
        this.showSharewareStatus();
    }
}

他这个思路比我的好,更简捷,不需要动态调试,不需要定位PeRA.class,直接就找 到YQUd.class。不过你可能会碰上一个问题,某些版本JD-GUI不能正确反编译 SplashWindow.class,搜索字符串失败。

暴破比较省事,高阶选手在可能的情况下提供keygen。我20岁的时候对netguy师兄佩 服得五体投地,他就是那种高阶选手。

TEAM MESMERiZE提供了通杀3.x、4.x的keygen,未做混淆,可以清楚地看到如何从注 册名变换到序列号,参看:

https://www.upload.ee/files/9009221/Charles.Keygen-MESMERiZE.rar.html

keygen.jar MD5 47d911ba0cecfd31c3312cbb3a344981 SHA256 973d364b71140dcf0aa2b181f1920f688a63cfe1bcdf776c0a4d463095fb1541

下面是keygen的主要逻辑,从注册名到序列号:

kg_charles_web_proxy_analyzer_v4_2.KG_Charles_Web_Proxy_Analyzer_v4_2.class


public String calculateSerial ( String name ) { / * 这个神密常量并没有直接出现在Charles代码中 / int serialCheckSum = 1418211210; / * 这个函数内置了RC5加密操作 * * RC5_SETUP( 1763497072, 2049034577 ) / int nameCheckSum = calcNameChecksum( name ); serialCheckSum ^= nameCheckSum; long serial = serialCheckSum; serial <<= 32; serial >>>= 32; serial <<= 32; / * 这个常量包含License类型,用网友的话说,是控制授权级别的。后面讲了如 * 何获取这两个常量。 / serial |= 0x1CAD6BC;

int     serialLow   = ( int )( serial & 0xFFFFFFFFFFFFFFFF );
int     serialHigh  = ( int )( serial >>> 32 & 0xFFFFFFFFFFFFFFFF );
int[]   serialEnc   = new int[2];

serialEnc[0]    = serialLow;
serialEnc[1]    = serialHigh;

int[]   serialDec   = new int[2];

RC5 serialDecrypter = new RC5();

serialDecrypter.RC5_SETUP( -334581843, -1259282228 );
/*
 * 注意,这里是解密操作,不是加密操作
 */
serialDecrypter.RC5_DECRYPT( serialEnc, serialDec );

long    serialDecrypted = ( serialDec[1] & 0xFFFFFFFF ) << 32;

serialDecrypted    |= serialDec[0] & 0xFFFFFFFF;

int xorCheckSum = calcXorChecksum( serial );

String  strSerial   = Integer.toHexString( xorCheckSum ) + Long.toHexString( serialDecrypted );

/*
 * 前2个字符是校验和
 */
strSerial   = String.format( "%02X", new Object[] { Integer.valueOf( xorCheckSum ) } ) + String.format( "%016X", new Object[] { Long.valueOf( serialDecrypted ) } );

this.tfSerial.setText( strSerial );
return strSerial;

}

下面是Charles中校验序列号的相关代码:

Y:\自己的技术文档\misc\破解Charles\4.5.1\YQUd_scz.java

com.xk72.charles.YQUd.class


public final class YQUd { / * 返回错误提示,用于throw new LicenseException() / private String tEdg ( int var1 ) { / * RC5_SETUP( 1763497072, 2049034577 ); / this.Rarr( 8800536498351690864L );

    try
    {
        String  var5;
        /*
         * dbSi[]中存放经RC5处理过的错误提示,反静态分析
         */
        byte[]  var2    = new byte[(var5 = dbSi[var1]).length() / 2];

        /*
         * 将hexstr转成byte[]
         */
        for ( int var3 = 0; var3 < var2.length; ++var3 )
        {
            var2[var3]  = ( byte )Integer.parseInt( var5.substring( var3 << 1, ( var3 << 1 ) + 2 ), 16 );
        }

        byte[]  var6;

        /*
         * RC5_EncryptArray()
         *
         * 注意,这里是加密操作,不是解密操作
         */
        for( var1 = ( var6 = this.NCuT( var2 ) ).length; var6[var1-1] == 0; --var1 )
        {
            ;
        }
        return new String( var6, 0, var1, "UTF-8" );
    }
    catch ( UnsupportedEncodingException var4 )
    {
        return "";
    }
}

/*
 * 检查序列号的第二步。之所以搞得这么复杂,完全是为了对抗静态分析。
 */
private boolean tEdg ( long var1 )
{
    /*
     * calcXorChecksum
     */
    int var3    = TryJ( var1 );
    /*
     * RC5_SETUP(),密钥与明文是同一组
     */
    this.Rarr( var1 );

    long    var4    = var1;

    for ( int var6 = 0; var6 < var3 + 35; ++var6 )
    {
        /*
         * long RC5_ENCRYPT ( long )
         */
        var4    = this.NCuT( var4 );
    }
    /*
     * 0x520AAC2983719004
     * 0x520AAC29 0x83719004
     * 1376431145 2205257732
     */
    return var4 == 5911726755176091652L;
}

/*
 * 检查序列号的第一步
 */
private long TryJ ( String name_var1, String serial_var2, int type_var3 )
{
    /*
     * 序列号18个字符
     */
    if ( serial_var2.length() != 18 )
    {
        /*
         * The license key is not the correct length. Please check your license key and try again.
         */
        throw new LicenseException( this.tEdg( 0 ) );
    }
    /*
     * 检查序列号黑名单,这些应该是曾经在Internet上贴出来过的
     */
    else if ( !serial_var2.equalsIgnoreCase( "7055ce2f8cb4f9405f" ) ... )
    {
        /*
         * 将序列号切割成3部分,2+8+8
         */
        long    serialDec_var4      = Long.parseLong(serial_var2.substring(2, 10), 16) << 32 | Long.parseLong(serial_var2.substring(10, 18), 16);
        int     xorCheckSum_var12   = Integer.parseInt(serial_var2.substring(0, 2), 16);
        /*
         * 0xB4F0E0CCEC0EAFAD
         * -1259282228 -334581843
         *
         * RC5_SETUP( -334581843, -1259282228 );
         */
        this.Rarr( -5408575981733630035L );
        long    serialEnc_var7;
         /*
          * calcXorChecksum( serial );
          * RC5_ENCRYPT( serialDec, serialEnc );
          *
          * 注意,这里是加密操作,不是解密操作
          *
          * 检查序列号校验和
          */
        if ( TryJ( serialEnc_var7 = this.NCuT( serialDec_var4 ) ) != xorCheckSum_var12 )
        {
            /*
             * 抛出异常,显示错误提示
             */
            throw new LicenseException( this.tEdg( 1 ) );
        }
        else
        {
            /*
             * 处理后的序列号的低32位包含License类型,常量0x1CAD6BC会
             * 出现在这个位置。之前这个地方看错了,少看了一组逻辑右移
             * (>>>),错写成0x1CAD6BC未被检查。
             */
            this.RjRQ   = ( int )( serialEnc_var7 << 32 >>> 32 >>> 24 );
            if ( this.RjRQ == 1 )
            {
                this.aqrV   = LicenseType.tEdg;
            }
            else
            {
                if ( this.RjRQ != type_var3 )
                {
                    if ( this.RjRQ < type_var3 )
                    {
                        /*
                         * 抛出异常,显示错误提示
                         */
                        throw new LicenseException( this.tEdg( 3 ) );
                    }
                    /*
                     * 抛出异常,显示错误提示
                     */
                    throw new LicenseException( this.tEdg( 1 ) );
                }
                /*
                 * 处理后的序列号的低32位包含License类型。
                 */
                switch( ( int )( serialEnc_var7 << 32 >>> 32 >>> 16 & 255L ) )
                {
                case 1:
                    this.aqrV   = LicenseType.tEdg;
                    break;
                case 2:
                    this.aqrV   = LicenseType.TryJ;
                    break;
                case 3:
                    this.aqrV   = LicenseType.NCuT;
                    break;
                default:
                    /*
                     * 抛出异常,显示错误提示
                     */
                    throw new LicenseException( this.tEdg( 1 ) );
                }
            }
            /*
             * RC5_SETUP( 1763497072, 2049034577 );
             */
            this.Rarr( 8800536498351690864L );
            try
            {
                byte[]  var10   = name_var1.getBytes( "UTF-8" );
                if ( ( type_var3 = ( var12 = var10.length ) + 4 ) % 8 != 0 )
                {
                    type_var3  += 8 - type_var3 % 8;
                }
                byte[]  var14   = new byte[type_var3];
                System.arraycopy( var10, 0, var14, 4, var12 );
                var14[0]    = (byte)(var12 >> 24);
                var14[1]    = (byte)(var12 >> 16);
                var14[2]    = (byte)(var12 >> 8);
                var14[3]    = (byte)var12;
                /*
                 * RC5_EncryptArray();
                 */
                byte[]  var6    = this.NCuT( var14 );
                int     var11   = 0;
                byte[]  var13   = var6;
                type_var3   = var6.length;
                /*
                 * calcNameChecksum( name )
                 */
                for( int var15 = 0; var15 < type_var3; ++var15 )
                {
                    byte    var5 = var13[var15];
                    /*
                     * nameCheckSum
                     */
                    var11   = ( var11 ^= var5 ) << 3 | var11 >>> 29;
                }
                /*
                 * XOR后应该得到"1418211210(0x54882F8A)"
                 */
                var11  ^= ( int )( serialEnc_var7 >> 32 );
                /*
                 * 0xA58D19C600000000
                 * 0xA58D19C654882F8A
                 *
                 * 接下来会转到"检查序列号的第二步"
                 */
                return -6517524747541020672L | ( long )var11 << 32 >>> 32;
            }
            catch ( UnsupportedEncodingException var9 )
            {
                return -1L;
            }
        }
    }
    else
    {
        throw new LicenseException( this.tEdg( 1 ) );
    }
}

/*
 * int calcXorChecksum ( long l )
 */
private static final int TryJ ( long var0 )

/*
 * RC5_EncryptArray(),进行RC5加密
 */
private byte[] NCuT ( byte[] var1 )

/*
 * RC5_ENCRYPT(),进行RC5加密
 *
 * 通过这个函数可以看出r=12
 */
private long NCuT ( long var1 )

/*
 * RC5_SETUP(),初始化对称密钥(8字节)
 *
 * Charles用的是RC5-32/12/8
 */
private void Rarr ( long var1 )

static
{
    /*
     * 经RC5处理过的错误提示
     */
    dbSi    = new String[]{ "b241993e8a1...", ... };
    /*
     * RC5-32/r/b的特征常量P(0xB7E15163),出现在RC5_SETUP()中
     *
     * https://en.wikipedia.org/wiki/RC5
     */
    xgCJ    = -1209970333;
}

TEAM MESMERiZE这伙人很厉害。calculateSerial()中出现的"1418211210"并未直接 出现在Charles代码中,单靠静态分析不可能得到这个神密常量。有两种可能,一种 是他们进行了动态调试,确实可以看到"1418211210";另一种是早期版本中出现过 "1418211210"。另一个常量0x1CAD6BC,我之前看错了YQUd.class,在某处少看了一 组逻辑右移(>>>),犯了低级错误,在注释中错写成校验序列号时0x1CAD6BC未被检查, 后来微博上有网友指正了该处错误。

为了得到"1418211210",简单地动态调试就可以,因为它与另一固定常量凑一起后出 现在"检查序列号的第二步",检查相应tEdg()的形参,其低32位就是。为了得到 0x1CAD6BC,可以拦截对应calcXorChecksum()的那个TryJ(),形参的低32位就是。这 个调试过程的前提是你有一对现成的"name:serial"。

keygen中的RC5.class值得保存,没有混淆,实际就是Java源码。

Charles用的是RC5-32/12/8,有个现成的Java实现可供参考:

https://hewgill.com/rc5/rc5java.zip

最常见的RC5配置是:

w=32 r=12 b=16 P(32)=0xb7e15163 (-0x481eae9d/-1209970333) Q(32)=0x9e3779b9 (-0x61c88647)

在RC5的具体实现中,有两种实现方式,一种是使用P、Q动态生成一张表,另一种是 在代码中直接嵌入这张表。对于RC5-32/r/b来说,搜索P(0xB7E15163),不要搜 Q(0x9E3779B9)。一是Q有可能以负值形式出现,二是从实现上讲有两种实现方式, stab[t]可能是预生成的,此时有P无Q,搜P可以保证命中。

这是我第二次碰上RC5,第一次碰上还是某次帮组织反APT时,一个极其强大的对手被 我方一个极其聪明的小伙子抓住了一丝尾巴,甚至都谈不上对手犯错,总归是极具传 奇色彩地还原了经RC5加密过的数据,待我临死前再来吹这个牛,如果那时我没得阿 尔茨海默症的话。

有了这个keygen,估计很长一段时间都不再需要暴破。早知道有这么个玩意儿,我剁 个毛线啊。在所谓的1024节,我们必须讨论一下程序,对吧。

2019-10-25 11:51 scz

这篇看的人很少,远不如《新版burp-loader-keygen-2.jar》。以前看一个搞Java逆 向的老外的blog,他当时说过类似的事;他有一篇讲技术原理的贴,浏览量很低,但 他觉得这种贴子自有其存在的意义,才是互联网上追寻技术的人们真正想看的内容; 深以为然。

分析别人的keygen是一种学习途径,可以在别人的基础上继续深入,比如我剁Burp时 就先分析surferxyz的keygen。有人会问,既然有现成的keygen,你这篇文章有什么 意义?我是这么想的,万一哪天注册算法有变,大概率不会变框架结构,很可能只变 几个常量,如果我了解旧版keygen的搞法,就很容易弄出新版keygen。另一方面,在 混淆过的代码中看出逻辑结构,本身也是一次逆向工程训练机会,你的功力就是在一 次次这种训练中增长的。

我在分享时,第一考虑自己的归档查找,第二考虑对他人的帮助程度,尽量避免装X 式分享。什么叫装X式分享?每个人心中有杆秤。我的原则是,尽可能剔除纯秀内容, 如果某项技术我不想分享,我就不提,当然,我也建议你别来问我,否则可能大家都 比较难堪。在分享内容中尽可能换位思考,顾及阅读者的收益,这倒不是我有多雷锋, 仅仅是从小养成的习惯,喜欢那种真诚的交流。通过这种真诚的分享,可以结交更多 的同道中人,可以共同进步,这不是套话,这是实际情况。这种分享,我已经坚持了 22年。

有人调侃我用TXT写blog。二十多年前我们上TERM版的BBS,一个75列回车换行的TXT 能最不失真地在BBS之间传播,在转载或者发回邮箱时不会丢失信息。一篇图文并茂 的技术文章固然有其优势,但与我个人而言,纯TXT足矣。