破解前的软件界面
找到安装目录
tagLyst
是基于electron
技术开发的工具,使用这种技术的软件很容易会被解包并查看到程序代码。
在tagLyst\run\resources
目录下,我们可以看到如下几个文件:
app.asar
default_app.asar
electron.asar
有关程序逻辑的主要代码在app.asar
中
app.asar 解包
electron
程序在发布的时候会通过asar
工具对源文件进行打包。首先通过下面的命令安装asar
工具(需要提前安装nodejs环境)
npm install -g asar
下面我们进行解包操作
asar extract app.asar ./app
通过这个命令把app.asar
文件解包到./app
下面
代码格式化
解包之后虽然我们能够看到代码,但是代码格式上却乱七八糟的,通过安装下面的工具对源码进行格式化操作
npm install --g prettier
安装成功后, 把我们可能需要分析的源码进行格式化
prettier --write app/main.js
prettier --write app/
开启调试功能
打开package.json文件
{
"name": "tagLyst Next",
"version": "3.1.0",
"main": "main.js",
"dependencies": {
"request": "^2.88.0"
}
}
通过查看package.json
文件,得知主程序文件为main.js
我们在main.js
文件中找到createWindows
函数,然后在合适的位置上添加下面的代码来启动调试功能。调试功能启用之后我们就可以像调试前端的方式去调试tagLyst
工具,并分析其破解过程。
function createWindows() {
...
mainWindow.webContents.openDevTools();
...
}
注册分析
打开tagLyst
工具,点击上面的购买升级
按钮,随便填入一个邮箱和一串激活码,并点击在线激活
按钮,我们发现会注册失败,对话框提示激活码并不正确。
cd tagLyst\run\resources\app
grep "激活码并不正确" . -rn
这里使用grep
工具搜索关键词(你可以使用任何搜索工具来操作)
这里我们找到了很多个选择,怎么确认哪一个才是呢,我也没有很好的办法,只能适当修改下搜索到的地方,并测试查看输出。
一次次尝试,很快就找到了地方,就是在app1.js
的Zc3baa406ebb4e97e02569e3c6e7a7a93
中
Zc3baa406ebb4e97e02569e3c6e7a7a93 = {
...
MSG_ACTIVATION_FAIL_DENIED:
"<h2>很遗憾!</h2>您输入的激活码并不正确 xxxx。请检查拼写。<br/>如有问题,请联系我们获得支持。",
...
}
我们继续搜索MSG_ACTIVATION_FAIL_DENIED
grep "MSG_ACTIVATION_FAIL_DENIED" . -rn
随后我们看到一个很关键的地方
而这段代码的else部分是这样的
所以这个变量e
就是成功的关键,那么这个e
是怎么过来的呢?
答案是runActivation
我们在runActivation
上面添加一个断点或者使用debugger
debugger;
一步一步执行到runActivation
函数里面
服务器返回数据
经过分析,我大概理解了这个代码,下面用注释的方式来解释:
function runActivation(e, t, n) {
// e: 邮箱地址
// t: 激活码
// n: 返回函数
try {
saveActivationEvidence(
"evidence1",
"from " + new Date(fs.statSync(__exePath).birthtime).format("yyyyMMdd")
);
} catch (e) {}
try {
saveActivationEvidence("evidence2", JSON.stringify(tglData.dashboard));
} catch (e) {}
if (SSO.noTrack) return console.log("No Track for ACT");
var a = nz(e, "").toLowerCase(),
i = nz(t, {});
try {
var l = nz(n, function () {}),
s = getDeviceInfo(a), // 获取设备ID信息
o = s.deviceID,
r = s.deviceTextEnc,
c = require("http"), // 发送POST请求使用
d = { // 把机器信息组包为json数据
data: JSON.stringify({
activationCode: a,
deviceTextEnc: r,
deviceID: o,
userID: nz(i.userID),
tag1: i.tag1,
tag2: i.tag2,
tag3: i.tag3,
evidence1: loadActivationEvidence("evidence1", ""),
evidence2: loadActivationEvidence("evidence2", ""),
evidence3: loadActivationEvidence("evidence3", ""),
}),
};
d = JSON.stringify(d);
// 构建POST请求数据
var f = {
method: "POST",
host: Pd5e58e4fab1ba92ff7d0bb4ce2459058.activationHost, // activation.taglyst.com
port: Pd5e58e4fab1ba92ff7d0bb4ce2459058.activationPort, // 12050
path: "/activate.do",
headers: {
"Content-Type": "application/json",
"Content-Length": d.length,
},
},
// 这个函数很关键,它是发送POST请求服务器的回复数据
// 当激活码无效时的返回数据{"error":"denied"}
// denied 是不是比较眼熟?没错就是提示激活码不正确前的那个e的值
g = function (e) {
try {
console.log('request: ' + e);
var t = JSON.parse(e); // 服务器返回的数据
n = nz(t.userID, ""); // 用户ID
i = nz(t.expiringDate, ""); // 激活码到期日
s = nz(t.error, ""); // error为denied
// 下面就是判断是不是Business的版本,该值是由服务器返回,并把数据写入到配置文件中
(Pd5e58e4fab1ba92ff7d0bb4ce2459058.bizc = nz(t.isBusinessClass, 0)
? 1
: 0),
Pd5e58e4fab1ba92ff7d0bb4ce2459058.bizc
? writeProjectData("vcls", "bizc")
: removeProjectData("vcls"); // 把bizc写入到[计算机名.vcls]文件中
nz(t.userRole, "").toLowerCase();
var o = nz(t.teamID, ""); // teamID 暂时没有理解是干啥的
// 下面就是保存数据并赋值服务器的数据
o &&
(writeProjectData(
"temd",
nz(Pd5e58e4fab1ba92ff7d0bb4ce2459058.temd, "")
),
(Pd5e58e4fab1ba92ff7d0bb4ce2459058.temd = o)),
n && // 保存用户ID
(writeProjectData("usrd", n),
(Pd5e58e4fab1ba92ff7d0bb4ce2459058.usrd = n)),
i &&
((Pd5e58e4fab1ba92ff7d0bb4ce2459058.avdt = new Date(i)),
(Pd5e58e4fab1ba92ff7d0bb4ce2459058.actd = 1),
(Pd5e58e4fab1ba92ff7d0bb4ce2459058.lckd = 0),
ckacv2 && ckacv2(),
writeProjectData("avdt", i), // 保存到期时间
writeProjectData("actc", a)), // 保存邮箱地址
s &&
"locked" == s && // 感觉是服务器封杀用户用的
((Pd5e58e4fab1ba92ff7d0bb4ce2459058.lckd = 1),
writeProjectData("actc", ""),
writeProjectData(
"avdt",
new Date("2001-1-1").format("yyyy-MM-dd")
)),
l(s); // 执行回调函数
} catch (e) {
console.log(e);
}
};
if (i.isOffline) {
try {
var u = parseOfflineActivationCode(a, o);
} catch (e) {
l("offlineFailed");
}
return (
cerr("OfflineActivationResData", JSON.stringify(u)),
void g(JSON.stringify(u))
);
}
(req = c.request(f, function (e) { // 发送POST请求
if (!a)
return console.log(
"* Activation Res will not work at startup without ActivationCode."
);
e.setEncoding("utf8"),
e.on("data", g), // 服务器回复数据,并调用上面的g函数
e.on("end", function () {
console.log("res ends.");
}),
e.on("error", function (e) {
console.error("res error:", e), l("failure");
});
})),
req.setTimeout(1e4, function () {
console.log("req timed out"), l("failure");
}),
req.on("error", function (e) {
console.log("req error", e), l("failure");
}),
req.write(d),
req.end();
} catch (e) {
console.log(e);
}
}
构思破解
- 当我们点击
在线激活
按钮时,会发送一个POST数据到http://activation.taglyst.com:12050/activate.do
服务器中,这些数据包含邮箱号,激活码,机器ID等信息。
- 随后服务器会返回一个
json
数据,如果激活码是无效的就返回 {"error":"denied"}
, 否则就返回正确的验证数据
- 所以我们可以尝试在服务器返回时,构造一个假数据来欺骗软件说服务器通过验证了,正确的验证信息是某某某
尝试破解
...
g = function (e) {
try {
console.log('request: ' + e);
var t = JSON.parse(e);
t.userID = "52pojie"; // 构造假ID
t.expiringDate = "2999/1/1"; // 到期时间
t.error = ""; // 清空error
t.isBusinessClass = 1; // 商业版本
n = nz(t.userID, "");
i = nz(t.expiringDate, "");
s = nz(t.error, "");
console.log("avdt:", i, s);
...
通过测试居然发现成功的破解了软件!!令人诧异。。。一个简单的欺骗就结束了??
重新打包
asar pack app app.asar
破解成果图
完结,撒花~~~
后语
通过此次分析,我们可想而知软件安全的重要性,开发者在考虑软件授权时应该要考虑的更多一些才行。tagLyst
虽然使用了服务器验证,但是返回数据居然是json
,而且是没有加密的数据,这是一个很严重的问题。
对于此次分析,我给出的建议是:
- 软件授权部分不应该使用容易被还原代码的语言设计(例如:JavaScript、python、java等),而且使用c/c++之类不容易被还原代码的语言设计(electron支持c语言开发的模块)
- 网络授权的api应该要加密数据,最好采用非对称加密方式来提高数据安全
- 授权部分的代码采用代码混淆技术来提高被反破解的难度
- 授权代码不仅仅采用一个api来验证,最好采用不用的接口在不同的地方或时间来验证
- c/c++语言设计授权验证,可以添加反调试机制、花指令、动态代码等多种方式来提高安全性