Hacking Python Applications: Reverse Engineering
In the last article we learned about some of the methods used to reverse engineer your target application. We also saw some of the tools that come bundled with pynject. In this article we will be using those tools to gain a better understanding of how Rift Wizard works, and what we can include in our cheat. I don't have any particular goal in mind, so we're just going to poke around blindly until we find some interesting stuff.
So, first of all let's boot up the game and play around a bit. The game is going to crash quite frequently while we mess with it. So let's make it easy to get going. I'm going to start a new windows terminal window and create two panes. One for pynject, and one for launching Rift Wizard.
To split the screen into two panes, we press 'Alt+Shift+='. I am going to shrink the new pane with 'Alt+Shift+Right'.
Now on the left side I navigate to my payload development directory which contains the two bundled payloads. On the right side I browse to Rift Wizard's installation directory. We can find this by right clicking the game in our steam library, clicking properties, clicking local files, and then browse. Now we have a problem.
Steam has the option to launch the game windowed, however when launching the executable from the terminal it defaults to fullscreen. I assume some argument is passed when the game is launched in windowed mode via steam. So let's launch Task Manager, and enable the command line column. (To do this right click on any of the columns and check 'Command line'.)
And there we have it. The argument is 'windowed'. Now we can quickly launch Rift Wizard in windowed mode from our terminal window. That means we can simultaneously poke around and see the results on the same screen.
So now we're going to inject the inspector payload into Rift Wizard, and begin getting a look at the objects of which it is composed. First we're going to scan for the process, and then we're going to launch pynject with the PID and the path to the inspector script as arguments.
When this script is run within the target process, it creates the above GUI window. Learning how to use this tool is not necessary, all it does is make the methods we learned in the last article simpler to use. You could accomplish the same things manually, however in this article we will be using it extensively. To get acquainted with its layout and features reference the official docs.
The first thing we're going to do is look at the global symbol list for globals which are simple data types. The first types we stumble upon are floats and bools. There are only eight of them, here they are.
Wow... do you see what I see? It appears to be a cheat mode! Probably written to assist the developer in debugging since there's no way to access it from within the game, and there's no mention of it anywhere online. I can assume from the names that 'can_enable_cheats' would allow enabling of cheats somewhere within the game. More interesting, we have 'cheats_enabled' which presumably would do exactly what it's named. Why don't we give it a shot? I wrote this simple toggle script.
if (cheats_enabled): cheats_enabled = False else: cheats_enabled = True
I inject it and nothing seems to happen. But the variable's value has changed in the inspector.
So I head into the game and begin mashing random keys. I hit the 'I' key, and... items!
So our script has succeeded. We can now give ourselves a bunch of seemingly random items. What other keys are mapped to what? Let's grab our notes and write down the noticeable effects of each key.
Great. So we've found some stuff. Written it all down, and made some changes to our simple toggler. Now we have a functional cheat.
# Useful # ------ # i = Give some random items. # h = Add health and max health. # x = Increment SP. # q = Spawn shrines at mouse. # d = Randomize rifts. # t = Teleport to mouse. # k = Kill all monsters. # o = Spawn orb at mouse. # + = Realm up. # - = Realm down. # p = Activate PDB. (Freezes process, use if you know what you're doing.) # Awful # ----- # g = Die. # v = Close game. # y = Subtract SP. # m = Spawn monsters at mouse. # b = Spawn mordred at mouse. # ??? # --- # l = I think it loads the "cheat_save" file. I have no idea how or when one is created however. # f = Does... something to the display. cls() global can_enable_cheats global cheats_enabled if (not can_enable_cheats): can_enable_cheats = True if (not cheats_enabled): cheats_enabled = True print("Cheats Enabled") else: cheats_enabled = False print("Cheats Disabled")
Now... is that it? We've technically succeeded, but does that feel satisfying? Sure the discovery was nice, but cheat keys overlap control keys. Half of our 'cheats' are actually anti-cheats! We need something more than a debugging mode. Let's keep searching.
There are no other flags I can see, and I do not have enough knowledge about the game to understand what effect changing some rates would have. How can we find what's currently happening on the screen?
Well, let's do some digging into classes. I see classes for each upgrade, spell, monster, and consumable. Beyond that I see subclasses of a "Level" class. These seem more general, not types of objects we may interact with in the game like a monster or an upgrade.
Hmm, this really stands out. It seemingly contains objects of many different types. We have powerup dots, NPCs, shops, portals, etc. This is everything we interact with in a playthrough. Lets find instances of a this "Level" object.
So we have three levels. One is actually mapped to a global symbol, 'test_level'. Wow. I wonder what that could be. Let's see what the test level has going on in there. Right click the instance, and press inspect.
We are presented with the first children of this instance. And I see an interesting attribute, 'player_unit'. Obviously this is a test instance so there is no player in this level. Upon looking inside the other two, both have a player unit. One of these must be the current level, and the other a level we've already been through. Let's check out the attributes of the Unit object 'player_unit'.
Awesome. This is definitely us, and these are some seriously powerful values! There is a boolean which denotes whether the Unit object is player controlled or not. So what if we got a list of all Unit instances from the gc, and checked each of them for the player controlled flag? You may be wondering how we will handle the multiple instances. The answer is we won't! What harm will it do to set an attribute of an object that used to be the player? Let's try and set the health of all player controlled units tracked by the gc to 999.
import gc for obj in list(gc.get_objects()): if (type(obj).__name__ == "Unit"): if(obj.is_player_controlled): obj.max_hp = 999 obj.cur_hp = 999
Success. Now we have free rein over the player's attributes and the level's other contents. So we now know how to find basic values and edit them from our scripts. It's really a lot of digging and testing. In the next article we're going to start with a goal and see if we can figure out how to accomplish it. Until next time!