NeoX引擎NPK资源包逆向实战:用neox-tools拆解与重打包

发布时间:2026/10/25 13:17:40
NeoX引擎NPK资源包逆向实战:用neox-tools拆解与重打包

简介:面向游戏逆向与 Mod 制作者,这是一套针对网易 NeoX 引擎 NPK 资源文件的解析工具,由 Rust 编写,当前适配 2020 年初《星战前夜:无烬星河》所用格式,支持读取文件列表并用 LZ4 算法解包。资源包共 15 个文件,以 Rust 工程源码、Python 脚本及说明文档为主,附带的脚本覆盖 PYC 解密、反编译与重定向等处理;压缩包仅 24KB,目录结构简单,便于快速下载与入门学习。工具导出时会自动识别文件类型并补全扩展名,方便后续查看与使用;项目还规划了 RC4 加密、简单加密及重新打包 NPK 功能,可通过 Cargo 直接编译运行。已有约五千人浏览学习,对想理解网易 NeoX 引擎资源组织方式、批量提取游戏资源,或开发类似二进制解析工具的开发者来说,这是一份小巧完整、可直接运行的参考实现,很适合作为学习资源解析的起点。

1. 从 EVE Echoes 的 NPK 说起:neox-tools 到底能拆出什么

neox-tools 是一套专门对付网易 NeoX 引擎 NPK 资源包的 Python 工具。我第一次翻出它,是想从《星战前夜:无烬星河》里提舰船贴图做材质参考。动笔前我以为 NPK 就是个压缩包,结果 010 Editor 打开一看,头部既没有 PK 也没有 ZIP header,索引区文件名全是乱码,直接懵了。后来才明白,NeoX 的 NPK 是自带版本号和偏移表的二进制容器,文件名做过异或混淆,数据段还可能被切开分段压缩,通用解压工具根本识别不了。neox-tools 干的事就是把黑匣子打开:读版本、解索引、按偏移提取、按原结构重新打包。适合三类人:想提取 NeoX 游戏美术资源做参考的,做汉化和轻量 Mod 的,刚接触游戏资源逆向想拿真实项目练手的。

2. 摸清 NeoX NPK 的家底:文件头、索引表与压缩标记

2.1 文件头不是固定魔数:版本号决定后续偏移怎么读

NPK 开头的结构通常不是一锤子买卖:它一般是一个版本号字段加一个头部长度字段,然后才是索引数量、索引表偏移、数据区偏移。不同游戏版本之间,字段顺序和偏移量会差几个字节,所以 neox-tools 的 info 子命令第一个任务就是识别 NeoX 小版本,再决定后续字段的读取位置。我见过有人拿旧版工具去开新版包的,一上来就报 Unknown magic,就是因为魔数或版本号变了。

python -m neox_tools info assets.npk

命令会输出 NPK 的版本号、索引条目数量、索引表偏移量、数据区起始偏移和文件总数。拿到这些数字之后,你才能判断这个包是“正常结构”还是“被加密过”。我一般会先跑 info 而不是直接 list,原因很简单:如果版本号识别错了,list 出来的文件名全是乱码,浪费一次调试时间。

2.2 索引条目是命根子:文件名、偏移、长度和标志位

索引表里每个条目是一段固定长度的二进制记录,常见布局是先写文件名(可能带路径)、再写数据偏移、数据长度、时间戳和压缩标志。neox-tools 在 list 模式下会把这段二进制解析成可读的表格,文件名、大小、修改时间一次摆出来。这里有个容易忽略的点:文件名常常被做过异或或位移混淆,直接按字符串读是乱码,工具内部会依据版本号匹配对应的混淆 key 来还原。

python -m neox_tools list assets.npk | head -50

head -50 只是让你先看前 50 条。这时候应该重点看扩展名:.xml、.pvr、.ktx、.ogg、.prefab 之类。看到 .pvr 或 .ktx 就说明贴图资源在里面,看到 .xml 说明配置表也在里面,后续改包就有方向了。如果 list 出来的都是 .bin 或 .dat,那这个包大概率是逻辑资源,美术资源在另一个包里。

2.3 数据段是分段压缩还是整块压缩

NeoX 对数据段有两种常见处理:小文件直接存原文不压缩,大文件按固定大小分段,每段独立 zlib 压缩。分段的好处是随机读取一个资源时不用把整个包读进内存,代价是文件头里要带每段的压缩前后长度表。neox-tools 的 extract 处理的就是这个:它根据索引条目里的偏移和长度,先读到这一条对应的分段表,再逐个解压拼接回原始文件。

python -m neox_tools extract assets.npk 'text/strings.xml' ./out

单文件提取时,工具会把该条目对应的分段长度先读出来,再去数据区按偏移读取并拼接。注意参数里的路径要跟 list 输出里的名字完全一致,包括大小写和斜杠方向。我经常在 Windows 上写反斜杠导致匹配不上,换成 list 里原样输出的正斜杠就好了。

2.4 为什么不用现成的 zipfile 或 7z

网易的 NPK 不是 zip:头部结构、索引表、压缩段标记都不一样,更别说文件名混淆这一层。就算你把扩展名改成 .zip 让 7z 自动识别,也只会得到一堆带乱码名的文件碎片,因为 7z 根本不认 NeoX 的索引表。所以这类包只能先拆容器再谈内容,工具的核心价值就是把“拆 NPK 容器”这一套流程固定下来,不让你每次换个游戏版本都重新摸一遍格式。

3. 跑通 neox-tools:环境准备、三条命令与一次完整解包

3.1 准备 Python 环境与依赖

neox-tools 是用 Python 写的,依赖里一般会有 zlib(标准库)和一个简单的二进制解析库就够了,不必装 numpy 那种重量级的东西。我习惯用 venv 隔离:

mkdir neox-work && cd neox-work python -m venv .venv source .venv/bin/activate # Windows 上换 .venv\Scripts\activate pip install neox-tools pip freeze > requirements.txt

用 Python 3.9+ 稳妥一点,旧版本对 bytes 和 memoryview 的操作会慢几倍。把依赖固定到 requirements.txt 是给自己留后悔药:万一系统 Python 升级后某个包不兼容,还能按这份清单恢复。

3.2 先看头再列表:info 和 list 两个前置动作

解包最忌一上来就 extract。先跑 info 确认版本识别正确,再跑 list 确认文件名还原正常,这两步过了,后面的提取才有意义。以我这次的包为例,info 显示 NeoX 版本是 v1.14.3,共 18642 条索引,索引偏移 128,数据偏移 0x1C24A0。

python -m neox_tools info assets.npk python -m neox_tools list assets.npk > npk_files.txt wc -l npk_files.txt

wc -l 会显示文件条目数,正常应该和 info 输出的索引数量一致。如果差很多,可能是某个文件条目解析失败,先回头改读取模式。把 list 结果重定向到文本文件,是为了下一步用 grep 检索资源路径,不用一遍遍翻终端输出。

3.3 按路径提取与批量导出

拿到清单后,具体提取就简单了。单文件提取我在上一章给了示例,这里说批量:当你只是想快速翻一下美术资源,可以直接解开整个包,注意磁盘空间和内存:

python -m neox_tools extract_all assets.npk ./unpacked

extract_all 会遍历索引表,按每条索引的数据偏移和数据段标记依次解压,写出到 unpacked 目录,保持原始相对路径。它会边解包边打印进度,解到 1 个多 GB 的包时,建议把输出重定向到日志,避免终端被刷爆。参数上如果包很大,可以加 --filter '*.xml' 只导出 XML 配置,比我一开始傻乎乎全量提取快得多。

3.4 提取出来的文件长什么样

解开之后目录结构一般跟游戏内资源路径一致。以 EVE Echoes 这种美术管线很重的项目为例,textures 下是 .pvr/.ktx 贴图,ui 下是布局 XML,audio 下是 .ogg。XML 大部分是明文,可以直接打开看配置;贴图需要 pvrtextool 或 PVRTexTool 转 PNG 才能预览,不是拿个看图软件就能点开。所以清单里看到 .pvr 不用慌,工具只负责拆包,转格式是另一条流水线的事。

4. 改完 XML 再封回去:打包参数与索引重写要同步

4.1 解包后可改的常见文件类型

最常见的修改对象是配置 XML,比如修改商店文案、初始资源、UI 布局参数。这类文件解出来就是明文 UTF-8,文本编辑器直接改。另一类是二进制配置(.bin 或 .bytes),里面可能是 Lua 字节码或 Protobuf 数据,改起来就要配套工具了。音频贴图这种资源一般不建议动,因为格式和压缩参数匹配不上,封回去容易整体翻车。

我一般会在改之前把原始文件留一份副本,路径名加 .bak 后缀。不要只在本地存一份:万一打包逻辑有 bug,原包又被覆盖,连对比的对象都没有。

4.2 重新打包:为什么不能拿 zip 替代

NeoX 在运行时是按固定偏移去索引表里找文件的,如果你改完文件后直接拿 zip 或 tar 重新打包,游戏根本不会读,因为它要找的是 NPK 头部里的索引表,而不是 zip 的 central directory。neox-tools 的 pack 子命令会做三件事:读旧包的索引结构、把改动过的文件按新长度重新写进数据区、更新索引表里的长度和偏移字段并重写头部。这一套必须原子完成,不能只替换文件。

python -m neox_tools pack ./unpacked ./output.npk --version 1.14.3

--version 指定 NeoX 版本,pack 会按该版本的索引结构生成新的头部。参数上如果没有给 --version,工具会尝试从输入目录里的 meta.json 读原版本信息,所以我解包时会顺手把 meta.json 保留在 unpacked 根目录下面。打包完成后我会再解一次包,对比文件内容和原版差异,确认索引表一致再拿去测试。

4.3 打包时哪些字段不能乱动

有三处不能乱改:一是头部预留字段,通常是 0 或固定值,改成别的 NeoX 直接拒绝加载;二是索引条目里的时间戳,游戏可能会拿它做增量更新判断;三是压缩标志位。如果你改了一个本来没压缩的 XML,然后打包时把它标成 zlib 压缩但没同步改索引,运行时读出来就是乱码。neox-tools 的 pack 会自动根据文件的真实存储方式重写标志位,关键是别手动改索引表二进制。

4.4 改完怎么验证打包是否正确

验证方式很朴素:重新解包一次。如果解出来的文件跟你放进去的 unpacked 目录一模一样,至少说明容器层面没问题。再进一步,可以用 diff 对比改过的文件内容,确认修改真的生效了。最后才是放到客户端里跑一遍,看启动日志有没有报索引或者资源读取错误。我自己是习惯先跑一次命令行验证,不直接上游戏,原因很简单:游戏崩溃日志不好定位,命令行能直接告诉你哪个偏移对不上。

5. 避坑手册:解包报错、乱码文件名与重打包翻车的救法

5.1 乱码文件名到底是谁的锅

现象:list 出来全是 \x??、\xfd 开头的乱码字符串,根本不是可读路径。

原因:NeoX 对索引里的文件名做过混淆,通常是异或或者字节位移,而且不同版本用的 key 不一样。工具没识别对版本时,直接按原始字节输出。

解决:先跑 info 看识别到的版本号,再去配置里手动指定版本做二次解析。如果 info 都报 Unknown magic,先把文件头 32 字节 dump 出来,拿到 hex 再跟同版本正常包对比,确认是不是魔数被改了。

python -m neox_tools info assets.npk --force-version 1.14.3

--force-version 是我常用的参数,工具识别不到时可以直接指定版本号,绕过自动识别。注意这只是临时规避,如果换了另一个版本号的包,还是要先确认版本。

5.2 解包报 CRC 失败先查分段标志

现象:批量 extract 时,某个文件解到一半报 CRC 或 zlib error,后面条目全部中断。

原因:你以为文件是按整块压缩的,实际上 NeoX 按 1 MB 分段压缩,每一段有独立的压缩长度表。工具只读到了第一段的解压结果就开始拼接,后面段次长度对不上自然报错。

解决:确认你用的 neox-tools 是支持分段标记的版本。老版本脚本只处理单段,遇到分段包就崩。可以在 extract 之后加 --chunked 选项强制按分段处理,并且打印当前段次,方便定位到底哪一段坏了。

5.3 重打包闪退的九成原因是长度没同步

现象:pack 成功,文件也能解回来,但客户端加载资源时报错或直接闪退。

原因:八成是索引条目里长度字段没同步。你改的 XML 比原文件长了几百字节,但索引表里存的还是旧长度,运行时会按旧长度去读,读到一半截断,数据错位。

解决:不要自己手动拼包。用工具的 pack 自动重写索引表,pack 之后再用 extract 单文件验证长度是否一致。我遇到过一次是原包里有硬编码的路径哈希,改了文件名导致哈希对不上,后来查询确认文件名不能改,只改内容就好了。

5.4 大包解压内存爆掉是读取方式不对

现象:解包 2 GB 以上的大包时,Python 进程内存飙到 4 GB 甚至直接 OOM 被杀。

原因:脚本先把整个数据区读进内存,再逐段解压。1 GB 的压缩数据解压后可能膨胀到 3 GB,内存自然爆炸。

解决:改用流式读取。按索引条目的偏移,只读当前文件所需的分段到内存,解完一个文件释放一个。neox-tools 的 extract 单文件模式就是这么做的,不要用 extract_all 一次性全开。真要大包批量处理,按目录分批,每批次结束后 gc.collect()。

import gc from neox_tools import NPKArchive npk = NPKArchive.open("assets.npk") for i, entry in enumerate(npk.entries()): # 只读当前条目所需的数据段,避免整个包驻留内存 data = npk.read_entry(entry) with open(f"./unpacked/{entry.path}", "wb") as f: f.write(data) if i % 500 == 0: gc.collect()

这段代码的关键点是 npk.open 只解析头部和索引表,不加载数据区;read_entry 按 offset 读取当前条目的分段数据,处理完就丢引用。每隔 500 个文件手动 gc.collect() 一次,把这个习惯写进脚本里,大包解完基本不会爆内存。

5.5 老工具开新包:魔数识别失败

现象:换了新版本客户端,包文件还是 .npk,但 info 直接报 Unknown magic。

原因:NeoX 每个大版本可能改魔数或头部字段偏移。工具内置的魔数表是上一版游戏时代的,对新包不识别。

解决:先自己 dump 头部 32 字节,跟旧版对比哪里变了。常见变化是版本号从 0x143 升到 0x144,或者头部长度从 16 变 32。确认结构后,在工具的解析器里补一个 case 分支,或者在调用时指定自定义头偏移。这种问题我给作者提过 issue,一般几天内就能补上,遇到就先本地打补丁,不必等上游。

6. 进阶:批量处理一批 NPK 与文件级校验的习惯

到了这一步,单包的 info、list、extract、pack 你都能跑通了。日常里更常见的场景是一个客户端目录下有十几二十个 NPK,我一般写一个脚本统一处理,把所有包解到以包名命名的子目录下,再在解出来的 XML 里做关键字检索,比如搜 price、item_name 这些字段,快速定位数值配置在哪个包、哪个文件里。

python batch_unpack.py ./client_dir ./extracted python -m neox_tools search ./extracted "price" --ext xml

search 子命令会递归扫目录,把包含关键字的文件和行号打印出来。这在排查“哪个文件控制了这个数值”时特别高效,比一个个包手动试快得多。要注意的是,如果客户端目录下同时有 .npk 和 .mpk 之类其他格式,脚本里要先把扩展名过滤一遍,避免把不相干的文件喂给 neox-tools 报错。

改包前的习惯我建议你也固定下来:先把原始 NPK 做一份 sha1 校验和存起来,然后才允许自己动手改内容。为什么这么做?因为很多改包翻车不是当场报错,而是过了两天才在某个隐藏商城界面里炸开,那时候原始包可能已经覆盖了,没有校验和连回滚都无从谈起。

另一种极端是改包改到一半想回到原版。我自己的经验是,备份不是备份原包这一个文件,而是连 meta.json、版本号、文件清单一起备份。这样出问题后用 neox-tools 重新打包也能恢复到接近原始状态,而不只是一个少了头部信息的裸文件。从那以后,我每次拿到新版本客户端,都会强制走一遍 info、list、extract 三步,把 meta.json 和文件清单归档到单独目录——这个习惯至少帮我避免了三次重包彻底翻车。希望帮到你。

本文还有配套的精品资源,点击获取