Bienvenido a OMSTD (Open Methodology for Security Tool Developers)

_images/logo_200x200.png

OMSTD (Open Methodology for Security Tool Developers) es una serie de casos de estudio, agrupados y categorizados a modo de guía, con los que lograr desarrollar herramientas bien construidas.

Aunque puede ser usada para crear cualquier tipo de herramientas y en cualquier lenguaje, se centra principalmente en el desarrollo de herramientas de hacking escritas en Python.

Sería muy recomendable para el lector leer detenidamente la sección Qué es OMSTD , ya que le ayudará a comprender esta guía.

Autor

Esta guía ha sido escrita e ideada por Daniel García (A.K.A. cr0hn).

Web oficial y Twitter

Esta guía, así como su código de ejemplo y presentaciones están publicadas de forma gratuita en su repositorio de Github:

Puedes seguir los avances, novedades y noticias de esta guía en nuestra cuenta de twitter:

Licencias

Código

Todo el código aquí expuesto se distribuye bajo licencia BSD 3-clausule. Puedes copiarlo y redistribuirlo sin restricción, conservando la autoría del mismo y sin obtener beneficio económico directo del mismo.

Texto

Esta guía, y todo el texto que la contiene, se distribuye bajo la licencia: Creative Commons 4.0 - Attribution-NonCommercial 4.0 International (CC BY-NC 4.0).

Colaborar con OMSTD

Cualquier idea, crítica (constructiva, por favor) o colaboración es muy bienvenida. Puedes ponerte en contacto por las vías:

  • Correo electrónico (cr0hn<<–AT–>>.cr0hn.com).
  • Usando los issues de github.
  • Haciendo un fork de proyecto y enviando un parche.

Si estás interesado en ayudar, puedes echar un vistazo a TODO.rst y ver las ideas pendientes de implementar.

Una de las finalidades es portar esta guía a otros idiomas. SI DOMINAS CUALQUIER OTRO IDIOMA ADEMÁS DEL ESPAÑOL, ANÍMATE A ECHAR UNA MANO.

¿A quién va dirigida esta guía?

Esta guía está dirigida a auditores de seguridad y pentesters que necesitan desarrollar sus propias herramientas (muchas veces on-the-fly) y quieren que éstas sean algo más que un simple script.

Un pentester puede ser muy bueno en tareas de hacking, pero no tiene porque tener tanto conocimiento en desarrollo, buenas prácticas, patrones de diseño, etc.

Para poder usar esta guía se recomienda:

  • Tener conocimientos básicos de programación en Python 3 (Si solo sabes Python 2, también podrás seguir la guía, no te preocupes).
  • Tener conocimientos básicos de seguridad informática

Organización de la guía

Bloques

Los casos de estudio se dividen en los siguientes bloques:

  • Desarrollo
    • Organización y estructura (ST): Cómo se organizan los proyectos.
    • Comportamiento (BH): Forma de interactuar con diferentes frameworks.
    • Interacción (IT): Interacción de usuario, con otros sistemas o con otros entornos.
    • Específico de lenguaje (LS): Trucos, buenas prácticas y usos concretos del lenguaje de desarrollo. Python en este caso.
    • Entrada/salida de información (IO): Generar de informes (Word, Excel...), exportar datos, importar información externa de XML/JSON...
    • Redistribución (RD): Crear de paquetes, redistribuirlos, compilarlos para varios sistemas, portarlos a varios entornos, uso correcto de sistemas de control de versiones, etc.
    • Despliegue (DP): Cómo poner en producción de forma correcta nuestra aplicación.
  • Hacking (HH): Ejemplos de casos concretos de hacking.
  • Cracking (CH): Ejemplos de casos concretos de cracking.
  • Malware (MH): Ejemplos de casos concretos de malware.
  • Forensic (FH): Ejemplos de casos concretos de forensic.
  • Hardening (DH): Ejemplos de casos concretos de hardening.

Identificación de los casos

A fin de hacer más re-usable este texto, se identificarán los casos de la siguiente forma:

  • XX[-EX]-YYY
    • Problema: Presentación y descripción del caso de estudio.
    • Solución: Solución propuesta.
    • Cómo: Cómo llevar a cabo la solución.
    • Anexo: Esta sección puede estar disponible o no. En ella se aclararán cuestiones muy concretas del caso de estudio.
Donde:
  • XX : Identificador de bloque.
  • YYY: Valor numérico con el formato: 001, 002, 003...
  • EX: Si el identificador tiene este sufijo, significa que se trata de un ejemplo completo o mini proyecto.

Índice

Inicio

Qué es OMSTD

OMSTD (O pen M ethodology for S ecurity T ool D evelopers) es una propuesta que intenta ser una guía de buenas prácticas fácil, intuitiva y práctica, para el desarrollo de aplicaciones de hacking.

Hacer herramientas de hacking no tiene porque ser tan complicado como puede parecer a priori, tan solo hay que seguir una serie de pautas de forma correcta.

Esta guía surge a partir de mi charla “El Poder de los reptiles: Cómo hacer herramientas de seguridad en Python” en IV Navaja Negra Conference.

El objetivo de este proyecto es crear una metodología abierta y colaborativa con la que poder servir de apoyo al desarrollo de nuevas herramientas.

Estado actual

Actualmente esta guía recoge un número bastante limitado de casos. Hay mucho por hacer y muchas ideas por documentar.

Poco a poco, y con la ayuda de todo el que quiera contribuir, espero que el número de casos de estudio y ejemplos crezca.

¡¡Aviso!!

Este texto es fruto de la experiencia, investigación propia y de los errores más comunes que me he encontrado cuando he tratado de desarrollar herramientas de hacking o usar otras en mi propio código.

El objetivo de este texto es ser una pequeña guía de buenas prácticas (y que espero ampliar en un futuro) para la creación de herramientas portables, bien diseñadas y mantenibles.

Las soluciones presentadas pueden no ser las mejores o más óptimas, son solo mis propuestas. Cualquier mejora o sugerencia es bienvenida.

Cómo usar esta guía

Teoría

Si es la primera vez que lees esta guía, te recomiendo que la leas los casos de estudio en el orden que están planteados.

Una vez estudiado y familiarizado con ellos tan solo tendrás que buscar el caso específico que necesites.

Ejemplos

Todos los ejemplos se encuentran colgando del directorio examples. En él podrás encontrar una serie de carpetas que coinciden con los códigos de los bloques (mirad más abajo ) y una sub-carpeta con valores numéricos de 3 dígitos, que corresponde con un caso de estudio concreto, por ejemplo:

example/bh/001/

Corresponderá con el caso de estudio 001 del tipo comportamiento (BH).

Instrucciones para empezar

Al igual que se explica en esta guía, ella misma está estructurada siguiendo el mismo modelo que plantea.

Tal vez sea adelantar acontecimientos pero, para ponértelo más fácil, estas son las instrucciones que deberías de seguir para poder ejecutar todos los casos de estudio:

1 - Instalar las dependencias de sistema
sudo python3.4 -m pip install virtualenvwrapper
2 - Configurar virtualenvwrapper
echo "export WORKON_HOME=$HOME/.virtualenvs" > ~/.bashrc
echo "export PROJECT_HOME=$HOME/Devel" >> ~/.bashrc
echo "source /usr/local/bin/virtualenvwrapper.sh"  >> ~/.bashrc
3 - Buscar el intérprete de Python 3.4
locate python3.4 | grep bin/python | grep python3
...
/opt/local/Library/Frameworks/Python.framework/Versions/3.4/bin/python3.4
/opt/local/Library/Frameworks/Python.framework/Versions/3.4/bin/python3.4-config
/opt/local/Library/Frameworks/Python.framework/Versions/3.4/bin/python3.4m
/opt/local/Library/Frameworks/Python.framework/Versions/3.4/bin/python3.4m-config
/opt/local/bin/python3.4
/opt/local/bin/python3.4-config
/opt/local/bin/python3.4m
4 - Crear el entorno virtual (o sandbox) de pruebas
mkvirtualenv -p /opt/local/bin/python3.4 omstd
5 - Instalar las dependencias globales de OMSTD

Situados en el directorio raiz del proyecto de OMSTD ejecutamos:

pip install -r requirements.txt
6 - Instalar las dependencias locales de cada ejemplo

Cada caso de estudio puede tener su propio fichero requirements.txt con sus propias dependencias. Esto es así para no obligar al lector a instalar todas las dependencias del proyecto, ya que puede que no las necesite todas.

Para instalar las dependencias de cada ejemplo ha de proceder como en el punto anterior con cada fichero listado de dependencias.

Cómo colaborar

OMSTD es un proyecto de carácter abierto y gratuito. Si te apetece compartir tu experiencia y conocimiento con otros, serás muy bienvenido.

Principalmente existen los siguientes tipos de colaboraciones:

Corrección de errores y mejoras

Si detectas cualquier fallo o algún punto mejorable puedes:

  • Abrir un ticket o incidencia y será tratado con la mayor brevedad posible (Anexo 2).
  • Enviar un parche que corrija dicho error (Anexo 1).

Propuestas de nuevos casos de estudios

Si deseas sugerir un nuevo caso de estudio, tan solo tienes que abrir un ticket con tu propuesta (Anexo 2).

Éste será clasificada y catalogada en función de la naturaleza del mismo.

Envío de un nuevo caso de estudio

El medio preferible para enviar nuevos casos de estudio es el siguiente:

  • Crear un fork del proyecto (Anexo 3).
  • Enviar un parche con el nuevo caso de estudio (Anexo 1).

De esta forma todo quedará registrado, para que todo el mundo pueda seguirlo, además de llevar un mejor orden.

Ayuda a la traducción

Este caso es muy parecido al anterior. pero con un cierto matiz:

  • Crear un fork del proyecto (Anexo 3).
  • Copiar la carpeta doc/es al directorio doc/LANG, donde LANG es el código ISO del lenguaje de la traducción. Por ejemplo: Si se está traduciendo a inglés sería doc/en. Sobre este directorio será sobre el que se trabajará y traducirá.
  • Enviar un parche con el nuevo caso de estudio (Anexo 1).

Anexos

Anexo 1: Envío de parches usando GitHub

Una vez hemos forkeado y hechos los cambios pertinentes en el código, para enviar un parche siga las siguientes instrucciones:

  1. Pulsamos en la opción de Pull Request:
_images/contrib-002.png
  1. Creamos un nueva nueva propuesta de parche pulsando en New pull request:
_images/contrib-003.png
  1. GitHub detectará los cambios realizados, extraídos de los commits que hayamos realizado, y preparará el request. Para finalizar el envío tan solo tenemos que pulsar en Create pull request:
_images/contrib-004.png
Anexo 2: Apertura de incidencias en GitHub

La apertura de incidencias en GitHub es muy sencilla, tan solo tenemos que utilizar su sistema de ticketing:

  1. Podemos ir directamente a los issues siguiendo el link https://github.com/cr0hn/OMSTD/issues, o podemos ir al home del proyecto, https://github.com/cr0hn/OMSTD, y pulsar en Issues, del menú de la derecha:
_images/contrib-005.png
  1. En esta pantalla nos aparecerán todas las incidencias y propuestas abiertas. Para crear una nueva pulsaremos en New Issue:
_images/contrib-006.png
  1. Para la apertura de la incidencia es necesario un título y una descripción. Es muy conveniente ser conciso en el título y explicar en detalle la incidencias, mejora o propuesta.
_images/contrib-007.png
Anexo 3: Crear un fork de un proyecto en GitHub

Crear un fork de un proyecto en GitHub es realmente fácil. Tan solo tendrás que:

  1. Identificarte con tu usuario,
  2. Ir al repositorio oficial del proyecto: https://github.com/cr0hn/OMSTD
  3. Hacer click en el botón superior derecho con el texto Fork. La siguiente imagen muestra cómo:
_images/contrib-001.png

Conceptos de desarrollo

Organización y estructura

En este bloque se tratan los casos de estudio relacionados con la organización y estructura de proyectos.


ST-001 - Estructura de proyecto
Problema

Muchas herramientas no contemplan la opción de ser usadas como librería (con un “import”) además de por linea de comandos, o la interfaz que tengan definida.

Su uso puede ser complicado y, muchas veces, tan solo pueden ser usadas por linea de comandos, lo que implica llamar a un proceso externo del sistema y parsear su salida.

Solución

Una estructura correcta en la organización de archivos, pensada para ser re-utilizable.

Cómo

En la imagen se puede ver la estructura propuesta:

_images/st-001.01.png

Donde:

  • LICENCE: Fichero de licencia. Contiene el texto legal con el que queremos redistribuir nuestro código.
  • README.rst: Fichero índice de documentación del proyecto. Será el primero en mostrarse y leerse por defecto.
  • requirements.txt: Contiene las dependencias de librerías externas de nuestro proyecto.
  • setup.py: Contiene información para la instalación, configuración y redistribución de nuestro software.
  • TODO.rst: Ideas futuras a implementar en nuestro proyecto. Es buena idea tenerlo por si alguien quiere colaborar con nuestro proyecto, ya que podrá encontrar tareas e ideas por hacer.
  • app_name: Carpeta, que actúa como paquete Python, con el nombre de nuestra aplicación.
  • app_name/bin: Ejecutables disponibles en nuestra aplicación.
  • app_name/doc: Archivos de documentación, usualmente escrita con Shpinx. Esta guía es un ejemplo de su uso.
  • app_name/lib: Librerías propias que genere nuestra aplicación.
  • app_name/test: Contiene los diferentes test de la aplicación: Unitarios, de integración, rendimiento...

ST-002 - Entrada de parámetros globales
Problema

Una vez preparada la estructura para nuestra aplicación, ahora queremos “llamar” o ejecutar el programa en cuestión. Problemas:

  • Añadir nuevos parámetros de entrada al programa, requiere muchos cambios.
  • El código se hace cada vez más enrevesado e inmanejable.
  • Cambios de versiones de un mismos programas son incompatibles.

Ejemplo ST-002.P01

Este ejemplo de programa que divide dos números. Acepta 2 parámetros por linea de comandos:

  • Nominado
  • Denominador

Si ocurre una división por 0 devuelve -1:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
import argparse


# ----------------------------------------------------------------------
def divide(param1, param2):
    try:
        return param1 / param2
    except ZeroDivisionError:
        return -1

# ----------------------------------------------------------------------
if __name__ == '__main__':

    parser = argparse.ArgumentParser(description='OMSTD Example')
    parser.add_argument("-p1", dest="param1", help="parameter1")
    parser.add_argument("-p2", dest="param1", help="parameter2")

    params = parser.parse_args()

    d = divide(params.param1, params.param2)

    print(d)

Ejemplo ST-002.P02

Ahora queremos añadir otro parámetro, “verbosity”, como opción a nuestro programa.

Vemos que hay que modificar el código en 2 sitios diferentes. En todos aquellos donde se tenga que pasar información de las opciones de ejecución:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
# ----------------------------------------------------------------------
def divide(param1, param2, verbosity):
    try:
        return param1 / param2
    except ZeroDivisionError as e:
        if verbosity > 0:
            print(e)
        return -1

# ----------------------------------------------------------------------
if __name__ == '__main__':

    parser = argparse.ArgumentParser(description='OMSTD Example')
    parser.add_argument("-p1", dest="param1", help="parameter1")
    parser.add_argument("-p2", dest="param1", help="parameter2")
    parser.add_argument("-v", dest="verbosity", type="int", help="verbosity")

    params = parser.parse_args()

    d = divide(params.param1, params.param2, params.verbosity)

    print(d)
Solución

Hacer un objeto global que sea el que contenga los parámetros globales de ejecución (proyectos grandes, como nmap, proyecto lo hacen de esta forma):

Cómo

Ejemplo ST-002.S01

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
# --------------------------------------------------------------------------
class Parameters:
    """Program parameters"""

    # ----------------------------------------------------------------------
    def __init__(self, **kwargs):
        self.param1 = kwargs.get("param1")
        self.param2 = kwargs.get("param2")
        self.verbosity = kwargs.get("verbosity")


# ----------------------------------------------------------------------
def divide(input_params):
    try:
        return input_params.param1 / input_params.param2
    except ZeroDivisionError as e:
        if input_params.verbosity > 0:
            print(e)
        return -1

# ----------------------------------------------------------------------
if __name__ == '__main__':

    parser = argparse.ArgumentParser(description='OMSTD Example')
    parser.add_argument("-p1", dest="param1", help="parameter1")
    parser.add_argument("-p2", dest="param1", help="parameter2")
    parser.add_argument("-v", dest="verbosity", type="int", help="verbosity")

    params = parser.parse_args()

    input_parameters = Parameters(param1=params.param1,
                                  param2=params.param2,
                                  verbosity=params.verbosity)

    d = divide(input_parameters)

    print(d)

ST-003 - Gestión de resultados
Problema

Recuperar la información de salida de una aplicación es muy complicado:

  • No está estandarizada
  • Hay que parsear muchos XML/JSON.
  • La herramienta solo reporta resultados por consola.

Usando el mismo ejemplo que en el anterior caso:

Ejemplo ST-003.P01

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
import argparse


# ----------------------------------------------------------------------
def divide(param1, param2):
    try:
        return param1 / param2
    except ZeroDivisionError:
        return -1

# ----------------------------------------------------------------------
if __name__ == '__main__':

    parser = argparse.ArgumentParser(description='OMSTD Example')
    parser.add_argument("-p1", dest="param1", help="parameter1")
    parser.add_argument("-p2", dest="param1", help="parameter2")

    params = parser.parse_args()

    d = divide(params.param1, params.param2)

    print(d)

Cuando queremos añadir un parámetro nuevo en la devolución, vemos que hay que modificar varias lineas de código:

Ejemplo ST-003.P02

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
# ----------------------------------------------------------------------
def divide(param1, param2):
    try:
        results = param1 / param2
        is_2_divisor = results % 2

        return results, is_2_divisor
    except ZeroDivisionError as e:
        return -1, False

# ----------------------------------------------------------------------
if __name__ == '__main__':

    parser = argparse.ArgumentParser(description='OMSTD Example')
    parser.add_argument("-p1", dest="param1", help="parameter1")
    parser.add_argument("-p2", dest="param1", help="parameter2")

    params = parser.parse_args()

    result, is_divisor = divide(params.param1, params.param2)

    print("Results: %s - %s" % (result, is_divisor))
Solución

Usar objetos genéricos que contengan la información resultante de la ejecución del a herramienta.

Además, estos objetos, nos permiten abstraer el almacenamiento de información, de cómo ésta es exportada o transformada: XML, JSON, HTML, PDF ...

Cómo

Ejemplo ST-003.S01

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
# --------------------------------------------------------------------------
class Results:
    """Program parameters"""

    # ----------------------------------------------------------------------
    def __init__(self, **kwargs):
        self.result = kwargs.get("result", 0)
        self.is_2_divisor = kwargs.get("is_2_multiple", False)


# ----------------------------------------------------------------------
def divide(param1, param2):
    try:
        results = param1 / param2
        is_2_divisor = results % 2

        return Results(result=results,
                       is_2_divisor=is_2_divisor)
    except ZeroDivisionError:
        return Results()

# ----------------------------------------------------------------------
if __name__ == '__main__':

    parser = argparse.ArgumentParser(description='ktcal2 - SSH brute forcer')
    parser.add_argument("-p1", dest="param1", help="parameter1")
    parser.add_argument("-p2", dest="param1", help="parameter2")
    parser.add_argument("-v", dest="verbosity", type="int", help="verbosity")

    params = parser.parse_args()

    result = divide(params.param1, params.param2)

    print("Results: %s - %s" % (result.result, result.is_2_divisor))

ST-004 - Unificar puntos de entrada
Problema

Tengo que cambiar mucho código y rehacer gran parte de mi aplicación, cada vez que quiero hacer una nueva UI (User Interface):

  • Linea de comandos.
  • Interfaz gráfica.
  • Web.
  • Que se use como librería.
_images/st-004.01.png
Solución

Centralizar el punto de entrada a la ejecución de tu aplicación en un único punto y que todas las UI usen el mismo.

_images/st-004.02.png
Cómo

Incluir el concepto de api.py y enseñar como el command line y el import funcionan igual.

_images/st-004.03.png

Ejemplo ST-004.P01

Tras incluir el fichero “api.py”, la UI de linea de comandos (st-004-s1.py) nos quedará como sigue:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
from .api import Parameters, run

# ----------------------------------------------------------------------
if __name__ == '__main__':

    parser = argparse.ArgumentParser(description='OMSTD Example')
    parser.add_argument("-p1", dest="param1", help="parameter 1")
    parser.add_argument("-p2", dest="param2", help="parameter 2")

    params = parser.parse_args()

    input_parameters = Parameters(param1=params.param1,
                                  param2=params.param2,
                                  verbosity=params.verbosity)

    run(params)

Si echamos un vistazo a api.py, podemos observar que este fichera centraliza las llamadas al resto de librerías:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
# Import data
from .lib.data import *

# Import libs
from .lib.operations import divide
from .lib.exports import display_results


# ----------------------------------------------------------------------
def run(input_parameters):

    # Run operation
    r = divide(input_parameters)

    # Display results
    display_results(r)

ST-005 - La linea de comandos
Problema

Crear una linea de comandos avanzada puede no resultar sencilla, y mucho menos intutiva.

Estos son algunos de los problemas más comunes que podemos encontrarnos:

  • Crear opciones posicionales: main.py -n -i 9 POSITIONAL_PARAM.
  • Crear una ayuda personalizada, cuando se ejecute la opción -h: main.py -h.
  • Los tipos de datos recogidos por la linea de comandos son incorrectos: main.py -t 4, por defecto 4 será tratado como un string, no como un int.
  • Ausencia de opciones por defecto.
  • Establecer ciertas opciones como obligatorios.
  • Establecer un opciones y una versión abreviada de las mismas: main.py -t 8 equivalente a main.py --threads 8.
  • Opciones sin parámetros, tratados como bool: main.py -j.
  • Opciones que puedan usarse como acumuladores: main.py -v -> main.py -vv -> main.py -vvv.
Solución

En este caso de prueba la solución no es otra que usar correctamente la libraría de Python argparse

Cómo

Para que sea más ilustrativo, vamos a partir de una linea de comandos básica y vamos ir mejorándola poco a poco.

La linea de comandos de la que partiremos la que sigue, de un posible escaneador de puertos:

Ejemplo ST-005.P01

1
2
3
4
5
6
7
8
9
if __name__ == "__main__":

    parser = argparse.ArgumentParser(description='OMSTD Example')
    parser.add_argument("-t", dest="targets", help="target")
    parser.add_argument('-v', dest='verbosity level')
    parser.add_argument("--open", dest="only_open", help="only display open ports")
    parser.add_argument("--port", dest="port")

    params = parser.parse_args()

Que mostrará el siguiente resultado por consola, cuando ejecutamos st-005-p1.py -h:

usage: st-005-p1.py [-h] [-t TARGETS] [-v VERBOSITY LEVEL] [--open ONLY_OPEN]
                [-p PORTS_RANGE]

OMSTD Example

optional arguments:
  -h, --help          show this help message and exit
  -t TARGETS          target
  -v VERBOSITY LEVEL
  --open ONLY_OPEN    only display open ports
  --port PORT

Nota

Nótese que se puede ejecutar sin parámetros y no se devolvería ningún error a pesar de que, por razones obvias, necesitamos como mínimo un parámetros: targets.

1 - Opciones obligatorios

En primer lugar vamos a solventar el problema del parámetro obligatorio y vamos a obligar a especificar un target:

1
2
3
4
5
6
7
parser = argparse.ArgumentParser(description='OMSTD Example')
parser.add_argument("-t", dest="targets", help="target", required=True)
parser.add_argument('-v', dest='verbosity' help='verbosity level', type=int)
parser.add_argument("--open", dest="only_open", help="only display open ports", default=0)
parser.add_argument("--port", dest="port", type=int, help="port to scan")

params = parser.parse_args()

Vemos cómo, tras en cambio, se obliga al usuario a especificar un target:

usage: st-005-p1.py [-h] -t TARGETS [-v VERBOSITY LEVEL] [--open ONLY_OPEN]
                    [-p PORTS_RANGE]
st-005-p1.py: error: the following arguments are required: -t
2 - Tipos de datos

Vemos que las opciones verbosity (0-2) y port son tratadas como string (por defecto), pero realmente son del tipo int.

Añadimos la mejora:

1
2
3
4
5
6
7
parser = argparse.ArgumentParser(description='OMSTD Example')
parser.add_argument("-t", dest="targets", help="target", required=True)
parser.add_argument('-v', dest='verbosity' help='verbosity level', type=int)
parser.add_argument("--open", dest="only_open", help="only display open ports", default=0)
parser.add_argument("--port", dest="port", type=int, help="port to scan")

params = parser.parse_args()
3 - Valores por defecto

Sería interesante contar con valores por defecto para cada tipo de parámetro.

De esta forma evitaremos errores, por ausencia de dichos valores por parte del usuarios, e introduciremos configuración predeterminada, lo que hará el uso de la herramienta más fácil:

1
2
3
4
5
6
7
8
parser = argparse.ArgumentParser(description='OMSTD Example')
parser.add_argument("-t", dest="targets", help="target", required=True)
parser.add_argument('-v', dest='verbosity' help='verbosity level', type=int, default=0)
parser.add_argument("--open", dest="only_open", help="only display open ports", default=0)
parser.add_argument("--port", dest="port", type=int, default=80,
    help="port to scan. Default: 80.")

params = parser.parse_args()
4 - Opciones sin parámetros

Si observamos, la opción –open, es realmente un booleano. Es decir, que solo necesitamos saber si está a True o a False.

Actualizamos para que dicha opción sea tratada como un booleano.

1
2
3
4
5
6
7
8
9
parser = argparse.ArgumentParser(description='OMSTD Example')
parser.add_argument("-t", dest="targets", help="target", required=True)
parser.add_argument('-v', dest='verbosity' help='verbosity level', type=int, default=0)
parser.add_argument("--open", dest="only_open", action="store_true",
    help="only display open ports", default=False)
parser.add_argument("--port", dest="port", type=int, default=80,
    help="port to scan. Default: 80.")

params = parser.parse_args()

Nota

Los datos booleanos no necesitan indicarles el tipo, cuando se usan con la opción action="store_true|false. El tipo está implícido cuando usamos el parámetro de configuración action="store_XXXX".

5 - Opciones abreviadas

Cuando tenemos muchas opciones en la linea de comandos, es intersante poder poner la opciones en formato más abrevidado.

Para nuestro ejemplo, cuando indicamos del puerto, podríamos crear la opción abreviada -p, además de --port como sigue:

1
2
3
4
5
6
7
8
9
parser = argparse.ArgumentParser(description='OMSTD Example')
parser.add_argument("-t", dest="targets", help="target", required=True)
parser.add_argument('-v', dest='verbosity' help='verbosity level', type=int, default=0)
parser.add_argument("--open", dest="only_open", action="store_true",
    help="only display open ports", default=False)
parser.add_argument("-p", "--port", dest="port", type=int, default=80,
    help="port to scan. Default: 80.")

params = parser.parse_args()
6 - Opciones auto-incrementales

Existen ciertas opciones que tienen más sentido que su valor vaya incrementando en función de las veces que se repite. Por ejemplo:

  • -v en lugar de 0.
  • -vv en lugar de 1.
  • -vvv en lugar de 2.

En nuestro ejemplo, dicho comportamiento es perfecto para el opación verbosity:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
parser = argparse.ArgumentParser(description='OMSTD Example')
parser.add_argument("-t", dest="targets", help="target", required=True)
parser.add_argument('-v', dest='verbosity' help='verbosity level: -v, -vv, -vvv.',
    action="count", type=int, default=0)
parser.add_argument("--open", dest="only_open", action="store_true",
    help="only display open ports", default=False)
parser.add_argument("-p", "--port", dest="port", type=int, default=80,
    help="port to scan. Default: 80.")

params = parser.parse_args()
7 - Opciones posicionales

Por último, puede que nos interese tener parámetro qeu no van precedidos de ninguna opción que empiece por -. Por ejemplo:

  • main.py 127.0.0.1 en lugar de main.py -t 127.0.0.1.

Esta forma de configurar las opciones de la linea de comandos se conoce como opciones posicionales.

Entenderemos mejor esto con un ejemplo:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
parser = argparse.ArgumentParser(description='OMSTD Example')
parser.add_argument("targets", metavar='TARGETS', help="targets to scan", nargs="+",
    required=True)
parser.add_argument('-v', dest='verbosity' help='verbosity level: -v, -vv, -vvv.',
    action="count", type=int, default=0)
parser.add_argument("--open", dest="only_open", action="store_true",
    help="only display open ports", default=False)
parser.add_argument("-p", "--port", dest="port", type=int, default=80,
    help="port to scan. Default: 80.")

params = parser.parse_args()

La linea de comando ahora se verá así:

usage: st-005-s1.py [-h] [-v] [--open] [-p PORT] TARGETS [TARGETS ...]

OMSTD Example

positional arguments:
  TARGETS               targets to scan

optional arguments:
  -h, --help            show this help message and exit
  -v                    verbosity level: -v, -vv, -vvv.
  --open                only display open ports
  -p PORT, --ports PORT
                        port to scan. Defaul: 80.

Una ver parseada la linea de comandos, en el objeto params, podremos obtener la lista de los parámetros posicionales leyendo el parámetro params.targets.

8 - Documentación extendida

Suele ser muy útil para nuestros programas poner pequeños ejemplos al final de la ayuda que nos proporciona la opción -h. Para esto, usamos el parámetro epilog, del módulo argparse:

st-005-s1.py:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
    footed_description = """

    Usage examples:
        + Basic scan:
            %(name)s 127.0.0.1
        + Specifing port to test:
            %(name)s -p 443 127.0.0.1
        + Only diplay open ports
            %(name)s -p 139 127.0.0.1

    """"" % dict(name="st-005-s1.py")

    parser = argparse.ArgumentParser(description='OMSTD Example', epilog=footed_description,
                                     formatter_class=argparse.RawDescriptionHelpFormatter)
    parser.add_argument("targets", metavar='TARGETS', help="targets to scan", nargs="+")
    parser.add_argument('-v', dest='verbosity', default=0, action="count",
                        help="verbosity level: -v, -vv, -vvv.")
    parser.add_argument("--open", dest="only_open", action="store_true",
                        help="only display open ports", default=False)
    parser.add_argument("-p", "--ports", dest="port", type=int,
                        help="port to scan. Defaul: 80.", default=80)

    params = parser.parse_args()

Este es el aspecto que tendría al ejecutar st-005-s1.py -h:

usage: st-005-s1.py [-h] [-v] [--open] [-p PORT] TARGETS [TARGETS ...]

OMSTD Example

positional arguments:
  TARGETS               targets to scan

optional arguments:
  -h, --help            show this help message and exit
  -v                    verbosity level: -v, -vv, -vvv.
  --open                only display open ports
  -p PORT, --ports PORT
                        port to scan. Defaul: 80.

Usage examples:
    + Basic scan:
        st-005-s1.py 127.0.0.1
    + Specifing port to test:
        st-005-s1.py -p 443 127.0.0.1
    + Only diplay open ports
        st-005-s1.py -p 139 127.0.0.1

Interacción

En este bloque se tratan los casos de estudio relacionados con:

  • Interacción de usuario
  • Interacción otros sistemas y entornos.
IT-001 - Linea de comandos como entrada

TODO

Específicos del lenguaje

En este bloque se tratan los casos de estudio relacionados con los aspectos específicos del lenguaje Python:

  • Trucos
  • Buenas prácticas
  • Consejos
  • Nuevas características introducidas por Python 3

LP-001 - Tareas no bloqueantes
Problema

Python es mucho más lento que otros frameworks o lenguajes, como nodejs, que usa conexiones no “bloqueantes” o “corrutinas”.

Nota

¿Qué es una corrutina (coroutine en inglés)?

Una corrutina es una compontente (función, clase o método) capaz de suspender su ejecución hasta recibir un cierto estímulo:

_images/lp-001.01.png
Solución

Desde Python 3.4 existen lo que se llaman “corrutinas” a través de la librería, incluida en el framework 3.4, asyncio (también conocido como Tulip).

Cómo

Ejemplo LP-001.P01

Este ejemplo muestra como descargar el contenido de una URL, usando la librería estándar de Python 3.4:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
import urllib.request


# ----------------------------------------------------------------------
def main():
    req = urllib.request.Request('http://www.navajanegra.com')
    response = urllib.request.urlopen(req)
    the_page = response.read()  # <---- BLOCKING POINT

    print(the_page)

if __name__ == '__main__':
    main()

Ejemplo LP-001.S01

En el siguiente podemos ver cómo, usando Tulip y la librería aiohttp, podemos hacer peticiones no bloqueantes muy fácilmente:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
import asyncio
import aiohttp


# ----------------------------------------------------------------------
@asyncio.coroutine
def print_page(url):
    response = yield from aiohttp.request('GET', url)
    body = yield from response.read_and_close(decode=True)

    print(body)


# ----------------------------------------------------------------------
def main():
    """Comment"""
    loop = asyncio.get_event_loop()
    loop.run_until_complete(print_page('http://navajanegra.com'))


if __name__ == '__main__':
    main()

Nota

aioHTTP es una librería independiente, construida usando asyncIO a bajo nivel, y que nos facilitará el manejo de conexiones HTTP.


LP-002 - Multithreading y procesamiento paralelo
Problema

Python no tiene hilos ni multithreads real debido al GIL (Global Interpreter Lock). Éste restringe la ejecución a un único hilo corriendo a la vez. Esto es así porque, cuando se diseñó python, se prefirió que el motor fuera más simple su implementación, a costa de sacrificar la eficiente.

Nota

Para entender mejor cómo funciona el GIL, se recomienda al lector las charlas y estudios de David Beazley.

Solución

Esto es cierto y no se puede hacer nada a día de hoy. Es el modo de funcionamiento de la VM de Python por defecto, CPython, no se puede lograr, multithreading real.

La solución, para lograr la ejecución pararela en Python, es usar multiprocessing.

Nota

Existen otras implementaciones de la máquina virtual de Python: Diferentes implementaciones de la máquina virtual de PYthon

En el resto de implementaciones SI que existe el mutithread real, pero tienen multitud de incompatibilidades y no es recomendable su uso para propósito general.

Cómo

Ejemplo LP-002.P01

El siguiente código muestra un ejemplo típico de multithreading en Python, en el que solo puede haber 10 threads en ejecución concurrente (que no paralela) a la vez:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import threading

sem = threading.Semaphore(10)


# ----------------------------------------------------------------------
def hello(i):
    print(i)
    sem.release()


# ----------------------------------------------------------------------
def main():
    threads = []

    for x in range(50):

        # Create thread
        t = threading.Thread(target=hello, args=(x,))

        # Start thread
        sem.acquire()
        t.start()

        threads.append(t)

    # Wait for end of all threads
    map(threading.Thread.join, threads)

if __name__ == '__main__':
    main()

Ejemplo LP-002.S01

El siguiente ejemplo muestra el mismo resultado, pero con multiprocessing:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
from multiprocessing.pool import Pool


# ----------------------------------------------------------------------
def hello(i):
    print(i)


# ----------------------------------------------------------------------
def main():
    p = Pool(10)

    p.map(hello, range(50))


if __name__ == '__main__':
    main()

Nota

El código anterior, además de ser multiproceso real, tiene las ventajas:

  • Al usar CTRL+C funcionará correctamente.
  • Usar una librería del framework, que gestiona internamente, un pool de procesos. Con esto evitamos tener que llevar el control manual del número de procesos concurrentes que pueden ejecutarse.

LP-003 - Callback, número de parámetros y partials
Problema

Cuando usamos librerías externas o de terceros, tenemos que adaptarnos a su API, pero no siempre es fácil. Supongamos la siguiente situación:

Tenemos una función que ha de invocarse con 3 parámetros a través de un callback externo, pero este callback solo pasa uno de los parámetros requeridos por nuestra función. Con un ejemplo se verá mejor:

  1. Tenemos una librería, llamada external.api, de la que queremos ejecutar una función action_with_callback y cuyo código es:

    external/api.py

    1
    2
    3
    4
    5
    6
    7
    8
    def action_with_callback(callback_func=None):
        from time import sleep
        from random import randint
    
        time_sleep = randint(10, 100)
        sleep(time_sleep)
    
        callback_func(time_sleep)
    
  2. En nuestro código queremos ejecutar diferentes acciones, en función lo que el usuario indice en la linea de comandos, pero no podemos por el modo de funcionamiento del API externo: No podemos pasar como parámetro la operación y el primer dato:

    lp-003-p1.py

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    from externa.api import action_with_callback
    
    
    def actions(operation, data1, data2):
        if operation == 0:
            return data1 / data2
        elif operation == 1:
            return data1 * data2
        elif operation == 2:
            return data1 % data2
    
    if __name__ == "__main__":
        import sys
    
        # Get first and second command line parameters:
        # arg 0 -> operation
        # arg 1 -> first value for operation
        operation = sys.argv[0]
        data1 = sys.argv[1]
    
        action_with_callback(callback_func=actions)  # Wrong!!!! -> operation and data1 missing
    
Solución

Usar los partials de Python.

Un partial en una función que se puede construida por partes, dejando una parte fija y otra variable. Siguiendo con el ejemplo anterior:

  1. deberíamos dejar como parte “fija”:

    • La operación a realizar
    • El primer dato
  2. Y como parte variable, el valor que devuelva el callback.

Es decir, que a partir de la primera función actions(operation, data1, data2), tenemos que generar una segunda con un solo parámetro:

actions(operation, data1, data2) -> TRANSFORM -> new_actions(data2)

Cómo

Para construir un partial en Python es muy sencillo, para ello debemos usar la librería functools:

lp-003-s1.py

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
from functools import partial

from externa.api import action_with_callback


def actions(operation, data1, data2):
    if operation == 0:
        return data1 / data2
    elif operation == 1:
        return data1 * data2
    elif operation == 2:
        return data1 % data2

if __name__ == "__main__":
    import sys

    # Get first and second command line parameters:
    # arg 0 -> operation
    # arg 1 -> first value for operation
    operation = sys.argv[0]
    data1 = sys.argv[1]

    new_actions = partial(actions, operation, data1)

    action_with_callback(callback_func=new_actions)  # Well!

LP-004 - “__main__” y ejecuciones accidentales
Problema

Cuando Python se invoca, por como está concebido, lee y ejecuta todas los los ficheros y dependencias.

Esto puede implicar:

  • Llamadas accidentales a métodos, funciones o variables.
  • Al no saber el orden exacto de carga de cada fichero, puede provocar errores muy difíciles de detectar.

Por ejemplo:

lp-004-p1.py

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
class _ListNumbers:
    """Singleton with list numbers"""

    def __init__(self):
        self.numbers = [x for x in range(100)]

    def get_number(self):
        return self.numbers[randint(0, len(self.numbers))]

ListNumbers = _ListNumbers()


def hello_word(number):
    print("Hello world with number:", number)


hello_word(ListNumbers.get_number())

Como podemos ver en el ejemplo, la clase con el listado de números debería de ejecutarse y cargarse antes, pero no podemos estar seguros. Ello depende de la implementación de la máquina virtual de Python.

Es sencillo imaginar por el lector que ocurriría si el orden de carga no fuera el “natural”: Se produciría una condición de carrera y nuestro código fallaría. Las condiciones de carrera son sumamente complicadas de detectar.

Solución

Usar la “etiqueta” __name__ para indicar que dicha sección contiene el punto de entrada principal, o función main, a nuestra aplicación.

Cómo

En el siguiente código podemos apreciar que el cambio es mínimo. Con este sencillo cambio nos ahorraremos multitud de problemas:

lp-004-s1.py

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
class _ListNumbers:
    """Singleton with list numbers"""

    def __init__(self):
        self.numbers = [x for x in range(100)]

    def get_number(self):
        return self.numbers[randint(0, len(self.numbers))]

ListNumbers = _ListNumbers()


def hello_word(number):
    print("Hello world with number:", number)


if __name__ == "__main__":

    hello_word(ListNumbers.get_number())
LP-005 - Apertura, cierre y olvidos en descriptores
Problema

Cuando trabajamos con handlers (o descriptores) ya sean de archivo, red o de cualquier otro tipo, sobre ellos hay 3 acciones que siempre llevaremos a cabo:

  1. Apertura del descriptor.
  2. Uso del descriptor
  3. Cierre del descriptor.

El paso 3, cierre del descriptor, es uno de los grandes olvidados por:

  • Se omite por descuido del programador.
  • No se cierra adecuadamente: a causa de algún problema y errores.

Esto dejará descriptores de sistema abiertos y huérfanos, ocupando recursos del sistema operativo y degradando el rendimiento

El siguiente ejemplo ilustra esta situación:

lp-005-p1.py

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
from tempfile import TemporaryFile


def main(number):
    """
    Generates 100 temporal files with random numbers
    """
    for x in range(100):
        f = TemporaryFile(mode="w")
        f.write(randint(100, 10000))

if __name__ == "__main__":

    main()

Como se puede ver, en el ejemplo se crean 100 archivos temporales pero no se cierran.

Solución

Python dispone de la palabra reservada with que se asegurará de:

  • Garantizar una sintaxis sencilla y legible.
  • Cerrar el descriptor, y re-intentando o forzando su cierre cuando sea necesario.

with funciona también con sockets, acceso a bases de datos, o cualquier estructura compatible.

Cómo

Para usar with tan solo tendremos usar la sintaxis:

1
2
3
with open("...", ".") as f:
    # ACTIONS
    f.write("my text")

Donde:

  • f” será el descriptor de fichero que usaremos en nuestro código.
  • Cualquier acceso que tengamos que hacer sobre el fichero solo podremos hacerlo debajo del bloque del with.

En el momento que la ejecución del bloque finalice, el descriptor se cerrará automáticamente por la VM de Python

Aquí podemos ver el ejemplo anterior usando with:

lp-005-s1.py

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
from tempfile import TemporaryFile


def main(number):
    """
    Generates 100 temporal files with random numbers
    """
    for x in range(100):
        with TemporaryFile(mode="w") as f:
            f.write(randint(100, 10000))

if __name__ == "__main__":

    main()

LP-006 - import relativos, absolutos, paquetes y demás hierbas
Problema

Cuando desarrollamos en Python es muy habitual toparnos rápidamentente con mensajes como:

...
ValueError: Attempted relative import in non-package
...
SystemError: Parent module '' not loaded, cannot perform relative import

Estos errores son muy abituales en Python 3.x. Esto es debido a que se está tratando de hacer un import cómo si se tratara de un paquete. Pero... ¿Qué significa esto?

Nota

Python 3.4 introdujo cambios en comportamiento interno cuando importa módulos y dependencias.

Puede leer más sobre este tema consultando el PEP-0366.

Se recomienda la lectura del post de taherh en StackOverFlow: http://stackoverflow.com/a/6655098 , sobre este tema.

Un par de conceptos:

Concepto: Paquete

En Python es un proyecto que puede ser importado, para ser usado como librería.

Éstos deberían tener import relativos, para asegurarse que la librería importada es del propio paquete, y no otra del sistema que tenga el mismo nombre.

Podemos ver la importancia de las rutas relativas en el siguiente ejemplo:

Supongamos que nuestra aplicación de ejemplo lp-006-p1.py

1
2
3
from lp_006_p1.bad import *

lp_006_p1_fn()

El ejemplo tiene la estrutura:

1
2
3
4
5
6
7
lp_006_p1
    \__ random
        \__ __init__.py
    \__ __init__.py
    \__ bad.py
    \__ good.py
\__ lp-006-p1.py

El paquete random tiene el mismo nombre que el incluido en Python, por lo que si escribimos el siguiente código:

lp-006-p1/bad.py

1
2
3
4
5
6
from random import *

# ----------------------------------------------------------------------
def lp_006_p1_fn():

    print(HELLO_VAR)

Cuantro se trata de mostrar la variable HELLO_VAR, no será encontrada porque realmente estamos importando es el paquete global de Python, y éste no contiene dicha variable. Por tanto se nos mostrará el siguiente error:

Traceback (most recent call last):
  File "lp-006-p1.py", line 26, in <module>
    lp_006_p1_fn()
  File "examples/develop/lp/006/lp_006_p1/bad.py", line 30, in lp_006_p1_fn

    print(HELLO_VAR)
NameError: name 'HELLO_VAR' is not defined
Concepto: Aplicación

Una definición informa de aplicación de Python es aquella que usa como programa independiente y que tiene como finalidad ser importado por una aplicación externa.

El fichero que contiene el punto de entrada a la aplicación, o main, no puede usar rutas relativas.

¿Por qué sucede esto?

Porque el uso de import relativos solo está permitido para paquetes y han de ser llamados desde paquetes externos, no pueden ser lanzados desde el propio paquete (por defecto).

Es decir, que si tenemos la estructura de directorios usada más arriba, el siguiente código no funcionará:

1
lp_006_p1_fn()

Y nos devolverá el error:

File "lp-006-p2.py", line 24, in <module>
   from .lp_006_p1.good import *
SystemError: Parent module '' not loaded, cannot perform relative import

A modo de resumen: No funciona porque se está usando una aplicación como un paquete.

Solución

Por su puesto existen soluciones para ambos casos. A modo de conceptual son los siguientes:

Paquetes

Usar import relativos, en lugar de absolutos, cuando queramos importar paquetes locales y que éstos no se confundan con los globales u otros que podamos tener instalados.

Aplicación

Hay ocasiones en los que querremos usar una aplicación como un paquete como, por ejemplo:

Cuando queramos crear un paquete que, además, pueda ser usado como aplicación.

La solución: forzar la aplicación a que se comporte como un paquete.

Cómo
Paquetes

La solución es la impotanción relativa o, lo que es lo mismo, indicar a Python que use el paquete local en lugar del global del sistema:

1
2
3
4
5
6
from .random import *

# ----------------------------------------------------------------------
def lp_006_p1_fn():

    print(HELLO_VAR)
hello!
Aplicación

Transformar una aplicación en un paquete no es nada intuitivo ni trivial. Tendremos que tener controlar:

  • Detectar si la aplicación es parte de un paquete o no.
  • Mover los import de la parte global al ámbido de la función que haga de punto de entrada. Es ha de ser así para que, cuando el intérprete de Python importe todo el código no ejecute ni cargue ninguna librería hasta que no hayamos transformado la aplicación en paquete.

El siguiente código solucionará nuestro problema:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
# Detect if applitaion is calling out of the box or inside a package.
if __name__ == "__main__" and __package__ is None:
    import sys
    import os

    # Load path of main program file (this file) as a part of path of python interpreter
    parent_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
    sys.path.insert(1, parent_dir)

    # Load parent package as a common package
    import lp_006_p1

    # Set environment package to this package
    __package__ = str("lp_006_p1")
    del sys, os

    # Continue normally
    from .lp_006_p1.good import *

    lp_006_p1_fn()

Explicación line a linea:

  • 1: Detectamos si el fichero es el punto de entrada a la aplicación (main) y si es un paquete.
  • 7-8: Cargamos el directorio desde el que es llamado el programa en la lista de paquetes disponibles de Python.
  • 11: Una vez añadido en el entorno de Python el directorio donde se encuentra nuestro fichero, cargamos el paquete padre, el que contiene el ejecutable, o .py.
  • 14: Establecemos la variable de entorno __package__, indicándole a Python que si que existe un paquete.
  • 18: Cargamos nuestra librerías con import relativos. Ahora ya no tendremos problemas.
  • 20: Continuamos la ejecución de nuestro código, como lo haríamos normalmente.

Entrada / Salida

En este bloque se tratan los casos de estudio relacionados con la entrada / salida de información

  • Generar de informes: Word, Excel...
  • Exportación / importación de información
  • Transformación de datos
  • Exportar información de forma portable: XML, JSON, YAML...
IO-001 - Envío de información por múltiples canales
Problema

Mi aplicación muestra información por pantalla pero he de cambiar mucho código cuando, posteriormente, quiero añadir nuevas funciones en esos mismos puntos nuevas acciones como:

  • Añadir niveles de logging.
  • Volcar esa misma información a un fichero.
  • Enviar esa información a más de una ubicación.

Ejemplo IO-001.P01

Este es el código normal, con un print(...):

1
2
3
4
5
6
7
8
9
# ----------------------------------------------------------------------
def hello():
    """Display a hello world text"""
    print("hello")

# ----------------------------------------------------------------------
if __name__ == '__main__':

    hello()

Ejemplo IO-001.P02

Vemos realmente el problema cuando queremos añadir más localizaciones donde enviar el texto:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
# ----------------------------------------------------------------------
def hello():
    """Display a hello world text"""
    print("hello")

    with open("my_file.txt", "") as f:
        f.write("hello")

# ----------------------------------------------------------------------
if __name__ == '__main__':

    hello()

Ejemplo IO-001.P03

Y todavía se complica más si queremos añadir niveles de verbosidad:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
# ----------------------------------------------------------------------
def hello(verbosity):
    """Display a hello world text"""
    print("hello")

    if verbosity > 0:
        with open("my_file.txt", "") as f:
            f.write("hello")

    if verbosity > 0:
        print("verbosity 1")

# ----------------------------------------------------------------------
if __name__ == '__main__':

    hello(1)
Solución

Usar un objeto estático y global que será el encargado de mostrar la información y que podremos configurar en función de lo que necesitemos.

Cómo

Para ello tenemos que declarar una clase estática global, que siga el patrón Singleton en Python, configurarla y llamarla de forma adecuada:

Nota

El patrón Singleton nos asegura que solo haya corriendo una instancia de un determinad objeto a la vez.

Si se crea otra nueva instancia, internamente no creará un nuevo objeto, sino que “rescatará” de la memoria el primer objeto que se creó y se reutilizará.

Ejemplo IO-001.S01

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
# ----------------------------------------------------------------------
class Displayer:
    instance = None

    def __new__(cls, *args, **kwargs):
        if cls.instance is None:
            cls.instance = object.__new__(cls, *args, **kwargs)
            cls.__initialized = False
        return cls.instance

    def config(self, **kwargs):
        self.out_file = kwargs.get("out_file", None)
        self.out_screen = kwargs.get("out_screen", True)
        self.verbosity = kwargs.get("verbosity", 0)

        if self.out_file:
            self.out_file_handler = open(self.out_file, "w")

    def display(self, message):
        if self.verbosity > 0:
            self.__display(message)

    def display_verbosity(self, message):
        if self.verbosity > 1:
            self.__display(message)

    def display_more_verbosity(self, message):
        if self.verbosity > 2:
            self.__display(message)

    def __display(self, message):
        if self.out_screen:
            print(message)
        if self.out_file_handler:
            self.out_file_handler.write(message)

    def __init__(self):
        if not self.__initialized:
            self.__initialized = True
            self.out_file = None
            self.out_file_handler = None
            self.out_screen = True
            self.verbosity = 0


# ----------------------------------------------------------------------
def hello():
    """Display a hello world text"""

    # Use displayer
    out = Displayer()
    out.display("hello")

    out.display_verbosity("hello")

    # This will not be displayed by the verbosity level to 1
    out.display_more_verbosity("hello")

# ----------------------------------------------------------------------
if __name__ == '__main__':

    # Config displayer
    d = Displayer()
    d.config(out_screen=True,
             out_file="~/my_log.txt",
             verbosity=1)

    # Call function
    hello(1)

Comportamiento

En este bloque se tratan los casos de estudio relacionados con la integración con las formas de comportarse el entorno y sus frameworks asociados.

BH-001 - Procesamiento de tareas costosas
Problema

No es fácil implementar un sistema de gestión y procesamiento de tareas en asíncronas o en background.

La finalidad de este tipo de tareas es poder realizar acciones de manera desatendida y, una vez finalizadas, informar de los resultados obtenidos.

Ejemplo:

Tenemos un sitio web con un formulario de contacto. Este formulario envía un correo al administrador del sitio web.

Problema:

Si esperamos a que el mail sea enviado, estaremos haciendo esperar al usuario y puede que este tiempo sea muy largo (Sobre todo si el envío de mail falla y hay que tratar de reenviar).

Solución

Usar Celery. Celery es un gestor de tareas hecho en Python y que alcanza muy buenos niveles de rendimiento.

¿Cómo funciona?

Celery está formado por 3 piezas principales:

  • Un broker.
  • El servicio de Celery.
  • La aplicación que interactúa con Celery, que será nuestra aplicación.

Esta animación puede que te ayude a comprender mejor su funcionamiento:

_images/bh-001.01.gif

Nota

Aspectos importantes a tener en cuenta:

  • Posibles problemas de portabilidad: Necesita un broker .
  • Posible solución para portabilidad (¡no para rendimiento!): Usar Django ORM + SQLite.
Cómo
Funcionamiento básico

Una forma de estructurar Celery es (¡puede haber más!):

_images/bh-001.02.png

Nota

Las carpetas deben de convertirse en paquetes, incluyendo un fichero __init__.py, en caso contrario no será reconocido y cargado por Celery.

Como se puede ver la imagen anterior, tenemos las siguientes carpetas clave:

  • framework/
    • framework/tasks/: Contiene nuestro código. Es el que contendrá los ficheros con los fuentes de nuestras tareas que queramos ejecutar.
    • framework/celery/: Contiene el único fichero celery.py con la información sobre cómo ha de ejecutarse Celery.

Archivos importantes:

framework/tasks/main_task.py

Este fichero contiene un ejemplo de código que se ejecutará como parte de una tarea. Dicha tarea se quedará a la espera que se le mande información para procesar.

En este ejemplo, cuando la tarea sea llamada, solamente mostrará un mensaje. Ésta es la forma más sencilla de crear tareas. Usando el decorador de Python @celery.task una función será convertida en una tarea.

framework/celery/celery.py

Contiene la información necesaria para cargar y configurar Celery. A continuación se muestran la lineas más importantes de este fichero:

start.py

Contiene la llamada a la tarea tipo Celery.

Nota

En Celery, para llamar a una tarea de forma asíncrona, debemos de hacerlo como en el ejemplo. Aunque nuestro código sea una función, celery se encargará internamente de convertirla en un objeto con métodos.

Las llamadas (o métodos) para llamar a la tarea son, según el API oficial:

  • my_task.delay(...)
  • my_task.apply_async(...)
Funcionamiento avanzado

Configuración:

Por defecto, Celery solo importa las tareas que tienen como nombre de fichero tasks.py. Usando un un pequeño truco, podemos localizar todos los ficheros con tareas de Celery, independientemente del nombre de fichero:

Ejemplo BH-001.P01

Ejemplo BH-001.S02

Invocar tareas por su nombre:

Otra forma, algo más avanzada, de invocar una tarea de Celery (además de my_task.delay() y my_task.apply_async()) consiste en realizar una llamada usando el nombre relativo de la tarea, en lugar de usar código Python de programación.

Veámoslo con un ejemplo:

Supongamos que una tarea está situada en: framework.tasks.send_mails, con el nombre de send_mail(). Conforme lo hemos visto hasta ahora la invocación sería como sigue:

1
2
3
4
framework.tasks.send_mails import send_mail

if __name__ == '__main__':
    send_mail.delay()

Con este nuevo método quedría como sigue:

1
2
3
4
5
6
7
from framework.celery.celery import celery

if __name__ == '__main__':

    celery.send_task("framework.tasks.send_mails.send_mail")  # Without params

    celery.send_task("framework.tasks.send_mails.send_mail", ("From my@my.com"))  # With params
Anexo BH-001: Arrancar un entorno Celery

Poner en funcionamiento un entorno que use Celery no es trivial. Han de arrancarse los servicios en el orden adecuado, esto es:

  1. Arrancar el broker
  2. Arrancar Celery
  3. Arrancar nuestra aplicación

A continuación se explican los pasos a seguir para arrancar un entorno que funcione con Celery:

1 - Arrancar el broker

Cada Broker tiene su propio método de arranque. En esta guía solo se cubrirá, de momento, RabbitMQ:

Para arrancar el servidor de RabbitMQ tenemos que escribir en una consola:

sudo rabbitmq-server &
            RabbitMQ 3.1.5. Copyright (C) 2007-2013 GoPivotal, Inc.
##  ##      Licensed under the MPL.  See http://www.rabbitmq.com/
##  ##
##########  Logs: /opt/local/var/log/rabbitmq/rabbit@localhost.log
######  ##        /opt/local/var/log/rabbitmq/rabbit@localhost-sasl.log
##########
            Starting broker... completed with 0 plugins.
2 - Arrancar Celery

Celery es un software que corre como servicio, actuando de orquestador entre nuestra aplicación y el sistema de mensajería. Debemos arrancar una instancia de Celery por cada aplicación que queramos correr.

La forma de arrancarlo está condicionada por la estructura de nuestro proyecto. La siguiente es una propuesta para de organización para nuestro código:

celery -A framework.celery.celery worker

Si lo queremos con más información de depuración:

celery -A framework.celery.celery worker --loglevel=info
[2014-11-03 16:35:54,223: WARNING/MainProcess] /Users/XXX/.virtualenvs/omstd/lib/python3.4/site-packages/celery/apps/worker.py:161: CDeprecationWarning:
Starting from version 3.2 Celery will refuse to accept pickle by default.

The pickle serializer is a security concern as it may give attackers
the ability to execute any command.  It's important to secure
your broker from unauthorized access when using pickle, so we think
that enabling pickle should require a deliberate action and not be
the default choice.

If you depend on pickle then you should set a setting to disable this
warning and to be sure that everything will continue working
when you upgrade to Celery 3.2::

    CELERY_ACCEPT_CONTENT = ['pickle', 'json', 'msgpack', 'yaml']

You must only enable the serializers that you will actually use.


  warnings.warn(CDeprecationWarning(W_PICKLE_DEPRECATED))

 -------------- celery@localhost v3.1.16 (Cipater)
---- **** -----
--- * ***  * -- Darwin-14.0.0-x86_64-i386-64bit
-- * - **** ---
- ** ---------- [config]
- ** ---------- .> app:         __main__:0x1085dba90
- ** ---------- .> transport:   amqp://guest:**@localhost:5672//
- ** ---------- .> results:     disabled
- *** --- * --- .> concurrency: 4 (prefork)
-- ******* ----
--- ***** ----- [queues]
 -------------- .> celery           exchange=celery(direct) key=celery


[tasks]
  . framework.tasks.export_results_task.export_to_csv
  . framework.tasks.yara_task.yara_task

[2014-11-03 16:35:54,258: INFO/MainProcess] Connected to amqp://guest:**@127.0.0.1:5672//
[2014-11-03 16:35:54,272: INFO/MainProcess] mingle: searching for neighbors
[2014-11-03 16:35:55,297: INFO/MainProcess] mingle: all alone
[2014-11-03 16:35:55,309: WARNING/MainProcess] celery@localhost ready.
3 - Lanzar nuestra aplicación

Por último, lanzar nuestra aplicación será como ejecutar un script normal en Python:

python start.py
4 - Demo funcionamiento

A continuación se puede ver una pequeña demo de funcionamiento de Celery:


BH-002 - Tareas programadas
Problema

La ejecución de forma programática o que se ejecuten cada X tiempo tiene los siguientes problemas (entre otros):

  • No es trivial de implementar.
  • No suele ser portable entre plataformas.
  • La implementación de métodos tradicionales abusa del uso de hilos o la suspensión del flujo de la ejecución de forma manual.
Solución

Celery Beat para ejecutar tareas programadas o temporizadas.

Cómo
Configuración

Tan solo tenemos que cambiar la configuración de Celery y añadir la tarea que queremos ejecutar y la repetición del mismo:

Ejemplo BH-002.S01

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
from .utils import find_tasks_in_project

# Celery taskes package
CELERY_MAIN_FOLDER = "framework"

# --------------------------------------------------------------------------
# Celery common properties
# --------------------------------------------------------------------------
CELERY_TASK_RESULT_EXPIRES = 3600
CELERY_IMPORTS = find_tasks_in_project("framework")
BROKER_URL = 'amqp://guest:guest@localhost:5672/'

# --------------------------------------------------------------------------
# Celery scheduled tasks
# --------------------------------------------------------------------------
CELERY_TIMEZONE = 'UTC'
# CELERYBEAT_SCHEDULE = {}

CELERYBEAT_SCHEDULE = {
    'add-every-30-seconds': {
        'task': 'framework.tasks.main_task',
        'schedule': timedelta(seconds=30),
        # 'args': (16, 16)
    },
}
Ejecución

Para llamar a Celery Beat, tan solo tendremos que ejecutar Celery como se describió en la sección anterior, pero añadiendo el parámetro -B:

celery -A framework.celery.celery worker -B --loglevel=info

Redistribución

En este bloque se tratan los casos de estudio relacionados con la redistribución del software y las diferentes formas de hacerlo:

  • Sistemas de control de versiones y correcto uso.
  • Creación de paquetes.
  • Inclusión en repositorios públicos.
  • Creación de binarios.
  • Portabilidad entre sistemas.

RD-001 - Inclusión de dependencias externas
Problema

Mi proyecto tiene dependencias y no se cómo hacer que sean fácilmente instalables para que se puede redistribuirlos de forma sencilla.

Solución

Usar pip + el fichero requirements.txt.

  • Pip es un gestor de paquetes Python, al más puro estilo “apt-get” de Debian.
  • requirements.txt: Fichero donde se detallan todas las dependencias del proyecto.
Cómo

En este link se puede encontrar el ejemplo: Ejemplo requirements.txt

Para instalar todas las dependencias usando pip:

pip -r requirements.txt

RD-002 - Sandbox y entornos virtuales
Problema

Las dependencias entre proyectos y tener que instalarlo todo en el sistema lo “ensucia” y hace que todo sea un caos.

Solución

Usar virtualenv:

  • Virtualenv: es una “sandbox” donde se instalarán todas las dependencias de tu software.
  • Además, podemos usar virtualenvwrapper, para añadir más funcionalidad y utilidades a virtualenv.
Cómo
Instalar virtualenwrapper

Nota

Virtualenvwrapper incluye el paquete “virtualenv”

pip install virtualenvwrapper
Crear un virtualenv
mkvirtualenv tutorial
Crear un virtualenv temporal
mktmpenv tutorial_tmp
Salir del virtualenv
deactivate

Despliegue

En este bloque se tratan los casos de estudio relacionados con el despliegue y puesta en producción de una aplicación.

TODO

Hacking

Este bloque recopila una serie de proyectos de ejemplo, que aplican los conceptos de la guía.

HH-001: Sniffing de redn (TODO)

Problema

La interpretación de protocolos de red, análisis, almacenamiento de resultados y, posteriormente, llevar a cabo un datamining para obtener información relevante no es trivial.

Construir un proyecto escalable, bien diseñado y eficiente es la clave.

Solución

Sniffing de protocolos de red, usando las librerías adecuadas: - Rip - Web - Telnet - DHCP - ...

Realizar un sistema de tiempo real, pero desatendido.

Implementar una abstracción con los sistemas de almacenamiento, a fin de lograr escalabilidad y portabilidad.

Cómo

TODO

Cracking

Este bloque recopila una serie de proyectos de ejemplo, que aplican los conceptos de la guía.

CH-001: Cracking: GPUs + CPU (TODO)

Problema

Cracking en modo cluster y paralelizado.

Solución

Hacer un diccionarios y enviar bloques de procesamiento a nodos que prueben dichos bloques. Estos nodos pueden ser remotos o locales.

Usando este método podemos conseguir: GPUs + CPUs + PCs viejos + red -> Win!

Cómo

TODO

CH-002: SSH Bruteforcing (TODO)

Problema

Bruteforcing de protocolos autenticados: SSH, Telnet, HTTP Basic Auth...

Solución

Propuesta de pequeño PoC de bruteforcing, en Python 3, con conexiones no bloqueantes.

Explicación del funcionamiento de ktcal2 (bruteforcer SSH con estas características)

Cómo

TODO

Malware

MH-EX-001: Análisis básico de malware con Yara

En este ejemplo práctico crearemos una pequeña herramienta que nos permita analizar, de forma muy básica, malware usando Yara.

Yara es una herramienta que tiene como objetivo ayudar a los investigadores de malware a clasificar y detectar malware.

El uso de Yara puede ser tan sencillo o complejo como queramos. La herramienta propuesta tiene como objetivo ser un PoC simple, funcional y práctico. Siéntase libre de copiar el código y ampliarlo como necesite.

Problema

Analizar muestras de malware en batch puede puede no resultar sencillo, sobre todo si las muestras son de gran tamaño o tenemos que aplicar muchas reglas.

La principal dificultad suele ser el correcto diseño, implementación y documentación de la aplicación.

Solución

La solución propuesta implementa un sistema de procesamiento paralelo, distribuido y automatizado de análisis de muestras, en el que se pueden añadir nuevas muestras en cualquier momento, quedando en cola de espera para ser atendidas.

La aplicación generará un resultado en formato CSV compatible con Excel.

Las soluciones aquí propuestas siguen los casos de estudio propuestos en el bloque de desarrollo.

Cómo
Funcionamiento
Ayuda en la linea de comandos

Nuestra aplicación se ejecuta a través de la linea de comandos. Para ver todas las opciones disponibles tan solo tenemos que escribir:

python start.py --help

Y como resultado:

usage: start.py [-h] -p SAMPLE_PATH [-v VERBOSITY] [-o OUTPUT_FILE]
            [--rules-path RULES_PATH]

OMSTD Malware

optional arguments:
  -h, --help            show this help message and exit
  -p SAMPLE_PATH, --sample-path SAMPLE_PATH
                        binary sample path
  -v VERBOSITY          enable verbose mode
  -o OUTPUT_FILE        output file name
  --rules-path RULES_PATH
                        yara rules path (default .rules/)
Ejecución básica

El funcionamiento es muy simple. El analizador está hecho usando Celery, por lo que hay que seguir los mismos pasos explicados en el anexo del caso de estudio BH-001 para ejecutar la aplicación: Pasos para lanzar Celery.

Una vez lanzado el servicio de Celery, así como sus dependencias, nuestra aplicación ya está a la espera de recibir muestras para ser analizadas. Para esto ejecutaremos:

python start.py -p samples/sample.txt -o results

Aquí podemos ver la salida generada: Un CSV con la información:

HelloWorld,True,Data: 'Hello world' (flags: 19 # offset: 0),1,Rule HelloWorld was found a match in binary 'sample.txt' with tags: No tags
HelloWorld,True,Data: 'Hello world' (flags: 19 # offset: 0),0,Rule HelloWorld was found a match in binary 'sample.txt' with tags: No tags
Another,False,,1,Rule Another was NOT found a match in binary 'sample.txt' with tags: No tags
SeeYou,True,Data: 'See you' (flags: 19 # offset: 12),1,Rule SeeYou was  found a match in binary 'sample.txt' with tags: No tags
Another,False,,0,Rule Another was NOT found a match in binary 'sample.txt' with tags: No tags
Muestras y reglas de ejemplo

Puede encontrar muestras y reglas Celery de ejemplo, para poder probar la herramienta inmediatamente:

En la sección Anexo puede encontrar un listado de enlaces con reglas listas para ser utilizadas.

Desglose de las piezas

Las piezas que compondrán la herramienta son las siguientes:

  • Servicio que recibe nuevas muestras.
  • Generador de resultados.
  • Medio para añadir nuevas muestras

Todo el código fuente lo puedes descargar y probar aquí.

La estructura y organización del proyecto sigue las directrices de ST-001.

Recepción de muestras

El servicio de recepción de muestras es un tarea de Celery, a la espera de recibir nueva información para analizar de forma asíncrona y en background.

El motor de análisis es muy sencillo. El análisis se hace en dos partes:

  • El análisis con Yara.
  • La interpretación y transformación de resultados.

Ambas partes están definidas como dos funciones en el fichero yara_task.py.

Analizador con Yara

La función yara_task, convertida en tarea con el decorador @celery.task, es la encargada de hacer la llamada a Yara y:

  1. Cargar las reglas Yara, con el método yara.compile(), indicándole el listado de ficheros “*.yara” con las reglas.
  2. Usando partials (LP-003) construimos la llamada al callback. Dicho callback será llamado por la librería de Yara, cada vez que ésta ejecute con éxito una regla.
  3. Finalmente se lanza la orden para hacer el “match”, rules.match(...), indicándole el partial anteriormente construido.

En el siguiente código se puede ver estos puntos:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
@celery.task
def yara_task(input_parameters):
    """
    Celery task that process a binary sample.

    :param input_parameters: Parameters object with global input parameters
    :type input_parameters: Parameters
    """

    # Load Yara rules and match with binary
    rules = yara.compile(filepaths=input_parameters.yara_rules_files)

    # Make custom callback function
    callback_function = partial(yara_callback, input_parameters)

    # Run Yara rules!
    rules.match(input_parameters.sample_path, callback=callback_function)

Analizador de resultados

La función yara_callback actúa como callback que Yara llamará cuando termine de procesar cada una de las reglas.

En ella, y tras comprobarse la validez de los parámetros de entrada, se llevan a cabo las siguientes acciones: #. Transformar los datos de entrada del formato Yara al formato interno, del tipo Results, según SP-003 #. Enviar la información, de forma asíncrona, a la tarea que se encarga de almacenar los resultados: celery.send_task("framework.tasks.export_results_task.export_to_csv", ...).

En el siguiente código se puede ver estos puntos:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
# ----------------------------------------------------------------------
def yara_callback(input_parameters, yara_results):
    """
    Call back for Yara match. Retreive Yara yara_results, as dictionary, and call Celery task that stores yara_results.

    :param input_parameters: Parameters object with global input parameters
    :type input_parameters: Parameters

    :param yara_results: Raw Yara match yara_results in format: {'matches': True, 'rule': 'HelloWorld', 'namespace': '0', 'tags': [], 'meta': {}, 'strings': [{'identifier': '$a', 'flags': 19, 'data': 'Hello world', 'offset': 0}]}
    :type yara_results: dict
    """

    # Inputs validations
    if not isinstance(yara_results, dict):
        raise TypeError("Expected dict, got '%s' instead" % type(yara_results))

    if yara_results is None:
        print("Yara rules returned not yara_results")
        return

    # Yara input format:
    #
    # {'matches': True,
    #  'meta': {},
    #  'namespace': '0',
    #  'rule': 'HelloWorld',
    #  'strings': [{'data': 'Hello world',
    #               'flags': 19,
    #               'identifier': '$a',
    #               'offset': 0}],
    #  'tags': []}
    #
    r_rule = yara_results['rule']
    r_matches = yara_results['matches']
    r_tags = ",".join(yara_results['tags']) if yara_results['tags'] else "No tags"
    r_namespace = int(yara_results['namespace'])

    # Fixing payload
    r_payload = "#".join(["Data: '%s' (flags: %s # offset: %s)" % (x['data'], x['flags'], x['offset'])
                          for x
                          in yara_results['strings']])

    # Make Results structure
    r = Results(rule=r_rule,
                matches=r_matches,
                payload=r_payload,
                namespace=r_namespace,
                description="Rule %s was %sfound a match in "
                            "binary '%s' with tags: %s" % (r_rule,
                                                           "" if r_matches else "NOT ",
                                                           input_parameters.sample_file_name,
                                                           r_tags))

    # Send info to exporter
    celery.send_task("framework.tasks.export_results_task.export_to_csv", (input_parameters, r))
Generador de resultados

Los resultados serán generados de forma asíncrona, al igual que la recepción de éstos. Para ello, se ha creado una tarea de Celery, a la espera de recibir nueva información, con la que generar los resultados deseados.

La herramienta genera los resultados a través de la función yara_callback(...). Ésta, una vez hecha la transformación, hace una llamada la a la tarea generadora de resultados, llamada: export_to_csv(...).

Hay varias formas de llamar a una tarea de Celery, como se puede estudiar en BH-001. En este caso nos hemos decantado por la opción my_task.send_task("..."), como se ha visto en la :ref:` sección anterior <mh-001-results-analysis-section>`.

La función, convertida en tarea, export_to_csv(...) hace 3 cosas muy sencillas:

  1. Una comprobación mínima de los parámetros de entrada.
  2. Abre un fichero, siguiendo las recomendaciones de LP-005, en modo “append” o para añadir información al final de éste.
  3. Escribe una nueva linea en el fichero en formato csv

Tal y como podemos ver en la lineas señaladas:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
@celery.task
def export_to_csv(input_params, results):
    """
    Export results to CSV file.
    
    :param input_params: Global Parameters instance with input configuration
    :type input_params: Parameters
    
    :param results: Results object with analysis results.
    :type results: Results
    """

    if not isinstance(input_params, Parameters):
        raise TypeError("Expected Parameters, got '%s' instead" % type(input_params))

    with open("%s.csv" % input_params.output_file, "a") as f:
        csv_writer = csv.writer(f)

        # Title
        # csv_writer.writerow(["# Rule", "matches", "payload", "description"])

        csv_writer.writerow([
            results.rule,
            results.matches,
            results.payload,
            results.namespace,
            results.description
        ])
Añadir nuevas muestras

Añadir nuevas muestras, es equivalente a decir: enviar nuevas muestras a la cola de análisis, para que sean procesadas cuando toque su turno.

Para este PoC se ha optado por algo sencillo: un linea de comandos (en secciones anteriores se muestra cómo ejecutarse).

Hemos tenido en cuenta las siguientes medidas o recomendaciones:

  • Hemos usado la librería standard argparser, siguiendo la sintaxis de la documentación oficial, y teniendo en cuenta las buenas prácticas especificadas en IT-001.
  • Además, hemos prevenido la ejecución accidental o a destiempo aplicando LP-004.
  • Hemos centralizado la ejecución en un punto, usando ST-004, dejando preparado el proyecto para otras interfaces de usuario.
  • Hemos usado un punto central para almacenar los parámetros de ejecución del usuario, como se recomienda en ST-002.

Tal y como podemos ver en la lineas señaladas:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
import argparse

from api import Parameters, run_all

# ----------------------------------------------------------------------
if __name__ == '__main__':

    parser = argparse.ArgumentParser(description='OMSTD Malware')
    parser.add_argument("-p", "--sample-path", type=str, dest="sample_path", help="binary sample path", required=True)
    parser.add_argument("-v", dest="verbosity", type=int, help="enable verbose mode", default=False)
    parser.add_argument("-o", dest="output_file", type=str, help="output file name", default=None)
    parser.add_argument("--rules-path", dest="rules_path", type=str, help="yara rules path (default .rules/)",
                        default=None)

    params = parser.parse_args()

    input_parameters = Parameters(sample_path=params.sample_path,
                                  output_file=params.output_file,
                                  verbosity=params.verbosity,
                                  rules_path=params.rules_path)

    run_all(input_parameters)

La ejecución tiene lugar a través de la función run_all(...), incluido en el fichero api.py. Si observamos el código, podemos comprobar que lo que hace dicha función es una llamada a la tarea de análisis, con sintaxis Celery:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
from lib.data import Parameters, Results
from framework.celery.celery import celery
from framework.tasks.yara_task import yara_task


# ----------------------------------------------------------------------
def run_all(input_parameters):

    # Display results
    yara_task.delay(input_parameters)

Forense

Este bloque recopila una serie de proyectos de ejemplo, que aplican los conceptos de la guía.

Hardening

Este bloque recopila una serie de proyectos de ejemplo, que aplican los conceptos de la guía.