Writing a Network Scanner using Python
Tags: security, python, cybersecurity, tutorial
Published: 2024-02-27 00:52:19

>  **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](https://nmap.org/)**.

## 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**](https://cdn-images-1.medium.com/max/4634/1*mda_kqjm4ONDM7jQnKZ7TQ.jpeg)

![**Fig 2. ARP Response**](https://cdn-images-1.medium.com/max/4954/1*LTdIvgRjXx9XewTRZVeAyg.jpeg)

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

## Modules Used:

 1. [argparse](https://docs.python.org/3.4/library/subprocess.html): To understand what this does read my first article [here](https://medium.com/@dharmilch18/changing-mac-address-using-python-8a16fc4a3563#f878).

 2. [Scapy](https://scapy.readthedocs.io/en/latest/extending.html): 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 ](https://www.virtualbox.org/)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](https://www.techrepublic.com/article/how-to-create-multiple-nat-networks-in-virtualbox/).

## Writing the Network Scanner

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

### **Step 1: Import the modules discussed above.**
{% gist https://gist.github.com/dharmil18/cac74e5b40f4fc6d896bdc7612418ce5.js %}
### **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.
{% gist https://gist.github.com/dharmil18/00a60695e7ec9c9e8db6b933c81452e4.js %}
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](https://medium.com/@dharmilch18/changing-mac-address-using-python-8a16fc4a3563#03c5).

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.
{% gist https://gist.github.com/dharmil18/34c0f7b4ca419bbb4e20c7631767b989.js %}
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.3

    **OUTPUT
    **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.3

    **OUTPUT**
    ###[ 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](https://en.wikipedia.org/wiki/Ethernet_frame) 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](https://www.geeksforgeeks.org/ethernet-frame-format/).

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.3

    **OUTPUT
    **###[ 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](https://cdn-images-1.medium.com/max/3756/1*ok3M83G4Ujj79XkY87hoqQ.png)

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.
{% gist https://gist.github.com/dharmil18/908abd40c9b82aad4ee37a69e419164b.js %}
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](https://cdn-images-1.medium.com/max/2038/1*s7JrqxS0JWCTefI0tyH__w.png)

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](https://cdn-images-1.medium.com/max/3840/1*JI3MNlVOdmBJd9ki-crvpw.png)

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](https://github.com/dharmil18/Network-Scanner).