TL; DR. El artículo habla sobre el desarrollo inverso del cliente de Dropbox, pirateando los mecanismos de ofuscación y descompilación del cliente en Python, así como cambiando el programa para activar las funciones de depuración que están ocultas en modo normal. Si solo está interesado en el código y las instrucciones correspondientes, desplácese hasta el final. Al momento de escribir esto, el código es compatible con las últimas versiones de Dropbox basadas en el intérprete CPython 3.6.Introduccion
Dropbox me ha fascinado de inmediato. El concepto sigue siendo engañosamente simple. Aquí está la carpeta. Poner archivos allí. Está sincronizado Yendo a otro dispositivo. Se sincroniza nuevamente. ¡La carpeta y los archivos ahora aparecieron allí también!
La cantidad de trabajo de fondo oculto es realmente sorprendente. En primer lugar, todos los problemas con los que tiene que lidiar al crear y mantener una aplicación multiplataforma para los principales sistemas operativos de escritorio (OS X, Linux, Windows) no desaparecen. Agregue a esto el soporte de varios navegadores web, varios sistemas operativos móviles. Y solo estamos hablando del lado del cliente. También estoy interesado en el backend de Dropbox, que me permitió lograr tal escalabilidad y baja latencia con la carga de trabajo increíblemente pesada creada por medio billón de usuarios.
Es por estas razones que siempre me ha gustado ver lo que hace Dropbox bajo el capó y cómo ha evolucionado a lo largo de los años. Hace unos ocho años, intenté por primera vez averiguar cómo funciona realmente el cliente de Dropbox cuando noté la transmisión de tráfico desconocido mientras estaba en el hotel. La investigación mostró que esto es parte de la función de Dropbox llamada LanSync, que le permite sincronizar más rápido si los hosts de Dropbox en la misma LAN tienen acceso a los mismos archivos. Sin embargo, el protocolo no estaba documentado y quería saber más. Por lo tanto, decidí echar un vistazo con más detalle y, como resultado, realicé ingeniería inversa de casi todo el programa. Este estudio nunca fue publicado, aunque a veces compartí notas con algunas personas.
Cuando abrimos Anvil Ventures, Chris y yo apreciamos una serie de herramientas para el almacenamiento, el intercambio y la colaboración de documentos. Uno de ellos, obviamente, era Dropbox, y para mí esta es otra razón para desenterrar estudios antiguos y verificarlos en la versión actual del cliente.
Descifrado y desofuscación
Primero, descargué el cliente para Linux y descubrí rápidamente que estaba escrito en Python. Debido a que la licencia de Python es bastante permisiva, es fácil para las personas modificar y distribuir el intérprete de Python junto con otras dependencias como el software comercial. Luego comencé la ingeniería inversa para entender cómo funciona el cliente.
En ese momento, los archivos con el código de bytes estaban en un archivo ZIP combinado con un binario ejecutable. El binario principal era solo un intérprete de Python modificado que se cargaba capturando mecanismos de importación de Python. Cada llamada de importación posterior se redirigió a este binario con un análisis de archivos ZIP. Por supuesto, es fácil extraer este ZIP del binario. Por ejemplo, la útil herramienta
binwalk la recupera con todos los archivos .pyc compilados en bytes.
Entonces no pude romper el cifrado de los archivos .pyc, pero al final tomé el objeto general de la biblioteca estándar de Python y lo volví a compilar, inyectando una puerta trasera dentro. Ahora que el cliente de Dropbox estaba cargando este objeto, podía ejecutar fácilmente código arbitrario de Python en un intérprete que funcionara. Aunque descubrí esto por mi cuenta, Florian Leda y Nicolas Raff utilizaron el mismo método en una
presentación en Hack.lu en 2012.
La capacidad de explorar y manipular código en ejecución en Dropbox ha revelado mucho. El código utilizó varios trucos de protección para dificultar el volcado de
objetos de código . Por ejemplo, en un intérprete de CPython normal, es fácil recuperar un código de bytes compilado que representa una función. Un simple ejemplo:
>>> def f(i=0): ... return i * i ... >>> f.__code__ <code object f at 0x109deb540, file "<stdin>", line 1> >>> f.__code__.co_code b'|\x00|\x00\x14\x00S\x00' >>> import dis >>> dis.dis(f) 2 0 LOAD_FAST 0 (i) 2 LOAD_FAST 0 (i) 4 BINARY_MULTIPLY 6 RETURN_VALUE >>>
Pero en la versión compilada de
Objects / codeobject.c, la propiedad
co_code co_code
eliminó de la lista abierta. Esta lista de miembros generalmente se ve así:
static PyMemberDef code_memberlist[] = { ... {"co_flags", T_INT, OFF(co_flags), READONLY}, {"co_code", T_OBJECT, OFF(co_code), READONLY}, {"co_consts", T_OBJECT, OFF(co_consts), READONLY}, ... };
La desaparición de la propiedad
co_code
hace que sea imposible volcar estos objetos de código.
Además, se han eliminado otras bibliotecas, como el
desensamblador Python estándar. Al final, todavía logré volcar los objetos de código en archivos, pero aún así no pude descompilarlos. Me llevó un tiempo darme cuenta de que los códigos de operación utilizados por el intérprete de Dropbox no coinciden con los códigos de operación estándar de Python. Por lo tanto, era necesario comprender los nuevos códigos de operación para volver a escribir los objetos de código en el bytecode original de Python.
Una opción es la reasignación de opcode. Hasta donde yo sé, esta técnica fue desarrollada por Rich Smith y presentada en
Defcon 18 . En esa charla, también mostró la herramienta
pyREtic para el
código de bytes Python de ingeniería inversa en la memoria. Parece que el código pyREtic es poco compatible y la herramienta está dirigida a los binarios "viejos" de Python 2.x. Para familiarizarse con las técnicas que se le ocurrió a Rich, es muy recomendable ver su actuación.
El método de traducción opcode toma todos los objetos de código de la biblioteca estándar de Python y los compara con los objetos extraídos del binario de Dropbox. Por ejemplo, codifique objetos de
hashlib.pyc o
socket.pyc , que están en la biblioteca estándar. Digamos, si cada vez que el
0x43
operación
0x43
corresponde al código de operación 0x21 desofuscado, podemos construir gradualmente una tabla de traducción para reescribir objetos de código. Estos objetos de código se pueden pasar a través del descompilador de Python. Para volcar, aún necesita un intérprete corregido con el objeto
co_code
correcto.
Otra opción es hackear el formato de serialización. En Python, la serialización se llama
cálculo de referencias . La deserialización de archivos ofuscados de la manera habitual no funcionó. Al realizar ingeniería inversa del binario en IDA Pro, descubrí el paso de descifrado. Hasta donde yo sé, el primero en publicar públicamente algo sobre este tema fue Hagen Fritch en
su blog . Allí se refiere a los cambios en las nuevas versiones de Dropbox (cuando Dropbox cambió de Python 2.5 a Python 2.7). El algoritmo funciona de la siguiente manera:
- Al desempacar un archivo pyc, se lee un encabezado para determinar la versión del cálculo de referencias. Este formato no está documentado, excepto por la implementación de CPython.
- El formato define una lista de tipos que están codificados en él. Tipos
True
, False
, floats
, etc., pero el más importante es el tipo para los code object
Python anteriores, code object
.
- Al cargar el
code object
, primero se leen dos valores adicionales del archivo de entrada.
- El primero es el valor
random
32 bits.
- El segundo es un valor de
length
32 bits que indica el tamaño del objeto de código serializado.
- Luego, los valores de
rand
y length
se rand
a una función RNG simple que genera seed
.
- Esta semilla se entrega al vórtice de Mersenne , que genera cuatro valores de 32 bits.
- Combinados, estos cuatro valores proporcionan una clave de cifrado para los datos serializados. El algoritmo de cifrado descifra los datos usando el Algoritmo de cifrado minúsculo .
En mi código, escribí el procedimiento de desarme Python desde cero. La parte que descifra los objetos de código se parece al fragmento a continuación. Cabe señalar que este método deberá llamarse de forma recursiva. El objeto de nivel superior para un archivo pyc es un objeto de código que contiene objetos de código, que pueden ser clases, funciones o lambdas. A su vez, también pueden contener métodos, funciones o lambdas. ¡Todos estos son objetos de código de la jerarquía!
def load_code(self): rand = self.r_long() length = self.r_long() seed = rng(rand, length) mt = MT19937(seed) key = [] for i in range(0, 4): key.append(mt.extract_number())
La capacidad de descifrar objetos de código significa que después de deserializar los procedimientos, debe volver a escribir el código de bytes real. Los objetos de código contienen información sobre números de línea, constantes y otra información. El bytecode real está en el objeto
co_code
. Cuando creamos la tabla de traducción de opcode, simplemente podemos reemplazar los valores ofuscados de Dropbox con los equivalentes estándar de Python 3.6.
Ahora los objetos de código están en el formato habitual de Python 3.6, y se pueden pasar al descompilador. La calidad de los descompiladores de Python ha crecido significativamente gracias al proyecto
incompleto de R. Bernstein6. La descompilación dio un resultado bastante bueno, y pude armar todo en una herramienta que descompila la versión actual del cliente de Dropbox de la mejor manera posible.
Si clonas este
repositorio y sigues las instrucciones, el resultado será algo como esto:
...
__main__ - INFO - Descompilado exitosamente dropbox / client / features / browse_search / __ init __. pyc
__main__ - INFO - Descifrar, parchar y descompilar _bootstrap_overrides.pyc
__main__ - INFO - Descompilado exitosamente _bootstrap_overrides.pyc
__principal__ - INFORMACIÓN - Procesó 3713 archivos (3591 se descompiló con éxito, 122 fallaron)
opcodemap - ADVERTENCIA - NO escribir mapa de opcode ya que forzar sobrescritura no está establecida
Esto significa que ahora tiene un directorio
out/
con una versión descompilada del código fuente de Dropbox.
Habilitar el seguimiento de Dropbox
En el código abierto, comencé a buscar algo interesante, y el siguiente fragmento me llamó la atención. Los controladores de rastreo en
out/dropbox/client/high_trace.py
se instalan solo si el ensamblaje no está congelado o la clave mágica o cookie que restringe la funcionalidad no está configurada en la línea
1430
.
1424 def install_global_trace_handlers(flags=None, args=None): 1425 global _tracing_initialized 1426 if _tracing_initialized: 1427 TRACE('!! Already enabled tracing system') 1428 return 1429 _tracing_initialized = True 1430 if not build_number.is_frozen() or magic_trace_key_is_set() or limited_support_cookie_is_set(): 1431 if not os.getenv('DBNOLOCALTRACE'): 1432 add_trace_handler(db_thread(LtraceThread)().trace) 1433 if os.getenv('DBTRACEFILE'): 1434 pass
La mención de compilaciones congeladas se refiere a las compilaciones de depuración interna de Dropbox. Y un poco más arriba en el mismo archivo puedes encontrar tales líneas:
272 def is_valid_time_limited_cookie(cookie): 273 try: 274 try: 275 t_when = int(cookie[:8], 16) ^ 1686035233 276 except ValueError: 277 return False 278 else: 279 if abs(time.time() - t_when) < SECONDS_PER_DAY * 2 and md5(make_bytes(cookie[:8]) + b'traceme').hexdigest()[:6] == cookie[8:]: 280 return True 281 except Exception: 282 report_exception() 283 284 return False 285 286 287 def limited_support_cookie_is_set(): 288 dbdev = os.getenv('DBDEV') 289 return dbdev is not None and is_valid_time_limited_cookie(dbdev) build_number/environment.py
Como puede ver en el método
limited_support_cookie_is_set
en la línea
287
, el seguimiento solo está habilitado si la variable de entorno denominada
DBDEV
configurada correctamente en cookies con una vida útil limitada. Bueno, eso es interesante! Y ahora sabemos cómo generar cookies de tiempo limitado. A juzgar por el nombre, los ingenieros de Dropbox pueden generar tales cookies y luego habilitar temporalmente el seguimiento en algunos casos cuando es necesario para ayudar a los clientes. Después de reiniciar Dropbox o reiniciar la computadora, incluso si la cookie especificada todavía está en su lugar, caducará automáticamente. Supongo que esto debería evitar, por ejemplo, la degradación del rendimiento debido al seguimiento continuo. También dificulta la ingeniería inversa de Dropbox.
Sin embargo, un pequeño script puede simplemente generar y configurar constantemente estas cookies. Algo como esto:
Luego se crea una cookie basada en el tiempo:
$ python3 setenv.py DBDEV=38b28b3f349714; export DBDEV;
Luego cargue correctamente la salida de este script en el entorno y ejecute el cliente de Dropbox.
$ eval `python3 setenv.py` $ ~/.dropbox-dist/dropbox-lnx_64-71.4.108/dropbox
Esto incluye salida de seguimiento, con formato colorido y todo eso. Se parece a este cliente no registrado:

Implementar nuevo código
Todo esto es un poco divertido. Estudiando más el código descompilado, descubrimos
out/build_number/environment.pyc
. Hay una función que comprueba si una determinada llave mágica está instalada. Esta clave no está codificada en el código, pero se compara con el hash SHA-256. Aquí está el fragmento correspondiente.
1 import hashlib, os 2 from typing import Optional, Text 3 _MAGIC_TRACE_KEY_IS_SET = None 4 5 def magic_trace_key_is_set(): 6 global _MAGIC_TRACE_KEY_IS_SET 7 if _MAGIC_TRACE_KEY_IS_SET is None: 8 dbdev = os.getenv('DBDEV') or '' 9 if isinstance(dbdev, Text): 10 bytes_dbdev = dbdev.encode('ascii') 11 else: 12 bytes_dbdev = dbdev 13 dbdev_hash = hashlib.sha256(bytes_dbdev).hexdigest() 14 _MAGIC_TRACE_KEY_IS_SET = dbdev_hash == 'e27eae61e774b19f4053361e523c771a92e838026da42c60e6b097d9cb2bc825' 15 return _MAGIC_TRACE_KEY_IS_SET
Este método se llama varias veces desde diferentes lugares en el código para verificar si la clave de rastreo mágica está configurada. Traté de descifrar el hash SHA-256 con la fuerza bruta
John the Ripper , pero me llevó demasiado tiempo realizar una búsqueda simple, y no pude reducir la cantidad de opciones porque no había conjeturas sobre el contenido. En Dropbox, los desarrolladores pueden tener una clave de desarrollo específica codificada, que instalan si es necesario, activando el modo "clave mágica" del cliente para el rastreo.
Esto me molestó porque quería encontrar una manera rápida y fácil de lanzar Dropbox con este conjunto de claves para el seguimiento. Así que escribí un procedimiento de cálculo de referencias que genera archivos pyc cifrados de acuerdo con el cifrado de Dropbox. Por lo tanto, pude ingresar mi propio código o simplemente reemplazar el hash anterior. Este código en el repositorio de Github está en el archivo
patchzip.py
. Como resultado, el hash se reemplaza por el hash SHA-256 de
ANVILVENTURES
. Luego, el objeto de código se vuelve a cifrar y se coloca en un archivo zip, donde se almacena todo el código ofuscado. Esto le permite hacer lo siguiente:
$ DBDEV = ANVILVENTURES; exportar DBDEV;
$ ~ / .dropbox-dist / dropbox-lnx_64-71.4.108 / dropbox
Ahora todas las funciones de depuración se muestran al hacer clic con el botón derecho en el icono de Dropbox en la bandeja del sistema.
Al estudiar más a fondo las fuentes descompiladas, en el archivo
dropbox/webdebugger/server.py
descubrí un método llamado
is_enabled
. Parece que está comprobando si habilitar el depurador web incorporado. En primer lugar, comprueba la llave mágica mencionada. Como reemplazamos el hash SHA-256, simplemente podemos establecer el valor en
ANVILVENTURES
. La segunda parte en las líneas
201
y
202
verifica si hay una variable de entorno llamada
DB<x>
que tiene
x
igual al hash SHA-256. El valor del entorno establece cookies de tiempo limitado, como ya hemos visto.
191 @classmethod 192 def is_enabled(cls): 193 if cls._magic_key_set: 194 return cls._magic_key_set 195 else: 196 cls._magic_key_set = False 197 if not magic_trace_key_is_set(): 198 return False 199 for var in os.environ: 200 if var.startswith('DB'): 201 var_hash = hashlib.sha256(make_bytes(var[2:])).hexdigest() 202 if var_hash == '5df50a9c69f00ac71f873d02ff14f3b86e39600312c0b603cbb76b8b8a433d3ff0757214287b25fb01' and is_valid_time_limited_cookie(os.environ[var]): 203 cls._magic_key_set = True 204 return True 205 206 return False
Usando exactamente la misma técnica, reemplazando este hash con el SHA-256 que se usó antes, ahora podemos cambiar el script
setenv
previamente escrito a algo como esto:
$ cat setenv.py … c = generate_time_cookie() output_env("DBDEV", "ANVILVENTURES") output_env("DBANVILVENTURES", c) $ python3 setenv.py DBDEV=ANVILVENTURES; export DBDEV; DBANVILVENTURES=38b285c4034a67; export DBANVILVENTURES $ eval `python3 setenv.py` $ ~/.dropbox-dist/dropbox-lnx_64-71.4.108/dropbox
Como puede ver, después de iniciar el cliente, se abre un nuevo puerto TCP para escuchar. No se abrirá si las variables de entorno no están configuradas correctamente.
$ netstat --tcp -lnp | grep dropbox
tcp 0 0 127.0.0.1:4242 0.0.0.0:* ESCUCHE 1517 / dropbox
Más adelante en el código, puede encontrar la interfaz de WebSocket en el archivo
webpdb.pyc
. Este es un contenedor para las utilidades estándar de Python
pdb . El acceso a la interfaz es a través de un servidor HTTP en este puerto.
Instalemos el cliente websocket y
probémoslo :
$ websocat -t ws: //127.0.0.1: 4242 / pdb
--Volver--
> /home/gvb/dropbox/webdebugger/webpdb.pyc(101)run()->Ninguno
>
(Pdb) de build_number.environment import magic_trace_key_is_set como ms
(Pdb) ms ()
Cierto
Por lo tanto, ahora tenemos un depurador completo en el cliente, que en todos los demás aspectos funciona como antes. Podemos ejecutar código arbitrario de Python, pudimos habilitar el menú interno de depuración y las funciones de rastreo. Todo esto será de gran ayuda en el análisis posterior del cliente de Dropbox.
Conclusión
Pudimos realizar ingeniería inversa en Dropbox, escribir herramientas de descifrado e inyección de código que funcionan con clientes actuales de Dropbox basados en Python 3.6. Realizamos ingeniería inversa de las funciones ocultas individuales y las activamos. Obviamente, el depurador realmente ayudará en la piratería adicional. Especialmente con una serie de archivos que no se pueden descompilar con éxito debido a las desventajas de descompyle6.
Código
El código se puede encontrar
en Github . Instrucciones de uso allí. Este repositorio también contiene mi antiguo código escrito en 2011. Debería funcionar con solo unas pocas modificaciones, siempre que alguien tenga versiones anteriores de Dropbox basadas en Python 2.7.
El repositorio también contiene scripts para transmitir códigos de operación, instrucciones para configurar las variables de entorno de Dropbox y todo lo necesario para cambiar el archivo zip.
Agradecimientos
Gracias a Brian de Anvil Ventures por revisar mi código. El trabajo en este código continuó durante varios años, de vez en cuando lo actualicé, introduje nuevos métodos y reescribí fragmentos para restaurarlo para que funcione en nuevas versiones de Dropbox.
Como se mencionó anteriormente, un gran punto de partida para las aplicaciones de Python de ingeniería inversa es el trabajo de Rich Smith, Florian Ledoux y Nicolas Raff, así como Hagen Fritch. Especialmente su trabajo es relevante para el desarrollo inverso de una de las aplicaciones de Python más grandes del mundo: el cliente de Dropbox.
Cabe señalar que la descompilación del código de Python ha avanzado mucho gracias al proyecto de Unpypy6 liderado por R. Bernstein. Este descompilador ha compilado y mejorado muchos descompiladores de Python diferentes.
También muchas gracias a los colegas Brian, Austin, Stefan y Chris por revisar este artículo.