Sunday, 20 May 2018

Solving MalwareBytes Crackme 2

This is a web version of the writeup I submitted for the second Malwarebytes crackme. It requires a few techniques including python decompilation, x86 reverse engineering and some minor cryptanalysis - which was pretty cool as you could solve it in a few different ways.

Stage 1

I usually start my analysis by looking at the strings in a binary as it gives a good 'feel' for the nature of the target, as well as hinting to any open source code that the author used which can make reversing easier. Running it through FLOSS (FireEye's version of Strings-on-steroids) gives us lots of helpful looking Python-related error messages such as "Error detected starting Python VM” - suggesting that at this stage we are dealing with a python program packed as windows executable.

These are usually easy to unpack if we can find out which packer was used -the top Google results confirm that the error message above is from 'PyInstaller'. It's documentation describes PyInstaller binaries as a bootloader with an archive appended to the end (start bytes PYZ), which you can unpack with the PyInstaller archive viewer. I found a tool called 'pyinstxtractor' which did the work for me, and it produced the following output:



The 'another' file immediately jumps out at the most promising location for the target code, with its odd name and exe manifest and sure enough a string search of it shows all the strings about secrets and flags that we see when the program runs. But what to do with it? I assumed it was python bytecode but running the 'uncompyle6' decompiler on it just gave me an error about it not being a .py or .pyc file.

Lets look at the magic bytes at the start of the file (in Binary Ninja):




63 00 00 00? What? I couldn't find it on any list of file signatures. Looking at more search results for “Decompiling Pyinstaller Binaries” and what do we see 5 results down? Hasherezades blog... solving a variation of this exact problem. I'm not sure this way of solving it is really in the spirit of the challenge, but that's what knowledge sharing is for.

Anyway, now I know that the file is just a .pyc file with the signature irritatingly stripped off – it's time to fix that (typing “03 f3 0d 0a 00 00 00 00” in Binary Ninjas hex mode) and start decompiling.

With the python code in our hands we can now tackle the credential check.

Check_login() just checks that we use the name 'hackerman' and check_password() succeeds if the md5 hash of our password is '42f749ade7f9e195bf475f37a44cafcb'. According to the md5 lookup page at https://hashkiller.co.uk – that password is 'Password123'. Hmm.

The final gate at this stage is a PIN check. The PIN is used as a seed for the random number generator, then create_url_key() appends 32 “random” decimal digits into a key. This PIN-derived key is compared in a similar way to the password, with its md5 hash having to match 'fb4b322c518e9f6a52af906e32aee955'.

The problem with PIN's (or PIN numbers if you like redundant acronyms) is that they only work if you can't brute force them – but since we have the hash and the hash-checking algorithm we can do:

    for i in range(10000):
        key = get_url_key(int(i))
        if check_key(key):
            print "Correct key is: %d"%i
            break

And instantly get a result of 9667. Note to self: Do PIN searches in reverse.

Stage 2

Using the credentials above will drop us down to decode_and_fetch_url(), which uses the key to decrypt an embedded string to: “https://i.imgur.com/dTHXed7.png“.

I'm always slightly worried about what I'm going to see when opening images embedded in suspicious binaries but this turned out to be a corrupt-looking png. 

I assumed it was going to be a steganography challenge, but looking further at the Python code we see it being fed to prepare_stage(), where the pixel values are converted to a PE file by get_encoded_data(), moved into executable memory with the help of VirtualAlloc and executed from offset +2.

I had some trouble running it from a decompiled python script as it kept crashing but stepping through it with a debugger seemed to fix it, oddly. I also dumped the payload to a file to run it without having to wait for an imgur download each time.

We can debug this payload by waiting until the “Are you ready?” dialog pops up and setting a breakpoint at VirtualAlloc() to find the buffer it gets placed in, but lets use static analysis as far as possible.
Off to our disassembler – since this isn't executed as by the normal PE loader mechanism we have to switch BinaryNinja to 'Raw View' so it doesn't try to impose PE section mappings on our disassembly.


(virtual_buf + 2) :



Above: The first two instructions use the call-pop technique to determine the memory location of the code, then adds 0x6d9 to it (destination +0x6e0) and then “returns” there.

The next section checks for valid PE/MZ signature bytes and retrieves the process environment block. It grabs the InMemoryOrderModuleList from the PEB and iterates through the module names with a simple hashing/checksum algorithm until it finds the entry for kernel32.dll.



It then uses the same technique to iterate through the symbols to find LoadLibraryA, GetProcAddress and VirtualAlloc (if you Google the constants you will find the loader was written by a Petya fan). The payload now has what it needs to load its plaintext list of imports at 0x302da.

Loading done, it creates a thread which calls EnumWindows every second. I'll switch back into PE mode and examine the callback.


After sending a WM_GETTEXT command to read the title text of each window, it searches for 'Notepad' and 'secret_console'. If a window with a suitable title exists it will enter a 'waiting for the command' loop which uses EnumChildWindows to look for text in its widgets in the same way – this time for 'dump_the_key'.

Once a window satisfying all these conditions has been found, it decrypts the blob of data at payload offset 0x30A00 . I'm not sure if the algorithm is a well known one but it's quite simple – though understanding it isn't required to get the flag I'll cover how it works:

It uses the RC4 key expansion algorithm to turn “dump_the_key” into an S-Box, however using the RC4 keystream generator only recovers the first byte.

The following python will decrypt the embedded blob:


#standard RC4 key expansion - https://gist.github.com/cdleary/188393
def expandkey(key):

    keybytes = [ord(x) for x in key]
    k = [x for x in range(256)]
    j = 0
    for i in range(256):
        j = (j +k[i]+keybytes[i % len(keybytes)]) % 256
        k[i], k[j] = k[j], k[i]
    return k

def decrypt(ciphertext, key):
    
    plainText = ""
    keyIndex = previous_keyIndex = tempVar = 0
    SBOX = expandkey(key)
    
    for ctIdx in range(len(ciphertext)):
        
        keyByte1 = SBOX[(keyIndex+1) & 0xff]
        keyIndex = (keyByte1 + tempVar) & 0xff
        keyByte2 = SBOX[keyIndex]

        keyStreamKeyIndex = (keyByte2+keyByte1)&0xff
        
        plainText += chr(SBOX[keyStreamKeyIndex] ^ ciphertext[ctIdx])
        
        tempVar = previous_keyIndex + 1
        previous_keyIndex = keyIndex
        
    return plainText

cipherTextBytesList = [0x4f, 0x08, …, 0x75, 0x98]
key = “dump_the_key”
plaintext = decrypt(cipherTextBytesList, key)



After decryption, Actxprxy.dll is loaded. This seemed odd at first glance - an Internet explorer activeX controls library? (So much badness) - but the first page is actually marked READ_WRITE and used as a covert channel to pass our 'plaintext' back to the python script, where it is Base64Decoded and decompressed. The python script can be modified to print it out for us to play with if dumping and decrypting it is too much work (and it is).

Stage 3

Now the script tells us that we are awesome - but not awesome enough to get the flag. We need to guess a colour first. The three RGB colour bytes are the key - each key byte taking turns being XOR'ed with a ciphertext byte.

Some observations about the ciphertext
  • Exec() is called on it, so it should be valid python
  • It might have “flag{“ in it
  • It looks quite interesting!


At the top we see what looks like a comma separated Python tuple and at the bottom we can make out a garbled but suspiciously flag-like “æláç{“ --------- “}” structure. In fact the printable ASCII bytes are separated in intervals of three, suggesting that one of the three key bytes is zero. As it has 'l' and '{' in the right place, i'm certain that 'æláç{' is 'flag{'.

This string is at offset 1065 in the ciphertext so the correct key offset must be (1065 % 3) -> 0. We can use a known-plaintext attack – xoring the ciphertext with the plaintext in a python console to recover the key:
>>> [ord(“æláç{“[i]) ^ ord(“flag{“[i]) for i in range(3)]

[128, 0, 128]

Using this key to decipher the rest of the blob gives us some python code:

def print_flag():

flag_hex = ( 0x73, 0x75, 0x72, 0x64, 0x65, 0x61, 0x68, 0x50, 0x20, 0x2D, 0x20, 0x22,0x2E, 0x6E, 
0x65, 0x64, 0x64, 0x69, 0x68, 0x20, 0x79, 0x6C, 0x6C, 0x75, 0x66, 0x65, 0x72, 0x61, 0x63, 0x20, 
0x6E, 0x65, 0x65, 0x62, 0x20, 0x73, 0x61, 0x68, 0x20, 0x74, 0x61, 0x68, 0x77, 0x20, 0x73, 0x65, 
0x76, 0x69, 0x65, 0x63, 0x72, 0x65, 0x70, 0x20, 0x77, 0x65, 0x66, 0x20, 0x61, 0x20, 0x66, 0x6F, 
0x20, 0x65, 0x63, 0x6E, 0x65, 0x67, 0x69, 0x6C, 0x6C, 0x65, 0x74, 0x6E, 0x69, 0x20, 0x65, 0x68, 
0x74, 0x20, 0x3B, 0x79, 0x6E, 0x61, 0x6D, 0x20, 0x73, 0x65, 0x76, 0x69, 0x65, 0x63, 0x65, 0x64, 
0x20, 0x65, 0x63, 0x6E, 0x61, 0x72, 0x61, 0x65, 0x70, 0x70, 0x61, 0x20, 0x74, 0x73, 0x72, 0x69, 
0x66, 0x20, 0x65, 0x68, 0x74, 0x20, 0x3B, 0x6D, 0x65, 0x65, 0x73, 0x20, 0x79, 0x65, 0x68, 0x74, 
0x20, 0x74, 0x61, 0x68, 0x77, 0x20, 0x73, 0x79, 0x61, 0x77, 0x6C, 0x61, 0x20, 0x74, 0x6F, 0x6E,
 0x20,0x65, 0x72, 0x61, 0x20, 0x73, 0x67, 0x6E, 0x69, 0x68, 0x54, 0x22 )

flag_str = ""

for i in flag_hex:

flag_str = chr(i) + flag_str

init()

print(Style.BRIGHT + Back.MAGENTA) + "flag{" + flag_str + "}" + (Style.RESET_ALL)

print_flag()

Running it yields our flag -

flag{"Things are not always what they seem; the first appearance deceives many; the intelligence of a few perceives what has been carefully hidden." - Phaedrus}






Monday, 14 May 2018

Reverse engineering the Path of Exile game protocol - Part 3: Introducing exileSniffer



Up to this point I haven't talked much about my workflow for extracting and examining the protocol data - so here a tool you can use to do it. This will walk through what it does and how it works.
Requirements: Windows, winpcap  (If Wireshark works then this should work too)


Starting packet capture

Initial screen with no Path of Exile clients running

When exileSniffer starts it will start scanning the process list for Path of Exile clients and sniff on the default interface for logon packets. It doesn't matter which order the processes are started in, but it's essential that exileSniffer is launched and listening before login starts.


A game client is detected - exileSniffer is scanning its memory for keys


The player has logged in - we are waiting for keys and testing them against the logon traffic



We found the correct salsa key and have started decrypting the logon stream.

The above shows packet 0x4 from the login server highlighted - so far I've ignored it and it has no purpose disaplayed. If we look at the hex pane we can see a bunch of zeroes and a hex 12 at the end, which doesn't really tell us much. It comes after the authentication data so maybe it's a "Logon success" message. Try a bad password and report back if you care about the login process I guess.



Analysis pane showing a League List packet

For most packets I've encountered I've attempted to give them a meaningful description in the packets list, but for some with lots of interesting data I've added some analysis in the Analysis pane.

There are a few workflow features
  • Filters and Filter Lists to prune out noisy packets and highlight interesting ones
  • A hash search function in 'Utilities' so we can test if any data we find is a known hash
  • Logging to disk - both raw and filtered streams
  • A JSON pipe, which I'll talk about next


Using exileSniffer decryption with other tools


The deserialisation and decoding is done in entirely seperate threads to the analysis and display of the data. exileSniffer opens a named pipe so that external tools can subscribe to the same decoded data as the UI receives, which allows us to solve some problems in new ways.

Here are some example Python 3 scripts.

Tool #1: Monitoring player health

import json
import codecs

def connectToPipe():
    pipename = "\\\\.\\pipe\\ExilePipe"
    pipe = codecs.open(pipename, 'rb', encoding='utf-16-le')
    return pipe

if __name__ == "__main__":

    currentPlayerID = 0
    
    pipe = connectToPipe()
    while True:
        ln = pipe.readline()
        if len(ln) == 0: continue
        js = json.loads(ln)

        if js['MsgType'] == 'SRV_NOTIFY_PLAYERID':
            currentPlayerID = js['Payload']['ID1']
            continue

        if js['MsgType'] == 'SRV_MOBILE_UPDATE_HMS':
            if js['Payload']['Stat'] == 0: #life
                print(js['Payload']['NewValue'])
            continue
        
    pipe.close()

This should hopefully be straightforward.

  • It looks for the SRV_NOTIFY_PLAYERID packet sent when joining the gameserver to figure out what our characters unique ID is.
  • It looks for SRV_MOBILE_UPDATE_HMS packets sent whenever the health/mana/shield of a mobile changes.
  • If the mobiles ID is our player, and the stat is Life, it prints the new value.

One improvement could be to read the players maximum health from the object creation packet and alert on low percentage.

All the packets I've found so far are listed in messageTypes.json in the application directory, but only the basic ones will be there. I suspect a huge amount of more obscure ones for certain skills, certain monsters, certain maps, crafts, vendor actions, league specific stuff, etc are all waiting to be found. You could also just enumerate them by injecting packets with the unknown ID's into the client, but if there is anything I don't need in life it's more of these packets to work through.

Tool #2: Monitoring party members

Here is a question someone asked on the PathOfExileDev subreddit.

Title: "Players joining your party not logged?"
Text: "It seems almost everything is in the log file, including changing instances, global chat, whispers, etc, but not members joining your party? Is this somewhere else in another log? Can I somehow keep track of when someone joins a party?"
The only reply:  "nope, the only log is clients.txt"

I feel like their expectations are already quite low if they consider the paltry contents of clients.txt to be 'almost everything' and seeing party member joins/quites doesn't seem like a big ask.

Looking into the party packets, it seems that the client doesn't get told 'X has joined, Y has left' but gets the whole party list every time the makeup of the party changes.

This Python script will track changes to the party and announce joiners and leavers.

import json
import codecs

def connectToPipe():
    pipename = "\\\\.\\pipe\\ExilePipe"
    pipe = codecs.open(pipename, 'rb', encoding='utf-16-le')
    return pipe

if __name__ == "__main__":
    inited = False
    oldNames = []
    
    pipe = connectToPipe()
    while True:
        ln = pipe.readline()
        if len(ln) == 0: continue
        js = json.loads(ln)

        if js['MsgType'] == 'SRV_PARTY_DETAILS':
            membersDict = js['Payload']['MemberList']
            updatedNames = [x['Name'] for x in membersDict if x['StatusText'] != 'Pending']
            
            if not inited:
                print("Party created with %d members"%(len(updatedNames)))
                inited = True
                oldNames = updatedNames
                continue

            newNames = list(set(updatedNames).difference(set(oldNames)))
            for x in newNames:
                print("%s joined the party"%x)
                
            leftNames = list(set(oldNames).difference(set(updatedNames)))
            for x in leftNames:
                print("%s left the party"%x)

            oldNames = updatedNames

        
    pipe.close()


Does this give an unfair advantage?

The first thing to consider is information: I was curious to see what the client was being sent that the player is not supposed to know about.

In general the server is quite good about only sending what the client needs to know - you can't see other players or monsters on the map unless they are almost on your screen. While you can see all of a players stats, devotions, XP, quests, etc - you can't see what items they are carrying or using - other than what is needed to render it.

I haven't played enough recently to remember if invisible things are a problem - but I suspect the 'is_hidden_monster' stat that some objects have could be made to lose it's effectiveness.

One thing I've noticed years ago when scanning game memory (and have found the cause for during this project) that probably could be exploited is the preload lists sent when you join a map. If a rogue exile, NPC grandmaster or strongbox exists: the server will tell the client as soon as it loads so the graphics can be preloaded. This allows you to load maps over and over again to look for something juicy without having to actually spend the time to explore them. My suggestion to the devs is to only send these to the client when the player is nearby, like when a new player joins the map and their totems are preloaded. The performance hit can't be that bad compared to people mass-loading new maps.

The second possible advantage is to be able to 'do' something that normal players can't do.

I know it's not going to be seen as much of a mitigation but it's worth highlighting that exileSniffer doesn't interact with the gameserver in anyway, the sole interaction with the client is reading key data while it is talking to the login server. This means players will still have to use other more intrusive programs to react to exileSniffer data. A google search for 'path of exile bots' shows that those programs are already out there and I'm not releasing anything that bot developers don't already know (and are using for a fee).

Further work

So I think the ability to write little scripts like that to interact with the games traffic is the coolest outcome from this project, but to write the above party script I had to go back and reverse the handling of the party info packet. This highlights the fact that huge swathes of the protocol are still left to be understood.

In particular:
  • Items. This is the big one as it's what PoE is all about but with the classes, mods, corruptions, sockets, links and up to 6 items within items also needing decoding  - it's a lot of work. It does at least lookup the hash to the GGPK item ID.
  • Objects. I've reversed most of the player creation packet so we can see handy things like all of the stats a player has and what microtransaction effects they are using, but monsters, npcs, pets, etc also need to be done.
  • The ID triad. An object identifier is (DWORD, DWORD, WORD) but I've not found any particular use for the second and third items, other than occasionally referring to the object that caused something to happen to the target object.
It would also probably be possible to remove the requirement to start sniffing before logon by brute forcing the Salsa iteration value.

Sunday, 13 May 2018

Reverse engineering the Path of Exile game protocol - Part 2: Decoding the protocol

This is effectively a puzzle, and it's really quite entertaining.


Getting Started

After the loginserver hands us off to the gameserver we start getting a really noisy decrypted stream -  especially if you are in town with lots of people moving around - so it helps to move our character to an area of wilderness where not much is happening.

The packets we see are almost all prefixed with a 00 or a 01 and we see the same sets of 2 bytes quite regularly so (like with the login packets) we assume the first two bytes are packet types.

There are two ways of working out what a particular packet ID does:

Behavioural analysis - Empty Packets

The only time you see a certain packet ID sent to the server is when you perform a certain action.

A very trivial example is when we release the mouse button and see packet
00DC
Now we can catch 00DC packets in our decoder, mark it as uninteresting and filter it out..

Behavioural analysis - Constant Length Packets

Next step up is the packet we see when using potions (stored in one of 5 belt slots) of the format 0037XXXXXXXX. It is followed by 4 bytes (XX) at the end which clearly correspond to the potion slot we activated.
[0037]00000004 -> Potion slot 5
[0037]00000001 -> Potion slot 2
We add a check that the number always falls in the range of our available slots, then filter it out.

This technique can be extended to gain insight into the game mechanics, too.
Lets look at packet ID 0xDA, which appears when we drag the mouse around with the button held:
00DA000001E10000010B
00DA000001E60000010C
00DA000001E80000010C
This is clearly the two 4 byte coordinates of the mouse. Now we have a method of determining locations on a game map, so if we see a monster walking around and hover our mouse over them we can search for the coordinates in other packets to find their location bytes.

Additionally, since 00DA is the mouse being dragged and 00DC is the mouse being released - it doesn't take a lot of imagination to assume that other packets close to that range might also describe mouse activity.

Behavioural analysis - Variable Length Packets

This is trickier. PoE's Client/server comms are generally not self describing at all, so if you don't understand a logical packet then you have no idea where it ends and the next one begins. Starting is the hard part but after identifying more and more packet types you can usually pick them apart in a raw blob.

Does a certain packet contain variable length fields? Strings are an obvious one (always preceded by their length) but when a change in one byte results in a drastic change in the whole structure of the following packet then it's probably time to start reversing the binary.


Packet injection

Dynamic analysis is more fun when it's interactive, so to test a theory about what a packet field might be (or to just fiddle with it and see what happens) we want to be able to inject packets into the client at run-time and see what happens. I didn't want to do this when talking to the real game servers because -
  • The client trusts the server. Bad input often makes it crash/hang in a variety of un-managed ways so if the server is even remotely as trusting of the client then it's might crash instance servers that people are playing on.
  • Testing is noisy, especially when the data is malformed. The accounts are free but getting IP-banned would be a pain.
  • On live servers there can be a lot of background activity which makes results hard to replicate.

The solution was to create a dummy server - just functional enough for a client (with modified crypt data) to log into as many times as we want with the same state each time. We can read everything the client sends and see what happens when we send it stuff.

"Do anything nice at the weekend Nia?"
Me inside: "I spent it reversing a video game to develop tortoise-choreography capability"
Me actually: "Nothing much, you?"


Binary analysis

There are hundreds of packet types and so many types of object, item, skill, monster, effect, etc that even if we could work each one out behaviourally - it's silly not to try and find handlers in the code and/or data files to help us. Ideally with so much data we want to automate the process too.

The joy of this process is using analysis of the data stream in conjunction with the binary and slowly building up a thorough understanding of both.

I couldn't see any obvious location for parsing the packet ID in binary ninja, so I fired up x64dbg to break in a location of code that I knew was triggered by incoming network data.

For this I plonked my character on a beach next to a monster called a Sand Spitter and let it... spit sand at me. Searching for relevant results in the module brought up a few dozen results - at least one of which was hit as a breakpoint:

Fortunately x64Dbg does handle unicode strings

After that it's simply a case of walking back through the call-stack to find the packet handling function that handles all incoming data.

From deserialisation to processing - a lazy shortcut

Reversing the deserialisation correctly is essential to being able to separate distinct packets out of large multipacket blobs and luckily it's usually quite easy to reconstruct from the static analysis.

Let's look at a small packet sent by the server when a character joins a map. Here is how packet 0xA4 is deserialised:

BinaryNinja graph view of the 0xA4 deserialiser
There are three calls to getR8BytesFromPacket, which fetch 2, 1 and 1 bytes from the decrypted buffer into the memory pointed to by RDX. Then it jumps to the end of the big deserialiser switch statement - job done.

I started off the protocol analysis by sticking to the deserialisers but eventually we have the less straightforward task of finding out how that data is used. Setting a hardware access breakpoint on the deserialised data takes us to...

BinaryNinja graph view of the function that processes paced 0xA4 data

Ugh. It's a hefty function - there's lots of work to do here and contextually the message doesn't seem important enough to be worth it at the moment - although the function is called from a few other locations so it may need looking at in the future. To get a rough idea of what the packet does we can lean on one of my favourite features of x64Dbg - display of strings from all the registers and memory locations referenced in the disassembly.



This way we don't have to figure out where interesting strings are, we can just step through the code until something meaningful pops out at us. By breaking at the highlighted location and injecting variations of the packet we can see that:
  • The 2 byte value is the channel number.
  • The 1 byte value is the channel type (Global, Trade, etc)
  • The 1 byte after that is the language (English, German, etc)
Obviously this is horrifically unsound - a value of 0xb33f might activate the "delete System32" function and we would never know about it, but in the circumstances i'm happy to move on to one of the hundreds of more interesting packets.


Digging Deeper - Path of Exile Internals

Finding the interesting stuff requires understanding of how the server encodes the endless combinations of objects and how the client interprets them. Let's explore some of its schemes by unravelling the SRV_ADD_OBJECT message (currently 0x135), sent by the server whenever an object is added to the map.

SRV_ADD_OBJECT starts (like many other messages) with a 10 byte ID field split 4:4:2.

[00 00 03 02] [FF FF FF FF] [00 00]
   ID 0x302        -1          0

The first 4 bytes are the objects reference, this will be sent by any messages that refer to the object.

Next comes a very important 4 byte field.

D7 AA 90 65

By stepping through the processing of this random looking data we can see that it's the Murmur2 non-cryptographic hash of the string "Metadata/Characters/Str/Str" - a reference to the Marauder (ie: Strength) player character. To understand this string we have to understand the content file.

The content file


Most of the clients data is stored in an 11GB file called content.ggpk in the game directory. Fortunately for me this has already been reverse engineered with the publication of the essential PyPoE set of tools.

The Marauder entry in Characters.dat, seen using the PyPoE GGPK viewer
Much of the challenge of reversing the PoE protocol is understanding where and how the data in packets references fields in this GGPK file, and how certain fields in the file change the processing of packets. It's a good way of forcing yourself to learn how C++ data structure functions appear in assembly language.


Stats


POE uses a variable byte width encoding scheme for some of its data (such as character stats) where the first 4 bits determine how many bytes of data follow. I should probably recognise it.

0x0078 =>   0x78
0x08f0  =>   0xF0
0x9730 =>   0x1730
0xf9178d7d11 => 0x178d7d11

It wasn't too unpleasant to reconstruct from the binary (see packet_processor::customSizeByteGet() and packet_processor::customSizeByteGet_signed() in the upcoming source code) but it would be nice to know what it was.

Reverse engineering is primarily a time problem


At this stage I've run out of quick wins. The different packet ID's, different data types, items, mobile objects, different network modes (lockstep or predictive) all have variations which need custom deserialisers written and custom decoders. It's certainly a much bigger project than anticipated and I kinda want my time back to work on other things - but each packet is another thing for the todo list.

In the final post I'll talk about exileSniffer, a GUI for decrypting and dissecting Path of Exile packets which can also make its data available to build custom tools.

Reverse engineering the Path of Exile game protocol - Part 1: Obtaining the plaintext

Part 2: Decoding the protocol

Part 3: Introducing exileSniffer


As I've just bought a shiny new Binary Ninja licence I thought I'd document trying it out on a target I have a passing familiarity with and how the experience differs from using IDA.

Update: Binary Ninja has been updated a lot since this was written, adding some functionality that I wanted (such as unicode string support!).

The target


Please excuse the Virtual Machine graphics
Path of Exile (POE) is a Diablo-like looting/trading/number increasing game which, aside from being free to play, also has a rather permissive EULA regarding reverse engineering clauses. Modifying the client is banned, as is scraping data from the website, but reversing and extracting data from the client isn't mentioned - so here we go.

I haven't really played the game seriously (eg: never levelled a skill gem to 20) but back in the days before builtin item filtering I wanted a way to figure out what items dropped and ping an alert, so you didn't miss valuable things in the mess of loot during fast placed gameplay. Shortly after starting work on it the developers added some really rich item filtering functionality, so after a bit of work decoding the protocol I shelved the project. A couple of years on and I bought a shiny new Binary Ninja licence and wanted some experience reversing a big production-quality C++ application with it - POE seemed like a good choice.


The game is funded through microtransactions like these, which are everything I've ever wanted in life.

POE offers a standalone client and one bundled with Steam - we can use the standalone one to avoid having to worry about any Valve-Anti-Cheat shenanigans.

Initial traffic analysis

While playing the game, Sysinternals TCPView tells us that PathOfExile_x64.exe has a TCP connection established to a remote server on port 6112.

Capturing a bit of traffic in Wireshark, we can see lots of packets with 2 byte payloads coming to and from this port and a few bigger payloads too but nothing with any readable strings in. We can assume the stream is encrypted, because that's the fashion these days, but as a further check we can do a quick entropy test.

Viewing the TCP stream in Wireshark

Exporting the packets in this stream from Wireshark to a pcapng file and use these runes (in Linux) to extract the payload bytes:
tshark -T fields -e data.data -r poecapture.pcapng | tr -d ':\n' | xxd -r -p > payloads.bin

This uses tshark to extract the data bytes as a hexdump, pipes them to tr which removes the formatting from the hexdump, pipes those to xxd to reverse the hex into a continuous blob of binary data and finally writes it to payloads.bin.

We can then use the handy ent tool (from an 'apt-get install ent' near you) to get a rough idea of how random the data inside is.
Entropy = 7.991133 bits per byte.
Optimum compression would reduce the size of this 19761 byte file by 0 percent.
Chi square distribution for 19761 samples is 242.41, and randomly would exceed this value 70.45 percent of the times.
Arithmetic mean value of data bytes is 126.4821 (127.5 = random).
Monte Carlo value for Pi is 3.205587610 (error 2.04 percent).
Serial correlation coefficient is -0.004831 (totally uncorrelated = 0.0).
The stream is indistinguishable from random noise - encrypted, rather than encoded, so we are going to have to go back to the start to see how we got here.


Logging in - the livestream



Fig 1: The PoE client login screen

Open up wireshark and press login with valid creds to see an exchange like this:

Fig 2: Example login packet capture


The client looks up the nearest login server, performs the 3 way TCP handshake and starts the stream with a 134 byte payload

Message 1. Client to Server
Fig 3: 2 Separately captured 134 byte initial payloads highlighted by Wireshark, with constants annotated
If we compare two of these payloads side by side, 6 constant bytes immediately jump out, with the rest being apparently unpredictable. If we look at the next payload in the sequence it has the same 4 byte prefix but without null bytes at the end, suggesting that they might be padding.

The 0x80 part of the constant bytes is particularly interesting because it's 128 in decimal. 128 + 6 constant bytes -> 134 bytes, the size of the payload, so a first guess for the message meaning is something like:
       
0001 : [Message ID or Counter]
0080 : [Length field]
xxxx : [Unknown data of length 0x80]
0000 : [Padding]

Message 2. Server to Client

Fig 3: 2 Separately captured 190 byte "response" payloads
This reply message is longer but has the same 0x80 field. Either the length guess is wrong or something comes after the initial data. Adding 0x80 to the offset of our mystery data (0x39) gives us 0xB9, which is the start of the green box highlighed. Another 2 byte value here - 0x38 (56) is exactly the length of the remaining bytes in the payload so it's a length field too.

Message 3. Client to Server

Fig 4: Payload 3, Client to Server, 147 bytes
Hmm. This isn't helpful - no predictable bytes. 147 bytes is a pretty weird size too, is the stream encrypted already? Payload 4 is 359 bytes of similarly unpredictable data, and the rest of the packets are no more useful.

We can now make the following assumptions/observations about the login sequence

Client --> Server: [128 byte blob A]
Server --> Client: [128 byte blob] + [56 byte blob]
Client --> Server: [147 bytes of ??]
Rest of the stream: [Unintelligible nonsense]

The 128 byte blobs could be challenges with a 56 byte response, but that's unlikely without some other identifying data. They could also be public keys, which makes more sense for opening an authentication exchange.

From here i'm tempted to try using a bad password to see if the stream changes (and possibly tell us something about the Client->Server blob (A) but it's a good habit to keep suspicious interactions with the server to a minimum. Instead, lets use the awesome free DNS spoofing tool ApateDNS to trick the client into interacting with our own server.

ApateDNS responding to all queries with a local IP
Now we write a python script to spoof the login server using the format we have just seen, but with a constant value for the mystery data since we don't know how to generate that yet. I'm not going to publish the script because releasing a framework for creating a private server significantly increases the chances of me getting in trouble for this blog.

When we press login we are greeted with an error message followed by immediate client termination.

The error our bad response caused

Rhoa.jpg (320×240)
A Rhoa. Also bad.

Unsurprisingly generating the data in these initial packets properly is quite important. This error string also gives us to something to look for in the binary to help narrow down where the login code is.

Another interesting thing to try is varying the metadata that the server responds with. Changing the first 2 bytes to 0x41 (65) results in this illuminating error message:
Unable To Deserialise Packet

Now we have confirmed that the first byte is a packet ID and we have a string to look for in the binary to find the packet deserialising code, awesome! Lets prepare the ground for some disassembly.


Initial Binary Recon- Libraries


First thing to understand is what external code the game uses. If it incorporates libraries with a documented API or open source code then that can save us a huge amount of work.

Sadly Binja doesn't have an import/export feature at the time of writing, but there are plenty of good free PE file analysis tools that do the job. I used pestudio, but it didn't find any imports other than standard windows libraries and the DLLs in the game directory. These are:
  • fmod.dll and fmodstudio.dll (v0.1.9.4): A game sound engine by Firelight Studios with an accessible API
  • d3dcomp_47.dll and d3dcompiler_47.dll: Signed Microsoft Direct3D shader compiler libraries which are probably well documented. If we are disassembling graphics routines then we are wasting our time.
  • bink2w32.dll: Video processing tool called bink from RAD game tools. Again, don't care about this.
Either the rest of the 25MB client is original or we have to go fishing for statically linked code, which is a bit trickier. The first way of dealing with this is FLIRT signatures, which Binary Ninja supports through a plugin. These only support a very limited number of libraries and would require restarting the program which means losing hours of single-threaded linear sweep analysis because the personal addition doesn't cache it.

Another route is to examine the strings. Binary ninja is really weak tool for this ($600 for the commercial version and no Unicode support? Come on.) because Path of Exile uses a Unicode all over the place. Since we already have pestudio open (Sysinternals strings is also great) we can export its strings list into Notepad++ to search.

Useful search strings include 'lib', 'copyright', 'crypt'. '::', 'error' (for descriptive error messages), and the names of common libraries like Boost and Rapidjson.

Highlights of the results include:
  • a handful of Boost references - No useful X-refs in the code but it's open source and often used all over a target binary so worth looking out for because we don't want to waste time reversing open source code.
  • "CLIENT libcurl 7.47.0" - An exact version of an open source networking library, awesome. This version has 27 disclosed vulnerabilities though, which is somewhat less good. It would be interesting to see if there is any unauthenticated attack surface - maybe in the patchserver or message of the day handling.
  • Lots of references to CryptoPP, which is the open source crypto++ library and very promising for our initial goal of 'decrypt stuff'.
  • "inflate 1.2.8"/"deflate 1.2.8" - zlib. More horrible arithmetic-heavy assembly that we can now understand from the source code or avoid entirely.
Another interesting result is found when sorting strings by length. There are multiple long strings of the form: 

"0x66c7ea5ba0aef7301b3621bee45a1e7d80f1aaa066ebcd917b05b6356a63bab8f7e220d44619f45d65535e651f284ceac5420ad9a90bd288cda759c49f8a395cf68b411861e54c179910132c7c44acc40e0deecc211d7bce5652ea[continued]"

If you find huge numbers built into a binary: it's often crypt related, and Crypto++ uses numbers in that format. 

Navigating the disassembly


With the potential access to the source code of some important chunks of the binary, it's time to start clicking graphs to get a feel for the target.

Loading PathOfExile_x64.exe into Binary Ninja and waiting 11 minutes for the standard single threaded analysis to finish yields less than 7000 identified functions. As IDA finds around 55,000 I would probably have given up at this point if that was the best it could do. Fortunately there is Linear Sweep option with a big (BETA) label next to it, which displayed over 50000 functions detected during the analysis but 33550 when counting through the console using len(bv.functions). Either way it's enough to work with.

What we want to find is a send or sendto with 134 bytes of data. Navigating to the Import Address Table section of the disassembly we see ordinals for many of the imports instead of names.


Switching to the imports section of pestudio tells us that sendto is ordinal 20 and and send is 19, so we can rename them and then look for the XRefs to see where the data sending routines are.

The packet deserialiser function


Searching for our "unable to deserialise packet" string gives us a result in a single function

Binary Ninja graph of the code which raises an error if the sequence number is bad

It's got a loop which either runs until its internal function returns 0 or exits out with the above error.

As we can see by the Xref window (a really pleasant quality of life improvement over IDA), this is called from two other known locations. An error string in the first suggests that it won't be executed if the client can't connect to the patch server, so we will ignore that. Lets look at the second candidate packet handler.

Just out of frame above is a loop which calls 'deserialisePacket?'

Whatever it does with the deserialised data is hidden in a C++ vtable, but we will worry about that later.

A more immediate problem is that going backwards is a dead end because the fancy new linear sweep algorithm has found no Xrefs to this function and I know from looking at the IDA results that there are multiple. Normally in this situation with IDA I would either hunt for suspicious non-disassembled blobs near the graph or fall back to dynamic analysis. Since computers were made to run code and I happen to have a computer, lets do that.

Getting the callstack


An easy way to find out how this data is being sent is to use API Monitor, a fantastic tool that hooks DLL functions and logs their arguments, the contents of buffers, return values and a limited call stack too. Here is what the login looks like:

The send() call of the clients 134 byte first payload
The tool shows send happening at offset 0x519380. Typing 'bv' for BinaryView in the Binary Ninja console tells us that the start address of the binary is 0x140000000, so adding the call stack offset and navigating ('G') to it gives us the location of the WinSock send() call used in the login.

Function at the top of the callstack, where send() is called

It has loads of Xrefs, so we follow the next offset in the call stack to find the right one:


Next function down on the callstack
Looking promising now. The call to htons suggests data is being prepared for sending. Before we delve into the other functions lets continue going down the call stack to find out how we got here.


3rd entry in the call stack - the login function
Now we know exactly where we are, shortly after the 134 byte send call is the candidatePacketHandler function that handles the response from our server. 

Putting it together

I don't think there is much value in further documenting the step by step reversing of the key agreement scheme, but I pieced it together by examining this area of code, the rest of the call stack and the CryptoPP source as it appears in the binary. It turns out that those huge hex-number strings from earlier are that the first payload are the p,q and g Diffie Hellman domain parameters for creating static and ephemeral client keys.

The clients ephemeral public key is sent to the server in the first packet and receives the servers ephemeral public key in response. A new addition since I looked at this protocol a while ago is that this key is now accompanied by a 56 byte DSA signature  - failure to verify it causes the 'Rhoa Exception #3' seen above.

These client keys, server static public key (embedded in the binary), and the server ephemeral public key (from the network) are used to agree a shared secret value, which is converted into a truncated 64 byte SHA-512 hash (or shared digest).

This hash is split into a 32 byte Salsa stream key and two 8 byte IV's, which both parties use to encrypt the connection.

What now?


Unfortunately for us (but good for security), the magic of public key crypto means we can't produce a matching key from what we know and what we can intercept. Instead we have to engage in some digital thievery by stealing the key data we need for stream decryption from the game clients memory.

The usual way to do this is to figure out the chain of pointers and memory offsets to work out where the key resides so we can extract it, but this is a huge pain in the neck because it breaks almost every time the developer updates the client (which happens a lot).

There is a way round that however: Salsa uses a special constant that is hardcoded into the Salsa20 key structure, so we can easily find all the Salsa keys in memory simply by scanning for this constant.

Crypto++ disassembly from the game client showing the Salsa20 Key structure being filled out with 32 bytes from the shared digest and the "expand-32byte k" constant.

Building a plaintext sniffer

Now we know how to find the key, here is what we need our to do once the player has logged in (and the key exists in memory):
  • Sniff the login and game server connections
  • Scan memory for the Salsa key constant
  • Extract the key and IV
  • Decrypt the intercepted login data and pass the plaintext to the decoder
The implementation isn't particularly interesting to read or write about - the source code is coming, but at a high level we use
  • The windows API with VirtualQueryEx and ReadProcessMemory to get the keydata
  • libtins to read packets via WinPCAP and provide a convenient ciphertext stream for us to decrypt
  • Crypto++ to do the decryption
Next up is the fun part: Decoding the protocol stream!

Nb: I should point out that the Binary Ninja disassembly has improved quite a lot in the months since this was first drafted... but still no unicode. The binary modification is an absolute joy but we won't talk about that here.