Port scanning is a way of figuring out which ports are listening and accepting connections. Since most services run on standard, documented ports, this information can be used to determine which services are running. The simplest form of port scanning involves trying to open TCP connections to every possible port on the target system. While this is effective, it's also noisy and detectable. Also, when connections are established, services will normally log the IP address. To avoid this, several clever techniques have been invented.
A port scanning tool called nmap, written by Fyodor, implements all of the following port-scanning techniques. This tool has become one of the most popular open source port-scanning tools.
A SYN scan is also sometimes called a half-open scan. This is because it doesn't actually open a full TCP connection. Recall the TCP/IP handshake: When a full connection is made, first a SYN packet is sent, then a SYN/ACK packet is sent back, and finally an ACK packet is returned to complete the handshake and open the connection. A SYN scan doesn't complete the handshake, so a full connection is never opened. Instead, only the initial SYN packet is sent, and the response is examined. If a SYN/ACK packet is received in response, that port must be accepting connections. This is recorded, and an RST packet is sent to tear down the connection to prevent the service from accidentally being DoSed.
Using nmap, a SYN scan can be performed using the command-line option -sS
. The program must be run as root, since the program isn't using standard sockets and needs raw network access.
reader@hacking:~/booksrc $ sudo nmap -sS 192.168.42.72 Starting Nmap 4.20 ( http://insecure.org ) at 2007-05-29 09:19 PDT Interesting ports on 192.168.42.72: Not shown: 1696 closed ports PORT STATE SERVICE 22/tcp open ssh Nmap finished: 1 IP address (1 host up) scanned in 0.094 seconds
In response to SYN scanning, new tools to detect and log half-open connections were created. So yet another collection of techniques for stealth port scanning evolved: FIN, X-mas, and Null scans. These all involve sending a nonsensical packet to every port on the target system. If a port is listening, these packets just get ignored. However, if the port is closed and the implementation follows protocol (RFC 793), an RST packet will be sent. This difference can be used to detect which ports are accepting connections, without actually opening any connections.
The FIN scan sends a FIN packet, the X-mas scan sends a packet with FIN, URG, and PUSH turned on (so named because the flags are lit up like a Christmas tree), and the Null scan sends a packet with no TCP flags set. While these types of scans are stealthier, they can also be unreliable. For instance, Microsoft's implementation of TCP doesn't send RST packets like it should, making this form of scanning ineffective.
Using nmap, FIN, X-mas, and NULL scans can be performed using the command-line options -sF, -sX
, and -sN
, respectively. Their output looks basically the same as the previous scan.
Another way to avoid detection is to hide among several decoys. This technique simply spoofs connections from various decoy IP addresses in between each real port-scanning connection. The responses from the spoofed connections aren't needed, since they are simply misleads. However, the spoofed decoy addresses must use real IP addresses of live hosts; otherwise, the target may be accidentally SYN flooded.
Decoys can be specified in nmap with the -D
command-line option. The sample nmap command shown below scans the IP 192.168.42.72, using 192.168.42.10 and 192.168.42.11 as decoys.
reader@hacking:~/booksrc $ sudo nmap -D 192.168.42.10,192.168.42.11 192.168.42.72
Idle scanning is a way to scan a target using spoofed packets from an idle host, by observing changes in the idle host. The attacker needs to find a usable idle host that is not sending or receiving any other network traffic and that has a TCP implementation that produces predictable IP IDs that change by a known increment with each packet. IP IDs are meant to be unique per packet per session, and they are commonly incremented by a fixed amount. Predictable IP IDs have never really been considered a security risk, and idle scanning takes advantage of this misconception. Newer operating systems, such as the recent Linux kernel, OpenBSD, and Windows Vista, randomize the IP ID, but older operating systems and hardware (such as printers) typically do not.
First, the attacker gets the current IP ID of the idle host by contacting it with a SYN packet or an unsolicited SYN/ACK packet and observing the IP ID of the response. By repeating this process a few more times, the increment applied to the IP ID with each packet can be determined.
Then, the attacker sends a spoofed SYN packet with the idle host's IP address to a port on the target machine. One of two things will happen, depending on whether that port on the victim machine is listening:
If that port is listening, a SYN/ACK packet will be sent back to the idle host. But since the idle host didn't actually send out the initial SYN packet, this response appears to be unsolicited to the idle host, and it responds by sending back an RST packet.
If that port isn't listening, the target machine doesn't send a SYN/ACK packet back to the idle host, so the idle host doesn't respond.
At this point, the attacker contacts the idle host again to determine how much the IP ID has incremented. If it has only incremented by one interval, no other packets were sent out by the idle host between the two checks. This implies that the port on the target machine is closed. If the IP ID has incremented by two intervals, one packet, presumably an RST packet, was sent out by the idle machine between the checks. This implies that the port on the target machine is open.
The steps are illustrated on the next page for both possible outcomes.
Of course, if the idle host isn't truly idle, the results will be skewed. If there is light traffic on the idle host, multiple packets can be sent for each port. If 20 packets are sent, then a change of 20 incremental steps should be an indication of an open port, and none, of a closed port. Even if there is light traffic, such as one or two non–scan-related packets sent by the idle host, this difference is large enough that it can still be detected.
If this technique is used properly on an idle host that doesn't have any logging capabilities, the attacker can scan any target without ever revealing his or her IP address.
After finding a suitable idle host, this type of scanning can be done with nmap using the -sI
command-line option followed by the idle host's address:
reader@hacking:~/booksrc $ sudo nmap -sI idlehost.com 192.168.42.7
Port scans are often used to profile systems before they are attacked. Knowing what ports are open allows an attacker to determine which services can be attacked. Many IDSs offer methods to detect port scans, but by then the information has already been leaked. While writing this chapter, I wondered if it is possible to prevent port scans before they actually happen. Hacking, really, is all about coming up with new ideas, so a newly developed method for proactive port-scanning defense will be presented here.
First of all, the FIN, Null, and X-mas scans can be prevented by a simple kernel modification. If the kernel never sends reset packets, these scans will turn up nothing. The following output uses grep
to find the kernel code responsible for sending reset packets.
reader@hacking:~/booksrc $ grep -n -A 20 "void.*send_reset" /usr/src/linux/net/ipv4/
tcp_ipv4.c
547:static void tcp_v4_send_reset(struct sock *sk, struct sk_buff *skb)
548-{
549- struct tcphdr *th = skb->h.th;
550- struct {
551- struct tcphdr th;
552-#ifdef CONFIG_TCP_MD5SIG
553- __be32 opt[(TCPOLEN_MD5SIG_ALIGNED >> 2)];
554-#endif
555- } rep;
556- struct ip_reply_arg arg;
557-#ifdef CONFIG_TCP_MD5SIG
558- struct tcp_md5sig_key *key;
559-#endif
560-
return; // Modification: Never send RST, always return.
561- /* Never send a reset in response to a reset. */
562- if (th->rst)
563- return;
564-
565- if (((struct rtable *)skb->dst)->rt_type != RTN_LOCAL)
566- return;
567-
reader@hacking:~/booksrc $
By adding the return
command (shown above in bold), the tcp_v4_send_reset()
kernel function will simply return instead of doing anything. After the kernel is recompiled, the resulting kernel won't send out reset packets, avoiding information leakage.
matrix@euclid:~ $ sudo nmap -T5 -sF 192.168.42.72 Starting Nmap 4.11 ( http://www.insecure.org/nmap/ ) at 2007-03-17 16:58 PDT Interesting ports on 192.168.42.72: Not shown: 1678 closed ports PORT STATE SERVICE 22/tcp open|filtered ssh 80/tcp open|filtered http MAC Address: 00:01:6C:EB:1D:50 (Foxconn) Nmap finished: 1 IP address (1 host up) scanned in 1.462 seconds matrix@euclid:~ $
matrix@euclid:~ $ sudo nmap -T5 -sF 192.168.42.72 Starting Nmap 4.11 ( http://www.insecure.org/nmap/ ) at 2007-03-17 16:58 PDT Interesting ports on 192.168.42.72: Not shown: 1678 closed ports PORT STATE SERVICE MAC Address: 00:01:6C:EB:1D:50 (Foxconn) Nmap finished: 1 IP address (1 host up) scanned in 1.462 seconds matrix@euclid:~ $
This works fine for scans that rely on RST packets, but preventing information leakage with SYN scans and full-connect scans is a bit more difficult. In order to maintain functionality, open ports have to respond with SYN/ACK packets—there is no way around that. But if all of the closed ports also responded with SYN/ACK packets, the amount of useful information an attacker could retrieve from port scans would be minimized. Simply opening every port would cause a major performance hit, though, which isn't desirable. Ideally, this should all be done without using a TCP stack. The following program does exactly that. It's a modification of the rst_hijack.c program, using a more complex BPF string to filter only SYN packets destined for closed ports. The callback function spoofs a legitimate looking SYN/ACK response to any SYN packet that makes it through the BPF. This will flood port scanners with a sea of false positives, which will hide legitimate ports.
#include <libnet.h> #include <pcap.h> #include "hacking.h" #define MAX_EXISTING_PORTS 30 void caught_packet(u_char *, const struct pcap_pkthdr *, const u_char *); int set_packet_filter(pcap_t *, struct in_addr *, u_short *); struct data_pass { int libnet_handle; u_char *packet; }; int main(int argc, char *argv[]) { struct pcap_pkthdr cap_header; const u_char *packet, *pkt_data; pcap_t *pcap_handle; char errbuf[PCAP_ERRBUF_SIZE]; // Same size as LIBNET_ERRBUF_SIZE char *device; u_long target_ip; int network, i; struct data_pass critical_libnet_data; u_short existing_ports[MAX_EXISTING_PORTS]; if((argc < 2) || (argc > MAX_EXISTING_PORTS+2)) { if(argc > 2) printf("Limited to tracking %d existing ports.\n", MAX_EXISTING_PORTS); else printf("Usage: %s <IP to shroud> [existing ports...]\n", argv[0]); exit(0); } target_ip = libnet_name_resolve(argv[1], LIBNET_RESOLVE); if (target_ip == -1) fatal("Invalid target address"); for(i=2; i < argc; i++) existing_ports[i-2] = (u_short) atoi(argv[i]); existing_ports[argc-2] = 0; device = pcap_lookupdev(errbuf); if(device == NULL) fatal(errbuf); pcap_handle = pcap_open_live(device, 128, 1, 0, errbuf); if(pcap_handle == NULL) fatal(errbuf); critical_libnet_data.libnet_handle = libnet_open_raw_sock(IPPROTO_RAW); if(critical_libnet_data.libnet_handle == -1) libnet_error(LIBNET_ERR_FATAL, "can't open network interface. -- this program must run as root.\n"); libnet_init_packet(LIBNET_IP_H + LIBNET_TCP_H, &(critical_libnet_data.packet)); if (critical_libnet_data.packet == NULL) libnet_error(LIBNET_ERR_FATAL, "can't initialize packet memory.\n"); libnet_seed_prand(); set_packet_filter(pcap_handle, (struct in_addr *)&target_ip, existing_ports); pcap_loop(pcap_handle, -1, caught_packet, (u_char *)&critical_libnet_data); pcap_close(pcap_handle); } /* Sets a packet filter to look for established TCP connections to target_ip */ int set_packet_filter(pcap_t *pcap_hdl, struct in_addr *target_ip, u_short *ports) { struct bpf_program filter; char *str_ptr, filter_string[90 + (25 * MAX_EXISTING_PORTS)]; int i=0; sprintf(filter_string, "dst host %s and ", inet_ntoa(*target_ip)); // Target IP strcat(filter_string, "tcp[tcpflags] & tcp-syn != 0 and tcp[tcpflags] & tcp-ack = 0"); if(ports[0] != 0) { // If there is at least one existing port str_ptr = filter_string + strlen(filter_string); if(ports[1] == 0) // There is only one existing port sprintf(str_ptr, " and not dst port %hu", ports[i]); else { // Two or more existing ports sprintf(str_ptr, " and not (dst port %hu", ports[i++]); while(ports[i] != 0) { str_ptr = filter_string + strlen(filter_string); sprintf(str_ptr, " or dst port %hu", ports[i++]); } strcat(filter_string, ")"); } } printf("DEBUG: filter string is \'%s\'\n", filter_string); if(pcap_compile(pcap_hdl, &filter, filter_string, 0, 0) == -1) fatal("pcap_compile failed"); if(pcap_setfilter(pcap_hdl, &filter) == -1) fatal("pcap_setfilter failed"); } void caught_packet(u_char *user_args, const struct pcap_pkthdr *cap_header, const u_char *packet) { u_char *pkt_data; struct libnet_ip_hdr *IPhdr; struct libnet_tcp_hdr *TCPhdr; struct data_pass *passed; int bcount; passed = (struct data_pass *) user_args; // Pass data using a pointer to a struct IPhdr = (struct libnet_ip_hdr *) (packet + LIBNET_ETH_H); TCPhdr = (struct libnet_tcp_hdr *) (packet + LIBNET_ETH_H + LIBNET_TCP_H); libnet_build_ip(LIBNET_TCP_H, // Size of the packet sans IP header IPTOS_LOWDELAY, // IP tos libnet_get_prand(LIBNET_PRu16), // IP ID (randomized) 0, // Frag stuff libnet_get_prand(LIBNET_PR8), // TTL (randomized) IPPROTO_TCP, // Transport protocol *((u_long *)&(IPhdr->ip_dst)), // Source IP (pretend we are dst) *((u_long *)&(IPhdr->ip_src)), // Destination IP (send back to src) NULL, // Payload (none) 0, // Payload length passed->packet); // Packet header memory libnet_build_tcp(htons(TCPhdr->th_dport),// Source TCP port (pretend we are dst) htons(TCPhdr->th_sport), // Destination TCP port (send back to src) htonl(TCPhdr->th_ack), // Sequence number (use previous ack) htonl((TCPhdr->th_seq) + 1), // Acknowledgement number (SYN's seq # + 1) TH_SYN | TH_ACK, // Control flags (RST flag set only) libnet_get_prand(LIBNET_PRu16), // Window size (randomized) 0, // Urgent pointer NULL, // Payload (none) 0, // Payload length (passed->packet) + LIBNET_IP_H);// Packet header memory if (libnet_do_checksum(passed->packet, IPPROTO_TCP, LIBNET_TCP_H) == -1) libnet_error(LIBNET_ERR_FATAL, "can't compute checksum\n"); bcount = libnet_write_ip(passed->libnet_handle, passed->packet, LIBNET_IP_H+LIBNET_TCP_H); if (bcount < LIBNET_IP_H + LIBNET_TCP_H) libnet_error(LIBNET_ERR_WARNING, "Warning: Incomplete packet written."); printf("bing!\n"); }
There are a few tricky parts in the code above, but you should be able to follow all of it. When the program is compiled and executed, it will shroud the IP address given as the first argument, with the exception of a list of existing ports provided as the remaining arguments.
reader@hacking:~/booksrc $ gcc $(libnet-config --defines) -o shroud shroud.c -lnet -lpcap reader@hacking:~/booksrc $ sudo ./shroud 192.168.42.72 22 80 DEBUG: filter string is 'dst host 192.168.42.72 and tcp[tcpflags] & tcp-syn != 0 and tcp[tcpflags] & tcp-ack = 0 and not (dst port 22 or dst port 80)'
While shroud is running, any port scanning attempts will show every port to be open.
matrix@euclid:~ $ sudo nmap -sS 192.168.0.189
Starting nmap V. 3.00 ( www.insecure.org/nmap/ )
Interesting ports on (192.168.0.189):
Port State Service
1/tcp open tcpmux
2/tcp open compressnet
3/tcp open compressnet
4/tcp open unknown
5/tcp open rje
6/tcp open unknown
7/tcp open echo
8/tcp open unknown
9/tcp open discard
10/tcp open unknown
11/tcp open systat
12/tcp open unknown
13/tcp open daytime
14/tcp open unknown
15/tcp open netstat
16/tcp open unknown
17/tcp open qotd
18/tcp open msp
19/tcp open chargen
20/tcp open ftp-data
21/tcp open ftp
22/tcp open ssh
23/tcp open telnet
24/tcp open priv-mail
25/tcp open smtp
[ output trimmed ]
32780/tcp open sometimes-rpc23
32786/tcp open sometimes-rpc25
32787/tcp open sometimes-rpc27
43188/tcp open reachout
44442/tcp open coldfusion-auth
44443/tcp open coldfusion-auth
47557/tcp open dbbrowse
49400/tcp open compaqdiag
54320/tcp open bo2k
61439/tcp open netprowler-manager
61440/tcp open netprowler-manager2
61441/tcp open netprowler-sensor
65301/tcp open pcanywhere
Nmap run completed -- 1 IP address (1 host up) scanned in 37 seconds
matrix@euclid:~ $
The only service that is actually running is ssh on port 22, but it is hidden in a sea of false positives. A dedicated attacker could simply telnet to every port to check the banners, but this technique could easily be expanded to spoof banners also.