About month ago JetBrains released new version of their popular IDE Intellij Idea v/2016.1.1. just 5 days later at one of russian torrent trackers appears crack for latest version… Seems it didn’t take too much time for chinese hacker “rover12421” to break JetBrains license system. Crack itself is plain jar file so I decided investigate a bit if it’s really so easy to hack Idea. My first assumption was it must be open source and it really is but unfortunately GitHub repo was already closed “due to DMCA takedown”(whatever it is) so I just decompiled crack with Bytecodeviewer (actually just minute ago when I started working on this article I found out in one of comments on Rover blog valid link to other repo on chinese clone of github)
So how does crack work? Let’s start from beginning.
As I mentioned crack itself is jar file which consists of two java classes Agent.java and Util.java with dependencies on GSON and bunch of ASM libraries (all of them asm, asm-analysis, asm-commons, asm-tree, asm-util, asm-xml) + dependency on jetbrains.jar(in Intellij Idea distributive it is part of idea.jar – “c:/Program Files (x86)/JetBrains/IntelliJ IDEA 2016.1.1/lib/idea.jar“) I think jetbrains jar is kind of API and any Jetbrains IDE have their own jar with specific implementation
Rover wrote
The crack code is `com.rover12421.crack.jerbrains.v2.Agent` So the crack code is easy.
Agent class is implemented according to javaagent requirements so that it allows to add crack on IDE classpath like
-javaagent:c:/Program Files (x86)/JetBrains/IntelliJ IDEA 2016.1.1/bin/JetbrainsCrack.jar
For that purpose you should modify vmoptions files: idea64.exe.vmoptions and idea.exe.vmoptions.
Now when you run Idea with idea.but in console you will see log like this
[ecko_toggle style=”solid” state=”closed” title=”console output”]
c:\Program Files (x86)\JetBrains\IntelliJ IDEA 2016.1.1\bin>idea.bat\ 'idea.bat\' is not recognized as an internal or external command, operable program or batch file. c:\Program Files (x86)\JetBrains\IntelliJ IDEA 2016.1.1\bin>idea.bat Java HotSpot(TM) Server VM warning: ignoring option MaxPermSize=250m; support was removed in 8.0 ************************************** * Jerbrains v2.5.3 Crack * * * * rover12421@163.com * * http://www.rover12421.com * * QQ Group: 126896013 * * https://plus.google.com/+LostKai * * * ************************************** Apr 27, 2016 5:47:35 PM java.util.prefs.WindowsPreferences <init> WARNING: Could not open/create prefs root node Software\JavaSoft\Prefs at root 0x80000002. Windows RegCreateKeyEx(...) returned error code 5. check clazz : com/jetbrains/ls/a/m check clazz : com/jetbrains/ls/a/q check clazz : com/jetbrains/ls/b/b Find one : Ljava/lang/String; : - Find two : (Ljava/lang/String;)Lcom/jetbrains/ls/a/s; clazzStr : com/jetbrains/ls/a/s Modify : com/jetbrains/ls/b/b > a > (Ljava/lang/String;)Lcom/jetbrains/ls/a/s; check clazz : com/jetbrains/ls/b/a Find one : Ljava/lang/String; : - check clazz : com/jetbrains/ls/b/a$d check clazz : com/jetbrains/ls/b/a$c check clazz : com/jetbrains/ls/b/a$b check clazz : com/jetbrains/ls/a/p check clazz : com/jetbrains/ls/a/s check clazz : com/jetbrains/ls/data/LicenseData check clazz : com/jetbrains/ls/data/ProductData check clazz : com/jetbrains/ls/a/h check clazz : com/jetbrains/ls/a/l check clazz : com/jetbrains/ls/a/l$a check clazz : com/jetbrains/ls/requests/AbstractRequest$UserIdentification check clazz : com/jetbrains/ls/a/j check clazz : com/jetbrains/ls/requests/AbstractRequest check clazz : com/jetbrains/ls/requests/ValidateKeyRequest check clazz : com/jetbrains/ls/requests/ValidateLicenseRequest check clazz : com/jetbrains/ls/requests/PingRequest check clazz : com/jetbrains/ls/requests/ExchangeKeyRequest check clazz : com/jetbrains/ls/requests/ObtainLicenseRequest check clazz : com/jetbrains/ls/requests/ObtainAgreementRequest check clazz : com/jetbrains/ls/a/u check clazz : com/jetbrains/ls/a/u$a check clazz : com/jetbrains/ls/a/u$e check clazz : com/jetbrains/ls/a/c check clazz : com/jetbrains/ls/a/u$c check clazz : com/jetbrains/ls/a/u$d check clazz : com/jetbrains/ls/e check clazz : com/jetbrains/ls/a/a check clazz : com/jetbrains/ls/a/r check clazz : com/jetbrains/ls/a/n check clazz : com/jetbrains/ls/a/k$c check clazz : com/jetbrains/ls/a/k check clazz : com/jetbrains/ls/a/k$a check clazz : com/jetbrains/ls/a/n$a check clazz : com/jetbrains/ls/a/k$b check clazz : com/jetbrains/ls/responses/ValidateKeyResponse check clazz : com/jetbrains/ls/responses/AbstractResponse check clazz : com/jetbrains/ls/responses/ResponseCode check clazz : com/jetbrains/ls/a/f check clazz : com/jetbrains/ls/a/f$i check clazz : com/jetbrains/ls/a/f$d check clazz : com/jetbrains/ls/a/f$c check clazz : com/jetbrains/ls/a/g check clazz : com/jetbrains/ls/a/g$a check clazz : com/jetbrains/ls/a/o check clazz : com/jetbrains/ls/a/o$a check clazz : com/jetbrains/ls/a/b check clazz : com/jetbrains/ls/a/e check clazz : com/jetbrains/ls/responses/ObtainAgreementResponse check clazz : com/jetbrains/ls/a/l$b check clazz : com/jetbrains/ls/a/i check clazz : com/jetbrains/ls/requests/ProlongTicketRequest check clazz : com/jetbrains/ls/requests/GetPermanentActivationRequest check clazz : com/jetbrains/ls/requests/ObtainPermanentTicketRequest check clazz : com/jetbrains/ls/requests/AbstractObtainTicketRequest check clazz : com/jetbrains/ls/requests/ReleaseTicketRequest check clazz : com/jetbrains/ls/requests/ObtainTicketRequest check clazz : com/jetbrains/ls/d
[/ecko_toggle]
Pay attention on this part
check clazz : com/jetbrains/ls/b/b Find one : Ljava/lang/String; : - Find two : (Ljava/lang/String;)Lcom/jetbrains/ls/a/s; clazzStr : com/jetbrains/ls/a/s Modify : com/jetbrains/ls/b/b > a > (Ljava/lang/String;)Lcom/jetbrains/ls/a/s;
What does it mean? What was exactly replaced? What did we append on classpath and why does it replace something inside IDE? Lets dig a bit sources. Inside java agent we implement premain method which executes code before execution main method of prime application. If you read javaagent specification you will see method premain has two parameters.
public static void premain(String agentArgs, Instrumentation inst);
Second one is Instrumentation. This parameter allows to add class transformer
inst.addTransformer(new MethodEntryTransformer());
and crack does it. Based on few conditions Rover figure out which class should be modified. It’s not really clear for me how he got those conditions. Well most of them are pretty clear except one condition which is pretty strange.
[ecko_toggle style=”solid” state=”closed” title=”MethodEntryTransformer”]
static class MethodEntryTransformer implements ClassFileTransformer { public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException { try { if (className!= null && className.startsWith("com/jetbrains/ls/")) { ClassReader cr = new ClassReader(classfileBuffer); com.rover12421.asm.tree.ClassNode cn = new com.rover12421.asm.tree.ClassNode(); cr.accept(cn, 0); System.out.println("check clazz : " + className); boolean find = false; List<FieldNode> fieldNodes = cn.fields; for (FieldNode fieldNode : fieldNodes) { if (Modifier.isStatic(fieldNode.access)) { if ("-".equals(fieldNode.value)) { find = true; System.out.println("Find one : " + fieldNode.desc + " : " + fieldNode.value); } } } /** * ClassWriter.COMPUTE_MAXS: 局部变量和操作数栈的大小就会自动计算 * ClassWriter.COMPUTE_FRAMES: 所有的大小都将自动为你计算 * * IDEA 16中,会因为没有 ClassWriter.COMPUTE_FRAMES 而导致修改失败! */ ClassWriter cw = new ClassWriter(cr, ClassWriter.COMPUTE_MAXS | ClassWriter.COMPUTE_FRAMES); List<MethodNode> methodNodes = cn.methods; for (MethodNode methodNode : methodNodes) { if (Modifier.isPublic(methodNode.access) && methodNode.desc.startsWith("(Ljava/lang/String;)Lcom/jetbrains/ls/")) { if (!find && !methodNode.desc.equals("(Ljava/lang/String;)Lcom/jetbrains/ls/data/LicenseData;")) { continue; } System.out.println("Find two : " + methodNode.desc); String clazzStr = methodNode.desc .substring("(Ljava/lang/String;)L".length(), methodNode.desc.length()-1); System.out.println("clazzStr : " + clazzStr); InsnList insns = new InsnList(); insns.add(new VarInsnNode(ALOAD, 1)); insns.add(new LdcInsnNode(clazzStr)); insns.add(new MethodInsnNode(INVOKESTATIC, "com/rover12421/crack/jerbrains/v2/Util", "json2LincenseObject", "(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/Object;", false) ); insns.add(new TypeInsnNode(CHECKCAST, clazzStr)); insns.add(new InsnNode(ARETURN)); methodNode.instructions.insert(insns); methodNode.visitEnd(); System.out.println("Modify : " + className + " > " + methodNode.name + " > " + methodNode.desc); cn.accept(cw); return cw.toByteArray(); } } } } catch (Throwable e) { // e.printStackTrace(); } return classfileBuffer; } }
[/ecko_toggle]
So first condition is if classname belongs to license package, so it’s pretty clear
if (className != null && className.startsWith("com/jetbrains/ls/")) {
second one
if (Modifier.isStatic(fieldNode.access) && "-".equals(fieldNode.value)) {
this condition is really unexpected for me if you have idea why we need it – write comment please. I could assume it’s mostly related to other IDE not Intellij Idea. Pay attention if class match this condition it’s enough to be modified
if you investigate a bit Idea code you will see this piece
package com.jetbrains.ls.b; public abstract class b { private final a a; protected static final String b = "-"; public static boolean c;
So this field b meet condition above. There is only one class com.intellij.ide.a.e.n$a which extend this class b.
We can assume this class also match last one condition
if (Modifier.isPublic(methodNode.access) && methodNode.desc.startsWith("(Ljava/lang/String;)Lcom/jetbrains/ls/")) { if (find || methodNode.desc .equals("(Ljava/lang/String;)Lcom/jetbrains/ls/data/LicenseData;")) {
And our assumption is correct – class com.intellij.ide.a.e.n$a contains such piece of code
protected final Object a(String paramString) { Method tmp26_23 = f(2413764496921669L);tmp26_23; try {}catch (InvocationTargetException localInvocationTargetException1) { throw localInvocationTargetException1.getTargetException(); } Method tmp47_44 = f(2017563408608889L);tmp47_44; try {}catch (InvocationTargetException localInvocationTargetException2) { throw localInvocationTargetException2.getTargetException(); } Gson localGson = (Gson)tmp47_44.invoke(tmp26_23.invoke(new GsonBuilder(), c(Date.class, new n.b(null))), K()); Method tmp76_73 = f(1858680122894985L);tmp76_73; try {}catch (InvocationTargetException localInvocationTargetException3) { throw localInvocationTargetException3.getTargetException(); } return (Object)tmp76_73.invoke(localGson, c(paramString, LicenseData.class)); }
It is the last puzzle, Rover just replaces result of method (actually the whole method is replaced) with his own LicenseData object.
You can check it on Util.class method json2LincenseObject
public static Object json2LincenseObject(String json, String clazzStr) { try { Class clazz = Class.forName(clazzStr.replaceAll("/", ".")); if (clazz.equals(LicenseData.class)) { return json2LincenseData(json); } Object object = clazz.newInstance(); Field[] fields = clazz.getDeclaredFields(); for (Field field : fields) { if (field.getType().equals(Object.class) && field.getGenericType().toString().equals("T")) { field.setAccessible(true); field.set(object, json2LincenseData(json)); return object; } } } catch (Throwable e) { e.printStackTrace(); } return null; } }
Definitely for Intellij Idea it should be enough only this part
return json2LincenseData(json);
but perhaps for other IDE we will need rest part of method above.
Also it’s not really clear why we really need to parse json at all only idea to set up your own name (licenseeName) in crack. Anyway lets check what we have inside json
ThisCrackLicenseId-{ "licenseId":"ThisCrackLicenseId", "licenseeName":"Rover12421", "assigneeName":"", "assigneeEmail":"rover12421@163.com", "licenseRestriction":"For Rover12421 Crack, Only Test! Please support genuine!!!", "checkConcurrentUse":false, "products":[ {"code":"II","paidUpTo":"2099-12-31"}, {"code":"DM","paidUpTo":"2099-12-31"}, {"code":"AC","paidUpTo":"2099-12-31"}, {"code":"RS0","paidUpTo":"2099-12-31"}, {"code":"WS","paidUpTo":"2099-12-31"}, {"code":"DPN","paidUpTo":"2099-12-31"}, {"code":"RC","paidUpTo":"2099-12-31"}, {"code":"PS","paidUpTo":"2099-12-31"}, {"code":"DC","paidUpTo":"2099-12-31"}, {"code":"RM","paidUpTo":"2099-12-31"}, {"code":"CL","paidUpTo":"2099-12-31"}, {"code":"PC","paidUpTo":"2099-12-31"}, {"code":"DB","paidUpTo":"2099-12-31"} ], "hash":"2911276/0", "gracePeriodDays":7, "autoProlongated":false}
I would say everything is pretty clear – list of products is list of IDE like II is IntellijIdea or PC is PyCharm and so on. One thing I don’t know how to calculate hash and if it’s verified by JetBrains at all so if you know just write comment