Creating a SYN port scanner
In this chapter we are going to use our knowledge on packets to create our own SYN port scanner.
A very simple port scanner
Port scanners are tools designed to probe a server for open ports. They are used by many people such as administrators or pentesters to check the attack surface of their systems and identify networks services running on them. One of the most well-known port scanners is nmap. These port scanners typically allow several different kind of scanning techniques. The most basic one is the TCP connect scan. With this technique the port scanner tries to complete a full TCP three-way handshake (
[SYN], [SYN ACK], [ACK], see Recap on network layers and protocols) with the targeted port on the system to scan. The following Python script does exactly this:
import socket def scan(host, port): s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) try: s.connect((host, port)) print("Port open: " + str(port)) s.close() except: print("Port closed: " + str(port)) host = "10.10.10.1" for port in [21, 22, 80]: scan(host, port)
root@kali:~# python3 simple_scanner.py Port closed: 21 Port closed: 22 Port open: 80
In Wireshark we can observe the exact behavior of our simple port scanner:
We are scanning for the three ports 21, 22 and 80. For the first two ports we see that they are closed as we receive a
[RST ACK] packet for each of them from the server. However, port 80 is open as we completed a TCP three-way handshake as well as a normal TCP connection termination initiated by us after that.
This kind of port scan is even possible if the user does not have the privileges to create raw packets on the system as the script asks the underlying operating system to establish the connection with the target machine by a
connect system call. However, this method is quite noisy, and the target machine is very likely to log the connection.
TCP SYN Scan
A faster and a little bit more stealthy port scan is a SYN scan. This is probably the most common technique for port scanners in general. Often also referred to as half-open scanning, because you don’t do a full TCP connection. You send a
[SYN] packet and then wait for the response. A
[SYN ACK] indicates that the port is open, while a
[RST] indicates that it’s closed. In contrast to the connect scan, in this method of scanning we do not complete the three-way handshake by sending a
[ACK] ourselves, but terminate the connection.
Looking back at an excerpt from the script with which we created a TCP/IP packet, what values would we need to change for a port scanner use case?
ip_header = b'\x45\x00\x00\x28' # Version, IHL, Type of Service | Total Length ip_header += b'\xab\xcd\x00\x00' # Identification | Flags, Fragment Offset ip_header += b'\x40\x06\xa6\xec' # TTL, Protocol | Header Checksum ip_header += b'\x0a\x0a\x0a\x02' # Source Address ip_header += b'\x0a\x0a\x0a\x01' # Destination Address tcp_header = b'\x30\x39\x00\x50' # Source Port | Destination Port tcp_header += b'\x00\x00\x00\x00' # Sequence Number tcp_header += b'\x00\x00\x00\x00' # Acknowledgement Number tcp_header += b'\x50\x02\x71\x10' # Data Offset, Reserved, Flags | Window Size tcp_header += b'\xe6\x32\x00\x00' # Checksum | Urgent Pointer
The most obvious changes necessary are probably the Destination Address and Destination Port. Changing those parameters makes it necessary for us to also adjust the Header Checksum and the Checksum from the TCP segment on the fly.
Let’s put together what our script needs to do:
- Take our target IP address and port as input
- Insert them into a blueprint for a
- Calculate the new checksums
- Send the packet
- Check the response for
[SYN, ACK], if present: port open, if not: port closed
- Repeat all steps above (with a different port)
Congratulations, you are now able to create your own TCP SYN port scanner! In order to be a little bit more flexible, a rewrite of the previous scripts should be done. The struct Python module helps in working with the binary data. In case you want to see how the steps above could look like in code, have a look at the following lines.
import socket from struct import * import binascii class Packet: def __init__(self, src_ip, dest_ip, dest_port): # https://docs.python.org/3.7/library/struct.html#format-characters # all values need to be at least one byte long (-> we need to add up some values) ############ # IP segment self.version = 0x4 self.ihl = 0x5 self.type_of_service = 0x0 self.total_length = 0x28 self.identification = 0xabcd self.flags = 0x0 self.fragment_offset = 0x0 self.ttl = 0x40 self.protocol = 0x6 self.header_checksum = 0x0 self.src_ip = src_ip self.dest_ip = dest_ip self.src_addr = socket.inet_aton(src_ip) self.dest_addr = socket.inet_aton(dest_ip) self.v_ihl = (self.version << 4) + self.ihl self.f_fo = (self.flags << 13) + self.fragment_offset ############# # TCP segment self.src_port = 0x3039 self.dest_port = dest_port self.seq_no = 0x0 self.ack_no = 0x0 self.data_offset = 0x5 self.reserved = 0x0 self.ns, self.cwr, self.ece, self.urg, self.ack, self.psh, self.rst, self.syn, self.fin = 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0x0 self.window_size = 0x7110 self.checksum = 0x0 self.urg_pointer = 0x0 self.data_offset_res_flags = (self.data_offset << 12) + (self.reserved << 9) + (self.ns << 8) + (self.cwr << 7) + (self.ece << 6) + (self.urg << 5) + (self.ack << 4) + (self.psh << 3) + (self.rst << 2) + (self.syn << 1) + self.fin ######## # packet self.tcp_header = b"" self.ip_header = b"" self.packet = b"" def calc_checksum(self, msg): s = 0 for i in range(0, len(msg), 2): w = (msg[i] << 8) + msg[i+1] s = s + w # s = 0x119cc s = (s >> 16) + (s & 0xffff) # s = 0x19cd s = ~s & 0xffff # s = 0xe632 return s def generate_tmp_ip_header(self): tmp_ip_header = pack("!BBHHHBBH4s4s", self.v_ihl, self.type_of_service, self.total_length, self.identification, self.f_fo, self.ttl, self.protocol, self.header_checksum, self.src_addr, self.dest_addr) return tmp_ip_header def generate_tmp_tcp_header(self): tmp_tcp_header = pack("!HHLLHHHH", self.src_port, self.dest_port, self.seq_no, self.ack_no, self.data_offset_res_flags, self.window_size, self.checksum, self.urg_pointer) return tmp_tcp_header def generate_packet(self): # IP header + checksum final_ip_header = pack("!BBHHHBBH4s4s", self.v_ihl, self.type_of_service, self.total_length, self.identification, self.f_fo, self.ttl, self.protocol, self.calc_checksum(self.generate_tmp_ip_header()), self.src_addr, self.dest_addr) # TCP header + checksum tmp_tcp_header = self.generate_tmp_tcp_header() pseudo_header = pack("!4s4sBBH", self.src_addr, self.dest_addr, self.checksum, self.protocol, len(tmp_tcp_header)) psh = pseudo_header + tmp_tcp_header final_tcp_header = pack("!HHLLHHHH", self.src_port, self.dest_port, self.seq_no, self.ack_no, self.data_offset_res_flags, self.window_size, self.calc_checksum(psh), self.urg_pointer) self.ip_header = final_ip_header self.tcp_header = final_tcp_header self.packet = final_ip_header + final_tcp_header def send_packet(self): s = socket.socket(socket.AF_INET, socket.SOCK_RAW, socket.IPPROTO_TCP) s.setsockopt(socket.IPPROTO_IP, socket.IP_HDRINCL, 1) s.sendto(self.packet, (self.dest_ip, 0)) data = s.recv(1024) s.close() return data # could work with e.g. struct.unpack() here # however, lazy PoC (012 = [SYN ACK]), therefore: def check_if_open(port, response): cont = binascii.hexlify(response) if cont[65:68] == b"012": print("Port "+str(port)+" is: open") else: print("Port "+str(port)+" is: closed") for port in [21, 22, 80, 8080]: p = Packet("10.10.10.2", "10.10.10.1", port) p.generate_packet() result = p.send_packet() check_if_open(port, result)
Small exercise: Compare the difference in the packets you get back from your own local test server (10.10.10.1), compared to for example the IP address of google.com on port 21 and port 80. How could you improve the upper script?
As you have seen, it’s very much possible to create and send raw packets manually, even in high level languages such as Python. However, a more comfortable way of working with sockets, packets and networking in general would be Scapy, an interactive packet manipulation program for Python. If you are interested in that area, I highly recommend you have a look at it. Overall, I hope you did learn a few things from this series, and maybe can use that knowledge for example in your next fuzzing project, pentest or as a basis for further improvement.
A word of warning: Use port scanners only on your own systems or servers where you have the explicit permission to scan. Depending of the jurisdiction you and/or your target system are in, you might get in conflict with the law when performing unsolicited port scans.