У китайцев особенное представление о копирайте — у них он просто не действует. В то же время свои наработки они защищают различными техническими средствами, почему-то «забывая» делиться ими со своими клиентами. Казалось бы, ситуация безвыходная: поступила партия китайских планшетов, и встала задача прошить их таким образом, чтобы контент заказчика не стирался при сбросе настроек, при этом имеется стоковая прошивка в неизвестном bin-формате, но отсутствует SDK. Что же делать, как собрать кастомную прошивку? Выход один — применить реверс-инжиниринг.
Разведка
Устройство, с которым предстояло поработать, было построено на базе GeneralPlus GP330xx SoC, а его системное ПО разработано с помощью OpenPlatform SDK, и, хотя китайцы заявляют о готовности предоставить исходные коды, они этого не делают. Несмотря на сложность поставленной задачи, оптимизма прибавлял включенный в устройстве по умолчанию рутовый доступ. Поэтому процесс изучения начался с запуска ADB Shell.
Все дисковое пространство планшета представляло собой одно большое блочное устройство NAND-флеш (/dev/block/nanda
), побитое на разделы:
Disk /dev/block/nanda: 7457 MB, 7457472512 bytes
4 heads, 16 sectors/track, 227584 cylinders
Units = cylinders of 64 * 512 = 32768 bytes
Device Boot Start End Blocks Id System
/dev/block/nanda1 257 174335 5570528 b Win95 FAT32
/dev/block/nanda2 174336 207103 1048576 83 Linux
/dev/block/nanda3 207104 223487 524288 83 Linux
/dev/block/nanda4 223488 227583 131072 83 Linux
Часть памяти была выделена под так называемую Internal SD card. Нужно остановиться на этом термине подробнее. В Android каждая прикладная программа запускается в своей песочнице и использует для доступа к файлам системный API. Этот API позволяет обращаться к внутренней памяти (Internal Storage) и внешней памяти (External Storage). При этом внешняя память делится на removable storage media (SD-карта, которая вставляется в слот на торце устройства) и internal (non-removable) storage (раздел внутренней памяти, «мимикрирующий» под SD-карту). В данном планшете именно под внутреннюю SD-карту был отведен самый большой раздел — /dev/block/nanda1. Поэтому его решено было разбить на два раздела, выделив один из них под контент заказчика, а второй — под внутреннюю SD-карту.
Устройство/dev/block/nanda размечено с помощью MBR, а не GPT, поэтому максимальное количество primary разделов равно четырем. С помощью fdisk был удален раздел /dev/block/nanda1, и на его месте создан extended-раздел с двумя подразделами /dev/block/nanda5 и /dev/block/nanda6.
Колдуем над разделами
Просматривая список смонтированных устройств, видим, что раздел /dev/block/vold/253:97 смонтирован на /mnt/sdcard.
root@android:/etc # mount
...
/dev/block/vold/253:97 /mnt/sdcard vfat rw,dirsync,nosuid,nodev,noexec,relatime,uid=1000,gid=1015,fmask=0602,dmask=0602,allow_utime=0020,codepage=cp437,iocharset=iso8859-1,shortname=mixed,utf8,errors=remount-ro 0 0
/dev/block/vold/253:97 /mnt/secure/asec vfat rw,dirsync,nosuid,nodev,noexec,relatime,uid=1000,gid=1015,fmask=0602,dmask=0602,allow_utime=0020,codepage=cp437,iocharset=iso8859-1,shortname=mixed,utf8,errors=remount-ro 0 0
...
Какая связь между /dev/block/vold/253:97 и /dev/block/nanda1? Vold — это Volume Management daemon, демон монтирования внешних носителей. У него имеется конфигурационный файл, по синтаксису похожий на стандартный никсовый fstab, под названием vold.fstab:
## Vold 2.0 Generic fstab
...
dev_mount sdcard /mnt/sdcard auto /devices/virtual/block/nanda /devices/virtual/block/nanda/nanda1 /devices/virtual/block/nanda/nanda2 /devices/virtual/block/nanda/nanda3 /devices/virtual/block/nanda/nanda4
...
На первый взгляд все понятно: /mnt/sdcard — это путь монтирования, auto — автоматический выбор первого подходящего для монтирования раздела из списка разделов, указанных далее (/devices/virtual/…). Однако файл vold.fstab в данном устройстве был, по сути, «заглушкой». При внесении модификаций в строчку dev_mount sdcard… (например, подмонтировать свежесозданный раздел, отличный от /devices/virtual/block/nanda/nanda1), демон отказывался работать. Трудно сказать наверняка, связано ли это с кастомизированным ядром или же с кастомизированным демоном, но, как бы то ни было, мотивы разработчиков такого решения не ясны.
Таким образом, оказалось, что ни /dev/block/nanda5, ни /dev/block/nanda6 невозможно подмонтировать с помощью vold. Дальше можно было пойти двумя путями:
- Запускать монтирование SD-карты из init-скриптов вручную. Правда, этот путь не мог гарантировать 100%-й совместимости со всеми Android internals, иными словами, нельзя было бы поручиться за стабильность работы системы, убрав из нее ключевой компонент «общения» с внешними накопителями — vold.
- Взять открытые исходники vold и попробовать собрать его для данного устройства. Гарантий также никаких, кроме того, это могло бы потребовать изрядного количества времени, которого, как всегда, не хватало.
Для такого метода решения задачи пришлось бы писать shell-скрипты, вызываемые через ADB, и получить результирующий бинарник прошивки никак бы не вышло, а это, в свою очередь, удорожило бы работу технических специалистов заказчика, так что этот путь был оставлен про запас и исследование продолжилось в новом направлении.
Решение пришло внезапно
Столкнувшись с такой проблемой, я решил еще раз внимательно изучить то, что было у нас в руках. Особый интерес вызывал прошивальщик, который, помимо самого файла прошивки firmware.bin, содержал еще ряд вспомогательных ресурсов: bootheader.bin, bootpack.bin, bootresource.bin, scanram.bin, updater.bin. Они также необходимы, но нерелевантны для нашей задачи. Больший интерес представляют файлы, которые используются прошивальщиком для загрузки своего собственного кода на устройство: small_isp.bin, cmdline, initrd и kernel.
Данное устройство использовало для прошивки так называемый ISP mode (это обозначение одного из режимов программирования флеш-памяти). Алгоритм работы прошивальщика можно условно разделить на четыре этапа:
- Технический специалист перезагружает устройство в режиме прошивки, зажав при его включении кнопки <Home + Power>.
- Прошивальщик опознает устройство по USB и перезагружает его в ISP mode.
- Прошивальщик загружает на устройство Linux, передавая файлы cmdline, initrd и kernel.
Файл kernel — это ядро ОС, initrd — раздел с ПО прошивальщика на стороне устройства, cmdline — параметры ядра, содержащие в себе размер файла initrd. - Загруженная на устройстве Linux начинает принимать от прошивальщика основные файлы прошивки, распаковывать их и записывать в соответствии с внутренними алгоритмами.
Что же представляли собой эти внутренние алгоритмы? Решение пришло внезапно. Оказалось, что initrd содержал в себе исходные коды прошивальщика на языке Lua, а также бинарники дополнительных Lua-модулей. Для распаковки initrd необходимо выполнить следующие команды:
# mkdir initrd-unpacked
# cd initrd-unpacked
# gunzip < ../initrd | cpio -i --make-directories
Для обратной упаковки (при необходимости; например, для тестирования модифицированных версий скриптов):
# find ./ | cpio -H newc -o > initrd.cpio
# gzip initrd.cpio
# mv initrd.cpio.gz initrd
Это может показаться странным, но действительно разработчики зачем-то придумали свой собственный формат прошивки, при этом оставив скрипты, оперирующие с этим форматом, в initrd в открытом виде.
Pluto
Хидеры прошивки планшета были запакованы с помощью модуля Pluto, который упаковывает Lua-таблицы в бинарный формат. Язык программирования Lua вообще активно использует подключаемые модули, представляющие собой so-библиотеки, которые добавляют те или иные API. В добавок ко всему, как следовало из документации, Pluto был платформо- и архитектурнозависим. Intel и ARM (на которой был построен планшет) существенно отличаются: Intel использует little-endian порядок байт в представлении чисел, а ARM — big-endian.
И здесь возникла серьезная проблема: стандартный модуль Pluto не распаковывал полученные данные. Были испробованы разные версии Lua и даже разные архитектуры CPU (x86, x86_64, ARM). Оказалось, что просто разработчики прошивки использовали свою, ни с чем ни совместимую версию Pluto.
Для того чтобы распаковать данные, пришлось воспользоваться эмулятором QEMU для архитектуры ARM и установить на него Debian Linux. А затем установить Lua и положить модуль pluto.so, извлеченный из initrd, в каталог модулей Lua.
Подключение Lua-модулей
Язык программирования Lua расширяется за счет внешних подключаемых модулей, которые могут быть написаны как на Lua, так и на C. В последнем случае это обычные so-библиотеки, экспортирующие ряд API-функций.
Их подключение производится с помощью функцииrequire
, а за путь поиска бинарных модулей отвечает переменнаяpackage.cpath
. У проприетарного LZO-модуля есть своя особенность подключения, которая заключается в его наименовании —lua_lzo.so
. При этом сам модуль называется lzo, из-за чего его подключение вместо привычного:package.cpath = package.cpath .. "/home/mikhail/lua_so/?.so require ;lzo;
следует производить так:
package.cpath = package.cpath .. /home/mikhail/lua_so/lua_?.so require lzo
Также стоит обратить внимание на пакетный менеджер LuaRocks, который позволяет устанавливать модули из единого репозитория и удобно их подключать. Например, в рамках данного исследования модули nixio и MD5 были подключены именно через LuaRocks.
LZO
Отдельную сложность преподнес также алгоритм сжатия LZO. Дело в том, что формат данных для этого алгоритма архивации не стандартизирован, поэтому сложно написать распаковщик, не зная, каким образом файл был запакован. Однако среди Lua-модулей initrd был модуль lua_lzo.so. На помощь пришел метод, описанный в предыдущем абзаце, правда, усложненный тем, что lua_lzo.so требовал в зависимости системную библиотеку liblzo.so (которая была взята из того же initrd) и нестандартное подключение модуля через package.cpath.
Алгоритм сжатия LZO
LZO — семейство блочных алгоритмов сжатия, обладающих важными для портативных компьютеров характеристиками:
- очень высокой скоростью распаковки;
- малым потреблением памяти;
- поблочной распаковкой данных, порциями небольшого размера.
С точки зрения реверс-инжиниринга он имеет два недостатка:
- LZO включает в себя девять алгоритмов сжатия, и к каждому из них идет свой распаковщик.
- Структура файлов LZO-архивов не стандартизирована, разные библиотеки генерируют разную структуру.
В нашем случае архивные данные имели следующий формат:
- Magic-последовательность («PMOC»).
- Размер блока данных, используемый при упаковке (131072). Напомню, что в ARM используется система little-endian, а значит, что это число соответствует hex-значению 0x00000200 (см. рис. 3).
- Блоки данных, содержащие:
- Размер блока (например, 1816).
- Запакованные данные обозначенного выше размера.
Это означает, что блок запакованных данных размером 1816 байт распакуется в 128 килобайт информации.
Распаковка выполняется в цикле, блоками данных. Для распаковки используются функции:
handle = >lzo.decompressInit(header)
, где header — magic number + размер блока архивации, handle — хендл, использующийся в двух других функциях... = lzo.decompressPorcess(handle)
lzo.decompressFinish(handle)
Примечательно то, что необходимо точно знать размер архива, чтобы распаковка выполнилась успешно. В противном случае распаковка зависает на статусе
DECOMPRESS_NEED_MORE_DATA
. Размер архива указан в заголовке 2 (см. рис. 1).
Компрессия данных выполняется сложнее, так как функции компрессии не документированы и их работоспособность выявлялась пробным путем. Функции аналогичны:
handle, header= lzo.compressInit(blockSize)
...= lzo.compressProcess(handle, data)
lzo.compressFinish(handle)
Отличительный момент компрессии от декомпрессии в том, что перед записью блока данных, полученных в результате выполнения функции lzo.compressProcess, необходимо записать размер упакованного блока данных. Это следует из общей документации на алгоритм сжатия LZO и из анализа архива, полученного при разборе оригинальной прошивки.
В итоге, исследуя исходный код скриптов, пытаясь понять их логику работы, форматы данных, а также проведя множество экспериментов, прошивку удалось распаковать.
Ресайз системного раздела
Распакованный файл системного раздела (system.bin
) представлял собой образ файловой системы ext4. Для того чтобы записать данные заказчика, его необходимо было расширить на 1 Гб. Для этого нужно сделать следующее:
- Расширить саму файловую систему.
- В заголовке 2, в таблице разделов, уменьшить на 1 Гб раздел
nanda1
и увеличить на столько же разделnanda2
. - Снова заархивировать
system.bin
, пересчитать контрольные суммы и записать их в заголовки.
Сам же ресайз системного раздела выполняется следующими командами:
# mkdir system_new
# losetup /dev/loop0 system.bin
# e2fsck -f /dev/loop0
# resize2fs /dev/loop0 2G
# mount /dev/loop0 system_new
...
# umount system_new
# losetup -d /dev/loop0
Работа с data-разделом
В рамках решения данной задачи часть изменений в системе производилась не только в /system
, но и в /data
. Для этого необходимо было распаковать dataImage.tar.gz
, сделать необходимые изменения и запаковать обратно. Подобным образом следует поступить и с userImage.tar.gz
, если требуется внести изменения в контент SD-карты.
Для упаковки с сохранением прав доступа используем следующие команды:
# tar cvf - . | gzip -9 - > ../user.tar.gz
# tar cvfp - . | gzip -9 - > ../data.tar.gz
Замена приложений по умолчанию
Заказчику было нужно не только записать свой контент в постоянную память устройства, но и заменить стандартный launcher своим собственным приложением, обеспечив требуемый User Experience.
Замена launcher’а (и других приложений по умолчанию) производилась путем редактирования файлов /data/system/packages.list и /data/system/packages.xml. Сначала дефолтные настройки выполнялись на устройстве, затем содержание файлов частично переносилось в прошивку.
Файл packages.list представляет собой список установленных в системе пакетов. Нужный пакет launcher’а называется com.soaw.launcher и добавляется строчкой:
com.soaw.launcher 10068 1 /data/data/com.soaw.launcher
А <packages.xml — это база данных установленных в системе пакетов и их метаданных, таких как сертификаты, права доступа, приложения по умолчанию и прочее. За настройку программ по умолчанию отвечают две записи. Первая запись — это метаданные launcher’а. Обрати внимание на атрибут index в теге cert, его значение должно быть на единицу больше уже существующего в файле, чтобы не случилось путаницы сертификатов.
package name="com.soaw.launcher" codePath="/system/app/SOAWLauncher.apk" nativeLibraryPath="/data/data/com.soaw.launcher/lib" flags="1" ft="141c2c2bbe0" it="141c2c2bbe0" ut="141c2c2bbe0" version="1" userId="10068"
sigs count=1;
cert index=20; key=... /sigs/package
Следующая запись — это настройки программ по умолчанию. Здесь задается выбор launcher’а и программы-медиаплеера.
<preferred-activities>
<item name="com.soaw.launcher/.activity.HomeActivity" match="100000" set="2">
<set name="com.android.launcher/com.android.launcher2.Launcher"/>
<set name="com.soaw.launcher/.activity.HomeActivity"/>
<filter>
<action name="android.intent.action.MAIN"/>
<cat name="android.intent.category.HOME"/>
<cat name="android.intent.category.DEFAULT"/>
</filter>
</item>
<item name="com.android.gallery3d/.app.MovieActivity" match="600000" set="2">
<set name="com.generalplus.GaGaPlayer/.MoviePlayerActivity"/>
<set name="com.android.gallery3d/.app.MovieActivity"/>
<filter>
<action name="android.intent.action.VIEW"/>
<cat name="android.intent.category.DEFAULT"/>
<type name="video/mp4"/>
</filter>
</item>
</preferred-activities>
Системные настройки
Как известно, Android имеет SQLite базу данных системных настроек, которую возможно модифицировать на этапе подготовки образа прошивки. Файл базы данных находится в /data/data/com.android.providers.settings/databases/settings.db.
Сокрытие нижней панели выполняется в таблице system следующими записями:
navigation_bar_mode = 4
navigation_bar_buttons_show = 0
navigation_bar_buttons_need_show = 0
Отключение экрана блокировки выполняется в таблице secure:
coockscreen.disabled = 1
Init-скрипты
Init-скрипты Android записываются на /
в момент загрузки устройства и поэтому, хотя они и могут быть отредактированы непосредственно на устройстве, при следующей перезагрузке будут перезаписаны оригинальными файлами. Вероятнее всего, они располагаются в initrd, но исследования на эту тему не проводились.
Заключение
Наша жизнь — процесс. Закрытые программные системы — потемки. Процесс познания потемок и есть реверс-инжиниринг. Этот подход помог не только решить основную бизнес-задачу — выпустить кастомную прошивку, но и узнать больше о внутреннем устройстве Android в целом, что, несомненно, весьма интересно для настоящего хакера. Важно помнить, что реверс-инжиниринг — инструмент легальный и универсальный. Не будь его, мир никогда бы не узнал об опаснейших бэкдорах в прошивках ведущих производителей сетевого оборудования, об аппаратных «закладках» в микропроцессорах, об утечках данных в популярных интернет-приложениях. Если кто-то изобрел «черный ящик», то всегда найдется тот, кто сможет понять, как он работает.
Автор: Михаил Емельченков
Впервые опубликовано в журнале «Хакер» от 02/2014.
Подпишись на «Хакер»