准备工作

  1. 下载原始jar包:aspose-html-22.8-jdk11.jar
  2. 准备反编译工具,这里推荐使用jd-gui或者jadx

破解思路

  • Aspose 系列产品主要依赖许可证文件(license.xml)的注册来工作。首先,让我们看看 Aspose 是如何注册许可证的:
1
2
3
4
5
6
7
8
9
InputStream is;
try {
is = Files.newInputStream(Paths.get("C:\\xxx\\license.xml"));
License license = new License();
license.setLicense(is);
is.close();
} catch (Exception e) {
throw new RuntimeException(e);
}
  • license.xml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<License>
<Data>
<Products>
<Product>Aspose.Total for Java</Product>
<Product>Aspose.Words for Java</Product>
</Products>
<EditionType>Enterprise</EditionType>
<SubscriptionExpiry>20991231</SubscriptionExpiry>
<LicenseExpiry>20991231</LicenseExpiry>
<SerialNumber>8bfe198c-7f0c-4ef8-8ff0-acc3237bf0d7</SerialNumber>
</Data>
<Signature>
sNLLKGMUdF0r8O1kKilWAGdgfs2BvJb/2Xp8p5iuDVfZXmhppo+d0Ran1P9TKdjV4ABwAgKXxJ3jcQTqE/2IRfqwnPf8itN8aFZlV3TJPYeD3yWE7IT55Gz6EijUpC7aKeoohTb4w2fpox58wWoF3SNp6sK6jDfiAUGEHYJ9pjU=
</Signature>
</License>
  • 破解的思路很简单,就是查看 setLicense 方法中实际做了哪些操作,然后对源码进行修改。我们使用 JD-GUI 反编译 jar 包,找到 License 类,位于 com.aspose.html 包下。

  • 我们可以看到有两个 setLicense 方法,而代码中调用的是第二种形式。

  • 这里使用了传入的文件流,我们可以重点查看z16调用的m1方法:

1
z16.m1(byteArrayInputStream);
  • 我们进入 z16 类,逐步查看里面的实现,找到一个可疑的方法。

  • 在这段代码中,m1 方法的主要功能是验证许可文件的签名,以确保文件的合法性和未被篡改。

    1. 获取许可类型:
    • 通过 switch 语句,根据许可的类型(如 “Professional” 或 “Enterprise”)进行区分,以确定许可文件的类型。
    1. 处理节点数据:
    • 将传入的 XML Node 数据转化为字符串并以 UTF-16LE 编码转换为字节数组 (arrayOfByte1)。
    • 从第二个节点 paramNode2 中提取 Base64 编码的签名,并将其解码为字节数组 (arrayOfByte2)。
    1. 选择签名算法:
    • 根据许可文件的安全要求,选择使用 SHA1withRSA 或 SHA256withRSA 作为签名算法。如果系统启用了 FIPS 安全模式,则需要使用 FIPS 认证的提供程序 (m186)。
    1. 公钥选择:
    • arrayOfString1 和 arrayOfString2 数组中存储了多个可能的公钥或公钥片段,通过索引选择合适的公钥。
    • 使用 m5 方法从字符串中生成公钥 (PublicKey)。
    1. 签名验证:
    • 使用 initVerify 方法初始化公钥验证。
    • 调用 update 方法传入已编码的数据字节数组 (arrayOfByte1),然后使用 verify 方法验证 Base64 解码后的签名 (arrayOfByte2) 是否匹配。
    1. 返回结果:
    • 验证成功后返回许可状态 paramz3,如果验证失败,则抛出异常并返回失败状态 (z3.m205)。
  • 那么破解的思路非常简单:直接让这个方法返回 paramz3 就行了。

修改字节码文件

  • 接下来,我们使用 Javassist 修改字节码文件。首先添加依赖:
1
2
3
4
5
<dependency>
<groupId>org.javassist</groupId>
<artifactId>javassist</artifactId>
<version>3.28.0-GA</version>
</dependency>
  • 我们只需要需要修改 z16 类中的 m1(Node node, Node node2, z3 z3Var) 方法,让它直接返回 paramz3。修改的代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
try {
String jarPath = "D:\\html\\aspose-html-22.8-jdk11.jar";
ClassPool classPool = ClassPool.getDefault();
classPool.insertClassPath(jarPath);

// z16类里的m1方法,同时三个参数类型分别是Node, Node, z3,这个z3我找了一下,是z16里的一个枚举类
CtClass ctClass = classPool.getCtClass("com.aspose.html.z16");
CtClass[] paramTypes = new CtClass[3];
paramTypes[0] = classPool.get("org.w3c.dom.Node");
paramTypes[1] = classPool.get("org.w3c.dom.Node");
paramTypes[2] = classPool.get("com.aspose.html.z16$z3");
CtMethod ctMethod = ctClass.getDeclaredMethod("m1", paramTypes);

// 修改方法体,使其直接返回第三个参数
ctMethod.setBody("{\n" +
" return $3;\n" +
" }");

ctClass.writeFile("D:\\html\\");

System.out.println("Modification completed successfully.");
} catch (Exception e) {
e.printStackTrace();
}
  • 编译执行之后会生成一个.class文件,我们把这个.class文件替换到原来的jar包中,并且删除jar包中原本的.RSA和.SF文件,使用mvn install安装一下这个jar包就可以调用了。

注意事项

  • ASPOSE.SLIDE、ASPOSE.WORDS等其他产品,破解思路基本都是一样的,都是看setLicense方法的实现,然后修改源码。
  • 破解版的 jar 包可能存在后台向外部服务器发送数据的风险,因此本文仅限于学习研究,不涉及商业用途。请勿将其用于非法活动。

怎么偷偷用?

方案设计

主要技术点

  1. 参数化Java程序

    • 通过 main 方法接受参数,并规范参数顺序:
      • args[0]:任务类型(如 report、summary)。
      • args[1]:统一业务参数(可为 JSON 等)。
      • args[2]:输出路径。
      • 其余参数:不同任务类型独有的参数。
    • 运行时使用 java -jar xxx.jar args0 args1 args2 传递参数。
    • Java 程序只能有一个 main 方法,所有任务分发逻辑在 main 内实现。
  2. 使用 Docker 进行环境隔离

    • 通过 Dockerfile` 创建独立的运行环境,确保所需的 Java 依赖完整可用。
    • 运行时使用 --network none 选项断网,防止商业库进行外部联网验证。
  3. 提供远程调用接口

    • 通过 Python Flask 创建 HTTP API,接收外部请求并执行 Docker 命令。
    • Java 代码通过 OkHttpClient 发送 HTTP 请求,实现远程文档生成任务。

代码实现

Java代码

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
public class DocGenerateApplication {

public static void main(String[] args) throws Exception {

for (int i = 0; i < args.length; i++) {
args[i] = args[i].replaceAll("\\r\\n|\\r|\\n", "");
}

InputStream is;
try {
is = Files.newInputStream(Paths.get("/data/license.xml"));
License license = new License();
license.setLicense(is);
is.close();
} catch (IOException e) {
throw new RuntimeException(e);
}

String type = args[0];
// 读取文件内容
String base64Content;
String content = "";
if (!(type.equals("TransDoc") || type.equals("TransDoc2PDF"))) {
base64Content = new String(Files.readAllBytes(Paths.get(args[1])), StandardCharsets.UTF_8);
byte[] decodedBytes = Base64.getDecoder().decode(base64Content);
content = new String(decodedBytes, StandardCharsets.UTF_8);
System.out.println("base64转换:" + content);
}

String outputPath = args[2];
switch (type) {
case "CareerPlan":
CareerPlanUtils.generateCareerPlanReport(content, outputPath, args[3]);
System.out.println("CareerPlan Success");
break;
case "CoursePaper":
CoursePaperUtil.generateCoursePaper(content, outputPath, args[3]);
System.out.println("CoursePaper Success");
break;
case "InternLog":
InternLogUtils.generateInternLog(content, outputPath, args[3], args[4]);
System.out.println("InternLog Success");
break;

...

default:
System.out.println("Unknown report type: " + type);
break;
}
}
}

核心命令

  • 重点是--network none,随后在启动jar包的时候,传递自己需要的参数。
1
2
3
docker run --network none
-e LANG = C.UTF - 8
doc-generate-test java -jar /data/ppt.jar args0 args1 args2 args3 args4 ...

Python代码

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
# encoding=utf-8
# !/usr/bin/env python3
from flask import Flask, request, jsonify
import subprocess
import logging
import pdb

LOG_PATH = "aspose_proxy.log"

logger = logging.getLogger() # 不加名称设置root logger
logger.setLevel(logging.DEBUG)
formatter = logging.Formatter(
'%(asctime)s-%(levelname)s: %(message)s',
datefmt='%Y-%m-%d %H:%M:%S')

# 使用FileHandler输出到文件
fh = logging.FileHandler(LOG_PATH)
fh.setLevel(logging.DEBUG)
fh.setFormatter(formatter)

# 使用StreamHandler输出到屏幕
ch = logging.StreamHandler()
ch.setLevel(logging.DEBUG)
ch.setFormatter(formatter)

# 添加两个Handler
logger.addHandler(ch)
logger.addHandler(fh)

app = Flask(__name__)


@app.route('/execute', methods=['POST'])
def execute_command():
try:
# 从请求中获取参数
args = request.form.to_dict()

# 构建命令行指令
cmd = [
"docker", "run", "--network", "none",
"-e", "LANG = C.UTF - 8",
"doc-generate-test", "java", "-jar", "/data/ppt.jar"
]

# cmd = ["java", "-jar", "C:/WorkSpace/ppt/target/ppt-0.0.1-SNAPSHOT.jar"]

# 拼接参数
for value in args.values():
cmd.append(value)

# 打印最终的命令行
logger.info("Executing command命令: " + ' '.join(cmd))

# 启动进程
process = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, encoding="UTF-8")

# 读取输出
stdout, stderr = process.communicate()

logger.info("Command output: " + stdout)
logger.info("Command error: " + stderr)
with open("stdout.txt", "w") as fout:
fout.write(stdout)
with open("stderr.txt", "w") as fout:
fout.write(stderr)

return jsonify({"message": "Command started", "pid": process.pid, "output": stdout, "error": stderr})

except Exception as e:
logger.exception("Exception:", e)
return jsonify({"error": str(e)}), 500


if __name__ == '__main__':
app.run(host='0.0.0.0', port=8848, debug=False)

调用代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public void sendExecutorCMD(String ...args) throws IOException {
String url = "http://localhost:8848/execute";
MultipartBody.Builder builder = new MultipartBody.Builder().setType(MultipartBody.FORM);
for (int i = 0; i < args.length; i++) {
log.info("添加参数:{}", args[i]);
String s = args[i].replaceAll("\\+", "/");
builder.addFormDataPart("param" + (i + 1), s);
}
RequestBody requestBody = builder.build();
Request request = new Request.Builder()
.url(url)
.post(requestBody)
.build();
Response response = okHttpClient.newCall(request).execute();
String result = response.body().string();
log.info("处理结果:{}", result);
}