ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

从乱码到清晰:一文读懂 unnpk 如何解开网易 NeoX 引擎的 NPK 资源包

从乱码到清晰:一文读懂 unnpk 如何解开网易 NeoX 引擎的 NPK 资源包 从乱码到清晰一文读懂 unnpk 如何解开网易 NeoX 引擎的 NPK 资源包【免费下载链接】unnpk解包网易游戏NeoX引擎NPK文件如阴阳师、魔法禁书目录。项目地址: https://gitcode.com/gh_mirrors/un/unnpk如果你玩过《阴阳师》或《魔法禁书目录》又恰好对游戏里的美术资源、剧情文本、战斗数值感兴趣你迟早会遇到这样一幕从手机或 PC 客户端里拖出一个.npk文件hexdump一开满屏十六进制乱码文件名、目录结构、资源格式统统看不出来。想提取一张立绘、分析一段脚本却连第一步都迈不出去。这就是 unnpk 存在的理由它是一个专门解包网易 NeoX 引擎 NPK 文件的命令行工具用 C 语言写成能把这种看起来像一坨乱码的资源包还原成一张张 PNG、一段段 XML、一份份可读的 Python 脚本。本文不打算堆术语而是带你从手上只有一个 npk 文件的真实处境出发走完解包、分析、解密脚本的完整旅程。先搞清楚NPK 到底是个什么东西在动手之前你需要建立一个直觉NPK 本质上就是一个打包容器和 ZIP、TAR 是同一类东西——把很多小文件塞进一个大文件里方便引擎加载和分发。但网易没有用现成的 ZIP而是自己设计了一套格式。好处是引擎可以在启动时只做一次文件打开操作之后通过内存偏移直接读取任意资源速度更快坏处是生态封闭社区没有现成工具可用普通玩家和研究者只能望洋兴叹。NPK 的内部结构并不复杂大致是三段式区域作用文件头记录标识、版本号、关键偏移量索引表一张目录逐条记录每个内部文件的元信息数据区真正的资源二进制数据图片、文本、脚本等换句话说解包 NPK 要做的事情很朴素先读文件头拿到索引表的位置再逐条读取索引按记录的偏移量和大小把数据一块块抠出来。unnpk 的核心逻辑就是把这个朴素流程写成了不到三百行的 C 代码。跟着 unnpk 走一遍完整流程第一步编译只有两个依赖unnpk 依赖 zlib解压用和 libmagic识别文件类型用装好依赖后编译非常简单# macOS brew install libmagic # CentOS / RHEL sudo yum install file-libs file-devel git clone https://gitcode.com/gh_mirrors/un/unnpk cd unnpk makemake执行完当前目录下会出现两个可执行文件unnpk和mapnpk。前者负责解包后者负责解剖——具体区别稍后讲。第二步解包一条命令拿到一个 npk 文件后解包只需要两个参数./unnpk script.npk scriptscript.npk是输入文件script是输出目录。命令跑完终端会打出一张表格像这样| Index | Offset | Size | Unzip size | zip | MIME Type | Extension | | -------- | --------- | ------- | ---------- | --- | --------- | --------- | | 0A0D60DC | 0x00001000 | 13.1 KB | 26.4 KB | Yes | text/plain | .txt |而磁盘上的script目录里文件已经被按 MIME 类型分类放好了script/ ├── text/ │ └── 0A0D60DC.txt ├── image/ │ └── 1F2A3B4C.png └── application/ └── D0E1F2A3.zip这里有两个细节值得留意它们正好揭示了 unnpk 的设计思路第一文件名是十六进制编号。因为 NPK 的索引表里存的是哈希值而不是真实文件名引擎靠哈希做 O(1) 查找解包工具拿不到原始文件名只能用索引号%08X命名。你看到0A0D60DC这种名字不必奇怪它只是这个文件在索引表里的编号。第二扩展名是猜出来的。解出来的数据本来是没有扩展名的裸二进制unnpk 用 libmagic 分析文件头部特征魔数识别出 PNG、JPEG、ZIP、XML 等类型后自动补上扩展名。这一步体验上的价值很大——没有它你解出来的只是一堆无法双击打开的未知文件。深入源码解开 NPK 的索引结构前面说逻辑很朴素现在我们把unnpk.c的核心代码拆开看你会理解为什么它能这么轻量。定位索引表偏移量 0x14// 读取map偏移量 fseek(npk, 0x14, SEEK_SET); uint32_t map_offset; fread(map_offset, 4, 1, npk);0x14十进制 20是文件头的固定偏移位置。游戏引擎把索引表的起始偏移量写在这个固定地址解包工具只要fseek过去读 4 个字节就能拿到目录在哪。这就是逆向工作的乐趣所在格式是别人设计的但只要你找到一个稳定的锚点整个结构就打开了。遍历索引表每条记录 28 字节uint32_t file_info[7]; for (int file_offset map_offset; file_offset npk_size; file_offset 7 * 4) { // 从索引表当前位置读取一条文件信息 fseek(npk, file_offset, SEEK_SET); fread(file_info, 4, 7, npk); ... }索引表的每条记录是7 个 32 位无符号整数共 28 字节含义如下字段含义file_info[0]文件哈希 / 索引编号解包后用作文件名file_info[1]数据在文件中的偏移量file_info[2]存储大小压缩后file_info[3]解压后大小file_info[4]/file_info[5]校验值mapnpk 中标记为 Chkfile_info[6]是否 zlib 压缩的标志循环从map_offset开始每次前进 28 字节直到文件末尾——这意味着索引表是顺序排列、连续存储的没有任何花哨结构。顺序读取也天然对 CPU 缓存友好这算是个隐性的性能优化。解压与容错宁可输出原样也不让你空手而归if (file_info[6] || file_info[2] ! file_info[3]) { // 存储大小和解压大小不一致说明数据被压缩了 switch (uncompress((uint8_t*)file_out_buf, file_destLen, (uint8_t*)file_read_buf, file_info[2])) { case Z_OK: break; case Z_DATA_ERROR: // 数据不是 zlib 格式直接把原始数据输出 fprintf(stderr, W: Z_DATA_ERROR: Data is not zlib, The raw data will be output\n); ... } }注意file_info[6]和file_info[2] ! file_info[3]是或的关系——压缩标志位为真或者存储大小与解压大小不一致都走解压分支。更贴心的是容错逻辑当uncompress失败时unnpk 不会直接放弃而是把原始数据原样写出去并打一条警告。对于逆向工作来说拿到原始字节总比什么都拿不到强说不定那本身就是未压缩的格式。类型识别libmagic 与魔法签名双保险解压出数据后unnpk 用 libmagic 判断 MIME 类型再根据类型决定扩展名。对常见格式MIME 判断就够了但游戏资源里总有冷门格式libmagic 认不出来于是源码里还有一段硬编码魔法else if (strcmp(file_out_buf 1, KTX) 0) { file_out_extension .ktx; // 移动端GPU纹理格式 } else if (strcmp(file_out_buf, RGIS) 0) { file_out_extension .RGIS; // 网易自研纹理格式 } else if (strcmp(file_out_buf, PKM) 0) { file_out_extension .PKM; // ETC1纹理压缩格式 }这三行代码透露了一个重要信息NeoX 引擎大量使用 KTX、PKM 这类 GPU 纹理格式。如果将来你在新游戏里解出未知扩展名的文件第一反应应该是它可能也是一种纹理格式而不是直接放弃。对文本类文件代码还会检查内容特征来细分类型文件头是NeoX就命名.NeoX.xml是{开头}结尾就当作.json包含vec4、tex2D等关键字就识别为.glsl着色器。这些判断都是看一眼开头就知道的廉价启发式却覆盖了游戏资源最常见的几类文本。更硬核的部分阴阳师 script.npk 的三层解密解出图片、XML 并不稀奇真正让 unnpk 项目封神的是它对脚本文件的处理能力。手游的逻辑代码通常编译成 Python 字节码.pyc打包进资源网易为了让别人读不了给脚本上了三道锁。解开它需要三步每一步对应tools/里的一个脚本。第一层rotor 流密码解密def unnpk(data): asdf_dn j2h56ogodh3se asdf_dt dziaq. asdf_df |os5v7!-234 asdf_tm asdf_dn * 4 (asdf_dt asdf_dn asdf_df) * 5 ... rotor rotor.newrotor(asdf_tm) data rotor.decrypt(data) data zlib.decompress(data) data _reverse_string(data) return data这来自script_redirect.py。加密的第一步用的是 Python 2 时代的rotor库模拟恩尼格玛密码机对整份文件做流加密密钥是几个看起来像乱敲的字符串拼接而成。作者通过逆向游戏内的redirect.py得到了这套参数——如果你要解其他网易游戏这三个asdf_*常量可能不同需要自己逆向。解密的顺序是rotor 解密 → zlib 解压 → 字节反转。_reverse_string里还藏着一个细节前 128 个字节先与 154 异或再整体反转这种混合了异或和倒序的处理是典型的防分析小手段。第二层marshal 反序列化与 opcode 重映射rotor 解密出来的其实是一段 Python 2.7 的 marshal 序列化数据.pyc去掉头部后的部分。但网易还动了手脚把 Python 虚拟机的操作码opcode重新映射了一遍——原本的1号指令在文件里变成了382变成了46以此类推。普通反编译工具拿到这种字节码会因为不认识指令直接报错。pyc_decryptor.py里维护了一张完整的映射表self.opcode_encrypt_map { 1: 38, 2: 46, 3: 37, 4: 66, 5: 12, 10: 35, 11: 67, ... } self.opcode_decrypt_map {self.opcode_encrypt_map[key]: key for key in self.opcode_encrypt_map}解密时先marshal.loads读出代码对象再用反向映射表逐字节还原 opcode。这里的关键工具是项目自带的pymarshal.py——它重写了 Python 的 marshal 模块多了一个_transform_opcode钩子能在序列化输出时动态替换字节码。你看它的核心逻辑def _transform_opcode(self, x): opcode bytearray(x) c 0 while c len(opcode): n self._opmap[opcode[c]] opcode[c] n if n 90: c 1 # 无参数指令 else: c 3 # 带参数指令跳过2字节参数 return str(opcode)这里还体现了对 Python 指令集的理解opcode 小于 90 的是无参数指令占 1 字节大于等于 90 的带 2 字节参数占 3 字节。遍历时必须按指令长度跳进否则会把参数当指令改坏。pymarshal.py相当于一个翻译中间层没有它opcode 映射就无从谈起。第三层反编译成可读源码最后一步交给社区工具 uncompyle2./tools/script_redirect.py 0A0D60DC 0A0D60DC.out ./tools/pyc_decryptor.py 0A0D60DC.out 0A0D60DC.pyc uncompyle2 -o 0A0D60DC.py 0A0D60DC.pyc三步走完一份可读的 Python 源码就还原出来了。这套rotor 解密 → marshal 解析 → opcode 还原 → 反编译的流程可以看成网易脚本防护的完整逆向样本每一层防护都对应一个逆向工具环环相扣。顺便说一句pyc_decryptor.py里还专门还原了.pyc的文件头\x03\xf3\x0d\x0a...因为 uncompyle2 校验文件头版本少了它反编译会失败。这种细节只有真正跑过一遍流程的人才写得出来。配套工具 mapnpk先解剖再解包解包之前你可能会想先看看这个 npk 里到底有什么。mapnpk就是干这个的——它只读索引表不解数据把结构信息以多种格式输出./mapnpk input.npk structure.md # Markdown 表格 ./mapnpk -f csv input.npk analysis.csv # 方便Excel分析 ./mapnpk -t int -f csv input.npk ints.csv-f控制输出格式markdown或csv-t控制数值展示方式hex十六进制、int十进制、original原始字节序。在排查解包后为什么文件对不上这类问题时先跑一遍 mapnpk 看索引表是否正常能帮你快速定位是格式变了还是数据坏了。你可能踩到的坑把社区里常见的问题汇总成一份 QA希望能帮你少走弯路Q编译时报找不到 magic.hAlibmagic 没装好。macOS 用brew install libmagicCentOS 用yum install file-devel。注意是-devel包光装运行库没有头文件。Q解出来的文件是Z_DATA_ERROR只有原始数据A先别慌这是 unnpk 的容错分支说明索引表声称该文件是 zlib 压缩的但实际数据不是。可能原因NPK 版本更新导致索引字段含义变了或者该文件本来就没压缩。用 hexdump 看下原始字节特征再判断。Qscript_redirect.py 解出来是乱码A大概率是密钥不匹配。阴阳师的asdf_dn、asdf_dt、asdf_df是针对特定版本的网易其他游戏大概率不同。README 里提到可以用 unnpk 从 script.npk 中解出redirect.pyc如 2018 年版本对应文件FB54F059逆向它就能拿到新密钥——这是一个工具解包自己的绝妙思路。QWindows 上能跑吗A能用但很折腾。依赖 libmagic 和 zlib 在 Windows 下都要手动配置 MinGW作者明确不推荐建议在 Linux/macOS 或虚拟机里使用。Q解出的脚本反编译失败A确认 Python 版本。这套工具链针对 Python 2.7 的 pycuncompyle2 只支持 Python 2 字节码。如果你的游戏脚本是 Python 3 编译的需要换对应的反编译工具。它能拿来做什么读到这里你应该已经感受到 unnpk 的三种典型用法资源研究提取游戏美术、音效、配置分析资源组织和纹理压缩方案这对想做游戏引擎研究的人是一手素材。脚本逻辑分析还原 Python 字节码看战斗公式、掉落概率、活动逻辑——很多安全研究和玩家社区分析都从这一步开始。MOD 与本地化拿到原始 XML 配置和文本资源改数值、做翻译、做替换前提是尊重开发者的用户协议。对于想学习逆向工程的人这个项目本身就是一份极好的教材C 语言文件解析unnpk、命令行参数解析args.c、二进制格式逆向NPK 索引、多层加密对抗script_redirect pyc_decryptor pymarshal每个模块都短小精悍、无框架负担读起来几乎没有门槛。写在最后回到开头的场景那个让你一筹莫展的.npk乱码文件现在你知道它内部其实井井有条——文件头藏着索引表的位置索引表记着每个资源的偏移和大小数据区里是 zlib 压缩的资源而压缩层之下可能还藏着 rotor 加密和 opcode 混淆。unnpk 做的就是一层层剥开这些外壳把引擎藏起来的东西重新摊开给你看。这种剥洋葱的过程正是逆向工程最迷人的地方它不需要魔法只需要耐心读二进制、找锚点、试错然后一点点逼近真相。如果你也想亲手试试仓库地址是https://gitcode.com/gh_mirrors/un/unnpk建议的阅读顺序是先读README.md跑通流程再读unnpk.c理解索引结构最后研究tools/下的三个 Python 脚本体会加密对抗的思路。说不定下一个为社区贡献新游戏适配的就是你。【免费下载链接】unnpk解包网易游戏NeoX引擎NPK文件如阴阳师、魔法禁书目录。项目地址: https://gitcode.com/gh_mirrors/un/unnpk创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表