CYBERSECURITY, IDENTITÄTSMANAGEMENT UND MULTI-FAKTOR-AUTHENTIFIZIERUNG

Pwn Adventure 3 – Intercepting Packets

Lesezeit: 10 Minuten

The proxy developed in our previous post will allow us to intercept and modify the content of the network communication between the client and the game server(s), thus allowing us to spawn at any location, forge new elements on the map and pick up any object.

Spawn location

During the network reversing exercise, we found out that the packet with the coordinates sent by the server to spawn the player can have multiple identification numbers. Therefore, we cannot identify the packet based on the id (opcode), but rather its format:

[II II] [?? ??] [XX XX XX XX] [YY YY YY YY] [ZZ ZZ ZZ ZZ] [RR RR] [YY YY] [PP PP]

We noticed that each time the user spawns for the first time, his direction (roll, yaw, pitch) is reset to 0. Which means the last six bytes are always set to 0x00. We also noticed that the second and third bytes are also always set to 0x00.

Spawn packet are always sent “alone”. By this, I mean that no other packets are concatenated to it. We thus build the following parser in our proxy to identify a spawn packet:

def parse(p_out):
    if len(p_out) == 22 and p_out[2:4] == "\x00\x00" and p_out[16:] == "\x00\x00\x00\x00\x00\x00":
        
        # DO SOMETHING
    
    return p_out

The function parse() is called any time the client receive or send a packet. The argument p_out is the data (buffer) received from or sent to the server.

Now that we have identified the spawn packet, we will replace the coordinate with an arbitrary value so we can spawn our character anywhere on the map (at least, that’s what we hope). We just need to replace the buffer p_out with the newly crafted spawn packet. For this first test, we will re-use the spawning coordinate in the Town location, but increase the z axis so that our character spawns in the sky.

In order to get the spawn position in Town, I just started the game with Wireshark and noted the given coordinate when spawning (I was located in the Town last time I disconnected).

Here we can see the coordinate (-39602.8, -18288.0, 2400.28). We then forge our new spawn packet accordingly:

def parse(p_out):
    if len(p_out) == 22 and p_out[2:4] == "\x00\x00" and p_out[16:] == "\x00\x00\x00\x00\x00\x00":
        
        packet_id = p_out[:2] # We re-use the packet ID
        x = -39602.8
        y = -18288.0
        z =   2400.28 + 10000
        
        p_out = packet_id
        p_out += struct.pack("=HfffHHH", 0, x, y, z, 0, 0, 0)

    return p_out

Now you just need to save the file, run it, configure your client to point to the proxy instead of the Master serve and see the result…

And it works! Now you can also play with the x and y axis to place the character wherever you want on the map. Bear in mind that the spawn trick only works at login. Whenever your character die and respawn, the packet structure (for respawn) is a bit different, therefore the parser doesn’t catch it. This means that you will have to logout and login again in order to spawn at the given location.

You will also notice that sometimes, you get disconnected. I think this is because Pwny Island is split in different locations. Each time you enter a location, the master server and/or game server need to change some parameter. However, whenever you change the spawn coordinate to another location, the server was not ready for you to be there and thus “crash”. When this happen, you just need to login again and it will works just fine.

Pick up elements

During the game, you will find elements that you can pick up or activate. For instance, at the beginning, in the cave, you have to pick up the Great Balls of Fire spell in order to burn the bushes and get out of the cave. Let say you are very lazy and don’t want to spent time picking up the spell. We can try to forge the “pick up element” packet and send it to the server. The packet is structured as follow:

[65 65] [NN NN NN NN]

Where N is the element identifier. We can find the element identified at the creation of the element. For the Great Balls of Fire, the element is create at the very beginning, whenever you join the game.

As you can see, the Great Balls of Fire has the identifier 0x0001. So our final packet will looks like this (identifier is sent in little-endian):

[65 65] [01 00 00 00]

Now the question is when to send that packet. Ideally, we want to send the packet only once to get the element, and not every time when a packet is received or sent by the client. One way to accurately decide when to send packets is to look at the chat conversation. If the user send a specific command in the chat interface, an additional packet is send (or received).

The chat packet is structured as follow:

[23 2A] [LL LL] [WW WW ...]

With L being the size of the message and W the message. Here we want to send the pick up packet whenever the user send the message “GreatBallsOfFire”:

def parse(p_out):
    
    def chat(msg):
        return "#*" + struct.pack("H", len(msg)) + msg

    def getme(elem_id):
        return "ee" + struct.pack("I", elem_id)
    
    
    if chat("GreatBallsOfFire") in p_out:
        p_out += getme(1)

    return p_out

In this code, I first forge the packet we should expect if the user send the message “GreatBallsOfFire” and we verify if it is somewhere in the buffer. If yes, I append to the buffer the packet to pick up the element 1 (i.e. the Great Balls of Fire).

So I created a new character, join the game, type “ENTER” to access the chat interface and send “GreatBallsOfFire”. Nothing happened. Maybe the server verifies if we are located near the element. Let’s add to the buffer a fake location near the element:

def parse(p_out):

    def chat(msg):
        return "#*" + struct.pack("H", len(msg)) + msg

    def getme(elem_id):
        return "ee" + struct.pack("I", elem_id)

    def loc(x, y, z):
        return "mv" + struct.pack("IIIfffBB", x, y, z, 0, 0, 0, 0, 0)
    
    
    if chat("GreatBallsOfFire") in p_out:
        p_out += loc(-43655.0, -55820.0, 322.0)
        p_out += getme(1)

    return p_out

The location (-43655.0, -55820.0, 322.0) is actually the location of the “GreatBallsOfFire (see Wireshark screenshot). Note that I added the location right before the pick up element packet. We want the server to update our location first so whenever it does the verification (if any), we will be seen next to the element.

Let’s try again. I join the game with our new character, send “GreatBallsOfFire” and… it works this time!

Create banner

This hack is useless, but it is just to make sure that you have understood the concept. The packet structure for printing a text on the screen is the following:

[6D 6B] [LL LL] [WW WW ...] [LL LL] [WW WW ...]

Where L is the length of the text and W is the text. The first string is the top title and the second is the bottom text. So if we want the text “Much skills | Such hacker!” we would add the following code:

def parse(p_out):

    def banner(msg_top, msg_bottom):
        return "ev" + struct.pack("H", len(msg_top)) + msg_top + struct.pack("H", len(msg_bottom)) + msg_bottom

    def chat(msg):
        return "#*" + struct.pack("H", len(msg)) + msg

    
    if chat("Banner") in p_out:
        p_in += banner("Much skills", "Such hacker!")

    return p_out

Create elements

Now let’s create element on the map. At the moment, there is no real reason for us to place arbitrary item on the map, but you will see later that this could be handy.

The packet to create a new element has the following structure:

[6D 6B] [NN NN NN NN] [?? ?? ?? ?? ??] [LL LL] [WW WW ...] [XX XX XX XX] [YY YY YY YY] [ZZ ZZ ZZ ZZ] [RR RR] [YY YY] [PP PP] [?? ?? ?? ??] [00 00]

N is the element identifier. W is the name of the object. X, Y, and Z are the location coordinate where to place the item and R, Y, P are the orientation of the item. The ?? bytes will be arbitrary set to 0x00.

In our first example, we will simply place a chest in front of us. So first let’s spawn at the following location: -39602.8, -18288.0, 2400.28 (Town). Then, whenever the user sends “CreateChest”, the proxy will send a packet to the client to create a BearChest item.

However, here, the tricky part is that the chat message is send from the client to the proxy; and the packet to create a new element should be sent from the proxy to the client. Therefore, we cannot just append the new packet to the initial buffer as this one will be forwarded to the server.

Therefore, we need to slightly change our parser() function as well as handle_write():

def handle_write(self):
    if self.write_buffer:
    (p_in, p_out) = parse(self.write_buffer)
    self.other.write_buffer += p_in
    sent = self.send(p_out)
    self.write_buffer = self.write_buffer[sent:]

def parse(p_out):
    p_in = ""
    # DO SOMETHING
    return (p_in, p_out)

Now we should be able to create our new item:

def parse(p_out):
    
    p_in = ""

    def createel(elid, item, x, y, z):
        packet = 'mk'
        packet += struct.pack("I", elid)
        packet += "\x00\x00\x00\x00\x00"
        packet += struct.pack("H", len(item)) + item
        packet += struct.pack("fffHHHBBBB", x, y, z, 0, 0, 0, 100, 0, 0, 0)
        return packet

    def chat(msg):
        return "#*" + struct.pack("H", len(msg)) + msg

    def newspawn(opcode, x, y, z):
        roll = 0
        yaw = 0
        pitch = 0
        return opcode + struct.pack("=HfffHHH", 0, x, y, z, roll, yaw, pitch)
    
    # Coordinate Town
    x = -39602.8
    y = -18288.0
    z = 2400.28
    
    if len(p_out) == 22 and p_out[2:4] == "\x00\x00" and p_out[16:] == "\x00\x00\x00\x00\x00\x00":
        p_out = newspawn(p_out[:2], x, y, z)

    
    if chat("CreateChest") in p_out:
        p_in += createel(1337, "BearChest", x + 500, y, z)

    return (p_in, p_out)

In this example we decided to place the chest 500 unit further from us on the y axis. So we join the game, we spawn in Town, we send the message “CreateChest” and we should see right in front of us a new chest that just appeared.

If you play with coordinates, you will notice that the chest is not affected by the gravity. Which means we could suspend object in the air, including you on top.

Loot and money

Maybe the part that you were all looking for: money and swag! Below is the structure for the packet sent whenever you get a new item in your inventory:

[63 70] [LL LL] [WW WW ...] [QQ QQ QQ QQ]

With L being the size of the item’s name, W the item’s name and Q the quantity. So in order to loot 100 * Bear Skins, I would need to send the following packet:

def parse(p_out):
 
    p_in = ""

    def more(item, qt):
        return "cp" + struct.pack("H", len(item)) + item + struct.pack("I", qt)

    def chat(msg):
        return "#*" + struct.pack("H", len(msg)) + msg

    if chat("GetSkin") in p_out:
        p_in += more("BearSkin", 100)

    return (p_in, p_out)

Here again, once in the game, we send the message “GetSkin” and Jackpot! We received 100 bear skins. Now let’s trade them and get money. Justin Tolerable in Town allows you to trade skins for money. Unfortunately, this doesn’t seem to work.

How about getting 100.000 * “Coin”. Here again, it works, but we can’t purchase anything with that money. Same with weapons, we can receive any weapon, but when using them, the enemy doesn’t loose a single health point.

We know from our previous experiment that the inventory is stored on the server side. Now, we know that before any transaction or usage, the server verifies that we indeed received that item (money, loot, ammo or weapon) in a legitimate way.

 Bonus – Write-up Unbearable revenge

In order to activate the quest, we need to talk to Justin Tolerable in Town (inside the library). Once activated, let’s go to the Unbearable Woods and find the bear chest. The fastest way would be to just spawn to the chest location: (-7894.0, 64482.0 + 500, 2663.0):

def parse(p_out):
    
    p_in = ""

    def newspawn(opcode, x, y, z):
        return opcode + struct.pack("=HfffHHH", 0, x, y, z, 0, 0, 0)
    if len(p_out) == 22 and p_out[2:4] == "\x00\x00" and p_out[16:] == "\x00\x00\x00\x00\x00\x00":
        
        x = -7894.0
        y = 64482.0 + 500
        z =   2663.0
        
        p_out = newspawn(p_out[:2], x, y, z)

    return (p_in, p_out)

Here we added an offset of 500 to the y axis so we don’t spawn at the exact location of the chest.

Once in front of the chest, it ask us to “Press E to try to unlock (5 minutes)”. Once activated, all the bears from the area comes to us. And that’s a lot of bear. We first have a wave of normal bears, then after a minute or two, a second wave of angry bears appears. There are too many of them. Even with infinite mana, we wouldn’t be able to kill them all. Furthermore, we have to stay within the circle around the chest (around 10 meters diameter).

The chest is located under a tree, which is quite convenient. Let’s try to spawn into that tree, away from the bears’ attacks. Since we will be too far from the chest, we will also have to “pick up” the BearChest element to activate it:

def parse(p_out):
    
    p_in = ""

    def newspawn(opcode, x, y, z):
        return opcode + struct.pack("=HfffHHH", 0, x, y, z, 0, 0, 0)
    
    def loc(x, y, z):
        return "mv" + struct.pack("fffHHHBB", x, y, z, 0, 0, 0, 0, 0)
    
    def getme(item):
        return "ee" + struct.pack("I", item)
    if len(p_out) == 22 and p_out[2:4] == "\x00\x00" and p_out[16:] == "\x00\x00\x00\x00\x00\x00":
        
        # Bear Chest coordinates
        x = -7894.0
        y = 64482.0
        z =  2663.0
        
        p_out = newspawn(p_out[:2], x, y, z + 3000)

    if chat("UnlockChest") in p_out:
        p_out += loc(x, y, z)
        p_out += getme(3) # 3 = BearChest element ID

    return (p_in, p_out)

Everything works like a charm. We can look at all the bear stacking around the chest from my tree. One minute left, almost done! Suddenly, the angry bears stand up and start shooting at us with AK47. It takes less than a second to kill us. We need to find another solution.

After poking around with the different location position, I thought “what about spawning under chest?”. The problem is that under the chest is the void, so we will fall in the water. What we can do to prevent that is to create a chest right under the character to prevent us to fall. And it works, I’m close enough activate the chest and the bears cannot it me. However, for an unknown reason, at some random point, the game decide that we are too far and terminate the timer. When using coordinates more and more close to the chest, we will eventually spawn at a magic position, where we don’t even need the chest underneath us. Our head is stuck above the ground with the body buried and somehow, the character doesn’t fall in the void.

def parse(p_out):
    
    p_in = ""

    def newspawn(opcode, x, y, z):
        return opcode + struct.pack("=HfffHHH", 0, x, y, z, 0, 0, 0)
    if len(p_out) == 22 and p_out[2:4] == "\x00\x00" and p_out[16:] == "\x00\x00\x00\x00\x00\x00":
        
        # Bear Chest coordinates
        x = -7894.0
        y = 64482.0
        z =  2663.0
        
        p_out = newspawn(p_out[:2], x, y, z - 244)

    return (p_in, p_out)

From there, we can easily activate the chest, and wait 5 minutes with the different wave of bears, angry bears, AK47 equipped bears. None of them can hit us. Once the time over, we can just pick up the loot, including the flag, directly from under chest. Then we need to jump a little to finally fall into the sea and walk to the beach.

Flag:  “They couldnt bear the sight of you”


Blog series: IntroductionReverse Engineering Network ProtocolPwn Adventure 3 Network ProtocolBuilding a Wireshark ParserAsynchronous Proxy in PythonIntercepting Packets – Reverse Engineering BinaryPatching BinaryHooking shared library

Von |2017-07-04T12:00:03+02:004. Juli, 2017 um 12:00 Uhr|KEYIDENTITY|Noch keine Kommentare

Über den Autor:

Manuela Kohlhas