新人小白记一次经典小游戏"大家来找茬"的资源文件解包过程(文末附C#源码)

查看 67|回复 9
作者:lies2014   
尽管担任论坛站务团队多年,但还是字自己新人小白是因为我没啥逆向基础,靠我肚子里的墨水瞎猜搞出来的一个帖子
正文开始。
PS:文章水平有点拉,不喜勿喷
如下图,这款游戏大概是90、00后的童年回忆吧


9207.jpg (131.77 KB, 下载次数: 1)
下载附件
2024-10-23 10:42 上传

[color=]没错,这款游戏叫做“5 spots II”,中文译为“大家来找茬 5 Spots II”玩过的同学举爪


Cache_-503a3c925745c8ee.jpg (15.25 KB, 下载次数: 1)
下载附件
2024-10-23 10:44 上传

今天来分享一下游戏的资源文件解包过程
打开游戏根目录下的data文件夹,里面有ad.dat、gfx.dat和sound.dat


7YJQW]0AIXZ`[email protected] (72.1 KB, 下载次数: 1)
下载附件
2024-10-23 10:50 上传

[color=]同学们已经想到了,图片文件和声音文件在gfx.dat和sound.dat中,那么恭喜你,答对了,送你两朵大红花


8N07JW}QF4W}38N$DWUYBDO.gif (19.08 KB, 下载次数: 0)
下载附件
2024-10-23 10:53 上传

在开始之前,大家需要了解两样东西
首先是ASCII码各个字符所对应的十六进制值,网上有对照表,这里有一篇CSDN文章,请不了解的同学提前预习
"

其次是常见图片文件、音频文件的头部的十六进制字节。先介绍文件头部,它也称为文件签名,是文件格式中非常重要的一部分。它位于文件的起始位置,通常包含一系列特定的字节序列,这些序列对于识别文件的类型和版本至关重要。
先说JPG头部,JPG的头部字节通常为FF D8开始
[Asm] 纯文本查看 复制代码FF D8 FF E0 00 10 4A 46 49 46
尾部为
[Asm] 纯文本查看 复制代码FF D9
然后说PNG头部,JPG的头部字节通常为
[Asm] 纯文本查看 复制代码89 50 4E 47 0D 0A 1A 0A
再说说GIF,JPG的头部字节有两种版本,一种是GIF89a另一种是GIF87a,目前GIF89a相对较多
GIF89a:
[Asm] 纯文本查看 复制代码47 49 46 38 39 61
GIF87a:
[Asm] 纯文本查看 复制代码47 49 46 38 37 61
最后说一下WAV和OGG头部字节
WAV格式一般为RIFF开头,但是RIFF格式windows环境下大部分多媒体文件遵循的一种文件结构,它有可能是WAV、也有可能是AVI视频、MIDI音频,无法判定,因此,我们还需要判定后面,后面有WAVEfmt,这样才可确定是WAV格式,鉴于此,WAVE的头部字节为
[Asm] 纯文本查看 复制代码52 49 46 46 24 20 0B 00 57 41 56 45 66 6D 74
OGG就比较好判断了,头部是“OGGs”,十六进制字节就是
[Asm] 纯文本查看 复制代码4F 67 67 53 S
好了,常见图片音频头部介绍完了
二话不说,我们直接上HEX文本编辑器(这里以010editor为例)
首先来分析gfx.dat


7_PEKLN@4MQ5IZ1LA8Z$)40.png (109.08 KB, 下载次数: 0)
下载附件
2024-10-23 10:50 上传

包文件头部为“VFS2”,这里是游戏厂商定义的资源包头部标识,不用管,向下看
此时我们发现了一些明文,这里正是包文件的内部的文件夹结构,目前还看不出是否为多层级文件夹


QQ截图20241023165708.png (116.84 KB, 下载次数: 0)
下载附件
2024-10-23 16:57 上传

到4D70行的时候才有文件名


QQ截图20241023170221.png (68.18 KB, 下载次数: 0)
下载附件
2024-10-23 17:03 上传

以上都是包文件的目录索引,到了5:D9F0才是数据区,因为索引区一开始都是SPR格式的数据文件,因此只能忽略,往下看浪费时间也没有意义,所以要换个思路。
本人猜测,文件前部分用00填充的比较多,所以包文件数据应该没有压缩,没有加密,也没有异或运算,接下来直接搜关键字吧。
如果图片存放在gfx.dat的话,那就搜一下常见图片格式头部的字节数据。
如图,按Ctrl+F,打开搜索,输入FF D8 FF E0 00 10 4A 46 49 46,然后搜索类型为Hex字节,按回车


QQ图片20241023171758.png (175.71 KB, 下载次数: 0)
下载附件
2024-10-23 17:18 上传

果然,搜到了JPG的头部,共有1502条记录。


%JCC_DH%WB0769L]8)6T~{J.png (169.74 KB, 下载次数: 0)
下载附件
2024-10-23 17:22 上传

这也充分证明了这种格式的游戏资源包文件压缩、加密、异或运算统统无。
但是这并没有结束,文件与文件之间是否有特征字节来分隔开现在还不得而知,因此还要尝试搜一下JPG的尾部字节。
然而,最害怕的事情还是发生了
我在搜索完FF D9后,后面直接又FF D8 FF E0 00 10 4A 46 49 46了


QQ截图20241023173247.png (209.57 KB, 下载次数: 0)
下载附件
2024-10-23 17:33 上传

还真没有特征字节,于是乎,我想起了之前,我猜测它们是记录这些子文件数据长度的。
接着,我随便找一个完整的JPG数据把它高亮圈中。


5T2(L1MROCY[P7_1J1KIG.png (364.45 KB, 下载次数: 0)
下载附件
2024-10-23 19:10 上传

此时状态栏显示的选中部分的长度为4700字节,因此我们可以判断这个JPG文件的大小为3303字节
为了验证我们拷贝的数据是不是一个完整的JPG图片,我们按Ctrl+Shift+N快捷键新建一个Hex文件


QQ截图20241023190633.png (169.7 KB, 下载次数: 0)
下载附件
2024-10-23 19:10 上传

很成功,我们顺利的解包了一个JPG文件


XDXB7$@G~G0$ITXQ2ANY98V.png (81.95 KB, 下载次数: 0)
下载附件
2024-10-23 19:10 上传




IMAGE.JPG (3.23 KB, 下载次数: 0)
下载附件
2024-10-23 19:10 上传


如果说索引区的子文件目录表中还穿插了一些其他数据,这个JPG图片长度是3303字节,转换成十六进制为CE 07,那么就搜一下
然而并没有结果,搜到的首个出现位置根本就不在索引区


6P1B1T87)R(3QNS]}[M_EOO.png (227.92 KB, 下载次数: 0)
下载附件
2024-10-23 19:12 上传

这就难办了,数据与数据之间没有分隔符,索引区的子文件后面的字节信息是什么也不知道,到这里思路戛然而止


58aebbd436fcbee03213ece80cb0bb7d.jpg (15.05 KB, 下载次数: 0)
下载附件
2024-10-23 17:28 上传

这里就需要借助IDA来对EXE进行静态分析了,那么不妨试一下
打开IDA,把文件拖入,开始静态分析。
我猜测EXE肯定会加载包内的JPG图片,于是搜索了一下,果然,搜到了很多关于加载JPG的操作,还是带有文件目录的,这也说明前期的包文件索引区的没有后缀的字符串确实是文件夹名称


EF$M2[K(620M{0MN3LANS`U.png (153.83 KB, 下载次数: 0)
下载附件
2024-10-23 19:19 上传

随后这个地方引起了我的注意


QQ图片20241023221601.png (130 KB, 下载次数: 0)
下载附件
2024-10-23 22:16 上传

在..text代码段的00404B38处,push了一点数据"Can't Load 'Background.jpg'",这说明这里是不能加载Background.jpg的提示,而不能加载的原因无非就是文件不存在或者拒绝访问,在这个场合下,拒绝访问的可能性几乎为0,因此,在这里应该能够追踪到关于解包的一些线索
在他的上方call了sub_40582A 函数
到了这里我就没有头绪了,希望大佬解读一下。


JX[KD}0K$HY(JK9UP4{TA{7.png (137.42 KB, 下载次数: 0)
下载附件
2024-10-23 22:32 上传

但是,一码归一码,虽然找不到解包算法,但我们还是可以通过文件头部特征来解包文件
既然我们知道JPG图片的头部和尾部的字节特征,那么我们写一个for循环语句把匹配到的首尾字节中间的部分单独复制出来不就可以了吗?
答案是肯定的!
C语言我不熟,易语言显得水平低,Python我不喜欢(因为严格的缩进要求我很不爽),那就用C#吧
先分析操作流程,首先我们要把游戏包文件读入到byte[]数据,也就是易语言所谓的字节集,然后创建一个for循环操作,循环次数为游戏包文件总长度,循环体进行if判断,只要是匹配到JPG头部字节的就将其拷贝到List中,并返回,最后,创建另一个for循环,循环次数为刚才返回的List成员数,保存文件,完成。
那就写一下代码吧
[C#] 纯文本查看 复制代码using System;
using System.Collections.Generic;
using System.IO;
namespace UNDAT
{
    internal class Program
    {
        ///
        /// 程序主函数
        ///
        ///
        static void Main(string[] args)
        {
            // 我们把所有解包操作封装在一个函数里,因为相关函数有抛出异常的操作,因此需要catch,以防程序意外退出
            try
            {
                UnPackJpegData("gfx.dat");// 解包目标:gfx.dat,请把程序和此文件放在同一个目录下
            }
            catch (Exception e)
            {
                Console.WriteLine(e.ToString());
            }
        }
        ///
        ///  DAT文件头部检查
        /// (字节集 被检查的字节集数据 ,
        /// 字节集 欲检查的字节集数据)
        /// 成功返回真,否则返回假
        ///
        private static bool HeadCheck(byte[] arr, byte[] target)
        {
            // 先看看它是不是太短
            if (arr.Length
        /// 解包指定文件
        /// (文本型 欲解包的DAT文件)
        /// 字节不匹配将抛出异常
        ///
        private static void UnPackJpegData(string filePath)
        {
            // 判断文件是否存在
            if (!File.Exists(filePath))
            {
                throw new Exception("对不起,所选定的文件不存在!!");
            }
            // 读入字节集数组
            byte[] byteArray = File.ReadAllBytes(filePath);
            // 先对DAT文件头部检查,看看他到底是不是这个游戏的DAT文件
            // DAT的头部是以“VFS2”开头的,先把他们写进一个字节集数组中
            byte[] headByte = { 0x56, 0x46, 0x53, 0x32 };
            // 开始对头部进行判断,如果头部不匹配,那么就认定它不是这个游戏的DAT文件,并抛出一个异常
            bool isGameDATFile = HeadCheck(byteArray, headByte);
            if (!isGameDATFile)
            {
                throw new Exception("对不起,所选定的文件不是游戏:5 spots II的DAT资源包文件!");
            }
            // 创建一个List ,用于存放已识别到的JPG图片
            List jpgs = ExtractJpgsFromBytes(byteArray);
            // 创建一个解包文件夹,格式为:“文件名+拓展名+UnPacked”
            string fileNameWithExt = Path.GetFileName(filePath);
            string extension = Path.GetExtension(fileNameWithExt);
            string fileNameWithoutExt = Path.GetFileNameWithoutExtension(fileNameWithExt);
            string unPackDirectory = fileNameWithoutExt + "_" + extension + "_UnPacked";
            Directory.CreateDirectory(unPackDirectory);
            // 保存每个JPEG为独立的文件,文件名格式:image_xxxxxxxx.jpg
            for (int i = 0; i
        /// 解包JPG文件
        /// (字节集 欲处理的数据)
        /// 成功返回处理好的List字节集
        ///
        public static List ExtractJpgsFromBytes(byte[] bytes)
        {
            // 创建一个List用于存放返回的JPG数据
            List jpgList = new List();
            // JPEG数据开始标记
            bool inJpeg = false;
            // JPEG数据起始位置
            int start = -2;
            // 开始for循环
            for (int i = 0; i
        /// 保存JPG图片
        /// (字节集 欲保存的JPG数据
        /// 文本型 欲保存的文件名称)
        ///
        public static void SaveJpgToFile(byte[] bytes, string fileName)
        {
            // 转成文件流,然后写到二进制流,最后保存文件
            FileStream fs = new FileStream(fileName, FileMode.Create);
            BinaryWriter writer = new BinaryWriter(fs);
            writer.Write(bytes);
            writer.Close();
            fs.Close();
        }
    }
}
[color=]写完编译,完美运行!


427bf985eb20e0e0df84c143388e00c5.jpg (16.65 KB, 下载次数: 0)
下载附件
2024-10-24 00:19 上传



{A@H%TW77%JV[2HQ8I3DL`O.png (389.22 KB, 下载次数: 0)
下载附件
2024-10-24 00:17 上传

[color=]当然这只是匹配到符合条件的JPG,还有一些JPG只有头部,没有尾部,这部分是识别不到的。
[color=]至于带文件名、带文件夹的完美解包,我卡在了exe静态分析中,回头我的把我的微机原理那本书找出来复习一下
[color=]也希望大佬给个关于完美解包的分析思路,
可开悬赏
[color=]。
[color=]写代码、写文章不易,还请同学们给点鼓励,鉴于sound.dat也是明文无压缩无加密的,最后丢一个解包sound.dat的代码吧,匹配机制为RIFF,回帖可看,五天后自动解除。
无名小银,如果您要查看本帖隐藏内容请回复

文件, 字节

zwgnhw131499   

单纯分析 dat 文件也能做解析器。

数据目录里有个 ad.dat,应该从它开始分析,比较容易上手。
笔记
56 46 53 32 "VFS2"
05 00 00 00 目录数量
05 00 00 00 文件数量
00 00 00 00 ??
94 02 00 00 目录列表大小
20 03 00 00 文件列表大小
// 目录列表 (id 从 1 开始)
00 00 00 00 "classic"
00 00 00 00 "demo"
...
// 文件列表
01 00 00 00 (dir id = 1)
char[0x80] "ad.txt"
CC 05 00 00  0x5cc - file start
1C 02 00 00  0x21c - file size
1C 02 00 00  0x21c - file size 2?
00 00 00 00  unused?
00 00 00 00  unused?
00 00 00 00  unused?
00 00 00 00  unused?
02 00 00 00 parent
char[0x80] "ad.txt"
E8 07 00 00  0x7e8
1D 00 00 00  0x1d
1D 00 00 00  0x1d
00 00 00 00  unused?
00 00 00 00  unused?
00 00 00 00  unused?
00 00 00 00  unused?
后同 ...
代码仓库 (Python) - https://github.com/FlyingRainyCats/bfg_vfs2
8672089   

谢谢分享!
zj23308   

学习一下新人小白记一次经典小游戏
lxuzhenguo   

谢谢分享了
Lty20000423   

感谢分享,写得真好
8672089   


爱飞的猫 发表于 2024-10-24 06:07
[md]单纯分析 dat 文件也能做解析器。
![](https://imgsrc.baidu.com/forum/pic/item/9358d109b3de9c825 ...

我尝试过,但是我不知道TXT怎么断开,所以就放弃了
zj23308   


烟99 发表于 2024-10-24 06:44
我尝试过,但是我不知道TXT怎么断开,所以就放弃了

找文件属性(文件名)附近的 4 字节数据,通常会有一个值代表它的长度。
例如 ads.dat 里,ad.txt 附近有两个值 0x5cc 和 0x21c,尝试一下偏移和文件长度(也有一些喜欢用结束位置),结束位置刚好也是换行符后面。
然后再看第二个文件,两个相邻的文件的数据也刚好连在一起。
合理猜测一下,这大概率就是存储的格式了。
lxuzhenguo   

谢谢分享!
zwgnhw131499   

非常优秀的帖子
您需要登录后才可以回帖 登录 | 立即注册