因为一些原因我阴差阳错的将 7.9.2 认为是最新版,于是我最开始的破解都是基于 7.9.2 制作的。 直接在我的主力机部署一份 Artifactory 用于测试和实验。看到使用的是 SpringFramework 并且看上去似乎没有什么混淆,大概破解起来不会太费劲。 事实上确实不费劲。
我准备先输入一个错误的许可证看看有没有什么报错、输出,幸运的是,他有!多亏了 SpringFramework 的精美报错,让我立刻定位到了报错的位置 ArtifactoryLicenseVerifier
2023-06-21T14:34:17.254Z [jfrt ] [ERROR] [28c05688836a496f] [ArtifactoryLicenseVerifier:120] [http-nio-8081-exec-8] - Failed to decrypt license: last block incomplete in decrypti接下来就是找到这个类,也很简单,一番搜索找到了 tomcat 和 artifactory.war 的位置,立刻用 jdgui 打开 artifactory.war 搜索这个类。 有些时候实在是找不到也可以写一个简单的 JavaAgent 然后输出这个类的位置即可,下面是我自用的小工具源码,最好不要用 Kotlin 写,因为可能会出现莫名的问题。 比如我就遇到了到死也没有任何类被 ClassFileTransformer 处理的怪问题。到处都 runCatching 也没发现任何异常,很是奇怪。因此最好用 Java 写。
public static void premain(String args, Instrumentation ins) { if (args != null) { System.out.println("Lama Javaagent Test App :: is now LOADED with class filter: " + args); } else { System.out.println("Lama Javaagent Test App :: is now LOADED"); } ins.addTransformer(new ClassFileTransformer() {
@Override public byte[] transform(Module module, ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException { String moduleName = "<null>"; if (module != null) { if (module.getName() != null) { moduleName = module.getName(); } else { moduleName = "<No Name>"; } } if (predict(className)) { System.out.println("Lama Javaagent Test App :: Module Transformation Triggered: className=" + className + ", module.getName=" + moduleName); } return classfileBuffer; }
public boolean predict(String className) { if (args == null) { return true; } return Objects.equals(className.replace("/", "."), args); } });}不要忘记在 build.gralde 里同时加入:
tasks.withType<Jar> { manifest { attributes["Premain-Class"] = "icu.lama.AgentMain" attributes["Can-Redefine-Classes"] = true // 一定别忘了这个 attributes["Can-Retransform-Classes"] = true // 一定别忘了这个 }}一番查找后,最终定位到 artifactory-addons-manager-7.9.2.jar 里的 org.artifactory.addon.ArtifactoryLicenseVerifier。
幸运的是,整个文件内只有一处调用了 Logger#error,直接锁定目标方法是 ArtifactoryLicenseVerifier#a(Ljava/lang/String)Lorg.jfrog.license.api.License
private static a d = new a();
private static License a(String paramString) { try { return d.a(paramString); } catch (LicenseException licenseException) { a.error(licenseException.getMessage()); b.debug(licenseException.getMessage(), (Throwable)licenseException); return null; }}这时候如果单看导入会发现他同时引入了两个 a,这时直接用 jdgui 跟一下类就行,如果出现了跟不了的情况,也可以通过分析两个类来进行确定具体是哪一个。
在这里很显然 org.jfrog.license.api.a 才是我们需要的。查看一下 org.jfrog.license.a.a 可以发现他是一个独立的类,看到内部有许多的位运算,再加上调用的地方均都是 String 类型变量
因此猜测这个类可能是解密字符串一类的东西,直接拿出来在jshell里运行一下
(new a(new long[] { 2731559786009819271L, -1294798864979958372L, 493539018728922049L, -7676131744833282422L, 5840148757141871421L, -9166198372908531643L })).toString()得到 ArtifactoryLicenseVerifier
查看库内有许多的类似调用,即 (new a(new long[] { 许多神秘数字... })).toString(),断定该类是用于字符串解密的类。
回到正题,查看 org.jfrog.license.api.a#a(Ljava/lang/String)Lorg.jfrog.license.api.License
public License a(String paramString) throws LicenseException { try { return b(paramString, c); } catch (LicenseException licenseException) { try { return c(paramString, b); } catch (LicenseException licenseException1) { return a(paramString, licenseException1); } }}由于是有异常抛出,而且两个 catch 都是 LicenseException,猜测 b 和 c 都是用于解密 License 的方法,且 c 有可能是 b 的 Fallback,那么为何不从 b 入手呢。
private License b(String paramString1, String paramString2) throws LicenseException { License license = (new org.jfrog.license.multiplatform.a()).a(paramString1, e(paramString2)); HashMap<Object, Object> hashMap = new HashMap<>(); for (Map.Entry entry : license.getProducts().entrySet()) hashMap.put(entry.getKey(), ((SignedProduct)entry.getValue()).parseProduct()); License license1 = new License(); license1.setValidateOnline(license.getValidateOnline()); license1.setProducts((Map)hashMap); license1.setVersion(license.getVersion()); return license1;}可以看到第一行调用了 org.jfrog.license.multiplatform.a#a 来生成一个 License 对象,那么这个应该是真正的解密 License 的地方,那么就来看看参数吧。
这个方法接受两个参数,一个是 String,另一个是 PublicKey,那么可以很自然的推测出 e(paramString2) 是用来从 paramString2 即 c 生成公钥对象用的。
对 c 进行解密发现是一串 Base64,根据方法 e 可以判断出这个公钥是 RSA 格式的。
接下来深入看 org.jfrog.license.multiplatform.a#a,首先是对输入的 License 内容进行 Base64 解密。
然后使用 this.a.<SignedLicense>a(arrayOfByte, SignedLicense.class) 将解密结果转成了 SignedLicense。
这就很显然了,这个 100% 是某个库的反序列化代码,如果跟一下会发现其实就是使用 SnakeYAML 进行反序列化。
同一个类中的另一个方法正是序列化,因此为我省了不少事,只要我可以创建一个合法的 SignedLicense,调用这个代码即可完成序列化。
public License a(String paramString, PublicKey paramPublicKey) throws LicenseException { byte[] arrayOfByte = Base64.decodeBase64(paramString); try { SignedLicense signedLicense = this.a.<SignedLicense>a(arrayOfByte, SignedLicense.class); signedLicense.verify(paramPublicKey, this.b); arrayOfByte = Base64.decodeBase64(signedLicense.getLicense().getBytes()); return b(paramPublicKey, arrayOfByte); } catch (SignatureException|java.security.InvalidKeyException|LicenseException signatureException) { return a(paramPublicKey, arrayOfByte); }}下一行使用了 signedLicense.verify(paramPublicKey, this.b); 很显然这是在验证许可证是否有效,this.b 是 Signature,也就是 Java 中对内容创建签名用的类。
结合前面的发现,签名算法应该是 RSA。签名如果验证失败了会抛出异常,那么我们假设验证成功,跟一下下一行,也就是另一个 Base64 解密。
private License b(PublicKey paramPublicKey, byte[] paramArrayOfbyte) throws LicenseException { License license = this.a.<License>a(paramArrayOfbyte, License.class); if (license.getProducts() == null || license.getProducts().size() == 0) throw new LicenseException("Failed to verify license, no products"); try { for (SignedProduct signedProduct : license.getProducts().values()) signedProduct.verify(paramPublicKey, this.b); } catch (SignatureException|java.security.InvalidKeyException signatureException) { throw new LicenseException("Failed to verify license", signatureException); } return license;}其实已经很显然了,将解密出来的 License 又调用了一次一摸一样的反序列化方法,然后就是对真正的 License 内所有 SignedProduct 进行公钥签名验证。
最关键的是 全程都在使用同一个公钥,也就是 org.jfrog.license.api.a#a.c。我们的破解思路已经很明确了:
幸运的是,提及的所有类均都包含了创建 License 所需要的方法,于是编写了下面的 Keygen。
val private = KeyFactory.getInstance("RSA", BCProviderFactory.getProvider()).generatePrivate(PKCS8EncodedKeySpec(Base64.getDecoder().decode(Constants.PRIVATE_KEY)))val sign = Signature.getInstance("SHA256withRSA", BCProviderFactory.getProvider())
val products = mutableMapOf<String, SignedProduct>()
while (true) { val product = Product() product.id = prompt("Enter product id(artifactory): ", "artifactory") product.expires = date(2099, 12, 31) product.isTrial = false product.owner = prompt("Owner(lamadaemon): ", "lamadaemon") product.validFrom = Date() product.type = Product.Type.ENTERPRISE_PLUS products += product.id to SignedProduct(product, private, sign) // 使用私钥对 Product 进行签名获得 SignedProduct if (prompt("\nDo you want add more products into this license(yes/no, default=no): ", listOf("yes", "no"), "no") == "no") { break }}
val license = License()license.version = 2license.validateOnline = falselicense.products = products
val sLicense = SignedLicense(license, private, sign) // 使用私钥对 License 进行签名 获得 SignedLicense
println(createFinalLicense(sLicense)) // 这里就是戳其进行序列化 + 转换 Base64,详情可见我的 Github,有完整的 Keygen 代码至此,我们已经成功使用我们自己的私钥创建了许可证,直接丢上去用肯定是不行的,所以我们需要想办法替换掉用于验证的公钥。 由于我们已经知道公钥的位置了,而且全程都在使用这个公钥,没有第二个地方了,因此编写一个 Javaagent 替换掉公钥即可。
这时,悲剧发生了,我在 Keygen 里用 Kotlin 写了这个 agent,然后我 debug 了整整两天!整整两天!! 最终用 Java 重写了一下,代码逻辑一模一样,他就可以正常工作。 这也就是为什么我不建议使用 Kotlin 编写 JavaAgent,是真的会遇到莫名其妙的问题。
在这里,我使用我比较熟悉的 javassist
package icu.lama.artifactory.agent.patches;
import javassist.CtClass;import javassist.CtField;import javassist.Modifier;import javassist.NotFoundException;import org.jetbrains.annotations.Nullable;
import java.util.Arrays;
public class PatcherLicenseParser extends ClassPatch { // 这是我的工具类,详情可以看我的 Github,有完整的代码 public PatcherLicenseParser() { super("org.jfrog.license.api.LicenseParser"); // 类名不一样因为有 Rename 机制,便于反混淆,实际因该是 "org.jfrog.license.api.a" } // 在这里只是我人为的将其重命名为 LicenseParser 实际上他混淆前名字应该是 LicenseManager,后面会说我怎么知道的
@Override byte[] onTransform(String className, CtClass clazz, byte[] classBuf) throws Throwable {
var publicKeyFieldC = clazz.getDeclaredField("c"); // 7.9.2 using c publicKeyFieldC.setModifiers(Modifier.PRIVATE + Modifier.STATIC); var override = "c = icu.lama.artifactory.agent.AgentMain.PUBLIC_KEY;";
// 关于这里为什么不用 getMethod,经过我实测似乎 getMethod 无法获取到 <clinit> var clinitMethod = Arrays.stream(clazz.getDeclaredBehaviors()).filter((it) -> "<clinit>".equals(it.getMethodInfo().getName())).findAny(); if (!clinitMethod.isPresent()) { throw new Throwable("Corrupted class!"); }
clinitMethod.get().insertAfter(overrids);
clazz.detach(); return clazz.toBytecode(); }}首先我先将公钥变量的 FINAL 修饰删去,这样就可以对其进行修改,然后在 <clinit>方法,也就是 static {},的末尾加入一行覆盖公钥的代码。
事实上我在重写之前为了测试方案可行性,先手动用 recaf 修改了
org.jfrog.license.api.a#a(Ljava/lang/String;)Lorg/jfrog/license/api/License的字节码,因为这里是唯一引用公钥的地方。
EX_START_1:LINE EX_START_1 75ALOAD thisALOAD 1GETSTATIC org/jfrog/license/api/a.c Ljava/lang/String; // 懒狗做法:将这个修改为 LDC "我的公钥内容"// 这时候的 Stack 应该是// 0: this// 1: 输入的 License// 2: 公钥INVOKEVIRTUAL org/jfrog/license/api/a.b(Ljava/lang/String;Ljava/lang/String;)Lorg/jfrog/license/api/License;EX_END_1:ARETURN修复 Javaagent 问题后成功的跑了起来,破解一切都正常,刚在群里宣布完破解结果,准备在服务器上部署的时候,突然发现,我下载的竟然不是最新版!
我是一直从 JFrog release-docker 下载的本体,但这里已经一万年没有更新过了。 还停留在
7.9.2,真正的最新版已经到了7.59.11
更新到 7.59.11 后果然我的破解不好使了,同样的思路,先输入错误的证书,看一下报错一模一样,再次定位到这个类,然后查看。 原来是 "c" 变成了 "d",对 agent 稍作修改
@Override byte[] onTransform(String className, CtClass clazz, byte[] classBuf) throws Throwable { var overrides = "";
var publicKeyFieldC = tryGetDeclaredField(clazz, "c"); // 7.9.2 using c if (publicKeyFieldC != null) { publicKeyFieldC.setModifiers(Modifier.PRIVATE + Modifier.STATIC); overrides += "c = icu.lama.artifactory.agent.AgentMain.PUBLIC_KEY;"; }
var publicKeyFieldD = tryGetDeclaredField(clazz, "d"); // 7.59 changed to d if (publicKeyFieldD != null) { publicKeyFieldD.setModifiers(Modifier.PRIVATE + Modifier.STATIC); overrides += "d = icu.lama.artifactory.agent.AgentMain.PUBLIC_KEY;"; }
var clinitMethod = Arrays.stream(clazz.getDeclaredBehaviors()).filter((it) -> "<clinit>".equals(it.getMethodInfo().getName())).findAny(); if (!clinitMethod.isPresent()) { throw new Throwable("Corrupted class!"); }
clinitMethod.get().insertAfter(overrides); clazz.detach(); return clazz.toBytecode();}
private @Nullable CtField tryGetDeclaredField(CtClass clazz, String field) { try { return clazz.getDeclaredField(field); } catch (NotFoundException e) { return null; }
}直接进行测试,发现竟然还是不行,报错位置变成了 LicenseManager,同样的方法找到这个类,在 license-manager 这个 jar 包内,打开一看立即就给了我一些似曾相识的感觉。
// 部分代码private static final String publicKey = (new ObfuscatedString(new long[] { <省略> })).toString();
private static final String jfrogPublicKey = (new ObfuscatedString(new long[] { <省略> })).toString();
private static final String jfrogPublicKeyTest = (new ObfuscatedString(new long[] { <省略> })).toString();
private static final String publicKeyTest = (new ObfuscatedString(new long[] { <省略> })).toString();
public License loadLicense(String licenseKey) throws LicenseException { try { return loadJFrogLicense(licenseKey, jfrogPublicKey); } catch (LicenseException e) { try { return loadLegacyLicense(licenseKey, publicKey); } catch (LicenseException e1) { return loadTestKey(licenseKey, e1); } }}对比一下 artifactory-addons-manager 也就是之前注入的类的 jar 包名字
private static final String b;
private static final String c;
private static final String d;
private static final String e;
static { b = (new org.jfrog.license.a.a(new long[] { <省略> })).toString(); c = (new org.jfrog.license.a.a(new long[] { <省略> })).toString(); d = (new org.jfrog.license.a.a(new long[] { <省略> })).toString(); e = (new org.jfrog.license.a.a(new long[] { <省略> })).toString();}
public License a(String paramString) throws LicenseException { try { return b(paramString, c); } catch (LicenseException licenseException) { try { return c(paramString, b); } catch (LicenseException licenseException1) { return a(paramString, licenseException1); } }}不能说很像,只能说一模一样! 这不就是解密版本的 artifactory-addons-manage r吗?!
无脑添加最终的破解点,又对 agent 进行小小的修正,报错消失了,当然,License也成功的被识别了,也成功的激活了 Artifactory
@Override byte[] onTransform(String className, CtClass clazz, byte[] classBuf) throws Throwable { var overrides = "";
var publicKeyFieldC = tryGetDeclaredField(clazz, "c"); // 7.9.2 using c if (publicKeyFieldC != null) { publicKeyFieldC.setModifiers(Modifier.PRIVATE + Modifier.STATIC); overrides += "c = icu.lama.artifactory.agent.AgentMain.PUBLIC_KEY;"; }
var publicKeyFieldD = tryGetDeclaredField(clazz, "d"); // 7.59 changed to d if (publicKeyFieldD != null) { publicKeyFieldD.setModifiers(Modifier.PRIVATE + Modifier.STATIC); overrides += "d = icu.lama.artifactory.agent.AgentMain.PUBLIC_KEY;"; }
var publicKeyFieldNObf = tryGetDeclaredField(clazz, "jfrogPublicKey"); // 7.59 in license-manager-7.63.3.jar, no obfuscation version of artifactory-addons-manager if (publicKeyFieldNObf != null) { publicKeyFieldNObf.setModifiers(Modifier.PRIVATE + Modifier.STATIC); overrides += "jfrogPublicKey = icu.lama.artifactory.agent.AgentMain.PUBLIC_KEY;"; }
var clinitMethod = Arrays.stream(clazz.getDeclaredBehaviors()).filter((it) -> "<clinit>".equals(it.getMethodInfo().getName())).findAny(); if (!clinitMethod.isPresent()) { throw new Throwable("Corrupted class!"); } clinitMethod.get().insertAfter(overrides);
clazz.detach(); return clazz.toBytecode();}至此,破解已经完成,后续的就是对 Keygen 和 Agent 进行功能上的完善,所有的代码已经发布在 Github 上,如果遇到新版本无法使用,请提交 Issue 或者在 Telegram 上向我寻求帮助
项目 Github: Lama3L9R/ArtifactoryKeygen 内含 Keygen 和 Agent 的完整代码