Attribution

This Python 201 guide is based on our notes from the TCM Security - Python 201 for Hackers video course by Riley Kidd.

We highly recommend this course as a great starting point for learning Python; it was instrumental in our understanding of the subject. We would like to extend a big thank you to the TCM Security Team for providing such valuable resources!

Welcome and Course Introduction

Project: https://247ctf.com

Course Overview

  • Level up from the Python 101 for Hackers course
  • Understand more advanced Python and programming techniques
  • Leverage Python to interface with the Windows API
  • Examples and projects focus on techniques useful to hackers
  • Limited use of theory and slides; you will learn by example
  • Won’t go into technical detail on non-Python topics
  • Programming in a Windows environment (mostly)
  • Examples and discussions focus on Python 3, not Python 2
  • Sublime as a text editor, but not required
  • Dracula theme for CMD and PowerShell, but not required

Prerequisites

Knowledge of Python 101 for Hackers topics and techniques, plus a high-level understanding of:

  • Burp Suite as a penetration testing tool
  • Symmetric vs Asymmetric cryptography
  • Wireshark and the network model
  • Pointers and structures

Course Tips and Tricks

  • Take detailed notes (written, typed, or both)
  • Course focus is on Python, but concepts apply to other languages too
  • Aim to use the mouse for everything (but the keyboard might win)
  • Pause, rewind, and implement lesson content yourself
  • If non-Python explanations are too high level: Google, Google, Google
  • Not a malware creation course: BE ETHICAL!

Topics Covered

  • Introduction
  • Windows Setup
  • Python 201
  • Object Oriented Programming
  • The Windows API
  • Extending Python
  • Python Projects
  • Extending Burp
  • Debugging
  • Closing

Setup on Windows

Check if Python is installed with CMD:

python -V

Installing Python:

Installing pip (installed by default):

Sublime Text

Small Comment Hack - Keybindings Settings

  • The issue: Block comment is not possible on European keyboard layouts
  • Solution: Change key bindings and create a new mapping:
[
    {
        "keys": ["ctrl+7"],
        "command": "toggle_comment",
        "args": { "block": false }
    },
    {
        "keys": ["ctrl+shift+7"],
        "command": "toggle_comment",
        "args": { "block": true }
    }
]

Python 201

Decorators

Decorators let you modify the behavior of a function without changing the function itself. They are useful for adding logging, verifying permissions, or performing generic checks before calling a function.

Requirements:

  • Outer function: Takes a function as an argument
  • Inner function: Wraps around the decorated function
# play around with commenting/uncommenting as you see fit

from datetime import datetime
import time

# a simple logging decorator
def logger(func):
    def wrapper():
        print("-"*50)
        print("> Execution started {}".format(datetime.today().strftime("%Y-%m-%d %H:%M:%S")))

        # call the actual function
        func()

        print("> Execution completed {}".format(datetime.today().strftime("%Y-%m-%d %H:%M:%S")))
        print("-"*50)

    # return the inner function
    return wrapper

# attach the decorator to the function -- Python syntactic sugar
@logger
def demo_function():
    print("Executing task!")
    time.sleep(1)
    print("Task completed!")


# call the demo function
# demo_function()

# decorator = Python syntactic sugar -- the same as @logger 
# not demo_function which is type function -- but a real function call
# logger(demo_function())

## -------------------- DECORATORS WITH ARGUMENTS --------------------- ##

def logger_args(func):
    def wrapper(*args, **kwargs):
        print("-"*50)
        print("> Execution started {}".format(datetime.today().strftime("%Y-%m-%d %H:%M:%S")))

        # call the actual function -- pass the arguments
        func(*args, **kwargs)

        print("> Execution completed {}".format(datetime.today().strftime("%Y-%m-%d %H:%M:%S")))
        print("-"*50)

    # return the inner function
    return wrapper

# adding the decorator
@logger_args
def demo_function_args(sleep_time):
    print("Executing task!")
    time.sleep(sleep_time)
    print("Task completed!")

demo_function_args(1)
demo_function_args(2)
demo_function_args(3)

Generators

Generators provide a simple way to create iterators by returning items one at a time. Instead of using the return statement, use yield:

  • return: Terminates the function entirely
  • yield: Pauses the function, saves the state and local variable information, so it can continue from that point for future calls

If you use yield, you cannot use return in the same function!

The next function returns the next item from the iterator, which is automatically implemented with a generator. The StopIteration error is raised when there is no more data to yield. A loop knows to stop automatically when StopIteration is raised.

Generators use lazy execution: they only produce items when asked for, rather than computing the entire sequence at once. This is great for memory-intensive processing and reading large files.

# play around with commenting/uncommenting as you see fit

def gen_demo():
    n = 1
    yield n

    n += 1
    yield n

    n += 1
    yield n

# a generator object -- will not start execution
# only with the next function
test = gen_demo()
print(test)

# looping manually
print(next(test))
print(next(test))
print(next(test))
# StopIteration is raised
# print(next(test))

# a loop knows to stop when "StopIteration" is raised
test2 = gen_demo()
for a in test2:
    print(a)


# creating a generator function with loops

def xor_static_key(a):
    key = 0x5
    for i in a:
        yield chr(ord(i) ^ key)

for i in xor_static_key("test"):
    print(i)


# creating anonymous generator expressions -- like with lambdas
xor_static_key2 = (chr(ord(i) ^ 0x5) for i in "test")

# generator object
print(xor_static_key2)

# printing the values -- manually
print(next(xor_static_key2))
print(next(xor_static_key2))

# printing the values -- loop
for i in xor_static_key2:
    print(i)

Serialization

Serialization is the process of translating data structures or objects into a format that can be stored or transmitted, then later reconstructed by deserializing the serialized object. It is useful for storing the state of something for later use and for transmitting data across the network.

Note: when serializing data, we are not storing objects – only the attributes of those objects. Therefore, be careful when deserializing because you are susceptible to code injection attacks. Also, not everything can be easily pickled.

# play around with commenting/uncommenting as you see fit

# --------------- SERIALIZATION/DESERIALIZATION --------------- #

import pickle

hackers = {"neut": 1, "geohot": 100, "neo": 1000}

for key, value in hackers.items():
    print(key, value)

# create a serialized version of it -- in a binary format
serialized = pickle.dumps(hackers)

# in bytes
print(serialized)

# deserialize and load
hackers_v2 = pickle.loads(serialized)
print(hackers_v2)

# printing the values
for key, value in hackers_v2.items():
    print(key, value)


# --------------- SAVING STATE TO A FILE --------------- #

# # storing some state into a file -- write binary
# with open("hackers.pickle", "wb") as handle:
#     pickle.dump(hackers, handle)


# --------------- LOADING STATE FROM A FILE --------------- #
# load serialized data from a file -- read binary
with open("hackers.pickle", "rb") as handle:
    hackers_v3 = pickle.load(handle)

print(hackers_v3)

Closures

Closures attach data to code – a function object that remembers values in enclosing scopes even when that variable goes out of scope or is removed.

A nested function is a function inside another function that has access to variables in the enclosing scope.

When using closures, make sure to have a nested function that refers to a value in the outer function. The outer function needs to return the nested/inner function.

# --------------- INNER/OUTER FUNCTIONS --------------- #

# # outer function
# def print_out(a):
#     print("Outer: {}".format(a))

#     # inner function -- has access to var a
#     def print_in():
#         print("\tInner: {}".format(a))

#     # call the inner function
#     print_in()

# # call the outer function
# print_out("test")

# ----- instead of just printing -- return it ----- #

# # outer function
# def print_out(a):
#     print("Outer: {}".format(a))

#     # inner function -- has access to var a
#     def print_in():
#         print("\tInner: {}".format(a))

#     # return the inner function
#     return print_in

# # bind it to an object
# test2 = print_out("test")

# # call the inner function -- using the value that was originally supplied
# # the value is still remembered even though we have already finished execution
# # of the print_out function
# test2()

# ----- specifically deleting the outer function ----- #

# outer function
def print_out(a):
    print("Outer: {}".format(a))

    # inner function -- has access to var a
    def print_in():
        print("\tInner: {}".format(a))

    # return the inner function
    return print_in

# bind it to an object
test2 = print_out("test")

# specifically delete the outer function
del print_out

# will still work -- data is attached to the code
test2()

# but calling outer function will not work anymore -- will throw an error
# print(print_out("test2"))

Object Oriented Programming

Introduction

Up until now, we used procedural programming where data and functions are mostly decoupled.

Object Oriented Programming (OOP) groups variables and methods. It structures software into reusable blueprints called classes. These blueprint templates can create objects through instantiation.

Classes contain data (attributes) and functions (methods).

Example:

  • A Person class
    • Name as an attribute
    • SayName as a method
  • Bob is a person object with a name that can be said
  • Alice is a person object with a name that can be said
  • Bob is not Alice

OOP Benefits

  • Model and group complex data in a reusable way
  • Leverage existing structures (inheritance)
  • Enable class-specific behavior (polymorphism)
  • Secure and protect attributes and methods (encapsulation)
  • Extensible and modular (overloading)

Classes, Objects, and Methods

Class attributes are defined directly in the class and shared by all objects of that class. Changes made to the class attribute will change for all instances of the class, but changing the class attribute value of an instance will not change the value for the class or other instances.

# play around with commenting/uncommenting as you see fit

class Person:
    # class documentation
    'Person base class'

    # class attribute
    wants_to_hack = True

    # constructor -- to initialize an instance of the object
    # self -- reference to the object itself -- here: Person
    # invoked automatically whenever a new object of the class is instantiated
    def __init__(self, name, age):
        self.name = name
        self.age = age

    def print_name(self):
        print("My name is {}".format(self.name))

    def print_age(self):
        print("My age is {}".format(self.age))

    def birthday(self):
        self.age += 1

print("-"*50)
## --------------- OBJECTS --------------- ##

# creating instances of the same class
bob = Person("bob", 30)
alice = Person("alice", 20)
mallory = Person("mallory", 50)

# objects -- __main__.Person
print(bob)
print(alice)
print(mallory)

print("-"*50)
## --------------- METHODS AND ATTRIBUTES --------------- ##

# using the methods
bob.print_name()
bob.print_age()
alice.print_name()
alice.print_age()
mallory.print_name()
mallory.print_age()

# changing attribute values
bob.age = 31
bob.print_age()
bob.birthday()
bob.print_age()
bob.birthday()
bob.print_age()

print(bob.name)
print(bob.age)

# __main__.Person
print(type(bob))

print("-"*50)
## --------------- CLASS SPECIFIC FUNCTIONS --------------- ##

# class specific function -- check if attribute exists
print(hasattr(bob, "age"))
print(hasattr(bob, "asd"))

# class specific function -- check for object values
print(getattr(bob, "age"))

# class specific function -- set attributes for an object
# if attribute does NOT exist --> it will create it
setattr(bob, "asd", 100)
print(getattr(bob, "asd"))

# class specific function -- delete attributes for an object
print(hasattr(bob, "asd"))    # True
delattr(bob, "asd")
# no longer exists
print(hasattr(bob, "asd"))    # False

print("-"*50)
## --------------- CLASS ATTRIBUTES --------------- ##
# defined directly in the class and shared by all objects of that class

print(Person.wants_to_hack)
print(bob.wants_to_hack)
print(alice.wants_to_hack)
print(mallory.wants_to_hack)

print("-"*5)
# changes made to the class attribute will change for all instances of the class
Person.wants_to_hack = "No way!"
print(Person.wants_to_hack)
print(bob.wants_to_hack)
print(alice.wants_to_hack)
print(mallory.wants_to_hack)

print("-"*5)
# but changing the class attribute value of an instance will not change the 
# value for the class or the other instances
bob.wants_to_hack = "Yes way!"
print(Person.wants_to_hack)
print(bob.wants_to_hack)    # only bob's value is changed
print(alice.wants_to_hack)
print(mallory.wants_to_hack)

print("-"*50)
## --------------- DELETING ATTRIBUTES --------------- ##
# it is possible to delete attributes, objects or the entire class itself with `del`

# delete an attribute
bob.print_name()
del bob.name
print(hasattr(bob, "name"))

# delete a class
# del Person
# # we can still access it
# print(alice.name)

# but creating new instances is no longer possible -- NameError
bob2 = Person("bob2", 35)

print("-"*50)
## --------------- BUILT-IN CLASS ATTRIBUTES --------------- ##
# built-in class attributes associated with ALL Python classes

# dictionary containing the class namespace
print(Person.__dict__)
# the class documentation string
print(Person.__doc__)
# the class name
print(Person.__name__)
# the module name in which the class is defined -- main in interactive mode
print(Person.__module__)

Inheritance

Inheritance creates a new class using details from an existing class without modifying that existing class. The new class is called a derived class or child class. The existing class is called a base class or parent class.

# (play around with commenting/uncommenting as you see fit)

## --------------- PARENT CLASS --------------- ##
class Person:
    # class documentation
    'Person base class'

    # class attribute
    wants_to_hack = True

    # constructor -- to initialize an instance of the object
    # self -- reference to the object itself -- here: Person
    # invoked automatically whenever a new object of the class is instantiated
    def __init__(self, name, age):
        self.name = name
        self.age = age

    def print_name(self):
        print("My name is {}".format(self.name))

    def print_age(self):
        print("My age is {}".format(self.age))

    def birthday(self):
        self.age += 1

## --------------- CHILD CLASS --------------- ##
class Hacker(Person):
    def __init__(self, name, age, cves):
        super().__init__(name, age)
        self.cves = cves

    def print_name(self):
        print("My name is {} and I have {} CVEs".format(self.name, self.cves))

    def total_cves(self):
        return self.cves

## --------------- INSTANTIATION --------------- ##
bob = Person("bob", 30)
alice = Hacker("alice", 20, 5)

bob.print_name()
alice.print_name()

print(bob.age)
print(alice.age)

bob.birthday()
alice.birthday()

print(bob.age)
print(alice.age)

# bob has no cve attribute
print(alice.total_cves())
print(hasattr(bob, "cves"))

print("-"*5)
## --------------- FUNCTIONS RELATED TO CLASS RELATIONSHIPS --------------- ##
print(issubclass(Hacker, Person))
print(issubclass(Person, Hacker))

# true if instance of the class or subclass
print("-"*5)
print(isinstance(bob, Person))
print(isinstance(bob, Hacker))

print(isinstance(alice, Person))
print(isinstance(alice, Hacker))

Encapsulation

Encapsulation prevents accidental modification of data within a class instance.

However, do not rely on it for security because you can use __dict__ to grab the data anyway. Once you know the attribute name, you can directly change it like bob._Person__age = 50.

Encapsulation restricts direct access to attributes and methods. By default, all methods and variables of a Python class are public. You can set an attribute to private by prefixing its name with __ like self.__name. Accessing private attributes is done via setter/getter methods.

# (play around with commenting/uncommenting as you see fit)
class Person:
    # class documentation
    'Person base class'

    # class attribute
    wants_to_hack = True

    # constructor -- to initialize an instance of the object
    # self -- reference to the object itself -- here: Person
    # invoked automatically whenever a new object of the class is instantiated
    def __init__(self, name, age):
        self.name = name
        self.__age = age

    def print_name(self):
        print("My name is {}".format(self.name))

    # getter method for age
    def get_age(self):
        return self.__age

    # setter method for age
    def set_age(self, age):
        self.__age = age

    def print_age(self):
        print("My age is {}".format(self.__age))

    def birthday(self):
        self.__age += 1


## --------------- ACCESSING PRIVATE ATTRIBUTES --------------- ##
bob = Person("age", 30)

# once set private -- it will no longer work
# print(bob.age)
# nor will this
# print(bob.__age)

# access it via getter methods
print(bob.get_age())
bob.set_age(31)
print(bob.get_age())
bob.birthday()
print(bob.get_age())

## --------------- GETTING AROUND ENCAPSULATION --------------- ##
# grab masked private data
print(bob.__dict__)

# change data
bob._Person__age = 50

# grab masked private data
print(bob.__dict__)

Polymorphism

Polymorphism is the ability to use a common interface for multiple different types. You can use the exact same function even when passing different types to that function.

# play around with commenting/uncommenting as you see fit

## --------------- PARENT CLASS --------------- ##
class Person:
    # class documentation
    'Person base class'

    # class attribute
    wants_to_hack = True

    # constructor -- to initialize an instance of the object
    # self -- reference to the object itself -- here: Person
    # invoked automatically whenever a new object of the class is instantiated
    def __init__(self, name, age):
        self.name = name
        self.age = age

    def print_name(self):
        print("My name is {}".format(self.name))

    def print_age(self):
        print("My age is {}".format(self.age))

    def birthday(self):
        self.age += 1

## --------------- CHILD CLASS --------------- ##
class Hacker(Person):
    def __init__(self, name, age, cves):
        super().__init__(name, age)
        self.cves = cves

    def print_name(self):
        print("My name is {} and I have {} CVEs".format(self.name, self.cves))

    def total_cves(self):
        return self.cves


print("-"*50)
## --------------- POLYMORPHISM -- BUILT-IN EXAMPLES --------------- ##

# the same function 'len' but with different types -- string vs list
print(len("string"))
print(len(['l','i','s','t']))


print("-"*50)
## --------------- POLYMORPHISM -- CLASS METHOD EXAMPLES --------------- ##
bob = Person("bob", 30)
alice = Hacker("alice", 25, 10)
people = [bob, alice]

for person in people:
    person.print_name()
    print(type(person))


print("-"*50)
## --------------- POLYMORPHISM -- ANY OBJECT --------------- ##
def obj_dump(object):
    object.print_name()
    print(object.age)
    object.birthday()
    print(object.age)
    print(object.__class__)
    print(object.__class__.__name__)


obj_dump(bob)
obj_dump(alice)

Operator Overloading

Operator overloading changes the meaning of an operator depending on the operands used.

# play around with commenting/uncommenting as you see fit

class Person:
    # class documentation
    'Person base class'

    # class attribute
    wants_to_hack = True

    # constructor -- to initialize an instance of the object
    # self -- reference to the object itself -- here: Person
    # invoked automatically whenever a new object of the class is instantiated
    def __init__(self, name, age):
        self.name = name
        self.age = age

    def print_name(self):
        print("My name is {}".format(self.name))

    def print_age(self):
        print("My age is {}".format(self.age))

    def birthday(self):
        self.age += 1

    # overload the str built-in method
    def __str__(self):
        return "My name is {} and I am {} years old.".format(self.name, self.age)

    # implement the built-in addition operator
    # other is expected to be another instance of the class, 
    # but not the same instance which is calling the method
    def __add__(self, other):
        return self.age + other.age

    # overloading the less than operator
    def __lt__(self, other):
        return self.age < other.age


print("-"*50)
## --------------- OPERATOR OVERLOADING -- BUILT-IN EXAMPLES --------------- ##
print(1 + 1)
print("1" + "1")

print("-"*50)
## --------------- OPERATOR OVERLOADING -- CLASS EXAMPLES --------------- ##
bob = Person("bob", 30)
alice = Person("alice", 35)

print("-"*5)
# call overloaded __str__ method
print(bob)

print("-"*5)
# try adding them together
# will not work without overwriting/implementing the __add__ method
print(bob + alice)
print(alice + bob)

print("-"*5)
# overloaded lt operator
print(bob < alice)
print(alice < bob)

Class Decorators

Property decorator (@property): Declares a method as a property object of the class.

Class method decorator (@classmethod): Python syntactic sugar.

  • Bound to a class rather than its object
  • Does not require creation of a class instance
  • Can only access class method attributes, not per-instance attributes
  • First attribute is cls to access class attributes

Factory methods: Use class methods as a type of factory to create instances of the class.

Static method decorator (@staticmethod): Defines a static method in the class.

  • Cannot access class attributes or per-instance attributes
  • Does not require a class instance
  • Can be called by both the class and instances of the class
  • Takes no parameters, not even self, and knows nothing about the class
  • Useful when you do not want a subclass to overwrite some method implementation
# play around with commenting/uncommenting as you see fit

class Person:
    # class documentation
    'Person base class'

    # class attribute
    wants_to_hack = True

    # constructor -- to initialize an instance of the object
    # self -- reference to the object itself -- here: Person
    # invoked automatically whenever a new object of the class is instantiated
    def __init__(self, name, age):
        self.name = name
        self.__age = age

    def print_name(self):
        print("My name is {}".format(self.name))

    # getter method for age
    def get_age(self):
        return self.__age

    # property decorator -- getter method
    @property
    def age(self):
        return self.__age

    # property decorator -- setter method
    @age.setter
    def age(self, age):
        self.__age = age

    # property decorator -- delete method
    @age.deleter
    def age(self):
        del self.__age

    # class method decorator -- `cls` for accessing class attributes
    @classmethod
    def wants_to(cls):
        return cls.wants_to_hack

    # class method decorator -- bob factory
    @classmethod
    def bob_factory(cls):
        return cls("bob", 30)

    # static method -- no parameters
    @staticmethod
    def static_print():
        print("I am the same!")
        
    # setter method for age    
    def set_age(self, age):
        self.__age = age

    def print_age(self):
        print("My age is {}".format(self.__age))

    def birthday(self):
        self.__age += 1


print("-"*50)
## --------------- CLASS DECORATORS -- PROPERTY DECORATOR --------------- ##
bob = Person("bob", 30)

# will not work without a property decorator
print(bob.age)

print("-"*5)

# setter test
bob.age = 50
print(bob.age)

# deleter test
# del bob.age
# print(hasattr(bob, "age"))    # false

print("-"*50)
## --------------- CLASS DECORATORS -- CLASS METHOD DECORATOR --------------- ##
print(Person.wants_to())

print("-"*5)
# create instances of bob with the factory
bob1 = Person.bob_factory()
bob2 = Person.bob_factory()
bob3 = Person.bob_factory()

bob1.print_name()
bob2.print_name()
bob3.print_name()

print("-"*50)
## --------------- CLASS DECORATORS -- STATIC METHOD DECORATOR --------------- ##
Person.static_print()
bob1.static_print()
bob2.static_print()
bob3.static_print()