Ingénierie inverse du client Dropbox

TL; DR. L'article parle du développement inverse du client Dropbox, du piratage des mécanismes d'obscurcissement et de décompilation du client en Python, ainsi que de la modification du programme pour activer les fonctions de débogage masquées en mode normal. Si vous n'êtes intéressé que par le code et les instructions appropriés, faites défiler jusqu'à la fin. Au moment d'écrire ces lignes, le code est compatible avec les dernières versions de Dropbox basées sur l'interpréteur CPython 3.6.

Présentation


Dropbox m'a tout de suite fasciné. Le concept est encore d'une simplicité trompeuse. Voici le dossier. Mettez-y des fichiers. Il est synchronisé. Aller sur un autre appareil. Il est à nouveau synchronisé. Le dossier et les fichiers y sont également apparus!

La quantité de travail d'arrière-plan caché est vraiment incroyable. Tout d'abord, tous les problèmes que vous devez gérer lors de la création et de la maintenance d'une application multiplateforme pour les principaux systèmes d'exploitation de bureau (OS X, Linux, Windows) ne disparaissent pas. Ajoutez à cela la prise en charge de divers navigateurs Web, de divers systèmes d'exploitation mobiles. Et nous ne parlons que du côté client. Je suis également intéressé par le backend Dropbox, qui m'a permis d'atteindre une telle évolutivité et une faible latence avec la charge de travail incroyablement lourde créée par un demi-milliard d'utilisateurs.

C'est pour ces raisons que j'ai toujours aimé regarder ce que Dropbox fait sous le capot et comment il a évolué au fil des ans. Il y a environ huit ans, j'ai d'abord essayé de savoir comment fonctionnait le client Dropbox lorsque j'ai remarqué la diffusion d'un trafic inconnu à l'hôtel. L'enquête a montré que cela faisait partie de la fonctionnalité Dropbox appelée LanSync, qui vous permet de synchroniser plus rapidement si les hôtes Dropbox sur le même LAN ont accès aux mêmes fichiers. Cependant, le protocole n'était pas documenté et je voulais en savoir plus. Par conséquent, j'ai décidé de jeter un coup d'œil plus en détail et, par conséquent, j'ai effectué l'ingénierie inverse de presque tout le programme. Cette étude n'a jamais été publiée, même si j'ai parfois partagé des notes avec certaines personnes.

Lorsque nous avons ouvert Anvil Ventures, Chris et moi avons apprécié un certain nombre d'outils pour le stockage, le partage et la collaboration de documents. Évidemment, l'un d'eux était Dropbox, et pour moi, c'est une autre raison de déterrer les anciennes études et de les vérifier sur la version actuelle du client.

Décryptage et désobfuscation


Tout d'abord, j'ai téléchargé le client pour Linux et j'ai rapidement découvert qu'il était écrit en Python. Étant donné que la licence Python est assez permissive, il est facile pour les gens de modifier et de distribuer l'interpréteur Python avec d'autres dépendances comme les logiciels commerciaux. J'ai ensuite commencé l'ingénierie inverse pour comprendre comment fonctionne le client.

A cette époque, les fichiers avec le bytecode étaient dans un fichier ZIP combiné à un binaire exécutable. Le binaire principal était juste un interpréteur Python modifié qui a été chargé en capturant les mécanismes d'importation Python. Chaque appel d'importation suivant a été redirigé vers ce binaire avec une analyse de fichier ZIP. Bien sûr, il est facile d'extraire ce ZIP du binaire. Par exemple, l'outil binwalk utile le récupère avec tous les fichiers .pyc compilés en octets.

Ensuite, je n'ai pas pu casser le cryptage des fichiers .pyc, mais à la fin j'ai pris l'objet général de la bibliothèque Python standard et l'ai recompilé, en injectant une porte dérobée à l'intérieur. Maintenant que le client Dropbox chargeait cet objet, je pouvais facilement exécuter du code Python arbitraire dans un interpréteur fonctionnel. Bien que je l'ai découvert par moi-même, Florian Leda et Nicolas Raff ont utilisé la même méthode lors d'une présentation sur Hack.lu en 2012.

La possibilité d'explorer et de manipuler du code en cours d'exécution dans Dropbox a beaucoup révélé. Le code a utilisé plusieurs astuces de protection pour rendre difficile le vidage d' objets de code . Par exemple, dans un interpréteur CPython standard, il est facile de récupérer un bytecode compilé représentant une fonction. Un exemple simple:

>>> 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 >>> 

Mais dans la version compilée d' Objects / codeobject.c, la propriété co_code a co_code supprimée de la liste ouverte. Cette liste de membres ressemble généralement à ceci:

  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 disparition de la propriété co_code rend impossible le vidage de ces objets de code.

De plus, d'autres bibliothèques, comme le désassembleur Python standard, ont été supprimées. Au final, j'ai quand même réussi à vider les objets de code dans des fichiers, mais je n'ai toujours pas pu les décompiler. Il m'a fallu un certain temps avant de réaliser que les opcodes utilisés par l'interpréteur Dropbox ne correspondent pas aux opcodes standard de Python. Ainsi, il était nécessaire de comprendre les nouveaux opcodes afin de réécrire les objets de code dans le bytecode Python d'origine.

Une option est le remappage opcode. Pour autant que je sache, cette technique a été développée par Rich Smith et introduite au Defcon 18 . Dans cette conférence, il a également montré l'outil pyREtic pour la rétro-ingénierie du bytecode Python en mémoire. Il semble que le code pyREtic soit mal supporté, et l'outil cible les "anciens" binaires Python 2.x. Pour se familiariser avec les techniques développées par Rich, il est fortement recommandé de regarder sa performance.

La méthode de traduction opcode prend tous les objets de code de la bibliothèque Python standard et les compare aux objets extraits du binaire Dropbox. Par exemple, codez les objets de hashlib.pyc ou socket.pyc , qui se trouvent dans la bibliothèque standard. Disons que si chaque fois que l'opcode 0x43 correspond à l' 0x21 0x21 désobfusqué, nous pouvons progressivement construire une table de traduction pour réécrire les objets de code. Ces objets de code peuvent ensuite être transmis via le décompilateur Python. Pour vider, vous avez toujours besoin d'un interpréteur corrigé avec le bon objet co_code .

Une autre option consiste à pirater le format de sérialisation. En Python, la sérialisation est appelée marshaling . La désérialisation des fichiers obscurcis de la manière habituelle n'a pas fonctionné. Lors de l'ingénierie inverse du binaire dans IDA Pro, j'ai découvert l'étape de décryptage. Pour autant que je sache, le premier à publier publiquement quelque chose à ce sujet a été Hagen Fritch sur son blog . Là, il fait référence aux changements dans les nouvelles versions de Dropbox (lorsque Dropbox est passé de Python 2.5 à Python 2.7). L'algorithme fonctionne comme suit:

  • Lors du déballage d'un fichier pyc, un en-tête est lu pour déterminer la version du marshaling. Ce format n'est pas documenté, à l'exception de l'implémentation de CPython lui-même.
  • Le format définit une liste de types qui y sont encodés. Types True , False , floats , etc., mais le plus important est le type des code object Python ci-dessus, code object .
  • Lors du chargement de l' code object , deux valeurs supplémentaires sont d'abord lues dans le fichier d'entrée.
  • Le premier est la valeur random 32 bits.
  • Le second est une valeur de length 32 bits indiquant la taille de l'objet de code sérialisé.
  • Ensuite, les valeurs de rand et de length sont rand à une simple fonction RNG qui génère des seed .
  • Cette graine est livrée au vortex de Mersenne , qui génère quatre valeurs 32 bits.
  • Combinées ensemble, ces quatre valeurs fournissent une clé de chiffrement pour les données sérialisées. L'algorithme de chiffrement déchiffre ensuite les données à l'aide de l' algorithme de chiffrement minuscule .

Dans mon code, j'ai écrit la procédure de démasquage Python à partir de zéro. La partie qui déchiffre les objets de code ressemble à quelque chose comme le fragment ci-dessous. Il est à noter que cette méthode devra être appelée récursivement. L'objet de niveau supérieur pour un fichier pyc est un objet de code qui contient lui-même des objets de code, qui peuvent être des classes, des fonctions ou des lambdas. À leur tour, ils peuvent également contenir des méthodes, des fonctions ou des lambdas. Ce sont tous des objets de code dans la hiérarchie!

  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()) # take care of padding for size calculation sz = (length + 15) & ~0xf words = sz / 4 # convert data to list of dwords buf = self._read(sz) data = list(struct.unpack("<%dL" % words, buf)) # decrypt and convert back to stream of bytes data = tea.tea_decipher(data, key) data = struct.pack("<%dL" % words, *data) 

La possibilité de déchiffrer les objets de code signifie qu'après désérialisation des procédures, vous devez réécrire le code d'octets réel. Les objets de code contiennent des informations sur les numéros de ligne, les constantes et d'autres informations. Le bytecode réel se trouve dans l'objet co_code . Lorsque nous avons créé la table de traduction d'opcode, nous pouvons simplement remplacer les valeurs Dropbox masquées par les équivalents Python 3.6 standard.

Maintenant, les objets de code sont au format Python 3.6 habituel, et ils peuvent être passés au décompilateur. La qualité des décompilateurs Python a considérablement augmenté grâce au projet uncompyle6 de R. Bernstein. La décompilation a donné un assez bon résultat, et j'ai pu tout rassembler dans un outil qui décompile la version actuelle du client Dropbox au mieux de ses capacités.

Si vous clonez ce référentiel et suivez les instructions, le résultat sera quelque chose comme ceci:

  ...
     __main__ - INFO - Dropbox / client / features / Browse_search / __ init __. pyc décompilé avec succès
     __main__ - INFO - Déchiffrement, correction et décompilation _bootstrap_overrides.pyc
     __main__ - INFO - Décompilation réussie de _bootstrap_overrides.pyc
     __main__ - INFO - 3713 fichiers traités (3591 décompilés avec succès, 122 échecs)
     opcodemap - AVERTISSEMENT - NE PAS écrire de carte d'opcode car l'écrasement forcé n'est pas défini 

Cela signifie que vous avez maintenant un répertoire out/ avec une version décompilée du code source Dropbox.

Activation du suivi Dropbox


Dans l'open source, j'ai commencé à chercher quelque chose d'intéressant, et le fragment suivant a attiré mon attention. Les gestionnaires de trace dans out/dropbox/client/high_trace.py sont installés uniquement si l'assembly n'est pas figé ou si la clé magique ou le cookie limitant la fonctionnalité n'est pas défini à la ligne 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 mention de versions figées fait référence aux versions de débogage interne de Dropbox. Et un peu plus haut dans le même fichier, vous pouvez trouver de telles lignes:

  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 

Comme vous pouvez le voir dans la méthode limited_support_cookie_is_set à la ligne 287 , le traçage n'est activé que si la variable d'environnement nommée DBDEV correctement définie sur les cookies avec une durée de vie limitée. Et bien c'est intéressant! Et maintenant, nous savons comment générer de tels cookies à durée limitée. À en juger par le nom, les ingénieurs de Dropbox peuvent générer de tels cookies, puis activer temporairement le traçage dans certains cas, lorsque cela est nécessaire pour prendre en charge les clients. Après avoir redémarré Dropbox ou redémarré l'ordinateur, même si le cookie spécifié est toujours en place, il expirera automatiquement. Je suppose que cela devrait empêcher, par exemple, la dégradation des performances due au suivi continu. Cela rend également difficile l'ingénierie inverse de Dropbox.

Cependant, un petit script peut simplement générer et définir constamment ces cookies. Quelque chose comme ça:

  #!/usr/bin/env python3 def output_env(name, value): print("%s=%s; export %s" % (name, value, name)) def generate_time_cookie(): t = int(time.time()) c = 1686035233 s = "%.8x" % (t ^ c) h = md5(s.encode("utf-8?") + b"traceme").hexdigest() ret = "%s%s" % (s, h[:6]) return ret c = generate_time_cookie() output_env("DBDEV", c) 

Ensuite, un cookie temporel est créé:

  $ python3 setenv.py DBDEV=38b28b3f349714; export DBDEV; 

Chargez ensuite correctement la sortie de ce script dans l'environnement et exécutez le client Dropbox.

  $ eval `python3 setenv.py` $ ~/.dropbox-dist/dropbox-lnx_64-71.4.108/dropbox 

Cela inclut la sortie de trace, avec un formatage coloré et tout ça. Il ressemble à ce client non enregistré:



Implémenter un nouveau code


Tout ça est un peu drôle. En étudiant le code décompilé plus loin, nous découvrons out/build_number/environment.pyc . Il existe une fonction qui vérifie si une certaine clé magique est installée. Cette clé n'est pas codée en dur dans le code, mais est comparée au hachage SHA-256. Voici l'extrait correspondant.

  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 

Cette méthode est appelée plusieurs fois à différents endroits du code pour vérifier si la clé de trace magique est définie. J'ai essayé de casser le hachage SHA-256 avec la force brute de John the Ripper , mais une force brute simple a pris trop de temps, et je n'ai pas pu réduire le nombre d'options car il n'y avait aucune supposition sur le contenu. Dans Dropbox, les développeurs peuvent avoir une clé de développement codée en dur spécifique, qu'ils installent si nécessaire, activant le mode «clé magique» du client pour le traçage.

Cela m'a ennuyé parce que je voulais trouver un moyen rapide et facile de lancer Dropbox avec cet ensemble de clés pour le traçage. J'ai donc écrit une procédure de marshaling qui génère des fichiers pyc cryptés selon le cryptage Dropbox. Ainsi, j'ai pu entrer mon propre code ou simplement remplacer le hachage ci-dessus. Ce code dans le référentiel Github se trouve dans le fichier patchzip.py . Par conséquent, le hachage est remplacé par le hachage SHA-256 d' ANVILVENTURES . Ensuite, l'objet code est rechiffré et placé dans un zip, où tout le code obscurci est stocké. Cela vous permet d'effectuer les opérations suivantes:

  $ DBDEV = ANVILVENTURES;  export DBDEV;
     $ ~ / .dropbox-dist / dropbox-lnx_64-71.4.108 / dropbox 

Toutes les fonctions de débogage s'affichent désormais lorsque vous cliquez avec le bouton droit sur l'icône Dropbox dans la barre d'état système.



En étudiant plus en détail les sources décompilées, dans le fichier dropbox/webdebugger/server.py j'ai découvert une méthode appelée is_enabled . Il semble qu'il vérifie s'il faut activer le débogueur Web intégré. Tout d'abord, il vérifie la clé magique mentionnée. Puisque nous avons remplacé le hachage SHA-256, nous pouvons simplement définir la valeur sur ANVILVENTURES . La deuxième partie des lignes 201 et 202 vérifie s'il existe une variable d'environnement nommée DB<x> qui a x égal au hachage SHA-256. La valeur de l'environnement définit des cookies à durée limitée, comme nous l'avons déjà vu.

  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 

En utilisant exactement la même technique, en remplaçant ce hachage par le SHA-256 qui était utilisé auparavant, nous pouvons maintenant changer le script setenv précédemment écrit en quelque chose comme ceci:

  $ 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 

Comme vous pouvez le voir, après le démarrage du client, un nouveau port TCP s'ouvre pour l'écoute. Il ne s'ouvrira pas si les variables d'environnement ne sont pas définies correctement.

  $ netstat --tcp -lnp |  grep dropbox
     tcp 0 0127.0.0.1:4242 0.0.0.0:* LISTEN 1517 / dropbox 

Plus loin dans le code, vous pouvez trouver l'interface WebSocket dans le fichier webpdb.pyc . Il s'agit d'un wrapper pour les utilitaires pdb python standard. L'accès à l'interface se fait via un serveur HTTP sur ce port. Installons le client websocket et essayons -le:

  $ websocat -t ws: //127.0.0.1: 4242 / pdb
     --Retour--
    
     > /home/gvb/dropbox/webdebugger/webpdb.pyc(101)run()->Aucun
     >
     (Pdb) depuis build_number.environment import magic_trace_key_is_set as ms
     (Pdb) ms ()
     Vrai 

Ainsi, nous avons maintenant un débogueur à part entière dans le client, qui à tous les autres égards fonctionne comme avant. Nous pouvons exécuter du code Python arbitraire, nous avons pu activer le menu de débogage interne et les fonctions de trace. Tout cela contribuera grandement à une analyse plus approfondie du client Dropbox.

Conclusion


Nous avons pu effectuer une rétro-ingénierie de Dropbox, écrire des outils de décryptage et d'injection de code qui fonctionnent avec les clients Dropbox actuels basés sur Python 3.6. Nous avons procédé à une ingénierie inverse des fonctions cachées individuelles et les avons activées. De toute évidence, le débogueur aidera vraiment à pirater davantage. Surtout avec un certain nombre de fichiers qui ne peuvent pas être décompilés avec succès en raison des inconvénients de decompyle6.

Code


Le code peut être trouvé sur Github . Instructions d'utilisation là-bas. Ce référentiel contient également mon ancien code écrit en 2011. Cela devrait fonctionner avec seulement quelques modifications, à condition que quelqu'un ait des versions plus anciennes de Dropbox basées sur Python 2.7.

Le référentiel contient également des scripts pour diffuser des opcodes, des instructions pour définir les variables d'environnement Dropbox et tout le nécessaire pour modifier un fichier zip.

Remerciements


Merci à Brian d'Anvil Ventures d'avoir révisé mon code. Le travail sur ce code s'est poursuivi pendant plusieurs années, de temps en temps je le mettais à jour, introduisais de nouvelles méthodes et réécrivais des fragments pour le restaurer pour qu'il fonctionne sur les nouvelles versions de Dropbox.

Comme mentionné précédemment, les travaux de Rich Smith, Florian Ledoux et Nicolas Raff, ainsi que Hagen Fritch, constituent un excellent point de départ pour les applications Python de rétro-ingénierie. Leur travail est particulièrement pertinent pour le développement inverse de l'une des plus grandes applications Python au monde - le client Dropbox.

Il est à noter que la décompilation du code Python a considérablement progressé grâce au projet uncompyle6 dirigé par R. Bernstein. Ce décompilateur a compilé et amélioré de nombreux décompilateurs Python différents.

Merci également à mes collègues Brian, Austin, Stefan et Chris d'avoir relu cet article.

Source: https://habr.com/ru/post/fr452276/


All Articles