summaryrefslogtreecommitdiff
path: root/_posts/2023-09-17-gsctool.md
diff options
context:
space:
mode:
Diffstat (limited to '_posts/2023-09-17-gsctool.md')
-rw-r--r--_posts/2023-09-17-gsctool.md537
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
+
+![Black Ops 1 Zombies](https://journal.lunar.sh/images/6/01.jpg)
+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](https://journal.lunar.sh/images/6/03.jpg)
+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:
+
+![GSC Dump](https://journal.lunar.sh/images/6/02.jpg)
+
+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
+
+![](https://journal.lunar.sh/images/6/gscdemo.png)
+
+# Signature
+
+```
++---------------------------------------+
+| .-. .-. .-. |
+| / \ / \ / \ |
+| / \ / \ / \ / |
+| \ / \ / \ / |
+| "_" "_" "_" |
+| |
+| _ _ _ _ _ _ ___ ___ _ _ |
+| | | | | | | \| | /_\ | _ \ / __| || | |
+| | |_| |_| | .` |/ _ \| /_\__ \ __ | |
+| |____\___/|_|\_/_/ \_\_|_(_)___/_||_| |
+| |
+| |
+| Lunar RF Labs |
+| https://lunar.sh |
+| |
+| Research Laboratories |
+| Copyright (C) 2022-2025 |
+| |
++---------------------------------------+
+```
+