From d9454c1ef1b140c21e096de80a98ce0b3e811681 Mon Sep 17 00:00:00 2001 From: Dylan Muller Date: Sat, 16 Sep 2023 12:34:28 +0200 Subject: gsctool: core: initial commit Initial commit of gsctool. A utility to inject custom GSC scripts for Call of Duty Black Ops 1 (PC). --- .gitmodules | 6 + Makefile | 13 ++ README.md | 31 +++ gsctool.c | 516 ++++++++++++++++++++++++++++++++++++++++++ gsctool.h | 96 ++++++++ gsctool/config.ini | 3 + gsctool/spawn_raygun/main.gsc | 31 +++ images/demo.png | Bin 0 -> 51826 bytes lib/cdl86 | 1 + lib/miniz | 1 + offsets.h | 17 ++ 11 files changed, 715 insertions(+) create mode 100644 .gitmodules create mode 100644 Makefile create mode 100644 README.md create mode 100644 gsctool.c create mode 100644 gsctool.h create mode 100644 gsctool/config.ini create mode 100644 gsctool/spawn_raygun/main.gsc create mode 100644 images/demo.png create mode 160000 lib/cdl86 create mode 160000 lib/miniz create mode 100644 offsets.h diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..182a0cb --- /dev/null +++ b/.gitmodules @@ -0,0 +1,6 @@ +[submodule "lib/cdl86"] + path = lib/cdl86 + url = https://github.com/lunarjournal/cdl86.git +[submodule "lib/miniz"] + path = lib/miniz + url = https://github.com/lunarjournal/miniz.git diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..598cda1 --- /dev/null +++ b/Makefile @@ -0,0 +1,13 @@ +CC=tcc.exe +CFLAGS=-m32 -shared -lmsvcrt +CFILES_GSC=gsctool.c lib/cdl86/cdl.c lib/miniz/miniz.c + +all: gsctool + +gsctool: gsctool.c + $(CC) $(CFLAGS) $(CFILES_GSC) -o gsctool.dll + +.PHONY: clean +clean: + rm -f gsctool.dll + diff --git a/README.md b/README.md new file mode 100644 index 0000000..9f21000 --- /dev/null +++ b/README.md @@ -0,0 +1,31 @@ +# gsctool + +Simple GSC loader and dumper for `Call Of Duty: Black Ops 1 (T5) (Microsoft Windows)`. + +To load the demo GSC plugin copy `gsctool` to the games root directory. + +This demo will give you a raygun on spawn in zombie mode (solo). + +* GSC dumps are written to `gsctool/cache` + +This project is intended to be a starting point for more advanced mods. + +# Instructions + +Run: `git submodule update --init --recursive` to clone submodules. + +Then build project using the provided makefile in Windows Subsystem for Linux +and inject resulting DLL. + +The compiler used for this project is `tcc`. + +Note: You can modify and reload GSC scripts while Black Ops is running by quiting +the level and starting it again. + +![Demo](https://raw.githubusercontent.com/lunarjournal/gsctool/main/images/demo.png) + +# Libraries + +* [cdl86](https://github.com/lunarjournal/cdl86) detour library +* [miniz](https://github.com/lunarjournal/miniz) zlib library + diff --git a/gsctool.c b/gsctool.c new file mode 100644 index 0000000..9c8c39c --- /dev/null +++ b/gsctool.c @@ -0,0 +1,516 @@ +/** + * Written by: Dylan Muller + * Copyright (c) 2023 + */ + +/* Global includes */ +#include +#include + +/* Local includes */ +#include "offsets.h" +#include "gsctool.h" +#include "lib/miniz/miniz.h" +#include "lib/cdl86/cdl.h" + +/* Local definitions */ +#define SIZE_BUF 1024 * 1024 +#define SIZE_PATH 256 + +/* Assign function pointers */ +/* Load GSC script */ +Scr_LoadScript_t Scr_LoadScript = (Scr_LoadScript_t)T5_Scr_LoadScript; +/* Get VM function handle */ +Scr_GetFunctionHandle_t Scr_GetFunctionHandle = (Scr_GetFunctionHandle_t)T5_Scr_GetFunctionHandle; +/* Create new execution thread */ +Scr_ExecThread_t Scr_ExecThread = (Scr_ExecThread_t)T5_Scr_ExecThread; +/* Free execution thread */ +Scr_FreeThread_t Scr_FreeThread = (Scr_FreeThread_t)T5_Scr_FreeThread; +/* Execute on level startup */ +Scr_LoadGameType_t Scr_LoadGameType = (Scr_LoadGameType_t)T5_Scr_LoadGameType; +/* Dvar lookup */ +Dvar_FindVar_t Dvar_FindVar = (Dvar_FindVar_t)T5_Dvar_FindVar; +/* Link asset/rawfile. Should be called before Scr_LoadScript */ +DB_LinkXAssetEntry_t DB_LinkXAssetEntry = (DB_LinkXAssetEntry_t)T5_DB_LinkXAssetEntry; +/* Hotfix to prevent crash */ +Assign_Hotfix_t Assign_Hotfix = (Assign_Hotfix_t)T5_Assign_Hotfix; +/* Remove anti-debug timer */ +Thread_Timer_t Thread_Timer = (Thread_Timer_t)T5_Thread_Timer; + +/* Global variables */ +int* init_trigger = (int*)T5_init_trigger; +uint8_t mod_dir[SIZE_PATH]; +uint8_t mod_dir_relative[SIZE_PATH]; +uint8_t entry_file[SIZE_PATH]; +int func_handle = 0; + +/* Strip extension from name */ +void strip_ext(uint8_t* fname) +{ + uint8_t* end = fname + strlen(fname); + + while (end > fname && *end != '.') + { + --end; + } + + if (end > fname) + { + *end = '\0'; + } +} + +/* Print banner */ +void print_banner() +{ + printf("[info] gsctool init\n"); + printf("[info] gsc dumper and loader\n"); +} + +/* Print mod info (config.ini) */ +void print_mod_info(uint8_t* dir, uint8_t* entry) +{ + printf("mod.dir=%s\n", dir); + printf("mod.entry=%s\n", entry); +} + +/* Print example config.ini */ +void print_mod_example() +{ + printf("example config.ini:\n"); + printf("mod.dir=example\n"); + printf("mod.entry=main.gsc\n"); +} + +/* Check if file exists */ +BOOL FileExists(LPCSTR szPath) +{ + DWORD attrib = GetFileAttributesA(szPath); + return (attrib != INVALID_FILE_ATTRIBUTES + && !(attrib & FILE_ATTRIBUTE_DIRECTORY)); +} + +/* Check if directory */ +BOOL DirectoryExists(LPCSTR szPath) +{ + DWORD attrib = GetFileAttributesA(szPath); + return (attrib != INVALID_FILE_ATTRIBUTES + && (attrib & FILE_ATTRIBUTE_DIRECTORY)); +} + +/* Hotfix to prevent crash when exiting gamemode. */ +int* __cdecl Assign_Hotfix_hk( + int* a1, int* a2 +) +{ + if (a1 != NULL && a2 != NULL) + { + return Assign_Hotfix(a1, a2); + } + else + { + return 0; + } +} + +/* AC bypass to allow detouring. */ +void __cdecl Thread_Timer_hk( + uint8_t a1, int a2, + int a3, int a4 +) +{ + return; +} + +/* Load our custom GSC script */ +int32_t __cdecl Scr_LoadScript_hk( + int32_t scriptInstance, + const uint8_t* scriptName +) +{ + uint8_t script_file[SIZE_PATH]; + uint8_t mapname_buffer[SIZE_PATH]; + uint8_t* mapname = NULL; + FILE* gsc_script = NULL;; + uint8_t* source_buffer = 0x0; + uint8_t* data_buffer = 0x0; + uint8_t* compress_buffer = 0x0; + uint32_t gsc_size = 0x0; + int cmp_status = 0x0; + RawFileData* file_data = 0x0; + mz_ulong gsc_compress_size = 0x0; + mz_ulong data_size = 0; + XAsset entry = { 0 }; + int ret = 0x0; + int result = 0x0; + + result = Scr_LoadScript(scriptInstance, scriptName); + + /* Get current map name */ + mapname = Dvar_FindVar("mapname"); + sprintf(mapname_buffer, "maps/%s", mapname); + + /* Are we loading script for current map? */ + if (strcmp(mapname_buffer, scriptName) == 0 + && strcmp(mapname_buffer, "maps/frontend") != 0) + { + /* Load GSC script to inject */ + FILE* gsc_script = fopen(entry_file, "rb"); + if (gsc_script) + { + /* Get GSC script size in bytes */ + fseek(gsc_script, 0, SEEK_END); + gsc_size = ftell(gsc_script); + fseek(gsc_script, 0, SEEK_SET); + + /* Allocate GSC script text buffer */ + source_buffer = (uint8_t*)malloc((sizeof(uint8_t) * gsc_size) + 1); + + /* Return if we cannot allocate memory */ + if (!source_buffer) + { + printf("[info] failed allocating memory\n"); + fclose(gsc_script); + return result; + } + /* Read GSC script into buffer */ + fread(source_buffer, sizeof(uint8_t), gsc_size, gsc_script); + + /* Don't forget null terminator! */ + source_buffer[gsc_size] = 0; + + /* Estimate compression bounds for allocator */ + gsc_compress_size = mz_compressBound(gsc_size + 1); + /* Allocate data buffer for compressed data */ + data_buffer = (uint8_t*)malloc((sizeof(uint8_t) * gsc_compress_size) + + sizeof(RawFileData)); + + /* Return if we cannot allocate memory */ + if (!data_buffer) + { + printf("[info] failed allocating memory\n"); + fclose(gsc_script); + free(source_buffer); + return result; + } + compress_buffer = (uint8_t*)(data_buffer + sizeof(RawFileData)); + /* Apply zlib compression */ + cmp_status = mz_compress((uint8_t*)compress_buffer, &gsc_compress_size, + (uint8_t*)source_buffer, gsc_size + 1); + /* Now free source buffer */ + free(source_buffer); + + if (cmp_status == MZ_OK){ + /* If compression succeeded set compression and deflate lengths */ + if (data_buffer) { + file_data = (RawFileData*)data_buffer; + file_data->compressedSize = gsc_compress_size; + file_data->deflatedSize = gsc_size + 1; + } + + /* Calculate total payload size */ + data_size = gsc_compress_size + sizeof(RawFileData); + /* Set entry type */ + entry.type = ASSET_TYPE_RAWFILE; + entry.header.rawFile = (RawFile*)malloc(sizeof(RawFile)); + + /* Return if we cannot allocate memory */ + if (!entry.header.rawFile) + { + printf("[info] failed allocating memory\n"); + fclose(gsc_script); + free(data_buffer); + return result; + } + entry.header.rawFile->buffer = data_buffer; + entry.header.rawFile->len = data_size; + entry.header.rawFile->name = (uint8_t*)entry_file; + + printf("[info] linking asset %s\n", entry_file); + /* Link script asset before compilation */ + DB_LinkXAssetEntry(&entry, 0); + + sprintf(script_file, "%s", entry_file); + strip_ext(script_file); + /* Compile GSC script */ + ret = Scr_LoadScript(0, script_file); + + /* Free asset */ + free(data_buffer); + free(entry.header.rawFile); + + printf("[info] retreiving function handle\n"); + /* Get main VM entry point */ + func_handle = Scr_GetFunctionHandle(0, script_file, "main"); + if (func_handle) + { + printf("[info] main @ %p\n", (uint8_t*)func_handle); + } + else + { + printf("[error] failed to retreive main handle\n"); + } + fclose(gsc_script); + } + } + } + + return result; +} + +/* Execute function handle at correct timing */ +void __cdecl Scr_LoadGameType_hk() +{ + Scr_LoadGameType(); + uint8_t* mapname = Dvar_FindVar("mapname"); + + if (strcmp(mapname, "frontend") == 0) + { + printf("[info] loaded main menu\n"); + } + else if (func_handle > 0) + { + printf("[info] executing main @ %p\n", (uint8_t*)func_handle); + int16_t handle = Scr_ExecThread(0, func_handle, 0); + Scr_FreeThread(handle, 0); + } + return; +} + +/* Recursively make directory */ +void __cdecl mkdir_recursive(uint8_t* path) +{ + int i = 0x0; + uint8_t* buffer = NULL; + + if (path != NULL) { + buffer = (uint8_t*)malloc(SIZE_PATH); + if (!buffer) + { + printf("[info] failed allocating memory\n"); + return; + } + int len_path = strlen(path); + for (i = 0; i < len_path; i++) + { + if (path[i] == '/') + { + strncpy(buffer, path, i); + buffer[i] = 0; + mkdir(buffer); + } + } + free(buffer); + } + + +} + +/* Dump GSC and CSC scripts as they are loaded */ +XAssetEntryPoolEntry* __cdecl DB_LinkXAssetEntry_hk( + XAsset* newEntry, + int32_t allowOverride +) +{ + struct RawFile* rawfile = newEntry->header.rawFile; + const uint8_t* data = 0x0; + uint8_t* buffer = (uint8_t*)malloc(SIZE_BUF); + uint8_t* path = (uint8_t*)malloc(SIZE_PATH); + uint8_t* script_name = 0x0; + z_stream stream; + int status = 0x0; + int inflate_len = 0x0; + FILE* inflated_script = 0x0; + + if (!buffer || !path) + { + printf("[info] failed allocating memory\n"); + return DB_LinkXAssetEntry(newEntry, allowOverride); + } + + if (newEntry->type == ASSET_TYPE_RAWFILE && rawfile!= NULL) + { + script_name = rawfile->name; + + + /* Check for GSC or CSC script */ + if (strstr(script_name, ".gsc") != NULL + || strstr(script_name, ".csc") != NULL) + { + memset(&stream, 0, sizeof(stream)); + /* Apply file data offset */ + data = (const uint8_t*)(rawfile->buffer + FILE_OFFSET); + + /* Setup decompression stream */ + stream.next_in = data; + stream.avail_in = rawfile->len - FILE_OFFSET; + stream.next_out = buffer; + stream.avail_out = SIZE_BUF; + + /* Initialize inflate with stream */ + inflateInit(&stream); + /* Decompress script... */ + status = inflate(&stream, Z_SYNC_FLUSH); + inflateEnd(&stream); + inflate_len = stream.total_out; + + /* On decompression success create dir and write file */ + if (status == Z_STREAM_END || status == Z_OK) + { + sprintf(path, "%s/%s", PATH_DUMP, script_name); + printf("[cache] %s\n", path); + mkdir_recursive(path); + inflated_script = fopen(path, "wb"); + if (inflated_script) + { + fwrite(buffer, sizeof(uint8_t), inflate_len, inflated_script); + fclose(inflated_script); + } + } + + } + } + free(buffer); + free(path); + + return DB_LinkXAssetEntry(newEntry, allowOverride); +} + +/* Main thread to execute once injected. */ +DWORD WINAPI init_thread(LPVOID lpParam) +{ + FILE* dependency_list = NULL; + FILE* config_file = NULL; + uint8_t* mod_dir_rel = (uint8_t*)malloc(SIZE_PATH); + uint8_t* entry_file_rel = (uint8_t*)malloc(SIZE_PATH); + + /* Wait until all threads have started. */ + while (! *init_trigger) { Sleep(10); } + + /* Allocate console and redirect std io. */ + AllocConsole(); + freopen("CONOUT$", "w", stdout); + freopen("CONIN$", "r", stdin); + + if (!mod_dir_rel || !entry_file_rel) + { + printf("[info] failed allocating memory\n"); + return EXIT_FAILURE; + } + /* Print startup banner */ + print_banner(); + + mkdir(PATH_ROOT); + + config_file = fopen(PATH_ROOT "/config.ini", "r"); + if (!config_file) + { + printf("[error] %s", "Error opening config.ini\n"); + return EXIT_SUCCESS; + } + + printf("[info] loaded config.ini\n"); + + if (fscanf(config_file, "mod.dir=%s\n", mod_dir_rel) != 1) + { + printf("[error] %s\n", "failed reading 'mod.dir' key in config file\n"); + print_mod_example(); + return EXIT_SUCCESS; + } + + if (fscanf(config_file, "mod.entry=%s\n", entry_file_rel) != 1) + { + printf("[error] %s\n", "failed reading 'mod.entry' from config file\n"); + print_mod_example(); + return EXIT_SUCCESS; + } + + fclose(config_file); + + printf("[info] mod info\n"); + print_mod_info(mod_dir_rel, entry_file_rel); + + sprintf(mod_dir, "%s/%s", PATH_ROOT, mod_dir_rel); + sprintf(mod_dir_relative, "%s", mod_dir_rel); + + printf("[info] searching for mod directory...\n"); + printf("%s\n", mod_dir); + if (!DirectoryExists(mod_dir)) + { + printf("[error] %s\n", "mod directory does not exist"); + return EXIT_SUCCESS; + } + + printf("[info] mod directory found\n"); + sprintf(entry_file, "%s/%s", mod_dir, entry_file_rel); + + free(entry_file_rel); + free(mod_dir_rel); + + printf("[info] searching for mod entry...\n"); + + if (!FileExists(entry_file)) + { + printf("[error] %s\n", "mod entry file does not exist, check config!"); + return EXIT_SUCCESS; + } + + printf("[info] mod entry found\n"); + printf("[info] applying detours...\n"); + + /* Subroutine patches. */ + struct cdl_jmp_patch thread_timer = + cdl_jmp_attach((void**)&Thread_Timer, Thread_Timer_hk); + struct cdl_jmp_patch assign_hotfix = + cdl_jmp_attach((void**)&Assign_Hotfix, Assign_Hotfix_hk); + struct cdl_jmp_patch link_asset = + cdl_jmp_attach((void**)&DB_LinkXAssetEntry, DB_LinkXAssetEntry_hk); + struct cdl_jmp_patch load_script = + cdl_jmp_attach((void**)&Scr_LoadScript, Scr_LoadScript_hk); + struct cdl_jmp_patch load_game_type = + cdl_jmp_attach((void**)&Scr_LoadGameType, Scr_LoadGameType_hk); + + /* Print debug info */ + printf("[info] Thread_Timer() detour info\n"); + cdl_jmp_dbg(&thread_timer); + printf("[info] Assign_Hotfix() detour info\n"); + cdl_jmp_dbg(&assign_hotfix); + printf("[info] DB_LinkXAssetEntry() detour info\n"); + cdl_jmp_dbg(&link_asset); + printf("[info] Scr_LoadScript() detour info\n"); + cdl_jmp_dbg(&load_script); + printf("[info] Scr_LoadGameType() detour info\n"); + cdl_jmp_dbg(&load_game_type); + + /* Flush std io */ + while (1) + { + flushall(); + Sleep(200); + } + + return EXIT_SUCCESS; + +} + +/* Main entry point */ +BOOL APIENTRY DllMain( + HMODULE hModule, + DWORD ul_reason_for_call, + LPVOID lpReserved +) +{ + switch (ul_reason_for_call) + { + case DLL_PROCESS_ATTACH: + CreateThread((LPSECURITY_ATTRIBUTES)0, 0, (LPTHREAD_START_ROUTINE)init_thread, + (LPVOID)hModule, 0, (LPDWORD)NULL); + break; + case DLL_THREAD_ATTACH: + case DLL_THREAD_DETACH: + case DLL_PROCESS_DETACH: + break; + } + return TRUE; +} + diff --git a/gsctool.h b/gsctool.h new file mode 100644 index 0000000..8737bc6 --- /dev/null +++ b/gsctool.h @@ -0,0 +1,96 @@ +#ifndef _MAIN_H +#define _MAIN_H + +#include +#include + +#define PATH_ROOT "gsctool" +#define PATH_DUMP PATH_ROOT "/cache" +#define FILE_OFFSET 0x8 + +typedef struct RawFileData +{ + int32_t deflatedSize; + int32_t compressedSize; +} RawFileData; + +typedef struct RawFile +{ + uint8_t* name; + int32_t len; + uint8_t* buffer; +} RawFile; + +typedef enum XAssetType +{ + ASSET_TYPE_RAWFILE = 0x24 +} XAssetType; + +typedef union XAssetHeader +{ + struct RawFile* rawFile; + void* data; +} XAssetHeader; + +typedef struct XAsset +{ + enum XAssetType type; + union XAssetHeader header; +} XAsset; + +typedef struct XAssetEntry +{ + XAsset asset; +} XAssetEntry; + +typedef union XAssetEntryPoolEntry +{ + struct XAssetEntry entry; + union XAssetEntryPoolEntry* next; +} XAssetEntryPoolEntry; + +typedef int32_t (__cdecl* Scr_LoadScript_t)( + int32_t scriptInstance, + const uint8_t* scriptName +); + +typedef int32_t (__cdecl* Scr_GetFunctionHandle_t)( + int32_t scriptInstance, + const uint8_t* scriptName, + const uint8_t* functioName +); + +typedef uint16_t (__cdecl* Scr_ExecThread_t)( + int32_t scriptInstance, + int32_t handle, + int32_t paramCount +); + +typedef uint16_t (__cdecl* Scr_FreeThread_t)( + uint16_t handle, + int scriptInstance +); + +typedef XAssetEntryPoolEntry* (__cdecl* DB_LinkXAssetEntry_t)( + XAsset* newEntry, + int32_t allowOverride +); + +typedef uint8_t* (__cdecl* Dvar_FindVar_t)( + uint8_t* variable +); + +typedef int* (__cdecl* Assign_Hotfix_t)( + int* a1, int* a2 +); + +typedef void (__cdecl* Thread_Timer_t)( + uint8_t a1, int a2, + int a3, int a4 +); + +typedef void (__cdecl* Scr_LoadGameType_t)( + void +); + +#endif diff --git a/gsctool/config.ini b/gsctool/config.ini new file mode 100644 index 0000000..b29db9c --- /dev/null +++ b/gsctool/config.ini @@ -0,0 +1,3 @@ +mod.dir=spawn_raygun +mod.entry=main.gsc + diff --git a/gsctool/spawn_raygun/main.gsc b/gsctool/spawn_raygun/main.gsc new file mode 100644 index 0000000..b1fef79 --- /dev/null +++ b/gsctool/spawn_raygun/main.gsc @@ -0,0 +1,31 @@ +#include maps\_utility; +#include common_scripts\utility; +#include maps\_hud_util; + +main() +{ + level thread onPlayerConnect(); +} + +onPlayerConnect() +{ + for(;;) + { + level waittill("connected", player); + player thread onPlayerSpawned(); + } +} + +onPlayerSpawned() +{ + self endon("disconnect"); + for(;;) + { + self waittill("spawned_player"); + self iprintln("Spawned raygun!"); + self GiveWeapon("ray_gun_zm"); + self SetWeaponAmmoStock("ray_gun_zm", 1000); + self SwitchToWeapon("ray_gun_zm"); + + } +} diff --git a/images/demo.png b/images/demo.png new file mode 100644 index 0000000..a70522b Binary files /dev/null and b/images/demo.png differ diff --git a/lib/cdl86 b/lib/cdl86 new file mode 160000 index 0000000..a3655fb --- /dev/null +++ b/lib/cdl86 @@ -0,0 +1 @@ +Subproject commit a3655fbccb17dbada53201bbcb22d641f7e5610b diff --git a/lib/miniz b/lib/miniz new file mode 160000 index 0000000..e55522a --- /dev/null +++ b/lib/miniz @@ -0,0 +1 @@ +Subproject commit e55522aa315852f79f24483e7f296c77e25f8252 diff --git a/offsets.h b/offsets.h new file mode 100644 index 0000000..0683701 --- /dev/null +++ b/offsets.h @@ -0,0 +1,17 @@ +#ifndef _OFFSETS_H +#define _OFFSETS_H + +/* Offsets for Call of Duty: Black Ops 1 (Win32) */ +/* Found using IDA */ +#define T5_Scr_LoadScript 0x00661AF0 +#define T5_Scr_GetFunctionHandle 0x004E3470 +#define T5_DB_LinkXAssetEntry 0x007A2F10 +#define T5_Scr_ExecThread 0x005598E0 +#define T5_Scr_FreeThread 0x005DE2C0 +#define T5_Scr_LoadGameType 0x004B7F80 +#define T5_Dvar_FindVar 0x0057FF80 +#define T5_Assign_Hotfix 0x007A4800 +#define T5_init_trigger 0x00C793B0 +#define T5_Thread_Timer 0x004C06E0 + +#endif -- cgit v1.2.3-70-g09d2