VolumeVerifier: Check hashes in Wii partitions

This commit is contained in:
JosJuice 2019-03-26 17:22:18 +01:00
parent 84cbd5150f
commit 4fd2d8e8c4
7 changed files with 184 additions and 116 deletions

View File

@ -99,7 +99,11 @@ public:
}
virtual Platform GetVolumeType() const = 0;
virtual bool SupportsIntegrityCheck() const { return false; }
virtual bool CheckIntegrity(const Partition& partition) const { return false; }
virtual bool CheckH3TableIntegrity(const Partition& partition) const { return false; }
virtual bool CheckBlockIntegrity(u64 block_index, const Partition& partition) const
{
return false;
}
virtual Region GetRegion() const = 0;
virtual Country GetCountry(const Partition& partition = PARTITION_NONE) const = 0;
virtual BlobType GetBlobType() const = 0;

View File

@ -14,6 +14,7 @@
#include "Common/Align.h"
#include "Common/Assert.h"
#include "Common/CommonTypes.h"
#include "Common/Logging/Log.h"
#include "Common/MsgHandler.h"
#include "Common/StringUtil.h"
#include "Common/Swap.h"
@ -62,6 +63,9 @@ void VolumeVerifier::Start()
CheckCorrectlySigned(PARTITION_NONE, GetStringT("This title is not correctly signed."));
CheckDiscSize();
CheckMisc();
std::sort(m_blocks.begin(), m_blocks.end(),
[](const BlockToVerify& b1, const BlockToVerify& b2) { return b1.offset < b2.offset; });
}
void VolumeVerifier::CheckPartitions()
@ -164,18 +168,7 @@ bool VolumeVerifier::CheckPartition(const Partition& partition)
else if (*type == PARTITION_UPDATE)
severity = Severity::Low;
std::string name = NameForPartitionType(*type, false);
if (ShouldHaveMasterpiecePartitions() && *type > 0xFF)
{
// i18n: This string is referring to a game mode in Super Smash Bros. Brawl called Masterpieces
// where you play demos of NES/SNES/N64 games. This string is referring to a specific such demo
// rather than the game mode as a whole, so please use the singular form. Official translations:
// 名作トライアル (Japanese), Masterpieces (English), Meisterstücke (German), Chefs-d'œuvre
// (French), Clásicos (Spanish), Capolavori (Italian), 클래식 게임 체험판 (Korean).
// If your language is not one of the languages above, consider leaving the string untranslated
// so that people will recognize it as the name of the game mode.
name = StringFromFormat(GetStringT("%s (Masterpiece)").c_str(), name.c_str());
}
const std::string name = GetPartitionName(type);
if (partition.offset % VolumeWii::BLOCK_TOTAL_SIZE != 0 ||
m_volume.PartitionOffsetToRawOffset(0, partition) % VolumeWii::BLOCK_TOTAL_SIZE != 0)
@ -189,6 +182,13 @@ bool VolumeVerifier::CheckPartition(const Partition& partition)
partition, StringFromFormat(GetStringT("The %s partition is not correctly signed.").c_str(),
name.c_str()));
if (m_volume.SupportsIntegrityCheck() && !m_volume.CheckH3TableIntegrity(partition))
{
const std::string text = StringFromFormat(
GetStringT("The H3 hash table for the %s partition is not correct.").c_str(), name.c_str());
AddProblem(Severity::Low, text);
}
bool invalid_disc_header = false;
std::vector<u8> disc_header(0x80);
constexpr u32 WII_MAGIC = 0x5D1C9EA3;
@ -262,9 +262,43 @@ bool VolumeVerifier::CheckPartition(const Partition& partition)
}
}
// Prepare for hash verification in the Process step
if (m_volume.SupportsIntegrityCheck())
{
u64 offset = m_volume.PartitionOffsetToRawOffset(0, partition);
const std::optional<u64> size =
m_volume.ReadSwappedAndShifted(partition.offset + 0x2bc, PARTITION_NONE);
const u64 end_offset = offset + size.value_or(0);
for (size_t i = 0; offset < end_offset; ++i, offset += VolumeWii::BLOCK_TOTAL_SIZE)
m_blocks.emplace_back(BlockToVerify{partition, offset, i});
m_block_errors.emplace(partition, 0);
}
return true;
}
std::string VolumeVerifier::GetPartitionName(std::optional<u32> type) const
{
if (!type)
return "???";
std::string name = NameForPartitionType(*type, false);
if (ShouldHaveMasterpiecePartitions() && *type > 0xFF)
{
// i18n: This string is referring to a game mode in Super Smash Bros. Brawl called Masterpieces
// where you play demos of NES/SNES/N64 games. This string is referring to a specific such demo
// rather than the game mode as a whole, so please use the singular form. Official translations:
// 名作トライアル (Japanese), Masterpieces (English), Meisterstücke (German), Chefs-d'œuvre
// (French), Clásicos (Spanish), Capolavori (Italian), 클래식 게임 체험판 (Korean).
// If your language is not one of the languages above, consider leaving the string untranslated
// so that people will recognize it as the name of the game mode.
name = StringFromFormat(GetStringT("%s (Masterpiece)").c_str(), name.c_str());
}
return name;
}
void VolumeVerifier::CheckCorrectlySigned(const Partition& partition, const std::string& error_text)
{
IOS::HLE::Kernel ios;
@ -596,7 +630,30 @@ void VolumeVerifier::Process()
if (m_progress == m_max_progress)
return;
m_progress += std::min(m_max_progress - m_progress, BLOCK_SIZE);
u64 bytes_to_read = BLOCK_SIZE;
if (m_block_index < m_blocks.size() && m_blocks[m_block_index].offset == m_progress)
{
bytes_to_read = VolumeWii::BLOCK_TOTAL_SIZE;
}
else if (m_block_index + 1 < m_blocks.size() && m_blocks[m_block_index + 1].offset > m_progress)
{
bytes_to_read = std::min(bytes_to_read, m_blocks[m_block_index + 1].offset - m_progress);
}
bytes_to_read = std::min(bytes_to_read, m_max_progress - m_progress);
m_progress += bytes_to_read;
while (m_block_index < m_blocks.size() && m_blocks[m_block_index].offset < m_progress)
{
if (!m_volume.CheckBlockIntegrity(m_blocks[m_block_index].block_index,
m_blocks[m_block_index].partition))
{
WARN_LOG(DISCIO, "Integrity check failed for block at 0x%" PRIx64,
m_blocks[m_block_index].offset);
m_block_errors[m_blocks[m_block_index].partition]++;
}
m_block_index++;
}
}
u64 VolumeVerifier::GetBytesProcessed() const
@ -615,6 +672,18 @@ void VolumeVerifier::Finish()
return;
m_done = true;
for (auto pair : m_block_errors)
{
if (pair.second > 0)
{
const std::string name = GetPartitionName(m_volume.GetPartitionType(pair.first));
AddProblem(Severity::Medium,
StringFromFormat(
GetStringT("Errors were found in %zu blocks in the %s partition.").c_str(),
pair.second, name.c_str()));
}
}
// Show the most serious problems at the top
std::stable_sort(m_result.problems.begin(), m_result.problems.end(),
[](const Problem& p1, const Problem& p2) { return p1.severity > p2.severity; });

View File

@ -4,6 +4,8 @@
#pragma once
#include <map>
#include <optional>
#include <string>
#include <vector>
@ -64,8 +66,16 @@ public:
const Result& GetResult() const;
private:
struct BlockToVerify
{
Partition partition;
u64 offset;
u64 block_index;
};
void CheckPartitions();
bool CheckPartition(const Partition& partition); // Returns false if partition should be ignored
std::string GetPartitionName(std::optional<u32> type) const;
void CheckCorrectlySigned(const Partition& partition, const std::string& error_text);
bool IsDebugSigned() const;
bool ShouldHaveChannelPartition() const;
@ -85,6 +95,10 @@ private:
bool m_is_datel;
bool m_is_not_retail;
std::vector<BlockToVerify> m_blocks;
size_t m_block_index = 0; // Index in m_blocks, not index in a specific partition
std::map<Partition, size_t> m_block_errors;
bool m_started;
bool m_done;
u64 m_progress;

View File

@ -109,6 +109,19 @@ VolumeWii::VolumeWii(std::unique_ptr<BlobReader> reader)
return cert_chain;
};
auto get_h3_table = [this, partition]() -> std::vector<u8> {
if (!m_encrypted)
return {};
const std::optional<u64> h3_table_offset =
ReadSwappedAndShifted(partition.offset + 0x2b4, PARTITION_NONE);
if (!h3_table_offset)
return {};
std::vector<u8> h3_table(H3_TABLE_SIZE);
if (!m_reader->Read(partition.offset + *h3_table_offset, H3_TABLE_SIZE, h3_table.data()))
return {};
return h3_table;
};
auto get_key = [this, partition]() -> std::unique_ptr<mbedtls_aes_context> {
const IOS::ES::TicketReader& ticket = *m_partitions[partition].ticket;
if (!ticket.IsValid())
@ -133,6 +146,7 @@ VolumeWii::VolumeWii(std::unique_ptr<BlobReader> reader)
Common::Lazy<IOS::ES::TicketReader>(get_ticket),
Common::Lazy<IOS::ES::TMDReader>(get_tmd),
Common::Lazy<std::vector<u8>>(get_cert_chain),
Common::Lazy<std::vector<u8>>(get_h3_table),
Common::Lazy<std::unique_ptr<FileSystem>>(get_file_system),
Common::Lazy<u64>(get_data_offset), *partition_type});
}
@ -409,82 +423,84 @@ u64 VolumeWii::GetRawSize() const
return m_reader->GetRawSize();
}
bool VolumeWii::CheckIntegrity(const Partition& partition) const
bool VolumeWii::CheckH3TableIntegrity(const Partition& partition) const
{
if (!m_encrypted)
return false;
// Get the decryption key for the partition
auto it = m_partitions.find(partition);
if (it == m_partitions.end())
return false;
const PartitionDetails& partition_details = it->second;
const std::vector<u8>& h3_table = *partition_details.h3_table;
if (h3_table.size() != H3_TABLE_SIZE)
return false;
const IOS::ES::TMDReader& tmd = *partition_details.tmd;
if (!tmd.IsValid())
return false;
const std::vector<IOS::ES::Content> contents = tmd.GetContents();
if (contents.size() != 1)
return false;
std::array<u8, 20> h3_table_sha1;
mbedtls_sha1(h3_table.data(), h3_table.size(), h3_table_sha1.data());
return h3_table_sha1 == contents[0].sha1;
}
bool VolumeWii::CheckBlockIntegrity(u64 block_index, const Partition& partition) const
{
auto it = m_partitions.find(partition);
if (it == m_partitions.end())
return false;
const PartitionDetails& partition_details = it->second;
constexpr size_t SHA1_SIZE = 20;
if (block_index / 64 * SHA1_SIZE >= partition_details.h3_table->size())
return false;
mbedtls_aes_context* aes_context = partition_details.key->get();
if (!aes_context)
return false;
// Get partition data size
const auto part_data_size = ReadSwappedAndShifted(partition.offset + 0x2BC, PARTITION_NONE);
if (!part_data_size)
const u64 cluster_offset =
partition.offset + *partition_details.data_offset + block_index * BLOCK_TOTAL_SIZE;
// Read and decrypt the cluster metadata
u8 cluster_metadata_crypted[BLOCK_HEADER_SIZE];
u8 cluster_metadata[BLOCK_HEADER_SIZE];
u8 iv[16] = {0};
if (!m_reader->Read(cluster_offset, BLOCK_HEADER_SIZE, cluster_metadata_crypted))
return false;
mbedtls_aes_crypt_cbc(aes_context, MBEDTLS_AES_DECRYPT, BLOCK_HEADER_SIZE, iv,
cluster_metadata_crypted, cluster_metadata);
u8 cluster_data[BLOCK_DATA_SIZE];
if (!Read(block_index * BLOCK_DATA_SIZE, BLOCK_DATA_SIZE, cluster_data, partition))
return false;
const u32 num_clusters = static_cast<u32>(part_data_size.value() / 0x8000);
for (u32 cluster_id = 0; cluster_id < num_clusters; ++cluster_id)
for (u32 hash_index = 0; hash_index < 31; ++hash_index)
{
const u64 cluster_offset =
partition.offset + *partition_details.data_offset + static_cast<u64>(cluster_id) * 0x8000;
// Read and decrypt the cluster metadata
u8 cluster_metadata_crypted[0x400];
u8 cluster_metadata[0x400];
u8 iv[16] = {0};
if (!m_reader->Read(cluster_offset, sizeof(cluster_metadata_crypted), cluster_metadata_crypted))
{
WARN_LOG(DISCIO, "Integrity Check: fail at cluster %d: could not read metadata", cluster_id);
u8 h0_hash[SHA1_SIZE];
mbedtls_sha1(cluster_data + hash_index * 0x400, 0x400, h0_hash);
if (memcmp(h0_hash, cluster_metadata + hash_index * SHA1_SIZE, SHA1_SIZE))
return false;
}
mbedtls_aes_crypt_cbc(aes_context, MBEDTLS_AES_DECRYPT, sizeof(cluster_metadata), iv,
cluster_metadata_crypted, cluster_metadata);
// Some clusters have invalid data and metadata because they aren't
// meant to be read by the game (for example, holes between files). To
// try to avoid reporting errors because of these clusters, we check
// the 0x00 paddings in the metadata.
//
// This may cause some false negatives though: some bad clusters may be
// skipped because they are *too* bad and are not even recognized as
// valid clusters. To be improved.
const u8* pad_begin = cluster_metadata + 0x26C;
const u8* pad_end = pad_begin + 0x14;
const bool meaningless = std::any_of(pad_begin, pad_end, [](u8 val) { return val != 0; });
if (meaningless)
continue;
u8 cluster_data[0x7C00];
if (!Read(cluster_id * sizeof(cluster_data), sizeof(cluster_data), cluster_data, partition))
{
WARN_LOG(DISCIO, "Integrity Check: fail at cluster %d: could not read data", cluster_id);
return false;
}
for (u32 hash_id = 0; hash_id < 31; ++hash_id)
{
u8 hash[20];
mbedtls_sha1(cluster_data + hash_id * sizeof(cluster_metadata), sizeof(cluster_metadata),
hash);
// Note that we do not use strncmp here
if (memcmp(hash, cluster_metadata + hash_id * sizeof(hash), sizeof(hash)))
{
WARN_LOG(DISCIO, "Integrity Check: fail at cluster %d: hash %d is invalid", cluster_id,
hash_id);
return false;
}
}
}
u8 h1_hash[SHA1_SIZE];
mbedtls_sha1(cluster_metadata, SHA1_SIZE * 31, h1_hash);
if (memcmp(h1_hash, cluster_metadata + 0x280 + (block_index % 8) * SHA1_SIZE, SHA1_SIZE))
return false;
u8 h2_hash[SHA1_SIZE];
mbedtls_sha1(cluster_metadata + 0x280, SHA1_SIZE * 8, h2_hash);
if (memcmp(h2_hash, cluster_metadata + 0x340 + (block_index / 8 % 8) * SHA1_SIZE, SHA1_SIZE))
return false;
u8 h3_hash[SHA1_SIZE];
mbedtls_sha1(cluster_metadata + 0x340, SHA1_SIZE * 8, h3_hash);
if (memcmp(h3_hash, partition_details.h3_table->data() + block_index / 64 * SHA1_SIZE, SHA1_SIZE))
return false;
return true;
}

View File

@ -56,8 +56,9 @@ public:
std::optional<u8> GetDiscNumber(const Partition& partition = PARTITION_NONE) const override;
Platform GetVolumeType() const override;
bool SupportsIntegrityCheck() const override { return true; }
bool CheckIntegrity(const Partition& partition) const override;
bool SupportsIntegrityCheck() const override { return m_encrypted; }
bool CheckH3TableIntegrity(const Partition& partition) const override;
bool CheckBlockIntegrity(u64 block_index, const Partition& partition) const override;
Region GetRegion() const override;
Country GetCountry(const Partition& partition = PARTITION_NONE) const override;
@ -66,6 +67,8 @@ public:
bool IsSizeAccurate() const override;
u64 GetRawSize() const override;
static constexpr unsigned int H3_TABLE_SIZE = 0x18000;
static constexpr unsigned int BLOCK_HEADER_SIZE = 0x0400;
static constexpr unsigned int BLOCK_DATA_SIZE = 0x7C00;
static constexpr unsigned int BLOCK_TOTAL_SIZE = BLOCK_HEADER_SIZE + BLOCK_DATA_SIZE;
@ -80,6 +83,7 @@ private:
Common::Lazy<IOS::ES::TicketReader> ticket;
Common::Lazy<IOS::ES::TMDReader> tmd;
Common::Lazy<std::vector<u8>> cert_chain;
Common::Lazy<std::vector<u8>> h3_table;
Common::Lazy<std::unique_ptr<FileSystem>> file_system;
Common::Lazy<u64> data_offset;
u32 type;

View File

@ -238,12 +238,6 @@ void FilesystemWidget::ShowContextMenu(const QPoint&)
if (!folder.isEmpty())
ExtractPartition(partition, folder);
});
if (m_volume->IsEncryptedAndHashed())
{
menu->addSeparator();
menu->addAction(tr("Check Partition Integrity"), this,
[this, partition] { CheckIntegrity(partition); });
}
break;
case EntryType::File:
menu->addAction(tr("Extract File..."), this, [this, partition, path] {
@ -327,35 +321,3 @@ void FilesystemWidget::ExtractFile(const DiscIO::Partition& partition, const QSt
else
ModalMessageBox::critical(this, tr("Error"), tr("Failed to extract file."));
}
void FilesystemWidget::CheckIntegrity(const DiscIO::Partition& partition)
{
QProgressDialog* dialog = new QProgressDialog(this);
std::future<bool> is_valid = std::async(
std::launch::async, [this, partition] { return m_volume->CheckIntegrity(partition); });
dialog->setLabelText(tr("Verifying integrity of partition..."));
dialog->setWindowFlags(dialog->windowFlags() & ~Qt::WindowContextHelpButtonHint);
dialog->setWindowTitle(tr("Verifying partition"));
dialog->setMinimum(0);
dialog->setMaximum(0);
dialog->show();
while (is_valid.wait_for(std::chrono::milliseconds(50)) != std::future_status::ready)
QCoreApplication::processEvents();
dialog->close();
if (is_valid.get())
{
ModalMessageBox::information(this, tr("Success"),
tr("Integrity check completed. No errors have been found."));
}
else
{
ModalMessageBox::critical(this, tr("Error"),
tr("Integrity check for partition failed. The disc image is most "
"likely corrupted or has been patched incorrectly."));
}
}

View File

@ -43,7 +43,6 @@ private:
const QString& out);
void ExtractFile(const DiscIO::Partition& partition, const QString& path, const QString& out);
bool ExtractSystemData(const DiscIO::Partition& partition, const QString& out);
void CheckIntegrity(const DiscIO::Partition& partition);
DiscIO::Partition GetPartitionFromID(int id);