This is a copy of the article 'Dissecting the network-protocol and -crypto behind Dark Souls Remastered.' originally written and published October 2, 2019.

Introduction

This post covers the complete game server messaging protocol of Dark Souls Remastered. All the information presented was obtained through reverse engineering of the patch 1.03 client, which is the latest version as of the writing of this article.


Overview

Before we get into the details let’s look at the protocol at a very high level. The general timeline of a FromNet connection is as follows:

* Client establishes TCP connection with fhd-steam-prod.fromsoftware-game.net.
* Client sends init packet to initiate the key exchange.
* Client and server negotiate a shared key and a pair of nonces. The key and nonces are used to encrypt all communications after this point.
* Client authentication via ISteamUser::GetAuthSessionTicket.
* Bidirectional communication using a request–response style message exchange until either party disconnects.

At the highest level of abstraction there are seven unique message types which can be identified by their opcodes. Each message type is discussed in more detail below.


Initialization (op:01)

The first packet sent by the client after the tcp connection is established is very simple and just contains the message size (msg_size fields never include the size of the msg_size field itself), an opcode and a constant magic byte with the value 0x64. It’s not entirely clear what the byte represents but it’s presumably a client version or an id that indicates the requested service. Changing the magic byte to any other value results in the termination of the connection by the server.

Cryptography & Key exchange (op:02-03)

With the exception of the init packet, all communication is encrypted and authenticated. This is achieved through the use of the XSalsa20Poly1305 stream cipher. RTTI strings and the correlation of the clients assembly code with known implementations of the cipher shows that the particular implementation in use is provided by the libsodium library. The specific functions used to en/decrypt messages are crypto_secretbox_detached and crypto_secretbox_open_detached respectively.
Messages sent prior to completion of the key exchange are encrypted with a pre-shared encryption key that is stored in an obfuscated form inside the client binary. MAC, nonce and a message size field are prepended to the cipher text or in other words, the cipher text struct inherits from:

Messages sent after a successful key/nonce exchange drop the nonce member as it’s not required anymore going forward.

The key exchange process aims to share 2 nonces and one key between the client and the server. Each nonce is used for one unique message direction and incremented by byte from left to right after each use. The exchange is initiated by the server and extends over two messages, below referred to as ServerKeyExch and ClientKeyExch. The server key exchange message provides the upper 0x10 bytes of the shared key, while the client message provides the lower 0x10 key bytes as well as both nonces. All the keys and nonces are randomly generated. The message structures are as follows:

The unique_token featured in both key exchange packets is a randomly generated array of bytes that gets generated by the server and echoed back by the client in the ClientKeyExch message. It doesn’t seem to have any other purpose.

The following shows a complete key exchange and the resulting key and nonces for reference:

Request/Response (op:04-05)

Once the shared key encryptor is initialized, the transfer of data relevant to the game can begin. The data exchange generally follows a Request-Response protocol. Client requests use the opcode 04, responses use the opcode 05. Corresponding request-response pairs are identified by a matching, unique sequence number. The actual data is transmitted in the form of a flatbuffer. Reversing the flatbuffer structure and deserializing the contained data is fairly complex and a topic for another day. Request structures aren’t padded and the size of the flatbuffer can be calculated using the message size field in the request header.

Push Requests (op:06)

Push requests are server messages that notify the client about activities managed by the game server. For example area event and matchmaking related status messages. Push requests are unidirectional and aren’t directly answered by the client, the client might send requests in response to receiving a push request though.

Heart Beat (op:07)

Heart beat messages are sent by the server when there hasn’t been any message exchange for longer than about 5 seconds. Heart beat messages are typically echoed back to the server, alternatively the client can send a request. If the client doesn’t respond at all, the connection will be terminated by the server.

Composite Messages

Messages are typically sent in isolation but there are cases where multiple messages are sent in a single buffer. Consequently, all recv buffers have to be inspected and potentially be split into individual messages. An example of such a composite is the buffer that contains the client key exchange message, this buffer will always contain a client request of type RequestLogin directly following the key exchange message. Decomposing receive buffers correctly is critical as dropping a message will result in desynchronisation of the shared key encryptor instances used to en/decrypt the network traffic.

Security

The described protocol has a number of severe design and security issues that allow a man in the middle (MITM) to fully decrypt, inspect and modify all transmitted data. This holds true even if the attacker has no knowledge of the pre-shared key and/or only captured the session traffic partially. One might argue that game server traffic isn’t very security critical but I strongly disagree. During the login, Steam auth packets are exchanged, which an attacker could use to impersonate and permanently ban a user from the service. From Software is also very well known for shipping buggy and exploitable parser code as part of their networking solution.
The following paragraphs briefly outline three of the major issues of the protocol:

1. Pre-shared key
The first and most obvious issue is a key exchange based on symmetric cryptography with a pre-shared key. Hiding keys in the client is futile and all information contained in the game client should be assumed to be compromised when designing a protocol. Knowledge of the embedded key allows a MITM to decrypt, inspect and modify all traffic if the message exchange is captured from the beginning.

2. Insecure client side RNG usage
During key exchange, the client provides 0x10 bytes of key material, as well as two xSalsa20 nonces. All those bytes originate from an insecurely seeded mt19937 RNG. The use of mt19937, which isn’t a cryptographically secure random number generator, completely breaks the security of the protocol, even if the key exchange wasn’t observed by a MITM. The insecure seeding is merely an additional convenience that reduces the time needed to reverse the keys.

3. Insecure server side RNG usage
This insecure RNG use isn’t limited to the client but also applies to the server, where the upper 0x10 bytes of the xSalsa20 keys are generated. This flaw has much bigger implications on the server side. With each login attempt, an attacker can exfiltrate 0x10 bytes from the server side RNG which allows the reverse engineering of the internal state of the RNG instance, which in turn allows the calculation of all, past and future, key material the server has and will produce until the server instance is rebooted.

Solutions

Secure key exchange is for the most part a solved issue and the protocol should use one of the well tested and believed to be secure protocols like the Diffie-Hellman key exchange protocol. From Software tried to fix the insecure key exchange in later products by using RSA. Unfortunately they didn’t really improve the security by doing so but that’s a story for another day.
The insecure RNG could be fixed by switching to a more suitable generator. This should be fairly simple since the client already links against libcrypto but unfortunately this issue has never been addressed in later iterations of the engine.