СофтХабрахабр

[Перевод] Реверс-инжиниринг клиента Dropbox

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

Введение

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

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

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

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

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

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

Теперь, когда клиент Dropbox загружал этот объект, я мог легко выполнить произвольный код Python в работающем интерпретаторе. Тогда я не мог сломать шифрование для файлов .pyc, а в итоге взял общий объект стандартной библиотеки 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_code", T_OBJECT, OFF(co_code), READONLY}, {"co_consts", T_OBJECT, OFF(co_consts), READONLY}, ... };

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

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

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

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

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

  • При распаковке 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.

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

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

... __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 у разработчиков может быть конкретный жёстко закодированный ключ разработки, который они устанавливают в случае необходимости, активируя режим работы клиента с «волшебным ключом» для трассировки.

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

$ 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 — клиента Dropbox. Как упоминалось ранее, отличной отправной точкой для реверс-инжиниринга приложений на Python являются работы Рича Смита, Флориан Леду и Николя Раффа, а также Хагена Фрича.

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

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

Показать больше

Похожие публикации

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»