公司动态
IronSightExtractor:从零逆向游戏pak档案与XOR解密提取实战
简介这是面向游戏逆向与文件格式分析人员的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”版本号是1flags等于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. 实操过程从零实现IronSightExtractor3.1 环境准备与依赖我使用的环境是Python 3.10Windows 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 bEROX 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(fbad 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字节、CRC324字节。实际写入时文件名长度如果超过255就会被截断。解析时先读出固定结构再根据文件名长度截断字节。核心代码如下def decrypt_bytes(data, start_offset): result bytearray(len(data)) base bIronFusion 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(parentsTrue, exist_okTrue) 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把资源预览和文件导出整合到同一个界面里目前命令行版本已经能应付绝大多数批量提取场景。本文还有配套的精品资源点击获取