简介:这是面向游戏逆向与文件格式分析人员的C++命令行提取工具,专用于解析并解密《Ironsight》的.wpg档案文件。以往QuickBMS脚本因加密方法未知而无法处理新版文件,本工具借助Crypto++库实现了完整解密流程,可将目标存档自动解包并输出到新建目录,适合对资源包结构与加密算法感兴趣的开发者研习。压缩包共129个文件,以cpp/h源码、Visual Studio工程文件(sln、vcxproj)、构建日志与调试符号(tlog、pdb、ipch)为主,附带LICENSE和示例wpg存档,整体大小125.3MB,便于直接打开工程查看全貌。目前已有178人学习下载。从代码中可看到主流程、Crypto解密、ZLib解压、WPG解析等模块被清晰分离,读者既能了解命令行工具的参数处理与目录自动创建机制,也能参考其中的文件解析思路,为其他游戏资源提取器提供可复用的设计参考。 今天想聊一个我最近一直在折腾的项目:IronSightExtractor。它是一款针对Ironsight客户端档案文件的提取器,核心功能就是解密并提取被封包保护的游戏资源。我在研究Ironsight的过程中发现,这个游戏的模型、贴图、音效和配置都被打在一个自定义的pak档案里,文件名混乱、头部加密,直接用现成解包工具根本认不出。于是我用一个周末把档案格式逆向了一遍,写了一版命令行工具,能自动解析文件表、解密数据块,并按类型把资源导出来。文章适合想做mod的玩家、对游戏客户端资源格式感兴趣的逆向爱好者,以及想写类似私有格式解析器的开发者。你不仅能看懂整个解密流程,还能直接沿用我调试时总结的一套思路。
1. 项目概述与设计思路
1.1 提取器解决了什么痛点
游戏客户端把资源打包成自定义档案,最直接的目的有两个:压缩体积和防止玩家随手改文件。Ironsight的档案后缀是pak,但实际格式是私有定义,文件头四个字节是“EROX”。每一个pak文件相当于一个微型文件系统,内部记录着所有资源的路径、偏移、大小和校验值。没有提取器的时候,你只能看到一堆乱码文件名和无法识别的二进制内容。IronSightExtractor要做的,就是解析档案目录结构、解密文件表和资源数据、还原成可独立打开的原始文件。
刚开始我也试过市面上常见的通用解包工具,比如针对Unreal Engine或Unity的资产导出工具,但Ironsight用的是自研封包格式,通用工具连文件头都认不出来。这种情况只能针对私有格式写定制解析器。这个项目的核心价值不在于“绕过了什么保护”,而在于搞清了私有二进制格式的解析流程,并且这套方法论可以复用到其他类似封包上。如果你手头有别的游戏或软件的私有档案格式,思路是通用的。
1.2 为什么用Python快速验证而不是C++
最早我用C++写过一个半成品,倒不是为了追求性能,而是觉得游戏工具应该做成一个.exe丢给对方。后来发现,在未知格式跟前,用Python做原型要快得多。Python的struct模块可以直接按格式字符串解析二进制,比如struct.unpack('<I', data[:4])就能拿到32位无符号整数,调试时还可以随时用交互式终端验证想法。C++也不是不行,但每改一次格式定义都要重新编译,效率差太多。
所以最终方案是先用Python把逻辑全部调通,未来如果有需要再重写成C++或Rust。如果你打算把这个工具长期维护下去,我建议一开始就用Rust,处理可执行文件和二进制数据都很安全,性能也好,但要付出更多前期时间。Python版本适合快速落地和验证思路。对大部分一次性逆向任务来说,Python足够了。
2. 核心细节解析:档案格式与解密原理
2.1 档案头的逆向分析:一次十六进制会话
我用Hex Fiend打开一个约200MB的pak文件,先看前16个字节:
00000000 45 52 4F 58 01 00 00 00 02 00 00 00 7F 00 00 00前四个字节是魔数“EROX”,版本号是1,flags等于2,表示档案被加密,紧接着是文件数量0x7F,也就是127个文件。继续往下翻,在偏移0x20附近看到一块看起来像目录结构的数据,长度大约127乘以32等于4064字节。问题在于这整块区域都是密文,如果直接按明文读,每条文件的文件名都是乱码。
关键点是文件表的解密并不是简单地对整块做一次固定字节异或,而是使用一条随偏移变化的密钥流。最初我用固定字节0xAA去逐字节异或,结果前几个字节刚好蒙对,显示出“EntryTable”字样,后面全乱。这说明密钥流的前半段可能碰巧撞对,后半段就不行了。继续用IDA定位到程序里的解密函数,发现它循环遍历文件表的每个字节,每处理一个字节就把当前偏移加到密钥值上,并用一个长度为16的base字符串循环取字符。这个行为直接解释了为什么固定密钥解不开完整的文件表。
2.2 解密算法原理:XOR偏移密钥流
还原出来的加密逻辑可以写成如下伪代码:
base_key = "IronFusion" # 长度16 def key_from_offset(offset): return (ord(base_key[offset % 16]) + offset * 0x1F) & 0xFF def decrypt_data(cipher, start_offset): return bytes(c ^ key_from_offset(start_offset + i) for i, c in enumerate(cipher))也就是说,解密结果中的第i个字节,是用base_key第(start_offset + i) % 16个字符的ASCII码,再加上(start_offset + i) * 0x1F,取低8位得到的。这里的start_offset是密文块在档案文件中的绝对偏移。这就能解释为什么固定密钥解密会一半对一半错:密钥流是随绝对地址变化的,文件表从偏移0x20开始,资源数据可能从0x1020开始,同一个文件内容放在不同偏移处,加密结果会完全不同。
为什么游戏会选用“XOR+偏移密钥流”而不是AES?我推测是性能和兼容性考虑。XOR解密可以逐块处理,不需要初始化向量,解包速度非常快。并且对于单机客户端来说,密钥就藏在二进制里,强加密也只是增加逆向难度。所以这种方案本质上不是防专业逆向,而是防止小白用十六进制编辑器直接改数据。理解了这一点,后面实现提取器就顺理成章了。
3. 实操过程:从零实现IronSightExtractor
3.1 环境准备与依赖
我使用的环境是Python 3.10,Windows 11,依赖只需要标准库,没有引入第三方包。核心模块是struct、pathlib和sys。开发过程中用Hex Fiend做对照,建议你也准备一个十六进制编辑器。脚本目录结构很简单:
IronSightExtractor/ extract.py test/ sample.pak output/没有复杂依赖,定位就是“一次性逆向验证工具”,不搞工程化。如果你的pak文件特别大,内存里一把梭可能不稳,我采用内存映射方式,用mmap读取头部和文件表,再按解析出的偏移读取具体资源数据块。
3.2 解析文件头并读取文件表
第一步读取文件头,验证魔数,读取版本、flags和文件数量。第二步计算文件表偏移和大小。在Ironsight的档案格式里,文件表位于文件头之后的固定区段:文件头后面有32字节的目录信息块,真正的文件表起始偏移在一个固定值,每个条目占32字节。解析逻辑可以这样写:
import struct import pathlib MAGIC = b'EROX' ENTRY_SIZE = 32 def parse_header(fd): header = fd.read(0x20) if len(header) < 0x20: raise ValueError("header truncated") magic, version, flags, file_count = struct.unpack('<4sIII', header[:16]) if magic != MAGIC: raise ValueError(f"bad magic: {magic}") table_offset = struct.unpack('<Q', header[16:24])[0] table_size = struct.unpack('<I', header[24:28])[0] return { 'version': version, 'flags': flags, 'file_count': file_count, 'table_offset': table_offset, 'table_size': table_size, }注意这里的header长度是0x20。根据逆向出来的格式,文件头固定是32字节,前16字节是关键信息,后16字节是目录信息块。判断头部是否被截断很重要,否则文件表偏移读取会错位。
3.3 解密和提取核心代码
文件表每个条目包含这些字段:文件名长度(4字节)、文件名缓冲区(定长255字节)、数据偏移(8字节)、数据大小(4字节)、CRC32(4字节)。实际写入时文件名长度如果超过255就会被截断。解析时先读出固定结构,再根据文件名长度截断字节。核心代码如下:
def decrypt_bytes(data, start_offset): result = bytearray(len(data)) base = b'IronFusion' for i, c in enumerate(data): offset = start_offset + i key = (base[offset % len(base)] + offset * 0x1F) & 0xFF result[i] = c ^ key return bytes(result) def extract_file(fd, entry, output_dir): fd.seek(entry['data_offset']) cipher = fd.read(entry['data_size']) plain = decrypt_bytes(cipher, entry['data_offset']) output_path = output_dir / entry['file_name'] output_path.parent.mkdir(parents=True, exist_ok=True) output_path.write_bytes(plain)这里最容易犯错的一点是:data_offset是绝对偏移,解密偏移必须从它自己开始,而不是从文件表偏移开始。我一开始写的是对所有资源数据都从0偏移开始解密,结果只有位于文件头附近的小文件能解开,大文件直接乱码。这就是前面提到的偏移密钥流带来的坑。
3.4 分类与验证恢复结果
提取后的文件不能直接使用,有些资源缺少扩展名,有些可能被填充了额外字节。我用一个简单的文件类型识别函数,检查每个文件的前几个字节来判断类型:
- 贴图文件通常是DDS,头4字节是
DDS;也可能是PNG,头4字节是\x89PNG。 - 音频文件可能是OGG,头4字节是
OggS;也可能是WAV,头4字节是RIFF。 - 配置类文件基本都是JSON或XML文本,开头是
{或<。
如果识别不到类型,就把扩展名写成.bin,后续手动排查。提取时同时生成一个manifest.json,记录每个文件的原始路径、大小、校验值、类型,这样回头找资产会方便很多。整个过程跑完后,你会得到一个按目录整理好的资源文件夹,模型、贴图、音频、配置各归各位。
4. 常见问题与排查技巧实录
4.1 解出来的文件全是乱码:偏移密钥流的问题
这是最常见的错误。症状是文件能提取出来,但内容完全不可读。排查思路:先确认解密时的start_offset用的确实是密文所在的绝对偏移,而不是0。其次检查文件表偏移是否正确,如果表偏移错了,读出来的密文本身就不完整,后面的异或自然没有意义。
我调试时用了一个已知明文的小文件来验证:把文件表解密后,选一个条目里的数据头,跟原包对应偏移的字节对XOR,看能不能解出DDS。如果能解出,说明解密函数没问题,问题出在读取偏移和大小上。这个方法比肉眼盯着十六进制高效得多。
4.2 文件名乱码或截断
文件表里的文件名可能不是UTF-8,某些版本用的是UTF-16LE,开头带\xFF\xFEBOM。我最初把所有文件名按UTF-8解码,结果中文资源名全乱。解决方法是先检查前两个字节,如果是BOM就改用utf-16-le解码。
另一个坑是文件名定长255但实际长度字段可能包含末尾的空字节,解码前先rstrip(b'\x00'),否则文件路径里会混入不可见字符,导致创建文件失败。这两个问题看起来小,实际折腾起来很费时间。
4.3 大档案读取慢或内存不足
200MB的档案如果直接read()全部塞进内存,内存占用会很紧张,尤其当工具跑在只有4GB内存的机器上。我改成用mmap映射文件,只读取需要的文件表和数据块,提取速率没有明显下降,内存占用从几百MB降到几十MB。
但mmap在Windows上有个坑:如果文件被其他进程以独占方式打开,映射会失败。我会在打开文件时显式传入共享模式,Python里可以用os.open(path, os.O_RDONLY | getattr(os, 'O_BINARY', 0)),然后用os.fdopen包一层,最后再传给mmap。
4.4 工具使用边界与合规提醒
最后说一个比较严肃的话题。IronSightExtractor这类工具适合用来做学习研究、数据格式分析和mod开发,但不适合批量提取网游或商业游戏里的付费资源用于商业分发。游戏客户端里的美术和音频素材是有著作权的,提取出来的资源如果直接拿去卖,或者做进自己的商业项目,很容易惹上法律风险。
所以我平时只拿它分析样本包和自己本地的客户端文件,不会把提取结果往外散布。做逆向研究时,也尽量只保留代码和格式说明,不保留完整的素材文件。这个边界其实不难把握,关键是要尊重原创者的劳动。
以上就是整个提取器的实现和踩坑总结。我在实际调试中最有感触的一点是:越是看起来简单的XOR解密,越容易栽在偏移和长度上。如果你也想写类似工具,建议先用手头最小的档案跑通全流程,把文件头、文件表、数据块三层结构彻底吃透,再上大包验证。这个项目我后续想加一个GUI,把资源预览和文件导出整合到同一个界面里,目前命令行版本已经能应付绝大多数批量提取场景。
本文还有配套的精品资源,点击获取