通过反编译破解 MongoDB 客户端工具 Studio 3T

点击数:85

本次破解的版本是:Studio 3T for MongoDB 2020.7.1

Studio 3T 是 MongoDB 非官方开源客户端 Robo 3T的高级版。Studio 3T 集成了 MongoDB的开发环境,拥有全面的功能界面和客户端功能,以及便捷、快速的特点,适用于Windows, Mac, 和Linux操作系统。

Studio 3T 新安装则会免费试用30天,之后就需要收费才能试用。网上给出的大多数教程是的windows系统下的删除注册表延长试用。

经过常看反编译的代码发现,Studio 3T 会在安装的过程中使用用户树根节点,将配置信息以k-v键值对的形式写在/3t/mongochef节点下面。

小提示:反编译查看入口类,也就是main方法,需要在 META-INF/MANIFEST.MF 文件清单中查看。

// 该类在 t3.common.lic.c 包下
public class j extends f {
  private final Preferences bJ;
  // 构造器,传递是Edition版本信息
  public j(af paramaf) {
      super((paramaf == af.ENTERPRISE));
      String str;
      switch (k.aa[paramaf.ordinal()]) {
        case 1:
          str = \"core\";
          break;
        case 2:
          str = \"pro\";
          break;
        case 3:
          str = \"enterprise\";
          break;
        default:
          throw new IllegalArgumentException(\"Edition is unknown.\");
      }
      this.bJ = Preferences.userRoot().node(\"/3t/mongochef/\" + str);
    }
    // ... 其他方法
}

这里不得不说说java.util.prefs.Preferences类,该类是在JDK1.4中首次提供的,可以用其保存应用程序的配置数据。JDK中的 Preferences 类也提供了一个让 Java 应用程序存放配置信息的统一方法,即每一个基于Java的应用程序都可以使用Preferences类存放自己的配置信息。

Preferences 类是平台无关的,也就是在 Windows 平台上运行的Java应用程序可以用Preferences类存放配置信息,在Linux平台上运行的Java应用程序也可以使用Preferences类来存放自己的配置信息,对应用程序来说,它只管用Preferences类就好了, 不用管最终的配置信息在程序运行平台上的具体存放位置。

Preferences 类将应用程序的配置信息存放在具体的操作系统平台上,具体来说,在Windows操作系统下存放在注册表中,在*nux平台(Linux、Mac等)下是放在使用应用程序的用户的home目录下面的一个隐藏文件中。

Preferences 类用树状结构来存放应用程序的配置信息,树中的每个节点的路径名是`/com/xxx/yyy这种形式的,每个节点上都存放了一个键值对组成的表。每个应用程序可以在属于自己的节点上存放自己的配置信息,这些配置信息就构成了一个表,这个表就是节点的内容。

Preferences类有两个树,分别是系统树(它用来存放全部用户的共有信息)和用户树(它用来存放用户自己的配置信息)。在Windows操作系统下,系统树的根节点是HKEY_LOCAL_MACHINE\\SOFTWARE\\JavaSoft\\Prefs,而用户树的根节点是HKEY_CURRENT_USER\\software\\JavaSoft\\Prefs。

说完 Preferences 类,看看上面代码 this.bJ = Preferences.userRoot().node(\"/3t/mongochef/\" + str);,这段代码是说,获取用户树的根节点,并且为应用程序在用户树中建立一个/3t/mongochef/+版本的节点或者获取已有的节点(如果已经存在),并且该节点的引用赋值到 this.bJ

//  下面两个方法在 t3.utils.a 类中
public static Instant MK() {
    try {
      Long long_ = Long.valueOf(Long.parseLong((new j(af.ENTERPRISE))
            .v(\"installation-date-\" + aD().Xw())));
      return Instant.ofEpochSecond(long_.longValue());
    } catch (Exception exception) {
      return Instant.now();
    }
  }

  public static void ML() {
    try {
      (new j(af.ENTERPRISE)).c(\"installation-date-\" +
          aD().Xw(),
          Long.toString(MK().toEpochMilli() / 1000L));
    } catch (IOException iOException) {
      Logger.error(iOException, \"Could not set installation date.\");
    }
  }

仔细看,这两个方法中用到了 j 这个类, 该类就是上面分析的类 t3.common.lic.c.j, 那么根据上面函数我们可以看出,在程序首次启动的时候会将一个 key 以 installation-date- + 日期开头,value为秒的时间戳写入到用户树根目录/3t/mongochef/+版本节点中,如果是企业版则为/3t/mongochef/enterprise节点。

\"MAC中Studio

打印结果如下:

installation-date-2020.7.1       ==============  1598427210
soduz3vqhnnja46uvu3szq--      ==============  BoieKNcYUCAwxMTDz1Y8aOyeC1/9sopwcJLxCXtW2H4PpXpUzYSqdZ2bZOaHtvrK
--3mkysnku52awb7yxrapw--      ============== tX1qGc7cjivq06bi2FZ/f1Ck+5RjaB/JA6JnLGmsdbPmc2355jeNLdDQOrhtQJEHi6BKu1ZrhJy7TC8EMT0uy5gHKswjBW5JuWGPgBUR9kMObm/T6jN3F2wAjrHxfmOe998jYsU1+P0I/a/dyLuB/yTItMNCTcD3T/h8vgvneXCeGcZ5NLl4AwsPknWEIavYPyxVK8QBHxMsuZEuNXtUJfTvkLLvTB2krLlkrt4G+1Q=
bnrrjeye6fas6qaqgi-exa--      ============== xlqdI9ex9ft3/MStURk32bMu2FNViZhSP/YBAo17SzAKLA4PbjoCK+3iEZitICdoFSpnObVj0eRw/HsG1pNBBppp6vTcarVIwTIAaQdJ0//H49rzcrJNHXrSKUe8CSROa8XLNfSAD0SfkHif+MBVFTV9QNtwFmmVOtFx6Q6a26baWyVSeT8HFfhjr+euPudGPE2isas+3Rm71lqaHoNnlw==

我们跟踪入口类t3.dataman.mongodb.app.Studio3TApp,我们就会看到下面的方法:

private static void zQ() {
    Display display = new Display();
    try {
      if (!(new u()).bM())
        System.exit(-1);
      a.MJ();
      if (a.an() && !a.aq()) {
        a.D();
      } else if (a.ao() && !a.aq()) {
        c c1 = a.MD();
        c c2 = a.MF();
        d d = new d(a.aD(), c1.W(), (c1.R() || c1.isNone()), c2 instanceof t3.common.lic.a.a);
        if (d.dd())
          a.E();
      } else {
        a.aHG().j(a.aD());
        a.aHG().Sq();
      }
      if (!a.ME()) {
        b.aL().a(10000);
      } else {
        b.aL().a(4000);
      }
      a.MJ();
      if (!a.ME()) {
        r.bjx.i(display);
        (new l(display.getActiveShell())).open();
        l.a(display.getActiveShell(), true);
      }
      a.MJ();
      if (!a.ME()) {
        Logger.error(\"No active license, exiting.\");
        System.exit(-1);
      }
    } catch (Exception exception) {
      Logger.error(exception, \"Unexpected exception while running setup wizard.\");
      System.exit(-1);
    } finally {
      display.dispose();
    }
  }

如果没有这行日志No active license, exiting.,恐怕不会那么容易找到。既然日志给出了提示,那么校验是否激活的函数入口便由此可找到。继续跟踪a.ME方法:

// ME是静态方法
public class a {
    public static boolean ME() {
    return MC().getStatus().ai();
  }
  // 。。。其他方法
}

发现返回的是 ai 方法的值,继续跟踪该方法:


public boolean ai() { return (this.J == e.ACTIVE); } // e 是枚举 public enum e { ACTIVE, LICENSE_EXPIRED, USAGE_TOKEN_EXPIRED, MACHINE_LIMIT_REACHED, NO_SEAT, USAGE_DENIED_UNKNOWN_REASON; }

e 是枚举类,由此可知,J 是枚举,MC() 是 t3.common.lic.p,p也是枚举,不过p 实现了接口 c,从代码中发现p 为枚举的目的是实现单例模式。getStatus返回的是t3.common.lic.d的实例。为了看着方便,也罢p的代码贴出来。

package t3.common.lic;

import java.util.EnumSet;
import t3.utils.af;

public enum p implements c {
  INSTANCE;

  private boolean isTampered;

  public String getName() {
    return this.isTampered ? \"NONE-TAMPERED\" : \"NONE\";
  }

  public Object accept(n paramn) {
    return paramn.b(this);
  }

  public EnumSet getEditions() {
    return EnumSet.noneOf(af.class);
  }

  public boolean isNone() {
    return true;
  }

  public d getStatus() {
    return d.n(\"You have no license installed yet.\");
  }

  public void setTampered(boolean paramBoolean) {
    this.isTampered = paramBoolean;
  }

  public boolean isTampered() {
    return this.isTampered;
  }
}

回头我们看看“t3.common.lic.d`类的代码:

package t3.common.lic;

import com.google.common.base.Preconditions;

public class d {
  // 枚举类,存储当前状态
  private final e J;

  // 提示信息
  private final String K;

  // 构造函数
  // parame 参数为枚举类型,软件状态
  // paramString 参数为提示信息
  private d(e parame, String paramString) {
    Preconditions.checkNotNull(paramString);
    this.J = parame;
    this.K = paramString;
  }
  // 返回当前J中保存的状态,枚举类型,getter方法
  public e Z() {
    return this.J;
  }

  // 静态方法m,创建d对象,参数为提示信息,默认为激活状态
  public static d m(String paramString) {
    return new d(e.ACTIVE, paramString);
  }

  // 创建d对象,设置license 过期状态
  public static d n(String paramString) {
    return new d(e.LICENSE_EXPIRED, paramString);
  }

  // 用户token过期
  public static d aa() {
    return new d(e.USAGE_TOKEN_EXPIRED, \"Please go online\");
  }

  // license限制使用
  public static d ab() {
    return new d(e.MACHINE_LIMIT_REACHED, \"License is being used in too many computers\");
  }

  public static d ac() {
    return new d(e.NO_SEAT, \"No seat assigned\");
  }

  public static d ad() {
    return new d(e.USAGE_DENIED_UNKNOWN_REASON, \"Update required\");
  }

  // 获取标题
  public String getTitle() {
    return this.K;
  }

  // 授权码是否过期
  public boolean ae() {
    return (this.J == e.LICENSE_EXPIRED);
  }

  // token是否过期
  public boolean af() {
    return (this.J == e.USAGE_TOKEN_EXPIRED);
  }

  // 是否为限制使用
  public boolean ag() {
    return (this.J == e.MACHINE_LIMIT_REACHED);
  }

  // 是否为未指定位置
  public boolean ah() {
    return (this.J == e.NO_SEAT);
  }

  // 是否未激活
  public boolean ai() {
    return (this.J == e.ACTIVE);
  }
}

看到这个类想必大家知道怎么破解了吧,把不该返回的信息按照你的想法设置即可。两种思路,第一种,在入口函数中,增加对d的调用,例如 d.m(\"已激活\");则激活软件。另外一种,就是修改上面函数的返回值。

package t3.common.lic;

// 删除google代码对空字符串的校验,没必要
public class d {
  private final e J;

  private final String K;

  private d(e parame, String paramString) {
    this.J = parame;
    this.K = paramString;
  }

  public e Z() {
    return this.J;
  }

  public static d m(String paramString) {
    return new d(e.ACTIVE, paramString);
  }

  public static d n(String paramString) {
    return new d(e.LICENSE_EXPIRED, paramString);
  }

  public static d aa() {
    return new d(e.USAGE_TOKEN_EXPIRED, \"Please go online\");
  }

  public static d ab() {
    return new d(e.MACHINE_LIMIT_REACHED, \"License is being used in too many computers\");
  }

  public static d ac() {
    return new d(e.NO_SEAT, \"No seat assigned\");
  }

  public static d ad() {
    return new d(e.USAGE_DENIED_UNKNOWN_REASON, \"Update required\");
  }

  public String getTitle() {
    return this.K;
  }

  // 永不过期
  public boolean ae() {
    // return (this.J == e.LICENSE_EXPIRED);
    return false;
  }
  // 永不过期
  public boolean af() {
    // return (this.J == e.USAGE_TOKEN_EXPIRED);
    return false;
  }

  public boolean ag() {
    return (this.J == e.MACHINE_LIMIT_REACHED);
  }

  // 一直是指定位置的
  public boolean ah() {
    // return (this.J == e.NO_SEAT);
    return false;
  }

  // 一直是激活的
  public boolean ai() {
    // return (this.J == e.ACTIVE);
    return true;
  }
}

将上面代码保存在目录为/Users/xxx/t3/common/lic/d.java中,然后对其编译。

javac -classpath /Applications/Studio\\ 3T.app/Contents/Resources/app/data-man-mongodb-ent-2020.7.1.jar /Users/xxx/t3/common/lic/d.java -d .

编译时发现需要jdk11。具体错误如下:

错误的类文件: /Applications/Studio 3T.app/Contents/Resources/app/data-man-mongodb-ent-2020.7.1.jar (t3/common/lic/e.class)
    类文件具有错误的版本 55.0, 应为 52.0
    请删除该文件或确保该文件位于正确的类路径子目录中。

下载并安装jdk11, 下载页面 https://www.oracle.com/java/technologies/javase-jdk11-downloads.html,选择 jdk-11.0.8_osx-x64_bin.tar.gz,用 tar.gz 解压即可使用,因为我们开发的jdk版本是8,没必要替换原来的。

下载jdk解压后放在合适的目录,直接在当前shell窗口中设置环境变量:

# 注意替换为自己安装的路径
export JAVA_HOME=/Users/xxx/Documents/software/jdk-11.0.8.jdk/Contents/Home

再次编译,发现已经成功,在/Users/xxx下生成了d.class文件,执行打包命令,将生成的 d.class 文件重新打包到data-man-mongodb-ent-2020.7.1.jar中。

jar uf /Applications/Studio\\ 3T.app/Contents/Resources/app/data-man-mongodb-ent-2020.7.1.jar t3/common/lic/d.class

至此,破解结束,启动Studio 3T发现已经没有限制了~

发表评论

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据