diff options
Diffstat (limited to '_posts/2023-09-17-gsctool.md')
-rw-r--r-- | _posts/2023-09-17-gsctool.md | 537 |
1 files changed, 537 insertions, 0 deletions
diff --git a/_posts/2023-09-17-gsctool.md b/_posts/2023-09-17-gsctool.md new file mode 100644 index 0000000..ca0bce0 --- /dev/null +++ b/_posts/2023-09-17-gsctool.md @@ -0,0 +1,537 @@ +--- +layout: post +title: A simple GSC loader for COD Black Ops 1 +author: Dylan Müller +--- + +> Learn how `GSC` scripts are loaded into [Black Ops 1](https://en.wikipedia.org/wiki/Call_of_Duty:_Black_Ops) and how to write your own +> simple `GSC` loader in `C` for the Microsoft Windows version of the game. + +1. [Fond Memories](#fond-memories) +2. [Game Script Code](#game-script-code) +3. [GSC Script Call Chain](#gsc-script-call-chain) +4. [Format of raw GSC scripts](#format-of-raw-gsc-scripts) +5. [Loading and executing GSC script assets](#loading-and-executing-gsc-script-assets) +6. [Black Ops Offsets](#black-ops-offsets) +7. [Source Code](#source-code) +8. [Demo](#demo) + +# Fond Memories + + +Call of Duty: Black Ops 1 + +`Call of Duty: Black Ops 1` for Microsoft Windows was released all the back back +in 2010! At the time I was still a teenager in high-school and a dedicated +gamer. + +It seemed like another purchase on steam back then but I would have never +expected a game to produce so many fond memories playing kino der toten zombies +with friends from all over the world. + +I mean who doesn't remember all the iconic glitches, mystery boxes, etc of the +game? Some say the `Black Ops 1` `zombies` was the best in the `Black Ops` series. +There is something about the game aesthetics that creates a rush of nostalgia +:video_game:. + +> Ok enough rambling, let's get technical... + +# Game Script Code + +`GSC` is an internal scripting language that is used to script missions and game +modes for `Black Ops 1`. It has a syntax similar to `C++` but is a very limited +language. Here is an example: + +``` +#include common_scripts\utility; +#include maps\_utility; + +init() +{ + level thread on_player_connect(); + + // TODO: put weapon init's here + maps\_flashgrenades::main(); + maps\_weaponobjects::init(); + maps\_explosive_bolt::init(); + maps\_flamethrower_plight::init(); + maps\_ballistic_knife::init(); +} + +on_player_connect() +{ + while( true ) + { + level waittill("connecting", player); + + player.usedWeapons = false; + player.hits = 0; + + player thread on_player_spawned(); + } +} +``` + +`GSC` scripts are only loaded and executed when a map or game has started and in +`Black Ops 1` they are reloaded every time a map or game mode is restarted. Writing +custom `GSC` scripts is thus the basis for the vast majority of `Black Ops 1` +modding efforts. + +`GSC` scripts are first loaded into memory and then compiled with an internal `GSC` +compiler. There is a public `GSC` compiler and decompiler on `GitHub`: +[gsc-tool](https://github.com/xensik/gsc-tool) but support for `Black Ops 1 (T5)` +seems to be missing. + +Luckily it turns out we don't actually need an external `GSC` compiler to compile +our own `GSC` scripts. We can (in theory) simply call the `compile()` function of +the internal `GSC` compiler and pass our mod script. + +Whenever we talk about hooking a function or calling an in-game function we +usually assume that static analysis has been performed on the game binary to +find functions and offsets of interest. `IDA Pro` is my disassembler of choice +but `Ghidra` is probably also worth mentioning. + +For debugging, `Cheat Engine` usually does the job quite well. However debugging +`Black Ops 1` is not as simple as attaching to the process and setting +breakpoints. + +As far as I know this was possible in the official release version of the game, +but the game received multiple updates/patches which introduced various +primitive `anti-debugging` and anti cheat detections which must be bypassed. + +Usually if I am starting reverse engineering on a game, my first quick option is +[scyllahide](https://github.com/nihilus/ScyllaHide). This seemed to work +for the latest `Black Ops 1` patch which allowed me to set breakpoints in game +which aided in the search for various `Black Ops 1` function addresses. + +As we know research is an important part of any reverse engineering task, +without adequate research you could be wasting hours, days or even months of +your time by doing the same work that others have done before. + +So I started research and found out that the source code for the `CoD 4` server +was leaked at some point and can be found in the following repo: +[cod4x_server](https://github.com/callofduty4x/CoD4x_Server) + +This is very valuable information and was the basis for understanding how `GSC` +scripts are loaded and compiled in game. + +# GSC Script Call Chain + +The next task was figuring out the call chain (sequence of function calls) which +is required for loading custom `GSC` scripts. So I decided to search for the +keyword `script` and stumbled upon a function called `Scr_LoadScript` +[here](https://github.com/lunarbin/CoD4x_Server/blob/14f0d8a205d80c9b30e308b57f0cd9bd6d7cdb1c/src/scr_vm_main.c#L1018) +which seemed to be responsible for loading a `GSC` or `CSC` script (`CSC` scripts are +almost identical to `GSC` scripts but are executed by the client instead of a +server). + +The only argument to `Scr_LoadScript` was a const character string however: + +``` +unsigned int Scr_LoadScript(const char* scriptname) +``` + +which I assumed was some type of resource/asset string. I therefore proceeded to +look for any functions that would load assets and stumbled upon `DB_AddXAsset` +[here](https://github.com/lunarbin/CoD4x_Server/blob/14f0d8a205d80c9b30e308b57f0cd9bd6d7cdb1c/src/bspc/db_miniload.cpp#L2919). + +The function signature for `DB_addXAasset` was as follows: + +``` +XAssetHeader __cdecl DB_AddXAsset(XAssetType type, XAssetHeader header) +``` + +I decided to search for the definition of `XAssetHeader`. `XAssetHeader` was +defined as a union of various +[structs](https://github.com/lunarbin/CoD4x_Server/blob/14f0d8a205d80c9b30e308b57f0cd9bd6d7cdb1c/src/xassets.h#L81): + +``` +union XAssetHeader +{ + struct XModelPieces *xmodelPieces; + struct PhysPreset *physPreset; + struct XAnimParts *parts; + ... + struct FxImpactTable *impactFx; + struct RawFile *rawfile; + struct StringTable *stringTable; + void *data; +}; +``` + +Out of all the structs listed `RawFile` seemed like the most interesting for our +purposes (loading `GSC` script assets). Let's have a look at the `Rawfile` +structure +[itself](https://github.com/lunarbin/CoD4x_Server/blob/14f0d8a205d80c9b30e308b57f0cd9bd6d7cdb1c/src/xassets/rawfile.h#L5): + +``` +struct RawFile +{ + const char *name; + int len; + const char *buffer; +}; +``` + +Seems simple enough: +* `name` is the resource/asset identifier. +* `len` is the total buffer length. +* `buffer` is the buffer holding our script to compile. + +One thing worth noting is that we don't actually know the format that scripts +are stored in before they are compiled (i.e format of `buffer`). For example, +are they plaintext, compressed or encrypted at all? We will discuss this further +on. + +Let us now for a brief moment turn our attention to the following code block: + +``` +XAssetEntry newEntry; + +newEntry.asset.type = type; +newEntry.asset.header = header; + +existingEntry = DB_LinkXAssetEntry(&newEntry, 0); +``` + +This seems to be doing the actual asset allocation. The first argument is of type +`XAssetEntry` which is defined as: + +``` +struct XAssetEntry +{ + struct XAsset asset; + byte zoneIndex; + bool inuse; + uint16_t nextHash; + uint16_t nextOverride; + uint16_t usageFrame; +}; +``` + +The first element of this struct `asset` looks interesting: + +``` +struct XAsset +{ + enum XAssetType type; + union XAssetHeader header; +}; +``` + +We know what `XAssetHeader` is and `XAssetType` is a simple enum which is used +to indicate the asset type: + +``` +enum XAssetType +{ + ASSET_TYPE_XMODELPIECES, + ASSET_TYPE_PHYSPRESET, + ASSET_TYPE_XANIMPARTS, + ... + ASSET_TYPE_RAWFILE +} +``` + +`ASSET_TYPE_RAWFILE` seems like the right enum value in our case. With all this +information we now know what is required to load a script asset: + +1. Allocate `RawFile` struct with script name, size and data buffer. +2. Initialise `XAssetHeader` union with pointer to our `Rawfile` struct. +3. Allocate `XAssetEntry` struct. +4. Set `asset.type` to `ASSET_TYPE_RAWFILE` (`XAssetEntry`). +5. Set `asset.header` to `XAssetHeader` allocated earlier (`XAssetEntry`). +6. Call `DB_LinkXAssetEntry` to 'link' the asset. + +After this series of steps it should be possible to call `Scr_LoadScript` with +the path of our asset. This would be the same as the `name` member of the +`RawFile` struct. + +# Format of raw GSC scripts + +We now have most of the required information to load `GSC` script assets into +memory but one question remains, what format are they in? + +To answer this question I thought a good starting point would be to hook/detour +`DB_LinkXAssetEntry` and dump the `RawFile` data buffer for a `GSC` script to a +file for analysis. + +To detour functions in the game we need to bypass an annoying `anti-debug` +feature introduced into the game that uses `GetTickCount` to analyse a thread's +timing behaviour. + +Using Cheat Engine I managed to trace the evil function to `0x004C06E0`: + +```c +#define T5_Thread_Timer 0x004C06E0 +``` + + +Anti-debug timer view in `IDA`. + +So make sure you patch this out otherwise the game will simply close after some +time after detouring. + +We would have to hook the function before loading a solo zombie match as script +assets are loaded into memory shortly after starting a game mode. I wrote a +simple detouring library called [cdl86](https://github.com/lunarbin/cdl86) +that can `hook/detour` functions either by inserting a `JMP` opcode at the target +functions address to a detour function or through a software breakpoint. + +I will leave this task up to the reader to perform. What you need is an address +and I took the liberty of finding the address of `DB_LinkXAssetEntry` for you in +`IDA Pro`. If you are wondering how I found the address, it would be a combination +of research, debugging and static analysis. + +A lot comes down to recognising patterns in the disassembly. In general as you +find the address of functions it gradually becomes easier to find the address of +other related functions. + +Bare in mind this is the address of the last `x86` (`32-bit`) `PC` version of the +game: + +```c +#define T5_DB_LinkXAssetEntry 0x007A2F10 +``` + +Once you have a `GSC` script dump, open it in your favorite text editor (I used +`HxD`). This is a dump of a small `GSC` script from memory: + + + +We can immediately see that the script is not stored in simple plaintext. In +this case I like to look for `magic` bytes which may indicate if the file was +compressed. `zlib` compression is one of the most common compression types for +binary files. + +Based off research I did, `zlib` uses the following magic bytes: + +``` +78 01 - No Compression/low +78 9C - Default Compression +78 DA - Best Compression +``` + +These bytes should appear not too far from the start of the file. Notice that in +the dump these bytes appear at offset `0x8` and `0x9` so there is a good chance +this file is `zlib` compressed. Let's test this theory, here is a simple `zlib` +`inflate` python script: + +``` +import zlib + +with open(sys.argv[1], 'rb') as compressed_data: + compressed_data = compressed_data.read() + unzipped = zlib.decompress(compressed_data[8:]) + with open(sys.argv[2], 'wb') as result: + result.write(unzipped); + result.close() +``` + +I get the following text: + +``` +// THIS FILE IS AUTOGENERATED, DO NOT MODIFY +main() +{ + self setModel("c_jap_takeo_body"); + self.voice = "vietnamese"; + self.skeleton = "base"; +} + +precache() +{ + precacheModel("c_jap_takeo_body"); +} + +``` +This text has a size of `0xC6`. + +Great and `+1` for the data being stored in plaintext! + +So what are the first `8` bytes used for? + +If you have a sharp eye you might have noticed that this 8 bytes actually +contains `2` values. + +The first `4` bytes seems to represent our `inflated` (decompressed) data size of +`0xC6` and the second value seems to represent the `size of the file - 8 bytes` +or the size of our compressed data which is `0x9A`. + +These values seem to be stored in `little-endian` format with the `4` byte integers +ordered as `LSB`. + +Great we now have enough information to load a plaintext `GSC` script, compress it +and load it as an asset using `DB_LinkXAssetEntry`. It's pretty cool that it is +this straightforward to decompress `GSC` scripts as it gives insight into how the +game modes/logic were implemented and plenty of ideas for mods of course! + +So in theory we could inject a `DLL`, read our `GSC` script, compress it, link it +with `DB_LinkXAssetEntry` and then call `Scr_LoadScript` right? We will discuss +this in the next section. + +# Loading and executing GSC script assets + +It turns out that we cannot simply call `Scr_LoadScript` when we like. As +mentioned above `CoD` `Black Ops` will load all script assets using +`DB_LinkXAssetEntry` and then compile them using `Scr_LoadScript` while a map is +loading. + +Therefore we have to load our `GSC` script at the correct time otherwise it won't +work. The strategy is therefore to hook `Scr_LoadScript` and then load our +custom `GSC` script asset when `Scr_LoadScript` is loading one of the last `GSC` +scripts for the current map. + +It turns out that one of the last `GSC` scripts contains the current map name. + +To get the value of the current map we can utilize the function `Dvar_FindVar` +which i found to be at address: `0x0057FF80`: + +``` +#define T5_Dvar_FindVar 0x0057FF80 +``` + +It's function prototype is defined as follows: +``` +typedef uint8_t* (__cdecl* Dvar_FindVar_t)( + uint8_t* variable +); +``` + +The variable in question is `mapname` which will return the current map. It +should be noted that `Scr_LoadScript` does not execute our `GSC` script however, +instead we defer execution of our main function in our `GSC` script until the map +has finished loading. + +To this end i found a convenient function that is called when the game is just +about to start at address `0x004B7F80`: + +``` +#define T5_Scr_LoadGameType 0x004B7F80 +``` + +with the following signature: +``` +typedef void (__cdecl* Scr_LoadGameType_t)( + void +); +``` + +To defer execution however we need to obtain the function handle after the `GSC` +script has been loaded in our hooked `Scr_LoadScript`. In this case we utilize a +function called `Scr_GetFunctionHandle` which returns the address of a function +which has been loaded into the `GSC` execution environment or `VM`. + +It has the following function signature: + +``` +typedef int32_t (__cdecl* Scr_GetFunctionHandle_t)( + int32_t scriptInstance, + const uint8_t* scriptName, + const uint8_t* functioName +); +``` + +`scriptInstance` is a variable which determines whether the script is a `GSC` or +`CSC` type script. A value of `0` indicates a `GSC` script while `1` indicated a `CSC` +script. + +The function handle to our loaded script is then stored in a global variable and +the function can be executed in it's own thread using `Scr_ExecThread` which has +the following prototype: + +``` +typedef uint16_t (__cdecl* Scr_ExecThread_t)( + int32_t scriptInstance, + int32_t handle, + int32_t paramCount +); +``` + +I found this function at address: `0x005598E0`: +``` +#define T5_Scr_ExecThread 0x005598E0 +``` + +I also noticed during static analysis that a call to `T5_Scr_FreeThread` was +always made following a call to `Scr_ExecThread` so that the pseudo code would +look something like: + +``` +int16_t handle = Scr_ExecThread(0, func_handle, 0); +Scr_FreeThread(handle, 0); +``` + +We also obviously also need a compression library and for this I am using +[miniz](https://github.com/lunarbin/miniz). + +So to summarize the steps to inject our `GSC` script via our `Scr_LoadScript` hook: + +1. Query map being loaded with `Dvar_FindVar`. +2. Compare current `scriptName` with map name. +3. On match open `GSC` script, compress and load asset as described above. +4. Call `Scr_LoadScript` on our loaded script asset. +5. Get function handle with `Scr_GetFunctionHandle`. + +To execute our `GSC` script entry function via our `Scr_LoadGameType_hk` hook: +1. Call `Scr_ExecThread` on function handle. +2. Call `Scr_FreeThread` on handle returned by `Scr_ExecThread`. + +# Black Ops Offsets + +I have included all the offsets I found for the game 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 +``` + +A simple challenge would be to find these offsets yourself, it is not difficult. + +`Black Ops 1` uses the `__cdecl` calling convention for functions. + +# Source Code + +I have included the source code of my simple `GSC` loader in the +[following](https://github.com/lunarbin/gsctool/) repo. + +I have also included a sample `GSC` script that spawns a raygun at spawn in +zombies solo mode and dumps `GSC` scripts as they are loaded via a +`DB_LinkXAssetEntry` hook. + +# Demo + + + +# Signature + +``` ++---------------------------------------+ +| .-. .-. .-. | +| / \ / \ / \ | +| / \ / \ / \ / | +| \ / \ / \ / | +| "_" "_" "_" | +| | +| _ _ _ _ _ _ ___ ___ _ _ | +| | | | | | | \| | /_\ | _ \ / __| || | | +| | |_| |_| | .` |/ _ \| /_\__ \ __ | | +| |____\___/|_|\_/_/ \_\_|_(_)___/_||_| | +| | +| | +| Lunar RF Labs | +| https://lunar.sh | +| | +| Research Laboratories | +| Copyright (C) 2022-2025 | +| | ++---------------------------------------+ +``` + |