Fuzzing SSH Clients

For folks wanting to learn exploit development, heres a step-by-step writing in how to progress in finding bugs and breaking code. Along the way Ill drop some tid-bits in things to look out for in code and how to think about it.

Our target to play with here is going to be “putty.exe”. A super popular Windows SSH client. This project involves a little bit of reversing as well as some python coding to see if we can trigger a crash in an SSH client. I have no idea if this is going to work since I’m writing this live as I go. The idea is that if we can make a fuzzer that triggers a crash in the SSH client, then we can reverse engineer the crash and see if its something we can exploit. It would be pretty cool to have a rouge SSH server that could exploit client targets that connect, and I think SSH is a good protocol to attack since SSH clients never really get exposed to malformed SSH messages.

So what kind of fuzzer would we need to crash an SSH client? Well, we could just send random garbage to the SSH client when trying to connect (dumb fuzzer), but the problem with that is we would never advance logic state in the SSH client to be able to test further into the protocol, since initial handshake would obviously fail.  For an advanced protocol like this, we want a “smart(er) fuzzer” so I wrote a mock SSH server thats pretty dumb for SSH server goes, but from fuzzer standpoint – smart, because it can play the SSH protocol game a bit with the client and goof with it along the way. Even if we dont exploit something, maybe it can be a good tutorial on how to fuzz and what things to keep an eye out for when breaking software.

Reversing SSH Client

Server/Client Version Exchange

To begin, I want to see where the SSH client processes Server responses. This isnt necessary, but I want to have an idea of whats going on in the client. This involves firing up Putty.exe in debugger and connecting to a live SSH Server and breaking on “ws2_32.dll!recv”.

Once we broke we can trace back the call stack to see where it came from, as we know this will be the area of code that processes the SSH server message, and sure enough we find the function that processes input.

In wireshark we see the server/client handshake starts with a client/server name exchange.

After breaking on recv and sifting around the caller’s routine, we eventually find the function responsible for parsing server response.

This just checks if the first 4 bytes start with “SSH-“. Then Server SSH version information is extracted. I can see in the code that it expects to parse a “ssh version number” after the first “-” and after next “-“, parses a “ssh version name”.

For this part in fuzzing, I just played with netcat, which will allow us to listen on the SSH port as-if we were an SSH server and send messages back.

I sent various weird names back (some with 3 “-“, some with non printable characters, some super long). Nothing worked. So before I moved on, I decided to look at this handler a bit more.

An example of something that looked good to break in it, was this piece of code

This takes the SSH Server Version response (“SSH-2.0-MySSHserver”), looks for the terminating “\r\n” socket response, and replacing it with a “NULL”. This is basically is to try and convert the response to a NULL terminated string and then process it.

Maybe you can see something to possibly break in this?

The thing I see, is we could possibly trigger unexpected behavior here if we have a server name sent like “SSH-2.0-\x00MySSHserver\r\n”, since we will have inserted an unexpected NULL before the client expects to cap the response with its NULL character (by replacing \r\n characters). This mismatch could possibly confuse parsing, so I tried to play with this, but no luck.

Algorithm Negotiation

Lets look at next step in communication which is algorithm negotiation. From looking at wireshark (and going over the SSH RFC https://tools.ietf.org/html/rfc4253#section-7.1), we can understand the Algorithm negotiation a bit and see there must be some good amount of parsing involved.

I briefly reverse this section of code in the client to see if anything obvious stands out.

This one looked super cool at first. “Binary Packet Length” is the “packet_length” field from a server response. Try to see if you can see whats wrong with this code.

…if you said integer overflow you would be right. Unfortunately a few functions before it will actually check if the packet_length’s most significant bit is set, and then error-out…lame…and weird that they used a signed number for size, but I guess they handled their cases properly so they are protected.

Writting Fuzzer

Code for this fuzzer can be found at https://github.com/RISCYBusiness/SSHClientFuzzer.

Since things are getting more complex and requiring the need for binary input, its probably time to write some kind of lightweight framework. After reversing some basic SSH protocol steps I made a framework contains a small mock SSH server and some ability to be fuzzy. The input fuzzer itself is really simple since I just want to be able to generate random strings and also mutate existing strings. The fuzzer class is this simple. The intelligence is how we use it.

class riscy_fuzzer:
    def __init__(self):
        self.hFile = None
        self.fuzz_style = FUZZ_STYLE.NONE
        self.fuzz_severity = 0

    def fuzz_int(self, low=0, high=0xFFFFFFFF):
        return random.randint(low, high)

    def fuzz_str(self, length):
        return os.urandom(length)
    
    def mutate_str(self, _string):
        str_len = len(_string)
        iterations = int(str_len*self.fuzz_severity)
        new_chars = list(_string)
        for _ in range(0, iterations):
            index = random.randrange(0, str_len-1)
            new_chars[index] = chr(random.randrange(0, 0xff))
        return "".join(new_chars)
        
    def load_sniper_data(self):
        data = self.hFile.read()
        hFile.seek(0)
        return data

As you can see, there is a variable “fuzz_severity”, which can be set by our Server and will change how crazy the strings will get.

Since different fuzzing styles are mostly orthogonal (meaning each style wouldnt have any additional effect if different styles were combined) we can seperate our fuzzing styles to perform one at a time.

class FUZZ_STYLE:
    NONE = 0 # none
    SNIPER = 1  # Load data from file (Manual Fuzzing)
    BUFFER_BUSTER = 2 # Generate large data for target fields
    MUTATE = 3  # Morph current data. This is good for parsing bugs

When we startup our mock server, we can configure it to be specialized in ONE particular fuzzing style.

Great, now we have some mock SSH server and can fuzz back responses but as always with fuzzing: “how do we know we crashed the target”? For this scenario we have a good rule we can apply…

If client does not respond back in TIMEOUT amount of time, then consider it dead and to log the packet that caused the “halt” (crash).

SSH client should at least close server connection upon client error, having it dangling for a while could indicate crash because client was unable to send RESET packet. This brings us to making a small watchdog to check if things are taking too long from the Server side.

class SSHServer():
    SSH_SERVER_NAME = 'SSH-2.0-riscy_fuzzer'
    BIND_IP = '127.0.0.1'
    BIND_PORT = 22
    ALLOWED_CONNECTIONS = 1
    TIMEOUT = 5
    
...

    def accept_binary_packet(self):
        """
        Listens for packet with expected SSH Binary Packet protocol
        """
        ready = select.select([self.conn], [], [], self.TIMEOUT)
        if not ready[0]:
            print '\nClient is stuck!...Here is last packet sent to it:\n{}'.format(self.last_response.encode('hex'))
        clientmsg = self.conn.recv(5) # read packet_length and random_padding header
        BINARY_PACKET_SIZE = struct.unpack('>I', clientmsg[0:4])[0] + int(clientmsg[4].encode('hex'), 16) # packet_length + random_padding
        while len(clientmsg) < BINARY_PACKET_SIZE-15:
           clientmsg += self.conn.recv(1024)
        return clientmsg

This is pretty good, but not perfect, as more complex bugs like UAFs/deeper memory corruption would probably go unnoticed, but we can worry about that later since I think this would cover A LOT.

If we apply this approach then we need our fuzzing process to be something like:

  1. Start mock SSH server
  2. Start and connect SSH client
  3. Fuzz responses
  4. Check if crashed via “TIMEOUT check” (log offending packet if so)
  5. Restart SSH client
  6. Repeat step 2

We write it up and run it. This is the back-and-forth client/server communication. Each green SSH packet has various fields fuzzed. We can let this run for a bit.

I ran this a bit on Putty, but no stall/crash. I decided to go into “sniper” mode for the fuzzer.

Sniper

 

My script will go ahead and network play over the file I modify from my hex editor, so its a pretty quick testing setup. The file has a proper algorithm negotiation SSH packet which I modify a bit and see a kindof cool behavior happen in putty.

 

…More to come this week…

Leave a Reply

Your email address will not be published. Required fields are marked *