In the previous part of this guide, we saw what raw sockets actually are and how to use them to build something useful(i.e. a tcpdump clone). In the last part of this guide, we will extend our knowledge about raw sockets by writing a simple TCP SYN port scanner. Before getting into the actual code, let us understand how does a port scanner work and what are the main techniques to perform port scanning.

A port scanner is a tool designed to look for open ports on a remote server. These tools are mainly used by system administrators and security researchers to get an idea of which network services are being executed on a given host.

# A quick overview of Nmap⌗

The most used and accurate port scanner available on UNIX systems is nmap. Nmap comes with a lot of options and a lot of different scanning techniques; you can install it on your computer, and you can try to perform a port scan on one of your server. If you do not have any server available, you can use the nmap playground: a testing environment provided by nmap.org itself that allow you to legally test their software. Go ahead and try it out:

$> nmap -p0- -v -A -T4 scanme.nmap.org Starting Nmap 7.91 ( https://nmap.org ) at 2021-06-09 12:26 CEST NSE: Loaded 153 scripts for scanning. NSE: Script Pre-scanning. Initiating NSE at 12:26 Completed NSE at 12:26, 0.00s elapsed Initiating NSE at 12:26 Completed NSE at 12:26, 0.00s elapsed Initiating NSE at 12:26 Completed NSE at 12:26, 0.00s elapsed Initiating Ping Scan at 12:26 Scanning scanme.nmap.org (45.33.32.156) [2 ports] Completed Ping Scan at 12:26, 0.17s elapsed (1 total hosts) Initiating Parallel DNS resolution of 1 host. at 12:26 Completed Parallel DNS resolution of 1 host. at 12:26, 0.35s elapsed Initiating Connect Scan at 12:26 Scanning scanme.nmap.org (45.33.32.156) [65536 ports] Discovered open port 80/tcp on 45.33.32.156 Discovered open port 22/tcp on 45.33.32.156 Connect Scan Timing: About 8.18% done; ETC: 12:33 (0:05:48 remaining) Stats: 0:00:57 elapsed; 0 hosts completed (1 up), 1 undergoing Connect Scan Connect Scan Timing: About 14.86% done; ETC: 12:33 (0:05:27 remaining) Connect Scan Timing: About 25.51% done; ETC: 12:33 (0:05:07 remaining) Connect Scan Timing: About 33.58% done; ETC: 12:33 (0:04:27 remaining) Connect Scan Timing: About 41.16% done; ETC: 12:33 (0:03:56 remaining) Connect Scan Timing: About 49.22% done; ETC: 12:33 (0:03:21 remaining) Connect Scan Timing: About 58.11% done; ETC: 12:33 (0:02:42 remaining) Connect Scan Timing: About 66.19% done; ETC: 12:33 (0:02:10 remaining) Connect Scan Timing: About 74.02% done; ETC: 12:33 (0:01:40 remaining) Discovered open port 9929/tcp on 45.33.32.156 Connect Scan Timing: About 81.40% done; ETC: 12:33 (0:01:12 remaining) Discovered open port 31337/tcp on 45.33.32.156 Connect Scan Timing: About 89.89% done; ETC: 12:33 (0:00:39 remaining) Completed Connect Scan at 12:33, 386.55s elapsed (65536 total ports) Initiating Service scan at 12:33 Scanning 4 services on scanme.nmap.org (45.33.32.156) Completed Service scan at 12:33, 6.37s elapsed (4 services on 1 host) NSE: Script scanning 45.33.32.156. Initiating NSE at 12:33 Completed NSE at 12:33, 5.61s elapsed Initiating NSE at 12:33 Completed NSE at 12:33, 0.71s elapsed Initiating NSE at 12:33 Completed NSE at 12:33, 0.00s elapsed Nmap scan report for scanme.nmap.org (45.33.32.156) Host is up (0.17s latency). Other addresses for scanme.nmap.org (not scanned): 2600:3c01::f03c:91ff:fe18:bb2f Not shown: 65530 closed ports PORT STATE SERVICE VERSION 22/tcp open ssh OpenSSH 6.6.1p1 Ubuntu 2ubuntu2.13 (Ubuntu Linux; protocol 2.0) | ssh-hostkey: | 1024 ac:00:a0:1a:82:ff:cc:55:99:dc:67:2b:34:97:6b:75 (DSA) | 2048 20:3d:2d:44:62:2a:b0:5a:9d:b5:b3:05:14:c2:a6:b2 (RSA) | 256 96:02:bb:5e:57:54:1c:4e:45:2f:56:4c:4a:24:b2:57 (ECDSA) |_ 256 33:fa:91:0f:e0:e1:7b:1f:6d:05:a2:b0:f1:54:41:56 (ED25519) 25/tcp filtered smtp 80/tcp open http Apache httpd 2.4.7 ((Ubuntu)) |_http-favicon: Nmap Project | http-methods: |_ Supported Methods: GET HEAD POST OPTIONS |_http-server-header: Apache/2.4.7 (Ubuntu) |_http-title: Go ahead and ScanMe! 9929/tcp open nping-echo Nping echo 11211/tcp filtered memcache 31337/tcp open tcpwrapped Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel NSE: Script Post-scanning. Initiating NSE at 12:33 Completed NSE at 12:33, 0.00s elapsed Initiating NSE at 12:33 Completed NSE at 12:33, 0.00s elapsed Initiating NSE at 12:33 Completed NSE at 12:33, 0.00s elapsed Read data files from: /usr/bin/../share/nmap Service detection performed. Please report any incorrect results at https://nmap.org/submit/ . Nmap done: 1 IP address (1 host up) scanned in 400.47 seconds  In this example we told nmap to scan every possible TCP port(-p0-), to enable OS/service detection(-A), to be verbose(-v) and to be as quick as possible(-T4). This gave us a lot of information about our target machine! # Port scanning techniques⌗ The port scanner we are going to build will only cover one of these scanning methods(i.e. the most simple) but still, it is relevant to understand the main differences between these approaches. ## ICMP scan⌗ Ping scan(or ICMP scan) is a type of scan used to make sure a host is up and running. It works by sending a ICMP echo request to the target; if you get any response, then the host is alive. The main downfall is that nowadays the majority of firewalls are configured to drop any ICMP packets, so you cannot rely on this approach to check if a host is alive. ## Half-Open TCP SYN scan⌗ This is probably the most common(and the fastest) port scanning technique. Before explaining how does it work, we need to make a step back and understand how a TCP connection is established: ### Three-way handshake process⌗ The RFC793 standard defines how a TCP connection is made. The process is usually referred as three-way handshake process since it consists in three different steps. 1. The client requests to establish a new TCP connection by sending a SYN(synchronized) packet to the remote host; 2. The server acknowledges the request by sending a SYN-ACK packet back to the client; 3. The client sends an ACK packet to complete the connection process. The interesting thing about this process is that if the port is closed, then the server sends us an RST packet, which confirms that the selected port is not open. If we don’t receive any packet at all(nor SYN-ACK nor RST), then it means that the network is filtered by a firewall. Since our scope is to check if a given port is open or not, we do not have to complete the whole handshake process; we can just send a SYN packet and wait for SYN-ACK response or for an RST/timeout. That’s the reason why it is called “half-open” scan. ## TCP Connect scan⌗ This is type of scan is basically like the previous one, except for the fact that we complete whole handshake process. This is a much slower process(since we need to send more packets for each port we intend to scan) but is indeed more stealthy from the previous one. In fact, the Half-Open SYN scan could be easily detected from an experienced sysadmin since it creates a lot of “interrupted” attempts of connections. Needless to say, neither the TCP connect scan is the most silent method of scanning a network: this type of scan logs a lot of successful attempts of establish a TCP connection without any exchange of data. Again, an experienced sysadmin can easily spot this kind of requests between a bunch of legit ones. # Stealthy scanning techniques⌗ So far we saw what are the basic port scanning techniques used by the majority of port scanner such as nmap. They all have, however, a big downfall: they are not stealthy at all. They are usually being logged and are easily spottable by a system administrator. To avoid this, other types of port scanning techniques has been developed by exploiting the characteristics of the TCP RFC to differentiate between open and closed ports. Another aspects of stealthy scans is that they typically do not require the handshaking process since they exploit some loophole of the RFC standard. ## NULL scan⌗ In the previous part of this tutorial, we developed a simple packet sniffer which printed out some information about the captured packets such as the IP header, the TCP header and the actual payload. The TCP header contained exactly 6 flags(URGENT, ACKNOWLEDGE, PUSH, RESET, SYNCHRONIZE, FINISH), below a brief explanation about their meaning: • URG: This flag informs the receiver that the data contained within the packet must be prioritized. Nowadays is quite obsolete, and honestly I did not find any real-world usage of it; • ACK: Acknowledge the client that data has been received; • PSH: When you send some data across a network, the transport layer waits to send the packet until the data segment has reached its maximum capacity. This is done to minimize the number of packets transmitted over a network(and so to avoid to jam the bandwidth). This is not desirable in all this situation where we want to send data as fast as possible(e.g., real time chatting applications such as Whatsapp, Telegram, etc.). To avoid this problem, we can set the PSH flag to 1 and the kernel will send out the packet as fast as possible; • RST: Terminate the connection; • SYN: Ask the remote server to establish a new connection; • FIN: Ask the server to terminate the connection. A NULL scan exploits a particular loophole of the RFC793 standard that force the server to respond with a RST packet to any illegal request. In this way, if we get a RST packet we know that the port is closed, if we do not receive any response, then the port is open(or filtered). The main advantages of this type of scan is that it’s very fast(since it does not have to establish any connection) and very furtive(it sneaks through firewalls and ACL filters without being spotted). ## FIN scan⌗ A FIN scan sends a TCP packet with only the FIN flag(Finish flag, used to request the termination of the connection) set. Again, since this is an illegal request for the RFC793 standard, the server must send us an RST packet if the port is closed and nothing if the port is either open or filtered. ## XMAS scan⌗ An XMAS scan is another stealthy port scanning technique that sends a TCP packet with URG, PSH and FIN flags set to 1. The reason behind its name is that, if you try to analyze the TCP header of a TCP packet with these flags set to 1, it looks like a Christmas tree: Again, since this kind of packets are considered illegal by the RFC793 standard, the server must send back a RST response on closed port and nothing on open/filtered port. The major disadvantage about these approaches is that not all operating systems implements RFC793 standard equally. Some systems(such as Microsoft Windows), send an RST response to any invalid packet even if the port is open(in the previous sections we assumed that open/filtered ports would have just ignored the illegal packet). This leads to the situation where all ports are being marked as closed. However, this downfall could be used to determine the host’s operating system: if you find at least one open port using this method, then it must not be Microsoft Windows. # Let’s build a TCP SYN port scanner⌗ Ok enough theory for today, let’s dive into the logic behind the port scanner: 1. Take an IP address/hostname and a list of ports from argv array; 2. For each port, start to sniff for SYN-ACK packets on another thread; 3. For each port, send a SYN packet; 4. Wait for a response(either open or closed port) from the child thread; As you can see, in this port scanner, we do not scan the whole range of TCP ports(i.e., 0-65535); instead, we ask the user for a list of ports, and we check whether they are open or not. If you do not like how I have implemented, you are free to fork the GitHub repo and modify it to scan the entire port spectrum by default. The code is heavily commented and is based on the code from the two previous parts of this tutorial, therefore you should be able to understand it by yourself. ## main.c⌗ /* SPS: * A SYN TCP "Half-Open" port scanner written in C * This port scanner is made for educational purposes * and should not be used on production * environments. Refer to the following link for * further information: https://blog.marcocetica.com/posts/socket_tutorial_part3/ * * Developed by Marco Cetica <ceticamarco@gmail.com> 2021 */ #include <stdio.h> // printf, puts #include <getopt.h> // getopt_long #include <string.h> // memset #include <stdlib.h> // malloc, atoi #include <unistd.h> // close syscall #include <ctype.h> // isdigit #include <sys/socket.h> // socket APIs #include <arpa/inet.h> // inet_ntoa #include <netinet/tcp.h> // TCP header #include <netinet/ip.h> // IP header #include <netdb.h> // gethostbyname #include <time.h> // for localtime #include "src/scanner.h" #define HELPER_MSG(name) printf("Try \"%s --help\" for more information.\n", name) #define MAX_PORTBUF_SIZE 1024 #define BUF_SIZE 65536 #define DNS_SERVER "1.1.1.1" #define DNS_SERVER_PORT 53 #define VERSION "0.0.1" typedef enum {false, true} bool; // Implement bool type // Private methods static unsigned int parse_ports_list(char *port_list, int *formatted_port_list); static const char *resolve_hostname(const char *address); static void get_client_ip(char *ip_addr); void helper() { puts("SPS is a SYN TCP port scanner for GNU/Linux systems\n" "-s, --hostname HOST | Set hostname to scan\n" "-p, --ports <PORT1,PORT2,...> | Check if port is open\n" "-h, --help | Print this helper\n" "-a, --about | About this tool\n" "Example: ./sps -s scanme.nmap.org -p 22,80\n" ); } int main(int argc, char **argv) { // Compute execution time double duration = 0.0; clock_t begin = clock(); if(argc < 2) { // Check argument count HELPER_MSG(argv[0]); return 1; } int sock_fd = 0; // Raw socket file descriptor struct in_addr server_ip; int ports[MAX_PORTBUF_SIZE] = {0}; // Port to be scanned unsigned int p_count = 0; const char *host; // Target host char ip_addr[MAX_PORTBUF_SIZE]; // Local IP address int opt = 0; const char *short_opts = "p:s:ha"; bool is_hostopt_enable = false, is_portopt_enable = false; struct option long_opts[] = { {"hostname", required_argument, NULL, 's'}, {"ports", required_argument, NULL, 'p'}, {"help", no_argument, NULL, 'h'}, {"about", no_argument, NULL, 'a'}, {NULL, 0, NULL, 0} }; // parse command line parameters while((opt = getopt_long(argc, argv, short_opts, long_opts, NULL)) != -1) { switch (opt) { case 's': { // Check if host is null if(optarg[0] == '\0') { printf("Error: \"-s\" parameter requires exactly one value.\n"); return 1; } // Save host parameter host = optarg, is_hostopt_enable = true; } break; case 'p': { // Check if port list is empty if(optarg[0] == '\0') { printf("Error: \"-p\" parameter requires at least one value.\n"); return 1; } // Parse port list p_count = parse_ports_list(optarg, ports); is_portopt_enable = true; } break; case 'a': #ifdef __STDC_VERSION__ printf("SRS - a SYN TCP port scanner for GNU/Linux systems.\n\ Developed by Marco Cetica 2021\n\ STDC_VERSION: %ld\n", __STDC_VERSION__); #else puts("SRS - a SYN TCP port scanner for GNU/Linux systems.\n\ Developed by Marco Cetica 2021\n"); #endif return 0; case 'h': helper(); return 0; case ':': case '?': default: return 1; } } // Check if both host and port options are specified if(is_hostopt_enable && is_portopt_enable) { // If host is a IPv4 address, store it if(inet_addr(host) != (in_addr_t)-1) server_ip.s_addr = inet_addr(host); else { // Otherwise, parse it first if(resolve_hostname(host) != NULL) server_ip.s_addr = inet_addr(resolve_hostname(host)); else { printf("Unable to resolve host \"%s\"\n", host); return 1; } } // Open raw socket sock_fd = socket(AF_INET, SOCK_RAW, IPPROTO_RAW); if(sock_fd < 0) { perror("Unable to create socket"); return 1; } // Prepare TCP/IP Header char datagram[DATAGRAM_BUF_SIZE]; struct iphdr *ip_head = (struct iphdr*)datagram; // IP header struct tcphdr *tcp_head = (struct tcphdr*)(datagram + sizeof(struct ip)); // TCP header get_client_ip(ip_addr); setup_datagram(datagram, server_ip, ip_addr, ip_head, tcp_head); // Print some info on the terminal time_t t = time(NULL); struct tm tm_s = *localtime(&t); printf("Starting SPS %s at %d-%02d-%02d %02d:%02d CEST\n", VERSION, (tm_s.tm_year + 1900), (tm_s.tm_mon + 1), (tm_s.tm_mday), (tm_s.tm_hour), (tm_s.tm_min) ); printf("PORT\t\tSTATE\n"); // For each port, set header's parameters and send SYN packet to host for(size_t current_port = 0; current_port < p_count; current_port++) { if(scan_port(sock_fd, datagram, server_ip, ip_addr, tcp_head, ports[current_port]) != 0) { perror("Unable to send SYN packet"); return 1; } } // Finally, close raw socket close(sock_fd); } else { printf("Error: both \"-p\" and \"-s\" must be specified.\n"); HELPER_MSG(argv[0]); return 1; } clock_t end = clock(); duration += (double)(end - begin) / CLOCKS_PER_SEC; printf("\nSPS done: %d ports scanned in %f seconds.\n", p_count, duration); return 0; } // Breaks "<PORT1,PORT2,...,PORTn>" into an array of integers unsigned int parse_ports_list(char *port_list, int *formatted_port_list) { char *token, *next; const char *separator = ","; unsigned int count = 0; // Start parsing string token = strtok(port_list, separator); while(token != NULL) { // Check if token is a number strtol(token, &next, 10); if((next == token) || (*next != '\0')) { printf("Error: port parameter must contains numeric values only.\n"); exit(1); } else formatted_port_list[count++] = atoi(token); // Get next value token = strtok(NULL, separator); } // Return number of ports for later usage return count; } // Convert a domain name into an IP address(IPv4). const char *resolve_hostname(const char *address) { struct hostent *host; if((host=gethostbyname(address)) == NULL) // Get host struct return NULL; return inet_ntoa(*((struct in_addr*)host->h_addr)); } // Retrieve IP address of client void get_client_ip(char *ip_addr) { // To get local ip address, we can send a UDP packet // to a DNS server, wait for its reply and then // read the "sin_addr" field from the header of // the packet. int sock = socket(AF_INET, SOCK_DGRAM, 0); if(sock < 1) { perror("Unable to create UDP socket"); exit(1); } struct sockaddr_in ip_client, name; memset(&ip_client, 0, sizeof(ip_client)); // Configure the header ip_client.sin_addr.s_addr = inet_addr(DNS_SERVER); ip_client.sin_port = htons(DNS_SERVER_PORT); ip_client.sin_family = AF_INET; // Establish a new connection int res = connect(sock, (struct sockaddr*)&ip_client, sizeof(ip_client)); if(res < 0) { perror("Unable to connect to remote host"); exit(1); } socklen_t name_len = sizeof(name); res = getsockname(sock, (struct sockaddr*)&name, &name_len); if(inet_ntop(AF_INET, &name.sin_addr, ip_addr, INET_ADDRSTRLEN) == NULL) { perror("Unable to retrieve IP address of the client"); exit(1); } // Close socket close(sock); }  ## scanner.h⌗ #ifndef SCANNER_H #define SCANNER_H #pragma once #include <stdio.h> // printf, puts #include <string.h> // memset #include <stdlib.h> // malloc, atoi #include <unistd.h> // close syscall #include <sys/socket.h> // socket APIs #include <arpa/inet.h> // inet_ntoa #include <netinet/tcp.h> // TCP header #include <netinet/ip.h> // IP header #include <pthread.h> // pthread_{create,join} #include "sniffer.h" #define DATAGRAM_BUF_SIZE 4096 typedef enum {false, true} bool; // Implement bool type // Needed for checksum computation struct pseudo_header { unsigned int source_addr; unsigned int dest_addr; unsigned char plc; unsigned char prt; unsigned short tcp_len; struct tcphdr tcp; }; struct target_header { struct in_addr target_ip; unsigned int target_port; }; struct datagram_header { char datagram[DATAGRAM_BUF_SIZE]; struct iphdr *ip_head; struct tcphdr *tcp_head; }; void setup_datagram(char *datagram, struct in_addr server_ip, const char *client_ip, struct iphdr *ip_head, struct tcphdr *tcp_head); unsigned short scan_port(int sock_fd, char *datagram, struct in_addr server_ip, const char *client_ip, struct tcphdr *tcp_head, unsigned int target_port); unsigned short compute_checksum(unsigned short *dgm, int bytes); #endif  ## scanner.c⌗ #include "scanner.h" void setup_datagram(char *datagram, struct in_addr server_ip, const char *client_ip, struct iphdr *ip_head, struct tcphdr *tcp_head) { // CLear datagram buffer memset(datagram, 0, DATAGRAM_BUF_SIZE); // Setup IP header ip_head->ihl = 5; // HELEN ip_head->version = 4; ip_head->tos = 0; // Type of service ip_head->tot_len = (sizeof(struct ip) + sizeof(struct tcphdr)); ip_head->id = htons(36521); ip_head->frag_off = htons(16384); ip_head->ttl = 64; ip_head->protocol = IPPROTO_TCP; ip_head->check = 0; ip_head->saddr = inet_addr(client_ip); ip_head->daddr = server_ip.s_addr; ip_head->check = compute_checksum((unsigned short*)datagram, ip_head->tot_len >> 1); // Setup TCP header tcp_head->source = htons(46300); // Source port tcp_head->dest = htons(80); tcp_head->seq = htonl(1105024978); tcp_head->ack_seq = 0; tcp_head->doff = (sizeof(struct tcphdr) / 4); tcp_head->fin = 0; tcp_head->syn = 1; // Set SYN flag tcp_head->rst = 0; tcp_head->psh = 0; tcp_head->ack = 0; tcp_head->urg = 0; tcp_head->window = htons(14600); // Maximum window size tcp_head->check = 0; tcp_head->urg_ptr = 0; } unsigned short scan_port(int sock_fd, char *datagram, struct in_addr server_ip, const char *client_ip, struct tcphdr *tcp_head, unsigned int target_port) { struct sockaddr_in ip_dest; struct pseudo_header psh; // Create new thread pthread_t sniff_th; struct target_header th; if(pthread_create(&sniff_th, NULL, sniffer_thread_callback, &th) < 0) { perror("Unable to create sniffer thread"); return 1; } // Save target IP and port for later usage th.target_ip = server_ip; th.target_port = target_port; // Setup packet info ip_dest.sin_family = AF_INET; ip_dest.sin_addr.s_addr = server_ip.s_addr; // Setup TCP header tcp_head->dest = htons(target_port); // Set target port tcp_head->check = 0; // Configure pseudo header(needed for checksum) psh.source_addr = inet_addr(client_ip); psh.dest_addr = ip_dest.sin_addr.s_addr; psh.plc = 0; psh.prt = IPPROTO_TCP; psh.tcp_len = htons(sizeof(struct tcphdr)); // Copy TCP header into our pseudo header memcpy(&psh.tcp, tcp_head, sizeof(struct tcphdr)); tcp_head->check = compute_checksum((unsigned short*)&psh, sizeof(struct pseudo_header)); // Send packet to target if(sendto(sock_fd, datagram, sizeof(struct iphdr) + sizeof(struct tcphdr), 0, (struct sockaddr*)&ip_dest, sizeof(ip_dest)) < 0) { perror("Unable to send SYN packet"); return 1; } // Wait for sniffer thread to receive a response pthread_join(sniff_th, NULL); return 0; } // Compute checksum of IP header // Refer to https://www.ietf.org/rfc/rfc793.txt for reference unsigned short compute_checksum(unsigned short *dgm, int bytes) { register long sum = 0; register short answer; unsigned int odd_byte; while(bytes > 1) { sum += *dgm++; bytes -= 2; } if(bytes == 1) { odd_byte = 0; *((unsigned char*)&odd_byte) = *(unsigned char*)dgm; sum += odd_byte; } sum = (sum >> 16) + (sum & 0xFFFF); sum += (sum >> 16); answer = (short)~sum; return answer; }  ## sniffer.h⌗ #ifndef SNIFFER_H #define SNIFFER_H #pragma once #include <stdio.h> // printf, puts #include <string.h> // memset #include <stdlib.h> // malloc, atoi #include <unistd.h> // close syscall #include <sys/socket.h> // socket APIs #include <arpa/inet.h> // inet_ntoa #include <netinet/tcp.h> // TCP header #include <netinet/ip.h> // IP header #include "scanner.h" #define BUF_SIZE 65536 enum status { CLOSED, OPEN }; void *sniffer_thread_callback(void *ptr); #endif  ## sniffer.c⌗ #include "sniffer.h" static void sniff_network(struct in_addr server_ip, const unsigned int port); static void print_result(unsigned int port, enum status st); void *sniffer_thread_callback(void *ptr) { struct target_header *th = ptr; sniff_network(th->target_ip, th->target_port); return (void*)NULL; } void sniff_network(struct in_addr server_ip, const unsigned int port) { int sock_raw; int saddr_size, data_size; struct sockaddr saddr; unsigned char *buf = (unsigned char*)malloc(BUF_SIZE); // Create new raw socket sock_raw = socket(AF_INET, SOCK_RAW, IPPROTO_TCP); if(sock_raw < 0) { perror("Unable to create socket"); exit(1); } saddr_size = sizeof(saddr); // Start receiving packets data_size = recvfrom(sock_raw, buf, BUF_SIZE, 0, (struct sockaddr*)&saddr, (socklen_t*)&saddr_size); if(data_size < 0) { perror("Unable to receive packets"); exit(1); } // Process data struct iphdr *ip_head = (struct iphdr*)buf; struct sockaddr_in source; unsigned short ip_head_len = ip_head->ihl*4; struct tcphdr *tcp_head = (struct tcphdr*)(buf + ip_head_len); memset(&source, 0, sizeof(source)); source.sin_addr.s_addr = ip_head->saddr; if(ip_head->protocol == IPPROTO_TCP) { // Now check whether it's a SYN-ACK packet or not if(tcp_head->syn == 1 && tcp_head->ack == 1 && source.sin_addr.s_addr == server_ip.s_addr) print_result(port, OPEN); else print_result(port, CLOSED); } free(buf); } // Print the scan result just like Nmap void print_result(unsigned int port, enum status st) { printf("%d/tcp\t\t%s\n", port, (st == OPEN ? "open" : "closed")); }  ## Makefile⌗ TARGET = sps CC = gcc CFLAGS = -Wall -Wextra -Werror -pedantic-errors -std=gnu11 LFLAGS = -lpthread all:$(TARGET)

$(TARGET): main.o scanner.a sniffer.a$(CC) $(CFLAGS)$(LFLAGS) $^ -o$@

main.o: main.c
$(CC)$(CFLAGS) -c $< -o$@

sniffer.a: sniffer.o
ar rcs $@$^

scanner.a: scanner.o
ar rcs $@$^

scanner.o: src/scanner.c src/scanner.h
$(CC)$(CFLAGS) -c -o $@$<

sniffer.o: src/sniffer.c src/sniffer.h
$(CC)$(CFLAGS) -c -o $@$<

clean:
rm -f *.o *.a $(TARGET)  Organize source files in the following way: ├── main.c ├── Makefile └── src ├── scanner.c ├── scanner.h ├── sniffer.c └── sniffer.h  And compile the whole thing with $> make clean all


Let’s try our tool to scan scanme.nmap.org:

\$> sudo ./sps -s scanme.nmap.org -p 80,22,9929,11211,31337
Starting SPS 0.0.1 at 2021-06-16 23:21 CEST
PORT		      STATE
80/tcp		    open
22/tcp		    open
9929/tcp       open
11211/tcp	    closed
31337/tcp	    open

SPS done: 5 ports scanned in 0.004033 seconds.


We did it! We got the same result(more or less) as the previous step. You can avoid copying the source code by cloning the GitHub repository.