mirror of
https://github.com/Vita3K/psvpfsparser.git
synced 2024-11-26 23:20:27 +00:00
438 lines
15 KiB
C++
438 lines
15 KiB
C++
#include "PfsPageMapper.h"
|
|
|
|
#include "SecretGenerator.h"
|
|
#include "UnicvDbParser.h"
|
|
#include "FilesDbParser.h"
|
|
|
|
PfsPageMapper::PfsPageMapper(std::shared_ptr<ICryptoOperations> cryptops, std::shared_ptr<IF00DKeyEncryptor> iF00D, std::ostream& output, const unsigned char* klicensee, const psvpfs::path& titleIdPath)
|
|
: m_cryptops(cryptops), m_iF00D(iF00D), m_output(output), m_titleIdPath(titleIdPath)
|
|
{
|
|
memcpy(m_klicensee, klicensee, 0x10);
|
|
}
|
|
|
|
std::shared_ptr<sce_junction> PfsPageMapper::brutforce_hashes(const std::unique_ptr<FilesDbParser>& filesDbParser, std::map<sce_junction, std::vector<std::uint8_t>>& fileDatas, const unsigned char* secret, const unsigned char* signature) const
|
|
{
|
|
const sce_ng_pfs_header_t& ngpfs = filesDbParser->get_header();
|
|
|
|
unsigned char signature_key[0x14] = {0};
|
|
|
|
if(img_spec_to_is_unicv(ngpfs.image_spec))
|
|
{
|
|
//we will be checking only first sector of each file hence we can precalculate a signature_key
|
|
//because both secret and sector_salt will not vary
|
|
int sector_salt = 0; //sector number is most likely a salt which is logically correct in terms of xts-aes
|
|
m_cryptops->hmac_sha1((unsigned char*)§or_salt, signature_key, 4, secret, 0x14);
|
|
}
|
|
else
|
|
{
|
|
//for icv files sector salt is not used. salt is global and is specified in the name of the file
|
|
//this means that we can just use secret as is
|
|
memcpy(signature_key, secret, 0x14);
|
|
}
|
|
|
|
//go through each first sector of the file
|
|
for(auto& f : fileDatas)
|
|
{
|
|
//calculate sector signature
|
|
unsigned char realSignature[0x14] = {0};
|
|
m_cryptops->hmac_sha1(f.second.data(), realSignature, f.second.size(), signature_key, 0x14);
|
|
|
|
//try to match the signatures
|
|
if(memcmp(signature, realSignature, 0x14) == 0)
|
|
{
|
|
std::shared_ptr<sce_junction> found_path(new sce_junction(f.first));
|
|
//remove newly found path from the search list to reduce time with each next iteration
|
|
fileDatas.erase(f.first);
|
|
return found_path;
|
|
}
|
|
}
|
|
|
|
return std::shared_ptr<sce_junction>();
|
|
}
|
|
|
|
//this is a tree walker function and it should not be a part of the class
|
|
int find_zero_sector_index(std::shared_ptr<merkle_tree_node<icv> > node, void* ctx)
|
|
{
|
|
std::pair<std::uint32_t, std::uint32_t>* ctx_pair = (std::pair<std::uint32_t, std::uint32_t>*)ctx;
|
|
|
|
if(node->isLeaf())
|
|
{
|
|
if(node->m_index == 0)
|
|
{
|
|
ctx_pair->second = ctx_pair->first; //save global counter to result
|
|
return -1;
|
|
}
|
|
else
|
|
{
|
|
ctx_pair->first++; //increase global counter
|
|
return 0;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
ctx_pair->first++; //increase global counter
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
//this is a tree walker function and it should not be a part of the class
|
|
int assign_hash(std::shared_ptr<merkle_tree_node<icv> > node, void* ctx)
|
|
{
|
|
if(!node->isLeaf())
|
|
return 0;
|
|
|
|
std::map<std::uint32_t, icv>* sectorHashMap = (std::map<std::uint32_t, icv>*)ctx;
|
|
|
|
auto item = sectorHashMap->find(node->m_index);
|
|
if(item == sectorHashMap->end())
|
|
throw std::runtime_error("Missing sector hash");
|
|
|
|
node->m_context.m_data.assign(item->second.m_data.begin(), item->second.m_data.end());
|
|
|
|
return 0;
|
|
}
|
|
|
|
//this is a tree walker function and it should not be a part of the class
|
|
int combine_hash(std::shared_ptr<merkle_tree_node<icv> > result, std::shared_ptr<merkle_tree_node<icv> > left, std::shared_ptr<merkle_tree_node<icv> > right, void* ctx)
|
|
{
|
|
unsigned char bytes28[0x28] = {0};
|
|
memcpy(bytes28, left->m_context.m_data.data(), 0x14);
|
|
memcpy(bytes28 + 0x14, right->m_context.m_data.data(), 0x14);
|
|
|
|
std::pair<std::shared_ptr<ICryptoOperations>, unsigned char*>* ctx_cast = (std::pair<std::shared_ptr<ICryptoOperations>, unsigned char*>*)ctx;
|
|
|
|
std::shared_ptr<ICryptoOperations> cryptops = ctx_cast->first;
|
|
unsigned char* secret = ctx_cast->second;
|
|
|
|
result->m_context.m_data.resize(0x14);
|
|
cryptops->hmac_sha1(bytes28, result->m_context.m_data.data(), 0x28, secret, 0x14);
|
|
|
|
return 0;
|
|
}
|
|
|
|
//this is a tree walker function and it should not be a part of the class
|
|
int collect_hash(std::shared_ptr<merkle_tree_node<icv> > node, void* ctx)
|
|
{
|
|
std::vector<icv>* hashTable = (std::vector<icv>*)ctx;
|
|
hashTable->push_back(node->m_context);
|
|
return 0;
|
|
}
|
|
|
|
int PfsPageMapper::compare_hash_tables(const std::vector<icv>& left, const std::vector<icv>& right)
|
|
{
|
|
if(left.size() != right.size())
|
|
return -1;
|
|
|
|
for(std::size_t i = 0; i < left.size(); i++)
|
|
{
|
|
if(memcmp(left[i].m_data.data(), right[i].m_data.data(), 0x14) != 0)
|
|
return -1;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
//pageMap - relates icv salt (icv filename) to junction (real file in filesystem)
|
|
//merkleTrees - relates icv table entry (icv file) to merkle tree of real file
|
|
//idea is to find icv table entry by icv filename - this way we can relate junction to merkle tree
|
|
//then we can read the file and hash it into merkle tree
|
|
//then merkle tree is collected into hash table
|
|
//then hash table is compared to the hash table from icv table entry
|
|
int PfsPageMapper::validate_merkle_trees(const std::unique_ptr<FilesDbParser>& filesDbParser, std::vector<std::pair<std::shared_ptr<sce_iftbl_base_t>, std::shared_ptr<merkle_tree<icv> > > >& merkleTrees)
|
|
{
|
|
const sce_ng_pfs_header_t& ngpfs = filesDbParser->get_header();
|
|
|
|
m_output << "Validating merkle trees..." << std::endl;
|
|
|
|
for(auto entry : merkleTrees)
|
|
{
|
|
//get table
|
|
std::shared_ptr<sce_iftbl_base_t> table = entry.first;
|
|
|
|
//calculate secret
|
|
unsigned char secret[0x14];
|
|
scePfsUtilGetSecret(m_cryptops, m_iF00D, secret, m_klicensee, ngpfs.files_salt, img_spec_to_crypto_engine_flag(ngpfs.image_spec), table->get_icv_salt(), 0);
|
|
|
|
//find junction
|
|
auto junctionIt = m_pageMap.find(table->get_icv_salt());
|
|
if(junctionIt == m_pageMap.end())
|
|
{
|
|
m_output << "Table item not found in page map" << std::endl;
|
|
return -1;
|
|
}
|
|
|
|
const sce_junction& junction = junctionIt->second;
|
|
|
|
//read junction into sector map
|
|
std::ifstream inputStream;
|
|
junction.open(inputStream);
|
|
|
|
std::uint32_t sectorSize = table->get_header()->get_fileSectorSize();
|
|
std::uintmax_t fileSize = junction.file_size();
|
|
|
|
std::uint32_t nSectors = static_cast<std::uint32_t>(fileSize / sectorSize);
|
|
std::uint32_t tailSize = fileSize % sectorSize;
|
|
|
|
std::map<std::uint32_t, icv> sectorHashMap;
|
|
|
|
std::vector<std::uint8_t> raw_data(sectorSize);
|
|
for(std::uint32_t i = 0; i < nSectors; i++)
|
|
{
|
|
auto currentItem = sectorHashMap.insert(std::make_pair(i, icv()));
|
|
icv& currentIcv = currentItem.first->second;
|
|
|
|
inputStream.read((char*)raw_data.data(), sectorSize);
|
|
|
|
currentIcv.m_data.resize(0x14);
|
|
m_cryptops->hmac_sha1(raw_data.data(), currentIcv.m_data.data(), sectorSize, secret, 0x14);
|
|
}
|
|
|
|
if(tailSize > 0)
|
|
{
|
|
auto currentItem = sectorHashMap.insert(std::make_pair(nSectors, icv()));
|
|
icv& currentIcv = currentItem.first->second;
|
|
|
|
inputStream.read((char*)raw_data.data(), tailSize);
|
|
|
|
currentIcv.m_data.resize(0x14);
|
|
m_cryptops->hmac_sha1(raw_data.data(), currentIcv.m_data.data(), tailSize, secret, 0x14);
|
|
}
|
|
|
|
try
|
|
{
|
|
//get merkle tree (it should already be indexed)
|
|
std::shared_ptr<merkle_tree<icv> > mkt = entry.second;
|
|
|
|
//assign hashes to leaves
|
|
walk_tree(mkt, assign_hash, §orHashMap);
|
|
|
|
//calculate node hashes
|
|
auto combine_ctx = std::make_pair(m_cryptops, secret);
|
|
bottom_top_walk_combine(mkt, combine_hash, &combine_ctx);
|
|
|
|
//collect hashes into table
|
|
std::vector<icv> hashTable;
|
|
walk_tree(mkt, collect_hash, &hashTable);
|
|
|
|
//compare tables
|
|
if(compare_hash_tables(hashTable, table->m_blocks.front().m_signatures) < 0)
|
|
{
|
|
m_output << "Merkle tree is invalid in file " << junction << std::endl;
|
|
return -1;
|
|
}
|
|
|
|
m_output << "File: " << std::hex << table->get_icv_salt() << " [OK]" << std::endl;
|
|
}
|
|
catch(std::runtime_error& e)
|
|
{
|
|
m_output << e.what() << std::endl;
|
|
return -1;
|
|
}
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
//filesDbParser and unicvDbParser are not made part of the context of PfsPageMapper
|
|
//the reason is because both filesDbParser and unicvDbParser have to be
|
|
//initialized with parse method externally prior to calling bruteforce_map
|
|
//having filesDbParser and unicvDbParser as constructor arguments will
|
|
//introduce ambiguity in usage of PfsPageMapper
|
|
int PfsPageMapper::bruteforce_map(const std::unique_ptr<FilesDbParser>& filesDbParser, const std::unique_ptr<UnicvDbParser>& unicvDbParser)
|
|
{
|
|
const sce_ng_pfs_header_t& ngpfs = filesDbParser->get_header();
|
|
const std::unique_ptr<sce_idb_base_t>& unicv = unicvDbParser->get_idatabase();
|
|
|
|
if(img_spec_to_is_unicv(ngpfs.image_spec))
|
|
m_output << "Building unicv.db -> files.db relation..." << std::endl;
|
|
else
|
|
m_output << "Building icv.db -> files.db relation..." << std::endl;
|
|
|
|
psvpfs::path root(m_titleIdPath);
|
|
|
|
//check file fileSectorSize
|
|
std::set<std::uint32_t> fileSectorSizes;
|
|
for(auto& t : unicv->m_tables)
|
|
{
|
|
//skip SCEINULL blocks
|
|
if(t->m_blocks.size() > 0)
|
|
fileSectorSizes.insert(t->get_header()->get_fileSectorSize());
|
|
}
|
|
|
|
if(fileSectorSizes.size() > 1)
|
|
{
|
|
m_output << "File sector size is not unique. This bruteforce mode is not supported now" << std::endl;
|
|
return -1;
|
|
}
|
|
|
|
std::uint32_t uniqueSectorSize = *fileSectorSizes.begin();
|
|
|
|
//get all files and directories
|
|
std::set<psvpfs::path> files;
|
|
std::set<psvpfs::path> directories;
|
|
getFileListNoPfs(root, files, directories);
|
|
|
|
//pre read all the files once
|
|
std::map<sce_junction, std::vector<std::uint8_t>> fileDatas;
|
|
for(auto& real_file : files)
|
|
{
|
|
sce_junction sp(real_file);
|
|
sp.link_to_real(real_file);
|
|
|
|
std::uintmax_t fsz = sp.file_size();
|
|
|
|
// using uniqueSectorSize here.
|
|
// in theory this size may vary per SCEIFTBL - this will make bruteforcing a bit harder.
|
|
// files can not be pre read in this case
|
|
// in practice though it does not change.
|
|
std::uintmax_t fsz_limited = (fsz < uniqueSectorSize) ? fsz : uniqueSectorSize;
|
|
|
|
//empty files should be allowed!
|
|
if(fsz_limited == 0)
|
|
{
|
|
m_output << "File " << sp << " is empty" << std::endl;
|
|
m_emptyFiles.insert(sp);
|
|
}
|
|
else
|
|
{
|
|
const auto& fdt = fileDatas.insert(std::make_pair(sp, std::vector<std::uint8_t>(static_cast<std::vector<std::uint8_t>::size_type>(fsz_limited))));
|
|
|
|
std::ifstream in;
|
|
if(!sp.open(in))
|
|
{
|
|
m_output << "Failed to open " << sp << std::endl;
|
|
return -1;
|
|
}
|
|
|
|
in.read((char*)fdt.first->second.data(), fsz_limited);
|
|
in.close();
|
|
}
|
|
}
|
|
|
|
std::vector<std::pair<std::shared_ptr<sce_iftbl_base_t>, std::shared_ptr<merkle_tree<icv> > > > merkleTrees;
|
|
|
|
//brutforce each sce_iftbl_t record
|
|
for(auto& t : unicv->m_tables)
|
|
{
|
|
//process only files that are not empty
|
|
if(t->get_header()->get_numSectors() > 0)
|
|
{
|
|
//generate secret - one secret per unicv.db page is required
|
|
unsigned char secret[0x14];
|
|
scePfsUtilGetSecret(m_cryptops, m_iF00D, secret, m_klicensee, ngpfs.files_salt, img_spec_to_crypto_engine_flag(ngpfs.image_spec), t->get_icv_salt(), 0);
|
|
|
|
std::shared_ptr<sce_junction> found_path;
|
|
|
|
if(img_spec_to_is_unicv(ngpfs.image_spec))
|
|
{
|
|
//in unicv - hash table has same order as sectors in a file
|
|
const unsigned char* zeroSectorIcv = t->m_blocks.front().m_signatures.front().m_data.data();
|
|
|
|
//try to find match by hash of zero sector
|
|
found_path = brutforce_hashes(filesDbParser, fileDatas, secret, zeroSectorIcv);
|
|
}
|
|
else
|
|
{
|
|
try
|
|
{
|
|
//create merkle tree for corresponding table
|
|
std::shared_ptr<merkle_tree<icv> > mkt = generate_merkle_tree<icv>(t->get_header()->get_numSectors());
|
|
index_merkle_tree(mkt);
|
|
|
|
//save merkle tree
|
|
merkleTrees.push_back(std::make_pair(t, mkt));
|
|
|
|
//use merkle tree to find index of zero sector in hash table
|
|
std::pair<std::uint32_t, std::uint32_t> ctx;
|
|
walk_tree(mkt, find_zero_sector_index, &ctx);
|
|
|
|
//in icv - hash table is ordered according to merkle tree structure
|
|
//that is why it is required to walk through the tree to find zero sector hash in hash table
|
|
const unsigned char* zeroSectorIcv = t->m_blocks.front().m_signatures.at(ctx.second).m_data.data();
|
|
|
|
//try to find match by hash of zero sector
|
|
found_path = brutforce_hashes(filesDbParser, fileDatas, secret, zeroSectorIcv);
|
|
}
|
|
catch(std::runtime_error& e)
|
|
{
|
|
m_output << e.what() << std::endl;
|
|
return -1;
|
|
}
|
|
}
|
|
|
|
if(found_path)
|
|
{
|
|
m_output << "Match found: " << std::hex << t->get_icv_salt() << " " << *found_path << std::endl;
|
|
m_pageMap.insert(std::make_pair(t->get_icv_salt(), *found_path));
|
|
}
|
|
else
|
|
{
|
|
m_output << "Match not found: " << std::hex << t->get_icv_salt() << std::endl;
|
|
return -1;
|
|
}
|
|
}
|
|
}
|
|
|
|
//in icv - additional step checks that hash table corresponds to merkle tree
|
|
if(!img_spec_to_is_unicv(ngpfs.image_spec))
|
|
{
|
|
if(validate_merkle_trees(filesDbParser, merkleTrees) < 0)
|
|
return -1;
|
|
}
|
|
|
|
if(files.size() != (m_pageMap.size() + m_emptyFiles.size()))
|
|
{
|
|
m_output << "Extra files are left after mapping (warning): " << (files.size() - (m_pageMap.size() + m_emptyFiles.size())) << std::endl;
|
|
}
|
|
|
|
if(fileDatas.size() != 0)
|
|
{
|
|
for(auto& f : fileDatas)
|
|
m_output << f.first << std::endl;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
//this is a test method that was supposed to be used for caching
|
|
int PfsPageMapper::load_page_map(const psvpfs::path& filepath, std::map<std::uint32_t, std::string>& pageMap) const
|
|
{
|
|
const auto& fp = filepath;
|
|
|
|
if(!psvpfs::exists(fp))
|
|
{
|
|
m_output << "File " << fp.generic_string() << " does not exist" << std::endl;
|
|
return -1;
|
|
}
|
|
|
|
std::ifstream in(fp.generic_string().c_str());
|
|
if(!in.is_open())
|
|
{
|
|
m_output << "Failed to open " << fp.generic_string() << std::endl;
|
|
return -1;
|
|
}
|
|
|
|
std::string line;
|
|
while(std::getline(in, line))
|
|
{
|
|
int index = line.find(' ');
|
|
std::string pageStr = line.substr(0, index);
|
|
std::string path = line.substr(index + 1);
|
|
std::uint32_t page = std::atoi(pageStr.c_str());
|
|
pageMap.insert(std::make_pair(page, path));
|
|
}
|
|
|
|
in.close();
|
|
|
|
return 0;
|
|
}
|
|
|
|
const std::map<std::uint32_t, sce_junction>& PfsPageMapper::get_pageMap() const
|
|
{
|
|
return m_pageMap;
|
|
}
|
|
|
|
const std::set<sce_junction>& PfsPageMapper::get_emptyFiles() const
|
|
{
|
|
return m_emptyFiles;
|
|
} |