【经验记录】在解包HAS游戏CG时的一些过程记录
本帖最后由 horizonLd 于 2026-4-17 03:44 编辑写在前面
去年的时候解包了“HAS:我用催眠APP把全校男生变成了我的X奴”这个游戏,而最近也在解一些其他的,突然想起来之前解HAS的经历,感觉蛮有趣的,遂发个帖记录一下过程。
指路一下当时的资源帖,当时还不会编辑帖子,现在也已经翻新惹~
还是想不出来帖子放在哪个分区合适,只是个人向的心得记录帖,最后还是感觉在CODE板块里合适点()
分析游戏
结合游玩过程和游戏目录的文件架构,基本可以确定是RPG Maker MV构建出的产物,因为根目录里有nw或node相关的dll库,所以应该是RPG Maker MV构建出的NW.js程序。
NW.js感觉有点像Electron,就是用写网页的方式写软件,那根目录下的content文件夹内就是这个“网页”的内容了。
那就可以一眼锁定到其中的“data.pak”,因为它文件体积最大,至此便找到了资源包的位置。
解包资源
我的第一反应是想,它是不是通用的压缩格式,只不过改了个后缀而已?所以我就直接尝试用7-zip打开它,居然是可以正常打开的,那它就是通用压缩格式。
尝试解压的时候提示需要密码,那密码肯定是会存在游戏中的,而且是在尝试加载资源包的时候会用到这个密码。既然本质是网页项目,那么加载资源包的逻辑大概会存在main.js里的,顺藤摸瓜接着找就好了
然后在翻阅main.js的时候我先看到了这两个函数:
function stringEncrypter(str) {
if (typeof str != 'string') {
return str;
};
var strEncrypted = '';
var ccode = 0;
for (var i = 0; i < str.length; i++) {
ccode = str.charCodeAt(i);
strEncrypted += (String(ccode).length - 1 + String(ccode));
};
return strEncrypted;
};
这是一个针对字符串的加密函数,下面还有一个对应的解密函数我省略掉了,那么我要找的密码可能和这两个函数有关。
稍微分析一下这个加密,加密过程是对输入的字符串逐字符遍历,取每一个字符的Unicode编码,计算其编码的长度减1,然后再与这个Unicode编码拼在一起,就是单个字符的密文。比如A的Unicode编码是“65”,“65”长度为2,减1为1,所以“A”加密就是“165”
再往下是这样一个函数(有点长我就不直接展开了):
function loadPackageData(targetobj, filepath, options) {
if (RPGMakerMV.xhrSucceeded) {
if (!isObject(options)) {
options = {};
};
var xhr = new XMLHttpRequest();
xhr.responseType = 'blob';
xhr.open('GET', filepath);
xhr.overrideMimeType('application/zip');
xhr.onload = () => {
if (xhr.status < 400) {
var zipReader = new zip.ZipReader(new zip.BlobReader(xhr.response));
zipReader.getEntries().then(entries => {
if (entries && entries.length) {
for (var i = 0; i < entries.length; i++) {
if (!entries.directory) {
targetobj.filename] = entries;
targetobj.filename] = RPGMakerMV.compileKey(options);
};
};
};
}).finally(() => {
zipReader.close();
if (typeof options.onLoad === 'function') {
options.onLoad();
};
});
} else {
if (typeof options.onError === 'function') {
options.onError();
};
}
};
xhr.onerror = () => {
if (typeof options.onError === 'function') {
options.onError();
};
};
xhr.send();
} else {
var message = 'Your browser does not allow to read local files.';
RPGMakerMV.printError('Error', message);
};
};
那很明显了,资源包“data.pak”就是一个有密码的zip压缩包,中间读取资源那段用到的“options”也就是要找的密码相关内容了
然后我就搜索“options”,定位到:
options = RPGMakerMV.decompileKey(RPGMakerMV.packageKey)
继续搜索“decompileKey”,找到了:
RPGMakerMV.decompileKey = function name(key) {
key = key || '';
if (key == '') {
return '';
};
return stringDecrypter(window.atob(window.atob(key)));
};
核心其实就是“stringDecrypter(window.atob(window.atob(key)))”,然后继续搜索刚刚的“packageKey”,找到:
RPGMakerMV.packageKey = window.packageKey || 'TWpFeU1qSXhNVEl5TVRBNQ==';
那至此密文和解密方法都找到了,所以只需要用他写好的解密方法运行一下就ok了
把如下代码丢给浏览器的F12控制台:
function stringDecrypter(str) {
if (typeof str != 'string') {
return str;
};
var strDecrypted = '';
var ccode = 0;
var clength = 0;
for (var i = 0; i < str.length; i++) {
clength = Number(str) + 1;
ccode = Number(str.substring(i + 1, i + clength + 1));
strDecrypted += (String.fromCharCode(ccode));
i += (clength);
};
return strDecrypted;
};
stringDecrypter(window.atob(window.atob('TWpFeU1qSXhNVEl5TVRBNQ==')));
控制台就会返回运行结果:
那这个“zpm”应该就是压缩包密码了,最后成功解压
最后の碎碎念
感觉搞到最后有点像解谜小游戏()但是最后稍微有点失望,因为作者为了省空间,只分别存了CG的差分部分,如下:
原来还要自己拼.jpg
我是强迫症晚期()准备找个时间把这些全拼一起,不然看着太难受了
求大家点点评分点点追随!求求惹求求惹!
好厉害,专业的程序佬,自己学了这么久的程序完全不懂怎么弄这些东西qq 没想到这游戏解包最麻烦的地方在于找密码,看上去难度真是很高的样子了 很详细的解包过程呀,尤其是找密码的这个步骤{:6_165:} 哇塞感觉楼主的分析特别专业,好强的能力,小白根本看不懂 楼主好有技术,整个过程看着就像推理小说一样,涨知识了呢{:6_165:} 楼主牛逼呀 反正我是不会的 还没那么深造 and一看楼主就知道 楼主喜欢这个虎(忘记名字了) 好厉害,我一看到编程就头疼,羡慕楼主的执行能力 我属于是跟着步骤来可能都有点问题的那种,lz好厉害 虽然需要自己拼挺麻烦的,但是确实是个省空间的好办法,大家都不想自己的空间被像微信一样的玩意给吃完 没有完全懂,但是思路很清晰,感谢分享! 感谢分享!以后遇到类似的情况可以参考了 很详细的过程了,感觉有时间可以去别的游戏尝试一下 一步步查找密码的过程非常有条理了 好強大的解包能力好有技術 从上个帖子来的。 会看代码真的很羡慕了 好厉害啊,差分作为防止包体过大也是一种方法了 他里面的背景不是同一个图片叠加氛围,或者说渲染条,而是单纯的晚上一个图片,早上一个图片,属于是我没想到的解决逻辑了(笑) 原来还要自己拼.jpg
懒狗看到加密之后估计就不会拆了,不过倒是提供了思路 大佬好厉害~解包这个东西没接触过看上去也不是很简单的样子