From 4508c6adb76df662439eb067b566953c482bfe14 Mon Sep 17 00:00:00 2001 From: Archie_UwU Date: Mon, 8 Apr 2024 17:55:04 +0200 Subject: [PATCH] Experimental x86 support - Added experimental x86 support - Known issue: Stage 2 hooks fail - Known issue: RValue Array offset isn't found - Fixed a number of warnings in x86 - Fixed disassembler not taking into account the current architecture - Fixed edgecases where the YYRunnerInterface stack base would be computed improperly - Disabled internal structure access's struct size checks in x86 --- ExamplePlugin/include/YYToolkit/Shared.hpp | 64 +++- README.md | 2 +- YYToolkit/YYToolkit.vcxproj.filters | 8 +- .../YYTK/Module Interface/Interface.cpp | 2 +- .../Module Internals/GameMaker/Generic.cpp | 345 ++++++++++++++++- .../Module Internals/GameMaker/VM-only.cpp | 4 +- .../Module Internals/GameMaker/YYC-only.cpp | 351 +++++++++++++++++- .../YYTK/Module Internals/Hooks/Hooks.cpp | 27 +- YYToolkit/source/YYTK/Shared.hpp | 56 ++- YYToolkit/source/YYTK/Tool.hpp | 9 +- 10 files changed, 826 insertions(+), 42 deletions(-) diff --git a/ExamplePlugin/include/YYToolkit/Shared.hpp b/ExamplePlugin/include/YYToolkit/Shared.hpp index 16631db..71de22f 100644 --- a/ExamplePlugin/include/YYToolkit/Shared.hpp +++ b/ExamplePlugin/include/YYToolkit/Shared.hpp @@ -9,7 +9,15 @@ #define YYTK_MAJOR 3 #define YYTK_MINOR 2 -#define YYTK_PATCH 0 +#define YYTK_PATCH 4 + +#ifndef YYTK_CPP_VERSION +#ifndef _MSVC_LANG +#define YYTK_CPP_VERSION __cplusplus +#else +#define YYTK_CPP_VERSION _MSVC_LANG +#endif // _MSVC_LANG +#endif // YYTK_CPP_VERSION #include #include @@ -200,26 +208,29 @@ namespace YYTK IN const char* Value ); +#if YYTK_CPP_VERSION > 202002L RValue( IN const char8_t* Value ); - +#endif // YYTK_CPP_VERSION RValue( IN std::string_view Value ); +#if YYTK_CPP_VERSION > 202002L RValue( IN std::u8string_view Value ); - +#endif // YYTK_CPP_VERSION RValue( IN const std::string& Value ); +#if YYTK_CPP_VERSION > 202002L RValue( IN const std::u8string& Value ); - +#endif // YYTK_CPP_VERSION RValue( IN std::string_view Value, IN YYTKInterface* Interface @@ -1851,6 +1862,11 @@ namespace YYTK IN int32_t InstanceID, OUT CInstance*& Instance ) = 0; + + virtual Aurie::AurieStatus InvokeWithObject( + IN const RValue& Object, + IN std::function Method + ) = 0; }; #if YYTK_DEFINE_INTERNAL @@ -1952,7 +1968,9 @@ namespace YYTK T* m_Last; int32_t m_Count; }; +#ifdef _WIN64 static_assert(sizeof(LinkedList) == 0x18); +#endif // _WIN64 enum eBuffer_Type : int32_t { @@ -2002,7 +2020,9 @@ namespace YYTK virtual uint8_t* Compress(int _offset, int _size, uint32_t& resultSize) = 0; virtual uint8_t* Decompress(uint32_t& resultSize) = 0; }; +#ifdef _WIN64 static_assert(sizeof(IBuffer) == 0x8); +#endif // _WIN64 struct CLayerElementBase { @@ -2024,14 +2044,18 @@ namespace YYTK CLayerElementBase* m_Blink; }; }; +#ifdef _WIN64 static_assert(sizeof(CLayerElementBase) == 0x30); +#endif // _WIN64 struct CLayerInstanceElement : CLayerElementBase { int32_t m_InstanceID; CInstance* m_Instance; }; +#ifdef _WIN64 static_assert(sizeof(CLayerInstanceElement) == 0x40); +#endif // _WIN64 struct CLayerSpriteElement : CLayerElementBase { @@ -2049,7 +2073,9 @@ namespace YYTK float m_X; float m_Y; }; +#ifdef _WIN64 static_assert(sizeof(CLayerSpriteElement) == 0x68); +#endif // _WIN64 __declspec(align(8)) struct CLayer { @@ -2075,7 +2101,9 @@ namespace YYTK CLayer* m_Blink; PVOID m_GCProxy; }; +#ifdef _WIN64 static_assert(sizeof(CLayer) == 0xA0); +#endif // _WIN64 // A representation of a room, as from the data.win file struct YYRoom @@ -2112,7 +2140,9 @@ namespace YYTK float m_PhysicsGravityY; float m_PhysicsPixelToMeters; }; +#ifdef _WIN64 static_assert(sizeof(YYRoom) == 0x58); +#endif // _WIN64 // Note: this is not how RValues store arrays template @@ -2121,7 +2151,9 @@ namespace YYTK int32_t Length; T* Array; }; +#ifdef _WIN64 static_assert(sizeof(CArrayStructure) == 0x10); +#endif // _WIN64 // Seems to be mostly stable, some elements at the end are however omitted struct CRoom @@ -2173,7 +2205,9 @@ namespace YYTK int32_t m_EffectLayerIdCount; int32_t m_EffectLayerIdMax; }; +#ifdef _WIN64 static_assert(sizeof(CRoom) == 0x218); +#endif // _WIN64 struct CInstanceBase { @@ -2181,7 +2215,9 @@ namespace YYTK RValue* m_YYVars; }; +#ifdef _WIN64 static_assert(sizeof(CInstanceBase) == 0x10); +#endif // _WIN64 enum EJSRetValBool : int32_t { @@ -2302,7 +2338,9 @@ namespace YYTK int32_t m_RValueInitType; int32_t m_CurrentSlot; }; +#ifdef _WIN64 static_assert(sizeof(YYObjectBase) == 0x88); +#endif // _WIN64 struct CScriptRef : YYObjectBase { @@ -2316,7 +2354,9 @@ namespace YYTK PVOID m_Construct; const char* m_Tag; }; +#ifdef _WIN64 static_assert(sizeof(CScriptRef) == 0xE0); +#endif // _WIN64 struct CPhysicsDataGM { @@ -2334,14 +2374,18 @@ namespace YYTK float m_PhysicsFriction; int m_PhysicsVertexCount; }; +#ifdef _WIN64 static_assert(sizeof(CPhysicsDataGM) == 0x30); +#endif // _WIN64 struct CEvent { CCode* m_Code; int32_t m_OwnerObjectID; }; +#ifdef _WIN64 static_assert(sizeof(CEvent) == 0x10); +#endif // _WIN64 struct CObjectGM { @@ -2359,13 +2403,17 @@ namespace YYTK int32_t m_Mask; int32_t m_ID; }; +#ifdef _WIN64 static_assert(sizeof(CObjectGM) == 0x98); +#endif // _WIN64 struct GCObjectContainer : YYObjectBase { CHashMap* m_YYObjectMap; }; +#ifdef _WIN64 static_assert(sizeof(GCObjectContainer) == 0x90); +#endif // _WIN64 struct YYRECT { @@ -2418,7 +2466,9 @@ namespace YYTK CInstance* m_Flink; CInstance* m_Blink; }; +#ifdef _WIN64 static_assert(sizeof(CInstanceInternal) == 0xF8); +#endif // _WIN64 struct CInstance : YYObjectBase { @@ -2442,7 +2492,9 @@ namespace YYTK public: CInstanceInternal Members; } Unmasked; +#ifdef _WIN64 static_assert(sizeof(Unmasked) == 0x100); +#endif // _WIN64 // 2022.1 => 2023.1 (may be used later, haven't checked) struct @@ -2453,7 +2505,9 @@ namespace YYTK public: CInstanceInternal Members; } Masked; +#ifdef _WIN64 static_assert(sizeof(Masked) == 0x108); +#endif // _WIN64 }; public: @@ -2471,7 +2525,9 @@ namespace YYTK }; // sizeof(0x1A8) is for PreMasked instances // sizeof(0x1B0) is for Masked instances +#ifdef _WIN64 static_assert(sizeof(CInstance) == 0x1A8 || sizeof(CInstance) == 0x1B0); +#endif // _WIN64 #endif // YYTK_DEFINE_INTERNAL } diff --git a/README.md b/README.md index 5bd34b0..697708d 100644 --- a/README.md +++ b/README.md @@ -33,7 +33,7 @@ YYTK is split on two branches, that being [Legacy](https://github.com/AurieFrame | GM 2023.x | ❌ | ✅ | | GM 2024.x beta | ❌ | ✅ | | VM runner support | ✅ | 🟡 | -| x86 runner support | ✅ | ❌ | +| x86 runner support | ✅ | 🟡 | | x64 runner support | 🟡 | ✅ | | Active Development | ❌ | ✅ | | Accessing global variables | ✅ | ✅ | diff --git a/YYToolkit/YYToolkit.vcxproj.filters b/YYToolkit/YYToolkit.vcxproj.filters index f874e7d..ab5410d 100644 --- a/YYToolkit/YYToolkit.vcxproj.filters +++ b/YYToolkit/YYToolkit.vcxproj.filters @@ -24,9 +24,6 @@ Zdrojové soubory - - Zdrojové soubory - Zdrojové soubory @@ -36,10 +33,13 @@ Zdrojové soubory + + Zdrojové soubory + Zdrojové soubory - + Zdrojové soubory diff --git a/YYToolkit/source/YYTK/Module Interface/Interface.cpp b/YYToolkit/source/YYTK/Module Interface/Interface.cpp index 1d226b6..de3a7f2 100644 --- a/YYToolkit/source/YYTK/Module Interface/Interface.cpp +++ b/YYToolkit/source/YYTK/Module Interface/Interface.cpp @@ -1138,7 +1138,7 @@ namespace YYTK return AURIE_MODULE_INTERNAL_ERROR; // Prevent ourselves from reading out-of-bounds - if (Index >= *m_BuiltinCount) + if (static_cast(Index) >= *m_BuiltinCount) return AURIE_INVALID_PARAMETER; VariableInformation = &m_BuiltinArray[Index]; diff --git a/YYToolkit/source/YYTK/Module Internals/GameMaker/Generic.cpp b/YYToolkit/source/YYTK/Module Internals/GameMaker/Generic.cpp index 757ee14..f721750 100644 --- a/YYToolkit/source/YYTK/Module Internals/GameMaker/Generic.cpp +++ b/YYToolkit/source/YYTK/Module Internals/GameMaker/Generic.cpp @@ -13,6 +13,11 @@ namespace YYTK IN size_t MaximumInstructionsWithoutFunction ) { + // Query our current architecture to know what we disassemble as + USHORT image_architecture = 0; + if (!AurieSuccess(PpGetCurrentArchitecture(image_architecture))) + return {}; + ZyanU8* memory_data = reinterpret_cast(MmAllocateMemory(g_ArSelfModule, MaximumSize)); if (!memory_data) return {}; @@ -37,9 +42,10 @@ namespace YYTK )) { ZydisDisassembledInstruction current_instruction; + ZyanStatus disassembly_status = ZydisDisassembleIntel( - ZYDIS_MACHINE_MODE_LONG_64, + (image_architecture == IMAGE_FILE_MACHINE_AMD64) ? ZYDIS_MACHINE_MODE_LONG_64 : ZYDIS_MACHINE_MODE_LEGACY_32, runtime_address, memory_data + offset, MaximumSize - offset, @@ -201,7 +207,169 @@ namespace YYTK // ===== Shared, GameMaker-reliant code ===== - AurieStatus GmpGetRunnerInterface( + // x86 version of GmpGetRunnerInterface. + AurieStatus GmpGetRunnerInterfaceX86( + OUT YYRunnerInterface& Interface + ) + { + // In x86, the pattern for finding the runner interface is incredibly simple. + /* + Loop Hero (GM 2.3.6) + + 85 C0 test eax, eax + 0F 88 4E 03 00 00 js loc_15D0F23 + 50 push eax + E8 C5 35 00 00 call sub_15D41A0 + C7 84 24 A0 00 00 00 00 00 00 00 mov [esp+0DB0h+var_D10], 0 + C7 44 24 24 F0 BF 54 01 mov [esp+0DB0h+var_D8C], offset sub_154BFF0 + C7 44 24 28 C0 0F 5D 01 mov [esp+0DB0h+var_D88], offset sub_15D0FC0 + C7 44 24 2C C0 DE 66 01 mov [esp+0DB0h+var_D84], offset sub_166DEC0 + C7 44 24 30 A0 B1 54 01 mov [esp+0DB0h+var_D80], offset sub_154B1A0 + ... + + Deltarune, Chapter 2 (GM 2022.2) + 85 C0 test eax, eax + 0F 88 A6 03 00 00 js loc_4F7FDB + 50 push eax + E8 B5 FB FC FF call sub_4C77F0 + C7 84 24 A0 00 00 00 00 00 00 00 mov [esp+0DCCh+var_D2C], 0 + C7 44 24 24 40 05 58 00 mov [esp+0DCCh+var_DA8], offset sub_580540 + C7 44 24 28 90 80 4F 00 mov [esp+0DCCh+var_DA4], offset sub_4F8090 + C7 44 24 2C E0 72 43 00 mov [esp+0DCCh+var_DA0], offset sub_4372E0 + C7 44 24 30 60 F5 57 00 mov [esp+0DCCh+var_D9C], offset sub_57F560 + C7 44 24 34 E0 85 56 00 mov [esp+0DCCh+var_D98], offset sub_5685E0 + + ... Diffing to see what's similar + 85 C0 test eax, eax + 0F 88 ?? ?? ?? ?? js ?? + 50 push eax + E8 ?? ?? ?? ?? call ?? + + then the C7s which are inconsistent lengths apart because of the first instruction + */ + + // Find the required pattern in the game + // There may be multiple that match, but only one is correct. + uint64_t text_section_base = 0; + size_t text_section_size = 0; + + // Get the .text section address for the game executable + AurieStatus last_status = Internal::PpiGetModuleSectionBounds( + GetModuleHandleW(nullptr), + ".text", + text_section_base, + text_section_size + ); + + if (!AurieSuccess(last_status)) + return last_status; + + // Since PpiGetModuleSectionBounds returns the offset to the .text section + // we need the base address of the game to add to the offset + char* game_base = reinterpret_cast(GetModuleHandleW(nullptr)); + + // Scan for all occurences of this pattern in memory + std::vector pattern_matches = {}; + GmpSigscanRegionEx( + reinterpret_cast((game_base + text_section_base)), + text_section_size, + UTEXT( + "\x85\xC0" // test eax, eax + "\x0F\x88\x00\x00\x00\x00" // js ?? + "\x50" // push eax + "\xE8\x00\x00\x00\x00" // call ?? + "\xC7" // first byte of a mov + ), + "xxxx????x", + pattern_matches + ); + + // TODO: Loop all matches, see if there's a long chain of ZYDIS_MNEMONIC_MOV, where: + // - operands[0].type = ZYDIS_OPERAND_TYPE_MEMORY + // - operands[1].type = ZYDIS_OPERAND_TYPE_IMMEDIATE + + if (pattern_matches.empty()) + return AURIE_OBJECT_NOT_FOUND; + + memset(&Interface, 0, sizeof(Interface)); + std::vector mov_instructions; + + for (const auto& match : pattern_matches) + { + // Now disassemble at the match, strip all instructions except movs that match the pattern above + std::vector instructions = GmpDisassemble( + reinterpret_cast(match), + 0x4FF, + 140 + ); + + // Get every mov from the instructions + for (const auto& instr : instructions) + { + // Skip anything that's not a mov + if (instr.RawForm.info.mnemonic != ZYDIS_MNEMONIC_MOV) + continue; + + // Strip any movs that aren't moving to some memory + if (instr.RawForm.operands[0].type != ZYDIS_OPERAND_TYPE_MEMORY) + continue; + + // Strip any movs that aren't moving from an immediate + if (instr.RawForm.operands[1].type != ZYDIS_OPERAND_TYPE_IMMEDIATE) + continue; + + mov_instructions.push_back(instr.RawForm); + } + + if (mov_instructions.size() > 80 && mov_instructions.size() < 104) + { + CmWriteWarning( + "Found %d functions in %d assembly instructions!", + mov_instructions.size(), + instructions.size() + ); + + break; + } + + mov_instructions.clear(); + } + + // Loop all compatible mov instructions, find the stack base + int64_t interface_start_on_stack = INT_MAX; + for (auto& instr : mov_instructions) + { + if (instr.operands[0].mem.disp.value < interface_start_on_stack) + interface_start_on_stack = instr.operands[0].mem.disp.value; + } + + // Now loop everything again and fill the struct + char* interface_base = reinterpret_cast(&Interface); + for (auto& instr : mov_instructions) + { + int64_t offset = instr.operands[0].mem.disp.value - interface_start_on_stack; + + if (offset >= sizeof(YYRunnerInterface)) + { + // TODO: instr.operands[1].imm.value might be relative? + CmWriteWarning( + "YYRunnerInterface+0x%04llx = 0x%p, but sizeof(YYRunnerInterface) = 0x%x", + offset, + instr.operands[1].imm.value, + sizeof(YYRunnerInterface) + ); + + continue; + } + + // Copy the function pointer to our interface copy + memcpy(interface_base + offset, &instr.operands[1].imm.value, sizeof(PVOID)); + } + + return AURIE_SUCCESS; + } + + AurieStatus GmpGetRunnerInterfaceX64( OUT YYRunnerInterface& Interface ) { @@ -362,7 +530,7 @@ namespace YYTK // we can't just take whatever offset is and memcpy(interface + offset, function, sizeof(function)). // We have to find where the structure actually starts on the stack. - int64_t interface_start_on_stack = 0; + int64_t interface_start_on_stack = INT_MAX; // We find it by looping all instructions and looking for the mov that has the lowest offset. // The first elements are gonna be at [rbp+whatever], and that whatever is then gonna start @@ -530,6 +698,23 @@ namespace YYTK return AURIE_SUCCESS; } + // Calls whatever architecture is currently running + AurieStatus GmpGetRunnerInterface( + OUT YYRunnerInterface& Interface + ) + { + USHORT architecture = 0; + AurieStatus last_status = PpGetCurrentArchitecture(architecture); + + if (!AurieSuccess(last_status)) + return last_status; + + if (architecture == IMAGE_FILE_MACHINE_AMD64) + return GmpGetRunnerInterfaceX64(Interface); + + return GmpGetRunnerInterfaceX86(Interface); + } + AurieStatus GmpFindScriptData( IN const YYRunnerInterface& Interface, IN TRoutine CopyStatic, @@ -607,7 +792,7 @@ namespace YYTK return AURIE_SUCCESS; } - Aurie::AurieStatus GmpFindCodeExecute( + AurieStatus GmpFindCodeExecuteX64( OUT PVOID* CodeExecute ) { @@ -628,7 +813,7 @@ namespace YYTK game_name.c_str(), UTEXT( "\xE8\x00\x00\x00\x00" // call - "\x0F\xB6\xD8" // mozvx ebx, al + "\x0F\xB6\xD8" // movzx ebx, al "\x3C\x01" // cmp al, 1 ), "x????xxxxx" @@ -672,7 +857,88 @@ namespace YYTK return AURIE_SUCCESS; } - AurieStatus GmpFindCurrentRoomData( + AurieStatus GmpFindCodeExecuteX86( + OUT PVOID* CodeExecute + ) + { + AurieStatus last_status = AURIE_SUCCESS; + + // Get the name of the game executable + std::wstring game_name; + last_status = MdGetImageFilename( + g_ArInitialImage, + game_name + ); + + if (!AurieSuccess(last_status)) + return last_status; + + // We're looking for a pattern in Code_Execute + size_t pattern_match = MmSigscanModule( + game_name.c_str(), + UTEXT( + "\xE8\x00\x00\x00\x00" // call + "\x8A\xD8" // mov bl, al + "\x83\xC4\x14" // add esp, 14 + ), + "x????xxxxx" + ); + + if (!pattern_match) + return AURIE_MODULE_INITIALIZATION_FAILED; + + std::vector instructions = GmpDisassemble( + reinterpret_cast(pattern_match), + 0x10, + 0xFF + ); + + // Get the first instruction at that address (the call instruction), and make sure it has the + // parameters we expect it to have (ie. is a call, and has 1 visible operand - the address.) + ZydisDisassembledInstruction& call_instruction = instructions.front().RawForm; + if (call_instruction.info.mnemonic != ZYDIS_MNEMONIC_CALL) + return AURIE_MODULE_INITIALIZATION_FAILED; + + if (call_instruction.info.operand_count_visible < 1) + return AURIE_MODULE_INITIALIZATION_FAILED; + + // Calculate the address of the function which we're calling (ExecuteIt) + ZyanU64 execute_it_address = 0; + if (!ZYAN_SUCCESS(ZydisCalcAbsoluteAddress( + &call_instruction.info, + &call_instruction.operands[0], + call_instruction.runtime_address, + &execute_it_address + ))) + { + return AURIE_MODULE_INITIALIZATION_FAILED; + } + + // We should've never gotten here if the pattern or translation fails. + assert(execute_it_address != 0); + + *CodeExecute = reinterpret_cast(execute_it_address); + + return AURIE_SUCCESS; + } + + AurieStatus GmpFindCodeExecute( + OUT PVOID* CodeExecute + ) + { + USHORT architecture = 0; + AurieStatus last_status = PpGetCurrentArchitecture(architecture); + + if (!AurieSuccess(last_status)) + return last_status; + + if (architecture == IMAGE_FILE_MACHINE_AMD64) + return GmpFindCodeExecuteX64(CodeExecute); + + return GmpFindCodeExecuteX86(CodeExecute); + } + + AurieStatus GmpFindCurrentRoomDataX64( IN FNSetVariable SV_BackgroundColor, OUT CRoom*** Run_Room ) @@ -722,4 +988,71 @@ namespace YYTK *Run_Room = reinterpret_cast(run_room_address); return AURIE_SUCCESS; } + + AurieStatus GmpFindCurrentRoomDataX86( + IN FNSetVariable SV_BackgroundColor, + OUT CRoom*** Run_Room + ) + { + // Disassemble 32 bytes at the function + std::vector instructions = GmpDisassemble( + SV_BackgroundColor, + 0x20, + 0xFF + ); + + // Find the first cmp instruction + size_t target_cmp_index = 0; + AurieStatus last_status = GmpFindMnemonicPattern( + instructions, + { + ZYDIS_MNEMONIC_CMP // cmp Run_Room, 0 + }, + target_cmp_index + ); + + if (!AurieSuccess(last_status)) + return last_status; + + const ZydisDisassembledInstruction& compare_instruction = instructions.at(target_cmp_index).RawForm; + + // This should always be the case. + // But if it's not, it might cause unforeseen bugs, so we assert that in debug builds + assert(compare_instruction.info.mnemonic == ZYDIS_MNEMONIC_CMP); + assert(compare_instruction.info.operand_count_visible == 2); + assert(compare_instruction.operands[0].type == ZYDIS_OPERAND_TYPE_MEMORY); + assert(compare_instruction.operands[1].type == ZYDIS_OPERAND_TYPE_IMMEDIATE); + + ZyanU64 run_room_address = 0; + ZydisCalcAbsoluteAddress( + &compare_instruction.info, + &compare_instruction.operands[0], + compare_instruction.runtime_address, + &run_room_address + ); + + // Make sure we have a valid address + if (!run_room_address) + return AURIE_MODULE_INITIALIZATION_FAILED; + + *Run_Room = reinterpret_cast(run_room_address); + return AURIE_SUCCESS; + } + + AurieStatus GmpFindCurrentRoomData( + IN FNSetVariable SV_BackgroundColor, + OUT CRoom*** Run_Room + ) + { + USHORT architecture = 0; + AurieStatus last_status = PpGetCurrentArchitecture(architecture); + + if (!AurieSuccess(last_status)) + return last_status; + + if (architecture == IMAGE_FILE_MACHINE_AMD64) + return GmpFindCurrentRoomDataX64(SV_BackgroundColor, Run_Room); + + return GmpFindCurrentRoomDataX86(SV_BackgroundColor, Run_Room); + } } \ No newline at end of file diff --git a/YYToolkit/source/YYTK/Module Internals/GameMaker/VM-only.cpp b/YYToolkit/source/YYTK/Module Internals/GameMaker/VM-only.cpp index e3fad2b..75d8358 100644 --- a/YYToolkit/source/YYTK/Module Internals/GameMaker/VM-only.cpp +++ b/YYToolkit/source/YYTK/Module Internals/GameMaker/VM-only.cpp @@ -102,7 +102,7 @@ namespace YYTK // This should be the second one (instructions[1]), // but I don't want to hardcode it... // TODO: Use mnemonic scan - int64_t jnz_instruction_index = -1; + intptr_t jnz_instruction_index = -1; for (size_t i = 0; i < instructions.size(); i++) { const auto& instruction = instructions.at(i).RawForm; @@ -385,6 +385,7 @@ namespace YYTK OUT int64_t* ArrayOffset ) { + UNREFERENCED_PARAMETER(Interface); // So there's one of two ways this is implemented: // Either it's a mov-call-test mnemonic pattern (and the function is called like normal) // In the other case, it's inlined into F_ArrayEquals, in which case we don't care >:( @@ -496,6 +497,7 @@ namespace YYTK OUT PVOID* DoCallScript ) { + UNREFERENCED_PARAMETER(DoCallScript); return AURIE_NOT_IMPLEMENTED; } } diff --git a/YYToolkit/source/YYTK/Module Internals/GameMaker/YYC-only.cpp b/YYToolkit/source/YYTK/Module Internals/GameMaker/YYC-only.cpp index 3aeb823..be4f835 100644 --- a/YYToolkit/source/YYTK/Module Internals/GameMaker/YYC-only.cpp +++ b/YYToolkit/source/YYTK/Module Internals/GameMaker/YYC-only.cpp @@ -7,7 +7,162 @@ namespace YYTK { namespace YYC { - AurieStatus GmpGetBuiltinInformation( + AurieStatus GmpGetBuiltinInformationX86( + OUT int32_t*& BuiltinCount, + OUT RVariableRoutine*& BuiltinArray + ) + { + AurieStatus last_status = AURIE_SUCCESS; + std::wstring game_name; + + last_status = MdGetImageFilename( + g_ArInitialImage, + game_name + ); + + if (!AurieSuccess(last_status)) + return last_status; + + // We're looking for a pattern in Variable_BuiltIn_Add. + + // The rough decompilation of this function is as follows: + /* + void Variable_BuiltIn_Add( + IN const char* Name, + IN FNGetVariable GetVariable + IN FNSetVariable SetVariable + ) + { + if (g_BuiltinVariableCount == 500) + { + ShowMessage("INTERNAL ERROR: Adding too many variables"); // <=== Good string xref! + return; + } + + const char* builtin_name = YYStrDup(Name); + g_BuiltinVariables[g_BuiltinVariableCount].m_GetVariable = GetVariable; + g_BuiltinVariables[g_BuiltinVariableCount].m_SetVariable = SetVariable; + g_BuiltinVariables[g_BuiltinVariableCount].m_CanBeSet = SetVariable != nullptr; + + g_BuiltinVarLookup->Insert(Name); + ++g_BuiltinVariableCount; + } + */ + + // We scan for the "if (g_BuiltinVariableCount == 500)" check + size_t pattern_match = MmSigscanModule( + game_name.c_str(), + UTEXT( + "\x3D\xF4\x01\x00\x00" // cmp eax, 0x1F4 + "\x75\x00" // jnz short ?? + ), + "xxxxxx?" + ); + + if (!pattern_match) + return AURIE_OBJECT_NOT_FOUND; + + // We disassemble the function starting at the pattern + std::vector instructions = GmpDisassemble( + reinterpret_cast(pattern_match), + 0x20, + 0xFF + ); + + // Now, scan for the first jnz instruction + // This should be the second one (instructions[1]), + // but I don't want to hardcode it... + intptr_t jnz_instruction_index = -1; + for (size_t i = 0; i < instructions.size(); i++) + { + const auto& instruction = instructions.at(i).RawForm; + + if (instruction.info.mnemonic != ZYDIS_MNEMONIC_JNZ) + continue; + + jnz_instruction_index = i; + } + + ZyanU64 jnz_target = 0; + + // Follow the jnz instruction (ie. we "pass" the check for eax < 500) + ZyanStatus zyan_status = ZydisCalcAbsoluteAddress( + &instructions[jnz_instruction_index].RawForm.info, + &instructions[jnz_instruction_index].RawForm.operands[0], + instructions[jnz_instruction_index].RawForm.runtime_address, + &jnz_target + ); + + // Translation failed? This shouldn't happen. + if (!ZYAN_SUCCESS(zyan_status) || !jnz_target) + return AURIE_EXTERNAL_ERROR; + + // Now we disassemble again, but this time at the target of the jnz + // ie. where the CPU jumps to if we pass the bounds check + instructions = GmpDisassemble( + reinterpret_cast(jnz_target), + 0x40, + 0xFF + ); + + ZyanU64 array_base_address = 0; + ZyanU64 array_numb_address = 0; + + for (const auto& instruction : instructions) + { + const auto& raw_instruction = instruction.RawForm; + + // Until we have the base address of the array, we have to check for LEA instructions + // TODO: 2023-newer-IDA.i64 seems to use different format? + // 48 89 83 00 FC 06 01 mov qword ptr ds:builtin_variables.f_name[rbx], rax + + // We're searching for two instructions that have the same format: + // Instruction 1: lea register, memory + // Instruction 2: movsxd register, memory + // + // We can therefore check for this format up front, reducing code duplication + if (raw_instruction.info.operand_count != 2) + { + continue; + } + + // Check that the operand types match + if (raw_instruction.operands[0].type != ZYDIS_OPERAND_TYPE_REGISTER) + { + continue; + } + + // Check that the operand types match (part 2) + if (raw_instruction.operands[1].type != ZYDIS_OPERAND_TYPE_MEMORY) + { + continue; + } + + // Until we find the base address of the builtin variable array, + // we check any LEA instruction we encounter. + if ((raw_instruction.info.mnemonic == ZYDIS_MNEMONIC_LEA) && (array_base_address == 0)) + { + array_base_address = raw_instruction.operands[1].mem.disp.value; + } + + // Until we have the address of the array "numb" (ie. the amount of elements used up) + // we have to check for MOVSXD instructions. It's the first one we encounter after the initial jmp + if ((raw_instruction.info.mnemonic == ZYDIS_MNEMONIC_MOV) && (array_numb_address == 0)) + { + array_numb_address = raw_instruction.operands[1].mem.disp.value; + } + } + + if (!array_base_address || !array_numb_address) + return AURIE_MODULE_DEPENDENCY_NOT_RESOLVED; + + BuiltinCount = reinterpret_cast(array_numb_address); + BuiltinArray = reinterpret_cast(array_base_address); + + return AURIE_SUCCESS; + } + + AurieStatus GmpGetBuiltinInformationX64( OUT int32_t*& BuiltinCount, OUT RVariableRoutine*& BuiltinArray ) @@ -72,7 +227,7 @@ namespace YYTK // Now, scan for the first jnz instruction // This should be the second one (instructions[1]), // but I don't want to hardcode it... - int64_t jnz_instruction_index = -1; + intptr_t jnz_instruction_index = -1; for (size_t i = 0; i < instructions.size(); i++) { const auto& instruction = instructions.at(i).RawForm; @@ -173,8 +328,93 @@ namespace YYTK return AURIE_SUCCESS; } + - AurieStatus GmpFindFunctionsArray( + AurieStatus GmpGetBuiltinInformation( + OUT int32_t*& BuiltinCount, + OUT RVariableRoutine*& BuiltinArray + ) + { + USHORT architecture = 0; + AurieStatus last_status = PpGetCurrentArchitecture(architecture); + + if (!AurieSuccess(last_status)) + return last_status; + + if (architecture == IMAGE_FILE_MACHINE_AMD64) + return GmpGetBuiltinInformationX64(BuiltinCount, BuiltinArray); + + return GmpGetBuiltinInformationX86(BuiltinCount, BuiltinArray); + } + + AurieStatus GmpFindFunctionsArrayX86( + IN const YYRunnerInterface& Interface, + OUT RFunction*** FunctionsArray + ) + { + if (!Interface.Code_Function_Find) + return AURIE_MODULE_INTERNAL_ERROR; + + // Disassemble this function + std::vector instructions = GmpDisassemble( + Interface.Code_Function_Find, + 0x7F, + 0xFF + ); + + // It just so happens the first 6-byte long MOV (1 byte difference from x64) + // instruction references the the_functions array. + // It usually looks like mov <32bit register>, [the_functions] + for (auto& instruction : instructions) + { + // The instruction has to be a mov + if (instruction.RawForm.info.mnemonic != ZYDIS_MNEMONIC_MOV) + continue; + + // The instruction has to be 6 bytes in length + if (instruction.RawForm.info.length != 6) + continue; + + // The instruction has to have 2 operands + // The first one (operands[0]) is the register being moved into + // The second one (operands[1]) is the address + if (instruction.RawForm.info.operand_count != 2) + continue; + + ZydisDecodedOperand& first_operand = instruction.RawForm.operands[0]; + ZydisDecodedOperand& second_operand = instruction.RawForm.operands[1]; + + // We have to be moving INTO a register, not FROM a register + if (first_operand.type != ZYDIS_OPERAND_TYPE_REGISTER) + continue; + + // We have to be moving from a memory location, not from a register + if (second_operand.type != ZYDIS_OPERAND_TYPE_MEMORY) + continue; + + // There has to be an offset... duh + if (!second_operand.mem.disp.has_displacement) + continue; + + // Calculate the absolute address + ZyanU64 call_address = 0; + ZydisCalcAbsoluteAddress( + &instruction.RawForm.info, + &second_operand, + instruction.RawForm.runtime_address, + &call_address + ); + + // It's a pointer to a pointer, we dereference it once to + // get the actual pointer to the first element in the array + *FunctionsArray = reinterpret_cast(call_address); + return AURIE_SUCCESS; + } + + return AURIE_OBJECT_NOT_FOUND; + } + + AurieStatus GmpFindFunctionsArrayX64( IN const YYRunnerInterface& Interface, OUT RFunction*** FunctionsArray ) @@ -254,7 +494,93 @@ namespace YYTK return AURIE_OBJECT_NOT_FOUND; } - AurieStatus GmpFindRoomData( + AurieStatus GmpFindFunctionsArray( + IN const YYRunnerInterface& Interface, + OUT RFunction*** FunctionsArray + ) + { + USHORT architecture = 0; + AurieStatus last_status = PpGetCurrentArchitecture(architecture); + + if (!AurieSuccess(last_status)) + return last_status; + + if (architecture == IMAGE_FILE_MACHINE_AMD64) + return GmpFindFunctionsArrayX64(Interface, FunctionsArray); + + return GmpFindFunctionsArrayX86(Interface, FunctionsArray); + } + + AurieStatus GmpFindRoomDataX86( + IN TRoutine RoomInstanceClear, + OUT FNRoomData* RoomData + ) + { + /* + We're disassembling F_RoomInstanceClear (different in x86) + + void F_RoomInstanceClear( + OUT RValue& Result, + IN CInstance* Self, + IN CInstance* Other, + IN int ArgumentCount, + IN RValue* Arguments + ) + { + int room_id = YYGetReal(Arguments, 0); + CRoom* room_data = Room_Data(room_id); // <=== looking for this + + if (room_data) + room_data->ClearStorageInstances(); + } + + It's the second call instruction. + */ + + // Disassemble 80 bytes at the function + std::vector instructions = GmpDisassemble( + RoomInstanceClear, + 0x50, + 0xFF + ); + + size_t target_call_index = 0; + AurieStatus last_status = GmpFindMnemonicPattern( + instructions, + { + ZYDIS_MNEMONIC_CALL, // call + ZYDIS_MNEMONIC_ADD, // add esp, 0xC + ZYDIS_MNEMONIC_TEST // test eax, eax + }, + target_call_index + ); + + if (!AurieSuccess(last_status)) + return last_status; + + const ZydisDisassembledInstruction& call_instruction = instructions.at(target_call_index).RawForm; + + // This should always be the case. + // But if it's not, it might cause unforeseen bugs, so we assert that in debug builds + assert(call_instruction.info.mnemonic == ZYDIS_MNEMONIC_CALL); + + ZyanU64 room_data_address = 0; + ZydisCalcAbsoluteAddress( + &call_instruction.info, + &call_instruction.operands[0], + call_instruction.runtime_address, + &room_data_address + ); + + // Make sure we have a valid address + if (!room_data_address) + return AURIE_MODULE_INITIALIZATION_FAILED; + + *RoomData = reinterpret_cast(room_data_address); + return AURIE_SUCCESS; + } + + AurieStatus GmpFindRoomDataX64( IN TRoutine RoomInstanceClear, OUT FNRoomData* RoomData ) @@ -323,6 +649,23 @@ namespace YYTK return AURIE_SUCCESS; } + AurieStatus GmpFindRoomData( + IN TRoutine RoomInstanceClear, + OUT FNRoomData* RoomData + ) + { + USHORT architecture = 0; + AurieStatus last_status = PpGetCurrentArchitecture(architecture); + + if (!AurieSuccess(last_status)) + return last_status; + + if (architecture == IMAGE_FILE_MACHINE_AMD64) + return GmpFindRoomDataX64(RoomInstanceClear, RoomData); + + return GmpFindRoomDataX86(RoomInstanceClear, RoomData); + } + AurieStatus GmpFindRVArrayOffset( IN TRoutine F_ArrayEquals, IN YYRunnerInterface& RunnerInterface, diff --git a/YYToolkit/source/YYTK/Module Internals/Hooks/Hooks.cpp b/YYToolkit/source/YYTK/Module Internals/Hooks/Hooks.cpp index dd05c12..ee194b2 100644 --- a/YYToolkit/source/YYTK/Module Internals/Hooks/Hooks.cpp +++ b/YYToolkit/source/YYTK/Module Internals/Hooks/Hooks.cpp @@ -22,7 +22,8 @@ namespace YYTK { auto original_function = g_OriginalWindowProc; - FunctionWrapper func_wrapper( + // decltype apparently doesn't work in x86 bruh + FunctionWrapper func_wrapper( original_function, WindowHandle, Message, @@ -53,9 +54,10 @@ namespace YYTK IN unsigned int Flags ) { - auto original_function = GetHookTrampoline("Present"); + // decltype apparently doesn't work in x86 bruh + auto original_function = GetHookTrampoline("Present"); - FunctionWrapper func_wrapper( + FunctionWrapper func_wrapper( original_function, _this, Sync, @@ -86,15 +88,16 @@ namespace YYTK IN UINT SwapChainFlags ) { - auto original_function = GetHookTrampoline("ResizeBuffers"); - - FunctionWrapper func_wrapper( - original_function, - _this, - BufferCount, - Width, - Height, - NewFormat, + // decltype apparently doesn't work in x86 bruh + auto original_function = GetHookTrampoline("ResizeBuffers"); + + FunctionWrapper func_wrapper( + original_function, + _this, + BufferCount, + Width, + Height, + NewFormat, SwapChainFlags ); diff --git a/YYToolkit/source/YYTK/Shared.hpp b/YYToolkit/source/YYTK/Shared.hpp index 828e9d6..71de22f 100644 --- a/YYToolkit/source/YYTK/Shared.hpp +++ b/YYToolkit/source/YYTK/Shared.hpp @@ -9,7 +9,7 @@ #define YYTK_MAJOR 3 #define YYTK_MINOR 2 -#define YYTK_PATCH 3 +#define YYTK_PATCH 4 #ifndef YYTK_CPP_VERSION #ifndef _MSVC_LANG @@ -250,11 +250,11 @@ namespace YYTK // Overloaded operators RValue& operator[]( IN size_t Index - ); + ); RValue& operator[]( IN std::string_view Element - ); + ); // STL-like access RValue& at( @@ -1968,7 +1968,9 @@ namespace YYTK T* m_Last; int32_t m_Count; }; +#ifdef _WIN64 static_assert(sizeof(LinkedList) == 0x18); +#endif // _WIN64 enum eBuffer_Type : int32_t { @@ -2018,7 +2020,9 @@ namespace YYTK virtual uint8_t* Compress(int _offset, int _size, uint32_t& resultSize) = 0; virtual uint8_t* Decompress(uint32_t& resultSize) = 0; }; +#ifdef _WIN64 static_assert(sizeof(IBuffer) == 0x8); +#endif // _WIN64 struct CLayerElementBase { @@ -2040,14 +2044,18 @@ namespace YYTK CLayerElementBase* m_Blink; }; }; +#ifdef _WIN64 static_assert(sizeof(CLayerElementBase) == 0x30); +#endif // _WIN64 struct CLayerInstanceElement : CLayerElementBase { int32_t m_InstanceID; CInstance* m_Instance; }; +#ifdef _WIN64 static_assert(sizeof(CLayerInstanceElement) == 0x40); +#endif // _WIN64 struct CLayerSpriteElement : CLayerElementBase { @@ -2065,7 +2073,9 @@ namespace YYTK float m_X; float m_Y; }; +#ifdef _WIN64 static_assert(sizeof(CLayerSpriteElement) == 0x68); +#endif // _WIN64 __declspec(align(8)) struct CLayer { @@ -2091,7 +2101,9 @@ namespace YYTK CLayer* m_Blink; PVOID m_GCProxy; }; +#ifdef _WIN64 static_assert(sizeof(CLayer) == 0xA0); +#endif // _WIN64 // A representation of a room, as from the data.win file struct YYRoom @@ -2128,7 +2140,9 @@ namespace YYTK float m_PhysicsGravityY; float m_PhysicsPixelToMeters; }; +#ifdef _WIN64 static_assert(sizeof(YYRoom) == 0x58); +#endif // _WIN64 // Note: this is not how RValues store arrays template @@ -2137,7 +2151,9 @@ namespace YYTK int32_t Length; T* Array; }; +#ifdef _WIN64 static_assert(sizeof(CArrayStructure) == 0x10); +#endif // _WIN64 // Seems to be mostly stable, some elements at the end are however omitted struct CRoom @@ -2189,7 +2205,9 @@ namespace YYTK int32_t m_EffectLayerIdCount; int32_t m_EffectLayerIdMax; }; +#ifdef _WIN64 static_assert(sizeof(CRoom) == 0x218); +#endif // _WIN64 struct CInstanceBase { @@ -2197,7 +2215,9 @@ namespace YYTK RValue* m_YYVars; }; +#ifdef _WIN64 static_assert(sizeof(CInstanceBase) == 0x10); +#endif // _WIN64 enum EJSRetValBool : int32_t { @@ -2210,21 +2230,21 @@ namespace YYTK IN YYObjectBase* Object, OUT RValue& Result, IN const char* Name - ); + ); using FNDeleteProperty = void(*)( IN YYObjectBase* Object, OUT RValue& Result, IN const char* Name, IN bool ThrowOnError - ); + ); using FNDefineOwnProperty = EJSRetValBool(*)( IN YYObjectBase* Object, IN const char* Name, OUT RValue& Result, IN bool ThrowOnError - ); + ); enum YYObjectKind : int32_t { @@ -2268,7 +2288,7 @@ namespace YYTK ) = 0; virtual bool Mark4GC( - uint32_t*, + uint32_t*, int ) = 0; @@ -2303,7 +2323,7 @@ namespace YYTK FNDeleteProperty m_DeleteProperty; FNDefineOwnProperty m_DefineOwnProperty; // Use GetInstanceMember instead - CHashMap* m_YYVarsMap; + CHashMap* m_YYVarsMap; CWeakRef** m_WeakRef; uint32_t m_WeakRefCount; uint32_t m_VariableCount; @@ -2318,7 +2338,9 @@ namespace YYTK int32_t m_RValueInitType; int32_t m_CurrentSlot; }; +#ifdef _WIN64 static_assert(sizeof(YYObjectBase) == 0x88); +#endif // _WIN64 struct CScriptRef : YYObjectBase { @@ -2332,7 +2354,9 @@ namespace YYTK PVOID m_Construct; const char* m_Tag; }; +#ifdef _WIN64 static_assert(sizeof(CScriptRef) == 0xE0); +#endif // _WIN64 struct CPhysicsDataGM { @@ -2350,14 +2374,18 @@ namespace YYTK float m_PhysicsFriction; int m_PhysicsVertexCount; }; +#ifdef _WIN64 static_assert(sizeof(CPhysicsDataGM) == 0x30); +#endif // _WIN64 struct CEvent { CCode* m_Code; int32_t m_OwnerObjectID; }; +#ifdef _WIN64 static_assert(sizeof(CEvent) == 0x10); +#endif // _WIN64 struct CObjectGM { @@ -2375,13 +2403,17 @@ namespace YYTK int32_t m_Mask; int32_t m_ID; }; +#ifdef _WIN64 static_assert(sizeof(CObjectGM) == 0x98); +#endif // _WIN64 struct GCObjectContainer : YYObjectBase { CHashMap* m_YYObjectMap; }; +#ifdef _WIN64 static_assert(sizeof(GCObjectContainer) == 0x90); +#endif // _WIN64 struct YYRECT { @@ -2434,7 +2466,9 @@ namespace YYTK CInstance* m_Flink; CInstance* m_Blink; }; +#ifdef _WIN64 static_assert(sizeof(CInstanceInternal) == 0xF8); +#endif // _WIN64 struct CInstance : YYObjectBase { @@ -2458,7 +2492,9 @@ namespace YYTK public: CInstanceInternal Members; } Unmasked; +#ifdef _WIN64 static_assert(sizeof(Unmasked) == 0x100); +#endif // _WIN64 // 2022.1 => 2023.1 (may be used later, haven't checked) struct @@ -2469,7 +2505,9 @@ namespace YYTK public: CInstanceInternal Members; } Masked; +#ifdef _WIN64 static_assert(sizeof(Masked) == 0x108); +#endif // _WIN64 }; public: @@ -2487,7 +2525,9 @@ namespace YYTK }; // sizeof(0x1A8) is for PreMasked instances // sizeof(0x1B0) is for Masked instances +#ifdef _WIN64 static_assert(sizeof(CInstance) == 0x1A8 || sizeof(CInstance) == 0x1B0); +#endif // _WIN64 #endif // YYTK_DEFINE_INTERNAL } diff --git a/YYToolkit/source/YYTK/Tool.hpp b/YYToolkit/source/YYTK/Tool.hpp index 002d096..7cfc990 100644 --- a/YYToolkit/source/YYTK/Tool.hpp +++ b/YYToolkit/source/YYTK/Tool.hpp @@ -33,8 +33,9 @@ namespace YYTK int32_t m_ArgumentCount; int32_t m_UsageCount; }; +#ifdef _WIN64 static_assert(sizeof(RFunctionStringFull) == 80); - +#endif // _WIN64 struct RFunctionStringRef { const char* m_Name; @@ -42,7 +43,9 @@ namespace YYTK int32_t m_ArgumentCount; int32_t m_UsageCount; }; +#ifdef _WIN64 static_assert(sizeof(RFunctionStringRef) == 24); +#endif // _WIN64 struct RFunction { @@ -62,7 +65,9 @@ namespace YYTK return *reinterpret_cast(reinterpret_cast(this) + (sizeof(RFunctionStringRef) * Index)); } }; +#ifdef _WIN64 static_assert(sizeof(RFunction) == 80); +#endif // _WIN64 struct RVariableRoutine { @@ -71,7 +76,9 @@ namespace YYTK FNSetVariable m_SetVariable; bool m_CanBeSet; }; +#ifdef _WIN64 static_assert(sizeof(RVariableRoutine) == 32); +#endif // _WIN64 struct TargettedInstruction {