Engenharia reversa do cliente do Dropbox

TL; DR. O artigo fala sobre o desenvolvimento reverso do cliente Dropbox, invadindo os mecanismos de ofuscação e descompilação do cliente em Python, além de alterar o programa para ativar as funções de depuração ocultas no modo normal. Se você estiver interessado apenas no código e nas instruções apropriadas, role até o final. No momento da redação deste texto, o código é compatível com as versões mais recentes do Dropbox, baseadas no interpretador CPython 3.6.

1. Introdução


O Dropbox me fascinou imediatamente. O conceito ainda é enganosamente simples. Aqui está a pasta Coloque arquivos lá. Está sincronizado. Indo para outro dispositivo. É sincronizado novamente. Pasta e arquivos agora apareceram lá também!

A quantidade de trabalho oculto em segundo plano é realmente incrível. Primeiro, todos os problemas com os quais você precisa lidar ao criar e manter um aplicativo de plataforma cruzada para os principais sistemas operacionais de desktop (OS X, Linux, Windows) não desaparecem. Adicione a isso o suporte de vários navegadores da web, vários sistemas operacionais móveis. E estamos falando apenas do lado do cliente. Também estou interessado no back-end do Dropbox, que me permitiu alcançar tal escalabilidade e baixa latência com a carga de trabalho incrivelmente pesada criada por meio bilhão de usuários.

É por esses motivos que eu sempre gostei de ver o que o Dropbox faz sob o capô e como ele evoluiu ao longo dos anos. Há cerca de oito anos, tentei descobrir como o cliente Dropbox realmente funciona quando notei a transmissão de tráfego desconhecido enquanto estava no hotel. A investigação mostrou que isso faz parte do recurso do Dropbox chamado LanSync, que permite sincronizar mais rapidamente se os hosts do Dropbox na mesma LAN tiverem acesso aos mesmos arquivos. No entanto, o protocolo não foi documentado e eu queria saber mais. Portanto, decidi dar uma olhada em mais detalhes e, como resultado, conduzi a engenharia reversa de quase todo o programa. Este estudo nunca foi publicado, embora às vezes eu compartilhasse notas com algumas pessoas.

Quando abrimos a Anvil Ventures, Chris e eu apreciamos várias ferramentas para armazenamento, compartilhamento e colaboração de documentos. Um deles, obviamente, era o Dropbox, e para mim esse é outro motivo para desenterrar estudos antigos e verificá-los na versão atual do cliente.

Descriptografia e desofuscação


Primeiro, baixei o cliente para Linux e descobri rapidamente que ele estava escrito em Python. Como a licença do Python é bastante permissiva, é fácil para as pessoas modificar e distribuir o intérprete do Python junto com outras dependências, como software comercial. Então comecei a engenharia reversa para entender como o cliente funciona.

Naquela época, os arquivos de bytecode estavam em um arquivo ZIP combinado com um binário executável. O binário principal era apenas um interpretador Python modificado que era carregado capturando mecanismos de importação do Python. Cada chamada de importação subsequente foi redirecionada para este binário com uma análise de arquivo ZIP. Obviamente, é fácil extrair esse ZIP do binário. Por exemplo, a ferramenta binwalk útil a recupera com todos os arquivos .pyc compilados por bytes.

Então não pude quebrar a criptografia dos arquivos .pyc, mas no final peguei o objeto geral da biblioteca Python padrão e o recompilei, injetando um backdoor dentro. Agora que o cliente Dropbox estava carregando esse objeto, eu poderia facilmente executar código Python arbitrário em um intérprete ativo. Embora eu tenha descoberto isso sozinho, o mesmo método foi usado por Florian Leda e Nicolas Raff em uma apresentação no Hack.lu em 2012.

A capacidade de explorar e manipular o código em execução no Dropbox revelou muito. O código usou vários truques de proteção para dificultar o despejo de objetos de código . Por exemplo, em um intérprete CPython comum, é fácil recuperar um bytecode compilado que representa uma função. Um exemplo simples:

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

Mas na versão compilada de Objects / codeobject.c, a propriedade co_code co_code removida da lista aberta. Esta lista de membros geralmente se parece com isso:

  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}, ... }; 

O desaparecimento da propriedade co_code torna impossível despejar esses objetos de código.

Além disso, outras bibliotecas, como o desmontador padrão do Python, foram removidas. No final, ainda consegui despejar os objetos de código em arquivos, mas ainda não consegui descompilá-los. Demorou um pouco para eu perceber que os opcodes usados ​​pelo interpretador do Dropbox não correspondem aos opcodes padrão do Python. Portanto, era necessário entender os novos opcodes para reescrever os objetos de código de volta no bytecode original do Python.

Uma opção é o remapeamento do código de operação. Até onde eu sei, essa técnica foi desenvolvida por Rich Smith e introduzida na Defcon 18 . Nessa palestra, ele também mostrou a ferramenta pyREtic para engenharia reversa do bytecode Python na memória. Parece que o código pyREtic é pouco suportado e a ferramenta tem como alvo os binários "antigos" do Python 2.x. Para se familiarizar com as técnicas criadas por Rich, é altamente recomendável assistir a seu desempenho.

O método de conversão de opcode pega todos os objetos de código da biblioteca Python padrão e os compara com os objetos extraídos do binário do Dropbox. Por exemplo, objetos de código de hashlib.pyc ou socket.pyc , que estão na biblioteca padrão. Digamos, se toda vez que o código de operação 0x43 corresponder ao código de operação 0x43 , podemos construir gradualmente uma tabela de conversão para reescrever objetos de código. Esses objetos de código podem ser passados ​​pelo descompilador Python. Para despejar, você ainda precisa de um intérprete corrigido com o objeto co_code correto.

Outra opção é hackear o formato de serialização. No Python, a serialização é chamada empacotamento . A desserialização de arquivos ofuscados da maneira usual não funcionou. Ao fazer engenharia reversa do binário no IDA Pro, descobri a etapa de descriptografia. Até onde eu sei, o primeiro a publicar publicamente algo sobre esse assunto foi Hagen Fritch em seu blog . Lá, ele se refere a alterações nas novas versões do Dropbox (quando o Dropbox mudou do Python 2.5 para o Python 2.7). O algoritmo funciona da seguinte maneira:

  • Ao descompactar um arquivo pyc, um cabeçalho é lido para determinar a versão do empacotamento. Este formato não está documentado, exceto pela implementação do próprio CPython.
  • O formato define uma lista de tipos que são codificados nele. Tipos True , False , floats , etc., mas o mais importante é o tipo para os code object Python acima, code object .
  • Ao carregar o code object , dois valores adicionais são lidos primeiro no arquivo de entrada.
  • O primeiro é o valor random 32 bits.
  • O segundo é um valor de length 32 bits, indicando o tamanho do objeto de código serializado.
  • Em seguida, os valores de rand e length são rand para uma função RNG simples que gera seed .
  • Essa semente é entregue ao vórtice de Mersenne , que gera quatro valores de 32 bits.
  • Combinados, esses quatro valores fornecem uma chave de criptografia para dados serializados. O algoritmo de criptografia descriptografa os dados usando o algoritmo de criptografia minúscula .

No meu código, escrevi o procedimento de desempacotamento do Python do zero. A parte que descriptografa os objetos de código se parece com o fragmento abaixo. Deve-se notar que esse método precisará ser chamado recursivamente. O objeto de nível superior para um arquivo pyc é um objeto de código que contém objetos de código, que podem ser classes, funções ou lambdas. Por sua vez, eles também podem conter métodos, funções ou lambdas. Estes são todos os objetos de código na hierarquia!

  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) 

A capacidade de descriptografar objetos de código significa que, após desserializar os procedimentos, você precisa reescrever o código de bytes real. Os objetos de código contêm informações sobre números de linha, constantes e outras informações. O bytecode real está no objeto co_code . Quando criamos a tabela de conversão de opcode, podemos simplesmente substituir os valores ocultos do Dropbox pelos equivalentes padrão do Python 3.6.

Agora, os objetos de código estão no formato usual do Python 3.6 e podem ser passados ​​para o descompilador. A qualidade dos descompiladores Python aumentou significativamente graças ao projeto unipolar6 de R. Bernstein. A descompilação deu um bom resultado, e eu pude reunir tudo em uma ferramenta que descompila a versão atual do cliente Dropbox da melhor maneira possível.

Se você clonar este repositório e seguir as instruções, o resultado será algo como isto:

  ...
     __main__ - INFO - Dropbox / cliente / features / browse_search / __ init __. pyc descompilado com êxito
     __main__ - INFO - Descriptografando, corrigindo e descompilando _bootstrap_overrides.pyc
     __main__ - INFO - _bootstrap_overrides.pyc descompilado com êxito
     __main__ - INFO - Arquivos 3713 processados ​​(3591 descompilado com sucesso, 122 com falha)
     opcodemap - AVISO - NÃO está escrevendo o mapa do opcode porque a substituição forçada não está definida 

Isso significa que agora você tem um diretório out/ com uma versão descompilada do código-fonte do Dropbox.

Ativando o rastreamento do Dropbox


No código aberto, comecei a procurar por algo interessante, e o fragmento a seguir chamou minha atenção. Os manipuladores de rastreamento em out/dropbox/client/high_trace.py serão instalados apenas se o assembly não estiver congelado ou se a chave mágica ou o cookie que restringe a funcionalidade não estiver definido na linha 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 

Mencionar construções congeladas refere-se às construções de depuração internas do Dropbox. E um pouco mais alto no mesmo arquivo, você pode encontrar essas linhas:

  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 você pode ver no método limited_support_cookie_is_set na linha 287 , o rastreamento só será ativado se a variável de ambiente denominada DBDEV configurada corretamente em cookies com uma vida útil limitada. Bem, isso é interessante! E agora sabemos como gerar esses cookies com tempo limitado. A julgar pelo nome, os engenheiros do Dropbox podem gerar esses cookies e, em alguns casos, ativar temporariamente o rastreamento em alguns casos quando for necessário oferecer suporte aos clientes. Após reiniciar o Dropbox ou reiniciar o computador, mesmo que o cookie especificado ainda esteja no local, ele expirará automaticamente. Suponho que isso deva impedir, por exemplo, a degradação do desempenho devido ao rastreamento contínuo. Isso também dificulta a engenharia reversa do Dropbox.

No entanto, um pequeno script pode simplesmente gerar e definir constantemente esses cookies. Algo assim:

  #!/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) 

Em seguida, um cookie baseado em tempo é criado:

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

Em seguida, carregue corretamente a saída desse script no ambiente e execute o cliente Dropbox.

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

Isso inclui saída de rastreamento, com formatação colorida e tudo mais. Parece algo como este cliente não registrado:



Implementar novo código


Tudo isso é um pouco engraçado. Estudando mais o código descompilado, descobrimos out/build_number/environment.pyc . Há uma função que verifica se uma certa chave mágica está instalada. Essa chave não está codificada no código, mas é comparada com o hash SHA-256. Aqui está o trecho correspondente.

  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 

Esse método é chamado várias vezes em diferentes locais do código para verificar se a chave de rastreamento mágico está definida. Tentei decifrar o hash SHA-256 com a força bruta de John, o Estripador , mas uma força bruta simples levou muito tempo e não pude reduzir o número de opções porque não havia palpites sobre o conteúdo. No Dropbox, os desenvolvedores podem ter uma chave de desenvolvimento codificada específica, que eles instalam, se necessário, ativando o modo "chave mágica" do cliente para rastreamento.

Isso me irritou porque eu queria encontrar uma maneira rápida e fácil de iniciar o Dropbox com este conjunto de chaves para rastreamento. Então, eu escrevi um procedimento de empacotamento que gera arquivos pyc criptografados de acordo com a criptografia do Dropbox. Assim, consegui inserir meu próprio código ou simplesmente substituir o hash acima. Esse código no repositório do Github está no arquivo patchzip.py . Como resultado, o hash é substituído pelo hash SHA-256 de ANVILVENTURES . Em seguida, o objeto de código é criptografado novamente e colocado em um zip, onde todo o código ofuscado é armazenado. Isso permite que você faça o seguinte:

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

Agora todas as funções de depuração são exibidas quando você clica com o botão direito do mouse no ícone do Dropbox na bandeja do sistema.



Estudando ainda mais as fontes descompiladas, no arquivo dropbox/webdebugger/server.py , descobri um método chamado is_enabled . Parece que está verificando se é necessário ativar o depurador da web interno. Primeiro de tudo, ele verifica a chave mágica mencionada. Como substituímos o hash SHA-256, podemos simplesmente definir o valor para ANVILVENTURES . A segunda parte nas linhas 201 e 202 verifica se existe uma variável de ambiente denominada DB<x> que possui x igual ao hash SHA-256. O valor do ambiente define cookies com limite de tempo, como já vimos.

  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 exatamente a mesma técnica, substituindo esse hash pelo SHA-256 usado anteriormente, agora podemos alterar o script setenv escrito anteriormente para algo como isto:

  $ 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 você pode ver, depois de iniciar o cliente, uma nova porta TCP é aberta para escuta. Ele não será aberto se as variáveis ​​de ambiente não estiverem definidas corretamente.

  $ netstat --tcp -lnp |  grep dropbox
     tcp 0 0 127.0.0.1:4242 0.0.0.0:* ESCUTE 1517 / dropbox 

Mais adiante no código, você pode encontrar a interface WebSocket no arquivo webpdb.pyc . Este é um invólucro para utilitários padrão python pdb . O acesso à interface é feito através de um servidor HTTP nesta porta. Vamos instalar o cliente websocket e experimentá -lo:

  $ websocat -t ws: //127.0.0.1: 4242 / pdb
     --Voltar--
    
     > /home/gvb/dropbox/webdebugger/webpdb.pyc(101)run()-> Nenhum
     >
     (Pdb) de build_number.environment importa magic_trace_key_is_set como ms
     (Pdb) ms ()
     Verdadeiro 

Portanto, agora temos um depurador completo no cliente, que em todos os outros aspectos funciona como antes. Podemos executar código Python arbitrário, conseguimos ativar o menu de depuração interno e rastrear as funções. Tudo isso ajudará bastante na análise mais aprofundada do cliente Dropbox.

Conclusão


Pudemos fazer engenharia reversa do Dropbox, escrever ferramentas de descriptografia e injeção de código que funcionem com os clientes atuais do Dropbox baseados no Python 3.6. Revertemos a engenharia das funções ocultas individuais e as ativamos. Obviamente, o depurador realmente ajudará em novos hackers. Especialmente com vários arquivos que não podem ser descompilados com êxito devido às desvantagens do decompyle6.

Código


O código pode ser encontrado no Github . Instruções de uso lá. Este repositório também contém meu código antigo escrito em 2011. Ele deve funcionar com apenas algumas modificações, desde que alguém tenha versões mais antigas do Dropbox com base no Python 2.7.

O repositório também contém scripts para difusão de códigos de operação, instruções para definir variáveis ​​de ambiente do Dropbox e tudo o necessário para alterar o arquivo zip.

Agradecimentos


Agradeço ao Brian da Anvil Ventures por revisar meu código. O trabalho nesse código continuou por vários anos; de tempos em tempos eu o atualizava, introduzia novos métodos e reescrevia fragmentos para restaurá-lo para que funcionasse em novas versões do Dropbox.

Como mencionado anteriormente, um excelente ponto de partida para aplicativos Python de engenharia reversa é o trabalho de Rich Smith, Florian Ledoux e Nicolas Raff, além de Hagen Fritch. Especialmente, o trabalho deles é relevante para o desenvolvimento reverso de um dos maiores aplicativos Python do mundo - o cliente Dropbox.

Deve-se notar que a descompilação do código Python avançou bastante graças ao projeto uncompyle6 liderado por R. Bernstein. Esse descompilador compilou e melhorou muitos descompiladores diferentes do Python.

Também muito obrigado aos colegas Brian, Austin, Stefan e Chris pela revisão deste artigo.

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


All Articles