标题: 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足矣。