close search bar

Sorry, not available in this language yet

close language selection

How to Cyber Security: Fuzz a tank

Jonathan Knudsen

Jul 12, 2020 / 7 min read

Defensics is a generational fuzzer, which means it creates test cases based on a detailed model of the input data. The result: test cases that are very realistic but messed up in some way. This technique is highly effective in burrowing into different control paths in the target and revealing vulnerabilities. Subjectively speaking, the test cases have high quality.

The disadvantage of generational fuzzing is that somebody has to create the data model for the inputs you are fuzzing. Luckily, Defensics already has an impressive array of prebuilt test suites, more than 250 of them, that cover many common network protocols and file formats. It’s like one of those expansive diner menus where you can order everything from scrambled eggs to moo shu pork.

Even so, you will sometimes have to test a piece of software that Defensics does not already have an appropriate test suite for. Maybe it is a proprietary protocol or something relatively obscure.

Regardless, the Defensics SDK allows you to harness the power of Defensics to create test suites for any type of data. In this article, I’ll walk through how easy it is to create such a test suite. I won’t cover the basics of setting up and using the Defensics SDK. For that information, consult the documentation. This article will highlight how to start modeling a custom protocol.

Our target: bzfs

For this example, our target software will be the server component of an open source tank battle game, BZFlag. BZFlag supports multiplayer games, where all players connect to a central server, bzfs.

It is well known that BZFlag has serious security flaws in its design. In particular, BZFlag clients are given much of the power in determining the course of gameplay. For example, the BZFlag client is responsible for reporting when it has been hit by a bullet and has blown up. Obviously, modified BZFlag clients can cheat widely and creatively. By modifying the source code, it is possible to create tanks that never die, tanks that hop like frogs, and more.

Our investigation here is not about application design vulnerabilities. Instead, we will focus on how the clients and game server communicate: via a proprietary network protocol carried on top of standard TCP connections and UDP datagrams. We won’t worry about the UDP messages during gameplay but will instead focus on the TCP-based negotiation when BZFlag clients join a server.

bzfs network protocol

The protocol is documented on a page that loudly proclaims its own inaccuracy. However, between this page and a capture of actual network traffic, we have enough information to model part of the protocol with the Defensics SDK.

Here is part of a conversation between a client (red) and a server (blue):

def client server

Hello, BZFlag!

Let’s start at the very beginning. After the client establishes a TCP connection to the server, it sends a client hello message and expects a server hello in response.

The client hello is simply the string “BZFLAG” followed by two sets of carriage return and line feed. This is simple to model in BNF:


CR = 0x0D       # US-ASCII CR, carriage return (13)
LF = 0x0A       # US-ASCII LF, linefeed (10)
CRLF = (CR LF)
ClientHello = ('BZFLAG' CRLF CRLF)

The server responds with a hello message containing a four-digit version number. It’s not clear what terminates the server hello; the protocol page says 0xFF, but the network capture shows a 0x00. We’ll build the model to handle either terminator.


NULL = 0x00
FF = 0xFF
DIGIT  = (0x30-0x39)
ServerHello = ('BZFS' 4(DIGIT) (NULL | FF))

Pulling these BNF definitions into the Java source code for our fuzzer is straightforward:


public void build(BuilderTools bt) throws IOException, EngineException {
  ElementFactory ef = bt.factory();

  ef.readTypes(bt.getPathToResource("bzflag.bnf"));
  MessageElement clientHello = ef.field("ClientHello", "ClientHello");
  MessageElement serverHello = ef.field("ServerHello", "ServerHello");

Then, defining a simple sequence where Defensics sends the client hello and expects a server hello in response looks like this:


// Injector tcp = ...
MessageElement sequence = ef.sequence(
  tcp.send(clientHello),
  tcp.receive(serverHello)
);

bt.setSequence(sequence);

This is enough of a data model to start testing! Defensics will apply anomalizations to the outgoing client hello message. It delivers the generated test cases to the target and expects to receive the server hello message in response.

Applying a length rule

The client hello and server hello messages are unique in the BZFlag protocol. All other messages in the conversation have this format:

  • Two-byte payload length
  • Two-byte message type
  • Payload (0..n bytes)

In the network capture above, for example, the second message that the client sends to the server has 6e 66 for the message type, which corresponds to MsgNegotiateFlags. The 00 5e at the very beginning is the length of the payload.

As a quick first cut, we’ll use the existing payload verbatim, but we’ll correctly model the length field. When Defensics creates test cases that anomalize the payload, it will set the length field correctly, which will help the test cases burrow into the target.

Let’s begin by defining the length rule in the Java source code, before we pull in the BNF definitions. That way, we can use the length rule in the BNF directly.


/RuleFactory rf = bt.rule()
rf.length("length16").format("int-16bit");

This simply instantiates a rule named length16 that is represented by a 16-bit integer.

In the BNF, let’s define a length field that we can use for any message type.


Length = 0x0000-0xffff

Next, we’ll use the MsgNegotiateFlags payload from the capture:


MsgNegotiateFlagsPayload = (
0x00 0x00 0x41 0x00
0x42 0x00 0x42 0x2a 0x42 0x55 0x42 0x59
0x43 0x42 0x43 0x4c 0x46 0x00 0x46 0x4f
0x47 0x00 0x47 0x2a 0x47 0x4d 0x49 0x42
0x49 0x44 0x4a 0x4d 0x4a 0x50 0x4c 0x00
0x4c 0x54 0x4d 0x00 0x4d 0x47 0x4d 0x51
0x4e 0x00 0x4e 0x4a 0x4f 0x00 0x4f 0x4f
0x50 0x2a 0x50 0x5a 0x51 0x54 0x52 0x00
0x52 0x2a 0x52 0x43 0x52 0x4f 0x52 0x54
0x53 0x42 0x53 0x45 0x53 0x48 0x53 0x52
0x53 0x54 0x53 0x57 0x54 0x00 0x54 0x48
0x54 0x52 0x55 0x53 0x56 0x00 0x57 0x41
0x57 0x47
)

Then we’ll tie them together into a complete message with the correct message type bytes. We’ll reference the length16 rule, indicating that the payload is the source for the rule and the target is the Length field.


MsgNegotiateFlags = @length16 (
  .length: !length16:target Length
  .type: 0x6e66
  !length16:source MsgNegotiateFlagsPayload
)

Let’s break down the various pieces of this definition:

  • @length16 indicates that the named rule will be used in this scope.
  • .length: is simply a label for the Length
  • !length16:target shows that the Length field is the target of the rule—in other words, the place where the output of the length rule will be written.
  • .type: is a label.
  • !length16:source indicates that MsgNegotiateFlagsPayload is the thing whose length should be calculated.

The following screenshot shows how the data model and the labels appear in Defensics for one of the test cases (which contains an anomaly in the payload).

def test case

Going deeper

Although we have a nice rule for the length on the negotiate flags message, it would be better if we actually modeled the payload as well. Doing this allows Defensics to apply smarter anomalizations, resulting in higher-quality test cases.

To see how this works, let’s look at a different message, MsgEnterWorld (which is not shown in the capture above). The payload of this message has the following fields:

  • Two-byte player type
  • Two-byte team color
  • Name string, padded with nulls to 32 bytes
  • Motto string, padded with nulls to 128 bytes
  • Token string, padded with nulls to 22 bytes
  • Version string, padded with nulls to 60 bytes

The type and color are easy; we’ll just define them with some suitable default values:


PlayerType = 0x0000
TeamColor = 0xfffe

The null-padded strings need special treatment. We can use Padding instances from RuleFactory. For the name string, for example, we can do this in the Java source code:


rf.padding("pad32").data("32(0x00)");

This creates a padding instance with the name pad32 and defines the pad to be 32 null bytes.

Then, in the BNF, we can make use of this instance as follows:


Name = @pad32 (
  .data: !pad32:source 'Name'
  .pad: !pad32:target ()
)

Bear in mind that the order is important. Just as with the length rule, you must define the pad32 instance in the Java code before you load in the BNF definitions with readTypes().

You can follow a similar pattern for the other null-padded strings. Then assemble the fields, using the same length rule, as follows:


MsgEnterPayload = (PlayerType TeamColor Name Motto Token Version)
MsgEnter = @length16 (
  .length: !length16:target Length
  .type: 0x656e
  !length16:source MsgEnterPayload)

Go forth and fuzz

The Defensics SDK exposes the power of the Defensics test case engine so that you can model and test any type of protocol or file format. As you can see, data modeling is quick and elegant in BNF, and the Defensics SDK supplies everything you need to make test cases the best that they can be.

This article gives you a taste of what is possible, showing you how to model a simple network protocol and add rules for length fields and field padding. We’ve only explored the ground floor of a skyscraper; the Defensics SDK has many powerful capabilities that you can use to create high-quality test cases for any type of target.

Combined with Defensics’ out-of-the-box features for instrumentation, test run control, integrations, and results, the Defensics SDK provides powerful, focused fuzzing capabilities for fuzzing any type of network protocol or file format.

Continue Reading

Explore Topics