69 Commits

Author SHA1 Message Date
ac67488255 v2.0.1 2023-08-03 09:08:29 +03:00
1375549216 Add support for 3rd 3.8.0 glb/sea/tw/kr/jp 2023-08-03 09:07:18 +03:00
19056bed0d Update 3rd tables to 3.8.0 2023-08-03 09:04:04 +03:00
8dfe04d005 v2.0.0 2023-08-03 08:35:18 +03:00
6bdb04a925 Document newly supported HI3 regions 2023-08-03 00:09:47 +03:00
9ccde2618b Update metadata.json with new HI3 regions 2023-08-03 00:02:35 +03:00
ce58ec89ef Implement multiregion support for 3rd 2023-08-02 23:32:12 +03:00
5b1ac8533d Add tables for 3rd sea/cn/tw/kr/jp 2023-08-02 23:17:35 +03:00
e554c8e57b Major core rewrite 2023-08-02 18:32:26 +03:00
be445e6db9 Fix _create_driver_file error message 2023-08-02 02:29:18 +03:00
6279bb573d Make _load_module_patched function static 2023-08-02 02:24:39 +03:00
eb38894de5 Refactor ace.c 2023-08-02 01:42:04 +03:00
2612ad2212 Only store the name of the game assembly 2023-08-02 01:39:39 +03:00
b3f64ba6f6 v1.1.13 2023-07-31 15:13:59 +03:00
dd15dc60e3 Remove I_WANT_A_BAN requirement 2023-07-30 00:50:04 +03:00
ac68448cbd Mark HSR as verified 2023-07-30 00:38:31 +03:00
80c817cb6b Renamed tp6.c to core.c 2023-07-28 01:51:04 +03:00
4e614e1d82 v1.1.12 2023-07-19 01:17:44 +03:00
8b9f8e68aa Change HSR version to v1.2.0 2023-07-17 23:57:58 +03:00
99c0c20a3d Add tables for SR v1.2.0 2023-07-17 23:56:37 +03:00
64a25b1607 Update checksums for SR 1.2.0 2023-07-17 23:56:37 +03:00
43e8adaf12 Change SR status to "unverified" from "unsafe" 2023-07-17 23:54:47 +03:00
0004c26d7a Recover the executable memory to it's original state 2023-07-16 17:29:06 +03:00
848ae06792 Write recovery data into the inject allocation 2023-07-16 14:58:18 +03:00
c979c980c1 v1.1.11 2023-07-09 00:03:31 +03:00
3d943b641b Fix additional issue introduced by previous rework 2023-07-09 00:01:48 +03:00
215af6e3db v1.1.10 2023-07-08 23:44:25 +03:00
7aef85851c Fixed subtle core patch bug introduced by previous rework 2023-07-08 23:43:34 +03:00
f7c24f5ab7 v1.1.9 2023-07-08 21:24:17 +03:00
2ebd40b621 Internal rework to avoid a possible edge case 2023-07-08 21:17:28 +03:00
7f43a042e8 v1.1.8 2023-07-08 16:34:12 +03:00
8164694c8f Point user to Jadeite Readme 2023-07-08 15:11:52 +02:00
61e657b2e4 Readme maintenance 2023-07-07 14:14:28 +03:00
7a8087e8a1 Refactor resource gen script 2023-07-07 13:59:28 +03:00
351fe85e2f Move blob file to objects kwarg in target declaration 2023-07-06 00:04:41 +03:00
938b663bd9 Change blob file extension 2023-07-06 00:02:17 +03:00
df1f611199 Publish precompiled tp6.c blob (lots of dirty hacks) 2023-07-05 23:59:27 +03:00
94efee7496 Don't use narrow characters with widestring functions 2023-07-04 01:08:03 +03:00
06be53ad8e Fix an incredibly dumb bitflag error 2023-07-04 01:03:21 +03:00
ce427556a3 More efficient string comparisions 2023-07-03 17:04:32 +03:00
15f56c9e5a v1.1.7 2023-07-03 14:07:38 +03:00
b860834be1 Switch to using wide strings in the injector 2023-07-03 14:04:04 +03:00
8c900f93fc Fix freeing heap on directory check 2023-07-03 11:57:08 +03:00
a7d68776bd Update credits section 2023-07-02 23:48:54 +03:00
6d742b2a15 v1.1.6 2023-07-02 23:29:47 +03:00
cf5d87f7a7 Refuse to launch if the patcher is inside the game directory 2023-07-02 23:21:17 +03:00
181d14e4ce Minor readme styling changes 2023-07-02 20:51:49 +03:00
0067ceb85c Document AAT third-party launchers 2023-07-02 20:50:49 +03:00
33cf0a65e8 Document HI3 v6.7.0 support 2023-06-30 11:55:12 +03:00
d30a2aba9e Update comment in hi3.c 2023-06-29 12:58:57 +03:00
05ed4980c5 v1.1.5 2023-06-29 12:52:46 +03:00
1341e1600c Add tables for HI3 v6.7.0 glb 2023-06-29 12:52:08 +03:00
0e2b60aecb Move envvar defs into a separate header 2023-06-26 12:53:07 +03:00
33f7dd89a9 Optimize assembly payloads 2023-06-26 12:25:44 +03:00
851ebb5b9a Use nonvolatile registers for storage 2023-06-26 12:18:25 +03:00
a0e79dcea0 Major injector refactoring 2023-06-25 12:32:19 +03:00
55fd21feef Move common assembly into a separate file 2023-06-25 02:19:24 +03:00
22a7fb2a60 Use NASM local labels 2023-06-25 01:51:18 +03:00
39147ac049 Show a warning if someone tries to run build.sh 2023-06-25 01:47:32 +03:00
9a1405c828 Use winapi types in pe.c 2023-06-25 01:42:32 +03:00
5473908df4 Show invalid UnityPlayer.dll checksum in hex 2023-06-23 20:00:27 +03:00
6a237fd247 Only write the modified field in the exe header 2023-06-23 18:55:22 +03:00
95dec1b0fd Remove useless includes in hsr.c 2023-06-21 17:47:42 +03:00
661ef826c3 Move magic number into def 2023-06-21 16:24:16 +03:00
6209157cf2 v1.1.4 2023-06-21 15:42:19 +03:00
f26bcbd0fc HOTFIX: fix module unloading with SRFIX_DISABLE 2023-06-21 15:41:46 +03:00
14c90f7137 v1.1.3 2023-06-21 14:52:38 +03:00
8f96ec4eec Unpatch WriteTextureStatisticUserData after execution 2023-06-21 14:47:46 +03:00
5421487212 Counter-based unloading logic 2023-06-21 14:25:22 +03:00
48 changed files with 890 additions and 591 deletions

2
.gitignore vendored
View File

@ -3,7 +3,7 @@
.directory .directory
# File withheld to make abuse more difficult # File withheld to make abuse more difficult
game_payload/src/tp6.c game_payload/src/core.c
build build
out out

View File

@ -1,11 +1,11 @@
### Games and regions ### Games and regions
- **3rd**: glb v6.6.0 - **3rd**: glb/sea/cn/tw/kr/jp v6.8.0
- **SR**: os/cn v1.1.0 (unsafe, refer to [configuration](#configuration)) - **SR**: os/cn v1.2.0 (potentially unsafe, but no bans were reported since v1.1.0)
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. 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 ### Information
The anticheat the games use is fundamentally incompatible with Wine in multiple ways. This tool launches the game without it (`injector/launcher_payload`) and imitates it's behaviour (`game_payload`). The anticheat the games use is fundamentally incompatible with Wine in multiple ways. This tool launches the game without it (`injector`) and imitates it's behaviour (`game_payload`).
**SR-specific**: this tool disables the use of DirectX shared resources in a rather hacky way. It is required, as there is no (and most likely never will be) shared resources support in DirectX translation layers (WineD3D/DXVK). Refer to [configuration](#configuration) if you wish to run the game without the fix. **SR-specific**: this tool disables the use of DirectX shared resources in a rather hacky way. It is required, as there is no (and most likely never will be) shared resources support in DirectX translation layers (WineD3D/DXVK). Refer to [configuration](#configuration) if you wish to run the game without the fix.
@ -14,11 +14,11 @@ The anticheat the games use is fundamentally incompatible with Wine in multiple
**This is not a cheating tool**. Using it with Windows is not possible, and Windows support is not planned or intended in any way. However, as it does not perform any on-disk file modifications, you may reuse the same game install for Windows if you have a dual-boot setup. **This is not a cheating tool**. Using it with Windows is not possible, and Windows support is not planned or intended in any way. However, as it does not perform any on-disk file modifications, you may reuse the same game install for Windows if you have a dual-boot setup.
### Usage ### 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. **Refer to [third-party launchers](#third-party-launchers)** 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.** **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. **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. **IMPORTANT: do not run the mfplat fix under Proton GE. Doing so may irreparably damage your game installation!**
Manual usage instructions: Manual usage instructions:
- Download the game you want to run - Download the game you want to run
@ -32,20 +32,19 @@ This tool is capable of starting the games from a different process. This may be
To pass commandline arguments to the game, append them after the launcher path: `wine jadeite.exe 'Z:\wine\path\to\game.exe' 'Z:\wine\path\to\launcher.exe' -arg1 -arg2 -arg3`. To use the default launcher process, use `--`: `wine jadeite.exe 'Z:\wine\path\to\game.exe' -- -arg1 -arg2 -arg3`. To pass commandline arguments to the game, append them after the launcher path: `wine jadeite.exe 'Z:\wine\path\to\game.exe' 'Z:\wine\path\to\launcher.exe' -arg1 -arg2 -arg3`. To use the default launcher process, use `--`: `wine jadeite.exe 'Z:\wine\path\to\game.exe' -- -arg1 -arg2 -arg3`.
### Configuration ### Configuration
These environment variables can be used to configure the behaviour of the tool. These environment variables can be used to configure the behaviour of the tool. Any value except empty string counts as set. `1` will be used in all examples.
- `WAIT_BEFORE_RESUME=1` - show a messagebox and wait for user input before resuming the game process. Useful on my side for debugging - `WAIT_BEFORE_RESUME=1` - show a messagebox and wait for user input before resuming the game process. Useful on my side for debugging
**SR-exclusive**: **SR-exclusive**:
- `I_WANT_A_BAN=1` - allows to launch SR. Please only use testing accounts, as there is an extremely high risk of getting banned - `SRFIX_DISABLE=1` - disable shared resources fix. Not recommended. Doing so will most likely cause the game to not run at all
- `SRFIX_DISABLE=1` - disable shared resources fix
### Internals ### Internals
This tool consists of three parts: the main injector (`injector`), the launcher payload (`injector/launcher_payload`) and the game payload (`game_payload`). This tool consists of three parts: the main injector (`injector/src/exe.c`), the launcher payload (`injector/src/dll.c`) 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. 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. A part of the source code is witheld (`game_payload/src/core.c`). This is a forced measure to make abuse more difficult. However, a precompiled blob is provided in the repo. `build.sh` will use it automatically.
### Guildelines ### Guildelines
1. **Please don't share this project in public.** This might attract unnecessary attention from either the Game Company or the Anticheat Company 1. **Please don't share this project in public.** This might attract unnecessary attention from either the Game Company or the Anticheat Company
@ -55,11 +54,13 @@ A part of the source code is witheld (`game_payload/src/tp6.c`). This is a force
Please do not report any issues with the Game to the official channels. Use the issue tracker of this repository Please do not report any issues with the Game to the official channels. Use the issue tracker of this repository
### Third-party launchers ### Third-party launchers
Will be written later - Honkers Launcher — Linux launcher for 3rd ([GitHub](https://github.com/an-anime-team/honkers-launcher) | [Codeberg](https://codeberg.org/an-anime-team/honkers-launcher))
- The Honkers Railway Launcher — Linux launcher for SR ([GitHub](https://github.com/an-anime-team/the-honkers-railway-launcher) | [Codeberg](https://codeberg.org/an-anime-team/the-honkers-railway-launcher))
### Credits ### Credits
- mkrsym1 — project leader, reverse engineering - mkrsym1 — project leader, reverse engineering
- Yor#1920 — major help with analyzing network activity - [EternalStudentDesuKa](https://github.com/EternalStudentDesuKa) — major help with analyzing network activity
- [An Anime Team](https://github.com/an-anime-team) — Honkers Launcher and The Honkers Railway Launcher
- Some others credited in the source code - Some others credited in the source code
License: MIT License: MIT

View File

@ -1,4 +1,5 @@
#!/usr/bin/env bash #!/usr/bin/env bash
set -e
strip="x86_64-w64-mingw32-strip" strip="x86_64-w64-mingw32-strip"
@ -6,12 +7,12 @@ rm -f jadeite.zip
rm -rf out rm -rf out
sh setup.sh --buildtype=release sh setup.sh --buildtype=release
ninja -C build meson compile -C build
mkdir out mkdir out
cp ./build/injector/jadeite.exe ./out cp ./build/injector/jadeite.exe ./out
cp ./build/injector/launcher_payload/launcher_payload.dll ./out cp ./build/injector/launcher_payload.dll ./out
cp ./build/game_payload/game_payload.dll ./out cp ./build/game_payload/game_payload.dll ./out
cp ./LICENSE.txt ./out cp ./LICENSE.txt ./out

BIN
game_payload/blob/core.o Normal file

Binary file not shown.

View File

@ -0,0 +1,4 @@
#!/usr/bin/env sh
cp "$1" "$2"
cp "$1" "$3"

View File

@ -0,0 +1,7 @@
#pragma once
#include <windows.h>
#include <game.h>
void core_setup_patcher(struct game_data *game, HMODULE baseModule);

View File

@ -6,6 +6,11 @@ enum game_id {
GAME_INVALID, GAME_INVALID,
GAME_HI3_GLB, GAME_HI3_GLB,
GAME_HI3_SEA,
GAME_HI3_CN,
GAME_HI3_TW,
GAME_HI3_KR,
GAME_HI3_JP,
GAME_HSR_OS, GAME_HSR_OS,
GAME_HSR_CN GAME_HSR_CN
@ -18,7 +23,7 @@ typedef void (*unityplayer_callback_t)(HMODULE unityModule);
struct game_data { struct game_data {
enum game_id id; // Temporary enum game_id id; // Temporary
const char *name; const char *name;
const char *assembly_path; const char *assembly_name;
const char *tp6_section_name; // Unused for now const char *tp6_section_name; // Unused for now
const char *tvm_section_name; const char *tvm_section_name;

View File

@ -0,0 +1,4 @@
#pragma once
void unload_ctr_inc();
void unload_ctr_dec();

View File

@ -1,7 +0,0 @@
#pragma once
#include <windows.h>
#include <game.h>
void tp6_setup_patcher(struct game_data *game, HMODULE thisModule, HMODULE baseModule);

View File

@ -1,3 +1,7 @@
fs = import('fs')
include_dir = include_directories('include')
# Input files # Input files
sources = [ sources = [
'src/main.c', 'src/main.c',
@ -8,33 +12,71 @@ sources = [
'src/hi3.c', 'src/hi3.c',
'src/hsr.c', 'src/hsr.c',
'src/utils.c', 'src/utils.c',
'src/msg.c', 'src/msg.c'
# File withheld to make abuse more difficult
'src/tp6.c'
] ]
resources = [ resources = [
'res/hi3/glb/allocations.dat', 'res/hi3/glb.dat',
'res/hi3/glb/entries.dat', 'res/hi3/sea.dat',
'res/hi3/cn.dat',
'res/hi3/tw.dat',
'res/hi3/kr.dat',
'res/hi3/jp.dat',
'res/hsr/os/allocations.dat', 'res/hsr/os.dat',
'res/hsr/os/entries.dat', 'res/hsr/cn.dat'
'res/hsr/cn/allocations.dat',
'res/hsr/cn/entries.dat'
] ]
# Generate resource files for ./res # Generate resource files for ./res
res_files = custom_target( res_header = custom_target(
'resources.[ho]', 'resources.h',
output: [ 'resources.o', 'resources.h' ], output: 'resources.h',
input: resources, input: resources,
command: [ gen_res, meson.current_source_dir(), '@OUTPUT0@', '@OUTPUT1@', '@INPUT@' ] command: [ gen_res, '--header', meson.current_source_dir(), '@OUTPUT0@', '@INPUT@' ]
) )
res_object = custom_target(
'resources.o',
output: 'resources.o',
input: resources,
command: [ gen_res, '--object', meson.current_source_dir(), '@OUTPUT0@', '@INPUT@' ]
)
if fs.exists('src/core.c')
# Compile the real file first (dirty hack)
core_fake_exe = executable(
'core.o',
'src/core.c',
res_header,
link_args: [ '-r' ], # Output an object file
include_directories: include_dir
)
# another dirty hack
copy_core = find_program('copy_core.sh')
core_target = [custom_target(
'copy_core',
output: 'core.o',
input: core_fake_exe.extract_all_objects(recursive: false),
command: [
copy_core,
'@INPUT0@',
'@OUTPUT0@', meson.current_source_dir() / 'blob/core.o'
]
)]
core_blob = []
else
message('Using precompiled core blob. Refer to the readme for more details')
core_target = []
core_blob = [ 'blob/core.o' ]
endif
shared_library( shared_library(
'game_payload', 'game_payload',
sources, sources,
res_files, res_header,
include_directories: 'include', res_object,
core_target,
objects: core_blob,
include_directories: include_dir,
name_prefix: '' name_prefix: ''
) )

BIN
game_payload/res/hi3/cn.dat Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
game_payload/res/hi3/jp.dat Normal file

Binary file not shown.

BIN
game_payload/res/hi3/kr.dat Normal file

Binary file not shown.

Binary file not shown.

BIN
game_payload/res/hi3/tw.dat Normal file

Binary file not shown.

BIN
game_payload/res/hsr/cn.dat Normal file

Binary file not shown.

Binary file not shown.

BIN
game_payload/res/hsr/os.dat Normal file

Binary file not shown.

Binary file not shown.

View File

@ -9,83 +9,75 @@ static void _dll_notification(ULONG reason, const PLDR_DLL_NOTIFICATION_DATA dat
return; return;
} }
// context should be set to the target module name, lowercase // context should be set to the target module name
wchar_t *targetModuleName = (wchar_t*)context; wchar_t *targetModuleName = (wchar_t*)context;
wchar_t lwModuleName[MAX_PATH]; if (wcsicmp(targetModuleName, data->Loaded.BaseDllName->Buffer) != 0) {
wcscpy(lwModuleName, data->Loaded.BaseDllName->Buffer); return;
_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);
} }
// 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);
}
static void _create_driver_file(const char *path) {
// They only report presence
HANDLE file = CreateFileA(path, GENERIC_WRITE, FILE_SHARE_READ, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
if (file == INVALID_HANDLE_VALUE) {
msg_err_a("Could not create driver file: %s", path);
}
CloseHandle(file);
} }
void ace_fake_driver_files() { void ace_fake_driver_files() {
// They only report presence _create_driver_file("ACE-BASE.sys");
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) {
msg_err_a("Could not create driver file: %s", wdDriverPath);
}
// Just in case // Just in case
HANDLE s32DriverFile = CreateFileA(s32DriverPath, GENERIC_WRITE, FILE_SHARE_READ, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL); _create_driver_file("C:\\windows\\system32\\drivers\\ACE-BASE.sys");
if (!s32DriverFile) { }
msg_err_a("Could not create driver file: %s", s32DriverPath);
static HMODULE _load_module_patched(wchar_t *path) {
// Get filename from the path
wchar_t *name = wcsrchr(path, '\\');
name = name ? name + 1 : path;
void *cookie;
LdrRegisterDllNotification(0, &_dll_notification, name, &cookie);
HMODULE module = LoadLibraryW(path);
if (!module) {
msg_err_w(L"Could not load module: %ls", path);
} }
CloseHandle(wdDriverFile); // LoadLibraryW is synchronous; the notification function has already finished executing
CloseHandle(s32DriverFile); LdrUnregisterDllNotification(cookie);
return module;
} }
HMODULE ace_load_base_module(const char *exeName) { HMODULE ace_load_base_module(const char *exeName) {
wchar_t baseModuleName[MAX_PATH]; wchar_t baseModuleName[MAX_PATH];
swprintf(baseModuleName, MAX_PATH, L"%sbase.dll", exeName); swprintf(baseModuleName, MAX_PATH, L"%sBase.dll", exeName);
wcslwr(baseModuleName); wcslwr(baseModuleName);
void *cookie; return _load_module_patched(baseModuleName);
LdrRegisterDllNotification(0, &_dll_notification, baseModuleName, &cookie);
HMODULE baseModule = LoadLibraryW(baseModuleName);
if (!baseModule) {
msg_err_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() { HMODULE ace_load_driver_module() {
const char *driverModulePath = "AntiCheatExpert/InGame/x64/ACE-DRV64.dll"; return _load_module_patched(L"AntiCheatExpert\\InGame\\x64\\ACE-DRV64.dll");
void *cookie;
LdrRegisterDllNotification(0, &_dll_notification, L"ace-drv64.dll", &cookie);
HMODULE driverModule = LoadLibraryA(driverModulePath);
if (!driverModule) {
msg_err_a("Could not load driver module: %s", driverModulePath);
}
// LoadLibraryA is synchronous; the notification function has already finished executing
LdrUnregisterDllNotification(cookie);
return driverModule;
} }

18
game_payload/src/core.md Normal file
View File

@ -0,0 +1,18 @@
### 1.0.0
- First version
### 1.1.0
- HSR support
### 1.1.9
- Fixed a bug which could cause the game to crash in odd scenarios
### 1.1.10
- Fixed a subtle bug introduced in 1.1.9
### 1.1.11
- Fixed an additional issue introduced in 1.1.9
### 2.0.0
- Almost a full rewrite, functionality unchanged
- Added support for HI3 sea/cn/tw/jp/kr

View File

@ -4,7 +4,7 @@
#include <game.h> #include <game.h>
const char *HI3_NAME = "BH3"; const char *HI3_NAME = "BH3";
const char *HI3_ASSEMBLY_PATH = "BH3_Data/Native/UserAssembly.dll"; const char *HI3_ASSEMBLY_NAME = "UserAssembly.dll";
const char *HI3_TP6_SECTION_NAME = ".bh3"; const char *HI3_TP6_SECTION_NAME = ".bh3";
const char *HI3_TVM_SECTION_NAME = ".tvm0"; const char *HI3_TVM_SECTION_NAME = ".tvm0";
@ -14,10 +14,14 @@ struct crc_id_pair {
}; };
const struct crc_id_pair HI3_REGIONS[] = { 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 // It may be possible to get rid of region-specific data altogether in the future
{ 0x34bdec99, GAME_HI3_GLB } // glb v6.6.0 { 0xcb8041ff, GAME_HI3_GLB }, // glb v6.8.0
{ 0x104cbfc5, GAME_HI3_SEA }, // sea v6.8.0
{ 0x2efd9099, GAME_HI3_CN }, // cn v6.8.0
{ 0x30fa5b0f, GAME_HI3_TW }, // tw v6.8.0
{ 0xe47327fb, GAME_HI3_KR }, // kr v6.8.0
{ 0x992b6b63, GAME_HI3_JP } // jp v6.8.0
}; };
void hi3_fill_data(struct game_data *buf) { void hi3_fill_data(struct game_data *buf) {
@ -31,12 +35,12 @@ void hi3_fill_data(struct game_data *buf) {
} }
if (id == GAME_INVALID) { if (id == GAME_INVALID) {
msg_err_a("Invalid UnityPlayer.dll checksum: %d", crc); msg_err_a("Invalid UnityPlayer.dll checksum: %x", crc);
} }
buf->id = id; buf->id = id;
buf->name = HI3_NAME; buf->name = HI3_NAME;
buf->assembly_path = HI3_ASSEMBLY_PATH; buf->assembly_name = HI3_ASSEMBLY_NAME;
buf->tp6_section_name = HI3_TP6_SECTION_NAME; buf->tp6_section_name = HI3_TP6_SECTION_NAME;
buf->tvm_section_name = HI3_TVM_SECTION_NAME; buf->tvm_section_name = HI3_TVM_SECTION_NAME;

View File

@ -1,10 +1,11 @@
#include <utils.h> #include <utils.h>
#include <msg.h> #include <msg.h>
#include <main.h>
#include <game.h> #include <game.h>
const char *HSR_NAME = "StarRail"; const char *HSR_NAME = "StarRail";
const char *HSR_ASSEMBLY_PATH = "GameAssembly.dll"; const char *HSR_ASSEMBLY_NAME = "GameAssembly.dll";
const char *HSR_TP6_SECTION_NAME = ".ace"; const char *HSR_TP6_SECTION_NAME = ".ace";
const char *HSR_TVM_SECTION_NAME = ".tvm0"; const char *HSR_TVM_SECTION_NAME = ".tvm0";
@ -16,37 +17,60 @@ struct crc_id_pair {
const struct crc_id_pair HSR_REGIONS[] = { const struct crc_id_pair HSR_REGIONS[] = {
// It may be possible to get rid of region-specific data altogether in the future // It may be possible to get rid of region-specific data altogether in the future
{ 0x2df53005, GAME_HSR_OS }, // os v1.1.0 { 0x9eb3084e, GAME_HSR_OS }, // os v1.2.0
{ 0x3e644d26, GAME_HSR_CN } // cn v1.1.0 { 0x14be07e9, GAME_HSR_CN } // cn v1.2.0
}; };
#define JUMP_SIZE (6 + sizeof(void*))
// Temporarily hardcoded offset
// v1.2.0, same for os and cn
#define WTSUD_PATCH_OFFSET 0x16430
char wtsud_original_bytes[JUMP_SIZE];
char *wtsud_patch_addr;
static void _wtsud_stub() {
// Recover original bytes
DWORD oldProtect;
VirtualProtect(wtsud_patch_addr, JUMP_SIZE, PAGE_EXECUTE_READWRITE, &oldProtect);
memcpy(wtsud_patch_addr, wtsud_original_bytes, JUMP_SIZE);
VirtualProtect(wtsud_patch_addr, JUMP_SIZE, oldProtect, &oldProtect);
unload_ctr_dec();
}
static void _unityplayer_callback(HMODULE unityModule) { static void _unityplayer_callback(HMODULE unityModule) {
if (utils_env_enabled("SRFIX_DISABLE")) { if (utils_env_enabled("SRFIX_DISABLE")) {
msg_info_a("Shared resources fix disabled. The game may not work"); msg_info_a("Shared resources fix disabled. The game may not work");
return; return;
} }
// Disable shared resources
// Temporarily hardcoded offset // Remove dependency on shared resources by patching WriteTextureStatisticUserData
// v1.1.0, same for os and cn unload_ctr_inc();
unsigned char *srAddr = ((unsigned char*)unityModule) + 0x16430;
wtsud_patch_addr = ((char*)unityModule) + WTSUD_PATCH_OFFSET;
DWORD oldProtect; DWORD oldProtect;
VirtualProtect(srAddr, 1, PAGE_EXECUTE_READWRITE, &oldProtect); VirtualProtect(wtsud_patch_addr, JUMP_SIZE, PAGE_EXECUTE_READWRITE, &oldProtect);
*srAddr = 0xC3; // ret // Save original bytes
memcpy(wtsud_original_bytes, wtsud_patch_addr, JUMP_SIZE);
VirtualProtect(srAddr, 1, oldProtect, &oldProtect); // Write jump
const char JUMP_INST[] = { 0xFF, 0x25, 0x00, 0x00, 0x00, 0x00 }; // jmp [$ + 6]
memcpy(wtsud_patch_addr, JUMP_INST, sizeof(JUMP_INST));
// Write destination address
void *destAddr = &_wtsud_stub;
memcpy(wtsud_patch_addr + sizeof(JUMP_INST), &destAddr, sizeof(destAddr));
VirtualProtect(wtsud_patch_addr, JUMP_SIZE, oldProtect, &oldProtect);
} }
void hsr_fill_data(struct game_data *buf) { void hsr_fill_data(struct game_data *buf) {
if (!utils_env_enabled("I_WANT_A_BAN")) {
msg_err_a("Using this tool with HSR is unsafe. Refer to the readme for more details");
} else {
msg_warn_a("Using this tool with HSR will most likely result in a ban. Please only use testing accounts");
}
uint32_t crc = utils_file_crc32c("UnityPlayer.dll"); uint32_t crc = utils_file_crc32c("UnityPlayer.dll");
enum game_id id = GAME_INVALID; enum game_id id = GAME_INVALID;
@ -57,12 +81,12 @@ void hsr_fill_data(struct game_data *buf) {
} }
if (id == GAME_INVALID) { if (id == GAME_INVALID) {
msg_err_a("Invalid UnityPlayer.dll checksum: %d", crc); msg_err_a("Invalid UnityPlayer.dll checksum: %x", crc);
} }
buf->id = id; buf->id = id;
buf->name = HSR_NAME; buf->name = HSR_NAME;
buf->assembly_path = HSR_ASSEMBLY_PATH; buf->assembly_name = HSR_ASSEMBLY_NAME;
buf->tp6_section_name = HSR_TP6_SECTION_NAME; buf->tp6_section_name = HSR_TP6_SECTION_NAME;
buf->tvm_section_name = HSR_TVM_SECTION_NAME; buf->tvm_section_name = HSR_TVM_SECTION_NAME;

View File

@ -3,15 +3,34 @@
#include <ntdll.h> #include <ntdll.h>
#include <ace.h> #include <ace.h>
#include <game.h> #include <game.h>
#include <tp6.h> #include <core.h>
#include <utils.h> #include <utils.h>
#include <main.h>
HMODULE this_module;
size_t unload_ctr = 0;
void unload_ctr_inc() {
unload_ctr++;
}
void unload_ctr_dec() {
unload_ctr--;
if (unload_ctr == 0) {
void *pFreeLibrary = GetProcAddress(GetModuleHandleA("kernel32.dll"), "FreeLibrary");
CreateThread(NULL, 0, pFreeLibrary, this_module, 0, NULL);
}
}
BOOL WINAPI DllMain(HINSTANCE instance, DWORD reason, LPVOID reserved) { BOOL WINAPI DllMain(HINSTANCE instance, DWORD reason, LPVOID reserved) {
// Only listen to attach // Only listen to attach
if (reason != DLL_PROCESS_ATTACH) { if (reason != DLL_PROCESS_ATTACH) {
return TRUE; return TRUE;
} }
this_module = instance;
// Dynamically link functions from ntdll // Dynamically link functions from ntdll
ntdll_link(); ntdll_link();
@ -27,7 +46,7 @@ BOOL WINAPI DllMain(HINSTANCE instance, DWORD reason, LPVOID reserved) {
ace_load_driver_module(); ace_load_driver_module();
// ...magic // ...magic
tp6_setup_patcher(&game, instance, baseModule); core_setup_patcher(&game, baseModule);
// Load the UnityPlayer module and invoke the callback // Load the UnityPlayer module and invoke the callback
HMODULE unityModule = LoadLibraryA("UnityPlayer.dll"); HMODULE unityModule = LoadLibraryA("UnityPlayer.dll");

View File

@ -1,5 +1,3 @@
#include <stdint.h>
#include <pe.h> #include <pe.h>
void pe_find_section(HMODULE module, const char *section, MEMORY_BASIC_INFORMATION *buf) { void pe_find_section(HMODULE module, const char *section, MEMORY_BASIC_INFORMATION *buf) {
@ -8,11 +6,11 @@ void pe_find_section(HMODULE module, const char *section, MEMORY_BASIC_INFORMATI
IMAGE_DOS_HEADER* dosHeader = (IMAGE_DOS_HEADER*)module; IMAGE_DOS_HEADER* dosHeader = (IMAGE_DOS_HEADER*)module;
IMAGE_NT_HEADERS64* ntHeaders = (IMAGE_NT_HEADERS64*)(cModule + dosHeader->e_lfanew); IMAGE_NT_HEADERS64* ntHeaders = (IMAGE_NT_HEADERS64*)(cModule + dosHeader->e_lfanew);
uint16_t sectionCount = ntHeaders->FileHeader.NumberOfSections; WORD sectionCount = ntHeaders->FileHeader.NumberOfSections;
IMAGE_SECTION_HEADER* sectionHeader = (IMAGE_SECTION_HEADER*)(ntHeaders + 1); IMAGE_SECTION_HEADER* sectionHeader = (IMAGE_SECTION_HEADER*)(ntHeaders + 1);
void* targetAddress = 0x0; void* targetAddress = 0x0;
for (uint16_t i = 0; i < sectionCount; i++) { for (WORD i = 0; i < sectionCount; i++) {
if (strncmp((char*)sectionHeader->Name, section, 8) == 0) { if (strncmp((char*)sectionHeader->Name, section, 8) == 0) {
targetAddress = (void*)(cModule + sectionHeader->VirtualAddress); targetAddress = (void*)(cModule + sectionHeader->VirtualAddress);
break; break;

View File

@ -1,5 +0,0 @@
### 1.0.0
- First version
### 1.1.0
- HSR support

View File

@ -31,5 +31,5 @@ uint32_t utils_file_crc32c(const char *filePath) {
char utils_env_enabled(const char *env) { char utils_env_enabled(const char *env) {
char *envText = getenv(env); char *envText = getenv(env);
return envText && strcmp(envText, "") != 0; return envText && *envText;
} }

View File

@ -2,19 +2,38 @@
linker="x86_64-w64-mingw32-ld" linker="x86_64-w64-mingw32-ld"
# Select output types
for i in {0..1}
do
case "$1" in
--header)
gen_header=1
shift
;;
--object)
gen_object=1
shift
;;
esac
done
# Read project directory # Read project directory
proj_dir=`realpath "$1"` proj_dir=`realpath "$1"`
shift shift
# Read output file destinations # Read output file destinations and make sure they don't exist
resources_o=`realpath "$1"` if [ "x${gen_object}" = "x1" ]; then
shift resources_o=`realpath "$1"`
resources_h=`realpath "$1"` shift
shift
# Make sure that the header does not exist rm -f "${resources_h}"
rm -f "${resources_h}" fi
rm -f "${resources_o}" if [ "x${gen_header}" = "x1" ]; then
resources_h=`realpath "$1"`
shift
rm -f "${resources_o}"
fi
# Recomupte relative paths to parameters # Recomupte relative paths to parameters
idx=0 idx=0
@ -26,24 +45,28 @@ do
idx="$(("${idx}" + 1))" idx="$(("${idx}" + 1))"
done done
# Create the object file if [ "x${gen_object}" = "x1" ]; then
pushd "${proj_dir}" >> /dev/null # Create the object file
$linker -r -b binary -o "${resources_o}" "${resource_files[@]}" pushd "${proj_dir}" >> /dev/null
popd >> /dev/null $linker -r -b binary -o "${resources_o}" "${resource_files[@]}"
popd >> /dev/null
fi
# Include stddef.h in the resources header (for size_t) if [ "x${gen_header}" = "x1" ]; then
echo "#include <stddef.h>" >> "${resources_h}" # Include stddef.h in the resources header (for size_t)
echo "#include <stddef.h>" >> "${resources_h}"
for resource in "${resource_files[@]}" for resource in "${resource_files[@]}"
do do
# Use relative path to the resource as the variable name # Use relative path to the resource as the variable name
var_name="_binary_${resource}" var_name="_binary_${resource}"
# Replace all non-alphanumeric characters with underscores # Replace all non-alphanumeric characters with underscores
var_name=`printf "${var_name}" | sed "s/[^a-zA-Z0-9]/_/g"` var_name=`printf "${var_name}" | sed "s/[^a-zA-Z0-9]/_/g"`
# Define externs in the header # Define externs in the header
echo "extern void *${var_name}_start;" >> "${resources_h}" echo "extern void *${var_name}_start;" >> "${resources_h}"
echo "extern void *${var_name}_size;" >> "${resources_h}" echo "extern void *${var_name}_size;" >> "${resources_h}"
echo "" >> "${resources_h}" echo "" >> "${resources_h}"
done done
fi

7
injector/include/envs.h Normal file
View File

@ -0,0 +1,7 @@
#pragma once
#define EPFX L"__JADEITE_"
#define ENV_EXE_PATH EPFX"TARGET_EXE_PATH"
#define ENV_DLL_PATH EPFX"INJECT_DLL_PATH"
#define ENV_PROC_CMD EPFX"PROCESS_COMMAND"

View File

@ -1,33 +1,5 @@
BITS 64 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 ; https://dennisbabkin.com/blog/?t=how-to-implement-getprocaddress-in-shellcode
GetKernel32ModuleHandle: GetKernel32ModuleHandle:
mov rax, gs:[60h] mov rax, gs:[60h]
@ -54,15 +26,15 @@ GetAddressOf_GetProcAddress:
mov r10, 41636f7250746547h ; "GetProcA" mov r10, 41636f7250746547h ; "GetProcA"
mov r11, 0073736572646441h ; "Address\0" mov r11, 0073736572646441h ; "Address\0"
GAO_GPA@1: .1:
mov r9d, [r8] mov r9d, [r8]
lea r9, [rcx + r9] lea r9, [rcx + r9]
; Function name comparision ; Function name comparision
cmp r10, [r9] cmp r10, [r9]
jnz GAO_GPA@2 jnz .2
cmp r11, [r9 + 7] cmp r11, [r9 + 7]
jnz GAO_GPA@2 jnz .2
; Found GetProcAddress ; Found GetProcAddress
neg rdx neg rdx
@ -79,20 +51,12 @@ GAO_GPA@1:
mov r10d, [r10 + rdx * 4] mov r10d, [r10 + rdx * 4]
lea rax, [rcx + r10] ; Function address lea rax, [rcx + r10] ; Function address
jmp GAO_GPA@end jmp .end
GAO_GPA@2: .2:
add r8, 4 add r8, 4
dec rdx dec rdx
jnz GAO_GPA@1 jnz .1
GAO_GPA@end: .end:
ret 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

View File

@ -0,0 +1,5 @@
#pragma once
#include <windows.h>
void inject(HANDLE process, const void *payload, size_t payloadSize, const wchar_t *dllPath);

View File

@ -1,18 +0,0 @@
# 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: ''
)

View File

@ -1,74 +0,0 @@
#include <stdio.h>
#include <injshared.h>
#include <lpayload.h>
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 = getenv(ENV_EXE_PATH);
// Get the path of the DLL to inject
char *injectDll = getenv(ENV_DLL_PATH);
// Get game commandline
char *cmdline = getenv(ENV_PROC_CMD);
// 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(
NULL,
cmdline,
NULL,
NULL,
FALSE,
CREATE_SUSPENDED,
NULL,
workdir,
&si,
&pi
)) {
char message[64];
sprintf(message, "Failed to start game process: %ld", GetLastError());
MessageBoxA(NULL, message, "Jadeite Launcher Payload", MB_OK | MB_ICONERROR);
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);
// Optional: wait for user input before resuming (useful for debugging)
char *waitEnabled = getenv("WAIT_BEFORE_RESUME");
if (waitEnabled && strcmp(waitEnabled, "") != 0) {
char message[64];
sprintf(message, "PID: %ld. Press OK to continue", pi.dwProcessId);
MessageBoxA(NULL, message, "Jadeite Launcher Payload", MB_OK | MB_ICONINFORMATION);
}
// Resume the process
ResumeThread(pi.hThread);
// The launcher process should now hang untill the game terminates
WaitForSingleObject(pi.hProcess, INFINITE);
return TRUE;
}

View File

@ -1,137 +0,0 @@
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
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 [rbp - 18h], rax
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] ; *GetModuleHandleA
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

View File

@ -1,21 +1,50 @@
# Assemble the payload that will be injected into the launcher include_dir = include_directories('include')
inj_payload_bin = asm_gen.process('src/payload.asm') str_include_dir = join_paths(meson.current_source_dir(), 'include')
# Embed it into the library # Assemble the payloads
inj_res_files = custom_target( launcher_payload_bin = asm_gen.process(
'ipayload.[oh]', 'src/launcher_p.asm',
output: [ 'ipayload.o', 'ipayload.h' ], extra_args: [ '-i', str_include_dir ]
input: [ inj_payload_bin ], )
command: [ gen_res, './injector', '@OUTPUT0@', '@OUTPUT1@', '@INPUT@' ]
game_payload_bin = asm_gen.process(
'src/game_p.asm',
extra_args: [ '-i', str_include_dir ]
)
# Embed them into .o files
exe_res_files = custom_target(
'launcher_p.[oh]',
output: [ 'launcher_p.o', 'launcher_p.h' ],
input: [ launcher_payload_bin ],
command: [ gen_res, '--header', '--object', './injector', '@OUTPUT0@', '@OUTPUT1@', '@INPUT@' ]
)
dll_res_files = custom_target(
'game_p.[oh]',
output: [ 'game_p.o', 'game_p.h' ],
input: [ game_payload_bin ],
command: [ gen_res, '--header', '--object', './injector', '@OUTPUT0@', '@OUTPUT1@', '@INPUT@' ]
) )
# Main injector exe # Main injector exe
executable( executable(
'jadeite', 'jadeite',
'src/injector.c', 'src/exe.c',
inj_res_files, 'src/inject.c',
include_directories: 'include', exe_res_files,
name_prefix: '' include_directories: include_dir,
name_prefix: '',
link_args: '-municode'
) )
subdir('launcher_payload') # Dll that will be injected into the launcher
shared_library(
'launcher_payload',
'src/dll.c',
'src/inject.c',
dll_res_files,
include_directories: include_dir,
name_prefix: '',
link_args: '-municode'
)

111
injector/src/dll.c Normal file
View File

@ -0,0 +1,111 @@
#include <stdio.h>
#include <inject.h>
#include <envs.h>
#include <game_p.h>
typedef char *(*wgufn_t)(wchar_t* path); // wine_get_unix_file_name
const wchar_t *J_MB_TITLE = L"Jadeite Launcher Payload";
BOOL WINAPI DllMain(HINSTANCE inst, DWORD reason, LPVOID reserved) {
// Only listen for attach
if (reason != DLL_PROCESS_ATTACH) {
return TRUE;
}
// Get target EXE path
wchar_t targetExe[MAX_PATH];
GetEnvironmentVariableW(ENV_EXE_PATH, targetExe, MAX_PATH);
// Get the path of the DLL to inject
wchar_t injectDll[MAX_PATH];
GetEnvironmentVariableW(ENV_DLL_PATH, injectDll, MAX_PATH);
// Get game commandline
wchar_t cmdline[8192];
GetEnvironmentVariableW(ENV_PROC_CMD, cmdline, sizeof(cmdline) / sizeof(wchar_t));
// Compute the working directory path
wchar_t workdir[MAX_PATH];
wcscpy(workdir, targetExe);
*(wcsrchr(workdir, L'\\')) = L'\0';
// SAFETY: verify that the injector is not inside the game directory
HMODULE kernel32 = GetModuleHandleA("kernel32.dll");
wgufn_t wine_get_unix_file_name = (wgufn_t)GetProcAddress(kernel32, "wine_get_unix_file_name");
if (wine_get_unix_file_name) {
char *unixInjectDll = wine_get_unix_file_name(injectDll);
char *unixWorkdir = wine_get_unix_file_name(workdir);
char *i = unixInjectDll, *w = unixWorkdir;
char startsWith = 0;
while (*i && *w) {
startsWith = *i == *w;
if (!startsWith) break;
i++, w++;
}
HANDLE heap = GetProcessHeap();
HeapFree(heap, 0, unixInjectDll);
HeapFree(heap, 0, unixWorkdir);
if (startsWith) {
MessageBoxW(NULL, L"Putting the patcher (or any other foreign PE binaries) inside the game directory is dangerous! Please move it elsewhere.", J_MB_TITLE, MB_OK | MB_ICONERROR);
exit(1);
}
} else {
MessageBoxW(NULL, L"Could not find wine_get_unix_file_name! Wine version too old?", J_MB_TITLE, MB_OK | MB_ICONWARNING);
}
// Start the game
STARTUPINFOW si;
ZeroMemory(&si, sizeof(si));
PROCESS_INFORMATION pi;
si.cb = sizeof(si);
ZeroMemory(&pi, sizeof(pi));
if (!CreateProcessW(
NULL,
cmdline,
NULL,
NULL,
FALSE,
CREATE_SUSPENDED,
NULL,
workdir,
&si,
&pi
)) {
wchar_t message[1024];
wsprintfW(message, L"Failed to start game process: %ld", GetLastError());
MessageBoxW(NULL, message, J_MB_TITLE, MB_OK | MB_ICONERROR);
exit(1);
}
// Inject
void *payloadStart = &_binary_game_p_o_p_game_p_bin_start;
size_t payloadSize = (size_t)&_binary_game_p_o_p_game_p_bin_size;
inject(pi.hProcess, payloadStart, payloadSize, injectDll);
// Optional: wait for user input before resuming (useful for debugging)
char *waitEnabled = getenv("WAIT_BEFORE_RESUME");
if (waitEnabled && *waitEnabled) {
wchar_t message[64];
wsprintfW(message, L"PID: %ld. Press OK to continue", pi.dwProcessId);
MessageBoxW(NULL, message, J_MB_TITLE, MB_OK | MB_ICONINFORMATION);
}
// Resume the process
ResumeThread(pi.hThread);
// The launcher process should now hang untill the game terminates
WaitForSingleObject(pi.hProcess, INFINITE);
return TRUE;
}

120
injector/src/exe.c Normal file
View File

@ -0,0 +1,120 @@
#include <stdio.h>
#include <inject.h>
#include <envs.h>
#include <launcher_p.h>
const wchar_t *LAUNCHER_INJECT_DLL = L"launcher_payload.dll";
const wchar_t *GAME_INJECT_DLL = L"game_payload.dll";
#define SHIFT(argc, argv) argc--, argv++
int wmain(int argc, wchar_t **argv) {
// Read arguments
wchar_t *gamePath = NULL;
wchar_t *launcherPath = NULL;
// Skip executable
SHIFT(argc, argv);
switch (argc) {
case 0:
wprintf(L"Usage: wine jadeite.exe [game path] <launcher path>\n");
return 0;
case 1:
gamePath = argv[0];
SHIFT(argc, argv);
launcherPath = L"--";
break;
default:
gamePath = argv[0];
SHIFT(argc, argv);
launcherPath = argv[0];
SHIFT(argc, argv);
break;
}
// Default launcher path
if (wcscmp(launcherPath, L"--") == 0) {
wprintf(L"No launcher process specified! Using explorer.exe\n");
launcherPath = L"C:\\Windows\\explorer.exe";
}
// cd into the injector directory
wchar_t injectorPath[MAX_PATH];
GetModuleFileNameW(GetModuleHandleW(NULL), injectorPath, MAX_PATH);
*(wcsrchr(injectorPath, L'\\')) = L'\0';
SetCurrentDirectoryW(injectorPath);
// Compute absolute paths
wchar_t gameExePath[MAX_PATH];
GetFullPathNameW(gamePath, MAX_PATH, gameExePath, NULL);
wchar_t gamePayloadPath[MAX_PATH];
GetFullPathNameW(GAME_INJECT_DLL, MAX_PATH, gamePayloadPath, NULL);
wchar_t launcherPayloadPath[MAX_PATH];
GetFullPathNameW(LAUNCHER_INJECT_DLL, MAX_PATH, launcherPayloadPath, NULL);
// Construct commandline for the game process
wchar_t cmdline[8192];
wsprintfW(cmdline, L"\"%ls\"", gameExePath);
while (argc) {
wchar_t arg[8192];
wsprintfW(arg, L" \"%ls\"", argv[0]);
wcscat(cmdline, arg);
SHIFT(argc, argv);
}
// Set envvars
SetEnvironmentVariableW(ENV_EXE_PATH, gameExePath);
SetEnvironmentVariableW(ENV_DLL_PATH, gamePayloadPath);
SetEnvironmentVariableW(ENV_PROC_CMD, cmdline);
// Start the launcher
wprintf(L"Starting '%ls' via '%ls'\n", gameExePath, launcherPath);
STARTUPINFOW si;
ZeroMemory(&si, sizeof(si));
PROCESS_INFORMATION pi;
si.cb = sizeof(si);
ZeroMemory(&pi, sizeof(pi));
if (!CreateProcessW(
launcherPath,
NULL,
NULL,
NULL,
FALSE,
CREATE_SUSPENDED,
NULL,
NULL,
&si,
&pi
)) {
fwprintf(stderr, L"Could not start process! (%ld)\n", GetLastError());
exit(1);
}
wprintf(L"Started launcher process (%ld)\n", pi.dwProcessId);
// Inject
void *payloadStart = &_binary_launcher_p_o_p_launcher_p_bin_start;
size_t payloadSize = (size_t)&_binary_launcher_p_o_p_launcher_p_bin_size; // yes this is valid
inject(pi.hProcess, payloadStart, payloadSize, launcherPayloadPath);
// Resume the process
ResumeThread(pi.hThread);
return 0;
}

186
injector/src/game_p.asm Normal file
View File

@ -0,0 +1,186 @@
BITS 64
; Macro definitions
; read dst, pSrc, size
%macro read 3
mov %1, [%2]
add %2, %3
%endmacro
; copy pDst, pSrc, temp, tempSize
%macro copy 4
mov %3, [%2]
mov [%1], %3
add %1, %4
add %2, %4
%endmacro
; unprotect addr, size, fn
%macro unprotect 3
mov rcx, %1
mov rdx, %2
mov r8, 40h ; PAGE_EXECUTE_READWRITE
lea r9, [rel oldProtect]
call %3
%endmacro
; reprotect addr, size, fn
%macro reprotect 3
mov rcx, %1
mov rdx, %2
lea r9, [rel oldProtect]
mov r8d, [r9]
call %3
%endmacro
main: ; Replacement entry point
push rsi
push rdi
push r12
push r13
push r14
call GetKernel32ModuleHandle
mov rsi, rax ; kernel32.dll
mov rcx, rax
call GetAddressOf_GetProcAddress
mov rdi, rax ; *GetProcAddress
mov rcx, rsi ; kernel32.dll
lea rdx, [rel s_VirtualProtect]
call rdi ; rax = *VirtualProtect
mov rcx, rax
call RecoverExecutable
mov rcx, rsi ; kernel32.dll
lea rdx, [rel s_LoadLibraryW]
call rdi ; rax = *LoadLibraryW
lea rcx, [rel dllPath]
call rax ; LoadLibraryW(dllPath)
mov rcx, rsi ; kernel32.dll
lea rdx, [rel s_GetModuleHandleA]
call rdi ; rax = *GetModuleHandle
mov r12, rax
mov rcx, 0
call rax ; rax = .exe base address
mov r13, rax
mov rcx, rsi ; kernel32.dll
lea rdx, [rel s_GetCommandLineW]
call rdi ; rax = *GetCommandLineW
call rax ; rax = command line
mov r14, rax
lea rcx, [rel s_UnityPlayer.dll]
call r12 ; rax = UnityPlayer.dll
mov rcx, rax
lea rdx, [rel s_UnityMain]
call rdi ; rax = *UnityMain
mov rcx, r13 ; .exe base address
mov rdx, 0 ; hPrevInstance - 0
mov r8, r14 ; command line
mov r9, 1 ; SW_NORMAL
call rax ; UnityMain(...)
pop r14
pop r13
pop r12
pop rdi
pop rsi
ret
RecoverExecutable: ; expects *VirtualProtect in rcx
push rbx
push r12
push r13
push r14
sub rsp, 8
mov r13, rcx
; Find the recovery data structure
lea rbx, [rel dllPath]
.search:
read ax, rbx, 2
test ax, ax
jnz .search
; Recover entry point bytes (6 + 8 = 14 total)
read r12, rbx, 8 ; Address
mov r14, r12
unprotect r14, 14, r13
copy r12, rbx, rax, 8
copy r12, rbx, eax, 4
copy r12, rbx, ax, 2
reprotect r14, 14, r13
; Recover import descriptor bytes (20 total)
read r12, rbx, 8
mov r14, r12
unprotect r14, 20, r13
copy r12, rbx, rax, 8
copy r12, rbx, rax, 8
copy r12, rbx, eax, 4
reprotect r14, 20, r13
; Recover import data directory entry size bytes (4 total)
read r12, rbx, 8
mov r14, r12
unprotect r14, 4, r13
copy r12, rbx, eax, 4
reprotect r14, 4, r13
add rsp, 8
pop r14
pop r13
pop r12
pop rbx
ret
%include "gpa.asm"
oldProtect: dd 0
; Strings
s_VirtualProtect: db "VirtualProtect", 0
s_LoadLibraryW: db "LoadLibraryW", 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

View File

@ -1,10 +1,21 @@
#include <windows.h> #include <inject.h>
#define EPFX "__JADEITE_" #define JUMP_SIZE (6 + sizeof(void*))
const char ENV_EXE_PATH[] = EPFX"TARGET_EXE_PATH"; // Original values to recover after the injection
const char ENV_DLL_PATH[] = EPFX"INJECT_DLL_PATH"; // Recovery is performed by the assembly payload
const char ENV_PROC_CMD[] = EPFX"PROCESS_COMMAND"; #pragma pack(push, 1)
struct recovery_data {
void *entryPointAddress;
char entryPointData[JUMP_SIZE];
void *importDescriptorAddress;
IMAGE_IMPORT_DESCRIPTOR importDescriptorData;
void *sizeFieldAddress;
DWORD sizeFieldData;
};
#pragma pack(pop)
static inline void write_protected_process_memory(HANDLE process, void *address, const void *buf, size_t size) { static inline void write_protected_process_memory(HANDLE process, void *address, const void *buf, size_t size) {
DWORD oldProtect; DWORD oldProtect;
@ -16,15 +27,8 @@ static inline void write_protected_process_memory(HANDLE process, void *address,
VirtualProtectEx(process, address, size, oldProtect, &oldProtect); VirtualProtectEx(process, address, size, oldProtect, &oldProtect);
} }
static inline void inject(HANDLE process, const void *payload, size_t payloadSize, const char *dllPath) { void inject(HANDLE process, const void *payload, size_t payloadSize, const wchar_t *dllPath) {
size_t _; size_t _; // Contrary to the docs, {Write,Read}ProcessMemory likes to crash if the last arg is NULL
// 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 // Find the EXE header in the process
char exeHeader[1024]; char exeHeader[1024];
@ -51,7 +55,7 @@ static inline void inject(HANDLE process, const void *payload, size_t payloadSiz
} }
// Skip DLLs // Skip DLLs
if ((ntHeaders->FileHeader.Characteristics | IMAGE_FILE_DLL) == IMAGE_FILE_DLL) { if ((ntHeaders->FileHeader.Characteristics & IMAGE_FILE_DLL) == IMAGE_FILE_DLL) {
goto cont; goto cont;
} }
@ -70,24 +74,60 @@ static inline void inject(HANDLE process, const void *payload, size_t payloadSiz
char *exe = (char*)memoryInfo.BaseAddress; char *exe = (char*)memoryInfo.BaseAddress;
// Inject the loader into the process
const unsigned char JUMP_INST[] = { 0xFF, 0x25, 0x00, 0x00, 0x00, 0x00 };
size_t dllPathSize = (wcslen(dllPath) + 1) * sizeof(wchar_t);
size_t allocSize = payloadSize + dllPathSize + sizeof(struct recovery_data);
char *remoteAlloc = VirtualAllocEx(process, NULL, allocSize, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
// Write the assembly payload and dll path
WriteProcessMemory(process, remoteAlloc, payload, payloadSize, &_);
WriteProcessMemory(process, remoteAlloc + payloadSize, dllPath, dllPathSize, &_);
// Modify the executable to run the assembly payload
// Recovery data structure
struct recovery_data rd;
// Replace the entry point with a jump to the loader // Replace the entry point with a jump to the loader
char *entryPoint = exe + ntHeaders->OptionalHeader.AddressOfEntryPoint; char *entryPoint = exe + ntHeaders->OptionalHeader.AddressOfEntryPoint;
const unsigned char JUMP_INST[] = { 0xFF, 0x25, 0x00, 0x00, 0x00, 0x00 }; // Save the original entry point address and bytes
rd.entryPointAddress = entryPoint;
ReadProcessMemory(process, rd.entryPointAddress, rd.entryPointData, sizeof(rd.entryPointData), &_);
// Replace the entry point with a jump to the assembly payload
write_protected_process_memory(process, entryPoint, JUMP_INST, sizeof(JUMP_INST)); write_protected_process_memory(process, entryPoint, JUMP_INST, sizeof(JUMP_INST));
write_protected_process_memory(process, entryPoint + sizeof(JUMP_INST), &remoteAlloc, sizeof(remoteAlloc)); write_protected_process_memory(process, entryPoint + sizeof(JUMP_INST), &remoteAlloc, sizeof(remoteAlloc));
// Break the import table to prevent any dlls from being loaded // Break the import table to prevent any dlls from being loaded
// Step 1: break the first import descriptor // Step 1: break the first import descriptor
char *importDescriptors = exe + ntHeaders->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress; char *importDescriptors = exe + ntHeaders->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress;
// Save the original descriptor address and bytes
rd.importDescriptorAddress = importDescriptors;
ReadProcessMemory(process, rd.importDescriptorAddress, &rd.importDescriptorData, sizeof(rd.importDescriptorData), &_);
// Overwrite with zeroes
IMAGE_IMPORT_DESCRIPTOR firstDescriptor; IMAGE_IMPORT_DESCRIPTOR firstDescriptor;
ZeroMemory(&firstDescriptor, sizeof(firstDescriptor)); ZeroMemory(&firstDescriptor, sizeof(firstDescriptor));
write_protected_process_memory(process, importDescriptors, &firstDescriptor, sizeof(firstDescriptor)); write_protected_process_memory(process, importDescriptors, &firstDescriptor, sizeof(firstDescriptor));
// Step 2: break the image data directory entry // Step 2: break the image data directory entry
ntHeaders->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].Size = 0; char* ddAddr = ((char*)&(ntHeaders->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].Size)) - exeHeader + exe;
// Save the original value
rd.sizeFieldAddress = ddAddr;
ReadProcessMemory(process, rd.sizeFieldAddress, &rd.sizeFieldData, sizeof(rd.sizeFieldData), &_);
write_protected_process_memory(process, exe, exeHeader, sizeof(exeHeader)); // Set to 0
DWORD newSize = 0;
write_protected_process_memory(process, ddAddr, &newSize, sizeof(newSize));
// Write recovery data to the allocation
WriteProcessMemory(process, remoteAlloc + payloadSize + dllPathSize, &rd, sizeof(rd), &_);
} }

View File

@ -1,119 +0,0 @@
#include <stdio.h>
#include <injshared.h>
#include <ipayload.h>
const char LAUNCHER_INJECT_DLL[] = "launcher_payload.dll";
const char GAME_INJECT_DLL[] = "game_payload.dll";
#define SHIFT(argc, argv) argc--, argv++
int main(int argc, char **argv) {
// Read arguments
char *gamePath = NULL;
char *launcherPath = NULL;
// Skip executable
SHIFT(argc, argv);
switch (argc) {
case 0:
printf("Usage: wine jadeite.exe [game path] <launcher path>\n");
return 0;
case 1:
gamePath = argv[0];
SHIFT(argc, argv);
launcherPath = "--";
break;
default:
gamePath = argv[0];
SHIFT(argc, argv);
launcherPath = argv[0];
SHIFT(argc, argv);
break;
}
// Default launcher path
if (strcmp(launcherPath, "--") == 0) {
printf("No launcher process specified! Using explorer.exe\n");
launcherPath = "C:\\Windows\\explorer.exe";
}
// cd into the injector directory
char injectorPath[MAX_PATH];
GetModuleFileNameA(GetModuleHandleA(NULL), injectorPath, sizeof(injectorPath));
*(strrchr(injectorPath, '\\')) = '\0';
SetCurrentDirectoryA(injectorPath);
// 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);
// Construct commandline for the game process
char cmdline[8192];
sprintf(cmdline, "\"%s\"", gameExePath);
while (argc) {
char arg[8192];
sprintf(arg, " \"%s\"", argv[0]);
strcat(cmdline, arg);
SHIFT(argc, argv);
}
// Set envvars
SetEnvironmentVariableA(ENV_EXE_PATH, gameExePath);
SetEnvironmentVariableA(ENV_DLL_PATH, gamePayloadPath);
SetEnvironmentVariableA(ENV_PROC_CMD, cmdline);
// Start the launcher
printf("Starting '%s' via '%s'\n", gameExePath, launcherPath);
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;
}

View File

@ -0,0 +1,34 @@
BITS 64
main: ; Replacement entry point
push rsi
call GetKernel32ModuleHandle
mov rsi, rax ; kernel32.dll
mov rcx, rax
call GetAddressOf_GetProcAddress
mov rcx, rsi ; kernel32.dll
lea rdx, [rel s_LoadLibraryW]
call rax ; rax = *LoadLibraryW
lea rcx, [rel dllPath]
call rax ; LoadLibraryA(dllPath)
pop rsi
ret
%include "gpa.asm"
; Strings
s_LoadLibraryW: db "LoadLibraryW", 0
dllPath:
; This will be filled out by the injector
; Path to the dll to inject into the launcher

View File

@ -1,4 +1,4 @@
project('jadeite', 'c', version: '1.1.2') project('jadeite', 'c', version: '2.0.1')
nasm = find_program('nasm') nasm = find_program('nasm')
gen_res = find_program('gen_resources.sh') gen_res = find_program('gen_resources.sh')
@ -8,6 +8,7 @@ asm_gen = generator(
nasm, nasm,
output: '@BASENAME@.bin', output: '@BASENAME@.bin',
arguments: [ arguments: [
'@EXTRA_ARGS@',
'-f', 'bin', '-f', 'bin',
'@INPUT@', '@INPUT@',
'-o', '@OUTPUT@' '-o', '@OUTPUT@'

View File

@ -1,22 +1,42 @@
{ {
"jadeite": { "jadeite": {
"version": "1.1.2" "version": "2.0.1"
}, },
"games": { "games": {
"hi3rd": { "hi3rd": {
"global": { "global": {
"status": "verified", "status": "verified",
"version": "6.6.0" "version": "6.8.0"
},
"sea": {
"status": "verified",
"version": "6.8.0"
},
"china": {
"status": "verified",
"version": "6.8.0"
},
"taiwan": {
"status": "verified",
"version": "6.8.0"
},
"korea": {
"status": "verified",
"version": "6.8.0"
},
"japan": {
"status": "verified",
"version": "6.8.0"
} }
}, },
"hsr": { "hsr": {
"global": { "global": {
"status": "unsafe", "status": "verified",
"version": "1.1.0" "version": "1.2.0"
}, },
"china": { "china": {
"status": "unsafe", "status": "verified",
"version": "1.1.0" "version": "1.2.0"
} }
} }
} }