DevUA

/CTO @Software Service and Innovation /JUGLviv leader /JDayLviv co-organiser www.jug.lviv.ua

Meta


Intellij Idea – inside a crack

Andriy AndrunevchynAndriy Andrunevchyn

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

/CTO @Software Service and Innovation /JUGLviv leader /JDayLviv co-organiser www.jug.lviv.ua