From ef90382bbb9af5680d33bafb39098c8b04d69873 Mon Sep 17 00:00:00 2001 From: Akash Mozumdar Date: Sat, 15 Jun 2019 19:38:44 -0400 Subject: [PATCH 1/6] fixed hookstrlen seeing ascii char+unicode space as null terminator --- test/main.cpp | 2 +- texthook/texthook.cc | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/test/main.cpp b/test/main.cpp index 66b88a9..7fa2f31 100644 --- a/test/main.cpp +++ b/test/main.cpp @@ -43,7 +43,7 @@ int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE, LPSTR, int) return FALSE; }, 0), SW_SHOW); - std::thread([] { while (true) Sleep(vars.at(0)), lstrlenW(L"こんにちは"); }).detach(); + std::thread([] { while (true) Sleep(vars.at(0)), lstrlenW(L"こんにちは\n (Hello)"); }).detach(); STARTUPINFOW info = { sizeof(info) }; wchar_t commandLine[] = { L"Textractor -p\"Test.exe\"" }; diff --git a/texthook/texthook.cc b/texthook/texthook.cc index c261062..2f70638 100644 --- a/texthook/texthook.cc +++ b/texthook/texthook.cc @@ -327,11 +327,11 @@ int TextHook::GetLength(uintptr_t base, uintptr_t in) int TextHook::HookStrlen(BYTE* data) { + if (!hp.null_length) return hp.type & USING_UNICODE ? wcslen((wchar_t*)data) * 2 : strlen((char*)data); BYTE* orig = data; - int nulls = hp.null_length ? hp.null_length : hp.type & USING_UNICODE ? 2 : 1; - for (int nullsRemaining = nulls; nullsRemaining > 0; ++data) + for (int nullsRemaining = hp.null_length; nullsRemaining > 0; ++data) if (*data == 0) nullsRemaining -= 1; - else nullsRemaining = nulls; + else nullsRemaining = hp.null_length; return data - orig; } From 88b797cd3379f0fa34e8db61b1e01ee296c2b2c9 Mon Sep 17 00:00:00 2001 From: Akash Mozumdar Date: Sun, 16 Jun 2019 15:28:59 -0400 Subject: [PATCH 2/6] use a default searchparam unless user specifies they want custom settings --- GUI/host/host.cpp | 2 +- GUI/mainwindow.cpp | 230 +++++++++++++++++++------------------ include/types.h | 5 +- text.cpp | 3 +- texthook/engine/engine.cc | 10 +- texthook/engine/match.cc | 33 +++--- texthook/engine/match64.cc | 10 +- texthook/hookfinder.cc | 11 +- texthook/main.cc | 2 +- texthook/main.h | 9 ++ 10 files changed, 174 insertions(+), 141 deletions(-) diff --git a/GUI/host/host.cpp b/GUI/host/host.cpp index 303bb45..00985c6 100644 --- a/GUI/host/host.cpp +++ b/GUI/host/host.cpp @@ -105,7 +105,7 @@ namespace auto& OnHookFound = processRecordsByIds->at(processId).OnHookFound; std::wstring wide = info.text; if (wide.size() > STRING) OnHookFound(info.hp, info.text); - info.hp.type = USING_STRING; + info.hp.type &= ~USING_UNICODE; if (auto converted = Util::StringToWideString((char*)info.text, Host::defaultCodepage)) if (converted->size() > STRING) OnHookFound(info.hp, converted.value()); info.hp.codepage = CP_UTF8; diff --git a/GUI/mainwindow.cpp b/GUI/mainwindow.cpp index f8119b4..bc90ca6 100644 --- a/GUI/mainwindow.cpp +++ b/GUI/mainwindow.cpp @@ -11,6 +11,7 @@ #include #include #include +#include #include #include #include @@ -21,7 +22,7 @@ extern const char* DETACH; extern const char* ADD_HOOK; extern const char* REMOVE_HOOKS; extern const char* SAVE_HOOKS; -extern const char* FIND_HOOKS; +extern const char* SEARCH_FOR_HOOKS; extern const char* SETTINGS; extern const char* EXTENSIONS; extern const char* SELECT_PROCESS; @@ -30,6 +31,7 @@ extern const char* SEARCH_GAME; extern const char* PROCESSES; extern const char* CODE_INFODUMP; extern const char* HOOK_SEARCH_UNSTABLE_WARNING; +extern const char* SEARCH_CJK; extern const char* SEARCH_PATTERN; extern const char* SEARCH_DURATION; extern const char* PATTERN_OFFSET; @@ -59,14 +61,14 @@ MainWindow::MainWindow(QWidget *parent) : extenWindow(new ExtenWindow(this)) { ui->setupUi(this); - for (auto[text, slot] : Array>{ + for (auto [text, slot] : Array>{ { ATTACH, &MainWindow::AttachProcess }, { LAUNCH, &MainWindow::LaunchProcess }, { DETACH, &MainWindow::DetachProcess }, { ADD_HOOK, &MainWindow::AddHook }, { REMOVE_HOOKS, &MainWindow::RemoveHooks }, { SAVE_HOOKS, &MainWindow::SaveHooks }, - { FIND_HOOKS, &MainWindow::FindHooks }, + { SEARCH_FOR_HOOKS, &MainWindow::FindHooks }, { SETTINGS, &MainWindow::Settings }, { EXTENSIONS, &MainWindow::Extensions } }) @@ -325,9 +327,10 @@ void MainWindow::RemoveHooks() } auto hookList = new QListWidget(this); hookList->setWindowFlags(Qt::Window | Qt::WindowCloseButtonHint); + hookList->setAttribute(Qt::WA_DeleteOnClose); hookList->setMinimumSize({ 300, 50 }); hookList->setWindowTitle(DOUBLE_CLICK_TO_REMOVE_HOOK); - for (auto[address, hp] : hooks) + for (auto [address, hp] : hooks) new QListWidgetItem(QString(hp.name) + "@" + QString::number(address, 16), hookList); connect(hookList, &QListWidget::itemDoubleClicked, [processId, hookList](QListWidgetItem* item) { @@ -364,120 +367,123 @@ void MainWindow::SaveHooks() void MainWindow::FindHooks() { - QMessageBox::information(this, FIND_HOOKS, HOOK_SEARCH_UNSTABLE_WARNING); - struct : QDialog - { - using QDialog::QDialog; - void launch() - { - auto layout = new QFormLayout(this); - auto patternInput = new QLineEdit(x64 ? "CC CC 48 89" : "CC CC 55 8B EC", this); - layout->addRow(SEARCH_PATTERN, patternInput); - for (auto[value, label] : Array>{ - { sp.searchTime = 20000, SEARCH_DURATION }, - { sp.offset = 2, PATTERN_OFFSET }, - }) - { - auto spinBox = new QSpinBox(this); - spinBox->setMaximum(INT_MAX); - spinBox->setValue(value); - layout->addRow(label, spinBox); - connect(spinBox, qOverload(&QSpinBox::valueChanged), [=, &value] { value = spinBox->value(); }); - } - for (auto[value, label] : Array>{ - { sp.minAddress = 0, MIN_ADDRESS }, - { sp.maxAddress = -1ULL, MAX_ADDRESS }, - { sp.padding = 0, STRING_OFFSET } - }) - { - auto input = new QLineEdit(QString::number(value, 16), this); - layout->addRow(label, input); - connect(input, &QLineEdit::textEdited, [&value](QString input) - { - bool ok; - if (uintptr_t newValue = input.toULongLong(&ok, 16); ok) value = newValue; - }); - } - auto filterInput = new QLineEdit(this); - layout->addRow(HOOK_SEARCH_FILTER, filterInput); - auto save = new QPushButton(START_HOOK_SEARCH, this); - layout->addWidget(save); - connect(save, &QPushButton::clicked, this, &QDialog::accept); - connect(save, &QPushButton::clicked, [this, patternInput, filterInput] - { - QByteArray pattern = QByteArray::fromHex(patternInput->text().replace("??", QString::number(XX, 16)).toUtf8()); - if (pattern.size() < 3) return; - std::wregex filter(L"."); - if (!filterInput->text().isEmpty()) try { filter = std::wregex(S(filterInput->text())); } catch (std::regex_error) {}; - memcpy(sp.pattern, pattern.data(), sp.length = min(pattern.size(), 25)); - auto hooks = std::make_shared(); - DWORD processId = this->processId; - try - { - Host::FindHooks(processId, sp, [processId, hooks, filter](HookParam hp, const std::wstring& text) - { - if (std::regex_search(text, filter)) hooks->append(S(Util::GenerateCode(hp, processId)) + ": " + S(text) + "\n"); - }); - } - catch (std::out_of_range) { return; } - QString fileName = QFileDialog::getSaveFileName(this, SAVE_SEARCH_RESULTS, "./Hooks.txt", TEXT_FILES); - if (fileName.isEmpty()) fileName = "Hooks.txt"; - std::thread([hooks, fileName] - { - for (int lastSize = 0; hooks->size() == 0 || hooks->size() != lastSize; Sleep(2000)) lastSize = hooks->size(); - QTextFile(fileName, QIODevice::WriteOnly | QIODevice::Truncate).write(hooks->toUtf8()); - hooks->clear(); - }).detach(); - }); - setWindowTitle(FIND_HOOKS); - exec(); - } + QMessageBox::information(this, SEARCH_FOR_HOOKS, HOOK_SEARCH_UNSTABLE_WARNING); - SearchParam sp = {}; - DWORD processId; - } searchDialog(this, Qt::WindowCloseButtonHint); - searchDialog.processId = GetSelectedProcessId(); - searchDialog.launch(); + DWORD processId = GetSelectedProcessId(); + SearchParam sp = {}; + bool customSettings = false; + std::wregex filter(L"."); + + QDialog dialog(this, Qt::WindowCloseButtonHint); + QFormLayout layout(&dialog); + QCheckBox cjkCheckbox(&dialog); + layout.addRow(SEARCH_CJK, &cjkCheckbox); + QDialogButtonBox confirm(QDialogButtonBox::Ok | QDialogButtonBox::Help, &dialog); + layout.addRow(&confirm); + confirm.button(QDialogButtonBox::Ok)->setText(START_HOOK_SEARCH); + confirm.button(QDialogButtonBox::Help)->setText(SETTINGS); + connect(&confirm, &QDialogButtonBox::helpRequested, [&customSettings] { customSettings = true; }); + connect(&confirm, &QDialogButtonBox::accepted, &dialog, &QDialog::accept); + connect(&confirm, &QDialogButtonBox::helpRequested, &dialog, &QDialog::accept); + dialog.setWindowTitle(SEARCH_FOR_HOOKS); + if (dialog.exec() == QDialog::Rejected) return; + + if (customSettings) + { + QDialog dialog(this, Qt::WindowCloseButtonHint); + QFormLayout layout(&dialog); + QLineEdit patternInput(x64 ? "CC CC 48 89" : "CC CC 55 8B EC", &dialog); + layout.addRow(SEARCH_PATTERN, &patternInput); + for (auto [value, label] : Array>{ + { sp.searchTime = 20000, SEARCH_DURATION }, + { sp.offset = 2, PATTERN_OFFSET }, + }) + { + auto spinBox = new QSpinBox(&dialog); + spinBox->setMaximum(INT_MAX); + spinBox->setValue(value); + layout.addRow(label, spinBox); + connect(spinBox, qOverload(&QSpinBox::valueChanged), [&value] (int newValue) { value = newValue; }); + } + for (auto [value, label] : Array>{ + { sp.minAddress = 0, MIN_ADDRESS }, + { sp.maxAddress = -1ULL, MAX_ADDRESS }, + { sp.padding = 0, STRING_OFFSET } + }) + { + auto input = new QLineEdit(QString::number(value, 16), &dialog); + layout.addRow(label, input); + connect(input, &QLineEdit::textEdited, [&value](QString input) + { + bool ok; + if (uintptr_t newValue = input.toULongLong(&ok, 16); ok) value = newValue; + }); + } + QLineEdit filterInput(".", &dialog); + layout.addRow(HOOK_SEARCH_FILTER, &filterInput); + QPushButton startButton(START_HOOK_SEARCH, &dialog); + layout.addWidget(&startButton); + connect(&startButton, &QPushButton::clicked, &dialog, &QDialog::accept); + if (dialog.exec() == QDialog::Rejected) return; + QByteArray pattern = QByteArray::fromHex(patternInput.text().replace("??", QString::number(XX, 16)).toUtf8()); + memcpy(sp.pattern, pattern.data(), sp.length = min(pattern.size(), 25)); + try { filter = std::wregex(S(filterInput.text())); } catch (std::regex_error) {}; + } + else + { + // sp.length is 0 in this branch, so default will be used + filter = cjkCheckbox.isChecked() ? std::wregex(L"[\\u3000-\\ua000]{4,}") : std::wregex(L"[\\u0020-\\u1000]{4,}"); + } + + auto hooks = std::make_shared(); + try + { + Host::FindHooks(processId, sp, [processId, hooks, filter](HookParam hp, const std::wstring& text) + { + if (std::regex_search(text, filter)) hooks->append(S(Util::GenerateCode(hp, processId)) + ": " + S(text) + "\n"); + }); + } catch (std::out_of_range) { return; } + QString saveFile = QFileDialog::getSaveFileName(this, SAVE_SEARCH_RESULTS, "./Hooks.txt", TEXT_FILES); + if (saveFile.isEmpty()) saveFile = "Hooks.txt"; + std::thread([hooks, saveFile] + { + for (int lastSize = 0; hooks->size() == 0 || hooks->size() != lastSize; Sleep(2000)) lastSize = hooks->size(); + QTextFile(saveFile, QIODevice::WriteOnly | QIODevice::Truncate).write(hooks->toUtf8()); + hooks->clear(); + }).detach(); } void MainWindow::Settings() { - struct : QDialog + QDialog dialog(this, Qt::WindowCloseButtonHint); + QSettings settings(CONFIG_FILE, QSettings::IniFormat, &dialog); + QFormLayout layout(&dialog); + QPushButton saveButton(SAVE_SETTINGS, &dialog); + layout.addWidget(&saveButton); + for (auto [value, label] : Array>{ + { Host::defaultCodepage, DEFAULT_CODEPAGE }, + { TextThread::maxBufferSize, MAX_BUFFER_SIZE }, + { TextThread::flushDelay, FLUSH_DELAY }, + }) { - using QDialog::QDialog; - void launch() - { - auto settings = new QSettings(CONFIG_FILE, QSettings::IniFormat, this); - auto layout = new QFormLayout(this); - auto save = new QPushButton(SAVE_SETTINGS, this); - layout->addWidget(save); - for (auto[value, label] : Array>{ - { Host::defaultCodepage, DEFAULT_CODEPAGE }, - { TextThread::maxBufferSize, MAX_BUFFER_SIZE }, - { TextThread::flushDelay, FLUSH_DELAY }, - }) - { - auto spinBox = new QSpinBox(this); - spinBox->setMaximum(INT_MAX); - spinBox->setValue(value); - layout->insertRow(0, label, spinBox); - connect(save, &QPushButton::clicked, [=, &value] { settings->setValue(label, value = spinBox->value()); }); - } - for (auto[value, label] : Array>{ - { TextThread::filterRepetition, FILTER_REPETITION }, - }) - { - auto checkBox = new QCheckBox(this); - checkBox->setChecked(value); - layout->insertRow(0, label, checkBox); - connect(save, &QPushButton::clicked, [=, &value] { settings->setValue(label, value = checkBox->isChecked()); }); - } - connect(save, &QPushButton::clicked, this, &QDialog::accept); - setWindowTitle(SETTINGS); - exec(); - } - } settingsDialog(this, Qt::WindowCloseButtonHint); - settingsDialog.launch(); + auto spinBox = new QSpinBox(&dialog); + spinBox->setMaximum(INT_MAX); + spinBox->setValue(value); + layout.insertRow(0, label, spinBox); + connect(&saveButton, &QPushButton::clicked, [spinBox, label, &settings, &value] { settings.setValue(label, value = spinBox->value()); }); + } + for (auto [value, label] : Array>{ + { TextThread::filterRepetition, FILTER_REPETITION }, + }) + { + auto checkBox = new QCheckBox(&dialog); + checkBox->setChecked(value); + layout.insertRow(0, label, checkBox); + connect(&saveButton, &QPushButton::clicked, [checkBox, label, &settings, &value] { settings.setValue(label, value = checkBox->isChecked()); }); + } + connect(&saveButton, &QPushButton::clicked, &dialog, &QDialog::accept); + dialog.setWindowTitle(SETTINGS); + dialog.exec(); } void MainWindow::Extensions() diff --git a/include/types.h b/include/types.h index e900e0d..e74cdaa 100644 --- a/include/types.h +++ b/include/types.h @@ -61,11 +61,12 @@ struct ThreadParam struct SearchParam { - BYTE pattern[25] = {}; // pattern in memory to search for - int length, // length of pattern + BYTE pattern[25]; // pattern in memory to search for + int length, // length of pattern (zero means this SearchParam is invalid and the default should be used) offset, // offset from start of pattern to add hook searchTime; // ms uintptr_t padding, minAddress, maxAddress; + void(*hookPostProcesser)(HookParam&); }; struct InsertHookCmd // From host diff --git a/text.cpp b/text.cpp index 696c138..f0cd20f 100644 --- a/text.cpp +++ b/text.cpp @@ -12,7 +12,7 @@ const char* DETACH = u8"Detach from game"; const char* ADD_HOOK = u8"Add hook"; const char* REMOVE_HOOKS = u8"Remove hook(s)"; const char* SAVE_HOOKS = u8"Save hook(s)"; -const char* FIND_HOOKS = u8"Find hooks"; +const char* SEARCH_FOR_HOOKS = u8"Search for hooks"; const char* SETTINGS = u8"Settings"; const char* EXTENSIONS = u8"Extensions"; const char* SELECT_PROCESS = u8"Select process"; @@ -51,6 +51,7 @@ const char* CONFIRM_EXTENSION_OVERWRITE = u8"Another version of this extension a const char* EXTENSION_WRITE_ERROR = u8"Failed to save extension"; const char* USE_JP_LOCALE = u8"Emulate japanese locale?"; const char* HOOK_SEARCH_UNSTABLE_WARNING = u8"Searching for hooks is unstable! Be prepared for your game to crash!"; +const char* SEARCH_CJK = u8"Search for Chinese/Japanese/Korean"; const char* SEARCH_PATTERN = u8"Search pattern (hex byte array)"; const char* SEARCH_DURATION = u8"Search duration (ms)"; const char* PATTERN_OFFSET = u8"Offset from pattern start"; diff --git a/texthook/engine/engine.cc b/texthook/engine/engine.cc index da7a892..d80de14 100644 --- a/texthook/engine/engine.cc +++ b/texthook/engine/engine.cc @@ -16818,7 +16818,7 @@ bool InsertVanillawareGCHook() /** Artikash 6/7/2019 * PPSSPP JIT code has pointers, but they are all added to an offset before being used. - Find that offset and report it to user so they can search for hooks properly. + Find that offset so that hook searching works properly. To find the offset, find a page of mapped memory with size 0x1f00000, read and write permissions, take its address and subtract 0x8000000. The above is useful for emulating PSP hardware, so unlikely to change between versions. */ @@ -16839,7 +16839,13 @@ bool FindPPSSPP() if (info.RegionSize == 0x1f00000 && info.Protect == PAGE_READWRITE && info.Type == MEM_MAPPED) { found = true; - ConsoleOutput("Textractor: PPSSPP memory found: use pattern 79 0F C7 85 and pattern offset 0 and string offset 0x%p to search for hooks", probe - 0x8000000); + ConsoleOutput("Textractor: PPSSPP memory found: searching for hooks should yield working hook codes"); + memcpy(spDefault.pattern, Array{ 0x79, 0x0f, 0xc7, 0x85 }, spDefault.length = 4); + spDefault.offset = 0; + spDefault.minAddress = 0; + spDefault.maxAddress = -1ULL; + spDefault.padding = (uintptr_t)probe - 0x8000000; + spDefault.hookPostProcesser = [](HookParam& hp) { hp.type |= NO_CONTEXT; }; } probe += info.RegionSize; } diff --git a/texthook/engine/match.cc b/texthook/engine/match.cc index 417e243..c1bb7a2 100644 --- a/texthook/engine/match.cc +++ b/texthook/engine/match.cc @@ -37,22 +37,25 @@ namespace Engine void Hijack() { - static bool hijacked = false; - if (hijacked) return; - GetModuleFileNameW(nullptr, processPath, MAX_PATH); - processName = wcsrchr(processPath, L'\\') + 1; - - processStartAddress = processStopAddress = (uintptr_t)GetModuleHandleW(nullptr); - MEMORY_BASIC_INFORMATION info; - do + static auto _ = [] { - VirtualQuery((void*)processStopAddress, &info, sizeof(info)); - processStopAddress = (uintptr_t)info.BaseAddress + info.RegionSize; - } while (info.Protect > PAGE_NOACCESS); - processStopAddress -= info.RegionSize; + GetModuleFileNameW(nullptr, processPath, MAX_PATH); + processName = wcsrchr(processPath, L'\\') + 1; - DetermineEngineType(); - hijacked = true; - ConsoleOutput("Textractor: finished hijacking process located from 0x%p to 0x%p", processStartAddress, processStopAddress); + processStartAddress = processStopAddress = (uintptr_t)GetModuleHandleW(nullptr); + MEMORY_BASIC_INFORMATION info; + do + { + VirtualQuery((void*)processStopAddress, &info, sizeof(info)); + processStopAddress = (uintptr_t)info.BaseAddress + info.RegionSize; + } while (info.Protect > PAGE_NOACCESS); + processStopAddress -= info.RegionSize; + spDefault.minAddress = processStartAddress; + spDefault.maxAddress = processStopAddress; + ConsoleOutput("Textractor: hijacking process located from 0x%p to 0x%p", processStartAddress, processStopAddress); + + DetermineEngineType(); + return NULL; + }(); } } diff --git a/texthook/engine/match64.cc b/texthook/engine/match64.cc index f378ae9..0461ecb 100644 --- a/texthook/engine/match64.cc +++ b/texthook/engine/match64.cc @@ -8,7 +8,7 @@ namespace Engine { /** Artikash 6/7/2019 * PPSSPP JIT code has pointers, but they are all added to an offset before being used. - Find that offset and report it to user so they can search for hooks properly. + Find that offset so that hook searching works properly. To find the offset, find a page of mapped memory with size 0x1f00000, read and write permissions, take its address and subtract 0x8000000. The above is useful for emulating PSP hardware, so unlikely to change between versions. */ @@ -29,7 +29,13 @@ namespace Engine if (info.RegionSize == 0x1f00000 && info.Protect == PAGE_READWRITE && info.Type == MEM_MAPPED) { found = true; - ConsoleOutput("Textractor: PPSSPP memory found: use pattern 79 10 41 C7 and pattern offset 0 and string offset 0x%p to search for hooks", probe - 0x8000000); + ConsoleOutput("Textractor: PPSSPP memory found: searching for hooks should yield working hook codes"); + memcpy(spDefault.pattern, Array{ 0x79, 0x10, 0x41, 0xc7 }, spDefault.length = 4); + spDefault.offset = 0; + spDefault.minAddress = 0; + spDefault.maxAddress = -1ULL; + spDefault.padding = (uintptr_t)probe - 0x8000000; + spDefault.hookPostProcesser = [](HookParam& hp) { hp.type |= NO_CONTEXT; }; } probe += info.RegionSize; } diff --git a/texthook/hookfinder.cc b/texthook/hookfinder.cc index c6814d2..985c036 100644 --- a/texthook/hookfinder.cc +++ b/texthook/hookfinder.cc @@ -11,7 +11,7 @@ extern WinMutex viewMutex; namespace { - SearchParam current; + SearchParam sp; constexpr int CACHE_SIZE = 500'000; struct HookRecord @@ -23,7 +23,8 @@ namespace hp.offset = offset; hp.type = USING_UNICODE | USING_STRING; hp.address = address; - hp.padding = current.padding; + hp.padding = sp.padding; + if (sp.hookPostProcesser) sp.hookPostProcesser(hp); NotifyHookFound(hp, (wchar_t*)text); } uint64_t address = 0; @@ -118,7 +119,7 @@ void Send(char** stack, uintptr_t address) for (int i = -registers; i < 6; ++i) { int length = 0, sum = 0; - char* str = stack[i] + current.padding; + char* str = stack[i] + sp.padding; __try { for (; (str[length] || str[length + 1]) && length < 500; length += 2) sum += str[length] + str[length + 1]; } __except (EXCEPTION_EXECUTE_HANDLER) {} if (length > STRING && length < 499) @@ -152,7 +153,7 @@ void Send(char** stack, uintptr_t address) } } -void SearchForHooks(SearchParam sp) +void SearchForHooks(SearchParam spUser) { std::thread([=] { @@ -162,7 +163,7 @@ void SearchForHooks(SearchParam sp) try { records = std::make_unique(recordsAvailable = CACHE_SIZE); } catch (std::bad_alloc) { return ConsoleOutput("Textractor: SearchForHooks ERROR (out of memory)"); } - current = sp; + sp = spUser.length == 0 ? spDefault : spUser; uintptr_t moduleStartAddress = (uintptr_t)GetModuleHandleW(ITH_DLL); uintptr_t moduleStopAddress = moduleStartAddress; diff --git a/texthook/main.cc b/texthook/main.cc index e3bc567..6658ff1 100644 --- a/texthook/main.cc +++ b/texthook/main.cc @@ -165,7 +165,7 @@ void NewHook(HookParam hp, LPCSTR lpname, DWORD flag) WideCharToMultiByte(hp.codepage, 0, hp.text, MAX_MODULE_SIZE, codepageText, MAX_MODULE_SIZE * 4, nullptr, nullptr); if (strlen(utf8Text) < 8 || strlen(codepageText) < 8 || wcslen(hp.text) < 4) return ConsoleOutput(NOT_ENOUGH_TEXT); ConsoleOutput(STARTING_SEARCH); - for (auto[addrs, type] : Array, HookParamType>>{ + for (auto [addrs, type] : Array, HookParamType>>{ { Util::SearchMemory(utf8Text, strlen(utf8Text), PAGE_READWRITE), USING_UTF8 }, { Util::SearchMemory(codepageText, strlen(codepageText), PAGE_READWRITE), USING_STRING }, { Util::SearchMemory(hp.text, wcslen(hp.text) * 2, PAGE_READWRITE), USING_UNICODE } diff --git a/texthook/main.h b/texthook/main.h index 6e07e60..6eebae4 100644 --- a/texthook/main.h +++ b/texthook/main.h @@ -14,6 +14,15 @@ void NotifyHookRemove(uint64_t addr, LPCSTR name); void NewHook(HookParam hp, LPCSTR name, DWORD flag = HOOK_ENGINE); void RemoveHook(uint64_t addr, int maxOffset = 9); +inline SearchParam spDefault = [] +{ + SearchParam sp = {}; + memcpy(sp.pattern, x64 ? Array{ 0xcc, 0xcc, 0x48, 0x89 } : Array{ 0xcc, 0xcc, 0x55, 0x8b, 0xec }, sp.length = x64 ? 4 : 5); + sp.offset = 2; + sp.searchTime = 20000; + return sp; +}(); + extern "C" // minhook library { enum MH_STATUS From 28d14bcc32fb81895eb8ce90d5b733d1372b7edf Mon Sep 17 00:00:00 2001 From: Akash Mozumdar Date: Sun, 16 Jun 2019 17:15:47 -0400 Subject: [PATCH 3/6] dont bother comparing to nullptr --- GUI/extenwindow.cpp | 2 +- GUI/host/host.cpp | 2 +- extensions/extension.h | 2 +- extensions/extrawindow.cpp | 4 ++-- extensions/lua.cpp | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/GUI/extenwindow.cpp b/GUI/extenwindow.cpp index 28e4fb3..dfd343b 100644 --- a/GUI/extenwindow.cpp +++ b/GUI/extenwindow.cpp @@ -118,7 +118,7 @@ bool ExtenWindow::eventFilter(QObject* target, QEvent* event) void ExtenWindow::keyPressEvent(QKeyEvent* event) { - if (event->key() == Qt::Key_Delete && ui->extenList->currentItem() != nullptr) + if (event->key() == Qt::Key_Delete && ui->extenList->currentItem()) { Unload(ui->extenList->currentIndex().row()); Sync(); diff --git a/GUI/host/host.cpp b/GUI/host/host.cpp index 00985c6..eb9f1a0 100644 --- a/GUI/host/host.cpp +++ b/GUI/host/host.cpp @@ -29,7 +29,7 @@ namespace TextHook GetHook(uint64_t addr) { - if (view == nullptr) return {}; + if (!view) return {}; std::scoped_lock lock(viewMutex); for (auto hook : view) if (hook.address == addr) return hook; diff --git a/extensions/extension.h b/extensions/extension.h index b7f5a8f..a8925e6 100644 --- a/extensions/extension.h +++ b/extensions/extension.h @@ -14,7 +14,7 @@ struct SentenceInfo // nullptr marks end of info array int64_t operator[](std::string propertyName) { - for (auto info = infoArray; info->name != nullptr; ++info) if (propertyName == info->name) return info->value; + for (auto info = infoArray; info->name; ++info) if (propertyName == info->name) return info->value; throw; } diff --git a/extensions/extrawindow.cpp b/extensions/extrawindow.cpp index d0f9508..28b352f 100644 --- a/extensions/extrawindow.cpp +++ b/extensions/extrawindow.cpp @@ -136,7 +136,7 @@ BOOL WINAPI DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved case DLL_PROCESS_DETACH: { std::lock_guard l(m); - if (window != nullptr) + if (window) { window->settings->setValue(WINDOW, window->geometry()); window->settings->sync(); @@ -155,7 +155,7 @@ BOOL WINAPI DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved bool ProcessSentence(std::wstring& sentence, SentenceInfo sentenceInfo) { std::lock_guard l(m); - if (window == nullptr || !sentenceInfo["current select"]) return false; + if (!window || !sentenceInfo["current select"]) return false; QMetaObject::invokeMethod(window, [=] { window->display->setText(QString::fromStdWString(sentence)); }); return false; } diff --git a/extensions/lua.cpp b/extensions/lua.cpp index c1ed1af..5c0eabc 100644 --- a/extensions/lua.cpp +++ b/extensions/lua.cpp @@ -127,7 +127,7 @@ bool ProcessSentence(std::wstring& sentence, SentenceInfo sentenceInfo) } lua_pushstring(L, WideStringToString(sentence).c_str()); lua_createtable(L, 0, 0); - for (auto info = sentenceInfo.infoArray; info->name != nullptr; ++info) + for (auto info = sentenceInfo.infoArray; info->name; ++info) { lua_pushstring(L, info->name); lua_pushinteger(L, info->value); From 3a103890addd84502c1ec06198a0305b42daa491 Mon Sep 17 00:00:00 2001 From: Akash Mozumdar Date: Sun, 16 Jun 2019 19:43:59 -0400 Subject: [PATCH 4/6] cache translations in file --- extensions/translatewrapper.cpp | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/extensions/translatewrapper.cpp b/extensions/translatewrapper.cpp index e695fd8..a02baf7 100644 --- a/extensions/translatewrapper.cpp +++ b/extensions/translatewrapper.cpp @@ -2,6 +2,7 @@ #include "network.h" #include #include +#include extern const char* SELECT_LANGUAGE; extern const char* SELECT_LANGUAGE_MESSAGE; @@ -12,6 +13,8 @@ extern QStringList languages; extern Synchronized translateTo; std::pair Translate(const std::wstring& text); +Synchronized> translationCache; + BOOL WINAPI DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved) { switch (ul_reason_for_call) @@ -33,10 +36,21 @@ BOOL WINAPI DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved .toStdWString() ); }); + + QFile file(QString("%1 Cache.txt").arg(TRANSLATION_PROVIDER)); + file.open(QIODevice::ReadOnly | QIODevice::Text); + QStringList savedCache = QString(file.readAll()).split("|T|\n", QString::SkipEmptyParts); + for (int i = 0; i < savedCache.size() - 1; i += 2) + translationCache->insert({ savedCache[i].toStdWString(), savedCache[i + 1].toStdWString() }); } break; case DLL_PROCESS_DETACH: { + QFile file(QString("%1 Cache.txt").arg(TRANSLATION_PROVIDER)); + file.open(QIODevice::WriteOnly | QIODevice::Text); + auto translationCache = ::translationCache.Acquire(); + for (const auto& [original, translation] : translationCache.contents) + file.write(QString::fromStdWString(FormatString(L"%s|T|\n%s|T|\n", original, translation)).toUtf8()); } break; } @@ -63,7 +77,6 @@ bool ProcessSentence(std::wstring& sentence, SentenceInfo sentenceInfo) const int tokenCount = 30, delay = 60 * 1000; Synchronized> tokens; } rateLimiter; - static Synchronized> translationCache; bool cache = false; std::wstring translation; From 566c0beb0a555d7c9620cbd882f9d32bfe5020ab Mon Sep 17 00:00:00 2001 From: Akash Mozumdar Date: Sun, 16 Jun 2019 22:57:41 -0400 Subject: [PATCH 5/6] make thread linker asynchronous. also refactored, but in a way that makes me very concerned about the stability of host --- GUI/host/host.cpp | 20 ++++++++++---------- GUI/host/host.h | 3 ++- GUI/mainwindow.cpp | 36 ++++++++++++++++++++++-------------- extensions/threadlinker.cpp | 12 +++++------- 4 files changed, 39 insertions(+), 32 deletions(-) diff --git a/GUI/host/host.cpp b/GUI/host/host.cpp index eb9f1a0..eff9c52 100644 --- a/GUI/host/host.cpp +++ b/GUI/host/host.cpp @@ -60,7 +60,7 @@ namespace size_t HashThreadParam(ThreadParam tp) { return std::hash()(tp.processId + tp.addr) + std::hash()(tp.ctx + tp.ctx2); } Synchronized>, std::recursive_mutex> textThreadsByParams; - Synchronized, std::recursive_mutex> processRecordsByIds; + Synchronized> processRecordsByIds; Host::ProcessEventHandler OnConnect, OnDisconnect; Host::ThreadEventHandler OnCreate, OnDestroy; @@ -102,7 +102,7 @@ namespace case HOST_NOTIFICATION_FOUND_HOOK: { auto info = *(HookFoundNotif*)buffer; - auto& OnHookFound = processRecordsByIds->at(processId).OnHookFound; + auto OnHookFound = processRecordsByIds->at(processId).OnHookFound; std::wstring wide = info.text; if (wide.size() > STRING) OnHookFound(info.hp, info.text); info.hp.type &= ~USING_UNICODE; @@ -132,7 +132,7 @@ namespace auto textThread = textThreadsByParams->find(tp); if (textThread == textThreadsByParams->end()) { - try { textThread = textThreadsByParams->try_emplace(tp, tp, Host::GetHookParam(tp)).first; } + try { textThread = textThreadsByParams->try_emplace(tp, tp, processRecordsByIds->at(tp.processId).GetHook(tp.addr).hp).first; } catch (std::out_of_range) { continue; } // probably garbage data in pipe, try again OnCreate(textThread->second); } @@ -158,8 +158,6 @@ namespace Host OnDestroy = [Destroy](TextThread& thread) { thread.Stop(); Destroy(thread); }; TextThread::Output = Output; - processRecordsByIds->try_emplace(console.processId, console.processId, INVALID_HANDLE_VALUE); - OnConnect(console.processId); textThreadsByParams->try_emplace(console, console, HookParam{}, CONSOLE); OnCreate(GetThread(console)); textThreadsByParams->try_emplace(clipboard, clipboard, HookParam{}, CLIPBOARD); @@ -233,16 +231,18 @@ namespace Host processRecordsByIds->at(processId).Send(FindHookCmd(sp)); } - HookParam GetHookParam(ThreadParam tp) - { - return processRecordsByIds->at(tp.processId).GetHook(tp.addr).hp; - } - TextThread& GetThread(ThreadParam tp) { return textThreadsByParams->at(tp); } + TextThread* GetThread(int64_t handle) + { + auto textThreadsByParams = ::textThreadsByParams.Acquire(); + auto thread = std::find_if(textThreadsByParams->begin(), textThreadsByParams->end(), [&](const auto& thread) { return thread.second.handle == handle; }); + return thread != textThreadsByParams->end() ? &thread->second : nullptr; + } + void AddConsoleOutput(std::wstring text) { GetThread(console).AddSentence(std::move(text)); diff --git a/GUI/host/host.h b/GUI/host/host.h index 9a203f5..ea7caa2 100644 --- a/GUI/host/host.h +++ b/GUI/host/host.h @@ -12,11 +12,12 @@ namespace Host void InjectProcess(DWORD processId); void DetachProcess(DWORD processId); + void InsertHook(DWORD processId, HookParam hp); void RemoveHook(DWORD processId, uint64_t address); void FindHooks(DWORD processId, SearchParam sp, HookEventHandler HookFound = {}); - HookParam GetHookParam(ThreadParam tp); + TextThread* GetThread(int64_t handle); TextThread& GetThread(ThreadParam tp); void AddConsoleOutput(std::wstring text); diff --git a/GUI/mainwindow.cpp b/GUI/mainwindow.cpp index bc90ca6..e5ae208 100644 --- a/GUI/mainwindow.cpp +++ b/GUI/mainwindow.cpp @@ -234,6 +234,13 @@ DWORD MainWindow::GetSelectedProcessId() std::array MainWindow::GetMiscInfo(TextThread& thread) { + void(*AddSentence)(MainWindow*, int64_t, const wchar_t*) = [](MainWindow* This, int64_t number, const wchar_t* sentence) + { + std::wstring sentenceStr = sentence; + // pointer from Host::GetThread may not stay valid unless on main thread + QMetaObject::invokeMethod(This, [=]() mutable { if (TextThread* thread = Host::GetThread(number)) thread->AddSentence(std::move(sentenceStr)); }); + }; + return { { { "current select", &thread == current }, @@ -242,6 +249,8 @@ std::array MainWindow::GetMiscInfo(TextThread& thread) { "hook address", (int64_t)thread.tp.addr }, { "text handle", thread.handle }, { "text name", (int64_t)thread.name.c_str() }, + { "this", (int64_t)this }, + { "void (*AddSentence)(void* this, int64_t number, const wchar_t* sentence)", (int64_t)AddSentence }, { nullptr, 0 } // nullptr marks end of info array } }; } @@ -323,7 +332,7 @@ void MainWindow::RemoveHooks() for (int i = 0; i < ui->ttCombo->count(); ++i) { ThreadParam tp = ParseTextThreadString(ui->ttCombo->itemText(i)); - if (tp.processId == GetSelectedProcessId()) hooks[tp.addr] = Host::GetHookParam(tp); + if (tp.processId == GetSelectedProcessId()) hooks[tp.addr] = Host::GetThread(tp).hp; } auto hookList = new QListWidget(this); hookList->setWindowFlags(Qt::Window | Qt::WindowCloseButtonHint); @@ -346,23 +355,22 @@ void MainWindow::RemoveHooks() void MainWindow::SaveHooks() { - if (auto processName = Util::GetModuleFilename(GetSelectedProcessId())) + auto processName = Util::GetModuleFilename(GetSelectedProcessId()); + if (!processName) return; + QHash hookCodes; + for (int i = 0; i < ui->ttCombo->count(); ++i) { - QHash hookCodes; - for (int i = 0; i < ui->ttCombo->count(); ++i) + ThreadParam tp = ParseTextThreadString(ui->ttCombo->itemText(i)); + if (tp.processId == GetSelectedProcessId()) { - ThreadParam tp = ParseTextThreadString(ui->ttCombo->itemText(i)); - if (tp.processId == GetSelectedProcessId()) - { - HookParam hp = Host::GetHookParam(tp); - if (!(hp.type & HOOK_ENGINE)) hookCodes[tp.addr] = S(Util::GenerateCode(hp, tp.processId)); - } + HookParam hp = Host::GetThread(tp).hp; + if (!(hp.type & HOOK_ENGINE)) hookCodes[tp.addr] = S(Util::GenerateCode(hp, tp.processId)); } - auto hookInfo = QStringList() << S(processName.value()) << hookCodes.values(); - ThreadParam tp = current.load()->tp; - if (tp.processId == GetSelectedProcessId()) hookInfo << QString("|%1:%2:%3").arg(tp.ctx).arg(tp.ctx2).arg(S(Util::GenerateCode(Host::GetHookParam(tp), tp.processId))); - QTextFile(HOOK_SAVE_FILE, QIODevice::WriteOnly | QIODevice::Append).write((hookInfo.join(" , ") + "\n").toUtf8()); } + auto hookInfo = QStringList() << S(processName.value()) << hookCodes.values(); + ThreadParam tp = current.load()->tp; + if (tp.processId == GetSelectedProcessId()) hookInfo << QString("|%1:%2:%3").arg(tp.ctx).arg(tp.ctx2).arg(S(Util::GenerateCode(Host::GetThread(tp).hp, tp.processId))); + QTextFile(HOOK_SAVE_FILE, QIODevice::WriteOnly | QIODevice::Append).write((hookInfo.join(" , ") + "\n").toUtf8()); } void MainWindow::FindHooks() diff --git a/extensions/threadlinker.cpp b/extensions/threadlinker.cpp index a78f70f..68e0014 100644 --- a/extensions/threadlinker.cpp +++ b/extensions/threadlinker.cpp @@ -93,13 +93,11 @@ BOOL WINAPI DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved bool ProcessSentence(std::wstring& sentence, SentenceInfo sentenceInfo) { std::lock_guard l(m); - static std::unordered_map queuedWritesByHandle; - int64_t textHandle = sentenceInfo["text handle"]; + int64_t textHandle = sentenceInfo["text number"]; - for (auto linkedHandle : linkedTextHandles[textHandle]) queuedWritesByHandle[linkedHandle] += L"\n" + sentence; + for (auto linkedHandle : linkedTextHandles[textHandle]) + ((void(*)(void*, int64_t, const wchar_t*))sentenceInfo["void (*AddSentence)(void* this, int64_t number, const wchar_t* sentence)"]) + ((void*)sentenceInfo["this"], linkedHandle, sentence.c_str()); - if (queuedWritesByHandle[textHandle].empty()) return false; - sentence += queuedWritesByHandle[textHandle]; - queuedWritesByHandle[textHandle].clear(); - return true; + return false; } From c85618cd0d9ce165db32d2364379ddef2c154ea6 Mon Sep 17 00:00:00 2001 From: Akash Mozumdar Date: Mon, 17 Jun 2019 00:42:42 -0400 Subject: [PATCH 6/6] allow custom fonts in extra window --- extensions/extrawindow.cpp | 45 ++++++++++++++++++++++++++++++++------ text.cpp | 3 +++ 2 files changed, 41 insertions(+), 7 deletions(-) diff --git a/extensions/extrawindow.cpp b/extensions/extrawindow.cpp index 28b352f..f21a5f7 100644 --- a/extensions/extrawindow.cpp +++ b/extensions/extrawindow.cpp @@ -6,6 +6,10 @@ #include #include #include +#include +#include +#include +#include #include #include #include @@ -16,7 +20,11 @@ extern const char* TOPMOST; extern const char* SIZE_LOCK; extern const char* BG_COLOR; extern const char* TEXT_COLOR; +extern const char* FONT; extern const char* FONT_SIZE; +extern const char* FONT_FAMILY; +extern const char* FONT_WEIGHT; +extern const char* SAVE_SETTINGS; std::mutex m; @@ -54,12 +62,32 @@ public: display->setPalette(newPalette); settings->setValue(TEXT_COLOR, color); }; - auto setFontSize = [=](int pt) + auto requestFont = [=] { - QFont newFont = display->font(); - newFont.setPointSize(pt); - display->setFont(newFont); - settings->setValue(FONT_SIZE, pt); + QFont font = display->font(); + auto fontDialog = new QDialog(this, Qt::WindowCloseButtonHint); + fontDialog->setAttribute(Qt::WA_DeleteOnClose); + fontDialog->setWindowTitle(FONT); + auto layout = new QFormLayout(fontDialog); + fontDialog->setLayout(layout); + auto fontFamily = new QLineEdit(font.family(), fontDialog); + layout->addRow(FONT_FAMILY, fontFamily); + auto fontSize = new QSpinBox(fontDialog); + fontSize->setValue(font.pointSize()); + layout->addRow(FONT_SIZE, fontSize); + auto fontWeight = new QSpinBox(fontDialog); + fontWeight->setValue(font.weight()); + layout->addRow(FONT_WEIGHT, fontWeight); + auto save = new QPushButton(SAVE_SETTINGS, fontDialog); + layout->addWidget(save); + connect(save, &QPushButton::clicked, fontDialog, &QDialog::accept); + fontDialog->open(); + connect(fontDialog, &QDialog::accepted, [=] + { + QFont font(fontFamily->text(), fontSize->value(), fontWeight->value()); + settings->setValue(FONT, font.toString()); + display->setFont(font); + }); }; auto setTopmost = [=](bool topmost) { @@ -75,7 +103,10 @@ public: setGeometry(settings->value(WINDOW, geometry()).toRect()); setLock(settings->value(SIZE_LOCK, false).toBool()); setTopmost(settings->value(TOPMOST, false).toBool()); - setFontSize(settings->value(FONT_SIZE, 16).toInt()); + QFont font = display->font(); + font.setPointSize(16); + font.fromString(settings->value(FONT, font.toString()).toString()); + display->setFont(font); setBackgroundColor(settings->value(BG_COLOR, palette().window().color()).value()); setTextColor(settings->value(TEXT_COLOR, display->palette().windowText().color()).value()); @@ -88,7 +119,7 @@ public: lock->setChecked(settings->value(SIZE_LOCK, false).toBool()); menu->addAction(BG_COLOR, [=] { setBackgroundColor(QColorDialog::getColor(bgColor, this, BG_COLOR, QColorDialog::ShowAlphaChannel)); }); menu->addAction(TEXT_COLOR, [=] { setTextColor(QColorDialog::getColor(display->palette().windowText().color(), this, TEXT_COLOR, QColorDialog::ShowAlphaChannel)); }); - menu->addAction(FONT_SIZE, [=] { setFontSize(QInputDialog::getInt(this, FONT_SIZE, "", display->font().pointSize(), 0, INT_MAX, 1, nullptr, Qt::WindowCloseButtonHint)); }); + menu->addAction(FONT, requestFont); display->setContextMenuPolicy(Qt::CustomContextMenu); connect(display, &QLabel::customContextMenuRequested, [=](QPoint point) { menu->exec(mapToGlobal(point)); }); connect(this, &QDialog::destroyed, [=] { settings->setValue(WINDOW, geometry()); }); diff --git a/text.cpp b/text.cpp index f0cd20f..2e4b4e1 100644 --- a/text.cpp +++ b/text.cpp @@ -113,7 +113,10 @@ const char* TOPMOST = u8"Always on Top"; const char* SIZE_LOCK = u8"Size Locked"; const char* BG_COLOR = u8"Background Color"; const char* TEXT_COLOR = u8"Text Color"; +const char* FONT = u8"Font"; +const char* FONT_FAMILY = u8"Font Family"; const char* FONT_SIZE = u8"Font Size"; +const char* FONT_WEIGHT = u8"Font Weight"; const char* LUA_INTRO = u8R"(--[[ ProcessSentence is called each time Textractor receives a sentence of text.