Reverse Engineering : PwnAdventure3 and Xonotic

I’ve been hacking a lot with my friend Gector, a fellow member of the UTW group, and we’ve been playing around with reverse engineering video games.

It all started with PwnAdventure3, a game purposefully made to be hacked. We found some tutorials on YouTube by LiveOverflow, a great hacking/CTF channel if you ever want to learn how to read reverse engineered data or play fun capture the flag challenges online.

LiveOverflow is currently making a great playlist & documentation of his hacks and finding ways to exploit PwnAdventure3, and I highly recommend trying it out for the sake of learning reverse engineering tools and just to have fun exploiting a video game :)

To be able to set up and play the game, you will have to set up some sort of game server to play on, which you can follow tutorials on how to do so here: https://github.com/LiveOverflow/PwnAdventure3 in the README section.

In two days of intensive hacking we got through part 6 of the tutorials: learning radare, IDA, using LD_PRELOAD to load the game with your configuration instead of the default game config, hovering, changing values to alter the game experience, and more. It was awesome to be able to see the correlation in the code and the in-game experience.

Gector got a little farther in the series than I did, since I haven’t been able to set up my own game server (my old dell server, kartoffel, isn’t able to support it). But hopefully he can set up a public gaming server that we can both work on, and hopefully rope another one of our hacker friends, Jay, into the fun as well.


A few weeks later, I found Gector reverse engineering another game from scratch: Xonotic. Xonotic is an open source multiplayer FPS game that seems to be fun (I haven’t actually played it, only hacked it).

He had wireshark open and was attempting to disassemble the packets being sent from his client to the server. All the data was in hex, displayed like so when we looked at the values:

0000  46 b3 e2 bc 64 14 6d c5 10 10 00 7d 00 00 65 a6    F...d.m....}..e.
0010  03 a5 65 00 00 e3 dd 7b 42 c8 0b 2c af 00 00 00     ..e....{B..,....
0020  00 00 00 00 00 00 00 00 00 00 00 00 00 00 e9 43    ...............C
0030  50 44 e0 fe 54 c4 00 f8 04 c3 4a 5d 3c 44 24 34   PD..T.....J]<D$4
0040  81 c4 00 f8 3f c3 00 00 03 a6 65 00 00 dd ec 7b     ....?.....e....{
0050  42 c8 0b 2c af 00 00 00 00 00 00 00 00 00 00 00     B..,............
0060  00 00 00 00 00 00 e9 43 50 44 e0 fe 54 c4 00 f8      .......CPD..T...
0070  04 c3 4a 5d 3c 44 24 34 81 c4 00 f8 3f c3 00 00      ..J]<D$4....?...
0080  32 a1 06 00 00 2

On the far left there were line numbers. In the middle is all the hex encoded data, and on the far right is the ASCII decoded (?) from it.

After scrolling through a bunch of packets over and over, we noticed a pattern. There’s a slew of random data in the beginning, 16 bytes of zeroes, repeating values, and then 2 byte counters throughout:

[46 b3 e2 bc 64 14 6d c5 10 10 00 7d 00 00 65 a6]       [ ] - random data
(03 a5 65 00 00 e3 dd 7b 42 c8 0b 2c af 00) [[00 00     ( ) - Repeating values + counter
00 00 00 00 00 00 00 00 00 00 00 00 00 00]] {e9 43     [[ ]] - Zeroes
50 44 e0 fe 54 c4 00 f8 04 c3 4a 5d 3c 44 24 34            { } - Repeating + counter
81 c4 00 f8 3f c3 00 00} ((03 a6 65 00 00 dd ec 7b       (( )) - Repeating + counter
42 c8 0b 2c af 00)) [[[00 00 00 00 00 00 00 00 00  00    [[[ ]]] - Zeroes
00 00 00 00 00 00]]] {e9 43 50 44 e0 fe 54 c4 00 f8     { } - Repeating + counter
04 c3 4a 5d 3c 44 24 34 81 c4 00 f8 3f c3 00 00}
<32 a1 06 00 00 2>                                                              < > - Random ending values

Once we figured out that pattern, we knew we could capture a lot of packets and sort through the data. There were consistent patterns that we space out:

screenshot.png

The random data, the sequential counters, the zeroes, and then the stuff at the end.

After messing around with seeing what data changed when, we honed in on 2 tasks:

1) to see where the x, y, and z coordinates were, and 2) to see where the data shows buttons being triggered

When separating the zeroes from everything else, it was easy to see that the keystrokes and mouse buttons were the only thing changing in the packets (aside from random data). These were our notes:

FACTS:
- 00 00 68 01 98 fe = Forward LEFT
- 00 00 68 01 68 01 = Forward RIGHT
- 00 00 98 fe 68 01 = Backward RIGHT
- 00 00 98 fe 98 fe = Backward LEFT
- 00 00 1c 02 00 00 = SPECTATOR
- 00 00 00 00 00 00 00 00 04 00 - Right mouse
- 00 00 00 00 00 00 00 00 01 00 - Left mouse (NOT "10", which is crouch)
- 00 00 00 00 00 00 00 00 08 00 - Middle mouse / zoom
And there is a "12" for _scrolling_ forward, and a "13" for scrolling backward.

combine together it seems:
- 00 00 68 01 00 00 00 00 02 00 - Forward jumping! "02"
- 00 00 68 01 00 00 00 00 10 00 - Forward crouching! "10"

From the beginning we tested w , s, a, and d to see what forward/back/left/right was. Once those were identified, Gector suggested that the data ought to send position/coordinate values to the server as well, so that was our next goal.

When changing only x, y, or z, we found that there was a section of three groups of 4 bytes that contained the information of the coordinates:

screenshot.png

To validate this, we tried converting these values, 0x1414adc4, 0x386c2bc2, and 0xb4577d42, to floating point numbers. But when we did, we got crazy values like 7.50636e-27, 5.63075e-05, and -2.0069e-07.

What?

We caught some more packets, found the changing hex and verified that it was only when we moved x, or y, or z, and then put it into the converter again.

Same issue. Super small exponential numbers that didn’t make any sense as coordinates. We were stumped: we tried different converters on the internet, we tried converting with the Python shell. What were we getting wrong? Surely they were the coordinates, we’d verified only a few dozen times by then.

And suddenly it hit me: we were trying to convert the data as little endian ordering, but the bytes were being ordered as big endian.

For those of you who don’t know this organizational convention, little endian ordering is where the least significant bit (or the last bit) of a byte (or series of bytes) is stored at the very end of the byte - the right side.

For example, little endian looks like this:

Most significant bit >> [1]101 1111 0011 010[1] << least significant bit

0xDF3A --> Most significant byte [DF], [3A] least significant byte

Whereas big endian looks like this, where the most significant is put at the end, and the least significant is at the “beginning”:

```LEAST significant bit » [1]101 0011 1111 110[1] « MOST significant bit

0x3ADF –> Most significant byte is still [DF], it’s just placed at the “end” ```

Taking this concept and applying it to our original values:

0x1414adc4, 0x386c2bc2, and 0xb4577d42 became: 0xc4ad1414, 0xc22b6c38, and 0x427d57b4

When we put those hex numbers in the converter, we got waaaaaay more accurate results: -1384.63, -42.8557, and 63.3356! You can play with the converter yourself here: https://gregstoll.dyndns.org/~gregstoll/floattohex/

There are a ton of other triggers we could identify in the data, but the next stop is to create a python proxy server and alter our IP to make the game think that we’re one of the master servers: that way we can catch the packets the client sends, alter them, and then pass them through to the real main server for executing hacks :)

Until then!

{thallia}

Posts you might also like