Defeating Denial of Service Attacks
Learn how attackers attempt to starve the resources associated with your application and how to protect yourself from these types of Denial of Service attacks.
September 18, 2000
Denial of Service (DoS) attacks can either bring down your application completely or reduce your level of service. These attacks often work by exploiting flaws in your application to cause a general failure, delivering excess data to cause memory starvation, causing one client to consume all available resources, or taking advantage of poorly designed features to consume too much CPU power. I've written about application errors that can cause failures in previous articles (i.e., "Good Programming and the Rules for Writing Secure Code", "Parsing POP"), so I'll just say one more time that there is no substitute for writing solid code.
This week, let's look at a few of the ways that attackers can attempt to starve the resources associated with your application. Arming yourself with this knowledge will give you some ideas on how to protect your applications against DoS attacks.
Keeping the Workers Busy
Resource starvation attacks involve either a poorly written client or an attacker that consumes all available resources to prevent normal clients from obtaining access. Consider a basic sockets application listening on a port—if you have a single-threaded application, it serves only one client at a time. The application accepts a connection, receives input, and sends a response. When the application is finished with the client, it goes back to listening on the port. If the server provides a very simple function (e.g., giving the time of day), this scheme might work for small numbers of clients. The TCP/IP stack will buffer a limited number of connections for you, and if you can process the requests as quickly as they are received, you can support a simple service with one thread. If your service isn’t simple and you can’t service requests quickly enough, the TCP backlog limit will be exceeded and your clients with get a connection refused or time out.
A more robust way to handle this situation is to create a pool of threads. The main thread takes care of starting the other threads and listens for connections. As connections come in and are established, the main thread hands off each task to a worker thread for completion. If the size of your thread pool is fixed, an attacker can simply send requests faster than you can complete them and keep the service from accepting clients, thereby causing a DoS attack. If a server expects input from a client, an attacker can tie up resources by either not sending expected input at all or by delaying as long as possible. Any scenario where an attacker can consume resources more quickly than the server can recover will lead to a DoS condition.
If the size of the worker thread (you typically use processes under UNIX) pool isn’t fixed, you can absorb a lot of attacks without dropping service entirely. The drawback to this approach is that you’ve now exposed yourself to another type of attack—your server can consume system resources without bounds. So instead of just bringing down your service, the attacker can often cripple the entire system. Giving your server an unlimited amount of system resources isn't a good solution.
One approach that can be very effective is to place limits on a particular client. You can keep a hash table (or a map object if you’re using Standard Template Library—STL) with the IP addresses of your clients along with the number of connections from each client. If a client tries to consume more than the allowed number of connections, you simply refuse these additional requests from that client. However, remember that the overhead associated with this table might slow you down (e.g., a typical IP stack can’t incur that type of performance hit). It is also a good idea to make the connection limit configurable—as soon as you hard code this number to 5, you’ll get a customer who needs 10 connections per client. Setting these connection limits means that an attacker has to launch a distributed attack to deny service to anyone other than themselves.
Another counter-measure you can use is to implement timeouts. If a client establishes a connection but doesn't send requests in a timely manner, you can close the connection and free that worker for a new client. In addition to timing out slow clients, you can use this practice as a good network programming strategy. For example, you should use timeouts to handle network errors—for example, a network link might have failed right after the connection was established. You can also vary your response according to utilization of your worker threads. If it appears that you’re under attack, you might want to drop connections more quickly than you otherwise would.
Memory Starvation
Another type of DoS attack involves memory starvation, which can manifest itself in several forms when you fail to design your application properly. For example, this situation can occur when you need to set up several complicated structures to service your client. An application might accept the connection, set up the structures, and ask the client to authenticate—the entire process requires quite a bit of work and memory just to respond to a client that wants to connect to your application. Because you don’t know whether, or when, the client will ever authenticate, it's much better to make sure you have a valid client before you start any complicated processing or allocate much memory.
A second form of this attack can occur when your application doesn’t properly bound user inputs. Many protocols allow a client to send an arbitrary amount of data, which can be difficult to handle. So, for example, if each client sends 2MB of data, it won't take many clients to run most machines out of RAM. To be safe, it's best to set limits on the amount of data you’ll accept, make those limits configurable, and drop clients that exceed the limits. Configurable input limits are important because users in very hostile environments might want very low limits, but other users might want more than the default amount.
CPU starvation attacks are more complicated, so I’ll save that discussion for another column. The basic point to remember is that people will do all sorts of things, some intentionally and some through user error, to cause a network service to fail. You need to arm your code as thoroughly as you can. Profiling your code to determine which functions consume the most resources can help you to optimize the functions that need it the most. I’ve seen programmers spend hours optimizing a piece of code that executes infrequently only to yield little benefit to the users. At the other extreme, I've worked with another programmer who always used profiling, so much so that at times the other programmers had to ask him to slow the code down!
I should stress that when you're protecting your code from DoS attacks, there is no ideal solution—any technique you use will have some disadvantages. You need to analyze what your service will be doing, think about the types of attacks that can occur, and plan accordingly.
About the Author
You May Also Like