Book Image

Mastering Python for Networking and Security - Second Edition

By : José Ortega
Book Image

Mastering Python for Networking and Security - Second Edition

By: José Ortega

Overview of this book

It’s now more apparent than ever that security is a critical aspect of IT infrastructure, and that devastating data breaches can occur from simple network line hacks. As shown in this book, combining the latest version of Python with an increased focus on network security can help you to level up your defenses against cyber attacks and cyber threats. Python is being used for increasingly advanced tasks, with the latest update introducing new libraries and packages featured in the Python 3.7.4 recommended version. Moreover, most scripts are compatible with the latest versions of Python and can also be executed in a virtual environment. This book will guide you through using these updated packages to build a secure network with the help of Python scripting. You’ll cover a range of topics, from building a network to the procedures you need to follow to secure it. Starting by exploring different packages and libraries, you’ll learn about various ways to build a network and connect with the Tor network through Python scripting. You will also learn how to assess a network's vulnerabilities using Python security scripting. Later, you’ll learn how to achieve endpoint protection by leveraging Python packages, along with writing forensic scripts. By the end of this Python book, you’ll be able to use Python to build secure apps using cryptography and steganography techniques.
Table of Contents (22 chapters)
1
Section 1: The Python Environment and System Programming Tools
4
Section 2: Network Scripting and Extracting Information from the Tor Network with Python
8
Section 3: Server Scripting and Port Scanning with Python
12
Section 4: Server Vulnerabilities and Security in Python Modules
16
Section 5: Python Forensics

Python functions, classes, and managing exceptions

In this section, we will review Python functions, classes, and how to manage exceptions in Python scripts. We will review some examples for declaring and using both in our script code. We’ll also review the main exceptions we can find in Python for inclusion in our scripts.

Python functions

A function is a block of code that performs a specific task when the function is called (invoked). You can use functions to make your code reusable, better organized, and more readable. Functions can have parameters and return values.

There are at least four basic types of functions in Python:

  • Built-in functions: These are an integral part of Python. You can see a complete list of Python’s built-in functions at https://docs.python.org/3/library/functions.html.
  • Functions that come from pre-installed modules.
  • User-defined functions: These are written by developers in their own code and they use them freely in Python.
  • The lambda function: This allow us to create anonymous functions that are built using expressions such as product = lambda x,y : x * y, where lambda is a Python keyword and x and y are the function parameters.

With the builtins module, we can see all classes and methods available by default in Python:

>>> import builtins
>>> dir(builtins)
[‘ArithmeticError’, ‘AssertionError’, ‘AttributeError’, ‘BaseException’, ‘BlockingIOError’, ‘BrokenPipeError’, ‘BufferError’, ‘BytesWarning’, ‘ChildProcessError’, ‘ConnectionAbortedError’, ‘ConnectionError’, ‘ConnectionRefusedError’, ‘ConnectionResetError’, ‘DeprecationWarning’, ‘EOFError’, ‘Ellipsis’, ‘EnvironmentError’, ‘Exception’, ‘False’, ‘FileExistsError’, ‘FileNotFoundError’, ‘FloatingPointError’, ‘FutureWarning’, ‘GeneratorExit’, ‘IOError’, ‘ImportError’, ‘ImportWarning’, ‘IndentationError’, ‘IndexError’, ‘InterruptedError’, ‘IsADirectoryError’, ‘KeyError’, ‘KeyboardInterrupt’, ‘LookupError’, ‘MemoryError’, ‘ModuleNotFoundError’, ‘NameError’, ‘None’, ‘NotADirectoryError’, ‘NotImplemented’, ‘NotImplementedError’, ‘OSError’, ‘OverflowError’, ‘PendingDeprecationWarning’, ‘PermissionError’, ‘ProcessLookupError’, ‘RecursionError’, ‘ReferenceError’, ‘ResourceWarning’, ‘RuntimeError’, ‘RuntimeWarning’, ‘StopAsyncIteration’, ‘StopIteration’, ‘SyntaxError’, ‘SyntaxWarning’, ‘SystemError’, ‘SystemExit’, ‘TabError’, ‘TimeoutError’, ‘True’, ‘TypeError’, ‘UnboundLocalError’, ‘UnicodeDecodeError’, ‘UnicodeEncodeError’, ‘UnicodeError’, ‘UnicodeTranslateError’, ‘UnicodeWarning’, ‘UserWarning’, ‘ValueError’, ‘Warning’, ‘ZeroDivisionError’, ‘__build_class__’, ‘__debug__’, ‘__doc__’, ‘__import__’, ‘__loader__’, ‘__name__’, ‘__package__’, ‘__spec__’, ‘abs’, ‘all’, ‘any’, ‘ascii’, ‘bin’, ‘bool’, ‘breakpoint’, ‘bytearray’, ‘bytes’, ‘callable’, ‘chr’, ‘classmethod’, ‘compile’, ‘complex’, ‘copyright’, ‘credits’, ‘delattr’, ‘dict’, ‘dir’, ‘divmod’, ‘enumerate’, ‘eval’, ‘exec’, ‘exit’, ‘filter’, ‘float’, ‘format’, ‘frozenset’, ‘getattr’, ‘globals’, ‘hasattr’, ‘hash’, ‘help’, ‘hex’, ‘id’, ‘input’, ‘int’, ‘isinstance’, ‘issubclass’, ‘iter’, ‘len’, ‘license’, ‘list’, ‘locals’, ‘map’, ‘max’, ‘memoryview’, ‘min’, ‘next’, ‘object’, ‘oct’, ‘open’, ‘ord’, ‘pow’, ‘print’, ‘property’, ‘quit’, ‘range’, ‘repr’, ‘reversed’, ‘round’, ‘set’, ‘setattr’, ‘slice’, ‘sorted’, ‘staticmethod’, ‘str’, ‘sum’, ‘super’, ‘tuple’, ‘type’, ‘vars’, ‘zip’]

In Python, functions include reusable code-ordered blocks. This allows a programmer usually to write a block of code to perform a single, connected action. Although Python offers several built-in features, a programmer may build user-defined functionality.

In addition to helping us program and debug by dividing the program into small parts, the functions also allow us to manage code in a more reusable manner.

Python functions are defined using the def keyword with the function name, followed by the function parameters. The function’s body is composed of Python statements to be executed. You have the option to return a value to the function caller at the end of the function, or if you do not assign a return value, it will return the None object by default.

For instance, we can define a function that returns True if the element is within the sequence given a sequence of numbers and an item passed by a parameter, and False otherwise:

>>> def contains(sequence,item):
>>>	for element in sequence:
>>>		if element == item:
>>>			return True
>>>		return False
>>> print contains([100,200,300,400],200)
True
>>> print contains([100,200,300,400],300)
True
>>> print contains([100,200,300,400],350)
False

Two important factors make parameters different and special:

  • Parameters only exist within the functions in which they were described, and the only place where the parameter can be specified is a space between a pair of parentheses in the def state.
  • Assigning a value to the parameter is done at the time of the function’s invocation by specifying the corresponding argument.

Python classes

Python is an object-oriented language that allows you to create classes from such descriptions and instantiate them. The functions specified inside the class are instance methods, also known as member functions.

Python’s way of constructing objects is via the class keyword. A Python object is an assembly of methods, variables, and properties. Lots of objects can be generated with the same class description.

Here is a simple example of a protocol object definition. You can find the following code in the protocol.py file:

class protocol(object):
def __init__(self, name, number,description):
                 self.name = name
        self.number = number
        self.description = description
def getProtocolInfo(self):
         return self.name+ “ “+str(self.number)+ “ “+self.description

In the previous code, we can see a method with the name __init__, which represents the class constructor. If a class has a constructor, it is invoked automatically and implicitly when the object of the class is instantiated.

The init method is a special method that acts as a constructor method to perform the necessary initialization operation. The method’s first parameter is a special keyword, and we use the self-identifier for the current object reference. Basically, the self keyword is a reference to the object itself and provides a way for its attributes and methods to access it.

The constructor method has to have the self parameter and may have more parameters than just self; if this happens, the way in which the class name is used to create the object must reflect the __init__ definition. This method is used to set up the object, in other words, properly initialize its internal state, create instance variables, instantiate any other objects if their existence is needed, and so on.

Important note

In Python, self is a reserved language word and is mandatory. It is the first parameter of traditional methods and through it you can access the class attributes and methods. This parameter is equivalent to the pointer that can be found in languages such as C ++ or Java.

An object is a set of the requirements and qualities assigned to a specific class. Classes form a hierarchy, which means that an object belonging to a specific class belongs to all the superclasses at the same time.

To build an object, write the class name followed by any parameter needed in parentheses. These are the parameters that will be transferred to the init method, which is the process that is called when the class is instantiated:

>>> protocol_http= protocol(“HTTP”, 80, “Hypertext transfer protocol”)

Now that we have created our object, we can access its attributes and methods through the object.attribute and object.method() syntax:

>>> protocol_http.name
>>> protocol_http.number
>>> protocol_http.description
>>> protocol_http.getProtocolInfo()

In summary, object programming is the art of defining and expanding classes. A class is a model of a very specific part of reality, reflecting properties and methods found in the real world. The new class may add new properties and new methods, and therefore may be more useful in specific applications.

Python inheritance

Let’s define one of the fundamental concepts of object programming, named inheritance. Any object bound to a specific level of a class hierarchy inherits all the traits (as well as the requirements and qualities) defined inside any of the superclasses.

The core principles of the languages of object-oriented programming are encapsulation, inheritance, and polymorphism. In an object-oriented language, by creating hierarchies, objects are related to others, and it is conceivable that some objects inherit the properties and methods of other objects, expanding their actions and/or specializing.

Inheritance allows us to create a new class from another, inherit its attributes and methods, and adapt or extend them as required. This facilitates the reuse of the code since you can implement the basic behaviors and data in a base class and specialize them in the derived classes.

To implement inheritance in Python, we need to add the name of the class that is inherited within parentheses to show that a class inherits from another class, as we can see in the following code:

>>>class MyList(list):
>>>	def max_min(self):
>>>		return max(self),min(self)
>>>myList= MyList()
>>>myList.extend([100,200,300,500])
>>>print(myList)
[100, 200, 300, 500]
>>>print(myList.max_min())
(500, 100)

As we can see in the previous example, inheritance is a common practice of passing attributes and methods from the superclass to a newly created class. The new class inherits all the already existing methods and attributes, but is able to add some new ones if needed.

Managing exceptions

Each time your code tries to do something wrong, Python stops your program, and it creates a special kind of data, called an exception. Both of these activities are known as raising an exception. We can say that Python always raises an exception (or that an exception has been raised) when it has no idea what to do with your code.

Exceptions are errors that Python detects during execution of the program. If the interpreter experiences an unusual circumstance, such as attempting to divide a number by 0 or attempting to access a file that does not exist, an exception is created or thrown, telling the user that there is a problem.

When the exception is not detected, the execution flow is interrupted, and the console shows the information associated with the exception so that the developer can solve the problem with the information returned by the exception.

Let’s see a Python code throwing an exception while attempting to divide 1 by 0. We’ll get the following error message if we execute it:

>>>def division(a,b):
>>>	return a/b
>>>def calculate():
>>>division(1,0)
>>>calculate()
Traceback (most recent call last):
    File “<stdin>”, line 1, in <module>
    File “<stdin>”, line 2, in calculate
    File “<stdin>”, line 2, in division
ZeroDivisionError: division by zero

In the previous example, we can see traceback, which consists of a list of the calls that caused the exception. As we see in the stack trace, the error was caused by the call to the calculate() method, which, in turn, calls division (1, 0), and ultimately the execution of the a/b sentence of division in line 2.

Important note

Python provides effective tools that allow you to observe exceptions, identify them, and handle them efficiently. This is possible due to the fact that all potential exceptions have their unambiguous names, so you can categorize them and react appropriately.

In Python, we can use a try/except block to resolve situations related to exception handling. Now, the program tries to run the division by zero. When the error happens, the exceptions manager captures the error and prints a message that is relevant to the exception:

>>>try:
>>>	print(“10/0 = “,str(10/0))
>>>except Exception as exception:
>>>	print(“Error =”,str(exception))
Error = division by zero

The try keyword begins a block of the code that may or may not be performing correctly. Next, Python tries to perform some operations; if it fails, an exception is raised and Python starts to look for a solution.

At this point, the except keyword starts a piece of code that will be executed if anything inside the try block goes wrong – if an exception is raised inside a previous try block, it will fail here, so the code located after the except keyword should provide an adequate reaction to the raised exception.

In the following example, we try to create a file-type object. If the file is not found in the filesystem, an exception of the IOError type is thrown, which we can capture thanks to our try except block:

>>>try:
>>>	f = open(‘file.txt’,”r”)
>>>except Exception as exception:
>>>	print(“File not found:”,str(exception)) 
File not found: [Errno 2] No such file or directory: ‘file.txt’

In the first block, Python tries to perform all instructions placed between the try: and except: statements; if nothing is wrong with the execution and all instructions are performed successfully, the execution jumps to the point after the last line of the except: block, and the block’s execution is considered complete.

The following code raises an exception related to accessing an element that does not exist in the list:

>>> list = []
>>> x = list[0]
Traceback (most recent call last):
IndexError: list index out of range

Python 3 defines 63 built-in exceptions, and all of them form a tree-shaped hierarchy. Some of the built-in exceptions are more general (they include other exceptions), while others are completely concrete. We can say that the closer to the root an exception is located, the more general (abstract) it is.

Some of the exceptions available by default are listed here (the class from which they are derived is in parentheses):

  • BaseException: The class from which all exceptions inherit.
  • Exception (BaseException): An exception is a special case of a more general class named BaseException.
  • ZeroDivisionError (ArithmeticError): An exception raised when the second argument of a division is 0. This is a special case of a more general exception class named ArithmeticError.
  • EnvironmentError (StandardError): This is a parent class of errors related to input/output.
  • IOError (EnvironmentError): This is an error in an input/output operation.
  • OSError (EnvironmentError): This is an error in a system call.
  • ImportError (StandardError): The module or the module element that you wanted to import was not found.

All the built-in Python exceptions form a hierarchy of classes. The following script dumps all predefined exception classes in the form of a tree-like printout.

You can find the following code in the get_exceptions_tree.py file:

def printExceptionsTree(ExceptionClass, level = 0):
        if level > 1:
                print(“     |” * (level - 1), end=””)
        if level > 0:
                print(“     +---”, end=””)
        print(ExceptionClass.__name__)
        for subclass in ExceptionClass.__subclasses__():
                printExceptionsTree(subclass, level + 1)
printExceptionsTree(BaseException)

As a tree is a perfect example of a recursive data structure, a recursion seems to be the best tool to traverse through it. The printExceptionsTree() function takes two arguments:

  • A point inside the tree from which we start traversing the tree
  • A level to build a simplified drawing of the tree’s branches

This could be a partial output of the previous script:

BaseException
     +---Exception
     |     +---TypeError
     |     +---StopAsyncIteration
     |     +---StopIteration
     |     +---ImportError
     |     |     +---ModuleNotFoundError
     |     |     +---ZipImportError
     |     +---OSError
     |     |     +---ConnectionError
     |     |     |     +---BrokenPipeError
     |     |     |     +---ConnectionAbortedError
     |     |     |     +---ConnectionRefusedError
     |     |     |     +---ConnectionResetError
     |     |     +---BlockingIOError
     |     |     +---ChildProcessError
     |     |     +---FileExistsError
     |     |     +---FileNotFoundError
     |     |     +---IsADirectoryError
     |     |     +---NotADirectoryError
     |     |     +---InterruptedError
     |     |     +---PermissionError
     |     |     +---ProcessLookupError
     |     |     +---TimeoutError
     |     |     +---UnsupportedOperation
     |     |     +---herror
     |     |     +---gaierror
     |     |     +---timeout
     |     |     +---Error
     |     |     |     +---SameFileError
     |     |     +---SpecialFileError
     |     |     +---ExecError
     |     |     +---ReadError

In the output of the previous script, we can see that the root of Python’s exception classes is the BaseException class (this is a superclass of all the other exceptions). For each of the encountered classes, performs the following set of operations:

  • Print its name, taken from the __name__ property.
  • Iterate through the list of subclasses delivered by the __subclasses__() method, and recursively invoke the printExceptionsTree() function, incrementing the nesting level, respectively.

Now that you know the functions, classes, and exceptions for working with Python, let’s move on to learning how to manage modules and packages. Also, we will review the use of some modules for managing parameters, including argparse and OptionParse.