S0cke3t
文章43
标签29
分类12
Xcology9 license analysis & crack

Xcology9 license analysis & crack

本文只做技术分析交流使用,请勿用于非法用途

前言

前几日,在我百无聊赖时。某位大佬突然v我说之前某微OA的license过期了,我一看 哦豁 真提示过期了。所以为了能让大佬愉快的挖洞,看看能不能把license过掉。所以就…

授权分析

首先我们通过该OA的路由配置确定了处理license的相关jar包,该包位于\WEB-INF\lib\ljstln.jar。其中LNparse为验证license的主要实现类,LN为验证license的入口文件,其中包含LN具体实例化以及获取相关属性值。

我们将其相关的依赖包拷贝出来并创建IDEA项目,分析具体验证流程。
先来看LN

首先定义license相关字段值,并在构造函数中进行字段的初始化

最后就是getter/setter,其中包含的函数很多,我们先看校验入口函数InLicense

函数根据message值判断是否要导入和更新license,跟进CkLicense

首先方法传入一个currentdate参数,其值为当前日期,随后获取程序license目录下.license文件路径,并将其传入ReadFromFile,跟进此方法

该方法首先实例化一个LNParse对象,随后尝试从缓存中读取license信息,如果缓存中没有相关信息,则调用LNParsegetLNBean进行license文件的解析,跟进。


getLNBean是验证license的主要实现,方法首先从.license文件中取出相应文件的内容

******.license其实是一个压缩包,其中包含4个文件
publicKey 解密DES key的公钥
licenseEncryptKey 私钥加密的DES key
license 加密的新版本license具体内容
license2 加密的兼容旧版本的license内容

获取文件内容后方法根据传入的key值使用不同的公钥,然后把license文件获取的publickey进行base64编码,并将编码后的publickey和对应key的realPublicKey进行对比,如果一致说明该license有效

随后声明JSONObject对象用于存放json格式license内容,并调用RSACoder.decryptByPublicKey解密出licensekey

然后调用DESCoder.decrypt用解密出的licensekey解密出具体的license内容

最后实例化一个LNBean并对相关属性进行赋值。回到LN.CkLicense

获取到license明文信息后,该函数判断当前日志是否大于license中expirdate的值,如果在许可日期内再判断license值,其中license值由this.companyname + this.licensecode + this.software + temphrmnum + this.expiredate + this.concurrentFlag拼接后的字符串调用Util.getEncrypt进行MD5加密得到。

而其中的licensecode则是对MAC地址进行MD5后加密得到,在判断所有值合法后Inlicense函数将license信息进行入库。
至此整个license的验证流程基本清晰了大概流程如下

step 1: 用license文件的公钥解密licensekey
其中licensekey是加密license内容的DES key
step2: 用解密出licensekey 使用DES解密出具体license内容
step3: 判断license和expirdate值是否相等

整个解密流程使用的DES对称和RSA非对称混合解密

流程验证

解密流程了解以后,我们根据相关方法签名,新建一个java文件,并调用getLNBean看一下license具体内容


可以看到根据调用流程可以成功拿到license的明文信息,下一步就是如何破解验证流程。
我们梳理一下解密流程后不难发现要想解密license信息就必须要知道DES key的值,而DES key是由开发商私钥进行加密,在没有私钥的情况下解密DES key是不可能的。所以我们唯一能干预的就是更改加解密的公私钥对,将解密的公钥更改为自己的公钥值,也就是getLNBean中的realPublicKey值,这样就能正确拿到DES key值。也就能正确解密出license的明文信息。
而要更改公钥值有两种方法可以实现,一是反编译license验证类,更改后重新打包。二是使用java agent技术重写realPublicKey值或者重写getLNBean方法。由于license验证类所依赖的包实在太多,反编译起来极为繁琐,所以此方法pass。

破解

首先创建一个java agent并hook ln/LNParse类,并获取其中的getLNBean使用javassist框架重写getLNBean方法,更改其中的realPublicKey值,最后调用detach重新加载此对象。核心代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {  
//System.out.println("premain load Class:" + className);
//return classfileBuffer; if ("ln/LNParse".equals(className)) {
// 从ClassPool获得CtClass对象
final ClassPool classPool = ClassPool.getDefault();
try {
ClassClassPath classPath = new ClassClassPath(this.getClass());
//System.out.println(getRealPath());
//这块可以直接使用EC的classpath
classPool.appendClassPath(".//clib//ljstln.jar");
classPool.appendClassPath(".//clib//commons-codec-1.11.jar");
classPool.appendClassPath(".//clib//json-20090211.jar");
classPool.insertClassPath(classPath);
final CtClass clazz = classPool.get("ln.LNParse");
CtMethod convertToAbbr = clazz.getDeclaredMethod("getLNBean");
String methodBody = "{byte[] publicKey = ln.Zip.getZipSomeByte($1, \"publicKey\");\n" +
" byte[] licenseEncryptKey = ln.Zip.getZipSomeByte($1, \"licenseEncryptKey\");\n" +
" byte[] licenseFile = ln.Zip.getZipSomeByte($1, \"license\");\n" +
" byte[] licenseFile2 = ln.Zip.getZipSomeByte($1, \"license2\");\n" +
" String realPublicKey = \"\";\n" +
" if (\"emessage2\".equals($2)) {\n" +
" realPublicKey = \"MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAIJWRm0eoQNEgZB9aUlM1PoT0N7cKCBCfkecycpeKeg57e73Fcj4ik9uYrGB01t38ut45iHJi8TLoeORYuUAhWUCAwEAAQ==\";\n" +
" } else if (\"ecology9\".equals($2)) {\n" +
" realPublicKey = \"MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAiPitGeOC5t98v3ILcS/BNeFgoaFkVoAgbo163rMkIVpYqdkauBln2vDZflJ+6mQj92G6LFzTMhi5WXgigh71ul6MIoZBa3CNg/1oXPE8p7NHRc9GkH5Y8n3Qm6r4mY6uqF1a4CNahfAi1IENjwWucHmgcSwfurihBirOQAeQX1dIkdAyvfTxUPOAyXb/CVFVhBggryJ7M83wfOk2z87DCgk9ZAre0NdaN/wCGmO6C2rAReGd32FUhwli/WdSfAYZD9bTDJ6Y0n/A2Mh54stToiRDTJm3l4qBQxVSQ+ezN91v4P1CQbrAvu4s+EdIf1TPvvuUX0yyEA8hle/uiVKKFwIDAQAB\";\n" +
" }\n" +
"\n" +
" String publicKeyStr = new String(org.apache.commons.codec.binary.Base64.encodeBase64(publicKey));\n" +
" if (!realPublicKey.equals(publicKeyStr)) {\n" +
" throw new Exception(\"license error!\");\n" +
" } else {\n" +
" org.json.JSONObject jsonLicense;\n" +
" try {\n" +
" byte[] licenseKey = ln.RSACoder.decryptByPublicKey(licenseEncryptKey, publicKey);\n" +
" jsonLicense = new org.json.JSONObject(new String(ln.DESCoder.decrypt(licenseFile, licenseKey)));\n" +
" } catch (java.security.InvalidKeyException var15) {\n" +
" var15.printStackTrace();\n" +
" byte[] licenseInfo2 = ln.DESCoder.decrypt(licenseFile2, $2.getBytes());\n" +
" jsonLicense = new org.json.JSONObject(new String(licenseInfo2));\n" +
" }\n" +
"\n" +
" ln.LNBean lnb = new ln.LNBean();\n" +
" lnb.setCompanyname(jsonLicense.getString(\"companyname\"));\n" +
" lnb.setLicensecode(jsonLicense.getString(\"licensecode\"));\n" +
" lnb.setHrmnum(jsonLicense.getString(\"hrmnum\"));\n" +
" lnb.setExpiredate(jsonLicense.getString(\"expiredate\"));\n" +
" lnb.setConcurrentFlag(jsonLicense.getString(\"concurrentFlag\"));\n" +
" lnb.setLicense(jsonLicense.getString(\"license\"));\n" +
"\n" +
" try {\n" +
" lnb.setCid(jsonLicense.getString(\"cid\"));\n" +
" } catch (Exception var14) {\n" +
" System.out.println(var14);\n" +
" }\n" +
"\n" +
" try {\n" +
" lnb.setScType(jsonLicense.getString(\"scType\"));\n" +
" } catch (Exception var13) {\n" +
" System.out.println(var13);\n" +
" }\n" +
"\n" +
" try {\n" +
" lnb.setScCount(jsonLicense.getString(\"scCount\"));\n" +
" } catch (Exception var12) {\n" +
" System.out.println(var12);\n" +
" }\n" +
"\n" +
" return lnb;\n" +
" }}";
convertToAbbr.setBody(methodBody);
// 返回字节码,并且detachCtClass对象
byte[] byteCode = clazz.toBytecode();
//detach的意思是将内存中曾经被javassist加载过的对象移除,如果下次有需要在内存中找不到会重新走javassist加载
clazz.detach();
System.out.println(public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
//System.out.println("premain load Class:" + className);
//return classfileBuffer; if ("ln/LNParse".equals(className)) {
// 从ClassPool获得CtClass对象
final ClassPool classPool = ClassPool.getDefault();
try {
ClassClassPath classPath = new ClassClassPath(this.getClass());
//System.out.println(getRealPath());
//这块可以直接使用EC的classpath
classPool.appendClassPath(".//clib//ljstln.jar");
classPool.appendClassPath(".//clib//commons-codec-1.11.jar");
classPool.appendClassPath(".//clib//json-20090211.jar");
classPool.insertClassPath(classPath);
final CtClass clazz = classPool.get("ln.LNParse");
CtMethod convertToAbbr = clazz.getDeclaredMethod("getLNBean");
String methodBody = "{byte[] publicKey = ln.Zip.getZipSomeByte($1, \"publicKey\");\n" +
" byte[] licenseEncryptKey = ln.Zip.getZipSomeByte($1, \"licenseEncryptKey\");\n" +
" byte[] licenseFile = ln.Zip.getZipSomeByte($1, \"license\");\n" +
" byte[] licenseFile2 = ln.Zip.getZipSomeByte($1, \"license2\");\n" +
" String realPublicKey = \"\";\n" +
" if (\"emessage2\".equals($2)) {\n" +
" realPublicKey = \"MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAIJWRm0eoQNEgZB9aUlM1PoT0N7cKCBCfkecycpeKeg57e73Fcj4ik9uYrGB01t38ut45iHJi8TLoeORYuUAhWUCAwEAAQ==\";\n" +
" } else if (\"ecology9\".equals($2)) {\n" +
" realPublicKey = \"MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAiPitGeOC5t98v3ILcS/BNeFgoaFkVoAgbo163rMkIVpYqdkauBln2vDZflJ+6mQj92G6LFzTMhi5WXgigh71ul6MIoZBa3CNg/1oXPE8p7NHRc9GkH5Y8n3Qm6r4mY6uqF1a4CNahfAi1IENjwWucHmgcSwfurihBirOQAeQX1dIkdAyvfTxUPOAyXb/CVFVhBggryJ7M83wfOk2z87DCgk9ZAre0NdaN/wCGmO6C2rAReGd32FUhwli/WdSfAYZD9bTDJ6Y0n/A2Mh54stToiRDTJm3l4qBQxVSQ+ezN91v4P1CQbrAvu4s+EdIf1TPvvuUX0yyEA8hle/uiVKKFwIDAQAB\";\n" +
" }\n" +
"\n" +
" String publicKeyStr = new String(org.apache.commons.codec.binary.Base64.encodeBase64(publicKey));\n" +
" if (!realPublicKey.equals(publicKeyStr)) {\n" +
" throw new Exception(\"license error!\");\n" +
" } else {\n" +
" org.json.JSONObject jsonLicense;\n" +
" try {\n" +
" byte[] licenseKey = ln.RSACoder.decryptByPublicKey(licenseEncryptKey, publicKey);\n" +
" jsonLicense = new org.json.JSONObject(new String(ln.DESCoder.decrypt(licenseFile, licenseKey)));\n" +
" } catch (java.security.InvalidKeyException var15) {\n" +
" var15.printStackTrace();\n" +
" byte[] licenseInfo2 = ln.DESCoder.decrypt(licenseFile2, $2.getBytes());\n" +
" jsonLicense = new org.json.JSONObject(new String(licenseInfo2));\n" +
" }\n" +
"\n" +
" ln.LNBean lnb = new ln.LNBean();\n" +
" lnb.setCompanyname(jsonLicense.getString(\"companyname\"));\n" +
" lnb.setLicensecode(jsonLicense.getString(\"licensecode\"));\n" +
" lnb.setHrmnum(jsonLicense.getString(\"hrmnum\"));\n" +
" lnb.setExpiredate(jsonLicense.getString(\"expiredate\"));\n" +
" lnb.setConcurrentFlag(jsonLicense.getString(\"concurrentFlag\"));\n" +
" lnb.setLicense(jsonLicense.getString(\"license\"));\n" +
"\n" +
" try {\n" +
" lnb.setCid(jsonLicense.getString(\"cid\"));\n" +
" } catch (Exception var14) {\n" +
" System.out.println(var14);\n" +
" }\n" +
"\n" +
" try {\n" +
" lnb.setScType(jsonLicense.getString(\"scType\"));\n" +
" } catch (Exception var13) {\n" +
" System.out.println(var13);\n" +
" }\n" +
"\n" +
" try {\n" +
" lnb.setScCount(jsonLicense.getString(\"scCount\"));\n" +
" } catch (Exception var12) {\n" +
" System.out.println(var12);\n" +
" }\n" +
"\n" +
" return lnb;\n" +
" }}";
convertToAbbr.setBody(methodBody);
// 返回字节码,并且detachCtClass对象
byte[] byteCode = clazz.toBytecode();
//detach的意思是将内存中曾经被javassist加载过的对象移除,如果下次有需要在内存中找不到会重新走javassist加载
clazz.detach();
System.out.println("detach success");

使用maven进行打包后,将jar包附加到调试选项里,并生成一个自定义的license文件进行测试


上机测试一下,首先修改Resin的\conf\resin.properties配置文件

加上 -javaagent:Ec-agent.jar
然后将jar包和依赖包放到Resin根目录中

启动服务后导入自定义license

Bingo~ 成品就不发出来了,怕收律师函-_- !

本文作者:S0cke3t
本文链接:https://www.saferoad.cc/Crack/Xcology9-license-analysis-crack.html
版权声明:本文采用 CC BY-NC-SA 3.0 CN 协议进行许可
×