Stegosaurus: una herramienta de esteganografía para embeber payloads dentro de bytecode de Python

Stegosaurus es una herramienta de esteganografía que permite incrustar payloads en archivos Python de bytecode (pyc o pyo).

El proceso de incorporación no altera el comportamiento del tiempo de ejecución o el tamaño del fichero portador (en adelante carrier) y, por lo general, da como resultado una codificación de baja densidad.

El payload se dispersa en todo el bytecode, por lo que herramientas como strings no mostrarán el payload. El módulo dis de Python devolverá los mismos resultados para el bytecode antes y después de que Stegosaurus se utilice para incrustar un payload.

En este momento, no se conocen trabajos previos o métodos de detección para este tipo de entrega de payloads.

Stegosaurus requiere Python 3.6 o posterior.

Uso
$ python3 -m stegosaurus -h
usage: stegosaurus.py [-h] [-p PAYLOAD] [-r] [-s] [-v] [-x] carrier

positional arguments:
  carrier               Carrier py, pyc or pyo file

optional arguments:
  -h, --help            show this help message and exit
  -p PAYLOAD, --payload PAYLOAD
                        Embed payload in carrier file
  -r, --report          Report max available payload size carrier supports
  -s, --side-by-side    Do not overwrite carrier file, install side by side
                        instead.
  -v, --verbose         Increase verbosity once per use
  -x, --extract         Extract payload from carrier file

Ejemplo

Supongamos que queremos incrustar un payload en el bytecode del siguiente script en Python, denominado example.py:
"""Example carrier file to embed our payload in.
"""

import math

def fibV1(n):
    if n == 0 or n == 1:
        return n
    return fibV1(n - 1) + fibV1(n - 2)

def fibV2(n):
    if n == 0 or n == 1:
        return n
    return int(((1 + math.sqrt(5))**n - (1 - math.sqrt(5))**n) / (2**n * math.sqrt(5)))

def main():
    result1 = fibV1(12)
    result2 = fibV2(12)

    print(result1)
    print(result2)

if __name__ == "__main__":
    main()

El primer paso es usar Stegosaurus para ver cuántos bytes puede contener nuestro payload sin cambiar el tamaño del archivo carrier.
$ python3 -m stegosaurus example.py -r
Carrier can support a payload of 20 bytes

Ahora ya sabemos que podemos incrustar de manera segura un payload de hasta 20 bytes. Con la opción -s instalaremos el carrier "side-by-side" con el bytecode sin modificar.
$ python3 -m stegosaurus example.py -s --payload "root pwd: 5+3g05aW"
Payload embedded in carrier

Al mirar en el disco, tanto el carrier como el bytecode original tienen el mismo tamaño:
$ ls -l __pycache__/example.cpython-36*
-rw-r--r--  1 jherron  staff  743 Mar 10 00:58 __pycache__/example.cpython-36-stegosaurus.pyc
-rw-r--r--  1 jherron  staff  743 Mar 10 00:58 __pycache__/example.cpython-36.pyc

Nota: Si se omite la opción -s, se sobrescribe el bytecode original.

El payload puede extraerse pasando la opción -x a Stegosaurus:
$ python3 -m stegosaurus __pycache__/example.cpython-36-stegosaurus.pyc -x
Extracted payload: root pwd: 5+3g05aW

El payload no tiene porque ser una cadena de caracteres en ascii, también son compatibles shellcodes:
$ python3 -m stegosaurus example.py -s --payload "\xeb\x2a\x5e\x89\x76"
Payload embedded in carrier

$ python3 -m stegosaurus __pycache__/example.cpython-36-stegosaurus.pyc -x
Extracted payload: \xeb\x2a\x5e\x89\x76

Para mostrar que el comportamiento del código de Python en tiempo de ejecución permanece después de que Stegosaurus incruste el payload:
$ python3 example.py
144
144

$ python3 __pycache__/example.cpython-36.pyc 
144
144

$ python3 __pycache__/example.cpython-36-stegosaurus.pyc 
144
144

Salida de strings después de que Stegosaurus incruste el payload (observa que el payload no se muestra):
$ python3 -m stegosaurus example.py -s --payload "PAYLOAD_IS_HERE"
Payload embedded in carrier

$ strings __pycache__/example.cpython-36-stegosaurus.pyc 
.Example carrier file to embed our payload in.
fibV1)
example.pyr
math
sqrt)
fibV2
print)
result1
result2r
main
__main__)
__doc__r

__name__r
<module>

$ python3 -m stegosaurus __pycache__/example.cpython-36-stegosaurus.pyc -x
Extracted payload: PAYLOAD_IS_HERE

Ejemplo de salida del módulo dis de Python, que no muestra diferencias antes y después de que Stegosaurus incorpore su payload:

Antes:

20 LOAD_GLOBAL              0 (int)
22 LOAD_CONST               2 (1)
24 LOAD_GLOBAL              1 (math)
26 LOAD_ATTR                2 (sqrt)
28 LOAD_CONST               3 (5)
30 CALL_FUNCTION            1
32 BINARY_ADD
34 LOAD_FAST                0 (n)
36 BINARY_POWER
38 LOAD_CONST               2 (1)
40 LOAD_GLOBAL              1 (math)
42 LOAD_ATTR                2 (sqrt)
44 LOAD_CONST               3 (5)
46 CALL_FUNCTION            1
48 BINARY_SUBTRACT
50 LOAD_FAST                0 (n)
52 BINARY_POWER
54 BINARY_SUBTRACT
56 LOAD_CONST               4 (2)

Después:
20 LOAD_GLOBAL              0 (int)
22 LOAD_CONST               2 (1)
24 LOAD_GLOBAL              1 (math)
26 LOAD_ATTR                2 (sqrt)
28 LOAD_CONST               3 (5)
30 CALL_FUNCTION            1
32 BINARY_ADD
34 LOAD_FAST                0 (n)
36 BINARY_POWER
38 LOAD_CONST               2 (1)
40 LOAD_GLOBAL              1 (math)
42 LOAD_ATTR                2 (sqrt)
44 LOAD_CONST               3 (5)
46 CALL_FUNCTION            1
48 BINARY_SUBTRACT
50 LOAD_FAST                0 (n)
52 BINARY_POWER
54 BINARY_SUBTRACT
56 LOAD_CONST               4 (2)

Usando Stegosaurus

Los payloads, los métodos de entrega y de recepción dependen completamente del usuario. Stegosaurus solo proporciona los medios para insertar y extraer payloads desde un archivo de bytecode de Python. Debido al objetivo de dejar intacto el tamaño del archivo, se puede usar una cantidad relativamente pequeña de bytes para entregar el payload. Esto puede requerir la distribución de payloads más grandes a través de múltiples archivos de bytecode, lo que tiene algunas ventajas, tales como:

  •     Entrega de un payload en fragmentos a lo largo del tiempo
  •     Algunas partes del payload pueden repartirse en varias ubicaciones y unirse cuando sea necesario
  •     Una sola porción comprometida no divulga todo el payload
  •     Evita la detección de un payload completo mediante la difusión a través de múltiples archivos aparentemente no relacionados
Nota: la posibilidad de distribuir grandes payloads a través de múltiples archivos de bytecode de Python no está implementada por el momento, ver TODO. 

Cómo funciona Stegosaurus 

Para incrustar un payload sin aumentar el tamaño del archivo, las zonas "muertas" deben identificarse dentro del bytecode. Una zona muerta se define como cualquier byte que, si se modifica, no afectará el comportamiento del script de Python. Python 3.6 introdujo zonas muertas fáciles de explotar. Sin embargo, miramos atrás un poco a la historia para establecer el escenario. 

El intérprete de referencia de Python, CPython tiene dos tipos de opcodes: los que tienen argumentos y los que no. En Python <= 3.5 las instrucciones en el bytecode ocupaban 1 o 3 bytes, dependiendo de si el opcode tomaba un valor o no. En Python 3.6 esto se cambió para que todas las instrucciones ocupen dos bytes. Los que no tienen argumentos simplemente establecen el segundo byte a cero y se ignora durante la ejecución. Esto significa que para cada instrucción en el bytecode que no toma un algoritmo, Stegosaurus puede insertar con seguridad un byte de payload. 

Algunos ejemplos de opcodes que no toman un argumento:
BINARY_SUBTRACT
INPLACE_ADD
RETURN_VALUE
GET_ITER
YIELD_VALUE
IMPORT_STAR
END_FINALLY
NOP
...

Para ver un ejemplo de los cambios en el bytecode, echa un vistazo al siguiente fragmento de Python:
def test(n):
    return n + 5 + n - 3

Usando dis con Python inferior a 3 .6:
0  LOAD_FAST                0 (n)
3  LOAD_CONST               1 (5)    <-- opcodes with an arg take 3 bytes
6  BINARY_ADD                        <-- opcodes without an arg take 1 byte
7  LOAD_FAST                0 (n)
10 BINARY_ADD          
11 LOAD_CONST               2 (3)
14 BINARY_SUBTRACT      
15 RETURN_VALUE

# :( no easy bytes to embed a payload

 
Sin embargo, con Python 3.6:
0  LOAD_FAST                0 (n)
2  LOAD_CONST               1 (5)    <-- all opcodes now occupy two bytes
4  BINARY_ADD                        <-- opcodes without an arg leave 1 byte for the payload
6  LOAD_FAST                0 (n)
8  BINARY_ADD
10 LOAD_CONST               2 (3)
12 BINARY_SUBTRACT
14 RETURN_VALUE

# :) easy bytes to embed a payload

Pasando -vv a Stegosaurus podemos ver cómo el payload está incrustado en estas zonas muertas:
$ python3 -m stegosaurus ../python_tests/loop.py -s -p "ABCDE" -vv
Read header and bytecode from carrier
BINARY_ADD (0)
BINARY_ADD (0)
BINARY_SUBTRACT (0)
RETURN_VALUE (0)
RETURN_VALUE (0)
Found 5 bytes available for payload
Payload embedded in carrier
BINARY_ADD (65)      <-- A
BINARY_ADD (66)      <-- B
BINARY_SUBTRACT (67) <-- C
RETURN_VALUE (68)    <-- D
RETURN_VALUE (69)    <-- E

Nota: Se eliminan de los registros los timestamps y niveles de depuración para facilitar la lectura.
 
Actualmente esta es la única zona muerta que explota Stegosaurus. Las mejoras futuras incluyen más identificación de zonas muertas como se menciona en
el TODO.

TODO
  • Agregar la opción de autodestrucción -d que purgará el payload del carrier después de la extracción
  • Método de soporte para distribuir el payload a través de múltiples archivos carrier
  • Proporcionar el indicador -t para probar si un payload puede estar presente dentro de un archivo carrier
  • Encontrar más zonas muertas dentro del bytecode para colocar el payload
  • Agregar una opción -g para aumentar el tamaño del archivo para admitir payloads más grandes para que los usuarios no se preocupen por un cambio en el tamaño del archivo (por ejemplo, si Stegosaurus se inyecta en un pipeline)
Contribuciones

Gracias a S0lll0s por:

     Evitar colocar el payload en largas ejecuciones de opcodes que no tengan un argumento, ya que esto puede llevar a la exposición del payload a través de herramientas como strings.

Contacto

Para cualquier pregunta, contactar con el autor:

Jon Herron
 

jon dot herron at yahoo.com

Fuente: https://bitbucket.org/jherron/stegosaurus/src

Comentarios

  1. IM-PREZIONANTE
    ¿Habéis probado si es compatible con pyinstaller o py2exe?

    ResponderEliminar
    Respuestas
    1. Yo lo probé, y se pierde el payload al convertirlo a exe..

      Eliminar

Publicar un comentario