Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 63 additions & 0 deletions systems/ps3.ixx
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
module;
#include <cstring>
#include <filesystem>
#include <format>
#include <map>
#include <ostream>
#include <span>
#include <string_view>
#include <vector>
#include "system.hh"
#include "throw_line.hh"

Expand Down Expand Up @@ -77,9 +79,35 @@ public:

if(!serial.empty())
os << std::format(" serial: {}", serial) << std::endl;

auto firmware_version = pupVersion(data_reader, root_directory->subEntry("PS3_UPDATE/PS3UPDAT.PUP"));
if(!firmware_version.empty())
os << std::format(" firmware version: {}", firmware_version) << std::endl;
}

private:
static constexpr std::string_view _PUP_MAGIC = "SCEUF\0\0\0";
static constexpr uint64_t _VERSION_ID = 0x100;
static constexpr uint32_t _VERSION_LENGTH = 4;

struct PUPHeader
{
uint8_t magic[8];
uint64_t version;
uint64_t build;
uint64_t records_count;
uint64_t header_size;
uint64_t data_size;
};

struct PUPFile
{
uint64_t type;
uint64_t offset;
uint64_t size;
uint64_t padding;
};

struct SFBHeader
{
uint8_t magic[4];
Expand Down Expand Up @@ -152,6 +180,41 @@ private:
return sfb;
}

std::string pupVersion(DataReader *data_reader, std::shared_ptr<iso9660::Entry> pup_file) const
{
if(!pup_file)
return "";

std::vector<uint8_t> sector_buffer(data_reader->sectorSize());

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You unconditionally jump to 0x3E location. PUP internally has a header that defines records count. You need to iterate records and find 0x100 record type that would be version (I once wrote PUP extractor / packer), jump to it's offset and read the resource.

{
private:
	static const u32 m_BUFFER_SIZE = 0x400 * 0x400; // 1Mb
	static const int m_DIGEST_SIZE = 20;
	static const int m_HMAC_KEY_SIZE = 0x40;
	static const u8 m_HMAC_KEY[m_HMAC_KEY_SIZE];
	static const char *m_MAGIC;
	static const string m_CONFIG_FILENAME;
	static const string m_HEX_PREFIX;
	
	struct Header
	{
		u8 magic[8];
		u64 version;
		u64 build;
		u64 records_count;
		u64 header_size;
		u64 data_size;
	};

	struct FileRecord
	{
		struct File
		{
			u64 type;
			u64 offset;
			u64 size;
			u64 padding;
		} file;
		
		struct Signature
		{
			u64 index;
			u8 hash[m_DIGEST_SIZE];
			u32 padding;
		} signature;
	};

	struct GlobalSignature
	{
		u8 hash[m_DIGEST_SIZE];
		u8 padding[12];
	};
	
	struct FileIdNameRecord
	{
		u64 id;
		string name;
	};
	
	struct Context
	{
		Header header;
		vector<FileRecord> records;
		GlobalSignature signature;
		map<u64, string> m_FilenameMap;
	};
	
	static const FileIdNameRecord m_ID_NAME_TABLE[];
	
private:
	streampos GetFileSize(ifstream &);
	string GetFilenameByType(u64, u64);
	bool LoadConfigFromFirmware(Context &ctx, string);
	bool LoadConfig(Context &ctx, string);
	bool SaveConfig(Context &ctx, string);
	void PrintConfig(Context &ctx);
	
public:
	bool ExtractFirmware(string, string);
	bool CreateFirmware(string, string);
};```

```const char *PS3PupProcessor::m_MAGIC = "SCEUF\0\0\0";
const string PS3PupProcessor::m_CONFIG_FILENAME("config.bin");
const string PS3PupProcessor::m_HEX_PREFIX("0x");

const PS3PupProcessor::FileIdNameRecord PS3PupProcessor::m_ID_NAME_TABLE[] =
{
	{0x100, string("version.txt")},
	{0x101, string("dots.txt")},
	{0x102, string("promo.txt")},
	{0x103, string("inc.txt")},
	{0x104, string("version_inc.txt")},
	{0x200, string("updater.self")},
	{0x201, string("vsh.tar")},
	{0x202, string("msg.xml")},
	{0x203, string("updater_inc.self")},
	{0x300, string("update.tar")}
};


streampos PS3PupProcessor::GetFileSize(ifstream &arg_if)
{
	streampos last_pos = arg_if.tellg();
	arg_if.seekg(0, ios::end);
	streampos result = arg_if.tellg();
	arg_if.seekg(last_pos);
	
	return result;
}


string PS3PupProcessor::GetFilenameByType(u64 a_id, u64 a_build)
{
	string result;
	
	u64 id = a_id;
	// WORKAROUND: starting from 2.40, dots.txt and msg.xml were swapped
	if(a_build >= 0x427f)
	{
		if(id == 0x101)
			id = 0x202;
		else if(id == 0x202)
			id = 0x101;
	}
	
	bool found = false;
	for(unsigned int i = 0; i < sizeof(m_ID_NAME_TABLE) / sizeof(m_ID_NAME_TABLE[0]); ++i)
	{
		if(m_ID_NAME_TABLE[i].id == id)
		{
			result = m_ID_NAME_TABLE[i].name;
			found = true;
			break;
		}
	}
	
	if(!found)
	{
		char buffer[0x100];
		sprintf(buffer, "unknown_%08X.bin", (int)a_id);
		result = string(buffer);
	}
	
	return result;
}


bool PS3PupProcessor::LoadConfigFromFirmware(Context &ctx, string a_input_fn)
{
	bool result = false;
	
	ifstream iF;
	iF.open(a_input_fn.c_str(), ios::binary);
	if(!iF.is_open())
	{
		cout << "error: unable to open file " << a_input_fn << endl;
		return result;
	}

	// read header
	iF.read((char *)&ctx.header, sizeof(ctx.header));
	
	// check magic
	if(memcmp(ctx.header.magic, m_MAGIC, sizeof(ctx.header.magic)))
	{
		cout << "error: " << a_input_fn << " isn't firmware PUP-file" << endl;
		return result;
	}
	
	// TRICK: we can't fix ctx.header.records_count, because we need to check hash before
	u64 records_count = ctx.header.records_count;
	BE2SYS(records_count);
	ctx.records.resize(records_count);
	
	// read file records
	for(u64 i = 0; i < records_count; ++i)
		iF.read((char *)&ctx.records[i].file, sizeof(ctx.records[i].file));
	
	// read signatures
	for(u64 i = 0; i < records_count; ++i)
		iF.read((char *)&ctx.records[i].signature, sizeof(ctx.records[i].signature));
	
	// read and check global signature
	iF.read((char *)&ctx.signature, sizeof(ctx.signature));
	u8 digest[m_DIGEST_SIZE];
	Sha1HmacCtx sha1_hmac_ctx;
	sha1_hmac_init(sha1_hmac_ctx, m_HMAC_KEY);
	sha1_hmac_update(sha1_hmac_ctx, (u8 *)&ctx.header, sizeof(ctx.header));
	for(u64 i = 0; i < records_count; ++i)
		sha1_hmac_update(sha1_hmac_ctx, (u8 *)&ctx.records[i].file, sizeof(ctx.records[i].file));
	for(u64 i = 0; i < records_count; ++i)
		sha1_hmac_update(sha1_hmac_ctx, (u8 *)&ctx.records[i].signature, sizeof(ctx.records[i].signature));
	sha1_hmac_final(sha1_hmac_ctx, digest);
	
	bool digest_diff = memcmp(ctx.signature.hash, digest, m_DIGEST_SIZE);
	
	cout << "global signature "
		<< (digest_diff ? "wrong" : "correct") << endl << endl;
	
	// convert endianness to system
	BE2SYS(ctx.header.version);
	BE2SYS(ctx.header.build);
	BE2SYS(ctx.header.records_count);
	BE2SYS(ctx.header.header_size);
	BE2SYS(ctx.header.data_size);
	for(u64 i = 0; i < records_count; ++i)
	{
		BE2SYS(ctx.records[i].file.type);
		BE2SYS(ctx.records[i].file.offset);
		BE2SYS(ctx.records[i].file.size);
	}
	for(u64 i = 0; i < records_count; ++i)
		BE2SYS(ctx.records[i].signature.index);

	iF.close();
	
	result = true;
	return result;
}


bool PS3PupProcessor::LoadConfig(Context &ctx, string a_input_fn)
{
	bool result = false;
	
	ifstream iF(a_input_fn.c_str(), ios::binary);
	if(!iF.is_open())
	{
		cout << "error: unable to open file " << a_input_fn << endl;
		return result;
	}
	
	memcpy((char *)ctx.header.magic, m_MAGIC, sizeof(ctx.header.magic));
	iF.read((char *)&ctx.header.version, sizeof(ctx.header.version));
	iF.read((char *)&ctx.header.build, sizeof(ctx.header.build));
	iF.read((char *)&ctx.header.records_count, sizeof(ctx.header.records_count));
	ctx.records.resize(ctx.header.records_count);
	for(u64 i = 0; i < ctx.header.records_count; ++i)
	{
		iF.read((char *)&ctx.records[i].file.type, sizeof(ctx.records[i].file.type));
		ctx.records[i].signature.index = i;
	}
	
	ctx.header.records_count = ctx.records.size();
	ctx.header.header_size = sizeof(ctx.header) +
		sizeof(FileRecord) * ctx.header.records_count + sizeof(ctx.signature);
	
	result = true;
	return result;
}


bool PS3PupProcessor::SaveConfig(Context &ctx, string a_output_fn)
{
	bool result = false;
	
	ofstream oF(a_output_fn.c_str(), ios::binary);
	if(!oF.is_open())
	{
		cout << "error: unable to create file " << a_output_fn << endl;
		return result;
	}
	
	oF.write((char *)&ctx.header.version, sizeof(ctx.header.version));
	oF.write((char *)&ctx.header.build, sizeof(ctx.header.build));
	oF.write((char *)&ctx.header.records_count, sizeof(ctx.header.records_count));
	for(u64 i = 0; i < ctx.header.records_count; ++i)
		oF.write((char *)&ctx.records[i].file.type, sizeof(ctx.records[i].file.type));
	
	result = true;
	return result;
}


void PS3PupProcessor::PrintConfig(Context &ctx)
{
	cout << setfill('0');

	// output file info
	cout << "GENERAL: " << endl;
	cout << "version: " << m_HEX_PREFIX << hex << setw(2)
		<< ctx.header.version << endl;
	cout << "build: " << m_HEX_PREFIX << hex << ctx.header.build << endl;
	cout << "signature: " << m_HEX_PREFIX;
		for(int j = 0; j < m_DIGEST_SIZE; ++j)
			cout << hex << setw(2) << (int)ctx.signature.hash[j];
	cout << endl;
	cout << "records count: " << dec << ctx.header.records_count << endl << endl;

	// records
	for(u64 i = 0; i < ctx.header.records_count; ++i)
	{
		cout << dec << "RECORD " << i + 1 << ": " << endl;
		cout << "type: " << m_HEX_PREFIX << hex << ctx.records[i].file.type
			<< " [" << GetFilenameByType(ctx.records[i].file.type, ctx.header.build) << "]" << endl;
		cout << "size: " << dec << ctx.records[i].file.size << endl;

		cout << "signature: " << m_HEX_PREFIX;
		for(int j = 0; j < m_DIGEST_SIZE; ++j)
			cout << hex << setw(2) << (int)ctx.records[i].signature.hash[j];
		cout << endl;
		cout << endl;
	}

	cout << endl;
}


bool PS3PupProcessor::ExtractFirmware(string a_input_fn, string a_output_path)
{
	bool result = false;
	Context ctx;

	if(!LoadConfigFromFirmware(ctx, a_input_fn))
		return result;
	
	ifstream iF;
	iF.open(a_input_fn.c_str(), ios::binary);
	if(!iF.is_open())
	{
		cout << "error: unable to open file " << a_input_fn << endl;
		return result;
	}

	// data extraction
	char *buffer = new char[m_BUFFER_SIZE];
	for(u64 i = 0; i < ctx.header.records_count; ++i)
	{
		cout << setfill('0');

		// show info about file
		string output_filename = GetFilenameByType(ctx.records[i].file.type, ctx.header.build);
		cout << "extracting file " << output_filename << endl;
		cout << "type: " << m_HEX_PREFIX << hex
			<< ctx.records[i].file.type << endl;
		cout << "size: " << dec << ctx.records[i].file.size << endl;

		streampos size = (streampos)ctx.records[i].file.size;
		streampos left_bytes = size;
		iF.seekg(ctx.records[i].file.offset);

		filesystem::create_directory(a_output_path);
		filesystem::path output_path(filesystem::path(a_output_path)
			/ filesystem::path(output_filename));
		
		ofstream oF;
		oF.open(output_path.string().c_str(), ios::binary);
		if(!oF.is_open())
		{
			cout << "error: unable to create file " << a_input_fn << endl;
			delete [] buffer;
			return result;
		}
		
		u8 digest[m_DIGEST_SIZE];
		Sha1HmacCtx sha1_hmac_ctx;
		sha1_hmac_init(sha1_hmac_ctx, m_HMAC_KEY);
		
		cout << setfill(' ');
		while(left_bytes)
		{
			int to_read = left_bytes > m_BUFFER_SIZE ?
				m_BUFFER_SIZE : (int)left_bytes;

			left_bytes -= to_read;
			iF.read(buffer, to_read);
			oF.write(buffer, to_read);
			
			sha1_hmac_update(sha1_hmac_ctx, (u8 *)buffer, to_read);
			
			int percentage = (1 - left_bytes / (float)size) * 100;
			cout << '\r';
			cout << "progress: ";
			cout.setf(ios::right, ios::basefield);
			cout.width(3);
			cout << percentage << '%' << flush;
		}
		
		sha1_hmac_final(sha1_hmac_ctx, digest);
		
		bool digest_diff = memcmp(ctx.records[i].signature.hash, digest, m_DIGEST_SIZE);
		
		cout << endl << "done, signature "
			<< (digest_diff ? "wrong" : "correct") << endl << endl;
		
		oF.close();
	}
	
	delete [] buffer;
	
	iF.close();
	SaveConfig(ctx, (filesystem::path(a_output_path) / filesystem::path(m_CONFIG_FILENAME)).string());
	
	result = true;
	return result;
}


bool PS3PupProcessor::CreateFirmware(string a_output_fn, string a_input_path)
{
	bool result = false;
	Context ctx;
	
	if(!LoadConfig(ctx, (filesystem::path(a_input_path) / filesystem::path(m_CONFIG_FILENAME)).string()))
		return result;
	
	ofstream oF;
	oF.open(a_output_fn.c_str(), ios::binary);
	if(!oF.is_open())
	{
		cout << "error: unable to create file " << a_output_fn << endl;
		return result;
	}

	ctx.header.data_size = 0;	
	oF.seekp(ctx.header.header_size);
	
	char *buffer = new char[m_BUFFER_SIZE];
	for(u64 i = 0; i < ctx.header.records_count; ++i)
	{
		cout << setfill('0');
		
		string input_filename = GetFilenameByType(ctx.records[i].file.type, ctx.header.build);
		
		filesystem::path input_path(filesystem::path(a_input_path)
			/ filesystem::path(input_filename));
		
		ifstream iF;
		iF.open(input_path.string().c_str(), ios::binary);
		if(!iF.is_open())
		{
			cout << "error: unable to open file " << input_filename << endl;
			delete [] buffer;
			return result;
		}
		
		ctx.records[i].file.offset = oF.tellp();
		ctx.records[i].file.size = GetFileSize(iF);
		ctx.header.data_size += ctx.records[i].file.size;

		cout << "writing file "<< input_filename << endl;
		cout << "type: " << m_HEX_PREFIX << hex
			<< ctx.records[i].file.type << endl;
		cout << "size: " << dec << ctx.records[i].file.size << endl;
		
		streampos size = (streampos)ctx.records[i].file.size;
		streampos left_bytes = size;
		
		Sha1HmacCtx sha1_hmac_ctx;
		sha1_hmac_init(sha1_hmac_ctx, m_HMAC_KEY);
		
		cout << setfill(' ');
		while(left_bytes)
		{
			int to_write = left_bytes > m_BUFFER_SIZE ?
				m_BUFFER_SIZE : (int)left_bytes;
			
			left_bytes -= to_write;
			iF.read(buffer, to_write);
			oF.write(buffer, to_write);

			sha1_hmac_update(sha1_hmac_ctx, (u8 *)buffer, to_write);
			
			int percentage = (1 - left_bytes / (float)size) * 100;
			cout << '\r';
			cout << "progress: ";
			cout.setf(ios::right, ios::basefield);
			cout.width(3);
			cout << percentage << '%' << flush;
		}
		
		sha1_hmac_final(sha1_hmac_ctx, ctx.records[i].signature.hash);
		
		cout << endl << "done" << endl << endl;
		
		iF.close();
	}
	
	delete [] buffer;
	
	// TRICK: preserve records_count, as it would be byteswapped to big endian
	u64 records_count = ctx.header.records_count;
	
	// clear paddings
	memset((void *)ctx.signature.padding, 0, sizeof(ctx.signature.padding));
	for(u64 i = 0; i < ctx.header.records_count; ++i)
		ctx.records[i].file.padding = 0;
	
	// convert header data back to big endian
	SYS2BE(ctx.header.version);
	SYS2BE(ctx.header.build);
	SYS2BE(ctx.header.records_count);
	SYS2BE(ctx.header.header_size);
	SYS2BE(ctx.header.data_size);
	for(u64 i = 0; i < records_count; ++i)
	{
		SYS2BE(ctx.records[i].file.type);
		SYS2BE(ctx.records[i].file.offset);
		SYS2BE(ctx.records[i].file.size);
	}
	for(u64 i = 0; i < records_count; ++i)
		SYS2BE(ctx.records[i].signature.index);
	
	// calculate global hash
	Sha1HmacCtx sha1_hmac_ctx;
	sha1_hmac_init(sha1_hmac_ctx, m_HMAC_KEY);
	sha1_hmac_update(sha1_hmac_ctx, (u8 *)&ctx.header, sizeof(ctx.header));
	for(u64 i = 0; i < records_count; ++i)
		sha1_hmac_update(sha1_hmac_ctx, (u8 *)&ctx.records[i].file, sizeof(ctx.records[i].file));
	for(u64 i = 0; i < records_count; ++i)
		sha1_hmac_update(sha1_hmac_ctx, (u8 *)&ctx.records[i].signature, sizeof(ctx.records[i].signature));
	sha1_hmac_final(sha1_hmac_ctx, ctx.signature.hash);
	
	// write header
	oF.seekp(0, ios::beg);
	oF.write((char *)&ctx.header, sizeof(ctx.header));
	for(u64 i = 0; i < records_count; ++i)
		oF.write((char *)&ctx.records[i].file, sizeof(ctx.records[i].file));
	for(u64 i = 0; i < records_count; ++i)
		oF.write((char *)&ctx.records[i].signature, sizeof(ctx.records[i].signature));
	oF.write((char *)&ctx.signature, sizeof(ctx.signature));
	
	oF.close();
	
	result = true;
	return result;
}```

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done. tried to implement it correctly but with as little complexity as possible

if(data_reader->read(sector_buffer.data(), pup_file->sectorsLBA(), 1) != 1)
return "";

auto header = (PUPHeader *)sector_buffer.data();

if(memcmp(header->magic, _PUP_MAGIC.data(), _PUP_MAGIC.size()))
return "";

uint32_t cur = sizeof(PUPHeader);
uint64_t records_count = endian_swap(header->records_count);
for(uint64_t i = 0; i < records_count; ++i)
{
if(cur + sizeof(PUPFile) > sector_buffer.size())
return "";

auto file = (PUPFile *)(sector_buffer.data() + cur);
cur += sizeof(PUPFile);
if(endian_swap(file->type) == 0x100)
{
uint64_t version_offset = endian_swap(file->offset);
if(version_offset > sector_buffer.size() - _VERSION_LENGTH)
return "";
return std::string((char *)(sector_buffer.data() + version_offset), _VERSION_LENGTH);
}
}

return "";
}

protected:
std::map<std::string, std::string> parseSFO(std::span<uint8_t> sfo_raw) const
{
Expand Down
Loading