При разработке кастомных прошивок для роутера, часто сталкиваемся с необходимостью подделать подпись, чтобы загрузить прошивку через стандартный веб-интерфейс. Это требует понимания процесса проверки образа в стандартной прошивке. Давайте погрузимся в реверс прошивок роутера, используя роутер D-Link DIR-806A B1 в качестве примера и поймем, как работает проверка подписи.
Еще по теме: Как извлечь образ прошивки с помощью Binwalk
Реверс прошивок роутера
D-Link DIR-806A B1 имеет 8 Мбайт флеш-памяти и 64 Мб перативки. Работает на базе чипа MediaTek MT7620A, с архитектурой MIPS и поддерживаемого ядром Linux. Загрузка стандартной прошивки осуществляется через U-Boot, имеющий встроенный клиент TFTP для восстановления, если случайно загрузить неудачную прошивку. Главное — сохранить работоспособность загрузчика, иначе придется прибегнуть к пайке.
На мой взгляд, идеальное устройство для тестов. Однако, один из недостатков — в стандартной комплектации DIR-806A нет USB. Но на плате присутствуют контакты для USB, так что при достаточном уровне умений и сноровки, порт можно добавить самостоятельно.
Начнем с подключения к UART, поэтому убедитесь, что у вас есть преобразователь уровней для UART. При подключении не забудьте «перекрестить» RX и TX. То есть, RX подключаем к TX, а TX — к RX. Настройки соединения — 57600 8N1.
Подключились к UART. Давайте попробуем прошить нашу «жертву», при этом будем наблюдать за консолью. Возможно, там есть строки, которые помогут нам найти части кода, ответственные за процесс прошивки?
1 2 |
signallin(6) start... mtd: "Linux" libmtd (_mtd_write_ex): to "/dev/mtd6", size: 0x6c4764, offset: 0x0, buffer: 0x2afa6000 |
Успех! Роутер выводит в консоль сообщение о начале обновления и указывает раздел, куда записывается прошивка. При анализе будем искать строки, а по строкам — место в коде. Классический подход! Попробуем найти строку start… в прошивке:
1 2 |
# grep -nr "start..." /sbin /sbin/fw_updater:12411:(%d) start... mtd: "%s" |
Мы нашли утилиту fw_updater. Попробуем её запустить:
1 2 3 |
fw_updater (6) usage: fwupdater |
Кажется, именно она применяет новую прошивку в разделе Linux.
Если бы не было никакого текста, пришлось бы анализировать сам веб-интерфейс. Там обязательно есть сообщения, коды ошибок и т.д., которые затем можно искать. Оттуда же можно выйти на утилиты прошивки. Либо наугад поискать утилиты, в названиях которых есть слова fw, firmware, update и т.п. Если утилита найдена, остается только разобрать ее в IDA Pro, Ghidra, Radare2 или любом другом реверс-инжиниринговом инструменте.
Теперь попробуем найти, откуда fw_updater вызывается в прошивке:
1 2 3 4 |
grep -nr "fw_updater" /lib /lib/libdhal.so:88250:/sbin/fw_updater /lib/libdhal.so:88254:/tmp/fw_updater |
Мы обнаружили, что fw_updater используется в библиотеке libdhal.so. Это очень любопытно. Попробуем взглянуть, что у нее внутри. Для реверса будем использовать Ghidra. Она позволяет преобразовать бинарный исполняемый файл в код на С. Это очень удобно, и не придется копаться в ассемблерном листинге.
Еще нам понадобится утилита, которая поможет перетащить нужный файл на компьютер. Можно воспользоваться любым средством пересылки файлов по сети. Если ничего подходящего на роутере нет, то можно распаковать прошивку на компьютере тем же binwalk. Универсальных сценариев не существует.
В нашем случае есть netcat. Перекидываем libdhal.so на компьютер:
1 |
cat /lib/libdhal.so | nc 10.0.0.245 5000 |
Создаем проект в Ghidra и дизассемблируем библиотеку. После беглого изучения нетрудно найти функцию с говорящим названием — check_firmware_in_buffer. Вот ее листинг на С:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
undefined4 check_firmware_in_buffer(int param_1,int param_2,undefined4 param_3,undefined4 param_4) { uint uVar1; ulong uVar2; int iVar3; char *pcVar4; char local_98; char local_97; undefined local_96; undefined local_95 [16]; undefined auStack_85 [17]; undefined auStack_74 [92]; logmessage("check_firmware","Check signature in the firmware",param_3,param_4); if (param_2 < 0x80) { pcVar4 = "Too small fw"; } else { uVar1 = *(uint *)(param_1 + param_2 + -4); if ((uVar1 >> 0x18 | uVar1 >> 8 & 0xff00 | uVar1 << 0x18 | (uVar1 & 0xff00) << 8) == 0xc0ffee) { local_96 = 0; pcVar4 = "cef285a2e29e40b2baab31277d44298b"; do { local_97 = pcVar4[1]; local_98 = *pcVar4; uVar2 = strtoul(&local_98,(char **)0x0,0x10); local_95[(uint)(pcVar4 + -0x83218) >> 1] = (char)uVar2; pcVar4 = pcVar4 + 2; } while (pcVar4 != ""); md5_init(auStack_74); md5_append(auStack_74,local_95,0x10); md5_append(auStack_74,param_1,param_2 + -0x14); md5_finish(auStack_74,auStack_85); param_3 = 0x10; iVar3 = memcmp((void *)(param_1 + param_2 + -0x14),auStack_85,0x10); if (iVar3 == 0) { logmessage("check_firmware","Signature OK!",param_3,param_4); return 2; } pcVar4 = "Wrong signature!"; } else { pcVar4 = "Wrong magic or version"; } } logmessage("check_firmware",pcVar4,param_3,param_4); return 0; } |
Именно в этой функции принимается решение о том, подходит прошивка для роутера или нет. Проверяется три вещи: размер образа, магическое число и хеш MD5.
После анализа этого кода приходим к выводу, что:
- param_1 — указатель на буфер с файлом прошивки;
- param_2 — размер буфера.
Взглянем вот на эту строчку:
1 |
uVar1 = *(uint *)(param_1 + param_2 + -4); |
Нетрудно понять, что магическое число располагается в последних четырех байтах прошивки и равно 0xc0ffee.
Теперь разберемся с хешем:
1 2 |
md5_append(auStack_74,local_95,0x10); md5_append(auStack_74,param_1,param_2 + -0x14); |
Видим, что хеш считается из массива local_95 размером 16 байт и файла прошивки (за исключением 20 байт в конце). 20 байт здесь — это размер MD5 плюс 4 байта магического числа. Массив local_95 строится из параметра pcVar4, в котором содержится UUID устройства.
Что происходит в этом куске кода:
1 2 3 |
local_98 = *pcVar4; uVar2 = strtoul(&local_98,(char **)0x0,0x10); local_95[(uint)(pcVar4 + -0x83218) >> 1] = (char)uVar2; |
Это конвертация байта из текстового представления в машинный код.
Самое время проверить, так ли мы хороши. Напишем скрипт на Python для расчета MD5:
1 2 3 4 5 6 7 8 |
import sys, os, hashlib size = os.path.getsize(sys.argv[1]) with open(sys.argv[1], "rb") as f: data = f.read(size - 20) hash_md5 = hashlib.md5() hash_md5.update(bytes.fromhex("cef285a2e29e40b2baab31277d44298b")) hash_md5.update(data) print(hash_md5.hexdigest()) |
На вход скрипт принимает прошивку. Результат выполнения ниже:
1 2 |
user@debian:~/md5$ python3 md5.py 2019.03.19-18.04_DIR_806A_MT7620A_3.0.1_release.bin e5fd006108c91a7fd4e43b23575fa7cd |
Сравнив полученный хеш с исходной прошивкой по смещению 0x6c4750, понимаем, что хеш рассчитан правильно.
Теперь мы легко можем создать прошивку, которая будет загружаться через стоковый веб‑интерфейс.
Хеш MD5, а тем более, как в нашем случае, сложенный из UUID и образа, встречается редко. Конкретная реализация будет зависеть от производителя, и тут кто во что горазд. Например, у отечественных вендоров вроде SNR или Keenetic с этим проще. В SNR заменили расчет суммы CRC32 ядра на CRC32 всей прошивки. А в Keenetic прописывают в прошивке магическое число, CRC32 и ID устройства.
Заключение
Примерно так же по сложности обстоят дела с прошивками роутеров Xiaomi. Там тоже есть и минимальный размер прошивки, и магическое число. Только вместо MD5 используется RSA. В общем, как повезет. Но еще не встречалась подпись, которую нельзя было бы отреверсить.
ПОЛЕЗНЫЕ ССЫЛКИ: