update ppsspp codes and massively improve performance of hook searching

This commit is contained in:
Akash Mozumdar 2019-09-10 21:59:59 -04:00
parent 0c9fc5c08a
commit 770f219078
9 changed files with 111 additions and 86 deletions

View File

@ -46,9 +46,9 @@ namespace
}).detach(); }).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: private:
@ -107,12 +107,12 @@ namespace
auto info = *(HookFoundNotif*)buffer; auto info = *(HookFoundNotif*)buffer;
auto OnHookFound = processRecordsByIds->at(processId).OnHookFound; auto OnHookFound = processRecordsByIds->at(processId).OnHookFound;
std::wstring wide = info.text; 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; info.hp.type &= ~USING_UNICODE;
if (auto converted = Util::StringToWideString((char*)info.text, info.hp.codepage)) 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 (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; break;
case HOST_NOTIFICATION_RMVHOOK: case HOST_NOTIFICATION_RMVHOOK:

View File

@ -7,7 +7,7 @@ namespace Host
{ {
using ProcessEventHandler = std::function<void(DWORD)>; using ProcessEventHandler = std::function<void(DWORD)>;
using ThreadEventHandler = std::function<void(TextThread&)>; using ThreadEventHandler = std::function<void(TextThread&)>;
using HookEventHandler = std::function<void(HookParam, const std::wstring& text)>; using HookEventHandler = std::function<void(HookParam, std::wstring text)>;
void Start(ProcessEventHandler Connect, ProcessEventHandler Disconnect, ThreadEventHandler Create, ThreadEventHandler Destroy, TextThread::OutputCallback Output); void Start(ProcessEventHandler Connect, ProcessEventHandler Disconnect, ThreadEventHandler Create, ThreadEventHandler Destroy, TextThread::OutputCallback Output);
void InjectProcess(DWORD processId); void InjectProcess(DWORD processId);

View File

@ -166,75 +166,70 @@ namespace
return hp; 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::wstring GenerateRCode(HookParam hp)
{ {
std::wstringstream RCode; std::wstring RCode = L"R";
RCode << "R";
if (hp.type & USING_UNICODE) if (hp.type & USING_UNICODE)
{ {
RCode << "Q"; RCode += L'Q';
if (hp.null_length != 0) RCode << hp.null_length << "<"; if (hp.null_length != 0) RCode += std::to_wstring(hp.null_length) + L'<';
} }
else else
{ {
RCode << "S"; RCode += L'S';
if (hp.null_length != 0) RCode << hp.null_length << "<"; if (hp.null_length != 0) RCode += std::to_wstring(hp.null_length) + L'<';
if (hp.codepage != 0) RCode << hp.codepage << "#"; 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;
return RCode.str();
} }
std::wstring GenerateHCode(HookParam hp, DWORD processId) std::wstring GenerateHCode(HookParam hp, DWORD processId)
{ {
std::wstringstream HCode; std::wstring HCode = L"H";
HCode << "H";
if (hp.type & USING_UNICODE) if (hp.type & USING_UNICODE)
{ {
if (hp.type & USING_STRING) HCode << "Q"; if (hp.type & USING_STRING) HCode += L'Q';
else HCode << "W"; else HCode += L'W';
} }
else else
{ {
if (hp.type & USING_STRING) HCode << "S"; if (hp.type & USING_STRING) HCode += L'S';
else if (hp.type & BIG_ENDIAN) HCode << "A"; else if (hp.type & BIG_ENDIAN) HCode += L'A';
else HCode << "B"; 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.type & NO_CONTEXT) HCode += L'N';
if (hp.text_fun || hp.filter_fun || hp.hook_fun || hp.length_fun) HCode << "X"; // no AGTH equivalent 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 += HexString(hp.padding) + L'+';
if (hp.padding) HCode << hp.padding << "+";
if (hp.offset < 0) hp.offset += 4; if (hp.offset < 0) hp.offset += 4;
if (hp.split < 0) hp.split += 4; if (hp.split < 0) hp.split += 4;
HCode << HexString(hp.offset); HCode += HexString(hp.offset);
if (hp.type & DATA_INDIRECT) HCode << "*" << HexString(hp.index); if (hp.type & DATA_INDIRECT) HCode += L'*' + HexString(hp.index);
if (hp.type & USING_SPLIT) HCode << ":" << HexString(hp.split); if (hp.type & USING_SPLIT) HCode += L':' + HexString(hp.split);
if (hp.type & SPLIT_INDIRECT) HCode << "*" << HexString(hp.split_index); if (hp.type & SPLIT_INDIRECT) HCode += L'*' + HexString(hp.split_index);
// Attempt to make the address relative // 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 (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 (MEMORY_BASIC_INFORMATION info = {}; VirtualQueryEx(process, (LPCVOID)hp.address, &info, sizeof(info)))
if (auto moduleName = Util::GetModuleFilename(processId, (HMODULE)info.AllocationBase)) 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); wcsncpy_s(hp.module, moduleName->c_str() + moduleName->rfind(L'\\') + 1, MAX_MODULE_SIZE - 1);
} }
HCode << "@" << hp.address; HCode += L'@' + HexString(hp.address);
if (hp.type & MODULE_OFFSET) HCode << ":" << hp.module; if (hp.type & MODULE_OFFSET) HCode += L':' + std::wstring(hp.module);
if (hp.type & FUNCTION_OFFSET) HCode << ":" << hp.function; 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( TEST(
assert(StringToWideString(u8"こんにちは").value() == L"こんにちは"), 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"/HQN936#-c*C:C*1C@4AA:gdi.dll:GetTextOutA")),
assert(ParseCode(L"HB4@0")), assert(ParseCode(L"HB4@0")),
assert(ParseCode(L"/RS65001#@44")), assert(ParseCode(L"/RS65001#@44")),

View File

@ -11,5 +11,5 @@ namespace Util
std::optional<std::wstring> GetClipboardText(); std::optional<std::wstring> GetClipboardText();
std::optional<std::wstring> StringToWideString(const std::string& text, UINT encoding = CP_UTF8); std::optional<std::wstring> StringToWideString(const std::string& text, UINT encoding = CP_UTF8);
std::optional<HookParam> ParseCode(std::wstring code); std::optional<HookParam> ParseCode(std::wstring code);
std::wstring GenerateCode(HookParam hp, DWORD processId); std::wstring GenerateCode(HookParam hp, DWORD processId = 0);
} }

View File

@ -3,6 +3,7 @@
#include "defs.h" #include "defs.h"
#include "host/util.h" #include "host/util.h"
#include <shellapi.h> #include <shellapi.h>
#include <QStringListModel>
#include <QMenu> #include <QMenu>
#include <QDialogButtonBox> #include <QDialogButtonBox>
#include <QFileDialog> #include <QFileDialog>
@ -518,44 +519,42 @@ void MainWindow::FindHooks()
filter = std::wregex(cjkCheckbox.isChecked() ? L"[\\u3000-\\ua000]{4,}" : L"[\\u0020-\\u1000]{4,}"); filter = std::wregex(cjkCheckbox.isChecked() ? L"[\\u3000-\\ua000]{4,}" : L"[\\u0020-\\u1000]{4,}");
} }
auto hooks = std::make_shared<std::vector<QString>>(); auto hooks = std::make_shared<QStringList>();
try 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; } catch (std::out_of_range) { return; }
std::thread([this, hooks, processId] std::thread([this, hooks]
{ {
DWORD64 cleanupTime = GetTickCount64() + 500'000; DWORD64 cleanupTime = GetTickCount64() + 500'000;
for (int lastSize = 0; hooks->size() == 0 || hooks->size() != lastSize; Sleep(2000)) for (int lastSize = 0; hooks->size() == 0 || hooks->size() != lastSize; Sleep(2000))
if (GetTickCount64() > cleanupTime) return; if (GetTickCount64() > cleanupTime) return;
else lastSize = hooks->size(); 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->setWindowFlags(Qt::Window | Qt::WindowCloseButtonHint);
hookList->setAttribute(Qt::WA_DeleteOnClose); hookList->setAttribute(Qt::WA_DeleteOnClose);
hookList->resize({ 750, 300 }); hookList->resize({ 750, 300 });
hookList->setWindowTitle(SEARCH_FOR_HOOKS); hookList->setWindowTitle(SEARCH_FOR_HOOKS);
for (const auto& hook : *hooks) new QListWidgetItem(hook, hookList); hookList->setUniformItemSizes(true);
connect(hookList, &QListWidget::itemClicked, [this](QListWidgetItem* item) { AddHook(item->text().split(" => ")[0]); }); hookList->setModel(new QStringListModel(*hooks, hookList));
connect(hookList, &QListView::clicked, [this, hookList](QModelIndex i) { AddHook(i.data().toString().split(" => ")[0]); });
hookList->show(); 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()) if (!saveFileName.isEmpty())
{ {
QTextFile saveFile(saveFileName, QIODevice::WriteOnly | QIODevice::Truncate); QTextFile saveFile(saveFileName, QIODevice::WriteOnly | QIODevice::Truncate);
for (const auto& hook : *hooks) for (const auto& hook : *hooks) saveFile.write(hook.toUtf8().append('\n')); // might OOM with .join('\n')
{
saveFile.write(hook.toUtf8());
saveFile.write("\n");
}
} }
hooks->clear(); hooks->clear();
}); });
}).detach(); }).detach();
} }

View File

@ -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* 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_INITIALIZED = u8"Textractor: search initialized with %zd hooks";
const char* HOOK_SEARCH_FINISHED = u8"Textractor: hook search finished, %d results found"; 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* FUNC_MISSING = u8"Textractor: function not present";
const char* MODULE_MISSING = u8"Textractor: module not present"; const char* MODULE_MISSING = u8"Textractor: module not present";
const char* GARBAGE_MEMORY = u8"Textractor: memory constantly changing, useless to read"; const char* GARBAGE_MEMORY = u8"Textractor: memory constantly changing, useless to read";

View File

@ -16945,12 +16945,19 @@ bool FindPPSSPP()
{ {
found = true; found = true;
ConsoleOutput("Textractor: PPSSPP memory found: searching for hooks should yield working hook codes"); ConsoleOutput("Textractor: PPSSPP memory found: searching for hooks should yield working hook codes");
memcpy(spDefault.pattern, Array<BYTE>{ 0x79, 0x0f, 0xc7, 0x85 }, spDefault.length = 4); // PPSSPP 1.8.0 compiles jal to sub dword ptr [ebp+0x360],??
memcpy(spDefault.pattern, Array<BYTE>{ 0x83, 0xAD, 0x60, 0x03, 0x00, 0x00 }, spDefault.length = 6);
spDefault.offset = 0; spDefault.offset = 0;
spDefault.minAddress = 0; spDefault.minAddress = 0;
spDefault.maxAddress = -1ULL; spDefault.maxAddress = -1ULL;
spDefault.padding = (uintptr_t)probe - 0x8000000; 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; probe += info.RegionSize;
} }

View File

@ -33,12 +33,19 @@ namespace Engine
{ {
found = true; found = true;
ConsoleOutput("Textractor: PPSSPP memory found: searching for hooks should yield working hook codes"); ConsoleOutput("Textractor: PPSSPP memory found: searching for hooks should yield working hook codes");
memcpy(spDefault.pattern, Array<BYTE>{ 0x79, 0x10, 0x41, 0xc7 }, spDefault.length = 4); // PPSSPP 1.8.0 compiles jal to sub dword ptr [r14+0x360],??
memcpy(spDefault.pattern, Array<BYTE>{ 0x41, 0x83, 0xae, 0x60, 0x03, 0x00, 0x00 }, spDefault.length = 7);
spDefault.offset = 0; spDefault.offset = 0;
spDefault.minAddress = 0; spDefault.minAddress = 0;
spDefault.maxAddress = -1ULL; spDefault.maxAddress = -1ULL;
spDefault.padding = (uintptr_t)probe - 0x8000000; 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; probe += info.RegionSize;
} }

View File

@ -6,6 +6,7 @@
extern const char* STARTING_SEARCH; extern const char* STARTING_SEARCH;
extern const char* HOOK_SEARCH_INITIALIZED; extern const char* HOOK_SEARCH_INITIALIZED;
extern const char* HOOK_SEARCH_FINISHED; extern const char* HOOK_SEARCH_FINISHED;
extern const char* OUT_OF_RECORDS_RETRY;
extern const char* NOT_ENOUGH_TEXT; extern const char* NOT_ENOUGH_TEXT;
extern const char* COULD_NOT_FIND; extern const char* COULD_NOT_FIND;
@ -15,7 +16,7 @@ namespace
{ {
SearchParam sp; 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 struct HookRecord
{ {
~HookRecord() ~HookRecord()
@ -38,6 +39,7 @@ namespace
long recordsAvailable; long recordsAvailable;
uint64_t signatureCache[CACHE_SIZE] = {}; uint64_t signatureCache[CACHE_SIZE] = {};
long sumCache[CACHE_SIZE] = {}; long sumCache[CACHE_SIZE] = {};
uintptr_t pageCache[CACHE_SIZE] = {};
#ifndef _WIN64 #ifndef _WIN64
BYTE trampoline[] = BYTE trampoline[] =
@ -114,16 +116,30 @@ namespace
#endif #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; __try
if (VirtualQuery(str, &info, sizeof(info)) == 0 || info.Protect < PAGE_READONLY || info.Protect & (PAGE_GUARD | PAGE_NOACCESS)) return true; {
volatile char _ = *(char*)data;
void* regionEnd = (BYTE*)info.BaseAddress + info.RegionSize; cacheEntry = GOOD_PAGE;
if ((BYTE*)str + MAX_STRING_SIZE <= regionEnd) return false; }
return IsBadStrPtr(regionEnd); __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) void Send(char** stack, uintptr_t address)
@ -133,14 +149,13 @@ void Send(char** stack, uintptr_t address)
if (recordsAvailable <= 0) return; if (recordsAvailable <= 0) return;
for (int i = -registers; i < 10; ++i) for (int i = -registers; i < 10; ++i)
{ {
int length = 0, sum = 0;
char* str = stack[i] + sp.padding; char* str = stack[i] + sp.padding;
if (IsBadStrPtr(str)) return; // seems to improve performance; TODO: more tests and benchmarks to confirm if (IsBadReadPtr(str) || IsBadReadPtr(str + MAX_STRING_SIZE)) continue;
__try { for (; (str[length] || str[length + 1]) && length < MAX_STRING_SIZE; length += 2) sum += str[length] + str[length + 1]; } __try
__except (EXCEPTION_EXECUTE_HANDLER) {}
if (length > STRING && length < MAX_STRING_SIZE - 1)
{ {
__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 // 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; 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 // 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 // can't store all the strings, so use sum as heuristic instead
if (_InterlockedIncrement(sumCache + (sum % CACHE_SIZE)) > 25) continue; if (_InterlockedIncrement(sumCache + (sum % CACHE_SIZE)) > 25) continue;
} long n = _InterlockedDecrement(&recordsAvailable);
__except (EXCEPTION_EXECUTE_HANDLER) {}
long n = _InterlockedDecrement(&recordsAvailable);
__try
{
if (n > 0) if (n > 0)
{ {
records[n].address = address; 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]; for (int j = 0; j < length; ++j) records[n].text[j] = str[j];
records[n].text[length] = 0; 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) {}
} }
} }