Hacking video games poses interesting challenges that sit outside the realm of traditional vulnerability research and exploit development. It requires a different perspective that aims to solve a set of goals that rely heavily on reverse engineering and shares similar techniques to that of malware analysis. However unlike traditional exploit development, when you hack a video game it provides immediate visual feedback.
At Somerset Recon, we find value in researching this form of hacking. While it is a bit esoteric, in the end it is still hunting for vulnerabilities in software. Additionally, much of the software in video games shares similarities to the software we regularly perform security assessments on. These similarities include utilizing custom protocols, assuming trust in the client, and using an architecture built upon legacy software/architecture with features bolted on, etc.
Hammerwatch is one game that encompasses all these elements. It is a top down multiplayer “hack-and-slash” dungeon-crawler that draws direct inspiration from the classic arcade game Gauntlet. The multiplayer gameplay uses a client-server architecture. When starting a multiplayer session a user hosts a game and other clients connect to that user’s game session. Our goal here was to unlock or create abilities in Hammerwatch that the game was not intended to have, to have those newly created abilities work in multiplayer, and to attempt do it all with style.
During our research, we quickly discovered that Hammerwatch had a very loose client-server model. Using the memory editor in Cheat Engine, we were able to set our health value and the server respected the change. This led us to believe that the client was responsible for updating the server of changes and that these changes were not double-checked by the server.
Our next steps were to reverse the codebase to observe what values in the game we could change. Since Hammerwatch is written on Mono, decompiling it with a .NET decompiler gives us the full C# codebase. We used dnSpy for this task. Loading the Hammerwatch.exe executable results in a tree-view which nicely displays all the classes in the game, including the character classes.
After reviewing the classes, we noted that the ranger character looked interesting, so we decided to focus our efforts on modifying the Ranger class. The Ranger class is extended by a subclass, PlayerRangerActorBehavior, that contains the properties and behaviors of our Ranger character. This subclass contains a function called "Damaged" that controls how the client calculates and reports damage to the world.
public override bool Damaged(WorldObject attacker, int dmg, IBuff buff, bool canKill) { if (EnemyHiveMind.Random.NextDouble() < (double)this.dodgeChance) { this.dodgeEffectColor = 1f; this.dodgeSnd.Play3D(this.actor.Position, false, -1f); Network.SendToAll("RangerDodged", new object[0]); return false; } return base.Damaged(attacker, dmg, buff, canKill); }
Most of the work is done in the base class, but the ranger can randomly dodge attacks depending on a random number generator. However, an invincibility cheat can be achieved simply by patching the dodge chance check to always return false.
public override bool Damaged(WorldObject attacker, int dmg, IBuff buff, bool canKill) { this.dodgeEffectColor = 1f; this.dodgeSnd.Play3D(this.actor.Position, false, -1f); Network.SendToAll("RangerDodged", new object[0]); return false; }
Success! The client is dodging all damage indicated by the character blinking, and those dodges are then propagated to the server.
More interesting cheats can be achieved when observing the ranger classes Attack function. This function works by starting some animations, calling ShootArrow in the direction the character is facing, and updating the world about this action.
public override void Attack(WorldActor actor) { this.attacking = 1; this.attacking = this.Sprite.Length; this.Sprite.Reset(); this.attackSnd.Play3D(actor.Position, false, -1f); this.ShootArrow(this.lookDir); Network.SendToAll("PlayerAttack", new object[] { this.lookDir }); }
By replacing the above call to ShootArrow and replacing it with a custom function, we are able to modify the default shoot arrow attack with a custom attack. This attack shoots multiple arrows in a perfect circle around the player, named appropriately as “RainingDeath”.
public void RainingDeath(int numOfArrows) { for (int arrow = 0; arrow < numOfArrows; arrow++) { float angle = 6.28318548f / (float)numOfArrows * (float)arrow; Vector2 newDirection = new Vector2((float)Math.Cos((double)angle), (float)Math.Sin((double)angle)); ShootArrow(newDirection); } }
The most interesting part about this cheat is the effect it has on the host and other clients. The other players do not render the “RainingDeath” animations, but they do process the damage to enemies correctly. While it seems odd, it makes perfect sense when you take a look at the how the game handles creature damage. In BaseCreature class, the Damaged function is the handler that’s called when an enemy takes damage. The function is large, but the interesting bits are the Network.SendToAll function calls.
public override bool Damaged(WorldObject attacker, int dmg, IBuff buff, bool canKill) { ………… if (base.Health <= 0f) { this.dead = true; if (buff != null && this.HasHitEffect(buff.EffectId)) { Network.SendToAll("UnitDiedWithHitEffect", new object[] { this.actor.NodeId, buff.EffectId }); } else { Network.SendToAll("UnitDied", new object[] { this.actor.NodeId }); } } … }
The SendToAll function sends a set of predefined commands to all connected players, including the host, and it is easily abused. While our previous modifications have been focused on modifying our local behavior and seeing if it would propagate to the server, it’s clear that all we had to do was issue “UnitDied” commands repeatedly until there was no one left.
Our analysis of Hammerwatch revealed that it does not implement robust client-server protections and anti-cheating measures. If the game’s authors had made a few design decisions differently these cheats would not be possible. Specifically, creating a mechanism for the server would verify the integrity of a client’s game binary to make sure it has not been tampered with before allowing it to connect, or modifying the client-server model so that only the server were able to receive action updates, keep track of creatures damage, and enforce rules.
While these issues may seem specific to Hammerwatch, they actually extend past this. In our experience, issues that we encounter in game hacking such as custom protocols with similar weaknesses of assuming trust or not properly verifying the sender, are common in software today. All of this combined to make our work with Hammerwatch a good lesson in security as well as a fun game to hack.