Reverse Engineering Lazy Monday Games’ Golf Gang

In July of last year, I tried to test myself and see if I could reverse engineer the save data format of a game that I was invited to playtest. Throughout the process, I wrote this document as a summary of my findings.

Reverse Engineering Lazy Monday Games’ Golf Gang

In July of last year, I tried reverse engineering Lazy Monday Games’ Golf Gang (a game I was play-testing) for fun. I thought it would be a good experience and that I would learn a thing or two about game dev in the process. The goal: save data manipulation.

Over the course of two evenings I did some sleuthing and kept notes about what I did in this document. After completion, I sent this document in its original form to Horticulture, the game’s lead developer, and he liked it so much that he asked me to private the repo immediately! I guess that means I did something right. He and I chatted for a little-bit afterwards about the report and ended up bouncing a few ideas off of each other. Cool guy, really enjoyed my interactions with him!

Below is a revised form of the document that I sent him originally, with some minor editorial changes to make it better suited for my blog. I recently got their permission to publish it, so I’m finally posting it here! I hope you all enjoy the read!

💡
Quick disclaimer: I took several screenshots of the game’s decompiled source code for use as visual assets in this report, and I’ve gotten permission from the lead developer to publish them on this site. As per his request, I’d like to remind everyone that all the usual “don’t reproduce/redistribute source code without consent from the copyright holder” terms apply here. Therefore, if you want to share, please do so with a link and not the images themselves; it helps both he and I out. Thanks for your understanding!

About The Game

According to their steam page, “Golf Gang is the fastest game of mini-golf you’ve ever played.” I saw it at first because a popular Destiny 2 content creator I watch named Datto played it with his friends in a video a few weeks ago. It looked really fun, and I immediately became interested. I had played Golf With Your Friends before and enjoyed it, but this looked like a more party-like variation of your typical steam golf game. I immediately applied for beta access in their server. Fast-forward a few weeks later, and the play test finally opened.

Before I talk about reversing the game, I want to talk about how much I like the game. It’s fun. I haven’t been able to play with friends yet because every single one of my pc gaming friends unintentionally coordinated and all decided to take vacations at the exact same time, but I played single player a bit.

  • I like the mechanic of scoring based off both speed and strokes.
  • I like the fact that your stroke recharges the slower you move.
  • The ‘lil camera tilt and vignette when shooting while moving is awesome and makes the game feel really high energy.

I’m excited to play this with friends! I’m even more excited for Workshop content to become available for it. Genuinely might replace Golf With Your Friends as my main “shoot the shit” golf game to play with friends. Good work developer!

💡
Editor’s Note: For context, Horticulture’s nickname on the Golf Gang Discord server at the time was literally “developer.” I’d find out his actual username only after we started chatting privately.

Reversing the game

My first goal when I got access to the game was to figure out if it was possible to manipulate the game to give me more currency. Currency isn’t really important in the game, it’s purely for cosmetics, so I thought “what the hell, let’s give it a shot”. It uses Steam information in the player card, so is user data stored in a server somewhere or is it local? Let’s find out.

I also wanted to try and find how the game’s settings are stored. Hopoo Games (Risk of Rain 2 devs, also a Unity game) store an escaped XML file within the XML player save to house keymap configurations and such. Perhaps I could find something similar here. (Lord, I hope not.)

Finding the Config/Save Data

I decided to start by doing some file sleuthing. Unfortunately for me, this game is really sparse! Good work developer!

I found some logs from before my version of the play test was released, in the game’s main Logs directory. Seems these logs weren’t removed during the build process, so they shipped to everyone. Oops!

Oops! Extra logs!

I wasn’t able to glean much from these logs, but I was able to determine that the game was using a peer-to-peer implementation of multiplayer, probably using UPnP. More importantly, I didn’t see any references to an authentication server, which means there is a possibility that user data is entirely localized to the machine.

Interesting.

I found a few other logs hidden in AppData LocalLow, but that was about it. There was a SteamCloud_ggUD.sav file here though, which wasn’t in plaintext. This meant that if it was the main save, and it was stored in a binary format, I’d need to find out some way to deserialize it. (Foreshadowing!)

I still didn’t find any files lying around for game settings, like keybinds or whatnot. Remembering that Unity games can use PlayerPrefs, which use the registry as a backend, I decided to look in the registry for anything interesting. Sure enough, there was my config.

RegEdit

There’s a lot here, but it looks purely for video/audio/game configuration. No user data. Smart! That means user data’s probably in that save file we found earlier.

Alright, well now that we know where everything is, let’s get to figuring out how to deserialize the save data.

Deserializing the save

Unity games are just a giant mishmash of .dll files. Fun fact: The game itself isn’t even compiled down to an executable! Unity ships (pretty much) the same rebranded boilerplate EXE with every game, and compiles all the game’s code into a DLL that’s shipped with and loaded by that .exe file. So, let’s find the game’s main DLL and see what we can reverse engineer.

💡
Bonus! I popped the .exe into Ida Pro anyway out of curiosity. Turns out this binary is being shipped with debug flags enabled. They didn’t ship the PDB unfortunately and the code we’re looking for is in the DLL anyway, so this is pretty much useless. Just thought I’d toss this tidbit in here because… Why not?

I loaded the Assembly-CSharp.dll and Assembly-CSharp-firstpass.dll files into dnSpy to see if I could find any interesting data structures in there and sure enough, there were a lot of objects. I started scrolling around, and immediately found one class that was of interest to me: UserProfile.

UserProfile

The UserProfile.Start() function looks like it interfaces with steam to get the username and avatar, and then uses a property from Achievements to get the rest of the data. So Let’s see how Achievements is initialized, then.

Achievements

Achievements.Awake() was pretty boring, but it makes a call to Achievements.ReadData() which is quite the looker. Inside it there’s a reference to Application.persistentDataPath and the name of save file we found earlier. persistentDataPath usually points to %user%/AppData/LocalLow/[DevName]/[GameName], which is where we found the SteamCloud save. This means that file we found is not just a red herring and more than likely the actual save file.

This file looks like it’s being read in as a binary and being deserialized into an object of type CloudData. That’s probably the class that houses our save format. So let’s see…

CloudData

Bingo! We now have our format. Now, it’s python time.

Editing the data

Ok. It’s a new day.

Last night I determined that the save did have the data that we wanted in a readable format. I found the uints almost immediately. However, there’s a lot of extra metadata in the save that I didn’t really know how to handle. Type declarations, etc. CloudData uses a C# Dictionary that stores a Tuple, how would I parse that in python? There’s no documentation about how this works anywhere on the internet other than “just use C# lol” so I spent most of last night trying to decompile Microsoft’s BinaryFormatter and figure out how it works.

Eventually my brain started hurting so much that I said “fuck it” and went to sleep. I just decided that the dictionary and list objects didn’t matter since by manipulating the uints I already found, like userGolfCoins, I could just give myself insane money and buy/do whatever I want. :)

As for the actual researching process, here’s how it went:

I took my save and imported it into HxDen to see what I could find. (Any hex editor would’ve worked to be honest. hexedit would work on Linux, I think.) There’s a lot of fluff there, but I quickly found the uints I was looking for. It looked like there was a group of them starting at 0x04E0, so now it was just checking to see if it lined up with the format of CloudData.

Uints

I booted up my game, found out that I had 50 “coins”, which is what I was looking for. Each uint takes up 4 Bytes, and userGolfCoins seems to be the third item in the object, so the number we’re looking at should be located at address 0x04E0 + 0x8 offset. Sure enough, we find value 0x00000032 stored in big endian, which when converted to base-10 is 50 coins.

It’s important to note, the next 4 bytes are the lifetimeEarnings uint which in theory should be equal to or greater than userGolfCoins, but interestingly enough, there are no checks for that in the code.

Anyway, I set both to 999999 (0x000F423F).

Custom Uints

Saved it, copied it to the right folder, made a backup of the old save, and started it up.

Success?

Huh. Not 999999 like I was expecting, but still an acceptable number.

Upon further investigation, It looks like I guessed the endian-ness of the file wrong. It’s little endian, starting at offset 0x04E3!

Whoops! Wrong Endian.

Let’s manufacture a new save file with the right endian-ness this time.

This time for sure
Success!

I think our work here is done. :)


Suggestions to the Devs

Before I get into the suggestions I have to improve the project, I’d like to compliment the developer. Like I said earlier, the way save data is stored is pretty smart, certainly better than plaintext. I don’t have that many complaints, but I do have a few small, notable ones.

1.) Code Optimization

There were a few chunks of unoptimized code I found here and there. Unity doesn’t do much optimization or obfuscation on code it compiles, so that makes it all the more important for the developer to be very wary and optimize the game as much as you can. For example, there are a few cases where algorithms used two sequential for loops instead of a single one with if statements, which would decrease computation time on large sets of data. There are also some cases where certain for statements are empty, and I don’t really know why they exist? An example is in Achievements.ReadData():

Achievements.ReadData

There’s just a random foreach on line 14 that seems to serve no purpose whatsoever. It could be dnSpy messing up in decompilation, but if not, why is it there?

I know these optimizations would provide marginal improvement at best, and it’s not like these functions are called millions of times each frame, so it’s not that important. I guess it’s just me being a stickler. :)

💡
Editor’s Note: Horticulture and I spoke about that empty foreach later in direct messages, and this loop seems to have contained debugging code that wasn’t included in the main release due to lack of debug symbol definitions.

2.) Adding a Checksum to the Save Data

A good rule of thumb that I was taught when I was getting into development went as follows: “If you’re ever storing data on a user’s machine— even with obfuscation— consider that data unsafe.”

It’s true. In this case the tamper was a hex edit that I did to give me money. Admittedly this is more complex than editing a line in a text file, but at the end of the day changing 8 bytes still isn’t too hard.

One way that you can at least hinder this behavior is by adding a checksum to the end of the save file. Add an extra byte array at the end of CloudData that houses a checksum of the rest of the object’s values. How this checksum is calculated is entirely up to you, but it should be recalculated and resaved every time the file is written out. The more obfuscated you make the input data, the better.

Once this checksum is in place, when you load a save, run the same checksum algorithm on the data that you just loaded. If the checksum returned doesn’t line up with the one provided in the save file, then you can conclude that the file has been tampered with and is invalid.

Here are some good docs from Microsoft explaining how to go about hashing in C#: Microsoft Docs

3.) Cleaning up the Build Distributon

Last suggestion is to just clean up the build a little more before distribution. The extra log files in the game directory, the EXE being compiled with debug symbols, etc. If you don’t have one already, consider writing a batch/python/whatever script that you can use to automatically check various components of the game (i.e. file structure, etc.) before it’s compressed and uploaded to Steam.

Summary

Good game. Real good game.

It’s fun, runs well, and was interesting to research! Definitely took more than a few hours to figure out how things worked. The data could be stored with a little better security, but it’s better than most by far. And when it came to suggestions, I had to nitpick, which is a real good sign.

To developer and the rest of the team at Lazy Monday, you’re doing a real good job. Keep it up! I can’t wait for Golf Gang to become my new go-to golf game to play while chilling with my friends. :)

💡
Editor’s Note: I’d also like to thank Horticulture and the rest of the team at Lazy Monday for dealing with me and being good sports throughout all of our discussions. Also, special thanks to Horticulture for allowing me to publish this write-up in the first place. They’re a great dev team, and I’m really looking forward to Golf Gang and recommending it to friends upon full release. Go give their game some love!