[recovery mode] Ручная правка uboot-elf во имя DHCP и SSH
Как-то раз мне попалась в руки железка AEWIN SCB-3240, которой следовало навеки поселиться в серверной стойке с целью никогда больше к ней не прикасаться. Использовать её планировалось не по прямому назначению, а с целью тестирования нашего продукта. Есть у Лаборатории Касперского традиция — выпускать антивирусный SDK для всех мыслимых платформ, лишь бы было там что-то, способное компилировать код на C. Соответственно, SDK нужны антивирусные базы, которые, несмотря на то, что они едины для всех продуктов, все же надо тестировать — на случай ошибки в самом SDK, или загрузчике баз, или каких-то особенностей платформы, или… В общем, миллион причин. И чтобы о проблемах мы узнавали немного раньше, чем из новых кейсов в саппорт, последней линией обороны стоят десятки железок, проверяющих каждый набор антивирусных баз на работоспособность.
То есть, интерес представляли не возможности AEWIN по работе с сетью, а исключительно её суть в виде MIPS / Linux. Проблема была в том, что железка не предусматривала никакого адекватного присоединения. Все, что мне предлагалось — консольный порт, telnet и никакого dhcp.
К сожалению, самые страшные опасения оказались не напрасными. У устройства не было никакого постоянного хранилища, и оно жило только от включения до перезагрузки, каждый раз разворачивая референсный образ.
Итого, что мне было нужно. Научить получать адрес по DHCP, научить её принимать подключения по SSH, и постараться не сломать по дороге.
Часть первая. SSH.
В качестве SSH-сервера решено было взять dropbear. На самом деле, никакого сравнительного тестирования и разбора фич не проводилось — выбрал то, с чем уже работал. Для первого шага нам понадобятся исходники dropbear — https://matt.ucc.asn.au/dropbear/ и OcteonSDK — последний доступен только для партнеров, так что, будем считать, он у нас уже есть.
Сам процесс прост. Распаковываем toolchain из состава SDK, распаковываем исходники dropbear, собираемся:
export CC=/home/pony/octeon_sdk/tools-gcc-4.1/bin/mips64-octeon-linux-gnu-gcc
./configure --host=mips64-octeon-linux-gnu --prefix=/home/pony/dropbear --exec-prefix=/home/pony/dropbear/bin --disable-zlib
make PROGRAMS="dropbear dbclient dropbearkey dropbearconvert scp"
make install
Забираем из /home/pony/dropbear/bin бинари
Часть вторая. Прошивка.
Тут нам понадобится TFTP-сервер и, для облегчения непосильного труда, OpenDHCP-сервер.
Подключаем железку первым сетевым портом (так быстрей получит ip) к рабочей машине, запускаем TFTP+OpenDHCP
Подключаемся к консольному порту — в моем случае это была пара CiscoConsole + TrendNet COM2USB, включаем железку. Пропускаем первый «Hit any key to stop autoboot», ждем второго —
Interface 3 has 4 ports (LOOP)
Interface 4 has 1 ports (AGL)
Hit any key to stop autoboot: 0
Получаем адрес по DHCP
Octeon aewin3240(ram)# dhcp
Смотрим, как оно загружается:
Octeon aewin3240(ram)# grepenv boot
bootcmd=fatload mmc 1:2 0x100000 vmlinux.64;bootoctlinux 0x100000 coremask=0xf mem=0
То есть образ системы хранится на внутренней MMC-карте. Нам надо этот образ забрать себе.
Считываем образ с железки в память — название и путь с файлу у нас есть (mmc 1:2 vmlinux.64), судя по команде загрузки, ожидается, что адрес 0x100000 свободен:
Octeon aewin3240(ram)# fatload mmc 1:2 0x100000 vmlinux.64
Заливаем его на TFTP (размер мы получаем как результат в консоли от предыдущего шага, надо только перевести его в hex):
Octeon aewin3240(ram)# tftpput 0x100000 0x332C650 192.168.0.1:vmlinux.64
Часть третья. Вивисекция.
Полученный файл — это самораспаковывающийся ELF, c образом initramfs в комплекте, который так же распаковывается вместе с ядром на этапе загрузки. Соответственно, нам нужно получить этот образ, внести в него нужные правки и собрать обратно. К сожалению, оригинала — с ядром и прочими файлами, из которых собирался ELF, у меня нет. Поэтому придется расчехлять пилу и хирургический клей. Посмотрим, что лежит в файле —
root@ponybuntu:/home/root/uboot# objdump -h vmlinux.64
Sections:
Idx Name Size VMA LMA File off Algn
…
11 .init.text 0002a534 ffffffff8090e000 ffffffff8090e000 0080f000 2**5
CONTENTS, ALLOC, LOAD, READONLY, CODE
12 .init.data 02aee0d0 ffffffff80938540 ffffffff80938540 00839540 2**5
CONTENTS, ALLOC, LOAD, DATA
13 .exit.text 00001920 ffffffff83426610 ffffffff83426610 03327610 2**2
CONTENTS, ALLOC, LOAD, READONLY, CODE
…
единственный кандидат на initramfs — секция .init.data с размером в 0x02aee0d0 = 45015248 байт
Для начала пойдем на поклон к Гентушникам и узнаем, что что образы initramfs хранятся в gzip. Ищем заголовки от GZIP-формата внутри образа:
root@ponybuntu:/home/root/uboot# binwalk vmlinux.64 | grep gzip
5962904 0x5AFC98 gzip compressed data, from Unix, NULL date: Thu Jan 1 03:00:00 1970, max compression
8705696 0x84D6A0 gzip compressed data, from Unix, last modified: Mon Feb 2 19:15:12 2015, max compression
Только 0x84D6A0 подходит по оффсету под .init.data — пробуем его вытащить. Архив явно не выходит за пределы секции, поэтому размер — адрес начала следующей секции минус адрес начала gzip:
0x03327610 — 0x0084D6A0 = 0x02AD9F70 = 44932976 bytes. Вырезаем его:
root@ponybuntu:/home/root/uboot# dd if=vmlinux.64 bs=1 oflag=seek_bytes skip=8705696 count=44932976 > ramfs.gz.orig
Распаковываем:
root@ponybuntu:/home/root/uboot# gunzip < ramfs.gz.orig > initramfs.cpio
gzip: stdin: decompression OK, trailing garbage ignored
И вот тут нас поджидает фейл. Судя по «trailing garbage ignored», не все в вырезанном куске оказалось архивом. Нам это еще аукнется, но пока, предположим, все в порядке.
Запоминаем размер initramfs.cpio = 143785984
Часть четвертая. Готовим образ.
Монтируем initramfs во временную папку:
root@ponybuntu:/tmp/ramfs# cpio -i -d -H newc --no-absolute-filenames < initramfs.cpio
root@ponybuntu:/tmp/ramfs# ls
bin dev etc examples home init lib lib32 lib32-fp lib64 lib64-fp linuxrc mnt mnth proc root sbin share sys tmp usr var
Складываем в /bin и /sbin бинари Dropbear и настраиваем его автозапуск + поднятие eth0 + получение IP в /sbin/rc — добавляем
echo Setting up network
ifconfig eth0 up
udhcpc -i eth0
#Start sshd if it exists
if [ -e /sbin/dropbear ]; then
echo Generating Drpbear keys
if [ ! -e /etc/ssh_host_rsa_key ]; then
/bin/dropbearkey -t rsa -f /etc/ssh_host_rsa_key
fi
if [ ! -e /etc/ssh_host_dsa_key ]; then
/bin/dropbearkey -t dss -f /etc/ssh_host_dsa_key
fi
echo Starting Dropbear
/sbin/dropbear -d /etc/ssh_host_dsa_key -r /etc/ssh_host_rsa_key
fi
Вычищаем /examples — трудолюбивые китайцы забыли это сделать, что дает нам лишнее место, которое можно использовать под наши файлы, чтобы итоговый архив не вылезал за пределы оригинального ELF
Собираем это обратно в образ initramfs:
root@ponybuntu:/tmp/ramfs# find. | cpio -H newc -o > /home/root/uboot/initramfs.cpio
Упаковываем образ в gzip:
root@ponybuntu:/home/root/uboot# gzip --best < initramfs.cpio > ramfs
Вклеиваем получившийся gzip на место старого:
root@ponybuntu:/home/root/uboot# dd conv=notrunc if=ramfs of=vmlinux.64 oflag=seek_bytes seek=8705696
87758+1 records in
87758+1 records out
44932351 bytes (45 MB) copied, 0.130304 s, 345 MB/s
Вот здесь я забегу вперед. Этот образ после заливки на железку достаточно быстро падает в kernel panic с жалобами на распаковку. И дело определенно в том, что «распаковщик» из ELFа не так флегматичен, как его коллега gzip, и явно ожидает, что будет знать размер архива абсолютно точно. Воспоминаем, что выше мы уже получали «trailing garbage», тонко намекавшее нам на то, что не все, что мы посчитали архивом, было таковым. По спекам gzip, последняя запись в архиве — размер неупакованных данных в LittleEndian. Вспоминаем размер initramfs.cpio = 143785984 = 0x08920000. Ищем это в предполагаемом конце архива:
root@ponybuntu:/home/root/uboot# tail -c 24 ramfs.gz.orig | hexdump -C
00000000 1f ff a9 e3 5f 8e 5b 98 18 00 00 92 08 00 00 00 |...._.[.........|
00000010 00 00 00 00 02 ad 9f 65 |.......e|
Находим: 00 00 92 08 — на 11 байт раньше актуального конца файла.
Значит, настоящий размер gzip с initramfs = 44932976 — 11 = 44932965 = 0x02AD9F65. Смотрим выше и видим, что последние байты изначально вытащенного нами архива — как раз и есть 02 ad 9f 65. Значит, «от начала архива и до конца секции .init.data» помещается не только архив, но и немножко нулей и размер архива. Который, видимо, считывается ядром при загрузке, и делается попытка распаковки.
Подготовленный выше «наш» архив с initramfs немного меньше чем оригинал — значит, мы можем вклеить его на место старого, а потом подправить размер в конце секции:
root@ponybuntu:/home/root/uboot# dd conv=notrunc if=ramfs of=vmlinux.64 oflag=seek_bytes seek=8705696
87758+1 records in
87758+1 records out
44932351 bytes (45 MB) copied, 0.130304 s, 345 MB/s
Проверяем, что от наших манипуляций файл не сломался:
root@ponybuntu:/home/root/uboot# readelf -h vmlinux.64
ELF Header:
Magic: 7f 45 4c 46 02 02 01 00 00 00 00 00 00 00 00 00
Class: ELF64
Data: 2's complement, big endian
Version: 1 (current)
OS/ABI: UNIX — System V
ABI Version: 0
Type: EXEC (Executable file)
Machine: MIPS R3000
Version: 0x1
Entry point address: 0xffffffff8067f330
Start of program headers: 64 (bytes into file)
Start of section headers: 53657872 (bytes into file)
Flags: 0x808b0001, noreorder, octeon, mips64r2
Size of this header: 64 (bytes)
Size of program headers: 56 (bytes)
Number of program headers: 2
Size of section headers: 64 (bytes)
Number of section headers: 21
Section header string table index: 20
Жалоб нет, будем надеяться, что все работает. Теперь нам надо подправить ожидаемый ядром размер архива. Тут я смалодушничал и воспользовался виндовым HexEdit, с мышкой и гуями — переходим по адресу 0x0332760C и меняем 02 AD 9F 65 на размер нашего нового архива — 44932351 = 02 AD 9C FF
Сохраняем, кладем на tftp-сервер, включаем железку, снова добираемся до второго Hit any key to stop autoboot, снова получаем адрес по dhcp. Скачиваем новый образ с tftp в память:
Octeon aewin3240(ram)# tftp 0x100000 192.168.0.1:vmlinux.64
Using octeth0 device
TFTP from server 192.168.0.1; our IP address is 192.168.0.2
Filename 'vmlinux.64'.
Load address: 0x100000
Loading: ##################################################
10.4 MiB/s
done
Bytes transferred = 53659216 (332c650 hex)
Вспоминаем, как оно раньше загружалось:
Octeon aewin3240(ram)# grepenv boot
bootcmd=fatload mmc 1:2 0x100000 vmlinux.64;bootoctlinux 0x100000 coremask=0xf mem=0
Считывать с флешки не нужно, образ уже в памяти по пути 0x100000, поэтому только запускаем — bootoctlinux 0x100000 coremask=0xf mem=0, видим:
BusyBox v1.20.2 (2015-02-02 22:37:53 CST) built-in shell (ash)
Enter 'help' for a list of built-in commands.
~ #
Отлично, оно грузится. SSH работает. Можно заменять оригинальный образ на наш.
Снова перегружаем железку до стадии «получен dhcp», снова скачиваем новый образ, записываем его из памяти на MMC рядом с оригиналом. На всякий случай:
Octeon aewin3240(ram)# fatwrite mmc 1:2 0x100000 vmlinux.64.new 332c650
writing vmlinux.64.new
53659216 bytes written
Меняем команду загрузки железки:
Octeon aewin3240(ram)# setenv bootcmd 'fatload mmc 1:2 0x100000 vmlinux.64.new;bootoctlinux 0x100000 coremask=0xf mem=0'
Octeon aewin3240(ram)# saveenv
Done.
Я не знаю, поможет ли эта success-story кому-то в будущем, но если бы я наткнулся на нечто подобное, мне это сэкономило бы день-другой, потраченный на гугл. Подавляющее большинство мануалов, найденных в сети, рассчитаны на наличие оригинальных файлов — ядра, описаний и прочего, из чего можно собрать u-boot-образ одной командой.