mirror of
https://github.com/spice2x/spice2x.github.io.git
synced 2026-10-03 16:58:13 -07:00
0f4ab63101
## Link to GitHub Issue or related Pull Request, if one exists
None
## Description of change
Patch Manager showed valid signature patches as broken: **"Bad patch;
patch is neither on or off"** instead of Enabled/Disabled, so they could
not be toggled.
`SignaturePatch::to_memory` had two bugs. In the configurator it cached
a file offset as `data_offset_ptr`, so status checks `memcmp`'d a fake
address. It also passed JSON `offset` into `find_pattern` while still
indexing the signature/replacement from 0, which mis-aligned every patch
with `offset != 0`. This change locates the signature start, applies
`offset` afterward, compares only the replacement window, and leaves the
pointer null so `is_patch_active` re-resolves from `data_offset`.
Not proposed as built-in patches. The JSON below is the reproduction
case: each entry uses `offset > 0` and a replacement shorter than the
signature.
## Testing
Reproduced in Patch Manager against `bm2dx.dll` using the signature JSON
below. Before the fix, every patch reported "neither on or off". After
the fix, each patch locates, shows Disabled/Enabled, and toggling writes
only the replacement bytes at `signature_match + offset`.
## Demo
<details>
<summary>Signature JSON used to reproduce (offset + short
replacement)</summary>
```json
[
{
"info": "streaming / getcm patches (type=signature)",
"gameCode": "LDJ",
"notes": "Each site uses a unique signature (usage=0 only)."
},
{
"type": "group",
"id": "streaming-getcm",
"name": "Streaming getcm",
"description": "Enable all children so streaming.common merges without Banner FS and getcm can fire without visiting Test Mode.",
"gameCode": "LDJ"
},
{
"name": "Streaming: merge common without Banner",
"description": "NOP jz in streaming.common callback so CM work table is filled even when Banner FS is still null.",
"caution": "Required. Without this, early common responses are discarded and getcm stays empty.",
"gameCode": "LDJ",
"type": "signature",
"group": "streaming-getcm",
"dllName": "bm2dx.dll",
"signature": "E8????????4885C00F84????????488D0D????????488D15????????41B848080000",
"replacement": "909090909090",
"offset": 8,
"usage": 0
},
{
"name": "Streaming: scheduler without Banner (common)",
"description": "NOP jz after Banner getter on the common branch of the periodic scheduler.",
"caution": "Enable with the getcm scheduler sibling. Unique via imul of common-interval dword.",
"gameCode": "LDJ",
"type": "signature",
"group": "streaming-getcm",
"dllName": "bm2dx.dll",
"signature": "E8????????4885C0742469057E791C0AE8030000",
"replacement": "9090",
"offset": 8,
"usage": 0
},
{
"name": "Streaming: scheduler without Banner (getcm)",
"description": "NOP jz after Banner getter on the getcm branch of the periodic scheduler.",
"caution": "Enable with the common scheduler sibling. Unique via imul of getcm-interval dword. Without this, getcm never schedules while Banner FS is null.",
"gameCode": "LDJ",
"type": "signature",
"group": "streaming-getcm",
"dllName": "bm2dx.dll",
"signature": "E8????????4885C07424690574B91D0AE8030000",
"replacement": "9090",
"offset": 8,
"usage": 0
},
{
"name": "Streaming: fall into getcm after common",
"description": "NOP jmp-after-common so the same scheduler tick can evaluate getcm instead of returning early.",
"caution": "Pair with Banner scheduler skips (or a live Banner FS).",
"gameCode": "LDJ",
"type": "signature",
"group": "streaming-getcm",
"dllName": "bm2dx.dll",
"signature": "891D????????E9????????E8????????4885C074246905",
"replacement": "9090909090",
"offset": 6,
"usage": 0
},
{
"name": "Streaming: getcm interval 1s #1",
"description": "Default getcm poll interval 1800s to 1s (first init store).",
"caution": "Enable #1 and #2 together. Trailing BF3C000000 distinguishes this init site.",
"gameCode": "LDJ",
"type": "signature",
"group": "streaming-getcm",
"dllName": "bm2dx.dll",
"signature": "C705????????18150000C705????????08070000C705????????201C0000BF3C000000",
"replacement": "01000000",
"offset": 16,
"usage": 0
},
{
"name": "Streaming: getcm interval 1s #2",
"description": "Default getcm poll interval 1800s to 1s (second init store).",
"caution": "Enable #1 and #2 together. Trailing 448925 distinguishes this init site.",
"gameCode": "LDJ",
"type": "signature",
"group": "streaming-getcm",
"dllName": "bm2dx.dll",
"signature": "C705????????18150000C705????????08070000C705????????201C0000448925",
"replacement": "01000000",
"offset": 16,
"usage": 0
}
]
```
</details>
Co-authored-by: Cursor <cursoragent@cursor.com>
1096 lines
43 KiB
C++
1096 lines
43 KiB
C++
#include "internal.h"
|
|
|
|
#include <algorithm>
|
|
#include <cstring>
|
|
#include <memory>
|
|
#include <windows.h>
|
|
#include <psapi.h>
|
|
#include <sstream>
|
|
#include <unordered_set>
|
|
#include "avs/game.h"
|
|
#include "cfg/configurator.h"
|
|
#include "external/robin_hood.h"
|
|
#include "launcher/launcher.h"
|
|
#include "util/fileutils.h"
|
|
#include "util/libutils.h"
|
|
#include "util/logging.h"
|
|
#include "util/memutils.h"
|
|
#include "util/sigscan.h"
|
|
#include "util/utils.h"
|
|
|
|
// std::min
|
|
#ifdef min
|
|
#undef min
|
|
#endif
|
|
|
|
// std::max
|
|
#ifdef max
|
|
#undef max
|
|
#endif
|
|
|
|
namespace patcher {
|
|
|
|
robin_hood::unordered_map<std::string, std::unique_ptr<std::vector<uint8_t>>> DLL_MAP;
|
|
robin_hood::unordered_map<std::string, std::unique_ptr<std::vector<uint8_t>>> DLL_MAP_ORG;
|
|
|
|
void clear_dll_maps() {
|
|
DLL_MAP.clear();
|
|
DLL_MAP_ORG.clear();
|
|
}
|
|
|
|
void hard_apply_patches() {
|
|
std::vector<std::filesystem::path> written_list;
|
|
|
|
// dll_name -> in-memory image; populated lazily, written back once at
|
|
// the end. Avoids re-reading and re-writing the full DLL per patch,
|
|
// which used to dominate the cost of "Overwrite game files" on games
|
|
// with large patch lists (and leaked a vector per call via bin_read's
|
|
// raw new'd return).
|
|
robin_hood::unordered_map<std::string, std::unique_ptr<std::vector<uint8_t>>> dll_cache;
|
|
std::unordered_set<std::string> dirty;
|
|
|
|
auto load_dll = [&](const std::string &dll_name) -> std::vector<uint8_t> * {
|
|
auto it = dll_cache.find(dll_name);
|
|
if (it != dll_cache.end()) {
|
|
return it->second ? it->second.get() : nullptr;
|
|
}
|
|
const auto dll_path = MODULE_PATH / dll_name;
|
|
create_dll_backup(written_list, dll_path);
|
|
std::unique_ptr<std::vector<uint8_t>> owned(fileutils::bin_read(dll_path));
|
|
if (!owned || owned->empty()) {
|
|
// remember the miss so subsequent patches don't re-read/backup
|
|
dll_cache.emplace(dll_name, nullptr);
|
|
return nullptr;
|
|
}
|
|
auto *ptr = owned.get();
|
|
dll_cache.emplace(dll_name, std::move(owned));
|
|
return ptr;
|
|
};
|
|
|
|
for (auto &patch : patches) {
|
|
switch (patch.type) {
|
|
case PatchType::Memory:
|
|
for (auto &memory_patch : patch.patches_memory) {
|
|
auto *dll_data = load_dll(memory_patch.dll_name);
|
|
if (!dll_data) {
|
|
continue;
|
|
}
|
|
const auto max_len = std::max(
|
|
memory_patch.data_disabled_len, memory_patch.data_enabled_len);
|
|
if (memory_patch.data_offset + max_len <= dll_data->size()) {
|
|
if (patch.enabled) {
|
|
memcpy(dll_data->data() + memory_patch.data_offset,
|
|
memory_patch.data_enabled.get(),
|
|
memory_patch.data_enabled_len);
|
|
} else {
|
|
memcpy(dll_data->data() + memory_patch.data_offset,
|
|
memory_patch.data_disabled.get(),
|
|
memory_patch.data_disabled_len);
|
|
}
|
|
dirty.insert(memory_patch.dll_name);
|
|
}
|
|
}
|
|
break;
|
|
case PatchType::Union:
|
|
if (!patch.enabled) {
|
|
break;
|
|
}
|
|
for (auto &union_patch : patch.patches_union) {
|
|
if (union_patch.name == patch.selected_union_name) {
|
|
auto *dll_data = load_dll(union_patch.dll_name);
|
|
if (!dll_data) {
|
|
break;
|
|
}
|
|
if (union_patch.offset + union_patch.data_len <= dll_data->size()) {
|
|
memcpy(dll_data->data() + union_patch.offset,
|
|
union_patch.data.get(), union_patch.data_len);
|
|
dirty.insert(union_patch.dll_name);
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
break;
|
|
case PatchType::Integer: {
|
|
if (!patch.enabled) {
|
|
break;
|
|
}
|
|
auto &numpatch = patch.patch_number;
|
|
auto *dll_data = load_dll(numpatch.dll_name);
|
|
if (!dll_data) {
|
|
break;
|
|
}
|
|
if (numpatch.data_offset + numpatch.size_in_bytes <= dll_data->size()) {
|
|
int_to_little_endian_bytes(
|
|
numpatch.value,
|
|
dll_data->data() + numpatch.data_offset,
|
|
numpatch.size_in_bytes);
|
|
dirty.insert(numpatch.dll_name);
|
|
}
|
|
break;
|
|
}
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
// single write-back per dirty DLL
|
|
for (const auto &dll_name : dirty) {
|
|
const auto &data = dll_cache.at(dll_name);
|
|
if (data) {
|
|
fileutils::bin_write(MODULE_PATH / dll_name, data->data(), data->size());
|
|
}
|
|
}
|
|
}
|
|
|
|
PatchStatus is_patch_active(PatchData &patch) {
|
|
|
|
// check patch type
|
|
switch (patch.type) {
|
|
case PatchType::Memory: {
|
|
|
|
// iterate patches
|
|
bool enabled = false;
|
|
bool disabled = false;
|
|
for (auto &memory_patch : patch.patches_memory) {
|
|
auto max_size = std::max(memory_patch.data_enabled_len, memory_patch.data_disabled_len);
|
|
|
|
// check for error to not try to get the pointer every frame
|
|
if (memory_patch.fatal_error) {
|
|
if (cfg::CONFIGURATOR_STANDALONE) {
|
|
patch.unverified = true;
|
|
return patch.enabled ? PatchStatus::Enabled : PatchStatus::Disabled;
|
|
}
|
|
return PatchStatus::Error;
|
|
}
|
|
|
|
// find data pointer if not known yet
|
|
if (memory_patch.data_offset_ptr == nullptr) {
|
|
// check if file exists
|
|
auto dll_path = MODULE_PATH / memory_patch.dll_name;
|
|
if (!fileutils::file_exists(dll_path)) {
|
|
// file does not exist so that's pretty fatal
|
|
memory_patch.fatal_error = true;
|
|
patch.error_reason = "DLL not found on disk";
|
|
return PatchStatus::Error;
|
|
}
|
|
|
|
// standalone mode
|
|
if (cfg::CONFIGURATOR_STANDALONE) {
|
|
auto file = find_in_dll_map(
|
|
memory_patch.dll_name, memory_patch.data_offset, max_size);
|
|
if (!file) {
|
|
patch.error_reason = "Invalid DLL or offset";
|
|
return PatchStatus::Error;
|
|
}
|
|
memory_patch.data_offset_ptr = &(*file)[memory_patch.data_offset];
|
|
|
|
} else {
|
|
// get module
|
|
auto module = libutils::try_module(dll_path);
|
|
if (!module) {
|
|
// no fatal error, might just not be loaded yet
|
|
patch.error_reason = "DLL not loaded into memory";
|
|
return PatchStatus::Error;
|
|
}
|
|
|
|
// convert offset to RVA
|
|
auto offset = libutils::offset2rva(dll_path, memory_patch.data_offset);
|
|
if (offset == -1) {
|
|
// RVA not found means unrecoverable
|
|
memory_patch.fatal_error = true;
|
|
patch.error_reason = "RVA not found";
|
|
return PatchStatus::Error;
|
|
}
|
|
|
|
// get module information
|
|
MODULEINFO module_info {};
|
|
if (!GetModuleInformation(
|
|
GetCurrentProcess(),
|
|
module,
|
|
&module_info,
|
|
sizeof(MODULEINFO))) {
|
|
// hmm, maybe try again sometime, not fatal
|
|
patch.error_reason = "Failed to get module info";
|
|
return PatchStatus::Error;
|
|
}
|
|
|
|
// check bounds
|
|
auto max_offset = static_cast<uintptr_t>(offset) + max_size;
|
|
auto image_size = static_cast<uintptr_t>(module_info.SizeOfImage);
|
|
if (max_offset >= image_size) {
|
|
// outside of bounds, invalid patch, fatal
|
|
memory_patch.fatal_error = true;
|
|
patch.error_reason = "Invalid DLL or offset";
|
|
return PatchStatus::Error;
|
|
}
|
|
|
|
// save pointer
|
|
auto dll_base = reinterpret_cast<uintptr_t>(module_info.lpBaseOfDll);
|
|
memory_patch.data_offset_ptr = reinterpret_cast<uint8_t *>(dll_base + offset);
|
|
}
|
|
}
|
|
|
|
// virtual protect
|
|
memutils::VProtectGuard guard(memory_patch.data_offset_ptr, max_size);
|
|
|
|
// compare
|
|
if (!guard.is_bad_address() && !memcmp(
|
|
memory_patch.data_enabled.get(),
|
|
memory_patch.data_offset_ptr,
|
|
memory_patch.data_enabled_len)) {
|
|
enabled = true;
|
|
} else if (!guard.is_bad_address() && !memcmp(
|
|
memory_patch.data_disabled.get(),
|
|
memory_patch.data_offset_ptr,
|
|
memory_patch.data_disabled_len)) {
|
|
disabled = true;
|
|
} else {
|
|
patch.error_reason = "Bad patch; patch is neither on or off (single patch)";
|
|
return PatchStatus::Error;
|
|
}
|
|
}
|
|
// check detection flags
|
|
if (enabled && disabled) {
|
|
patch.error_reason = "Bad patch; patch is both on and off (cumulative)";
|
|
return PatchStatus::Error;
|
|
} else if (enabled) {
|
|
return PatchStatus::Enabled;
|
|
} else if (disabled) {
|
|
return PatchStatus::Disabled;
|
|
} else {
|
|
patch.error_reason = "Bad patch; patch is neither on or off (cumulative)";
|
|
return PatchStatus::Error;
|
|
}
|
|
}
|
|
case PatchType::Signature: {
|
|
return PatchStatus::Error;
|
|
}
|
|
case PatchType::Union: {
|
|
// iterate patches
|
|
bool match_found = false;
|
|
patch.selected_union_name = "";
|
|
for (auto &union_patch : patch.patches_union) {
|
|
// check for error to not try to get the pointer every frame
|
|
if (union_patch.fatal_error) {
|
|
if (cfg::CONFIGURATOR_STANDALONE) {
|
|
patch.unverified = true;
|
|
return patch.enabled ? PatchStatus::Enabled : PatchStatus::Disabled;
|
|
}
|
|
return PatchStatus::Error;
|
|
}
|
|
|
|
// find data pointer if not known yet
|
|
if (union_patch.data_offset_ptr == nullptr) {
|
|
// check if file exists
|
|
auto dll_path = MODULE_PATH / union_patch.dll_name;
|
|
if (!fileutils::file_exists(dll_path)) {
|
|
// file does not exist so that's pretty fatal
|
|
union_patch.fatal_error = true;
|
|
patch.error_reason = "DLL not found on disk";
|
|
return PatchStatus::Error;
|
|
}
|
|
|
|
// standalone mode
|
|
if (cfg::CONFIGURATOR_STANDALONE) {
|
|
auto file = find_in_dll_map(
|
|
union_patch.dll_name, union_patch.offset, union_patch.data_len);
|
|
if (!file) {
|
|
patch.error_reason = "Invalid DLL or offset";
|
|
return PatchStatus::Error;
|
|
}
|
|
union_patch.data_offset_ptr = &(*file)[union_patch.offset];
|
|
|
|
} else {
|
|
// get module
|
|
auto module = libutils::try_module(dll_path);
|
|
if (!module) {
|
|
// no fatal error, might just not be loaded yet
|
|
patch.error_reason = "DLL not loaded into memory";
|
|
return PatchStatus::Error;
|
|
}
|
|
|
|
// convert offset to RVA
|
|
auto offset = libutils::offset2rva(dll_path, union_patch.offset);
|
|
if (offset == -1) {
|
|
// RVA not found means unrecoverable
|
|
union_patch.fatal_error = true;
|
|
patch.error_reason = "RVA not found";
|
|
return PatchStatus::Error;
|
|
}
|
|
|
|
// get module information
|
|
MODULEINFO module_info {};
|
|
if (!GetModuleInformation(
|
|
GetCurrentProcess(),
|
|
module,
|
|
&module_info,
|
|
sizeof(MODULEINFO))) {
|
|
// hmm, maybe try again sometime, not fatal
|
|
patch.error_reason = "Failed to get module info";
|
|
return PatchStatus::Error;
|
|
}
|
|
|
|
// check bounds
|
|
auto max_offset = static_cast<uintptr_t>(offset) + union_patch.data_len;
|
|
auto image_size = static_cast<uintptr_t>(module_info.SizeOfImage);
|
|
if (max_offset >= image_size) {
|
|
// outside of bounds, invalid patch, fatal
|
|
union_patch.fatal_error = true;
|
|
patch.error_reason = "Invalid DLL or offset";
|
|
return PatchStatus::Error;
|
|
}
|
|
|
|
// save pointer
|
|
auto dll_base = reinterpret_cast<uintptr_t>(module_info.lpBaseOfDll);
|
|
union_patch.data_offset_ptr = reinterpret_cast<uint8_t *>(dll_base + offset);
|
|
}
|
|
}
|
|
|
|
// virtual protect
|
|
memutils::VProtectGuard guard(union_patch.data_offset_ptr, union_patch.data_len);
|
|
if (guard.is_bad_address()) {
|
|
patch.error_reason = "Invalid offset, bad address";
|
|
return PatchStatus::Error;
|
|
}
|
|
|
|
// is this union patch enabled in DLL?
|
|
if (!match_found &&
|
|
memcmp(union_patch.data.get(), union_patch.data_offset_ptr, union_patch.data_len) == 0) {
|
|
match_found = true;
|
|
patch.selected_union_name = union_patch.name;
|
|
}
|
|
|
|
// if everything is OK, continue to check other patches in this union
|
|
}
|
|
// none of the union patches match what's in the DLL
|
|
if (!match_found) {
|
|
patch.error_reason = "No match found in union";
|
|
return PatchStatus::Error;
|
|
}
|
|
return patch.enabled ? PatchStatus::Enabled : PatchStatus::Disabled;
|
|
}
|
|
case PatchType::Integer: {
|
|
auto& numpatch = patch.patch_number;
|
|
numpatch.value = 0;
|
|
|
|
// check for fatal error and give up early
|
|
if (numpatch.fatal_error) {
|
|
if (cfg::CONFIGURATOR_STANDALONE) {
|
|
patch.unverified = true;
|
|
return patch.enabled ? PatchStatus::Enabled : PatchStatus::Disabled;
|
|
}
|
|
return PatchStatus::Error;
|
|
}
|
|
|
|
// find data pointer if not known yet
|
|
if (numpatch.data_offset_ptr == nullptr) {
|
|
// check if file exists
|
|
auto dll_path = MODULE_PATH / numpatch.dll_name;
|
|
if (!fileutils::file_exists(dll_path)) {
|
|
// file does not exist so that's pretty fatal
|
|
numpatch.fatal_error = true;
|
|
patch.error_reason = "DLL not found on disk";
|
|
return PatchStatus::Error;
|
|
}
|
|
|
|
// standalone mode
|
|
if (cfg::CONFIGURATOR_STANDALONE) {
|
|
const auto file = find_in_dll_map(
|
|
numpatch.dll_name, numpatch.data_offset, numpatch.size_in_bytes);
|
|
if (!file) {
|
|
patch.error_reason = "Invalid DLL or offset";
|
|
return PatchStatus::Error;
|
|
}
|
|
numpatch.data_offset_ptr = &(*file)[numpatch.data_offset];
|
|
} else {
|
|
// get module
|
|
const auto module = libutils::try_module(dll_path);
|
|
if (!module) {
|
|
// no fatal error, might just not be loaded yet
|
|
patch.error_reason = "DLL not loaded into memory";
|
|
return PatchStatus::Error;
|
|
}
|
|
|
|
// convert offset to RVA
|
|
const auto offset = libutils::offset2rva(dll_path, numpatch.data_offset);
|
|
if (offset == -1) {
|
|
// RVA not found means unrecoverable
|
|
numpatch.fatal_error = true;
|
|
patch.error_reason = "RVA not found";
|
|
return PatchStatus::Error;
|
|
}
|
|
|
|
// get module information
|
|
MODULEINFO module_info {};
|
|
if (!GetModuleInformation(
|
|
GetCurrentProcess(),
|
|
module,
|
|
&module_info,
|
|
sizeof(MODULEINFO))) {
|
|
|
|
// hmm, maybe try again sometime, not fatal
|
|
patch.error_reason = "Failed to get module info";
|
|
return PatchStatus::Error;
|
|
}
|
|
|
|
// check bounds
|
|
const auto max_offset = static_cast<uintptr_t>(offset) + numpatch.size_in_bytes;
|
|
const auto image_size = static_cast<uintptr_t>(module_info.SizeOfImage);
|
|
if (max_offset >= image_size) {
|
|
// outside of bounds, invalid patch, fatal
|
|
numpatch.fatal_error = true;
|
|
patch.error_reason = "Invalid DLL or offset";
|
|
return PatchStatus::Error;
|
|
}
|
|
|
|
// save pointer so we don't have to do this again next frame
|
|
const auto dll_base = reinterpret_cast<uintptr_t>(module_info.lpBaseOfDll);
|
|
numpatch.data_offset_ptr = reinterpret_cast<uint8_t *>(dll_base + offset);
|
|
}
|
|
}
|
|
|
|
// virtual protect
|
|
memutils::VProtectGuard guard(numpatch.data_offset_ptr, numpatch.size_in_bytes);
|
|
if (guard.is_bad_address()) {
|
|
patch.error_reason = "Invalid offset, bad address";
|
|
return PatchStatus::Error;
|
|
}
|
|
|
|
// what is the current value? check bounds
|
|
const auto value_in_dll =
|
|
parse_little_endian_int(numpatch.data_offset_ptr, numpatch.size_in_bytes);
|
|
if (value_in_dll < numpatch.min || numpatch.max < value_in_dll) {
|
|
patch.error_reason = "Number out of range, check min/max";
|
|
return PatchStatus::Error;
|
|
}
|
|
|
|
patch.patch_number.value = value_in_dll;
|
|
return patch.enabled ? PatchStatus::Enabled : PatchStatus::Disabled;
|
|
}
|
|
case PatchType::Unknown:
|
|
default:
|
|
patch.error_reason = "Unknown patch type";
|
|
return PatchStatus::Error;
|
|
}
|
|
}
|
|
|
|
bool apply_patch(PatchData &patch, bool active) {
|
|
|
|
// check patch type
|
|
switch (patch.type) {
|
|
case PatchType::Memory: {
|
|
|
|
// iterate memory patches
|
|
for (auto &memory_patch : patch.patches_memory) {
|
|
|
|
/*
|
|
* we won't use the cached data_offset_ptr here
|
|
* that makes it more reliable, also only happens on load/toggle
|
|
*/
|
|
|
|
// determine source/target buffer/size
|
|
uint8_t *src_buf = active
|
|
? memory_patch.data_disabled.get()
|
|
: memory_patch.data_enabled.get();
|
|
size_t src_len = active
|
|
? memory_patch.data_disabled_len
|
|
: memory_patch.data_enabled_len;
|
|
uint8_t *target_buf = active
|
|
? memory_patch.data_enabled.get()
|
|
: memory_patch.data_disabled.get();
|
|
size_t target_len = active
|
|
? memory_patch.data_enabled_len
|
|
: memory_patch.data_disabled_len;
|
|
|
|
// standalone mode
|
|
if (cfg::CONFIGURATOR_STANDALONE) {
|
|
auto max_len = std::max(src_len, target_len);
|
|
// find file from DLL_MAP
|
|
auto dll_file = find_in_dll_map(
|
|
memory_patch.dll_name, memory_patch.data_offset, max_len);
|
|
if (!dll_file) {
|
|
return false;
|
|
}
|
|
// find offset into file
|
|
if (memory_patch.data_offset_ptr == nullptr) {
|
|
memory_patch.data_offset_ptr =
|
|
&(*dll_file)[memory_patch.data_offset];
|
|
}
|
|
if (memory_patch.data_offset_ptr == nullptr) {
|
|
return false;
|
|
}
|
|
// copy target to memory if src matches
|
|
if (memcmp(memory_patch.data_offset_ptr, src_buf, src_len) == 0) {
|
|
memcpy(memory_patch.data_offset_ptr, target_buf, target_len);
|
|
}
|
|
|
|
} else {
|
|
|
|
// get pointer to offset
|
|
auto max_len = std::max(src_len, target_len);
|
|
if (memory_patch.data_offset_ptr == nullptr) {
|
|
memory_patch.data_offset_ptr =
|
|
get_dll_offset_for_patch_apply(
|
|
memory_patch.dll_name,
|
|
memory_patch.data_offset,
|
|
max_len);
|
|
if (memory_patch.data_offset_ptr == nullptr) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// virtual protect
|
|
memutils::VProtectGuard guard(
|
|
memory_patch.data_offset_ptr, max_len);
|
|
|
|
// copy target to memory if src matches
|
|
if (memcmp(memory_patch.data_offset_ptr, src_buf, src_len) == 0) {
|
|
memcpy(memory_patch.data_offset_ptr, target_buf, target_len);
|
|
}
|
|
}
|
|
}
|
|
|
|
// success
|
|
return true;
|
|
}
|
|
case PatchType::Signature: {
|
|
return false;
|
|
}
|
|
case PatchType::Union: {
|
|
// Find the selected union patch
|
|
auto it = std::find_if(patch.patches_union.begin(), patch.patches_union.end(),
|
|
[&](const UnionPatch& up) { return up.name == patch.selected_union_name; });
|
|
if (it == patch.patches_union.end()) {
|
|
return false;
|
|
}
|
|
auto& union_patch = *it;
|
|
|
|
// find data_offset_ptr
|
|
if (cfg::CONFIGURATOR_STANDALONE) {
|
|
// find file from DLL_MAP
|
|
auto dll_file = find_in_dll_map(
|
|
union_patch.dll_name, union_patch.offset, union_patch.data_len);
|
|
if (!dll_file) {
|
|
return false;
|
|
}
|
|
// find offset into file
|
|
if (union_patch.data_offset_ptr == nullptr) {
|
|
union_patch.data_offset_ptr =
|
|
reinterpret_cast<uint8_t*>(union_patch.offset + &(*dll_file)[0]);
|
|
}
|
|
} else {
|
|
if (union_patch.data_offset_ptr == nullptr) {
|
|
union_patch.data_offset_ptr = get_dll_offset_for_patch_apply(
|
|
union_patch.dll_name,
|
|
union_patch.offset,
|
|
union_patch.data_len);
|
|
}
|
|
}
|
|
|
|
if (union_patch.data_offset_ptr == nullptr) {
|
|
return false;
|
|
}
|
|
|
|
// Apply the selected union patch
|
|
memutils::VProtectGuard guard(union_patch.data_offset_ptr, union_patch.data_len);
|
|
if (active) {
|
|
// apply the selected patch
|
|
memcpy(union_patch.data_offset_ptr, union_patch.data.get(), union_patch.data_len);
|
|
return true;
|
|
} else {
|
|
// restore from original file on disk
|
|
return restore_bytes_from_dll_map_org(
|
|
union_patch.data_offset_ptr,
|
|
union_patch.dll_name,
|
|
union_patch.offset,
|
|
union_patch.data_len);
|
|
}
|
|
}
|
|
case PatchType::Integer: {
|
|
auto& numpatch = patch.patch_number;
|
|
if (cfg::CONFIGURATOR_STANDALONE) {
|
|
// find file from DLL_MAP
|
|
auto dll_file = find_in_dll_map(
|
|
numpatch.dll_name, numpatch.data_offset, numpatch.size_in_bytes);
|
|
if (!dll_file) {
|
|
return false;
|
|
}
|
|
// find offset into file
|
|
if (numpatch.data_offset_ptr == nullptr) {
|
|
numpatch.data_offset_ptr =
|
|
reinterpret_cast<uint8_t*>(numpatch.data_offset + &(*dll_file)[0]);
|
|
}
|
|
} else {
|
|
if (numpatch.data_offset_ptr == nullptr) {
|
|
numpatch.data_offset_ptr = get_dll_offset_for_patch_apply(
|
|
numpatch.dll_name,
|
|
numpatch.data_offset,
|
|
numpatch.size_in_bytes);
|
|
}
|
|
}
|
|
if (numpatch.data_offset_ptr == nullptr) {
|
|
return false;
|
|
}
|
|
memutils::VProtectGuard guard(numpatch.data_offset_ptr, numpatch.size_in_bytes);
|
|
if (active) {
|
|
// apply the selected patch
|
|
int_to_little_endian_bytes(
|
|
numpatch.value, numpatch.data_offset_ptr, numpatch.size_in_bytes);
|
|
return true;
|
|
} else {
|
|
// restore from original file on disk
|
|
return restore_bytes_from_dll_map_org(
|
|
numpatch.data_offset_ptr,
|
|
numpatch.dll_name,
|
|
numpatch.data_offset,
|
|
numpatch.size_in_bytes);
|
|
}
|
|
return false;
|
|
}
|
|
default: {
|
|
|
|
// unknown patch type - fail
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
|
|
MemoryPatch SignaturePatch::to_memory(PatchData *patch) {
|
|
|
|
// check if file exists
|
|
auto dll_path = MODULE_PATH / dll_name;
|
|
if (!fileutils::file_exists(dll_path)) {
|
|
|
|
// file does not exist so that's pretty fatal
|
|
return {.fatal_error = true};
|
|
}
|
|
|
|
// remove spaces
|
|
signature.erase(std::remove(signature.begin(), signature.end(), ' '), signature.end());
|
|
replacement.erase(std::remove(replacement.begin(), replacement.end(), ' '), replacement.end());
|
|
|
|
if (signature.empty() || (signature.length() % 2) != 0
|
|
|| replacement.empty() || (replacement.length() % 2) != 0) {
|
|
return {.fatal_error = true};
|
|
}
|
|
|
|
// build pattern
|
|
std::string pattern_str(signature);
|
|
strreplace(pattern_str, "??", "00");
|
|
strreplace(pattern_str, "XX", "00");
|
|
auto pattern_bin = std::make_unique<uint8_t[]>(signature.length() / 2);
|
|
if (!hex2bin(pattern_str.c_str(), pattern_bin.get())) {
|
|
return {.fatal_error = true};
|
|
}
|
|
|
|
// build signature mask
|
|
std::ostringstream signature_mask;
|
|
for (size_t i = 0; i < signature.length(); i += 2) {
|
|
if (signature[i] == '?' || signature[i] == 'X') {
|
|
if (signature[i + 1] == '?' || signature[i + 1] == 'X') {
|
|
signature_mask << '?';
|
|
} else {
|
|
return {.fatal_error = true};
|
|
}
|
|
} else {
|
|
signature_mask << 'X';
|
|
}
|
|
}
|
|
std::string signature_mask_str = signature_mask.str();
|
|
|
|
// build replace data
|
|
std::string replace_data_str(replacement);
|
|
strreplace(replace_data_str, "??", "00");
|
|
strreplace(replace_data_str, "XX", "00");
|
|
auto replace_data_bin = std::make_unique<uint8_t[]>(replacement.length() / 2);
|
|
if (!hex2bin(replace_data_str.c_str(), replace_data_bin.get())) {
|
|
return {.fatal_error = true};
|
|
}
|
|
|
|
// build replace mask
|
|
std::ostringstream replace_mask;
|
|
for (size_t i = 0; i < replacement.length(); i += 2) {
|
|
if (replacement[i] == '?' || replacement[i] == 'X') {
|
|
if (replacement[i + 1] == '?' || replacement[i + 1] == 'X') {
|
|
replace_mask << '?';
|
|
} else {
|
|
return {.fatal_error = true};
|
|
}
|
|
} else {
|
|
replace_mask << 'X';
|
|
}
|
|
}
|
|
std::string replace_mask_str = replace_mask.str();
|
|
|
|
// replacement applies at signature_match + offset; must stay inside the signature
|
|
const size_t sig_len = signature_mask_str.length();
|
|
const size_t repl_len = replace_mask_str.length();
|
|
if (offset > sig_len || repl_len == 0 || offset + repl_len > sig_len) {
|
|
log_warning("patchmanager",
|
|
"signature patch '{}': offset {} + replacement {} exceeds signature {}",
|
|
patch->name, offset, repl_len, sig_len);
|
|
return {.fatal_error = true};
|
|
}
|
|
|
|
// Locate signature *start* (find_pattern offset arg = 0). JSON "offset" is applied
|
|
// below. Old code passed JSON offset into find_pattern but still indexed
|
|
// signature/replacement from 0 → mis-aligned when offset != 0.
|
|
uint64_t data_offset = 0;
|
|
uint8_t *read_ptr = nullptr;
|
|
HMODULE module = nullptr;
|
|
bool module_free = false;
|
|
|
|
if (cfg::CONFIGURATOR_STANDALONE) {
|
|
|
|
auto it = DLL_MAP.find(dll_name);
|
|
if (it == DLL_MAP.end()) {
|
|
DLL_MAP[dll_name] =
|
|
std::unique_ptr<std::vector<uint8_t>>(
|
|
fileutils::bin_read(dll_path));
|
|
it = DLL_MAP.find(dll_name);
|
|
}
|
|
|
|
// base=0 → file offset of signature start; 0 also means "not found"
|
|
const intptr_t match = find_pattern(
|
|
*it->second, 0, pattern_bin.get(), signature_mask_str.c_str(), 0, usage);
|
|
if (match == 0) {
|
|
return {.fatal_error = true};
|
|
}
|
|
data_offset = static_cast<uint64_t>(match) + offset;
|
|
if (data_offset + repl_len > it->second->size()) {
|
|
return {.fatal_error = true};
|
|
}
|
|
read_ptr = it->second->data() + data_offset;
|
|
|
|
} else {
|
|
|
|
module = libutils::try_module(dll_path);
|
|
if (!module) {
|
|
module = libutils::try_library(dll_path);
|
|
if (module) {
|
|
module_free = true;
|
|
} else {
|
|
return {.fatal_error = true};
|
|
}
|
|
}
|
|
|
|
const intptr_t match_va = find_pattern(
|
|
module, pattern_bin.get(), signature_mask_str.c_str(), 0, usage);
|
|
auto *match_ptr = reinterpret_cast<uint8_t *>(match_va);
|
|
if (match_ptr == nullptr) {
|
|
if (module_free) {
|
|
FreeLibrary(module);
|
|
}
|
|
return {.fatal_error = true};
|
|
}
|
|
|
|
const intptr_t file_off = libutils::rva2offset(
|
|
dll_path, (intptr_t) (match_ptr - (uint8_t *) module));
|
|
if (file_off < 0) {
|
|
if (module_free) {
|
|
FreeLibrary(module);
|
|
}
|
|
return {.fatal_error = true};
|
|
}
|
|
data_offset = static_cast<uint64_t>(file_off) + offset;
|
|
read_ptr = match_ptr + offset;
|
|
}
|
|
|
|
// Build disabled/enabled for the replacement window only.
|
|
std::shared_ptr<uint8_t[]> data_disabled(new uint8_t[repl_len]);
|
|
std::shared_ptr<uint8_t[]> data_enabled(new uint8_t[repl_len]);
|
|
{
|
|
memutils::VProtectGuard data_guard(read_ptr, repl_len);
|
|
for (size_t i = 0; i < repl_len; ++i) {
|
|
const size_t si = static_cast<size_t>(offset) + i;
|
|
if (signature_mask_str[si] != 'X') {
|
|
data_disabled.get()[i] = read_ptr[i];
|
|
} else {
|
|
data_disabled.get()[i] = pattern_bin.get()[si];
|
|
}
|
|
if (replace_mask_str[i] != 'X') {
|
|
data_enabled.get()[i] = read_ptr[i];
|
|
} else {
|
|
data_enabled.get()[i] = replace_data_bin.get()[i];
|
|
}
|
|
}
|
|
}
|
|
|
|
// Drop temporary LoadLibrary mapping only after reads above.
|
|
if (module_free) {
|
|
FreeLibrary(module);
|
|
}
|
|
|
|
log_misc("patchmanager", "found {}: {:#08X}: {} -> {}",
|
|
patch->name, data_offset,
|
|
bin2hex(data_disabled.get(), repl_len),
|
|
bin2hex(data_enabled.get(), repl_len));
|
|
|
|
// BUGFIX: never cache a pointer here.
|
|
// - standalone used to store (uint8_t*)file_offset, so is_patch_active skipped
|
|
// re-resolve and memcmp'd a fake address → "neither on or off".
|
|
// - in-game used to keep a pointer that FreeLibrary may invalidate.
|
|
// is_patch_active / apply_patch re-resolve from data_offset when ptr is nullptr.
|
|
return MemoryPatch {
|
|
.dll_name = dll_name,
|
|
.data_disabled = std::move(data_disabled),
|
|
.data_disabled_len = repl_len,
|
|
.data_enabled = std::move(data_enabled),
|
|
.data_enabled_len = repl_len,
|
|
.data_offset = data_offset,
|
|
.data_offset_ptr = nullptr,
|
|
};
|
|
}
|
|
|
|
std::vector<uint8_t>* find_in_dll_map(
|
|
const std::string& dll_name, size_t offset, size_t size) {
|
|
|
|
auto dlls = DLL_MAP.find(dll_name);
|
|
if (dlls == DLL_MAP.end()) {
|
|
// not found; load DLL into map
|
|
DLL_MAP[dll_name] =
|
|
std::unique_ptr<std::vector<uint8_t>>(fileutils::bin_read(MODULE_PATH / dll_name));
|
|
}
|
|
|
|
// find file
|
|
auto file = DLL_MAP[dll_name].get();
|
|
|
|
// check bounds
|
|
if (file->size() < offset + size) {
|
|
return nullptr;
|
|
}
|
|
|
|
return file;
|
|
}
|
|
|
|
std::vector<uint8_t>* find_in_dll_map_org(
|
|
const std::string& dll_name, size_t offset, size_t size) {
|
|
|
|
auto dlls = DLL_MAP_ORG.find(dll_name);
|
|
if (dlls == DLL_MAP_ORG.end()) {
|
|
// not found; load DLL into map
|
|
DLL_MAP_ORG[dll_name] =
|
|
std::unique_ptr<std::vector<uint8_t>>(fileutils::bin_read(MODULE_PATH / dll_name));
|
|
}
|
|
|
|
// find file
|
|
auto file = DLL_MAP_ORG[dll_name].get();
|
|
if (!file) {
|
|
log_warning("patchmanager", "could not load file into memory: {}", dll_name);
|
|
return nullptr;
|
|
}
|
|
|
|
// check bounds
|
|
if (file->size() < offset + size) {
|
|
return nullptr;
|
|
}
|
|
|
|
return file;
|
|
}
|
|
|
|
static bool get_file_times(
|
|
const std::filesystem::path& path,
|
|
FILETIME* creation,
|
|
FILETIME* access,
|
|
FILETIME* write) {
|
|
|
|
const auto handle = CreateFileW(
|
|
path.c_str(),
|
|
GENERIC_READ,
|
|
FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
|
|
nullptr,
|
|
OPEN_EXISTING,
|
|
FILE_ATTRIBUTE_NORMAL,
|
|
nullptr);
|
|
if (handle == INVALID_HANDLE_VALUE) {
|
|
return false;
|
|
}
|
|
const bool ok = GetFileTime(handle, creation, access, write) != 0;
|
|
CloseHandle(handle);
|
|
return ok;
|
|
}
|
|
|
|
static bool set_file_times(
|
|
const std::filesystem::path& path,
|
|
const FILETIME* creation,
|
|
const FILETIME* access,
|
|
const FILETIME* write) {
|
|
|
|
const auto handle = CreateFileW(
|
|
path.c_str(),
|
|
FILE_WRITE_ATTRIBUTES,
|
|
FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
|
|
nullptr,
|
|
OPEN_EXISTING,
|
|
FILE_ATTRIBUTE_NORMAL,
|
|
nullptr);
|
|
if (handle == INVALID_HANDLE_VALUE) {
|
|
return false;
|
|
}
|
|
const bool ok = SetFileTime(handle, creation, access, write) != 0;
|
|
CloseHandle(handle);
|
|
return ok;
|
|
}
|
|
|
|
void create_dll_backup(
|
|
std::vector<std::filesystem::path>& written_list, const std::filesystem::path& dll_path) {
|
|
|
|
// if dll_path is not in written_list, create a file backup.
|
|
if (std::find(written_list.begin(), written_list.end(), dll_path) == written_list.end()) {
|
|
written_list.push_back(dll_path);
|
|
auto dll_bak_path = dll_path;
|
|
dll_bak_path += ".bak";
|
|
try {
|
|
if (!fileutils::file_exists(dll_bak_path)) {
|
|
FILETIME creation {};
|
|
FILETIME access {};
|
|
FILETIME write {};
|
|
const bool have_times =
|
|
get_file_times(dll_path, &creation, &access, &write);
|
|
if (!have_times) {
|
|
log_warning(
|
|
"patchmanager",
|
|
"could not read timestamps before DLL backup for: {}",
|
|
dll_path);
|
|
}
|
|
|
|
std::filesystem::copy(dll_path, dll_bak_path);
|
|
|
|
if (have_times && !set_file_times(dll_bak_path, &creation, &access, &write)) {
|
|
log_warning(
|
|
"patchmanager",
|
|
"could not restore timestamps on DLL backup for: {}",
|
|
dll_bak_path);
|
|
}
|
|
}
|
|
log_info("patchmanager", "created DLL backup for: {}", dll_path);
|
|
} catch (const std::filesystem::filesystem_error& e) {
|
|
log_warning(
|
|
"patchmanager",
|
|
"filesystem error while creating DLL backup for {}, error: {}",
|
|
dll_path, e.what());
|
|
}
|
|
}
|
|
}
|
|
|
|
std::string fix_up_dll_name(const std::string& dll_name) {
|
|
// IIDX omnimix dll name fix
|
|
if (dll_name == "bm2dx.dll" && avs::game::is_model("LDJ") && avs::game::REV[0] == 'X') {
|
|
return avs::game::DLL_NAME;
|
|
}
|
|
|
|
// BST 1/2 combined release dll name fix
|
|
if (dll_name == "beatstream.dll" &&
|
|
(avs::game::DLL_NAME == "beatstream1.dll" || avs::game::DLL_NAME == "beatstream2.dll")) {
|
|
return avs::game::DLL_NAME;
|
|
}
|
|
|
|
return dll_name;
|
|
}
|
|
|
|
uint8_t* get_dll_offset_for_patch_apply(
|
|
const std::string& dll_name, const uint64_t data_offset, const size_t size_in_bytes) {
|
|
|
|
/// check if file exists
|
|
auto dll_path = MODULE_PATH / dll_name;
|
|
if (!fileutils::file_exists(dll_path)) {
|
|
log_warning("patchmanager", "{} does not exist", dll_path);
|
|
return nullptr;
|
|
}
|
|
|
|
// get module
|
|
auto module = libutils::try_module(dll_path);
|
|
if (!module) {
|
|
log_warning("patchmanager", "cannot get module: {}", dll_path);
|
|
return nullptr;
|
|
}
|
|
|
|
// convert offset to RVA
|
|
auto offset = libutils::offset2rva(dll_path, (intptr_t)data_offset);
|
|
if (offset == -1) {
|
|
log_warning(
|
|
"patchmanager", "cannot convert offset to RVA: {}, {}",
|
|
dll_path, data_offset);
|
|
return nullptr;
|
|
}
|
|
|
|
// get module information
|
|
MODULEINFO module_info{};
|
|
if (!GetModuleInformation(
|
|
GetCurrentProcess(),
|
|
module,
|
|
&module_info,
|
|
sizeof(MODULEINFO))) {
|
|
|
|
log_warning(
|
|
"patchmanager", "GetModuleInformation failed for {}, gle: {}",
|
|
dll_path, GetLastError());
|
|
|
|
return nullptr;
|
|
}
|
|
|
|
// transmute pointer
|
|
auto dll_base = reinterpret_cast<uint8_t *>(module_info.lpBaseOfDll);
|
|
auto dll_image_size = static_cast<uintptr_t>(module_info.SizeOfImage);
|
|
|
|
// check bounds
|
|
auto max_offset = static_cast<uintptr_t>(offset + size_in_bytes);
|
|
if (max_offset >= dll_image_size) {
|
|
log_warning(
|
|
"patchmanager", "invalid offset bounds for {} ({})",
|
|
dll_name, max_offset);
|
|
return nullptr;
|
|
}
|
|
|
|
return &dll_base[offset];
|
|
}
|
|
|
|
int64_t parse_little_endian_int(uint8_t* bytes, size_t size) {
|
|
uint64_t result = 0;
|
|
for (size_t i = 0; i < size; i++) {
|
|
result |= bytes[i] << i * 8;
|
|
}
|
|
return static_cast<int64_t>(result);
|
|
}
|
|
|
|
void int_to_little_endian_bytes(int64_t value, uint8_t* bytes, size_t size) {
|
|
uint64_t v = static_cast<uint64_t>(value);
|
|
for (size_t i = 0; i < size; i++) {
|
|
bytes[i] = (v >> (i * 8)) & 0xff;
|
|
}
|
|
}
|
|
|
|
bool restore_bytes_from_dll_map_org(
|
|
uint8_t* destination, const std::string& dll_name, size_t offset, size_t size) {
|
|
|
|
const auto& orig_file = find_in_dll_map_org(dll_name, offset, size);
|
|
if (!orig_file) {
|
|
return false;
|
|
}
|
|
|
|
memcpy(destination, orig_file->data() + offset, size);
|
|
return true;
|
|
}
|
|
|
|
|
|
void print_auto_apply_status(PatchData &patch) {
|
|
switch (patch.type) {
|
|
case PatchType::Union:
|
|
log_info(
|
|
"patchmanager", "auto apply: {} = {}",
|
|
patch.name, patch.selected_union_name);
|
|
break;
|
|
case PatchType::Integer:
|
|
log_info(
|
|
"patchmanager", "auto apply: {} = {}",
|
|
patch.name, patch.patch_number.value);
|
|
break;
|
|
case PatchType::Memory:
|
|
case PatchType::Signature:
|
|
default:
|
|
log_info("patchmanager", "auto apply: {} = ON", patch.name);
|
|
break;
|
|
}
|
|
}
|
|
}
|