本次破解的版本是: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 文件清单中查看。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 该类在 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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//  下面两个方法在 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节点。我们通过写一段代码读取保存在/3t/mongochef/enterprise节点树中的key-value,已验证我们的分析。

图1 3t节点树中的kv分析

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
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方法:

1
2
3
4
5
6
7
// ME是静态方法
public class a {
public static boolean ME() {
return MC().getStatus().ai();
}
// 。。。其他方法
}

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

1
2
3
4
5
6
7
8
9
	
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的代码贴出来。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
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`类的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
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("已激活");则激活软件。另外一种,就是修改上面函数的返回值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
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中,然后对其编译。

1
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。具体错误如下:

1
2
3
错误的类文件: /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窗口中设置环境变量:

1
2
# 注意替换为自己安装的路径
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中。

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

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