summaryrefslogtreecommitdiff
path: root/_posts/2023-09-17-gsctool.md
blob: ca0bce0eb0f72bde11519d2f708dbbc6ee81b85a (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
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               |
|                                       |
+---------------------------------------+
```