Writing a Network Scanner using Python

Dharmil Chhadva
Level Up Coding
Published in
12 min readJul 25, 2020

--

Note: The following tutorial works on any Linux Distributions, provided you have the root access i.e. you’ll have to execute this script using the root user.

What is a Network Scanner?

A network scanner is a software tool that scans the network for connected devices. It is also used for diagnostic and investigative purposes to find and categorize what devices are running on a network. This tool takes an IP address or a range of IP addresses as input and then scans each IP Addresses sequentially and determines whether a device is present on that particular IP address or not. It scans the network and returns an IP address and it’s corresponding MAC address if the device is present. A popular tool that’s commonly used CyberSecurity professionals is nmap.

How does it work?

To understand how the Network Scanner scans the entire network we need to first understand what is ARP (Address Resolution Protocol).

In a network, most of the computers use the IP Address to communicate with other devices, however, in reality, the communication happens over the MAC Address. ARP is used to find out the MAC Address of a particular device whose IP address is known. For instance, a device wants to communicate with the other device on the network, then the sending device uses ARP to find the MAC Address of the device that it wants to communicate with. ARP involves two steps to find the MAC address:

  1. The sending device sends an ARP Request containing the IP Address of the device it wants to communicate with. This request is broadcasted meaning every device in the network will receive this but only the device with the intended IP address will respond.
  2. After receiving the broadcast message, the device with the IP address equal to the IP address in the message will send an ARP Response containing its MAC Adress to the sender.

Network Scanner uses ARP Request and Response to scan the entire network to find active devices on the network and also to find their MAC Addresses.

If it is still not clear what ARP is and how it works then refer to the images below.

Fig 1. ARP Request
Fig 2. ARP Response

Now that we know how does the network scanner works internally, let’s start writing it in Python.

Modules Used:

  1. argparse: To understand what this does read my first article here.
  2. Scapy: Enables the user to send, sniff and dissect and forge network packets. This capability allows the development of tools that can probe, scan, or attack networks. It can forge or decode packets of a wide number of protocols, send them on the wire, capture them, match requests and replies, and much more. It easily handles most tasks like scanning, tracerouting, probing, unit tests, attacks, or network discovery.

Okay, before jumping to writing the code in Python I’ll tell you about the setup that I have: I am currently on Windows 10 and I have Virtualbox running with two VMs (1. Kali Linux and 2. Windows 10). I will execute the python script on my Kali Linux machine and try to scan the entire network. Note: The VMs are configured to use NatNetwork in Virtualbox. To know more about NatNetwork and how to configure a VM to use it, read this.

Writing the Network Scanner

As we know what modules are required, let’s start writing the script.

Step 1: Import the modules discussed above.

Importing Modules

Step 2: Implementing the functionality to allow the users to pass command line arguments.

To add this feature to our script, we’ll need to make use of the argparse module that we imported in Step 1.

Adding Command Line Arguments Functionality

To know more about how argparse works and what does the above part of the script does and what functionality it adds to the script, read the entire Step 2 from my previous article of how to change MAC Address of a device.

The above code allows the user to provide the input for interface value as follows:

root@kali:~# python3 network_scanner.py -t IP_Address/IP_Addresses

OR

root@kali:~# python3 network_scanner.py --target IP_Address/IP_Addresses

IP_Address = Specific IP Address that you want to scan to check whether there is a device that’s using this IP Address or not.

IP_Addresses = Range of IP Addresses that you want to scan.

Step 3: Writing function that scans the network

In this function, we’ll have to do the following things to be able to scan the network:

  • Create an ARP Request.
  • Create an Ethernet Frame.
  • Place the ARP Request inside the Ethernet Frame.
  • Send the combined frame and receive responses.
  • Parse the responses and print the results.
Scan Function

Note that the function scan takes an IP Address as an argument. Now, let's break down the above code line by line.

def scan(ip):
.....
.....
scan('10.0.2.15')

Now the first thing that we want to do is to create an ARP Request (See Fig 1.) frame using scapy.

arp_req_frame = scapy.ARP(pdst = ip)

The above line of code creates an ARP Request frame using scapy’s ARP Class with the destination IP Address (pdst) provided as an argument to the function. The destination IP Address is set to the IP Address passed to the function because we want to direct the ARP Request frame to the IP Address we want. For instance, if ip = ‘10.0.2.15’ then the ARP Request is targetted for that IP Address only.

Now, the question is how did I come to know that destination IP Address is represented as pdst in ARP class? For this, scapy has a built-in function that we can use to find what member variables (variables) or fields a class has.

arp_req_frame = scapy.ARP(pdst = ip)
print(scapy.ls(scapy.ARP()))
OUTPUT
root@kali:~/Desktop/Network Scanner# python3 network_scanner.py -t 10.0.2.1/24
hwtype : XShortField = 1 (1)
ptype : XShortEnumField = 2048 (2048)
hwlen : FieldLenField = None (None)
plen : FieldLenField = None (None)
op : ShortEnumField = 1 (1)
hwsrc : MultipleTypeField = '08:00:27:35:21:2e' (None)
psrc : MultipleTypeField = '10.0.2.9' (None)
hwdst : MultipleTypeField = '00:00:00:00:00:00' (None)
pdst : MultipleTypeField = '0.0.0.0' (None)

Note: 10.0.2.1/24 means IP Addresses from 10.0.2.1 to 10.0.2.254

The scapy.ls() function returns the fields a particular class has. It works for every class that scapy provides. We just have to pass the class name as scapy.class_name() to it. In the example above, we wanted to know about the fields or member variables the ARP class has and so we used the scapy.ls(scapy.ARP()). This function then gave us the response as shown in the output above. This function provides the name of the fields, a small description of that field, and the default value that each field holds.

The main fields to know are:

  • hwsrc = Source MAC Address.
  • psrc = Source IP Address.
  • hwdst = Destination MAC Address.
  • pdst = Destination IP Address.

Note: The hwsrc and psrc will always have the default value of the machine on which the ARP Request packet was created. In our case, the IP Address of my Kali machine is ‘10.0.2.9’ and MAC Address is ‘08:00:27:35:21:2e’ and therefore the field psrc and hwsrc has these values in them. Also, the default value for hwdst and pdst will ‘00:00:00:00:00:00’ and ‘0.0.0.0’ respectively.

So, this is how we can change any field’s value as we want while creating an instance of the class. Additionally, we can also view the ARP Request itself by another function that scapy provides.

arp_req_frame = scapy.ARP(pdst = ip)
print(arp_req_frame.summary())

Upon execution of the above code, we get the following output. You can see that it says ‘who has 10.0.2.3 says 10.0.2.9’, this means the packet is a request packet and it’s asking everyone that who has the IP Address of 10.0.2.3. The second part i.e says 10.0.2.9 provides information about the source or sender of the ARP Request to all the receiving devices and thus the receiving device with the IP Address 10.0.2.3 will know whom to send the ARP Response. You can see that the output generated below is the same as what Fig 1 illustrates.

root@kali:~/Desktop/Network Scanner# python3 network_scanner.py -t 10.0.2.3OUTPUT
root@kali:~/Desktop/Network Scanner# python3 network_scanner.py -t 10.0.2.3
ARP who has 10.0.2.3 says 10.0.2.9

Another useful function that scapy provides is show().

arp_req_frame = scapy.ARP(pdst = ip)
print(arp_req_frame.show())

After the execution, it generates the following output.

root@kali:~/Desktop/Network Scanner# python3 network_scanner.py -t 10.0.2.3OUTPUT
###[ ARP ]###
hwtype = 0x1
ptype = IPv4
hwlen = None
plen = None
op = who-has
hwsrc = 08:00:27:35:21:2e
psrc = 10.0.2.9
hwdst = 00:00:00:00:00:00
pdst = 10.0.2.3

The above output shows the various fields of an ARP Request packet and its corresponding values. Note that the pdst (destination IP Address) has a value of 10.0.2.3 because when executing the code we provide a command-line argument for which IP Address to scan and this IP Address is then set to pdst. This is because at the time of creating an instance of the ARP class we provided the IP Address of the destination device using scapy.ARP(pdst = ip) where ip is a variable name and its value is taken from the command-line argument that was passed at the time of execution. We already covered how to add command-line argument functionality that accepts input in Step 2.

After creating the ARP Request packet we need to now create an Ethernet Frame. The Ethernet frame contains fields such as Source and Destination Hardware (MAC) among others. Now, as the communication inside a network is carried out using the MAC Address, we can set the value of destination hardware address field to theMAC Address to which we want to communicate. Learn more about Ethernet Frame here.

The ARP Request is supposed to be broadcasted (transmitted to every IP Address in a network). Therefore, to broadcast the ARP Request we set the Destination Address field of Ethernet field to ‘ff:ff:ff:ff:ff:ff’ as this is a broadcast MAC Address. We now do this using scapy.

broadcast_ether_frame = scapy.Ether(dst = "ff:ff:ff:ff:ff:ff")

The above line of code creates an Ethernet Frame with dst (Destination Address) is set to ‘ff:ff:ff:ff:ff:ff’. Note, we can use scapy.ls() and show() function on Ethernet class. The fields of the Ethernet frame can be viewed using scapy.ls() function similarly to how we did it on ARP Class. The show() function can also be used similarly as we did for ARP Class. Output for both the function can be seen below.

print(scapy.ls(scapy.Ether()))OUTPUT
dst : DestMACField = 'ff:ff:ff:ff:ff:ff' (None)
src : SourceMACField = '08:00:27:35:21:2e' (None)
type : XShortEnumField = 36864 (36864)
--------------------------------------------------------------------print(broadcast_ether_frame.show())OUTPUT
###[ Ethernet ]###
dst = ff:ff:ff:ff:ff:ff
src = 08:00:27:35:21:2e
type = 0x9000

The next step is to combine the ARP Request and the Ethernet Frame. We did this using scapy because it allows a very convenient way to combine frames. We do it by the following line of code.

broadcast_ether_arp_req_frame = broadcast_ether_frame/arp_req_frame

The above code creates a new frame by combining the ARP Request and Ethernet Frame using the ‘/’ sign. This is because scapy allows us to combine frames using this.

Now if we call the show() function we can see that the final combined frame consists of both Ethernet and ARP Request.

root@kali:~/Desktop/Network Scanner# python3 network_scanner.py -t 10.0.2.3OUTPUT
###[ Ethernet ]###
dst = ff:ff:ff:ff:ff:ff
src = 08:00:27:35:21:2e
type = ARP
###[ ARP ]###
hwtype = 0x1
ptype = IPv4
hwlen = None
plen = None
op = who-has
hwsrc = 08:00:27:35:21:2e
psrc = 10.0.2.9
hwdst = 00:00:00:00:00:00
pdst = 10.0.2.3

We can see that the type field of Ethernet Frame now holds ARP as its type. Also, the ARP part of the frame is identified as ARP Request by the value of the op field. If op = who-has then it means its an ARP Request and if op = is-at then it is ARP Response.

Now the only thing that is remaining is to sent the combined frame and to receive the responses. To send the requests and receive the responses we will use a function provided by scapy that not only sends the requests but also returns the responses.

This function returns the captured responses as a python tuple out of which the first element of the tuple has the answered responses where the devices responded and the second element has the responses which were unanswered. The responses which are in the unanswered list mean that there are no devices that use those IP Addresses.

The function that scapy provides to do the above tasks is called scapy.srp(). This function takes the actual frame to be transmitted as an argument. You can see that we have passed broadcast_ether_arp_req_frame (our final combined frame) is passed to the function below. It also takes a timeout input which tells the scapy for how much time period it should wait to receive a response before moving further. What this means is, from the below example timeout = 1 means the scapy will wait for 1 second for the response and if the response is not received it will move further to send the packet to the next IP Address. The argument verbose = False is not important and it only stops the scapy from printing its own messages on the screen.

answered_list= scapy.srp(broadcast_ether_arp_req_frame, timeout = 1, verbose = False)
print('Total Number of Responses ->', len(answered_list))
print('\n-----------Answered Responses---------')
print('Number of Answered Responsed ->', len(answered_list[0]))
print('\n')
for i in range(0,len(answered_list[0])):
print(answered_list[0][i])
print('\n')
print('\n-----------UnAnswered Responses---------')
print('Number of UnAnswered Responsed ->', len(answered_list[1]))
print('\n')
for i in range(0,len(answered_list[1])):
print(answered_list[1][i])
print('\n')

The above code displays the total number of answered (responded) responses and the actual responses itself. It also displays the total number of unanswered (not responded) responses and the actual responses.

Note: This code is not used in the final script and is just for understanding the output returned by scapy.srp() function. The above code generates the following output.

scapy.srp() function’s Output

From the above image, we can see that the total number of responses received was two. This is because the tuple consists of two elements answered and unanswered. Furthermore, we can see that the total answered responses are three which means that only three devices responded with ARP Response. Below this, we can also see the actual responses itself. After this, we see that the total unanswered responses are 253 which means 253 devices did not respond. We can also see some of the responses that scapy recorded for unanswered responses.

The main aim of showing the above output was to make you understand that we only need the answered responses and not the unanswered one. Therefore, we can access only the answered responses using the following line of code.

answered_list = scapy.srp(broadcast_ether_arp_req_frame, timeout = 1, verbose = False)[0]

As the answered_list is a tuple we can extract the individual elements by providing the index in square brackets. The above code will only store answered responses.

Now, all that is left is to extract the IP Address and the MAC Address from each of the answered responses. We’ll use Python dictionaries inside of a list for that purpose.

result = []
for i in range(0,len(answered_list)):
client_dict = {"ip" : answered_list[i][1].psrc, "mac" : answered_list[i][1].hwsrc}
result.append(client_dict)
print(result)

The above code creates an empty list called result and then creates a dictionary called client_dict for each response and then appends it to the result list. The client_dict has two keys ‘ip’ and ‘mac’ which stores the IP Address and MAC Address respectively. The output of the above code for our case will be:

root@kali:~/Desktop/Network Scanner# python3 network_scanner.py -t 10.0.2.1/24[{'ip': '10.0.2.1', 'mac': '52:54:00:12:35:00'}, {'ip': '10.0.2.2', 'mac': '52:54:00:12:35:00'}, {'ip': '10.0.2.3', 'mac': '08:00:27:22:d8:10'}]

The next step in the scan() function is to return the result list. We’ll use the result list in another function to print the results in a certain format.

Step 4: Writing function to print the results in a certain format.

Display function to print in a certain format

The above display() function takes the result list as an input and displays the result in a certain format. It produces the output shown below.

root@kali:~/Desktop/Network Scanner# python3 network_scanner.py -t 10.0.2.1/24
-----------------------------------
IP Address MAC Address
-----------------------------------
10.0.2.1 52:54:00:12:35:00
10.0.2.2 52:54:00:12:35:00
10.0.2.3 08:00:27:22:d8:10

This is where the script is complete and we have created a network scanner using Python successfully.

Working Example

I am using a Virtualbox setup with two VMs (Kali Linux and Windows 10). I’ll show you that the scanner that we created works perfectly fine. I’ll execute the script in Kali Linux and I will also start Windows 10 VM. The expectation is that the output should contain the IP Address and MAC Address of the Windows 10 VM also.

Windows 10 VM

From the above image, we can note two things about the Windows 10 VM:

  1. IP Address = 10.0.2.15
  2. MAC Address = 08-00-27-e6-e5-59 (represented as Physical Address in the image below the Description field)

Now, the output generated by the script on the Kali Linux machine.

Script Output on Kali Linux VM

From the above image, we can see that the network scanner script that we created scans the entire network including the Windows 10 VM which is in the same network.

Thank You for reading. The entire code can found in my Github Repository.

--

--