add salvage-gci action
This commit is contained in:
+122
-1
@@ -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
@@ -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
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user