1
0
mirror of https://github.com/dolphin-emu/dolphin synced 2024-06-28 22:46:42 +00:00

Compare commits

...

2 Commits

Author SHA1 Message Date
LillyJadeKatrin
c6c80cbeba
Merge 6ae420b7da into 10a95a4d5b 2024-06-26 03:43:56 +00:00
LillyJadeKatrin
6ae420b7da Add Approved Patch Allowlist for Achievements
Prototype of a system to whitelist known game patches that are allowed to be used while RetroAchievements Hardcore mode is active. ApprovedInis.txt contains known hashes for the ini files as they appear in the repo, and can be compared to the local versions of these files to ensure they have not been edited locally by the player. ApprovedInis.txt is hashed and verified similarly first, with its hash residing as a const string within AchievementManager.h, ensuring ApprovedInis and the hashes within cannot be modified without editing Dolphin's source code and recompiling completely.
2024-06-25 23:43:42 -04:00
7 changed files with 183 additions and 13 deletions

13
Data/Sys/ApprovedInis.txt Normal file
View File

@ -0,0 +1,13 @@
# The purpose of this file is to provide a list of hashes for approved ini files for RetroAchievements use.
# Files approved in this list may be used; if any hash is incorrect then it has been modified by the
# player and risks being used for cheating.
# When editing any ini file in Sys/GameSettings, check if that file is here. If it is, and the changes you
# have made do not modify gameplay in a cheating fashion, please recalculate the SHA1 of that file and
# update it in this list. Then, recalculate the SHA1 of *this* file and update it in AchievementManager.h.
GCCE01 START
# Fix buffer overrun bug (crash at Goblin Wall)
6C107FEC15C76201233CA2645EB5FAB4FF9751CE
# Fix GBA connections
483BDB94615C690045C3759795AF13CE76552286
GCCE01 END

View File

@ -1,5 +1,8 @@
# GCCE01 - FINAL FANTASY Crystal Chronicles
# This patch is approved for RetroAchievements use. Please see
# Sys/ApprovedInis.txt for more information.
[OnFrame]
# Fix incorrect bounds check before an EFB to RAM copy that causes buffer overruns.
# With this patch enabled, it is safe to set EFBToTextureEnable = True.

View File

@ -14,16 +14,21 @@
#include <rcheevos/include/rc_hash.h>
#include "Common/Assert.h"
#include "Common/BitUtils.h"
#include "Common/CommonPaths.h"
#include "Common/FileUtil.h"
#include "Common/IOFile.h"
#include "Common/Image.h"
#include "Common/Logging/Log.h"
#include "Common/ScopeGuard.h"
#include "Common/Version.h"
#include "Common/WorkQueueThread.h"
#include "Core/ActionReplay.h"
#include "Core/Config/AchievementSettings.h"
#include "Core/Core.h"
#include "Core/GeckoCode.h"
#include "Core/HW/Memmap.h"
#include "Core/PatchEngine.h"
#include "Core/PowerPC/MMU.h"
#include "Core/System.h"
#include "DiscIO/Blob.h"
@ -262,6 +267,130 @@ bool AchievementManager::IsHardcoreModeActive() const
return rc_client_is_processing_required(m_client);
}
bool AchievementManager::VerifyPatchHash(const Common::SHA1::Digest& digest) const
{
File::IOFile list_file;
if (!list_file.Open(
fmt::format("{}{}{}", File::GetSysDirectory(), DIR_SEP, APPROVED_LIST_FILENAME), "rb"))
{
ERROR_LOG_FMT(ACHIEVEMENTS, "Failed to open approved patch hash list. Disabling all patches.");
return false;
}
std::vector<u8> buffer(list_file.GetSize());
if (!list_file.ReadBytes(buffer.data(), list_file.GetSize()))
{
ERROR_LOG_FMT(ACHIEVEMENTS, "Failed to read approved patch hash list. Disabling all patches.");
return false;
}
list_file.Close();
auto context = Common::SHA1::CreateContext();
context->Update(buffer);
auto list_hash = context->Finish();
if (list_hash != APPROVED_LIST_HASH)
{
ERROR_LOG_FMT(ACHIEVEMENTS, "Approved list hash does not match expected hash");
return false;
}
std::string hash;
for (size_t ix = 0; ix < digest.size(); ix++)
{
u8 upper = digest[ix] / 16;
hash.append(1, upper + ((upper > 9) ? 'A' - 10 : '0'));
u8 lower = digest[ix] % 16;
hash.append(1, lower + ((lower > 9) ? 'A' - 10 : '0'));
}
auto section_begin_itr =
std::search(buffer.begin(), buffer.end(), m_game_ini_id.begin(), m_game_ini_id.end());
auto section_end_itr =
std::search(section_begin_itr + 1, buffer.end(), m_game_ini_id.begin(), m_game_ini_id.end());
auto hash_itr = std::search(section_begin_itr, section_end_itr, hash.begin(), hash.end());
if (hash_itr == section_end_itr)
{
ERROR_LOG_FMT(ACHIEVEMENTS, "Ini hash {} from file {} not found in approved list", hash,
m_game_ini_id);
return false;
}
return true;
}
template <size_t length>
void UpdateFromArray(Common::SHA1::Context& context, const std::array<u8, length>& data)
{
context.Update(data.data(), data.size());
}
bool AchievementManager::IsPatchApproved(const PatchEngine::Patch& patch) const
{
if (!IsHardcoreModeActive())
return true;
INFO_LOG_FMT(ACHIEVEMENTS, "Verifying patch {}", patch.name);
auto context = Common::SHA1::CreateContext();
UpdateFromArray(*context, Common::BitCastToArray<u8>(static_cast<u64>(patch.entries.size())));
for (const auto& entry : patch.entries)
{
UpdateFromArray(*context, Common::BitCastToArray<u8>(entry.type));
UpdateFromArray(*context, Common::BitCastToArray<u8>(entry.address));
UpdateFromArray(*context, Common::BitCastToArray<u8>(entry.value));
UpdateFromArray(*context, Common::BitCastToArray<u8>(entry.comparand));
UpdateFromArray(*context, Common::BitCastToArray<u8>(entry.conditional));
}
auto digest = context->Finish();
bool verified = VerifyPatchHash(digest);
if (!verified)
{
OSD::AddMessage(
fmt::format("Failed to verify patch {} from file {}.", patch.name, m_game_ini_id),
OSD::Duration::VERY_LONG, OSD::Color::RED);
OSD::AddMessage("Disable hardcore mode to enable this patch.", OSD::Duration::VERY_LONG,
OSD::Color::RED);
}
return verified;
}
bool AchievementManager::IsGeckoCodeApproved(const Gecko::GeckoCode& code) const
{
if (!IsHardcoreModeActive())
return true;
INFO_LOG_FMT(ACHIEVEMENTS, "Verifying gecko code {}", code.name);
auto context = Common::SHA1::CreateContext();
UpdateFromArray(*context, Common::BitCastToArray<u8>(static_cast<u64>(code.codes.size())));
for (const auto& entry : code.codes)
{
UpdateFromArray(*context, Common::BitCastToArray<u8>(entry.address));
UpdateFromArray(*context, Common::BitCastToArray<u8>(entry.data));
}
auto digest = context->Finish();
return VerifyPatchHash(digest);
}
bool AchievementManager::IsARCodeApproved(const ActionReplay::ARCode& code) const
{
if (!IsHardcoreModeActive())
return true;
INFO_LOG_FMT(ACHIEVEMENTS, "Verifying AR code {}", code.name);
auto context = Common::SHA1::CreateContext();
UpdateFromArray(*context, Common::BitCastToArray<u8>(static_cast<u64>(code.ops.size())));
for (const auto& entry : code.ops)
{
UpdateFromArray(*context, Common::BitCastToArray<u8>(entry.cmd_addr));
UpdateFromArray(*context, Common::BitCastToArray<u8>(entry.value));
}
auto digest = context->Finish();
return VerifyPatchHash(digest);
}
void AchievementManager::SetSpectatorMode()
{
rc_client_set_spectator_mode_enabled(m_client, Config::Get(Config::RA_SPECTATOR_ENABLED));

View File

@ -28,6 +28,9 @@
#include "Common/Event.h"
#include "Common/HttpRequest.h"
#include "Common/WorkQueueThread.h"
#include "Core/ActionReplay.h"
#include "Core/GeckoCode.h"
#include "Core/PatchEngine.h"
#include "DiscIO/Volume.h"
#include "VideoCommon/Assets/CustomTextureData.h"
@ -60,6 +63,10 @@ public:
static constexpr std::string_view GRAY = "transparent";
static constexpr std::string_view GOLD = "#FFD700";
static constexpr std::string_view BLUE = "#0B71C1";
static constexpr std::string_view APPROVED_LIST_FILENAME = "ApprovedInis.txt";
static const inline Common::SHA1::Digest APPROVED_LIST_HASH = {
0xA1, 0x1D, 0xDE, 0x49, 0x87, 0xE1, 0x86, 0xD0, 0x59, 0x40,
0xFB, 0xF5, 0x98, 0x39, 0x54, 0xD6, 0x2A, 0xF1, 0x33, 0xD6};
struct LeaderboardEntry
{
@ -105,6 +112,10 @@ public:
std::recursive_mutex& GetLock();
void SetHardcoreMode();
bool IsHardcoreModeActive() const;
void SetGameIniId(const std::string& game_ini_id) { m_game_ini_id = game_ini_id; }
bool IsPatchApproved(const PatchEngine::Patch& patch) const;
bool IsGeckoCodeApproved(const Gecko::GeckoCode& gecko_code) const;
bool IsARCodeApproved(const ActionReplay::ARCode& ar_code) const;
void SetSpectatorMode();
std::string_view GetPlayerDisplayName() const;
u32 GetPlayerScore() const;
@ -161,6 +172,8 @@ private:
rc_client_leaderboard_entry_list_t* list,
rc_client_t* client, void* userdata);
bool VerifyPatchHash(const Common::SHA1::Digest& digest) const;
static void HandleAchievementTriggeredEvent(const rc_client_event_t* client_event);
static void HandleLeaderboardStartedEvent(const rc_client_event_t* client_event);
static void HandleLeaderboardFailedEvent(const rc_client_event_t* client_event);
@ -206,6 +219,8 @@ private:
std::chrono::steady_clock::time_point m_last_rp_time = std::chrono::steady_clock::now();
std::chrono::steady_clock::time_point m_last_progress_message = std::chrono::steady_clock::now();
std::string m_game_ini_id;
std::unordered_map<AchievementId, LeaderboardStatus> m_leaderboard_map;
bool m_challenges_updated = false;
std::unordered_set<AchievementId> m_active_challenges;
@ -237,6 +252,10 @@ public:
constexpr bool IsHardcoreModeActive() { return false; }
bool IsPatchApproved(const PatchEngine::Patch& patch) const { return true; }
bool IsGeckoCodeApproved(const Gecko::GeckoCode& gecko_code) const { return true; }
bool IsARCodeApproved(const ActionReplay::ARCode& ar_code) const { return true; }
constexpr void LoadGame(const std::string&, const DiscIO::Volume*) {}
constexpr void DoFrame() {}

View File

@ -39,6 +39,7 @@
#include "Common/MsgHandler.h"
#include "Core/ARDecrypt.h"
#include "Core/AchievementManager.h"
#include "Core/CheatCodes.h"
#include "Core/Config/MainSettings.h"
#include "Core/PowerPC/MMU.h"
@ -199,13 +200,15 @@ std::vector<ARCode> LoadCodes(const Common::IniFile& global_ini, const Common::I
{
if (!current_code.ops.empty())
{
codes.push_back(current_code);
if (AchievementManager::GetInstance().IsARCodeApproved(current_code))
codes.push_back(current_code);
current_code.ops.clear();
}
if (!encrypted_lines.empty())
{
DecryptARCode(encrypted_lines, &current_code.ops);
codes.push_back(current_code);
if (AchievementManager::GetInstance().IsARCodeApproved(current_code))
codes.push_back(current_code);
current_code.ops.clear();
encrypted_lines.clear();
}
@ -227,7 +230,8 @@ std::vector<ARCode> LoadCodes(const Common::IniFile& global_ini, const Common::I
}
// Handle the last code correctly.
if (!current_code.ops.empty())
if (!current_code.ops.empty() &&
AchievementManager::GetInstance().IsARCodeApproved(current_code))
{
codes.push_back(current_code);
}

View File

@ -13,6 +13,7 @@
#include "Common/IniFile.h"
#include "Common/Logging/Log.h"
#include "Common/StringUtil.h"
#include "Core/AchievementManager.h"
#include "Core/CheatCodes.h"
namespace Gecko
@ -155,7 +156,7 @@ std::vector<GeckoCode> LoadCodes(const Common::IniFile& globalIni, const Common:
ss.seekg(1);
[[fallthrough]];
case '$':
if (!gcode.name.empty())
if (!gcode.name.empty() && AchievementManager::GetInstance().IsGeckoCodeApproved(gcode))
gcodes.push_back(gcode);
gcode = GeckoCode();
gcode.enabled = (1 == ss.tellg()); // silly
@ -189,7 +190,7 @@ std::vector<GeckoCode> LoadCodes(const Common::IniFile& globalIni, const Common:
}
// add the last code
if (!gcode.name.empty())
if (!gcode.name.empty() && AchievementManager::GetInstance().IsGeckoCodeApproved(gcode))
{
gcodes.push_back(gcode);
}

View File

@ -118,7 +118,8 @@ void LoadPatchSection(const std::string& section, std::vector<Patch>* patches,
if (line[0] == '$')
{
// Take care of the previous code
if (!currentPatch.name.empty())
if (!currentPatch.name.empty() &&
AchievementManager::GetInstance().IsPatchApproved(currentPatch))
{
patches->push_back(currentPatch);
}
@ -135,7 +136,8 @@ void LoadPatchSection(const std::string& section, std::vector<Patch>* patches,
}
}
if (!currentPatch.name.empty() && !currentPatch.entries.empty())
if (!currentPatch.name.empty() && !currentPatch.entries.empty() &&
AchievementManager::GetInstance().IsPatchApproved(currentPatch))
{
patches->push_back(currentPatch);
}
@ -214,6 +216,11 @@ void LoadPatches()
Common::IniFile globalIni = sconfig.LoadDefaultGameIni();
Common::IniFile localIni = sconfig.LoadLocalGameIni();
#ifdef USE_RETRO_ACHIEVEMENTS
std::lock_guard lg{AchievementManager::GetInstance().GetLock()};
AchievementManager::GetInstance().SetGameIniId(sconfig.GetGameID());
#endif // USE_RETRO_ACHIEVEMENTS
LoadPatchSection("OnFrame", &s_on_frame, globalIni, localIni);
// Check if I'm syncing Codes
@ -233,9 +240,6 @@ void LoadPatches()
static void ApplyPatches(const Core::CPUThreadGuard& guard, const std::vector<Patch>& patches)
{
if (AchievementManager::GetInstance().IsHardcoreModeActive())
return;
for (const Patch& patch : patches)
{
if (patch.enabled)
@ -277,9 +281,6 @@ static void ApplyPatches(const Core::CPUThreadGuard& guard, const std::vector<Pa
static void ApplyMemoryPatches(const Core::CPUThreadGuard& guard,
std::span<const std::size_t> memory_patch_indices)
{
if (AchievementManager::GetInstance().IsHardcoreModeActive())
return;
std::lock_guard lock(s_on_frame_memory_mutex);
for (std::size_t index : memory_patch_indices)
{