Target
Our target in this lesson will be Wesnoth 1.14.9.
Overview
In the previous lesson, we created an external client that would connect to a Wesnoth server and listen for and respond to specific chat messages. A major downside to this approach is that we had to reverse the entire authentication process so that our client could connect to the server. Our goal in this lesson is to create a proxy. This will allow us to use a regular game client and only intercept and modify the traffic we care about from the client.
Reason for Proxying
The best way to understand our purpose for creating a proxy is to observe the network traffic when connecting to a server with two clients on the same host. On your lesson machine, start a Wesnoth server and connect to it with a legitimate copy of the game. Next, modify the client that we wrote in the previous lesson and remove all the authentication. Instead, have it simply send a chat message:
...
freeaddrinfo(result);
if (ConnectSocket == INVALID_SOCKET) {
printf("Unable to connect to server!\n");
WSACleanup();
return 1;
}
const unsigned char first_message[] = "[message]\nmessage=\"ChatBot connected\"\nroom=\"lobby\"\nsender=\"ChatBot\"\n[/message]";
send_data(first_message, sizeof(first_message), ConnectSocket);
...
Finally, open up Wireshark and start monitoring for traffic on the local adapter, as we have discussed in previous lessons. When you start the modified external client, you should see the following error on the server:
We are familiar with this error from our initial analysis, so we know that it occurs when we do not provide a valid handshake. Even though both our game and external client are running on the same machine, they have different sockets and are treated as completely separate connections by the server:
If we examine the Wireshark traffic from the game client and our external client, we can observe this behavior. While the game is busy sending traffic on port 51120, we can see the TCP handshake of our external client on port 51123:
If we want to intercept and modify a game’s client traffic, we will need to use a different approach.
Proxying Traffic
When using TCP, we know that a connection is established between a client and server after completing a handshake. To intercept and inject our own traffic into this connection, the easiest approach is to be a man-in-the-middle (MitM) of this connection.
At this position, we can modify requests from the client before they are sent to the server, as well as responses from the server before they are sent to the client. This is commonly known as a proxy, or an agent that simply relays traffic from a source to a destination. A visualization of this model is shown below:
The key to this model is that our proxy is acting as the server to the client and the client to the server. This means that the server completes a handshake with our proxy, allowing us to inject whatever traffic we want. Since we are forwarding legitimate traffic from the client, we do not need to reverse traffic (such as the authentication mechanism) because the client will handle that for us.
A proxy consists of three main sections of code:
- A socket to listen for client traffic
- A socket to send traffic to the server
- Logic to relay traffic from the client to the server and the server to the client
To simplify this lesson, we will create a proxy that will respond to the \wave event, identical to the external client we wrote in the previous lab. For these operations, the Wesnoth client must send data to the server for the server to respond. When using a proxy for other operations, additional logic will need to be added in to pass server traffic to the client.
The full code for this lesson is available on github.
Listening for Client Traffic
To listen for client traffic, we will create a listen socket. Once we receive a connection, we will establish a connection with the client. To do this, we can build off the Microsoft server example:
#define DEFAULT_PORT "27015"
WSADATA wsaData;
int iResult;
SOCKET ListenSocket = INVALID_SOCKET;
SOCKET ClientSocket = INVALID_SOCKET;
struct addrinfo* result = NULL,
hints;
iResult = WSAStartup(MAKEWORD(2, 2), &wsaData);
ZeroMemory(&hints, sizeof(hints));
hints.ai_family = AF_INET;
hints.ai_socktype = SOCK_STREAM;
hints.ai_protocol = IPPROTO_TCP;
hints.ai_flags = AI_PASSIVE;
iResult = getaddrinfo(NULL, DEFAULT_PORT, &hints, &result);
ListenSocket = socket(result->ai_family, result->ai_socktype, result->ai_protocol);
iResult = bind(ListenSocket, result->ai_addr, (int)result->ai_addrlen);
freeaddrinfo(result);
iResult = listen(ListenSocket, SOMAXCONN);
ClientSocket = accept(ListenSocket, NULL, NULL);
closesocket(ListenSocket);
This will create a socket on port 27015 that will accept a single connection.
Sending Traffic to Server
For sending traffic to the server, we can build off the code that we already discussed in the previous lesson:
SOCKET ServerSocket = INVALID_SOCKET;
ZeroMemory(&hints, sizeof(hints));
hints.ai_family = AF_INET;
hints.ai_socktype = SOCK_STREAM;
hints.ai_protocol = IPPROTO_TCP;
iResult = getaddrinfo("127.0.0.1", "15000", &hints, &result);
ServerSocket = socket(result->ai_family, result->ai_socktype, result->ai_protocol);
iResult = connect(ServerSocket, result->ai_addr, (int)result->ai_addrlen);
freeaddrinfo(result);
Like we discussed above, our proxy will forward client packets to the server and server packets to the client. However, not all client packets will require a response from the server. For example, in Wesnoth, sending a chat message does not require a response back. To ensure that our proxy does not get stuck waiting for a server response, we need to set a timeout on the server socket. This timeout will cause any recv calls to fail after a set amount of time:
DWORD timeout = 1000;
setsockopt(ServerSocket, SOL_SOCKET, SO_RCVTIMEO, (char*)&timeout, sizeof(timeout));
Relaying Traffic
With both of our sockets created, we can now focus on relaying the traffic between the client and server. Since Wesnoth does not send out server responses unless a client sends a request, our proxy can be simplified to the following events:
- Wait for a request from the client.
- Send that request to the server.
- Wait for a response from the server.
- If a response comes back, send to the client. Otherwise, start waiting for the next client request.
After each event, our program will sleep for a short period to ensure that the traffic between the client and server does not get desynchronized. First, we will wait for a request from the client:
#define DEFAULT_BUFLEN 512
int iSendResult;
unsigned char recvbuf[DEFAULT_BUFLEN];
int recvbuflen = DEFAULT_BUFLEN;
do {
iResult = recv(ClientSocket, (char*)recvbuf, recvbuflen, 0);
Sleep(100);
If we retrieve a request, we pass this data to the server:
if (iResult > 0) {
printf("Bytes received: %d\n", iResult);
iSendResult = send(ServerSocket, (char*)recvbuf, iResult, 0);
Sleep(100);
printf("Bytes sent: %d\n", iSendResult);
Next, we wait for a response from the server. If a response actually comes back, we forward this on to the client:
iResult = recv(ServerSocket, (char*)recvbuf, recvbuflen, 0);
Sleep(100);
if (iResult != SOCKET_ERROR) {
iSendResult = send(ClientSocket, (char*)recvbuf, iResult, 0);
Sleep(100);
}
Finally, we continue the loop while we have a result, or if we have a timeout from the server:
} while (iResult > 0 || WSAGetLastError() == WSAETIMEDOUT);
At this point, our proxy will properly pass traffic from a client to a server. We can verify this by running the proxy, connecting to it in Wesnoth by setting the server to localhost:27015, and confirming that we can connect to the actual server running on localhost:15000.
As a proof-of-concept, we can now add in logic to intercept and modify traffic. First, we can import our parse_data and send_data functions from our last lesson. Next, we will modify our main loop to check any client requests and see if they contain the chat message \wave. If so, we will send an additional packet with a Hello! message:
if (iResult > 0) {
printf("Bytes received: %d\n", iResult);
if (parse_data(recvbuf, iResult)) {
const unsigned char message[] = "[message]\nmessage=\"Hello!\"\nroom=\"lobby\"\nsender=\"ChatBot\"\n[/message]";
send_data(message, sizeof(message), ServerSocket);
Sleep(100);
}
If you connect to the proxy now and send the chat message \wave, you will see that an additional message appears on the server, indicating that we successfully injected traffic into the connection: