前言
最近马上开学了,闲的无聊,无意中得到一个软件,
开始,打开看是一个微软的软件,然后 丢OD,丢ida都不行
我开始还以为,然后看属性页是微软的,心里想 微软公司这么牛逼,这俩都调试不了
一、属性页写着微软?
右键一看,Company:Microsoft Corporation,产品名 Internet Explorer。
【md图片截图】
微软什么时候开发b-c录单软件了?
当然是假的。但假得有讲究——不是改的,是 Windows 自带的 IExpress 打包器(System32\iexpress.exe,每台 Windows 都有)生成的。IExpress 打包时直接复用微软自家的 wextract.exe 当外壳,版本资源原样继承,所以全世界的 IExpress 包属性页都长一个样。
实锤靠结构。文件 0x137E8 偏移处有标准 CAB 头(MSCF),cbCFHeader=0x2C 是 IExpress 专属值,再加上签名一栏是空的——微软真发的包不可能没签。判定成立。
拆法很朴素,把 CAB 切出来用系统自带 tar 读:
$fs=[IO.File]::OpenRead($exe); $fs.Position=0x137E8
# 切出 115,964,386 字节 → payload.cab
tar.exe -tf payload.cab
# runtime.7z | launcher.vbs | 7z.exe | 7z.dll
二、 一层一层扒
launcher.vbs 的逻辑:把 7z 解压到 %LOCALAPPDATA%\ZhddBaodanSingleRun\run-随机数,取目录里第一个 .exe 运行。每次运行留 300MB 垃圾不清(运行计数 669 次),"取第一个 exe"的写法还欢迎本地恶意程序抢跑。
asar 是 Electron 的资源格式,头是 JSON 树,数据按偏移排后面。手写了个解析器:
const headerPickleSize = buf.readUInt32LE(4);
const dataOffset = 8 + headerPickleSize;
const header = JSON.parse(buf.slice(16, 16 + buf.readUInt32LE(12)).toString('utf8'));
// 递归遍历 header.files,按 offset/size 从 dataOffset 切数据
三、asar解包后全部文件解包出来,然后惊喜了——源码是明文的。中文目录名、没混淆的 JS、带注释,甚至还有二十多个单元测试文件。技术栈 Electron 38.4.0 + koffi + nut-tree,业务是六合*盘口录单:录单、兑奖、风控、套利、微信自动录单。
直接源码,dump
【截图目录】
四、 这包被人动过手脚
然后我就审查源码,在里面翻到一份不该存在的文件 04_asar_extract_tolerant_report.json,是 asar 解包作业日志,路径写着:
E:\baodanxitongup\智慧订单相关\01_原生智慧订单\逆向解包\智慧订单3.10.118.0逆向解包_20260519
厚礼蟹,这发行包是有人对 3.10.118.0 逆向解包后重新组装的。有人搞过他,并且这个包是破解版的
其中看这个文件 。integrity-check.cjs 本该做完整性校验,实际是个恒返回 true 的空壳:
function safeCheck() {
return { safe: true, valid: true, success: true, issues: [], errors: [] };
}
module.exports = new Proxy({}, { get() { return safeCheck; } });
真正的校验逻辑编成了 .jsc 字节码躺在旁边,没人加载。加密狗检测 dongle.cjs 同样是空壳,加密狗的 DLL 还没删干净。外壳版本资源 3.10.118.0,package.json 里 3.20.120,对不上。
五、卡密体系,纸糊的
这软件靠卡密收费。卡长这样:
CLS-G00A5-xxxxx-xxxxxx-GLOBAL00-xxxxxxxxxx
签名算法在 license.cjs 里,明文:
// L11 —— 密钥跟着代码一起发行
const SECRET = "***";
// L218 —— "签名"就是拿密钥拼串喂给自研哈希
function signCard(parts) {
return shortHash([SECRET, parts.kind, String(parts.days),
parts.issued, parts.nonce, parts.machineToken].join("|"), 12);
}
// L58 —— shortHash:FNV-1a 变体 + murmur 变体拼 base36,非加密哈希
function shortHash(input, length = 12) {
let h1 = 0x811c9dc5, h2 = 0x9e3779b9;
for (const c of input) { /* 两个 32 位乘法哈希 */ }
return (h1.toString(36) + h2.toString(36)).toUpperCase().slice(0, 12);
}
L248 还有 createCardCode——发卡器本体,直接导出的。整段长这样:
function createCardCode(options = {}) {
const mode = options.mode === "bound" ? "bound" : "generic";
const kind = mode === "bound" ? "B" : "G";
const days = Math.max(1, Number.parseInt(options.days, 10) || 0);
const issued = (Number.parseInt(options.issuedAtSec, 10) || Math.floor(Date.now() / 1000))
.toString(36)
.toUpperCase();
const nonce = String(options.nonce || randomToken(6))
.toUpperCase()
.replace(/[^A-Z0-9]/g, "")
.slice(0, 6)
.padEnd(6, "0");
const token = machineToken(options.machineCode, mode);
const signature = signCard({
kind, days, issued, nonce, machineToken: token
});
return `${LICENSE_PREFIX}-${kind}${encodeDays(days)}-${issued}-${nonce}-${token}-${signature}`;
}
哈哈哈,看出来问题了吧:签发时间取本机时钟,随机数本地生成,签名本地算——从输入到输出没有一步碰网络。这函数在应该待在服务端,现在躺在客户端的导出表里,L1023 还以 generateCardCode 的名字再导出了一份。
有效期更离谱。激活时写到本地明文 JSON:
// L696
expiresAtMs: baseTime + parsed.days * DAY_MS,
而启动检查只验卡号字符串的签名,到期判断直接读这个不参与签名的字段:
// L432 —— 唯一被密码学验证的是卡号字符串本身
const parsed = record?.licenseKey ? parseCardCode(record.licenseKey) : null;
// L438 —— 到期判断读的是明文 JSON 里的数字
const expired = Boolean(record) && record.expiresAtMs <= Date.now();
防重用也是纯本机:used-cards.json 只存在每台机器自己的 userData 里。在线校验整套空操作:
// L964
function mustVerifyOnline() { return false; }
// L801 —— 所谓"在线验证"就是本地 getStatus 换了个名字
function onlineVerify(appRoot) { return runtimeVerify(appRoot); }
我写了个 PoC,只调它自己的导出函数:
const L = require('.../electron/license.cjs');
const card = L.createCardCode({ days: 365 }); // 现场发卡
L.activateLicense(root, card); // 激活
// 然后改 local-license.json 里的 expiresAtMs 到 2099 年
跑出来的结果:
现场生成的年卡卡号: CCLS-G00A5-xxxxx-xxxxxx-GLOBAL00-xxxxxxxxxx
{"valid":true,"status":"可激活","message":"年卡校验通过"}
{"success":true,"message":"年卡激活成功","remainingDays":365}
篡改后 -> {"valid":true,"status":"已授权","remainingDays":26788}
同卡换目录再激活 -> {"success":true,"message":"年卡激活成功"}
然后我拿去激活,成功
【md激活】
六、 有混淆?也拆一下
bootstrap.cjs 钩住 Module._compile,对 ZHENC001 开头的文件做 AES 解密。原文件长这样(节选,混淆后的样子):
const ALGORITHM=_0x1e417f(0x150,'IE@0'),KEY_SEED=_0x1e417f(0x134,'IE@0'),
ENCRYPTED_MARKER=Buffer[_0x1e417f(0x146,'OPtt')](_0x1e417f(0x157,'L3iT'));
写了个 harness 把它的字符串数组和 RC4 解码器抽出来跑,常量全还原:
ALGORITHM = "aes-256-cbc"
KEY_SEED = "zhihui_order_asar_protection_2025_v1"
MARKER = "ZHENC001"
key = sha256(KEY_SEED + "_key") // 32 字节
iv = md5(KEY_SEED + "_iv") // 16 字节
然后全包扫 ZHENC001 头——零个文件。加密器装好了,忘了加密,就挺离谱。
其他十来个混淆文件(server.mjs、preload.cjs、微信发送模块)用了字符串数组 + RC4。解法迭代三版:暴力枚举有太多坑,最后改成先从源码解析调用点配对再精确解码,150 对命中 148。还原出的内容包括:微信发消息走 koffi 调 FindWindowW + 剪贴板粘贴 + 模拟按键,找不到窗口就落 ps1 用 -ExecutionPolicy Bypass 跑;本地 server 的 HTTPS 证书默认口令 changeit;还有个 wxapi 微信网关绑定 0.0.0.0:8062 无鉴权,同网段谁都能控制你的微信。
七、 学到什么
其实文中大多都是踩了很多坑,不过让我见识到了,win 95 后,系统自带的IExpress打包器,这也算刷新了一个知识点,其他的就大多看代码解出来的,收工!!