经过PM监控发现,首先读取的文件为 filelist.dat, 这个文件在bin中,而他的格式刚好也是FPSF格式。
为了搞清楚这种格式的解密算法。
我们直接跟踪这个函数的算法,因为汇编中 readfile后,然后会分配一块固定的缓存来存取这块数据,我们经过断点,在读取文件的附近,单步调试跟踪这块内存的解析。
汇编分析的过程有点费肝...不对着汇编代码逐步解析记录了,结果分析到这个文件采用了一个对称加密的算法,这个算法核心伪代码大致如下。
//掩码 6A 5C ,定义死的
preB = 6A
for(index,b in buf)
mask1 = preB + index
preB = b
result = (b xor mask1) - 5C
在汇编解码过程中对应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;
}
单文件解析完成后,就是多文件的批量解析了。这块是常规代码没有啥
我写了一个提取器,然后无非就是遍历目录下的 bpf和lpf文件进行逐个解析与输出。
解析完成后:
filelist的内容为:
注意这里要用日语 shift-jis字符查看,后面的注释才不是乱码。 可以看出来是对应了 data目录下的每个文件夹的 bpf/lpf文件列表,并且有一些备注,这些备注对应了data目录下那些子目录的作用,我开始以为是遍历data下面的所有目录就行,bin下这个list是为了精确的管控目录,比如data下面某个目录因为升级等不需要了,也没有删除或者删除失败,这样有bin下的这个总表的设计,就不需要去进行目录遍历,以这个总表为准,这样的设计更合适一些。
data目录中bpf解析后如下:
可以根据之前的packfile结构看出来,他对应 .dat文件的偏移,以及打包在里面的文件和目录结构,在前面的PackFileParser中,我们就是根据 这个列表 去解析 dat文件,并且重建真实目录和原始文件。
这里开发商其实就是在lt的真实目录进行打包,可以简单理解一个目录加一个zip,将整个目录进行打包成为片段,一方面加密,另外一方面可能是为了更新等。
游戏运行过程中其实他整体在内存中去维护了这么一个目录结构,并且需要的时候取实时解析文件,并不会落地为目录,因为落地后估计很多人直接就提取走了资源, 在这里我将他全部落地到磁盘中。落地后结构如下:
除了map和sound外的其他目录为从 .dat中重新解析出来。 这里面有很多的文件夹和文件类型。 整体文件目录结构和 lithtech 很像,但是里面的下一级文件类型差别很大。
应需还是传下整体的解析源码: github.com/lang2858/FP…
后面的文章中进行里面的每种格式进行分析。 主要是 模型 、动画 、地图等。