
Раскрытие уязвимостей камер Accfly: CVE-2020-25782, CVE-2020-25783, CVE-2020-25784, CVE-2020-25785.
В начале 2020 года, на моём прежнем месте работы, мне довелось принять участие во внутреннем мероприятии в стиле pwn2own. Было несколько доступных целей, но больше всего меня заинтересовала беспроводная охранная камера Accfly. К сожалению, я не успел завершить своё исследование к самому мероприятию, но, поскольку других попыток взломать это устройство не было, я продолжил работу.
Основное внимание в исследовании было уделено уязвимостям, которые могут привести к удалённому выполнению кода (RCE). Такой тип уязвимости позволяет атакующему получить полный контроль над устройством, а в случае видеокамеры может привести к полной компрометации конфиденциальности владельца. К сожалению, прошивка устройства оказалась нашпигована подобными проблемами.
Во-первых, устройство не предоставляет никакой аутентификации. В результате атакующий, способный подключиться к нему, может свободно получить к нему доступ и перенастроить его. В простейшей форме можно постоянно перезагружать устройство, делая его полностью непригодным для легитимного пользователя. Масштаб этой атаки несколько ограничен, поскольку устройство предназначено для использования в сети Wi-Fi, обычно за NAT, и, таким образом, недоступно напрямую из Интернета. Однако отсутствие шифрования между устройством и приложением на смартфоне владельца, а также использование сервера производителя в качестве прокси для связи, создаёт возможность для атак MitM или манипуляций с DNS, которые могут обойти ограничение NAT в Wi-Fi.
Кроме того, приложение использует проприетарный бинарный протокол для связи. Он реализован на смеси C и C++ и, как выяснилось, полон небезопасных функций обработки строк. Основной исполняемый файл содержит огромное количество неиспользуемого кода, что говорит о том, что он повторно используется на других устройствах. Это усложняет сопровождение и увеличивает поверхность атаки. Приложение не включает никаких современных механизмов безопасности, которые защитили бы его от многих распространённых техник эксплуатации. Более того, оно даже не ограничивает права пользователя, работая от root — с максимально доступными привилегиями.
В результате этого исследования были задокументированы следующие четыре уязвимости.
CNetClientManage::ServerIP_Proto_Set при обработке входящих сообщенийCNetClientTalk::OprMsg при обработке входящих сообщенийCNetClientGuard::SubOprMsg при обработке входящих сообщенийCFtpProtocol::FtpLogin во время процедуры обновленияДля трёх из них были разработаны эксплойты RCE, которые позволяют атакующему получить полный контроль над устройством. Однако из-за отсутствия ответа производителя на попытки сообщить об уязвимостях этот репозиторий содержит только ограниченные PoC-эксплойты, которые просто вызывают крах приложения.
Проблемы были обнаружены в версии ПО V3.10.73 и подтверждены в версии ПО V4.15.77 — последней доступной на момент публикации (26 января 2021 года).
Если у вас возникнут вопросы, не стесняйтесь связаться со мной по электронной почте (см. git-коммит) или через GitHub issues. Если у вас есть IoT-устройство, которое, по вашему мнению, может быть интересно взломать, вы ищете исследователя безопасности или просто хотите поздороваться, я буду рад услышать вас. Вы также можете угостить меня кофе!
Целевое устройство — это видеокамера, управляемая сопутствующим мобильным приложением. Мой анализ начался с сетевого трафика камеры и продолжился изучением её прошивки. Для всей связи используется собственный бинарный протокол. Команды либо отправляются напрямую на мобильное устройство, когда оно находится в той же сети, либо проходят через сервер производителя устройства. Само программное обеспечение камеры прослушивает несколько портов: TCP (23456,34567) и UDP (34568, 34569). Сетевой трафик не шифруется и не аутентифицируется, что допускает MitM-атаки или прямой доступ, когда камера доступна в сети. Вероятно, доступ к видеопотоку также возможен без аутентификации, но я недостаточно изучил проприетарный протокол, чтобы проверить это.
После краткого обзора взаимодействия следующим шагом была попытка получить доступ к прошивке устройства. Моей первой попыткой было скачать её напрямую, перехватив процесс обновления устройства, но ничего подобного в сетевом трафике не происходило. Я бы застрял на этом этапе, если бы не своевременная помощь коллеги, который извлёк прошивку из флеш-памяти, что позволило мне продолжить исследование.
Выяснилось, что прошивка запускает Linux на MIPS-процессоре с little-endian порядком байтов. Есть ровно один интересный процесс, называемый Alloca, который отвечает за захват видео, а также обрабатывает все сетевые взаимодействия. Приложение написано на C++ и содержит много кода, который на этом устройстве не используется. Это указывает на то, что то же самое ПО используется и на других устройствах.
Хотя эта проблема была обнаружена последней, она имеет решающее значение для реальной эксплуатации большинства остальных, поскольку все они проистекают из использования небезопасных строковых функций языка C. Хотя существует несколько техник, которые можно использовать для успешного выполнения кода в аналогичных сценариях, приложение создано таким образом, что они в основном бесполезны. Основная проблема в том, что код и данные Alloca статически размещаются по низким адресам ( < 0x01000000). Поэтому попытки повторного использования существующего кода (то есть ROP и подобных) бесполезны, поскольку они требуют возможности записывать адреса в память программы. Поскольку строки языка C используют \x00 в качестве завершающего символа, а строковые функции прекращают обработку на первом таком байте, невозможно использовать более одного NULL-байта. Кроме того, расположение стека рандомизировано, а приложение интенсивно использует многопоточность, что делает другие техники гораздо менее надёжными.
Эта уязвимость является результатом совместного использования данных несколькими потоками и небезопасного использования strcpy. Хотя я долго анализировал эту конкретную проблему, мне не удалось заметить возможность использовать её в качестве вектора утечки данных вплоть до последних нескольких недель перед этой публикацией. Что довольно интересно, благодаря утечке адреса кучи C++-объекта эта уязвимость также позволяет выполнять удалённый код. Однако эта атака не включена в данный отчёт.
Приложение Alloca может обновлять себя через FTP. Эту операцию может запросить сервер, который также предоставляет необходимые имя пользователя, пароль и имя файла. Функция, которая запускает обновление, показана здесь:

Три вызова strcpy, очевидно, небезопасны и приводят к переполнению кучи, поскольку объект ftpUpgrade выделяется динамически. К сожалению, порядок выполнения копирований и структура объекта ftpUpgrade делают невозможным реальный запуск потока, который утечёт данные. Более внимательный взгляд на входящий пакет выявляет следующую структуру:
struct ftp_upgrade_pkt {
struct pktHeader;
char username[16];
char password[16];
char filename[128];
}
в то время как объект ftpUpgrade выглядит примерно так:
struct CNetClientFtpUpgrade {
// ... something here
char filename[128];
char unknown[6];
char username[16];
char password[16];
CFtpDownlad *;
CNetClientConnect*;
int something[5];
bool threadRunning;
// ... and more
}
Утечка может произойти после того, как один из внутренних указателей (CFtpDownload*, CNetClientConnect*) будет заполнен приложением. Кроме того, имя пользователя и пароль копируются (ещё раз, но на этот раз безопасно) в только что созданный объект до того, как его указатель сохраняется в месте, доступном для утечки, поэтому утечь может только filename. В результате имя файла должно быть очень длинным, но из-за порядка и поведения strcpy при завершении строк достаточно длинное имя файла приведёт к ещё более длинным имени пользователя и паролю, которые в итоге перезапишут threadRunning и вообще не запустят поток.
Если бы этот код был однопоточным, мало что можно было бы сделать. Но поскольку новый поток FtpDownload порождается и выполняет функцию DownloadFile, это открывает интересную возможность, так как он разделяет объект CNetClientFtpUpgrade с потоком, обрабатывающим входящие пакеты. Он не только выполняет несколько операций ввода-вывода, которые можно контролировать извне (DNS-запросы, обработка FTP-соединений), но и пытается подключиться к FTP до 10 раз (это делается в вызывающем коде DownloadFile). Это позволяет управлять выполнением потока FtpDownload (блокируя его на операциях ввода-вывода), тем самым давая потоку обработки сообщений время на обработку других запросов.

Короче говоря, просто отправляя несколько запросов на обновление, можно изменить filename (и другие параметры), используемые уже запущенным потоком FtpDownload, и получить утёкший адрес кучи. В качестве бонуса функция FtpSize (выделена зелёным) использует буфер внутри объекта, на который ссылается утёкший адрес, для хранения самого filename, что позволяет тривиально внедрить шеллкод первой стадии. Единственные ограничения здесь — длина и отсутствие NULL-байтов из-за использования strcpy. Предоставлен пример PoC, который просто утекает адрес кучи с устройства.
неаутентифицированное переполнение буфера в стеке в функции CNetClientManage::ServerIP_Proto_Set
Полное отсутствие аутентификации при обработке входящего трафика направило меня на поиск обработчиков пакетов. Одна из интересных функций — ServerIP_Proto_Set. Похоже, она используется для создания статического переопределения разрешения DNS. Я не нашёл способа перенаправить трафик таким образом, но здесь есть ещё одно переполнение буфера (выделено оранжевым).

Данные, напрямую читаемые из пакета, используются внутри функции sprintf. В этом случае предполагается, что данные из пакета поместятся в 16-байтовый буфер, но использование простого формата %s позволяет записать столько байт, сколько пожелает атакующий, при условии, что они не содержат NULL.
Эта уязвимость довольно ограничена. Хотя на стек можно записать много данных, так что использование NOP-sledge могло бы сработать, записать NULL-байты невозможно. Даже попытка записать один-единственный NULL-байт провалится, поскольку формат функции sprintf предваряет его символом \n. Ещё одно препятствие — объект CMutex, который хранится после буфера. Любая попытка переполнения должна заполнить этот мьютекс корректным значением (или, по крайней мере, таким, которое удовлетворит деструктор CGuard — выделен красным). Это проблематично, поскольку деструктор дважды разыменовывает переданную переменную, а затем использует её значение в вызове pthread_mutex_unlock. После некоторых экспериментов я выяснил, что буфера, заполненного NULL, достаточно для корректного возврата из pthread_mutex_unlock, но его всё равно нужно разыменовать в корректный адрес памяти.
На помощь приходит утечка. Атака немного сложна, поскольку нам нужен адрес кучи, не содержащий NULL-байтов. К счастью, поиск упрощается тем, что устройство предоставляет нам возможность удалённой перезагрузки без аутентификации. Каждый раз предоставляя другое выделение адресного пространства кучи. Так что можно просто перезагружать устройство и утекать адрес, пока не будет найден подходящий. Удобно, что это также позволяет сохранить короткую первую стадию шеллкода. Поскольку нам нужно преодолеть проблему с мьютексом (нужен указатель на указатель), мы утекаем ещё один адрес, на этот раз передавая ранее утёкший адрес в качестве имени файла. Следующая диаграмма показывает ожидаемую компоновку памяти:

Если всё пойдёт по плану, можно передать второй адрес как мьютекс, а первый — как адрес возврата. Однако для простого краха приложения это не требуется, как и сделано в PoC.
неаутентифицированное переполнение буфера в куче в функции CNetClientTalk::OprMsg
Предполагается, что устройство поддерживает двустороннюю голосовую связь. Ещё один обработчик входящих пакетов, похоже, отвечает за приём и воспроизведение аудио. Сетевой пакет audio_pkt_hdr описывается следующей структурой:
struct audio_pkt_hdr {
struct pktHeader field_0x0;
int field_0x14
int field_0x18
int field_0x1c
char audioBuff[0x140];
}
Одно из полей структуры pktHeader — длина пакета (в том виде, в котором он передаётся по сети). Это поле может быть свободно задано отправителем. Уязвимая часть — копирование данных непосредственно из входящего пакета с использованием недоверенного значения длины, указанного в заголовке входящего пакета.

Как можно видеть, объект CNetClientTalk создаётся следующим конструктором:

поэтому указанный выше вызов memcpy приводит к переполнению буфера в куче. К сожалению, реальная эксплуатация этой проблемы довольно сложна. Хотя можно многократно перезаписывать кучу, я не нашёл способа контролировать, какие данные будут сохранены в куче после переполняемого буфера. Поскольку в приложении более 50 активных потоков, некоторые из которых отвечают за обработку аудио и видео, оно постоянно выделяет и освобождает память. В результате данные в куче постоянно меняются, что затрудняет прогнозирование того, что хранится после буфера, и его корректную перезапись.
неаутентифицированное переполнение буфера в стеке в функции CNetClientGuard::SubOprMsg
А вот ещё один обработчик входящих пакетов. На этот раз сетевой пакет имеет следующую структуру (общий заголовок пакета опущен):
struct pkt_hdr_sub_cliGuard {
dword deviceId;
dword userId;
dword magic;
dword subCmd;
dword field_0x10;
dword field_0x14;
dword field_0x18;
dword field_0x1c;
dword guard_icommand;
dword moreThenRandomStackValue;
dword itemCnt;
char array_of_0x18[24];
};
Опять же, интересная часть — последний массив (поскольку мы можем увеличивать этот пакет сколько угодно), который содержит некую внутреннюю структуру размером 24. Уязвимость проистекает из предположения, что полученный itemCnt не превысит 6, поскольку буфер назначения копирования имеет размер 144 (=24*6), что видно на следующем листинге (выделено оранжевым):

На этот раз копирование выполняется с помощью memcpy (выделено зелёным), поэтому ограничений на допустимые символы нет. Копирование выполняется частями в цикле while (отмечен синим). Стоит заметить, что счётчик cnt_v0 уменьшается внутри цикла, поэтому части копируются в обратном порядке. С учётом переменных, следующих за уязвимым буфером buf, для переполнения необходимо 256 байт, затем 4 регистра ($s0-$s3) и $ra. Поскольку мы не знаем компоновку памяти, код PoC использует технику ROP. Используется один гаджет, который воспроизводит один из встроенных звуков устройства (и вызывает крах).
неаутентифицированное переполнение буфера в стеке в функции CFtpProtocol::FtpLogin
Одним из первых направлений моего анализа был поиск процедуры обновления. Как я выяснил, устройство имеет функцию обновления через FTP, которую можно запустить, отправив запрос на обновление, и которая приводит к загрузке прошивки с внешнего FTP-сайта. Как и в случае с другими уязвимостями, перед запросом обновления устройства не требуется аутентификация. Углублённый анализ FTP-функциональности выявил переполнение буфера в стеке в функции CFtpProtocol::FtpLogin. Как мы видим на декомпилированном листинге ниже, функция передаёт массив char размером 256 в функцию FtpPwd.

FtpPwd используется для получения текущей рабочей директории с FTP-сервера. Она заполняет свой внутренний буфер ответом объёмом до 1500 байт, а затем копирует их в предоставленный буфер. Эта последовательность вызовов приводит к переполнению на 1242 байта. В этом случае допустимые символы очень ограничены, поскольку использование " (двойной кавычки) привело бы к сокращению входной строки (strchr используется для поиска char в C-строке) и не вызвало бы переполнения буфера. К счастью, достаточно доставить один-единственный адрес, на который будет перенаправлено выполнение кода.

Для эксплуатации этой уязвимости необходимо либо контролировать DNS, либо перенаправить (или перехватить с помощью MitM) соединение с FTP-сервером. В приложении нет никаких современных средств защиты, поэтому можно выполнять код непосредственно со стека. Без утечки адреса лучшее, что можно сделать, — либо угадать расположение стека, либо перенаправить выполнение на одну функцию, которая затем вызовет крах приложения. Моей первой попыткой было именно это — воспроизведение одного из встроенных звуков, что и представлено в виде PoC. Используя утечку, можно получить полный контроль над устройством.