#include "CatSystem.h" // jichi 5/10/2014 // See also: http://bbs.sumisora.org/read.php?tid=11044704&fpage=2 // // Old engine: グリザイアの迷宮 // 0053cc4e cc int3 // 0053cc4f cc int3 // 0053cc50 6a ff push -0x1 ; jichi: hook here // 0053cc52 68 6b486000 push .0060486b // 0053cc57 64:a1 00000000 mov eax,dword ptr fs:[0] // 0053cc5d 50 push eax // 0053cc5e 81ec 24020000 sub esp,0x224 // 0053cc64 a1 f8647600 mov eax,dword ptr ds:[0x7664f8] // 0053cc69 33c4 xor eax,esp // 0053cc6b 898424 20020000 mov dword ptr ss:[esp+0x220],eax // 0053cc72 53 push ebx // 0053cc73 55 push ebp // 0053cc74 56 push esi // 0053cc75 57 push edi // // Stack: // 0544e974 0053d593 return to .0053d593 from .0053cc50 // 0544e978 045cc820 // 0544e97c 00008dc5 : jichi: text // 0544e980 00000016 // 0544e984 0452f2e4 // 0544e988 00000000 // 0544e98c 00000001 // 0544e990 0544ea94 // 0544e994 04513840 // 0544e998 0452f2b8 // 0544e99c 04577638 // 0544e9a0 04620450 // 0544e9a4 00000080 // 0544e9a8 00000080 // 0544e9ac 004914f3 return to .004914f3 from .0055c692 // // Registers: // edx 0 // ebx 00000016 // // // New engine: イノセントガール // Stack: // 051ae508 0054e9d1 return to .0054e9d1 from .0054e310 // 051ae50c 04361650 // 051ae510 00008ca9 ; jichi: text // 051ae514 0000001a // 051ae518 04343864 // 051ae51c 00000000 // 051ae520 00000001 // 051ae524 051ae62c // 051ae528 041edc20 // 051ae52c 04343830 // 051ae530 0434a8b0 // 051ae534 0434a7f0 // 051ae538 00000080 // 051ae53c 00000080 // 051ae540 3f560000 // 051ae544 437f8000 // 051ae548 4433e000 // 051ae54c 16f60c00 // 051ae550 051ae650 // 051ae554 042c4c20 // 051ae558 0000002c // 051ae55c 00439bc5 return to .00439bc5 from .0043af60 // // Registers & stack: // Scenario: // eax 04361650 // ecx 04357640 // edx 04343864 // ebx 0000001a // esp 051ae508 // ebp 00008169 // esi 04357640 // edi 051ae62c // eip 0054e310 .0054e310 // // 051ae508 0054e9d1 return to .0054e9d1 from .0054e310 // 051ae50c 04361650 // 051ae510 00008169 // 051ae514 0000001a // 051ae518 04343864 // 051ae51c 00000000 // 051ae520 00000001 // 051ae524 051ae62c // 051ae528 041edc20 // 051ae52c 04343830 // 051ae530 0434a8b0 // 051ae534 0434a7f0 // 051ae538 00000080 // 051ae53c 00000080 // 051ae540 3f560000 // 051ae544 437f8000 // 051ae548 4433e000 // 051ae54c 16f60c00 // 051ae550 051ae650 // 051ae554 042c4c20 // 051ae558 0000002c // // Name: // // eax 04362430 // ecx 17025230 // edx 0430b6e4 // ebx 0000001a // esp 051ae508 // ebp 00008179 // esi 17025230 // edi 051ae62c // eip 0054e310 .0054e310 // // 051ae508 0054e9d1 return to .0054e9d1 from .0054e310 // 051ae50c 04362430 // 051ae510 00008179 // 051ae514 0000001a // 051ae518 0430b6e4 // 051ae51c 00000000 // 051ae520 00000001 // 051ae524 051ae62c // 051ae528 041edae0 // 051ae52c 0430b6b0 // 051ae530 0434a790 // 051ae534 0434a910 // 051ae538 00000080 // 051ae53c 00000080 // 051ae540 3efa0000 // 051ae544 4483f000 // 051ae548 44322000 // 051ae54c 16f60aa0 // 051ae550 051ae650 // 051ae554 042c4c20 // 051ae558 0000002c static void SpecialHookCatSystem3(hook_stack *stack, HookParam *, uintptr_t *data, uintptr_t *split, size_t *len) { // DWORD ch = *data = *(DWORD *)(esp_base + hp->offset); // arg2 DWORD ch = *data = stack->stack[2]; *len = LeadByteTable[(ch >> 8) & 0xff]; // CODEC_ANSI_BE *split = stack->edx >> 16; } bool InsertCatSystemHook() { // DWORD search=0x95EB60F; // DWORD j,i=SearchPattern(processStartAddress,processStopAddress-processStartAddress,&search,4); // if (i==0) return; // i+=processStartAddress; // for (j=i-0x100;i>j;i--) // if (*(DWORD*)i==0xcccccccc) break; // if (i==j) return; // hp.address=i+4; // hp.offset=get_reg(regs::eax); // hp.index=4; // hp.type =CODEC_ANSI_BE|DATA_INDIRECT|USING_SPLIT|SPLIT_INDIRECT; // hp.length_offset=1; enum { beg = 0xff6acccc }; // jichi 7/12/2014: beginning of the function enum { addr_offset = 2 }; // skip two leading 0xcc ULONG addr = MemDbg::findCallerAddress((ULONG)::GetTextMetricsA, beg, processStartAddress, processStopAddress); if (!addr) { ConsoleOutput("CatSystem2: pattern not exist"); return false; } HookParam hp; hp.address = addr + addr_offset; // skip 1 push? hp.offset = get_stack(2); // text character is in arg2 // jichi 12/23/2014: Modify split for new catsystem bool newEngine = Util::CheckFile(L"cs2conf.dll"); if (newEngine) { // hp.text_fun = SpecialHookCatSystem3; // type not needed // NewHook(hp, "CatSystem3"); // ConsoleOutput("INSERT CatSystem3"); hp.type = CODEC_ANSI_BE | USING_SPLIT; hp.split = get_reg(regs::esi); ConsoleOutput("INSERT CatSystem3new"); return NewHook(hp, "CatSystem3new"); } else { BYTE check[] = {0x66, 0x83, 0xff, 0x20, // 0x20 0x0f, 0x84, XX4, 0xb8, 0x40, 0x81, 0x00, 0x00, // 0x8140 0x66, 0x3b, 0xf8}; if (MemDbg::findBytes(check, sizeof(check), addr, addr + 0x100)) { hp.split = get_stack(1); hp.offset = get_reg(regs::edx); } else { hp.split = get_reg(regs::edx); } hp.type = CODEC_ANSI_BE | USING_SPLIT; ConsoleOutput("INSERT CatSystem2"); return NewHook(hp, "CatSystem2"); } } bool InsertCatSystem2Hook() { /* * Sample games: * https://vndb.org/v26987 */ const BYTE bytes[] = { 0x38, 0x08, // cmp [eax],cl 0x0F, 0x84, XX4, // je cs2.exe+23E490 0x66, 0x66, 0x0F, 0x1F, 0x84, 0x00, XX4, // nop word ptr [eax+eax+00000000] 0x4F, // dec edi 0xC7, 0x85, XX4, XX4, // mov [ebp-000005A0],00000000 0x33, 0xF6, // xor esi,esi 0xC7, 0x85, XX4, XX4, // mov [ebp-0000057C],00000000 0x85, 0xFF // test edi,edi }; ULONG range = min(processStopAddress - processStartAddress, MAX_REL_ADDR); ULONG addr = MemDbg::findBytes(bytes, sizeof(bytes), processStartAddress, processStartAddress + range); if (!addr) { return false; } HookParam hp; hp.address = addr; hp.offset = get_reg(regs::eax); hp.type = USING_STRING | CODEC_UTF8; hp.filter_fun = [](void *data, size_t *len, HookParam *hp) { static std::regex rx(R"(\[(.+?)/.+\])"); auto _ = std::regex_replace(std::string((char *)data, *len), rx, "$1"); return write_string_overwrite(data, len, _); }; return NewHook(hp, "CatSystem2new"); } namespace { // unnamed namespace Patch { namespace Private { // String in ecx // bool __fastcall isLeadByteChar(const char *s, DWORD edx) // bool isLeadByteChar(hook_stack*s,void* data, size_t* len,uintptr_t*role) // { // auto pc=(CHAR*)s->ecx; // s->eax=(bool)((pc)&&dynsjis::isleadbyte(*pc)); // return false; // //return dynsjis::isleadstr(s); // no idea why this will cause Grisaia3 to hang // //return ::IsDBCSLeadByte(HIBYTE(testChar)); // } bool isLeadByteChar(char *s) { return s && dynsjis::isleadchar(*s); // return dynsjis::isleadstr(s); // no idea why this will cause Grisaia3 to hang // return ::IsDBCSLeadByte(HIBYTE(testChar)); } __declspec(naked) bool thiscallisLeadByteChar() { __asm { push ecx call isLeadByteChar pop ecx ret } } } // namespace Private /** * Sample game: ゆきこいめると * * This function is found by searching the following instruction: * 00511C8E 3C 81 CMP AL,0x81 * * This function is very similar to that in LC-ScriptEngine. * * Return 1 if the first byte in arg1 is leading byte else 0. * * 00511C7C CC INT3 * 00511C7D CC INT3 * 00511C7E CC INT3 * 00511C7F CC INT3 * 00511C80 8B4C24 04 MOV ECX,DWORD PTR SS:[ESP+0x4] * 00511C84 85C9 TEST ECX,ECX * 00511C86 74 2F JE SHORT .00511CB7 * 00511C88 8A01 MOV AL,BYTE PTR DS:[ECX] * 00511C8A 84C0 TEST AL,AL * 00511C8C 74 29 JE SHORT .00511CB7 * 00511C8E 3C 81 CMP AL,0x81 * 00511C90 72 04 JB SHORT .00511C96 * 00511C92 3C 9F CMP AL,0x9F * 00511C94 76 08 JBE SHORT .00511C9E * 00511C96 3C E0 CMP AL,0xE0 * 00511C98 72 1D JB SHORT .00511CB7 * 00511C9A 3C EF CMP AL,0xEF * 00511C9C 77 19 JA SHORT .00511CB7 * 00511C9E 8A41 01 MOV AL,BYTE PTR DS:[ECX+0x1] * 00511CA1 3C 40 CMP AL,0x40 * 00511CA3 72 04 JB SHORT .00511CA9 * 00511CA5 3C 7E CMP AL,0x7E * 00511CA7 76 08 JBE SHORT .00511CB1 * 00511CA9 3C 80 CMP AL,0x80 * 00511CAB 72 0A JB SHORT .00511CB7 * 00511CAD 3C FC CMP AL,0xFC * 00511CAF 77 06 JA SHORT .00511CB7 * 00511CB1 B8 01000000 MOV EAX,0x1 * 00511CB6 C3 RETN * 00511CB7 33C0 XOR EAX,EAX * 00511CB9 C3 RETN * 00511CBA CC INT3 * 00511CBB CC INT3 * 00511CBC CC INT3 * 00511CBD CC INT3 * * Sample game: Grisaia3 グリザイアの楽園 * 0050747F CC INT3 * 00507480 8B4C24 04 MOV ECX,DWORD PTR SS:[ESP+0x4] ; jichi: text in arg1 * 00507484 85C9 TEST ECX,ECX * 00507486 74 2F JE SHORT .005074B7 * 00507488 8A01 MOV AL,BYTE PTR DS:[ECX] * 0050748A 84C0 TEST AL,AL * 0050748C 74 29 JE SHORT .005074B7 * 0050748E 3C 81 CMP AL,0x81 * 00507490 72 04 JB SHORT .00507496 * 00507492 3C 9F CMP AL,0x9F * 00507494 76 08 JBE SHORT .0050749E * 00507496 3C E0 CMP AL,0xE0 * 00507498 72 1D JB SHORT .005074B7 * 0050749A 3C EF CMP AL,0xEF * 0050749C 77 19 JA SHORT .005074B7 * 0050749E 8A41 01 MOV AL,BYTE PTR DS:[ECX+0x1] * 005074A1 3C 40 CMP AL,0x40 * 005074A3 72 04 JB SHORT .005074A9 * 005074A5 3C 7E CMP AL,0x7E * 005074A7 76 08 JBE SHORT .005074B1 * 005074A9 3C 80 CMP AL,0x80 * 005074AB 72 0A JB SHORT .005074B7 * 005074AD 3C FC CMP AL,0xFC * 005074AF 77 06 JA SHORT .005074B7 * 005074B1 B8 01000000 MOV EAX,0x1 * 005074B6 C3 RETN * 005074B7 33C0 XOR EAX,EAX * 005074B9 C3 RETN * 005074BA CC INT3 * 005074BB CC INT3 * 005074BC CC INT3 * 005074BD CC INT3 * * Sample game: Grisaia1 グリザイアの果実 * 0041488A CC INT3 * 0041488B CC INT3 * 0041488C CC INT3 * 0041488D CC INT3 * 0041488E CC INT3 * 0041488F CC INT3 * 00414890 85C9 TEST ECX,ECX ; jichi: text in ecx * 00414892 74 2F JE SHORT Grisaia.004148C3 * 00414894 8A01 MOV AL,BYTE PTR DS:[ECX] * 00414896 84C0 TEST AL,AL * 00414898 74 29 JE SHORT Grisaia.004148C3 * 0041489A 3C 81 CMP AL,0x81 * 0041489C 72 04 JB SHORT Grisaia.004148A2 * 0041489E 3C 9F CMP AL,0x9F * 004148A0 76 08 JBE SHORT Grisaia.004148AA * 004148A2 3C E0 CMP AL,0xE0 * 004148A4 72 1D JB SHORT Grisaia.004148C3 * 004148A6 3C EF CMP AL,0xEF * 004148A8 77 19 JA SHORT Grisaia.004148C3 * 004148AA 8A41 01 MOV AL,BYTE PTR DS:[ECX+0x1] * 004148AD 3C 40 CMP AL,0x40 * 004148AF 72 04 JB SHORT Grisaia.004148B5 * 004148B1 3C 7E CMP AL,0x7E * 004148B3 76 08 JBE SHORT Grisaia.004148BD * 004148B5 3C 80 CMP AL,0x80 * 004148B7 72 0A JB SHORT Grisaia.004148C3 * 004148B9 3C FC CMP AL,0xFC * 004148BB 77 06 JA SHORT Grisaia.004148C3 * 004148BD B8 01000000 MOV EAX,0x1 * 004148C2 C3 RETN * 004148C3 33C0 XOR EAX,EAX * 004148C5 C3 RETN * 004148C6 CC INT3 * 004148C7 CC INT3 * 004148C8 CC INT3 */ ULONG patchEncoding(ULONG startAddress, ULONG stopAddress) { const uint8_t bytes[] = { 0x74, 0x29, // 00511c8c 74 29 je short .00511cb7 0x3c, 0x81 // 00511c8e 3c 81 cmp al,0x81 }; ULONG addr = MemDbg::findBytes(bytes, sizeof(bytes), startAddress, stopAddress); if (!addr) return false; addr = MemDbg::findEnclosingAlignedFunction(addr); if (!addr) return false; for (auto p = addr; p - addr < 20; p += ::disasm((LPCVOID)p)) if (*(WORD *)p == 0xc985) // 00414890 85C9 TEST ECX,ECX ; jichi: text in ecx return addr; // winhook::replace_fun(p, (ULONG)Private::isLeadByteChar); return 0; } } // namespace Patch /** * Sample game: ゆきこいめると * * Example prefix to skip: * 03751294 81 40 5C 70 63 81 75 83 7B 83 4E 82 CC 8E AF 82  \pc「ボクの識・ * * 033CF370 5C 6E 81 40 5C 70 63 8C 4A 82 E8 95 D4 82 BB 82 \n \pc繰り返そ・ * 033CF380 A4 81 41 96 7B 93 96 82 C9 81 41 82 B1 82 CC 8B 、、本当に、この・ * 033CF390 47 90 DF 82 CD 81 41 83 8D 83 4E 82 C8 82 B1 82 G節は、ロクなこ・ * 033CF3A0 C6 82 AA 82 C8 82 A2 81 42 00 AA 82 C8 82 A2 81 ニがない。.ェない・ * 033CF3B0 42 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 B............... * 033CF3C0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ * 033CF3D0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ * 033CF3E0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ * 033CF3F0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ * 033CF400 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ * * Sample choice texts: * * str 155 選択肢 * * 0 op01 最初から始める * * 1 select_go_tar たるひ初キスシーンを見る */ template strT ltrim(strT text) { strT lastText = nullptr; while (*text && text != lastText) { lastText = text; if (text[0] == 0x20) text++; if ((UINT8)text[0] == 0x81 && (UINT8)text[1] == 0x40) // skip space \u3000 (0x8140 in sjis) text += 2; if (text[0] == '\\') { text++; while (::islower(text[0]) || text[0] == '@') text++; } } while ((signed char)text[0] > 0 && text[0] != '[') // skip all leading ascii characters except "[" needed for ruby text++; return text; } // Remove trailing '\@' size_t rtrim(LPCSTR text) { size_t size = ::strlen(text); while (size >= 2 && text[size - 2] == '\\' && (UINT8)text[size - 1] <= 127) size -= 2; return size; } namespace ScenarioHook { namespace Private { bool isOtherText(LPCSTR text) { /* Sample game: ゆきこいめると */ return ::strcmp(text, "\x91\x49\x91\xf0\x8e\x88") == 0; /* 選択肢 */ } /** * Sample game: 果つることなき未来ヨリ * * Sample ecx: * * 03283A88 24 00 CD 02 76 16 02 00 24 00 CD 02 58 00 CD 02 $.ヘv.$.ヘX.ヘ * 03283A98 BD 2D 01 00 1C 1C 49 03 14 65 06 00 14 65 06 00 ス-.Ie.e. * this is ID, this is the same ID: 0x066514 * 03283AA8 80 64 06 00 20 8C 06 00 24 00 6C 0D 00 00 10 00 €d. ・.$.l.... * this is ID: 0x066480 * 03283AB8 C8 F1 C2 00 21 00 00 00 48 A9 75 00 E8 A9 96 00 ネ.!...Hゥu.隧・ * 03283AC8 00 00 00 00 48 80 4F 03 00 00 00 00 CC CC CC CC ....H€O....フフフフ * 03283AD8 CC CC CC CC CC CC CC CC CC CC CC CC CC CC CC CC フフフフフフフフフフフフフフフフ */ // struct ClassArgument // for ecx //{ // DWORD unknown[7], // split1, // 0x20 - 9 // split2; // 0x20 // // split1 - split2 is always 0x94 // DWORD split() const { return split1 - split2; } // // }; static bool containsNamePunct_(const char *text) { static const char *puncts[] = { "\x81\x41" /* 、 */ , "\x81\x43" /* , */ , "\x81\x42" /* 。 */ //, "\x81\x48" /* ? */ , "\x81\x49" /* ! */ , "\x81\x63" /* … */ , "\x81\x64" /* ‥ */ //, "\x81\x79" /* 【 */ //, "\x81\x7a" /* 】 */ , "\x81\x75" /* 「 */ , "\x81\x76" /* 」 */ , "\x81\x77" /* 『 */ , "\x81\x78" /* 』 */ //, "\x81\x69" /* ( */ //, "\x81\x6a" /* ) */ //, "\x81\x6f" /* { */ //, "\x81\x70" /* } */ //, "\x81\x71" /* 〈 */ //, "\x81\x72" /* 〉 */ , "\x81\x6d" /* [ */ , "\x81\x6e" /* ] */ //, "\x81\x83", /* < */ //, "\x81\x84", /* > */ , "\x81\x65" /* ‘ */ , "\x81\x66" /* ’ */ , "\x81\x67" /* “ */ , "\x81\x68" /* ” */ }; for (size_t i = 0; i < sizeof(puncts) / sizeof(*puncts); i++) if (::strstr(text, puncts[i])) return true; if (::strstr(text, "\x81\x48") /* ? */ && !::strstr(text, "\x81\x48\x81\x48\x81\x48")) /* ??? */ return true; return false; } bool guessIsNameText(const char *text, size_t size) { enum { MaximumNameSize = 0x10 }; if (!size) size = ::strlen(text); return size < MaximumNameSize && !containsNamePunct_(text); } LPSTR trimmedText; size_t trimmedSize; bool hookBefore(hook_stack *s, void *data, size_t *len, uintptr_t *role) { // static std::unordered_set hashes_; auto text = (LPSTR)s->eax; // arg1 if (!text || !*text || all_ascii(text)) return false; // Alternatively, if do not skip ascii chars, edx is always 0x4ef74 for Japanese texts // if (s->edx != 0x4ef74) // return true; trimmedText = ltrim(text); if (!trimmedText || !*trimmedText) return false; trimmedSize = rtrim(trimmedText); *role = Engine::OtherRole; // DOUT(QString::fromLocal8Bit((LPCSTR)s->esi)); // auto splitText = (LPCSTR)s->esi; // if (::strcmp(splitText, "MES_SETNAME")) // This is for scenario text with voice // if (::strcmp(splitText, "MES_SETFACE")) // if (::strcmp(splitText, "pcm")) // first scenario or history without text // return true; // auto retaddr = s->stack[1]; // caller // auto retaddr = s->stack[13]; // parent caller // auto split = *(DWORD *)s->esi; // auto split = s->esi - s->eax; // DOUT(split); // auto self = (ClassArgument *)s->ecx; // auto split = self->split(); // enum { sig = 0 }; auto self = s->ecx; if (!Engine::isAddressWritable(self)) // old cs2 game such as Grisaia self = s->stack[2]; // arg1 ULONG groupId = self; if (Engine::isAddressWritable(self)) groupId = *(DWORD *)(self + 0x20); { static ULONG minimumGroupId_ = -1; // I assume scenario thread to have minimum groupId // if (session_.addText(groupId, Engine::hashCharArray(text))) { if (groupId <= minimumGroupId_) { minimumGroupId_ = groupId; *role = Engine::ScenarioRole; if (isOtherText(text)) *role = Engine::OtherRole; else if (::isdigit(text[0])) *role = Engine::ChoiceRole; else if (trimmedText == text && !trimmedText[trimmedSize] // no prefix and suffix && guessIsNameText(trimmedText, trimmedSize)) *role = Engine::NameRole; } } std::string oldData(trimmedText, trimmedSize); strReplace(oldData, "\\n", "\n"); return write_string_overwrite(data, len, oldData); } void hookafter(hook_stack *s, void *data, size_t len) { auto newData = std::string((char *)data, len); strReplace(newData, "\n", "\\n"); if (trimmedText[trimmedSize]) newData.append(trimmedText + trimmedSize); ::strcpy(trimmedText, newData.c_str()); } } // namespace Private /** * Sample game: 果つることなき未来ヨリ * * Debugging message: * - Hook to GetGlyphOutlineA * - Find "MES_SHOW" address on the stack * Alternatively, find the address of "fes.int/flow.fes" immediately after the game is launched * - Use hardware breakpoint to find out when "MES_SHOW" is overridden * Only stop when text is written by valid scenario text. * * 00503ADE CC INT3 * 00503ADF CC INT3 * 00503AE0 8B4424 0C MOV EAX,DWORD PTR SS:[ESP+0xC] * 00503AE4 8B4C24 04 MOV ECX,DWORD PTR SS:[ESP+0x4] * 00503AE8 56 PUSH ESI * 00503AE9 FF30 PUSH DWORD PTR DS:[EAX] * 00503AEB E8 102F1600 CALL Hatsumir.00666A00 ; jichi: text in eax after this call * 00503AF0 BE 18058900 MOV ESI,Hatsumir.00890518 ; ASCII "fes.int/flow.fes" * 00503AF5 8BC8 MOV ECX,EAX ; jichi: esi is the target location * 00503AF7 2BF0 SUB ESI,EAX * 00503AF9 8DA424 00000000 LEA ESP,DWORD PTR SS:[ESP] * 00503B00 8A11 MOV DL,BYTE PTR DS:[ECX] * 00503B02 8D49 01 LEA ECX,DWORD PTR DS:[ECX+0x1] * 00503B05 88540E FF MOV BYTE PTR DS:[ESI+ECX-0x1],DL ; jichi: target location modified here * 00503B09 84D2 TEST DL,DL * 00503B0B ^75 F3 JNZ SHORT Hatsumir.00503B00 * 00503B0D 8B4C24 0C MOV ECX,DWORD PTR SS:[ESP+0xC] * 00503B11 50 PUSH EAX * 00503B12 68 18058900 PUSH Hatsumir.00890518 ; ASCII "fes.int/flow.fes" * 00503B17 8B89 B4000000 MOV ECX,DWORD PTR DS:[ECX+0xB4] * 00503B1D E8 EE030B00 CALL Hatsumir.005B3F10 * 00503B22 B8 02000000 MOV EAX,0x2 * 00503B27 5E POP ESI * 00503B28 C2 1000 RETN 0x10 * 00503B2B CC INT3 * 00503B2C CC INT3 * 00503B2D CC INT3 * 00503B2E CC INT3 * * EAX 0353B1A0 ; jichi: text here * ECX 00D86D08 * EDX 0004EF74 * EBX 00012DB2 * ESP 0525EBAC * EBP 0525ED6C * ESI 00D86D08 * EDI 00000000 * EIP 00503AF0 Hatsumir.00503AF0 * * 0525EBAC 00D86D08 * 0525EBB0 0066998E RETURN to Hatsumir.0066998E * 0525EBB4 00D86D08 * 0525EBB8 00B16188 * 0525EBBC 035527D8 * 0525EBC0 0525EBE4 * 0525EBC4 00B16188 * 0525EBC8 00D86D08 * 0525EBCC 0525F62B ASCII "ript.kcs" * 0525EBD0 00000004 * 0525EBD4 00000116 * 0525EBD8 00000003 * 0525EBDC 00000003 * 0525EBE0 00665C08 RETURN to Hatsumir.00665C08 * 0525EBE4 CCCCCCCC * 0525EBE8 0525F620 ASCII "kcs.int/sscript.kcs" * 0525EBEC 00694D94 Hatsumir.00694D94 * 0525EBF0 004B278F RETURN to Hatsumir.004B278F from Hatsumir.00666CA0 * 0525EBF4 B3307379 * 0525EBF8 0525ED04 * 0525EBFC 00B16188 * 0525EC00 0525ED04 * 0525EC04 00B16188 * 0525EC08 00CC5440 * 0525EC0C 02368938 * 0525EC10 0069448C ASCII "%s/%s" * 0525EC14 00B45B18 ASCII "kcs.int" * 0525EC18 00000001 * 0525EC1C 023741E0 * 0525EC20 0000000A * 0525EC24 0049DBB3 RETURN to Hatsumir.0049DBB3 from Hatsumir.00605A84 * 0525EC28 72637373 * 0525EC2C 2E747069 * 0525EC30 0073636B Hatsumir.0073636B * 0525EC34 0525ED04 * 0525EC38 0053ECDE RETURN to Hatsumir.0053ECDE from Hatsumir.004970C0 * 0525EC3C 0525EC80 * 0525EC40 023D9FB8 * * Alternative ruby hook: * It will hook to the beginning of the Ruby processing function, which is not better than the current approach. * http://lab.aralgood.com/index.php?mid=board_lecture&search_target=title_content&search_keyword=CS&document_srl=1993027 * * Sample game: Grisaia3 グリザイアの楽園 * * 004B00CB CC INT3 * 004B00CC CC INT3 * 004B00CD CC INT3 * 004B00CE CC INT3 * 004B00CF CC INT3 * 004B00D0 8B4424 0C MOV EAX,DWORD PTR SS:[ESP+0xC] * 004B00D4 8B08 MOV ECX,DWORD PTR DS:[EAX] * 004B00D6 56 PUSH ESI * 004B00D7 51 PUSH ECX * 004B00D8 8B4C24 0C MOV ECX,DWORD PTR SS:[ESP+0xC] * 004B00DC E8 7F191300 CALL .005E1A60 * 004B00E1 BE D0E87B00 MOV ESI,.007BE8D0 * 004B00E6 8BC8 MOV ECX,EAX * 004B00E8 2BF0 SUB ESI,EAX * 004B00EA 8D9B 00000000 LEA EBX,DWORD PTR DS:[EBX] * 004B00F0 8A11 MOV DL,BYTE PTR DS:[ECX] * 004B00F2 88140E MOV BYTE PTR DS:[ESI+ECX],DL * 004B00F5 41 INC ECX * 004B00F6 84D2 TEST DL,DL * 004B00F8 ^75 F6 JNZ SHORT .004B00F0 * 004B00FA 8B5424 0C MOV EDX,DWORD PTR SS:[ESP+0xC] * 004B00FE 8B8A B4000000 MOV ECX,DWORD PTR DS:[EDX+0xB4] * 004B0104 50 PUSH EAX * 004B0105 68 D0E87B00 PUSH .007BE8D0 * 004B010A E8 818D0600 CALL .00518E90 * 004B010F B8 02000000 MOV EAX,0x2 * 004B0114 5E POP ESI * 004B0115 C2 1000 RETN 0x10 * 004B0118 CC INT3 * 004B0119 CC INT3 * 004B011A CC INT3 * 004B011B CC INT3 * 004B011C CC INT3 * * Sample game: Grisaia1 グリザイアの果実 * 00498579 CC INT3 * 0049857A CC INT3 * 0049857B CC INT3 * 0049857C CC INT3 * 0049857D CC INT3 * 0049857E CC INT3 * 0049857F CC INT3 * 00498580 8B4424 0C MOV EAX,DWORD PTR SS:[ESP+0xC] * 00498584 8B08 MOV ECX,DWORD PTR DS:[EAX] ; jichi: ecx is no longer a pointer * 00498586 8B4424 04 MOV EAX,DWORD PTR SS:[ESP+0x4] * 0049858A 56 PUSH ESI * 0049858B E8 10920500 CALL Grisaia.004F17A0 * 00498590 BE D89C7600 MOV ESI,Grisaia.00769CD8 ; ASCII "bgm01" * 00498595 8BC8 MOV ECX,EAX * 00498597 2BF0 SUB ESI,EAX * 00498599 8DA424 00000000 LEA ESP,DWORD PTR SS:[ESP] * 004985A0 8A11 MOV DL,BYTE PTR DS:[ECX] * 004985A2 88140E MOV BYTE PTR DS:[ESI+ECX],DL * 004985A5 41 INC ECX * 004985A6 84D2 TEST DL,DL * 004985A8 ^75 F6 JNZ SHORT Grisaia.004985A0 * 004985AA 8B4C24 0C MOV ECX,DWORD PTR SS:[ESP+0xC] * 004985AE 8B91 B4000000 MOV EDX,DWORD PTR DS:[ECX+0xB4] * 004985B4 50 PUSH EAX * 004985B5 68 D89C7600 PUSH Grisaia.00769CD8 ; ASCII "bgm01" * 004985BA 52 PUSH EDX * 004985BB E8 701C0600 CALL Grisaia.004FA230 * 004985C0 B8 02000000 MOV EAX,0x2 * 004985C5 5E POP ESI * 004985C6 C2 1000 RETN 0x10 * 004985C9 CC INT3 * 004985CA CC INT3 * 004985CB CC INT3 * 004985CC CC INT3 * 004985CD CC INT3 */ bool attach(ULONG startAddress, ULONG stopAddress, HookParamType code) { const uint8_t bytes[] = { 0xe8, XX4, // 004b00dc e8 7f191300 call .005e1a60 ; jichi: hook after here 0xbe, XX4, // 004b00e1 be d0e87b00 mov esi,.007be8d0 0x8b, 0xc8, // 004b00e6 8bc8 mov ecx,eax 0x2b, 0xf0 // 004b00e8 2bf0 sub esi,eax // XX2, XX, 0x00,0x00,0x00 // 004b00ea 8d9b 00000000 lea ebx,dword ptr ds:[ebx] }; ULONG addr = MemDbg::findBytes(bytes, sizeof(bytes), startAddress, stopAddress); if (addr == 0) return false; HookParam hp; hp.address = addr + 5; hp.type = USING_STRING | EMBED_ABLE; if (code) hp.type |= code; else hp.type |= EMBED_DYNA_SJIS; hp.hook_before = Private::hookBefore; hp.hook_after = Private::hookafter; hp.hook_font = F_GetGlyphOutlineA; hp.filter_fun = [](void *data, size_t *len, HookParam *hp) { static std::regex rx(R"(\[(.+?)/.+\])"); auto _ = std::regex_replace(std::string((char *)data, *len), rx, "$1"); return write_string_overwrite(data, len, _); }; static ULONG p; p = Patch::patchEncoding(startAddress, stopAddress); if (p) { hp.type |= EMBED_DYNA_SJIS; hp.hook_font = F_GetGlyphOutlineA; patch_fun = []() { if (*(WORD *)p == 0xc985) { // test ecx,ecx , thiscall ReplaceFunction((PVOID)p, (PVOID)(ULONG)Patch::Private::thiscallisLeadByteChar); } else ReplaceFunction((PVOID)p, (PVOID)(ULONG)Patch::Private::isLeadByteChar); }; } return NewHook(hp, "EmbedCS2"); } } } // namespace ScenarioHook bool CatSystem::attach_function() { HookParamType code = CODEC_ANSI_LE; auto b1 = InsertCatSystemHook(); if (!b1) { b1 |= InsertCatSystem2Hook(); code = CODEC_UTF8; } auto embed = ScenarioHook::attach(processStartAddress, processStopAddress, code); b1 |= embed; return b1; }