Skip to content

019: Wrap Up

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

Once again, I apologize for the size of the last post. But it was the one that pulled all the pieces together. I didn't want to try to chop it up until it was working. And, as usual, once you start to wire in pieces, you discover refactors and bugs that need to be dealt with.

In this section, we will try to wrap of a few last details and then I may write a few extra things later.

Unimplemented Packet

First, let's add the Unimplemented packet in the Packets folder and make it inherit from Packet. This messages is covered by 11.4. Reserved Messages. It is a very simple packet that just has the packet sequence number that was rejected as a uint32.

    public class Unimplemented : Packet
    {
        public override PacketType PacketType
        {
            get
            {
                return PacketType.SSH_MSG_UNIMPLEMENTED;
            }
        }

        public uint RejectedPacketNumber { get; set; }

        public override void Load(ByteReader reader)
        {
            RejectedPacketNumber = reader.GetUInt32();
        }

        protected override void InternalGetBytes(ByteWriter writer)
        {
            // uint32 packet sequence number of rejected message
            writer.WriteUInt32(RejectedPacketNumber);
        }
    }

Now we find the places in our code where our TODO comments tell us to send this message. Notice that the RFC specifically says we are to ignore these messages. It does not tell you to disconnect the client.

    public class Client
    {
        ...
        private void HandlePacket(Packet packet)
        {
            try
            {
                HandleSpecificPacket((dynamic)packet);
            }
            catch(RuntimeBinderException)
            {
                m_Logger.LogWarning($"Unhandled packet type: {packet.PacketType}");

                Unimplemented unimplemented = new Unimplemented()
                {
                    RejectedPacketNumber = packet.PacketSequence
                };
                Send(unimplemented);
            }
        }
        ...
        public Packet ReadPacket()
        {
            ...
            using (ByteReader packetReader = new ByteReader(payload))
            {
                PacketType type = (PacketType)packetReader.GetByte();

                if (Packet.PacketTypes.ContainsKey(type))
                {

                    Packet packet = Activator.CreateInstance(Packet.PacketTypes[type]) as Packet;
                    packet.Load(packetReader);
                    packet.PacketSequence = packetNumber;
                    return packet;
                }

                m_Logger.LogWarning($"Unimplemented packet type: {type}");

                Unimplemented unimplemented = new Unimplemented()
                {
                    RejectedPacketNumber = packetNumber
                };
                Send(unimplemented);
            }

            return null;
        }
    }

The server now indicates the issue:

...
warn: 127.0.0.1:15734[0]
      Unimplemented packet type: SSH_MSG_SERVICE_REQUEST

And the OpenSSH client received it:

...
debug3: send packet: type 5
debug3: receive packet: type 3
debug1: Received SSH2_MSG_UNIMPLEMENTED for 3

Key Re-Exchange

The RFC explains that key re-exchange should be considered after 1 hour of connectivity or 1 gigabyte of data transferred. So we will want to track the total bytes sent and received and the date/time we need to re-exchange our keys. We need to add a few things to the Client code to facilitate this:

    public class Client
    {
        ...
        private long m_TotalBytesTransferred = 0;
        private DateTime m_KeyTimeout = DateTime.UtcNow.AddHours(1);
        ...
        public void Poll()
        {
            ...
                        Packet packet = ReadPacket();
                        while (packet != null)
                        {
                            m_Logger.LogDebug($"Received Packet: {packet.PacketType}");
                            HandlePacket(packet);
                            packet = ReadPacket();
                        }

                        ConsiderReExchange();
            ...
        }
        ...
        private void HandleSpecificPacket(NewKeys packet)
        {
            m_Logger.LogDebug("Received NewKeys");

            m_ActiveExchangeContext = m_PendingExchangeContext;
            m_PendingExchangeContext = null;

            // Reset re-exchange values
            m_TotalBytesTransferred = 0;
            m_KeyTimeout = DateTime.UtcNow.AddHours(1);
        }
        ...
        private void Send(byte[] data)
        {
            if (!IsConnected)
                return;

            // Increase bytes transferred
            m_TotalBytesTransferred += data.Length;

            m_Socket.Send(data);
        }
        ...
        public Packet ReadPacket()
        {
            ...
            uint payloadLength = packetLength - paddingLength - 1;
            byte[] fullPacket = firstBlock.Concat(restOfPacket).ToArray();

            // Track total bytes read
            m_TotalBytesTransferred += fullPacket.Length;
            ...
        }
        ...
        private void ConsiderReExchange()
        {
            const long OneGB = (1024 * 1024 * 1024);
            if ((m_TotalBytesTransferred > OneGB) || (m_KeyTimeout < DateTime.UtcNow))
            {
                // Time to get new keys!
                m_TotalBytesTransferred = 0;
                m_KeyTimeout = DateTime.UtcNow.AddHours(1);

                m_Logger.LogDebug("Trigger re-exchange from server");
                m_PendingExchangeContext = new ExchangeContext();
                Send(m_KexInitServerToClient);
            }
        }
    }

This is a lot of little changes, but basically, track the total bytes sent or received. Track the time we should reconsider. Then after every set of packets we process in Poll(), we should CondiderReExchange(). If we do decide to re-exchange, then create a new pending ExchangeContext and send our KEXINIT to the client. When we get a NewKeys, we reset all tracked values. This doesn't have any impact the the output of the server or OpenSSH, but it should work.

Validate Client Protocol Version Exchange

As described in the RFC 4.2. Protocol Version Exchange, we should ensure the client sent us a valid Protocol Version Exchange value. A simple string parser should be good enough:

    public class Client
    {
        ...
        public void Poll()
        {
            ...
                        ReadProtocolVersionExchange();
                        if (m_HasCompletedProtocolVersionExchange)
                        {
                            // TODO: Consider processing Protocol Version Exchange for validity
                            m_Logger.LogDebug($"Received ProtocolVersionExchange: {m_ProtocolVersionExchange}");
                            ValidateProtocolVersionExchange();
                        }
            ...
        }
        ...
        private void ValidateProtocolVersionExchange()
        {
            // https://tools.ietf.org/html/rfc4253#section-4.2
            //SSH-protoversion-softwareversion SP comments

            string[] pveParts = m_ProtocolVersionExchange.Split(new char[] { ' ' }, StringSplitOptions.RemoveEmptyEntries);
            if (pveParts.Length == 0)
                throw new UnauthorizedAccessException("Invalid Protocol Version Exchange was received - No Data");

            string[] versionParts = pveParts[0].Split(new char[] { '-' }, StringSplitOptions.RemoveEmptyEntries);
            if (versionParts.Length < 3)
                throw new UnauthorizedAccessException($"Invalid Protocol Version Exchange was received - Not enough dashes - {pveParts[0]}");

            if (versionParts[1] != "2.0")
                throw new UnauthorizedAccessException($"Invalid Protocol Version Exchange was received - Unsupported Version - {versionParts[1]}");

            // If we get here, all is well!
        }

There is one more feature that I want to add, but it is big enough to require it's own section. So, the code for this is tagged with Wrap_Up_Unimplemented_And_TODOs. In the next section we'll cover adding an SSHServerException and refactor all exceptions in the code to allow us to provide a disconnect message to the client.