--- layout: post title: A simple GSC loader for COD Black Ops 1 author: Dylan Müller author_url: https://linkedin.com/in/dylanmuller --- > 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://lunarjournal.github.io/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/lunarjournal/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/lunarjournal/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/lunarjournal/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/lunarjournal/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/lunarjournal/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/lunarjournal/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 who will have 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://lunarjournal.github.io/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/lunarjournal/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://lunarjournal.github.io/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/lunarjournal/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/lunarjournal/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 {% include youtube.html id='Gs9mHXyiGNg' %} # Signature ``` +---------------------------------------+ | .-. .-. .-. | | / \ / \ / \ | | / \ / \ / \ / | | \ / \ / \ / | | "_" "_" "_" | | | | _ _ _ _ _ _ ___ ___ _ _ | | | | | | | | \| | /_\ | _ \ / __| || | | | | |_| |_| | .` |/ _ \| /_\__ \ __ | | | |____\___/|_|\_/_/ \_\_|_(_)___/_||_| | | | | | | Lunar RF Labs | | https://lunar.sh | | | | RF Research Laboratories | | Copyright (C) 2022-2024 | | | +---------------------------------------+ ```