Skip to content

007: Binary Packet Protocol

Shane DeSeranno edited this page Oct 10, 2017 · 10 revisions

Okay, this is where we start to talk about the core protocol of SSH. Looking at the Binary Packet Protocol section of the RFC, you can see it explains what a packet looks like:

uint32    packet_length
byte      padding_length
byte[n1]  payload; n1 = packet_length - padding_length - 1
byte[n2]  random padding; n2 = padding_length
byte[m]   mac (Message Authentication Code - MAC); m = mac_length

That is it! Simple, right? Not exactly, and this is where I spent most of my time when digesting the RFC. Okay, first we need to define what the various types are. For reference, you can see the Data Type Representations Used in the SSH Protocols.

uint32: This is a 32 bit (4 byte) unsigned integer value. However, special care must be taken to understand that this is stored in 'network byte order' or 'big endian'. This means, the first byte is the most significant and the last byte is the least. It is very important to note that the way data is stored in memory depends on the processor being used. In C#, we will be using BitConverter class to convert from a uint value into it's bytes, and this class has the IsLittleEndian static property to allow us to detect this. If the BitConverter.IsLittleEndian is true, we will need to reverse the bytes produced by BitConverter.GetBytes() to ensure the bytes are in network byte order (or big endian).

The rest of the fields are pretty simple: byte or array of bytes of some length.

packet_length: This is the entire packet length, not including the MAC (which we will cover later, but isn't used for non-encrypted packets.)

padding_length: This can be any value from 4 to 255, and it serves 2 purposes. It helps make the entire packet the exact length of one block of the encryption algorithm. This makes it easier to encrypt/decrypt one block at a time. It also adds extra "random" data to the packet, which further obfuscates the packet and data.

payload: This is the actual packet data. We will be reading this data to reconstruct messages from the client, and build the data when we send packets.

random padding: This is the extra random padding we added.

mac: Not used for unencryped packets, so we'll discuss this as we get to decryption and encryption, for now, it's omitted.

For a clear drawing of the packet, please refer to the SSH Wiki Page

When a client first connects, nothing is encrypted and both sides are sending raw data. This means, you don't need to worry about decompressing or decrypting the data at this point. However, it is important to understand that the first thing both sides need to do is decide on a shared set of algorithms to use for each piece. And to further complicate things, each direction can use a different algorithm. That is, the client could send a message to the server using one algorithm, but the server could potentially use a different algorithm for the return data. In most cases, however, they will end up being the same in each direction.

So, I think we know what we need to be able to read a packet from the socket and prepare to process is. One other small note is that he protocol recommends a maximum packet size of 35,000 bytes. We can use this to set our receive and send buffer size to reasonable sizes. I chose 2 times the max packet size.

In the next section Reading a Packet, we'll add the code necessary to read a packet from the client.