Textractor_test/texthook/engine/match64.cc

1060 lines
37 KiB
C++
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#include "match.h"
#include "main.h"
#include "texthook.h"
#include "native/pchooks.h"
#include "mono/monoobject.h"
#include "mono/funcinfo.h"
#include "engine.h"
#include "util.h"
#include "cpputil/cppcstring.h"
// Warning: The offset in ITH has -4 offset comparing to pusha and AGTH
enum pusha_off
{
pusha_rax_off = -0xC,
pusha_rbx_off = -0x14,
pusha_rcx_off = -0x1c,
pusha_rdx_off = -0x24,
pusha_rsp_off = -0x2c,
pusha_rbp_off = -0x34,
pusha_rsi_off = -0x3c,
pusha_rdi_off = -0x44,
pusha_r8_off = -0x4c,
pusha_r9_off = -0x54,
pusha_r10_off = -0x5c,
pusha_r11_off = -0x64,
pusha_r12_off = -0x6c,
pusha_r13_off = -0x74,
pusha_r14_off = -0x7c,
pusha_r15_off = -0x84,
pusha_off = -0x8c // pushad offset
};
#define retof(rsp_base) *(uintptr_t *)(rsp_base) // return address
#define regof(name, rsp_base) *(uintptr_t *)((rsp_base) + pusha_##name##_off - 4)
#define argof(count, rsp_base) *(uintptr_t *)((rsp_base) + 4 * (count)) // starts from 1 instead of 0
enum { VNR_TEXT_CAPACITY = 1500 }; // estimated max number of bytes allowed in VNR, slightly larger than VNR's text limit (1000)
namespace { // unnamed helpers
#define XX2 XX,XX // WORD
#define XX4 XX2,XX2 // DWORD
#define XX8 XX4,XX4 // QWORD
// jichi 8/18/2013: Original maximum relative address in ITH
//enum { MAX_REL_ADDR = 0x200000 };
// jichi 10/1/2013: Increase relative address limit. Certain game engine like Artemis has larger code region
enum : DWORD { MAX_REL_ADDR = 0x00300000 };
static union {
char text_buffer[0x1000];
wchar_t wc_buffer[0x800];
struct { // CodeSection
DWORD base;
DWORD size;
} code_section[0x200];
};
DWORD text_buffer_length;
// 7/29/2014 jichi: I should move these functions to different files
// String utilities
// Return the address of the first non-zero address
LPCSTR reverse_search_begin(const char *s, int maxsize = VNR_TEXT_CAPACITY)
{
if (*s)
for (int i = 0; i < maxsize; i++, s--)
if (!*s)
return s + 1;
return nullptr;
}
bool all_ascii(const char *s, int maxsize = VNR_TEXT_CAPACITY)
{
if (s)
for (int i = 0; i < maxsize && *s; i++, s++)
if ((BYTE)*s > 127) // unsigned char
return false;
return true;
}
bool all_ascii(const wchar_t *s, int maxsize = VNR_TEXT_CAPACITY)
{
if (s)
for (int i = 0; i < maxsize && *s; i++, s++)
if (*s > 127) // unsigned char
return false;
return true;
}
// String filters
void CharReplacer(char *str, size_t *size, char fr, char to)
{
size_t len = *size;
for (size_t i = 0; i < len; i++)
if (str[i] == fr)
str[i] = to;
}
void WideCharReplacer(wchar_t *str, size_t *size, wchar_t fr, wchar_t to)
{
size_t len = *size / 2;
for (size_t i = 0; i < len; i++)
if (str[i] == fr)
str[i] = to;
}
void CharFilter(char *str, size_t *size, char ch)
{
size_t len = *size,
curlen;
for (char *cur = (char *)::memchr(str, ch, len);
(cur && --len && (curlen = len - (cur - str)));
cur = (char *)::memchr(cur, ch, curlen))
::memmove(cur, cur + 1, curlen);
*size = len;
}
void WideCharFilter(wchar_t *str, size_t *size, wchar_t ch)
{
size_t len = *size / 2,
curlen;
for (wchar_t *cur = cpp_wcsnchr(str, ch, len);
(cur && --len && (curlen = len - (cur - str)));
cur = cpp_wcsnchr(cur, ch, curlen))
::memmove(cur, cur + 1, 2 * curlen);
*size = len * 2;
}
void CharsFilter(char *str, size_t *size, const char *chars)
{
size_t len = *size,
curlen;
for (char *cur = cpp_strnpbrk(str, chars, len);
(cur && --len && (curlen = len - (cur - str)));
cur = cpp_strnpbrk(cur, chars, curlen))
::memmove(cur, cur + 1, curlen);
*size = len;
}
void WideCharsFilter(wchar_t *str, size_t *size, const wchar_t *chars)
{
size_t len = *size / 2,
curlen;
for (wchar_t *cur = cpp_wcsnpbrk(str, chars, len);
(cur && --len && (curlen = len - (cur - str)));
cur = cpp_wcsnpbrk(cur, chars, curlen))
::memmove(cur, cur + 1, 2 * curlen);
*size = len * 2;
}
void StringFilter(char *str, size_t *size, const char *remove, size_t removelen)
{
size_t len = *size,
curlen;
for (char *cur = cpp_strnstr(str, remove, len);
(cur && (len -= removelen) && (curlen = len - (cur - str)));
cur = cpp_strnstr(cur, remove, curlen))
::memmove(cur, cur + removelen, curlen);
*size = len;
}
void WideStringFilter(wchar_t *str, size_t *size, const wchar_t *remove, size_t removelen)
{
size_t len = *size / 2,
curlen;
for (wchar_t *cur = cpp_wcsnstr(str, remove, len);
(cur && (len -= removelen) && (curlen = len - (cur - str)));
cur = cpp_wcsnstr(cur, remove, curlen))
::memmove(cur, cur + removelen, 2 * curlen);
*size = len * 2;
}
void StringFilterBetween(char *str, size_t *size, const char *fr, size_t frlen, const char *to, size_t tolen)
{
size_t len = *size,
curlen;
for (char *cur = cpp_strnstr(str, fr, len);
cur;
cur = cpp_strnstr(cur, fr, curlen)) {
curlen = (len - frlen) - (cur - str);
auto end = cpp_strnstr(cur + frlen, to, curlen);
if (!end)
break;
curlen = len - (end - str) - tolen;
::memmove(cur, end + tolen, curlen);
len -= tolen + (end - cur);
}
*size = len;
}
void WideStringFilterBetween(wchar_t *str, size_t *size, const wchar_t *fr, size_t frlen, const wchar_t *to, size_t tolen)
{
size_t len = *size / 2,
curlen;
for (wchar_t *cur = cpp_wcsnstr(str, fr, len);
cur;
cur = cpp_wcsnstr(cur, fr, curlen)) {
curlen = (len - frlen) - (cur - str);
auto end = cpp_wcsnstr(cur + frlen, to, curlen);
if (!end)
break;
curlen = len - (end - str) - tolen;
::memmove(cur, end + tolen, 2 * curlen);
len -= tolen + (end - cur);
}
*size = len * 2;
}
void StringCharReplacer(char *str, size_t *size, const char *src, size_t srclen, char ch)
{
size_t len = *size,
curlen;
for (char *cur = cpp_strnstr(str, src, len);
cur && len;
cur = cpp_strnstr(cur, src, curlen)) {
*cur++ = ch;
len -= srclen - 1;
curlen = len - (cur - str);
if (curlen == 0)
break;
::memmove(cur, cur + srclen - 1, curlen);
}
*size = len;
}
void WideStringCharReplacer(wchar_t *str, size_t *size, const wchar_t *src, size_t srclen, wchar_t ch)
{
size_t len = *size / 2,
curlen;
for (wchar_t *cur = cpp_wcsnstr(str, src, len);
cur && len;
cur = cpp_wcsnstr(cur, src, curlen)) {
*cur++ = ch;
len -= srclen - 1;
curlen = len - (cur - str);
if (curlen == 0)
break;
::memmove(cur, cur + srclen -1, 2 * curlen);
}
*size = len * 2;
}
// NOTE: I assume srclen >= dstlen
void StringReplacer(char *str, size_t *size, const char *src, size_t srclen, const char *dst, size_t dstlen)
{
size_t len = *size,
curlen;
for (char *cur = cpp_strnstr(str, src, len);
cur && len;
cur = cpp_strnstr(cur, src, curlen)) {
::memcpy(cur, dst, dstlen);
cur += dstlen;
len -= srclen - dstlen;
curlen = len - (cur - str);
if (curlen == 0)
break;
if (srclen > dstlen)
::memmove(cur, cur + srclen - dstlen, curlen);
}
*size = len;
}
void WideStringReplacer(wchar_t *str, size_t *size, const wchar_t *src, size_t srclen, const wchar_t *dst, size_t dstlen)
{
size_t len = *size / 2,
curlen;
for (wchar_t *cur = cpp_wcsnstr(str, src, len);
cur && len;
cur = cpp_wcsnstr(cur, src, curlen)) {
::memcpy(cur, dst, 2 * dstlen);
cur += dstlen;
len -= srclen - dstlen;
curlen = len - (cur - str);
if (curlen == 0)
break;
if (srclen > dstlen)
::memmove(cur, cur + srclen - dstlen, 2 * curlen);
}
*size = len * 2;
}
bool NewLineCharFilter(LPVOID data, DWORD *size, HookParam *, BYTE)
{
CharFilter(reinterpret_cast<LPSTR>(data), reinterpret_cast<size_t *>(size),
'\n');
return true;
}
bool NewLineWideCharFilter(LPVOID data, DWORD *size, HookParam *, BYTE)
{
CharFilter(reinterpret_cast<LPSTR>(data), reinterpret_cast<size_t *>(size),
L'\n');
return true;
}
bool NewLineStringFilter(LPVOID data, DWORD *size, HookParam *, BYTE)
{
StringFilter(reinterpret_cast<LPSTR>(data), reinterpret_cast<size_t *>(size),
"\\n", 2);
return true;
}
bool NewLineWideStringFilter(LPVOID data, DWORD *size, HookParam *, BYTE)
{
WideStringFilter(reinterpret_cast<LPWSTR>(data), reinterpret_cast<size_t *>(size),
L"\\n", 2);
return true;
}
bool NewLineCharToSpaceFilter(LPVOID data, DWORD *size, HookParam *, BYTE)
{
CharReplacer(reinterpret_cast<LPSTR>(data), reinterpret_cast<size_t *>(size), '\n', ' ');
return true;
}
bool NewLineWideCharToSpaceFilter(LPVOID data, DWORD *size, HookParam *, BYTE)
{
WideCharReplacer(reinterpret_cast<LPWSTR>(data), reinterpret_cast<size_t *>(size), L'\n', L' ');
return true;
}
// Remove every characters <= 0x1f (i.e. before space ' ') except 0xa and 0xd.
bool IllegalCharsFilter(LPVOID data, DWORD *size, HookParam *, BYTE)
{
CharsFilter(reinterpret_cast<LPSTR>(data), reinterpret_cast<size_t *>(size),
"\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0b\x0c\x0e\x0f\x10\x11\x12\x12\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f");
return true;
}
bool IllegalWideCharsFilter(LPVOID data, DWORD *size, HookParam *, BYTE)
{
WideCharsFilter(reinterpret_cast<LPWSTR>(data), reinterpret_cast<size_t *>(size),
L"\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0b\x0c\x0e\x0f\x10\x11\x12\x12\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f");
return true;
}
} // unnamed namespace
#define XX2 XX,XX // WORD
#define XX4 XX2,XX2 // DWORD
#define XX8 XX4,XX4 // QWORD
namespace Engine
{
enum : DWORD { X64_MAX_REL_ADDR = 0x00300000 };
/** Artikash 6/7/2019
* PPSSPP JIT code has pointers, but they are all added to an offset before being used.
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.
*/
bool FindPPSSPP()
{
bool found = false;
SYSTEM_INFO systemInfo;
GetNativeSystemInfo(&systemInfo);
for (BYTE* probe = NULL; probe < systemInfo.lpMaximumApplicationAddress;)
{
MEMORY_BASIC_INFORMATION info;
if (!VirtualQuery(probe, &info, sizeof(info)))
{
probe += systemInfo.dwPageSize;
}
else
{
if (info.RegionSize == 0x1f00000 && info.Protect == PAGE_READWRITE && info.Type == MEM_MAPPED)
{
found = true;
ConsoleOutput("Textractor: PPSSPP memory found: searching for hooks should yield working hook codes");
// 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 | 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;
}
}
return found;
}
//MonoImage* mono_assembly_get_image(MonoAssembly* assembly)获取程序集的镜像。后面几乎所有的操作都会以MonoImage* 为第一个参数。
static uintptr_t (*mono_assembly_get_image)(uintptr_t) = NULL;
// const char* mono_image_get_name(MonoImage * image) :获取程序集名。我们用它判断哪个程序集是我们的目标
static char* (*mono_image_get_name)(uintptr_t) = NULL;
//MonoClass* mono_class_from_name (MonoImage *image, const char* name_space, const char *name):通过类名获取类(非实例)。
static uintptr_t(*mono_class_from_name)(uintptr_t, char*, char*) = NULL;
//MonoVTable* mono_class_vtable (MonoDomain *domain, MonoClass *klass)获取vtable我们通过它可以找到静态字段的起始地址。
static uintptr_t(*mono_class_vtable)(uintptr_t, uintptr_t) = NULL;
//void* mono_vtable_get_static_field_data (MonoVTable *vt):获取静态字段的起始地址。
static void* (*mono_vtable_get_static_field_data)(uintptr_t) = NULL;
//MonoMethod* mono_class_get_method_from_name (MonoClass *klass, const char *name, int param_count):获取方法(非native code地址)。
//其中param_count是参数数量可以输入-1来省略。此函数无法获取重载的方法但对于我们来说足够了。
static uintptr_t(*mono_class_get_method_from_name)(uintptr_t, char*,int) = NULL;
//获取属性。用它可以进一步获得属性的getter和setter。
//MonoProperty* mono_class_get_property_from_name(MonoClass* klass, const char* name)
static uintptr_t(*mono_class_get_property_from_name)(uintptr_t, char*) = NULL;
//获取属性的getter和setter。
//MonoMethod* mono_property_get_get_method(MonoProperty* prop) 与 MonoMethod* mono_property_get_set_method(MonoProperty* prop)
static uintptr_t(*mono_property_get_set_method)(uintptr_t) = NULL;
// 不安全返回方法的地址如果方法尚未编译则JIT开始编译。这个是解决问题的核心方法。 gpointer mono_compile_method (MonoMethod *method):
static uint64_t* (*mono_compile_method)(uintptr_t) = NULL;
//获取函数的非托管块指针 (native) gpointer mono_method_get_unmanaged_thunk (MonoMethod *method)
//使用这个来获取native代码 方法尚未编译,会执行编译 并提取 x86版本可能使用的是__stdcall
static uint64_t* (*mono_method_get_unmanaged_thunk)(uintptr_t) = NULL;
//MonoDomain* mono_get_root_domain (void) :获取主作用域。用于附加线程以及获取静态字段的地址。
static MonoDomain* (*mono_get_root_domain)() = NULL;
//void mono_thread_attach (MonoDomain*):附加到进程的主线程。这个操作是必须的。
static void (*mono_thread_attach)(MonoDomain*) = NULL;
//MonoAssembly* assembly而后者则是void* user_data
int getV8StringLength(uintptr_t stack, uintptr_t data) {
int len = *(int*)(data - 4);
int checkLength = len > 0 && len < PIPE_BUFFER_SIZE ? len : 0;
//检查是否为错误的unicode字符
for (size_t i = 0; i < checkLength; i++)
{
if (*(WORD*)(data + i * 2) == 0x0)
return 0;
}
return checkLength * 2;
}
void MonoCallBack(uintptr_t assembly, void* userData) {
uintptr_t mono_property = NULL;
uintptr_t image=mono_assembly_get_image(assembly);
// TMP_Text TextMeshProUGUI
auto mono_tmp_class=mono_class_from_name(image, "TMPro", "TMP_Text");
auto mono_ugui_class = mono_class_from_name(image, "UnityEngine.UI", "Text");
auto mono_ngui_class = mono_class_from_name(image, "", "UILabel");
if (!mono_tmp_class && !mono_ugui_class && !mono_ngui_class)
return;
if (mono_tmp_class) {
mono_property = mono_class_get_property_from_name(mono_tmp_class, "text");
}
else if (mono_ugui_class)
{
mono_property = mono_class_get_property_from_name(mono_ugui_class, "text");
}
else if (mono_ngui_class) {
mono_property = mono_class_get_property_from_name(mono_ngui_class, "text");
}
if (mono_property == NULL)
return;
auto mono_set_method= mono_property_get_set_method(mono_property);
//注意必须调用mono_thread_attach 附加到主domain 才能调用 mono_method_get_unmanaged_thunk mono_compile_method 或mono_runtime_invoke
mono_thread_attach(mono_get_root_domain());
uint64_t* method_pointer= mono_compile_method(mono_set_method);
if (method_pointer) {
HookParam hp = {};
hp.type = USING_STRING | USING_UNICODE;
hp.address = (uint64_t)method_pointer;
hp.offset = -0x28; // rdx
//hp.index = 0;
hp.padding = 0x14;
if (mono_tmp_class) {
ConsoleOutput("Mono_X64,Insert: TextMeshProUGUI_set_text Hook BY:IOV");
hp.length_fun = getV8StringLength;
NewHook(hp, "TextMeshProUGUI_set_text");
}
else if (mono_ugui_class)
{
ConsoleOutput("Mono_X64,Insert: UGUI_set_text Hook BY:IOV");
hp.length_fun = getV8StringLength;
NewHook(hp, "UGUI_set_text");
}
else if (mono_ngui_class)
{
ConsoleOutput("Mono_X64,Insert: NGUI_set_text Hook BY:IOV");
hp.length_fun = getV8StringLength;
NewHook(hp, "NGUI_set_text");
}
}
}
bool InsertMonoHooksByAssembly(HMODULE module) {
//void mono_assembly_foreach (GFunc func, gpointer user_data)
//遍历程序集。用于获取目标程序集的指针。其中的func 是一个回调函数要自己写。它有两个参数前者就是MonoAssembly*而后者则是user_data
static auto mono_assembly_foreach = (void (*)(void (*)(uintptr_t, void*), uintptr_t))GetProcAddress(module, "mono_assembly_foreach");
mono_assembly_get_image= (uintptr_t(*)(uintptr_t))GetProcAddress(module, "mono_assembly_get_image");
mono_image_get_name = (char* (*)(uintptr_t))GetProcAddress(module, "mono_image_get_name");
mono_class_from_name = (uintptr_t(*)(uintptr_t, char*, char*))GetProcAddress(module, "mono_class_from_name");
mono_class_get_property_from_name = (uintptr_t(*)(uintptr_t, char*))GetProcAddress(module, "mono_class_get_property_from_name");
mono_property_get_set_method = (uintptr_t(*)(uintptr_t))GetProcAddress(module, "mono_property_get_set_method");
mono_compile_method = (uint64_t * (*)(uintptr_t))GetProcAddress(module, "mono_compile_method");
//mono_method_get_unmanaged_thunk= (uint64_t * (*)(uintptr_t))GetProcAddress(module, "mono_method_get_unmanaged_thunk");
mono_get_root_domain = (MonoDomain * (*)())GetProcAddress(module, "mono_get_root_domain");
mono_thread_attach = (void (*)(MonoDomain*))GetProcAddress(module, "mono_thread_attach");
if (mono_assembly_foreach && mono_assembly_get_image && mono_image_get_name && mono_class_from_name &&
mono_class_get_property_from_name && mono_property_get_set_method && mono_compile_method &&
mono_get_root_domain && mono_thread_attach) {
mono_assembly_foreach(MonoCallBack, NULL);
return true;
}
else
{
return false;
}
}
bool InsertMonoHooks(HMODULE module)
{
return InsertMonoHooksByAssembly(module);
auto SpecialHookMonoString = nullptr;
static HMODULE mono = module;
bool ret = false;
for (auto func : Array<MonoFunction>{ MONO_FUNCTIONS_INITIALIZER })
{
HookParam hp = {};
if (!(hp.address = (uintptr_t)GetProcAddress(mono, func.functionName))) continue;
hp.type = HOOK_EMPTY;
NewHook(hp, "Mono Searcher");
ret = true;
}
/* Artikash 2/13/2019:
How to hook Mono/Unity3D:
Find all standard function prologs in memory with write/execute permission: these represent possible JIT compiled functions
Then use Mono APIs to reflect what these functions are, and hook them if they are string member functions
Mono calling convention uses 'this' as first argument
Must be dynamic hook bootstrapped from other mono api or mono_domain_get won't work
*/
trigger_fun = [](LPVOID addr, DWORD, DWORD)
{
static auto getDomain = (MonoDomain*(*)())GetProcAddress(mono, "mono_domain_get");
static auto getJitInfo = (MonoObject*(*)(MonoDomain*, uintptr_t))GetProcAddress(mono, "mono_jit_info_table_find");
static auto getName = (char*(*)(uintptr_t))GetProcAddress(mono, "mono_pmip");
if (!getDomain || !getName || !getJitInfo) goto failed;
static auto domain = getDomain();
if (!domain) goto failed;
ConsoleOutput("Textractor: Mono Dynamic ENTER (hooks = %s)", *loadedConfig ? loadedConfig : "brute force");
const BYTE prolog1[] = { 0x55, 0x48, 0x8b, 0xec };
const BYTE prolog2[] = { 0x48, 0x83, 0xec };
for (auto [prolog, size] : Array<const BYTE*, size_t>{ { prolog1, sizeof(prolog1) }, { prolog2, sizeof(prolog2) } })
for (auto addr : Util::SearchMemory(prolog, size, PAGE_EXECUTE_READWRITE))
{
[](uint64_t addr)
{
__try
{
if (getJitInfo(domain, addr))
if (char* name = getName(addr))
if (strstr(name, "0x0") && ShouldMonoHook(name))
{
HookParam hp = {};
hp.address = addr;
hp.type = USING_STRING | USING_UNICODE | FULL_STRING;
if (!*loadedConfig) hp.type |= KNOWN_UNSTABLE;
hp.offset = -0x20; // rcx
hp.padding = 20;
char nameForUser[HOOK_NAME_SIZE] = {};
strncpy_s(nameForUser, name + 1, HOOK_NAME_SIZE - 1);
if (char* end = strstr(nameForUser, " + 0x0")) *end = 0;
if (char* end = strstr(nameForUser, "{")) *end = 0;
hp.length_fun = [](uintptr_t, uintptr_t data)
{
/* Artikash 6/18/2019:
even though this should get the true length mono uses internally
there's still some garbage picked up on https://vndb.org/v20403 demo, don't know why */
int len = *(int*)(data - 4);
return len > 0 && len < PIPE_BUFFER_SIZE ? len * 2 : 0;
};
NewHook(hp, nameForUser);
}
}
__except (EXCEPTION_EXECUTE_HANDLER) {}
}(addr);
}
if (!*loadedConfig) ConsoleOutput("Textractor: Mono Dynamic used brute force: if performance issues arise, please specify the correct hook in the game configuration");
return true;
failed:
ConsoleOutput("Textractor: Mono Dynamic failed");
return true;
};
return ret;
}
// Artikash 6/23/2019: V8 (JavaScript runtime) has rcx = string** at v8::String::Write
// sample game https://www.freem.ne.jp/dl/win/18963
bool InsertV8Hook(HMODULE module)
{
auto getV8Length = [](uintptr_t, uintptr_t data)
{
int len = *(int*)(data - 4);
return len > 0 && len < PIPE_BUFFER_SIZE ? len * 2 : 0;
};
uint64_t addr1 = (uint64_t)GetProcAddress(module, "?Write@String@v8@@QEBAHPEAGHHH@Z"),
// Artikash 6/7/2021: Add new hook for new version of V8 used by RPG Maker MZ
addr2 = (uint64_t)GetProcAddress(module, "??$WriteToFlat@G@String@internal@v8@@SAXV012@PEAGHH@Z");
if (addr1 || addr2)
{
std::tie(spDefault.minAddress, spDefault.maxAddress) = Util::QueryModuleLimits(module);
spDefault.maxRecords = Util::SearchMemory(spDefault.pattern, spDefault.length, PAGE_EXECUTE, spDefault.minAddress, spDefault.maxAddress).size() * 20;
ConsoleOutput("Textractor: JavaScript hook is known to be low quality: try searching for hooks if you don't like it");
}
if (addr1)
{
HookParam hp = {};
hp.type = USING_STRING | USING_UNICODE | DATA_INDIRECT;
hp.address = addr1;
hp.offset = -0x20; // rcx
hp.index = 0;
hp.padding = 23;
hp.length_fun = getV8Length;
NewHook(hp, "JavaScript");
}
if (addr2)
{
HookParam hp = {};
hp.type = USING_STRING | USING_UNICODE;
hp.address = addr2;
hp.offset = -0x20; // rcx
hp.padding = 11;
hp.length_fun = getV8Length;
NewHook(hp, "JavaScript");
}
return addr1 || addr2;
}
/** Artikash 8/10/2018: Ren'py
*
* Sample games: https://vndb.org/v19843 https://vndb.org/v12038 and many more OELVNs
*
* Uses CPython, and links to python27.dll. PyUicodeUCS2_Format is the function used to process text.
* first argument. offset 0x18 from that is a wchar_t* to the actual string
* ebx seems to work well as the split param, not sure why
*/
bool InsertRenpyHook()
{
wchar_t python[] = L"python2X.dll", libpython[] = L"libpython2.X.dll";
for (wchar_t* name : { python, libpython })
{
wchar_t* pos = wcschr(name, L'X');
for (int pythonMinorVersion = 0; pythonMinorVersion <= 8; ++pythonMinorVersion)
{
*pos = L'0' + pythonMinorVersion;
if (HMODULE module = GetModuleHandleW(name))
{
wcscpy_s(spDefault.exportModule, name);
HookParam hp = {};
hp.address = (uintptr_t)GetProcAddress(module, "PyUnicodeUCS2_Format");
if (!hp.address)
{
ConsoleOutput("Textractor: Ren'py failed: failed to find PyUnicodeUCS2_Format");
return false;
}
hp.offset = pusha_rcx_off -4; // rcx
hp.index = 0x18;
hp.length_offset = 0;
//hp.split = pusha_rsp_off -4;
hp.type = USING_STRING | USING_UNICODE | NO_CONTEXT | DATA_INDIRECT /* | USING_SPLIT*/;
hp.filter_fun = [](LPVOID data, DWORD *size, HookParam *, BYTE)
{
static std::wstring prevText;
auto text = reinterpret_cast<LPWSTR>(data);
auto len = reinterpret_cast<size_t *>(size);
if (cpp_wcsnstr(text, L"%", *len/sizeof(wchar_t)))
return false;
if (cpp_wcsnstr(text, L"{", *len/sizeof(wchar_t))) {
WideStringCharReplacer(text, len, L"{i}", 3, L'\'');
WideStringCharReplacer(text, len, L"{/i}", 4, L'\'');
WideStringFilterBetween(text, len, L"{", 1, L"}", 1);
}
WideStringFilter(text, len, L"^", 2); // remove ^ followed by 1 char
WideCharReplacer(text, len, L'\n', L' ');
if (prevText.length()==*len/sizeof(wchar_t) && prevText.find(text, 0, *len/sizeof(wchar_t))!=std::string::npos) // Check if the string is the same as the previous one
return false;
prevText.assign(text, *len/sizeof(wchar_t));
return true;
};
NewHook(hp, "Ren'py");
return true;
}
}
}
ConsoleOutput("Textractor: Ren'py failed: failed to find python2X.dll");
return false;
}
int getGodoStringLength(uintptr_t stack, uintptr_t data) {
int len = *(int*)(data - 4);
len--;
int checkLength = len > 0 && len < PIPE_BUFFER_SIZE ? len : 0;
//检查是否为错误的unicode字符
for (size_t i = 0; i < checkLength; i++)
{
if (*(WORD*)(data + i * 2) == 0x0)
return 0;
}
return checkLength * 2;
}
//BY:IOV
bool InsertGodotHook_X64() {
const BYTE bytes[] = { 0x8B,0x40,0xFC,0x83,0xF8,0x01,0x83,0xD0,0xFF,0x41,0x39,0xC6 };
ULONG64 range = min(processStopAddress - processStartAddress, X64_MAX_REL_ADDR);
for (auto addr : Util::SearchMemory(bytes, sizeof(bytes), PAGE_EXECUTE, processStartAddress, processStartAddress + range)) {
HookParam myhp = {};
myhp.address = addr;
myhp.type = USING_STRING | USING_UNICODE | NO_CONTEXT; // /HQ 不使用上下文区分 把所有线程的文本都提取
//myhp.padding = 0xc;//[esp+4]+padding
// data_offset
myhp.offset = -0xC-4;//RCX
myhp.length_fun = getGodoStringLength;
char nameForUser[HOOK_NAME_SIZE] = "RichTextLabel_add_text";
NewHook(myhp, nameForUser);
ConsoleOutput("Insert: Godot_add_text_X64 Hook ");
return true;
}
ConsoleOutput("vnreng:Godot_x64: pattern not found");
return false;
} bool InsertGodotHook2_X64() {
//by Blu3train
/*
* Sample games:
* https://vndb.org/r109138
*/
const BYTE bytes[] = {
0x48, 0x8B, 0x94, 0x24, XX4, // mov rdx,[rsp+000001C0] <- hook here
0x4C, 0x89, 0xE1, // mov rcx,r12
0xE8, XX4, // call NULL-Windows.exe+D150
0x49, 0x8B, 0x06, // mov rax,[r14]
0x48, 0x85, 0xC0, // test rax,rax
0x0F, 0x85, XX4 // jne NULL-Windows.exe+A359D4
};
ULONG64 range = min(processStopAddress - processStartAddress, X64_MAX_REL_ADDR);
for (auto addr : Util::SearchMemory(bytes, sizeof(bytes), PAGE_EXECUTE, processStartAddress, processStartAddress + range)) {
HookParam hp = {};
hp.address = addr;
hp.offset = pusha_rcx_off -4; //RCX
hp.type = USING_STRING | USING_UNICODE;
ConsoleOutput("vnreng: INSERT Godot2_x64 Hook ");
NewHook(hp, "Godot2_x64");
return true;
}
ConsoleOutput("vnreng:Godot2_x64: pattern not found");
return false;
}
bool InsertGodotHooks_X64()
{
return InsertGodotHook2_X64();
}
bool InsertRenpy3Hook()
{
//by Blu3train
/*
* Sample games:
* https://vndb.org/v45820
* https://vndb.org/v26318
* https://vndb.org/v39954
* https://vndb.org/r110220
* https://vndb.org/r114981
* https://vndb.org/v33647
* https://vndb.org/r73160
* https://vndb.org/v44518
* https://vndb.org/v31994
* https://vndb.org/r115756
*/
wchar_t python[] = L"python3X.dll", libpython[] = L"libpython3.X.dll";
for (wchar_t* name : { python, libpython })
{
wchar_t* pos = wcschr(name, L'X');
for (int pythonMinorVersion = 0; pythonMinorVersion <= 9; ++pythonMinorVersion)
{
*pos = L'0' + pythonMinorVersion;
if (HMODULE module = GetModuleHandleW(name))
{
wcscpy_s(spDefault.exportModule, name);
HookParam hp = {};
hp.address = (uintptr_t)GetProcAddress(module, "PyUnicode_Format");
if (!hp.address)
{
ConsoleOutput("Textractor: Ren'py 3 failed: failed to find PyUnicode_Format");
return false;
}
hp.offset = pusha_rcx_off -4; // rcx
hp.padding = 0x48;
hp.length_offset = 0;
hp.text_fun = [](DWORD rsp_base, HookParam *pHp, BYTE, DWORD* data, DWORD* split, DWORD* count)
{
uint64_t r10 = regof(r10, rsp_base);
uint64_t r11 = regof(r11, rsp_base);
if (r10==0x03FF || r11==0x03FF) {
uint64_t rcx = regof(rcx, rsp_base);
BYTE unicode = !(*(BYTE*)(rcx + 0x20) & 0x40); // [rcx+0x20) bit 0x40 == 0
if (unicode) {
*data += 0x48; //padding
*count = wcslen((wchar_t*)*data) * sizeof(wchar_t);
return;
}
}
*count = 0;
};
hp.type = USING_STRING | USING_UNICODE | NO_CONTEXT;
hp.filter_fun = [](LPVOID data, DWORD *size, HookParam *, BYTE)
{
auto text = reinterpret_cast<LPWSTR>(data);
auto len = reinterpret_cast<size_t *>(size);
if (cpp_wcsnstr(text, L"%", *len/sizeof(wchar_t)))
return false;
if (cpp_wcsnstr(text, L"{", *len/sizeof(wchar_t))) {
WideStringCharReplacer(text, len, L"{i}", 3, L'\'');
WideStringCharReplacer(text, len, L"{/i}", 4, L'\'');
WideStringFilterBetween(text, len, L"{", 1, L"}", 1);
}
//CP_OEMCP -The current system OEM code page
WideCharToMultiByte(CP_OEMCP, 0, text, -1, text_buffer, 0x1000, NULL, NULL);
text_buffer_length = *len/sizeof(wchar_t); // saved for not unicode hook
return true;
};
NewHook(hp, "Ren'py 3 unicode");
hp.address += 6;
hp.padding = 0x30;
hp.text_fun = [](DWORD rsp_base, HookParam *pHp, BYTE, DWORD* data, DWORD* split, DWORD* count)
{
uint64_t r10 = regof(r10, rsp_base);
uint64_t r11 = regof(r11, rsp_base);
if (r10==0x03FF || r11==0x03FF) {
uint64_t rcx = regof(rcx, rsp_base);
BYTE unicode = !(*(BYTE*)(rcx + 0x20) & 0x40); // [rcx+0x20) bit 0x40 == 0
*data += unicode ? 0x48 : 0x30; //padding
*count = ::strlen((char*)*data);
if (!cpp_strnstr((char*)*data, "%", *count)) // not garbage
return;
}
*count = 0;
};
hp.type = USING_STRING | NO_CONTEXT;
hp.filter_fun = [](LPVOID data, DWORD *size, HookParam *, BYTE)
{
auto text = reinterpret_cast<LPSTR>(data);
auto len = reinterpret_cast<size_t *>(size);
if (text[0]!=0 && text[1]==0) {
// text from unicode hook
*len = text_buffer_length;
::memmove(text, text_buffer, *len);
}
if (cpp_strnstr(text, "%", *len))
return false;
if (cpp_strnstr(text, "{", *len)) {
StringCharReplacer(text, len, "{i}", 3, L'\'');
StringCharReplacer(text, len, "{/i}", 4, L'\'');
StringFilterBetween(text, len, "{", 1, "}", 1);
}
return true;
};
NewHook(hp, "Ren'py 3");
return true;
}
}
}
ConsoleOutput("Textractor: Ren'py 3 failed: failed to find python3X.dll");
return false;
}
bool InsertLucaSystemHook()
{
//by Blu3train
/*
* Sample games:
* https://vndb.org/r108105
*/
const BYTE bytes[] = {
0xCC, // int 3
0x48, XX4, // mov [rsp+18],rbx <- hook here
0x55, // push rbp
0x56, // push rsi
0x57, // push rdi
0x41, 0x54, // push r12
0x41, 0x55, // push r13
0x41, 0x56, // push r14
0x41, 0x57, // push r15
0x48, 0x8D, 0xAC, 0x24, XX4, // lea rbp,[rsp-00003810]
0xB8, XX4 // mov eax,00003910
};
ULONG64 range = min(processStopAddress - processStartAddress, X64_MAX_REL_ADDR);
for (auto addr : Util::SearchMemory(bytes, sizeof(bytes), PAGE_EXECUTE, processStartAddress, processStartAddress + range)) {
HookParam hp = {};
hp.address = addr + 1;
hp.offset = pusha_rdx_off -4; //RDX
hp.filter_fun = [](LPVOID data, DWORD *size, HookParam *, BYTE)
{
auto text = reinterpret_cast<LPWSTR>(data);
auto len = reinterpret_cast<size_t *>(size);
if (text[0] == L'\x3000') { //removes space at the beginning of the sentence
*len -= 2;
::memmove(text, text + 1, *len);
}
if ( *text == L'@' ) //Name in square brackets instead of '@'
if ( wchar_t *match2 = cpp_wcsnchr(text+1, L'@', *len/2-1) ) {
*text = L'[';
*match2 = L']';
}
WideStringFilterBetween(text, len, L"$C(", 3, L")", 1);
WideStringFilter(text, len, L"$A", 3); // remove $A followed by 1 char
WideStringCharReplacer(text, len, L"$d", 2, L'\n');
WideCharFilter(text, len, L'\xFF3F');
//ruby
WideStringFilter(text, len, L"$[", 2);
WideStringFilterBetween(text, len, L"$/", 2, L"$]", 2);
return true;
};
hp.type = USING_UNICODE | USING_STRING;
ConsoleOutput("vnreng: INSERT LucaSystem Hook ");
NewHook(hp, "LucaSystem");
return true;
}
ConsoleOutput("vnreng:LucaSystem: pattern not found");
return false;
}
bool InsertArtemisHook() {
//by Blu3train
/*
* Sample games:
* https://vndb.org/v45247
*/
const BYTE bytes[] = {
0xCC, // int 3
0x40, 0x57, // push rdi <- hook here
0x48, 0x83, 0xEC, 0x40, // sub rsp,40
0x48, 0xC7, 0x44, 0x24, 0x30, XX4, // mov qword ptr [rsp+30],FFFFFFFFFFFFFFFE
0x48, 0x89, 0x5C, 0x24, 0x50 // mov [rsp+50],rbx
};
ULONG64 range = min(processStopAddress - processStartAddress, X64_MAX_REL_ADDR);
for (auto addr : Util::SearchMemory(bytes, sizeof(bytes), PAGE_EXECUTE, processStartAddress, processStartAddress + range)) {
HookParam hp = {};
hp.address = addr + 1;
hp.offset = -0x44 -4; //RDI
hp.type = USING_STRING | USING_UTF8 | NO_CONTEXT;
ConsoleOutput("vnreng: INSERT Artemis Hook ");
NewHook(hp, "Artemis");
return true;
}
ConsoleOutput("vnreng:Artemis: pattern not found");
return false;
}
bool InsertKiriKiriZHook()
{
//by Blu3train
/*
* Sample games:
* RJ351843
*/
const BYTE bytes[] = {
0xCC, // int 3
0x4C, 0x89, 0x44, 0x24, 0x18, // mov [rsp+18],r8 <- hook here
0x48, 0x89, 0x54, 0x24, 0x10, // mov [rsp+10],rdx
0x53, // push rbx
0x56, // push rsi
0x57, // push rdi
0x41, 0x54, // push r12
0x41, 0x55, // push r13
0x41, 0x56, // push r14
0x41, 0x57, // push r15
0x48, 0x83, 0xEC, 0x40, // sub rsp,40
0x48, 0xC7, 0x44, 0x24, 0x30, 0xFE, 0xFF, 0xFF, 0xFF // mov qword ptr [rsp+30],FFFFFFFFFFFFFFFE
};
ULONG64 range = min(processStopAddress - processStartAddress, X64_MAX_REL_ADDR);
for (auto addr : Util::SearchMemory(bytes, sizeof(bytes), PAGE_EXECUTE, processStartAddress, processStartAddress + range)) {
HookParam hp = {};
hp.address = addr + 1;
hp.offset = -0x1C -4; //RCX
hp.index = 0x18;
hp.length_offset = 1;
hp.type = USING_UNICODE | DATA_INDIRECT;
ConsoleOutput("vnreng: INSERT KiriKiriZ Hook ");
NewHook(hp, "KiriKiriZ");
return true;
}
ConsoleOutput("vnreng:KiriKiriZ: pattern not found");
return false;
}
bool UnsafeDetermineEngineType()
{
if (Util::CheckFile(L"PPSSPP*.exe") && FindPPSSPP()) return true;
if (Util::CheckFile(L"*.pck")) {
return InsertGodotHooks_X64();
}
for (const wchar_t* moduleName : { (const wchar_t*)NULL, L"node.dll", L"nw.dll" }) if (InsertV8Hook(GetModuleHandleW(moduleName))) return true;
if (GetModuleHandleW(L"GameAssembly.dll")) // TODO: is there a way to autofind hook?
{
ConsoleOutput("Textractor: Precompiled Unity found (searching for hooks should work)");
wcscpy_s(spDefault.boundaryModule, L"GameAssembly.dll");
spDefault.padding = 20;
return true;
}
if (Util::CheckFile(L"files/*.PAK")) {
InsertLucaSystemHook();
return true;
}
if (Util::CheckFile(L"*.pfs")) {
InsertArtemisHook();
return true;
}
if (Util::CheckFile(L"*.py") && InsertRenpyHook() || InsertRenpy3Hook()) return true;
for (const wchar_t* monoName : { L"mono.dll", L"mono-2.0-bdwgc.dll" }) if (HMODULE module = GetModuleHandleW(monoName)) if (InsertMonoHooks(module)) return true;
if (Util::CheckFile(L"*.xp3") || Util::SearchResourceString(L"TVP(KIRIKIRI) Z ")) { // TVP(KIRIKIRI) Z CORE
if (InsertKiriKiriZHook())
return true;
}
for (std::wstring DXVersion : { L"d3dx9", L"d3dx10" })
if (HMODULE module = GetModuleHandleW(DXVersion.c_str())) PcHooks::hookD3DXFunctions(module);
else for (int i = 0; i < 50; ++i)
if (HMODULE module = GetModuleHandleW((DXVersion + L"_" + std::to_wstring(i)).c_str())) PcHooks::hookD3DXFunctions(module);
PcHooks::hookGDIFunctions();
PcHooks::hookGDIPlusFunctions();
return false;
}
}