Now that we have reversed most of the network protocol between the game server and the client, let’s intercept and manipulate it. I personally prefer Python when it’s about to build quick and dirty scripts. Here we need to build an asynchronous proxy that handle binary data. In this case, I will use Python 2.7 with the native libraries asyncore, socket and struct.
Thanks to Wireshark and our dissector, we can visualise and analyse the network traffic between the game server and the client. What would be interesting now is to intercept and manipulate the traffic for “profit”. For instance, changing the spawning coordinate and decide where the user will appear. For this, we will build a proxy in Python.
Initially, we have our client connected directly to your Master/Game Server. The entire communication happen in one single TCP session, which means only one SYN – SYN/ACK – ACK.
In order to intercept the communication, we will have to divert the entire traffic through our proxy. For this, we will configure our client to use our proxy server as Master/Game Server, then on the proxy, forward all to traffic to the actual Master/Game Server and do the same for the communication in the other direction, i.e. traffic from client goes to server, traffic from server goes to client. This means the proxy should maintain two TCP sessions.
In order to re-route the traffic to the proxy, we need to re-configure the client to set the proxy as the Master Server. If you followed the installing procedure from the Pacman’s Army, the easiest solution would be to change your /etc/hosts file and replace the IP of the Master Server with the Proxy (e.g. 10.0.1.4):
# PwnAdventure3 Server 10.0.1.4 pwn3server
Or you can also change your server.ini file:
[MasterServer] Hostname=10.0.1.4 Port=3333
The file server.ini is located at:
- macOS: Pwn Adventure 3.app/Contents/PwnAdventure3/PwnAdventure3.app/Contents/UE4/PwnAdventure3/Content/Server/server.ini
- Linux: PwnAdventure3_Data/PwnAdventure3/PwnAdventure3/Content/Server/server.ini
Once the change applied, the client will now try to connect directly to the Proxy. We therefore need to create a listener on port 3333 (Master Server service). Our script is based on this one from Marek.
This is how it works with asyncore and socket.
import asyncore import socket class ProxServer(asyncore.dispatcher): def __init__(self, src_port): self.src_port = src_port asyncore.dispatcher.__init__(self) self.create_socket(socket.AF_INET, socket.SOCK_STREAM) self.set_reuse_addr() self.bind(('0.0.0.0', src_port)) self.listen(5) if __name__ == '__main__': master = ProxServer(3333) asyncore.loop()
Once a connection is establish, we need to create a new TCP connection with the Master Server. For this, we will use asyncore handle_accept() function:
class ProxServer(asyncore.dispatcher): def __init__(self, src_port, dst_host, dst_port): self.src_port = src_port self.dst_host = dst_host self.dst_port = dst_port asyncore.dispatcher.__init__(self) self.create_socket(socket.AF_INET, socket.SOCK_STREAM) self.set_reuse_addr() self.bind(('0.0.0.0', src_port)) self.listen(5) def handle_accept(self): pair = self.accept() if not pair: return left, addr = pair try: right = socket.create_connection((self.dst_host, self.dst_port)) except socket.error, e: if e.errno is not errno.ECONNREFUSED: raise left.close() def close(self): asyncore.dispatcher.close(self) if __name__ == '__main__': # 10.0.1.3 = Master Server master = ProxServer(3333, '10.0.1.3', 3333) asyncore.loop()
Now that the two TCP connections are established (TCP 1 = left and TCP 2 = right), we need to forward input from TCP 1 to output TCP 2; and forward input TCP 2 to output TCP 1. For this, we edit the handler_accept() to link each other:
def handle_accept(self): pair = self.accept() if not pair: return left, addr = pair try: right = socket.create_connection((self.dst_host, self.dst_port)) except socket.error, e: if e.errno is not errno.ECONNREFUSED: raise left.close() client = Sock(left) server = Sock(right) client.other = server server.other = client
And here is the class Sock() used previously:
class Sock(asyncore.dispatcher): write_buffer = '' def readable(self): return not self.other.write_buffer def handle_read(self): self.other.write_buffer += self.recv(4096*4) def handle_write(self): if self.write_buffer: pkt = parse(self.write_buffer) sent = self.send(pkt) self.write_buffer = self.write_buffer[sent:] def handle_close(self): self.close() if self.other.other: self.other.close() self.other = None
We created a buffer write_buffer for each TCP connection. Whenever one receives data, handle_read() will copy it into the other’s buffer. Whenever the asyncore loop see that the socket is ready to write, it will call the handle_write(), which will send the content of the write_buffer to the TCP connection.
Before the data is sent to the TCP socket, the content is parsed with parse(). This will be our custom function to parse and manipulate the content between the client and the Game Server.
We are now intercepting the network traffic between the client and the Master Server (port 3333), however, the most interesting traffic is the one between the client and the Game Server (hosted on the same IP but different port). Depending on the capacity of the server, the Game Server is typically using port between 3000-3010. If you are hosting the server locally and only a few users are connected, the Game Server will most likely be using port 3000. Therefore, we need to setup another listener on the according ports:
if __name__ == '__main__': master = ProxServer(3333, '10.0.1.3', 3333) game = ProxServer(3000, '10.0.1.3', 3000) print "Proxy ready..." asyncore.loop()
Here you go, now you just need to edit your parse() function to inject, modify and/or delete content in the network communication. You will find the full proxy script in our GitHub repository with some parsing examples.
Blog series: Introduction – Reverse Engineering Network Protocol – Pwn Adventure 3 Network Protocol – Building a Wireshark Parser – Asynchronous Proxy in Python – Intercepting Packets – Reverse Engineering Binary – Patching Binary – Hooking shared library