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
Ejemplo
Supongamos que queremos incrustar un payload en el bytecode del siguiente script en Python, denominado example.py:
El primer paso es usar Stegosaurus para ver cuántos bytes puede contener nuestro payload sin cambiar el tamaño del archivo carrier.
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.
Al mirar en el disco, tanto el carrier como el bytecode original tienen el mismo tamaño:
Nota: Si se omite la opción -s, se sobrescribe el bytecode original.
El payload puede extraerse pasando la opción -x a Stegosaurus:
El payload no tiene porque ser una cadena de caracteres en ascii, también son compatibles shellcodes:
Para mostrar que el comportamiento del código de Python en tiempo de ejecución permanece después de que Stegosaurus incruste el payload:
Salida de strings después de que Stegosaurus incruste el payload (observa que el payload no se muestra):
Ejemplo de salida del módulo dis de Python, que no muestra diferencias antes y después de que Stegosaurus incorpore su payload:
Antes:
Después:
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:
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:
Para ver un ejemplo de los cambios en el bytecode, echa un vistazo al siguiente fragmento de Python:
Usando dis con Python inferior a 3 .6:
Sin embargo, con Python 3.6: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
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
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)
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
IM-PREZIONANTE
ResponderEliminar¿Habéis probado si es compatible con pyinstaller o py2exe?
Yo lo probé, y se pierde el payload al convertirlo a exe..
Eliminar