某VSCode主题插件的破解
0. 基本情况
VSCode的插件位置都是固定的。为 ~/.vscode/extensions/<extension-id>,这里倒是很干净。

到build文件夹翻一翻,也就index.js值得注意,其他都是对应的主题文件,甚至是明文放在那的。
说实话,我自己搓一个主题插件,引用这里的资源就解决了。
但是为了表示我的尊重,还是看看这个js文件吧。
1. 去混淆
格式化之后大概长这个样子:

很明显的,这是 javascript-obfuscator 的效果,不过又有点差别。
注意到,a0d是核心的字符串解密函数。
那么首先看看a0d。
其他那些比如ry之类的比较好处理。无论是写一个ast transform还是简简单单用正则替换就行了。
function a0d(h, H) {
h = h - (-0x1 * 0xb33 + 0xd16 + 0x11 * 0x1);
var j = a0G();
var X = j[h];
if (a0d["BKJzYs"] === undefined) {
var L = function (w) {
var d =
"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789+/=";
var i = "",
l = "",
Y = i + L,
b =
("" +
function () {
return -0xa96 + -0x1178 + 0x7e * 0x39;
})["indexOf"]("\x0a") !== -(0x17a3 + 0x1b5b + -0x32fd);
for (
var g = 0x174 * -0x11 + 0x20 * -0x50 + -0x115a * -0x2,
W,
I,
N = -0x1f2 + -0x137 * 0x6 + -0x49e * -0x2;
(I = w["charAt"](N++));
~I &&
((W =
g % (-0xca6 + 0x2a1 * -0x5 + 0x19cf)
? W * (0x26f4 + 0x1a91 + -0x31 * 0x155) + I
: I),
g++ % (0x8a4 + -0x6 * 0x30f + 0x9ba))
? (i +=
b ||
Y["charCodeAt"](N + (0x2511 + -0x57f + 0x1f88 * -0x1)) -
(-0x89f * 0x1 + -0x1 * -0x22c5 + 0x22d * -0xc) !==
0x5b3 * -0x1 + 0x195c + 0x2cf * -0x7
? String["fromCharCode"](
(-0xe7d + -0x89c * 0x1 + 0x1818) &
(W >>
((-(-0x1d47 * 0x1 + 0x1 * -0xeed + 0x2c36) * g) &
(0x503 * 0x6 + -0x1567 + -0x8a5))),
)
: g)
: 0x1823 * -0x1 + -0x283 + -0x8e2 * -0x3
) {
I = d["indexOf"](I);
}
for (
var y = -0x131 * -0xa + 0x1753 * 0x1 + -0x233d, z = i["length"];
y < z;
y++
) {
l +=
"%" +
("00" +
i["charCodeAt"](y)["toString"](-0xe5c + 0x137b + 0x5 * -0x103))[
"slice"
](-(0x1 * 0x1a9f + 0xfcb * 0x1 + -0x2a68));
}
return decodeURIComponent(l);
};
var A = function (w, d) {
var k = [],
l = 0x2558 + 0x4f0 * 0x2 + -0x2f38,
Y,
b = "";
w = L(w);
var g;
for (
g = 0xa72 + -0x1cf0 + 0x127e;
g < -0x7 * -0x4a7 + 0x14fa + -0x348b;
g++
) {
k[g] = g;
}
for (
g = 0xe51 + 0x32a * -0x8 + 0x1 * 0xaff;
g < -0x1 * -0x126c + -0x1342 * -0x1 + -0x24ae;
g++
) {
((l =
(l + k[g] + d["charCodeAt"](g % d["length"])) %
(0x2 * -0xc35 + -0xc5e * 0x3 + -0xfa1 * -0x4)),
(Y = k[g]),
(k[g] = k[l]),
(k[l] = Y));
}
((g = -0x10 * 0x97 + -0x8eb + 0x125b),
(l = 0x11 * 0x194 + -0xd3 * -0x4 + -0x1e20));
for (var W = 0x88d * -0x4 + -0xd78 + 0x36 * 0xe2; W < w["length"]; W++) {
((g =
(g + (0xa * 0xc9 + 0xfd0 + -0x7e3 * 0x3)) %
(0x20f0 + -0x162 * -0x16 + -0xf97 * 0x4)),
(l = (l + k[g]) % (-0x426 + -0xa * 0x369 + -0x40 * -0x9d)),
(Y = k[g]),
(k[g] = k[l]),
(k[l] = Y),
(b += String["fromCharCode"](
w["charCodeAt"](W) ^
k[(k[g] + k[l]) % (0x1 * -0x26c9 + -0x156a + 0x3d33)],
)));
}
return b;
};
((a0d["JspkdI"] = A), (a0d["JHiKqS"] = {}), (a0d["BKJzYs"] = !![]));
}
var s = j[0x20ef + 0x1d7a + -0x3e69],
r = h + s,
G = a0d["JHiKqS"][r];
if (!G) {
if (a0d["yoYzxQ"] === undefined) {
var w = function (d) {
((this["xvjjSz"] = d),
(this["qZqPMr"] = [
0xee2 + 0x11f * 0x1f + -0x31a2,
0x63d * 0x1 + -0x579 * 0x6 + 0x1a99,
-0x1c8b + -0x1 * 0xf0 + 0x1d7b,
]),
(this["jmlwIp"] = function () {
return "newState";
}),
(this["VRWpfs"] = "\x5cw+\x20*\x5c(\x5c)\x20*{\x5cw+\x20*"),
(this["oIYdZf"] = "[\x27|\x22].+[\x27|\x22];?\x20*}"));
};
((w["prototype"]["iBcuGd"] = function () {
var d = new RegExp(this["VRWpfs"] + this["oIYdZf"]),
i = d["test"](this["jmlwIp"]["toString"]())
? --this["qZqPMr"][0x197a * 0x1 + 0x2 * -0x141 + -0x1 * 0x16f7]
: --this["qZqPMr"][-0x83 * -0x28 + 0x12ad + -0x2725];
return this["HAVcXs"](i);
}),
(w["prototype"]["HAVcXs"] = function (d) {
if (!Boolean(~d)) return d;
return this["lMPBik"](this["xvjjSz"]);
}),
(w["prototype"]["lMPBik"] = function (d) {
for (
var k = -0x239d + -0x1c37 + 0x3fd4, l = this["qZqPMr"]["length"];
k < l;
k++
) {
(this["qZqPMr"]["push"](Math["round"](Math["random"]())),
(l = this["qZqPMr"]["length"]));
}
return d(this["qZqPMr"][-0x23d3 + -0x5b1 + 0x2984]);
}),
("" +
function () {
return 0x19ed + 0x1ecf + -0x38bc;
})["indexOf"]("\x0a") === -(0xd98 + 0x26f6 + 0x4c7 * -0xb) &&
new w(a0d)["iBcuGd"](),
(a0d["yoYzxQ"] = !![]));
}
((X = a0d["JspkdI"](X, H)), (a0d["JHiKqS"][r] = X));
} else X = G;
return X;
}
注意到,这是一个RC4解密。
那么根据经验,前面肯定有对常量池的重排序。
其中显然,常量池与函数a0G相关。
那么先重命名。
VSCode的F2很好用,它不是简单的替换,还有作用域分析,不会改错。
另外,后面的重命名就不提了,反正名字这个事也不算太要紧。
此外,显然,代码中还有反格式化检测。这个也要拔除掉。
重写并利用一些新特性,结果如下:
function modifiedBase64Decode(input) { // 非标准的base64,这里把大小写互换了
const swapped = input.replace(/[a-zA-Z]/g, (char) =>
char <= "Z" ? char.toLowerCase() : char.toUpperCase()
);
const binaryStr = atob(swapped);
const bytes = new Uint8Array(binaryStr.length);
for (let i = 0; i < binaryStr.length; i++) {
bytes[i] = binaryStr.charCodeAt(i);
}
return new TextDecoder().decode(bytes);
}
function rc4Decrypt(encryptedData, key) {
const data = modifiedBase64Decode(encryptedData);
const sBox = Array.from({ length: 256 }, (_, i) => i);
let j = 0;
for (let i = 0; i < 256; i++) {
j = (j + sBox[i] + key.charCodeAt(i % key.length)) % 256;
[sBox[i], sBox[j]] = [sBox[j], sBox[i]];
}
let i = 0;
j = 0;
let result = "";
for (let k = 0; k < data.length; k++) {
i = (i + 1) % 256;
j = (j + sBox[i]) % 256;
[sBox[i], sBox[j]] = [sBox[j], sBox[i]];
const keyByte = sBox[(sBox[i] + sBox[j]) % 256];
const decryptedChar = String.fromCharCode(data.charCodeAt(k) ^ keyByte);
result += decryptedChar;
}
return result;
}
function rc4Decode(index, password) {
if (!rc4Decode.cache) {
rc4Decode.cache = {};
}
const realIndex = index - 500;
if (rc4Decode.cache[realIndex]) {
return rc4Decode.cache[realIndex];
}
const constantPool = getConstantPool();
const encryptedItem = constantPool[realIndex];
const decryptedItem = rc4Decrypt(encryptedItem, password);
rc4Decode.cache[realIndex] = decryptedItem;
return decryptedItem;
}
rc4Decode["BKJzYs"] = true; // 补环境,以防万一
rc4Decode["yoYzxQ"] = true;
rc4Decode["JHiKqS"] = rc4Decode.cache;
rc4Decode["JspkdI"] = rc4Decrypt;
让后就是想办法去掉字符串加密了,首先,内联掉那些转发器,同时完成常量折叠。这里我让AI写了一个脚本来处理:
#!/usr/bin/env npx ts-node
/**
* 内联 & 常量折叠工具
*
* 内联:将简单转发函数替换为被调用的目标函数。
* 常量折叠:对可静态求值的表达式(算术、位运算、比较等)直接计算结果。
*
* 处理顺序:
* 1. 识别并内联符合模式的函数调用(不在输出中删除函数定义)。
* 2. 遍历 AST,将仅包含字面量/运算符的表达式替换为计算结果。
*
* 用法:
* npx ts-node inline-fold.ts <input.js> [output.js]
* 若省略 output.js,结果输出到 stdout。
*/
import * as ts from 'typescript';
import * as fs from 'fs';
import * as path from 'path';
interface InlineInfo {
paramNames: string[]; // 形参名称列表
innerCallee: ts.Expression; // return 语句中的被调函数 AST
innerCallArgs: ts.NodeArray<ts.Expression>; // return 语句中的实参 AST
innerSymbol: ts.Symbol; // 被调函数的符号(用于可见性判断)
}
/**
* AST 节点深拷贝,兼容 TypeScript 5.x / 6.x。
* 对标识符、数字、字符串等叶子节点显式创建新节点,
* 其余通过递归遍历拷贝。
*/
function cloneNode<T extends ts.Node>(node: T, context: ts.TransformationContext): T {
if (ts.isIdentifier(node)) {
return ts.factory.createIdentifier(node.text) as unknown as T;
}
if (ts.isNumericLiteral(node)) {
return ts.factory.createNumericLiteral(node.text) as unknown as T;
}
if (ts.isStringLiteral(node)) {
return ts.factory.createStringLiteral(node.text) as unknown as T;
}
// 通用递归拷贝(用于 BinaryExpression 等复合节点)
const visitor = (n: ts.Node): ts.Node => ts.visitEachChild(n, (child) => child, context);
return ts.visitNode(node, visitor) as T;
}
/**
* 尝试对表达式求值,成功则返回相应的字面量节点,否则返回 undefined。
* 支持数字、字符串、布尔值以及常见运算符。
*/
function tryEvaluate(node: ts.Expression): ts.Expression | undefined {
const result = evaluate(node);
if (result === undefined) return undefined;
if (typeof result === 'number') {
if (result >= 0) {
return ts.factory.createNumericLiteral(String(result));
}else{
return ts.factory.createPrefixUnaryExpression(ts.SyntaxKind.MinusToken, ts.factory.createNumericLiteral(String(-result)))
}
}
if (typeof result === 'boolean') {
return result ? ts.factory.createTrue() : ts.factory.createFalse();
}
if (typeof result === 'string') {
return ts.factory.createStringLiteral(result);
}
return undefined;
}
function evaluate(node: ts.Expression): number | string | boolean | undefined {
// 字面量
if (ts.isNumericLiteral(node)) return Number(node.text);
if (ts.isStringLiteral(node)) return node.text;
if (node.kind === ts.SyntaxKind.TrueKeyword) return true;
if (node.kind === ts.SyntaxKind.FalseKeyword) return false;
// 括号
if (ts.isParenthesizedExpression(node)) return evaluate(node.expression);
// 一元运算
if (ts.isPrefixUnaryExpression(node)) {
const operand = evaluate(node.operand);
if (operand === undefined) return undefined;
switch (node.operator) {
case ts.SyntaxKind.PlusToken:
return +operand;
case ts.SyntaxKind.MinusToken:
return -(operand as number);
case ts.SyntaxKind.TildeToken:
return ~(operand as number);
case ts.SyntaxKind.ExclamationToken:
return !operand;
default:
return undefined;
}
}
// 二元运算
if (ts.isBinaryExpression(node)) {
const left = evaluate(node.left);
if (left === undefined) return undefined;
const right = evaluate(node.right);
if (right === undefined) return undefined;
const l = left as number,
r = right as number;
switch (node.operatorToken.kind) {
// 算术
case ts.SyntaxKind.PlusToken:
if (typeof left === 'string' || typeof right === 'string') return String(left) + String(right);
return l + r;
case ts.SyntaxKind.MinusToken:
return l - r;
case ts.SyntaxKind.AsteriskToken:
return l * r;
case ts.SyntaxKind.SlashToken:
return l / r;
case ts.SyntaxKind.PercentToken:
return l % r;
case ts.SyntaxKind.AsteriskAsteriskToken:
return l ** r;
// 位运算
case ts.SyntaxKind.AmpersandToken:
return l & r;
case ts.SyntaxKind.BarToken:
return l | r;
case ts.SyntaxKind.CaretToken:
return l ^ r;
case ts.SyntaxKind.LessThanLessThanToken:
return l << r;
case ts.SyntaxKind.GreaterThanGreaterThanToken:
return l >> r;
case ts.SyntaxKind.GreaterThanGreaterThanGreaterThanToken:
return l >>> r;
// 逻辑运算(返回 boolean)
case ts.SyntaxKind.AmpersandAmpersandToken:
return !!(l && r);
case ts.SyntaxKind.BarBarToken:
return !!(l || r);
// 比较运算
case ts.SyntaxKind.LessThanToken:
return l < r;
case ts.SyntaxKind.GreaterThanToken:
return l > r;
case ts.SyntaxKind.LessThanEqualsToken:
return l <= r;
case ts.SyntaxKind.GreaterThanEqualsToken:
return l >= r;
case ts.SyntaxKind.EqualsEqualsToken:
return left == right;
case ts.SyntaxKind.ExclamationEqualsToken:
return left != right;
case ts.SyntaxKind.EqualsEqualsEqualsToken:
return left === right;
case ts.SyntaxKind.ExclamationEqualsEqualsToken:
return left !== right;
default:
return undefined;
}
}
// 条件表达式 (a ? b : c) —— 可选支持,这里先忽略
return undefined;
}
function main() {
const args = process.argv.slice(2);
if (args.length === 0) {
console.error('Usage: inline-functions <input.js> [output.js]');
process.exit(1);
}
const inputFile = path.resolve(args[0]);
const outputFile = args[1] ? path.resolve(args[1]) : undefined;
// --- 1. 建立 Program 与 TypeChecker ---
const program = ts.createProgram([inputFile], {
allowJs: true,
target: ts.ScriptTarget.Latest,
});
const sourceFile = program.getSourceFile(inputFile);
if (!sourceFile) {
console.error('Could not read source file:', inputFile);
process.exit(1);
}
const checker = program.getTypeChecker();
// --- 2. 收集所有符合模式的函数声明 ---
const inlineInfos = new Map<ts.Symbol, InlineInfo>();
function collectFunctions(node: ts.Node) {
if (ts.isFunctionDeclaration(node) && node.body && node.name) {
const { statements } = node.body;
if (statements.length === 1 && ts.isReturnStatement(statements[0])) {
const returnExpr = statements[0].expression;
if (returnExpr && ts.isCallExpression(returnExpr)) {
const paramNames: string[] = [];
let allSimple = true;
for (const p of node.parameters) {
if (ts.isIdentifier(p.name)) {
paramNames.push(p.name.text);
} else {
allSimple = false;
break;
}
}
if (allSimple) {
const funcSymbol = checker.getSymbolAtLocation(node.name);
const innerSymbol = checker.getSymbolAtLocation(returnExpr.expression);
if (funcSymbol && innerSymbol) {
inlineInfos.set(funcSymbol, {
paramNames,
innerCallee: returnExpr.expression,
innerCallArgs: returnExpr.arguments,
innerSymbol,
});
}
}
}
}
}
ts.forEachChild(node, collectFunctions);
}
collectFunctions(sourceFile);
if (inlineInfos.size === 0) {
console.log('No inline candidates found.');
}
// --- 3. 收集调用点并进行作用域可见性分析 ---
const callToInfo = new Map<ts.CallExpression, InlineInfo>();
const visibleCalls = new Set<ts.CallExpression>();
const totalCallCount = new Map<ts.Symbol, number>();
const inlineCallCount = new Map<ts.Symbol, number>();
function collectCalls(node: ts.Node) {
if (ts.isCallExpression(node) && ts.isIdentifier(node.expression)) {
const symbol = checker.getSymbolAtLocation(node.expression);
if (symbol && inlineInfos.has(symbol)) {
const info = inlineInfos.get(symbol)!;
totalCallCount.set(symbol, (totalCallCount.get(symbol) ?? 0) + 1);
const symbolsInScope = checker.getSymbolsInScope(node, ts.SymbolFlags.All);
if (symbolsInScope.includes(info.innerSymbol)) {
callToInfo.set(node, info);
visibleCalls.add(node);
inlineCallCount.set(symbol, (inlineCallCount.get(symbol) ?? 0) + 1);
}
}
}
ts.forEachChild(node, collectCalls);
}
collectCalls(sourceFile);
if (visibleCalls.size === 0) {
console.log('No visible calls to inline.');
}
// --- 4. AST 变换:替换调用 & 移除无调用的函数定义 ---
const inlineTransformer = (context: ts.TransformationContext): ts.Transformer<ts.SourceFile> => {
return (root: ts.SourceFile) => {
function visitor(node: ts.Node): ts.Node | undefined {
// 4. 替换内联调用
if (ts.isCallExpression(node) && visibleCalls.has(node)) {
const info = callToInfo.get(node)!;
// 递归替换表达式中的形参标识符为对应实参
const replaceParams = (expr: ts.Expression): ts.Expression => {
const replacer = (n: ts.Node): ts.Node => {
if (ts.isIdentifier(n)) {
const idx = info.paramNames.indexOf(n.text);
if (idx !== -1) {
return cloneNode(node.arguments[idx], context);
}
}
return ts.visitEachChild(n, replacer, context);
};
return replacer(expr) as ts.Expression;
};
const newCallee = replaceParams(info.innerCallee);
const newArgs = info.innerCallArgs.map((arg) => replaceParams(arg));
return ts.factory.createCallExpression(newCallee, undefined, newArgs);
}
return ts.visitEachChild(node, visitor, context);
}
return ts.visitNode(root, visitor) as ts.SourceFile;
};
};
const constFoldTransformer = (context: ts.TransformationContext): ts.Transformer<ts.SourceFile> => {
return (root) => {
function visitor(node: ts.Node): ts.Node {
// 如果是表达式且可完全求值,替换为字面量
if (ts.isExpression(node)) {
const folded = tryEvaluate(node);
if (folded) return folded;
}
return ts.visitEachChild(node, visitor, context);
}
return ts.visitNode(root, visitor) as ts.SourceFile;
};
};
const result = ts.transform(sourceFile, [inlineTransformer, constFoldTransformer]);
const transformed = result.transformed[0] as ts.SourceFile;
const printer = ts.createPrinter({ newLine: ts.NewLineKind.LineFeed });
const output = printer.printFile(transformed);
result.dispose();
// --- 5. 输出结果 ---
if (outputFile) {
fs.writeFileSync(outputFile, output, 'utf-8');
console.log(`Output written to ${outputFile}`);
} else {
console.log(output);
}
}
function outputOrPrint(inputFile: string, outputFile?: string) {
const content = fs.readFileSync(inputFile, 'utf-8');
if (outputFile) {
fs.writeFileSync(outputFile, content, 'utf-8');
} else {
console.log(content);
}
}
main();
可靠性我不保证。建议使用时用每一步都用git commit一下,免得出问题无法挽回。
循环几次,就能还原的大差不差。

之后先看看第一个函数。它的目的是改变参数池的顺序,得到特定的样子。
提取一下对应代码,在控制台跑出来结果就行,让后替换到常量池。删掉这个函数就行。
让后用正则提取rc4Decode的调用,把这个送进控制台,就可以得到他们的真实值。让后再替换回去,就得到了比较清晰的代码了。至少勉强能看懂了。


2. 分析逻辑
搜索 trial 可以找到对应的字符串的位置。主题名称已经抹除。

根据此,找到 L16020-16200 的 a0s3 函数。大致扫一眼,这是管理试用期的函数。它检查试用是否到期,未到期则继续,首次安装则激活试用,如果已经激活直接退出。

大致的逻辑就在上面了,依据注释就能读懂,但还是给一下文字讲解。
参数 h 是 vscode 扩展上下文。
首先, L16105 处,会从vscode secret读取到一个参数 xxxTheme.licenseKey,如果已经输入 licenseKey 就通过验证。
然后,L16106 获取记录的试用开始日期,如果没有就触发分支启动试用,并且记录时间。
时间为 UTC 字符串的 Base64 编码。
如果过期,触发 L16123 处逻辑,强迫激活、卸载二选一。L16135 真的是自卸载。
好家伙,白嫖连基础版都没有吗?
最后是试用有效的分支,它又启动了一个定时任务,每天调用自己。
这样其实总体上没有空子钻,因为 vscode 的 secret 还是比较安全的。
此外,再看看激活对应的函数。上面可以看到这对应一个命令,可以直接找到这个命令的处理函数 a0LB ,位于 L15691-15723

可见,激活核心函数在 a0LV 。该函数位于 L15606-15664 。

这个是联网激活。a0LF 函数其实就真的只是把许可证提交上去,然后验证返回的jwt。jwt在代码中内置了公钥,所以也伪造不了。
具体许可证长什么样子也不知道,写注册机也没办法写。只知道通过某个渠道开头是 VSCVT。但是总共 18 位剩下的都完全不知道。
那么这条路大概走不通。
3. 解决
看起来时候无懈可击,真的就没办法了吗?
只能每次版本更新重新分析,改代码了吗?
我不甘心!
<br/>
那么回去看 a0s3 试用验证函数。注意到,这里的验证很宽松。
只要许可证的值不为空,或者当前时间不大于激活时间加七天,就可以通过。
那么我们就要想办法设置这几个secret。
但是设置secret需要得到对应插件的上下文。而vscode传递上下文是直接传递进插件模块的activate函数的。
那么,很好办,劫持activate就行。改一下日期,无限试用就行。
在原来文件的末尾,加上这一段代码即可:
// ---- 劫持开始 ----
const _originalActivate = module.exports.activate;
if (_originalActivate) {
module.exports.activate = async function (context) {
try {
// 1. 构造一个遥远的未来日期(UTC)
const futureDate = new Date('3000-12-30T00:00:00Z').toUTCString();
// 2. Base64 编码
const encoded = Buffer.from(futureDate).toString('base64');
// 3. 写入 Secret
await context.secrets.store('xxxTheme.trialActivation', encoded);
// 可选:同时写入许可证键,跳过整个试用到正版流程
await context.secrets.store('xxxTheme.licenseKey', 'vaildLicenseKey000');
} catch (e) {
console.error('trial hack failed:', e);
}
// 4. 调用原激活函数,不破坏插件功能
return _originalActivate.apply(this, arguments);
};
}
// ---- 劫持结束 ----
4. 其他
好久没碰逆向了,也是没办法。高中三年都没时间弄,现在也有点手生了。
这篇文章属于想到哪写哪,主要就是我的思考路径。可以看到,也走了一下不必要的弯路。
此外,不得不夸一夸AI在这方面的能力,确实很强。比如上面有部分脚本就是AI生成的。
另外,让AI看混淆后的代码是比人有优势的,因为无论名字再怎么恶心,在AI眼中和正常名称一样都是几个Token罢了。但人天生的就会忍不住理解变量名称的含义,让后和那些随机字符串大眼瞪小眼。
写的不算很好,还请不要苛责,多多包涵。
对了,大部分图片都是 avif 格式,如果你的浏览器加载不出来,说明太老了需要升级了。
最后,叠个甲:
仅用于个人学习研究,请支持正版软件。
如需长期使用,建议购买付费许可,以支持作者继续更新。