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:
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.
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
Then I’ll use the “Create Hotspot” function to create a wireless AP hosted through the VM
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.
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.
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.
Games have text too! But it’s typically embedded in a custom protocol.
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"))
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:
For a crash course on the subject of reverse engineering protocols, I recommend watching PancakesCon 2 - netspooky - Reverse Engineering & ASCII Art For Beginners
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! We can rewrite the text of an NPC.
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/
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:
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!