From 770f2190788dacc50ad007a7cff38a1c56db0492 Mon Sep 17 00:00:00 2001 From: Akash Mozumdar Date: Tue, 10 Sep 2019 21:59:59 -0400 Subject: [PATCH] update ppsspp codes and massively improve performance of hook searching --- GUI/host/host.cpp | 10 +++--- GUI/host/host.h | 2 +- GUI/host/util.cpp | 71 ++++++++++++++++++-------------------- GUI/host/util.h | 2 +- GUI/mainwindow.cpp | 29 ++++++++-------- text.cpp | 1 + texthook/engine/engine.cc | 11 ++++-- texthook/engine/match64.cc | 11 ++++-- texthook/hookfinder.cc | 60 ++++++++++++++++++++------------ 9 files changed, 111 insertions(+), 86 deletions(-) diff --git a/GUI/host/host.cpp b/GUI/host/host.cpp index fb7a595..53b749b 100644 --- a/GUI/host/host.cpp +++ b/GUI/host/host.cpp @@ -46,9 +46,9 @@ namespace }).detach(); } - Host::HookEventHandler OnHookFound = [](HookParam hp, const std::wstring& text) + Host::HookEventHandler OnHookFound = [](HookParam hp, std::wstring text) { - Host::AddConsoleOutput(Util::GenerateCode(hp, 0) + L": " + text); + Host::AddConsoleOutput(Util::GenerateCode(hp) + L": " + text); }; private: @@ -107,12 +107,12 @@ namespace auto info = *(HookFoundNotif*)buffer; auto OnHookFound = processRecordsByIds->at(processId).OnHookFound; std::wstring wide = info.text; - if (wide.size() > STRING) OnHookFound(info.hp, info.text); + if (wide.size() > STRING) OnHookFound(info.hp, std::move(info.text)); info.hp.type &= ~USING_UNICODE; if (auto converted = Util::StringToWideString((char*)info.text, info.hp.codepage)) - if (converted->size() > STRING) OnHookFound(info.hp, converted.value()); + if (converted->size() > STRING) OnHookFound(info.hp, std::move(converted.value())); if (auto converted = Util::StringToWideString((char*)info.text, info.hp.codepage = CP_UTF8)) - if (converted->size() > STRING) OnHookFound(info.hp, converted.value()); + if (converted->size() > STRING) OnHookFound(info.hp, std::move(converted.value())); } break; case HOST_NOTIFICATION_RMVHOOK: diff --git a/GUI/host/host.h b/GUI/host/host.h index ea7caa2..1016b8f 100644 --- a/GUI/host/host.h +++ b/GUI/host/host.h @@ -7,7 +7,7 @@ namespace Host { using ProcessEventHandler = std::function; using ThreadEventHandler = std::function; - using HookEventHandler = std::function; + using HookEventHandler = std::function; void Start(ProcessEventHandler Connect, ProcessEventHandler Disconnect, ThreadEventHandler Create, ThreadEventHandler Destroy, TextThread::OutputCallback Output); void InjectProcess(DWORD processId); diff --git a/GUI/host/util.cpp b/GUI/host/util.cpp index a40fc9e..5fd406f 100644 --- a/GUI/host/util.cpp +++ b/GUI/host/util.cpp @@ -166,75 +166,70 @@ namespace return hp; } - std::wstring HexString(int64_t num) // only needed for signed nums + std::wstring HexString(int64_t num) { - return (std::wstringstream() << std::uppercase << std::hex << (num < 0 ? "-" : "") << abs(num)).str(); + if (num < 0) return FormatString(L"-%I64X", -num); + return FormatString(L"%I64X", num); } std::wstring GenerateRCode(HookParam hp) { - std::wstringstream RCode; - RCode << "R"; + std::wstring RCode = L"R"; if (hp.type & USING_UNICODE) { - RCode << "Q"; - if (hp.null_length != 0) RCode << hp.null_length << "<"; + RCode += L'Q'; + if (hp.null_length != 0) RCode += std::to_wstring(hp.null_length) + L'<'; } else { - RCode << "S"; - if (hp.null_length != 0) RCode << hp.null_length << "<"; - if (hp.codepage != 0) RCode << hp.codepage << "#"; + RCode += L'S'; + if (hp.null_length != 0) RCode += std::to_wstring(hp.null_length) + L'<'; + if (hp.codepage != 0) RCode += std::to_wstring(hp.codepage) + L'#'; } - RCode << std::uppercase << std::hex; + RCode += L'@' + HexString(hp.address); - RCode << "@" << hp.address; - - return RCode.str(); + return RCode; } std::wstring GenerateHCode(HookParam hp, DWORD processId) { - std::wstringstream HCode; - HCode << "H"; + std::wstring HCode = L"H"; if (hp.type & USING_UNICODE) { - if (hp.type & USING_STRING) HCode << "Q"; - else HCode << "W"; + if (hp.type & USING_STRING) HCode += L'Q'; + else HCode += L'W'; } else { - if (hp.type & USING_STRING) HCode << "S"; - else if (hp.type & BIG_ENDIAN) HCode << "A"; - else HCode << "B"; + if (hp.type & USING_STRING) HCode += L'S'; + else if (hp.type & BIG_ENDIAN) HCode += L'A'; + else HCode += L'B'; } - if (hp.type & FULL_STRING) HCode << "F"; + if (hp.type & FULL_STRING) HCode += L'F'; - if (hp.null_length != 0) HCode << hp.null_length << "<"; + if (hp.null_length != 0) HCode += std::to_wstring(hp.null_length) + L'<'; - if (hp.type & NO_CONTEXT) HCode << "N"; - if (hp.text_fun || hp.filter_fun || hp.hook_fun || hp.length_fun) HCode << "X"; // no AGTH equivalent + if (hp.type & NO_CONTEXT) HCode += L'N'; + if (hp.text_fun || hp.filter_fun || hp.hook_fun || hp.length_fun) HCode += L'X'; // no AGTH equivalent - if (hp.codepage != 0 && !(hp.type & USING_UNICODE)) HCode << hp.codepage << "#"; + if (hp.codepage != 0 && !(hp.type & USING_UNICODE)) HCode += std::to_wstring(hp.codepage) + L'#'; - HCode << std::uppercase << std::hex; - - if (hp.padding) HCode << hp.padding << "+"; + if (hp.padding) HCode += HexString(hp.padding) + L'+'; if (hp.offset < 0) hp.offset += 4; if (hp.split < 0) hp.split += 4; - HCode << HexString(hp.offset); - if (hp.type & DATA_INDIRECT) HCode << "*" << HexString(hp.index); - if (hp.type & USING_SPLIT) HCode << ":" << HexString(hp.split); - if (hp.type & SPLIT_INDIRECT) HCode << "*" << HexString(hp.split_index); + HCode += HexString(hp.offset); + if (hp.type & DATA_INDIRECT) HCode += L'*' + HexString(hp.index); + if (hp.type & USING_SPLIT) HCode += L':' + HexString(hp.split); + if (hp.type & SPLIT_INDIRECT) HCode += L'*' + HexString(hp.split_index); // Attempt to make the address relative - if (!(hp.type & MODULE_OFFSET)) + if (processId && !(hp.type & MODULE_OFFSET)) if (AutoHandle<> process = OpenProcess(PROCESS_VM_READ | PROCESS_QUERY_INFORMATION, FALSE, processId)) if (MEMORY_BASIC_INFORMATION info = {}; VirtualQueryEx(process, (LPCVOID)hp.address, &info, sizeof(info))) if (auto moduleName = Util::GetModuleFilename(processId, (HMODULE)info.AllocationBase)) @@ -244,11 +239,11 @@ namespace wcsncpy_s(hp.module, moduleName->c_str() + moduleName->rfind(L'\\') + 1, MAX_MODULE_SIZE - 1); } - HCode << "@" << hp.address; - if (hp.type & MODULE_OFFSET) HCode << ":" << hp.module; - if (hp.type & FUNCTION_OFFSET) HCode << ":" << hp.function; + HCode += L'@' + HexString(hp.address); + if (hp.type & MODULE_OFFSET) HCode += L':' + std::wstring(hp.module); + if (hp.type & FUNCTION_OFFSET) HCode += L':' + std::wstring(hp.function, hp.function + MAX_MODULE_SIZE); - return HCode.str(); + return HCode; } } @@ -313,6 +308,8 @@ namespace Util TEST( assert(StringToWideString(u8"こんにちは").value() == L"こんにちは"), + assert(HexString(-12) == L"-C"), + assert(HexString(12) == L"C"), assert(ParseCode(L"/HQN936#-c*C:C*1C@4AA:gdi.dll:GetTextOutA")), assert(ParseCode(L"HB4@0")), assert(ParseCode(L"/RS65001#@44")), diff --git a/GUI/host/util.h b/GUI/host/util.h index 9ea63d6..aa8eaa4 100644 --- a/GUI/host/util.h +++ b/GUI/host/util.h @@ -11,5 +11,5 @@ namespace Util std::optional GetClipboardText(); std::optional StringToWideString(const std::string& text, UINT encoding = CP_UTF8); std::optional ParseCode(std::wstring code); - std::wstring GenerateCode(HookParam hp, DWORD processId); + std::wstring GenerateCode(HookParam hp, DWORD processId = 0); } diff --git a/GUI/mainwindow.cpp b/GUI/mainwindow.cpp index 8a7cc62..0fee557 100644 --- a/GUI/mainwindow.cpp +++ b/GUI/mainwindow.cpp @@ -3,6 +3,7 @@ #include "defs.h" #include "host/util.h" #include +#include #include #include #include @@ -518,44 +519,42 @@ void MainWindow::FindHooks() filter = std::wregex(cjkCheckbox.isChecked() ? L"[\\u3000-\\ua000]{4,}" : L"[\\u0020-\\u1000]{4,}"); } - auto hooks = std::make_shared>(); + auto hooks = std::make_shared(); try { - Host::FindHooks(processId, sp, [hooks, processId, filter](HookParam hp, const std::wstring& text) + Host::FindHooks(processId, sp, [hooks, filter](HookParam hp, const std::wstring& text) { - if (std::regex_search(text, filter)) hooks->push_back(S(Util::GenerateCode(hp, processId) + L" => " + text)); + if (std::regex_search(text, filter)) hooks->push_back(S(Util::GenerateCode(hp) + L" => " + text)); }); } catch (std::out_of_range) { return; } - std::thread([this, hooks, processId] + std::thread([this, hooks] { DWORD64 cleanupTime = GetTickCount64() + 500'000; for (int lastSize = 0; hooks->size() == 0 || hooks->size() != lastSize; Sleep(2000)) if (GetTickCount64() > cleanupTime) return; else lastSize = hooks->size(); - QMetaObject::invokeMethod(this, [this, hooks, processId] + + QMetaObject::invokeMethod(this, [this, hooks] { - auto hookList = new QListWidget(this); + auto hookList = new QListView(this); hookList->setWindowFlags(Qt::Window | Qt::WindowCloseButtonHint); hookList->setAttribute(Qt::WA_DeleteOnClose); hookList->resize({ 750, 300 }); hookList->setWindowTitle(SEARCH_FOR_HOOKS); - for (const auto& hook : *hooks) new QListWidgetItem(hook, hookList); - connect(hookList, &QListWidget::itemClicked, [this](QListWidgetItem* item) { AddHook(item->text().split(" => ")[0]); }); + hookList->setUniformItemSizes(true); + hookList->setModel(new QStringListModel(*hooks, hookList)); + connect(hookList, &QListView::clicked, [this, hookList](QModelIndex i) { AddHook(i.data().toString().split(" => ")[0]); }); hookList->show(); - QString saveFileName = QFileDialog::getSaveFileName(this, SAVE_SEARCH_RESULTS, "./results.txt", TEXT_FILES, nullptr); + QString saveFileName = QFileDialog::getSaveFileName(this, SAVE_SEARCH_RESULTS, "./results.txt", TEXT_FILES); if (!saveFileName.isEmpty()) { QTextFile saveFile(saveFileName, QIODevice::WriteOnly | QIODevice::Truncate); - for (const auto& hook : *hooks) - { - saveFile.write(hook.toUtf8()); - saveFile.write("\n"); - } + for (const auto& hook : *hooks) saveFile.write(hook.toUtf8().append('\n')); // might OOM with .join('\n') } hooks->clear(); - }); + }); }).detach(); } diff --git a/text.cpp b/text.cpp index 5d8f2f5..918c8ab 100644 --- a/text.cpp +++ b/text.cpp @@ -103,6 +103,7 @@ const char* STARTING_SEARCH = u8"Textractor: starting search"; const char* NOT_ENOUGH_TEXT = u8"Textractor: not enough text to search accurately"; const char* HOOK_SEARCH_INITIALIZED = u8"Textractor: search initialized with %zd hooks"; const char* HOOK_SEARCH_FINISHED = u8"Textractor: hook search finished, %d results found"; +const char* OUT_OF_RECORDS_RETRY = u8"Textractor: out of search records, please retry if results are poor (default record count increased)"; const char* FUNC_MISSING = u8"Textractor: function not present"; const char* MODULE_MISSING = u8"Textractor: module not present"; const char* GARBAGE_MEMORY = u8"Textractor: memory constantly changing, useless to read"; diff --git a/texthook/engine/engine.cc b/texthook/engine/engine.cc index 37cfe1f..8fd290c 100644 --- a/texthook/engine/engine.cc +++ b/texthook/engine/engine.cc @@ -16945,12 +16945,19 @@ bool FindPPSSPP() { found = true; ConsoleOutput("Textractor: PPSSPP memory found: searching for hooks should yield working hook codes"); - memcpy(spDefault.pattern, Array{ 0x79, 0x0f, 0xc7, 0x85 }, spDefault.length = 4); + // PPSSPP 1.8.0 compiles jal to sub dword ptr [ebp+0x360],?? + memcpy(spDefault.pattern, Array{ 0x83, 0xAD, 0x60, 0x03, 0x00, 0x00 }, spDefault.length = 6); spDefault.offset = 0; spDefault.minAddress = 0; spDefault.maxAddress = -1ULL; spDefault.padding = (uintptr_t)probe - 0x8000000; - spDefault.hookPostProcessor = [](HookParam& hp) { hp.type |= NO_CONTEXT; }; + spDefault.maxRecords = 500'000; + spDefault.hookPostProcessor = [](HookParam& hp) + { + hp.type |= NO_CONTEXT | USING_SPLIT | SPLIT_INDIRECT; + hp.split = pusha_ebp_off - 4; + hp.split_index = -8; // this is where PPSSPP 1.8.0 stores its return address stack + }; } probe += info.RegionSize; } diff --git a/texthook/engine/match64.cc b/texthook/engine/match64.cc index e9787fb..9af4c82 100644 --- a/texthook/engine/match64.cc +++ b/texthook/engine/match64.cc @@ -33,12 +33,19 @@ namespace Engine { found = true; ConsoleOutput("Textractor: PPSSPP memory found: searching for hooks should yield working hook codes"); - memcpy(spDefault.pattern, Array{ 0x79, 0x10, 0x41, 0xc7 }, spDefault.length = 4); + // PPSSPP 1.8.0 compiles jal to sub dword ptr [r14+0x360],?? + memcpy(spDefault.pattern, Array{ 0x41, 0x83, 0xae, 0x60, 0x03, 0x00, 0x00 }, spDefault.length = 7); spDefault.offset = 0; spDefault.minAddress = 0; spDefault.maxAddress = -1ULL; spDefault.padding = (uintptr_t)probe - 0x8000000; - spDefault.hookPostProcessor = [](HookParam& hp) { hp.type |= NO_CONTEXT; }; + spDefault.maxRecords = 500'000; + spDefault.hookPostProcessor = [](HookParam& hp) + { + hp.type |= NO_CONTEXT | USING_SPLIT | SPLIT_INDIRECT; + hp.split = -0x80; // r14 + hp.split_index = -8; // this is where PPSSPP 1.8.0 stores its return address stack + }; } probe += info.RegionSize; } diff --git a/texthook/hookfinder.cc b/texthook/hookfinder.cc index 7299254..82722ee 100644 --- a/texthook/hookfinder.cc +++ b/texthook/hookfinder.cc @@ -6,6 +6,7 @@ extern const char* STARTING_SEARCH; extern const char* HOOK_SEARCH_INITIALIZED; extern const char* HOOK_SEARCH_FINISHED; +extern const char* OUT_OF_RECORDS_RETRY; extern const char* NOT_ENOUGH_TEXT; extern const char* COULD_NOT_FIND; @@ -15,7 +16,7 @@ namespace { SearchParam sp; - constexpr int MAX_STRING_SIZE = 500, CACHE_SIZE = 300'000; + constexpr int MAX_STRING_SIZE = 500, CACHE_SIZE = 0x40000, GOOD_PAGE = -1; struct HookRecord { ~HookRecord() @@ -38,6 +39,7 @@ namespace long recordsAvailable; uint64_t signatureCache[CACHE_SIZE] = {}; long sumCache[CACHE_SIZE] = {}; + uintptr_t pageCache[CACHE_SIZE] = {}; #ifndef _WIN64 BYTE trampoline[] = @@ -114,16 +116,30 @@ namespace #endif } -bool IsBadStrPtr(void* str) +bool IsBadReadPtr(void* data) { - if (str < (void*)0x1000) return true; + if (data > records.get() && data < records.get() + sp.maxRecords) return true; + uintptr_t BAD_PAGE = (uintptr_t)data >> 12; + auto& cacheEntry = pageCache[BAD_PAGE % CACHE_SIZE]; + if (cacheEntry == BAD_PAGE) return true; + if (cacheEntry == GOOD_PAGE) return false; - MEMORY_BASIC_INFORMATION info; - if (VirtualQuery(str, &info, sizeof(info)) == 0 || info.Protect < PAGE_READONLY || info.Protect & (PAGE_GUARD | PAGE_NOACCESS)) return true; - - void* regionEnd = (BYTE*)info.BaseAddress + info.RegionSize; - if ((BYTE*)str + MAX_STRING_SIZE <= regionEnd) return false; - return IsBadStrPtr(regionEnd); + __try + { + volatile char _ = *(char*)data; + cacheEntry = GOOD_PAGE; + } + __except (EXCEPTION_EXECUTE_HANDLER) + { + if (GetExceptionCode() == EXCEPTION_GUARD_PAGE) + { + MEMORY_BASIC_INFORMATION info; + VirtualQuery(data, &info, sizeof(info)); + VirtualProtect(data, 1, info.Protect | PAGE_GUARD, DUMMY); + } + cacheEntry = BAD_PAGE; + } + return cacheEntry == BAD_PAGE; } void Send(char** stack, uintptr_t address) @@ -133,14 +149,13 @@ void Send(char** stack, uintptr_t address) if (recordsAvailable <= 0) return; for (int i = -registers; i < 10; ++i) { - int length = 0, sum = 0; char* str = stack[i] + sp.padding; - if (IsBadStrPtr(str)) return; // seems to improve performance; TODO: more tests and benchmarks to confirm - __try { for (; (str[length] || str[length + 1]) && length < MAX_STRING_SIZE; length += 2) sum += str[length] + str[length + 1]; } - __except (EXCEPTION_EXECUTE_HANDLER) {} - if (length > STRING && length < MAX_STRING_SIZE - 1) + if (IsBadReadPtr(str) || IsBadReadPtr(str + MAX_STRING_SIZE)) continue; + __try { - __try + int length = 0, sum = 0; + for (; (str[length] || str[length + 1]) && length < MAX_STRING_SIZE; length += 2) sum += *(uint16_t*)(str + length); + if (length > STRING && length < MAX_STRING_SIZE - 1) { // many duplicate results with same address, offset, and third/fourth character will be found: filter them out uint64_t signature = ((uint64_t)i << 56) | ((uint64_t)(str[2] + str[3]) << 48) | address; @@ -149,12 +164,7 @@ void Send(char** stack, uintptr_t address) // if there are huge amount of strings that are the same, it's probably garbage: filter them out // can't store all the strings, so use sum as heuristic instead if (_InterlockedIncrement(sumCache + (sum % CACHE_SIZE)) > 25) continue; - } - __except (EXCEPTION_EXECUTE_HANDLER) {} - - long n = _InterlockedDecrement(&recordsAvailable); - __try - { + long n = _InterlockedDecrement(&recordsAvailable); if (n > 0) { records[n].address = address; @@ -162,10 +172,14 @@ void Send(char** stack, uintptr_t address) for (int j = 0; j < length; ++j) records[n].text[j] = str[j]; records[n].text[length] = 0; } + if (n == 0) + { + spDefault.maxRecords = sp.maxRecords * 2; + ConsoleOutput(OUT_OF_RECORDS_RETRY); + } } - __except (EXCEPTION_EXECUTE_HANDLER) { records[n].address = 0; } - } + __except (EXCEPTION_EXECUTE_HANDLER) {} } }