add salvage-gci action

This commit is contained in:
Martin Michelsen
2023-05-07 21:33:12 -07:00
parent 8dc5e9f281
commit 90a3be7803
3 changed files with 170 additions and 16 deletions
+122 -1
View File
@@ -124,6 +124,10 @@ The actions are:\n\
checksum is also recomputed and stored in the encrypted file. CRYPT-OPTION\n\
is required; it can be either --sys=SYSTEM-FILENAME or --seed=ROUND1-SEED\n\
(specified in hex).\n\
salvage-gci INPUT-FILENAME [--round2] [CRYPT-OPTION] [--bytes=SIZE]\n\
Attempt to find either the round-1 or round-2 decryption seed for a\n\
corrupted GCI file. If --round2 is given, then CRYPT-OPTION must be given\n\
(and should specify either a valid system file or the round1 seed).\n\
find-decryption-seed <OPTIONS...>\n\
Perform a brute-force search for a decryption seed of the given data. The\n\
ciphertext is specified with the --encrypted=DATA option and the expected\n\
@@ -205,6 +209,7 @@ enum class Behavior {
ENCRYPT_GCI_SAVE,
DECRYPT_GCI_SAVE,
FIND_DECRYPTION_SEED,
SALVAGE_GCI,
DECODE_QUEST_FILE,
DECODE_SJIS,
EXTRACT_GSL,
@@ -232,6 +237,7 @@ static bool behavior_takes_input_filename(Behavior b) {
(b == Behavior::DECRYPT_DATA) ||
(b == Behavior::DECRYPT_TRIVIAL_DATA) ||
(b == Behavior::DECRYPT_GCI_SAVE) ||
(b == Behavior::SALVAGE_GCI) ||
(b == Behavior::ENCRYPT_GCI_SAVE) ||
(b == Behavior::DECODE_QUEST_FILE) ||
(b == Behavior::DECODE_SJIS) ||
@@ -279,7 +285,13 @@ int main(int argc, char** argv) {
bool big_endian = false;
bool skip_little_endian = false;
bool skip_big_endian = false;
bool round2 = false;
bool skip_checksum = false;
uint64_t override_round2_seed = 0xFFFFFFFFFFFFFFFF;
size_t offset = 0;
size_t stride = 1;
size_t num_threads = 0;
size_t bytes = 0;
const char* find_decryption_seed_ciphertext = nullptr;
vector<const char*> find_decryption_seed_plaintexts;
const char* input_filename = nullptr;
@@ -309,8 +321,20 @@ int main(int argc, char** argv) {
cli_version = GameVersion::XB;
} else if (!strcmp(argv[x], "--bb")) {
cli_version = GameVersion::BB;
} else if (!strcmp(argv[x], "--round2")) {
round2 = true;
} else if (!strncmp(argv[x], "--bytes=", 8)) {
bytes = strtoull(&argv[x][8], nullptr, 0);
} else if (!strncmp(argv[x], "--offset=", 9)) {
offset = strtoull(&argv[x][9], nullptr, 0);
} else if (!strncmp(argv[x], "--stride=", 9)) {
stride = strtoull(&argv[x][9], nullptr, 0);
} else if (!strcmp(argv[x], "--skip-checksum")) {
skip_checksum = true;
} else if (!strncmp(argv[x], "--seed=", 7)) {
seed = &argv[x][7];
} else if (!strncmp(argv[x], "--round2-seed=", 14)) {
override_round2_seed = strtoull(&argv[x][14], nullptr, 16);
} else if (!strncmp(argv[x], "--key=", 6)) {
key_file_name = &argv[x][6];
} else if (!strncmp(argv[x], "--sys=", 6)) {
@@ -370,6 +394,8 @@ int main(int argc, char** argv) {
behavior = Behavior::ENCRYPT_GCI_SAVE;
} else if (!strcmp(argv[x], "find-decryption-seed")) {
behavior = Behavior::FIND_DECRYPTION_SEED;
} else if (!strcmp(argv[x], "salvage-gci")) {
behavior = Behavior::SALVAGE_GCI;
} else if (!strcmp(argv[x], "decode-sjis")) {
behavior = Behavior::DECODE_SJIS;
} else if (!strcmp(argv[x], "decode-gci")) {
@@ -661,7 +687,7 @@ int main(int argc, char** argv) {
if (is_decrypt) {
const void* data_section = r.getv(header.data_size);
auto decrypted = decrypt_gci_fixed_size_file_data_section<StructT>(
data_section, header.data_size, round1_seed);
data_section, header.data_size, round1_seed, skip_checksum, override_round2_seed);
*reinterpret_cast<StructT*>(data.data() + data_start_offset) = decrypted;
} else {
const auto& s = r.get<StructT>();
@@ -700,6 +726,101 @@ int main(int argc, char** argv) {
break;
}
case Behavior::SALVAGE_GCI: {
uint64_t likely_round1_seed = 0xFFFFFFFFFFFFFFFF;
if (system_filename) {
try {
string system_data = load_file(system_filename);
StringReader r(system_data);
const auto& header = r.get<PSOGCIFileHeader>();
header.check();
const auto& system = r.get<PSOGCSystemFile>();
likely_round1_seed = system.creation_timestamp;
log_info("System file appears to be in order; round1 seed is %08" PRIX64, likely_round1_seed);
} catch (const exception& e) {
log_warning("Cannot parse system file (%s); ignoring it", e.what());
}
} else if (!seed.empty()) {
likely_round1_seed = stoul(seed, nullptr, 16);
log_info("Specified round1 seed is %08" PRIX64, likely_round1_seed);
}
if (round2 && likely_round1_seed > 0x100000000) {
throw invalid_argument("cannot find round2 seed without known round1 seed");
}
auto data = read_input_data();
StringReader r(data);
const auto& header = r.get<PSOGCIFileHeader>();
header.check();
const void* data_section = r.getv(header.data_size);
auto process_file = [&]<typename StructT>() {
vector<multimap<size_t, uint32_t>> top_seeds_by_thread(
num_threads ? num_threads : thread::hardware_concurrency());
parallel_range<uint64_t>(
[&](uint64_t seed, size_t thread_num) -> bool {
size_t zero_count;
if (round2) {
string decrypted = decrypt_gci_fixed_size_file_data_section_for_salvage(
data_section, header.data_size, likely_round1_seed, seed, bytes);
zero_count = count_zeroes(
decrypted.data() + offset,
decrypted.size() - offset,
stride);
} else {
auto decrypted = decrypt_gci_fixed_size_file_data_section<StructT>(
data_section,
header.data_size,
seed,
true);
zero_count = count_zeroes(
reinterpret_cast<const uint8_t*>(&decrypted) + offset,
sizeof(decrypted) - offset,
stride);
}
auto& top_seeds = top_seeds_by_thread[thread_num];
if (top_seeds.size() < 10 || (zero_count >= top_seeds.begin()->second)) {
top_seeds.emplace(zero_count, seed);
if (top_seeds.size() > 10) {
top_seeds.erase(top_seeds.begin());
}
}
return false;
},
0,
0x100000000,
num_threads);
multimap<size_t, uint32_t> top_seeds;
for (const auto& thread_top_seeds : top_seeds_by_thread) {
for (const auto& it : thread_top_seeds) {
top_seeds.emplace(it.first, it.second);
}
}
for (const auto& it : top_seeds) {
const char* sys_seed_str = (!round2 && (it.second == likely_round1_seed))
? " (this is the seed from the system file)"
: "";
log_info("Round %c seed %08" PRIX32 " resulted in %zu zero bytes%s",
round2 ? '2' : '1', it.second, it.first, sys_seed_str);
}
};
if (header.data_size == sizeof(PSOGCGuildCardFile)) {
process_file.template operator()<PSOGCGuildCardFile>();
} else if (header.is_ep12() && (header.data_size == sizeof(PSOGCCharacterFile))) {
process_file.template operator()<PSOGCCharacterFile>();
} else if (header.is_ep3() && (header.data_size == sizeof(PSOGCEp3CharacterFile))) {
process_file.template operator()<PSOGCEp3CharacterFile>();
} else {
throw runtime_error("unrecognized save type");
}
break;
}
case Behavior::FIND_DECRYPTION_SEED: {
if (find_decryption_seed_plaintexts.empty() || !find_decryption_seed_ciphertext) {
throw runtime_error("both --encrypted and --decrypted must be specified");
+16 -2
View File
@@ -70,8 +70,7 @@ void PSOGCIFileHeader::check() const {
if (this->game_id[1] != 'P') {
throw runtime_error("GCI file is not for Phantasy Star Online");
}
if ((this->game_id[1] != 'P') ||
((this->game_id[2] != 'S') && (this->game_id[2] != 'O'))) {
if ((this->game_id[2] != 'S') && (this->game_id[2] != 'O')) {
throw runtime_error("GCI file is not for Phantasy Star Online");
}
}
@@ -101,3 +100,18 @@ uint32_t compute_psogc_timestamp(
uint32_t res_day = (day - 1) + year_start_day + month_start_day[month - 1];
return second + (minute + (hour + (res_day * 24)) * 60) * 60;
}
string decrypt_gci_fixed_size_file_data_section_for_salvage(
const void* data_section,
size_t size,
uint32_t round1_seed,
uint64_t round2_seed,
size_t max_decrypt_bytes) {
string decrypted = decrypt_gci_or_vms_v2_data_section<true>(
data_section, size, round1_seed, max_decrypt_bytes);
PSOV2Encryption round2_crypt(round2_seed);
round2_crypt.encrypt_big_endian(decrypted.data(), decrypted.size());
return decrypted;
}
+32 -13
View File
@@ -2,6 +2,7 @@
#include <stdint.h>
#include <algorithm>
#include <phosg/Encoding.hh>
#include <phosg/Hash.hh>
#include <phosg/Random.hh>
@@ -275,12 +276,17 @@ struct PSOGCGuildCardFile {
template <bool IsBigEndian>
std::string decrypt_gci_or_vms_v2_data_section(
const void* data_section, size_t size, uint32_t round1_seed) {
const void* data_section, size_t size, uint32_t round1_seed, size_t max_decrypt_bytes = 0) {
if (max_decrypt_bytes == 0) {
max_decrypt_bytes = size;
} else {
max_decrypt_bytes = std::min<size_t>(max_decrypt_bytes, size);
}
std::string decrypted(size, '\0');
std::string decrypted(max_decrypt_bytes, '\0');
PSOV2Encryption shuf_crypt(round1_seed);
ShuffleTables shuf(shuf_crypt);
shuf.shuffle(decrypted.data(), data_section, size, true);
shuf.shuffle(decrypted.data(), data_section, max_decrypt_bytes, true);
size_t orig_size = decrypted.size();
decrypted.resize((decrypted.size() + 3) & (~3));
@@ -311,7 +317,11 @@ std::string encrypt_gci_or_vms_v2_data_section(
template <typename StructT>
StructT decrypt_gci_fixed_size_file_data_section(
const void* data_section, size_t size, uint32_t round1_seed) {
const void* data_section,
size_t size,
uint32_t round1_seed,
bool skip_checksum = false,
uint64_t override_round2_seed = 0xFFFFFFFFFFFFFFFF) {
std::string decrypted = decrypt_gci_or_vms_v2_data_section<true>(
data_section, size, round1_seed);
@@ -320,22 +330,31 @@ StructT decrypt_gci_fixed_size_file_data_section(
}
StructT ret = *reinterpret_cast<const StructT*>(decrypted.data());
PSOV2Encryption round2_crypt(ret.round2_seed);
PSOV2Encryption round2_crypt(override_round2_seed < 0x100000000 ? override_round2_seed : ret.round2_seed.load());
round2_crypt.encrypt_big_endian(&ret, offsetof(StructT, round2_seed));
uint32_t expected_crc = ret.checksum;
ret.checksum = 0;
uint32_t actual_crc = crc32(&ret, sizeof(ret));
ret.checksum = expected_crc;
if (expected_crc != actual_crc) {
throw std::runtime_error(string_printf(
"incorrect decrypted data section checksum: expected %08" PRIX32 "; received %08" PRIX32,
expected_crc, actual_crc));
if (!skip_checksum) {
uint32_t expected_crc = ret.checksum;
ret.checksum = 0;
uint32_t actual_crc = crc32(&ret, sizeof(ret));
ret.checksum = expected_crc;
if (expected_crc != actual_crc) {
throw std::runtime_error(string_printf(
"incorrect decrypted data section checksum: expected %08" PRIX32 "; received %08" PRIX32,
expected_crc, actual_crc));
}
}
return ret;
}
std::string decrypt_gci_fixed_size_file_data_section_for_salvage(
const void* data_section,
size_t size,
uint32_t round1_seed,
uint64_t round2_seed,
size_t max_decrypt_bytes);
template <typename StructT>
std::string encrypt_gci_fixed_size_file_data_section(
const StructT& s, uint32_t round1_seed) {