forked from Public-Mirror/Textractor
update ppsspp codes and massively improve performance of hook searching
This commit is contained in:
parent
0c9fc5c08a
commit
770f219078
@ -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:
|
||||
|
@ -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);
|
||||
|
@ -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")),
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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();
|
||||
});
|
||||
|
1
text.cpp
1
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";
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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) {}
|
||||
}
|
||||
}
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user