Packet-Editing Games in Golang

Jul 16, 2021

8 mins read

It’s easy to set up an IDS or other infrastructure to drop packets that match rules. There are many tools for real-time inspection of connections that can handle higher level protocols like HTTP or TLS. This article aims to go a bit lower and address how to edit packets in flight. We’ll be looking at it through the lens of editing packets for a game using Golang.

Some basic guidelines / personal policy:

  • This is not a guide of how to cheat at online games and should not be used as such.
  • There are plenty of old multiplayer games that have been lost to the tides of time with copyright’s long expired that are perfect for learning how they work.
  • Examples as shown are editing packets Server –> Client. Any changes to client state could be done much easier using Cheat Engine etc…
  • No modified/forged packets are sent from Client –> Server in any examples. There is a very obvious reason for this.
  • Cheating ruins games for others and yourself.

With that out of the way, let’s get started:

If you’re interested in more projects like this, give me a follow on Twitter @_mattata. I’m always working on something fun.

Tools

You should generally be familiar with Wireshark, general networking, and Go.

For ease of use and a controlled environment, I usually start by downloading a Ubuntu Desktop ISO and spin up a Virtual machine in VirtualBox.

I’ll add a USB filter to expose a Wireless USB to the VM USB Filter)

Then I’ll use the “Create Hotspot” function to create a wireless AP hosted through the VM Hotspot)

You could of course also do this with a custom DHCP setup with IP Forwarding and maybe even a selective VPN. I’m lazy, so I do this because it takes 3 clicks and I’m done. The main idea is to be able to route traffic through the VM. However you want to accomplish that is up to you.

Basic Example

Here is a simple example that will route packets from TCP source port 9999 into nfqueue 0. The Go code will trigger a callback when a packet enters the queue which allows us to modify the data using the gopacket library. Once we are done with the packet, we can issue a verdict to allow the modified packet out of the queue and continue on to it’s destination.

In this case, we’re looking for packets containing “magic string” which we will replace with “modified value”

package main

import (
	"bytes"
	"encoding/hex"
	"fmt"
	"io/ioutil"
	"log"
	"os"
	"os/exec"
	"os/signal"
	"strings"
	"syscall"

	"github.com/chifflier/nfqueue-go/nfqueue"

	"github.com/sergi/go-diff/diffmatchpatch"

	"github.com/google/gopacket"
	"github.com/google/gopacket/layers"
)

func realCallback(payload *nfqueue.Payload) int {
	// Decode a packet
	packet := gopacket.NewPacket(payload.Data, layers.LayerTypeIPv4, gopacket.Default)
	// Get the TCP layer from this packet
	if tcpLayer := packet.Layer(layers.LayerTypeTCP); tcpLayer != nil {
		// Get actual TCP data from this layer
		tcp, _ := tcpLayer.(*layers.TCP)
		fmt.Printf("From src port %d to dst port %d\n", tcp.SrcPort, tcp.DstPort)
	}
	//Log Initial State
	fmt.Printf("  id: %d\n", payload.Id)
	fmt.Println(hex.Dump(payload.Data))
	if app := packet.ApplicationLayer(); app != nil {
		if strings.Contains(string(app.Payload()), "magic string") {
			// modify payload of application layer
			*packet.ApplicationLayer().(*gopacket.Payload) = bytes.ReplaceAll(app.Payload(), []byte("magic string"), []byte("modified value"))
			// if its tcp we need to tell it which network layer is being used
			// to be able to handle multiple protocols we can add a if clause around this
			packet.TransportLayer().(*layers.TCP).SetNetworkLayerForChecksum(packet.NetworkLayer())

			buffer := gopacket.NewSerializeBuffer()
			options := gopacket.SerializeOptions{
				ComputeChecksums: true,
				FixLengths:       true,
			}

			// Serialize Packet to get raw bytes
			if err := gopacket.SerializePacket(buffer, options, packet); err != nil {
				log.Fatalln(err)
			}

			packetBytes := buffer.Bytes()
			
			//Pretty color diff on the hexdump
			dmp := diffmatchpatch.New()
			diffs := dmp.DiffMain(hex.Dump(payload.Data), hex.Dump(packetBytes), true)
			fmt.Println(dmp.DiffPrettyText(diffs))
			//Set the packet verdict as modified
			payload.SetVerdictModified(nfqueue.NF_ACCEPT, packetBytes)
			return 0
		}
	}
	fmt.Println("-- ")
	payload.SetVerdict(nfqueue.NF_ACCEPT)
	return 0
}

func main() {
	//Create go nfqueue
	q := new(nfqueue.Queue)
	//Set callback for queue
	q.SetCallback(realCallback)
	//Initialize queue
	q.Init()
	//Generic reset for bind
	q.Unbind(syscall.AF_INET)
	q.Bind(syscall.AF_INET)
	//Create nfqueue "0"
	q.CreateQueue(0)

	//Set iptables rule to route packets from sourc eport 9999 to queue number 0
	cmd := exec.Command("iptables", "-t", "raw", "-A", "PREROUTING", "-p", "tcp", "--source-port", "9999", "-j", "NFQUEUE", "--queue-num", "0")
	stdout, err := cmd.Output()

	if err != nil {
		fmt.Println(err.Error())
		return
	} else {
		fmt.Println(string(stdout))
	}

	//Listener for CNTRL+C
	c := make(chan os.Signal, 1)
	signal.Notify(c, os.Interrupt)
	log.SetOutput(ioutil.Discard)
	go func() {
		for sig := range c {
			// sig is a ^C, handle it
			_ = sig
			q.StopLoop()
		}
	}()

	// XXX Drop privileges here

	q.Loop()
	q.DestroyQueue()
	q.Close()

	//Remove iptables rules that route packets into nfqueue
	unroute := exec.Command("iptables", "-F", "-t", "raw")
	stdoutUnroute, err := unroute.Output()

	if err != nil {
		fmt.Println(err.Error())
		return
	} else {
		fmt.Println(string(stdoutUnroute))
	}

	os.Exit(0)
}

So let’s see it in action eh? You’ll need libnetfilter-queue-dev installed in order to compile.

Outside of the VM, open a netcat listener on port 9999

nc -vvv -l 192.168.8.154 9999

Now, from inside the VM open a terminal and connect to the netcat listener outside the VM:

nc -vvv 192.168.8.154

We’ll type the string “hello” and see that it goes through without issue.

Then we’ll type the string “magic string” and note that is is modified in flight.

Basic

The packet was modified because it had a source port of 9999, was routed into nfqueue 0, and contained the string “magic string”, so we replaced it before allowing it to continue to it’s destination.

Game Example

Games have text too! But it’s typically embedded in a custom protocol.

Chatbox

00000070  00 0a 7c d9 e7 00 06 00  37 02 7b 00 12 46 61 74  |..|.....7.{..Fat|
00000080  68 65 72 20 41 65 72 65  63 6b 00 00 e7 00 03 a3  |her Aereck......|
00000090  00 2d 57 65 6c 63 6f 6d  65 20 74 6f 20 74 68 65  |.-Welcome to the|
000000a0  20 63 68 75 72 63 68 20  6f 66 20 68 6f 6c 79 20  | church of holy |
000000b0  53 61 72 61 64 6f 6d 69  6e 2e 00 00 e7 00 05 a0  |Saradomin.......

Let’s see if we can replace the characters name “Father Aereck” with “Remy”

// modify payload of application layer
*packet.ApplicationLayer().(*gopacket.Payload) = bytes.ReplaceAll(app.Payload(), []byte("Father Aereck"), []byte("REMY"))

Connection lost

We can see that the bytes were replaced, but the game client glitched out. What happened?

The client reset because the custom protocol used for it couldn’t be parsed correctly! Even outside of editing packets, most protocols will have error handling for situations like this. Packet errors absolutely do occur in the real world.

But why wasn’t the protocol parsed correctly? All we did was change some text!

Incorrect! We changed the text and the length of the text. Unless you’re dealing with a text based protocol, you’ll usually encounter something called TLV in binary protocols.

We’re dealing with a binary protocol now, not a text based protocol like a simple netcat pipe.

The type and length are fixed in size (typically 1-4 bytes), and the value field is of variable size. These fields are used as follows:

  • Type
    • A binary code, often simply alphanumeric, which indicates the kind of field that this part of the message represents;
  • Length
    • The size of the value field (typically in bytes);
  • Value
    • Variable-sized series of bytes which contains data for this part of the message.

For a crash course on the subject of reverse engineering protocols, I recommend watching PancakesCon 2 - netspooky - Reverse Engineering & ASCII Art For Beginners

Attempt 2

This time, we’re going to replace the text, but we’re going to pad the replaced text so that it matches the same length of the original value using spaces

//OLD
//bytes.ReplaceAll(app.Payload(), []byte("Father Aereck"), []byte("REMY"))
//New
bytes.ReplaceAll(app.Payload(), []byte("Father Aereck"), []byte("REMY         "))

success

Success! We can rewrite the text of an NPC.

Next Steps

Start looking into where the Length (L) value is defined in the protocol so that you don’t need to arbitrarily pad the Value (V). Once you know the format of the L and V, you might start playing with different Types (T).

From there, you’re well on your way to understanding how things work behind the scenes.

This will of course require staring at hex output, but there are tools that make it much easier. A method called “differential analysis” can be used to narrow down specific pieces of a protocol so that they are easier to understand.

A useful tool for this is pDiff which is demoed in the PancakesCon youtube video linked above.

You can also use a WebAssembly implementation in-browser that I wrote here: https://remyhax.xyz/tools/pdiffwasm/

Closing Notes

The setup I use with a hosted Wifi AP allows packet editing for anything with WiFi support which keeps it versatile. iPhone, Android, Playstation, XBOX, Nintendo, etc…

The examples above explicitly modify packets destined for the client. You probably aren’t allowed to (and shouldn’t!) modify packets destined to the server.

Again, and I cannot be any more explicit about this:

  • Cheating ruins games for everyone. Don’t do it.
  • Archive.org has tons of old games you can self host to poke around.
  • Don’t mess with servers you don’t own or manage.

That being said, dissecting and playing with protocols is extremely fun.

Dissect the protocol because it’s fun.

Play the games because they’re fun.

Don’t mix those two.

-remy

Sharing is caring!