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();
}
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:

View File

@ -7,7 +7,7 @@ namespace Host
{
using ProcessEventHandler = std::function<void(DWORD)>;
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 InjectProcess(DWORD processId);

View File

@ -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")),

View File

@ -11,5 +11,5 @@ namespace Util
std::optional<std::wstring> GetClipboardText();
std::optional<std::wstring> StringToWideString(const std::string& text, UINT encoding = CP_UTF8);
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 "host/util.h"
#include <shellapi.h>
#include <QStringListModel>
#include <QMenu>
#include <QDialogButtonBox>
#include <QFileDialog>
@ -518,41 +519,39 @@ void MainWindow::FindHooks()
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
{
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();
});

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* 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";

View File

@ -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<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.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;
}

View File

@ -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<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.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;
}

View File

@ -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) {}
}
}