summaryrefslogtreecommitdiff
path: root/source/notes/star_trek_armada/star_trek_armada.md
blob: a4e03d04b33bdc1212d752c196436cf7156885de (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
# Fixing Star Trek: Armada

After a few weeks of DS9 brainwashing I felt like playing [Star Trek: Armada](https://en.wikipedia.org/wiki/Star_Trek:_Armada). A cool 20ish year old game, worked fine last time I played it on a Windows XP computer, should be fine, right?

## Insufficient Memory

![Not enough memory](not_enough_memory.png)\  

Not good. This computer has 32gb, undoubtedly something somewhere overflowed. I really wanted to play, so I grabbed OllyDbg and started looking to fix the problem.

Running under Olly let me debug at the point the message box is opened, and going up the stack a little eventually lead me to this interesting code:

<div class="codebox">
#eval cat memory_check | aha --no-header --stylesheet
</div>

[`GlobalMemoryStatus`](https://msdn.microsoft.com/en-us/library/windows/desktop/aa366586(v=vs.85).aspx) is a Windows API to retrieve memory information, so this is a reasonable place to start looking for why the game would think I have less than 50mb of memory.

That function populates a `MEMORYSTATUS` struct:

<div class="codebox">
```c
typedef struct _MEMORYSTATUS {
  DWORD  dwLength;
  DWORD  dwMemoryLoad;
  SIZE_T dwTotalPhys;
  SIZE_T dwAvailPhys;
  SIZE_T dwTotalPageFile;
  SIZE_T dwAvailPageFile;
  SIZE_T dwTotalVirtual;
  SIZE_T dwAvailVirtual;
} MEMORYSTATUS, *LPMEMORYSTATUS;
```
</div>

and noting the MSDN page:

> On computers with more than 4 GB of memory, the GlobalMemoryStatus function can return incorrect information, reporting a value of –1 to indicate an overflow.

Having 32gb, this probably returns -1, and a signed comparison is being done against an expected value.

Placing a breakpoint on `0x5005b0` and looking at what `eax` points at yields this:

<div class="codebox"><pre>
0018F678|20 00 00 00|27 00 00 00| ...'...
0018F680|<b>FF FF FF FF</b>|<b>FF FF FF FF</b>|........
0018F688|<b>FF FF FF FF</b>|<b>FF FF FF FF</b>|........
0018F690|00 00 FE 7F|00 40 6F 7A|......oz
</pre></div>

in bold, the values for `dwTotalPhys`, `dwAvailPhys`, `dlTotalPageFile`, and `dwAvailPageFile`. All of these are 0xFFFFFFFF, aka -1. The values for virtual memory are reasonable, just shy of 2GB.

Confirming the issue, in the assembly above I can see those values are loaded into registers at `0x5005dc` and `0x5005df`, and compared against constants at `0x5005e2` and `0x5005f4`.

The comparisons aren't terribly interesting, but the conditional branches they result in are: `0x5005f2` and `0x5005fa` are branches with `jge`, which interprets the flags `cmp` set as if they were for a **signed** comparison.

This probably means the author of this code wrote these checks like:

<div class="codebox">
```c
MEMORYSTATUS* memInfo;
GlobalMemoryStatus(memInfo);
if (
  (int)(memInfo->dwTotalPhys) <= 28 * 1024 * 1024 &&
  (int)(memInfo->dwTotalPhys) <= 30 * 1024 * 1024
) {
  failMemCheck();
}
```
</div>

where instead the constant should have been cast to `SIZE_T` (an unsigned type).

This is easily fixable: instead of `jge`, use `jae`. Replacing `0x7d` at `0x5005fe` and `0x5005fa` with `0x73` and rerunning seems to convince the game all is fine.

Except now it crashes at startup.

## Crash at Start

Occasionally, the game will resize the parent window (explorer.exe, for how I was running it) to fullscreen and move it to the top left, so there's some memory corruption happening. Probably passing the parent window's handle to Windows functions, rather than its own.

Either this function was doing something important that I broke by changing the comparison, or something elsewhere is also buggy.

Looking through functions that are called, there's a fair amount of sprintf, but enough to make me look elsewhere first. There are also calls to getenv, getcwd, and putenv, only one of each, so it's easy to verify if those are relevant or not.

They get called in the function at `0x0043feb0`, in this region:

<div class="codebox">
#eval cat sprintf_overflow | aha --no-header --stylesheet
</div>

This is promising. My `%PATH%` is fairly large for unrelated reasons, and the buffer that `sprintf` at `0x43ff8c` writes into is `ebp - 0x430`. The formatted string consists of `%PATH%;%CWD%\AI;%CWD%\Missions`, and if that ends up being larger than ~1kb, it begins arbitrarily corrupting the stack.

The produced string might be important, but to verify this, I replaced the entire `sprintf` with `nop`:

<div class="codebox">
#eval cat sprintf_overflow_fix | aha --no-header --stylesheet
</div>

And reran:

![star trek armada main scren](armada.png)\  

It starts successfully!

Getting into a game shows other issues:

![ingame graphics glitches](uh.png)\  

At this point it's likely issues with D3D APIs I don't know so well, so I fiddled with compatibility settings in the hope that something would work. Disabling desktop composition did the trick:

TODO: image of ingame

Dunno what desktop composition does other than having to do with Windows Vista and later composing the display differently, so a fun followup might be fixing the game to not require this setting.

## Bonus: "Please Insert CD"

If I didn't have the iso of my disk mounted, I was greeted with a pop-up asking me, `Please insert CD`. If you were inclined to do something about that...

radare: `/ Please\x20insert\x20CD`

<div class="codebox">
#eval cat find_cd_strcheck | aha --no-header --stylesheet
</div>

Finding that string (or most of it) at `0x0061a9b4`

Figuring out where the string gets used evades the combination of my skills with radare and time I was willing to put in, but Olly was easier:

1. Right click on any instruction -> search for -> all referenced text strings
1. Right click on strings -> search for "Please insert CD "
1. Double click the highlighted string to show what instruction referenced it
1. That's approximately where the check is done

Now I think normally there would be a conditional branch at `0x440551` that might show a dialog and close the game, but *for some reason* the test and branch are nop'd out to unconditionally never show me a "Please insert Armada CD" dialog...!