5. BelleIsle FPSF格式解析

197 阅读10分钟

经过PM监控发现,首先读取的文件为 filelist.dat, 这个文件在bin中,而他的格式刚好也是FPSF格式。

image.png

为了搞清楚这种格式的解密算法。

我们直接跟踪这个函数的算法,因为汇编中 readfile后,然后会分配一块固定的缓存来存取这块数据,我们经过断点,在读取文件的附近,单步调试跟踪这块内存的解析。

汇编分析的过程有点费肝...不对着汇编代码逐步解析记录了,结果分析到这个文件采用了一个对称加密的算法,这个算法核心伪代码大致如下。

   //掩码   6A 5C  ,定义死的  
   preB = 6A
   for(index,b in buf)
       mask1 = preB + index
       preB = b
       result = (b xor mask1) - 5C

image.png

在汇编解码过程中对应ue的字节数,逐位去记录下意思,这之中除了上面的AES算法外,还有一个就是对这个文件有两种版本,而且在末尾字节对齐等上有点特殊的处理逻辑。

既然分析出来了,那就是写一段代码,直接把这个文件先解析出来了看看里面的庐山真面目。

先定义一个PackFile的文件结构体,里面的成员大多是我猜的,有些猜不出来意思的就占位写着,属于完整的解析,每个字节都会被存储到。

#pragma once
#include <stdint.h>
#include <string>

using namespace std;

//文件头/信息摘要
typedef struct FILE_INFO_SUMMARY  
{
	uint8_t filePathLen;
	char filePath[256]; 
	uint32_t datFileIndex; //version_20000中映射dat文件索引名,和DAT_FILE_INFO_SUMMARY.pathFormat去拼接dat路径
	uint8_t secType;  //加密类型 低四位看代码目前是<=0x4 的取值范围,2为固定算法,3位配置密钥
	uint32_t startOffset; //起始偏移位置 seek下
	uint32_t allFileLen;
	uint32_t firstFileLen;
	uint32_t flag1; //未知作用
	uint32_t flag2; //未知作用
} File_Info_Summary;

typedef struct DAT_FILE_INFO_SUMMARY  
{
	uint8_t allInfoLen;  //单独读4个字节出来解密
	uint8_t pathFormatLen;
	char pathFormat[256]; 
	uint32_t datFileCount; //总共多少个dat映射文件。用这个数字从0顺序生成%08x 固定8位的16进制就是dat的对应文件名
} Dat_File_Info_Summary;

typedef struct TAIL_EXTRA_INFO  //尾部附加信息
{
	uint8_t extraTypeLen; //类型长度 4
	char extraType[16];  //附加类型  如:"SCRK" 为密钥类型
	uint32_t extraLen;  //附加信息长度 0x13
	char extra[128]; //附加信息 如果是加密key 如B0000中末尾为 "BelleIslePF-SCR-KEY"  
}Tail_Extra_Info;

class PackFile
{
//*FPSF Head*/
public: //==懒得写get set方法了
	string m_Tag;  //文件头标记 FPSF文件是一个加密打包文件 
	uint32_t m_Version; //版本  version分为0x10000、0x20000两个版本,0x20000类型会有对应的.dat文件,头部多了点信息头
	uint32_t m_EncryptionFileInfocount;  //加密 文件头/信息个数
	uint32_t m_EncryptionAllFileinfoSize;  //加密 头信息总大小
	Dat_File_Info_Summary m_DataFileInfo;  //versino_20000 用到的dat信息摘要头 

//*FPSF Tail Extras*/
public:
	uint32_t m_ExtrasCount;  //附加信息个数
	uint32_t m_ExtrasBufLen;  //附加信息总长度  
	Tail_Extra_Info* m_Extras; //附加信息列表

public:
	File_Info_Summary* m_Fileinfo_Sums; 
	uint32_t m_Fileinfo_Totalsize;

public:
	PackFile(void);
	~PackFile(void);
};

单个文件解析器的头文件如下:

#pragma once
#include <string>
#include <fstream>
#include "PackFile.h"
#include <io.h>
#include<iostream>
#include <assert.h>
#include <direct.h>
#include "utils.h"

using namespace std;

/**
* 文件解析器
*/
class PackFileParser
{
private:
	string m_dataRootDir;
	string m_FPSFFile_Path;
	ifstream m_FPSFFile_Stream;
	fstream m_FPSFDecrypt_Stream;
    PackFile* m_PackFile;

public:
	PackFileParser(void);
	~PackFileParser(void);

private:
	int parseFPSFHead();
	int parseDataFilesetInfo();
	int parseFileInfoSummary();
	int parseFPSFTailExtras();
	int extractV1(const char* saveDir);
	int extractV2(const char* saveDir);
	int decrypt(char *buf, uint32_t size);
	int decrypt_SCRK(char *buf, uint32_t size, char* key, uint32_t keylen);
public:
	int parse(const char* fpsfPath, const char* dataRootDir);
	int extractAll(const char* saveDir);
};

解析过程(有点长,代码也随手写的,有点烂,以解析成功为目的,不管代码是否漂亮了),这个cpp先全部贴这里了,不准备上github, 中间有一些注释,解释太累了,因为每步都要对着汇编处和UE原始字节处多个解析文件来回对比验证,解密完了就是解密后的格式结构解析,如下去看很多小细节点经过多文件测试才出来的,肝~~~

#include "StdAfx.h"
#include "PackFileParser.h"

PackFileParser::PackFileParser(void)
{
	m_PackFile = new PackFile();
}

PackFileParser::~PackFileParser(void)
{
	if ( m_FPSFFile_Stream.is_open() )
		m_FPSFFile_Stream.close(); 
	if ( m_FPSFDecrypt_Stream.is_open() )
		m_FPSFDecrypt_Stream.close(); 
	delete m_PackFile;
}

int PackFileParser::parse(const char* fpsfPath, const char* dataRootDir)
{
	m_FPSFFile_Path.assign(fpsfPath);
	m_dataRootDir.assign(dataRootDir);
	m_FPSFFile_Stream.open((char*)m_FPSFFile_Path.c_str(),ios::in | ios:: binary);
	if ( !m_FPSFFile_Stream.is_open() ) //打开文件失败 -1
		return -1;

	int rst = 1;
	do{
		//===test写输出,方便查看和调试===
		string tmpFilesInfoPath;
		tmpFilesInfoPath.append(m_FPSFFile_Path);
		tmpFilesInfoPath.append(".DecryptHeadTail.txt");
		if (_access(tmpFilesInfoPath.c_str(), 0) == 0) //文件存在删除
			remove(tmpFilesInfoPath.c_str());
		m_FPSFDecrypt_Stream.open(tmpFilesInfoPath.c_str(), fstream::out | fstream::binary);
		if ( !m_FPSFDecrypt_Stream.is_open() ) //打开输出解密文件失败
		{
			return -2;
			break;
		}
		//==
		if (rst = parseFPSFHead() < 0) //读取文件头 固定16字节
			break;
		if(m_PackFile->m_Version == 0x20000) //V2 读取dat信息 
		{
			if (rst = parseDataFilesetInfo() < 0)  //4字节+ Dat_File_Info_Summary allInfoLen
				break;
		}
		if (rst = parseFileInfoSummary() < 0) //读取目录文件信息列表  m_EncryptionAllFileinfoSize字节
			break;

		//中间v20000没有数据了,在data中去。直接是尾巴
		//v10000中是具体的file data数据,然后是尾巴
		int seekpos = 0;
		if(m_PackFile->m_Version == 0x10000)
			seekpos = 16 + m_PackFile->m_EncryptionAllFileinfoSize + m_PackFile->m_Fileinfo_Totalsize;
		else if(m_PackFile->m_Version == 0x20000)
			seekpos = 16 + (4 + m_PackFile->m_DataFileInfo.allInfoLen) + m_PackFile->m_EncryptionAllFileinfoSize;
		m_FPSFFile_Stream.seekg(0, std::ios_base::end); //移动末尾看长度
		if(m_FPSFFile_Stream.tellg() <= seekpos)
			break;

		m_FPSFFile_Stream.seekg(seekpos);
		if (rst = parseFPSFTailExtras() < 0) //读尾巴
			break;
	}while(0);

	if ( m_FPSFFile_Stream.is_open() )
		m_FPSFFile_Stream.close(); 
	if ( m_FPSFDecrypt_Stream.is_open() )
		m_FPSFDecrypt_Stream.close(); 
	return rst;
}

//读包头信息
int PackFileParser::parseFPSFHead()
{
	char buffer[16];
	char * p= (char *)&buffer;
	m_FPSFFile_Stream.read(p, sizeof(buffer));
	m_FPSFDecrypt_Stream.write(buffer , 16);

	m_PackFile->m_Tag = string(p,4);  //"FPSF"
	if(m_PackFile->m_Tag.compare("FPSF") != 0) //文件类型错误 -3
		return -3;   
	p+=4;
	memcpy(&m_PackFile->m_Version, p, 4);
	p+=4;
	memcpy(&m_PackFile->m_EncryptionFileInfocount, p, 4);
	p+=4;
	memcpy(&m_PackFile->m_EncryptionAllFileinfoSize, p, 4);

	return 1;
}

int PackFileParser::parseDataFilesetInfo()
{
	char tempBuf[4];
	m_FPSFFile_Stream.read((char *)&tempBuf, 4);
	decrypt((char *)&tempBuf,4);  //解密1字节
	m_FPSFDecrypt_Stream.write(tempBuf , 4);

	uint8_t len;
	memcpy(&len, tempBuf, 1); 
	m_PackFile->m_DataFileInfo.allInfoLen=len;
	
	char * buf = (char *)malloc(len);
	char * p = buf;
	m_FPSFFile_Stream.read(buf, len); 
	decrypt(buf, len); //解密真实头
	m_FPSFDecrypt_Stream.write(buf, len);

	m_PackFile->m_DataFileInfo.pathFormatLen = p[0];  //赋值
	p++;
	memset(&m_PackFile->m_DataFileInfo.pathFormat,0, 256);
	memcpy(&m_PackFile->m_DataFileInfo.pathFormat, p, m_PackFile->m_DataFileInfo.pathFormatLen);
	p+=m_PackFile->m_DataFileInfo.pathFormatLen;
	memcpy(&m_PackFile->m_DataFileInfo.datFileCount, p, 4);

	free(buf);
	return 1;
}

int PackFileParser::parseFileInfoSummary()
{
	char* buf = (char *)malloc(m_PackFile->m_EncryptionAllFileinfoSize);
	char* p =buf;
	m_FPSFFile_Stream.read(buf, m_PackFile->m_EncryptionAllFileinfoSize);
	decrypt(buf,m_PackFile->m_EncryptionAllFileinfoSize);  //解密
	m_FPSFDecrypt_Stream.write(buf , m_PackFile->m_EncryptionAllFileinfoSize);
	m_PackFile->m_Fileinfo_Totalsize = 0;

	uint32_t allModuleLen=0;
	m_PackFile->m_Fileinfo_Sums = (File_Info_Summary*)malloc(m_PackFile->m_EncryptionFileInfocount *sizeof(File_Info_Summary));
	for(uint32_t i=0;i<m_PackFile->m_EncryptionFileInfocount;i++)
	{  
		//逐个解析
		File_Info_Summary* fi = &m_PackFile->m_Fileinfo_Sums[i];

		memcpy(&fi->filePathLen, p, 1);
		p+=1;
		memset(&fi->filePath,0, 256);
		memcpy(&fi->filePath, p, fi->filePathLen);
		p+=fi->filePathLen;
		if(m_PackFile->m_Version == 0x20000)
		{
			memcpy(&fi->datFileIndex, p, 4);
			p+=4;
		}
		memcpy(&fi->secType, p, 1);
		p+=1;
		memcpy(&fi->startOffset, p, 4);
		p+=4;

		memcpy(&fi->allFileLen, p, 4);
		p+=4;
		m_PackFile->m_Fileinfo_Totalsize += fi->allFileLen;

		memcpy(&fi->firstFileLen, p, 4);
		p+=4;
		allModuleLen+=fi->firstFileLen;

		memcpy(&fi->flag1, p, 4);
		p+=4;
		memcpy(&fi->flag2, p, 4);
		p+=4;
	}
	free(buf);
	return 1;
}

int PackFileParser::parseFPSFTailExtras()
{
	char temp[8];
	m_FPSFFile_Stream.read(temp, 8);
	decrypt(temp,8);
	m_FPSFDecrypt_Stream.write(temp, 8);

	memcpy(&m_PackFile->m_ExtrasCount, &temp[0], 4);
	memcpy(&m_PackFile->m_ExtrasBufLen, &temp[4], 4);
	if(m_PackFile->m_ExtrasCount <=0) //没有附加信息
		return 1;
	if(m_PackFile->m_ExtrasBufLen <=0)
		return 1;
	assert(m_PackFile->m_ExtrasCount == 1); //看是否存在多个情况
	
	char* buf = (char *)malloc(m_PackFile->m_ExtrasBufLen); //读密钥
	char* p = buf;
	m_FPSFFile_Stream.read(buf, m_PackFile->m_ExtrasBufLen);
	decrypt(buf,m_PackFile->m_ExtrasBufLen);
	m_FPSFDecrypt_Stream.write(buf,m_PackFile->m_ExtrasBufLen);

	m_PackFile->m_Extras = (Tail_Extra_Info*)malloc(m_PackFile->m_ExtrasCount *sizeof(Tail_Extra_Info));

	for(uint32_t i=0;i < m_PackFile->m_ExtrasCount;i++)
	{
		Tail_Extra_Info* p_extra = &m_PackFile->m_Extras[i];
		memcpy(&p_extra->extraTypeLen, p, 1);
		p+=1;
		memset(&p_extra->extraType,0, 16);
		memcpy(&p_extra->extraType, p, p_extra->extraTypeLen);
		p+=p_extra->extraTypeLen;
		memcpy(&p_extra->extraLen, p, 4);
		p+=4;
		memset(&p_extra->extra,0, 128);
		memcpy(&p_extra->extra, p, p_extra->extraLen);
	}
	free(buf);
	return 1;
}

int PackFileParser::extractV1(const char* saveDir)
{
	m_FPSFFile_Stream.open((char*)m_FPSFFile_Path.c_str(),ios::in | ios:: binary);
	if ( !m_FPSFFile_Stream.is_open() ) //打开文件失败 -1
		return -1;

	for(uint32_t i=0;i<m_PackFile->m_EncryptionFileInfocount; i++)
	{
		File_Info_Summary* fi = &m_PackFile->m_Fileinfo_Sums[i];

		char * buf = (char *)malloc(fi->firstFileLen);
		//起始位置seek下
		m_FPSFFile_Stream.seekg(fi->startOffset);
		m_FPSFFile_Stream.read(buf, fi->firstFileLen);
		//解密
		decrypt(buf,fi->firstFileLen);
		//==写入文件看看
		char *tempStr= strchr(fi->filePath,'\\')+1;
		string saveFile;
		saveFile.append(saveDir);
		saveFile.append(tempStr);
		createDirectory(saveFile); //如果子目录不存在创建子目录
		fstream file_output;    
		file_output.open(saveFile, fstream::out | fstream::binary);
		file_output.write(buf , fi->firstFileLen);
		file_output.close();
		//==
		free(buf); //释放临时缓存

		cout<<"[Success Pack V1 File] "<<  saveFile  <<endl;
	}
	if ( m_FPSFFile_Stream.is_open() )
		m_FPSFFile_Stream.close(); 
	return 1;
}

int PackFileParser::extractV2(const char* saveDir) //去dat映射文件中去解析内容
{
	for(uint32_t i=0;i<m_PackFile->m_EncryptionFileInfocount;i++)
	{
		File_Info_Summary* fi = &m_PackFile->m_Fileinfo_Sums[i];
		char *tempStr= strchr(fi->filePath,'\\')+1;
		assert(fi->datFileIndex <= m_PackFile->m_DataFileInfo.datFileCount); //断言索引 不会超过dat文件数

		char datfilePath[256] = {0}; 
		sprintf((char*)&datfilePath, m_PackFile->m_DataFileInfo.pathFormat,fi->datFileIndex); //  ./B0000/00000000.dat
		char *tempdatPath = strchr((char*)&datfilePath, '/')+1; //or string.substr(2)    B0000/00000000.dat
		string dataRealPath;
		dataRealPath.append(m_dataRootDir);
		dataRealPath.append(tempdatPath);
		stringReplace(dataRealPath, "/", "\\");

		ifstream dat_ifs(dataRealPath.c_str(),ios::in | ios:: binary);
		assert(dat_ifs.is_open());
		char* buf = (char *)malloc(fi->firstFileLen);
		dat_ifs.seekg(fi->startOffset);
		dat_ifs.read(buf, fi->firstFileLen);
		//解密算法
		if(fi->secType == 2)
			decrypt(buf,fi->firstFileLen);
		else if(fi->secType == 3)
		{
			//遍历它,去除加密头的那个
			Tail_Extra_Info* secExtra = NULL;
			for(uint32_t i=0;i< m_PackFile->m_ExtrasCount;i++)
			{
				Tail_Extra_Info* p_Extra = &m_PackFile->m_Extras[i];
				if(strcmp(p_Extra->extraType, "SCRK")==0){
					secExtra = p_Extra;
					break;
				}
			}
			assert(secExtra);
			decrypt_SCRK(buf,fi->firstFileLen, secExtra->extra, secExtra->extraLen);
		}
		else
			assert(0);

		//==保存文件
		tempStr= strchr(fi->filePath,'\\')+1;
		string saveFile;
		saveFile.append(saveDir);
		saveFile.append(tempStr);
		createDirectory(saveFile); //如果子目录不存在创建子目录
		fstream file_output;    
		file_output.open(saveFile, fstream::out | fstream::binary);
		file_output.write(buf , fi->firstFileLen);
		file_output.close();
		//==
		free(buf); //释放临时缓存

		cout<<"[Success Pack V2 File] "<<  saveFile  <<endl;

		if (dat_ifs.is_open())
			dat_ifs.close();
	}
	return 1;
}

int PackFileParser::extractAll(const char* saveDir)
{
	if(m_PackFile->m_Version == 0x10000)
	{
		return extractV1(saveDir);
	}

	if(m_PackFile->m_Version == 0x20000)
	{
		return extractV2(saveDir);
	}

	return 1;
}

int PackFileParser::decrypt(char *buf, uint32_t size)
{ 
	char preB = 0x6A; //写死
	char result;
	for(uint32_t i=0;i<size;i++)
	{
		result = (char)((preB+i)^buf[i]) - 0x5C; //写死
		preB = buf[i];
		buf[i] = result;
	}
	return 0;
}

int PackFileParser::decrypt_SCRK(char *buf, uint32_t size, char* key, uint32_t keylen)
{ 
	//这种算法更简单,直接用key做减法
	int keyCursor = 0;
	for(uint32_t i=0;i<size;i++)
	{
		buf[i] -= key[keyCursor];
		keyCursor++;
		keyCursor %= keylen;
	}
	return 0;
}

单文件解析完成后,就是多文件的批量解析了。这块是常规代码没有啥

image.png

我写了一个提取器,然后无非就是遍历目录下的 bpf和lpf文件进行逐个解析与输出。

解析完成后:

filelist的内容为:

image.png

注意这里要用日语 shift-jis字符查看,后面的注释才不是乱码。 可以看出来是对应了 data目录下的每个文件夹的 bpf/lpf文件列表,并且有一些备注,这些备注对应了data目录下那些子目录的作用,我开始以为是遍历data下面的所有目录就行,bin下这个list是为了精确的管控目录,比如data下面某个目录因为升级等不需要了,也没有删除或者删除失败,这样有bin下的这个总表的设计,就不需要去进行目录遍历,以这个总表为准,这样的设计更合适一些。

data目录中bpf解析后如下:

image.png

可以根据之前的packfile结构看出来,他对应 .dat文件的偏移,以及打包在里面的文件和目录结构,在前面的PackFileParser中,我们就是根据 这个列表 去解析 dat文件,并且重建真实目录和原始文件。

这里开发商其实就是在lt的真实目录进行打包,可以简单理解一个目录加一个zip,将整个目录进行打包成为片段,一方面加密,另外一方面可能是为了更新等。

游戏运行过程中其实他整体在内存中去维护了这么一个目录结构,并且需要的时候取实时解析文件,并不会落地为目录,因为落地后估计很多人直接就提取走了资源, 在这里我将他全部落地到磁盘中。落地后结构如下:

image.png

除了map和sound外的其他目录为从 .dat中重新解析出来。 这里面有很多的文件夹和文件类型。 整体文件目录结构和 lithtech 很像,但是里面的下一级文件类型差别很大。

应需还是传下整体的解析源码: github.com/lang2858/FP…

后面的文章中进行里面的每种格式进行分析。 主要是 模型 、动画 、地图等。