Saltar a la navegación Saltar al contenido principal Ir al pie de página

Retro Gaming Vulnerability Research: Warcraft 2

19 diciembre 2023

By Caleb Watt

This blog post is part one in a short series on learning some basic game hacking techniques. I’ve chosen Warcraft 2 for a variety of reasons:

  • Old games have more lax security (no anti-cheat)
  • Easy to run on even modern OSes (Runs on Windows XP – 11 using GoG)
  • Easily obtained in a digital format for relatively cheap ($10 USD on GoG)

With those things in mind, most older RTS games work in a similar manner, and you should be able to apply these techniques to other games, though maybe not the tooling I’ve developed for this.

Why reverse engineer games?

While hacking apps in general can be complex, games often add additional layers of complexity, such as rendering engines, physics engines, network inputs (peer to peer and server-based, LAN or internet). Anyone interested in this should really have at least a basic understanding of their target architecture (x86/x86-64 for most PC games). It helps to know C or C++, and really you need to know roughly how games work (on a deeper level than just playing them).

So what do you get out of reversing and security testing a game? Quite a few things. You can sometimes make bug fixes, both official and unofficial. Maybe add or update features with your own mods. And of course, you can find

In this post, I’ll outline the methodology I use for selecting a game, and walk through the methodology using Warcraft 2 as an example for each step. I chose this game because it runs (somewhat well) on modern operating systems like Windows 10/11, it’s cheap at $10 USD, has offline LAN play, and is very old (so there’s no anti-cheat). This makes it an ideal target for teaching black-box reversing of games.

While modern games have some differences like anti-cheat and encrypted traffic, that’s just security dressing. In the low end it’s still running loops, processing traffic, and updating a game state. Each game client sends packets on any action, and all other clients simulate each other client to show the player an accurate gamestate, even without a dedicated server to synchronize everything.

Methodology

At a high level you can begin a game-hacking project by thinking about the following topics:

  • End goal
  • Prior work
  • Attack Surface
  • Analysis
  • Exploitation

So let’s dive into each of these steps, and use Warcraft 2 as an example. The last step, exploitation, will not be shown in this blog post.

End Goal

With Warcraft 2, I would like to look for security bugs, such as memory corruption, that are remotely exploitable via other players in a match. This was first brought to my attention when GOG re-released Warcraft 2 with it still supporting online and LAN play. Warcraft 2 was first released in 1995, with the Battle.net edition for online play being released in 1999. Smashing the Stack for Fun and Profit was released in 1996. Playing a game online that was released while Stack Smashing was new is probably not a wise move. But why just assume it’s unwise when we could spend hours reverse engineering it and confirming that one way or the other?

Ultimately I probably wouldn’t play this outside of a LAN setting with trusted friends either way, but it should be fun to dig into it.

When selecting a target, depending on your goal, it’s useful to consider the following:

  • Is my target single-player, multi-player, or both? If it is multiplayer, is it peer-to-peer or server based? Online or LAN?
    • WC2 is both, peer-to-peer, online and LAN.
  • How old is this game? Older games tend to be somewhat simpler, but may be hard to run. New games will usually run, but often have more sophisticated anti-cheat.
    • If you use the GoG installer, and run the Enhanced Edition, it should run fine. Good luck if you have the older CD releases.
  • What is the attack surface? Custom maps, in-game chat, chat markdown, game-state traffic… all of this could be interesting.
    • These blog posts will look mostly just at network traffic, though fuzzing via map parsing is another interesting attack surface.
  • Is there prior work done on this game?
    • Quite a lot, though we won’t look at all of it.

Prior Work

This is an interesting topic, and depends partly on your goals. I wanted to perform clean-room reversing on Warcraft 2, so while there is a known leak of source code for the Warcraft 2 PS1 version, I’ve not downloaded or viewed it in any way, to ensure I am not tainted. I can’t comment on how similar it is to the PC version I’m playing, though I know the PS1 version lacked multiplayer, so that attack surface is likely not in the code (given the very limited 2MB of main ram the PS1 used, I’m betting Blizzard stripped out anything unnecessary.) I also believe it did not have custom maps, so the attack surface would be very limited, and ultimately not very useful to my efforts.

For any other game, there are many options to get a leg up on hacking. Many of these will only apply to closed-source games, as open-source means you can pretty easily read/modify it.

  • Are there any open-source re-implementations?
  • Existing mods
  • Known hacks/bugs
  • Source code availability
    • Open-source intentionally (such as Arx Fatalis or the original Doom)
    • Or leaked accidentally (such as Warcraft 2 PS1)
  • Debugging symbols
  • Leaked dev/beta builds

Any of the above can give you a huge leg up. For Warcraft 2, while I will not view the leaked source (if you want to find it I leave that up to you, and will not link to something that breaches copyright law), Stratagus/Wargus sounds promising initially, however upon deeper analysis it is a completely separate open-source RTS engine that has a Warcraft 2-ish mod over the top of it. It plays pretty similarly to the end-user, but the underlying code is not at all the same, so any bugs in it won’t help our purposes.

Attack Surface

You will want to consider what is the attack surface of a given game, and how will this line up with your goals? If you want to mod/bug fix, then remote attack surface isn’t usually necessary (though Ratchet and Clank: Up Your Arsenal reportedly did get a patch via a remotely-exploitable buffer overflow, since it lacked patching by default, so being creative is always cool).

In the case of Warcraft 2, we’ll considered the following attack surfaces:

  • Gamestate traffic
  • In-game chat
  • Custom maps

Analysis

This next section will be long, and I will attempt to outline how I reversed packet structures for gamestate and chat. Please be warned, there is some x86 disassembly, and it is probably a bit technical and dry. I apologize, if reverse engineering is not your jam, I am unable to make it seem cooler than it is.

Starting off, I connected two Warcraft 2 clients via LAN connection. Then I used Wireshark to start capturing traffic. I narrowed it down to the two clients by setting the following filter for just their IPs and UDP traffic.

Like many games, Warcraft 2 sends constant heartbeat packets back and forth between clients. This is to ensure that both clients are still running. If we break into the game in a debugger, the other client will pop-up a warning that it can’t reach the first client. Luckily, Warcraft 2 handles this very gracefully. Some games will kick the paused client or even crash.

All that being said, if you start watching traffic, your Wireshark UI quickly looks like this and becomes hard to read.

Here are three of those repeating heartbeats in a row, notice any patterns?

000000000166a7c8f5c39687c2000000015a0604fa6c5187c2000ca1ffffff0801017108620180​

00000000015a0604fa6c5187c20000000166a7c8f5c39687c2000cb7ffffff080000702ce20180​

000000000166a7c8f5c39687c2000000015a0604fa6c5187c2000ca0ffffff0801017209e20180

Note: The packets all start with the null bytes followed by a x01, then two 8-byte patterns. That second 8-byte pattern is flipped with the first, this is consistent when packets are sent from one client to another. Finally there are 2 bytes that are the length of the remaining bytes. Based on limited reversing, I believe this is used as the IPX wrapper for networking, which Warcraft 2 used for networking, as it’s old enough that TCP/IP was not as common. We’ll ignore those first 27 bytes on each packet (this is a shortcut, it took me reversing the game, breaking on packet receive, and seeing what the code did with the bytes to notice those are used in an IPX library, while gamestate packet parsing starts with the x00c1.) I may be wrong about this, once I realized in the debugger that the gamestate parsing didn’t read these, I mostly ignored them.

So now a single packet (spaced into bytes) looks like this:

a1 ff ff ff 08 01 01 71 08 62 01 80​

If we look at some non-heartbeat packets, we can find some other similarities, and begin to suss out meanings to these bytes. If you are used to reading network protocol formats on the wire, you may notice some potential fields here, such as lengths or counters. For now we’ll move on.

In the case of Warcraft 2, we are lucky that heartbeats are a static length, and very few gamestate updates are that same length, so an easy fix is to apply the following view filter in Wireshark

ip.addr == [client0-IP]  amp; amp; ip.addr == [client1-IP]  amp; amp; udp  amp; amp; frame.len != 81

Once we do, the traffic looks like this, with only game state traffic showing:

If heartbeats were differing lengths we’d have to reverse the traffic format more so that we could create a more nuanced filter. For instance, eventually we’ll learn that the packets contain a certain byte in a certain position to indicate what the packet is used for. If we reversed some of the game parsing we could learn what command byte is used for a heartbeat, and look for that specifically (which is what I did in the tooling to auto-ignore heartbeats).

Once we can analyze traffic in a more simple manner, we can start to issue commands in game, and look at the results in Wireshark. Let’s take an in-game chat for example. Press Enter, then type any arbitrary message, and Enter again to send it. We can repeat this two or three times, and do it from the second client back.

These similar messages will let us begin to see similarities and differences in each message.

Ignoring the beginning 25 bytes, the following is heartbeat packet from earlier compared to a chat message that said “asdfasdf”. You may begin to notice similarities and differences between these packets:

a0 ff ff ff 08 01 01 72 09 e2 01 80​

4a eb ff ff 13 01 01 c2 3b 41 23 23 fd 01 61 73 64 66 61 73 64 66 00​

If we keep the message length the same, I.E. send “asdfasdf” each time, the packet lengths are the same, and each will look something like the following (grouped for ease of viewing):

a0ffffff 08 0101 7209e2 01 80​

4aebffff 13 0101 c23b41 23 23fd01 617364666173646600​

  • The first four bytes seem to be counting down
    • Each subsequent packet has it decremented by one, these were quite a few packets apart so it’s decremented a quite a bit
  • The next byte is the length of the remaining bytes, inclusive of itself
    • x08 = 8 bytes for heartbeat, x13 = 19 bytes for the chat
  • The next two bytes indicate which player sent them
    • Both exampled above show a x0101, for coming from player 01. Other packets not shown use x0000, coming from player 00
  • The next three bytes changed for each packet
    • including from otherwise nearly identical heartbeats or chats.
    • Eventually we’ll learn these are checksums, for now we’ll ignore them.
  • The next byte (number 11) is static within similar packets
    • Eventually we’ll learn this is the “command byte” as I call it. It’s checked in a few different switch statements to determine how to process each packet. All heartbeats usex01, all chats use x23
  • Then there is a static x80 at the end of heart beats
  • Instead of the single static x80, chat messages have another three bytes before the message
    • Eventually I realized this was another checksum, by watching how it changes with each message, and trying to modify it on the wire and seeing the game handle it differently.
  • Finally we have the 9 bytes (in this case) that make up the actual chat
    • x617364666173646600 = “asdfasdfx00”, or our message with a null byte to terminate the string

Some of this took me reversing the game, breaking on the recvfrom function, and following the flow through code to see what it did. Some of this just took many similar packets to determine what each part is. Following this same idea over and over for each type of packets, we can slowly learn what packets look like for each gamestate update.

Armed with this knowledge, I began building out my tooling to be a little more dynamic than just network monitoring and a debugger. I’m a huge fan of Frida, having used it many times in the past, and decided to use that to write the test tooling, since this game lacks an anti-cheat. The version published at the same time as this blog post shows messages inbound and outbound, but does not offer the user the chance to modify them at this time (there is code for that, but it’s partially complete and commented out for now.)

You’ll notice at least one of the gamestat updates listed in the screenshot above shows another command (select unit). Reversing other commands was similar. Perform a lot of them, ignore the fields we knew, and look for new fields. I was able to make a partial table of command bytes by reading the 11th byte of any command.

  • x1c – Move UI button pressed​
  • x1c – Cancel UI button pressed​
  • x1e – Place building​
  • x1f – Stop moving​
  • x21 – Build/research​
  • x22 – Cancel build/research​
  • x23 – Send chat​
  • x28 – Move unit​
  • x05 – Broadcast game​ lobby to network
  • x13 – Update selected scenario​ to lobby
  • x1b – Select unit

Note: in WC2 commands such as move or attack don’t include a unit ID to say which unit is moving. The Select command does, then the next command sent applies to the last-selected unit. This is different than some RTS games, including the open-source re-implementation Wargus mentioned above.

Up until now, I had to do minimal reversing to figure out the gamestate traffic. It was at this point I really dug in with my debugger to start to figure out the more advanced parts, such as how are the checksums generated. If we want to spoof most any packets, we’re going to need one or more checksums. So let’s do a quick overview of how I began to do this.

Note: I was using Ida Pro for this project, along with it’s debugger, but the free NSA-provided Ghidra should work, or really any debugger you prefer. Screenshots are a work copy of Ida Pro, so may look different than yours.

First, I decided to break on the sendto function. I know from past experience that that is the native windows networking call for sending UDP packets, likewise recvfrom is for reading them. We know this game will likely be using these as Wireshark has shown us all UDP traffic up to this point.

Both of these are loaded in the process when it begins. Given that this game lacks ASLR, they can be found at x0048c88c for recvfrom and x0048c892 for sendto

You can follow code flow in the debugger from sendto, and look at the function arguments to see what will be sent. Note that the listed pointer and length show a buffer with a familiar structure, and lacks the beginning 25 bytes, since that isn’t part of what UDP is expecting.

Alternatively, since you know the address of the function, you can use Frida with onEnter to read the arguments, shown below:

This was the start of my tooling, and originally it spit all packet buffers out like this:

The next thing we really want to track down is the checksum bytes, as we’ll need to be able to generate valid ones based on new payloads in order to start spoofing more interesting payloads, and move beyond read-only.

I setup a conditional break in Ida Pro to break on recvfrom, but only if the command byte is decimal 40 (command byte x28, which is when a unit is given the Move order, based on our above table). This was done by setting the break on memory address x0045cd3f (the first step after recvfrom completes), but only if the debug byte for the buffer that was read is equal to decimal 40. This is shown below:

Note: The buffer is in a different static location for different packet types, such as a heartbeat vs a gamestate update. You may have to monitor the debugger if you want to break on certain packets. I used Frida to spit out the pointer to the buffer that was argument 1 for the sendto function.

At this point, we can follow the flow of execution one step at a time, and take a lot of notes (shown in blue to the right of each line in Ida):

Then more notes:

I took even more notes, outside of Ida:

I also took yet more notes in my nearby, trusty notepad:

And this is the reality of reverse engineering, at least for a mere mortal such as myself. It’s a lot of slowly crawling through execution in a debugger, seeing how things work, making notes of that, then confirming it on the next run. Also, accidentally hitting Run instead of Step-Into, and having to start that trace over. (I really should patch out that shortcut…)

So let’s take these notes and the above approach, and start to look at the first checksum in a packets (this is in all gamestate packets, found in bytes 8 – 10.) Some packets have more than one checksum, such as chat messages, but this methodology can be repeated to also learn how to generate those.

Following the execution after it conditionally breaks on a Move command, we can see that at memory location x0045ba15 the code looks at two bytes from the checksum, by comparing register di to ax, where previously it had generated two bytes and stored them in this register, and stored the bytes from the checksum part of the packet in this second register. It turns out that it does two bytes, then the first byte later, depending on the packet type (a Join Game packet for instance leaves one of the first three checksums equal to x00, due to not knowing required info such as the counter bytes).

Note: The blue note on line 2 was placed by me to remember what this was later once I realized what it was doing.

So once I knew I was near the checksum generation logic, I was able to recognize the following flow that had common checksum-generation code, such as shifting bits, repeating operations in a loop equal to the number of times based on the length of the packet, then compare the bytes generated. Length-based shifting patterns are common for simple checksums, so this is promising:

So we just need to see what fields it generates based on, and do those in the reverse-order for any given buffer. If the app logic is very complex (this isn’t really) then I’d stick with doing it all in assembly like above. If it’s somewhat simple like the above, a decompiler can often turn it into easier-to-read C code. Below is Ghidra’s decompilation of this function:

I re-wrote the checksum instructions in Python, to more easily integrate with wc2shell, partly shown below:

In this case, this code only checks/generates the second and third byte of the first three checksum bytes.

Some Join Game packets only use only those two (there are two similar packets for joining, command x0a and x09) this is a good packet to start fuzzing on. Additionally it involves an 11 character string for the player name.

I leave it as an exercise to the reader to extend wc2shell further to add the first checksum byte and attempt to fuzz other traffic. The current version of wc2shell can generate the two checksum bytes shown above and inject traffic (if you uncomment the writes in the Python and JS files).

Thanks for reading!