Skip to content

Instantly share code, notes, and snippets.

@thoroc
Created April 8, 2015 03:30
Show Gist options
  • Star 9 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save thoroc/174aa634b44e2fc370f1 to your computer and use it in GitHub Desktop.
Save thoroc/174aa634b44e2fc370f1 to your computer and use it in GitHub Desktop.
How I hacked into the Warface in-game protocol

How I hacked into the Warface in-game protocol

Disclaimer

This analysis of Warface in-game communication protocol is against multiple points of the Crytek Terms of Service. So... I am not responsible for any other people acts trying to reproduce what's shown here, and I discourage anyone not aware of the possible risks (permanent ban, account deletion, etc. ). Please do read the Crytek ToS before attempting to reproduce what's described here.

Update: Some of the exploits or remarks have already been fixed the time you read this. Indeed, while I was writing this document, I also raised the issues to Crytek devs and let them time to digest. I've kept them here in hope it will make good stories to tell. Sadly for them, the main content of this analysis still remains valid.

Summary

In this analysis, we'll go through the reverse-engineering process I've made in order to create an XMPP proxy for Warface, letting anyone chat to any in-game connected player without the need to launch the game itself at all.

[TOC]

Why?

While being an addict to Warface, a Free-To-Play First-Person-Shooter made by Crytek, I've had the idea to create real-time statistics based on the in-game available data. Indeed, every minute, thousands of in-game statistics get updated, but there is no [public] API to retrieve any data. A basic example is the Warface currency you earn when you complete missions and spend to repair your equipment ; This amount is hard to keep-up with, while it could be displayed in a graph to understand what's going on, like your daily profit, for instance.

The game was still in beta when I had this idea, and thought that Crytek would eventually introduce such a feature, and over time I just forgot about all that.

Then, three months ago, Crytek released an online monthly clan leader-board, letting anyone to look at their clan performance over the month and better understand how clan points were earned. However, this ladder was hourly updated and didn't track previous updates, meaning you couldn't follow your clan progression over the month, only its results. Since it was a web service, it gave me the idea to fetch the ladder data hourly and create charts based on that.

The clan ladder

The online monthly clan leader-board is updated almost hourly and contains for each clan its points earned during the current month. Even if it doesn't provide the overall ladder (since the beginning), it tells if your clan is going up or down.

The simplest thing to do with this kind of workflow is to create a ``cron'' that will wake up a program that fetches the online ladder and save it somewhere.

Since I didn't have a dedicated server nor a 24/7-up machine to work with, I simply created an account on 000webhost and coded a simple PHP script that would curl the ladder when a new update is available.

Bring in the web-inspector

Launching the web-inspector already reveals a great deal. In fact, each time the page is loaded, it calls a REST API to know if the servers are available, with... a game id? Interesting...

XHR request to fetch game information

When we change the target server ("realm"), it doesn't refresh the page but instead calls another API that, this time, contains only the ladder in HTML form :

XHR request to fetch clan ladder

The page https://www.warface.com/en/leaderboard/getList is surely the one to curl as it is a simple GET query. As you may have noticed if you've clicked the previous link, it gives a 404 page not found. Looking back to the XHR request loaded from the official page, we can see it uses X-Requested-With: XMLHttpRequest that the server seems to check. Since anyone can fake HTTP headers, this really isn't an obstacle.

So far, we've seen two APIs, let me describe them :

  • https://www.warface.com/en/leaderboard/ Only responsible for the monthly clan ladder, no trace so far of a more general API giving the overall ladder or the previous results.

  • https://rest.api.gface.com/gface-rest/ Entire GFace REST API giving information about games and players. GFace was an attempt by Crytek to socialize the world of gaming. It used to be mandatory to log in via their online launcher in order to play the game through some sort of a web-canvas filled by a video proxy plugin. Many requests have been made to drop this method as it was very slow and buggy. Since then, a PC-side launcher has been made -- we will explore it in a sec. This REST API seems like the remaining of it.

Charts for the win

Within a couple hours, by using HighCharts, I've been able to create a simple web page providing monthly statistics. At first, it used to track only the top 10 clans as well as mine, but tracking siblings clans was quickly needed in order to visualize mutual progression:

Monthly clan ladder progression

On these charts, we can easily see when we go up or down in the ladder and why ; For example, a clan member may have left, making the clan lose 3 places, or, another clan is having a bigger hourly progression (more Clan Wars, more active players, etc. ) leading them to tackle us.

The Game

OK, nothing really fancy so far, just some PHP code and a crontab. I didn't bring you up there only to see this. So far my goal was to create statistics of in-game available data. With the monthly clan ladder, Crytek provided an API that provides some of the in-game data, but that wasn't enough for me ;-)

If the data I needed couldn't be found online directly, I needed to get connected to the Warface server like the game, and process queries/responses exactly like it does, right?

Wireshark is your friend

OK, let's fire up Wireshark and record the network activity while the launcher is starting.

Warface Launcher under Wireshark

Hmm, bad news, the launcher is using TLS in order to encrypt the communications. And without the public keys, it isn't possible to see what's going on there.

Hold on -- a quick look in the installation folder reveals that the launcher is actually a simple webkit-based app using v8 and Node.js.

Launcher install directory

Why is this information important ? Well, Webkit and other web engine, such as Gecko, look in the environment variable SSLKEYLOGFILE to know where to log the SSL public keys. Wireshark knows how to read this file if you feed it the "(Pre)-Master-Secret log filename" in the SSL configuration.

Wireshark SSL configuration

This enables us to see the decrypted comm within Wireshark and then discover that the launcher is... a simple web page fetched from https://launcher.warface.com

Decrypted SSL Stream of the launcher

We can then look at all the requests sent to the Warface servers and understand how the launcher actually works :

  1. https://launcher.warface.com/app -- Gives us what to display (such as game news).
  2. https://launcher.warface.com/app/auth -- Creates an identification token based on the login/password given. If the credentials are incorrect, it returns an error.
  3. https://rest.api.gface.com/gface-rest/gface-rest/auth/session/check.json -- Checks that the identification token is valid.
  4. https://rest.api.gface.com/gface-rest/auth/ticket/create.json -- Creates a ticket based on an identification token.
  5. https://rest.api.gface.com/gface-rest/auth/login/ticket -- Logs in the ticket and gives various account information such as the "online id" and the "profile id".
  6. Once all this information has been gathered, the launcher starts Game.exe.

Wait, what, XMPP ?

While Wireshark is hot and running, let's click on the gorgeous ``Play'' button the launcher shows us, and...

Warface starting an XMPP connection

... XMPP ! This is really interesting for a game to actually use a well known (Hipchat, Slack, Hangout, Facebook, etc... ) instant messaging presence protocol . As we can see above, as soon as the connection to the server is made, the game is initiating a secured connection using <starttls/> after the server sent us its <features/> showing TLS was an option.

If we try to give to use Pidgin on the XMPP server the game uses, it doesn't work because of a... parse error ?

Pidgin error on direct XMPP connection

Didn't we see previously that it was an XMPP stream ?

Hmm, wait, Wireshark indeed told us it was XMPP, but not exactly : The XMPP packets are prefixed with 12 bytes, almost identical every time :

Hex dump of the stream content

We can easily split the header into 3 fields:

Description Value Size (bytes)
Magic number 0xFEEDDEAD 4
Size of the packet n 4
Unknown field 0 4

Ok, now we know that, let's create a basic proxy that will answer Warface server with the appropriate header and answer Pidgin without that header. Simple, right ?

The art of being a MITM

We need to create a local server that will accept clients on the port 4242 (because everyone knows that's the only valid port to use) - basically, it will be the proxy of this Man-In-The-Middle attack:

#include <stdio.h>
#include <stdlib.h>
// Other includes...
#include <pthread.h>

int  main() {
    int ls = init_serv(4242);
    while (1)
        accept_client(ls); // calls proxy(client_fd)
    return 0;
}

These two functions are quite simple and use POSIX functions to create a server (the Internet is full of examples about this).

Note: Since some techniques to prevent Pidgin from dropping the XMPP connection are making the code hundreds of lines long, for the sake of keeping things concise here, not all the source code is shown -- it isn't relevant in this analysis to learn how to create a proxy, Google can help you with that.

Now, we create two threads, one that listens to the Warface server and talks to Pidgin, and one that listens to Pidgin and talks to the Warface server:

void proxy(int fd) {
    int wfs = connect_wf("com-eu.wfw.warface.com", 5222);
 
    int arg[] = malloc(sizeof(int) * 2);
    arg[0] = wfs, arg[1] = fd;
 
    pthread_t tl, ts;
    pthread_create(&tl, NULL, tlistener, arg);
    pthread_create(&ts, NULL, tsender, arg);
 
    pthread_join(tl), pthread_join(ts);
 
    close(wfs), free(arg);
} 

And the two threads look like this:

#define BUFF_SIZE 4096
void *tsender(void *varg)
{
    char buff_in[BUFF_SIZE];
    int *arg = (int *) varg;
    int wfs = arg[0], fd = arg[1];
    int size;
 
    do {
        do {
            size = read(fd, buff_in, BUFF_SIZE);
            send_stream(wfs, buff_in, size);
        } while (size == BUFF_SIZE);
        flush_stream(wfs);
    } while (size > 0);

    pthread_exit(NULL);
}

And here is the send routine...

void send_stream(int fd, char *msg, unsigned msg_size) {
    unsigned id = 0xFEEDDEAD, unk0 = 0;

    send(fd, &id, 4, MSG_MORE);
    send(fd, &msg_size, 4, MSG_MORE);
    send(fd, &unk0, 4, MSG_MORE);
    send(fd, msg, msg_size, MSG_MORE);
}

void flush_stream(int fd) {
    send(fd, NULL, 0, 0);
}

... and finally the read one:

char *read_stream(int fd) {
    unsigned id, msg_size, unk0 = 0;
    int read_size = 0, offset = 0;
 
    read(fd, &id, 4);
    read(fd, &msg_size, 4);
    read(fd, &unk0, 4);
 
    char *msg = calloc(msg_size + 1, 1);
    do {
        read_size = read(fd, msg + offset, msg_size);
        offset += read_size;
        msg_size -= read_size;
    } while (msg_size > 0);
 
    return msg;
}

Now that we have this basic proxy, we can configure Pidgin a little bit more :

  • Security : Only if available
  • Enable plain text authentication
  • Port : 4242
  • Server : localhost

But now it complains about no available mechanism. Indeed, the features returned by the server don't contain a PLAIN but a WARFACE one :

<stream:features>
  <starttls xmlns='urn:ietf:params:xml:ns:xmpp-tls'/>
  <mechanisms xmlns='urn:ietf:params:xml:ns:xmpp-sasl'>
    <mechanism>WARFACE</mechanism>
  </mechanisms>
</stream:features>

Since we already have a proxy setup, we can fake up the answers and say to Pidgin that PLAIN is available, and patch the authentication it sends back to Warface server such that it uses the WARFACE mechanism. This man-in-the-middle attack is virtually impossible with TLS enabled; This is why we disabled it.

From clever to dumb...

We now need to log into the XMPP service. This is done by the SASL authentication which requires a login and a password. At this point, the game knows :

  • Our ingame nickname -- FooBar
  • Our online id -- 1234567890
  • Our gface profile id -- 424242
  • The identification token -- f0a82f90-e874-e43c-88e8-bdaa0345acfb
  • The gface ticket -- 37631adfb6da42d8ac651aa25901eec0
  • The gface token -- 09f28a0f-478e-c34e-8e88-bfca5430aadb

By playing with all the possible combinations, I've come up to the conclusion that the login is your identification token and the password is your... online id. Wait, what ?! Oh sorry, it's even better: the password can actually be anything, but it will be used as your XMPP JID (password@warface) and be checked later against your online id. Someone definitely forgot to drink his morning coffee, one day...

One last thing to know is the resource to use. In XMPP/Jabber parlance, a resource is the location or a connecting device. For instance, you can connect with the same account multiple times by using a different resource. By simply looking into the game memory using CheatEngine, we can see that all requests use the resource GameClient. Any other resource seems to be rejected by the server.

Note: Since there is only one resource available, following the XMPP specifications, two connections cannot use the same resource. This also means you can bind a session with the online id of someone else and disconnect him while he's playing (when you're playing against him for example... ) -

Update: Fixed with update of the 1st April 2015.

Here are the final settings for Pidgin:

  • User: identification token (here f0a82f90-e874-e43c-88e8-bdaa0345acfb)
  • Domain: warface
  • Password: online id (here 1234567890)
  • Resource: GameClient
  • Alias (optional - will be used when sending messages): nickname (here FooBar)

OK, we're in! What can we do then? We can list all the in-game rooms and their participants:

Pidgin listing in-game rooms

... but when we try to join any room, we get a "Conflict error - That nickname is registered by another user". This is really strange since Pidgin made all the tedious work for us. Something is missing there...

From dumb to annoying...

Something, apparently not related to XMPP, is missing and prevents us from joining rooms. I've notived I can use the gface token instead of the identification token as a user, thought it could change anything, but no luck so far.

Looking back at Wireshark, we can see the game is initiating a TLS connection with <starttls/>. We need for analysis purposes to disable this TLS encryption in order to understand what it does after starting the stream.

We could try, the same way as before, to patch the <features/> received from the server to remove TLS as a possible option. This results in a frustrating message from the game in its logs :

[Warning][15:58:33.188]: [CryOnline] Client is configured to require TLS but either the server didn't offer TLS or TLS support is not compiled in.

What do you mean, "configured to require"? Would that imply that it could be configured somewhere? By looking into the game install directory we can see two files: user_%d.cfg and game.cfg. The first one contains some plain text useless configuration whereas the second one seems to be encrypted:

Configuration files

Browsing some time on the web, someone can easily find what I've seen; The CryEngine Toolkit lets the developer create RSA-encrypted configuration files. This prevents anyone to know and modify what's inside them. Oh, that sounds evil.

From annoying to actually really stupid

While CheatEngine is still launched, let's look for this "online_" pattern that seems to be common, at least in the user configuration:

CheatEngine showing game options

Oh, online_use_tls seems to be an interesting name for an option to disable TLS. Sadly, adding it to user_%d.cfg gives no luck and the game is still complaining about TLS not being available. Maybe this file is loaded before game.cfg which overrides all our conflicting options ? Ok then, let's empty the encrypted game.cfg and only put these plain text lines (maybe it will work?):

online_use_tls = 0
online_use_protect = 1

Dear god, that actually worked! My game starts full screen (whereas it was configured to be windowed) and Wireshark doesn't report any TLS communications anymore! The game sadly ends up by crashing on a parse error with some query looking fine at first... but oh well, we have 30 seconds of queries to analyse before even taking care of that.

First, we notice a query is sent to k01.warface containing a tag <account login='' password=''/> where this time the login is our online id and the password is our identification token, which makes a lot more sense than the order used for SASL.

XMPP packets in plain text in Wireshark

This query returns us the full list of all available "game servers" and, a very interesting attribute, the active token, which is nothing more than "$WF", online id, the login date, and a hash. This token is needed later to join and switch "channels" :

<iq from='k01.warface' type='result'>
  <query xmlns='urn:cryonline:k01'>
    <account user='1234567890'
             active_token='$WF_1234567890_20150307234312_8KVQm1rHcSiviMwq'
             load_balancing_type='server'>
      <masterservers>
        <server resource='pvp_newbie_3' server_id='103'
                channel='pvp_newbie' rank_group='all'
                load='0.000000' quickplay_load='0.000000'
                online='0' bootstrap=''
                min_rank='1' max_rank='8'/>
        <server resource='pvp_newbie_1' .../>
        ...
      </masterservers>
    </account>
  </query>
</iq>

We also notice the XML namespace urn:cryonline:k01. If this namespace is not present (for example http://jabber.org/protocol/disco#info), k01.warface will return an error "Invalid namespace". This tells us it's a custom extension, which is common when creating an XMPP service. We thus need the list of all the queries available since it won't follow XMPP core specs.

After the <account/> query giving us our active token, the Game sends a <join_channel/> query. At this point, players start to send us their status update ("Online", "Room", "Playing", etc... ) which in XMPP could have been done using the <presence/> stanza, but oh well...

Just Passing Through

Now that we know we need to send an <account/> followed by a <join_channel/> query in order to join rooms, lets add that into our proxy. This is a little bit more difficult as before, as we need to extract from the <account/> result our active token and use it in the <join_channel/> query ; strstr() and strncpy() will do the job just fine.

Moment of truth ... double clicking on global.pvp_pro_4 and ...

Joining a game room from Pidgin

As you can see, it is spamming us from joining/leaving messages, and it happens every time someone changes room or channel and opens or quits the game. I hope the bandwidth is not overflown due to that.

Anyway, global rooms are useless to join, let's join our clan room... Hmm, how do I know which one it is ? In game, the clan has a name whereas in this XMPP server clan rooms have an ID, like "clan.4242@warface". Ok, let's try to join every clan room until I find mine, the server should deny me accessing a room if its not my clan, sounds good, isn't it ?

What if I told you I was able to join the first clan room I clicked on ? Would you say I had an infinite amount of luck and should have played lottery ? Or... do you already see where it goes ? Yep, I was able to join any room including private ones such as other clan rooms.

This isn't even enough: Since I am not in other clan member list, I don't appear as a participant of the clan room in game. Thus I was completely invisible to other clan room members. Second fun fact : Since I am not using the in-game client, I can live translate everything that is said by people speaking other languages I don't understand. As soon as I've understand it was pure cheating and was way off my initial goals (create statistics of in-game available data), I immediately quit the rooms I didn't belonged to.

Encrypted...! Again?

Ok, enough of playing on red hot coals, back to Wireshark: What are the next queries ? Uh, oh, what is that ?! Surely nothing like XML !

Encryption reveals unknown field usage

On the above picture we can see a plain text query at the top and an encrypted message at the bottom, where we can see similar patterns. The query ID in the top query contains series of 0's that can be noticed in the below message with series of D's. Between the two, our online id is sent and the server replies us a random number, always between 0 and 255.

We also notice that the unknown field is non zero when it comes to this part :

Unknwon field value Origin Packet content
0 Both Plain text
1 Both Encrypted
2 Client online_id
3 Server Random number
4 Client Empty (ACK?)

Ok, now the question is, how do we decrypt this message ? First, if it has common sequences with the original string: It cannot be SSL or Vigenere. It's inevitably an algorithm that performs character by character and doesn't evolve, like a rotX.

$$ f(48, 1234567890, 116) = 68 $$

With couple of tries, and by patching the sent online id, and with the server sending me the same random number, I've been able to reproduce the same message, which means the online id is not used. Thus, we have :

$$ f(48, 116) = 68 $$

... which is... a simple XOR ! If it's not obvious for you, don't feel alone, a total of 3 hours was needed for me to finally see the divine light.

With a simple read/write/xor program, we can decipher the communication :

#include <stdlib.h>
#include <unistd.h>
#define BUFF_SIZE 256

int main(int argc, char *argv[]) {
    unsigned char key = strtol(argv[1], NULL, 10);
    unsigned char buff[BUFF_SIZE];
    size_t size;

    while ((size = read(0, buff, BUFF_SIZE)) > 0) {
        for (int i = 0; i < size; ++i) {
            buff[i] = buff[i] ^ key;
            if (buff[i] > '~' || buff[i] < ' ')
                buff[i] = ' ';
        }
        write(1, buff, size);
    }

    return 0;
}

The decrypted communication reveals again, unsurprisingly, tons of XMPP queries.

A Game of queries

The entire in-game event system seems to be driven by XMPP queries (or stanza as the XMPP purists call it). Aside from k01.warface, there is another bot, ms.warface (and his cousin masterserver@warface) that seems to answer to other queries the first bot doesn't know about. Since k01.warface logged us in, this tells us it could be a more general bot that is shared among different Crytek games whereas the second bot is local to Warface. So far I have no idea what k01 means.

Most of the queries, as they return too much data, are returned as compressed stanza of the form :

<iq type="result">
	<query xmlns="urn:cryonline:k01">
		<data query_name="items" compressedData="..." originalSize="12825"/>
	</query>
</iq>

... where compressedData is an attribute that contains the base64 version of a zlib compressed XML file.

Just for the fun of saying it, to get the information it needs, the Game is passing through :

  1. TCP/IP
  2. TLS
  3. xor
  4. XMPP
  5. base64
  6. zlib
  7. XML

So the next time you see a loading bar, instead of thinking it parses huge queries, think it that way: It is going back and forth through useless layers of encryption/compression.

Queries workflow

Let's draw a chart to sum up. What is interesting in this workflow, is that k01 serves as a registrar for the game clients over XMPP. Without being registered by k01, we are not able to join rooms and perform other queries.

Launcher->WF:/app/auth
WF-->Launcher:token

Launcher->GFACE:/auth/session/check
Launcher->GFACE:/auth/ticket/create
GFACE-->Launcher:ticket

Launcher->Game:Launches
Game->GFACE:/auth/login/ticket
GFACE-->Game:online_id/profile_id/nickname

Game->XMPP: SASL(token/online_id)
XMPP-->Game: online_id@warface/GameClient

Game->k01: <account/>
k01->GFACE: Check
k01->XMPP: Register
k01->ms: Session
k01-->Game:<account/>

ms-->Game:<friend_list/>
ms-->Game:<clan_info/>

Players->Game: <peer_status_update/>
Players->Game: <peer_clan_member_update/>

Game->XMPP: <presence/>
XMPP-->Players: <presence/>
Game->XMPP: <message/>
XMPP-->Players: <message/>

Query list

During these 30 seconds of communication a lot of queries are going back and forth that we can reproduce with a third party XMPP client. Below are some of the queries I've managed to use, just to name a few :

Results received only
  • Results for <account/> from ms.warface
  • friend_list
  • clan_info
  • update_cry_money
  • Results sent by other players' game
  • peer_clan_member_update
  • peer_status_update
Queries to ms.warface
  • items
  • gameroom_get room_type='14' size='108' received='0' cancelled='0' token='0'
  • get_account_profiles version='1.1.1.3519' user_id='online_id' token='account_token'
  • get_achievements/achievement profile_id='profile_id'
  • get_configs
  • get_last_seen_date profile_id='profile_id'
  • get_player_stats
  • missions_get_list
  • ...
Queries to masterserver@warface
  • admin_cmd command='' args='' -- hmmm, not wanna touch this one
  • clan_info_sync -- returns a clan_info
  • clan_list
  • get_contracts
  • get_cry_money
  • get_profile_performance
  • get_storage_items
  • telemetry_getleaderboard limit='10'
  • ...
Queries to k01.warface
  • account login='online_id' password='token'
  • get_master_server
  • join_channel version='1.1.1.3519' token='account_token' profile_id='profile_id' user_id='online_id' resource='pve_12' build_type='--release'
  • switch_channel version='1.1.1.3519' token='account_token' profile_id='profile_id' user_id='online_id' resource='pve_12' build_type='--release'
  • player_status prev_status='1' new_status='9' to='pve_12'
  • profile_info_get_status nickname='nickname'
Queries to players' game
  • peer_player_info -- returns some of the info get_player_stats returned to the player

Hidden for the blinds

In the previous query list, you may have noticed two very interesting ones: <clan_list/> and <telemetry_getleaderboard/>. Indeed, the game doesn't provide the full clan/player ladder -- only clan top 10 and our clan position -- which leads us to guess our clan performance over other clans. Since the very beginning of this game, the information of a full clan or player ladder was hidden, and people tried to build their own top 100 clan ladder by asking other clan what their rank was. The monthly clan ladder gave some visibility on what was going on in the full clan ladder, but sometimes it's stills out of our mind why we lose/gain 10 places in one hour.

Sadly, <clan_list/> doesn't provide the full list. It only gives us something like this :

<?xml version="1.0"?>
<clan_list>
  <clan_performance position="24">
    <clan name="RusSpecForce" clan_id="13" description="ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAtIFRlYW1TcGVhayAzIC0gIA0gICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBTZXJ2ZXI6ICAgdHNYWFguemFwLWhvc3RpbmcuY29tOjIwMjMgDSAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgDSAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIFNlcnZlcjogNjIuWFhYLlhYLlhYWDoxMDAyMyAgKHBhczogPHNuaXA+KSAgIA0gICAgICAgICAgICAgICAgICAgICAgIA0gICAgICAgICAgIA0gICAgICAgICAgICAgICAgICAgICAgICAgICANICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIA0gICAgICAgIA0=" creation_date="1380709291" master="BornAlex" clan_points="36591506" members="50" master_badge="125" master_stripe="9002" master_mark="9000"/>
    <clan name="X-Force" clan_id="3" description="..." .../>
    <clan name="(aXe)" clan_id="20" description="..."  .../>
    ...
  </clan_performance>
</clan_list>

Hold on just a sec ... Is that the base64 version of the private clan description ? Yep, it is ! Here is the proof of why it shouldn't be shared to all the players' game when they simply ask for the clan ladder :

                            - TeamSpeak 3 -
              Server:    tsXXX.zap-hosting.com:2023
              Server: 62.XXX.XX.XXX:10023  (pas: <snip>)         

Note: If you're a clan in the top 10, please, find another way to share with your members such information, as any malicious hacker can get all of them (TS servers, websites, passwords, ...) as you shared them in your clan description you thought was private.

And what about <telemetry_getleaderboard/> ?

<?xml version="1.0"?>
<telemetry_getleaderboard>
  <player rank="1" profile_id="40" nickname="SnakePlissken" total="11682000" class="3"/>
  <player rank="2" profile_id="76" nickname="Vladifer" total="11682000" class="2"/>
  <player rank="3" profile_id="141" nickname="Sarchi" total="11682000" class="0"/>
  ...
</telemetry_getleaderboard>

Better luck this time, it returns the player list that are above our position. The limit attribute is by default set to 10. There also seems to be some sort of caching system as we cannot instantly ask for top 10 then top 100 -- it will return us the previous top 10 we already asked. We also notice that the top 80 is only composed of rank 70 players that have the same score of 11682000 as they cannot go higher. Plus, top 80 is sorted by profile_id -- thus, it won't move anymore -- so congratulations, SnakePlissken, you are n°1 on EU server forever as you were the first registered player to reach rank 70 !

Beyond TLS

oSpy

During these 30 seconds of plain text exchanges between the game and Warface servers there were some interesting queries we managed to explore, but we need to go deeper. Because of the option that disables the TLS we added, the game crashes due to some missing cyclic buffer manipulation -- that I had to implement myself in my proxy in order to make Pidgin happy. This missing case may be handled by the TLS layer itself. Thus, we need to have the TLS activated in order to continue, but at the same time be able to read the non-encrypted version of the packets.

This is where oSpy comes in action. It captures the sent and received packets, as Wireshark does, but on the process level instead of the interface one. This enables oSpy to hook the crypt API and record the plain text exchanges.

oSpy

As most of the exchanges are using that silly XOR, we need to save the comm from oSpy, which produces a bzip archive, containing an XML file. Thus, a simple use of xmllint --xpath and base64 -d to extract the XOR'd comm, and a simple bruteforce over the 255 possible XOR keys will lead us to the actual XMPP queries.

Anyway, all of that to finally discover nothing that useful. Indeed, most of the queries the game sends are called without arguments, and the others it receives (like challenge updates) arrive without any warn up from masterserver@warface (when a game room ends to keep the same example).

Match creation

The only thing relevant in that part, IMHO, is the game room updates that happens just before a match is launched :

<iq from="k01.warface" type="get">
  <query xmlns="urn:cryonline:k01">
    <data query_name="gameroom_sync" compressedData="..." originalSize="1081"/>
  </query>
</iq>

Here, the compressedData is translated to the XML below, where we can see the session ID:

<?xml version="1.0"?>
<gameroom_sync bcast_receivers="16645828@warface/GameClient,19280517@warface/GameClient,19612164@warface/GameClient,1234567890@warface/GameClient">
  <game_room room_id="28007" room_type="1">
    <core teams_switched="0" room_name="Sotan05s RAUM" private="0"
          players="4" can_start="1" team_balanced="1" revision="238">
      <players>
        <player profile_id="1303890"/>
        <player profile_id="419196"/>
        <player profile_id="424242"/>
        <player profile_id="1196277"/>
      </players>
    </core>
    <session id="823427570887578502" status="2" game_progress="1" revision="130"/>
    <custom_params friendly_fire="0" enemy_outlines="1" auto_team_balance="0"
			       dead_can_chat="1" join_in_the_process="1" max_players="5"
				   class_restriction="253" inventory_slot="2113929215"
				   revision="148"/>
    <mission mission_key="ecf18337-7404-485f-bb4e-7c61b417b000" no_teams="1"
		     name="@fd_mission_easy02_2" setting="favela/favela_base"
			 description="@mission_desc_favela_j02" image="mapImgFDj02_normal"
			 difficulty="easy" type="easymission" time_of_day="9:06"
			 revision="147">
      <objectives factor="2">
        <objective id="0" type="primary"/>
        <objective id="17" type="secondary"/>
      </objectives>
      <CrownRewardsThresholds>
        <TotalPerformance bronze="38000" silver="74000" gold="117000"/>
        <Time bronze="4193359" silver="4193499" gold="4193564"/>
      </CrownRewardsThresholds>
      <CrownRewards bronze="2" silver="4" gold="6"/>
    </mission>
    <room_master master="424242" revision="115"/>
  </game_room>
</gameroom_sync>

This reveals us of options someone can trick using either a custom proxy (good luck !) or with a hook that makes the game ignore, let's say, the attribute can_start (just saying !), or, even eviler, change the parameter max_players.

UDP, at last

What got me worried from the beginning of this analysis, was if the game system actually relied on XMPP to process in-game player positions and actions, as we find traces of queries such as gameroom_kick or on_voting_started. What is sure, thought, is that it doesn't rely on XMPP to display connected resources (as in players in the room) but rather synchronize all that information with custom XMPP queries. Thankfully, the game is running using UDP, but doesn't stop listening to XMPP in background for other updates, such as clan discussions.

Note: Knowing that the game is still listening to XMPP when you are playing, enables the room master to kick any player even if a match has started (where he needed to launch a kick vote instead). He could do that with an in-game XMPP console or a kick hook trigger, without the need of returning to the game room. It also means that if the XMPP stream is lost (let's say someone else tries to connect with your online_id as we saw previously), you are kicked out of the game.

Anyway, were did I saw the game was using UDP ? A simple query among others stole my attention :

<iq to="masterserver@warface/pve_11" type="get">
 <query xmlns="urn:cryonline:k01">
  <session_join/>
 </query>
</iq>

<iq from="masterserver@warface/pve_11" type="result">
 <query xmlns="urn:cryonline:k01">
  <session_join server="ded15-eu.host.warface.com_60016" hostname="37.58.104.245" port="60016" local="0"/>
 </query>
</iq>

... and Wireshark :

Wireshark shows UDP

I won't go through reverse-engineering the UDP comm, but a first look at this shows non-crypted raw binary data. The only ascii part is the handshake that shares some game room parameters shown in the above picture.

Final Words

I need to say that, despite the bad conclusions, it was an exiting adventure. Before that analysis I'd never been interested in reverse-engineering game protocols. On top of that, I feel I had an enormous amount of luck, mainly due to the poorly chosen designs Crytek devs made there.

Brainstorming at Crytek

All of this was the work of a full-time week of spring-break, and I think it was one of the most productive short vacations I've ever had so far. I made all of this for fun; Some people like doing crosswords, I like most reverse-engineering, I find it more unpredictable :P

Sadly, I won't release my POC proxy for Pidgin in its current state. IMHO, the source code is messy and lacks of error handling cases, it requires you to manually do the auth and cannot be connected while you are playing (only one XMPP resource available). Plus, if Crytek devs doesn't fix the clan room stuff, it could be considered as a cheat tool and, ironically, I hate "hackers".

I hope you've enjoyed this story as much as I did !

Levak.

Analytics

@f0rb1d
Copy link

f0rb1d commented Jan 22, 2018

Nice work!

@datgrog
Copy link

datgrog commented Jan 31, 2018

Really cool to read 👍

@afrostalin
Copy link

Nice stuff

@amrhead
Copy link

amrhead commented Aug 23, 2019

afrostalin motherfuck'a

@andli
Copy link

andli commented Sep 17, 2019

I really really loved this writeup. Great job, good writing. Thanks!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment