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