# DragonSector 2021 - EasyNFT

## Introduction

The challenge gives us a domain `easynft.hackable.software` and a Tarball
([files.tar.gz](./files.tar.gz)), which contains an
[easynft.pcap](./task/easynft.pcap) and a [README.md](./task/README.md)

The `README.md` state:

```  
One of our servers is acting strangely lately.  
There are even rumours that it gives out flags if [asked in a right
way](https://xkcd.com/424/).

Our forensic team didn't find any backdoors, but when we try to list firewall
rules, `nft` just hangs.  
The best we could get is this the netlink dump attached in easynft.pcap.

Could you help us figure out what's going on?

P.S. Obviously, the flag was redacted from the dump.  
```

So apparently, the pcap contains traces of interactions between the `nft`
binary and the `netfilters` backend.

## Pcap analysis

Quickly scanning the capture with wireshark, we identify the typical keywords
from a routing table (`filter`, `input`, `output`, `forward`...), several
interesting keywords (`flag`, `hack`) and a fake flag
`dnrgs{REDACTEDREDACTEDREDACTEDREDACTED}`.

The rest of the data seems binary and we cannot get much more from the
capture.

We also notice that all packets start with a constant **16 bytes** `Linux
netlink (cooked header)`, followed by a variable length and content `Netlink
message`.

## Decoding the packets

My `tshark` jutsu isn't that great, so we start by dumping the whole capture
with an hexdump of each packets:

```  
tshark -r easynft.pcap -x  
```

And we clean it up to keep only the hex bytes on a single line

```  
tshark -r easynft.pcap -x | grep -v "0000" | awk -F "  " '{print $2}' | tr -d ' ' | perl -00 -lpe 'tr/\n//d' | grep -Ev '^\s* > easynft_netlink.dump  
```

We now have a 23 lines file with the `Netlink messages` ready to parse. (see
[easynft_netlink.dump](./easynft_netlink.dump))

As I usually pick go as my first language choice, I've found the
`github.com/mdlayher/netlink` library exposing a promising
[Message.UnmarshalBinary([]byte)
error](https://pkg.go.dev/github.com/mdlayher/[email protected]?utm_source=gopls#Message.UnmarshalBinary)
method we could feed with our packet bytes and see what happen.

We also notice when checking the source code of this method, that the first 4
bytes of the message are **its length**. And some of the packet hex strings
contains more bytes than these first bytes indicate. This means we can have
multiple `netlink.Message` per packet.

From here, the parsing code is quite straigtforward:

```go  
package main

import (  
	"bufio"  
	"encoding/binary"  
	"encoding/hex"  
	"fmt"  
	"os"

	"github.com/mdlayher/netlink"  
)

func main() {  
	f, err := os.Open("hex_netlink.dump")  
	if err != nil {  
		panic(err)  
	}  
	defer f.Close()

	scanner := bufio.NewScanner(f)  
	var messages []netlink.Message

	for scanner.Scan() {  
		packetHex := scanner.Text()

		d, err := hex.DecodeString(packetHex)  
		if err != nil {  
			panic(err)  
		}

		// packet may contains multiple messages, so split on size  
       // https://github.com/mdlayher/netlink/blob/v1.4.1/message.go#L234  
		for {  
			size := binary.LittleEndian.Uint32(d[:4])

			packet := d  
			if len(d) > int(size) {  
				packet = d[:size]  
			}  
			d = d[size:]

			msg := netlink.Message{}  
			if err := msg.UnmarshalBinary(packet); err != nil {  
				panic(fmt.Errorf("failed to unmarshal: %v - packet: %x", err, d))  
			}

			messages = append(messages, msg)  
			if len(d) == 0 {  
				break  
			}  
		}  
	}

	fmt.Printf("Parsed %d messages\n", len(messages))  
	for _, m := range messages {  
		fmt.Printf("%#v\n", m)  
	}  
}  
```

As an output, we get **28 decoded `netlink.Message`**

```  
Parsed 28 messages  
netlink.Message{Header:netlink.Header{Length:0x14, Type:0xa10, Flags:0x1,
Sequence:0x0, PID:0x0}, Data:[]uint8{0x0, 0x0, 0x0, 0x0}}  
netlink.Message{Header:netlink.Header{Length:0x2c, Type:0xa0f, Flags:0x0,
Sequence:0x0, PID:0x200a}, Data:[]uint8{0x0, 0x0, 0xbb, 0x75, 0x8, 0x0, 0x1,
0x0, 0x0, 0x0, 0xbb, 0x75, 0x8, 0x0, 0x2, 0x0, 0x0, 0x0, 0x20, 0xa, 0x8, 0x0,
0x3, 0x0, 0x6e, 0x66, 0x74, 0x0}}  
netlink.Message{Header:netlink.Header{Length:0x14, Type:0xa01, Flags:0x301,
Sequence:0x0, PID:0x0}, Data:[]uint8{0x0, 0x0, 0x0, 0x0}}  
...truncated...  
```

We now have more raw bytes in the `Data` field, which the `Type` field could
help us identify and understand further.

## Identifying Netlink messages

From the pcap, we saw wireshark identified the messages are using the `netlink
netfilter` protocol. Turns out `netfilter` is a sub component of the whole
netfilter framework, responsible of the packet routing. Luckily, another
library exists extending the netlink's one, to provide us the netfilter
decoding features, such as [unmarshalling netlink messages into netfilter
header and attributes](https://pkg.go.dev/github.com/ti-
mo/[email protected]?utm_source=gopls#UnmarshalNetlink)

We can update our previous loop over the messages to add the netfilter
decoding:

```go  
   // add import "github.com/ti-mo/netfilter"  
   fmt.Printf("Parsed %d messages\n", len(messages))  
	for _, m := range messages {  
		header, attrs, err := netfilter.UnmarshalNetlink(m)  
		if err != nil {  
			panic(err)  
		}  
		fmt.Printf("%s\n", header)  
		for _, attr := range attrs {  
			fmt.Printf("\t%s\n", attr.String())  
		}  
	}  
```

This start giving us a bit more info on what's going on:

```  
Parsed 28 messages  
<Subsystem: NFSubsysNFTables, Message Type: 16, Family: ProtoUnspec, Version:
0, ResourceID: 0>  
<Subsystem: NFSubsysNFTables, Message Type: 15, Family: ProtoUnspec, Version:
0, ResourceID: 47989>  
       <Length 4, Type 1, Nested false, NetByteOrder false, [0 0 187 117]>  
       <Length 4, Type 2, Nested false, NetByteOrder false, [0 0 32 10]>  
       <Length 4, Type 3, Nested false, NetByteOrder false, [110 102 116 0]>  
<Subsystem: NFSubsysNFTables, Message Type: 1, Family: ProtoUnspec, Version:
0, ResourceID: 0>  
<Subsystem: NFSubsysNFTables, Message Type: 0, Family: ProtoIPv4, Version: 0,
ResourceID: 47989>  
       <Length 7, Type 1, Nested false, NetByteOrder false, [102 105 108 116 101 114 0]>  
       <Length 4, Type 2, Nested false, NetByteOrder false, [0 0 0 0]>  
       <Length 4, Type 3, Nested false, NetByteOrder false, [0 0 0 5]>  
       <Length 8, Type 4, Nested false, NetByteOrder false, [0 0 0 0 0 0 0 150]>  
<Subsystem: NFSubsysNone, Message Type: 3, Family: ProtoUnspec, Version: 0,
ResourceID: 0>  
...truncated...  
```

Now, it's time to start trying to understand these message types. This has
been a quite long search, which eventually ended in the linux kernel sources,
on the magic `nf_tables_msg_types` enum. (see
[nf_tables.h#L101](https://elixir.bootlin.com/linux/v5.15.5/source/include/uapi/linux/netfilter/nf_tables.h#L101)).

Using the kernel sources comments and navigating the various enum, we end up
replicating them to allow looking up those meaningfull names in place of the
message types using 2 global variables:

```go  
//
https://elixir.bootlin.com/linux/v5.15.5/source/include/uapi/linux/netfilter/nf_tables.h#L101  
var messageTypeNames = []string{  
	"NFT_MSG_NEWTABLE",  
	"NFT_MSG_GETTABLE",  
	"NFT_MSG_DELTABLE",  
	"NFT_MSG_NEWCHAIN",  
	"NFT_MSG_GETCHAIN",  
	"NFT_MSG_DELCHAIN",  
	"NFT_MSG_NEWRULE",  
	"NFT_MSG_GETRULE",  
	// ... truncated ...  
}

var attributeTypeNames = map[string][]string{  
	"NFT_MSG_NEWGEN": { // https://elixir.bootlin.com/linux/v5.15.5/source/include/uapi/linux/netfilter/nf_tables.h#L1505  
		"NFTA_GEN_UNSPEC",  
		"NFTA_GEN_ID",  
		"NFTA_GEN_PROC_PID",  
		"NFTA_GEN_PROC_NAME",  
	},  
	"NFT_MSG_NEWTABLE": { // https://elixir.bootlin.com/linux/v5.15.5/source/include/uapi/linux/netfilter/nf_tables.h#L181  
		"NFTA_TABLE_UNSPEC",  
		"NFTA_TABLE_NAME",  
		"NFTA_TABLE_FLAGS",  
		"NFTA_TABLE_USE",  
		"NFTA_TABLE_HANDLE",  
		"NFTA_TABLE_PAD",  
		"NFTA_TABLE_USERDATA",  
		"NFTA_TABLE_OWNER",  
		"__NFTA_TABLE_MAx",  
	},  
	"NFT_MSG_NEWCHAIN": { // https://elixir.bootlin.com/linux/v5.15.5/source/include/uapi/linux/netfilter/nf_tables.h#L218  
		"NFTA_CHAIN_UNSPEC",  
		"NFTA_CHAIN_TABLE",  
		"NFTA_CHAIN_HANDLE",  
		"NFTA_CHAIN_NAME",  
		"NFTA_CHAIN_HOOK",  
		"NFTA_CHAIN_POLICY",  
   // ... truncated ...  
}  
```

Now we update our loop once again to make use of those pretty names:

```go  
   fmt.Printf("Parsed %d messages\n", len(messages))  
	for _, m := range messages {  
		header, attrs, err := netfilter.UnmarshalNetlink(m)  
		if err != nil {  
			panic(err)  
		}  
       // lookup message name from its message type  
		fmt.Printf("%s\n", messageTypeNames[header.MessageType])  
		for _, attr := range attrs {  
           // lookup attribute name from its message type and attribute type  
           // or just keep default string repr if no name exists  
			attributeName := attr.String()   
			attrNames, ok := attributeTypeNames[messageTypeNames[header.MessageType]]  
			if ok {  
				attributeName = attrNames[int(attr.Type)]  
			}  
			fmt.Printf("\t%s - %q\n", attributeName, attr.Data)  
		}  
	}  
```

and tada! we can now put some sense on all of that:

```  
Parsed 28 messages  
NFT_MSG_GETGEN  
NFT_MSG_NEWGEN  
       NFTA_GEN_ID - "\x00\x00\xbbu"  
       NFTA_GEN_PROC_PID - "\x00\x00 \n"  
       NFTA_GEN_PROC_NAME - "nft\x00"  
NFT_MSG_GETTABLE  
NFT_MSG_NEWTABLE  
       NFTA_TABLE_NAME - "filter\x00"  
       NFTA_TABLE_FLAGS - "\x00\x00\x00\x00"  
       NFTA_TABLE_USE - "\x00\x00\x00\x05"  
       NFTA_TABLE_HANDLE - "\x00\x00\x00\x00\x00\x00\x00\x96"  
NFT_MSG_NEWCHAIN  
NFT_MSG_GETCHAIN  
NFT_MSG_NEWCHAIN  
       NFTA_CHAIN_TABLE - "filter\x00"  
       NFTA_CHAIN_HANDLE - "\x00\x00\x00\x00\x00\x00\x00\x01"  
       NFTA_CHAIN_NAME - "input\x00"  
... truncated ...  
```

From here, we clearly see client requests (such as the one issued using the
`nft` commands - `NFT_MSG_GET...`) and server response (`NFT_MSG_NEW...`),
which then get parsed by `nft` to display tables, rules or whatever was
requested to the terminal.

So we now start to see some human readable table, chains, rules and other
pieces of a routing table:

```  
filter {  
   # chains  
   input {}  
   forward {}  
   output {  
       # rule 0x5  
   }  
   hack {  
       # rule 0xa  
   }  
  
   # set  
   flag {  
       # fake flag stored here  
   }  
}  
```

We're now having **2 rules** and a **set** still containing raw binary that we
have to decode further the attributes.  
Some of these attributes clearly contains multiple sub-attributes, so we can
write a simple function to decode them all:

```go  
func printAttrRecursive(data []byte, level int) {  
	attrs, err := netfilter.UnmarshalAttributes(data)  
	if err != nil {  
		return  
	}

	for _, attr := range attrs {  
		if len(attr.Data) > 0 {  
			fmt.Printf("%sType: %d: %q\n", strings.Repeat("\t", level), attr.Type, attr.Data)  
			printAttrRecursive(attr.Data, level+1)  
		}  
	}  
}  
```

and update our previous loop on attributes:

```go  
       fmt.Printf("%s\n", messageTypeNames[header.MessageType])  
		for _, attr := range attrs {  
			// lookup attribute name from its message type and attribute type  
			// or just keep default string repr if no name exists  
			attributeName := attr.String()  
			attrNames, ok := attributeTypeNames[messageTypeNames[header.MessageType]]  
			if ok {  
				attributeName = attrNames[int(attr.Type)]  
			}

			switch attributeName {  
			case "NFTA_SET_ELEM_LIST_ELEMENTS":  
				fmt.Printf("\t%s\n", attributeName)  
				printAttrRecursive(attr.Data, 2)  
			case "NFTA_RULE_EXPRESSIONS":  
				fmt.Printf("\t%s\n", attributeName)  
				printAttrRecursive(attr.Data, 2)  
			default:  
				fmt.Printf("\t%s - %q\n", attributeName, attr.Data)  
			}  
		}  
```

- Program sources: [./netlinkdump/main.go](./netlinkdump/main.go)  
- Program output: [./netlink.dump](./netlink.dump)

## Decoding the full routing table

Now we'll start manually replacing the various types with their constant
names, by looking up the parent in the kernel sources.

We end up with a nice decoded routing table, after looking up every enum
values / structs for all kind of attributes such as:

- nft_immediate_attributes: https://elixir.bootlin.com/linux/v5.15.5/source/include/uapi/linux/netfilter/nf_tables.h#L531  
- nft_cmp_attributes: https://elixir.bootlin.com/linux/v5.15.5/source/include/uapi/linux/netfilter/nf_tables.h#L648  
- nft_cmp_ops: https://elixir.bootlin.com/linux/v5.15.5/source/include/uapi/linux/netfilter/nf_tables.h#L632  
- nft_payload_attributes: https://elixir.bootlin.com/linux/v5.15.5/source/include/uapi/linux/netfilter/nf_tables.h#L792

```  
filter {  
   # set  
   flag {  
       0: "dnrgs{REDACTEDREDACTEDREDACTEDREDACTED}"  
   }  
  
   # chains  
   input {}  
   forward {}  
   output {  
       immediate {  
           NFTA_IMMEDIATE_DREG: 0  
           NFTA_IMMEDIATE_DATA:   
               NFTA_DATA_VALUE: "\xff\xff\xff\xfd"  
               NFTA_DATA_VERDICT : "hack\x00"  
       }  
   }  
   hack {  
       payload {  
           NFTA_PAYLOAD_DREG:      1  
           NFTA_PAYLOAD_BASE:      NFT_PAYLOAD_TRANSPORT_HEADER  
           NFTA_PAYLOAD_OFFSET:    0x1c  
           NFTA_PAYLOAD_LEN:       8  
       }  
       cmp {  
           NFTA_CMP_SREG:  1  
           NFTA_CMP_OP:    NFT_CMP_EQ  
           NFTA_CMP_DATA:    
               NFTA_DATA_VALUE: dd48d0cfd3103cd4  
       }  
       immediate {  
           NFTA_IMMEDIATE_DREG: 0x12  
           NFTA_IMMEDIATE_DATA:  
               NFTA_DATA_VALUE: 0  
       }  
       lookup {  
           NFTA_LOOKUP_SET:    flag  
           NFTA_LOOKUP_SREG:   0x12  
           NFTA_LOOKUP_DREG:   1  
           NFTA_LOOKUP_FLAGS:  0  
       }  
       payload {  
           NFTA_PAYLOAD_SREG:          1  
           NFTA_PAYLOAD_BASE:          2  
           NFTA_PAYLOAD_OFFSET:        0x3c  
           NFTA_PAYLOAD_LEN:           0x27  
           NFTA_PAYLOAD_CSUM_TYPE:     0  
           NFTA_PAYLOAD_CSUM_OFFSET:   0  
           NFTA_PAYLOAD_CSUM_FLAGS:    0  
       }  
   }  
}  
```

From here, we're almost done! The `hack` rule is now *human readable*, and we
can see that 8 bytes are loaded from our incoming packet (offset `0x1c` from
the `NFT_PAYLOAD_TRANSPORT_HEADER` section), then compared to the
`dd48d0cfd3103cd4` value, and when it matches, the flag value is loaded from
the set, and its `0x27` bytes are written to the response packet at the offset
`0x3c`. It's now time to craft our packet!

## Flag time!

Now having the expected payload and the various offsets, we can craft a
packet. Seeing no mention of any specific protocol or ports on the routing
rules, we could just use any. But given the specific offets of the flag
(`0x3c`), we need somehow to control the response size. We could try forging
some TCP packet, since the port 22 is open, but the response packet is to
small to contains the flag. Some bigger packets could work later during the
SSH handshake, but we have some easier options: ICMP !

Then, let's go with some easy to craft `ICMP echo` packets, where we can send
a packet of at least `0x3c` bytes and get it echoed back:

```go  
package main

import (  
	"bytes"  
	"encoding/hex"  
	"fmt"  
	"log"  
	"math/rand"  
	"net"

	"golang.org/x/net/icmp"  
	"golang.org/x/net/ipv4"  
)

func main() {  
	targetIP := "34.159.43.116"

	payload, _ := hex.DecodeString("dd48d0cfd3103cd4")  
	payloadOffset := 0x1c  
	responseOffset := 0x3c  
	responseSize := 0x27

	// craft the expected payload given the above offets  
	padding := bytes.Repeat([]byte("A"), payloadOffset-len(payload))  
	data := append(padding, payload...)  
	responseFill := bytes.Repeat([]byte("B"), (responseOffset - (payloadOffset + len(payload)) + responseSize))  
	data = append(data, responseFill...)

	// Make a new ICMP message  
	m := icmp.Message{  
		Type: ipv4.ICMPTypeEcho, Code: 0,  
		Body: &icmp.Echo{  
			ID: rand.Int(), Seq: rand.Int(),  
			Data: data,  
		},  
	}  
	packet, err := m.Marshal(nil)  
	if err != nil {  
		panic(err)  
	}

	conn, err := net.Dial("ip4:icmp", targetIP)  
	if err != nil {  
		log.Fatalf("Dial: %s\n", err)  
	}

	n, err := conn.Write(packet)  
	if err != nil {  
		panic(err)  
	}  
	fmt.Printf("write %d bytes\n", n)  
	fmt.Println(hex.Dump(packet))  
}  
```

- Source: [./packetforge/main.go](./packetforge/main.go)

Now fire a tcpdump in a window:

```  
sudo tcpdump -n host 34.159.43.116 -X  
```

and run that script:

```  
sudo go run ./packetforge/main.go  
```

And the flag should show up on the tcpdump window:

```  
	0x0000:  4500 0077 db92 0000 3c01 e215 229f 2b74  E..w....<...".+t  
	0x0010:  c0a8 b222 0000 59cd fd52 164f 4141 4141  ..."..Y..R.OAAAA  
	0x0020:  4141 4141 4141 4141 4141 4141 4141 4141  AAAAAAAAAAAAAAAA  
	0x0030:  dd48 d0cf d310 3cd4 4242 4242 4242 4242  .H....<.BBBBBBBB  
	0x0040:  4242 4242 4242 4242 4242 4242 4242 4242  BBBBBBBBBBBBBBBB  
	0x0050:  4472 676e 537b 6338 6439 3862 3037 6434  DrgnS{c8d98b07d4  
	0x0060:  6332 6634 6133 6363 6332 6663 3130 6234  c2f4a3ccc2fc10b4  
	0x0070:  6636 3238 3135 7d                        f62815}  
```

> Note: we can't just read the packet response from the socket connection in
> the code, as the packet get modified by the routing table, and the checksum
> isn't recomputed (got: 0xfd52, want: 0xc180). We could've read it using a
> lower level connection (such as raw socket & syscalls), but tcpdump does the
> trick here.

## Conclusion

Despite having missed the flag validation by a couple of minutes, this was a
pretty fun and interesting challenge. The multiple rounds of decoding, each
providing some more bits of information kept me hooked. Having only a very
high level of understanding of `nftables` and overall packet routing, diving
in the sources unveiled quite a lot of the magic of it, even if there's still
lots of dark spot!

Original writeup (https://github.com/daeMOn63/ctf-
writeups/tree/main/dragonsector21/easyNFT).# DragonSector 2021 - EasyNFT

## Introduction

The challenge gives us a domain `easynft.hackable.software` and a Tarball
([files.tar.gz](./files.tar.gz)), which contains an
[easynft.pcap](./task/easynft.pcap) and a [README.md](./task/README.md)

The `README.md` state:

```  
One of our servers is acting strangely lately.  
There are even rumours that it gives out flags if [asked in a right
way](https://xkcd.com/424/).

Our forensic team didn't find any backdoors, but when we try to list firewall
rules, `nft` just hangs.  
The best we could get is this the netlink dump attached in easynft.pcap.

Could you help us figure out what's going on?

P.S. Obviously, the flag was redacted from the dump.  
```

So apparently, the pcap contains traces of interactions between the `nft`
binary and the `netfilters` backend.

## Pcap analysis

Quickly scanning the capture with wireshark, we identify the typical keywords
from a routing table (`filter`, `input`, `output`, `forward`...), several
interesting keywords (`flag`, `hack`) and a fake flag
`dnrgs{REDACTEDREDACTEDREDACTEDREDACTED}`.

The rest of the data seems binary and we cannot get much more from the
capture.

We also notice that all packets start with a constant **16 bytes** `Linux
netlink (cooked header)`, followed by a variable length and content `Netlink
message`.

## Decoding the packets

My `tshark` jutsu isn't that great, so we start by dumping the whole capture
with an hexdump of each packets:

```  
tshark -r easynft.pcap -x  
```

And we clean it up to keep only the hex bytes on a single line

```  
tshark -r easynft.pcap -x | grep -v "0000" | awk -F " " '{print $2}' | tr -d ' ' | perl -00 -lpe 'tr/\n//d' | grep -Ev '^\s* > easynft_netlink.dump  
```

We now have a 23 lines file with the `Netlink messages` ready to parse. (see
[easynft_netlink.dump](./easynft_netlink.dump))

As I usually pick go as my first language choice, I've found the
`github.com/mdlayher/netlink` library exposing a promising
[Message.UnmarshalBinary([]byte)
error](https://pkg.go.dev/github.com/mdlayher/[email
protected]?utm_source=gopls#Message.UnmarshalBinary) method we could feed with
our packet bytes and see what happen.

We also notice when checking the source code of this method, that the first 4
bytes of the message are **its length**. And some of the packet hex strings
contains more bytes than these first bytes indicate. This means we can have
multiple `netlink.Message` per packet.

From here, the parsing code is quite straigtforward:

```go  
package main

import (  
"bufio"  
"encoding/binary"  
"encoding/hex"  
"fmt"  
"os"

"github.com/mdlayher/netlink"  
)

func main() {  
f, err := os.Open("hex_netlink.dump")  
if err != nil {  
panic(err)  
}  
defer f.Close()

scanner := bufio.NewScanner(f)  
var messages []netlink.Message

for scanner.Scan() {  
packetHex := scanner.Text()

d, err := hex.DecodeString(packetHex)  
if err != nil {  
panic(err)  
}

// packet may contains multiple messages, so split on size  
// https://github.com/mdlayher/netlink/blob/v1.4.1/message.go#L234  
for {  
size := binary.LittleEndian.Uint32(d[:4])

packet := d  
if len(d) > int(size) {  
packet = d[:size]  
}  
d = d[size:]

msg := netlink.Message{}  
if err := msg.UnmarshalBinary(packet); err != nil {  
panic(fmt.Errorf("failed to unmarshal: %v - packet: %x", err, d))  
}

messages = append(messages, msg)  
if len(d) == 0 {  
break  
}  
}  
}

fmt.Printf("Parsed %d messages\n", len(messages))  
for _, m := range messages {  
fmt.Printf("%#v\n", m)  
}  
}  
```

As an output, we get **28 decoded `netlink.Message`**

```  
Parsed 28 messages  
netlink.Message{Header:netlink.Header{Length:0x14, Type:0xa10, Flags:0x1,
Sequence:0x0, PID:0x0}, Data:[]uint8{0x0, 0x0, 0x0, 0x0}}  
netlink.Message{Header:netlink.Header{Length:0x2c, Type:0xa0f, Flags:0x0,
Sequence:0x0, PID:0x200a}, Data:[]uint8{0x0, 0x0, 0xbb, 0x75, 0x8, 0x0, 0x1,
0x0, 0x0, 0x0, 0xbb, 0x75, 0x8, 0x0, 0x2, 0x0, 0x0, 0x0, 0x20, 0xa, 0x8, 0x0,
0x3, 0x0, 0x6e, 0x66, 0x74, 0x0}}  
netlink.Message{Header:netlink.Header{Length:0x14, Type:0xa01, Flags:0x301,
Sequence:0x0, PID:0x0}, Data:[]uint8{0x0, 0x0, 0x0, 0x0}}  
...truncated...  
```

We now have more raw bytes in the `Data` field, which the `Type` field could
help us identify and understand further.

## Identifying Netlink messages

From the pcap, we saw wireshark identified the messages are using the `netlink
netfilter` protocol. Turns out `netfilter` is a sub component of the whole
netfilter framework, responsible of the packet routing. Luckily, another
library exists extending the netlink's one, to provide us the netfilter
decoding features, such as [unmarshalling netlink messages into netfilter
header and attributes](https://pkg.go.dev/github.com/ti-mo/[email
protected]?utm_source=gopls#UnmarshalNetlink)

We can update our previous loop over the messages to add the netfilter
decoding:

```go  
// add import "github.com/ti-mo/netfilter"  
fmt.Printf("Parsed %d messages\n", len(messages))  
for _, m := range messages {  
header, attrs, err := netfilter.UnmarshalNetlink(m)  
if err != nil {  
panic(err)  
}  
fmt.Printf("%s\n", header)  
for _, attr := range attrs {  
fmt.Printf("\t%s\n", attr.String())  
}  
}  
```

This start giving us a bit more info on what's going on:

```  
Parsed 28 messages  
<Subsystem: NFSubsysNFTables, Message Type: 16, Family: ProtoUnspec, Version:
0, ResourceID: 0>  
<Subsystem: NFSubsysNFTables, Message Type: 15, Family: ProtoUnspec, Version:
0, ResourceID: 47989>  
<Length 4, Type 1, Nested false, NetByteOrder false, [0 0 187 117]>  
<Length 4, Type 2, Nested false, NetByteOrder false, [0 0 32 10]>  
<Length 4, Type 3, Nested false, NetByteOrder false, [110 102 116 0]>  
<Subsystem: NFSubsysNFTables, Message Type: 1, Family: ProtoUnspec, Version:
0, ResourceID: 0>  
<Subsystem: NFSubsysNFTables, Message Type: 0, Family: ProtoIPv4, Version: 0,
ResourceID: 47989>  
<Length 7, Type 1, Nested false, NetByteOrder false, [102 105 108 116 101 114
0]>  
<Length 4, Type 2, Nested false, NetByteOrder false, [0 0 0 0]>  
<Length 4, Type 3, Nested false, NetByteOrder false, [0 0 0 5]>  
<Length 8, Type 4, Nested false, NetByteOrder false, [0 0 0 0 0 0 0 150]>  
<Subsystem: NFSubsysNone, Message Type: 3, Family: ProtoUnspec, Version: 0,
ResourceID: 0>  
...truncated...  
```

Now, it's time to start trying to understand these message types. This has
been a quite long search, which eventually ended in the linux kernel sources,
on the magic `nf_tables_msg_types` enum. (see
[nf_tables.h#L101](https://elixir.bootlin.com/linux/v5.15.5/source/include/uapi/linux/netfilter/nf_tables.h#L101)).

Using the kernel sources comments and navigating the various enum, we end up
replicating them to allow looking up those meaningfull names in place of the
message types using 2 global variables:

```go  
//
https://elixir.bootlin.com/linux/v5.15.5/source/include/uapi/linux/netfilter/nf_tables.h#L101  
var messageTypeNames = []string{  
"NFT_MSG_NEWTABLE",  
"NFT_MSG_GETTABLE",  
"NFT_MSG_DELTABLE",  
"NFT_MSG_NEWCHAIN",  
"NFT_MSG_GETCHAIN",  
"NFT_MSG_DELCHAIN",  
"NFT_MSG_NEWRULE",  
"NFT_MSG_GETRULE",  
// ... truncated ...  
}

var attributeTypeNames = map[string][]string{  
"NFT_MSG_NEWGEN": { //
https://elixir.bootlin.com/linux/v5.15.5/source/include/uapi/linux/netfilter/nf_tables.h#L1505  
"NFTA_GEN_UNSPEC",  
"NFTA_GEN_ID",  
"NFTA_GEN_PROC_PID",  
"NFTA_GEN_PROC_NAME",  
},  
"NFT_MSG_NEWTABLE": { //
https://elixir.bootlin.com/linux/v5.15.5/source/include/uapi/linux/netfilter/nf_tables.h#L181  
"NFTA_TABLE_UNSPEC",  
"NFTA_TABLE_NAME",  
"NFTA_TABLE_FLAGS",  
"NFTA_TABLE_USE",  
"NFTA_TABLE_HANDLE",  
"NFTA_TABLE_PAD",  
"NFTA_TABLE_USERDATA",  
"NFTA_TABLE_OWNER",  
"__NFTA_TABLE_MAx",  
},  
"NFT_MSG_NEWCHAIN": { //
https://elixir.bootlin.com/linux/v5.15.5/source/include/uapi/linux/netfilter/nf_tables.h#L218  
"NFTA_CHAIN_UNSPEC",  
"NFTA_CHAIN_TABLE",  
"NFTA_CHAIN_HANDLE",  
"NFTA_CHAIN_NAME",  
"NFTA_CHAIN_HOOK",  
"NFTA_CHAIN_POLICY",  
// ... truncated ...  
}  
```

Now we update our loop once again to make use of those pretty names:

```go  
fmt.Printf("Parsed %d messages\n", len(messages))  
for _, m := range messages {  
header, attrs, err := netfilter.UnmarshalNetlink(m)  
if err != nil {  
panic(err)  
}  
// lookup message name from its message type  
fmt.Printf("%s\n", messageTypeNames[header.MessageType])  
for _, attr := range attrs {  
// lookup attribute name from its message type and attribute type  
// or just keep default string repr if no name exists  
attributeName := attr.String()  
attrNames, ok := attributeTypeNames[messageTypeNames[header.MessageType]]  
if ok {  
attributeName = attrNames[int(attr.Type)]  
}  
fmt.Printf("\t%s - %q\n", attributeName, attr.Data)  
}  
}  
```

and tada! we can now put some sense on all of that:

```  
Parsed 28 messages  
NFT_MSG_GETGEN  
NFT_MSG_NEWGEN  
NFTA_GEN_ID - "\x00\x00\xbbu"  
NFTA_GEN_PROC_PID - "\x00\x00 \n"  
NFTA_GEN_PROC_NAME - "nft\x00"  
NFT_MSG_GETTABLE  
NFT_MSG_NEWTABLE  
NFTA_TABLE_NAME - "filter\x00"  
NFTA_TABLE_FLAGS - "\x00\x00\x00\x00"  
NFTA_TABLE_USE - "\x00\x00\x00\x05"  
NFTA_TABLE_HANDLE - "\x00\x00\x00\x00\x00\x00\x00\x96"  
NFT_MSG_NEWCHAIN  
NFT_MSG_GETCHAIN  
NFT_MSG_NEWCHAIN  
NFTA_CHAIN_TABLE - "filter\x00"  
NFTA_CHAIN_HANDLE - "\x00\x00\x00\x00\x00\x00\x00\x01"  
NFTA_CHAIN_NAME - "input\x00"  
... truncated ...  
```

From here, we clearly see client requests (such as the one issued using the
`nft` commands - `NFT_MSG_GET...`) and server response (`NFT_MSG_NEW...`),
which then get parsed by `nft` to display tables, rules or whatever was
requested to the terminal.

So we now start to see some human readable table, chains, rules and other
pieces of a routing table:

```  
filter {  
# chains  
input {}  
forward {}  
output {  
# rule 0x5  
}  
hack {  
# rule 0xa  
}  
  
# set  
flag {  
# fake flag stored here  
}  
}  
```

We're now having **2 rules** and a **set** still containing raw binary that we
have to decode further the attributes.  
Some of these attributes clearly contains multiple sub-attributes, so we can
write a simple function to decode them all:

```go  
func printAttrRecursive(data []byte, level int) {  
attrs, err := netfilter.UnmarshalAttributes(data)  
if err != nil {  
return  
}

for _, attr := range attrs {  
if len(attr.Data) > 0 {  
fmt.Printf("%sType: %d: %q\n", strings.Repeat("\t", level), attr.Type,
attr.Data)  
printAttrRecursive(attr.Data, level+1)  
}  
}  
}  
```

and update our previous loop on attributes:

```go  
fmt.Printf("%s\n", messageTypeNames[header.MessageType])  
for _, attr := range attrs {  
// lookup attribute name from its message type and attribute type  
// or just keep default string repr if no name exists  
attributeName := attr.String()  
attrNames, ok := attributeTypeNames[messageTypeNames[header.MessageType]]  
if ok {  
attributeName = attrNames[int(attr.Type)]  
}

switch attributeName {  
case "NFTA_SET_ELEM_LIST_ELEMENTS":  
fmt.Printf("\t%s\n", attributeName)  
printAttrRecursive(attr.Data, 2)  
case "NFTA_RULE_EXPRESSIONS":  
fmt.Printf("\t%s\n", attributeName)  
printAttrRecursive(attr.Data, 2)  
default:  
fmt.Printf("\t%s - %q\n", attributeName, attr.Data)  
}  
}  
```

\- Program sources: [./netlinkdump/main.go](./netlinkdump/main.go)  
\- Program output: [./netlink.dump](./netlink.dump)

## Decoding the full routing table

Now we'll start manually replacing the various types with their constant
names, by looking up the parent in the kernel sources.

We end up with a nice decoded routing table, after looking up every enum
values / structs for all kind of attributes such as:

\- nft_immediate_attributes:
https://elixir.bootlin.com/linux/v5.15.5/source/include/uapi/linux/netfilter/nf_tables.h#L531  
\- nft_cmp_attributes:
https://elixir.bootlin.com/linux/v5.15.5/source/include/uapi/linux/netfilter/nf_tables.h#L648  
\- nft_cmp_ops:
https://elixir.bootlin.com/linux/v5.15.5/source/include/uapi/linux/netfilter/nf_tables.h#L632  
\- nft_payload_attributes:
https://elixir.bootlin.com/linux/v5.15.5/source/include/uapi/linux/netfilter/nf_tables.h#L792

```  
filter {  
# set  
flag {  
0: "dnrgs{REDACTEDREDACTEDREDACTEDREDACTED}"  
}  
  
# chains  
input {}  
forward {}  
output {  
immediate {  
NFTA_IMMEDIATE_DREG: 0  
NFTA_IMMEDIATE_DATA:  
NFTA_DATA_VALUE: "\xff\xff\xff\xfd"  
NFTA_DATA_VERDICT : "hack\x00"  
}  
}  
hack {  
payload {  
NFTA_PAYLOAD_DREG: 1  
NFTA_PAYLOAD_BASE: NFT_PAYLOAD_TRANSPORT_HEADER  
NFTA_PAYLOAD_OFFSET: 0x1c  
NFTA_PAYLOAD_LEN: 8  
}  
cmp {  
NFTA_CMP_SREG: 1  
NFTA_CMP_OP: NFT_CMP_EQ  
NFTA_CMP_DATA:  
NFTA_DATA_VALUE: dd48d0cfd3103cd4  
}  
immediate {  
NFTA_IMMEDIATE_DREG: 0x12  
NFTA_IMMEDIATE_DATA:  
NFTA_DATA_VALUE: 0  
}  
lookup {  
NFTA_LOOKUP_SET: flag  
NFTA_LOOKUP_SREG: 0x12  
NFTA_LOOKUP_DREG: 1  
NFTA_LOOKUP_FLAGS: 0  
}  
payload {  
NFTA_PAYLOAD_SREG: 1  
NFTA_PAYLOAD_BASE: 2  
NFTA_PAYLOAD_OFFSET: 0x3c  
NFTA_PAYLOAD_LEN: 0x27  
NFTA_PAYLOAD_CSUM_TYPE: 0  
NFTA_PAYLOAD_CSUM_OFFSET: 0  
NFTA_PAYLOAD_CSUM_FLAGS: 0  
}  
}  
}  
```

From here, we're almost done! The `hack` rule is now *human readable*, and we
can see that 8 bytes are loaded from our incoming packet (offset `0x1c` from
the `NFT_PAYLOAD_TRANSPORT_HEADER` section), then compared to the
`dd48d0cfd3103cd4` value, and when it matches, the flag value is loaded from
the set, and its `0x27` bytes are written to the response packet at the offset
`0x3c`. It's now time to craft our packet!

## Flag time!

Now having the expected payload and the various offsets, we can craft a
packet. Seeing no mention of any specific protocol or ports on the routing
rules, we could just use any. But given the specific offets of the flag
(`0x3c`), we need somehow to control the response size. We could try forging
some TCP packet, since the port 22 is open, but the response packet is to
small to contains the flag. Some bigger packets could work later during the
SSH handshake, but we have some easier options: ICMP !

Then, let's go with some easy to craft `ICMP echo` packets, where we can send
a packet of at least `0x3c` bytes and get it echoed back:

```go  
package main

import (  
"bytes"  
"encoding/hex"  
"fmt"  
"log"  
"math/rand"  
"net"

"golang.org/x/net/icmp"  
"golang.org/x/net/ipv4"  
)

func main() {  
targetIP := "34.159.43.116"

payload, _ := hex.DecodeString("dd48d0cfd3103cd4")  
payloadOffset := 0x1c  
responseOffset := 0x3c  
responseSize := 0x27

// craft the expected payload given the above offets  
padding := bytes.Repeat([]byte("A"), payloadOffset-len(payload))  
data := append(padding, payload...)  
responseFill := bytes.Repeat([]byte("B"), (responseOffset - (payloadOffset +
len(payload)) + responseSize))  
data = append(data, responseFill...)

// Make a new ICMP message  
m := icmp.Message{  
Type: ipv4.ICMPTypeEcho, Code: 0,  
Body: &icmp.Echo{  
ID: rand.Int(), Seq: rand.Int(),  
Data: data,  
},  
}  
packet, err := m.Marshal(nil)  
if err != nil {  
panic(err)  
}

conn, err := net.Dial("ip4:icmp", targetIP)  
if err != nil {  
log.Fatalf("Dial: %s\n", err)  
}

n, err := conn.Write(packet)  
if err != nil {  
panic(err)  
}  
fmt.Printf("write %d bytes\n", n)  
fmt.Println(hex.Dump(packet))  
}  
```

\- Source: [./packetforge/main.go](./packetforge/main.go)

Now fire a tcpdump in a window:

```  
sudo tcpdump -n host 34.159.43.116 -X  
```

and run that script:

```  
sudo go run ./packetforge/main.go  
```

And the flag should show up on the tcpdump window:

```  
0x0000: 4500 0077 db92 0000 3c01 e215 229f 2b74 E..w....<...".+t  
0x0010: c0a8 b222 0000 59cd fd52 164f 4141 4141 ..."..Y..R.OAAAA  
0x0020: 4141 4141 4141 4141 4141 4141 4141 4141 AAAAAAAAAAAAAAAA  
0x0030: dd48 d0cf d310 3cd4 4242 4242 4242 4242 .H....<.BBBBBBBB  
0x0040: 4242 4242 4242 4242 4242 4242 4242 4242 BBBBBBBBBBBBBBBB  
0x0050: 4472 676e 537b 6338 6439 3862 3037 6434 DrgnS{c8d98b07d4  
0x0060: 6332 6634 6133 6363 6332 6663 3130 6234 c2f4a3ccc2fc10b4  
0x0070: 6636 3238 3135 7d f62815}  
```

> Note: we can't just read the packet response from the socket connection in
> the code, as the packet get modified by the routing table, and the checksum
> isn't recomputed (got: 0xfd52, want: 0xc180). We could've read it using a
> lower level connection (such as raw socket & syscalls), but tcpdump does the
> trick here.

## Conclusion

Despite having missed the flag validation by a couple of minutes, this was a
pretty fun and interesting challenge. The multiple rounds of decoding, each
providing some more bits of information kept me hooked. Having only a very
high level of understanding of `nftables` and overall packet routing, diving
in the sources unveiled quite a lot of the magic of it, even if there's still
lots of dark spot!

Original writeup (https://github.com/daeMOn63/ctf-
writeups/tree/main/dragonsector21/easyNFT).