Target

Our target in this lesson will be Wesnoth 1.14.9.

Identify

Like many games, Wesnoth has a multiplayer mode that allows multiple players to join a lobby, chat with each other, and play games against each other over a network. Our goal in this lesson is to analyze the packets used for connecting to the lobby and create a client that will connect to a lobby.

Multiplayer Lobby

Understand

For multiple players to communicate over a network, all of the clients (in this case, the Wesnoth game executable) must agree on a network model and protocol. They also must agree on the data each packet will contain. If there is a server, the server must also agree with all of these components.

Since this data is structured, we can first identify the network model and protocol used by the game. Then, we can observe the packets being sent and reverse the data to determine what each packet is doing. We can then use the data in these packets to create our own client using the Windows’ Socket API.

Local Server

If you start Wesnoth and click Multiplayer, you will see the following screen:

Multiplayer Lobby

These entries indicate that Wesnoth is using a client-server model. If we explore the Wesnoth game folder in C:\Program Files (x86)\Battle for Wesnoth 1.14.9, you will find a program called wesnothd.exe. Reading the documentation on the developer’s website, we know that this is a server daemon that allows you to host a server. It can be run by invoking it from the command prompt:

Multiplayer Lobby

With the server running, we can now connect to it from Wesnoth. In the previous lesson, we discussed how clients need to know two pieces of information to connect to another host: the IP address and the port. In this case, the server is running on our local machine. There are several reserved IP address ranges that will never be used for normal network assignments. One of these is the range from 127.0.0.0 to 127.255.255.255, which is reserved for loopback addresses on the local machine. The loopback component indicates that the external network will not be able to access these IP ranges.

On all operating systems, 127.0.0.1 will always direct to your current host. In addition, localhost is a hostname that directs to 127.0.0.1. Therefore, we know that the IP for this server is 127.0.0.1 or localhost. From the documentation, we know that the server runs by default on port 15000. With these two pieces of information, we can connect to the server.

Choose Connect to Server and then enter in localhost:15000:

Multiplayer Lobby

When you hit Connect, your client will join a multiplayer lobby. If you observe the server running in the command prompt, you should see that it has printed out the connection event:

Multiplayer Lobby

Observing Packets

With the server running, we can close the Wesnoth client and start the process of observing packets. There are many tools that can be used, but for these lessons, we will use Wireshark. This can be installed via:

choco install wireshark

The first time you use Wireshark, you will also need to install the WinPcap driver as instructed by the program.

With Wireshark and the driver installed, you can pick a network interface to observe on:

Wireshark Interface

Each listed network interface represents a piece of software or hardware that connects to a public or private network. For example, the Ethernet0 interface listed is the default network card used to communicate over the Internet for this particular VM. Depending on your VM and computer, these interfaces may be different.

In this case, we know all of the traffic for our game will be flowing on the loopback interface, so select Adapter for loopback traffic capture. Upon selecting this, Wireshark will start monitoring for packets. Open up Wesnoth and connect to the local server. When connecting, you should see Wireshark log multiple packets:

Wireshark Logging

Initially, this can appear overwhelming, so let’s break down what exactly we are seeing here and identify what we care about. In the protocol column, we can see that Wesnoth is using TCP:

Wireshark Logging

We know from the previous lesson that TCP initiates an upfront connection and acknowledges when packets have been received. This initial negotiation is known as a three-way handshake, and has three parts:

  1. One side sends a packet with a SYN flag.
  2. The other side responds with SYN and ACK flags.
  3. The first side sends an ACK flag.

We can see this behavior in the first few packets highlighted below:

Wireshark Logging

Since we know 15000 is the server’s port, we can determine that 50563 is our client’s port. However, this number will probably not match the number you are seeing. If we close Wesnoth and start it again, we will also see that this number changes:

Wireshark Logging

This is an example of an ephemeral, or short-lived, port. Since the Wesnoth client does not need to be discoverable by other users, it can choose a “random” available port each time it starts up. When the client is closed, it will free this port. This is in contrast to the server, which always needs to be discoverable on port 15000 by clients.

None of the packets we have examined so far contain any data. We can determine this by looking at the len member:

Wireshark Logging

Looking at all the packets, we can see that the only packets with data have the PSH flag. This flag tells the TCP connection to immediately send whatever data is inside the packet to the associated application instead of placing it in a buffer. We can filter for this flag in Wireshark to only see the packets that we care about via tcp.flags.push == 1:

Wireshark Logging

Packet Structure

When you select a packet in Wireshark, it displays the full packet broken down in different segments. For example, if we select the first packet of len 4, we will see the following view:

Wireshark Logging

As you select different components (like IP and TCP), the associated parts of the packet will be highlighted in the bottom section. For example, by selecting the TCP component, the following section is highlighted:

Wireshark Logging

For the purposes of this lesson, we can ignore the IP and TCP components and focus on the data component for all the packets:

Wireshark Logging

Given that data in packets is often compressed, it is difficult to determine the purpose of a single packet in isolation. Instead, it is easier to look at the overall flow of network traffic and determine the role of each packet. In our case, the flow of network traffic for connecting looks like:

Wireshark Logging

We know that packets from 50563 -> 15000 represent communication sent from our client to the server, and 15000 -> 50563 represent communication sent from the server to our client. As such, the network traffic looks like:

  1. Client -> Server Packet 1
  2. Server -> Client
  3. Client -> Server Packet 2
  4. Server -> Client
  5. Client -> Server Packet 3

Since we are writing a client, we will only need to reverse the three packets being sent from the client to the server.

Sockets

Each OS will have its own set of API’s that allows you to interact with the networking stack. In Windows, this API is known as Winsock. Microsoft has comprehensive documentation available on how to use this API, including the process to establish a socket. This is available here.

Microsoft also provides a complete example using these API’s here. This example will create a TCP connection to a provided IP on port 27015 and send a single packet containing the data this is a test. It will then continuously wait for packets from the server. We will base our code on this example.

A good starting place is to see how the server responds when we simply use the code as is. Since we are targeting a specific IP, we will remove the following code from the example:

if (argc != 2) {
  printf("usage: %s server-name\n", argv[0]);
  return 1;
}

Next, we can modify the getaddrinfo function to use the values for our server:

iResult = getaddrinfo("127.0.0.1", "15000", &hints, &result);

With these changes, compile the code and run the executable. If you have Wireshark still logging, you should notice your host sending a packet with the data this is a test to the server. If you look at the server process, you will see the following message:

Wesnoth Server Message

Reversing Packets

This message indicates that the first packets sent by our client must initiate some sort of handshake. Let’s compare the first message logged by the server for a valid connection:

Wesnoth Server Message

Going back to our Wireshark capture, we can see that the first packet sent by the client contains 00 00 00 00. The server then responds with data. From this, we can assume that the data 00 00 00 00 is interpreted by the Wesnoth server as the start of a handshake.

We can modify our socket example to perform this behavior. First, remove the following code since we will be writing our own sending code:

// Send an initial buffer
iResult = send( ConnectSocket, sendbuf, (int)strlen(sendbuf), 0 );
if (iResult == SOCKET_ERROR) {
  printf("send failed with error: %d\n", WSAGetLastError());
  closesocket(ConnectSocket);
  WSACleanup();
  return 1;
}

printf("Bytes Sent: %ld\n", iResult);

// shutdown the connection since no more data will be sent
iResult = shutdown(ConnectSocket, SD_SEND);
if (iResult == SOCKET_ERROR) {
  printf("shutdown failed with error: %d\n", WSAGetLastError());
  closesocket(ConnectSocket);
  WSACleanup();
  return 1;
}

Next, create a buffer that will hold our 00 00 00 00 data:

const unsigned char buff_handshake_p1[] = {
  0x00, 0x00, 0x00, 0x00
};

Finally, add the following code to send this data and receive a single packet back:

iResult = send(ConnectSocket, (const char*)buff_handshake_p1, (int)sizeof(buff_handshake_p1), 0);
printf("Bytes Sent: %ld\n", iResult);

iResult = recv(ConnectSocket, recvbuf, recvbuflen, 0);
printf("Bytes received: %d\n", iResult);

If you run this program, you will receive 41 bytes back. This is equal to the two responses sent by the server in Wireshark, indicating that the first packet sent by the client initiates the handshake:

Wesnoth Server Message

From the server messages, we can see that the next packet the client is responsible for is sending their current version. An example of this packet’s data is shown below:

0x00, 0x00, 0x00, 0x2f, 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0xff, 0x8b, 0x2e, 0x4b, 0x2d, 0x2a, 0xce,
0xcc, 0xcf, 0x8b, 0xe5, 0xe2, 0x84, 0xb2, 0x6c, 0x95, 0x0c,
0xf5, 0x0c, 0x4d, 0xf4, 0x2c, 0x95, 0xb8, 0xa2, 0xf5, 0xe1,
0x92, 0x5c, 0x00, 0xc0, 0x38, 0xd3, 0xd7, 0x28, 0x00, 0x00,
0x00

Even when converted into ASCII, our game version (1.14.9) does not appear in this data. This is because, like most games, Wesnoth compresses all data by default. In future lessons, we will examine the compression scheme used so that we can create packets with custom data. However, in this lesson, we will not need to do this since this data does not change. You can verify that by joining the same server multiple times with Wireshark running.

Let’s add this packet to our program to send as well:

const unsigned char buff_handshake_p2[] = {
  0x00, 0x00, 0x00, 0x2f, 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00,
  0x00, 0x00, 0x00, 0xff, 0x8b, 0x2e, 0x4b, 0x2d, 0x2a, 0xce,
  0xcc, 0xcf, 0x8b, 0xe5, 0xe2, 0x84, 0xb2, 0x6c, 0x95, 0x0c,
  0xf5, 0x0c, 0x4d, 0xf4, 0x2c, 0x95, 0xb8, 0xa2, 0xf5, 0xe1,
  0x92, 0x5c, 0x00, 0xc0, 0x38, 0xd3, 0xd7, 0x28, 0x00, 0x00,
  0x00
};

iResult = send(ConnectSocket, (const char*)buff_handshake_p2, (int)sizeof(buff_handshake_p2), 0);
printf("Bytes Sent: %ld\n", iResult);

iResult = recv(ConnectSocket, recvbuf, recvbuflen, 0);
printf("Bytes received: %d\n", iResult);

With this additional packet, the Wesnoth server will now think that a client is sending them a game’s version before closing the connection:

Wesnoth Server Message

Finally, we can add in the name that our client will send. Since we have control over this field, we can use it to observe the compression scheme in use. With Wireshark running, connect to a server with two usernames, one short and one long. In the long username, make sure multiple characters repeat in a row. This will allow us to detect patterns. In this lab, we will use the examples of FFFAAAKKKEEE and IEUser. Their related packets look like:

Wesnoth Server Message

The highlighted areas most likely represent the compressed name of the user. Let’s try sending the data from the FFFAAAKKKEEE request, but slightly modifying the bytes for the name:

const unsigned char buff_send_name[] = {
  0x00, 0x00, 0x00, 0x3a, 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00,
  0x00, 0x00, 0x00, 0xff, 0x8b, 0xce, 0xc9, 0x4f, 0xcf, 0xcc,
  0x8b, 0xe5, 0xe2, 0x2c, 0x2d, 0x4e, 0x2d, 0xca, 0x4b, 0xcc,
  0x4d, 0xb5, 0x55, 0x72, 0x74, 0x74, 0x74, 0x74, 0x74, 0xf4,
  0xf6, 0xf6, 0x76, 0x75, 0x75, 0x55, 0xe2, 0x8a, 0xd6, 0x87,
  0xaa, 0xe0, 0x02, 0x00, 0xa1, 0xfc, 0x19, 0x4c, 0x2b, 0x00,
  0x00, 0x00
};

iResult = send(ConnectSocket, (const char*)buff_send_name, (int)sizeof(buff_send_name), 0);
printf("Bytes Sent: %ld\n", iResult);

If we observe the server, we see the following error:

Wesnoth Server Message

This verifies that the packet is being compressed, and even indicates the compression scheme (simple_wml). We can use this information in future lessons when we want to create our own packet. For this lesson, we can just modify buff_send_name to contain the original data:

const unsigned char buff_send_name[] = {
0x00, 0x00, 0x00, 0x3a, 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0xff, 0x8b, 0xce, 0xc9, 0x4f, 0xcf, 0xcc,
0x8b, 0xe5, 0xe2, 0x2c, 0x2d, 0x4e, 0x2d, 0xca, 0x4b, 0xcc,
0x4d, 0xb5, 0x55, 0x72, 0x73, 0x73, 0x73, 0x74, 0x74, 0xf4,
0xf6, 0xf6, 0x76, 0x75, 0x75, 0x55, 0xe2, 0x8a, 0xd6, 0x87,
0xaa, 0xe0, 0x02, 0x00, 0xa1, 0xfc, 0x19, 0x4c, 0x2b, 0x00,
0x00, 0x00
};

With this change, our client will now connect to the server using the name FFFAAAKKKEEE. If you join the lobby with a legitimate client, you will notice that our client is also connected.

Wesnoth Server Message

The full code for this client is available on github.

 

results matching ""

    No results matching ""