Hotaru's Notebook

Mybatis Plus for IntelliJ IDEA 破解

Preface

前天发现了个不错的插件,叫 Mybatis Plus,是一个 IntelliJ IDEA 插件,用于协助编写使用MyBatis的程序。 插件官网上还给出了使用插件的 效果视频,感觉这插件能大大提升开发效率,于是安装它打算试试。 不料,安装后发现插件不允许试用,只能先购买后使用。于是我查了一些资料,试着自己处理掉DRM。

准备工作

按照 引用1引用2 的方法,使用 jbe 直接修改类字节码。不过jbe貌似有bug,每次保存方法以后都会丢失一些信息(比如 保存Method body的字节码后,Method里 堆(Stack)的最大高度之类的信息就丢失了),导致IDEA载入这个插件的时候抛出载入类文件异常。 于是我改用 jd-gui 结合IntelliJ IDEA自带的Decompiler,反编译出原有的java代码,然后手动修改代码使其通过编译检查。 注意:每次修改代码后都进行编译,然后将编译后的类文件替换掉原来的jar包里的类文件,最后重启IDEA看效果和调试输出。

开工

作为切入点的两个类:JavaServiceJavaUtil

先从 引用1引用2 中所提到的两个类下手:

com.seventh7.mybatis.service.JavaService
com.seventh7.mybatis.util.JavaUtil

用IDEA新建一个Java项目,并在 Project Structure 里选择 IntelliJ Platform Plugin SDK。没有这个选项就创建一个即可,如下图:

Project SDK for IDEA plugin.jpg

整个java项目的文件结构如下:

Project file structure.jpg

目录 作用
/libs/ 插件的原版jar包和它引用的一些包
/src/main/java/ 按照原版jar包里的包结构创建,
用于替换原版jar包里的类.
/src/main/resources/ 目前只存放 MANIFEST.MF, 用于记录原版jar包对应的版本号.

图中还有一些上面没有提到过的类,这些类以后会用到。只用这一张图也省得我再做其他的图了。

先从 JavaUtils 类下手好了

先把类反编译出来放到对应的包下面。反编译的类一般都会有些编译错误,导致这些错误的原因包括但不限于:

  1. Java泛型的类型擦除。
  2. 类型强制转换时候的 Unchecked 警告。
  3. 反编译工具不完善,没能及时更新。

泛型类型擦除这个比较好解决,根据IDE的提示猜出对应的泛型类型就好。猜不出来就先不用泛型,看运行时的异常再决定用什么类型。 以编译通过为基础,尽可能减少可能会导致运行时异常的代码,其他不会导致运行时异常的就不要管它。

按照 引用2 的说明,对 refVaild()Z 做修改。修改前:

public static synchronized boolean refValid() { // Line #194
	if(!validated) {
		validated = true;

		try {
			String e = MybatisSetting.getInstance().getKey();
			String result = MybatisSetting.getInstance().getResult();
			if(!StringUtils.isBlank(e) && !StringUtils.isBlank(result)) {
				Key publicKey = Codec.loadKey(e);
				Codec.decrypt(publicKey, Hexs.toBytes(result));
				valid = true;
			} else {
				valid = false;
			}
		} catch (Exception var3) {
			valid = false;
		}
	}

	return valid;
}

修改后:

public static synchronized boolean refValid() {
    validated = true; // 用于判断 refVaild()Z 是否被调用过了.
    valid = true; // 用于判断DRM是否通过.
    return valid;
}

然后我又添加了个 public static String printStackTrace(String message),调用后会像java抛出异常信息一样在 System.err 打印出调用堆。只不过这个方法只打印这个插件所在java包的信息,其余的不打印。这样,我就可以在任意地方插入调用这个方法的代码,然后从IDEA的标准输出里观察调试信息。代码很简单,我就不贴了。

好奇心驱使,我又翻了翻这个类,发现又有个内部类叫 public static class SystemData,负责收集系统的各种信息,比如 系统什么名字、用的什么JVM。有个方法叫 public static synchronized JavaUtils.SystemData getInstance(),负责初始化一个 SystemData 类的实例,直接修改它就能免除系统信息被收集。

然后是 JavaService

直接修改 stop()Vstopped()Z 即可,像下面这样:

public boolean stopped() { // Line #204
    return false;
}

public void stop() { //Line #208
}

在修改的过程中发现了一个 LicenseData.EMPYT。这个类名就很可疑,加上原来的代码好像想要把许可信息置空:

MybatisSetting.getInstance().setLicenseData(LicenseData.EMPTY); // Line #212

于是干脆进去看看这个类有什么蹊跷。

探索 LicenseData

其实这个类直到后来我才重视它,不过他也没有太大的重要性。我直接把需要修改的地方标注出来好了。

// 原来的 EMPTY 定义,简而言之就是没有填写 key 和 result。
public static final LicenseData EMPTY = new LicenseData("", "");
// 随便给它个key和result就好,反正DRM的验证部分在 `JavaUtils.refVaild()Z` 已经被搞定了。
public static final LicenseData EMPTY = new LicenseData("fake_key", "fake_result");

然后是 getType()Ljava/lang/reflect/Type;。在之后的逆向过程中发现 这个Method很可能是方便Gson反序列化用的。

// 原来的:
public static Type getType() {
    return new TypeToken() { }.getType();
}
// 修改成:
public static Type getType() {
    return new TypeToken<LicenseData>() { }.getType();
}

LicenseData 相邻的包和类

既然挖到DRM部分了,那当然要好好看一看还有什么东西了。下面列出 com.seventh7.mybatis.ref 包下各个类的作用:

包/类名 作用
.http HTTP库
.license.ActivationDriver 访问插件服务器, 查询key对应的激活信息.
.license.ActivationResult 联网验证key后得到的结果(成功或者失败).
.license.LicenseData 许可证信息, 包含 key 和激活结果.
.license.RefConfigurable 提供一个UI(文本框)输入key, 以此来激活插件.
.CmProject IDEA载入一个Project后会调用 initComponent()V,
而这个插件的作用就是 每当载入项目时前都向服务器上报key、激活信息以及系统信息.
.RefProject 负责通过IDEA发布通知, 告诉用户该激活了.
.Uris 与服务器通信要用到的一些URL.

下面,一步一步的来解决DRM和信息上报问题。

AcitvationDriver 类和 ActivationResult

ActivationDriver 类只有一个静态方法 ActivationResult activate(String licenseKey)。方法里的代码很简单:拿key询问服务器是否有效。有效的话 方法就返回一个激活成功的信息,反之 返回激活失败的信息。

ActivationDriver.activate(String licenseKey) 返回的是一个 ActivationResult 类,这个类有两个工厂方法(Factory Method): success(LicenseData licenseData)fail(String messsage)。于是 只需要对 ActivationDriver 进行修改就好了。

为何不直接对 ActivationResult 进行修改,使其永远返回激活成功? 因为:

  1. 这个类比 ActivationDriver 复杂一些,所以改起来没有 ActivationDriver 简单。
  2. 以后插件开发者再往代码的其他地方散布DRM时候,如果又用到这个类了,也方便以后再排查出来。

修改后的 ActivationDriver 很简单:

public class ActivationDriver {
    public static ActivationResult activate(String licenseKey) {
        // 因为 LicenseData.EMPTY 已经被改过了,于是这里直接用就行。
        return ActivationResult.success(LicenseData.EMPTY);
    }
}

屏蔽 CmProject 类向服务器上报各种信息

方法也非常简单,在对应的包下面新建同名类并实现 com.intellij.openapi.components.ProjectComponent 接口,代码如下:

package com.seventh7.mybatis.ref;

import com.intellij.openapi.components.ProjectComponent;
import org.jetbrains.annotations.NotNull;

public class CmProject implements ProjectComponent {
    @Override
    public void initComponent() {
        // 原来的类在这里进行信息上报,于是这里什么也不做。
    }

    @NotNull
    @Override
    public String getComponentName() {
        // 和原来的类一样,该返回啥返回啥。
        return "CmProject";
    }

    // 其他要实现的方法的方法体里全都放空。
}

在修改 CmProject 之前我看了看这个类的代码,代码里除了HTTP通信以外,还与一个叫 MybatisSettings 的类在打交道。下面就开始对这个类进行研究和修改。

探索 MybatisSettings

这个类位于 com.seventh7.mybatis.settings 包里,负责读写插件的配置信息,比如 key 和一些插件的设置都会在这里进行存取。

不过,后来又发现还有个地方会读取配置信息而不经过这个类。

经过一番代码阅读,挑出了5个比较显眼的方法,并分别插入 printStackTrace(String message),看看还有谁会通过它读写许可信息。

这里值得说一下的是,setKey(String key) 方法做了点儿小工作:

// 原来的代码:
public void setKey(String key) {
    this.key = key;
    // 如果key是null或者是空字符串,那就 ++insert。
    if(StringUtils.isBlank(this.key)) {
        ++this.insert;
    }
}
// 修改后:
public void setKey(String key) {
    if (StringUtils.isBlank(key)) {
        this.key = "fake_key";
    } else {
        this.key = key;
    }
}

经过一番查找后发现,在IDEA提供给插件粗放配置文件的地方(在 ~/.IntelliJIdea<版本号>/config/options/),这个类在管理一个文件来存放插件的配置信息,文件名为 mybatis.xml。文件内容大致像下面这样,我只挑出重点的部分贴出来:

<application>
    <component KEY="" RESULT="" Insert="6" />
</application>

这样,上面那个 ++this.insert 就能解释清楚了:每当输入的key为空的时候,就吧 Insert 的值 +1。至于插件开发者为何这么做,本文后面会提到。 替换掉jar包类文件,然后重启IDEA并观察调试输出。发现一个叫 Utils$MLicenseValidator 的类调用了 setLicenseData() 方法,看来我又发现了其他隐藏起来的DRM验证了。

探索 Utils$MLicenseValidator

这个内部类位于 com.seventh7.mybatis.generate0.Utils$MLicenseValidator,不过类的内容很简单。下面贴出修改后的代码:

public static class MLicenseValidator implements LicenseValidator { // Line #79
    @Override
    public void validateLicense(License license) throws InvalidLicenseException {
        // 只是简单的设置成"空许可证"即可.
        try {
            MybatisSetting.getInstance().setLicenseData(LicenseData.EMPTY);
        } catch (IOException ignore) {
        }
    }
}

尝试关掉IDEA的 Completion

到目前为止,经过上面的代码修改后,看上去应该没有DRM验证的部分了。于是我覆盖掉原来jar包里的类,然后启动IDEA看效果。 IDEA启动后刚载入好java文件,就提示我"你确定要退出IDEA吗?"。我记得我没点关闭啊,于是我点Cancel,不到1秒又蹦出来了。我反反复复点了几次Cancel,IDEA一直弹出是否退出的对话框。看来肯定是这个插件搞的鬼了。 于是我打开jd-gui并载入这个插件的jar包,然后使用搜索 quit exit 之类的关键字。幸运的是我很快就找到了导致IDEA会退出的代码。 这段代码位于 com.seventh7.mybatis.dom.model.Completion。代码内容也很简单:判断插件配置文件里的 Insert 是否大于5,并且 能否在IDEA里找到这个插件的ID(也就意味着插件载入到IDEA了)。条件符合的话就删掉一个类文件(com.seventh7.mybatis.definitionsearch.MapperDefinitionSearch$1),然后不断的提示要求退出IDEA。这样,当下次启动IDEA的时候,插件会因为缺少类文件而无法工作。 解决方法也很简单:创建同名类并继承 Thread 类,覆盖 run()V 并且该方法内不写任何代码。

总结

经过两天的周折,终于把这个插件搞定了。

更新(2016-08-08):Rover12421用JavaAgent写了个破解工具 项目地址:https://coding.net/u/rover12421/p/MyBatisPluginCrack

References: 1. http://blog.lanyus.com/archives/178.html 2. http://blog.lanyus.com/archives/115.html

#Java EE #Intellij IDEA #CRACK #Mybatis