Практический реверс-инжиниринг. Часть 5 – Поиск уязвимостей в прошивке

Практический реверс-инжиниринг. Часть 5 – Поиск уязвимостей в прошивке

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

Автор: Juan Carlos Jiménez

  • Часть 1: Поиск отладочных портов.
  • Часть 2: Исследование прошивки.
  • Часть 3: Исследование информационных потоков.
  • Часть 4: Полная выгрузка информации из флеш-памяти.

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

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

Далее я расскажу некоторые теоретические основы, касающиеся архитектуры Linux, дизассемблирования бинарных файлов и других моментах, имеющих отношения к нашей теме. Если вам не интересно читать азы, можете сразу переходить к разделу «Поиск алгоритма, генерирующего стандартный пароль для WiFi». Вначале мы изучим доступный исходный код и при помощи утилиты grep попытаемся найти интересные бинарные файлы с целью дальнейшего дизассемблирования и поиска различных алгоритмов.

Получение исходных текстов

Linux, U-Boot и другие утилиты, используемые роутером, идут под лицензией General Public License, которая предписывает, что весь исходный код любого проекта должен быть доступен каждому.

Исходники могут стать серьезным подспорьем во время реверс-инжиниринга. Ядро и загрузчик представляют для нас особый интерес, и не только для поиска уязвимостей, связанных с безопасностью.

При поиске исходных текстов для проектов, идущих под лицензией GPL, возможны следующие варианты развития событий:

  • Исходный код полностью доступен на сайте производителя. Пример: продукция компании Apple или Amazon.
  • Исходный код доступен по запросу.
    • Производитель отсылает вам исходные тексты по электронной почте.
    • Производитель просит «разумную стоимость» за отправку CD диска с исходным кодом.
  • Производитель игнорирует (незаконно) все ваши запросы. Если подобное происходит, подумайте о том, как сделать благо при помощи плохого поступка.

В случае с нашим роутером исходный код был в свободном доступе. После весьма продолжительных поисков мне удалось найти нужные файлы на мобильной версии сайта:

 
Рисунок 1: Исходные тексты приложений, используемых роутером

Однако сразу же возникает два закономерных вопроса. Все ли исходные тексты представлены на сайте, и может ли так случиться, что при компиляции бинарных файлов для роутера используются другие исходные тексы.

Верификация бинарных файлов

Теоретически мы могли бы скомпилировать исходные тексты и сравнить результаты с прошивкой, которая была извлечена из устройства. На практике все намного сложнее.

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

Если вы хотите узнать больше о совместимости бинарных файлов, рекомендую ознакомиться с этой статьей, где авторы пытаются преодолеть различные препятствия при проверке официальных бинарных файлов приложения TrueCrypt на предмет отсутствия встроенных вредоносов.

Введение в архитектуру Linux

В предыдущих статья мы рассматривали различные компоненты, найденные в прошивке: загрузчик, ядро, файловая система и некоторые защищенные области памяти с настройками. Чтобы знать, где искать нужный компонент, важно понимать, как устроена система. Рассмотрим архитектуру системы на примере нашего устройства:


Рисунок 2: Общая архитектура системы (загрузка и среда выполнения)

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

  • Аппаратная часть: процессор, флеш-память, RAM и другие компоненты, физически соединенные друг с другом.
  • Ядро операционной системы Linux: умеет управлять аппаратной частью. За основу разработчики взяли исходный код ядра, написали драйвера для конкретного устройства и скомпилировались все вместе в единое ядро. Этот компонент управляет памятью, читает и пишет в аппаратные регистры и т. д. В более сложных системах «модули ядра» позволяют отделить драйвера в файловой системе, которые при необходимости подгружаются динамически. В большинстве встроенных систем не требуется подобный уровень гибкости и разработчики встраивают все драйвера внутрь ядра.
  • libc (“библиотека С”): Данная библиотека выполняет роль общей обертки для системных вызовов, включая очень популярные функции наподобие printf, malloc или system. Разработчики могут использовать системные вызовы напрямую, но в большинстве случаев намного удобнее пользоваться библиотекой libc. Вместо популярной библиотеки glibc (GNU C library), на нашем устройстве используется версия, оптимизированная для встроенных устройств: uClibc
  • Пользовательские приложения: исполняемые бинарные файлы в папке /bin/ и разделяемые объекты в директории /lib/ (библиотеки, содержащие функции, используемые различными бинарными файлами) включают в себя большую часть высокоуровневой логики. Разделяемые объекты используются для экономии дискового пространства, когда наиболее часто используемые функции хранятся в одном месте.

Исходный код загрузчика

Ранее неоднократно упоминалось, что в роутере используется загрузчик U-Boot. U-Boot идет под лицензией GPL, однако на сайте Huawei исходные тексты отсутствуют.

Исходный код загрузчика был бы очень полезен во многих случаях. В частности, можно было бы выяснить, как запустить другую прошивку на устройстве или произвести какие-либо изменения. Загрузчики разнятся по набору встроенных функций. В моем случае я не особо интересовался функционалом U-Boot и не предпринимал особых усилий по поиску исходников.

Исходный код ядра

Давайте посмотрим, если что-то интересное в исходниках ядра. Помните кнопку сброса до заводских настроек? Эта кнопка является частью аппаратного уровня. Следовательно, GPIO-пин (General Purpose Input/Output; Входы/выходы общего назначения), который детектирует нажатие кнопки, должен управляться драйверами. На рисунке ниже показаны логи, полученные при мониторинге UART-порта в предыдущей статье:


Рисунок 3: Логи, снятые с UART-порта

При помощи утилиты grep мы можем обнаружить, как различные компоненты системы (ядро, бинарные файлы и разделяемые объекты) участвуют в формировании логов, показанных выше.


Рисунок 4: Строки из лога содержатся в различных компонентах системы

Исследуя исходники ядра, мы можем найти слабости в системе безопасности и другие уязвимости, которые иногда рассматриваются производителем как «допустимые риски». Что более важно: мы можем использовать драйвера для компиляции и запуска нашей собственной операционной системы на устройстве.

Исходный код пользовательской части

Поскольку мы имеем дело с лицензией GPL, некоторые компоненты из пользовательской части также идут с открытым исходным кодом (например, busybox и iptables). По моему личному опыту, поиска в базе данных с публичными уязвимостями вполне достаточно, чтобы найти нужный эксплоит.

Кстати говоря, если вы хотите найти свежие уязвимости, бэкдоры или секретные данные, лучше заняться исследованием проектов без открытого исходного кода. Системы, заточенные под определенные устройства и без исходников, которые разрабатываются производителем или распространителем оборудования, не проходят столь тщательного тестирования, и, как следствие, могут содержать множество дыр.

В нашем случае большая часть системы из пользовательской части представлена только в виде бинарных файлов, а поскольку у нас есть доступ ко всей файловой системе, нам ничто не мешает поискать уязвимости и в закрытой части.

Если исходники отсутствуют, то нужно найти способ анализа машинного кода. И здесь нам поможет дизассемблер.

Дизассемблирование бинарных файлов

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

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

Популярные дизассемблеры

Существует множество дизассемблеров под популярные архитектуры наподобие MIPS, отличающиеся друг по функционалу и удобству. Я приведу 3 наиболее популярных и мощных утилиты, присутствующих в данный момент на рынке:

  • IDA Pro: наиболее популярный дизассемблер/отладчик. Богатый набор функций, мультиплатформенность, мощное сообщество, документация, плагины и так далее. Единственный недостаток – цена. Единичная лицензия на версию PRО, которая необходима для дизассемблирования бинарных файлов, заточенных под архитектуру MIPS, стоит свыше 1000 долларов.
  • Radare2: дизассемблер с открытым исходным кодом с необычайно продвинутым интерфейсом и комадной строкой. Вокруг Radare2 также сформировалось сильное сообщество исследователей разных мастей. С другой стороны, продвинутость – синоним сложности, что требует серьезных временных затрат на изучение.
  • Binary Ninja: дизассемблер с закрытым исходным кодом, но по приемлемой цене (100 долларов за одну лицензию). В целом, находится где-то по середине между IDA и Radare. Эта утилита является относительно новой (выпущена в 2016), но с каждым днем набирает все большую популярность. Поддерживает некоторые архитектуры. К сожалению, MIPS пока не входит в этот список (но поддержка должна появиться вскоре). Кроме того, отсутствуют некоторые функции, которые мне были нужны для исследования бинарных файлов. Лично я планируют вернуться к изучению этого дизассемблера чуть позже.

Для отображения ассемблерного кода в более читабельном виде во всех дизассемблерах предусмотрен режим «Graph View», используя который можно наглядно увидеть возможные ветви выполнения внутри бинарного файла:


Рисунок 5: Наглядное отображение кода в дизассемблере

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

Дизассемблер проделывает за нас много рутинной работы и отображает ссылки на функции и строки (см. рисунок выше), чего может быть достаточно для нахождения интересных деталей. Однако в большинстве случаев все же требуется понимать ассемблерный код, по крайней мере в некоторой степени.

Преобразование машинных инструкций в ассемблерный код

Давайте взглянем на формат наших бинарных файлов:

$ file bin/busybox
bin/busybox: ELF 32-bit LSB executable, MIPS, MIPS-II version 1 (SYSV), dynamically linked (uses shared libs), corrupted section header size

Поскольку заголовки формата ELF не привязаны к платформе, уже сейчас можно получить некоторую информацию относительно бинарных файлов. По результатам выполнения команды выше видно, что бинарные файлы заточены под 32-битную архитектура MIPS с порядком следования байтов LSB (младший значимый бит). Кроме того, используются разделяемые библиотеки.

Та же самая информация указана в документации на роутер, где говорится о том, что используется ядро процессора MIPS24KEc.


Рисунок 6: Выдержка из документации на роутер

Зная точную версия ядра процессора, мы можем выяснить производителя (Imagination Technologies) и найти соответствующий даташит.

После изучения базовой информации можно приступать к анализу файла в дизассемблере, который поможет подтвердить некоторые наши находки и отобразить содержимое файла в виде ассемблерного кода. Чтобы понимать суть, необходимо ознакомиться с набором инструкций и именами регистров.

Помимо инструкций и регистров в некоторых архитектурах могут быть дополнительные тонкости. Один из примеров – слоты задержки, присутствующие в архитектуре MIPS, а конкретно - инструкции, появляющиеся сразу после инструкций перехода (например, beqz, jalr), но которые запускаются перед самим переходом. Подобная нелинейность была бы совершенно немыслима в других архитектурах.

Некоторые интересные статьи на тему MIPS: IntrotoMIPSReversingusingRadare2, MIPSAssemblerandRuntimeSimulator, Toolchainstocross-compileforMIPStargets.

Пример дизассемблирования бинарных файлов из пользовательской части

Возвращаемся к событию, связанному с нажатием кнопки сброса настроек. После нажатия были сгенерировано несколько сообщений (логов) на UART-порту, однако не все, которые положены. Поскольку мы не смогли найти строку ‘button has been pressed’ в исходном коде ядра, логично предположить, что этот функционал находится где-то в пользовательской части. Попробуем найти бинарный файл, который выводит это сообщение:

~/Tech/Reversing/Huawei-HG533_TalkTalk/router_filesystem
$ grep -i -r "restore default success" .
Binary file ./bin/cli matches
Binary file ./bin/equipcmd matches
Binary file ./lib/libcfmapi.so matches

По результатам поисков нашлось 3 файла: 2 исполняемых файла в папке /bin/ и один разделяемый объект в папке /lib/. Начнем с анализа /bin/equipcmd в IDA:


Рисунок 7: Анализ файла /bin/equipcmd в IDA

Если приглядеться к рисунку выше, то просматривается код на С, который преобразован в ассемблерные инструкции. Мы видим «чистый конфигурационный файл», очень согласуется с командами типа ERASE, которые мы наблюдали при перехвате трафика на шине SPI. Далее в зависимости от результата печатается одна из следующих строк: «restore default success» (сброс произошел успешно) или «restore default fail» (во время сброса возникла ошибка).

Эта функция - идеальный пример для слотов задержки: инструкции addiu, устанавливающие строки как аргументы в регистр -$a0- при двух вызовах функции puts, находятся в слотах задержки инструкций передачи управления в случае равенства нулю и инструкций перехода с сохранением адреса следующей инструкции (JAL).

Как видно на рисунке выше, IDA именует функции, используемые в бинарном файле. Подобное не происходит в случае с другими бинарными файлами, и сейчас мы разберемся, почему.

Имена функций в бинарном файле – Введение в таблицу символов

В формате ELF предусмотрены таблицы символов, представляющие собой блоки информации внутри бинарного файла, которые содержат полезные отладочные сведения. Часть этой информации содержит читабельные имена функций, используемых в бинарном файле. Эта возможность значительно облегчает отладку. С другой стороны, при выпуске готового релиза данную информацию следует удалить, хотя в нашем случае разработчики этого не сделали.

Для удаления отладочной информации можно использовать утилиты наподобие strip, которые знают, что нужно удалить, а что оставить. Подобные инструменты преследуют две цели: уменьшают память посредством удаления данных, которые не нужны во время выполнения, и усложняют процесс реверс-инжиниринга. Имена функций определяют контекст кода, который мы исследуем, что чрезвычайно полезно и удобно.

Иногда, в основном при дизассемблировании разделяемых объектов, вы можете обнаружить присутствие некоторых имен функций. В некоторых случаях имена будут отсутствовать вовсе. То, что вы увидите в дальнейшем, представляют собой динамические символы из таблицы .dymsym. Ранее мы уже обсуждали, что разделяемые объекты экономят много памяти, когда общие участки кода, используемые в различных местах (например, функция printf()), хранятся в одном месте. Чтобы найти нужный кусок информации внутри разделяемого объекта, вызывающая сторона использует понятное и удобочитаемое имя. Следовательно, имена функций и переменных, необходимых для сторонних приложений, в обязательном порядке должны храниться внутри бинарного файла. Остальные имена можно удалить. В формате ELF предусмотрено две таблицы символов: .dynsym - с общедоступными именами и .symtab – для внутренних нужд.

За более подробной информаций о таблицах символов и других тонкостях формата ELF, обращайтесь к следующим документам: The ELF Format - How programs look from the inside, Inside ELF Symbol Tables и ELF spec (PDF).

Поиск алгоритма, генерирующего стандартные WiFi пароли

Помните алгоритм генерации WiFi паролей, о котором мы говорили в третьей части? Тогда я приводил доводы, почему эта модель роутера, скорее всего, не содержит подобного алгоритма. Однако давайте все же попробуем копнуть в эту сторону.

Если не забыли, ниже показан WiFi пароль, используемый по умолчанию в моем роутере:


Рисунок 8: Стандартный WiFi пароль

На данный момент нам известно, что:

  • На каждом устройстве предварительно настроены разные учетные записи.
  • Эти учетные записи могут быть установлены производителем или сгенерированы на устройстве. Из предыдущих частей мы уже знаем, что и SSID и пароль хранятся в зарезервированной области флеш-памяти рядом друг с другом.
  • Если эта информация установлена на фабрике, роутеру остается считать эти данные из нужной области памяти.
  • Если эта информация генерируется на устройстве и затем сохраняется в памяти, значит, должен существовать алгоритм, который на базе одинаковых входных параметров будет всегда генерировать одинаковые результаты. Если входные параметры – общедоступны (например, MAC адрес), и мы сможем восстановить этот алгоритм, тогда впоследствии мы сможем сгенерировать стандартные пароли для любого роутера, использующего этот алгоритм.

Попробуем разобраться, как обстоят дела на самом деле.

Поиск жестко установленных строк

Давайте предположим, что алгоритм генерации существует. Между именем пользователя и паролем существует только одна строка, присутствующая на всех устройствах: TALKTALK-. Эта строка добавляется к последним 6 символам MAC адреса. Если алгоритм генерации существует, эта строка должна присутствовать.

$ grep -r 'TALKTALK-' .
Binary file ./bin/cms matches
Binary file ./bin/nmbd matches
Binary file ./bin/smbd matches

2 из 3 найденных бинарных файлов (nmbd и smbd) являются частью приложения samba, которое используется с целью организации USB накопителя в качестве сетевого хранителя информации. Эти файлы, вероятно, отвечают за идентификацию роутера в сети.

Рассмотрим подробнее файл /bin/cms.

Исследование связанных функций


Рисунок 9: Участок функции ATP_WLAN_INIT

Кажется, что участок кода на рисунке представляет собой алгоритм, генерирующий SSID. Этот код находится внутри огромной функции с именем ATP_WLAN_Init. Здесь выполняются следующие операции:

  • Ищется MAC адрес устройстве, на котором запускается функция:
    • mac = BSP_NET_GetBaseMacAddress()
  • Создается строка SSID:
    • snprintf(SSID, "TALKTALK-%02x%02x%02x", mac[3], mac[4], mac[5])
  • Созданная строка где-то сохраняется:
    • ATP_DBSetPara(SavedRegister3, 0xE8801E09, SSID)

К сожалению, сразу после этой ветки выполняется функция ATP_DBSave, начинают запускаться  команды и т. п.:


Рисунок 10: Инструкции, выполняемые после формирования и сохранения SSID

Дальнейшее исследования этой функции и других ссылок на ATP_DBSave не выявили ничего интересного.

Результаты поисков

Потратив некоторое время на поиски и изучение потенциально интересных участков кода, я не нашел чего-то, что бы имело отношение к алгоритму генерации паролей. Сей факт подтвердил мои подозрения, которые у меня были с момента нахождения стандартных учетных записей в защищенной области флеш-памяти. Производитель придерживается высоких стандартов безопасности и зашивает учетные записи на этапе производства, вследствие чего нет необходимости использовать алгоритм генерации. Поскольку разработчики изготавливают собственное оборудование и могут вносить любые изменения в технологию производства, решение пойти на такой шаг – очень правильное.
Возможно, в будущем я вернусь к этому вопросу или попробую исследовать другую модель роутера (и задокументирую весь процесс), однако знайте, что описанный метод универсальный и рабочий. Истории знакомо большое количество свободно доступных генераторов WiFi паролей.

Поскольку мы умеем находить нужные участки кода в бинарных файлах, давайте посмотрим, что еще интересного можно сделать.

Поиск уязвимостей, связанных с инжектированием команд

Бреши, связанные с инжектированием команд, наиболее распространены, легки в поиске и в то же время одни из самых опасных. Идея, лежащая в основе подобного рода уязвимостей, предельно проста. Ищется входная строка, которая будет использоваться в качестве аргумента для команды шелла. При подборе строки мы пытаемся обойти фильтры, реализованные разработчиками. Во встроенных устройствах в результате эксплуатации подобных брешей зачастую можно получить полный доступ к системе с правами суперпользователя.

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


Рисунок 11: Пример выполнения команды ping через веб-интерфейс

Как только получена информация о целевом сервере, у нас есть два пути: найти библиотеку с реализацией протокола ICMP и осуществить вызов напрямую через веб-интерфейс или использовать стандартную команду ping. Последний способ легче в реализации, экономит память и вообще является более очевидным. При использовании пользовательских входных данных (об адресе целевого сервера) с последующей передачей как части аргумента команды возникают серьезные угрозы. Давайте рассмотрим, как происходит обработка этой информации веб-приложением /bin/web:


Рисунок 12: Процедура формирования команды ping

Вызов библиотечной функции system()  (не путать с системным вызовом / syscall) – наипростейший способ выполнить шелл-команду через приложения. Иногда разработчики делают обертку для функции system() с целью фильтрации входной информации. Однако всегда существует нечто, что не учтено или попросту проигнорировано.

Поиск ссылок на функцию system – эффективный способ поиска «намеков» на уязвимости, связанные с инжектированием команд. Далее мы смотрим детально каждый вызов, где используется нефильтрованные входные данные. На рисунке ниже показаны все ссылки на функцию system() в бинарном файле /bin/web:


Рисунок 13: Перечень ссылок на функцию system()

Даже по именам функций уже можно понять, передаются ли входные пользовательские данные в функцию system(). Также можно увидеть ссылки на PIN и PUK коды, объекты, связанные с сим-картами и т. д. Скорее всего, это приложение также используется в мобильных устройствах.

Я попытался найти способы обхода фильтрации внутри функции atp_gethostbyname (все, что не являлось именем домена, вызывало ошибку), но не смог найти что-либо интересного. Последующий анализ может показать, что я был неправ. В целом, идея заключается в том, чтобы инжектировать команду подобного рода:


Рисунок 14: Попытка инжектировать команды через веб-интерфейс

Что в итоге должно было бы привести к выполнению команды ping google.com -c 1; reboot; ping 192.168.1.1 > /dev/null. Если роутер перезагрузится, значит, мы нашли способ обхода фильтра.

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

Еще один пример сетевого интерфейса, потенциально уязвимого к удаленным инъекциям, - протокол «LAN-Side DSL CPE Configuration» (или TR-064). Даже несмотря на то, что этот протокол был спроектирован для внутренних сетевых нужд, в прошлом использовался для настройки роутеров через интернет. Бреши, связанные с инжектированием команд, в некоторых реализациях этого протокола использовались для удаленного извлечение учетных записей от WiFi при помощи нескольких пакетов.

В нашей модели роутера есть бинарный файл /bin/tr064. При анализе этого файла сразу же находим нечто интересное внутри функции main():


Рисунок 15: Участок внутри функции main()

На рисунке выше показан RSA ключ, используемый во время SSL аутентификации, который мы нашли во второй части. Теперь мы можем выступать от имени роутера и искать уязвимости на соседних серверах или искать другие методы атак. Более важным является то, что мы разгадали загадку секретного ключа, найденного во время исследования прошивки.

Поиск более сложных уязвимостей

Даже если мы не смогли найти брешей, связанных с инжектированием команд, всегда есть другие способы получить контроль над роутером. Наиболее распространенные методы связаны с переполнением буфера. Любая входная строка, вне зависимости от предназначения, обрабатывается и модифицируется внутри кода. Ошибка при вычислении нужного буфера и отсутствие проверок во время строковых операций могут привести к переполнению, которое может использовать злоумышленник с целью получения контроля над системой.

Идея, лежащая в основе переполнения буфера, довольно проста: мы передаем строку, содержащую код для выполнения, и переопределяем определенный адрес в приложении так, чтобы поток выполнения переключился на инжектированный код. Если все прошло удачно, то нам становится доступен функционал бинарного файла. В случае с роутером, где все программы запускаются от имени суперпользователя, по сути, мы получаем полный контроль над системой.


Рисунок 16: Пример эксплуатации уязвимости на базе переполнения буфера

Разработка эксплоита для подобного рода брешей не такая простая задача, как в случае с инжектированием команд и поиском узких мест при фильтрации. В случае с переполнением буфера используются различные сценарии и техники. В некоторых ситуациях могут потребоваться методы наподобие возвратно-ориентированного программирования. Кстати говоря, большинство встроенных систем сильно отстали от персональных компьютеров в плане методов противодействия эксплоитам. Техники наподобие ASLR (Address Space Layout Randomization; Рандомизация размещения адресного пространства), которые серьезно затрудняют написание эксплоитов, во встроенных системах либо отключены, либо не реализованы вовсе.

Если вы хотите найти потенциальную брешь, вы можете либо изучать методы разработки эксплоитов, либо воспользоваться техниками, которые мы использовали ранее. Например, поискать места ввода строки и участки кода, которые обрабатывают ввод (имена функций, жестко установленные строки и т. д.). Далее можно попытаться обойти фильтры при помощи отсылки нестандартной строки, которая, в случае некорректной обработки, ляжет в основу бреши, пригодной для эксплуатации.

Для нас наиболее интересны функции, связанные с манипуляцией строками. Например, strcpy, strcat, sprintf или более безопасные аналоги: strncpy, strncat, которые также могут быть уязвимы к некоторым техникам, но в целом более устойчивы к разного рода брешам.

Рисунок 17: Примеры потенциально уязвимых вызовов функций

Даже когда мы не уверены, что в функцию, извлеченную из бинарного файла /bin/tr064, передается информация, вводимая пользователем, этот пример все равно подходит для рассмотрения. Как только вы нашли потенциально небезопасные строковые операции, где могут обрабатываться входные данные, необходимо выяснить, есть ли там уязвимости.

Попробуйте добиться аварийного завершения работы приложения посредством отсылки входных данных очень большого размера. По результатам экспериментов можно провести анализ, задавая самому себе следующие вопросы: почему произошло аварийное завершение? Сколько символов нужно отослать, чтобы аварийного завершения не было? Какая полезная нагрузка подойдет в нашем случае? В каком месте памяти окажется полезная нагрузка? И так далее. Я могу описать процесс более детально, но в сети уже существует огромное количество литературы на эту тему.

Экспериментируйте не только с самыми очевидными входными данными, которые, скорее всего, обрабатываются и фильтруются должным образом. Используя утилиты наподобие burp (или даже сам браузер), мы можем изменять cookies, чтобы вызвать переполнение буфера.

Бреши наподобие CSRF также чрезвычайно часто встречаются во встроенных устройствах, использующих веб-интерфейсы. Эксплуатация подобных уязвимостей с целью записи в файлы или обхода систем аутентификации, как правило, приводит к получению полного контроля над роутером. Особенно в комбинации с брешами, связанными с инжектированием команд. Обход аутентификации в роутере с веб-интерфейсом, доступным через интернет, может стать основой удаленной атаки типа «человек посередине». Подобные типы атак также встречаются достаточно часто, однако описание деталей выходит за рамки этой статьи

Декомпиляция бинарных файлов

Когда вы декомпилируете бинарный файл вместо обычной ретрансляции машинные инструкции в ассемблерный код, декомпилятор использует различные алгоритмы для идентификации функций, циклов, ветвей и т.д. и показывает результат в виде кода на высокоуровневом языке, например, на С или Python.

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

Основываясь на своем небольшом опыте, могу сказать, что дополнительный слой абстракции в виде высокоуровневого языка, недостаточно надежен. В случае с дорогими декомпиляторами проблем может не возникнуть (IDA поддерживает пару архитектур), но я не нашел ни одного достойного декомпилятора, поддерживающего архитектуру MIPS. Кстати говоря, если хотите попробовать, рекомендую онлайн декомпилятор RetDec, который поддерживает множество архитектур, в том числе и MIPS.


Рисунок 18: Результаты декомпиляции в виде высокоуровневого языка

Даже на высокоуровневом языке декомпилированный код выглядит не очень.

Дальнейшие исследования

Вне зависимости от того хотим ли мы изучить найденный алгоритм или отладить эксплоит или найти уязвимость другого типа, возможность запуска (и, если возможно, отладки) бинарного файла в среде, которую мы полностью контролируем, была бы очень кстати. В большинстве случаев (наш не является исключением) отладка непосредственно на оборудовании невозможна. В следующей статье я расскажу об эмуляции процессора с целью отладки бинарных файлов в нашей собственной системе.

Советы и трюки

Как удалить ошибочные перекрестные ссылки (xref)

Иногда адрес загружается в 16/32-битный регистр с целью выравнивания. Содержимое этого адреса не влияет на остальной код. Просто обычное выравнивание. Если этот адрес, связанный с регистром, начинает указывать на корректные данные, IDA переименует адрес в ассемблерном коде и отобразит содержимое в комментариях.

Если x-ref’ы для вас не имеют смысла, выберите переменную и нажмите клавишу o с целью игнорирования содержимого и отображения только адреса. Таким образом, код станет более понятным.

Настройка прототипов функций, чтобы в IDA выводились аргументы рядом с вызовами

Установите курсор на функцию и нажмите клавишу y. Затем установите прототип функции, например: int memcpy(void *restrict dst, const void *restrict src, int n); Обратите внимание, что IDA понимает только встроенные типы. Нельзя использовать типы наподобие size_t.

И снова мы можем использовать объявления с ключевым словом extern из  исходного кода. Где возможно, находим объявления для конкретной функции и используем те же самые типы и имена в качестве аргументов в IDA.

Поиск нужной информации внутри исходного кода

Если мы, например, хотим выяснить первый и второй параметр функции ATP_DBSetPara, то иногда можем воспользоваться исходниками. Множество функций не реализованы внутри ядра или других компонентах, однако могут использоваться в одном из этих мест. Исходный код мы не увидим, но можем найти объявление с ключевым словом extern. В некоторых случаях в исходном коде можно найти комментарии или имена переменных. Очень полезная информация, которую нельзя получить после дизассемблирования.


Рисунок 19: Пример описания и объявление функции ATP_DBSetPara внутри исходного кода

К сожалению, в примере выше, документация на функцию оказалась не очень полезной. Видимо, были проблемы с кодировкой, и все написанной на китайском было утеряно. По крайней мере, теперь мы знаем, что первый аргумент – список ключей, второй – ParamCMO, являющийся константой в дизассемблированной версии и, скорее всего, представляет собой ссылку на ключ, который мы пытаемся установить.

Методы дизассемблирования: линейная развертка и рекурсивный спуск

Структура бинарного файла зависит от компилятора, разработчика и других факторов. Как следствие, дизассемблер не всегда «понимает» схему вызова функций, что приводит к появлению множества «бесхозных» функций, которые существуют в бинарном файле, но не имеют известной вызывающей функции.

Выбор дизассемблера зависит от того, хотите ли вы видеть подобные функции или нет. Часть этих методов чрезвычайно важна (например, функция ping в изученном ранее бинарном файле web).

  • При линейная развертке происходит побайтовое чтение. Все, что похоже на функцию, показывается пользователю. Этот метод требует серьезной проработки алгоритма, чтобы снизить количество ошибок.
  • При рекурсивном спуске мы ищем все функции, вызываемые из функции main(). Затем находим функции, вызываемые в найденных функциях и далее продолжаем рекурсивное отображение пока не найдем все функции. Этот метод очень надежный, но не отображает функции, на которые нет стандартной/прямой ссылки.

Если вам кажется, что часть данных утеряна, пользуйтесь дизассемблером, который поддерживает метод линейной развертки. С другой стороны, при использовании этого метода не забывайте, что дизассемблированный код должен выглядеть более менее логично.

Домашний Wi-Fi – ваша крепость или картонный домик?

Узнайте, как построить неприступную стену