Получаем доступ к шифрованному разделу LUKS виртуальной машины на примере IBM APIConnect_Management_Essentials

Компания IBM предлагает образ виртуальной машины (APIConnect_Management_Essentials-5.0.2.0iFix1_20160708-1149_f64536a3a6a5_ea05c0b.ova), который к сожалению не работает в KMV.
Сегодня я расскажу как преобразовать этот образ в KVM или даже LXC (с небольшими доработками), покажу как получать доступ к шифрованному разделу и расскажу о недостатках реализации шифрования корневого раздела инженерами IBM.

Сам файл ova представляет собой обычный архив, который распаковывается простой командой tar xf APIConnect_Management_Essentials-5.0.2.0iFix1_20160708-1149_f64536a3a6a5_ea05c0b.ova. Внутри находятся образы с расширением vmdk, которые на самом деле являются не совсем vmdk, а stream-optimized. Такой образ надо сначала конвертировать командой vmware-vdiskmanager -r rel_apimgmt-disk1.vmdk -t 0 rel_apimgmt-disk1-1.vdmk, а потом уже обычный vmdk конвертируется в формат qcow2 при помощи qemu: qemu-img convert -f vmdk -O qcow2rel_apimgmt-disk1-1.vdmk .rel_apimgmt-disk1-1.vdmkqcow2.

Однако попытка запустить виртуальную машину KVM с этим образом успеха не дала — по каким-то причинам загрузка не шла, а получить доступ к single mode из загрузчика было нельзя — защищен паролем.

Тогда я просто подключил этот образ как блочное устройство командой qemu-nbd —connect=/dev/nbd0qcow2rel_apimgmt-disk1-1.vdmk .rel_apimgmt-disk1-1.vdmkqcow2. Доступ к разделам на этом устройстве я создал при помощи команды kpartx -arvs /dev/nbd0.

На этом устройстве обнаружилось два linux раздела — загрузчик и зашифрованная корневая система LUKS. Теперь мне нужно просто было получить доступ к зашифрованным файлам и влить их в контейнер KVM/LXC.

Смонтировав загрузочный раздел, я обнаружил там стандартный grub с таким конфигом:

...
root (hd0,0)
kernel ...
initrd /boot/initrd.img.gz
...

Как видно, ключ монтирования зашифрованной файловой системы расположен в initrd.img.gz. Сам файл представляет банальный архив cpio сжатый xf. Распаковка выполняется в два этапа. Сначала xf:  mv initrd.img.gz initrd.img.gz.xz; xz -d initrd.img.xz.  Затем cpio:  cpio -idv <initrd.img.

В результате я получил доступ ко всем файлам загрузчика, среди которых быстро нашел обращение к LUKS в /init файле загрузчика:

...

update_uefi

. /bin/luks.sh

if [ "$ISVM" = "0" ]
then
 print_status " ptc"
 /bin/tcsd 
 COUNT=0
 until /bin/busybox pidof tcsd > /dev/null 2>&1
 do
 /bin/sleep 1
 print_status "."
 COUNT=`/bin/busybox expr ${COUNT} + 1`
 if [ ${COUNT} -gt 60 ]
 then
 exit_handler "!tc"
 fi
 done
fi

LUKS=`/bin/findfs LABEL=DPBOOT 2>/dev/null| /bin/sed -e 's/1/2/'`
if [ -z ${LUKS} ]
then
 exit_handler "!lu"
else 
 print_status " plu"
fi

if mount_luks_partition ${LUKS} flash2
then
 print_status " pmp"
else
 if mount_luks_partition_from_upgrade_key ${LUKS} flash2
 then
 print_status " pms"
 else
 exit_handler "!ul"
 fi
fi

...

Как легко понять, функции работы с LUKS подключаются из файла /bin/luks.sh, затем вызывается функция mount_luks_partition_from_upgrade_key. Посмотрим, что она из себя представляет:

# First argument is disk partition
# Second argument is name under /dev/mapper
mount_luks_partition_from_upgrade_key() {

if [ "$ISVM" = "1" ] || [ -n "$3" ]
 then
 ${DPINFO} --bootstrap 2>>${MSGLOG} | ${CRYPTSETUP} --verbose --key-file=- --key-slot 0 luksOpen $1 $2 2>>${MSGLOG} 1>>${MSGLOG} || return 1
 else
 ${DPINFO} 2>>${MSGLOG} | ${CRYPTSETUP} --verbose --key-file=- --key-slot 0 luksOpen $1 $2 2>>${MSGLOG} 1>>${MSGLOG} || return 1
 fi

return 0
}

Тут, как видно, выполняется некий DPINFO с ключем —bootstrap, stderr идет в некий лог, а stdout выдает ключ, который используется LUKS. Что же это за DPINFO? В luks.sh он указан так: DPINFO=${DPINFO:-/bin/dpinfo}. Проверим, что это за файл: file bin/dpinfo
bin/dpinfo: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.18, BuildID[sha1]=5bb8ac6d9449bc1fd153318b4ebe61916a883a37, stripped. Похоже, что его можно запустить:

# bin/dpinfo --help
Unknown option '--help'

Usage: dpinfo [options]
 --version
 --isnew
 --partnum
 --xmlfile
 --isVM
 --liger
 --statuuid

Как видно, он отрабатывает. Причем не сообщает о ключе —bootstrap, с которым отрабатывает и выдает ключ для LUKS на stdout. Попробуем подключить LUKS раздел: bin/dpinfo —bootstrap | cryptsetup —key-file=- —key-slot 0 luksOpen /dev/nbd0p2 nbd0p2_luks  Появился новый раздел /dev/mapper/nbd0p2_luks. Теперь его можно монтировать и получить доступ ко всем файлам. Можно сделать туда chroot и запускать команды:

cat /etc/redhat-release 
IBM API Connect Management Appliance (5.0.2.0iFix1-20160708-1149_f64536a3a6a5)

Образ основан на базе CentOS 6 и использует версию ядра  2.6.32-131.21.1.106.

Я скопировал все файлы на новый раздел, затем вошел в chroot и установил загрузчик на этот раздел. Потом поправил скрипт, чтоб он не подключал LUKS, а сразу монтировал корневой раздел. После этого я смог загрузить образ в KVM. С небольшими доработками можно запускать и в LXC.

Теперь о том, какие ошибки допустили инженеры IBM при создании шифрованного образа.

Прежде всего — это раздел загрузчика grub без шифрования, где можно в принципе уже изменить все, что угодно и получить доступ к зашифрованным данным.

Второе — это легкость получения ключа для расшифровки. Ведь эту утилиту я запустил внутри хоста, а не виртуальной машины и она тем не менее отдала нужный ключ.

Как бы делал я? Двойное шифрование без загрузчика grub!

Во-первых я бы собрал ядро linux с встроенным initrd через опцию ядра CONFIG_INITRAMFS_SOURCE и поддержкой EFI.

Во-вторых я бы создал EFI образ, на котором в разметке GPT я бы поместил в boot раздел мое ядро в качестве EFI загрузчика через efibootmgr.

В-третьих, в качестве ключа я бы использовал хеш с областью памяти, где размещен EFI и контрольной суммой стартового раздела после загрузки. Это усложнило бы подбор ключа и исключило замену загрузчика на собственный.

И наконец — самое главное. Загрузчик передает управление в расшифрованный стартовый раздел, где выполняется скрипт, который расшифровывает корневой раздел с участием контрольной суммы загрузочного раздела EFI, в который можно дописать несколько байт в неиспользованный сектор.

Таким образом был бы только загрузчик и зашифрованный раздел. Конечно, можно скачать загрузчик и извлечь из него бинарник, дизассемблировать и узнать хеш, потом выполнить загрузку со своим загрузчиком и подсунуть нужные данные. И повторить это еще раз для второго раздела с дизассемблированием и чтением байтов из секторов. Что не просто сложно, но еще и утомительно долго.