Skip to content

010: Sending A Packet

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

We are going to start by creating our first packet class, KexInit. This class will inherit from Packet. Create this class next to the Packet class. And since every packet needs a packet type, we need to create a PacketType enum with the values from RFC 4250.

    public enum PacketType : byte
    {
        SSH_MSG_DISCONNECT = 1,
        SSH_MSG_IGNORE = 2,
        SSH_MSG_UNIMPLEMENTED = 3,
        SSH_MSG_DEBUG = 4,
        SSH_MSG_SERVICE_REQUEST = 5,
        SSH_MSG_SERVICE_ACCEPT = 6,
        SSH_MSG_KEXINIT = 20,
        SSH_MSG_NEWKEYS = 21,
        SSH_MSG_USERAUTH_REQUEST = 50,
        SSH_MSG_USERAUTH_FAILURE = 51,
        SSH_MSG_USERAUTH_SUCCESS = 52,
        SSH_MSG_USERAUTH_BANNER = 53,
        SSH_MSG_GLOBAL_REQUEST = 80,
        SSH_MSG_REQUEST_SUCCESS = 81,
        SSH_MSG_REQUEST_FAILURE = 82,
        SSH_MSG_CHANNEL_OPEN = 90,
        SSH_MSG_CHANNEL_OPEN_CONFIRMATION = 91,
        SSH_MSG_CHANNEL_OPEN_FAILURE = 92,
        SSH_MSG_CHANNEL_WINDOW_ADJUST = 93,
        SSH_MSG_CHANNEL_DATA = 94,
        SSH_MSG_CHANNEL_EXTENDED_DATA = 95,
        SSH_MSG_CHANNEL_EOF = 96,
        SSH_MSG_CHANNEL_CLOSE = 97,
        SSH_MSG_CHANNEL_REQUEST = 98,
        SSH_MSG_CHANNEL_SUCCESS = 99,
        SSH_MSG_CHANNEL_FAILURE = 100,
    }

Then we need to add the PacketType abstract property to the Packet class to force all types that inherit this to specify their PacketType:

    public abstract class Packet
    {
        ...
        public abstract PacketType PacketType { get; }
        ...
    }

Finally, add the override to the KexInit class, which now looks like this:

    public class KexInit : Packet
    {
        public override PacketType PacketType
        {
            get
            {
                return PacketType.SSH_MSG_KEXINIT;
            }
        }
    }

The next order of business is adding the ByteWriter we'll use to convert a packet into a byte[]: (sorry for the huge block of code)

    public class ByteWriter : IDisposable
    {
        private MemoryStream m_Stream = new MemoryStream();

        public void WritePacketType(PacketType packetType)
        {
            WriteByte((byte)packetType);
        }

        public void WriteBytes(byte[] value)
        {
            WriteUInt32((uint)value.Count());
            WriteRawBytes(value);
        }

        public void WriteString(string value)
        {
            WriteString(value, Encoding.ASCII);
        }

        public void WriteString(string value, Encoding encoding)
        {
            WriteBytes(encoding.GetBytes(value));
        }

        public void WriteStringList(IEnumerable list)
        {
            WriteString(string.Join(",", list));
        }

        public void WriteUInt32(uint value)
        {
            byte[] data = BitConverter.GetBytes(value);
            if (BitConverter.IsLittleEndian)
                data = data.Reverse().ToArray();
            WriteRawBytes(data);
        }

        public void WriteMPInt(byte[] value)
        {
            if ((value.Length == 1) && (value[0] == 0))
            {
                WriteUInt32(0);
                return;
            }

            uint length = (uint)value.Length;
            if (((value[0] & 0x80) != 0))
            {
                WriteUInt32((uint)(length + 1));
                WriteByte(0x00);
            }
            else
            {
                WriteUInt32((uint)length);
            }

            WriteRawBytes(value);
        }

        public void WriteRawBytes(byte[] value)
        {
            if (disposedValue)
                throw new ObjectDisposedException("ByteWriter");
            m_Stream.Write(value, 0, value.Count());
        }

        public void WriteByte(byte value)
        {
            if (disposedValue)
                throw new ObjectDisposedException("ByteWriter");
            m_Stream.WriteByte(value);
        }

        public byte[] ToByteArray()
        {
            if (disposedValue)
                throw new ObjectDisposedException("ByteWriter");
            return m_Stream.ToArray();
        }

        #region IDisposable Support
        private bool disposedValue = false; // To detect redundant calls

        protected virtual void Dispose(bool disposing)
        {
            if (!disposedValue)
            {
                if (disposing)
                {
                    m_Stream.Dispose();
                    m_Stream = null;
                }

                disposedValue = true;
            }
        }

        public void Dispose()
        {
            // Do not change this code. Put cleanup code in Dispose(bool disposing) above.
            Dispose(true);
        }
        #endregion
    }

Okay, we are now ready to add a way to read and write packets. We will add ToByteArray() method on packet that will be used to turn a packet into a byte[], later this will do all of the compression and encoding for us. We will also add GetBytes() method to just build the payload. Finally, we'll add Load() and InternalGetBytes() abstract methods which must be implemented on all classes that inherit from Packet.

        public byte[] ToByteArray()
        {
            // TODO: Keep track of the received packet sequence (used for MAC)

            byte[] payload = GetBytes();

            // TODO: Compress the payload if necessary

            // TODO: Get the block size based on the ClientToServer cipher

            uint blockSize = 8;

            byte paddingLength = (byte)(blockSize - (payload.Length + 5) % blockSize);
            if (paddingLength < 4)
                paddingLength += (byte)blockSize;

            byte[] padding = new byte[paddingLength];
            RandomNumberGenerator.Create().GetBytes(padding);

            uint packetLength = (uint)(payload.Length + paddingLength + 1);

            using (ByteWriter writer = new ByteWriter())
            {
                writer.WriteUInt32(packetLength);
                writer.WriteByte(paddingLength);
                writer.WriteRawBytes(payload);
                writer.WriteRawBytes(padding);

                payload = writer.ToByteArray();
            }

            // TODO: Encrypt the payload if necessary

            // TODO: Write MAC if necesssary

            return payload;
        }

        public byte[] GetBytes()
        {
            using (ByteWriter writer = new ByteWriter())
            {
                writer.WritePacketType(PacketType);
                InternalGetBytes(writer);
                return writer.ToByteArray();
            }
        }

        protected abstract void Load(ByteReader reader);
        protected abstract void InternalGetBytes(ByteWriter writer);

Now we are getting somewhere! But there is still more code to write! Now we need to implement the abstract methods on KexInit! We also added all of the properties of the packet:

public class KexInit : Packet
    {
        public override PacketType PacketType
        {
            get
            {
                return PacketType.SSH_MSG_KEXINIT;
            }
        }

        public byte[] Cookie { get; set; } = new byte[16];
        public List<string> KexAlgorithms { get; private set; } = new List<string>();
        public List<string> ServerHostKeyAlgorithms { get; private set; } = new List<string>();
        public List<string> EncryptionAlgorithmsClientToServer { get; private set; } = new List<string>();
        public List<string> EncryptionAlgorithmsServerToClient { get; private set; } = new List<string>();
        public List<string> MacAlgorithmsClientToServer { get; private set; } = new List<string>();
        public List<string> MacAlgorithmsServerToClient { get; private set; } = new List<string>();
        public List<string> CompressionAlgorithmsClientToServer { get; private set; } = new List<string>();
        public List<string> CompressionAlgorithmsServerToClient { get; private set; } = new List<string>();
        public List<string> LanguagesClientToServer { get; private set; } = new List<string>();
        public List<string> LanguagesServerToClient { get; private set; } = new List<string>();
        public bool FirstKexPacketFollows { get; set; }

        public KexInit()
        {
            RandomNumberGenerator.Create().GetBytes(Cookie);
        }

        protected override void InternalGetBytes(ByteWriter writer)
        {
            writer.WriteRawBytes(Cookie);
            writer.WriteStringList(KexAlgorithms);
            writer.WriteStringList(ServerHostKeyAlgorithms);
            writer.WriteStringList(EncryptionAlgorithmsClientToServer);
            writer.WriteStringList(EncryptionAlgorithmsServerToClient);
            writer.WriteStringList(MacAlgorithmsClientToServer);
            writer.WriteStringList(MacAlgorithmsServerToClient);
            writer.WriteStringList(CompressionAlgorithmsClientToServer);
            writer.WriteStringList(CompressionAlgorithmsServerToClient);
            writer.WriteStringList(LanguagesClientToServer);
            writer.WriteStringList(LanguagesServerToClient);
            writer.WriteByte(FirstKexPacketFollows ? (byte)0x01 : (byte)0x00);
            writer.WriteUInt32(0);
        }

        protected override void Load(ByteReader reader)
        {
            Cookie = reader.GetBytes(16);
            KexAlgorithms = reader.GetNameList();
            ServerHostKeyAlgorithms = reader.GetNameList();
            EncryptionAlgorithmsClientToServer = reader.GetNameList();
            EncryptionAlgorithmsServerToClient = reader.GetNameList();
            MacAlgorithmsClientToServer = reader.GetNameList();
            MacAlgorithmsServerToClient = reader.GetNameList();
            CompressionAlgorithmsClientToServer = reader.GetNameList();
            CompressionAlgorithmsServerToClient = reader.GetNameList();
            LanguagesClientToServer = reader.GetNameList();
            LanguagesServerToClient = reader.GetNameList();
            FirstKexPacketFollows = reader.GetBoolean();
            /*
              uint32       0 (reserved for future extension)
            */
            uint reserved = reader.GetUInt32();
        }
    }

Wow, getting closer. Now we'll add a little C# Reflection magic to store a dictionary of packet types to map to the C# class:

        private static Dictionary<PacketType, Type> s_PacketTypes = new Dictionary<PacketType, Type>();

        static Packet()
        {
            var packets = Assembly.GetEntryAssembly().GetTypes().Where(t => typeof(Packet).IsAssignableFrom(t));
            foreach(var packet in packets)
            {
                try
                {
                    Packet packetInstance = Activator.CreateInstance(packet) as Packet;
                    s_PacketTypes[packetInstance.PacketType] = packet;
                }
                catch { }
            }
        }

Now we can update our ReadPacket() method to create a packet and load it:

            using (ByteReader packetReader = new ByteReader(payload))
            {
                PacketType type = (PacketType)packetReader.GetByte();

                if (s_PacketTypes.ContainsKey(type))
                {

                    Packet packet = Activator.CreateInstance(s_PacketTypes[type]) as Packet;
                    packet.Load(packetReader);
                    
                    // TODO: Store the packet sequence for use later

                    return packet;
                }
            }

I also added a log to print the packet type when we receive it in Client:

    m_Logger.LogDebug($"Received Packet: {packet.PacketType}");

The last order of business for this long and code filled section is to send our KexInit packet to the client.

        ...
        private KexInit m_KexInitServerToClient = new KexInit();
        ...
        public Client(Socket socket, ILogger logger)
        {
            m_Socket = socket;
            m_Logger = logger;

            // TODO: Add supported algoritms to m_KexInitServerToClient

            const int socketBufferSize = 2 * Packet.MaxPacketSize;
            m_Socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.SendBuffer, socketBufferSize);
            m_Socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReceiveBuffer, socketBufferSize);
            m_Socket.SetSocketOption(SocketOptionLevel.Tcp, SocketOptionName.NoDelay, true);
            m_Socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.DontLinger, true);

            // 4.2.Protocol Version Exchange - https://tools.ietf.org/html/rfc4253#section-4.2
            Send($"{Server.ProtocolVersionExchange}\r\n");

            // 7.1.  Algorithm Negotiation - https://tools.ietf.org/html/rfc4253#section-7.1
            Send(m_KexInitServerToClient);
        }
        ...
        public void Send(Packet packet)
        {
            Send(packet.ToByteArray());
        }
        ...

With all of this added, we should be able to run our code and see:

info: SSHServer[0]
      Starting up...
info: SSHServer[0]
      Listening on port: 22
dbug: SSHServer[0]
      New Client: 127.0.0.1:30460
dbug: 127.0.0.1:30460[0]
      Sending raw string: SSH-2.0-SSHServer
dbug: 127.0.0.1:30460[0]
      Received ProtocolVersionExchange: SSH-2.0-OpenSSH_7.2p2 Ubuntu-4ubuntu2.1
dbug: 127.0.0.1:30460[0]
      Received Packet: SSH_MSG_KEXINIT
dbug: 127.0.0.1:30460[0]
      Disconnected

And OpenSSH with full debugging info shows:

debug2: peer server KEXINIT proposal
debug2: KEX algorithms:
debug2: host key algorithms:
debug2: ciphers ctos:
debug2: ciphers stoc:
debug2: MACs ctos:
debug2: MACs stoc:
debug2: compression ctos:
debug2: compression stoc:
debug2: languages ctos:
debug2: languages stoc:
debug2: first_kex_follows 0
debug2: reserved 0
debug1: kex: algorithm: (no match)
Unable to negotiate with 127.0.0.1 port 22: no matching key exchange method found. Their offer:

So, it clearly received are empty lists and decided to disconnect as it has no way to securely communicate with us. WOW, so much progress! The complete code up until now is tagged with Sending_A_Packet. Next, we'll have to talk about all of the pieces we need such as: KEX, Host Key, Ciphers, MACs, and Compression algorithms! When you are ready, process to The Algorithms