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 легко восстановить скомпилированный байт-код, представляющий функцию. Простой пример:
Но в скомпилированной версии Objects/codeobject.c свойство
Исчезновение свойства
Кроме того, были удалены другие библиотеки, такие как стандартный дизассемблер Python. В итоге мне всё-таки удалось сделать дамп объектов кода в файлы, но я всё ещё не мог их декомпилировать. Потребовалось некоторое время, прежде чем я понял, что опкоды, используемые интерпретатором Dropbox, не совпадают со стандартными опкодами Python. Таким образом, нужно было разобраться в новых опкодах, чтобы переписать объекты кода обратно в оригинальный байт-код Python.
Один из вариантов — трансляция опкодов (opcode remapping). Насколько мне известно, эту технику разработал Рич Смит и представил на Defcon 18. В том выступлении он также показал инструмент pyREtic для реверс-инжиниринга байт-кода Python в памяти. Кажется, код pyREtic слабо поддерживается, а инструмент нацелен на «старые» бинарники Python 2.х. Для знакомства с техниками, которые придумал Рич, очень рекомендуется посмотреть его выступление.
Метод трансляции опкодов берёт все объекты кода стандартной библиотеки Python и сравнивает их с объектами, извлечёнными из бинарника Dropbox. Например, объекты кода из hashlib.pyc или socket.pyc, которые находятся в стандартной библиотеке. Скажем, если каждый раз опкод
Другой вариант — взломать формат сериализации. В Python сериализация называется маршалинг. Десериализация обфусцированных файлов обычным способом не сработала. При обратной разработке двоичного файла в IDA Pro я обнаружил этап расшифровки. Насколько я знаю, первым что-то публично опубликовал на эту тему Хаген Фрич в своём блоге. Там он ссылается на изменения в новых версиях Dropbox (когда Dropbox перешёл с Python 2.5 на Python 2.7). Алгоритм работает следующим образом:
В своём коде я с нуля написал процедуру демаршалинга на Python. Часть, которая расшифровывает объекты кода, выглядит примерно как фрагмент ниже. Следует отметить, что этот метод придётся вызывать рекурсивно. Объект верхнего уровня для файла pyc — это объект кода, который сам содержит объекты кода, которые могут быть классами, функциями или лямбдами. В свою очередь, они тоже могут содержать методы, функции или лямбды. Всё это объекты кода вниз по иерархии!
Возможность расшифровать объекты кода означает, что после десериализации процедур нужно переписать фактический байт-код. Объекты кода содержат информацию о номерах строк, константах и другую информацию. Фактический байт-код находится в объекте
Теперь объекты кода в обычном формате Python 3.6, и их можно передать в декомпилятор. Качество декомпиляторов Python значительно выросло благодаря проекту uncompyle6 Р. Бернштейна. Декомпиляция дала довольно хороший результат, и я смог собрать всё вместе в инструменте, который в меру своих возможностей декомпилирует текущую версию клиента Dropbox.
Если вы клонируете этот репозиторий и выполните инструкции, результат будет примерно таким:
Это означает, что теперь у вас есть каталог
В открытых исходниках я начал искать что-нибудь интересное, и моё внимание привлёк следующий фрагмент. Обработчики трассировки в
Упоминание frozen-билдов относится к внутренним отладочным билдам Dropbox. А немного выше в этом же файле можно найти такие строчки:
Как видно из метода
Однако небольшой скрипт может просто постоянно генерировать и устанавливать эти куки. Что-то вроде этого:
Затем создаётся файл cookie на основе времени:
Затем правильно загрузите выдачу этого скрипта в окружение и запустите клиент Dropbox.
Это включает вывод трассировки, с разноцветным форматированием и всё такое. Выглядит примерно как в этом незарегистрированном клиенте:
Всё это слегка забавно. Изучая дальше декомпилированный код, мы находим
Этот метод многократно вызывается из разных мест в коде для проверки, установлен ли волшебный ключ трассировки. Я попытался было взломать хеш SHA-256 брутфорсом John the Ripper, но простой перебор идёт слишком долго, а я не мог сократить количество вариантов, потому что не было догадок о содержимом. В Dropbox у разработчиков может быть конкретный жёстко закодированный ключ разработки, который они устанавливают в случае необходимости, активируя режим работы клиента с «волшебным ключом» для трассировки.
Это меня раздражало, поскольку я хотел найти быстрый и простой способ запустить Dropbox с этим набором ключей для трассировки. Поэтому я написал процедуру маршалинга, которая генерирует зашифрованные файлы pyc в соответствии с шифрованием Dropbox. Таким образом, я смог ввести свой собственный код или просто заменить вышеуказанный хеш. Этот код в репозитории на Github находится в файле
Теперь все функции отладки отображаются при щелчке правой кнопкой мыши по значку Dropbox в системном трее.
Изучая дальше декомпилированные исходники, в файле
Используя точно такую же технику, заменив этот хеш на SHA-256, который использовался раньше, мы теперь можем изменить ранее написанный скрипт
Как видим, после запуска клиента открывается на прослушивание новый порт TCP. Он не откроется, если правильно не задать переменные окружения.
Дальше в коде можно найти интерфейс WebSocket в файле
Таким образом, теперь у нас полноценный отладчик в клиенте, который во всех остальных отношениях работает как и раньше. Мы можем выполнять произвольный код Python, нам удалось включить внутреннее меню отладки и функции трассировки. Всё это очень поможет в дальнейшем анализе клиента Dropbox.
Нам удалось провести обратную разработку Dropbox, написать инструменты для расшифровки и инъекции кода, которые работают с текущими клиентами Dropbox на базе Python 3.6. Мы сделали реверс-инжиниринг отдельных скрытых функций и активировали их. Очевидно, что отладчик действительно поможет в дальнейшем взломе. Особенно с рядом файлов, которые не удаётся успешно декомпилировать из-за недостатков decompyle6.
Код можно найти на Github. Инструкции по использованию там же. В этом репозитории лежит также мой старый код, написанный в 2011 году. Он должен работать всего с несколькими модификациями при условии, что у кого-то более старые версии Dropbox, основанные на Python 2.7.
В репозитории также лежат скрипты для трансляции опкодов, инструкция по установке переменных среды Dropbox и всё необходимое для изменения zip-файла.
Спасибо Брайану из Anvil Ventures за ревью моего кода. Работа над этим кодом продолжалась несколько лет, время от времени я его обновлял, внедрял новые методы и переписывал фрагменты, чтобы восстановить его работу на новых версиях Dropbox.
Как упоминалось ранее, отличной отправной точкой для реверс-инжиниринга приложений на Python являются работы Рича Смита, Флориан Леду и Николя Раффа, а также Хагена Фрича. Особенно их работа актуальна для обратной разработки одного из самых больших в мире приложений на Python — клиента Dropbox.
Следует отметить, что декомпиляция питоновского кода сильно продвинулась благодаря проекту uncompyle6 во главе с Р. Бернштейном. В этом декомпиляторе собрано и улучшено множество различных декомпиляторов Python.
Также большое спасибо коллегам Брайану, Остину, Стефану и Крису за рецензирование этой статьи.
Введение
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.
Также большое спасибо коллегам Брайану, Остину, Стефану и Крису за рецензирование этой статьи.
claimc
У меня Dropbox на старте около 2 минут проверяет наличие обновлений в файлах (околок 140к файлов). Попробовал написать на C++ короткую программу, обнаруживающую изменения в файлах по времени модификации mtime. Обрабатывает туже папку за 1 сек. Захотелось написать свой Dropbox.: )
Одумался, как только вспомнил, что придется находить дельту в измененном файле, чтобы не перекачивать его по сети целиком, не храня копию каждого файла на клиенте.