commit 72626c2c187e5f7666b6bee4fffea2f46a1336e6 Author: mkrsym1 Date: Tue Jun 6 00:23:08 2023 +0300 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2c1d1f6 --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +.vscode + +.directory + +# File withheld to make abuse more difficult +game_payload/src/tp6.c + +build +out +jadeite.zip diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..3154867 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 mkrsym1 + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..93fee9e --- /dev/null +++ b/README.md @@ -0,0 +1,49 @@ +# PROOF OF CONCEPT. DO NOT USE IF YOU DON'T KNOW WHAT YOU'RE DOING + +### Games and regions +This project is in the proof-of-concept stage. Currently, only **3rd glb v6.6.0** is supported. It may be possilbe to completely remove the region and version-specific data in the future. Refer to the source code in `game_payload/src` for details. + +### Information +The anticheat the games use is fundamentally incompatible with Wine in multiple ways. This tool launches the game without it (`inject/launcher_payload`) and imitates it's behaviour (`game_payload`). + +Does not work on Windows. + +### Usage +**Refer to [Third-party launchers](#third-party-launchers) (will be written later)** for convenient usage. If you don't want to (or can't) use third-party launchers, continue reading the section below. + +**Wine 8.0+ is recommended**, as lower versions leak "The Wine project" as the device identifier. Not critical, but taking a precaution never hurt anyone. **DXVK is strongly recommended.** + +3rd-specific: In some cases, and if you're not using Proton GE, **a fix for Media Foundation may be required to play videos. The Game may crash without it.** You can download it from [here](https://github.com/z0z0z/mf-install). You might need to [limit the number of cores available to the game](https://github.com/z0z0z/mf-install/issues/44) if your CPU has more than 8. + +Manual usage instructions: +- Download the game you want to run +- Download a release from this repository +- Extract the archive (**NOT INTO THE GAME DIRECTORY! THIS IS IMPORTANT!**) +- Block analytics servers in your `hosts` file. You can find the list in SERVERS.txt +- Run `wine jadeite.exe "Z:\\wine\\path\\to\\game.exe"` + +This tool is capable of starting the games from a different process. This may be useful for spoofing the parent process (SR is known to report it). Use `wine jadeite.exe "Z:\\wine\\path\\to\\game.exe" "Z:\\wine\\path\\to\\launcher.exe"`. `explorer.exe` is used as the default. + +### Internals +This tool consists of three parts: the main injector (`injector`), the launcher payload (`injector/launcher_payload`) and the game payload (`game_payload`). + +I am very bad at explaining, so just take a look at the source code. Maybe I'll write a detailed explanation in the future. + +A part of the source code is witheld (`game_payload/src/tp6.c`). This is a forced measure to make abuse more difficult. + +### Guildelines +1. **Please don't share this project in public.** This might attract unnecessary attention from either the Game Company or the Anticheat Company +2. **Please don't abuse this project for cheating.** We're just trying to play the games through Wine + +### Troubleshooting +Please do not report any issues with the Game to the official channels. Use the issue tracker of this repository + +### Third-party launchers +Will be written later + +### Credits +- mkrsym1 — project leader, reverse engineering +- Yor#1920 — major help with analyzing network activity +- Some others credited in the source code + +License: MIT diff --git a/SERVERS.txt b/SERVERS.txt new file mode 100644 index 0000000..4d8ab07 --- /dev/null +++ b/SERVERS.txt @@ -0,0 +1,4 @@ +# Honkai Impact 3rd logging servers: +0.0.0.0 log-upload-os.hoyoverse.com +0.0.0.0 sg-public-data-api.hoyoverse.com +0.0.0.0 dump.gamesafe.qq.com diff --git a/build.sh b/build.sh new file mode 100644 index 0000000..228429f --- /dev/null +++ b/build.sh @@ -0,0 +1,19 @@ +#!/usr/bin/env sh + +rm -f jadeite.zip +rm -rf out + +sh setup.sh +ninja -C build + +mkdir out + +cp ./build/injector/jadeite.exe ./out +cp ./build/injector/launcher_payload/launcher_payload.dll ./out +cp ./build/game_payload/game_payload.dll ./out +cp ./LICENSE.txt ./out + +if [ "x$1" = "xrelease" ]; then + cd out + zip ../jadeite.zip * +fi diff --git a/game_payload/include/ace.h b/game_payload/include/ace.h new file mode 100644 index 0000000..60437b9 --- /dev/null +++ b/game_payload/include/ace.h @@ -0,0 +1,8 @@ +#pragma once + +#include + +void ace_fake_driver_files(); + +HMODULE ace_load_base_module(const char *exeName); +HMODULE ace_load_driver_module(); diff --git a/game_payload/include/crc32.h b/game_payload/include/crc32.h new file mode 100644 index 0000000..9d82725 --- /dev/null +++ b/game_payload/include/crc32.h @@ -0,0 +1,24 @@ +#pragma once + +// Modified from https://stackoverflow.com/a/27950866 + +#include +#include + +/* CRC-32C (iSCSI) polynomial in reversed bit order. */ +#define __POLY 0x82f63b78 + +static inline uint32_t crc32c(uint32_t crc, const unsigned char *buf, size_t len) { + crc = ~crc; + + while (len--) { + crc ^= *buf++; + for (int k = 0; k < 8; k++) { + crc = crc & 1 ? (crc >> 1) ^ __POLY : crc >> 1; + } + } + + return ~crc; +} + +#undef __POLY diff --git a/game_payload/include/err.h b/game_payload/include/err.h new file mode 100644 index 0000000..41428b9 --- /dev/null +++ b/game_payload/include/err.h @@ -0,0 +1,6 @@ +#pragma once + +#include + +void err_mb_a(const char *format, ...); +void err_mb_w(const wchar_t *format, ...); diff --git a/game_payload/include/game.h b/game_payload/include/game.h new file mode 100644 index 0000000..d5ae6e1 --- /dev/null +++ b/game_payload/include/game.h @@ -0,0 +1,20 @@ +#pragma once + +#include + +enum game_id { + GAME_INVALID, + + GAME_HI3_GLB +}; + +struct game_data { + enum game_id id; // Temporary + const char *name; + const char *assembly_path; + const wchar_t *assembly_name_lwr; + const char *tp6_section_name; // Unused for now + const char *tvm_section_name; +}; + +void game_detect(struct game_data *buf); diff --git a/game_payload/include/hi3.h b/game_payload/include/hi3.h new file mode 100644 index 0000000..ad5703c --- /dev/null +++ b/game_payload/include/hi3.h @@ -0,0 +1,5 @@ +#pragma once + +#include + +void hi3_fill_data(struct game_data *buf); diff --git a/game_payload/include/ntdll.h b/game_payload/include/ntdll.h new file mode 100644 index 0000000..058ddc5 --- /dev/null +++ b/game_payload/include/ntdll.h @@ -0,0 +1,36 @@ +#pragma once + +#include +#include + +// https://learn.microsoft.com/en-us/windows/win32/devnotes/ldrdllnotification +typedef struct _LDR_DLL_LOADED_NOTIFICATION_DATA { + ULONG Flags; //Reserved. + PCUNICODE_STRING FullDllName; //The full path name of the DLL module. + PCUNICODE_STRING BaseDllName; //The base file name of the DLL module. + PVOID DllBase; //A pointer to the base address for the DLL in memory. + ULONG SizeOfImage; //The size of the DLL image, in bytes. +} LDR_DLL_LOADED_NOTIFICATION_DATA, *PLDR_DLL_LOADED_NOTIFICATION_DATA; + +typedef struct _LDR_DLL_UNLOADED_NOTIFICATION_DATA { + ULONG Flags; //Reserved. + PCUNICODE_STRING FullDllName; //The full path name of the DLL module. + PCUNICODE_STRING BaseDllName; //The base file name of the DLL module. + PVOID DllBase; //A pointer to the base address for the DLL in memory. + ULONG SizeOfImage; //The size of the DLL image, in bytes. +} LDR_DLL_UNLOADED_NOTIFICATION_DATA, *PLDR_DLL_UNLOADED_NOTIFICATION_DATA; + +typedef union _LDR_DLL_NOTIFICATION_DATA { + LDR_DLL_LOADED_NOTIFICATION_DATA Loaded; + LDR_DLL_UNLOADED_NOTIFICATION_DATA Unloaded; +} LDR_DLL_NOTIFICATION_DATA, *PLDR_DLL_NOTIFICATION_DATA; + +typedef void (*LdrDllNotification_t)(ULONG reason, const PLDR_DLL_NOTIFICATION_DATA data, void *context); + +typedef NTSTATUS (*LdrRegisterDllNotification_t)(ULONG flags, LdrDllNotification_t notificationFunction, void *context, void **cookie); +typedef NTSTATUS (*LdrUnregisterDllNotification_t)(void *cookie); + +extern LdrRegisterDllNotification_t LdrRegisterDllNotification; +extern LdrUnregisterDllNotification_t LdrUnregisterDllNotification; + +void ntdll_link(); diff --git a/game_payload/include/pe.h b/game_payload/include/pe.h new file mode 100644 index 0000000..f1843f6 --- /dev/null +++ b/game_payload/include/pe.h @@ -0,0 +1,6 @@ +#pragma once + +#include + +void pe_find_section(HMODULE module, const char *section, MEMORY_BASIC_INFORMATION *buf); +void *pe_find_entry_point(HMODULE module); diff --git a/game_payload/include/tp6.h b/game_payload/include/tp6.h new file mode 100644 index 0000000..76bec34 --- /dev/null +++ b/game_payload/include/tp6.h @@ -0,0 +1,7 @@ +#pragma once + +#include + +#include + +void tp6_setup_patcher(struct game_data *game, HMODULE thisModule, HMODULE baseModule); diff --git a/game_payload/include/utils.h b/game_payload/include/utils.h new file mode 100644 index 0000000..2a2a876 --- /dev/null +++ b/game_payload/include/utils.h @@ -0,0 +1,5 @@ +#pragma once + +#include + +uint32_t utils_file_crc32c(const char *filePath); diff --git a/game_payload/meson.build b/game_payload/meson.build new file mode 100644 index 0000000..29847d2 --- /dev/null +++ b/game_payload/meson.build @@ -0,0 +1,34 @@ +# Input files +sources = [ + 'src/main.c', + 'src/ntdll.c', + 'src/ace.c', + 'src/pe.c', + 'src/game.c', + 'src/hi3.c', + 'src/utils.c', + 'src/err.c', + + # File withheld to make abuse more difficult + 'src/tp6.c' +] +resources = [ + 'res/hi3/glb/allocations.dat', + 'res/hi3/glb/entries.dat' +] + +# Generate resource files for ./res +res_files = custom_target( + 'resources.[ho]', + output: [ 'resources.o', 'resources.h' ], + input: resources, + command: [ gen_res, meson.current_source_dir(), '@OUTPUT0@', '@OUTPUT1@', '@INPUT@' ] +) + +shared_library( + 'game_payload', + sources, + res_files, + include_directories: 'include', + name_prefix: '' +) diff --git a/game_payload/res/hi3/glb/allocations.dat b/game_payload/res/hi3/glb/allocations.dat new file mode 100644 index 0000000..9ea3afd Binary files /dev/null and b/game_payload/res/hi3/glb/allocations.dat differ diff --git a/game_payload/res/hi3/glb/entries.dat b/game_payload/res/hi3/glb/entries.dat new file mode 100644 index 0000000..07a5e00 Binary files /dev/null and b/game_payload/res/hi3/glb/entries.dat differ diff --git a/game_payload/src/ace.c b/game_payload/src/ace.c new file mode 100644 index 0000000..ec04163 --- /dev/null +++ b/game_payload/src/ace.c @@ -0,0 +1,91 @@ +#include +#include +#include + +#include + +static void _dll_notification(ULONG reason, const PLDR_DLL_NOTIFICATION_DATA data, void *context) { + if (reason != 1) { // 1 - attach + return; + } + + // context should be set to the target module name, lowercase + wchar_t *targetModuleName = (wchar_t*)context; + + wchar_t lwModuleName[MAX_PATH]; + wcscpy(lwModuleName, data->Loaded.BaseDllName->Buffer); + _wcslwr(lwModuleName); + + if (wcscmp(targetModuleName, lwModuleName) == 0) { + // Replace entry point with a stub + void *entryPoint = pe_find_entry_point(data->Loaded.DllBase); + + const char ENTRY_POINT_STUB[] = { + 0xB8, 0x01, 0x00, 0x00, 0x00, // mov eax, 1 + 0xC3 // ret + }; + + DWORD oldProtect; + VirtualProtect(entryPoint, sizeof(ENTRY_POINT_STUB), PAGE_EXECUTE_READWRITE, &oldProtect); + + memcpy(entryPoint, ENTRY_POINT_STUB, sizeof(ENTRY_POINT_STUB)); + + VirtualProtect(entryPoint, sizeof(ENTRY_POINT_STUB), oldProtect, &oldProtect); + } +} + +void ace_fake_driver_files() { + // They only report presence + const char *wdDriverPath = "ACE-BASE.sys"; + const char *s32DriverPath = "C:\\windows\\system32\\drivers\\ACE-BASE.sys"; + + HANDLE wdDriverFile = CreateFileA(wdDriverPath, GENERIC_WRITE, FILE_SHARE_READ, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL); + if (!wdDriverFile) { + err_mb_a("Could not create driver file: %s", wdDriverPath); + } + + // Just in case + HANDLE s32DriverFile = CreateFileA(s32DriverPath, GENERIC_WRITE, FILE_SHARE_READ, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL); + if (!s32DriverFile) { + err_mb_a("Could not create driver file: %s", s32DriverPath); + } + + CloseHandle(wdDriverFile); + CloseHandle(s32DriverFile); +} + +HMODULE ace_load_base_module(const char *exeName) { + wchar_t baseModuleName[MAX_PATH]; + swprintf(baseModuleName, MAX_PATH, L"%sbase.dll", exeName); + wcslwr(baseModuleName); + + void *cookie; + LdrRegisterDllNotification(0, &_dll_notification, baseModuleName, &cookie); + + HMODULE baseModule = LoadLibraryW(baseModuleName); + if (!baseModule) { + err_mb_w(L"Could not load base module: %ls", baseModuleName); + } + + // LoadLibraryA is synchronous; the notification function has already finished executing + LdrUnregisterDllNotification(cookie); + + return baseModule; +} + +HMODULE ace_load_driver_module() { + const char *driverModulePath = "AntiCheatExpert/InGame/x64/ACE-DRV64.dll"; + + void *cookie; + LdrRegisterDllNotification(0, &_dll_notification, L"ace-drv64.dll", &cookie); + + HMODULE driverModule = LoadLibraryA(driverModulePath); + if (!driverModule) { + err_mb_a("Could not load driver module: %s", driverModulePath); + } + + // LoadLibraryA is synchronous; the notification function has already finished executing + LdrUnregisterDllNotification(cookie); + + return driverModule; +} diff --git a/game_payload/src/err.c b/game_payload/src/err.c new file mode 100644 index 0000000..a91be5a --- /dev/null +++ b/game_payload/src/err.c @@ -0,0 +1,26 @@ +#include +#include + +#include + +#define DEF_ERROR_FN(name, type, printfn, mbfn, projname) \ + void name(const type *format, ...) { \ + va_list args; \ + va_start(args, format); \ + \ + int count = printfn(NULL, 0, format, args) + 1; \ + \ + type *buf = malloc(count * sizeof(type)); \ + printfn(buf, count, format, args); \ + \ + mbfn(NULL, buf, projname, MB_OK | MB_ICONERROR); \ + \ + va_end(args); \ + \ + free(buf); \ + exit(1); \ + } + + +DEF_ERROR_FN(err_mb_a, char, _vsnprintf, MessageBoxA, "Jadeite Autopatcher") +DEF_ERROR_FN(err_mb_w, wchar_t, _vsnwprintf, MessageBoxW, L"Jadeite Autopatcher") diff --git a/game_payload/src/game.c b/game_payload/src/game.c new file mode 100644 index 0000000..c87de37 --- /dev/null +++ b/game_payload/src/game.c @@ -0,0 +1,19 @@ +#include +#include + +#include + +void game_detect(struct game_data *buf) { + wchar_t exePath[MAX_PATH]; + GetModuleFileNameW(NULL, exePath, MAX_PATH); + + wchar_t *exeName = wcsrchr(exePath, L'\\') + 1; + wcslwr(exeName); + + // Only HI3 is supported for now + if (wcscmp(exeName, L"bh3.exe") == 0) { + hi3_fill_data(buf); + } else { + err_mb_w(L"Unknown game: %ls", exeName); + } +} diff --git a/game_payload/src/hi3.c b/game_payload/src/hi3.c new file mode 100644 index 0000000..672237a --- /dev/null +++ b/game_payload/src/hi3.c @@ -0,0 +1,44 @@ +#include +#include + +#include + +const char *HI3_NAME = "BH3"; +const char *HI3_ASSEMBLY_PATH = "BH3_Data/Native/UserAssembly.dll"; +const wchar_t *HI3_ASSEMBLY_NAME_LWR = L"userassembly.dll"; +const char *HI3_TP6_SECTION_NAME = ".bh3"; +const char *HI3_TVM_SECTION_NAME = ".tvm0"; + +struct crc_id_pair { + uint32_t crc; + enum game_id id; +}; + +const struct crc_id_pair HI3_REGIONS[] = { + // Only glb for now + // It may be possible to get rid of region-specific data altogether in the future + + { 0x34bdec99, GAME_HI3_GLB } // glb v6.6.0 +}; + +void hi3_fill_data(struct game_data *buf) { + uint32_t crc = utils_file_crc32c("UnityPlayer.dll"); + + enum game_id id = GAME_INVALID; + for (size_t i = 0; i < sizeof(HI3_REGIONS) / sizeof(struct crc_id_pair); i++) { + if (HI3_REGIONS[i].crc == crc) { + id = HI3_REGIONS[i].id; + } + } + + if (id == GAME_INVALID) { + err_mb_a("Invalid UnityPlayer.dll checksum: %d", crc); + } + + buf->id = id; + buf->name = HI3_NAME; + buf->assembly_path = HI3_ASSEMBLY_PATH; + buf->assembly_name_lwr = HI3_ASSEMBLY_NAME_LWR; + buf->tp6_section_name = HI3_TP6_SECTION_NAME; + buf->tvm_section_name = HI3_TVM_SECTION_NAME; +} diff --git a/game_payload/src/main.c b/game_payload/src/main.c new file mode 100644 index 0000000..1e7095e --- /dev/null +++ b/game_payload/src/main.c @@ -0,0 +1,33 @@ +#include + +#include +#include +#include +#include +#include + +BOOL WINAPI DllMain(HINSTANCE instance, DWORD reason, LPVOID reserved) { + // Only listen to attach + if (reason != DLL_PROCESS_ATTACH) { + return TRUE; + } + + // Dynamically link functions from ntdll + ntdll_link(); + + // Detect which game the user is trying to run + struct game_data game; + game_detect(&game); + + // Create fake ACE driver files + ace_fake_driver_files(); + + // Load both ACE modules + HMODULE baseModule = ace_load_base_module(game.name); + ace_load_driver_module(); + + // ...magic + tp6_setup_patcher(&game, instance, baseModule); + + return TRUE; +} diff --git a/game_payload/src/ntdll.c b/game_payload/src/ntdll.c new file mode 100644 index 0000000..5ddef95 --- /dev/null +++ b/game_payload/src/ntdll.c @@ -0,0 +1,11 @@ +#include + +LdrRegisterDllNotification_t LdrRegisterDllNotification; +LdrUnregisterDllNotification_t LdrUnregisterDllNotification; + +void ntdll_link() { + HMODULE ntdll = GetModuleHandleA("ntdll.dll"); + + LdrRegisterDllNotification = (LdrRegisterDllNotification_t)GetProcAddress(ntdll, "LdrRegisterDllNotification"); + LdrUnregisterDllNotification = (LdrUnregisterDllNotification_t)GetProcAddress(ntdll, "LdrUnregisterDllNotification"); +} diff --git a/game_payload/src/pe.c b/game_payload/src/pe.c new file mode 100644 index 0000000..f0e6c91 --- /dev/null +++ b/game_payload/src/pe.c @@ -0,0 +1,34 @@ +#include + +#include + +void pe_find_section(HMODULE module, const char *section, MEMORY_BASIC_INFORMATION *buf) { + char *cModule = (char*)module; + + IMAGE_DOS_HEADER* dosHeader = (IMAGE_DOS_HEADER*)module; + IMAGE_NT_HEADERS64* ntHeaders = (IMAGE_NT_HEADERS64*)(cModule + dosHeader->e_lfanew); + + uint16_t sectionCount = ntHeaders->FileHeader.NumberOfSections; + IMAGE_SECTION_HEADER* sectionHeader = (IMAGE_SECTION_HEADER*)(ntHeaders + 1); + + void* targetAddress = 0x0; + for (uint16_t i = 0; i < sectionCount; i++) { + if (strncmp((char*)sectionHeader->Name, section, 8) == 0) { + targetAddress = (void*)(cModule + sectionHeader->VirtualAddress); + break; + } + + sectionHeader++; + } + + VirtualQuery(targetAddress, buf, sizeof(MEMORY_BASIC_INFORMATION)); +} + +void *pe_find_entry_point(HMODULE module) { + char *cModule = (char*)module; + + IMAGE_DOS_HEADER* dosHeader = (IMAGE_DOS_HEADER*)module; + IMAGE_NT_HEADERS64* ntHeaders = (IMAGE_NT_HEADERS64*)(cModule + dosHeader->e_lfanew); + + return cModule + ntHeaders->OptionalHeader.AddressOfEntryPoint; +} diff --git a/game_payload/src/tp6.md b/game_payload/src/tp6.md new file mode 100644 index 0000000..9f85346 --- /dev/null +++ b/game_payload/src/tp6.md @@ -0,0 +1,2 @@ +### 1.0.0 +- First version diff --git a/game_payload/src/utils.c b/game_payload/src/utils.c new file mode 100644 index 0000000..6b8bd4c --- /dev/null +++ b/game_payload/src/utils.c @@ -0,0 +1,30 @@ +#include + +#include +#include + +#include + +uint32_t utils_file_crc32c(const char *filePath) { + HANDLE file = CreateFileA(filePath, FILE_READ_ACCESS, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL); + if (!file) { + err_mb_a("Could not open file: %s", filePath); + } + + LARGE_INTEGER fileSize; + GetFileSizeEx(file, &fileSize); + + HANDLE hMap = CreateFileMappingA(file, NULL, PAGE_READONLY, 0, 0, NULL); + char *map = MapViewOfFile(hMap, FILE_MAP_READ, 0, 0, 0); + if (!map) { + err_mb_a("Could not create file mapping for %s", filePath); + } + + uint32_t crc = crc32c(0, (unsigned char*)map, fileSize.QuadPart); + + UnmapViewOfFile(map); + CloseHandle(hMap); + CloseHandle(file); + + return crc; +} diff --git a/gen_resources.sh b/gen_resources.sh new file mode 100644 index 0000000..aae6c68 --- /dev/null +++ b/gen_resources.sh @@ -0,0 +1,49 @@ +#!/usr/bin/env bash + +linker="x86_64-w64-mingw32-ld" + +# Read project directory +proj_dir=`realpath "$1"` +shift + +# Read output file destinations +resources_o=`realpath "$1"` +shift +resources_h=`realpath "$1"` +shift + +# Make sure that the header does not exist +rm -f "${resources_h}" +rm -f "${resources_o}" + +# Recomupte relative paths to parameters +idx=0 +resource_files=() + +for path in "$@" +do + resource_files["${idx}"]=`realpath --relative-to="${proj_dir}" "${path}"` + idx="$(("${idx}" + 1))" +done + +# Create the object file +pushd "${proj_dir}" >> /dev/null +$linker -r -b binary -o "${resources_o}" "${resource_files[@]}" +popd >> /dev/null + +# Include stddef.h in the resources header (for size_t) +echo "#include " >> "${resources_h}" + +for resource in "${resource_files[@]}" +do + # Use relative path to the resource as the variable name + var_name="_binary_${resource}" + + # Replace all non-alphanumeric characters with underscores + var_name=`printf "${var_name}" | sed "s/[^a-zA-Z0-9]/_/g"` + + # Define externs in the header + echo "extern void *${var_name}_start;" >> "${resources_h}" + echo "extern void *${var_name}_size;" >> "${resources_h}" + echo "" >> "${resources_h}" +done diff --git a/injector/include/injshared.h b/injector/include/injshared.h new file mode 100644 index 0000000..b09eace --- /dev/null +++ b/injector/include/injshared.h @@ -0,0 +1,87 @@ +#include + +static inline void write_protected_process_memory(HANDLE process, void *address, const void *buf, size_t size) { + DWORD oldProtect; + VirtualProtectEx(process, address, size, PAGE_EXECUTE_READWRITE, &oldProtect); + + size_t bytesWritten; + WriteProcessMemory(process, address, buf, size, &bytesWritten); + + VirtualProtectEx(process, address, size, oldProtect, &oldProtect); +} + +static inline void inject(HANDLE process, const void *payload, size_t payloadSize, const char *dllPath) { + size_t _; + + // Inject the loader into the module + size_t dllPathLen = strlen(dllPath) + 1; + + char *remoteAlloc = VirtualAllocEx(process, NULL, payloadSize + dllPathLen, MEM_COMMIT, PAGE_EXECUTE_READWRITE); + WriteProcessMemory(process, remoteAlloc, payload, payloadSize, &_); + WriteProcessMemory(process, remoteAlloc + payloadSize, dllPath, dllPathLen, &_); + + // Find the EXE header in the process + char exeHeader[1024]; + IMAGE_DOS_HEADER *dosHeader; + IMAGE_NT_HEADERS64 *ntHeaders; + + MEMORY_BASIC_INFORMATION memoryInfo; + char *currentAddress = 0x0; + while (VirtualQueryEx(process, currentAddress, &memoryInfo, sizeof(memoryInfo))) { + ReadProcessMemory(process, currentAddress, exeHeader, sizeof(exeHeader), &_); + + dosHeader = (IMAGE_DOS_HEADER*)exeHeader; + + // DOS header magic "MZ" + if (dosHeader->e_magic != 0x5A4D) { + goto cont; + } + + ntHeaders = (IMAGE_NT_HEADERS64*)(exeHeader + dosHeader->e_lfanew); + + // NT header signature "PE" + if (ntHeaders->Signature != 0x4550) { + goto cont; + } + + // Skip DLLs + if ((ntHeaders->FileHeader.Characteristics | IMAGE_FILE_DLL) == IMAGE_FILE_DLL) { + goto cont; + } + + // Skip potential headers without an entry point + // I have no idea how and why they exist, but apparently they do + if (ntHeaders->OptionalHeader.AddressOfEntryPoint == 0) { + goto cont; + } + + // Found EXE header + break; + + cont: + currentAddress += memoryInfo.RegionSize; + } + + char *exe = (char*)memoryInfo.BaseAddress; + + // Replace the entry point with a jump to the loader + char *entryPoint = exe + ntHeaders->OptionalHeader.AddressOfEntryPoint; + + const unsigned char JUMP_INST[] = { 0xFF, 0x25, 0x00, 0x00, 0x00, 0x00 }; + + write_protected_process_memory(process, entryPoint, JUMP_INST, sizeof(JUMP_INST)); + write_protected_process_memory(process, entryPoint + sizeof(JUMP_INST), &remoteAlloc, sizeof(remoteAlloc)); + + // Break the import table to prevent any dlls from being loaded + // Step 1: break the first import descriptor + char *importDescriptors = exe + ntHeaders->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress; + IMAGE_IMPORT_DESCRIPTOR firstDescriptor; + ZeroMemory(&firstDescriptor, sizeof(firstDescriptor)); + + write_protected_process_memory(process, importDescriptors, &firstDescriptor, sizeof(firstDescriptor)); + + // Step 2: break the image data directory entry + ntHeaders->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].Size = 0; + + write_protected_process_memory(process, exe, exeHeader, sizeof(exeHeader)); +} diff --git a/injector/launcher_payload/meson.build b/injector/launcher_payload/meson.build new file mode 100644 index 0000000..4786385 --- /dev/null +++ b/injector/launcher_payload/meson.build @@ -0,0 +1,18 @@ +# Assemble the payload that will be injected into the game +l_payload_bin = asm_gen.process('src/payload.asm') + +# Embed it into the library +l_res_files = custom_target( + 'lpayload.[oh]', + output: [ 'lpayload.o', 'lpayload.h' ], + input: [ l_payload_bin ], + command: [ gen_res, './injector/launcher_payload', '@OUTPUT0@', '@OUTPUT1@', '@INPUT@' ] +) + +shared_library( + 'launcher_payload', + 'src/dll.c', + l_res_files, + include_directories: '../include', + name_prefix: '' +) diff --git a/injector/launcher_payload/src/dll.c b/injector/launcher_payload/src/dll.c new file mode 100644 index 0000000..8ba1403 --- /dev/null +++ b/injector/launcher_payload/src/dll.c @@ -0,0 +1,67 @@ +#include + +#include + +const char EXE_ENV[] = "JADEITE_TARGET_EXE_PATH"; +const char INJECT_DLL_ENV[] = "JADEITE_INJECT_DLL_PATH"; + +static inline void read_env(const char *env, char *dest, size_t size) { + GetEnvironmentVariableA(env, dest, size); + SetEnvironmentVariableA(env, ""); +} + +BOOL WINAPI DllMain(HINSTANCE inst, DWORD reason, LPVOID reserved) { + // Only listen for attach + if (reason != DLL_PROCESS_ATTACH) { + return TRUE; + } + + // Get target EXE path + char targetExe[MAX_PATH]; + read_env(EXE_ENV, targetExe, sizeof(targetExe)); + + // Get the path of the DLL to inject + char injectDll[MAX_PATH]; + read_env(INJECT_DLL_ENV, injectDll, sizeof(injectDll)); + + // Compute the working directory path + char workdir[MAX_PATH]; + strcpy(workdir, targetExe); + *(strrchr(workdir, '\\')) = '\0'; + + // Start the game + STARTUPINFO si; + ZeroMemory(&si, sizeof(si)); + + PROCESS_INFORMATION pi; + si.cb = sizeof(si); + ZeroMemory(&pi, sizeof(pi)); + + if (!CreateProcessA( + targetExe, + NULL, + NULL, + NULL, + FALSE, + CREATE_SUSPENDED, + NULL, + workdir, + &si, + &pi + )) { + exit(1); + } + + // Inject + void *payloadStart = &_binary_lpayload_o_p_payload_bin_start; + size_t payloadSize = (size_t)&_binary_lpayload_o_p_payload_bin_size; + inject(pi.hProcess, payloadStart, payloadSize, injectDll); + + // Resume the process + ResumeThread(pi.hThread); + + // The launcher process should now hang untill the game terminates + WaitForSingleObject(pi.hProcess, INFINITE); + + return TRUE; +} diff --git a/injector/launcher_payload/src/payload.asm b/injector/launcher_payload/src/payload.asm new file mode 100644 index 0000000..10048bc --- /dev/null +++ b/injector/launcher_payload/src/payload.asm @@ -0,0 +1,137 @@ +BITS 64 + +main: ; Replacement entry point + push rbp + mov rbp, rsp + sub rsp, 30h + 90h + + + call GetKernel32ModuleHandle + mov [rbp - 8h], rax ; kernel32.dll + + mov rcx, rax + call GetAddressOf_GetProcAddress + mov [rbp - 10h], rax ; *GetProcAddress + + + mov rcx, [rbp - 8h] ; kernel32.dll + lea rdx, [rel s_LoadLibraryA] + mov rax, [rbp - 10h] ; *GetProcAddress + call rax ; rax = *LoadLibraryA + mov [rbp - 18h], rax + + lea rcx, [rel dllPath] + call rax ; LoadLibraryA(dllPath) + + + mov rcx, [rbp - 8h] ; kernel32.dll + lea rdx, [rel s_GetModuleHandleA] + mov rax, [rbp - 10h] ; *GetProcAddress + call rax ; rax = *GetModuleHandle + + mov rcx, 0 + call rax ; rax = .exe base address + mov [rbp - 20h], rax + + mov rcx, [rbp - 8h] ; kernel32.dll + lea rdx, [rel s_GetCommandLineW] + mov rax, [rbp - 10h] ; *GetProcAddress + call rax ; rax = *GetCommandLineW + + call rax ; rax = command line + mov [rbp - 28h], rax + + + lea rcx, [rel s_UnityPlayer.dll] + mov rax, [rbp - 18h] ; *LoadLibraryA + call rax ; rax = UnityPlayer.dll + + mov rcx, rax + lea rdx, [rel s_UnityMain] + mov rax, [rbp - 10h] ; *GetProcAddress + call rax ; rax = *UnityMain + + mov rcx, [rbp - 20h] ; .exe base address + mov rdx, 0 ; hPrevInstance - 0 + mov r8, [rbp - 28h] ; command line + mov r9, 1 ; SW_NORMAL + call rax ; UnityMain(...) + + + add rsp, 30h + 90h + pop rbp + ret + + +; https://dennisbabkin.com/blog/?t=how-to-implement-getprocaddress-in-shellcode +GetKernel32ModuleHandle: + mov rax, gs:[60h] + mov rax, [rax + 18h] + mov rax, [rax + 20h] + mov rax, [rax] + mov rax, [rax] + mov rax, [rax + 20h] + ret + + +GetAddressOf_GetProcAddress: + mov eax, [rcx + 3ch] + add rax, rcx + lea rax, [rax + 88h] + + mov edx, [rax] + lea rax, [rcx + rdx] + + mov edx, [rax + 18h] + mov r8d, [rax + 20h] + lea r8, [rcx + r8] + + mov r10, 41636f7250746547h ; "GetProcA" + mov r11, 0073736572646441h ; "Address\0" + +GAO_GPA@1: + mov r9d, [r8] + lea r9, [rcx + r9] + + ; Function name comparision + cmp r10, [r9] + jnz GAO_GPA@2 + cmp r11, [r9 + 7] + jnz GAO_GPA@2 + + ; Found GetProcAddress + neg rdx + mov r10d, [rax + 18h] + lea rdx, [r10 + rdx] + + mov r10d, [rax + 24h] + lea r10, [rcx + r10] + movzx rdx, word [r10 + rdx * 2] + + mov r10d, [rax + 1ch] + lea r10, [rcx + r10] + + mov r10d, [r10 + rdx * 4] + + lea rax, [rcx + r10] ; Function address + jmp GAO_GPA@end + +GAO_GPA@2: + add r8, 4 + dec rdx + jnz GAO_GPA@1 + +GAO_GPA@end: + ret + + +; Strings +s_LoadLibraryA: db "LoadLibraryA", 0 +s_GetModuleHandleA: db "GetModuleHandleA", 0 +s_GetCommandLineW: db "GetCommandLineW", 0 +s_UnityPlayer.dll: db "UnityPlayer.dll", 0 +s_UnityMain: db "UnityMain", 0 + +dllPath: + ; This will be filled out by the launcher payload dll + ; Path to the dll to inject into the game diff --git a/injector/meson.build b/injector/meson.build new file mode 100644 index 0000000..474ac4d --- /dev/null +++ b/injector/meson.build @@ -0,0 +1,21 @@ +# Assemble the payload that will be injected into the launcher +inj_payload_bin = asm_gen.process('src/payload.asm') + +# Embed it into the library +inj_res_files = custom_target( + 'ipayload.[oh]', + output: [ 'ipayload.o', 'ipayload.h' ], + input: [ inj_payload_bin ], + command: [ gen_res, './injector', '@OUTPUT0@', '@OUTPUT1@', '@INPUT@' ] +) + +# Main injector exe +executable( + 'jadeite', + 'src/injector.c', + inj_res_files, + include_directories: 'include', + name_prefix: '' +) + +subdir('launcher_payload') diff --git a/injector/src/injector.c b/injector/src/injector.c new file mode 100644 index 0000000..820fa50 --- /dev/null +++ b/injector/src/injector.c @@ -0,0 +1,87 @@ +#include + +#include + +#include + +const char EXE_ENV[] = "JADEITE_TARGET_EXE_PATH"; +const char INJECT_DLL_ENV[] = "JADEITE_INJECT_DLL_PATH"; + +const char LAUNCHER_INJECT_DLL[] = "launcher_payload.dll"; +const char GAME_INJECT_DLL[] = "game_payload.dll"; + +int main(int argc, char **argv) { + // Read arguments + char *gamePath = NULL; + char *launcherPath = NULL; + + switch (argc) { + case 1: + printf("Usage: wine jadeite.exe [game path] \n"); + return 0; + case 2: + printf("No launcher process specified! Using explorer.exe\n"); + gamePath = argv[1]; + launcherPath = "C:\\Windows\\explorer.exe"; + break; + case 3: + gamePath = argv[1]; + launcherPath = argv[2]; + break; + default: + fprintf(stderr, "Too many arguments! (%d)\n", argc); + return 1; + } + + // Compute absolute paths + char gameExePath[MAX_PATH]; + GetFullPathNameA(gamePath, sizeof(gameExePath), gameExePath, NULL); + + char gamePayloadPath[MAX_PATH]; + GetFullPathNameA(GAME_INJECT_DLL, sizeof(gamePayloadPath), gamePayloadPath, NULL); + + char launcherPayloadPath[MAX_PATH]; + GetFullPathNameA(LAUNCHER_INJECT_DLL, sizeof(launcherPayloadPath), launcherPayloadPath, NULL); + + printf("Starting \"%s\" via \"%s\"\n", gameExePath, launcherPath); + + // Set envvars + SetEnvironmentVariableA(EXE_ENV, gameExePath); + SetEnvironmentVariableA(INJECT_DLL_ENV, gamePayloadPath); + + // Start the launcher + STARTUPINFO si; + ZeroMemory(&si, sizeof(si)); + + PROCESS_INFORMATION pi; + si.cb = sizeof(si); + ZeroMemory(&pi, sizeof(pi)); + + if (!CreateProcessA( + launcherPath, + NULL, + NULL, + NULL, + FALSE, + CREATE_SUSPENDED, + NULL, + NULL, + &si, + &pi + )) { + fprintf(stderr, "Could not start process! (%ld)\n", GetLastError()); + exit(1); + } + + printf("Started launcher process (%ld)\n", pi.dwProcessId); + + // Inject + void *payloadStart = &_binary_ipayload_o_p_payload_bin_start; + size_t payloadSize = (size_t)&_binary_ipayload_o_p_payload_bin_size; // yes this is valid + inject(pi.hProcess, payloadStart, payloadSize, launcherPayloadPath); + + // Resume the process + ResumeThread(pi.hThread); + + return 0; +} diff --git a/injector/src/payload.asm b/injector/src/payload.asm new file mode 100644 index 0000000..269d963 --- /dev/null +++ b/injector/src/payload.asm @@ -0,0 +1,98 @@ +BITS 64 + +main: ; Replacement entry point + push rbp + mov rbp, rsp + sub rsp, 10h + 90h + + + call GetKernel32ModuleHandle + mov [rbp - 8h], rax ; kernel32.dll + + mov rcx, rax + call GetAddressOf_GetProcAddress + mov [rbp - 10h], rax ; *GetProcAddress + + + mov rcx, [rbp - 8h] ; kernel32.dll + lea rdx, [rel s_LoadLibraryA] + mov rax, [rbp - 10h] ; *GetProcAddress + call rax ; rax = *LoadLibraryA + + lea rcx, [rel dllPath] + call rax ; LoadLibraryA(dllPath) + + + add rsp, 10h + 90h + pop rbp + ret + + +; https://dennisbabkin.com/blog/?t=how-to-implement-getprocaddress-in-shellcode +GetKernel32ModuleHandle: + mov rax, gs:[60h] + mov rax, [rax + 18h] + mov rax, [rax + 20h] + mov rax, [rax] + mov rax, [rax] + mov rax, [rax + 20h] + ret + + +GetAddressOf_GetProcAddress: + mov eax, [rcx + 3ch] + add rax, rcx + lea rax, [rax + 88h] + + mov edx, [rax] + lea rax, [rcx + rdx] + + mov edx, [rax + 18h] + mov r8d, [rax + 20h] + lea r8, [rcx + r8] + + mov r10, 41636f7250746547h ; "GetProcA" + mov r11, 0073736572646441h ; "Address\0" + +GAO_GPA@1: + mov r9d, [r8] + lea r9, [rcx + r9] + + ; Function name comparision + cmp r10, [r9] + jnz GAO_GPA@2 + cmp r11, [r9 + 7] + jnz GAO_GPA@2 + + ; Found GetProcAddress + neg rdx + mov r10d, [rax + 18h] + lea rdx, [r10 + rdx] + + mov r10d, [rax + 24h] + lea r10, [rcx + r10] + movzx rdx, word [r10 + rdx * 2] + + mov r10d, [rax + 1ch] + lea r10, [rcx + r10] + + mov r10d, [r10 + rdx * 4] + + lea rax, [rcx + r10] ; Function address + jmp GAO_GPA@end + +GAO_GPA@2: + add r8, 4 + dec rdx + jnz GAO_GPA@1 + +GAO_GPA@end: + ret + + +; Strings +s_LoadLibraryA: db "LoadLibraryA", 0 + +dllPath: + ; This will be filled out by the injector + ; Path to the dll to inject into the launcher diff --git a/meson.build b/meson.build new file mode 100644 index 0000000..2ea7867 --- /dev/null +++ b/meson.build @@ -0,0 +1,21 @@ +project('jadeite', 'c', version: '1.0.0') + +nasm = find_program('nasm') +gen_res = find_program('gen_resources.sh') + +# Generator for compiling asm files +asm_gen = generator( + nasm, + output: '@BASENAME@.bin', + arguments: [ + '-f', 'bin', + '@INPUT@', + '-o', '@OUTPUT@' + ] +) + +# Payload that gets injected into the game +subdir('game_payload') + +# The injector exe and dll +subdir('injector') diff --git a/mingw_cross.txt b/mingw_cross.txt new file mode 100644 index 0000000..1a7967b --- /dev/null +++ b/mingw_cross.txt @@ -0,0 +1,13 @@ +[binaries] +c = 'x86_64-w64-mingw32-gcc' +cpp = 'x86_64-w64-mingw32-g++' +ld = 'x86_64-w64-mingw32-ld' +ar = 'x86_64-w64-mingw32-ar' +strip = 'x86_64-w64-mingw32-strip' +pkgconfig = 'x86_64-w64-mingw32-pkg-config' + +[host_machine] +system = 'windows' +cpu_family = 'x86_64' +cpu = 'x86_64' +endian = 'little' diff --git a/setup.sh b/setup.sh new file mode 100644 index 0000000..fdf562c --- /dev/null +++ b/setup.sh @@ -0,0 +1,4 @@ +#!/usr/bin/env sh + +rm -rf build +meson setup --cross-file mingw_cross.txt build