CS177: Project 1 - Simple Network Client (10% of project score)

Project Goals


The goals of this project are:

  • to register and get set up for the projects
  • to develop a simple network client

Administrative Information


The project is an individual project. It is due on Thursday, April 20, 2023, 23:59:59 PST (no deadline extensions; late flags will not be accepted).

Implement a simple network client


The goal of this project is to implement a simple network client. This client should connect to a remote service and exchange a sequence of messages without hanging or crashing. This is important because all our lab projects leverage the CTFd infrastructure, and this will require you to connect to and interact with various remote services.

As a first step, please register with our CTFd site. We shared the information about the location of this site in Piazza. Once you have registered, you can log in and go to "Challenges." There, you find the instructions to spin up the server instance for you (which is running in a Docker container). The site will give you the address and port where your server is running and listening for your connections.

Once you have launched your service, you can start to implement your client. This client should connect to the server. Then, it will interact with the server in multiple rounds. The goal is to implement a very simple (key, value)-storage system. In a nutshell, in each round, the server will ask the client to store or retrieve a value, or it requests some status information. Once the server is convinced that the client works properly, it will send a flag. The goal of your client is to obtain that flag.

Each round consists of three messages:

  1. Task Request Message: The first message is from the client to the server, asking for a task.
  2. Task Message: The second message is from the server to the client. It specifies a specific task that the client must carry out.
  3. Task Response Message: The third message is from the client to the server. It is sent once the client has performed the given task, and it contains the task's result or status.

In this project, we implement a simple protocol-buffer-based protocol between the client and server. At first, it may seem a lot of work to implement all the different types and fields of the messages (that are described below). This is where protobufs will make your life much easier. The use of protobuf and its libraries make it very convenient to define the format of messages and to read and write the data.

For our project, each exchange between the client and the server has two parts. The first part is a fixed-length unsigned short integer (which we call m_len) that indicates the length of the following message. For the second part, which is the actual message, we will be using protobuf. The exact format is as follows:

  1. m_len (unsigned short): This is the length, in bytes, of the message, and it is always sent first. The server expects that length value in big-endian (also called network byte order).
  2. Each message starts with a type (unsigned 32-bit int) field: This is an integer that determines the type of message as well as the different message bodies (which depend on the type of message). As mentioned above, the type and body of the message will be sent using protobuf. The possible (valid) type values are:
    • 1 (Client -> Server): Request a task: The client checks in with the server and requests a task.
    • 2 (S->C): Set value for key K in the storage. The message body contains two mandatory fields:
      1. key (unsigned 32-bit int): A numerical key K
      2. value (string): A string value that needs to be associated with the integer key. If the server has previously sent a value for a specific key K, then the old value needs to be overwritten with the new value.
    • 3 (C->S): Acknowledge that the set command has been successful.
    • 4 (S->C): Get a value for a previously stored key K. The message body contains one mandatory field:
      1. key (unsigned 32-bit int): A numerical key K
    • 5 (C->S): Return the value for a previously stored key
      1. value (string): The value that was previously stored for the requested key K.
    • 6 (C->S): Return an error, indicating that there is no value for key K.
    • 7 (S->C): Request the number of valid (key, value) pairs.
    • 8 (C->S): Return the number of valid (key, value) pairs. The message body contains one mandatory field:
      1. count (unsigned 32-bit int): The number of unique key, value pairs that the client currently holds. 0 when there are none. You can assume that we will not overflow the value counter.
    • 9 (S->C): Indicates an error where the server does not understand a request (or response) from the client, or where the response of the client is incorrect. The connection is closed immediately after this message. The message body contains one mandatory field:
      1. error (string): A (possibly empty) string, indicating the problem that the server has encountered.
    • 10 (S->C): Flag message. Your goal is to receive this message. Once you get this, your client can disconnect, and you can submit us the flag. Once you do, you have solved the first project.
      1. flag (string): The flag string.

The objective of this project is to implement a robust TCP client that we will need as the basis for the subsequent security challenges. One common stumbling block is that many developer are unaware that, as a stream or byte-oriented protocol, TCP makes no particular guarantees that a send of n bytes will be matched with a recv of n bytes. In practice, this means that it is not enough when reading data to simply read an expected n bytes, but instead, one must anticipate short reads and receive in a loop until all expected bytes have been accumulated.

The server will issue a series of set (Type 2), get (Type 4), and status (Type 7) messages. Once your client has convinced the server that it correctly implements the protocol over a number of rounds, it will send you the flag (Type 10).

You don't need to use any persistent storage for keeping the key, value pairs. You can simply hold everything in memory while you interact with the server.

You can use any language to implement the client, since you will only interact with our service over the network. But I would recommend Python as a great choice.