Pull to refresh

Реверс-инжиниринг клиента Dropbox

Reading time12 min
Views10K
Original author: Vincent Berg
TL;DR. В статье рассказывается об обратной разработке клиента Dropbox, взломе механизмов обфускации и декомпиляции клиента на Python, а также изменении программы для активации функций отладки, которые скрыты в обычном режиме. Если вас интересует только соответствующий код и инструкции, пролистайте до конца. На момент написания статьи код совместим с последними версиями Dropbox, основанными на интерпретаторе CPython 3.6.

Введение


Dropbox очаровал меня сразу с момента своего появления. Концепция по-прежнему обманчиво проста. Вот папка. Кладёшь туда файлы. Он синхронизируется. Переходишь к другому устройству. Он опять синхронизируется. Папка и файлы теперь появились и там!

Объём скрытой фоновой работы на самом деле поражает. Во-первых, никуда не исчезают все проблемы, с которыми приходится иметь дело при создании и обслуживании кросс-платформенного приложения для основных десктопных операционных систем (OS X, Linux, Windows). Добавьте к этому поддержку различных веб-браузеров, различных мобильных операционных систем. И мы говорим только о клиентской части. Меня интересует также бэкенд Dropbox, который позволил достичь такой масштабируемости и низкой задержки с безумно тяжёлой рабочей нагрузкой, которую создают полмиллиарда пользователей.

Именно по этим причинам мне всегда нравилось смотреть, что Dropbox делает под капотом и как он развивался на протяжении многих лет. Примерно восемь лет назад я впервые попробовал выяснить, как на самом деле работает клиент Dropbox, когда я заметил трансляцию неизвестного трафика, находясь в отеле. Расследование показало, что это часть функции Dropbox под названием LanSync, которая позволяет быстрее синхронизироваться, если узлы Dropbox в той же локальной сети имеют доступ к тем же файлам. Однако протокол не был задокументирован, и мне хотелось узнать больше. Поэтому я решил взглянуть более подробно, и в итоге провёл реверс-инжиниринг почти всей программы. Это исследование никогда не публиковалось, хотя я иногда делился заметками с некоторыми людьми.

Открыв компанию Anvil Ventures, мы с Крисом оценили ряд инструментов для хранения документов, совместного использования и совместной работы. Одним из них, очевидно, стал Dropbox, а для меня это ещё одна причина откопать старые исследования и проверить их на текущей версии клиента.

Расшифровка и деобфускация


Сначала я загрузил клиент для Linux и быстро выяснил, что он написан на Python. Поскольку лицензия Python довольно разрешительна, людям легко модифицировать и распространять интерпретатор Python вместе с другими зависимостями как коммерческое программное обеспечение. Затем я приступил к реверс-инжинирингу, чтобы понять, как работает клиент.

В то время файлы с байт-кодом лежали в ZIP-файле, объединённом с исполняемым бинарником. Основной двоичный файл представлял собой просто модифицированный интерпретатор Python, который загружался путём захвата механизмов импорта Python. Каждый последующий вызов импорта перенаправлялся в этот бинарник с разбором ZIP-файла. Конечно, несложно извлечь этот ZIP из бинарника. Например, полезный инструмент binwalk извлекает его со всеми байт-скомпилированными файлами .pyc.

Тогда я не мог сломать шифрование для файлов .pyc, а в итоге взял общий объект стандартной библиотеки Python и перекомпилировал его, внедрив внутрь «бэкдор». Теперь, когда клиент Dropbox загружал этот объект, я мог легко выполнить произвольный код Python в работающем интерпретаторе. Хотя я обнаружил это самостоятельно, тот же метод использовал Флориан Леду и Николя Рафф в презентации на Hack.lu в 2012 году.

Возможность исследовать и манипулировать запущенным кодом в Dropbox позволила многое выяснить. В коде использовалось несколько защитных трюков, чтобы затруднить дамп объектов кода. Например, в обычном интерпретаторе CPython легко восстановить скомпилированный байт-код, представляющий функцию. Простой пример:

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

Но в скомпилированной версии Objects/codeobject.c свойство co_code удалили из открытого списка. Этот member list обычно выглядит примерно так:

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

Исчезновение свойства co_code делает невозможным дамп этих объектов кода.

Кроме того, были удалены другие библиотеки, такие как стандартный дизассемблер Python. В итоге мне всё-таки удалось сделать дамп объектов кода в файлы, но я всё ещё не мог их декомпилировать. Потребовалось некоторое время, прежде чем я понял, что опкоды, используемые интерпретатором Dropbox, не совпадают со стандартными опкодами Python. Таким образом, нужно было разобраться в новых опкодах, чтобы переписать объекты кода обратно в оригинальный байт-код Python.

Один из вариантов — трансляция опкодов (opcode remapping). Насколько мне известно, эту технику разработал Рич Смит и представил на Defcon 18. В том выступлении он также показал инструмент pyREtic для реверс-инжиниринга байт-кода Python в памяти. Кажется, код pyREtic слабо поддерживается, а инструмент нацелен на «старые» бинарники Python 2.х. Для знакомства с техниками, которые придумал Рич, очень рекомендуется посмотреть его выступление.

Метод трансляции опкодов берёт все объекты кода стандартной библиотеки Python и сравнивает их с объектами, извлечёнными из бинарника Dropbox. Например, объекты кода из hashlib.pyc или socket.pyc, которые находятся в стандартной библиотеке. Скажем, если каждый раз опкод 0x43 соответствует деобфусцированному опкоду 0x21, можно постепенно построить таблицу трансляции для перезаписи объектов кода. Затем эти объекты кода можно провести через декомпилятор Python. Чтобы делать дампы, по-прежнему нужен исправленный интерпретатор с корректным объектом co_code.

Другой вариант — взломать формат сериализации. В Python сериализация называется маршалинг. Десериализация обфусцированных файлов обычным способом не сработала. При обратной разработке двоичного файла в IDA Pro я обнаружил этап расшифровки. Насколько я знаю, первым что-то публично опубликовал на эту тему Хаген Фрич в своём блоге. Там он ссылается на изменения в новых версиях Dropbox (когда Dropbox перешёл с Python 2.5 на Python 2.7). Алгоритм работает следующим образом:

  • При распаковке pyc-файла считывается заголовок для определения версии маршалинга. Этот формат не документирован, за исключением самой реализации CPython.
  • Формат определяет список типов, которые в нём закодированы. Типы True, False, floats и т. д., но самый важный — тип для вышеупомянутых объектов кода Python, code object.
  • При загрузке code object сначала считываются два дополнительных значения из входного файла.
  • Первое из них — 32-битное значение random.
  • Второе — 32-битное значение length, обозначающее размер сериализированного объекта кода.
  • Затем значения rand и length подаются в простую функцию RNG, которая генерирует seed.
  • Данный сид поставляется в вихрь Мерсенна, который генерирует четыре 32-битных значения.
  • Объединённые вместе, эти четыре значения дают ключ шифрования для сериализированных данных. Алгоритм шифрования затем расшифровывает данные с помощью Tiny Encryption Algorithm.

В своём коде я с нуля написал процедуру демаршалинга на Python. Часть, которая расшифровывает объекты кода, выглядит примерно как фрагмент ниже. Следует отметить, что этот метод придётся вызывать рекурсивно. Объект верхнего уровня для файла pyc — это объект кода, который сам содержит объекты кода, которые могут быть классами, функциями или лямбдами. В свою очередь, они тоже могут содержать методы, функции или лямбды. Всё это объекты кода вниз по иерархии!

    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)

Возможность расшифровать объекты кода означает, что после десериализации процедур нужно переписать фактический байт-код. Объекты кода содержат информацию о номерах строк, константах и другую информацию. Фактический байт-код находится в объекте co_code. Когда мы построили таблицу трансляции опкодов, то можем просто заменить обфусцированные значения Dropbox на стандартные эквиваленты Python 3.6.

Теперь объекты кода в обычном формате Python 3.6, и их можно передать в декомпилятор. Качество декомпиляторов Python значительно выросло благодаря проекту uncompyle6 Р. Бернштейна. Декомпиляция дала довольно хороший результат, и я смог собрать всё вместе в инструменте, который в меру своих возможностей декомпилирует текущую версию клиента Dropbox.

Если вы клонируете этот репозиторий и выполните инструкции, результат будет примерно таким:

    ...
    __main__ - INFO - Successfully decompiled dropbox/client/features/browse_search/__init__.pyc
    __main__ - INFO - Decrypting, patching and decompiling _bootstrap_overrides.pyc
    __main__ - INFO - Successfully decompiled _bootstrap_overrides.pyc
    __main__ - INFO - Processed 3713 files (3591 succesfully decompiled, 122 failed)
    opcodemap - WARNING - NOT writing opcode map as force overwrite not set

Это означает, что теперь у вас есть каталог out/ с декомпилированной версией исходного кода Dropbox.

Включение трассировки Dropbox


В открытых исходниках я начал искать что-нибудь интересное, и моё внимание привлёк следующий фрагмент. Обработчики трассировки в out/dropbox/client/high_trace.py устанавливаются только в том случае, если сборка не заморожена или в строке 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

Упоминание frozen-билдов относится к внутренним отладочным билдам Dropbox. А немного выше в этом же файле можно найти такие строчки:

    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

Как видно из метода limited_support_cookie_is_set в строке 287, трассировка включается только в том случае, если переменная среды с названием DBDEV правильно установлена в куки с ограниченным временем жизни. Что ж, это интересно! И теперь мы знаем, как генерировать такие куки, ограниченные по времени. Судя по названию, инженеры Dropbox могут генерировать такие куки, а затем временно включать трассировку в отдельных случаях, когда это требуется для поддержки клиентов. После перезагрузки Dropbox или перезагрузки компьютера, даже если указанный файл cookie всё ещё на месте, он автоматически истекает. Предполагаю, что это должно предотвратить, например, ухудшение производительности из-за непрерывной трассировки. Это также затрудняет обратную разработку Dropbox.

Однако небольшой скрипт может просто постоянно генерировать и устанавливать эти куки. Что-то вроде этого:

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

Затем создаётся файл cookie на основе времени:

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

Затем правильно загрузите выдачу этого скрипта в окружение и запустите клиент Dropbox.

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

Это включает вывод трассировки, с разноцветным форматированием и всё такое. Выглядит примерно как в этом незарегистрированном клиенте:



Внедрение нового кода


Всё это слегка забавно. Изучая дальше декомпилированный код, мы находим out/build_number/environment.pyc. Там есть функция, которая проверяет, установлен ли определённый magic key. Этот ключ не закодирован жёстко в коде, но сравнивается с хэшем SHA-256. Вот соответствующий фрагмент.

      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

Этот метод многократно вызывается из разных мест в коде для проверки, установлен ли волшебный ключ трассировки. Я попытался было взломать хеш SHA-256 брутфорсом John the Ripper, но простой перебор идёт слишком долго, а я не мог сократить количество вариантов, потому что не было догадок о содержимом. В Dropbox у разработчиков может быть конкретный жёстко закодированный ключ разработки, который они устанавливают в случае необходимости, активируя режим работы клиента с «волшебным ключом» для трассировки.

Это меня раздражало, поскольку я хотел найти быстрый и простой способ запустить Dropbox с этим набором ключей для трассировки. Поэтому я написал процедуру маршалинга, которая генерирует зашифрованные файлы pyc в соответствии с шифрованием Dropbox. Таким образом, я смог ввести свой собственный код или просто заменить вышеуказанный хеш. Этот код в репозитории на Github находится в файле patchzip.py. В итоге хеш заменяется SHA-256 хешем ANVILVENTURES. Потом объект кода повторно шифруется и помещается в zip, где хранится весь обфусцированный код. Это позволяет сделать следующее:

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

Теперь все функции отладки отображаются при щелчке правой кнопкой мыши по значку Dropbox в системном трее.



Изучая дальше декомпилированные исходники, в файле dropbox/webdebugger/server.py я обнаружил метод с названием is_enabled. Похоже, он проверяет, нужно ли включить встроенный веб-отладчик. Прежде всего, он проверяет упомянутый волшебный ключ. Поскольку мы заменили хеш SHA-256, то мы можем просто установить значение ANVILVENTURES. Вторая часть в строках 201 и 202 проверяет, есть ли переменная окружения с именем DB<x>, у которой x равно хешу SHA-256. Значение окружения устанавливает куки с ограничением по времени, как мы уже видели.

    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

Используя точно такую же технику, заменив этот хеш на SHA-256, который использовался раньше, мы теперь можем изменить ранее написанный скрипт setenv на что-то вроде такого:

    $ 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

Как видим, после запуска клиента открывается на прослушивание новый порт TCP. Он не откроется, если правильно не задать переменные окружения.

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

Дальше в коде можно найти интерфейс WebSocket в файле webpdb.pyc. Это обёртка для стандартных питоновских утилит pdb. Доступ к интерфейсу осуществляется через HTTP-сервер на этом порту. Давайте установим клиент websocket и испытаем его:

    $ websocat -t ws://127.0.0.1:4242/pdb
    --Return--
    
    > /home/gvb/dropbox/webdebugger/webpdb.pyc(101)run()->None
    >
    (Pdb) from build_number.environment import magic_trace_key_is_set as ms
    (Pdb) ms()
    True

Таким образом, теперь у нас полноценный отладчик в клиенте, который во всех остальных отношениях работает как и раньше. Мы можем выполнять произвольный код Python, нам удалось включить внутреннее меню отладки и функции трассировки. Всё это очень поможет в дальнейшем анализе клиента Dropbox.

Заключение


Нам удалось провести обратную разработку Dropbox, написать инструменты для расшифровки и инъекции кода, которые работают с текущими клиентами Dropbox на базе Python 3.6. Мы сделали реверс-инжиниринг отдельных скрытых функций и активировали их. Очевидно, что отладчик действительно поможет в дальнейшем взломе. Особенно с рядом файлов, которые не удаётся успешно декомпилировать из-за недостатков decompyle6.

Код


Код можно найти на Github. Инструкции по использованию там же. В этом репозитории лежит также мой старый код, написанный в 2011 году. Он должен работать всего с несколькими модификациями при условии, что у кого-то более старые версии Dropbox, основанные на Python 2.7.

В репозитории также лежат скрипты для трансляции опкодов, инструкция по установке переменных среды Dropbox и всё необходимое для изменения zip-файла.

Благодарности


Спасибо Брайану из Anvil Ventures за ревью моего кода. Работа над этим кодом продолжалась несколько лет, время от времени я его обновлял, внедрял новые методы и переписывал фрагменты, чтобы восстановить его работу на новых версиях Dropbox.

Как упоминалось ранее, отличной отправной точкой для реверс-инжиниринга приложений на Python являются работы Рича Смита, Флориан Леду и Николя Раффа, а также Хагена Фрича. Особенно их работа актуальна для обратной разработки одного из самых больших в мире приложений на Python — клиента Dropbox.

Следует отметить, что декомпиляция питоновского кода сильно продвинулась благодаря проекту uncompyle6 во главе с Р. Бернштейном. В этом декомпиляторе собрано и улучшено множество различных декомпиляторов Python.

Также большое спасибо коллегам Брайану, Остину, Стефану и Крису за рецензирование этой статьи.
Tags:
Hubs:
+39
Comments2

Articles

Change theme settings